Skip to content

Commit 599311b

Browse files
committed
[ETCM-103] Refactor handling of checking for stale block
1 parent 0c80251 commit 599311b

File tree

7 files changed

+172
-89
lines changed

7 files changed

+172
-89
lines changed

src/main/resources/application.conf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,16 @@ mantis {
403403
# Max number of mpt nodes held in memory in state sync, before saving them into database
404404
# 100k is around 60mb (each key-value pair has around 600bytes)
405405
state-sync-persistBatch-size = 100000
406+
407+
# If new pivot block received from network will be less than fast sync current pivot block, the re-try to chose new
408+
# pivot will be scheduler after this time. Avarage block time in etc/eth is around 15s so after this time, most of
409+
# network peers should have new best block
410+
pivot-block-reSchedule-interval = 15.seconds
411+
412+
# If for most network peers, the following condition will be true:
413+
# (peer.bestKnownBlock - pivot-block-offset) - node.curentPivotBlock > max-pivot-age
414+
# it fast sync pivot block has become stale and it needs update
415+
max-pivot-block-age = 96
406416
}
407417

408418
pruning {

src/main/scala/io/iohk/ethereum/blockchain/sync/FastSync.scala

Lines changed: 104 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import java.time.Instant
44

55
import akka.actor._
66
import akka.util.ByteString
7+
import cats.data.NonEmptyList
78
import io.iohk.ethereum.blockchain.sync.FastSyncReceiptsValidator.ReceiptsValidationResult
89
import io.iohk.ethereum.blockchain.sync.PeerRequestHandler.ResponseReceived
910
import io.iohk.ethereum.blockchain.sync.SyncBlocksValidator.BlockBodyValidationResult
@@ -69,29 +70,9 @@ class FastSync(
6970
}
7071

7172
def startWithState(syncState: SyncState): Unit = {
72-
if (syncState.updatingPivotBlock) {
73-
log.info(s"FastSync interrupted during pivot block update, choosing new pivot block")
74-
val syncingHandler = new SyncingHandler(syncState)
75-
val pivotBlockSelector = context.actorOf(
76-
PivotBlockSelector.props(etcPeerManager, peerEventBus, syncConfig, scheduler, context.self),
77-
"pivot-block-selector"
78-
)
79-
pivotBlockSelector ! PivotBlockSelector.SelectPivotBlock
80-
context become syncingHandler.waitingForPivotBlockUpdate(ImportedLastBlock)
81-
} else {
82-
log.info(
83-
s"Starting block synchronization (fast mode), pivot block ${syncState.pivotBlock.number}, " +
84-
s"block to download to ${syncState.safeDownloadTarget}"
85-
)
86-
val syncingHandler = new SyncingHandler(syncState)
87-
context become syncingHandler.receive
88-
if (syncState.isBlockchainWorkFinished && !syncState.stateSyncFinished) {
89-
log.info(s"Blockchain sync was completed, starting state sync to block ${syncState.pivotBlock.idTag}")
90-
// chain has already been downloaded we can start state sync
91-
syncingHandler.startStateSync(syncState.pivotBlock)
92-
}
93-
syncingHandler.processSyncing()
94-
}
73+
log.info(s"Starting with existing state and asking for new pivot block")
74+
val syncingHandler = new SyncingHandler(syncState)
75+
syncingHandler.askForPivotBlockUpdate(NodeRestart)
9576
}
9677

9778
def startFromScratch(): Unit = {
@@ -115,14 +96,17 @@ class FastSync(
11596
pivotBlockHeader,
11697
safeDownloadTarget = pivotBlockHeader.number + syncConfig.fastSyncBlockValidationX
11798
)
118-
startWithState(initialSyncState)
99+
val syncingHandler = new SyncingHandler(initialSyncState)
100+
context.become(syncingHandler.receive)
101+
syncingHandler.processSyncing()
119102
}
120103
}
121104

