UNPKG

@ethereumjs/blockchain

Version:
1,028 lines 52.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Blockchain = void 0; const block_1 = require("@ethereumjs/block"); const common_1 = require("@ethereumjs/common"); const util_1 = require("@ethereumjs/util"); const debug_1 = require("debug"); const eventemitter3_1 = require("eventemitter3"); const casper_ts_1 = require("./consensus/casper.js"); const helpers_ts_1 = require("./db/helpers.js"); const manager_ts_1 = require("./db/manager.js"); const operation_ts_1 = require("./db/operation.js"); /** * Blockchain implementation to create and maintain a valid canonical chain * of block headers or blocks with support for reorgs and the ability to provide * custom DB backends. * * By default consensus validation is not provided since with the switch to * Proof-of-Stake consensus is validated by the Ethereum consensus layer. * If consensus validation is desired for Ethash or Clique blockchains the * optional `consensusDict` option can be used to pass in validation objects. */ class Blockchain { /** * Creates new Blockchain object. * * @deprecated The direct usage of this constructor is discouraged since * non-finalized async initialization might lead to side effects. Please * use the async {@link createBlockchain} constructor instead (same API). * * @param opts An object with the options that this constructor takes. See * {@link BlockchainOptions}. */ constructor(opts = {}) { /** * This is used to track which canonical blocks are deleted. After a method calls * `_deleteCanonicalChainReferences`, if this array has any items, the * `deletedCanonicalBlocks` event is emitted with the array as argument. */ this._deletedBlocks = []; this.DEBUG = typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false; this._debug = (0, debug_1.default)('blockchain:#'); if (opts.common) { this.common = opts.common; } else { const DEFAULT_CHAIN = common_1.Mainnet; const DEFAULT_HARDFORK = common_1.Hardfork.Chainstart; this.common = new common_1.Common({ chain: DEFAULT_CHAIN, hardfork: DEFAULT_HARDFORK, }); } this._hardforkByHeadBlockNumber = opts.hardforkByHeadBlockNumber ?? false; this._validateBlocks = opts.validateBlocks ?? true; this._validateConsensus = opts.validateConsensus ?? false; this._customGenesisState = opts.genesisState; this.db = opts.db ?? new util_1.MapDB(); this.dbManager = new manager_ts_1.DBManager(this.db, this.common); this.events = new eventemitter3_1.EventEmitter(); this._consensusDict = {}; this._consensusDict[common_1.ConsensusAlgorithm.Casper] = new casper_ts_1.CasperConsensus(); if (opts.consensusDict !== undefined) { this._consensusDict = { ...this._consensusDict, ...opts.consensusDict }; } this._consensusCheck(); this._heads = {}; this._lock = new util_1.Lock(); if (opts.genesisBlock && !opts.genesisBlock.isGenesis()) { throw 'supplied block is not a genesis block'; } } _consensusCheck() { if (this._validateConsensus && this.consensus === undefined) { throw (0, util_1.EthereumJSErrorWithoutCode)(`Consensus object for ${this.common.consensusAlgorithm()} must be passed (see consensusDict option) if consensus validation is activated`); } } /** * Returns an eventual consensus object matching the current consensus algorithm from Common * or undefined if non available */ get consensus() { return this._consensusDict[this.common.consensusAlgorithm()]; } /** * Returns a deep copy of this {@link Blockchain} instance. * * Note: this does not make a copy of the underlying db * since it is unknown if the source is on disk or in memory. * This should not be a significant issue in most usage since * the queries will only reflect the instance's known data. * If you would like this copied blockchain to use another db * set the {@link db} of this returned instance to a copy of * the original. */ shallowCopy() { const copiedBlockchain = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this)); copiedBlockchain.common = this.common.copy(); return copiedBlockchain; } /** * Run a function after acquiring a lock. It is implied that we have already * initialized the module (or we are calling this from the init function, like * `_setCanonicalGenesisBlock`) * @param action - function to run after acquiring a lock * @hidden */ async runWithLock(action) { try { await this._lock.acquire(); const value = await action(); return value; } finally { this._lock.release(); } } /** * Returns the specified iterator head. * * This function replaces the old Blockchain.getHead() method. Note that * the function deviates from the old behavior and returns the * genesis hash instead of the current head block if an iterator * has not been run. This matches the behavior of {@link Blockchain.iterator}. * * @param name - Optional name of the iterator head (default: 'vm') */ async getIteratorHead(name = 'vm') { return this.runWithLock(async () => { return (await this.getHead(name, false)); }); } /** * This method differs from `getIteratorHead`. If the head is not found, it returns `undefined`. * @param name - Optional name of the iterator head (default: 'vm') * @returns */ async getIteratorHeadSafe(name = 'vm') { return this.runWithLock(async () => { return this.getHead(name, true); }); } async getHead(name, returnUndefinedIfNotSet = false) { const headHash = this._heads[name]; if (headHash === undefined && returnUndefinedIfNotSet) { return undefined; } const hash = this._heads[name] ?? this.genesisBlock.hash(); const block = await this.getBlock(hash); return block; } /** * Returns the latest header in the canonical chain. */ async getCanonicalHeadHeader() { return this.runWithLock(async () => { if (!this._headHeaderHash) throw (0, util_1.EthereumJSErrorWithoutCode)('No head header set'); const header = await this._getHeader(this._headHeaderHash); return header; }); } /** * Returns the latest full block in the canonical chain. */ async getCanonicalHeadBlock() { return this.runWithLock(async () => { if (!this._headBlockHash) throw (0, util_1.EthereumJSErrorWithoutCode)('No head block set'); return this.getBlock(this._headBlockHash); }); } /** * Adds blocks to the blockchain. * * If an invalid block is met the function will throw, blocks before will * nevertheless remain in the DB. If any of the saved blocks has a higher * total difficulty than the current max total difficulty the canonical * chain is rebuilt and any stale heads/hashes are overwritten. * @param blocks - The blocks to be added to the blockchain */ async putBlocks(blocks) { for (let i = 0; i < blocks.length; i++) { await this.putBlock(blocks[i]); } this.DEBUG && this._debug(`put ${blocks.length} blocks`); } /** * Adds a block to the blockchain. * * If the block is valid and has a higher total difficulty than the current * max total difficulty, the canonical chain is rebuilt and any stale * heads/hashes are overwritten. * @param block - The block to be added to the blockchain */ async putBlock(block) { await this._putBlockOrHeader(block); } /** * Adds many headers to the blockchain. * * If an invalid header is met the function will throw, headers before will * nevertheless remain in the DB. If any of the saved headers has a higher * total difficulty than the current max total difficulty the canonical * chain is rebuilt and any stale heads/hashes are overwritten. * @param headers - The headers to be added to the blockchain */ async putHeaders(headers) { for (let i = 0; i < headers.length; i++) { await this.putHeader(headers[i]); } this.DEBUG && this._debug(`put ${headers.length} headers`); } /** * Adds a header to the blockchain. * * If this header is valid and it has a higher total difficulty than the current * max total difficulty, the canonical chain is rebuilt and any stale * heads/hashes are overwritten. * @param header - The header to be added to the blockchain */ async putHeader(header) { await this._putBlockOrHeader(header); } /** * Resets the canonical chain to canonicalHead number * * This updates the head hashes (if affected) to the hash corresponding to * canonicalHead and cleans up canonical references greater than canonicalHead * @param canonicalHead - The number to which chain should be reset to */ async resetCanonicalHead(canonicalHead) { let hash; let canonicalHeadHash; if (this.DEBUG) { canonicalHeadHash = (await this.getCanonicalHeadHeader()).hash(); } await this.runWithLock(async () => { hash = await this.dbManager.numberToHash(canonicalHead); if (hash === undefined) { throw (0, util_1.EthereumJSErrorWithoutCode)(`no block for ${canonicalHead} found in DB`); } const header = await this._getHeader(hash, canonicalHead); const dbOps = []; await this._deleteCanonicalChainReferences(canonicalHead + util_1.BIGINT_1, hash, dbOps); const ops = dbOps.concat(this._saveHeadOps()); await this.dbManager.batch(ops); await this.checkAndTransitionHardForkByNumber(canonicalHead, header.timestamp); }); if (this._deletedBlocks.length > 0) { this.events.emit('deletedCanonicalBlocks', this._deletedBlocks); for (const block of this._deletedBlocks) this.DEBUG && this._debug(`deleted block along head reset: number ${block.header.number} hash ${(0, util_1.bytesToHex)(block.hash())}`); this.DEBUG && this._debug(`Canonical head set from ${(0, util_1.bytesToHex)(canonicalHeadHash)} to ${(0, util_1.bytesToHex)(hash)} (number ${(0, util_1.bigIntToHex)(canonicalHead)})`); this._deletedBlocks = []; } } /** * Entrypoint for putting any block or block header. Verifies this block, * checks the total TD: if this TD is higher than the current highest TD, we * have thus found a new canonical block and have to rewrite the canonical * chain. This also updates the head block hashes. If any of the older known * canonical chains just became stale, then we also reset every _heads header * which points to a stale header to the last verified header which was in the * old canonical chain, but also in the new canonical chain. This thus rolls * back these headers so that these can be updated to the "new" canonical * header using the iterator method. * @hidden */ async _putBlockOrHeader(item) { await this.runWithLock(async () => { // Save the current sane state incase _putBlockOrHeader midway with some // dirty changes in head trackers const oldHeads = Object.assign({}, this._heads); const oldHeadHeaderHash = this._headHeaderHash; const oldHeadBlockHash = this._headBlockHash; try { const block = item instanceof block_1.BlockHeader ? new block_1.Block(item, undefined, undefined, undefined, { common: item.common }, undefined) : item; const isGenesis = block.isGenesis(); // we cannot overwrite the Genesis block after initializing the Blockchain if (isGenesis) { if ((0, util_1.equalsBytes)(this.genesisBlock.hash(), block.hash())) { // Try to re-put the existing genesis block, accept this return; } throw (0, util_1.EthereumJSErrorWithoutCode)('Cannot put a different genesis block than current blockchain genesis: create a new Blockchain'); } const { header } = block; const blockHash = header.hash(); const blockNumber = header.number; let td = header.difficulty; const currentTd = { header: util_1.BIGINT_0, block: util_1.BIGINT_0 }; let dbOps = []; if (block.common.chainId() !== this.common.chainId()) { throw (0, util_1.EthereumJSErrorWithoutCode)(`Chain mismatch while trying to put block or header. Chain ID of block: ${block.common.chainId}, chain ID of blockchain : ${this.common.chainId}`); } if (this._validateBlocks && !isGenesis && item instanceof block_1.Block) { // this calls into `getBlock`, which is why we cannot lock yet await this.validateBlock(block); } if (this._validateConsensus) { await this.consensus.validateConsensus(block); } // set total difficulty in the current context scope if (this._headHeaderHash) { currentTd.header = await this.getTotalDifficulty(this._headHeaderHash); } if (this._headBlockHash) { currentTd.block = await this.getTotalDifficulty(this._headBlockHash); } // calculate the total difficulty of the new block const parentTd = await this.getParentTD(header); if (!block.isGenesis()) { td += parentTd; } // save total difficulty to the database dbOps = dbOps.concat((0, helpers_ts_1.DBSetTD)(td, blockNumber, blockHash)); // save header/block to the database, but save the input not our wrapper block dbOps = dbOps.concat((0, helpers_ts_1.DBSetBlockOrHeader)(item)); let commonAncestor; let ancestorHeaders; // if total difficulty is higher than current, add it to canonical chain if (block.isGenesis() || td > currentTd.header || block.common.consensusType() === common_1.ConsensusType.ProofOfStake) { const foundCommon = await this.findCommonAncestor(header); commonAncestor = foundCommon.commonAncestor; ancestorHeaders = foundCommon.ancestorHeaders; this._headHeaderHash = blockHash; if (item instanceof block_1.Block) { this._headBlockHash = blockHash; } if (this._hardforkByHeadBlockNumber) { await this.checkAndTransitionHardForkByNumber(blockNumber, header.timestamp); } // delete higher number assignments and overwrite stale canonical chain await this._deleteCanonicalChainReferences(blockNumber + util_1.BIGINT_1, blockHash, dbOps); // from the current header block, check the blockchain in reverse (i.e. // traverse `parentHash`) until `numberToHash` matches the current // number/hash in the canonical chain also: overwrite any heads if these // heads are stale in `_heads` and `_headBlockHash` await this._rebuildCanonical(header, dbOps); } else { // the TD is lower than the current highest TD so we will add the block // to the DB, but will not mark it as the canonical chain. if (td > currentTd.block && item instanceof block_1.Block) { this._headBlockHash = blockHash; } // save hash to number lookup info even if rebuild not needed dbOps.push((0, helpers_ts_1.DBSetHashToNumber)(blockHash, blockNumber)); } const ops = dbOps.concat(this._saveHeadOps()); await this.dbManager.batch(ops); await this.consensus?.newBlock(block, commonAncestor, ancestorHeaders); this.DEBUG && this._debug(`put block number=${block.header.number} hash=${(0, util_1.bytesToHex)(blockHash)}`); } catch (e) { // restore head to the previously sane state this._heads = oldHeads; this._headHeaderHash = oldHeadHeaderHash; this._headBlockHash = oldHeadBlockHash; throw e; } }); if (this._deletedBlocks.length > 0) { this.events.emit('deletedCanonicalBlocks', this._deletedBlocks); for (const block of this._deletedBlocks) this.DEBUG && this._debug(`delete stale canonical block number=${block.header.number} hash=${(0, util_1.bytesToHex)(block.hash())}`); this._deletedBlocks = []; } } /** * Validates a block header, throwing if invalid. It is being validated against the reported `parentHash`. * It verifies the current block against the `parentHash`: * - The `parentHash` is part of the blockchain (it is a valid header) * - Current block number is parent block number + 1 * - Current block has a strictly higher timestamp * - Additional PoW checks -> * - Current block has valid difficulty and gas limit * - In case that the header is an uncle header, it should not be too old or young in the chain. * - Additional PoA clique checks -> * - Checks on coinbase and mixHash * - Current block has a timestamp diff greater or equal to PERIOD * - Current block has difficulty correctly marked as INTURN or NOTURN * @param header - header to be validated * @param height - If this is an uncle header, this is the height of the block that is including it */ async validateHeader(header, height) { if (header.isGenesis()) { return; } const parentHeader = await this._getHeader(header.parentHash); const { number } = header; if (number !== parentHeader.number + util_1.BIGINT_1) { throw (0, util_1.EthereumJSErrorWithoutCode)(`invalid number ${header.errorStr()}`); } if (header.timestamp <= parentHeader.timestamp) { throw (0, util_1.EthereumJSErrorWithoutCode)(`invalid timestamp ${header.errorStr()}`); } if (!(header.common.consensusType() === 'pos')) await this.consensus?.validateDifficulty(header); if (this.common.consensusAlgorithm() === common_1.ConsensusAlgorithm.Clique) { const period = this.common.consensusConfig().period; // Timestamp diff between blocks is lower than PERIOD (clique) if (parentHeader.timestamp + BigInt(period) > header.timestamp) { throw (0, util_1.EthereumJSErrorWithoutCode)(`invalid timestamp diff (lower than period) ${header.errorStr()}`); } } header.validateGasLimit(parentHeader); if (height !== undefined) { const dif = height - parentHeader.number; if (!(dif < util_1.BIGINT_8 && dif > util_1.BIGINT_1)) { throw (0, util_1.EthereumJSErrorWithoutCode)(`uncle block has a parent that is too old or too young ${header.errorStr()}`); } } // check blockchain dependent EIP1559 values if (header.common.isActivatedEIP(1559)) { // check if the base fee is correct let expectedBaseFee; const londonHfBlock = this.common.hardforkBlock(common_1.Hardfork.London); const isInitialEIP1559Block = number === londonHfBlock; if (isInitialEIP1559Block) { expectedBaseFee = header.common.param('initialBaseFee'); } else { expectedBaseFee = parentHeader.calcNextBaseFee(); } if (header.baseFeePerGas !== expectedBaseFee) { throw (0, util_1.EthereumJSErrorWithoutCode)(`Invalid block: base fee not correct ${header.errorStr()}`); } } if (header.common.isActivatedEIP(4844)) { const expectedExcessBlobGas = parentHeader.calcNextExcessBlobGas(header.common); if (header.excessBlobGas !== expectedExcessBlobGas) { throw (0, util_1.EthereumJSErrorWithoutCode)(`expected blob gas: ${expectedExcessBlobGas}, got: ${header.excessBlobGas}`); } } if (header.common.isActivatedEIP(7685)) { if (header.requestsHash === undefined) { throw (0, util_1.EthereumJSErrorWithoutCode)(`requestsHash must be provided when EIP-7685 is active`); } } } /** * Validates a block, by validating the header against the current chain, any uncle headers, and then * whether the block is internally consistent * @param block block to be validated */ async validateBlock(block) { await this.validateHeader(block.header); await this._validateUncleHeaders(block); await block.validateData(false); // TODO: Rethink how validateHeader vs validateBlobTransactions works since the parentHeader is retrieved multiple times // (one for each uncle header and then for validateBlobTxs). const parentBlock = await this.getBlock(block.header.parentHash); block.validateBlobTransactions(parentBlock.header); } /** * The following rules are checked in this method: * Uncle Header is a valid header. * Uncle Header is an orphan, i.e. it is not one of the headers of the canonical chain. * Uncle Header has a parentHash which points to the canonical chain. This parentHash is within the last 7 blocks. * Uncle Header is not already included as uncle in another block. * @param block - block for which uncles are being validated */ async _validateUncleHeaders(block) { const uncleHeaders = block.uncleHeaders; if (uncleHeaders.length === 0) { return; } // Each Uncle Header is a valid header await Promise.all(uncleHeaders.map((uh) => this.validateHeader(uh, block.header.number))); // Check how many blocks we should get in order to validate the uncle. // In the worst case, we get 8 blocks, in the best case, we only get 1 block. const canonicalBlockMap = []; let lowestUncleNumber = block.header.number; uncleHeaders.map((header) => { if (header.number < lowestUncleNumber) { lowestUncleNumber = header.number; } }); // Helper variable: set hash to `true` if hash is part of the canonical chain const canonicalChainHashes = {}; // Helper variable: set hash to `true` if uncle hash is included in any canonical block const includedUncles = {}; // Due to the header validation check above, we know that `getBlocks` is between 1 and 8 inclusive. const getBlocks = Number(block.header.number - lowestUncleNumber + util_1.BIGINT_1); // See Geth: https://github.com/ethereum/go-ethereum/blob/b63bffe8202d46ea10ac8c4f441c582642193ac8/consensus/ethash/consensus.go#L207 // Here we get the necessary blocks from the chain. let parentHash = block.header.parentHash; for (let i = 0; i < getBlocks; i++) { const parentBlock = await this.getBlock(parentHash); canonicalBlockMap.push(parentBlock); // mark block hash as part of the canonical chain canonicalChainHashes[(0, util_1.bytesToUnprefixedHex)(parentBlock.hash())] = true; // for each of the uncles, mark the uncle as included parentBlock.uncleHeaders.map((uh) => { includedUncles[(0, util_1.bytesToUnprefixedHex)(uh.hash())] = true; }); parentHash = parentBlock.header.parentHash; } // Here we check: // Uncle Header is an orphan, i.e. it is not one of the headers of the canonical chain. // Uncle Header is not already included as uncle in another block. // Uncle Header has a parentHash which points to the canonical chain. uncleHeaders.map((uh) => { const uncleHash = (0, util_1.bytesToUnprefixedHex)(uh.hash()); const parentHash = (0, util_1.bytesToUnprefixedHex)(uh.parentHash); if (!canonicalChainHashes[parentHash]) { throw (0, util_1.EthereumJSErrorWithoutCode)(`The parent hash of the uncle header is not part of the canonical chain ${block.errorStr()}`); } if (includedUncles[uncleHash]) { throw (0, util_1.EthereumJSErrorWithoutCode)(`The uncle is already included in the canonical chain ${block.errorStr()}`); } if (canonicalChainHashes[uncleHash]) { throw (0, util_1.EthereumJSErrorWithoutCode)(`The uncle is a canonical block ${block.errorStr()}`); } }); } /** * Gets a block by its hash or number. If a number is provided, the returned * block will be the canonical block at that number in the chain * * @param blockId - The block's hash or number. If a hash is provided, then * this will be immediately looked up, otherwise it will wait until we have * unlocked the DB */ async getBlock(blockId) { // cannot wait for a lock here: it is used both in `validate` of `Block` // (calls `getBlock` to get `parentHash`) it is also called from `runBlock` // in the `VM` if we encounter a `BLOCKHASH` opcode: then a bigint is used we // need to then read the block from the canonical chain Q: is this safe? We // know it is OK if we call it from the iterator... (runBlock) const block = await this.dbManager.getBlock(blockId); if (block === undefined) { if (typeof blockId === 'object') { throw (0, util_1.EthereumJSErrorWithoutCode)(`Block with hash ${(0, util_1.bytesToHex)(blockId)} not found in DB`); } else { throw (0, util_1.EthereumJSErrorWithoutCode)(`Block number ${blockId} not found in DB`); } } return block; } /** * Gets total difficulty for a block specified by hash and number */ async getTotalDifficulty(hash, number) { if (number === undefined) { number = await this.dbManager.hashToNumber(hash); if (number === undefined) { throw (0, util_1.EthereumJSErrorWithoutCode)(`Block with hash ${(0, util_1.bytesToHex)(hash)} not found in DB`); } } return this.dbManager.getTotalDifficulty(hash, number); } /** * Gets total difficulty for a header's parent, helpful for determining terminal block * @param header - Block header whose parent td is desired */ async getParentTD(header) { return header.number === util_1.BIGINT_0 ? header.difficulty : this.getTotalDifficulty(header.parentHash, header.number - util_1.BIGINT_1); } /** * Looks up many blocks relative to blockId Note: due to `GetBlockHeaders * (0x03)` (ETH wire protocol) we have to support skip/reverse as well. * @param blockId - The block's hash or number * @param maxBlocks - Max number of blocks to return * @param skip - Number of blocks to skip apart * @param reverse - Fetch blocks in reverse */ async getBlocks(blockId, maxBlocks, skip, reverse) { return this.runWithLock(async () => { const blocks = []; let i = -1; const nextBlock = async (blockId) => { let block; try { block = await this.getBlock(blockId); } catch (err) { if (err.message.includes('not found in DB') === true) { return; } else throw err; } i++; const nextBlockNumber = block.header.number + BigInt(reverse ? -1 : 1); if (i !== 0 && skip && i % (skip + 1) !== 0) { return nextBlock(nextBlockNumber); } blocks.push(block); if (blocks.length < maxBlocks) { await nextBlock(nextBlockNumber); } }; await nextBlock(blockId); return blocks; }); } /** * Given an ordered array, returns an array of hashes that are not in the * blockchain yet. Uses binary search to find out what hashes are missing. * Therefore, the array needs to be ordered upon number. * @param hashes - Ordered array of hashes (ordered on `number`). */ async selectNeededHashes(hashes) { return this.runWithLock(async () => { let max; let mid; let min; max = hashes.length - 1; mid = min = 0; while (max >= min) { let number; try { number = await this.dbManager.hashToNumber(hashes[mid]); } catch (err) { if (err.message.includes('not found in DB') === true) { number = undefined; } else throw err; } if (number !== undefined) { min = mid + 1; } else { max = mid - 1; } mid = Math.floor((min + max) / 2); } return hashes.slice(min); }); } /** * Completely deletes a block from the blockchain including any references to * this block. If this block was in the canonical chain, then also each child * block of this block is deleted Also, if this was a canonical block, each * head header which is part of this now stale chain will be set to the * parentHeader of this block An example reason to execute is when running the * block in the VM invalidates this block: this will then reset the canonical * head to the past block (which has been validated in the past by the VM, so * we can be sure it is correct). * @param blockHash - The hash of the block to be deleted */ async delBlock(blockHash) { // Q: is it safe to make this not wait for a lock? this is called from // `BlockchainTestsRunner` in case `runBlock` throws (i.e. the block is invalid). // But is this the way to go? If we know this is called from the // iterator we are safe, but if this is called from anywhere // else then this might lead to a concurrency problem? await this._delBlock(blockHash); } /** * @hidden */ async _delBlock(blockHash) { const dbOps = []; // get header const header = await this._getHeader(blockHash); const blockHeader = header; const blockNumber = blockHeader.number; const parentHash = blockHeader.parentHash; // check if block is in the canonical chain const canonicalHash = await this.safeNumberToHash(blockNumber); const inCanonical = canonicalHash !== false && (0, util_1.equalsBytes)(canonicalHash, blockHash); // delete the block, and if block is in the canonical chain, delete all // children as well await this._delChild(blockHash, blockNumber, inCanonical ? parentHash : null, dbOps); // delete all number to hash mappings for deleted block number and above if (inCanonical) { await this._deleteCanonicalChainReferences(blockNumber, parentHash, dbOps); } await this.dbManager.batch(dbOps); if (this._deletedBlocks.length > 0) { this.events.emit('deletedCanonicalBlocks', this._deletedBlocks); for (const block of this._deletedBlocks) this.DEBUG && this._debug(`delete stale canonical block number=${block.header.number} hash=${blockHash})}`); this._deletedBlocks = []; } } /** * Updates the `DatabaseOperation` list to delete a block from the DB, * identified by `blockHash` and `blockNumber`. Deletes fields from `Header`, * `Body`, `HashToNumber` and `TotalDifficulty` tables. If child blocks of * this current block are in the canonical chain, delete these as well. Does * not actually commit these changes to the DB. Sets `_headHeaderHash` and * `_headBlockHash` to `headHash` if any of these matches the current child to * be deleted. * @param blockHash - the block hash to delete * @param blockNumber - the number corresponding to the block hash * @param headHash - the current head of the chain (if null, do not update * `_headHeaderHash` and `_headBlockHash`) * @param ops - the `DatabaseOperation` list to add the delete operations to * @hidden */ async _delChild(blockHash, blockNumber, headHash, ops) { // delete header, body, hash to number mapping and td ops.push(helpers_ts_1.DBOp.del(operation_ts_1.DBTarget.Header, { blockHash, blockNumber })); ops.push(helpers_ts_1.DBOp.del(operation_ts_1.DBTarget.Body, { blockHash, blockNumber })); ops.push(helpers_ts_1.DBOp.del(operation_ts_1.DBTarget.HashToNumber, { blockHash })); ops.push(helpers_ts_1.DBOp.del(operation_ts_1.DBTarget.TotalDifficulty, { blockHash, blockNumber })); if (!headHash) { return; } if (this._headHeaderHash !== undefined && (0, util_1.equalsBytes)(this._headHeaderHash, blockHash) === true) { this._headHeaderHash = headHash; } if (this._headBlockHash !== undefined && (0, util_1.equalsBytes)(this._headBlockHash, blockHash)) { this._headBlockHash = headHash; } try { const childHeader = await this.getCanonicalHeader(blockNumber + util_1.BIGINT_1); await this._delChild(childHeader.hash(), childHeader.number, headHash, ops); } catch (err) { if (err.message.includes('not found in canonical chain') !== true) { throw err; } } } /** * Iterates through blocks starting at the specified iterator head and calls * the onBlock function on each block. The current location of an iterator * head can be retrieved using {@link Blockchain.getIteratorHead}. * * @param name - Name of the state root head * @param onBlock - Function called on each block with params (block, reorg) * @param maxBlocks - How many blocks to run. By default, run all unprocessed blocks in the canonical chain. * @param releaseLockOnCallback - Do not lock the blockchain for running the callback (default: `false`) * @returns number of blocks actually iterated */ async iterator(name, onBlock, maxBlocks, releaseLockOnCallback) { return this.runWithLock(async () => { let headHash = this._heads[name] ?? this.genesisBlock.hash(); if (typeof maxBlocks === 'number' && maxBlocks < 0) { throw 'If maxBlocks is provided, it has to be a non-negative number'; } let headBlockNumber = await this.dbManager.hashToNumber(headHash); // `headBlockNumber` should always exist since it defaults to the genesis block let nextBlockNumber = headBlockNumber + util_1.BIGINT_1; let blocksRanCounter = 0; let lastBlock; try { while (maxBlocks !== blocksRanCounter) { try { let nextBlock = await this.getBlock(nextBlockNumber); const reorg = lastBlock ? !(0, util_1.equalsBytes)(lastBlock.hash(), nextBlock.header.parentHash) : false; if (reorg) { // If reorg has happened, the _heads must have been updated so lets reload the counters headHash = this._heads[name] ?? this.genesisBlock.hash(); headBlockNumber = await this.dbManager.hashToNumber(headHash); nextBlockNumber = headBlockNumber + util_1.BIGINT_1; nextBlock = await this.getBlock(nextBlockNumber); } // While running onBlock with released lock, reorgs can happen via putBlocks let reorgWhileOnBlock = false; if (releaseLockOnCallback === true) { this._lock.release(); } try { await onBlock(nextBlock, reorg); } finally { if (releaseLockOnCallback === true) { await this._lock.acquire(); // If lock was released check if reorg occurred const nextBlockMayBeReorged = await this.getBlock(nextBlockNumber).catch((_e) => null); reorgWhileOnBlock = nextBlockMayBeReorged ? !(0, util_1.equalsBytes)(nextBlockMayBeReorged.hash(), nextBlock.hash()) : true; } } // if there was no reorg, update head if (!reorgWhileOnBlock) { this._heads[name] = nextBlock.hash(); lastBlock = nextBlock; nextBlockNumber++; } // Successful execution of onBlock, move the head pointer blocksRanCounter++; } catch (error) { if (error.message.includes('not found in DB')) { break; } else { throw error; } } } return blocksRanCounter; } finally { await this._saveHeads(); } }); } /** * Set header hash of a certain `tag`. * When calling the iterator, the iterator will start running the first child block after the header hash currently stored. * @param tag - The tag to save the headHash to * @param headHash - The head hash to save */ async setIteratorHead(tag, headHash) { await this.runWithLock(async () => { this._heads[tag] = headHash; await this._saveHeads(); }); } /* Methods regarding reorg operations */ /** * Find the common ancestor of the new block and the old block. * @param newHeader - the new block header */ async findCommonAncestor(newHeader) { if (!this._headHeaderHash) throw (0, util_1.EthereumJSErrorWithoutCode)('No head header set'); const ancestorHeaders = new Set(); let header = await this._getHeader(this._headHeaderHash); if (header.number > newHeader.number) { header = await this.getCanonicalHeader(newHeader.number); ancestorHeaders.add(header); } else { while (header.number !== newHeader.number && newHeader.number > util_1.BIGINT_0) { newHeader = await this._getHeader(newHeader.parentHash, newHeader.number - util_1.BIGINT_1); ancestorHeaders.add(newHeader); } } if (header.number !== newHeader.number) { throw (0, util_1.EthereumJSErrorWithoutCode)('Failed to find ancient header'); } while (!(0, util_1.equalsBytes)(header.hash(), newHeader.hash()) && header.number > util_1.BIGINT_0) { header = await this.getCanonicalHeader(header.number - util_1.BIGINT_1); ancestorHeaders.add(header); newHeader = await this._getHeader(newHeader.parentHash, newHeader.number - util_1.BIGINT_1); ancestorHeaders.add(newHeader); } if (!(0, util_1.equalsBytes)(header.hash(), newHeader.hash())) { throw (0, util_1.EthereumJSErrorWithoutCode)('Failed to find ancient header'); } this.DEBUG && this._debug(`found common ancestor with hash=${(0, util_1.bytesToHex)(header.hash())}`); this.DEBUG && this._debug(`total ancestor headers num=${ancestorHeaders.size}`); return { commonAncestor: header, ancestorHeaders: Array.from(ancestorHeaders), }; } /** * Pushes DB operations to delete canonical number assignments for specified * block number and above. This only deletes `NumberToHash` references and not * the blocks themselves. Note: this does not write to the DB but only pushes * to a DB operations list. * @param blockNumber - the block number from which we start deleting * canonical chain assignments (including this block) * @param headHash - the hash of the current canonical chain head. The _heads * reference matching any hash of any of the deleted blocks will be set to * this * @param ops - the DatabaseOperation list to write DatabaseOperations to * @hidden */ async _deleteCanonicalChainReferences(blockNumber, headHash, ops) { try { let hash; hash = await this.safeNumberToHash(blockNumber); while (hash !== false) { ops.push(helpers_ts_1.DBOp.del(operation_ts_1.DBTarget.NumberToHash, { blockNumber })); if (this.events.listenerCount('deletedCanonicalBlocks') > 0) { const block = await this.getBlock(blockNumber); this._deletedBlocks.push(block); } // reset stale iterator heads to current canonical head this can, for // instance, make the VM run "older" (i.e. lower number blocks than last // executed block) blocks to verify the chain up to the current, actual, // head. for (const name of Object.keys(this._heads)) { if ((0, util_1.equalsBytes)(this._heads[name], hash)) { this._heads[name] = headHash; } } // reset stale headHeader to current canonical if (this._headHeaderHash !== undefined && (0, util_1.equalsBytes)(this._headHeaderHash, hash) === true) { this._headHeaderHash = headHash; } // reset stale headBlock to current canonical if (this._headBlockHash !== undefined && (0, util_1.equalsBytes)(this._headBlockHash, hash) === true) { this._headBlockHash = headHash; } blockNumber++; hash = await this.safeNumberToHash(blockNumber); } this.DEBUG && this._deletedBlocks.length > 0 && this._debug(`deleted ${this._deletedBlocks.length} stale canonical blocks in total`); } catch (e) { // Ensure that if this method throws, `_deletedBlocks` is reset to the empty array this._deletedBlocks = []; throw e; } } /** * Given a `header`, put all operations to change the canonical chain directly * into `ops`. This walks the supplied `header` backwards. It is thus assumed * that this header should be canonical header. For each header the * corresponding hash corresponding to the current canonical chain in the DB * is checked. If the number => hash reference does not correspond to the * reference in the DB, we overwrite this reference with the implied number => * hash reference Also, each `_heads` member is checked; if these point to a * stale hash, then the hash which we terminate the loop (i.e. the first hash * which matches the number => hash of the implied chain) is put as this stale * head hash. The same happens to _headBlockHash. * @param header - The canonical header. * @param ops - The database operations list. * @hidden */ async _rebuildCanonical(header, ops) { let currentNumber = header.number; let currentCanonicalHash = header.hash(); // track the staleHash: this is the hash currently in the DB which matches // the block number of the provided header. let staleHash = false; let staleHeads = []; let staleHeadBlock = false; const loopCondition = async () => { staleHash = await this.safeNumberToHash(currentNumber); currentCanonicalHash = header.hash(); return staleHash === false || !(0, util_1.equalsBytes)(currentCanonicalHash, staleHash); }; while (await loopCondition()) { // handle genesis block const blockHash = header.hash(); const blockNumber = header.number; if (blockNumber === util_1.BIGINT_0) { break; } (0, helpers_ts_1.DBSaveLookups)(blockHash, blockNumber).map((op) => { ops.push(op); }); // mark each key `_heads` which is currently set to the hash in the DB as // stale to overwrite later in `_deleteCanonicalChainReferences`. for (const name of Object.keys(this._heads)) { if (staleHash && (0, util_1.equalsBytes)(this._heads[name], staleHash)) { staleHeads.push(name); } } // flag stale headBlock for reset if (staleHash && this._headBlockHash !== undefined && (0, util_1.equalsBytes)(this._headBlockHash, staleHash) === true) { staleHeadBlock = true; } header = await this._getHeader(header.parentHash, --currentNumber); if (header === undefined) { staleHeads = []; break; } } // When the stale hash is equal to the blockHash of the provided header, // set stale heads to last previously valid canonical block for (const name of staleHeads) { this._heads[name] = currentCanonicalHash; } // set stale headBlock to last previously valid canonical block if (staleHeadBlock) { this._headBlockHash = currentCanonicalHash; } this.DEBUG && this._debug(`stale heads found num=${staleHeads.length}`); } /* Helper functions */ /** * Builds the `DatabaseOperation[]` list which describes the DB operations to * write the heads, head header hash and the head header block to the DB * @hidden */ _saveHeadOps() { // Convert DB heads to hex strings for efficient storage in DB // LevelDB doesn't handle Uint8Arrays properly when they are part // of a JSON object being stored as a value in the DB const hexHeads = Object.fromEntries(Object.entries(this._heads).map((entry) => [entry[0], (0, util_1.bytesToUnprefixedHex)(entry[1])])); return [ helpers_ts_1.DBOp.set(operation_ts_1.DBTarget.Heads, hexHeads), helpers_ts_1.DBOp.set(operation_ts_1.DBTarget.HeadHeader, this._headHeaderHash), helpers_ts_1.DBOp.set(operation_ts_1.DBTarget.HeadBlock, this._headBlockHash), ]; } /** * Gets the `DatabaseOperation[]` list to save `_heads`, `_headHeaderHash` and * `_headBlockHash` and writes these to the DB * @hidden */ async _saveHeads() { return this.dbManager.batch(this._saveHeadOps()); } /** * Gets a header by hash and number. Header can exist outside the canonical * chain * * @hidden */ async _getHeader(hash, number) { if (number === undefined) { number = await this.dbManager.hashToNumber(hash); if (number === undefined) throw (0, util_1.EthereumJSErrorWithoutCode)(`no header for ${(0, util_1.bytesToHex)(hash)} found in DB`); } return this.dbManager.getHeader(hash, number); } async checkAndTransitionHar