Skip to content

Commit 6e183f8

Browse files
authored
Merge pull request #940 from input-output-hk/bug/ETCM-678-after-node-restart
etcm-678 etcm-660 fix for removing chain after the node restart
2 parents 23534aa + d049089 commit 6e183f8

File tree

6 files changed

+104
-44
lines changed

6 files changed

+104
-44
lines changed

src/it/scala/io/iohk/ethereum/ledger/BlockImporterItSpec.scala

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ import cats.data.NonEmptyList
66
import io.iohk.ethereum.blockchain.sync.regular.BlockImporter.NewCheckpoint
77
import io.iohk.ethereum.blockchain.sync.regular.{BlockFetcher, BlockImporter}
88
import io.iohk.ethereum.checkpointing.CheckpointingTestHelpers
9+
import io.iohk.ethereum.consensus.{GetBlockHeaderByHash, GetNBlocksBack}
910
import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator
11+
import io.iohk.ethereum.consensus.ethash.validators.{OmmersValidator, StdOmmersValidator}
12+
import io.iohk.ethereum.consensus.validators.Validators
1013
import io.iohk.ethereum.domain._
1114
import io.iohk.ethereum.mpt.MerklePatriciaTrie
1215
import io.iohk.ethereum.utils.Config.SyncConfig
1316
import io.iohk.ethereum.utils.Config
14-
import io.iohk.ethereum.{Fixtures, ObjectGenerators, crypto}
17+
import io.iohk.ethereum.{Fixtures, Mocks, ObjectGenerators, crypto}
1518
import io.iohk.ethereum.ledger.Ledger.BlockResult
1619
import monix.execution.Scheduler
1720
import org.scalamock.scalatest.MockFactory
1821
import org.scalatest.BeforeAndAfterAll
22+
import org.scalatest.concurrent.Eventually.eventually
1923
import org.scalatest.flatspec.AsyncFlatSpecLike
2024
import org.scalatest.matchers.should.Matchers
2125

@@ -62,6 +66,18 @@ class BlockImporterItSpec
6266
ethCompatibleStorage = true
6367
)
6468

69+
override protected lazy val successValidators: Validators = new Mocks.MockValidatorsAlwaysSucceed {
70+
override val ommersValidator: OmmersValidator = (
71+
parentHash: ByteString,
72+
blockNumber: BigInt,
73+
ommers: Seq[BlockHeader],
74+
getBlockHeaderByHash: GetBlockHeaderByHash,
75+
getNBlocksBack: GetNBlocksBack
76+
) =>
77+
new StdOmmersValidator(blockchainConfig, blockHeaderValidator)
78+
.validate(parentHash, blockNumber, ommers, getBlockHeaderByHash, getNBlocksBack)
79+
}
80+
6581
override lazy val ledger = new TestLedgerImpl(successValidators) {
6682
override private[ledger] lazy val blockExecution =
6783
new BlockExecution(blockchain, blockchainConfig, consensus.blockPreparator, blockValidation) {
@@ -135,9 +151,8 @@ class BlockImporterItSpec
135151
blockImporter ! BlockImporter.Start
136152
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(newBranch))
137153

138-
Thread.sleep(1000)
139154
//because the blocks are not valid, we shouldn't reorganise, but at least stay with a current chain, and the best block of the current chain is oldBlock4
140-
blockchain.getBestBlock().get shouldEqual oldBlock4
155+
eventually { blockchain.getBestBlock().get shouldEqual oldBlock4 }
141156
}
142157

143158
it should "return a correct new best block after reorganising longer chain to a shorter one if its weight is bigger" in {
@@ -149,8 +164,34 @@ class BlockImporterItSpec
149164

150165
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(newBranch))
151166

