@ethereumjs/blockchain
Version:
A module to store and interact with blocks
1,028 lines • 52.8 kB
JavaScript
"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