11package com .wavesplatform .state .appender
22
3+ import cats .data .OptionT
4+ import cats .syntax .all .*
5+ import com .wavesplatform .block .Block
6+ import com .wavesplatform .block .Block .BlockId
7+ import com .wavesplatform .common .state .ByteStr
38import com .wavesplatform .common .utils .EitherExt2 .*
49import com .wavesplatform .consensus .PoSSelector
510import com .wavesplatform .features .BlockchainFeatures
@@ -17,9 +22,11 @@ import monix.eval.Task
1722import monix .execution .Scheduler
1823import org .influxdb .dto .Point
1924
20- import scala .util .{Left , Right }
25+ import scala .annotation .tailrec
26+ import scala .util .chaining .*
2127
2228object ExtensionAppender extends ScorexLogging {
29+ private case class AppendData (newBlocks : Seq [Block ], lastCommonBlockId : BlockId , lastCommonHeight : Int )
2330
2431 def apply (
2532 blockchainUpdater : BlockchainUpdater & Blockchain ,
@@ -29,111 +36,125 @@ object ExtensionAppender extends ScorexLogging {
2936 invalidBlocks : InvalidBlockStorage ,
3037 peerDatabase : PeerDatabase ,
3138 scheduler : Scheduler
32- )(ch : Channel , extensionBlocks : ExtensionBlocks ): Task [Either [ValidationError , Option [BigInt ]]] = {
33- def appendExtension (extension : ExtensionBlocks ): Either [ValidationError , Option [BigInt ]] =
39+ )(ch : Channel , extension : ExtensionBlocks ): Task [Either [ValidationError , Option [BigInt ]]] = {
40+ type Result [A ] = Either [ValidationError , A ]
41+ def appendExtension (): OptionT [Result , BigInt ] =
3442 if (extension.remoteScore <= blockchainUpdater.score) {
3543 log.trace(s " Ignoring extension $extension because declared remote was not greater than local score ${blockchainUpdater.score}" )
36- Right (None )
37- } else {
38- extension.blocks
39- .collectFirst { case b if ! b.signatureValid() => GenericError (s " Block $b has invalid signature " ) }
40- .toLeft(extension)
41- .flatMap { extensionWithValidSignatures =>
42- val newBlocks = extensionWithValidSignatures.blocks.dropWhile(blockchainUpdater.contains)
43-
44- newBlocks.headOption.map(_.header.reference) match {
45- case Some (lastCommonBlockId) =>
46- val initialHeight = blockchainUpdater.height
47-
48- val droppedBlocksEi = for {
49- commonBlockHeight <- blockchainUpdater.heightOf(lastCommonBlockId).toRight(GenericError (" Fork contains no common parent" ))
50- droppedBlocks <- {
51- if (commonBlockHeight < initialHeight)
52- blockchainUpdater.removeAfter(lastCommonBlockId)
53- else Right (Seq .empty)
54- }
55- } yield (commonBlockHeight, droppedBlocks)
56-
57- droppedBlocksEi.flatMap { case (commonBlockHeight, droppedBlocks) =>
58- newBlocks.zipWithIndex.foreach { case (block, idx) =>
59- val rideV6Activated = blockchainUpdater.isFeatureActivated(BlockchainFeatures .RideV6 , commonBlockHeight + idx + 1 )
60- ParSignatureChecker .checkTxSignatures(block.transactionData, rideV6Activated)
61- }
62-
63- val forkApplicationResultEi = {
64- newBlocks.view
65- .map { b =>
66- b -> appendExtensionBlock(blockchainUpdater, pos, time, verify = true , txSignParCheck = false )(
67- b,
68- extension.snapshots.get(b.id())
69- )
70- .map {
71- case (_ : Applied , height) => BlockStats .applied(b, BlockStats .Source .Ext , height)
72- case _ =>
73- }
74- }
75- .zipWithIndex
76- .collectFirst { case ((b, Left (e)), i) => (i, b, e) }
77- .fold[Either [ValidationError , Unit ]](Right (())) { case (i, declinedBlock, e) =>
78- e match {
79- case _ : TxValidationError .BlockFromFuture =>
80- case _ => invalidBlocks.add(declinedBlock.id(), e)
81- }
82-
83- newBlocks.view
84- .dropWhile(_ != declinedBlock)
85- .foreach(BlockStats .declined(_, BlockStats .Source .Ext ))
86-
87- if (i == 0 ) log.warn(s " Can't process fork starting with $lastCommonBlockId, error appending block $declinedBlock: $e" )
88- else
89- log.warn(
90- s " Processed only ${i + 1 } of ${newBlocks.size} blocks from extension, error appending next block $declinedBlock: $e"
91- )
92-
93- Left (e)
94- }
95- }
96-
97- forkApplicationResultEi match {
98- case Left (e) =>
99- blockchainUpdater.removeAfter(lastCommonBlockId).explicitGet()
100- droppedBlocks.foreach { x =>
101- blockchainUpdater.processBlock(x.block, x.hitSource, x.snapshot, x.generatorSet).explicitGet()
102- }
103- Left (e)
104-
105- case Right (_) =>
106- val depth = initialHeight - commonBlockHeight
107- if (depth > 0 ) {
108- Metrics .write(
109- Point
110- .measurement(" rollback" )
111- .addField(" depth" , initialHeight - commonBlockHeight)
112- .addField(" txs" , droppedBlocks.size)
113- )
114- }
115-
116- val newTransactions = newBlocks.view.flatMap(_.transactionData).toSet
117- utxStorage.removeAll(newTransactions)
118- utxStorage.addAndScheduleCleanup(droppedBlocks.flatMap(_._1.transactionData).filterNot(newTransactions))
119- Right (Some (blockchainUpdater.score))
120- }
121- }
122-
123- case None =>
124- log.debug(" No new blocks found in extension" )
125- Right (None )
126- }
44+ OptionT .none[Result , BigInt ]
45+ } else
46+ for {
47+ _ <- OptionT .liftF(validateSignatures())
48+ appendData <- OptionT .fromOption[Result ](dropCommonPrefix())
49+ _ <- OptionT .liftF(processNewBlocks(appendData))
50+ } yield blockchainUpdater.score
51+
52+ def validateSignatures (): Either [ValidationError , ExtensionBlocks ] =
53+ extension.blocks
54+ .collectFirst { case b if ! b.signatureValid() => GenericError (s " Block $b has invalid signature " ) }
55+ .toLeft(extension)
56+
57+ def dropCommonPrefix (): Option [AppendData ] = {
58+ @ tailrec def loop (last : AppendData ): Option [AppendData ] = last.newBlocks match {
59+ case Nil =>
60+ log.debug(" No new blocks found in extension" )
61+ None
62+
63+ case b +: rest =>
64+ blockchainUpdater.heightOf(b.id()) match {
65+ case None => last.some
66+ case Some (h) => loop(AppendData (rest, b.id(), h))
12767 }
12868 }
12969
130- log.debug(s " ${id(ch)} Attempting to append extension ${formatBlocks(extensionBlocks.blocks)}" )
131- Task (appendExtension(extensionBlocks)).executeOn(scheduler).map {
70+ loop(
71+ AppendData (
72+ extension.blocks,
73+ blockchainUpdater.lastBlockId.getOrElse(throw new RuntimeException (" Empty blockchain" )),
74+ blockchainUpdater.height
75+ )
76+ )
77+ }
78+
79+ def processNewBlocks (appendData : AppendData ): Either [ValidationError , Unit ] = {
80+ val originalForkHeight = blockchainUpdater.height
81+ for {
82+ discardedBlocks <-
83+ if (appendData.lastCommonHeight < originalForkHeight) blockchainUpdater.removeAfter(appendData.lastCommonBlockId)
84+ else Right (Seq .empty)
85+ _ = precheckSignatures(appendData)
86+ _ <- applyFork(appendData).tap {
87+ case Left (_) => restoreDiscardedBlocks(appendData.lastCommonBlockId, discardedBlocks)
88+ case Right (_) =>
89+ val depth = originalForkHeight - appendData.lastCommonHeight
90+ if (depth > 0 )
91+ Metrics .write(
92+ Point
93+ .measurement(" rollback" )
94+ .addField(" depth" , depth)
95+ .addField(" txs" , discardedBlocks.size)
96+ )
97+
98+ val newTxs = appendData.newBlocks.flatMap(_.transactionData)
99+ val newTxIds = newTxs.view.map(_.id()).toSet
100+ utxStorage.removeIds(newTxIds)
101+
102+ val discardedTxs = discardedBlocks.flatMap(_._1.transactionData)
103+ utxStorage.addAndScheduleCleanup(discardedTxs.filterNot(tx => newTxIds.contains(tx.id()))) // In a case of re-appending issues
104+ }
105+ } yield ()
106+ }
107+
108+ def precheckSignatures (appendData : AppendData ): Unit =
109+ appendData.newBlocks.zipWithIndex.foreach { case (block, idx) =>
110+ val rideV6Activated = blockchainUpdater.isFeatureActivated(BlockchainFeatures .RideV6 , appendData.lastCommonHeight + idx + 1 )
111+ ParSignatureChecker .checkTxSignatures(block.transactionData, rideV6Activated)
112+ }
113+
114+ def applyFork (appendData : AppendData ): Either [ValidationError , Unit ] = appendData.newBlocks.view
115+ .map { b =>
116+ val s = extension.snapshots.get(b.id())
117+ val r = appendExtensionBlock(blockchainUpdater, pos, time, verify = true , txSignParCheck = false )(b, s).map {
118+ case (_ : Applied , height) => BlockStats .applied(b, BlockStats .Source .Ext , height)
119+ case _ =>
120+ }
121+ b -> r
122+ }
123+ .zipWithIndex
124+ .collectFirst { case ((b, Left (e)), i) => (i, b, e) }
125+ .fold(Either .unit[ValidationError ]) { case (i, declinedBlock, e) =>
126+ e match {
127+ case _ : TxValidationError .BlockFromFuture =>
128+ case _ => invalidBlocks.add(declinedBlock.id(), e)
129+ }
130+
131+ appendData.newBlocks.view
132+ .dropWhile(_ != declinedBlock)
133+ .foreach(BlockStats .declined(_, BlockStats .Source .Ext ))
134+
135+ log.warn(
136+ if (i == 0 ) s " Can't process fork starting with ${appendData.lastCommonBlockId}, error appending block $declinedBlock: $e"
137+ else s " Processed only ${i + 1 } of ${appendData.newBlocks.size} blocks from extension, error appending next block $declinedBlock: $e"
138+ )
139+
140+ Left (e)
141+ }
142+
143+ def restoreDiscardedBlocks (lastCommonBlockId : ByteStr , blocks : DiscardedBlocks ): Unit = {
144+ blockchainUpdater.removeAfter(lastCommonBlockId).explicitGet()
145+ blocks.foreach { x =>
146+ blockchainUpdater.processBlock(x.block, x.hitSource, x.snapshot, x.generatorSet).explicitGet()
147+ }
148+ }
149+
150+ val formattedBlocksStr = formatBlocks(extension.blocks)
151+ log.debug(s " ${id(ch)} Attempting to append extension $formattedBlocksStr" )
152+ Task (appendExtension().value).executeOn(scheduler).map {
132153 case Right (maybeNewScore) =>
133- log.debug(s " ${id(ch)} Successfully appended extension ${formatBlocks(extensionBlocks.blocks)} " )
154+ log.debug(s " ${id(ch)} Successfully appended extension $formattedBlocksStr " )
134155 Right (maybeNewScore)
135156 case Left (ve) =>
136- val errorMessage = s " ${id(ch)} Error appending extension ${formatBlocks(extensionBlocks.blocks)} : $ve"
157+ val errorMessage = s " ${id(ch)} Error appending extension $formattedBlocksStr : $ve"
137158 log.warn(errorMessage)
138159 peerDatabase.blacklistAndClose(ch, errorMessage)
139160 Left (ve)
0 commit comments