From 3866a3a6dbc769002105ae9d19d142577979a55e Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 26 Mar 2024 21:19:39 +0100 Subject: [PATCH] monorepo: work on eip7002 exits --- packages/block/src/block.ts | 65 +++++++++++++++++++++++++--- packages/block/src/from-rpc.ts | 9 +++- packages/block/src/header.ts | 34 ++++++++++++++- packages/block/src/types.ts | 29 +++++++++++-- packages/common/src/eips.ts | 13 ++++++ packages/util/src/exit.ts | 78 ++++++++++++++++++++++++++++++++++ packages/util/src/index.ts | 4 ++ 7 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 packages/util/src/exit.ts diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index e9e20fdc06..f0e435973d 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -5,6 +5,7 @@ import { BlobEIP4844Transaction, Capability, TransactionFactory } from '@ethereu import { BIGINT_0, Deposit, + Exit, KECCAK256_RLP, Withdrawal, bigIntToHex, @@ -41,7 +42,7 @@ import type { TxOptions, TypedTransaction, } from '@ethereumjs/tx' -import type { DepositBytes, EthersProvider, WithdrawalBytes } from '@ethereumjs/util' +import type { DepositBytes, EthersProvider, ExitBytes, WithdrawalBytes } from '@ethereumjs/util' /** * An object that represents the block. @@ -52,6 +53,7 @@ export class Block { public readonly uncleHeaders: BlockHeader[] = [] public readonly withdrawals?: Withdrawal[] public readonly deposits?: Deposit[] + public readonly exits?: Exit[] public readonly common: Common protected keccakFunction: (msg: Uint8Array) => Uint8Array @@ -112,6 +114,7 @@ export class Block { transactions: txsData, uncleHeaders: uhsData, withdrawals: withdrawalsData, + exits: exitData, executionWitness: executionWitnessData, deposits: depositData, } = blockData @@ -151,6 +154,7 @@ export class Block { // The witness data is planned to come in rlp serialized bytes so leave this // stub till that time const executionWitness = executionWitnessData + const exits = exitData?.map(Exit.fromExitData) const deposits = depositData?.map(Deposit.fromDepositData) @@ -161,7 +165,8 @@ export class Block { withdrawals, opts, executionWitness, - deposits + deposits, + exits ) } @@ -196,8 +201,15 @@ export class Block { // 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, withdrawalBytes, depositBytes, executionWitnessBytes] = - values + const [ + headerData, + txsData, + uhsData, + withdrawalBytes, + depositBytes, + exitBytes, + executionWitnessBytes, + ] = values const header = BlockHeader.fromValuesArray(headerData, opts) if ( @@ -217,6 +229,14 @@ export class Block { 'Invalid serialized block input: EIP-6110 is active, and no deposits were provided as array' ) } + if ( + header.common.isActivatedEIP(7002) && + (exitBytes === undefined || !Array.isArray(exitBytes)) + ) { + throw new Error( + 'Invalid serialized block input: EIP-7002 is active, and no exits were provided as array' + ) + } // parse transactions const transactions = [] @@ -267,6 +287,16 @@ export class Block { })) ?.map(Deposit.fromDepositData) : undefined + // TODO fix my type, also figure out why we cannot parse this the same as WithdrawalBytes + // @ts-ignore + const exits = header.common.isActivatedEIP(7002) + ? (exitBytes as ExitBytes[]) + ?.map(([address, validatorPubkey]) => ({ + address, + validatorPubkey, + })) + ?.map(Exit.fromExitData) + : undefined // executionWitness are not part of the EL fetched blocks via eth_ bodies method // they are currently only available via the engine api constructed blocks @@ -290,7 +320,8 @@ export class Block { withdrawals, opts, executionWitness, - deposits + deposits, + exits ) } @@ -464,7 +495,8 @@ export class Block { withdrawals?: Withdrawal[], opts: BlockOptions = {}, executionWitness?: VerkleExecutionWitness | null, - deposits?: Deposit[] + deposits?: Deposit[], + exits?: Exit[] ) { this.header = header ?? BlockHeader.fromHeaderData({}, opts) this.common = this.header.common @@ -473,6 +505,7 @@ export class Block { this.transactions = transactions this.withdrawals = withdrawals ?? (this.common.isActivatedEIP(4895) ? [] : undefined) this.deposits = deposits ?? (this.common.isActivatedEIP(6110) ? [] : undefined) + this.exits = exits ?? (this.common.isActivatedEIP(7002) ? [] : 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 @@ -526,6 +559,10 @@ export class Block { throw new Error(`Cannot have executionWitness field if EIP 6800 is not active `) } + if (!this.common.isActivatedEIP(7002) && exits !== undefined && exits !== null) { + throw new Error(`Cannot have exits field if EIP 7002 is not active`) + } + const freeze = opts?.freeze ?? true if (freeze) { Object.freeze(this) @@ -551,6 +588,10 @@ export class Block { if (depositsRaw) { bytesArray.push(depositsRaw) } + const exitsRaw = this.exits?.map((ex) => ex.raw()) + if (exitsRaw) { + bytesArray.push(exitsRaw) + } if (this.executionWitness !== undefined && this.executionWitness !== null) { const executionWitnessBytes = RLP.encode(JSON.stringify(this.executionWitness)) bytesArray.push(executionWitnessBytes as any) @@ -724,6 +765,12 @@ export class Block { throw new Error(`Invalid block: ethereumjs stateless client needs executionWitness`) } } + + if (this.common.isActivatedEIP(7002)) { + if (this.exits === undefined || this.exits === null) { + throw new Error('Invalid block: missing exits') + } + } } /** @@ -873,12 +920,18 @@ export class Block { deposits: this.deposits.map((deposit) => deposit.toJSON()), } : {} + const exitsAttr = this.exits + ? { + exits: this.exits.map((exit) => exit.toJSON()), + } + : {} return { header: this.header.toJSON(), transactions: this.transactions.map((tx) => tx.toJSON()), uncleHeaders: this.uncleHeaders.map((uh) => uh.toJSON()), ...withdrawalsAttr, ...depositsAttr, + ...exitsAttr, } } diff --git a/packages/block/src/from-rpc.ts b/packages/block/src/from-rpc.ts index 09ddc720ff..22a1d02023 100644 --- a/packages/block/src/from-rpc.ts +++ b/packages/block/src/from-rpc.ts @@ -55,7 +55,14 @@ export function blockFromRpc( const uncleHeaders = uncles.map((uh) => blockHeaderFromRpc(uh, options)) return Block.fromBlockData( - { header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals }, + { + header, + transactions, + uncleHeaders, + withdrawals: blockParams.withdrawals, + deposits: blockParams.deposits, + exits: blockParams.exits, + }, options ) } diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index a14c72481d..29cf45531c 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -64,6 +64,7 @@ export class BlockHeader { public readonly blobGasUsed?: bigint public readonly excessBlobGas?: bigint public readonly parentBeaconBlockRoot?: Uint8Array + public readonly exitsRoot?: Uint8Array public readonly common: Common @@ -118,7 +119,8 @@ export class BlockHeader { */ public static fromValuesArray(values: BlockHeaderBytes, opts: BlockOptions = {}) { const headerData = valuesArrayToHeaderData(values) - const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot } = headerData + const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot, exitsRoot } = + headerData const header = BlockHeader.fromHeaderData(headerData, opts) // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (header.common.isActivatedEIP(1559) && baseFeePerGas === undefined) { @@ -139,6 +141,9 @@ export class BlockHeader { if (header.common.isActivatedEIP(4788) && parentBeaconBlockRoot === undefined) { throw new Error('invalid header. parentBeaconBlockRoot should be provided') } + if (header.common.isActivatedEIP(7002) && exitsRoot === undefined) { + throw new Error('invalid header. exitsRoot should be provided') + } return header } /** @@ -224,6 +229,7 @@ export class BlockHeader { blobGasUsed: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined, excessBlobGas: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined, parentBeaconBlockRoot: this.common.isActivatedEIP(4788) ? zeros(32) : undefined, + exitsRoot: this.common.isActivatedEIP(7002) ? zeros(32) : undefined, } const baseFeePerGas = @@ -239,6 +245,8 @@ export class BlockHeader { const parentBeaconBlockRoot = toType(headerData.parentBeaconBlockRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.parentBeaconBlockRoot + const exitsRoot = + toType(headerData.exitsRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.exitsRoot if (!this.common.isActivatedEIP(1559) && baseFeePerGas !== undefined) { throw new Error('A base fee for a block can only be set with EIP1559 being activated') @@ -272,6 +280,10 @@ export class BlockHeader { ) } + if (!this.common.isActivatedEIP(7200) && exitsRoot !== undefined) { + throw new Error('A exitsRoot for a header can only be provided with EIP7002 being activated') + } + this.parentHash = parentHash this.uncleHash = uncleHash this.coinbase = coinbase @@ -293,6 +305,7 @@ export class BlockHeader { this.blobGasUsed = blobGasUsed this.excessBlobGas = excessBlobGas this.parentBeaconBlockRoot = parentBeaconBlockRoot + this.exitsRoot = exitsRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -431,6 +444,19 @@ export class BlockHeader { throw new Error(msg) } } + + if (this.common.isActivatedEIP(7002) === true) { + if (this.exitsRoot === undefined) { + const msg = this._errorMsg('EIP7002 has no exitsRoot field') + throw new Error(msg) + } + if (this.exitsRoot.length !== 32) { + const msg = this._errorMsg( + `exitsRoot must be 32 bytes, received ${this.exitsRoot.length} bytes` + ) + throw new Error(msg) + } + } } /** @@ -721,6 +747,9 @@ export class BlockHeader { if (this.common.isActivatedEIP(4788) === true) { rawItems.push(this.parentBeaconBlockRoot!) } + if (this.common.isActivatedEIP(7002)) { + rawItems.push(this.exitsRoot!) + } return rawItems } @@ -990,6 +1019,9 @@ export class BlockHeader { if (this.common.isActivatedEIP(4788) === true) { jsonDict.parentBeaconBlockRoot = bytesToHex(this.parentBeaconBlockRoot!) } + if (this.common.isActivatedEIP(7002)) { + jsonDict.exitsRoot = bytesToHex(this.exitsRoot!) + } return jsonDict } diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 85331f3bf3..5fe5d05896 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -7,7 +7,10 @@ import type { BytesLike, DepositBytes, DepositData, + ExitBytes, + ExitData, JsonRpcDeposit, + JsonRpcExit, JsonRpcWithdrawal, PrefixedHexString, WithdrawalBytes, @@ -140,6 +143,7 @@ export interface HeaderData { blobGasUsed?: BigIntLike excessBlobGas?: BigIntLike parentBeaconBlockRoot?: BytesLike + exitsRoot?: BytesLike } /** @@ -161,10 +165,15 @@ export interface BlockData { * EIP-6100: Deposits in EL (experimental) */ deposits?: Array + /** + * EIP-7002: Exits in EL (experimental) + */ + exits?: Array } export type WithdrawalsBytes = WithdrawalBytes[] export type DepositsBytes = DepositBytes[] +export type ExitsBytes = ExitBytes[] export type ExecutionWitnessBytes = Uint8Array export type BlockBytes = @@ -175,16 +184,26 @@ export type BlockBytes = TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes, - ExecutionWitnessBytes + DepositsBytes, + ExitsBytes + // The type here is the BlockBytes without 6800, but with 6110 + 7002 ] - | [BlockHeaderBytes, TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes, DepositsBytes] | [ BlockHeaderBytes, TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes, DepositsBytes, - ExecutionWitnessBytes + ExitBytes, + ExecutionWitnessBytes // This includes 6800 with 6110 + 7002 + ] + | [BlockHeaderBytes, TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes, DepositsBytes] + | [ + BlockHeaderBytes, + TransactionsBytes, + UncleHeadersBytes, + WithdrawalsBytes, + ExecutionWitnessBytes // This only includes 6800 sso without 6110 + 7002 ] /** @@ -215,6 +234,7 @@ export interface JsonBlock { uncleHeaders?: JsonHeader[] withdrawals?: JsonRpcWithdrawal[] deposits?: JsonRpcDeposit[] + exits?: JsonRpcExit[] executionWitness?: VerkleExecutionWitness | null } @@ -243,6 +263,7 @@ export interface JsonHeader { blobGasUsed?: string excessBlobGas?: string parentBeaconBlockRoot?: string + exitsRoot?: string } /* @@ -274,6 +295,8 @@ export interface JsonRpcBlock { withdrawalsRoot?: string // If EIP-4895 is enabled for this block, the root of the withdrawal trie of the block. deposits?: Array // If EIP-6110 is enabled for this block, array of deposits depositsRoot?: string // If EIP-6110 is enabled for this block, the root of the deposit trie of the block. + exits?: Array // If EIP-7002 is enabled for this block, array of exits + exitsRoot?: string // If EIP-7002 is enabled for this block, the root of the exits trie of the block. blobGasUsed?: string // If EIP-4844 is enabled for this block, returns the blob gas used for the block excessBlobGas?: string // If EIP-4844 is enabled for this block, returns the excess blob gas for the block parentBeaconBlockRoot?: string // If EIP-4788 is enabled for this block, returns parent beacon block root diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 27189f7a81..b7ae2aa01f 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -517,6 +517,19 @@ export const EIPs: EIPsDict = { }, }, }, + 7002: { + comment: 'Execution layer triggerable exits (experimental)', + url: 'https://github.com/ethereum/EIPs/commit/35589d35c40576d932923542b31a4d8f7812c3e7', + status: Status.Draft, + minimumHardfork: Hardfork.Paris, + requiredEIPs: [], + gasPrices: { + validatorExcessAddress: { + v: BigInt('0x0f1ee3e66777F27a7703400644C6fCE41527E017'), + d: 'Address of the validator excess address', + }, + }, + }, 7516: { comment: 'BLOBBASEFEE opcode', url: 'https://eips.ethereum.org/EIPS/eip-7516', diff --git a/packages/util/src/exit.ts b/packages/util/src/exit.ts new file mode 100644 index 0000000000..8d44d19180 --- /dev/null +++ b/packages/util/src/exit.ts @@ -0,0 +1,78 @@ +import { bytesToHex } from './bytes.js' +import { TypeOutput, toType } from './types.js' + +import type { BytesLike, PrefixedHexString } from './types.js' + +/** + * Flexible input data type for EIP-4895 withdrawal data with amount in Gwei to + * match CL representation and for eventual ssz withdrawalsRoot + */ +export type ExitData = { + address: BytesLike + validatorPubkey: BytesLike +} + +/** + * JSON RPC interface for EIP-7002 exit data + */ +export interface JsonRpcExit { + address: PrefixedHexString // 20 bytes + validatorPubkey: PrefixedHexString // 48 bytes +} + +export type ExitBytes = [Uint8Array, Uint8Array] + +/** + * Representation of EIP-7002 exit data + */ +export class Exit { + constructor(public readonly address: Uint8Array, public readonly validatorPubkey: Uint8Array) {} + + public static fromExitData(exitData: ExitData) { + const { address, validatorPubkey } = exitData + + const vPubKey = toType(validatorPubkey, TypeOutput.Uint8Array) + const addr = toType(address, TypeOutput.Uint8Array) + + return new Exit(addr, vPubKey) + } + + public static fromValuesArray(exitArray: ExitBytes) { + if (exitArray.length !== 2) { + throw Error(`Invalid exitArray length expected=2 actual=${exitArray.length}`) + } + const [address, validatorPubkey] = exitArray + return Exit.fromExitData({ validatorPubkey, address }) + } + + /** + * Convert a withdrawal to a buffer array + * @param withdrawal the withdrawal to convert + * @returns buffer array of the withdrawal + */ + public static toBytesArray(exit: Exit | ExitData): ExitBytes { + const { validatorPubkey, address } = exit + const validatorPubkeyBytes = toType(validatorPubkey, TypeOutput.Uint8Array) + const addressBytes = toType(address, TypeOutput.Uint8Array) + + return [addressBytes, validatorPubkeyBytes] + } + + raw() { + return Exit.toBytesArray(this) + } + + toValue() { + return { + address: this.address, + validatorPubkey: this.validatorPubkey, + } + } + + toJSON() { + return { + address: this.address.toString(), + validatorPubkey: bytesToHex(this.validatorPubkey), + } + } +} diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index dd35dccdd8..d4c678f215 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -32,6 +32,10 @@ export * from './withdrawal.js' * Deposit type */ export * from './deposits.js' +/* + * Exit type + */ +export * from './exit.js' /** * ECDSA signature