UNPKG

@ethereumjs/block

Version:
446 lines 19.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Block = void 0; const common_1 = require("@ethereumjs/common"); const mpt_1 = require("@ethereumjs/mpt"); const rlp_1 = require("@ethereumjs/rlp"); const tx_1 = require("@ethereumjs/tx"); const util_1 = require("@ethereumjs/util"); const keccak_js_1 = require("ethereum-cryptography/keccak.js"); const sha256_js_1 = require("ethereum-cryptography/sha256.js"); /* eslint-disable */ // This is to allow for a proper and linked collection of constructors for the class header. // For tree shaking/code size this should be no problem since types go away on transpilation. // TODO: See if there is an easier way to achieve the same result. // See: https://github.com/microsoft/TypeScript/issues/47558 // (situation will eventually improve on Typescript and/or Eslint update) const index_ts_1 = require("../index.js"); /** * Class representing a block in the Ethereum network. The {@link BlockHeader} has its own * class and can be used independently, for a block it is included in the form of the * {@link Block.header} property. * * A block object can be created with one of the following constructor methods * (separate from the Block class to allow for tree shaking): * * - {@link createBlock } * - {@link createBlockFromBytesArray } * - {@link createBlockFromRLP } * - {@link createBlockFromRPC } * - {@link createBlockFromJSONRPCProvider } * - {@link createBlockFromExecutionPayload } * - {@link createBlockFromBeaconPayloadJSON } */ class Block { /** * This constructor takes the values, validates them, assigns them and freezes the object. * * @deprecated Use the static factory methods (see {@link Block} for an overview) to assist in creating * a Block object from varying data types and options. */ constructor(header, transactions = [], uncleHeaders = [], withdrawals, opts = {}, executionWitness) { this.transactions = []; this.uncleHeaders = []; this.cache = {}; this.header = header ?? new index_ts_1.BlockHeader({}, opts); this.common = this.header.common; this.keccakFunction = this.common.customCrypto.keccak256 ?? keccak_js_1.keccak256; this.sha256Function = this.common.customCrypto.sha256 ?? sha256_js_1.sha256; this.transactions = transactions; this.withdrawals = withdrawals ?? (this.common.isActivatedEIP(4895) ? [] : undefined); this.executionWitness = executionWitness; // null indicates an intentional absence of value or unavailability // undefined indicates that the executionWitness should be initialized with the default state if (this.common.isActivatedEIP(6800) && this.executionWitness === undefined) { this.executionWitness = { // TODO: Evaluate how default parentStateRoot should be handled? parentStateRoot: '0x', stateDiff: [], verkleProof: { commitmentsByPath: [], d: '0x', depthExtensionPresent: '0x', ipaProof: { cl: [], cr: [], finalEvaluation: '0x', }, otherStems: [], }, }; } this.uncleHeaders = uncleHeaders; if (uncleHeaders.length > 0) { this.validateUncles(); if (this.common.consensusType() === common_1.ConsensusType.ProofOfAuthority) { const msg = this._errorMsg('Block initialization with uncleHeaders on a PoA network is not allowed'); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } if (this.common.consensusType() === common_1.ConsensusType.ProofOfStake) { const msg = this._errorMsg('Block initialization with uncleHeaders on a PoS network is not allowed'); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } } if (!this.common.isActivatedEIP(4895) && withdrawals !== undefined) { throw (0, util_1.EthereumJSErrorWithoutCode)('Cannot have a withdrawals field if EIP 4895 is not active'); } if (!this.common.isActivatedEIP(6800) && executionWitness !== undefined && executionWitness !== null) { throw (0, util_1.EthereumJSErrorWithoutCode)(`Cannot have executionWitness field if EIP 6800 is not active `); } const freeze = opts?.freeze ?? true; if (freeze) { Object.freeze(this); } } /** * Returns a Array of the raw Bytes Arrays of this block, in order. */ raw() { const bytesArray = [ this.header.raw(), this.transactions.map((tx) => tx.supports(tx_1.Capability.EIP2718TypedTransaction) ? tx.serialize() : tx.raw()), this.uncleHeaders.map((uh) => uh.raw()), ]; const withdrawalsRaw = this.withdrawals?.map((wt) => wt.raw()); if (withdrawalsRaw) { bytesArray.push(withdrawalsRaw); } if (this.executionWitness !== undefined && this.executionWitness !== null) { const executionWitnessBytes = rlp_1.RLP.encode(JSON.stringify(this.executionWitness)); bytesArray.push(executionWitnessBytes); } return bytesArray; } /** * Returns the hash of the block. */ hash() { return this.header.hash(); } /** * Determines if this block is the genesis block. */ isGenesis() { return this.header.isGenesis(); } /** * Returns the rlp encoding of the block. */ serialize() { return rlp_1.RLP.encode(this.raw()); } /** * Generates transaction trie for validation. */ async genTxTrie() { return (0, index_ts_1.genTransactionsTrieRoot)(this.transactions, new mpt_1.MerklePatriciaTrie({ common: this.common })); } /** * Validates the transaction trie by generating a trie * and do a check on the root hash. * @returns True if the transaction trie is valid, false otherwise */ async transactionsTrieIsValid() { let result; if (this.transactions.length === 0) { result = (0, util_1.equalsBytes)(this.header.transactionsTrie, util_1.KECCAK256_RLP); return result; } if (this.cache.txTrieRoot === undefined) { this.cache.txTrieRoot = await this.genTxTrie(); } result = (0, util_1.equalsBytes)(this.cache.txTrieRoot, this.header.transactionsTrie); return result; } /** * Validates transaction signatures and minimum gas requirements. * @returns {string[]} an array of error strings */ getTransactionsValidationErrors() { const errors = []; let blobGasUsed = util_1.BIGINT_0; // eslint-disable-next-line prefer-const for (let [i, tx] of this.transactions.entries()) { const errs = tx.getValidationErrors(); if (this.common.isActivatedEIP(1559)) { if (tx.supports(tx_1.Capability.EIP1559FeeMarket)) { tx = tx; if (tx.maxFeePerGas < this.header.baseFeePerGas) { errs.push('tx unable to pay base fee (EIP-1559 tx)'); } } else { tx = tx; if (tx.gasPrice < this.header.baseFeePerGas) { errs.push('tx unable to pay base fee (non EIP-1559 tx)'); } } } if (this.common.isActivatedEIP(4844)) { const blobGasLimit = this.common.param('maxBlobGasPerBlock'); const blobGasPerBlob = this.common.param('blobGasPerBlob'); if (tx instanceof tx_1.Blob4844Tx) { blobGasUsed += BigInt(tx.numBlobs()) * blobGasPerBlob; if (blobGasUsed > blobGasLimit) { errs.push(`tx causes total blob gas of ${blobGasUsed} to exceed maximum blob gas per block of ${blobGasLimit}`); } } } if (errs.length > 0) { errors.push(`errors at tx ${i}: ${errs.join(', ')}`); } } if (this.common.isActivatedEIP(4844)) { if (blobGasUsed !== this.header.blobGasUsed) { errors.push(`invalid blobGasUsed expected=${this.header.blobGasUsed} actual=${blobGasUsed}`); } } return errors; } /** * Validates transaction signatures and minimum gas requirements. * @returns True if all transactions are valid, false otherwise */ transactionsAreValid() { const errors = this.getTransactionsValidationErrors(); return errors.length === 0; } /** * Validates the block data, throwing if invalid. * This can be checked on the Block itself without needing access to any parent block * It checks: * - All transactions are valid * - The transactions trie is valid * - The uncle hash is valid * @param onlyHeader if only passed the header, skip validating txTrie and unclesHash (default: false) * @param verifyTxs if set to `false`, will not check for transaction validation errors (default: true) */ async validateData(onlyHeader = false, verifyTxs = true) { if (verifyTxs) { const txErrors = this.getTransactionsValidationErrors(); if (txErrors.length > 0) { const msg = this._errorMsg(`invalid transactions: ${txErrors.join(' ')}`); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } } if (onlyHeader) { return; } if (verifyTxs) { for (const [index, tx] of this.transactions.entries()) { if (!tx.isSigned()) { const msg = this._errorMsg(`invalid transactions: transaction at index ${index} is unsigned`); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } } } if (!(await this.transactionsTrieIsValid())) { const msg = this._errorMsg('invalid transaction trie'); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } if (!this.uncleHashIsValid()) { const msg = this._errorMsg('invalid uncle hash'); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } if (this.common.isActivatedEIP(4895) && !(await this.withdrawalsTrieIsValid())) { const msg = this._errorMsg('invalid withdrawals trie'); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } // Validation for Verkle blocks // Unnecessary in this implementation since we're providing defaults if those fields are undefined // TODO: Decide if we should actually require this or not if (this.common.isActivatedEIP(6800)) { if (this.executionWitness === undefined) { throw (0, util_1.EthereumJSErrorWithoutCode)(`Invalid block: missing executionWitness`); } if (this.executionWitness === null) { throw (0, util_1.EthereumJSErrorWithoutCode)(`Invalid block: ethereumjs stateless client needs executionWitness`); } } } /** * Validates that blob gas fee for each transaction is greater than or equal to the * blobGasPrice for the block and that total blob gas in block is less than maximum * blob gas per block * @param parentHeader header of parent block */ validateBlobTransactions(parentHeader) { if (this.common.isActivatedEIP(4844)) { const blobGasLimit = this.common.param('maxBlobGasPerBlock'); const blobGasPerBlob = this.common.param('blobGasPerBlob'); let blobGasUsed = util_1.BIGINT_0; const expectedExcessBlobGas = parentHeader.calcNextExcessBlobGas(this.common); if (this.header.excessBlobGas !== expectedExcessBlobGas) { throw (0, util_1.EthereumJSErrorWithoutCode)(`block excessBlobGas mismatch: have ${this.header.excessBlobGas}, want ${expectedExcessBlobGas}`); } let blobGasPrice; for (const tx of this.transactions) { if (tx instanceof tx_1.Blob4844Tx) { blobGasPrice = blobGasPrice ?? this.header.getBlobGasPrice(); if (tx.maxFeePerBlobGas < blobGasPrice) { throw (0, util_1.EthereumJSErrorWithoutCode)(`blob transaction maxFeePerBlobGas ${tx.maxFeePerBlobGas} < than block blob gas price ${blobGasPrice} - ${this.errorStr()}`); } blobGasUsed += BigInt(tx.blobVersionedHashes.length) * blobGasPerBlob; if (blobGasUsed > blobGasLimit) { throw (0, util_1.EthereumJSErrorWithoutCode)(`tx causes total blob gas of ${blobGasUsed} to exceed maximum blob gas per block of ${blobGasLimit}`); } } } if (this.header.blobGasUsed !== blobGasUsed) { throw (0, util_1.EthereumJSErrorWithoutCode)(`block blobGasUsed mismatch: have ${this.header.blobGasUsed}, want ${blobGasUsed}`); } } } /** * Validates the uncle's hash. * @returns true if the uncle's hash is valid, false otherwise. */ uncleHashIsValid() { if (this.uncleHeaders.length === 0) { return (0, util_1.equalsBytes)(util_1.KECCAK256_RLP_ARRAY, this.header.uncleHash); } const uncles = this.uncleHeaders.map((uh) => uh.raw()); const raw = rlp_1.RLP.encode(uncles); return (0, util_1.equalsBytes)(this.keccakFunction(raw), this.header.uncleHash); } /** * Validates the withdrawal root * @returns true if the withdrawals trie root is valid, false otherwise */ async withdrawalsTrieIsValid() { if (!this.common.isActivatedEIP(4895)) { throw (0, util_1.EthereumJSErrorWithoutCode)('EIP 4895 is not activated'); } let result; if (this.withdrawals.length === 0) { result = (0, util_1.equalsBytes)(this.header.withdrawalsRoot, util_1.KECCAK256_RLP); return result; } if (this.cache.withdrawalsTrieRoot === undefined) { this.cache.withdrawalsTrieRoot = await (0, index_ts_1.genWithdrawalsTrieRoot)(this.withdrawals, new mpt_1.MerklePatriciaTrie({ common: this.common })); } result = (0, util_1.equalsBytes)(this.cache.withdrawalsTrieRoot, this.header.withdrawalsRoot); return result; } /** * Consistency checks for uncles included in the block, if any. * * Throws if invalid. * * The rules for uncles checked are the following: * Header has at most 2 uncles. * Header does not count an uncle twice. */ validateUncles() { if (this.isGenesis()) { return; } // Header has at most 2 uncles if (this.uncleHeaders.length > 2) { const msg = this._errorMsg('too many uncle headers'); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } // Header does not count an uncle twice. const uncleHashes = this.uncleHeaders.map((header) => (0, util_1.bytesToHex)(header.hash())); if (!(new Set(uncleHashes).size === uncleHashes.length)) { const msg = this._errorMsg('duplicate uncles'); throw (0, util_1.EthereumJSErrorWithoutCode)(msg); } } /** * Validates if the block gasLimit remains in the boundaries set by the protocol. * Throws if invalid * * @param parentBlock - the parent of this `Block` */ validateGasLimit(parentBlock) { return this.header.validateGasLimit(parentBlock.header); } /** * Returns the block in JSON format. */ toJSON() { const withdrawalsAttr = this.withdrawals ? { withdrawals: this.withdrawals.map((wt) => wt.toJSON()), } : {}; return { header: this.header.toJSON(), transactions: this.transactions.map((tx) => tx.toJSON()), uncleHeaders: this.uncleHeaders.map((uh) => uh.toJSON()), ...withdrawalsAttr, }; } /** * Maps the block properties to the execution payload structure from the beacon chain, * see https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#ExecutionPayload * * @returns dict with the execution payload parameters with camel case naming */ toExecutionPayload() { const blockJSON = this.toJSON(); const header = blockJSON.header; const transactions = this.transactions.map((tx) => (0, util_1.bytesToHex)(tx.serialize())) ?? []; const withdrawalsArr = blockJSON.withdrawals ? { withdrawals: blockJSON.withdrawals } : {}; const executionPayload = { blockNumber: header.number, parentHash: header.parentHash, feeRecipient: header.coinbase, stateRoot: header.stateRoot, receiptsRoot: header.receiptTrie, logsBloom: header.logsBloom, gasLimit: header.gasLimit, gasUsed: header.gasUsed, timestamp: header.timestamp, extraData: header.extraData, baseFeePerGas: header.baseFeePerGas, blobGasUsed: header.blobGasUsed, excessBlobGas: header.excessBlobGas, blockHash: (0, util_1.bytesToHex)(this.hash()), prevRandao: header.mixHash, transactions, ...withdrawalsArr, parentBeaconBlockRoot: header.parentBeaconBlockRoot, requestsHash: header.requestsHash, executionWitness: this.executionWitness, }; return executionPayload; } /** * Return a compact error string representation of the object */ errorStr() { let hash = ''; try { hash = (0, util_1.bytesToHex)(this.hash()); } catch { hash = 'error'; } let hf = ''; try { hf = this.common.hardfork(); } catch { hf = 'error'; } let errorStr = `block number=${this.header.number} hash=${hash} `; errorStr += `hf=${hf} baseFeePerGas=${this.header.baseFeePerGas ?? 'none'} `; errorStr += `txs=${this.transactions.length} uncles=${this.uncleHeaders.length}`; return errorStr; } /** * Internal helper function to create an annotated error message * * @param msg Base error message * @hidden */ _errorMsg(msg) { return `${msg} (${this.errorStr()})`; } } exports.Block = Block; //# sourceMappingURL=block.js.map