152-
Thread.sleep(200)
153-
blockchain.getBestBlock().get shouldEqual newBlock3
167+
eventually { Thread.sleep(200); blockchain.getBestBlock().get shouldEqual newBlock3 }
168+
}
169+
170+
it should "return Unknown branch, in case of PickedBlocks with block that has a parent that's not in the chain" in {
171+
val newBlock4ParentOldBlock3: Block =
172+
getBlock(genesisBlock.number + 4, difficulty = 104, parent = oldBlock3.header.hash)
173+
val newBlock4WeightParentOldBlock3 = oldWeight3.increase(newBlock4ParentOldBlock3.header)
174+
175+
//Block n5 with oldBlock4 as parent
176+
val newBlock5ParentOldBlock4: Block =
177+
getBlock(
178+
genesisBlock.number + 5,
179+
difficulty = 108,
180+
parent = oldBlock4.header.hash,
181+
ommers = Seq(oldBlock4.header)
182+
)
183+
184+
blockchain.save(oldBlock2, Nil, oldWeight2, saveAsBestBlock = true)
185+
blockchain.save(oldBlock3, Nil, oldWeight3, saveAsBestBlock = true)
186+
blockchain.save(oldBlock4, Nil, oldWeight4, saveAsBestBlock = true)
187+
// simulation of node restart
188+
blockchain.saveBestKnownBlocks(blockchain.getBestBlockNumber() - 1)
189+
blockchain.save(newBlock4ParentOldBlock3, Nil, newBlock4WeightParentOldBlock3, saveAsBestBlock = true)
190+
191+
//not reorganising anymore until oldBlock4(not part of the chain anymore), no block/ommer validation when not part of the chain, resolveBranch is returning UnknownBranch
192+
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(List(newBlock5ParentOldBlock4)))
193+
194+
eventually { blockchain.getBestBlock().get shouldEqual newBlock4ParentOldBlock3 }
154195
}
155196

156197
it should "switch to a branch with a checkpoint" in {
@@ -163,9 +204,8 @@ class BlockImporterItSpec
163204

164205
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(newBranch))
165206

166-
Thread.sleep(200)
167-
blockchain.getBestBlock().get shouldEqual oldBlock5WithCheckpoint
168-
blockchain.getLatestCheckpointBlockNumber() shouldEqual oldBlock5WithCheckpoint.header.number
207+
eventually { blockchain.getBestBlock().get shouldEqual oldBlock5WithCheckpoint }
208+
eventually { blockchain.getLatestCheckpointBlockNumber() shouldEqual oldBlock5WithCheckpoint.header.number }
169209
}
170210

171211
it should "switch to a branch with a newer checkpoint" in {
@@ -178,9 +218,8 @@ class BlockImporterItSpec
178218

179219
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(newBranch))
180220

181-
Thread.sleep(200)
182-
blockchain.getBestBlock().get shouldEqual newBlock4WithCheckpoint
183-
blockchain.getLatestCheckpointBlockNumber() shouldEqual newBlock4WithCheckpoint.header.number
221+
eventually { blockchain.getBestBlock().get shouldEqual newBlock4WithCheckpoint }
222+
eventually { blockchain.getLatestCheckpointBlockNumber() shouldEqual newBlock4WithCheckpoint.header.number }
184223
}
185224

186225
it should "return a correct checkpointed block after receiving a request for generating a new checkpoint" in {
@@ -199,8 +238,9 @@ class BlockImporterItSpec
199238

200239
val checkpointBlock = checkpointBlockGenerator.generate(newBlock5, Checkpoint(signatures))
201240

202-
Thread.sleep(1000)
203-
blockchain.getBestBlock().get shouldEqual checkpointBlock
204-
blockchain.getLatestCheckpointBlockNumber() shouldEqual newBlock5.header.number + 1
241+
eventually { Thread.sleep(1000); blockchain.getBestBlock().get shouldEqual checkpointBlock }
242+
eventually {
243+
Thread.sleep(1000); blockchain.getLatestCheckpointBlockNumber() shouldEqual newBlock5.header.number + 1
244+
}
205245
}
206246
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,19 @@ trait Blockchain {
215215
def getStateStorage: StateStorage
216216

217217
def mptStateSavedKeys(): Observable[Either[IterationError, ByteString]]
218+
219+
/**
220+
* Strict check if given block hash is in chain
221+
* Using any of getXXXByHash is not always accurate - after restart the best block is often lower than before restart
222+
* The result of that is returning data of blocks which we don't consider as a part of the chain anymore
223+
* @param hash block hash
224+
*/
225+
def isInChain(hash: ByteString): Boolean = {
226+
(for {
227+
header <- getBlockHeaderByHash(hash) if header.number <= getBestBlockNumber()
228+
hash <- getHashByBlockNumber(header.number)
229+
} yield header.hash == hash).getOrElse(false)
230+
}
218231
}
219232
// scalastyle:on
220233

