# Nimbus # Copyright (c) 2024 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or # http://www.apache.org/licenses/LICENSE-2.0) # * MIT license ([LICENSE-MIT](LICENSE-MIT) or # http://opensource.org/licenses/MIT) # at your option. This file may not be copied, modified, or distributed except # according to those terms. {.push raises: [].} import std/tables, ../../common, ../../db/core_db, ../../evm/types, ../../evm/state, ../validate, ../executor/process_block export common, core_db type CursorDesc = object forkJunction: BlockNumber hash: Hash256 BlockDesc = object blk: EthBlock receipts: seq[Receipt] BaseDesc = object hash: Hash256 header: BlockHeader CanonicalDesc = object cursorHash: Hash256 header: BlockHeader ForkedChainRef* = ref object stagingTx: CoreDbTxRef db: CoreDbRef com: CommonRef blocks: Table[Hash256, BlockDesc] baseHash: Hash256 baseHeader: BlockHeader cursorHash: Hash256 cursorHeader: BlockHeader cursorHeads: seq[CursorDesc] extraValidation: bool baseDistance: uint64 const BaseDistance = 128 # ------------------------------------------------------------------------------ # Private # ------------------------------------------------------------------------------ template shouldNotKeyError(body: untyped) = try: body except KeyError as exc: raiseAssert exc.msg proc processBlock(c: ForkedChainRef, parent: BlockHeader, blk: EthBlock): Result[seq[Receipt], string] = template header(): BlockHeader = blk.header let vmState = BaseVMState() vmState.init(parent, header, c.com) c.com.hardForkTransition(header) if c.extraValidation: ?c.com.validateHeaderAndKinship(blk, vmState.parent, checkSealOK = false) ?vmState.processBlock( blk, skipValidation = false, skipReceipts = false, skipUncles = true, ) # We still need to write header to database # because validateUncles still need it let blockHash = header.blockHash() if not c.db.persistHeader( blockHash, header, c.com.startOfHistory): return err("Could not persist header") # update currentBlock *after* we persist it # so the rpc return consistent result # between eth_blockNumber and eth_syncing c.com.syncCurrent = header.number ok(move(vmState.receipts)) func updateCursorHeads(c: ForkedChainRef, cursorHash: Hash256, header: BlockHeader) = # Example of cursorHeads and cursor # # -- A1 - A2 - A3 -- D5 - D6 # / / # base - B1 - B2 - B3 - B4 # \ # --- C3 - C4 # # A3, B4, C4, and D6, are in cursorHeads # Any one of them with blockHash == cursorHash # is the active chain with cursor pointing to the # latest block of that chain. for i in 0.. head.header.number: c.blocks.del(prevHash) else: break prevHash = header.parentHash if c.cursorHeads.len == 0: return # Update cursorHeads if indeed we trim for i in 0.. StagedBlocksThreshold # We need to persist some of the in-memory stuff # to a "staging area" or disk-backed memory but it must not afect `base`. # `base` is the point of no return, we only update it on finality. c.replaySegment(header.parentHash) c.validateBlock(c.cursorHeader, blk) proc forkChoice*(c: ForkedChainRef, headHash: Hash256, finalizedHash: Hash256): Result[void, string] = # If there are multiple heads, find which chain headHash belongs to let head = ?c.findCanonicalHead(headHash) # Finalized block must be part of canonical chain let finalizedHeader = ?c.canonicalChain(finalizedHash, headHash) let newBase = c.calculateNewBase( finalizedHeader, headHash, head.header) if newBase.hash == c.baseHash: # The base is not updated but the cursor maybe need update if c.cursorHash != head.cursorHash: if not c.stagingTx.isNil: c.stagingTx.rollback() c.stagingTx = c.db.newTransaction() c.replaySegment(headHash) c.trimCanonicalChain(head, headHash) if c.cursorHash != headHash: c.cursorHeader = head.header c.cursorHash = headHash if c.stagingTx.isNil: # setHead below don't go straight to db c.stagingTx = c.db.newTransaction() c.setHead(headHash, head.header.number) return ok() # At this point cursorHeader.number > baseHeader.number if newBase.hash == c.cursorHash: # Paranoid check, guaranteed by `newBase.hash == c.cursorHash` doAssert(not c.stagingTx.isNil) # CL decide to move backward and then forward? if c.cursorHeader.number < head.header.number: c.replaySegment(headHash, c.cursorHeader, c.cursorHash) # Current segment is canonical chain c.writeBaggage(newBase.hash) c.setHead(headHash, head.header.number) c.stagingTx.commit() c.stagingTx = nil # Move base to newBase c.updateBase(newBase.hash, c.cursorHeader, head.cursorHash) # Save and record the block number before the last saved block state. c.db.persistent(newBase.header.number).isOkOr: return err("Failed to save state: " & $$error) return ok() # At this point finalizedHeader.number is <= headHeader.number # and possibly switched to other chain beside the one with cursor doAssert(finalizedHeader.number <= head.header.number) doAssert(newBase.header.number <= finalizedHeader.number) # Write segment from base+1 to newBase into database c.stagingTx.rollback() c.stagingTx = c.db.newTransaction() if newBase.header.number > c.baseHeader.number: c.replaySegment(newBase.hash) c.writeBaggage(newBase.hash) c.stagingTx.commit() c.stagingTx = nil # Update base forward to newBase c.updateBase(newBase.hash, newBase.header, head.cursorHash) c.db.persistent(newBase.header.number).isOkOr: return err("Failed to save state: " & $$error) if c.stagingTx.isNil: # replaySegment or setHead below don't # go straight to db c.stagingTx = c.db.newTransaction() # Move chain state forward to current head if newBase.header.number < head.header.number: c.replaySegment(headHash) c.setHead(headHash, head.header.number) # Move cursor to current head c.trimCanonicalChain(head, headHash) if c.cursorHash != headHash: c.cursorHeader = head.header c.cursorHash = headHash ok() func haveBlockAndState*(c: ForkedChainRef, hash: Hash256): bool = if c.blocks.hasKey(hash): return true if c.baseHash == hash: return true false func stateReady*(c: ForkedChainRef, header: BlockHeader): bool = let blockHash = header.blockHash blockHash == c.cursorHash func com*(c: ForkedChainRef): CommonRef = c.com func db*(c: ForkedChainRef): CoreDbRef = c.db func latestHeader*(c: ForkedChainRef): BlockHeader = c.cursorHeader func latestHash*(c: ForkedChainRef): Hash256 = c.cursorHash proc headerByNumber*(c: ForkedChainRef, number: BlockNumber): Result[BlockHeader, string] = if number > c.cursorHeader.number: return err("Requested block number not exists: " & $number) if number == c.cursorHeader.number: return ok(c.cursorHeader) if number == c.baseHeader.number: return ok(c.baseHeader) if number < c.baseHeader.number: var header: BlockHeader if c.db.getBlockHeader(number, header): return ok(header) else: return err("Failed to get block with number: " & $number) shouldNotKeyError: var prevHash = c.cursorHeader.parentHash while prevHash != c.baseHash: let header = c.blocks[prevHash].blk.header if header.number == number: return ok(header) prevHash = header.parentHash doAssert(false, "headerByNumber: Unreachable code")