Skip to content

Commit dd6d8e0

Browse files
authored
Merge pull request #716 from input-output-hk/etcm-75-blockchain-support-checkpoint
[ETCM-75] Support for checkpoint blocks in Blockchain
2 parents d0f7480 + 28fa411 commit dd6d8e0

File tree

11 files changed

+219
-61
lines changed

11 files changed

+219
-61
lines changed

src/it/scala/io/iohk/ethereum/txExecTest/util/DumpChainApp.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,13 @@ object DumpChainApp extends App with NodeKeyBuilder with SecureRandomBuilder wit
162162

163163
def saveBlockNumber(number: BigInt, hash: NodeHash): Unit = ???
164164

165-
def saveBestKnownBlock(number: BigInt): Unit = ???
165+
def saveBestKnownBlocks(bestBlockNumber: BigInt, latestCheckpointNumber: Option[BigInt] = None): Unit = ???
166166

167167
def getBestBlock(): Block = ???
168168

169169
override def save(block: Block, receipts: Seq[Receipt], totalDifficulty: BigInt, saveAsBestBlock: Boolean): Unit = ???
170170

171171
override def getStateStorage: StateStorage = ???
172+
173+
override def getLatestCheckpointBlockNumber(): BigInt = ???
172174
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ class FastSync(
280280
blockchain.removeBlock(headerToRemove.hash, withState = false)
281281
}
282282
}
283+
// TODO (maybe ETCM-77): Manage last checkpoint number too
283284
appStateStorage.putBestBlockNumber((startBlock - blocksToDiscard - 1) max 0).commit()
284285
}
285286

