diff --git a/packages/block/src/block/block.ts b/packages/block/src/block/block.ts index b6613a27912..29f75296c48 100644 --- a/packages/block/src/block/block.ts +++ b/packages/block/src/block/block.ts @@ -5,8 +5,11 @@ import { Blob4844Tx, Capability } from '@ethereumjs/tx' import { BIGINT_0, CLRequestType, + ErrorCode, KECCAK256_RLP, KECCAK256_RLP_ARRAY, + UsageError, + ValueError, bytesToHex, equalsBytes, } from '@ethereumjs/util' @@ -133,18 +136,21 @@ export class Block { const msg = this._errorMsg( 'Block initialization with uncleHeaders on a PoA network is not allowed', ) - throw new Error(msg) + throw new UsageError(msg, ErrorCode.INVALID_OPTION_USAGE) } 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) + throw new UsageError(msg, ErrorCode.INVALID_OPTION_USAGE) } } if (!this.common.isActivatedEIP(4895) && withdrawals !== undefined) { - throw new Error('Cannot have a withdrawals field if EIP 4895 is not active') + throw new UsageError( + 'Cannot have a withdrawals field if EIP 4895 is not active', + ErrorCode.EIP_NOT_ACTIVATED, + ) } if ( @@ -152,11 +158,17 @@ export class Block { executionWitness !== undefined && executionWitness !== null ) { - throw new Error(`Cannot have executionWitness field if EIP 6800 is not active `) + throw new UsageError( + `Cannot have executionWitness field if EIP 6800 is not active `, + ErrorCode.EIP_NOT_ACTIVATED, + ) } if (!this.common.isActivatedEIP(7685) && requests !== undefined) { - throw new Error(`Cannot have requests field if EIP 7685 is not active`) + throw new UsageError( + `Cannot have requests field if EIP 7685 is not active`, + ErrorCode.EIP_NOT_ACTIVATED, + ) } // Requests should be sorted in monotonically ascending order based on type @@ -164,7 +176,10 @@ export class Block { 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') + throw new ValueError( + 'requests are not sorted in ascending order', + ErrorCode.INVALID_VALUE, + ) } } const freeze = opts?.freeze ?? true @@ -250,7 +265,7 @@ export class Block { async requestsTrieIsValid(requestsInput?: CLRequest[]): Promise { if (!this.common.isActivatedEIP(7685)) { - throw new Error('EIP 7685 is not activated') + throw new UsageError('EIP 7685 is not activated', ErrorCode.EIP_NOT_ACTIVATED) } const requests = requestsInput ?? this.requests! @@ -459,7 +474,7 @@ export class Block { */ async withdrawalsTrieIsValid(): Promise { if (!this.common.isActivatedEIP(4895)) { - throw new Error('EIP 4895 is not activated') + throw new UsageError('EIP 4895 is not activated', ErrorCode.EIP_NOT_ACTIVATED) } let result diff --git a/packages/block/src/consensus/clique.ts b/packages/block/src/consensus/clique.ts index 3a89a463192..7b670ae1cb8 100644 --- a/packages/block/src/consensus/clique.ts +++ b/packages/block/src/consensus/clique.ts @@ -4,6 +4,8 @@ import { Address, BIGINT_0, BIGINT_27, + ErrorCode, + UsageError, bigIntToBytes, bytesToBigInt, concatBytes, @@ -28,7 +30,7 @@ export function requireClique(header: BlockHeader, name: string) { const msg = header['_errorMsg']( `BlockHeader.${name}() call only supported for clique PoA networks`, ) - throw new Error(msg) + throw new UsageError(msg, ErrorCode.INVALID_METHOD_CALL) } } @@ -84,7 +86,7 @@ export function cliqueEpochTransitionSigners(header: BlockHeader): Address[] { requireClique(header, 'cliqueEpochTransitionSigners') if (!cliqueIsEpochTransition(header)) { const msg = header['_errorMsg']('Signers are only included in epoch transition blocks (clique)') - throw new Error(msg) + throw new UsageError(msg, ErrorCode.INVALID_METHOD_CALL) } const start = CLIQUE_EXTRA_VANITY diff --git a/packages/block/src/header/constructors.ts b/packages/block/src/header/constructors.ts index 97222b970d5..826e5d1a87b 100644 --- a/packages/block/src/header/constructors.ts +++ b/packages/block/src/header/constructors.ts @@ -1,5 +1,5 @@ import { RLP } from '@ethereumjs/rlp' -import { bigIntToBytes, equalsBytes } from '@ethereumjs/util' +import { ErrorCode, ValueError, bigIntToBytes, equalsBytes } from '@ethereumjs/util' import { generateCliqueBlockExtraData } from '../consensus/clique.js' import { numberToHex, valuesArrayToHeaderData } from '../helpers.js' @@ -34,22 +34,34 @@ export function createBlockHeaderFromBytesArray(values: BlockHeaderBytes, opts: eip1559ActivationBlock !== undefined && equalsBytes(eip1559ActivationBlock, number as Uint8Array) ) { - throw new Error('invalid header. baseFeePerGas should be provided') + throw new ValueError( + 'invalid header. baseFeePerGas should be provided', + ErrorCode.INVALID_VALUE, + ) } } if (header.common.isActivatedEIP(4844)) { if (excessBlobGas === undefined) { - throw new Error('invalid header. excessBlobGas should be provided') + throw new ValueError( + 'invalid header. excessBlobGas should be provided', + ErrorCode.INVALID_VALUE, + ) } else if (blobGasUsed === undefined) { - throw new Error('invalid header. blobGasUsed should be provided') + throw new ValueError( + 'invalid header. blobGasUsed should be provided', + ErrorCode.INVALID_VALUE, + ) } } if (header.common.isActivatedEIP(4788) && parentBeaconBlockRoot === undefined) { - throw new Error('invalid header. parentBeaconBlockRoot should be provided') + throw new ValueError( + 'invalid header. parentBeaconBlockRoot should be provided', + ErrorCode.INVALID_VALUE, + ) } if (header.common.isActivatedEIP(7685) && requestsRoot === undefined) { - throw new Error('invalid header. requestsRoot should be provided') + throw new ValueError('invalid header. requestsRoot should be provided', ErrorCode.INVALID_VALUE) } return header } @@ -66,7 +78,7 @@ export function createBlockHeaderFromRLP( ) { const values = RLP.decode(serializedHeaderData) if (!Array.isArray(values)) { - throw new Error('Invalid serialized header input. Must be array') + throw new ValueError('Invalid serialized header input. Must be array', ErrorCode.INVALID_VALUE) } return createBlockHeaderFromBytesArray(values as Uint8Array[], opts) } diff --git a/packages/block/src/header/header.ts b/packages/block/src/header/header.ts index 79675dafedd..b8461ee3080 100644 --- a/packages/block/src/header/header.ts +++ b/packages/block/src/header/header.ts @@ -6,9 +6,12 @@ import { BIGINT_1, BIGINT_2, BIGINT_7, + ErrorCode, KECCAK256_RLP, KECCAK256_RLP_ARRAY, TypeOutput, + UsageError, + ValueError, bigIntToHex, bigIntToUnpaddedBytes, bytesToHex, @@ -75,10 +78,13 @@ export class BlockHeader { */ get prevRandao() { if (!this.common.isActivatedEIP(4399)) { - const msg = this._errorMsg( + throw new UsageError( 'The prevRandao parameter can only be accessed when EIP-4399 is activated', + ErrorCode.EIP_NOT_ACTIVATED, + { + objectContext: this.errorStr(), + }, ) - throw new Error(msg) } return this.mixHash } @@ -179,7 +185,10 @@ export class BlockHeader { toType(headerData.requestsRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.requestsRoot if (!this.common.isActivatedEIP(1559) && baseFeePerGas !== undefined) { - throw new Error('A base fee for a block can only be set with EIP1559 being activated') + throw new UsageError( + 'A base fee for a block can only be set with EIP1559 being activated', + ErrorCode.EIP_NOT_ACTIVATED, + ) } if (!this.common.isActivatedEIP(4895) && withdrawalsRoot !== undefined) { @@ -258,33 +267,41 @@ export class BlockHeader { const { parentHash, stateRoot, transactionsTrie, receiptTrie, mixHash, nonce } = this if (parentHash.length !== 32) { - const msg = this._errorMsg(`parentHash must be 32 bytes, received ${parentHash.length} bytes`) - throw new Error(msg) + throw new ValueError(`parentHash must be 32 bytes`, ErrorCode.INVALID_VALUE_LENGTH, { + objectContext: this.errorStr(), + received: `${parentHash.length} bytes`, + }) } if (stateRoot.length !== 32) { - const msg = this._errorMsg(`stateRoot must be 32 bytes, received ${stateRoot.length} bytes`) - throw new Error(msg) + throw new ValueError(`stateRoot must be 32 bytes`, ErrorCode.INVALID_VALUE_LENGTH, { + objectContext: this.errorStr(), + received: `${stateRoot.length} bytes`, + }) } if (transactionsTrie.length !== 32) { - const msg = this._errorMsg( - `transactionsTrie must be 32 bytes, received ${transactionsTrie.length} bytes`, - ) - throw new Error(msg) + throw new ValueError('transactionsTrie must be 32 bytes', ErrorCode.INVALID_VALUE_LENGTH, { + objectContext: this.errorStr(), + received: `${bytesToHex(transactionsTrie)} (${transactionsTrie.length} bytes)`, + }) } if (receiptTrie.length !== 32) { - const msg = this._errorMsg( - `receiptTrie must be 32 bytes, received ${receiptTrie.length} bytes`, - ) - throw new Error(msg) + throw new ValueError('receiptTrie must be 32 bytes', ErrorCode.INVALID_VALUE_LENGTH, { + objectContext: this.errorStr(), + received: `${bytesToHex(receiptTrie)} (${receiptTrie.length} bytes)`, + }) } if (mixHash.length !== 32) { - const msg = this._errorMsg(`mixHash must be 32 bytes, received ${mixHash.length} bytes`) - throw new Error(msg) + throw new ValueError('mixHash must be 32 bytes', ErrorCode.INVALID_VALUE_LENGTH, { + objectContext: this.errorStr(), + received: `${bytesToHex(mixHash)} (${mixHash.length} bytes)`, + }) } if (nonce.length !== 8) { - const msg = this._errorMsg(`nonce must be 8 bytes, received ${nonce.length} bytes`) - throw new Error(msg) + throw new ValueError('nonce must be 8 bytes', ErrorCode.INVALID_VALUE_LENGTH, { + objectContext: this.errorStr(), + received: `${bytesToHex(nonce)} (${nonce.length} bytes)`, + }) } // check if the block used too much gas @@ -429,8 +446,9 @@ export class BlockHeader { } } if (error) { - const msg = this._errorMsg(`Invalid PoS block: ${errorMsg}`) - throw new Error(msg) + throw new ValueError(`Invalid PoS block${errorMsg}`, ErrorCode.INVALID_OBJECT, { + objectContext: this.errorStr(), + }) } } } @@ -659,14 +677,22 @@ export class BlockHeader { */ ethashCanonicalDifficulty(parentBlockHeader: BlockHeader): bigint { if (this.common.consensusType() !== ConsensusType.ProofOfWork) { - const msg = this._errorMsg('difficulty calculation is only supported on PoW chains') - throw new Error(msg) + throw new UsageError( + 'difficulty calculation is only supported on PoW chains', + ErrorCode.INVALID_METHOD_CALL, + { + objectContext: this.errorStr(), + }, + ) } if (this.common.consensusAlgorithm() !== ConsensusAlgorithm.Ethash) { - const msg = this._errorMsg( + throw new UsageError( 'difficulty calculation currently only supports the ethash algorithm', + ErrorCode.INVALID_METHOD_CALL, + { + objectContext: this.errorStr(), + }, ) - throw new Error(msg) } const blockTs = this.timestamp const { timestamp: parentTs, difficulty: parentDif } = parentBlockHeader diff --git a/packages/util/src/errors.ts b/packages/util/src/errors.ts new file mode 100644 index 00000000000..1eba2d31dc9 --- /dev/null +++ b/packages/util/src/errors.ts @@ -0,0 +1,92 @@ +/** + * Generic EthereumJS error class with metadata attached + * + * Kudos to https://github.com/ChainSafe/lodestar monorepo + * for the inspiration :-) + */ +type EthereumJSErrorType = { + objectContext: string +} + +export class EthereumJSError extends Error { + code: string + context: T | {} + + constructor(msg: string, code: string, context?: T) { + super(msg) + this.code = code + this.context = context ?? {} + } + + getContext(): Record { + return { code: this.code, ...this.context } + } + + /** + * Get the metadata and the stacktrace for the error. + */ + toObject(): Record { + return { + ...this.getContext(), + stack: this.stack ?? '', + } + } +} + +/** + * Error Codes + */ +export enum ErrorCode { + // Value Errors + INVALID_OBJECT = 'INVALID_OBJECT', + INVALID_VALUE = 'INVALID_VALUE', + INVALID_VALUE_LENGTH = 'INVALID_VALUE_LENGTH', + TOO_FEW_VALUES = 'TOO_FEW_VALUES', + TOO_MANY_VALUES = 'TOO_MANY_VALUES', + + // Usage Errors + EIP_NOT_ACTIVATED = 'EIP_NOT_ACTIVATED', + INCOMPATIBLE_LIBRARY_VERSION = 'INCOMPATIBLE_LIBRARY_VERSION', + INVALID_OPTION_USAGE = 'INVALID_OPTION_USAGE', + INVALID_METHOD_CALL = 'INVALID_METHOD_CALL', +} + +/** + * Generic error types + */ + +/** + * Type flavors for ValueError + */ +export type ValueErrorType = + | { + received: string + } + | { + objectContext: string + received: string + } + | EthereumJSErrorType + +/** + * Type flavors for UsageError + */ +export type UsageErrorType = EthereumJSErrorType + +/** + * Generic Errors + * + * Use directly or in a subclassed context for error comparison (`e instanceof ValueError`) + */ + +/** + * Error along Object Value + */ +export class ValueError extends EthereumJSError {} + +/** + * Error along API Usage + * + * Use directly or in a subclassed context for error comparison (`e instanceof UsageError`) + */ +export class UsageError extends EthereumJSError {} diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index d1f6dd95e73..2776c03d037 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -28,6 +28,11 @@ export * from './db.js' */ export * from './withdrawal.js' +/** + * EthereumJS Extended Errors + */ +export * from './errors.js' + /** * ECDSA signature */