@ethereumjs/block
Version:
Provides Block serialization and help functions
1,089 lines (985 loc) • 35.7 kB
text/typescript
import { ConsensusType } from '@ethereumjs/common'
import { RLP } from '@ethereumjs/rlp'
import { Trie } from '@ethereumjs/trie'
import { BlobEIP4844Transaction, Capability, TransactionFactory } from '@ethereumjs/tx'
import {
BIGINT_0,
CLRequestFactory,
CLRequestType,
ConsolidationRequest,
DepositRequest,
KECCAK256_RLP,
KECCAK256_RLP_ARRAY,
Withdrawal,
WithdrawalRequest,
bigIntToHex,
bytesToHex,
bytesToUtf8,
equalsBytes,
fetchFromProvider,
getProvider,
hexToBytes,
intToHex,
isHexString,
} from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { executionPayloadFromBeaconPayload } from './from-beacon-payload.js'
import { blockFromRpc } from './from-rpc.js'
import { BlockHeader } from './header.js'
import type { BeaconPayloadJson } from './from-beacon-payload.js'
import type {
BlockBytes,
BlockData,
BlockOptions,
ExecutionPayload,
ExecutionWitnessBytes,
HeaderData,
JsonBlock,
JsonRpcBlock,
RequestsBytes,
WithdrawalsBytes,
} from './types.js'
import type { Common } from '@ethereumjs/common'
import type {
FeeMarketEIP1559Transaction,
LegacyTransaction,
TxOptions,
TypedTransaction,
} from '@ethereumjs/tx'
import type {
CLRequest,
EthersProvider,
PrefixedHexString,
RequestBytes,
VerkleExecutionWitness,
WithdrawalBytes,
} from '@ethereumjs/util'
/**
* An object that represents the block.
*/
export class Block {
public readonly header: BlockHeader
public readonly transactions: TypedTransaction[] = []
public readonly uncleHeaders: BlockHeader[] = []
public readonly withdrawals?: Withdrawal[]
public readonly requests?: CLRequest<CLRequestType>[]
public readonly common: Common
protected keccakFunction: (msg: Uint8Array) => Uint8Array
/**
* EIP-6800: Verkle Proof Data (experimental)
* null implies that the non default executionWitness might exist but not available
* and will not lead to execution of the block via vm with verkle stateless manager
*/
public readonly executionWitness?: VerkleExecutionWitness | null
protected cache: {
txTrieRoot?: Uint8Array
withdrawalsTrieRoot?: Uint8Array
requestsRoot?: Uint8Array
} = {}
/**
* Returns the withdrawals trie root for array of Withdrawal.
* @param wts array of Withdrawal to compute the root of
* @param optional emptyTrie to use to generate the root
*/
public static async genWithdrawalsTrieRoot(wts: Withdrawal[], emptyTrie?: Trie) {
const trie = emptyTrie ?? new Trie()
for (const [i, wt] of wts.entries()) {
await trie.put(RLP.encode(i), RLP.encode(wt.raw()))
}
return trie.root()
}
/**
* Returns the txs trie root for array of TypedTransaction
* @param txs array of TypedTransaction to compute the root of
* @param optional emptyTrie to use to generate the root
*/
public static async genTransactionsTrieRoot(txs: TypedTransaction[], emptyTrie?: Trie) {
const trie = emptyTrie ?? new Trie()
for (const [i, tx] of txs.entries()) {
await trie.put(RLP.encode(i), tx.serialize())
}
return trie.root()
}
/**
* Returns the requests trie root for an array of CLRequests
* @param requests - an array of CLRequests
* @param emptyTrie optional empty trie used to generate the root
* @returns a 32 byte Uint8Array representing the requests trie root
*/
public static async genRequestsTrieRoot(requests: CLRequest<CLRequestType>[], emptyTrie?: Trie) {
// Requests should be sorted in monotonically ascending order based on type
// and whatever internal sorting logic is defined by each request type
if (requests.length > 1) {
for (let x = 1; x < requests.length; x++) {
if (requests[x].type < requests[x - 1].type)
throw new Error('requests are not sorted in ascending order')
}
}
const trie = emptyTrie ?? new Trie()
for (const [i, req] of requests.entries()) {
await trie.put(RLP.encode(i), req.serialize())
}
return trie.root()
}
/**
* Static constructor to create a block from a block data dictionary
*
* @param blockData
* @param opts
*/
public static fromBlockData(blockData: BlockData = {}, opts?: BlockOptions) {
const {
header: headerData,
transactions: txsData,
uncleHeaders: uhsData,
withdrawals: withdrawalsData,
executionWitness: executionWitnessData,
requests: clRequests,
} = blockData
const header = BlockHeader.fromHeaderData(headerData, opts)
// parse transactions
const transactions = []
for (const txData of txsData ?? []) {
const tx = TransactionFactory.fromTxData(txData, {
...opts,
// Use header common in case of setHardfork being activated
common: header.common,
} as TxOptions)
transactions.push(tx)
}
// parse uncle headers
const uncleHeaders = []
const uncleOpts: BlockOptions = {
...opts,
// Use header common in case of setHardfork being activated
common: header.common,
// Disable this option here (all other options carried over), since this overwrites the provided Difficulty to an incorrect value
calcDifficultyFromHeader: undefined,
}
// Uncles are obsolete post-merge, any hardfork by option implies setHardfork
if (opts?.setHardfork !== undefined) {
uncleOpts.setHardfork = true
}
for (const uhData of uhsData ?? []) {
const uh = BlockHeader.fromHeaderData(uhData, uncleOpts)
uncleHeaders.push(uh)
}
const withdrawals = withdrawalsData?.map(Withdrawal.fromWithdrawalData)
// The witness data is planned to come in rlp serialized bytes so leave this
// stub till that time
const executionWitness = executionWitnessData
return new Block(
header,
transactions,
uncleHeaders,
withdrawals,
opts,
clRequests,
executionWitness
)
}
/**
* Static constructor to create a block from a RLP-serialized block
*
* @param serialized
* @param opts
*/
public static fromRLPSerializedBlock(serialized: Uint8Array, opts?: BlockOptions) {
const values = RLP.decode(Uint8Array.from(serialized)) as BlockBytes
if (!Array.isArray(values)) {
throw new Error('Invalid serialized block input. Must be array')
}
return Block.fromValuesArray(values, opts)
}
/**
* Static constructor to create a block from an array of Bytes values
*
* @param values
* @param opts
*/
public static fromValuesArray(values: BlockBytes, opts?: BlockOptions) {
if (values.length > 5) {
throw new Error(
`invalid block. More values=${values.length} than expected were received (at most 5)`
)
}
// First try to load header so that we can use its common (in case of setHardfork being activated)
// to correctly make checks on the hardforks
const [headerData, txsData, uhsData, ...valuesTail] = values
const header = BlockHeader.fromValuesArray(headerData, opts)
// conditional assignment of rest of values and splicing them out from the valuesTail
const withdrawalBytes = header.common.isActivatedEIP(4895)
? (valuesTail.splice(0, 1)[0] as WithdrawalsBytes)
: undefined
const requestBytes = header.common.isActivatedEIP(7685)
? (valuesTail.splice(0, 1)[0] as RequestsBytes)
: undefined
// if witness bytes are not present that we should assume that witness has not been provided
// in that scenario pass null as undefined is used for default witness assignment
const executionWitnessBytes = header.common.isActivatedEIP(6800)
? (valuesTail.splice(0, 1)[0] as ExecutionWitnessBytes)
: null
if (
header.common.isActivatedEIP(4895) &&
(withdrawalBytes === undefined || !Array.isArray(withdrawalBytes))
) {
throw new Error(
'Invalid serialized block input: EIP-4895 is active, and no withdrawals were provided as array'
)
}
if (
header.common.isActivatedEIP(7685) &&
(requestBytes === undefined || !Array.isArray(requestBytes))
) {
throw new Error(
'Invalid serialized block input: EIP-7685 is active, and no requestBytes were provided as array'
)
}
if (header.common.isActivatedEIP(6800) && executionWitnessBytes === undefined) {
throw new Error(
'Invalid serialized block input: EIP-6800 is active, and execution witness is undefined'
)
}
// parse transactions
const transactions = []
for (const txData of txsData ?? []) {
transactions.push(
TransactionFactory.fromBlockBodyData(txData, {
...opts,
// Use header common in case of setHardfork being activated
common: header.common,
})
)
}
// parse uncle headers
const uncleHeaders = []
const uncleOpts: BlockOptions = {
...opts,
// Use header common in case of setHardfork being activated
common: header.common,
// Disable this option here (all other options carried over), since this overwrites the provided Difficulty to an incorrect value
calcDifficultyFromHeader: undefined,
}
// Uncles are obsolete post-merge, any hardfork by option implies setHardfork
if (opts?.setHardfork !== undefined) {
uncleOpts.setHardfork = true
}
for (const uncleHeaderData of uhsData ?? []) {
uncleHeaders.push(BlockHeader.fromValuesArray(uncleHeaderData, uncleOpts))
}
const withdrawals = (withdrawalBytes as WithdrawalBytes[])
?.map(([index, validatorIndex, address, amount]) => ({
index,
validatorIndex,
address,
amount,
}))
?.map(Withdrawal.fromWithdrawalData)
let requests
if (header.common.isActivatedEIP(7685)) {
requests = (requestBytes as RequestBytes[]).map((bytes) =>
CLRequestFactory.fromSerializedRequest(bytes)
)
}
// executionWitness are not part of the EL fetched blocks via eth_ bodies method
// they are currently only available via the engine api constructed blocks
let executionWitness
if (header.common.isActivatedEIP(6800)) {
if (executionWitnessBytes !== undefined) {
executionWitness = JSON.parse(bytesToUtf8(RLP.decode(executionWitnessBytes) as Uint8Array))
} else if (opts?.executionWitness !== undefined) {
executionWitness = opts.executionWitness
} else {
// don't assign default witness if eip 6800 is implemented as it leads to incorrect
// assumptions while executing the block. if not present in input implies its unavailable
executionWitness = null
}
}
return new Block(
header,
transactions,
uncleHeaders,
withdrawals,
opts,
requests,
executionWitness
)
}
/**
* Creates a new block object from Ethereum JSON RPC.
*
* @param blockParams - Ethereum JSON RPC of block (eth_getBlockByNumber)
* @param uncles - Optional list of Ethereum JSON RPC of uncles (eth_getUncleByBlockHashAndIndex)
* @param opts - An object describing the blockchain
*/
public static fromRPC(blockData: JsonRpcBlock, uncles?: any[], opts?: BlockOptions) {
return blockFromRpc(blockData, uncles, opts)
}
/**
* Method to retrieve a block from a JSON-RPC provider and format as a {@link Block}
* @param provider either a url for a remote provider or an Ethers JsonRpcProvider object
* @param blockTag block hash or block number to be run
* @param opts {@link BlockOptions}
* @returns the block specified by `blockTag`
*/
public static fromJsonRpcProvider = async (
provider: string | EthersProvider,
blockTag: string | bigint,
opts: BlockOptions
) => {
let blockData
const providerUrl = getProvider(provider)
if (typeof blockTag === 'string' && blockTag.length === 66) {
blockData = await fetchFromProvider(providerUrl, {
method: 'eth_getBlockByHash',
params: [blockTag, true],
})
} else if (typeof blockTag === 'bigint') {
blockData = await fetchFromProvider(providerUrl, {
method: 'eth_getBlockByNumber',
params: [bigIntToHex(blockTag), true],
})
} else if (
isHexString(blockTag) ||
blockTag === 'latest' ||
blockTag === 'earliest' ||
blockTag === 'pending' ||
blockTag === 'finalized' ||
blockTag === 'safe'
) {
blockData = await fetchFromProvider(providerUrl, {
method: 'eth_getBlockByNumber',
params: [blockTag, true],
})
} else {
throw new Error(
`expected blockTag to be block hash, bigint, hex prefixed string, or earliest/latest/pending; got ${blockTag}`
)
}
if (blockData === null) {
throw new Error('No block data returned from provider')
}
const uncleHeaders = []
if (blockData.uncles.length > 0) {
for (let x = 0; x < blockData.uncles.length; x++) {
const headerData = await fetchFromProvider(providerUrl, {
method: 'eth_getUncleByBlockHashAndIndex',
params: [blockData.hash, intToHex(x)],
})
uncleHeaders.push(headerData)
}
}
return blockFromRpc(blockData, uncleHeaders, opts)
}
/**
* Method to retrieve a block from an execution payload
* @param execution payload constructed from beacon payload
* @param opts {@link BlockOptions}
* @returns the block constructed block
*/
public static async fromExecutionPayload(
payload: ExecutionPayload,
opts?: BlockOptions
): Promise<Block> {
const {
blockNumber: number,
receiptsRoot: receiptTrie,
prevRandao: mixHash,
feeRecipient: coinbase,
transactions,
withdrawals: withdrawalsData,
depositRequests,
withdrawalRequests,
consolidationRequests,
executionWitness,
} = payload
const txs = []
for (const [index, serializedTx] of transactions.entries()) {
try {
const tx = TransactionFactory.fromSerializedData(
hexToBytes(serializedTx as PrefixedHexString),
{
common: opts?.common,
}
)
txs.push(tx)
} catch (error) {
const validationError = `Invalid tx at index ${index}: ${error}`
throw validationError
}
}
const transactionsTrie = await Block.genTransactionsTrieRoot(
txs,
new Trie({ common: opts?.common })
)
const withdrawals = withdrawalsData?.map((wData) => Withdrawal.fromWithdrawalData(wData))
const withdrawalsRoot = withdrawals
? await Block.genWithdrawalsTrieRoot(withdrawals, new Trie({ common: opts?.common }))
: undefined
const hasDepositRequests = depositRequests !== undefined && depositRequests !== null
const hasWithdrawalRequests = withdrawalRequests !== undefined && withdrawalRequests !== null
const hasConsolidationRequests =
consolidationRequests !== undefined && consolidationRequests !== null
const requests =
hasDepositRequests || hasWithdrawalRequests || hasConsolidationRequests
? ([] as CLRequest<CLRequestType>[])
: undefined
if (depositRequests !== undefined && depositRequests !== null) {
for (const dJson of depositRequests) {
requests!.push(DepositRequest.fromJSON(dJson))
}
}
if (withdrawalRequests !== undefined && withdrawalRequests !== null) {
for (const wJson of withdrawalRequests) {
requests!.push(WithdrawalRequest.fromJSON(wJson))
}
}
if (consolidationRequests !== undefined && consolidationRequests !== null) {
for (const cJson of consolidationRequests) {
requests!.push(ConsolidationRequest.fromJSON(cJson))
}
}
const requestsRoot = requests
? await Block.genRequestsTrieRoot(requests, new Trie({ common: opts?.common }))
: undefined
const header: HeaderData = {
...payload,
number,
receiptTrie,
transactionsTrie,
withdrawalsRoot,
mixHash,
coinbase,
requestsRoot,
}
// we are not setting setHardfork as common is already set to the correct hf
const block = Block.fromBlockData(
{ header, transactions: txs, withdrawals, executionWitness, requests },
opts
)
if (
block.common.isActivatedEIP(6800) &&
(executionWitness === undefined || executionWitness === null)
) {
throw Error('Missing executionWitness for EIP-6800 activated executionPayload')
}
// Verify blockHash matches payload
if (!equalsBytes(block.hash(), hexToBytes(payload.blockHash as PrefixedHexString))) {
const validationError = `Invalid blockHash, expected: ${
payload.blockHash
}, received: ${bytesToHex(block.hash())}`
throw Error(validationError)
}
return block
}
/**
* Method to retrieve a block from a beacon payload json
* @param payload json of a beacon beacon fetched from beacon apis
* @param opts {@link BlockOptions}
* @returns the block constructed block
*/
public static async fromBeaconPayloadJson(
payload: BeaconPayloadJson,
opts?: BlockOptions
): Promise<Block> {
const executionPayload = executionPayloadFromBeaconPayload(payload)
return Block.fromExecutionPayload(executionPayload, opts)
}
/**
* This constructor takes the values, validates them, assigns them and freezes the object.
* Use the static factory methods to assist in creating a Block object from varying data types and options.
*/
constructor(
header?: BlockHeader,
transactions: TypedTransaction[] = [],
uncleHeaders: BlockHeader[] = [],
withdrawals?: Withdrawal[],
opts: BlockOptions = {},
requests?: CLRequest<CLRequestType>[],
executionWitness?: VerkleExecutionWitness | null
) {
this.header = header ?? BlockHeader.fromHeaderData({}, opts)
this.common = this.header.common
this.keccakFunction = this.common.customCrypto.keccak256 ?? keccak256
this.transactions = transactions
this.withdrawals = withdrawals ?? (this.common.isActivatedEIP(4895) ? [] : undefined)
this.executionWitness = executionWitness
this.requests = requests ?? (this.common.isActivatedEIP(7685) ? [] : undefined)
// 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 = {
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() === ConsensusType.ProofOfAuthority) {
const msg = this._errorMsg(
'Block initialization with uncleHeaders on a PoA network is not allowed'
)
throw new Error(msg)
}
if (this.common.consensusType() === ConsensusType.ProofOfStake) {
const msg = this._errorMsg(
'Block initialization with uncleHeaders on a PoS network is not allowed'
)
throw new Error(msg)
}
}
if (!this.common.isActivatedEIP(4895) && withdrawals !== undefined) {
throw new Error('Cannot have a withdrawals field if EIP 4895 is not active')
}
if (
!this.common.isActivatedEIP(6800) &&
executionWitness !== undefined &&
executionWitness !== null
) {
throw new Error(`Cannot have executionWitness field if EIP 6800 is not active `)
}
if (!this.common.isActivatedEIP(7685) && requests !== undefined) {
throw new Error(`Cannot have requests field if EIP 7685 is not active`)
}
// Requests should be sorted in monotonically ascending order based on type
// and whatever internal sorting logic is defined by each request type
if (requests !== undefined && requests.length > 1) {
for (let x = 1; x < requests.length; x++) {
if (requests[x].type < requests[x - 1].type)
throw new Error('requests are not sorted in ascending order')
}
}
const freeze = opts?.freeze ?? true
if (freeze) {
Object.freeze(this)
}
}
/**
* Returns a Array of the raw Bytes Arrays of this block, in order.
*/
raw(): BlockBytes {
const bytesArray = <BlockBytes>[
this.header.raw(),
this.transactions.map((tx) =>
tx.supports(Capability.EIP2718TypedTransaction) ? tx.serialize() : tx.raw()
) as Uint8Array[],
this.uncleHeaders.map((uh) => uh.raw()),
]
const withdrawalsRaw = this.withdrawals?.map((wt) => wt.raw())
if (withdrawalsRaw) {
bytesArray.push(withdrawalsRaw)
}
const requestsRaw = this.requests?.map((req) => req.serialize())
if (requestsRaw) {
bytesArray.push(requestsRaw)
}
if (this.executionWitness !== undefined && this.executionWitness !== null) {
const executionWitnessBytes = RLP.encode(JSON.stringify(this.executionWitness))
bytesArray.push(executionWitnessBytes as any)
}
return bytesArray
}
/**
* Returns the hash of the block.
*/
hash(): Uint8Array {
return this.header.hash()
}
/**
* Determines if this block is the genesis block.
*/
isGenesis(): boolean {
return this.header.isGenesis()
}
/**
* Returns the rlp encoding of the block.
*/
serialize(): Uint8Array {
return RLP.encode(this.raw())
}
/**
* Generates transaction trie for validation.
*/
async genTxTrie(): Promise<Uint8Array> {
return Block.genTransactionsTrieRoot(this.transactions, new Trie({ 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(): Promise<boolean> {
let result
if (this.transactions.length === 0) {
result = equalsBytes(this.header.transactionsTrie, KECCAK256_RLP)
return result
}
if (this.cache.txTrieRoot === undefined) {
this.cache.txTrieRoot = await this.genTxTrie()
}
result = equalsBytes(this.cache.txTrieRoot, this.header.transactionsTrie)
return result
}
async requestsTrieIsValid(requestsInput?: CLRequest<CLRequestType>[]): Promise<boolean> {
if (!this.common.isActivatedEIP(7685)) {
throw new Error('EIP 7685 is not activated')
}
const requests = requestsInput ?? this.requests!
if (requests!.length === 0) {
return equalsBytes(this.header.requestsRoot!, KECCAK256_RLP)
}
if (requestsInput === undefined) {
if (this.cache.requestsRoot === undefined) {
this.cache.requestsRoot = await Block.genRequestsTrieRoot(this.requests!)
}
return equalsBytes(this.cache.requestsRoot, this.header.requestsRoot!)
} else {
const reportedRoot = await Block.genRequestsTrieRoot(requests)
return equalsBytes(reportedRoot, this.header.requestsRoot!)
}
}
/**
* Validates transaction signatures and minimum gas requirements.
* @returns {string[]} an array of error strings
*/
getTransactionsValidationErrors(): string[] {
const errors: string[] = []
let blobGasUsed = BIGINT_0
const blobGasLimit = this.common.param('gasConfig', 'maxblobGasPerBlock')
const blobGasPerBlob = this.common.param('gasConfig', 'blobGasPerBlob')
// 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(Capability.EIP1559FeeMarket)) {
tx = tx as FeeMarketEIP1559Transaction
if (tx.maxFeePerGas < this.header.baseFeePerGas!) {
errs.push('tx unable to pay base fee (EIP-1559 tx)')
}
} else {
tx = tx as LegacyTransaction
if (tx.gasPrice < this.header.baseFeePerGas!) {
errs.push('tx unable to pay base fee (non EIP-1559 tx)')
}
}
}
if (this.common.isActivatedEIP(4844)) {
if (tx instanceof BlobEIP4844Transaction) {
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(): boolean {
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: boolean = false, verifyTxs: boolean = true): Promise<void> {
if (verifyTxs) {
const txErrors = this.getTransactionsValidationErrors()
if (txErrors.length > 0) {
const msg = this._errorMsg(`invalid transactions: ${txErrors.join(' ')}`)
throw new Error(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 new Error(msg)
}
}
}
if (!(await this.transactionsTrieIsValid())) {
const msg = this._errorMsg('invalid transaction trie')
throw new Error(msg)
}
if (!this.uncleHashIsValid()) {
const msg = this._errorMsg('invalid uncle hash')
throw new Error(msg)
}
if (this.common.isActivatedEIP(4895) && !(await this.withdrawalsTrieIsValid())) {
const msg = this._errorMsg('invalid withdrawals trie')
throw new Error(msg)
}
// Validation for Verkle blocks
// Unnecessary in this implementation since we're providing defaults if those fields are undefined
if (this.common.isActivatedEIP(6800)) {
if (this.executionWitness === undefined) {
throw new Error(`Invalid block: missing executionWitness`)
}
if (this.executionWitness === null) {
throw new Error(`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: BlockHeader) {
if (this.common.isActivatedEIP(4844)) {
const blobGasLimit = this.common.param('gasConfig', 'maxblobGasPerBlock')
const blobGasPerBlob = this.common.param('gasConfig', 'blobGasPerBlob')
let blobGasUsed = BIGINT_0
const expectedExcessBlobGas = parentHeader.calcNextExcessBlobGas()
if (this.header.excessBlobGas !== expectedExcessBlobGas) {
throw new Error(
`block excessBlobGas mismatch: have ${this.header.excessBlobGas}, want ${expectedExcessBlobGas}`
)
}
let blobGasPrice
for (const tx of this.transactions) {
if (tx instanceof BlobEIP4844Transaction) {
blobGasPrice = blobGasPrice ?? this.header.getBlobGasPrice()
if (tx.maxFeePerBlobGas < blobGasPrice) {
throw new Error(
`blob transaction maxFeePerBlobGas ${
tx.maxFeePerBlobGas
} < than block blob gas price ${blobGasPrice} - ${this.errorStr()}`
)
}
blobGasUsed += BigInt(tx.blobVersionedHashes.length) * blobGasPerBlob
if (blobGasUsed > blobGasLimit) {
throw new Error(
`tx causes total blob gas of ${blobGasUsed} to exceed maximum blob gas per block of ${blobGasLimit}`
)
}
}
}
if (this.header.blobGasUsed !== blobGasUsed) {
throw new Error(
`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(): boolean {
if (this.uncleHeaders.length === 0) {
return equalsBytes(KECCAK256_RLP_ARRAY, this.header.uncleHash)
}
const uncles = this.uncleHeaders.map((uh) => uh.raw())
const raw = RLP.encode(uncles)
return equalsBytes(this.keccakFunction(raw), this.header.uncleHash)
}
/**
* Validates the withdrawal root
* @returns true if the withdrawals trie root is valid, false otherwise
*/
async withdrawalsTrieIsValid(): Promise<boolean> {
if (!this.common.isActivatedEIP(4895)) {
throw new Error('EIP 4895 is not activated')
}
let result
if (this.withdrawals!.length === 0) {
result = equalsBytes(this.header.withdrawalsRoot!, KECCAK256_RLP)
return result
}
if (this.cache.withdrawalsTrieRoot === undefined) {
this.cache.withdrawalsTrieRoot = await Block.genWithdrawalsTrieRoot(
this.withdrawals!,
new Trie({ common: this.common })
)
}
result = 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 new Error(msg)
}
// Header does not count an uncle twice.
const uncleHashes = this.uncleHeaders.map((header) => bytesToHex(header.hash()))
if (!(new Set(uncleHashes).size === uncleHashes.length)) {
const msg = this._errorMsg('duplicate uncles')
throw new Error(msg)
}
}
/**
* Returns the canonical difficulty for this block.
*
* @param parentBlock - the parent of this `Block`
*/
ethashCanonicalDifficulty(parentBlock: Block): bigint {
return this.header.ethashCanonicalDifficulty(parentBlock.header)
}
/**
* 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: Block) {
return this.header.validateGasLimit(parentBlock.header)
}
/**
* Returns the block in JSON format.
*/
toJSON(): JsonBlock {
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,
requests: this.requests?.map((req) => bytesToHex(req.serialize())),
}
}
/**
* 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(): ExecutionPayload {
const blockJson = this.toJSON()
const header = blockJson.header!
const transactions = this.transactions.map((tx) => bytesToHex(tx.serialize())) ?? []
const withdrawalsArr = blockJson.withdrawals ? { withdrawals: blockJson.withdrawals } : {}
const executionPayload: 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: bytesToHex(this.hash()),
prevRandao: header.mixHash!,
transactions,
...withdrawalsArr,
parentBeaconBlockRoot: header.parentBeaconBlockRoot,
executionWitness: this.executionWitness,
// lets add the request fields first and then iterate over requests to fill them up
depositRequests: this.common.isActivatedEIP(6110) ? [] : undefined,
withdrawalRequests: this.common.isActivatedEIP(7002) ? [] : undefined,
consolidationRequests: this.common.isActivatedEIP(7251) ? [] : undefined,
}
if (this.requests !== undefined) {
for (const request of this.requests) {
switch (request.type) {
case CLRequestType.Deposit:
executionPayload.depositRequests!.push((request as DepositRequest).toJSON())
continue
case CLRequestType.Withdrawal:
executionPayload.withdrawalRequests!.push((request as WithdrawalRequest).toJSON())
continue
case CLRequestType.Consolidation:
executionPayload.consolidationRequests!.push((request as ConsolidationRequest).toJSON())
continue
}
}
} else if (
executionPayload.depositRequests !== undefined ||
executionPayload.withdrawalRequests !== undefined ||
executionPayload.consolidationRequests !== undefined
) {
throw Error(`Undefined requests for activated deposit or withdrawal requests`)
}
return executionPayload
}
/**
* Return a compact error string representation of the object
*/
public errorStr() {
let hash = ''
try {
hash = bytesToHex(this.hash())
} catch (e: any) {
hash = 'error'
}
let hf = ''
try {
hf = this.common.hardfork()
} catch (e: any) {
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
*/
protected _errorMsg(msg: string) {
return `${msg} (${this.errorStr()})`
}
}