Skip to content

Commit b45bf0c

Browse files
authored
Merge pull request #925 from input-output-hk/bug/ETCM-636
Bug/ETCM-636
2 parents e9f0cf7 + c866c95 commit b45bf0c

File tree

9 files changed

+284
-30
lines changed

9 files changed

+284
-30
lines changed

project/Dependencies.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ object Dependencies {
5959

6060
val testing: Seq[ModuleID] = Seq(
6161
"org.scalatest" %% "scalatest" % "3.2.2" % "it,test",
62-
"org.scalamock" %% "scalamock" % "5.0.0" % "test",
62+
"org.scalamock" %% "scalamock" % "5.0.0" % "it,test",
6363
"org.scalatestplus" %% "scalacheck-1-15" % "3.2.3.0" % "test",
6464
"org.scalacheck" %% "scalacheck" % "1.15.1" % "it,test",
6565
"com.softwaremill.diffx" %% "diffx-core" % "0.3.30" % "test",
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package io.iohk.ethereum.ledger
2+
3+
import akka.testkit.TestProbe
4+
import akka.util.ByteString
5+
import cats.data.NonEmptyList
6+
import io.iohk.ethereum.blockchain.sync.regular.BlockImporter.NewCheckpoint
7+
import io.iohk.ethereum.blockchain.sync.regular.{BlockFetcher, BlockImporter}
8+
import io.iohk.ethereum.checkpointing.CheckpointingTestHelpers
9+
import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator
10+
import io.iohk.ethereum.domain._
11+
import io.iohk.ethereum.mpt.MerklePatriciaTrie
12+
import io.iohk.ethereum.utils.Config.SyncConfig
13+
import io.iohk.ethereum.utils.Config
14+
import io.iohk.ethereum.{Fixtures, ObjectGenerators, crypto}
15+
import io.iohk.ethereum.ledger.Ledger.BlockResult
16+
import monix.execution.Scheduler
17+
import org.scalamock.scalatest.MockFactory
18+
import org.scalatest.BeforeAndAfterAll
19+
import org.scalatest.flatspec.AsyncFlatSpecLike
20+
import org.scalatest.matchers.should.Matchers
21+
22+
import scala.concurrent.duration._
23+
24+
class BlockImporterItSpec extends MockFactory with TestSetupWithVmAndValidators with AsyncFlatSpecLike with Matchers with BeforeAndAfterAll {
25+
26+
implicit val testScheduler = Scheduler.fixedPool("test", 32)
27+
28+
override def afterAll(): Unit = {
29+
testScheduler.shutdown()
30+
testScheduler.awaitTermination(60.second)
31+
}
32+
33+
val blockQueue = BlockQueue(blockchain, SyncConfig(Config.config))
34+
35+
val genesis = Block(
36+
Fixtures.Blocks.Genesis.header.copy(stateRoot = ByteString(MerklePatriciaTrie.EmptyRootHash)),
37+
Fixtures.Blocks.Genesis.body
38+
)
39+
val genesisWeight = ChainWeight.zero.increase(genesis.header)
40+
41+
blockchain.save(genesis, Seq(), genesisWeight, saveAsBestBlock = true)
42+
43+
lazy val checkpointBlockGenerator: CheckpointBlockGenerator = new CheckpointBlockGenerator
44+
45+
val fetcherProbe = TestProbe()
46+
val ommersPoolProbe = TestProbe()
47+
val broadcasterProbe = TestProbe()
48+
val pendingTransactionsManagerProbe = TestProbe()
49+
val supervisor = TestProbe()
50+
51+
val emptyWorld: InMemoryWorldStateProxy =
52+
blockchain.getWorldStateProxy(
53+
-1,
54+
UInt256.Zero,
55+
ByteString(MerklePatriciaTrie.EmptyRootHash),
56+
noEmptyAccounts = false,
57+
ethCompatibleStorage = true
58+
)
59+
60+
override lazy val ledger = new TestLedgerImpl(successValidators) {
61+
override private[ledger] lazy val blockExecution = new BlockExecution(blockchain, blockchainConfig, consensus.blockPreparator, blockValidation) {
62+
override def executeAndValidateBlock(block: Block, alreadyValidated: Boolean = false): Either[BlockExecutionError, Seq[Receipt]] =
63+
Right(BlockResult(emptyWorld).receipts)
64+
}
65+
}
66+
67+
val blockImporter = system.actorOf(
68+
BlockImporter.props(
69+
fetcherProbe.ref,
70+
ledger,
71+
blockchain,
72+
syncConfig,
73+
ommersPoolProbe.ref,
74+
broadcasterProbe.ref,
75+
pendingTransactionsManagerProbe.ref,
76+
checkpointBlockGenerator,
77+
supervisor.ref
78+
))
79+
80+
val genesisBlock = blockchain.genesisBlock
81+
val block1: Block = getBlock(genesisBlock.number + 1, parent = genesisBlock.header.hash)
82+
// new chain is shorter but has a higher weight
83+
val newBlock2: Block = getBlock(genesisBlock.number + 2, difficulty = 108, parent = block1.header.hash)
84+
val newBlock3: Block = getBlock(genesisBlock.number + 3, difficulty = 300, parent = newBlock2.header.hash)
85+
val oldBlock2: Block = getBlock(genesisBlock.number + 2, difficulty = 102, parent = block1.header.hash)
86+
val oldBlock3: Block = getBlock(genesisBlock.number + 3, difficulty = 103, parent = oldBlock2.header.hash)
87+
val oldBlock4: Block = getBlock(genesisBlock.number + 4, difficulty = 104, parent = oldBlock3.header.hash)
88+
89+
val weight1 = ChainWeight.totalDifficultyOnly(block1.header.difficulty)
90+
val newWeight2 = weight1.increase(newBlock2.header)
91+
val newWeight3 = newWeight2.increase(newBlock3.header)
92+
val oldWeight2 = weight1.increase(oldBlock2.header)
93+
val oldWeight3 = oldWeight2.increase(oldBlock3.header)
94+
val oldWeight4 = oldWeight3.increase(oldBlock4.header)
95+
96+
//saving initial main chain
97+
blockchain.save(block1, Nil, weight1, saveAsBestBlock = true)
98+
blockchain.save(oldBlock2, Nil, oldWeight2, saveAsBestBlock = true)
99+
blockchain.save(oldBlock3, Nil, oldWeight3, saveAsBestBlock = true)
100+
blockchain.save(oldBlock4, Nil, oldWeight4, saveAsBestBlock = true)
101+
102+
val oldBranch = List(oldBlock2, oldBlock3, oldBlock4)
103+
val newBranch = List(newBlock2, newBlock3)
104+
105+
blockImporter ! BlockImporter.Start
106+
107+
"BlockImporter" should "not discard blocks of the main chain if the reorganisation failed" in {
108+
109+
//ledger with not mocked blockExecution
110+
val ledger = new TestLedgerImpl(successValidators)
111+
val blockImporter = system.actorOf(
112+
BlockImporter.props(
113+
fetcherProbe.ref,
114+
ledger,
115+
blockchain,
116+
syncConfig,
117+
ommersPoolProbe.ref,
118+
broadcasterProbe.ref,
119+
pendingTransactionsManagerProbe.ref,
120+
checkpointBlockGenerator,
121+
supervisor.ref
122+
))
123+
124+
blockImporter ! BlockImporter.Start
125+
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(newBranch))
126+
127+
Thread.sleep(1000)
128+
//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
129+
blockchain.getBestBlock().get shouldEqual oldBlock4
130+
}
131+
132+
it should "return a correct new best block after reorganising longer chain to a shorter one if its weight is bigger" in {
133+
134+
//returning discarded initial chain
135+
blockchain.save(oldBlock2, Nil, oldWeight2, saveAsBestBlock = true)
136+
blockchain.save(oldBlock3, Nil, oldWeight3, saveAsBestBlock = true)
137+
blockchain.save(oldBlock4, Nil, oldWeight4, saveAsBestBlock = true)
138+
139+
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(newBranch))
140+
141+
Thread.sleep(200)
142+
blockchain.getBestBlock().get shouldEqual newBlock3
143+
}
144+
145+
146+
it should "switch to a branch with a checkpoint" in {
147+
148+
val checkpoint = ObjectGenerators.fakeCheckpointGen(3, 3).sample.get
149+
val oldBlock5WithCheckpoint: Block = checkpointBlockGenerator.generate(oldBlock4, checkpoint)
150+
blockchain.save(oldBlock5WithCheckpoint, Nil, oldWeight4, saveAsBestBlock = true)
151+
152+
val newBranch = List(newBlock2, newBlock3)
153+
154+
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(newBranch))
155+
156+
Thread.sleep(200)
157+
blockchain.getBestBlock().get shouldEqual oldBlock5WithCheckpoint
158+
blockchain.getLatestCheckpointBlockNumber() shouldEqual oldBlock5WithCheckpoint.header.number
159+
}
160+
161+
it should "switch to a branch with a newer checkpoint" in {
162+
163+
val checkpoint = ObjectGenerators.fakeCheckpointGen(3, 3).sample.get
164+
val newBlock4WithCheckpoint: Block = checkpointBlockGenerator.generate(newBlock3, checkpoint)
165+
blockchain.save(newBlock4WithCheckpoint, Nil, newWeight3, saveAsBestBlock = true)
166+
167+
val newBranch = List(newBlock4WithCheckpoint)
168+
169+
blockImporter ! BlockFetcher.PickedBlocks(NonEmptyList.fromListUnsafe(newBranch))
170+
171+
Thread.sleep(200)
172+
blockchain.getBestBlock().get shouldEqual newBlock4WithCheckpoint
173+
blockchain.getLatestCheckpointBlockNumber() shouldEqual newBlock4WithCheckpoint.header.number
174+
}
175+
176+
it should "return a correct checkpointed block after receiving a request for generating a new checkpoint" in {
177+
178+
val parent = blockchain.getBestBlock().get
179+
val newBlock5: Block = getBlock(genesisBlock.number + 5, difficulty = 104, parent = parent.header.hash)
180+
val newWeight5 = newWeight3.increase(newBlock5.header)
181+
182+
blockchain.save(newBlock5, Nil, newWeight5, saveAsBestBlock = true)
183+
184+
val signatures = CheckpointingTestHelpers.createCheckpointSignatures(
185+
Seq(crypto.generateKeyPair(secureRandom)),
186+
newBlock5.hash
187+
)
188+
blockImporter ! NewCheckpoint(newBlock5.hash, signatures)
189+
190+
val checkpointBlock = checkpointBlockGenerator.generate(newBlock5, Checkpoint(signatures))
191+
192+
Thread.sleep(1000)
193+
blockchain.getBestBlock().get shouldEqual checkpointBlock
194+
blockchain.getLatestCheckpointBlockNumber() shouldEqual newBlock5.header.number + 1
195+
}
196+
}

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,6 @@ class BlockchainImpl(
341341
}
342342