src/main/scala/io/iohk/ethereum/db/storage/StateStorage.scala

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ trait StateStorage {
1818
def getBackingStorage(bn: BigInt): MptStorage
1919
def getReadOnlyStorage: MptStorage
2020

21-
def onBlockSave(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlock: Option[BigInt] => Unit): Unit
22-
def onBlockRollback(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlock: Option[BigInt] => Unit): Unit
21+
def onBlockSave(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlocksData: () => Unit): Unit
22+
def onBlockRollback(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlocksData: () => Unit): Unit
2323

2424
def saveNode(nodeHash: NodeHash, nodeEncoded: NodeEncoded, bn: BigInt)
2525
def getNode(nodeHash: NodeHash): Option[MptNode]
@@ -34,15 +34,15 @@ class ArchiveStateStorage(private val nodeStorage: NodeStorage,
3434
true
3535
}
3636

37-
override def onBlockSave(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlock: Option[BigInt] => Unit): Unit = {
37+
override def onBlockSave(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlocksData: () => Unit): Unit = {
3838
if (cachedNodeStorage.persist()) {
39-
updateBestBlock(None)
39+
updateBestBlocksData()
4040
}
4141
}
4242

43-
override def onBlockRollback(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlock: Option[BigInt] => Unit): Unit = {
43+
override def onBlockRollback(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlocksData: () => Unit): Unit = {
4444
if (cachedNodeStorage.persist()) {
45-
updateBestBlock(None)
45+
updateBestBlocksData()
4646
}
4747
}
4848

@@ -71,21 +71,21 @@ class ReferenceCountedStateStorage(private val nodeStorage: NodeStorage,
7171
true
7272
}
7373

74-
override def onBlockSave(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlock: Option[BigInt] => Unit): Unit = {
74+
override def onBlockSave(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlocksData: () => Unit): Unit = {
7575
val blockToPrune = bn - pruningHistory
7676

7777
ReferenceCountNodeStorage.prune(blockToPrune, cachedNodeStorage, inMemory = blockToPrune > currentBestSavedBlock)
7878

7979
if (cachedNodeStorage.persist()) {
80-
updateBestBlock(None)
80+
updateBestBlocksData()
8181
}
8282
}
8383

84-
override def onBlockRollback(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlock: Option[BigInt] => Unit): Unit = {
84+
override def onBlockRollback(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlocksData: () => Unit): Unit = {
8585
ReferenceCountNodeStorage.rollback(bn, cachedNodeStorage, inMemory = bn > currentBestSavedBlock)
8686

8787
if (cachedNodeStorage.persist()) {
88-
updateBestBlock(None)
88+
updateBestBlocksData()
8989
}
9090
}
9191

@@ -120,19 +120,19 @@ class CachedReferenceCountedStateStorage(private val nodeStorage: NodeStorage,
120120
}
121121
}
122122

123-
override def onBlockSave(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlock: Option[BigInt] => Unit): Unit = {
123+
override def onBlockSave(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlocksData: () => Unit): Unit = {
124124
val blockToPrune = bn - pruningHistory
125125
changeLog.persistChangeLog(bn)
126126
changeLog.getDeathRowFromStorage(blockToPrune).foreach {deathRow =>
127127
CachedReferenceCountedStorage.prune(deathRow, lruCache, blockToPrune)
128128
}
129129
if (CachedReferenceCountedStorage.persistCache(lruCache, nodeStorage)) {
130-
updateBestBlock(None)
130+
updateBestBlocksData()
131131
}
132132
changeLog.removeBlockMetaData(blockToPrune)
133133
}
134134

135-
override def onBlockRollback(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlock: Option[BigInt] => Unit): Unit = {
135+
override def onBlockRollback(bn: BigInt, currentBestSavedBlock: BigInt)(updateBestBlocksData: () => Unit): Unit = {
136136
changeLog.getChangeLogFromStorage(bn).foreach { changeLog =>
137137
CachedReferenceCountedStorage.rollback(lruCache, nodeStorage, changeLog, bn)
138138
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ case class Block(header: BlockHeader, body: BlockBody) {
2525

2626
def hash: ByteString = header.hash
2727

28+
val hasCheckpoint: Boolean = header.hasCheckpoint
29+
2830
def isParentOf(child: Block): Boolean = number + 1 == child.number && child.header.parentHash == hash
2931
}
3032

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

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import io.iohk.ethereum.db.storage.TransactionMappingStorage.TransactionLocation
1010
import io.iohk.ethereum.db.storage._
1111
import io.iohk.ethereum.db.storage.pruning.PruningMode
1212
import io.iohk.ethereum.domain
13+
import io.iohk.ethereum.domain.BlockchainImpl.BestBlockLatestCheckpointNumbers
1314
import io.iohk.ethereum.ledger.{InMemoryWorldStateProxy, InMemoryWorldStateProxyStorage}
1415
import io.iohk.ethereum.mpt.{MerklePatriciaTrie, MptNode}
1516
import io.iohk.ethereum.vm.{Storage, WorldStateProxy}
1617

18+
import scala.annotation.tailrec
19+
1720
/**
1821
* Entity to be used to persist and query Blockchain related objects (blocks, transactions, ommers)
1922
*/
@@ -123,6 +126,7 @@ trait Blockchain {
123126

124127
def getBestBlock(): Block
125128

129+
def getLatestCheckpointBlockNumber(): BigInt
126130

127131
/**
128132
* Persists full block along with receipts and total difficulty
@@ -158,7 +162,7 @@ trait Blockchain {
158162

159163
def storeTotalDifficulty(blockhash: ByteString, totalDifficulty: BigInt): DataSourceBatchUpdate
160164

161-
def saveBestKnownBlock(number: BigInt): Unit
165+
def saveBestKnownBlocks(bestBlockNumber: BigInt, latestCheckpointNumber: Option[BigInt] = None): Unit
162166

163167
def saveNode(nodeHash: NodeHash, nodeEncoded: NodeEncoded, blockNumber: BigInt): Unit
164168

@@ -209,7 +213,8 @@ class BlockchainImpl(
209213

210214
// There is always only one writer thread (ensured by actor), but can by many readers (api calls)
211215
// to ensure visibility of writes, needs to be volatile or atomic ref
212-
private val bestKnownBlock: AtomicReference[BigInt] = new AtomicReference(BigInt(0))
216+
private val bestKnownBlockAndLatestCheckpoint: AtomicReference[BestBlockLatestCheckpointNumbers] =
217+
new AtomicReference(BestBlockLatestCheckpointNumbers(BigInt(0), BigInt(0)))
213218

214219
override def getBlockHeaderByHash(hash: ByteString): Option[BlockHeader] =
215220
blockHeadersStorage.get(hash)
@@ -225,12 +230,22 @@ class BlockchainImpl(
225230

226231
override def getBestBlockNumber(): BigInt = {
227232
val bestBlockNum = appStateStorage.getBestBlockNumber()
228-
if (bestKnownBlock.get() > bestBlockNum)
229-
bestKnownBlock.get()
233+
if (bestKnownBlockAndLatestCheckpoint.get().bestBlockNumber > bestBlockNum)
234+
bestKnownBlockAndLatestCheckpoint.get().bestBlockNumber
230235
else
231236
bestBlockNum
232237
}
233238

239+
override def getLatestCheckpointBlockNumber(): BigInt = {
240+
val latestCheckpointNumberInStorage = appStateStorage.getLatestCheckpointBlockNumber()
241+
// The latest checkpoint number is firstly saved in memory and then persisted to the storage only when it's time to persist cache.
242+
// The latest checkpoint number in memory can be bigger than the number in storage because the cache wasn't persisted yet
243+
if (bestKnownBlockAndLatestCheckpoint.get().latestCheckpointNumber > latestCheckpointNumberInStorage)
244+
bestKnownBlockAndLatestCheckpoint.get().latestCheckpointNumber
245+
else
246+
latestCheckpointNumberInStorage
247+
}
248+
234249
override def getBestBlock(): Block =
235250
getBlockByNumber(getBestBlockNumber()).get
236251

@@ -252,8 +267,10 @@ class BlockchainImpl(
252267
ByteString(mpt.get(position).getOrElse(BigInt(0)).toByteArray)
253268
}
254269

255-
def saveBestBlock(bestBlock: Option[BigInt]): Unit = {
256-
bestBlock.fold(appStateStorage.putBestBlockNumber(getBestBlockNumber()).commit())(best => appStateStorage.putBestBlockNumber(best).commit())
270+
private def persistBestBlocksData(): Unit = {
271+
appStateStorage.putBestBlockNumber(getBestBlockNumber())
272+
.and(appStateStorage.putLatestCheckpointBlockNumber(getLatestCheckpointBlockNumber()))
273+
.commit()
257274
}
258275

259276
def save(block: Block, receipts: Seq[Receipt], totalDifficulty: BigInt, saveAsBestBlock: Boolean): Unit = {
@@ -263,8 +280,12 @@ class BlockchainImpl(
263280
.commit()
264281

265282
// not transactional part
266-
stateStorage.onBlockSave(block.header.number, appStateStorage.getBestBlockNumber())(saveBestBlock)
267-
if (saveAsBestBlock) {
283+
// the best blocks data will be persisted only when the cache will be persisted
284+
stateStorage.onBlockSave(block.header.number, appStateStorage.getBestBlockNumber())(persistBestBlocksData)
285+
286+
if (saveAsBestBlock && block.hasCheckpoint) {
287+
saveBestKnownBlockAndLatestCheckpointNumber(block.header.number, block.header.number)
288+
} else if (saveAsBestBlock) {
268289
saveBestKnownBlock(block.header.number)
269290
}
270291
}
@@ -289,8 +310,21 @@ class BlockchainImpl(
289310
override def storeEvmCode(hash: ByteString, evmCode: ByteString): DataSourceBatchUpdate =
290311
evmCodeStorage.put(hash, evmCode)
291312

292-
override def saveBestKnownBlock(number: BigInt): Unit = {
293-
bestKnownBlock.set(number)
313+
override def saveBestKnownBlocks(bestBlockNumber: BigInt, latestCheckpointNumber: Option[BigInt] = None): Unit = {
314+
latestCheckpointNumber match {
315+
case Some(number) =>
316+
saveBestKnownBlockAndLatestCheckpointNumber(bestBlockNumber, number)
317+
case None =>
318+
saveBestKnownBlock(bestBlockNumber)
319+
}
320+
}
321+
322+
private def saveBestKnownBlock(bestBlockNumber: BigInt): Unit = {
323+
bestKnownBlockAndLatestCheckpoint.updateAndGet(_.copy(bestBlockNumber = bestBlockNumber))
324+
}
325+
326+
private def saveBestKnownBlockAndLatestCheckpointNumber(number: BigInt, latestCheckpointNumber: BigInt): Unit = {
327+
bestKnownBlockAndLatestCheckpoint.set(BestBlockLatestCheckpointNumbers(number, latestCheckpointNumber))
294328
}
295329

296330
def storeTotalDifficulty(blockhash: ByteString, td: BigInt): DataSourceBatchUpdate =
@@ -310,10 +344,18 @@ class BlockchainImpl(
310344
blockNumberMappingStorage.remove(number)
311345
}
312346

347+
// scalastyle:off method.length
313348
override def removeBlock(blockHash: ByteString, withState: Boolean): Unit = {
314349
val maybeBlockHeader = getBlockHeaderByHash(blockHash)
315350
val maybeTxList = getBlockBodyByHash(blockHash).map(_.transactionList)
316-
val bestSavedBlock = getBestBlockNumber()
351+
val bestBlocks = bestKnownBlockAndLatestCheckpoint.get()
352+
// as we are decreasing block numbers in memory more often than in storage,
353+
// we can't use here getBestBlockNumber / getLatestCheckpointBlockNumber
354+
val bestBlockNumber = if(bestBlocks.bestBlockNumber != 0) bestBlocks.bestBlockNumber else appStateStorage.getBestBlockNumber()
355+
val latestCheckpointNumber = {
356+
if(bestBlocks.latestCheckpointNumber != 0) bestBlocks.latestCheckpointNumber
357+
else appStateStorage.getLatestCheckpointBlockNumber()
358+
}
317359

318360
val blockNumberMappingUpdates = {
319361
maybeBlockHeader.fold(blockNumberMappingStorage.emptyBatchUpdate)( h =>
@@ -323,6 +365,22 @@ class BlockchainImpl(
323365
)
324366
}
325367

368+
val (checkpointUpdates, prevCheckpointNumber): (DataSourceBatchUpdate, Option[BigInt]) = maybeBlockHeader match {
369+
case Some(header) =>
370+
if (header.hasCheckpoint && header.number == latestCheckpointNumber) {
371+
val prev = findPreviousCheckpointBlockNumber(header.number, header.number)
372+
prev.map { num =>
373+
(appStateStorage.putLatestCheckpointBlockNumber(num), Some(num))
374+
}.getOrElse {
375+
(appStateStorage.removeLatestCheckpointBlockNumber(), Some(0))
376+
}
377+
} else (appStateStorage.emptyBatchUpdate, None)
378+
case None =>
379+
(appStateStorage.emptyBatchUpdate, None)
380+
}
381+
382+
val newBestBlockNumber: BigInt = if(bestBlockNumber >= 1) bestBlockNumber - 1 else 0
383+
326384
blockHeadersStorage.remove(blockHash)
327385
.and(blockBodiesStorage.remove(blockHash))
328386
.and(totalDifficultyStorage.remove(blockHash))
@@ -332,11 +390,40 @@ class BlockchainImpl(
332390
.commit()
333391

334392
// not transactional part
393+
saveBestKnownBlocks(newBestBlockNumber, prevCheckpointNumber)
394+
335395
maybeBlockHeader.foreach { h =>
336-
if (withState)
337-
stateStorage.onBlockRollback(h.number, bestSavedBlock)(saveBestBlock)
396+
if (withState) {
397+
val bestBlocksUpdates = appStateStorage.putBestBlockNumber(newBestBlockNumber)
398+
.and(checkpointUpdates)
399+
stateStorage.onBlockRollback(h.number, bestBlockNumber)(() => bestBlocksUpdates.commit())
400+
}
338401
}
339402
}
403+
// scalastyle:on method.length
404+
405+
/**
406+
* Recursive function which try to find the previous checkpoint by traversing blocks from top to the bottom.
407+
* In case of finding the checkpoint block number, the function will finish the job and return result
408+
*/
409+
@tailrec
410+
private def findPreviousCheckpointBlockNumber(
411+
blockNumberToCheck: BigInt,
412+
latestCheckpointBlockNumber: BigInt
413+
): Option[BigInt] = {
414+
if (blockNumberToCheck > 0) {
415+
val maybePreviousCheckpointBlockNumber = for {
416+
currentBlock <- getBlockByNumber(blockNumberToCheck)
417+
if currentBlock.hasCheckpoint &&
418+
currentBlock.number < latestCheckpointBlockNumber
419+
} yield currentBlock.number
420+
421+
maybePreviousCheckpointBlockNumber match {
422+
case Some(_) => maybePreviousCheckpointBlockNumber
423+
case None => findPreviousCheckpointBlockNumber(blockNumberToCheck - 1, latestCheckpointBlockNumber)
424+
}
425+
} else None
426+
}
340427

341428
private def saveTxsLocations(blockHash: ByteString, blockBody: BlockBody): DataSourceBatchUpdate =
342429
blockBody.transactionList.zipWithIndex.foldLeft(transactionMappingStorage.emptyBatchUpdate) {
@@ -386,8 +473,8 @@ class BlockchainImpl(
386473

387474
//FIXME EC-495 this method should not be need when best block is handled properly during rollback
388475
def persistCachedNodes(): Unit = {
389-
if (stateStorage.forcePersist(RollBackFlush)){
390-
appStateStorage.putBestBlockNumber(getBestBlockNumber()).commit()
476+
if (stateStorage.forcePersist(RollBackFlush)) {
477+
persistBestBlocksData()
391478
}
392479
}
393480
}
@@ -423,4 +510,6 @@ object BlockchainImpl {
423510
appStateStorage = storages.appStateStorage,
424511
stateStorage = storages.stateStorage
425512
)
513+
514+
private case class BestBlockLatestCheckpointNumbers(bestBlockNumber: BigInt, latestCheckpointNumber: BigInt)
426515
}

src/main/scala/io/iohk/ethereum/jsonrpc/TestService.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ class TestService(
121121
(blockchain.getBestBlockNumber() until request.blockNum by -1).foreach { n =>
122122
blockchain.removeBlock(blockchain.getBlockHeaderByNumber(n).get.hash, withState = false)
123123
}
124-
blockchain.saveBestKnownBlock(request.blockNum)
125124
Future.successful(Right(RewindToBlockResponse()))
126125
}
127126

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class BlockImport(
3131
validationResult <- validationResult
3232
importResult <- importResult
3333
} yield {
34-
validationResult.fold(error => handleImportTopValidationError(error, block, currentBestBlock, importResult), _ => importResult)
34+
validationResult.fold(error => handleImportTopValidationError(error, block, importResult), _ => importResult)
3535
}
3636
}
3737

@@ -82,7 +82,6 @@ class BlockImport(
8282
private def handleImportTopValidationError(
8383
error: ValidationBeforeExecError,
8484
block: Block,
85-
bestBlockBeforeImport: Block,
8685
blockImportResult: BlockImportResult
8786
): BlockImportResult = {
8887
blockImportResult match {
@@ -92,7 +91,6 @@ class BlockImport(
9291
blockQueue.removeSubtree(hash)
9392
blockchain.removeBlock(hash, withState = true)
9493
}
95-
blockchain.saveBestKnownBlock(bestBlockBeforeImport.header.number)
9694
case _ => ()
9795
}
9896
handleBlockValidationError(error, block)
@@ -197,8 +195,14 @@ class BlockImport(
197195
blockchain.save(block, receipts, td, saveAsBestBlock = false)
198196
}
199197

198+
import cats.implicits._
199+
val checkpointNumber = oldBranch
200+
.collect {
201+
case BlockData(block, _, _) if block.hasCheckpoint => block.number
202+
}.maximumOption
203+
200204
val bestNumber = oldBranch.last.block.header.number
201-
blockchain.saveBestKnownBlock(bestNumber)
205+
blockchain.saveBestKnownBlocks(bestNumber, checkpointNumber)
202206
executedBlocks.foreach(data => blockQueue.enqueueBlock(data.block, bestNumber))
203207

204208
newBranch.diff(executedBlocks.map(_.block)).headOption.foreach { block =>

0 commit comments

Comments
 (0)