src/main/scala/io/iohk/ethereum/ledger/BranchResolution.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ class BranchResolution(blockchain: Blockchain) extends Logger {
1111
InvalidBranch
1212
} else {
1313
val knownParentOrGenesis = blockchain
14-
.getBlockHeaderByHash(headers.head.parentHash)
15-
.isDefined || headers.head.hash == blockchain.genesisHeader.hash
14+
.isInChain(headers.head.parentHash) || headers.head.hash == blockchain.genesisHeader.hash
1615

1716
if (!knownParentOrGenesis)
1817
UnknownBranch

src/test/scala/io/iohk/ethereum/domain/BlockchainSpec.scala

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,35 +26,44 @@ class BlockchainSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyCh
2626
val validBlock = Fixtures.Blocks.ValidBlock.block
2727
blockchain.storeBlock(validBlock).commit()
2828
val block = blockchain.getBlockByHash(validBlock.header.hash)
29-
assert(block.isDefined)
30-
assert(validBlock == block.get)
29+
block.isDefined should ===(true)
30+
validBlock should ===(block.get)
3131
val blockHeader = blockchain.getBlockHeaderByHash(validBlock.header.hash)
32-
assert(blockHeader.isDefined)
33-
assert(validBlock.header == blockHeader.get)
32+
blockHeader.isDefined should ===(true)
33+
validBlock.header should ===(blockHeader.get)
3434
val blockBody = blockchain.getBlockBodyByHash(validBlock.header.hash)
35-
assert(blockBody.isDefined)
36-
assert(validBlock.body == blockBody.get)
35+
blockBody.isDefined should ===(true)
36+
validBlock.body should ===(blockBody.get)
3737
}
3838

3939
it should "be able to store a block and retrieve it by number" in new EphemBlockchainTestSetup {
4040
val validBlock = Fixtures.Blocks.ValidBlock.block
4141
blockchain.storeBlock(validBlock).commit()
4242
val block = blockchain.getBlockByNumber(validBlock.header.number)
43-
assert(block.isDefined)
44-
assert(validBlock == block.get)
43+
block.isDefined should ===(true)
44+
validBlock should ===(block.get)
45+
}
46+
47+
it should "be able to do strict check of block existence in the chain" in new EphemBlockchainTestSetup {
48+
val validBlock = Fixtures.Blocks.ValidBlock.block
49+
blockchain.save(validBlock, Seq.empty, ChainWeight(100, 100), saveAsBestBlock = true)
50+
blockchain.isInChain(validBlock.hash) === (false)
51+
// simulation of node restart
52+
blockchain.saveBestKnownBlocks(validBlock.header.number - 1)
53+
blockchain.isInChain(validBlock.hash) should ===(false)
4554
}
4655

4756
it should "be able to query a stored blockHeader by it's number" in new EphemBlockchainTestSetup {
4857
val validHeader = Fixtures.Blocks.ValidBlock.header
4958
blockchain.storeBlockHeader(validHeader).commit()
5059
val header = blockchain.getBlockHeaderByNumber(validHeader.number)
51-
assert(header.isDefined)
52-
assert(validHeader == header.get)
60+
header.isDefined should ===(true)
61+
validHeader should ===(header.get)
5362
}
5463