122105
// scalastyle:off number.of.methods
123106
private class SyncingHandler(initialSyncState: SyncState) {
124107

125108
private val BlockHeadersHandlerName = "block-headers-request-handler"
109+
//not part of syncstate as we do not want to persist is.
126110
private var stateSyncRestartRequested = false
127111

128112
private var requestedHeaders: Map[Peer, BigInt] = Map.empty
@@ -158,13 +142,11 @@ class FastSync(
158142
private val heartBeat =
159143
scheduler.scheduleWithFixedDelay(syncRetryInterval, syncRetryInterval * 2, self, ProcessSyncing)
160144

161-
def startStateSync(targetBlockHeader: BlockHeader): Unit = {
162-
syncStateScheduler ! StartSyncingTo(targetBlockHeader.stateRoot, targetBlockHeader.number)
163-
}
164-
165145
def receive: Receive = handleCommonMessages orElse {
166146
case UpdatePivotBlock(state) => updatePivotBlock(state)
167-
case WaitingForNewTargetBlock => updatePivotBlock(ImportedLastBlock)
147+
case WaitingForNewTargetBlock =>
148+
log.info("State sync stopped until receiving new pivot block")
149+
updatePivotBlock(ImportedLastBlock)
168150
case ProcessSyncing => processSyncing()
169151
case PrintStatus => printStatus()
170152
case PersistSyncState => persistSyncState()
@@ -206,49 +188,65 @@ class FastSync(
206188
handleRequestFailure(assignedHandlers(ref), ref, "Unexpected error")
207189
}
208190

209-
def waitingForPivotBlockUpdate(processState: FinalBlockProcessingResult): Receive = handleCommonMessages orElse {
191+
def askForPivotBlockUpdate(updateReason: PivotBlockUpdateReason): Unit = {
192+
syncState = syncState.copy(updatingPivotBlock = true)
193+
log.info("Asking for new pivot block")
194+
val pivotBlockSelector = {
195+
context.actorOf(
196+
PivotBlockSelector.props(etcPeerManager, peerEventBus, syncConfig, scheduler, context.self)
197+
)
198+
}
199+
pivotBlockSelector ! PivotBlockSelector.SelectPivotBlock
200+
context become waitingForPivotBlockUpdate(updateReason)
201+
}
202+
203+
def reScheduleAskForNewPivot(updateReason: PivotBlockUpdateReason): Unit = {
204+
syncState = syncState.copy(pivotBlockUpdateFailures = syncState.pivotBlockUpdateFailures + 1)
205+
scheduler.scheduleOnce(syncConfig.pivotBlockReScheduleInterval, self, UpdatePivotBlock(updateReason))
206+
}
207+
208+
def waitingForPivotBlockUpdate(updateReason: PivotBlockUpdateReason): Receive = handleCommonMessages orElse {
210209
case PivotBlockSelector.Result(pivotBlockHeader) =>
211210
log.info(s"New pivot block with number ${pivotBlockHeader.number} received")
212211
if (pivotBlockHeader.number >= syncState.pivotBlock.number) {
213-
updatePivotSyncState(processState, pivotBlockHeader)
214-
syncState = syncState.copy(updatingPivotBlock = false)
215-
context become this.receive
216-
processSyncing()
212+
if (pivotBlockHeader.number == syncState.pivotBlock.number && updateReason.nodeRestart) {
213+
// it can happen after quick node restart than pivot block has not changed in the network. To keep whole
214+
// fast sync machinery running as expected we need to make sure that we will receive better pivot than current
215+
log.info("Received stale pivot after restart, asking for new pivot")
216+
reScheduleAskForNewPivot(updateReason)
217+
} else {
218+
updatePivotSyncState(updateReason, pivotBlockHeader)
219+
syncState = syncState.copy(updatingPivotBlock = false)
220+
context become this.receive
221+
processSyncing()
222+
}
217223
} else {
218-
syncState = syncState.copy(pivotBlockUpdateFailures = syncState.pivotBlockUpdateFailures + 1)
219-
scheduler.scheduleOnce(syncRetryInterval, self, UpdatePivotBlock(processState))
224+
log.info("Received target block is older than old one, re-scheduling asking for new one")
225+
reScheduleAskForNewPivot(updateReason)
220226
}
221227

222228
case PersistSyncState => persistSyncState()
223229

224230
case UpdatePivotBlock(state) => updatePivotBlock(state)
225231
}
226232

227-
private def updatePivotBlock(state: FinalBlockProcessingResult): Unit = {
233+
private def updatePivotBlock(state: PivotBlockUpdateReason): Unit = {
228234
if (syncState.pivotBlockUpdateFailures <= syncConfig.maximumTargetUpdateFailures) {
229235
if (assignedHandlers.nonEmpty || syncState.blockChainWorkQueued) {
230236
log.info(s"Still waiting for some responses, rescheduling pivot block update")
231237
scheduler.scheduleOnce(1.second, self, UpdatePivotBlock(state))
232238
processSyncing()
233239
} else {
234-
syncState = syncState.copy(updatingPivotBlock = true)
235-
log.info("Asking for new pivot block")
236-
val pivotBlockSelector = {
237-
context.actorOf(
238-
PivotBlockSelector.props(etcPeerManager, peerEventBus, syncConfig, scheduler, context.self)
239-
)
240-
}
241-
pivotBlockSelector ! PivotBlockSelector.SelectPivotBlock
242-
context become waitingForPivotBlockUpdate(state)
240+
askForPivotBlockUpdate(state)
243241
}
244242
} else {
245243
log.warning(s"Sync failure! Number of pivot block update failures reached maximum.")
246244
sys.exit(1)
247245
}
248246
}
249247

250-
private def updatePivotSyncState(state: FinalBlockProcessingResult, pivotBlockHeader: BlockHeader): Unit =
251-
state match {
248+
private def updatePivotSyncState(updateReason: PivotBlockUpdateReason, pivotBlockHeader: BlockHeader): Unit =
249+
updateReason match {
252250
case ImportedLastBlock =>
253251
if (pivotBlockHeader.number - syncState.pivotBlock.number <= syncConfig.maxTargetDifference) {
254252
log.info(s"Current pivot block is fresh enough, starting state download")
@@ -277,6 +275,17 @@ class FastSync(
277275
)
278276
syncState =
279277
syncState.updatePivotBlock(pivotBlockHeader, syncConfig.fastSyncBlockValidationX, updateFailures = true)
278+
279+
case NodeRestart =>
280+
// in case of node restart we are sure that new pivotBlockHeader > current pivotBlockHeader
281+
syncState = syncState.updatePivotBlock(
282+
pivotBlockHeader,
283+
syncConfig.fastSyncBlockValidationX,
284+
updateFailures = false
285+
)
286+
log.info(
287+
s"Changing pivot block to ${pivotBlockHeader.number}, new safe target is ${syncState.safeDownloadTarget}"
288+
)
280289
}
281290

282291
private def removeRequestHandler(handler: ActorRef): Unit = {
@@ -527,31 +536,48 @@ class FastSync(
527536
}
528537
}
529538

539+
def hasBestBlockFreshEnoughToUpdatePivotBlock(info: PeerInfo, state: SyncState, syncConfig: SyncConfig): Boolean = {
540+
(info.maxBlockNumber - syncConfig.pivotBlockOffset) - state.pivotBlock.number > syncConfig.maxPivotBlockAge
541+
}
542+
530543
private def getPeerWithTooFreshNewBlock(
531-
peers: Map[Peer, PeerInfo],
544+
peers: NonEmptyList[(Peer, PeerInfo)],
532545
state: SyncState,
533546
syncConfig: SyncConfig
534-
): List[Peer] = {
547+
): List[(Peer, BigInt)] = {
535548
peers.collect {
536-
case (peer, info)
537-
if (info.maxBlockNumber - syncConfig.pivotBlockOffset) - state.pivotBlock.number > FastSync.maxTargetBlockAge =>
538-
peer
539-
}.toList
549+
case (peer, info) if hasBestBlockFreshEnoughToUpdatePivotBlock(info, state, syncConfig) =>
550+
(peer, info.maxBlockNumber)
551+
}
540552
}
541553

542-
private def shouldUpdateStateTargetBlock(): Boolean = {
543-
val availablePeers = peersToDownloadFrom
544-
if (availablePeers.isEmpty) {
554+
def noBlockchainWorkRemaining: Boolean =
555+
syncState.isBlockchainWorkFinished && assignedHandlers.isEmpty
556+
557+
def notInTheMiddleOfUpdate: Boolean =
558+
!(syncState.updatingPivotBlock || stateSyncRestartRequested)
559+
560+
def pivotBlockIsStale(): Boolean = {
561+
val currentPeers = peersToDownloadFrom.toList
562+
if (currentPeers.isEmpty) {
545563
false
546564
} else {
547-
if (availablePeers.size < syncConfig.minPeersToChoosePivotBlock) {
548-
getPeerWithTooFreshNewBlock(availablePeers, syncState, syncConfig).size == availablePeers.size
565+
val peerWithBestBlockInNetwork = currentPeers.maxBy(peerWithNum => peerWithNum._2.maxBlockNumber)
566+
567+
val peersWithTooFreshPossiblePivotBlock =
568+
getPeerWithTooFreshNewBlock(NonEmptyList.fromListUnsafe(currentPeers), syncState, syncConfig)
569+
570+
if (peersWithTooFreshPossiblePivotBlock.isEmpty) {
571+
log.info(s"There are not peers with to fresh possible pivot block, " +
572+
s"best peer has block with number: ${peerWithBestBlockInNetwork._2.maxBlockNumber}")
573+
false
549574
} else {
550-
getPeerWithTooFreshNewBlock(
551-
availablePeers,
552-
syncState,
553-
syncConfig
554-
).size >= syncConfig.minPeersToChoosePivotBlock
575+
val pivotBlockIsStale = peersWithTooFreshPossiblePivotBlock.size >= minPeersToChoosePivotBlock
576+
577+
log.info(s"There are ${peersWithTooFreshPossiblePivotBlock.size} peers with possible new pivot block, " +
578+
s"best known pivot in current peer list has number ${peerWithBestBlockInNetwork._2.maxBlockNumber}")
579+
580+
pivotBlockIsStale
555581
}
556582
}
557583
}
@@ -562,23 +588,9 @@ class FastSync(
562588
} else {
563589
if (blockchainDataToDownload) {
564590
processDownloads()
565-
} else if (syncState.isBlockchainWorkFinished && assignedHandlers.isEmpty && !syncState.stateSyncFinished) {
566-
if (peersToDownloadFrom.nonEmpty) {
567-
val bestBLock = peersToDownloadFrom.map { case (p, info) => info.maxBlockNumber }.max
568-
log.info(
569-
s"BestKnownBlock in network is ${bestBLock}. Target block is ${syncState.pivotBlock.number}." +
570-
s"Difference is ${bestBLock - syncState.pivotBlock.number}"
571-
)
572-
}
573-
574-
if (shouldUpdateStateTargetBlock() && !stateSyncRestartRequested) {
575-
val bestBLock = peersToDownloadFrom.map { case (p, info) => info.maxBlockNumber }.max
576-
log.info(
577-
s"Updating state sync target block. " +
578-
s"BestKnownBlock in network is ${bestBLock}. Target block is ${syncState.pivotBlock.number}." +
579-
s"Difference is ${bestBLock - syncState.pivotBlock.number}"
580-
)
581-
591+
} else if (noBlockchainWorkRemaining && !syncState.stateSyncFinished && notInTheMiddleOfUpdate) {
592+
if (pivotBlockIsStale()){
593+
log.info("Restarting state sync to new pivot block")
582594
syncStateScheduler ! RestartRequested
583595
stateSyncRestartRequested = true
584596
}
@@ -763,7 +775,7 @@ object FastSync {
763775
)
764776
)
765777

766-
private case class UpdatePivotBlock(state: FinalBlockProcessingResult)
778+
private case class UpdatePivotBlock(state: PivotBlockUpdateReason)
767779
private case object ProcessSyncing
768780

769781
private[sync] case object PersistSyncState
@@ -841,8 +853,15 @@ object FastSync {
841853

842854
case object ImportedPivotBlock extends HeaderProcessingResult
843855

844-
sealed abstract class FinalBlockProcessingResult
856+
sealed abstract class PivotBlockUpdateReason {
857+
def nodeRestart: Boolean = this match {
858+
case ImportedLastBlock => false
859+
case LastBlockValidationFailed => false
860+
case NodeRestart => true
861+
}
862+
}
845863

846-
case object ImportedLastBlock extends FinalBlockProcessingResult
847-
case object LastBlockValidationFailed extends FinalBlockProcessingResult
864+
case object ImportedLastBlock extends PivotBlockUpdateReason
865+
case object LastBlockValidationFailed extends PivotBlockUpdateReason
866+
case object NodeRestart extends PivotBlockUpdateReason
848867
}

src/main/scala/io/iohk/ethereum/utils/Config.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ object Config {
126126
maximumTargetUpdateFailures: Int,
127127
stateSyncBloomFilterSize: Int,
128128
stateSyncPersistBatchSize: Int,
129+
pivotBlockReScheduleInterval: FiniteDuration,
130+
maxPivotBlockAge: Int
129131
)
130132

131133
object SyncConfig {
@@ -166,7 +168,9 @@ object Config {
166168
maxTargetDifference = syncConfig.getInt("max-target-difference"),
167169
maximumTargetUpdateFailures = syncConfig.getInt("maximum-target-update-failures"),
168170
stateSyncBloomFilterSize = syncConfig.getInt("state-sync-bloomFilter-size"),
169-
stateSyncPersistBatchSize = syncConfig.getInt("state-sync-persistBatch-size")
171+
stateSyncPersistBatchSize = syncConfig.getInt("state-sync-persistBatch-size"),
172+
pivotBlockReScheduleInterval = syncConfig.getDuration("pivot-block-reSchedule-interval").toMillis.millis,
173+
maxPivotBlockAge = syncConfig.getInt("max-pivot-block-age"),
170174
)
171175
}
172176
}

src/test/resources/application.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ mantis {
121121
state-sync-bloomFilter-size = 20000
122122

123123
state-sync-persistBatch-size = 10000
124+
125+
pivot-block-reSchedule-interval = 15.seconds
126+
127+
max-pivot-block-age = 96
124128
}
125129

126130
keyStore {

src/test/scala/io/iohk/ethereum/blockchain/sync/StateSyncUtils.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ object StateSyncUtils extends EphemBlockchainTestSetup {
2929
}
3030
}
3131

32-
def buildWorld(accountData: Seq[MptNodeData]): ByteString = {
32+
def buildWorld(accountData: Seq[MptNodeData], existingTree: Option[ByteString] = None): ByteString = {
3333
val init: InMemoryWorldStateProxy = bl
3434
.getWorldStateProxy(
3535
blockNumber = 1,
3636
accountStartNonce = blockchainConfig.accountStartNonce,
37-
stateRootHash = None,
37+
stateRootHash = existingTree,
3838
noEmptyAccounts = true,
3939
ethCompatibleStorage = blockchainConfig.ethCompatibleStorage
4040
)

0 commit comments

Comments
 (0)