@ethereumjs/block
Version:
Provides Block serialization and help functions
527 lines (476 loc) • 17.2 kB
text/typescript
import { ConsensusType } from '@ethereumjs/common'
import { MerklePatriciaTrie } from '@ethereumjs/mpt'
import { RLP } from '@ethereumjs/rlp'
import { Blob4844Tx, Capability } from '@ethereumjs/tx'
import {
BIGINT_0,
EthereumJSErrorWithoutCode,
KECCAK256_RLP,
KECCAK256_RLP_ARRAY,
bytesToHex,
equalsBytes,
} from '@ethereumjs/util'
import { sha256 } from '@noble/hashes/sha2.js'
import { keccak_256 } from '@noble/hashes/sha3.js'
import type { Common } from '@ethereumjs/common'
import type { FeeMarket1559Tx, LegacyTx, TypedTransaction } from '@ethereumjs/tx'
import type { Withdrawal } from '@ethereumjs/util'
/* 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)
import {
BlockHeader,
type createBlock,
type createBlockFromBeaconPayloadJSON,
type createBlockFromBytesArray,
type createBlockFromExecutionPayload,
type createBlockFromJSONRPCProvider,
type createBlockFromRLP,
type createBlockFromRPC,
genTransactionsTrieRoot,
genWithdrawalsTrieRoot,
} from '../index.ts'
/* eslint-enable */
import type { BlockBytes, BlockOptions, ExecutionPayload, JSONBlock } from '../types.ts'
/**
* 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 }
*/
export class Block {
public readonly header: BlockHeader
public readonly transactions: TypedTransaction[] = []
public readonly uncleHeaders: BlockHeader[] = []
public readonly withdrawals?: Withdrawal[]
public readonly common: Common
protected keccakFunction: (msg: Uint8Array) => Uint8Array
protected sha256Function: (msg: Uint8Array) => Uint8Array
protected cache: {
txTrieRoot?: Uint8Array
withdrawalsTrieRoot?: Uint8Array
} = {}
/**
* 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?: BlockHeader,
transactions: TypedTransaction[] = [],
uncleHeaders: BlockHeader[] = [],
withdrawals?: Withdrawal[],
opts: BlockOptions = {},
) {
this.header = header ?? new BlockHeader({}, opts)
this.common = this.header.common
this.keccakFunction = this.common.customCrypto.keccak256 ?? keccak_256
this.sha256Function = this.common.customCrypto.sha256 ?? sha256
this.transactions = transactions
this.withdrawals = withdrawals ?? (this.common.isActivatedEIP(4895) ? [] : undefined)
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 EthereumJSErrorWithoutCode(msg)
}
if (this.common.consensusType() === ConsensusType.ProofOfStake) {
const msg = this._errorMsg(
'Block initialization with uncleHeaders on a PoS network is not allowed',
)
throw EthereumJSErrorWithoutCode(msg)
}
}
if (!this.common.isActivatedEIP(4895) && withdrawals !== undefined) {
throw EthereumJSErrorWithoutCode('Cannot have a withdrawals field if EIP 4895 is not active')
}
const freeze = opts?.freeze ?? true
if (freeze) {
Object.freeze(this)
}
}
/**
* Returns an array of the raw byte arrays for 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)
}
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 genTransactionsTrieRoot(
this.transactions,
new 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(): 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
}
/**
* Validates transaction signatures and minimum gas requirements.
* @returns {string[]} an array of error strings
*/
getTransactionsValidationErrors(): string[] {
const errors: string[] = []
let blobGasUsed = 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(Capability.EIP1559FeeMarket)) {
tx = tx as FeeMarket1559Tx
if (tx.maxFeePerGas < this.header.baseFeePerGas!) {
errs.push('tx unable to pay base fee (EIP-1559 tx)')
}
} else {
tx = tx as LegacyTx
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.getBlobGasSchedule().maxBlobGasPerBlock
const blobGasPerBlob = this.common.param('blobGasPerBlob')
if (tx instanceof 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(): 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
* - Block size limit (EIP-7934)
* @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)
* @param validateBlockSize if set to `true`, will check for block size limit (EIP-7934) (default: false)
*/
async validateData(
onlyHeader: boolean = false,
verifyTxs: boolean = true,
validateBlockSize: boolean = false,
): Promise<void> {
// EIP-7934: RLP Execution Block Size Limit validation
if (validateBlockSize && this.common.isActivatedEIP(7934)) {
const rlpEncoded = this.serialize()
const maxRlpBlockSize = this.common.param('maxRlpBlockSize')
if (rlpEncoded.length > maxRlpBlockSize) {
const msg = this._errorMsg(
`Block size exceeds maximum RLP block size limit: ${rlpEncoded.length} bytes > ${maxRlpBlockSize} bytes`,
)
throw EthereumJSErrorWithoutCode(msg)
}
}
if (verifyTxs) {
const txErrors = this.getTransactionsValidationErrors()
if (txErrors.length > 0) {
const msg = this._errorMsg(`invalid transactions: ${txErrors.join(' ')}`)
throw 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 EthereumJSErrorWithoutCode(msg)
}
}
}
if (!(await this.transactionsTrieIsValid())) {
const msg = this._errorMsg('invalid transaction trie')
throw EthereumJSErrorWithoutCode(msg)
}
if (!this.uncleHashIsValid()) {
const msg = this._errorMsg('invalid uncle hash')
throw EthereumJSErrorWithoutCode(msg)
}
if (this.common.isActivatedEIP(4895) && !(await this.withdrawalsTrieIsValid())) {
const msg = this._errorMsg('invalid withdrawals trie')
throw EthereumJSErrorWithoutCode(msg)
}
}
/**
* 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.getBlobGasSchedule().maxBlobGasPerBlock
const blobGasPerBlob = this.common.param('blobGasPerBlob')
let blobGasUsed = BIGINT_0
const expectedExcessBlobGas = parentHeader.calcNextExcessBlobGas(this.common)
if (this.header.excessBlobGas !== expectedExcessBlobGas) {
throw EthereumJSErrorWithoutCode(
`block excessBlobGas mismatch: have ${this.header.excessBlobGas}, want ${expectedExcessBlobGas}`,
)
}
let blobGasPrice
for (const tx of this.transactions) {
if (tx instanceof Blob4844Tx) {
blobGasPrice = blobGasPrice ?? this.header.getBlobGasPrice()
if (tx.maxFeePerBlobGas < blobGasPrice) {
throw EthereumJSErrorWithoutCode(
`blob transaction maxFeePerBlobGas ${
tx.maxFeePerBlobGas
} < than block blob gas price ${blobGasPrice} - ${this.errorStr()}`,
)
}
blobGasUsed += BigInt(tx.blobVersionedHashes.length) * blobGasPerBlob
if (blobGasUsed > blobGasLimit) {
throw EthereumJSErrorWithoutCode(
`tx causes total blob gas of ${blobGasUsed} to exceed maximum blob gas per block of ${blobGasLimit}`,
)
}
}
}
if (this.header.blobGasUsed !== blobGasUsed) {
throw 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(): 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 EthereumJSErrorWithoutCode('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 genWithdrawalsTrieRoot(
this.withdrawals!,
new MerklePatriciaTrie({ 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 EthereumJSErrorWithoutCode(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 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: 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,
}
}
/**
* 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,
requestsHash: header.requestsHash,
}
return executionPayload
}
/**
* Return a compact error string representation of the object
*/
public errorStr() {
let hash = ''
try {
hash = 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
*/
protected _errorMsg(msg: string) {
return `${msg} (${this.errorStr()})`
}
}