5564
it should "not return a value if not stored" in new EphemBlockchainTestSetup {
56-
assert(blockchain.getBlockByNumber(Fixtures.Blocks.ValidBlock.header.number).isEmpty)
57-
assert(blockchain.getBlockByHash(Fixtures.Blocks.ValidBlock.header.hash).isEmpty)
65+
blockchain.getBlockByNumber(Fixtures.Blocks.ValidBlock.header.number).isEmpty should ===(true)
66+
blockchain.getBlockByHash(Fixtures.Blocks.ValidBlock.header.hash).isEmpty should ===(true)
5867
}
5968

6069
it should "be able to store a block with checkpoint and retrieve it and checkpoint" in new EphemBlockchainTestSetup {
@@ -66,8 +75,8 @@ class BlockchainSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyCh
6675
blockchain.save(validBlock, Seq.empty, ChainWeight(0, 0), saveAsBestBlock = true)
6776

6877
val retrievedBlock = blockchain.getBlockByHash(validBlock.header.hash)
69-
assert(retrievedBlock.isDefined)
70-
assert(validBlock == retrievedBlock.get)
78+
retrievedBlock.isDefined should ===(true)
79+
validBlock should ===(retrievedBlock.get)
7180

7281
blockchain.getLatestCheckpointBlockNumber() should ===(validBlock.number)
7382
blockchain.getBestBlockNumber() should ===(validBlock.number)

src/test/scala/io/iohk/ethereum/ledger/BranchResolutionSpec.scala

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ class BranchResolutionSpec
4141
val headers = getChainHeadersNel(5, 10)
4242

4343
setGenesisHeader(genesisHeader) // Check genesis block
44-
setBestBlockNumber(10)
45-
setHeaderByHash(headers.head.parentHash, None)
44+
setHeaderInChain(headers.head.parentHash, result = false)
4645

4746
ledger.resolveBranch(headers) shouldEqual UnknownBranch
4847
}
@@ -52,21 +51,21 @@ class BranchResolutionSpec
5251
val headers = getChainHeadersNel(1, 10)
5352

5453
setBestBlockNumber(10)
55-
setHeaderByHash(headers.head.parentHash, Some(getBlock(0).header))
54+
setHeaderInChain(headers.head.parentHash)
5655
setChainWeightByHash(headers.head.parentHash, ChainWeight.zero)
5756

5857
val oldBlocks = getChain(1, 10, headers.head.parentHash, headers.head.difficulty - 1)
5958
oldBlocks.map(b => setBlockByNumber(b.header.number, Some(b)))
6059

61-
ledger.resolveBranch(headers) shouldEqual NewBetterBranch(oldBlocks.toList)
60+
ledger.resolveBranch(headers) shouldEqual NewBetterBranch(oldBlocks)
6261
}
6362

