Skip to content

Commit 327d295

Browse files
author
Nicolás Tallar
authored
[ETCM-186] Fix strict pick with too few blocks (#723)
1 parent 6967d09 commit 327d295

File tree

6 files changed

+99
-37
lines changed

6 files changed

+99
-37
lines changed

src/main/scala/io/iohk/ethereum/blockchain/sync/regular/BlockFetcher.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,17 @@ class BlockFetcher(
6868
private def handleCommands(state: BlockFetcherState): Receive = {
6969
case PickBlocks(amount) => state.pickBlocks(amount) |> handlePickedBlocks(state) |> fetchBlocks
7070
case StrictPickBlocks(from, atLeastWith) =>
71-
val minBlock = from.min(atLeastWith).max(1)
72-
log.debug("Strict Pick blocks from {} to {}", from, atLeastWith)
71+
// FIXME: Consider having StrictPickBlocks calls guaranteeing this
72+
// from parameter could be negative or 0 so we should cap it to 1 if that's the case
73+
val fromCapped = from.max(1)
74+
val minBlock = fromCapped.min(atLeastWith).max(1)
75+
log.debug("Strict Pick blocks from {} to {}", fromCapped, atLeastWith)
7376
log.debug("Lowest available block is {}", state.lowestBlock)
7477

7578
val newState = if (minBlock < state.lowestBlock) {
7679
state.invalidateBlocksFrom(minBlock, None)._2
7780
} else {
78-
state.strictPickBlocks(from, atLeastWith) |> handlePickedBlocks(state)
81+
state.strictPickBlocks(fromCapped, atLeastWith) |> handlePickedBlocks(state)
7982
}
8083

8184
fetchBlocks(newState)

src/main/scala/io/iohk/ethereum/blockchain/sync/regular/BlockFetcherState.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,15 @@ case class BlockFetcherState(
107107
None
108108
}
109109

110+
/**
111+
* Returns all the ready blocks but only if it includes blocks with number:
112+
* - lower = min(from, atLeastWith)
113+
* - upper = max(from, atLeastWith)
114+
*/
110115
def strictPickBlocks(from: BigInt, atLeastWith: BigInt): Option[(NonEmptyList[Block], BlockFetcherState)] = {
111116
val lower = from.min(atLeastWith)
112117
val upper = from.max(atLeastWith)
118+
113119
readyBlocks.some
114120
.filter(_.headOption.exists(block => block.number <= lower))
115121
.filter(_.lastOption.exists(block => block.number >= upper))

src/main/scala/io/iohk/ethereum/blockchain/sync/regular/BlockImporter.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ class BlockImporter(
105105

106106
private def importBlocks(blocks: NonEmptyList[Block]): ImportFn =
107107
importWith {
108-
log.debug("Attempting to import blocks starting from {}", blocks.head.number)
108+
log.debug(
109+
"Attempting to import blocks starting from {} and ending with {}",
110+
blocks.head.number,
111+
blocks.last.number
112+
)
109113
Future
110114
.successful(resolveBranch(blocks))
111115
.flatMap {

src/main/scala/io/iohk/ethereum/domain/BlockHeader.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ case class BlockHeader(
2727

2828
override def toString: String = {
2929
s"""BlockHeader {
30+
|hash: $hashAsHexString
3031
|parentHash: ${Hex.toHexString(parentHash.toArray[Byte])}
3132
|ommersHash: ${Hex.toHexString(ommersHash.toArray[Byte])}
3233
|beneficiary: ${Hex.toHexString(beneficiary.toArray[Byte])}

src/test/scala/io/iohk/ethereum/blockchain/sync/regular/BlockFetcherStateSpec.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,5 @@ class BlockFetcherStateSpec extends TestKit(ActorSystem()) with AnyWordSpecLike
3333
newState.knownTop shouldEqual newBestBlock.number
3434
}
3535
}
36-
3736
}
3837
}

src/test/scala/io/iohk/ethereum/blockchain/sync/regular/RegularSyncSpec.scala

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -134,38 +134,6 @@ class RegularSyncSpec extends RegularSyncFixtures with AnyWordSpecLike with Befo
134134
}
135135

136136
"resolving branches" should {
137-
trait FakeLedger { self: Fixture =>
138-
class FakeLedgerImpl extends TestLedgerImpl {
139-
override def importBlock(block: Block)(
140-
implicit blockExecutionContext: ExecutionContext): Future[BlockImportResult] = {
141-
val result: BlockImportResult = if (didTryToImportBlock(block)) {
142-
DuplicateBlock
143-
} else {
144-
if (importedBlocks.isEmpty || bestBlock.isParentOf(block) || importedBlocks.exists(_.isParentOf(block))) {
145-
importedBlocks.add(block)
146-
BlockImportedToTop(List(BlockData(block, Nil, block.header.difficulty)))
147-
} else if (block.number > bestBlock.number) {
148-
importedBlocks.add(block)
149-
BlockEnqueued
150-
} else {
151-
BlockImportFailed("foo")
152-
}
153-
}
154-
155-
Future.successful(result)
156-
}
157-
158-
override def resolveBranch(headers: scala.Seq[BlockHeader]): BranchResolutionResult = {
159-
val importedHashes = importedBlocks.map(_.hash).toSet
160-
161-
if (importedBlocks.isEmpty || (importedHashes.contains(headers.head.parentHash) && headers.last.number > bestBlock.number)) {
162-
NewBetterBranch(Nil)
163-
} else {
164-
UnknownBranch
165-
}
166-
}
167-
}
168-
}
169137

170138
"go back to earlier block in order to find a common parent with new branch" in new Fixture(testSystem)
171139
with FakeLedger {
@@ -216,6 +184,55 @@ class RegularSyncSpec extends RegularSyncFixtures with AnyWordSpecLike with Befo
216184
}
217185
}
218186

187+
"go back to earlier positive block in order to resolve a fork when branch smaller than branch resolution size" in new Fixture(testSystem)
188+
with FakeLedger {
189+
implicit val ec: ExecutionContext = system.dispatcher
190+
override lazy val blockchain: BlockchainImpl = stub[BlockchainImpl]
191+
(blockchain.getBestBlockNumber _).when().onCall(() => ledger.bestBlock.number)
192+
override lazy val ledger: TestLedgerImpl = new FakeLedgerImpl()
193+
override lazy val syncConfig = defaultSyncConfig.copy(
194+
syncRetryInterval = 1.second,
195+
printStatusInterval = 0.5.seconds,
196+
branchResolutionRequestSize = 12, // Over the original branch size
197+
198+
// Big so that they don't impact the test
199+
blockHeadersPerRequest = 50,
200+
blockBodiesPerRequest = 50,
201+
blocksBatchSize = 50
202+
)
203+
204+
val originalBranch = getBlocks(10, genesis)
205+
val betterBranch = getBlocks(originalBranch.size * 2, genesis)
206+
207+
class ForkingAutoPilot(blocksToRespond: List[Block], forkedBlocks: Option[List[Block]])
208+
extends PeersClientAutoPilot(blocksToRespond) {
209+
override def overrides(sender: ActorRef): PartialFunction[Any, Option[AutoPilot]] = {
210+
case req @ PeersClient.Request(GetBlockBodies(hashes), _, _) =>
211+
val defaultResult = defaultHandlers(sender)(req)
212+
if (forkedBlocks.nonEmpty && hashes.contains(blocksToRespond.last.hash)) {
213+
Some(new ForkingAutoPilot(forkedBlocks.get, None))
214+
} else
215+
defaultResult
216+
}
217+
}
218+
219+
peersClient.setAutoPilot(new ForkingAutoPilot(originalBranch, Some(betterBranch)))
220+
221+
Await.result(ledger.importBlock(genesis), remainingOrDefault)
222+
223+
regularSync ! RegularSync.Start
224+
225+
peerEventBus.expectMsgClass(classOf[Subscribe])
226+
val blockFetcher = peerEventBus.sender()
227+
peerEventBus.reply(MessageFromPeer(NewBlock(originalBranch.last, originalBranch.last.number), defaultPeer.id))
228+
229+
awaitCond(ledger.bestBlock == originalBranch.last, 5.seconds)
230+
231+
// As node will be on top, we have to re-trigger the fetching process by simulating a block from the fork being broadcasted
232+
blockFetcher ! MessageFromPeer(NewBlock(betterBranch.last, betterBranch.last.number), defaultPeer.id)
233+
awaitCond(ledger.bestBlock == betterBranch.last, 5.seconds)
234+
}
235+
219236
"fetching state node" should {
220237
abstract class MissingStateNodeFixture(system: ActorSystem) extends Fixture(system) {
221238
val failingBlock: Block = testBlocksChunked.head.head
@@ -457,4 +474,36 @@ class RegularSyncSpec extends RegularSyncFixtures with AnyWordSpecLike with Befo
457474
}
458475
}
459476
}
477+
478+
trait FakeLedger { self: Fixture =>
479+
class FakeLedgerImpl extends TestLedgerImpl {
480+
override def importBlock(block: Block)(
481+
implicit blockExecutionContext: ExecutionContext): Future[BlockImportResult] = {
482+
val result: BlockImportResult = if (didTryToImportBlock(block)) {
483+
DuplicateBlock
484+
} else {
485+
if (importedBlocks.isEmpty || bestBlock.isParentOf(block) || importedBlocks.exists(_.isParentOf(block))) {
486+
importedBlocks.add(block)
487+
BlockImportedToTop(List(BlockData(block, Nil, block.header.difficulty)))
488+
} else if (block.number > bestBlock.number) {
489+
importedBlocks.add(block)
490+
BlockEnqueued
491+
} else {
492+
BlockImportFailed("foo")
493+
}
494+
}
495+
496+
Future.successful(result)
497+
}
498+
499+
override def resolveBranch(headers: Seq[BlockHeader]): BranchResolutionResult = {
500+
val importedHashes = importedBlocks.map(_.hash).toSet
501+
502+
if (importedBlocks.isEmpty || (importedHashes.contains(headers.head.parentHash) && headers.last.number > bestBlock.number))
503+
NewBetterBranch(Nil)
504+
else
505+
UnknownBranch
506+
}
507+
}
508+
}
460509
}

0 commit comments

Comments
 (0)