343343
def save(block: Block, receipts: Seq[Receipt], weight: ChainWeight, saveAsBestBlock: Boolean): Unit = {
344-
log.debug("Saving new block block {} to database", block.idTag)
345-
storeBlock(block)
346-
.and(storeReceipts(block.header.hash, receipts))
347-
.and(storeChainWeight(block.header.hash, weight))
348-
.commit()
349-
350344
if (saveAsBestBlock && block.hasCheckpoint) {
351345
log.debug(
352346
"New best known block block number - {}, new best checkpoint number - {}",
@@ -362,6 +356,12 @@ class BlockchainImpl(
362356
saveBestKnownBlock(block.header.number)
363357
}
364358

359+
log.debug("Saving new block block {} to database", block.idTag)
360+
storeBlock(block)
361+
.and(storeReceipts(block.header.hash, receipts))
362+
.and(storeChainWeight(block.header.hash, weight))
363+
.commit()
364+
365365
// not transactional part
366366
// the best blocks data will be persisted only when the cache will be persisted
367367
stateStorage.onBlockSave(block.header.number, appStateStorage.getBestBlockNumber())(persistBestBlocksData)
@@ -397,20 +397,17 @@ class BlockchainImpl(
397397
}
398398
}
399399

400-
private def saveBestKnownBlock(bestBlockNumber: BigInt): Unit = {
400+
private def saveBestKnownBlock(bestBlockNumber: BigInt): Unit =
401401
bestKnownBlockAndLatestCheckpoint.updateAndGet(_.copy(bestBlockNumber = bestBlockNumber))
402-
}
403402

404-
private def saveBestKnownBlockAndLatestCheckpointNumber(number: BigInt, latestCheckpointNumber: BigInt): Unit = {
403+
private def saveBestKnownBlockAndLatestCheckpointNumber(number: BigInt, latestCheckpointNumber: BigInt): Unit =
405404
bestKnownBlockAndLatestCheckpoint.set(BestBlockLatestCheckpointNumbers(number, latestCheckpointNumber))
406-
}
407405

408406
def storeChainWeight(blockhash: ByteString, weight: ChainWeight): DataSourceBatchUpdate =
409407
chainWeightStorage.put(blockhash, weight)
410408

411-
def saveNode(nodeHash: NodeHash, nodeEncoded: NodeEncoded, blockNumber: BigInt): Unit = {
409+
def saveNode(nodeHash: NodeHash, nodeEncoded: NodeEncoded, blockNumber: BigInt): Unit =
412410
stateStorage.saveNode(nodeHash, nodeEncoded, blockNumber)
413-
}
414411

415412
override protected def getHashByBlockNumber(number: BigInt): Option[ByteString] =
416413
blockNumberMappingStorage.get(number)

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import io.iohk.ethereum.ledger.BlockExecutionError.MissingParentError
55
import io.iohk.ethereum.ledger.Ledger.BlockResult
66
import io.iohk.ethereum.utils.{BlockchainConfig, DaoForkConfig, Logger}
77
import io.iohk.ethereum.vm.EvmConfig
8+
89
import scala.annotation.tailrec
10+
import cats.implicits._
11+
import io.iohk.ethereum.mpt.MerklePatriciaTrie.MPTException
912

1013
class BlockExecution(
1114
blockchain: BlockchainImpl,
@@ -59,7 +62,9 @@ class BlockExecution(
5962
.getBlockHeaderByHash(block.header.parentHash)
6063
.toRight(MissingParentError) // Should not never occur because validated earlier
6164
execResult <- executeBlockTransactions(block, parent)
62-
worldToPersist = blockPreparator.payBlockReward(block, execResult.worldState)
65+
worldToPersist <- Either
66+
.catchOnly[MPTException](blockPreparator.payBlockReward(block, execResult.worldState))
67+
.leftMap(BlockExecutionError.MPTError.apply)
6368
// State root hash needs to be up-to-date for validateBlockAfterExecution
6469
worldPersisted = InMemoryWorldStateProxy.persistState(worldToPersist)
6570
} yield execResult.copy(worldState = worldPersisted)
@@ -169,19 +174,21 @@ sealed trait BlockExecutionError {
169174

170175
sealed trait BlockExecutionSuccess
171176

172-
case object BlockExecutionSuccess extends BlockExecutionSuccess
177+
final case object BlockExecutionSuccess extends BlockExecutionSuccess
173178

174179
object BlockExecutionError {
175-
case class ValidationBeforeExecError(reason: Any) extends BlockExecutionError
180+
final case class ValidationBeforeExecError(reason: Any) extends BlockExecutionError
176181

177-
case class StateBeforeFailure(worldState: InMemoryWorldStateProxy, acumGas: BigInt, acumReceipts: Seq[Receipt])
182+
final case class StateBeforeFailure(worldState: InMemoryWorldStateProxy, acumGas: BigInt, acumReceipts: Seq[Receipt])
178183

179-
case class TxsExecutionError(stx: SignedTransaction, stateBeforeError: StateBeforeFailure, reason: String)
184+
final case class TxsExecutionError(stx: SignedTransaction, stateBeforeError: StateBeforeFailure, reason: String)
180185
extends BlockExecutionError
181186

182-
case class ValidationAfterExecError(reason: String) extends BlockExecutionError
187+
final case class ValidationAfterExecError(reason: String) extends BlockExecutionError
183188

184-
case object MissingParentError extends BlockExecutionError {
189+
final case object MissingParentError extends BlockExecutionError {
185190
override val reason: Any = "Cannot find parent"
186191
}
192+
193+
final case class MPTError(reason: MPTException) extends BlockExecutionError
187194
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ class BlockValidation(consensus: Consensus, blockchain: Blockchain, blockQueue:
4343

4444
def validateBlockAfterExecution(
4545
block: Block,
46-
hash: ByteString,
46+
stateRootHash: ByteString,
4747
receipts: Seq[Receipt],
4848
gasUsed: BigInt
4949
): Either[BlockExecutionError, BlockExecutionSuccess] = {
5050
consensus.validators.validateBlockAfterExecution(
5151
block = block,
52-
stateRootHash = hash,
52+
stateRootHash = stateRootHash,
5353
receipts = receipts,
5454
gasUsed = gasUsed
5555
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import scala.concurrent.ExecutionContext
2121
* [[io.iohk.ethereum.nodebuilder.Node Node]].
2222
*/
2323
trait ScenarioSetup extends StdTestConsensusBuilder with SyncConfigBuilder with StdLedgerBuilder {
24-
protected lazy val executionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
25-
protected lazy val monixScheduler = Scheduler(executionContext)
24+
protected lazy val executionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
25+
protected lazy val monixScheduler = Scheduler(executionContextExecutor)
2626
protected lazy val successValidators: Validators = Mocks.MockValidatorsAlwaysSucceed
2727
protected lazy val failureValidators: Validators = Mocks.MockValidatorsAlwaysFail
2828
protected lazy val ethashValidators: ValidatorsExecutor = ValidatorsExecutor(blockchainConfig, Protocol.Ethash)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class BlockFetcherSpec
102102
peersClient.expectMsgPF() { case PeersClient.Request(msg, _, _) if msg == firstGetBlockHeadersRequest => () }
103103
}
104104

105-
"should not enqueue requested blocks if the received bodies does not match" in new TestSetup {
105+
"should not enqueue requested blocks if the received bodies do not match" in new TestSetup {
106106

107107
// Important: Here we are forcing the mismatch between request headers and received bodies
108108
override lazy val validators = new MockValidatorsFailingOnBlockBodies

src/test/scala/io/iohk/ethereum/db/dataSource/DataSourceTestBehavior.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package io.iohk.ethereum.db.dataSource
22

33
import java.io.File
44
import java.nio.file.Files
5-
65
import io.iohk.ethereum.ObjectGenerators
76
import io.iohk.ethereum.db.dataSource.DataSource.{Key, Namespace, Value}
7+
import io.iohk.ethereum.db.dataSource.RocksDbDataSource.RocksDbDataSourceClosedException
88
import org.scalatest.flatspec.AnyFlatSpec
99
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
1010

@@ -48,6 +48,15 @@ trait DataSourceTestBehavior extends ScalaCheckPropertyChecks with ObjectGenerat
4848
}
4949
}
5050

51+
it should "throw an exception if the rocksdb storage is unavailable" in {
52+
withDir { path =>
53+
val dataSource = createDataSource(path)
54+
val someByteString = byteStringOfLengthNGen(KeySizeWithoutPrefix).sample.get
55+
dataSource.destroy()
56+
assertThrows[RocksDbDataSourceClosedException](dataSource.update(prepareUpdate(toUpsert = Seq(someByteString -> someByteString))))
57+
}
58+
}
59+
5160
it should "allow to remove keys" in {
5261
val key1 = byteStringOfLengthNGen(KeySizeWithoutPrefix).sample.get
5362
val key2 = byteStringOfLengthNGen(KeySizeWithoutPrefix).sample.get
@@ -111,5 +120,4 @@ trait DataSourceTestBehavior extends ScalaCheckPropertyChecks with ObjectGenerat
111120
}
112121
}
113122
// scalastyle:on
114-
115123
}

0 commit comments

Comments
 (0)