6463
"report no need for a chain switch the headers do not have chain weight greater than currently known branch" in
6564
new BranchResolutionTestSetup {
6665
val headers = getChainHeadersNel(1, 10)
6766

6867
setBestBlockNumber(10)
69-
setHeaderByHash(headers.head.parentHash, Some(getBlock(0).header))
68+
setHeaderInChain(headers.head.parentHash)
7069
setChainWeightByHash(headers.head.parentHash, ChainWeight.zero)
7170

7271
val oldBlocks = getChain(1, 10, headers.head.parentHash, headers.head.difficulty)
@@ -78,6 +77,7 @@ class BranchResolutionSpec
7877
"correctly handle a branch that goes up to the genesis block" in new BranchResolutionTestSetup {
7978
val headers = genesisHeader :: getChainHeadersNel(1, 10, genesisHeader.hash)
8079

80+
setHeaderInChain(genesisHeader.parentHash, result = false)
8181
setGenesisHeader(genesisHeader)
8282
setBestBlockNumber(10)
8383
setChainWeightByHash(genesisHeader.hash, ChainWeight.zero)
@@ -93,6 +93,7 @@ class BranchResolutionSpec
9393
val differentGenesis: BlockHeader = genesisHeader.copy(extraData = ByteString("I'm different ;("))
9494
val headers = differentGenesis :: getChainHeadersNel(1, 10, differentGenesis.hash)
9595

96+
setHeaderInChain(differentGenesis.parentHash, result = false)
9697
setGenesisHeader(genesisHeader)
9798
setBestBlockNumber(10)
9899

@@ -104,7 +105,7 @@ class BranchResolutionSpec
104105
val commonParent = headers.toList(1)
105106

106107
setBestBlockNumber(8)
107-
setHeaderByHash(headers.head.parentHash, Some(getBlock(0).header))
108+
setHeaderInChain(headers.head.parentHash)
108109
setChainWeightByHash(commonParent.hash, ChainWeight.zero)
109110

110111
val oldBlocks = getChain(3, 8, commonParent.hash)
@@ -123,7 +124,7 @@ class BranchResolutionSpec
123124
val longerBranchLowerWeight = getChain(2, 10, commonParent.hash, difficulty = 100)
124125
val shorterBranchHigherWeight = getChainNel(2, 8, commonParent.hash, difficulty = 200)
125126

126-
setHeaderByHash(commonParent.hash, Some(commonParent.header))
127+
setHeaderInChain(commonParent.hash)
127128
setChainWeightForBlock(commonParent, parentWeight)
128129
setBestBlockNumber(longerBranchLowerWeight.last.number)
129130
longerBranchLowerWeight.foreach(b => setBlockByNumber(b.number, Some(b)))
@@ -150,7 +151,7 @@ class BranchResolutionSpec
150151

151152
val noCheckpointBranch = getChain(2, checkpointBranchLength + 2, commonParent.hash)
152153

153-
setHeaderByHash(commonParent.hash, Some(commonParent.header))
154+
setHeaderInChain(commonParent.hash)
154155
setChainWeightForBlock(commonParent, parentWeight)
155156
setBestBlockNumber(noCheckpointBranch.last.number)
156157
noCheckpointBranch.foreach(b => setBlockByNumber(b.number, Some(b)))
@@ -176,7 +177,7 @@ class BranchResolutionSpec
176177

177178
val noCheckpointBranch = getChainNel(2, checkpointBranchLength + 2, commonParent.hash)
178179

179-
setHeaderByHash(commonParent.hash, Some(commonParent.header))
180+
setHeaderInChain(commonParent.hash)
180181
setChainWeightForBlock(commonParent, parentWeight)
181182
setBestBlockNumber(checkpointBranch.last.number)
182183
checkpointBranch.map(b => setBlockByNumber(b.number, Some(b)))

src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,16 +400,14 @@ trait MockBlockchain extends MockFactory { self: TestSetupWithVmAndValidators =>
400400
.once()
401401
}
402402

403-
def setHeaderByHash(hash: ByteString, header: Option[BlockHeader]): CallHandler1[ByteString, Option[BlockHeader]] =
404-
(blockchain.getBlockHeaderByHash _).expects(hash).returning(header)
403+
def setHeaderInChain(hash: ByteString, result: Boolean = true): CallHandler1[ByteString, Boolean] =
404+
(blockchain.isInChain _).expects(hash).returning(result)
405405

406406
def setBlockByNumber(number: BigInt, block: Option[Block]): CallHandler1[BigInt, Option[Block]] =
407407
(blockchain.getBlockByNumber _).expects(number).returning(block)
408408

409-
def setGenesisHeader(header: BlockHeader): Unit = {
409+
def setGenesisHeader(header: BlockHeader): Unit =
410410
(() => blockchain.genesisHeader).expects().returning(header)
411-
setHeaderByHash(header.parentHash, None)
412-
}
413411
}
414412

415413
trait EphemBlockchain extends TestSetupWithVmAndValidators with MockFactory {

0 commit comments

Comments
 (0)