diff --git a/package-lock.json b/package-lock.json index a2f61344cf..efb4935d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -548,6 +548,74 @@ "tough-cookie": "^4.1.4" } }, + "node_modules/@chainsafe/as-sha256": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.4.2.tgz", + "integrity": "sha512-HJ8GZBRjLeWtRsAXf3EbNsNzmTGpzTFjfpSf4yHkLYC+E52DhT6hwz+7qpj6I/EmFzSUm5tYYvT9K8GZokLQCQ==", + "license": "Apache-2.0" + }, + "node_modules/@chainsafe/hashtree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@chainsafe/hashtree/-/hashtree-1.0.1.tgz", + "integrity": "sha512-bleu9FjqBeR/l6W1u2Lz+HsS0b0LLJX2eUt3hOPBN7VqOhidx8wzkVh2S7YurS+iTQtfdK4K5QU9tcTGNrGwDg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "@chainsafe/hashtree-darwin-arm64": "1.0.1", + "@chainsafe/hashtree-linux-arm64-gnu": "1.0.1", + "@chainsafe/hashtree-linux-x64-gnu": "1.0.1" + } + }, + "node_modules/@chainsafe/hashtree-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@chainsafe/hashtree-darwin-arm64/-/hashtree-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-+KmEgQMpO7FDL3klAcpXbQ4DPZvfCe0qSaBBrtT4vLF8V1JGm3sp+j7oibtxtOsLKz7nJMiK1pZExi7vjXu8og==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/hashtree-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@chainsafe/hashtree-linux-arm64-gnu/-/hashtree-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-p1hnhGq2aFY+Zhdn1Q6L/6yLYNKjqXfn/Pc8jiM0e3+Lf/hB+yCdqYVu1pto26BrZjugCFZfupHaL4DjUTDttw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/hashtree-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@chainsafe/hashtree-linux-x64-gnu/-/hashtree-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-uCIGuUWuWV0LiB4KLMy6JFa7Jp6NmPl3hKF5BYWu8TzUBe7vSXMZfqTzGxXPggFYN2/0KymfRdG9iDCOJfGRqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@chainsafe/is-ip": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@chainsafe/is-ip/-/is-ip-2.0.2.tgz", @@ -561,6 +629,43 @@ "@chainsafe/is-ip": "^2.0.1" } }, + "node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.7.2.tgz", + "integrity": "sha512-BUAqrmSUmy6bZhXxnhpR+aYoEDdCeS1dQvq/aje0CDEB14ZHF9UVN2mL9MolOD0ANUiP1OaPG3KfVBxvuW8aTg==", + "license": "Apache-2.0", + "dependencies": { + "@chainsafe/as-sha256": "^0.4.2", + "@noble/hashes": "^1.3.0" + } + }, + "node_modules/@chainsafe/ssz": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.18.0.tgz", + "integrity": "sha512-1ikTjk3JK6+fsGWiT5IvQU0AP6gF3fDzGmPfkKthbcbgTUR8fjB83Ywp9ko/ZoiDGfrSFkATgT4hvRzclu0IAA==", + "license": "Apache-2.0", + "dependencies": { + "@chainsafe/as-sha256": "0.5.0", + "@chainsafe/persistent-merkle-tree": "0.8.0" + } + }, + "node_modules/@chainsafe/ssz/node_modules/@chainsafe/as-sha256": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.5.0.tgz", + "integrity": "sha512-dTIY6oUZNdC5yDTVP5Qc9hAlKAsn0QTQ2DnQvvsbTnKSTbYs3p5RPN0aIUqN0liXei/9h24c7V0dkV44cnWIQA==", + "license": "Apache-2.0" + }, + "node_modules/@chainsafe/ssz/node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.8.0.tgz", + "integrity": "sha512-hh6C1JO6SKlr0QGNTNtTLqgGVMA/Bc20wD6CeMHp+wqbFKCULRJuBUxhF4WDx/7mX8QlqF3nFriF/Eo8oYJ4/A==", + "license": "Apache-2.0", + "dependencies": { + "@chainsafe/as-sha256": "0.5.0", + "@chainsafe/hashtree": "1.0.1", + "@noble/hashes": "^1.3.0" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -17244,6 +17349,7 @@ "version": "5.3.0", "license": "MPL-2.0", "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/common": "^4.4.0", "@ethereumjs/mpt": "^6.2.2", "@ethereumjs/rlp": "^5.0.2", @@ -17738,6 +17844,7 @@ "version": "5.4.0", "license": "MPL-2.0", "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/common": "^4.4.0", "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/util": "^9.1.0", @@ -17760,6 +17867,8 @@ "version": "9.1.0", "license": "MPL-2.0", "dependencies": { + "@chainsafe/persistent-merkle-tree": "^0.7.2", + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/rlp": "^5.0.2", "ethereum-cryptography": "^3.0.0" }, @@ -17801,6 +17910,7 @@ "version": "8.1.0", "license": "MPL-2.0", "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/block": "^5.3.0", "@ethereumjs/common": "^4.4.0", "@ethereumjs/evm": "^3.1.0", diff --git a/packages/block/package.json b/packages/block/package.json index e4eec3634a..5b607d079e 100644 --- a/packages/block/package.json +++ b/packages/block/package.json @@ -47,6 +47,7 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/common": "^4.4.0", "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/mpt": "^6.2.2", diff --git a/packages/block/src/block/block.ts b/packages/block/src/block/block.ts index f1867d6849..8c10acfd9a 100644 --- a/packages/block/src/block/block.ts +++ b/packages/block/src/block/block.ts @@ -18,6 +18,7 @@ import { keccak256 } from 'ethereum-cryptography/keccak.js' // 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 { genTransactionsSszRoot, genWithdrawalsSszRoot } from '../helpers.js' import { genRequestsTrieRoot, genTransactionsTrieRoot, @@ -226,10 +227,9 @@ export class Block { * Generates transaction trie for validation. */ async genTxTrie(): Promise { - return genTransactionsTrieRoot( - this.transactions, - new MerklePatriciaTrie({ common: this.common }), - ) + return this.common.isActivatedEIP(6493) + ? genTransactionsSszRoot(this.transactions) + : genTransactionsTrieRoot(this.transactions, new MerklePatriciaTrie({ common: this.common })) } /** @@ -238,16 +238,10 @@ export class Block { * @returns True if the transaction trie is valid, false otherwise */ async transactionsTrieIsValid(): Promise { - 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) + const result = equalsBytes(this.cache.txTrieRoot, this.header.transactionsTrie) return result } @@ -367,7 +361,9 @@ export class Block { } if (!(await this.transactionsTrieIsValid())) { - const msg = this._errorMsg('invalid transaction trie') + const msg = this._errorMsg( + `invalid transaction trie expected=${bytesToHex(this.cache.txTrieRoot!)}`, + ) throw new Error(msg) } @@ -456,6 +452,12 @@ export class Block { return equalsBytes(this.keccakFunction(raw), this.header.uncleHash) } + async genWithdrawalsTrie(): Promise { + return this.common.isActivatedEIP(6493) + ? genWithdrawalsSszRoot(this.withdrawals!) + : genWithdrawalsTrieRoot(this.withdrawals!, new MerklePatriciaTrie({ common: this.common })) + } + /** * Validates the withdrawal root * @returns true if the withdrawals trie root is valid, false otherwise @@ -465,19 +467,10 @@ export class Block { 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 genWithdrawalsTrieRoot( - this.withdrawals!, - new MerklePatriciaTrie({ common: this.common }), - ) + this.cache.withdrawalsTrieRoot = await this.genWithdrawalsTrie() } - result = equalsBytes(this.cache.withdrawalsTrieRoot, this.header.withdrawalsRoot!) + const result = equalsBytes(this.cache.withdrawalsTrieRoot, this.header.withdrawalsRoot!) return result } @@ -546,7 +539,9 @@ export class Block { toExecutionPayload(): ExecutionPayload { const blockJSON = this.toJSON() const header = blockJSON.header! - const transactions = this.transactions.map((tx) => bytesToHex(tx.serialize())) ?? [] + const transactions = this.common.isActivatedEIP(6493) + ? this.transactions.map((tx) => tx.toExecutionPayloadTx()) + : this.transactions.map((tx) => bytesToHex(tx.serialize())) const withdrawalsArr = blockJSON.withdrawals ? { withdrawals: blockJSON.withdrawals } : {} const executionPayload: ExecutionPayload = { @@ -574,6 +569,7 @@ export class Block { depositRequests: this.common.isActivatedEIP(6110) ? [] : undefined, withdrawalRequests: this.common.isActivatedEIP(7002) ? [] : undefined, consolidationRequests: this.common.isActivatedEIP(7251) ? [] : undefined, + systemLogsRoot: this.common.isActivatedEIP(6493) ? header.systemLogsRoot : undefined, } if (this.requests !== undefined) { diff --git a/packages/block/src/block/constructors.ts b/packages/block/src/block/constructors.ts index 0bc2b45182..9c23101439 100644 --- a/packages/block/src/block/constructors.ts +++ b/packages/block/src/block/constructors.ts @@ -4,6 +4,7 @@ import { type TxOptions, createTx, createTxFromBlockBodyData, + createTxFromExecutionPayloadTx, createTxFromRLP, normalizeTxParams, } from '@ethereumjs/tx' @@ -25,7 +26,13 @@ import { } from '@ethereumjs/util' import { generateCliqueBlockExtraData } from '../consensus/clique.js' -import { genRequestsTrieRoot, genTransactionsTrieRoot, genWithdrawalsTrieRoot } from '../helpers.js' +import { + genRequestsTrieRoot, + genTransactionsSszRoot, + genTransactionsTrieRoot, + genWithdrawalsSszRoot, + genWithdrawalsTrieRoot, +} from '../helpers.js' import { Block, createBlockHeader, @@ -46,6 +53,7 @@ import type { RequestsBytes, WithdrawalsBytes, } from '../types.js' +import type { Common } from '@ethereumjs/common' import type { TypedTransaction } from '@ethereumjs/tx' import type { CLRequest, @@ -373,7 +381,7 @@ export const createBlockFromJSONRPCProvider = async ( */ export async function createBlockFromExecutionPayload( payload: ExecutionPayload, - opts?: BlockOptions, + opts: BlockOptions & { common: Common }, ): Promise { const { blockNumber: number, @@ -389,11 +397,24 @@ export async function createBlockFromExecutionPayload( } = payload const txs = [] - for (const [index, serializedTx] of transactions.entries()) { + for (const [index, serializedTxOrPayload] of transactions.entries()) { try { - const tx = createTxFromRLP(hexToBytes(serializedTx as PrefixedHexString), { - common: opts?.common, - }) + let tx + if (opts.common.isActivatedEIP(6493)) { + if (typeof serializedTxOrPayload === 'string') { + throw Error('EIP 6493 activated for transaction bytes') + } + tx = createTxFromExecutionPayloadTx(serializedTxOrPayload, { + common: opts?.common, + }) + } else { + if (typeof serializedTxOrPayload !== 'string') { + throw Error('EIP 6493 not activated for transaction payload') + } + tx = createTxFromRLP(hexToBytes(serializedTxOrPayload as PrefixedHexString), { + common: opts?.common, + }) + } txs.push(tx) } catch (error) { const validationError = `Invalid tx at index ${index}: ${error}` @@ -401,13 +422,14 @@ export async function createBlockFromExecutionPayload( } } - const transactionsTrie = await genTransactionsTrieRoot( - txs, - new MerklePatriciaTrie({ common: opts?.common }), - ) + const transactionsTrie = opts.common.isActivatedEIP(6493) + ? await genTransactionsSszRoot(txs) + : await genTransactionsTrieRoot(txs, new MerklePatriciaTrie({ common: opts?.common })) const withdrawals = withdrawalsData?.map((wData) => createWithdrawal(wData)) const withdrawalsRoot = withdrawals - ? await genWithdrawalsTrieRoot(withdrawals, new MerklePatriciaTrie({ common: opts?.common })) + ? opts.common.isActivatedEIP(6493) + ? genWithdrawalsSszRoot(withdrawals) + : await genWithdrawalsTrieRoot(withdrawals, new MerklePatriciaTrie({ common: opts?.common })) : undefined const hasDepositRequests = depositRequests !== undefined && depositRequests !== null @@ -481,7 +503,7 @@ export async function createBlockFromExecutionPayload( */ export async function createBlockFromBeaconPayloadJSON( payload: BeaconPayloadJSON, - opts?: BlockOptions, + opts: BlockOptions & { common: Common }, ): Promise { const executionPayload = executionPayloadFromBeaconPayload(payload) return createBlockFromExecutionPayload(executionPayload, opts) diff --git a/packages/block/src/from-beacon-payload.ts b/packages/block/src/from-beacon-payload.ts index d6d93467f6..5f713cba23 100644 --- a/packages/block/src/from-beacon-payload.ts +++ b/packages/block/src/from-beacon-payload.ts @@ -1,7 +1,12 @@ import { bigIntToHex } from '@ethereumjs/util' import type { ExecutionPayload } from './types.js' -import type { NumericString, PrefixedHexString, VerkleExecutionWitness } from '@ethereumjs/util' +import type { + NumericString, + PrefixedHexString, + VerkleExecutionWitness, + ssz, +} from '@ethereumjs/util' type BeaconWithdrawal = { index: PrefixedHexString @@ -30,7 +35,53 @@ type BeaconConsolidationRequest = { target_pubkey: PrefixedHexString } -// Payload JSON that one gets using the beacon apis +export type BeaconFeesPerGasV1 = { + regular: PrefixedHexString | null // Quantity 64 bytes + blob: PrefixedHexString | null // Quantity 64 bytes +} + +export type BeaconAccessTupleV1 = { + address: PrefixedHexString // DATA 20 bytes + storage_keys: PrefixedHexString[] // Data 32 bytes MAX_ACCESS_LIST_STORAGE_KEYS array +} + +export type BeaconAuthorizationPayloadV1 = { + magic: PrefixedHexString + chain_id: PrefixedHexString + address: PrefixedHexString + nonce: PrefixedHexString +} + +export type BeaconExecutionSignatureV1 = { + secp256k1: PrefixedHexString | null // DATA 65 bytes or null +} + +export type BeaconAuthorizationV1 = { + payload: BeaconAuthorizationPayloadV1 + signature: BeaconExecutionSignatureV1 +} + +export type BeaconTransactionPayloadV1 = { + type: PrefixedHexString | null // Quantity, 1 byte + chain_id: PrefixedHexString | null // Quantity 8 bytes + nonce: PrefixedHexString | null // Quantity 8 bytes + max_fees_per_gas: BeaconFeesPerGasV1 | null + gas: PrefixedHexString | null // Quantity 8 bytes + to: PrefixedHexString | null // DATA 20 bytes + value: PrefixedHexString | null // Quantity 64 bytes + input: PrefixedHexString | null // max MAX_CALLDATA_SIZE bytes, + access_list: BeaconAccessTupleV1[] | null + max_priority_fees_per_gas: BeaconFeesPerGasV1 | null + blob_versioned_hashes: PrefixedHexString[] | null // DATA 32 bytes array + authorization_list: BeaconAuthorizationV1[] | null +} + +type BeaconTransactionV1 = { + payload: BeaconTransactionPayloadV1 + signature: BeaconExecutionSignatureV1 +} + +// Payload json that one gets using the beacon apis // curl localhost:5052/eth/v2/beacon/blocks/56610 | jq .data.message.body.execution_payload export type BeaconPayloadJSON = { parent_hash: PrefixedHexString @@ -46,7 +97,7 @@ export type BeaconPayloadJSON = { extra_data: PrefixedHexString base_fee_per_gas: NumericString block_hash: PrefixedHexString - transactions: PrefixedHexString[] + transactions: PrefixedHexString[] | BeaconTransactionV1[] withdrawals?: BeaconWithdrawal[] blob_gas_used?: NumericString excess_blob_gas?: NumericString @@ -55,6 +106,7 @@ export type BeaconPayloadJSON = { deposit_requests?: BeaconDepositRequest[] withdrawal_requests?: BeaconWithdrawalRequest[] consolidation_requests?: BeaconConsolidationRequest[] + system_logs_root?: PrefixedHexString // the casing of VerkleExecutionWitness remains same camel case for now execution_witness?: VerkleExecutionWitness @@ -121,6 +173,48 @@ function parseExecutionWitnessFromSnakeJSON({ * The JSON data can be retrieved from a consensus layer (CL) client on this Beacon API `/eth/v2/beacon/blocks/[block number]` */ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJSON): ExecutionPayload { + const transactions = + typeof payload.transactions[0] === 'object' + ? (payload.transactions as BeaconTransactionV1[]).map((btxv1) => { + return { + payload: { + type: btxv1.payload.type, + chainId: btxv1.payload.chain_id, + nonce: btxv1.payload.nonce, + maxFeesPerGas: btxv1.payload.max_fees_per_gas, + to: btxv1.payload.to, + value: btxv1.payload.value, + input: btxv1.payload.input, + accessList: + btxv1.payload.access_list?.map((bal: BeaconAccessTupleV1) => { + return { + address: bal.address, + storageKeys: bal.storage_keys, + } + }) ?? null, + maxPriorityFeesPerGas: btxv1.payload.max_priority_fees_per_gas, + blobVersionedHashes: btxv1.payload.blob_versioned_hashes, + authorizationList: + btxv1.payload.authorization_list?.map((bal: BeaconAuthorizationV1) => { + const { payload, signature } = bal + return { + payload: { + magic: payload.magic, + chainId: payload.chain_id, + address: payload.address, + nonce: payload.nonce, + }, + signature, + } + }) ?? null, + }, + signature: { + secp256k1: btxv1.signature.secp256k1, + }, + } as ssz.TransactionV1 + }) + : (payload.transactions as PrefixedHexString[]) + const executionPayload: ExecutionPayload = { parentHash: payload.parent_hash, feeRecipient: payload.fee_recipient, @@ -135,7 +229,7 @@ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJSON): E extraData: payload.extra_data, baseFeePerGas: bigIntToHex(BigInt(payload.base_fee_per_gas)), blockHash: payload.block_hash, - transactions: payload.transactions, + transactions, } if (payload.withdrawals !== undefined && payload.withdrawals !== null) { @@ -184,6 +278,10 @@ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJSON): E ) } + if (payload.system_logs_root !== undefined && payload.system_logs_root !== null) { + executionPayload.systemLogsRoot = payload.system_logs_root + } + if (payload.execution_witness !== undefined && payload.execution_witness !== null) { // the casing structure in payload could be camel case or snake depending upon the CL executionPayload.executionWitness = diff --git a/packages/block/src/header/header.ts b/packages/block/src/header/header.ts index 79675dafed..6db0a8a5d3 100644 --- a/packages/block/src/header/header.ts +++ b/packages/block/src/header/header.ts @@ -16,6 +16,7 @@ import { createZeroAddress, equalsBytes, hexToBytes, + ssz, toType, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' @@ -29,6 +30,9 @@ import { fakeExponential } from '../helpers.js' import { paramsBlock } from '../params.js' import type { BlockHeaderBytes, BlockOptions, HeaderData, JSONHeader } from '../types.js' +import type { ValueOf } from '@chainsafe/ssz' + +export type SSZHeaderType = ValueOf interface HeaderCache { hash: Uint8Array | undefined @@ -61,6 +65,7 @@ export class BlockHeader { public readonly excessBlobGas?: bigint public readonly parentBeaconBlockRoot?: Uint8Array public readonly requestsRoot?: Uint8Array + public readonly systemLogsRoot?: Uint8Array public readonly common: Common @@ -162,6 +167,7 @@ export class BlockHeader { excessBlobGas: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined, parentBeaconBlockRoot: this.common.isActivatedEIP(4788) ? new Uint8Array(32) : undefined, requestsRoot: this.common.isActivatedEIP(7685) ? KECCAK256_RLP : undefined, + systemLogsRoot: this.common.isActivatedEIP(6493) ? KECCAK256_RLP : undefined, } const baseFeePerGas = @@ -177,6 +183,8 @@ export class BlockHeader { hardforkDefaults.parentBeaconBlockRoot const requestsRoot = toType(headerData.requestsRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.requestsRoot + const systemLogsRoot = + toType(headerData.systemLogsRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.systemLogsRoot if (!this.common.isActivatedEIP(1559) && baseFeePerGas !== undefined) { throw new Error('A base fee for a block can only be set with EIP1559 being activated') @@ -208,6 +216,10 @@ export class BlockHeader { throw new Error('requestsRoot can only be provided with EIP 7685 activated') } + if (!this.common.isActivatedEIP(6493) && systemLogsRoot !== undefined) { + throw new Error('systemLogsRoot can only be provided with EIP 6493 activated') + } + this.parentHash = parentHash this.uncleHash = uncleHash this.coinbase = coinbase @@ -229,6 +241,7 @@ export class BlockHeader { this.excessBlobGas = excessBlobGas this.parentBeaconBlockRoot = parentBeaconBlockRoot this.requestsRoot = requestsRoot + this.systemLogsRoot = systemLogsRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -349,6 +362,13 @@ export class BlockHeader { throw new Error(msg) } } + + if (this.common.isActivatedEIP(6493)) { + if (this.systemLogsRoot === undefined) { + const msg = this._errorMsg('EIP6493 block has no systemLogsRoot field') + throw new Error(msg) + } + } } /** @@ -628,21 +648,63 @@ export class BlockHeader { if (this.common.isActivatedEIP(7685)) { rawItems.push(this.requestsRoot!) } + if (this.common.isActivatedEIP(6493)) { + rawItems.push(this.systemLogsRoot!) + } return rawItems } + sszRaw(): SSZHeaderType { + const header = { + parentHash: this.parentHash, + coinbase: this.coinbase.bytes, + stateRoot: this.stateRoot, + transactionsTrie: this.transactionsTrie, + receiptsTrie: this.receiptTrie, + number: this.number, + gasLimits: { + regular: this.gasLimit, + blob: this.common.isActivatedEIP(4844) ? this.common.param('maxblobGasPerBlock') : null, + }, + gasUsed: { regular: this.gasUsed, blob: this.blobGasUsed ?? null }, + timestamp: this.timestamp, + extraData: this.extraData, + mixHash: this.mixHash, + baseFeePerGas: { + regular: this.baseFeePerGas ?? null, + blob: this.common.isActivatedEIP(4844) ? this.getBlobGasPrice() : null, + }, + withdrawalsRoot: this.withdrawalsRoot ?? null, + excessGas: { regular: null, blob: this.excessBlobGas ?? null }, + parentBeaconBlockRoot: this.parentBeaconBlockRoot ?? null, + requestsRoot: this.requestsRoot ?? null, + systemLogsRoot: this.systemLogsRoot ?? null, + } + + return header + } + + calcHash(): Uint8Array { + if (this.common.isActivatedEIP(6493)) { + const hash = ssz.BlockHeader.hashTreeRoot(this.sszRaw()) + return hash + } else { + return this.keccakFunction(RLP.encode(this.raw())) + } + } + /** * Returns the hash of the block header. */ hash(): Uint8Array { if (Object.isFrozen(this)) { if (!this.cache.hash) { - this.cache.hash = this.keccakFunction(RLP.encode(this.raw())) as Uint8Array + this.cache.hash = this.calcHash() } return this.cache.hash } - return this.keccakFunction(RLP.encode(this.raw())) + return this.calcHash() } /** @@ -770,6 +832,9 @@ export class BlockHeader { if (this.common.isActivatedEIP(7685)) { JSONDict.requestsRoot = bytesToHex(this.requestsRoot!) } + if (this.common.isActivatedEIP(6493)) { + JSONDict.systemLogsRoot = bytesToHex(this.systemLogsRoot!) + } return JSONDict } diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index 9fc406788b..24ec12f795 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -1,12 +1,15 @@ import { MerklePatriciaTrie } from '@ethereumjs/mpt' import { RLP } from '@ethereumjs/rlp' import { Blob4844Tx } from '@ethereumjs/tx' -import { BIGINT_0, BIGINT_1, TypeOutput, isHexString, toType } from '@ethereumjs/util' +import { BIGINT_0, BIGINT_1, TypeOutput, isHexString, ssz, toType } from '@ethereumjs/util' import type { BlockHeaderBytes, HeaderData } from './types.js' +import type { ValueOf } from '@chainsafe/ssz' import type { TypedTransaction } from '@ethereumjs/tx' import type { CLRequest, CLRequestType, PrefixedHexString, Withdrawal } from '@ethereumjs/util' +export type SSZTransactionType = ValueOf + /** * Returns a 0x-prefixed hex number string from a hex string or string integer. * @param {string} input string to check, convert, and return @@ -47,9 +50,10 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { excessBlobGas, parentBeaconBlockRoot, requestsRoot, + systemLogsRoot, ] = values - if (values.length > 21) { + if (values.length > 22) { throw new Error( `invalid header. More values than expected were received. Max: 20, got: ${values.length}`, ) @@ -82,6 +86,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { excessBlobGas, parentBeaconBlockRoot, requestsRoot, + systemLogsRoot, } } @@ -132,6 +137,11 @@ export async function genWithdrawalsTrieRoot(wts: Withdrawal[], emptyTrie?: Merk return trie.root() } +export function genWithdrawalsSszRoot(wts: Withdrawal[]) { + const withdrawals = wts.map((wt) => wt.toValue()) + return ssz.Withdrawals.hashTreeRoot(withdrawals) +} + /** * Returns the txs trie root for array of TypedTransaction * @param txs array of TypedTransaction to compute the root of @@ -148,6 +158,11 @@ export async function genTransactionsTrieRoot( return trie.root() } +export async function genTransactionsSszRoot(txs: TypedTransaction[]) { + const transactions = txs.map((tx) => tx.sszRaw() as unknown as SSZTransactionType) + return ssz.Transactions.hashTreeRoot(transactions) +} + /** * Returns the requests trie root for an array of CLRequests * @param requests - an array of CLRequests diff --git a/packages/block/src/index.ts b/packages/block/src/index.ts index 074d1a1c04..887b75358b 100644 --- a/packages/block/src/index.ts +++ b/packages/block/src/index.ts @@ -5,7 +5,9 @@ export { type BeaconPayloadJSON, executionPayloadFromBeaconPayload } from './fro export * from './header/index.js' export { genRequestsTrieRoot, + genTransactionsSszRoot, genTransactionsTrieRoot, + genWithdrawalsSszRoot, genWithdrawalsTrieRoot, getDifficulty, valuesArrayToHeaderData, diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 145ef4ef20..52d35c3df2 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -17,6 +17,7 @@ import type { WithdrawalBytes, WithdrawalData, WithdrawalRequestV1, + ssz, } from '@ethereumjs/util' /** @@ -115,6 +116,7 @@ export interface HeaderData { excessBlobGas?: BigIntLike parentBeaconBlockRoot?: BytesLike requestsRoot?: BytesLike + systemLogsRoot?: BytesLike } /** @@ -208,6 +210,7 @@ export interface JSONHeader { excessBlobGas?: PrefixedHexString parentBeaconBlockRoot?: PrefixedHexString requestsRoot?: PrefixedHexString + systemLogsRoot?: PrefixedHexString } /* @@ -242,6 +245,7 @@ export interface JSONRPCBlock { parentBeaconBlockRoot?: PrefixedHexString // If EIP-4788 is enabled for this block, returns parent beacon block root executionWitness?: VerkleExecutionWitness | null // If Verkle is enabled for this block requestsRoot?: PrefixedHexString // If EIP-7685 is enabled for this block, returns the requests root + systemLogsRoot?: PrefixedHexString requests?: Array // If EIP-7685 is enabled for this block, array of serialized CL requests } @@ -267,7 +271,7 @@ export type ExecutionPayload = { extraData: PrefixedHexString // DATA, 0 to 32 Bytes baseFeePerGas: PrefixedHexString // QUANTITY, 256 Bits blockHash: PrefixedHexString // DATA, 32 Bytes - transactions: PrefixedHexString[] // Array of DATA - Array of transaction rlp strings, + transactions: PrefixedHexString[] | ssz.TransactionV1[] // Array of DATA - Array of transaction rlp strings, withdrawals?: WithdrawalV1[] // Array of withdrawal objects blobGasUsed?: PrefixedHexString // QUANTITY, 64 Bits excessBlobGas?: PrefixedHexString // QUANTITY, 64 Bits @@ -277,4 +281,5 @@ export type ExecutionPayload = { depositRequests?: DepositRequestV1[] // Array of 6110 deposit requests withdrawalRequests?: WithdrawalRequestV1[] // Array of 7002 withdrawal requests consolidationRequests?: ConsolidationRequestV1[] // Array of 7251 consolidation requests + systemLogsRoot?: PrefixedHexString } diff --git a/packages/block/test/eip4895block.spec.ts b/packages/block/test/eip4895block.spec.ts index 98d2baed05..5ec4c0f90e 100644 --- a/packages/block/test/eip4895block.spec.ts +++ b/packages/block/test/eip4895block.spec.ts @@ -5,7 +5,6 @@ import { KECCAK256_RLP, createWithdrawalFromBytesArray, hexToBytes, - randomBytes, } from '@ethereumjs/util' import { assert, describe, it } from 'vitest' @@ -230,14 +229,4 @@ describe('EIP4895 tests', () => { 'should provide withdrawals array when 4895 is active', ) }) - - it('should return early when withdrawals root equals KECCAK256_RLP', async () => { - const block = createBlock({}, { common }) - // Set invalid withdrawalsRoot in cache - block['cache'].withdrawalsTrieRoot = randomBytes(32) - assert.ok( - await block.withdrawalsTrieIsValid(), - 'correctly executed code path where withdrawals length is 0', - ) - }) }) diff --git a/packages/client/bin/cli.ts b/packages/client/bin/cli.ts index 0fc6c1bcab..5b19bd65be 100755 --- a/packages/client/bin/cli.ts +++ b/packages/client/bin/cli.ts @@ -151,6 +151,12 @@ const args: ClientOpts = yargs boolean: true, default: true, }) + // just a hack to insert 6493 on pragueTime for input genesis + .option('eip6493AtPrague', { + describe: 'Just for stablecontainer devnets testing', + boolean: true, + default: true, + }) .option('bootnodes', { describe: 'Comma-separated list of network bootnodes (format: "enode://@,enode://..." ("[?discport=]" not supported) or path to a bootnode.txt file', @@ -1028,6 +1034,12 @@ async function run() { // Use geth genesis parameters file if specified const genesisFile = JSON.parse(readFileSync(args.gethGenesis, 'utf-8')) const chainName = path.parse(args.gethGenesis).base.split('.')[0] + // just a hack for stable container devnets to schedule 6493 at prague + if (args.eip6493AtPrague === true) { + genesisFile.config.eip6493Time = genesisFile.config.pragueTime + console.log('Scheduling eip6493AtPrague', genesisFile.config) + } + common = createCommonFromGethGenesis(genesisFile, { chain: chainName, mergeForkIdPostMerge: args.mergeForkIdPostMerge, diff --git a/packages/client/src/miner/pendingBlock.ts b/packages/client/src/miner/pendingBlock.ts index cc17cf8316..df3d6074a5 100644 --- a/packages/client/src/miner/pendingBlock.ts +++ b/packages/client/src/miner/pendingBlock.ts @@ -199,10 +199,15 @@ export class PendingBlock { allowedBlobs = 0 } // Add current txs in pool - const txs = await this.txPool.txsByPriceAndNonce(vm, { - baseFee: baseFeePerGas, - allowedBlobs, - }) + const txs = await this.txPool + .txsByPriceAndNonce(vm, { + baseFee: baseFeePerGas, + allowedBlobs, + }) + .catch((e) => { + console.log('txsByPriceAndNonce', e) + return [] + }) this.config.logger.info( `Pending: Assembling block from ${txs.length} eligible txs (baseFee: ${baseFeePerGas})`, ) @@ -270,10 +275,15 @@ export class PendingBlock { // Add new txs that the pool received const txs = ( - await this.txPool.txsByPriceAndNonce(vm, { - baseFee: headerData.baseFeePerGas! as bigint, - allowedBlobs, - }) + await this.txPool + .txsByPriceAndNonce(vm, { + baseFee: headerData.baseFeePerGas! as bigint, + allowedBlobs, + }) + .catch((e) => { + console.log('txsByPriceAndNonce', e) + return [] + }) ).filter( (tx) => (builder as any).transactions.some((t: TypedTransaction) => @@ -325,6 +335,7 @@ export class PendingBlock { blockFull = true // Falls through default: + console.log({ addTxResult }) skippedByAddErrors++ } index++ @@ -347,6 +358,7 @@ export class PendingBlock { }) addTxResult = AddTxResult.Success } catch (error: any) { + console.log('addTransaction', error) if (error.message === 'tx has a higher gas limit than the remaining gas in the block') { if (builder.gasUsed > (builder as any).headerData.gasLimit - BigInt(21000)) { // If block has less than 21000 gas remaining, consider it full @@ -363,8 +375,9 @@ export class PendingBlock { ) addTxResult = AddTxResult.RemovedByErrors } else { + console.log(error) // If there is an error adding a tx, it will be skipped - this.config.logger.debug( + this.config.logger.warn( `Pending: Skipping tx ${bytesToHex( tx.hash(), )}, error encountered when trying to add tx:\n${error}`, diff --git a/packages/client/src/rpc/helpers.ts b/packages/client/src/rpc/helpers.ts index 24cc138b40..cd12f244dc 100644 --- a/packages/client/src/rpc/helpers.ts +++ b/packages/client/src/rpc/helpers.ts @@ -36,7 +36,16 @@ export function callWithStackTrace(handler: Function, debug: boolean) { /** * Returns tx formatted to the standard JSON-RPC fields */ -export const toJSONRPCTx = (tx: TypedTransaction, block?: Block, txIndex?: number): JSONRPCTx => { +export const toJSONRPCTx = ( + tx: TypedTransaction, + block?: Block, + txIndex?: number, + inclusionProof?: { + merkleBranch: Uint8Array[] + transactionsRoot: Uint8Array + transactionRoot: Uint8Array + }, +): JSONRPCTx => { const txJSON = tx.toJSON() return { blockHash: block ? bytesToHex(block.hash()) : null, @@ -61,6 +70,14 @@ export const toJSONRPCTx = (tx: TypedTransaction, block?: Block, txIndex?: numbe maxFeePerBlobGas: txJSON.maxFeePerBlobGas, blobVersionedHashes: txJSON.blobVersionedHashes, yParity: txJSON.yParity, + inclusionProof: + inclusionProof !== undefined + ? { + merkleBranch: inclusionProof.merkleBranch.map((elem) => bytesToHex(elem)), + transactionsRoot: bytesToHex(inclusionProof.transactionsRoot), + transactionRoot: bytesToHex(inclusionProof.transactionRoot), + } + : undefined, } } diff --git a/packages/client/src/rpc/modules/engine/engine.ts b/packages/client/src/rpc/modules/engine/engine.ts index edc3e69432..98dfda5fcc 100644 --- a/packages/client/src/rpc/modules/engine/engine.ts +++ b/packages/client/src/rpc/modules/engine/engine.ts @@ -346,116 +346,63 @@ export class Engine { private async newPayload( params: [ExecutionPayload, (Bytes32[] | null)?, (Bytes32 | null)?], ): Promise { - const [payload, blobVersionedHashes, parentBeaconBlockRoot] = params - if (this.config.synchronized) { - this.connectionManager.newPayloadLog() - } - const { parentHash, blockHash } = payload - - // we can be strict and return with invalid if this block was previous invalidated in - // invalidBlocks cache, but to have a more robust behavior instead: - // - // we remove this block from invalidBlocks for it to be evaluated again against the - // new data/corrections the CL might be calling newPayload with - this.invalidBlocks.delete(blockHash.slice(2)) - - /** - * See if block can be assembled from payload - */ - // newpayloadv3 comes with parentBeaconBlockRoot out of the payload - const { block: headBlock, error } = await assembleBlock( - { - ...payload, - // ExecutionPayload only handles undefined - parentBeaconBlockRoot: parentBeaconBlockRoot ?? undefined, - }, - this.chain, - this.chainCache, - ) - if (headBlock === undefined || error !== undefined) { - let response = error - if (!response) { - const validationError = `Error assembling block from payload during initialization` - this.config.logger.debug(validationError) - const latestValidHash = await validHash( - hexToBytes(parentHash as PrefixedHexString), - this.chain, - this.chainCache, - ) - response = { status: Status.INVALID, latestValidHash, validationError } + try { + const [payload, blobVersionedHashes, parentBeaconBlockRoot] = params + if (this.config.synchronized) { + this.connectionManager.newPayloadLog() } - // skip marking the block invalid as this is more of a data issue from CL - return response - } + const { parentHash, blockHash } = payload - /** - * Validate blob versioned hashes in the context of EIP-4844 blob transactions - */ - if (headBlock.common.isActivatedEIP(4844)) { - let validationError: string | null = null - if (blobVersionedHashes === undefined || blobVersionedHashes === null) { - validationError = `Error verifying blobVersionedHashes: received none` - } else { - validationError = validate4844BlobVersionedHashes(headBlock, blobVersionedHashes) - } + // we can be strict and return with invalid if this block was previous invalidated in + // invalidBlocks cache, but to have a more robust behavior instead: + // + // we remove this block from invalidBlocks for it to be evaluated again against the + // new data/corrections the CL might be calling newPayload with + this.invalidBlocks.delete(blockHash.slice(2)) - // if there was a validation error return invalid - if (validationError !== null) { - this.config.logger.debug(validationError) - const latestValidHash = await validHash( - hexToBytes(parentHash as PrefixedHexString), - this.chain, - this.chainCache, - ) - const response = { status: Status.INVALID, latestValidHash, validationError } - // skip marking the block invalid as this is more of a data issue from CL - return response - } - } else if (blobVersionedHashes !== undefined && blobVersionedHashes !== null) { - const validationError = `Invalid blobVersionedHashes before EIP-4844 is activated` - const latestValidHash = await validHash( - hexToBytes(parentHash as PrefixedHexString), + /** + * See if block can be assembled from payload + */ + // newpayloadv3 comes with parentBeaconBlockRoot out of the payload + const { block: headBlock, error } = await assembleBlock( + { + ...payload, + // ExecutionPayload only handles undefined + parentBeaconBlockRoot: parentBeaconBlockRoot ?? undefined, + }, this.chain, this.chainCache, ) - const response = { status: Status.INVALID, latestValidHash, validationError } - // skip marking the block invalid as this is more of a data issue from CL - return response - } - - /** - * Stats and hardfork updates - */ - this.connectionManager.updatePayloadStats(headBlock) - const hardfork = headBlock.common.hardfork() - if (hardfork !== this.lastNewPayloadHF && this.lastNewPayloadHF !== '') { - this.config.logger.info( - `Hardfork change along new payload block number=${headBlock.header.number} hash=${short( - headBlock.hash(), - )} old=${this.lastNewPayloadHF} new=${hardfork}`, - ) - } - this.lastNewPayloadHF = hardfork - - try { - /** - * get the parent from beacon skeleton or from remoteBlocks cache or from the chain - * to run basic validations based on parent - */ - const parent = - (await this.skeleton.getBlockByHash(hexToBytes(parentHash as PrefixedHexString), true)) ?? - this.remoteBlocks.get(parentHash.slice(2)) ?? - (await this.chain.getBlock(hexToBytes(parentHash as PrefixedHexString))) + if (headBlock === undefined || error !== undefined) { + let response = error + if (!response) { + const validationError = `Error assembling block from payload during initialization` + this.config.logger.debug(validationError) + const latestValidHash = await validHash( + hexToBytes(parentHash as PrefixedHexString), + this.chain, + this.chainCache, + ) + response = { status: Status.INVALID, latestValidHash, validationError } + } + // skip marking the block invalid as this is more of a data issue from CL + return response + } /** - * validate 4844 transactions and fields as these validations generally happen on putBlocks - * when parent is confirmed to be in the chain. But we can do it here early + * Validate blob versioned hashes in the context of EIP-4844 blob transactions */ if (headBlock.common.isActivatedEIP(4844)) { - try { - headBlock.validateBlobTransactions(parent.header) - } catch (error: any) { - const validationError = `Invalid 4844 transactions: ${error}` + let validationError: string | null = null + if (blobVersionedHashes === undefined || blobVersionedHashes === null) { + validationError = `Error verifying blobVersionedHashes: received none` + } else { + validationError = validate4844BlobVersionedHashes(headBlock, blobVersionedHashes) + } + + // if there was a validation error return invalid + if (validationError !== null) { + this.config.logger.debug(validationError) const latestValidHash = await validHash( hexToBytes(parentHash as PrefixedHexString), this.chain, @@ -465,26 +412,149 @@ export class Engine { // skip marking the block invalid as this is more of a data issue from CL return response } + } else if (blobVersionedHashes !== undefined && blobVersionedHashes !== null) { + const validationError = `Invalid blobVersionedHashes before EIP-4844 is activated` + const latestValidHash = await validHash( + hexToBytes(parentHash as PrefixedHexString), + this.chain, + this.chainCache, + ) + const response = { status: Status.INVALID, latestValidHash, validationError } + // skip marking the block invalid as this is more of a data issue from CL + return response } /** - * Check for executed parent + * Stats and hardfork updates */ - const executedParentExists = - this.executedBlocks.get(parentHash.slice(2)) ?? - (await validExecutedChainBlock(hexToBytes(parentHash as PrefixedHexString), this.chain)) - // If the parent is not executed throw an error, it will be caught and return SYNCING or ACCEPTED. - if (!executedParentExists) { - throw new Error(`Parent block not yet executed number=${parent.header.number}`) + this.connectionManager.updatePayloadStats(headBlock) + const hardfork = headBlock.common.hardfork() + if (hardfork !== this.lastNewPayloadHF && this.lastNewPayloadHF !== '') { + this.config.logger.info( + `Hardfork change along new payload block number=${headBlock.header.number} hash=${short( + headBlock.hash(), + )} old=${this.lastNewPayloadHF} new=${hardfork}`, + ) } - } catch (error: any) { - // Stash the block for a potential forced forkchoice update to it later. - this.remoteBlocks.set(bytesToUnprefixedHex(headBlock.hash()), headBlock) + this.lastNewPayloadHF = hardfork + try { + /** + * get the parent from beacon skeleton or from remoteBlocks cache or from the chain + * to run basic validations based on parent + */ + const parent = + (await this.skeleton.getBlockByHash(hexToBytes(parentHash as PrefixedHexString), true)) ?? + this.remoteBlocks.get(parentHash.slice(2)) ?? + (await this.chain.getBlock(hexToBytes(parentHash as PrefixedHexString))) + + /** + * validate 4844 transactions and fields as these validations generally happen on putBlocks + * when parent is confirmed to be in the chain. But we can do it here early + */ + if (headBlock.common.isActivatedEIP(4844)) { + try { + headBlock.validateBlobTransactions(parent.header) + } catch (error: any) { + const validationError = `Invalid 4844 transactions: ${error}` + const latestValidHash = await validHash( + hexToBytes(parentHash as PrefixedHexString), + this.chain, + this.chainCache, + ) + const response = { status: Status.INVALID, latestValidHash, validationError } + // skip marking the block invalid as this is more of a data issue from CL + return response + } + } + + /** + * Check for executed parent + */ + const executedParentExists = + this.executedBlocks.get(parentHash.slice(2)) ?? + (await validExecutedChainBlock(hexToBytes(parentHash as PrefixedHexString), this.chain)) + // If the parent is not executed throw an error, it will be caught and return SYNCING or ACCEPTED. + if (!executedParentExists) { + throw new Error(`Parent block not yet executed number=${parent.header.number}`) + } + } catch (error: any) { + // Stash the block for a potential forced forkchoice update to it later. + this.remoteBlocks.set(bytesToUnprefixedHex(headBlock.hash()), headBlock) + + const optimisticLookup = !(await this.skeleton.setHead(headBlock, false)) + /** + * Invalid skeleton PUT + */ + if ( + this.skeleton.fillStatus?.status === PutStatus.INVALID && + optimisticLookup && + headBlock.header.number >= this.skeleton.fillStatus.height + ) { + const latestValidHash = + this.chain.blocks.latest !== null + ? await validHash(this.chain.blocks.latest.hash(), this.chain, this.chainCache) + : bytesToHex(new Uint8Array(32)) + const response = { + status: Status.INVALID, + validationError: this.skeleton.fillStatus.validationError ?? '', + latestValidHash, + } + return response + } + + /** + * Invalid execution + */ + if ( + this.execution.chainStatus?.status === ExecStatus.INVALID && + optimisticLookup && + headBlock.header.number >= this.execution.chainStatus.height + ) { + // if the invalid block is canonical along the current chain return invalid + const invalidBlock = await this.skeleton.getBlockByHash( + this.execution.chainStatus.hash, + true, + ) + if (invalidBlock !== undefined) { + // hard luck: block along canonical chain is invalid + const latestValidHash = await validHash( + invalidBlock.header.parentHash, + this.chain, + this.chainCache, + ) + const validationError = `Block number=${invalidBlock.header.number} hash=${short( + invalidBlock.hash(), + )} root=${short(invalidBlock.header.stateRoot)} along the canonical chain is invalid` + + const response = { + status: Status.INVALID, + latestValidHash, + validationError, + } + return response + } + } + + const status = + // If the transitioned to beacon sync and this block can extend beacon chain then + optimisticLookup === true ? Status.SYNCING : Status.ACCEPTED + const response = { status, validationError: null, latestValidHash: null } + return response + } + + // This optimistic lookup keeps skeleton updated even if for e.g. beacon sync might not have + // been initialized here but a batch of blocks new payloads arrive, most likely during sync + // We still can't switch to beacon sync here especially if the chain is pre merge and there + // is pow block which this client would like to mint and attempt proposing it + // + // Call skeleton.setHead without forcing head change to return if the block is reorged or not + // Do optimistic lookup if not reorged + // + // TODO: Determine if this optimistic lookup can be combined with the optimistic lookup above + // from within the catch clause (by skipping the code from the catch clause), code looks + // identical, same for executedBlockExists code below ?? const optimisticLookup = !(await this.skeleton.setHead(headBlock, false)) - /** - * Invalid skeleton PUT - */ if ( this.skeleton.fillStatus?.status === PutStatus.INVALID && optimisticLookup && @@ -502,9 +572,23 @@ export class Engine { return response } - /** - * Invalid execution - */ + this.remoteBlocks.set(bytesToUnprefixedHex(headBlock.hash()), headBlock) + + // we should check if the block exists executed in remoteBlocks or in chain as a check since stateroot + // exists in statemanager is not sufficient because an invalid crafted block with valid block hash with + // some pre-executed stateroot can be sent + const executedBlockExists = + this.executedBlocks.get(blockHash.slice(2)) ?? + (await validExecutedChainBlock(hexToBytes(blockHash as PrefixedHexString), this.chain)) + if (executedBlockExists) { + const response = { + status: Status.VALID, + latestValidHash: blockHash as PrefixedHexString, + validationError: null, + } + return response + } + if ( this.execution.chainStatus?.status === ExecStatus.INVALID && optimisticLookup && @@ -535,215 +619,143 @@ export class Engine { } } - const status = - // If the transitioned to beacon sync and this block can extend beacon chain then - optimisticLookup === true ? Status.SYNCING : Status.ACCEPTED - const response = { status, validationError: null, latestValidHash: null } - return response - } - - // This optimistic lookup keeps skeleton updated even if for e.g. beacon sync might not have - // been initialized here but a batch of blocks new payloads arrive, most likely during sync - // We still can't switch to beacon sync here especially if the chain is pre merge and there - // is pow block which this client would like to mint and attempt proposing it - // - // Call skeleton.setHead without forcing head change to return if the block is reorged or not - // Do optimistic lookup if not reorged - // - // TODO: Determine if this optimistic lookup can be combined with the optimistic lookup above - // from within the catch clause (by skipping the code from the catch clause), code looks - // identical, same for executedBlockExists code below ?? - const optimisticLookup = !(await this.skeleton.setHead(headBlock, false)) - if ( - this.skeleton.fillStatus?.status === PutStatus.INVALID && - optimisticLookup && - headBlock.header.number >= this.skeleton.fillStatus.height - ) { - const latestValidHash = - this.chain.blocks.latest !== null - ? await validHash(this.chain.blocks.latest.hash(), this.chain, this.chainCache) - : bytesToHex(new Uint8Array(32)) - const response = { - status: Status.INVALID, - validationError: this.skeleton.fillStatus.validationError ?? '', - latestValidHash, + /** + * 1. Determine non-executed blocks from beyond vmHead to headBlock + * 2. Iterate through non-executed blocks + * 3. Determine if block should be executed by some extra conditions + * 4. Execute block with this.execution.runWithoutSetHead() + */ + const vmHead = + this.chainCache.executedBlocks.get(parentHash.slice(2)) ?? + (await this.chain.blockchain.getIteratorHead()) + let blocks: Block[] + try { + // find parents till vmHead but limit lookups till engineParentLookupMaxDepth + blocks = await recursivelyFindParents( + vmHead.hash(), + headBlock.header.parentHash, + this.chain, + ) + } catch (error) { + const response = { status: Status.SYNCING, latestValidHash: null, validationError: null } + return response } - return response - } - this.remoteBlocks.set(bytesToUnprefixedHex(headBlock.hash()), headBlock) + blocks.push(headBlock) - // we should check if the block exists executed in remoteBlocks or in chain as a check since stateroot - // exists in statemanager is not sufficient because an invalid crafted block with valid block hash with - // some pre-executed stateroot can be sent - const executedBlockExists = - this.executedBlocks.get(blockHash.slice(2)) ?? - (await validExecutedChainBlock(hexToBytes(blockHash as PrefixedHexString), this.chain)) - if (executedBlockExists) { - const response = { - status: Status.VALID, - latestValidHash: blockHash as PrefixedHexString, - validationError: null, - } - return response - } - - if ( - this.execution.chainStatus?.status === ExecStatus.INVALID && - optimisticLookup && - headBlock.header.number >= this.execution.chainStatus.height - ) { - // if the invalid block is canonical along the current chain return invalid - const invalidBlock = await this.skeleton.getBlockByHash(this.execution.chainStatus.hash, true) - if (invalidBlock !== undefined) { - // hard luck: block along canonical chain is invalid + let lastBlock: Block + try { + for (const [i, block] of blocks.entries()) { + lastBlock = block + const bHash = block.hash() + + const isBlockExecuted = + (this.executedBlocks.get(bytesToUnprefixedHex(bHash)) ?? + (await validExecutedChainBlock(bHash, this.chain))) !== null + + if (!isBlockExecuted) { + // Only execute + // i) if number of blocks pending to be executed are within limit + // ii) Txs to execute in blocking call is within the supported limit + // else return SYNCING/ACCEPTED and let skeleton led chain execution catch up + const shouldExecuteBlock = + blocks.length - i <= this.chain.config.engineNewpayloadMaxExecute && + block.transactions.length <= this.chain.config.engineNewpayloadMaxTxsExecute + + const executed = + shouldExecuteBlock && + (await (async () => { + // just keeping its name different from the parentBlock to not confuse the context even + // though scope rules will not let it conflict with the parent of the new payload block + const blockParent = + i > 0 + ? blocks[i - 1] + : (this.chainCache.remoteBlocks.get( + bytesToHex(block.header.parentHash).slice(2), + ) ?? (await this.chain.getBlock(block.header.parentHash))) + const blockExecuted = await this.execution.runWithoutSetHead({ + block, + root: blockParent.header.stateRoot, + setHardfork: true, + parentBlock: blockParent, + }) + return blockExecuted + })()) + + // if can't be executed then return syncing/accepted + if (!executed) { + this.config.logger.debug( + `Skipping block(s) execution for headBlock=${headBlock.header.number} hash=${short( + headBlock.hash(), + )} : pendingBlocks=${blocks.length - i}(limit=${ + this.chain.config.engineNewpayloadMaxExecute + }) transactions=${block.transactions.length}(limit=${ + this.chain.config.engineNewpayloadMaxTxsExecute + }) executionBusy=${this.execution.running}`, + ) + // determined status to be returned depending on if block could extend chain or not + const status = optimisticLookup === true ? Status.SYNCING : Status.ACCEPTED + const response = { status, latestValidHash: null, validationError: null } + return response + } else { + this.executedBlocks.set(bytesToUnprefixedHex(block.hash()), block) + } + } + } + } catch (error) { const latestValidHash = await validHash( - invalidBlock.header.parentHash, + headBlock.header.parentHash, this.chain, this.chainCache, ) - const validationError = `Block number=${invalidBlock.header.number} hash=${short( - invalidBlock.hash(), - )} root=${short(invalidBlock.header.stateRoot)} along the canonical chain is invalid` - - const response = { - status: Status.INVALID, - latestValidHash, - validationError, - } - return response - } - } - - /** - * 1. Determine non-executed blocks from beyond vmHead to headBlock - * 2. Iterate through non-executed blocks - * 3. Determine if block should be executed by some extra conditions - * 4. Execute block with this.execution.runWithoutSetHead() - */ - const vmHead = - this.chainCache.executedBlocks.get(parentHash.slice(2)) ?? - (await this.chain.blockchain.getIteratorHead()) - let blocks: Block[] - try { - // find parents till vmHead but limit lookups till engineParentLookupMaxDepth - blocks = await recursivelyFindParents(vmHead.hash(), headBlock.header.parentHash, this.chain) - } catch (error) { - const response = { status: Status.SYNCING, latestValidHash: null, validationError: null } - return response - } - - blocks.push(headBlock) - let lastBlock: Block - try { - for (const [i, block] of blocks.entries()) { - lastBlock = block - const bHash = block.hash() - - const isBlockExecuted = - (this.executedBlocks.get(bytesToUnprefixedHex(bHash)) ?? - (await validExecutedChainBlock(bHash, this.chain))) !== null - - if (!isBlockExecuted) { - // Only execute - // i) if number of blocks pending to be executed are within limit - // ii) Txs to execute in blocking call is within the supported limit - // else return SYNCING/ACCEPTED and let skeleton led chain execution catch up - const shouldExecuteBlock = - blocks.length - i <= this.chain.config.engineNewpayloadMaxExecute && - block.transactions.length <= this.chain.config.engineNewpayloadMaxTxsExecute - - const executed = - shouldExecuteBlock && - (await (async () => { - // just keeping its name different from the parentBlock to not confuse the context even - // though scope rules will not let it conflict with the parent of the new payload block - const blockParent = - i > 0 - ? blocks[i - 1] - : (this.chainCache.remoteBlocks.get( - bytesToHex(block.header.parentHash).slice(2), - ) ?? (await this.chain.getBlock(block.header.parentHash))) - const blockExecuted = await this.execution.runWithoutSetHead({ - block, - root: blockParent.header.stateRoot, - setHardfork: true, - parentBlock: blockParent, - }) - return blockExecuted - })()) - - // if can't be executed then return syncing/accepted - if (!executed) { - this.config.logger.debug( - `Skipping block(s) execution for headBlock=${headBlock.header.number} hash=${short( - headBlock.hash(), - )} : pendingBlocks=${blocks.length - i}(limit=${ - this.chain.config.engineNewpayloadMaxExecute - }) transactions=${block.transactions.length}(limit=${ - this.chain.config.engineNewpayloadMaxTxsExecute - }) executionBusy=${this.execution.running}`, - ) - // determined status to be returned depending on if block could extend chain or not - const status = optimisticLookup === true ? Status.SYNCING : Status.ACCEPTED - const response = { status, latestValidHash: null, validationError: null } + const errorMsg = `${error}`.toLowerCase() + if (errorMsg.includes('block') && errorMsg.includes('not found')) { + if (blocks.length > 1) { + // this error can come if the block tries to load a previous block yet not in the chain via BLOCKHASH + // opcode. + // + // i) error coding of the evm errors should be a better way to handle this OR + // ii) figure out a way to pass let the evm access the above blocks which is what connects this + // chain to vmhead. to be handled in skeleton refactoring to blockchain class + + const response = { status: Status.SYNCING, latestValidHash, validationError: null } return response } else { - this.executedBlocks.set(bytesToUnprefixedHex(block.hash()), block) + throw { + code: INTERNAL_ERROR, + message: errorMsg, + } } } - } - } catch (error) { - const latestValidHash = await validHash( - headBlock.header.parentHash, - this.chain, - this.chainCache, - ) - const errorMsg = `${error}`.toLowerCase() - if (errorMsg.includes('block') && errorMsg.includes('not found')) { - if (blocks.length > 1) { - // this error can come if the block tries to load a previous block yet not in the chain via BLOCKHASH - // opcode. - // - // i) error coding of the evm errors should be a better way to handle this OR - // ii) figure out a way to pass let the evm access the above blocks which is what connects this - // chain to vmhead. to be handled in skeleton refactoring to blockchain class - - const response = { status: Status.SYNCING, latestValidHash, validationError: null } - return response - } else { - throw { - code: INTERNAL_ERROR, - message: errorMsg, - } - } - } + const validationError = `Error verifying block while running: ${errorMsg}` + this.config.logger.error(validationError) - const validationError = `Error verifying block while running: ${errorMsg}` - this.config.logger.error(validationError) + const response = { status: Status.INVALID, latestValidHash, validationError } + this.invalidBlocks.set(blockHash.slice(2), error as Error) + this.remoteBlocks.delete(blockHash.slice(2)) + try { + await this.chain.blockchain.delBlock(lastBlock!.hash()) + // eslint-disable-next-line no-empty + } catch {} + try { + await this.skeleton.deleteBlock(lastBlock!) + // eslint-disable-next-line no-empty + } catch {} + return response + } - const response = { status: Status.INVALID, latestValidHash, validationError } - this.invalidBlocks.set(blockHash.slice(2), error as Error) - this.remoteBlocks.delete(blockHash.slice(2)) - try { - await this.chain.blockchain.delBlock(lastBlock!.hash()) - // eslint-disable-next-line no-empty - } catch {} - try { - await this.skeleton.deleteBlock(lastBlock!) - // eslint-disable-next-line no-empty - } catch {} + const response = { + status: Status.VALID, + latestValidHash: bytesToHex(headBlock.hash()), + validationError: null, + } return response + } catch (e) { + console.log('newPayload', e) + throw e } - - const response = { - status: Status.VALID, - latestValidHash: bytesToHex(headBlock.hash()), - validationError: null, - } - return response } /** @@ -1344,7 +1356,7 @@ export class Engine { throw Error(`runWithoutSetHead did not execute the block for payload=${payloadId}`) } - this.executedBlocks.set(bytesToUnprefixedHex(block.hash()), block) + // this.executedBlocks.set(bytesToUnprefixedHex(block.hash()), block) /** * Creates the payload in ExecutionPayloadV1 format to be returned */ @@ -1388,6 +1400,7 @@ export class Engine { ) return executionPayload } catch (error: any) { + console.log('getPayload', error) if (validEngineCodes.includes(error.code)) throw error throw { code: INTERNAL_ERROR, diff --git a/packages/client/src/rpc/modules/engine/util/newPayload.ts b/packages/client/src/rpc/modules/engine/util/newPayload.ts index 594f24b85f..9a9ddab07b 100644 --- a/packages/client/src/rpc/modules/engine/util/newPayload.ts +++ b/packages/client/src/rpc/modules/engine/util/newPayload.ts @@ -34,6 +34,7 @@ export const assembleBlock = async ( await block.validateData() return { block } } catch (error) { + console.log(error) const validationError = `Error assembling block from payload: ${error}` config.logger.error(validationError) const latestValidHash = await validHash( diff --git a/packages/client/src/rpc/modules/engine/validators.ts b/packages/client/src/rpc/modules/engine/validators.ts index a90704cb5d..5ce3b6ae1c 100644 --- a/packages/client/src/rpc/modules/engine/validators.ts +++ b/packages/client/src/rpc/modules/engine/validators.ts @@ -1,5 +1,43 @@ import { validators } from '../../validation.js' +const transaction = validators.hexOrObject( + validators.object({ + payload: validators.object({ + type: validators.nullOptional(validators.uint8), + chainId: validators.nullOptional(validators.uint64), + nonce: validators.nullOptional(validators.uint64), + maxFeesPerGas: validators.nullOptional( + validators.object({ + regular: validators.nullOptional(validators.uint256), + blob: validators.nullOptional(validators.uint256), + }), + ), + gas: validators.nullOptional(validators.uint64), + to: validators.nullOptional(validators.address), + value: validators.nullOptional(validators.uint256), + input: validators.nullOptional(validators.hex), + accessList: validators.nullOptional( + validators.array( + validators.object({ + address: validators.address, + storageKeys: validators.array(validators.bytes32), + }), + ), + ), + maxPriorityFeesPerGas: validators.nullOptional( + validators.object({ + regular: validators.nullOptional(validators.uint256), + blob: validators.nullOptional(validators.uint256), + }), + ), + blobVersionedHashes: validators.nullOptional(validators.array(validators.bytes32)), + }), + signature: validators.object({ + secp256k1: validators.nullOptional(validators.hex), + }), + }), +) + export const executionPayloadV1FieldValidators = { parentHash: validators.blockHash, feeRecipient: validators.address, @@ -14,7 +52,7 @@ export const executionPayloadV1FieldValidators = { extraData: validators.variableBytes32, baseFeePerGas: validators.uint256, blockHash: validators.blockHash, - transactions: validators.array(validators.hex), + transactions: validators.array(transaction), } export const executionPayloadV2FieldValidators = { ...executionPayloadV1FieldValidators, @@ -31,6 +69,7 @@ export const executionPayloadV4FieldValidators = { depositRequests: validators.array(validators.depositRequest()), withdrawalRequests: validators.array(validators.withdrawalRequest()), consolidationRequests: validators.array(validators.consolidationRequest()), + systemLogsRoot: validators.bytes32, } export const forkchoiceFieldValidators = { diff --git a/packages/client/src/rpc/modules/eth.ts b/packages/client/src/rpc/modules/eth.ts index d34c4434f8..43a89932d5 100644 --- a/packages/client/src/rpc/modules/eth.ts +++ b/packages/client/src/rpc/modules/eth.ts @@ -28,6 +28,7 @@ import { intToHex, isHexString, setLengthLeft, + ssz, toType, } from '@ethereumjs/util' import { @@ -818,7 +819,18 @@ export class Eth { } const tx = block.transactions[txIndex] - return toJSONRPCTx(tx, block, txIndex) + let inclusionProof = undefined + if (block.common.isActivatedEIP(6493)) { + inclusionProof = inclusionProof = { + transactionsRoot: block.header.transactionsTrie, + ...ssz.computeTransactionInclusionProof( + block.transactions.map((tx) => tx.sszRaw()), + txIndex, + ), + } + } + + return toJSONRPCTx(tx, block, txIndex, inclusionProof) } catch (error: any) { throw { code: INVALID_PARAMS, @@ -843,7 +855,18 @@ export class Eth { } const tx = block.transactions[txIndex] - return toJSONRPCTx(tx, block, txIndex) + let inclusionProof = undefined + if (block.common.isActivatedEIP(6493)) { + inclusionProof = inclusionProof = { + transactionsRoot: block.header.transactionsTrie, + ...ssz.computeTransactionInclusionProof( + block.transactions.map((tx) => tx.sszRaw()), + txIndex, + ), + } + } + + return toJSONRPCTx(tx, block, txIndex, inclusionProof) } catch (error: any) { throw { code: INVALID_PARAMS, @@ -864,8 +887,20 @@ export class Eth { if (!result) return null const [_receipt, blockHash, txIndex] = result const block = await this._chain.getBlock(blockHash) + const tx = block.transactions[txIndex] - return toJSONRPCTx(tx, block, txIndex) + let inclusionProof = undefined + if (block.common.isActivatedEIP(6493)) { + inclusionProof = { + transactionsRoot: block.header.transactionsTrie, + ...ssz.computeTransactionInclusionProof( + block.transactions.map((tx) => tx.sszRaw()), + txIndex, + ), + } + } + + return toJSONRPCTx(tx, block, txIndex, inclusionProof) } /** diff --git a/packages/client/src/rpc/validation.ts b/packages/client/src/rpc/validation.ts index cc929dfe4e..091644fcbc 100644 --- a/packages/client/src/rpc/validation.ts +++ b/packages/client/src/rpc/validation.ts @@ -184,6 +184,9 @@ export const validators = { get bytes256() { return (params: any[], index: number) => bytes(256, params, index) }, + get uint8() { + return (params: any[], index: number) => uint(8, params, index) + }, get uint64() { return (params: any[], index: number) => uint(64, params, index) }, @@ -586,6 +589,24 @@ export const validators = { } }, + get hexOrObject() { + return (validator: Function) => { + return (params: any[], index: number) => { + const validate = (field: any, validator: Function) => { + if (field === undefined) return + const v = validator([field], 0) + if (v !== undefined) return v + } + + if (typeof params[index] !== 'object') { + return validate(params[index], this.hex) + } + + return validator(params, index) + } + } + }, + /** * object validator to check if type is object with * required keys and expected validation of values @@ -750,6 +771,24 @@ export const validators = { } }, + get nullOptional() { + return (validator: any) => { + return (params: any, index: number) => { + if (params[index] === null) { + return + } + + if (params[index] === undefined) { + return { + code: INVALID_PARAMS, + message: `invalid undefined argument for nullOptional at ${index}`, + } + } + return validator(params, index) + } + } + }, + /** * Validator that passes if any of the specified validators pass * @param validator validator to check against the value diff --git a/packages/client/src/service/txpool.ts b/packages/client/src/service/txpool.ts index 6a1ef4201a..a66e53a9f8 100644 --- a/packages/client/src/service/txpool.ts +++ b/packages/client/src/service/txpool.ts @@ -839,6 +839,9 @@ export class TxPool { this.normalizedGasPrice(b, baseFee) - this.normalizedGasPrice(a, baseFee) < BIGINT_0, }) as QHeap for (const [address, txs] of byNonce) { + if (txs.length === 0) { + continue + } byPrice.insert(txs[0]) byNonce.set(address, txs.slice(1)) } diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index aa5faab06c..06acd42d98 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -109,6 +109,7 @@ export interface ClientOpts { gethGenesis?: string trustedSetup?: string mergeForkIdPostMerge?: boolean + eip6493AtPrague?: boolean bootnodes?: string | string[] port?: number extIP?: string diff --git a/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts b/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts new file mode 100644 index 0000000000..3b1b0171f4 --- /dev/null +++ b/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts @@ -0,0 +1,255 @@ +import { createTx } from '@ethereumjs/tx' +import { bigIntToAddressBytes, bigIntToHex, bytesToHex, hexToBytes } from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +import { beaconData } from '../../testdata/blocks/beacon.js' +import { postMergeData } from '../../testdata/geth-genesis/post-merge.js' +import { getRPCClient, setupChain } from '../helpers.js' + +const method = 'engine_newPayloadV4' +const [blockData] = beaconData + +const parentBeaconBlockRoot = '0x42942949c4ed512cd85c2cb54ca88591338cbb0564d3a2bea7961a639ef29d64' +const validForkChoiceState = { + headBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', + safeBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', + finalizedBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', +} +const validPayloadAttributes = { + timestamp: '0x64ba84fd', + prevRandao: '0xff00000000000000000000000000000000000000000000000000000000000000', + suggestedFeeRecipient: '0xaa00000000000000000000000000000000000000', +} + +const validPayload = [ + validForkChoiceState, + { + ...validPayloadAttributes, + withdrawals: [], + parentBeaconBlockRoot, + }, +] + +function readyEip6493Genesis(genesisJSON: any) { + const pragueTime = 1689945325 + // deep copy json and add shanghai and cancun to genesis to avoid contamination + const pragueJson = JSON.parse(JSON.stringify(genesisJSON)) + pragueJson.config.shanghaiTime = pragueTime + pragueJson.config.cancunTime = pragueTime + pragueJson.config.pragueTime = pragueTime + pragueJson.config.eip6493Time = pragueTime + pragueJson.config.chainId = 1223334 + // eslint-disable-next-line @typescript-eslint/no-use-before-define + Object.assign(pragueJson.alloc, electraGenesisContracts) + return { pragueJson, pragueTime } +} + +describe(`${method}: call with executionPayloadV4`, () => { + it('valid data', async () => { + // get the genesis json with late enougt date with respect to block data in batchBlocks + + const { pragueJson, pragueTime } = readyEip6493Genesis(postMergeData) + const { service, server, common } = await setupChain(pragueJson, 'post-merge', { engine: true }) + const rpc = getRPCClient(server) + const validBlock = { + ...blockData, + timestamp: bigIntToHex(BigInt(pragueTime)), + withdrawals: [], + blobGasUsed: '0x0', + excessBlobGas: '0x0', + depositRequests: [], + withdrawalRequests: [], + consolidationRequests: [], + systemLogsRoot: "0x3850240388ff8bed46a8631179e63ad67e28c343be54906cfaec0c3a2d95e71e", + receiptsRoot: '0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1', + parentHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', + stateRoot: '0x9d95c5098ef0f1b45fef49659318055ac4f06dc6601d7baf3656a391381981e3', + blockHash: '0x390042a0aefa4a11387652e215dd698a45dc5698d152ee0270a162e697420352', + } + let res + + res = await rpc.request(`eth_getBlockByNumber`, ['0x0', false]) + assert.equal(res.result.hash, validForkChoiceState.headBlockHash) + + res = await rpc.request(method, [validBlock, [], parentBeaconBlockRoot]) + console.log(res) + assert.equal(res.result.status, 'VALID') + + res = await rpc.request('engine_forkchoiceUpdatedV3', validPayload) + const payloadId = res.result.payloadId + assert.ok(payloadId !== undefined && payloadId !== null, 'valid payloadId should be received') + + // address 0x610adc49ecd66cbf176a8247ebd59096c031bd9f has been sufficiently funded in genesis + const pk = hexToBytes('0x9c9996335451aab4fc4eac58e31a8c300e095cdbcee532d53d09280e83360355') + const depositTx = createTx( + { + data: '0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001208cd4e5a69709cf8ee5b1b73d6efbf3f33bcac92fb7e4ce62b2467542fb50a72d0000000000000000000000000000000000000000000000000000000000000030ac842878bb70009552a4cfcad801d6e659c50bd50d7d03306790cb455ce7363c5b6972f0159d170f625a99b2064dbefc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020010000000000000000000000818ccb1c4eda80270b04d6df822b1e72dd83c3030000000000000000000000000000000000000000000000000000000000000060a747f75c72d0cf0d2b52504c7385b516f0523e2f0842416399f42b4aee5c6384a5674f6426b1cc3d0827886fa9b909e616f5c9f61f986013ed2b9bf37071cbae951136265b549f44e3c8e26233c0433e9124b7fd0dc86e82f9fedfc0a179d769', + value: 32000000000000000000n, + gasLimit: 15000000n, + maxFeePerGas: 100n, + type: 2, + to: '0x00000000219ab540356cBB839Cbe05303d7705Fa', + }, + { common }, + ).sign(pk) + await service.txPool.add(depositTx, true) + + const normalLegacyTx = createTx( + { + data: '0x', + value: 32000000000000000000n, + gasLimit: 15000000n, + gasPrice: 100n, + type: 0, + to: '0x10000000219ab540356cBB839Cbe05303d7705Fa', + nonce: 1, + }, + { common }, + ).sign(pk) + await service.txPool.add(normalLegacyTx, true) + + console.log({ + normalLegacyTx: normalLegacyTx.toJSON(), + payloadjson: normalLegacyTx.toExecutionPayloadTx(), + }) + + res = await rpc.request('engine_getPayloadV4', [payloadId]) + const { executionPayload } = res.result + assert.ok(executionPayload.transactions.length === 2, 'two transactions should have been added') + assert.ok( + executionPayload.depositRequests?.length === 1, + 'depositRequests should have 1 deposit request', + ) + assert.ok( + executionPayload.withdrawalRequests !== undefined, + 'depositRequests field should be received', + ) + + console.log(executionPayload) + + res = await rpc.request(method, [executionPayload, [], parentBeaconBlockRoot]) + assert.equal(res.result.status, 'VALID') + + const newBlockHashHex = executionPayload.blockHash + // add this block to the blockchain + res = await rpc.request('engine_forkchoiceUpdatedV3', [ + { + safeBlockHash: newBlockHashHex, + finalizedBlockHash: newBlockHashHex, + headBlockHash: newBlockHashHex, + }, + null, + ]) + console.log(res) + assert.equal(res.result.payloadStatus.status, 'VALID') + + const ivcContractHex = bytesToHex(bigIntToAddressBytes(common.param('ivcPredeployAddress'))) + + res = await rpc.request('eth_getStorageAt', [ + ivcContractHex, + '0x4026bcffe6920ff0e02a91018a719f2080a2463f25b23d34d6ed73aadae3264a', + 'latest', + ]) + assert.equal( + res.result, + '0x88cce54f379f5607098522664e399bf4fee6f3e90127f8fc88f760fd4529211b', + 'ivc root at updated topic should match', + ) + }) +}) + +const electraGenesisContracts = { + // sender corresponding to the priv key 0x9c9996335451aab4fc4eac58e31a8c300e095cdbcee532d53d09280e83360355 + '0x610adc49ecd66cbf176a8247ebd59096c031bd9f': { balance: '0x6d6172697573766477000000' }, + // eip 2925 contract + '0x0aae40965e6800cd9b1f4b05ff21581047e3f91e': { + balance: '0', + nonce: '1', + code: '0x3373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35611fff60014303165500', + }, + // consolidation requests contract + '0x00b42dbF2194e931E80326D950320f7d9Dbeac02': { + balance: '0', + nonce: '1', + code: '0x3373fffffffffffffffffffffffffffffffffffffffe146098573615156028575f545f5260205ff35b36606014156101445760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061014457600154600101600155600354806004026004013381556001015f35815560010160203581556001016040359055600101600355005b6003546002548082038060011160ac575060015b5f5b81811460f15780607402838201600402600401805490600101805490600101805490600101549260601b84529083601401528260340152906054015260010160ae565b9101809214610103579060025561010e565b90505f6002555f6003555b5f548061049d141561011d57505f5b6001546001828201116101325750505f610138565b01600190035b5f555f6001556074025ff35b5f5ffd', + }, + // withdrawals request contract + '0x00A3ca265EBcb825B45F985A16CEFB49958cE017': { + balance: '0', + nonce: '1', + code: '0x3373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b366038141561012e5760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061012e57600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460dd5780604c02838201600302600401805490600101805490600101549160601b83528260140152906034015260010160a6565b910180921460ed579060025560f8565b90505f6002555f6003555b5f548061049d141561010757505f5b60015460028282011161011c5750505f610122565b01600290035b5f555f600155604c025ff35b5f5ffd', + storage: { + '0x0000000000000000000000000000000000000000000000000000000000000000': + '0x000000000000000000000000000000000000000000000000000000000000049d', + }, + }, + // beacon deposit contract for deposit receipts + '0x00000000219ab540356cBB839Cbe05303d7705Fa': { + balance: '0', + code: '0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a2646970667358221220dceca8706b29e917dacf25fceef95acac8d90d765ac926663ce4096195952b6164736f6c634300060b0033', + storage: { + '0x0000000000000000000000000000000000000000000000000000000000000022': + '0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b', + '0x0000000000000000000000000000000000000000000000000000000000000023': + '0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71', + '0x0000000000000000000000000000000000000000000000000000000000000024': + '0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c', + '0x0000000000000000000000000000000000000000000000000000000000000025': + '0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c', + '0x0000000000000000000000000000000000000000000000000000000000000026': + '0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30', + '0x0000000000000000000000000000000000000000000000000000000000000027': + '0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1', + '0x0000000000000000000000000000000000000000000000000000000000000028': + '0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c', + '0x0000000000000000000000000000000000000000000000000000000000000029': + '0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193', + '0x000000000000000000000000000000000000000000000000000000000000002a': + '0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1', + '0x000000000000000000000000000000000000000000000000000000000000002b': + '0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b', + '0x000000000000000000000000000000000000000000000000000000000000002c': + '0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220', + '0x000000000000000000000000000000000000000000000000000000000000002d': + '0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f', + '0x000000000000000000000000000000000000000000000000000000000000002e': + '0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e', + '0x000000000000000000000000000000000000000000000000000000000000002f': + '0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784', + '0x0000000000000000000000000000000000000000000000000000000000000030': + '0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb', + '0x0000000000000000000000000000000000000000000000000000000000000031': + '0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb', + '0x0000000000000000000000000000000000000000000000000000000000000032': + '0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab', + '0x0000000000000000000000000000000000000000000000000000000000000033': + '0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4', + '0x0000000000000000000000000000000000000000000000000000000000000034': + '0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f', + '0x0000000000000000000000000000000000000000000000000000000000000035': + '0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa', + '0x0000000000000000000000000000000000000000000000000000000000000036': + '0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c', + '0x0000000000000000000000000000000000000000000000000000000000000037': + '0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167', + '0x0000000000000000000000000000000000000000000000000000000000000038': + '0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7', + '0x0000000000000000000000000000000000000000000000000000000000000039': + '0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0', + '0x000000000000000000000000000000000000000000000000000000000000003a': + '0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544', + '0x000000000000000000000000000000000000000000000000000000000000003b': + '0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765', + '0x000000000000000000000000000000000000000000000000000000000000003c': + '0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4', + '0x000000000000000000000000000000000000000000000000000000000000003d': + '0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1', + '0x000000000000000000000000000000000000000000000000000000000000003e': + '0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636', + '0x000000000000000000000000000000000000000000000000000000000000003f': + '0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c', + '0x0000000000000000000000000000000000000000000000000000000000000040': + '0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7', + }, + }, +} diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 091fae9ce4..8e4d2a77ec 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -327,6 +327,16 @@ export const eipsDict: EIPsDict = { minimumHardfork: Hardfork.London, requiredEIPs: [4750, 5450], }, + /** + * Description : SSZ Transaction Signature Scheme + * URL : https://eips.ethereum.org/EIPS/eip-6493 + * Status : Draft + */ + 6493: { + // TODO: Set correct minimum hardfork + minimumHardfork: Hardfork.Cancun, + requiredEIPs: [], + }, /** * Description : SELFDESTRUCT only in same transaction * URL : https://eips.ethereum.org/EIPS/eip-6780 diff --git a/packages/common/src/enums.ts b/packages/common/src/enums.ts index d4fbf08b0a..9c82bedd61 100644 --- a/packages/common/src/enums.ts +++ b/packages/common/src/enums.ts @@ -71,6 +71,7 @@ export enum Hardfork { Shanghai = 'shanghai', Cancun = 'cancun', Prague = 'prague', + Eip6493 = 'eip6493', Osaka = 'osaka', } diff --git a/packages/common/src/hardforks.ts b/packages/common/src/hardforks.ts index 527ae642a1..c57422ca78 100644 --- a/packages/common/src/hardforks.ts +++ b/packages/common/src/hardforks.ts @@ -162,6 +162,14 @@ export const hardforksDict: HardforksDict = { //eips: [663, 3540, 3670, 4200, 4750, 5450, 6206, 7069, 7480, 7620, 7692, 7698], // This is EOF-only eips: [2537, 2935, 6110, 7002, 7251, 7685, 7702], // This is current prague without EOF }, + /** + * Description: Experimental hardfork to test eip 6493 for 6493 devnets will be removed(incomplete/experimental) + * URL : + * Status : Final + */ + eip6493: { + eips: [6493], + }, /** * Description: Next feature hardfork after prague, internally used for verkle testing/implementation (incomplete/experimental) * URL : https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/osaka.md diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 6fcbc92f44..9c348adf03 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -145,6 +145,7 @@ function parseGethParams(json: any) { [Hardfork.Shanghai]: { name: 'shanghaiTime', postMerge: true, isTimestamp: true }, [Hardfork.Cancun]: { name: 'cancunTime', postMerge: true, isTimestamp: true }, [Hardfork.Prague]: { name: 'pragueTime', postMerge: true, isTimestamp: true }, + [Hardfork.Eip6493]: { name: 'eip6493Time', postMerge: true, isTimestamp: true }, [Hardfork.Osaka]: { name: 'osakaTime', postMerge: true, isTimestamp: true }, } diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 869b479db7..b28a324744 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -84,6 +84,7 @@ export class EVM implements EVMInterface { Hardfork.Shanghai, Hardfork.Cancun, Hardfork.Prague, + Hardfork.Eip6493, Hardfork.Osaka, ] protected _tx?: { @@ -178,7 +179,7 @@ export class EVM implements EVMInterface { // Supported EIPs const supportedEIPs = [ 663, 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3198, 3529, 3540, 3541, 3607, 3651, 3670, - 3855, 3860, 4200, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6780, 6800, + 3855, 3860, 4200, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6493, 6780, 6800, 7002, 7069, 7251, 7480, 7516, 7620, 7685, 7692, 7698, 7702, 7709, ] diff --git a/packages/tx/package.json b/packages/tx/package.json index f9d320e003..fb0414d844 100644 --- a/packages/tx/package.json +++ b/packages/tx/package.json @@ -54,6 +54,7 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/common": "^4.4.0", "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/util": "^9.1.0", diff --git a/packages/tx/src/1559/constructors.ts b/packages/tx/src/1559/constructors.ts index 623659832e..7663bb6855 100644 --- a/packages/tx/src/1559/constructors.ts +++ b/packages/tx/src/1559/constructors.ts @@ -1,5 +1,11 @@ import { RLP } from '@ethereumjs/rlp' -import { bytesToBigInt, bytesToHex, equalsBytes, validateNoLeadingZeroes } from '@ethereumjs/util' +import { + bigIntToUnpaddedBytes, + bytesToBigInt, + bytesToHex, + equalsBytes, + validateNoLeadingZeroes, +} from '@ethereumjs/util' import { TransactionType } from '../types.js' import { txTypeBytes, validateNotArray } from '../util.js' @@ -8,6 +14,9 @@ import { FeeMarket1559Tx } from './tx.js' import type { TxOptions } from '../types.js' import type { TxData, TxValuesArray } from './tx.js' +import type { ValueOf } from '@chainsafe/ssz' +import type { ssz } from '@ethereumjs/util' +export type Eip1559TransactionType = ValueOf /** * Instantiate a transaction from a data dictionary. @@ -98,3 +107,46 @@ export function createFeeMarket1559TxFromRLP(serialized: Uint8Array, opts: TxOpt return create1559FeeMarketTxFromBytesArray(values as TxValuesArray, opts) } + +export function createFeeMarket1559TxFromSszTx( + sszWrappedTx: Eip1559TransactionType, + opts: TxOptions = {}, +) { + const { + payload: { + nonce, + chainId, + maxFeesPerGas: { regular: maxFeePerGas }, + gas: gasLimit, + to, + value, + input: data, + accessList, + maxPriorityFeesPerGas: { regular: maxPriorityFeePerGas }, + }, + signature: { secp256k1 }, + } = sszWrappedTx + + // TODO: bytes to bigint => bigint to unpadded bytes seem redundant and set for optimization + const r = bytesToBigInt(secp256k1.slice(0, 32)) + const s = bytesToBigInt(secp256k1.slice(32, 64)) + const v = bytesToBigInt(secp256k1.slice(64)) + + return create1559FeeMarketTxFromBytesArray( + [ + bigIntToUnpaddedBytes(chainId), + bigIntToUnpaddedBytes(nonce), + bigIntToUnpaddedBytes(maxPriorityFeePerGas), + bigIntToUnpaddedBytes(maxFeePerGas), + bigIntToUnpaddedBytes(gasLimit), + to ?? new Uint8Array(0), + bigIntToUnpaddedBytes(value), + data, + accessList.map(({ address, storageKeys }) => [address, storageKeys]), + bigIntToUnpaddedBytes(v), + bigIntToUnpaddedBytes(r), + bigIntToUnpaddedBytes(s), + ], + opts, + ) +} diff --git a/packages/tx/src/1559/tx.ts b/packages/tx/src/1559/tx.ts index 372f824e31..fe71419b69 100644 --- a/packages/tx/src/1559/tx.ts +++ b/packages/tx/src/1559/tx.ts @@ -6,6 +6,7 @@ import { bigIntToHex, bigIntToUnpaddedBytes, bytesToBigInt, + setLengthLeft, toBytes, } from '@ethereumjs/util' @@ -20,6 +21,7 @@ import { AccessLists, validateNotArray } from '../util.js' import { createFeeMarket1559Tx } from './constructors.js' +import type { SSZTransactionType } from '../baseTransaction.js' import type { AccessList, AccessListBytes, @@ -164,6 +166,38 @@ export class FeeMarket1559Tx extends BaseTransaction ({ address, storageKeys })), + maxPriorityFeesPerGas: { regular: this.maxPriorityFeePerGas, blob: null }, + blobVersionedHashes: null, + authorizationList: null, + } + + const yParity = this.v + const signature = { + secp256k1: Uint8Array.from([ + ...setLengthLeft(bigIntToUnpaddedBytes(this.r), 32), + ...setLengthLeft(bigIntToUnpaddedBytes(this.s), 32), + ...setLengthLeft(bigIntToUnpaddedBytes(yParity), 1), + ]), + } + + return { payload, signature } + } + /** * Returns the serialized encoding of the EIP-1559 transaction. * diff --git a/packages/tx/src/2930/constructors.ts b/packages/tx/src/2930/constructors.ts index 7373239e97..bb3d068289 100644 --- a/packages/tx/src/2930/constructors.ts +++ b/packages/tx/src/2930/constructors.ts @@ -1,5 +1,11 @@ import { RLP } from '@ethereumjs/rlp' -import { bytesToBigInt, bytesToHex, equalsBytes, validateNoLeadingZeroes } from '@ethereumjs/util' +import { + bigIntToUnpaddedBytes, + bytesToBigInt, + bytesToHex, + equalsBytes, + validateNoLeadingZeroes, +} from '@ethereumjs/util' import { TransactionType } from '../types.js' import { txTypeBytes, validateNotArray } from '../util.js' @@ -8,6 +14,9 @@ import { AccessList2930Transaction } from './tx.js' import type { AccessList, TxOptions } from '../types.js' import type { TxData, TxValuesArray } from './tx.js' +import type { ValueOf } from '@chainsafe/ssz' +import type { ssz } from '@ethereumjs/util' +export type Eip2930TransactionType = ValueOf /** * Instantiate a transaction from a data dictionary. @@ -86,3 +95,43 @@ export function createAccessList2930TxFromRLP(serialized: Uint8Array, opts: TxOp return createAccessList2930TxFromBytesArray(values as TxValuesArray, opts) } + +export function createAccessList2930TxFromSszTx( + sszWrappedTx: Eip2930TransactionType, + opts: TxOptions = {}, +) { + const { + payload: { + nonce, + chainId, + maxFeesPerGas: { regular: gasPrice }, + gas: gasLimit, + to, + value, + input: data, + accessList, + }, + signature: { secp256k1 }, + } = sszWrappedTx + + const r = bytesToBigInt(secp256k1.slice(0, 32)) + const s = bytesToBigInt(secp256k1.slice(32, 64)) + const v = bytesToBigInt(secp256k1.slice(64)) + + return createAccessList2930TxFromBytesArray( + [ + bigIntToUnpaddedBytes(chainId), + bigIntToUnpaddedBytes(nonce), + bigIntToUnpaddedBytes(gasPrice), + bigIntToUnpaddedBytes(gasLimit), + to, + bigIntToUnpaddedBytes(value), + data, + accessList.map(({ address, storageKeys }) => [address, storageKeys]), + bigIntToUnpaddedBytes(v), + bigIntToUnpaddedBytes(r), + bigIntToUnpaddedBytes(s), + ] as TxValuesArray, + opts, + ) +} diff --git a/packages/tx/src/2930/tx.ts b/packages/tx/src/2930/tx.ts index 10565728db..118fa6b41e 100644 --- a/packages/tx/src/2930/tx.ts +++ b/packages/tx/src/2930/tx.ts @@ -5,6 +5,7 @@ import { bigIntToHex, bigIntToUnpaddedBytes, bytesToBigInt, + setLengthLeft, toBytes, } from '@ethereumjs/util' @@ -18,6 +19,7 @@ import { AccessLists, validateNotArray } from '../util.js' import { createAccessList2930Tx } from './constructors.js' +import type { SSZTransactionType } from '../baseTransaction.js' import type { AccessList, AccessListBytes, @@ -146,6 +148,39 @@ export class AccessList2930Transaction extends BaseTransaction ({ address, storageKeys })), + maxPriorityFeesPerGas: null, + blobVersionedHashes: null, + authorizationList: null, + } + + const yParity = this.v + + const signature = { + secp256k1: Uint8Array.from([ + ...setLengthLeft(bigIntToUnpaddedBytes(this.r), 32), + ...setLengthLeft(bigIntToUnpaddedBytes(this.s), 32), + ...setLengthLeft(bigIntToUnpaddedBytes(yParity), 1), + ]), + } + + return { payload, signature } + } + /** * Returns the serialized encoding of the EIP-2930 transaction. * diff --git a/packages/tx/src/4844/constructors.ts b/packages/tx/src/4844/constructors.ts index ec2ff0ecec..063a3c06e5 100644 --- a/packages/tx/src/4844/constructors.ts +++ b/packages/tx/src/4844/constructors.ts @@ -1,6 +1,7 @@ import { RLP } from '@ethereumjs/rlp' import { bigIntToHex, + bigIntToUnpaddedBytes, blobsToCommitments, blobsToProofs, bytesToBigInt, @@ -24,7 +25,10 @@ import type { TxOptions, } from '../types.js' import type { TxData, TxValuesArray } from './tx.js' -import type { KZG, PrefixedHexString } from '@ethereumjs/util' +import type { ValueOf } from '@chainsafe/ssz' +import type { KZG, PrefixedHexString, ssz } from '@ethereumjs/util' + +export type Eip4844TransactionType = ValueOf const validateBlobTransactionNetworkWrapper = ( blobVersionedHashes: PrefixedHexString[], @@ -334,3 +338,48 @@ export function blobTxNetworkWrapperToJSON( kzgProofs: tx.kzgProofs!, } } + +export function createBlob4844TxFromSszTx( + sszWrappedTx: Eip4844TransactionType, + opts: TxOptions = {}, +) { + const { + payload: { + nonce, + chainId, + maxFeesPerGas: { regular: maxFeePerGas, blob: maxFeePerBlobGas }, + gas: gasLimit, + to, + value, + input: data, + accessList, + maxPriorityFeesPerGas: { regular: maxPriorityFeePerGas }, + blobVersionedHashes, + }, + signature: { secp256k1 }, + } = sszWrappedTx + + const r = bytesToBigInt(secp256k1.slice(0, 32)) + const s = bytesToBigInt(secp256k1.slice(32, 64)) + const v = bytesToBigInt(secp256k1.slice(64)) + + return createBlob4844TxFromBytesArray( + [ + bigIntToUnpaddedBytes(chainId), + bigIntToUnpaddedBytes(nonce), + bigIntToUnpaddedBytes(maxPriorityFeePerGas), + bigIntToUnpaddedBytes(maxFeePerGas), + bigIntToUnpaddedBytes(gasLimit), + to, + bigIntToUnpaddedBytes(value), + data, + accessList.map(({ address, storageKeys }) => [address, storageKeys]), + bigIntToUnpaddedBytes(maxFeePerBlobGas), + blobVersionedHashes, + bigIntToUnpaddedBytes(v), + bigIntToUnpaddedBytes(r), + bigIntToUnpaddedBytes(s), + ], + opts, + ) +} diff --git a/packages/tx/src/4844/tx.ts b/packages/tx/src/4844/tx.ts index 909f462a55..0e5b0646a9 100644 --- a/packages/tx/src/4844/tx.ts +++ b/packages/tx/src/4844/tx.ts @@ -8,6 +8,7 @@ import { bigIntToUnpaddedBytes, bytesToBigInt, hexToBytes, + setLengthLeft, toBytes, toType, } from '@ethereumjs/util' @@ -24,6 +25,7 @@ import { AccessLists, validateNotArray } from '../util.js' import { createBlob4844Tx } from './constructors.js' +import type { SSZTransactionType } from '../baseTransaction.js' import type { AccessList, AccessListBytes, @@ -217,6 +219,42 @@ export class Blob4844Tx extends BaseTransaction { ] } + sszRaw(): SSZTransactionType { + if (this.r === undefined || this.s === undefined || this.v === undefined) { + throw Error(`Transaction not signed for sszSerialize`) + } + + const payload = { + type: BigInt(this.type), + chainId: this.chainId, + nonce: this.nonce, + maxFeesPerGas: { regular: this.maxFeePerGas, blob: this.maxFeePerBlobGas }, + gas: this.gasLimit, + to: this.to?.bytes ?? null, + value: this.value, + input: this.data, + accessList: this.accessList.map(([address, storageKeys]) => ({ address, storageKeys })), + maxPriorityFeesPerGas: { + regular: this.maxPriorityFeePerGas, + blob: this.maxPriorityFeePerGas, + }, + blobVersionedHashes: this.blobVersionedHashes.map((vh) => hexToBytes(vh)), + authorizationList: null, + } + + const yParity = this.v + + const signature = { + secp256k1: Uint8Array.from([ + ...setLengthLeft(bigIntToUnpaddedBytes(this.r), 32), + ...setLengthLeft(bigIntToUnpaddedBytes(this.s), 32), + ...setLengthLeft(bigIntToUnpaddedBytes(yParity), 1), + ]), + } + + return { payload, signature } + } + /** * Returns the serialized encoding of the EIP-4844 transaction. * diff --git a/packages/tx/src/7702/tx.ts b/packages/tx/src/7702/tx.ts index d9ccc9086c..c78f524678 100644 --- a/packages/tx/src/7702/tx.ts +++ b/packages/tx/src/7702/tx.ts @@ -20,6 +20,7 @@ import { AccessLists, AuthorizationLists, validateNotArray } from '../util.js' import { createEOACode7702Tx } from './constructors.js' +import type { SSZTransactionType } from '../baseTransaction.js' import type { AccessList, AccessListBytes, @@ -184,6 +185,9 @@ export class EOACode7702Transaction extends BaseTransaction /** * This base class will likely be subject to further @@ -247,6 +251,7 @@ export abstract class BaseTransaction * representation for external signing use {@link BaseTransaction.getMessageToSign}. */ abstract raw(): TxValuesArray[T] + abstract sszRaw(): SSZTransactionType /** * Returns the encoding of the transaction. @@ -361,6 +366,10 @@ export abstract class BaseTransaction } } + toExecutionPayloadTx(): ssz.TransactionV1 { + return toPayloadJson(this.sszRaw()) + } + /** * Returns a new transaction with the same data fields as the current, but now signed * @param v The `v` value of the signature diff --git a/packages/tx/src/index.ts b/packages/tx/src/index.ts index 281e7aeff6..2021b66af6 100644 --- a/packages/tx/src/index.ts +++ b/packages/tx/src/index.ts @@ -11,6 +11,7 @@ export * from './params.js' export { createTx, createTxFromBlockBodyData, + createTxFromExecutionPayloadTx, createTxFromJSONRPCProvider, createTxFromRLP, createTxFromRPC, diff --git a/packages/tx/src/legacy/constructors.ts b/packages/tx/src/legacy/constructors.ts index f69acedb25..30c77dbef8 100644 --- a/packages/tx/src/legacy/constructors.ts +++ b/packages/tx/src/legacy/constructors.ts @@ -1,10 +1,15 @@ import { RLP } from '@ethereumjs/rlp' -import { validateNoLeadingZeroes } from '@ethereumjs/util' +import { BIGINT_2, bytesToBigInt, validateNoLeadingZeroes } from '@ethereumjs/util' import { LegacyTx } from './tx.js' import type { TxOptions } from '../types.js' import type { TxData, TxValuesArray } from './tx.js' +import type { ValueOf } from '@chainsafe/ssz' +import type { ssz } from '@ethereumjs/util' + +export type ReplayableTransactionType = ValueOf +export type LegacyTransactionType = ValueOf /** * Instantiate a transaction from a data dictionary. @@ -67,3 +72,37 @@ export function createLegacyTxFromRLP(serialized: Uint8Array, opts: TxOptions = return createLegacyTxFromBytesArray(values as TxValuesArray, opts) } + +export function createLegacyTxFromSszTx( + sszWrappedTx: ReplayableTransactionType | LegacyTransactionType, + opts: TxOptions = {}, +) { + const { + payload: { + nonce, + chainId, + maxFeesPerGas: { regular: gasPrice }, + gas: gasLimit, + to, + value, + input: data, + }, + signature: { secp256k1 }, + } = sszWrappedTx as LegacyTransactionType + + const r = bytesToBigInt(secp256k1.slice(0, 32)) + const s = bytesToBigInt(secp256k1.slice(32, 64)) + const yParity = bytesToBigInt(secp256k1.slice(64)) + + let v + if (chainId !== null && chainId !== undefined) { + v = yParity + BIGINT_2 * chainId + BigInt(35) + } else { + v = yParity + BigInt(27) + } + + return createLegacyTxFromBytesArray( + [nonce, gasPrice, gasLimit, to, value, data, v, r, s] as TxValuesArray, + opts, + ) +} diff --git a/packages/tx/src/legacy/tx.ts b/packages/tx/src/legacy/tx.ts index 2b02ec2e02..e7c5b7b1f1 100644 --- a/packages/tx/src/legacy/tx.ts +++ b/packages/tx/src/legacy/tx.ts @@ -1,12 +1,17 @@ import { Common } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' import { + BIGINT_0, + BIGINT_1, BIGINT_2, BIGINT_8, MAX_INTEGER, + bigIntToBytes, bigIntToHex, bigIntToUnpaddedBytes, bytesToBigInt, + calculateSigRecovery, + setLengthLeft, toBytes, unpadBytes, } from '@ethereumjs/util' @@ -20,6 +25,7 @@ import { validateNotArray } from '../util.js' import { createLegacyTx } from './constructors.js' +import type { SSZTransactionType } from '../baseTransaction.js' import type { TxData as AllTypesTxData, TxValuesArray as AllTypesTxValuesArray, @@ -128,6 +134,43 @@ export class LegacyTx extends BaseTransaction { ] } + sszRaw(): SSZTransactionType { + if (this.r === undefined || this.s === undefined || this.v === undefined) { + throw Error(`Transaction not signed for sszSerialize`) + } + + const chainId = this.supports(Capability.EIP155ReplayProtection) ? this.common.chainId() : null + const payload = { + type: BigInt(this.type), + chainId, + nonce: this.nonce, + maxFeesPerGas: { regular: this.gasPrice, blob: null }, + gas: this.gasLimit, + to: this.to?.bytes ?? null, + value: this.value, + input: this.data, + accessList: null, + maxPriorityFeesPerGas: null, + blobVersionedHashes: null, + authorizationList: null, + } + + const yParity = calculateSigRecovery(this.v, chainId ?? undefined) + if (yParity !== BIGINT_0 && yParity !== BIGINT_1) { + throw Error(`Invalid yParity=${yParity} v=${this.v} chainid:${this.common.chainId()}`) + } + + const signature = { + secp256k1: Uint8Array.from([ + ...setLengthLeft(bigIntToBytes(this.r), 32), + ...setLengthLeft(bigIntToBytes(this.s), 32), + ...setLengthLeft(bigIntToBytes(yParity), 1), + ]), + } + + return { payload, signature } + } + /** * Returns the serialized encoding of the legacy transaction. * diff --git a/packages/tx/src/transactionFactory.ts b/packages/tx/src/transactionFactory.ts index a9d2153ec7..59d988f0f3 100644 --- a/packages/tx/src/transactionFactory.ts +++ b/packages/tx/src/transactionFactory.ts @@ -1,13 +1,26 @@ import { fetchFromProvider, getProvider } from '@ethereumjs/util' -import { createFeeMarket1559Tx, createFeeMarket1559TxFromRLP } from './1559/constructors.js' -import { createAccessList2930Tx, createAccessList2930TxFromRLP } from './2930/constructors.js' -import { createBlob4844Tx, createBlob4844TxFromRLP } from './4844/constructors.js' +import { + createFeeMarket1559Tx, + createFeeMarket1559TxFromRLP, + createFeeMarket1559TxFromSszTx, +} from './1559/constructors.js' +import { + createAccessList2930Tx, + createAccessList2930TxFromRLP, + createAccessList2930TxFromSszTx, +} from './2930/constructors.js' +import { + createBlob4844Tx, + createBlob4844TxFromRLP, + createBlob4844TxFromSszTx, +} from './4844/constructors.js' import { createEOACode7702Tx, createEOACode7702TxFromRLP } from './7702/constructors.js' import { createLegacyTx, createLegacyTxFromBytesArray, createLegacyTxFromRLP, + createLegacyTxFromSszTx, } from './legacy/constructors.js' import { TransactionType, @@ -17,10 +30,15 @@ import { isFeeMarket1559TxData, isLegacyTxData, } from './types.js' -import { normalizeTxParams } from './util.js' +import { fromPayloadJson, normalizeTxParams } from './util.js' +import type { Eip1559TransactionType } from './1559/constructors.js' +import type { Eip2930TransactionType } from './2930/constructors.js' +import type { Eip4844TransactionType } from './4844/constructors.js' +import type { LegacyTransactionType, ReplayableTransactionType } from './legacy/constructors.js' import type { Transaction, TxData, TxOptions, TypedTxData } from './types.js' -import type { EthersProvider } from '@ethereumjs/util' +import type { SSZTransaction } from './util.js' +import type { EthersProvider, ssz } from '@ethereumjs/util' /** * Create a transaction from a `txData` object * @@ -139,3 +157,45 @@ export async function createTxFromJSONRPCProvider( } return createTxFromRPC(txData, txOptions) } + +export function createTxFromSszTx( + sszStableTx: SSZTransaction, + txOptions: TxOptions = {}, +): Transaction[T] { + const txType = Number(sszStableTx.payload.type) + + switch (txType) { + case TransactionType.Legacy: + return createLegacyTxFromSszTx( + sszStableTx as ReplayableTransactionType | LegacyTransactionType, + txOptions, + ) as Transaction[T] + case TransactionType.AccessListEIP2930: + return createAccessList2930TxFromSszTx( + sszStableTx as Eip2930TransactionType, + txOptions, + ) as Transaction[T] + case TransactionType.FeeMarketEIP1559: + return createFeeMarket1559TxFromSszTx( + sszStableTx as Eip1559TransactionType, + txOptions, + ) as Transaction[T] + case TransactionType.BlobEIP4844: + return createBlob4844TxFromSszTx( + sszStableTx as Eip4844TransactionType, + txOptions, + ) as Transaction[T] + case TransactionType.EOACodeEIP7702: + throw Error('not implemented') + default: + throw new Error(`TypedTransaction with ID ${txType} unknown`) + } +} + +export function createTxFromExecutionPayloadTx( + data: ssz.TransactionV1, + txOptions: TxOptions = {}, +): Transaction[T] { + const sszStableTx = fromPayloadJson(data) + return createTxFromSszTx(sszStableTx, txOptions) +} diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index 118a5f6791..32dcf987e0 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -576,6 +576,7 @@ export interface JSONRPCTx { maxFeePerBlobGas?: string // QUANTITY - max data fee for blob transactions blobVersionedHashes?: string[] // DATA - array of 32 byte versioned hashes for blob transactions yParity?: string // DATA - parity of the y-coordinate of the public key + inclusionProof?: { merkleBranch: string[]; transactionsRoot: string; transactionRoot: string } // DATA - array of 32 byte merkle hash for eip 6493 inclusion proof with 0 as transactions root } /* diff --git a/packages/tx/src/util.ts b/packages/tx/src/util.ts index 7e6f30964b..474a9c7053 100644 --- a/packages/tx/src/util.ts +++ b/packages/tx/src/util.ts @@ -6,8 +6,10 @@ import { type PrefixedHexString, SECP256K1_ORDER_DIV_2, TypeOutput, + bigIntToHex, bytesToBigInt, bytesToHex, + hexToBigInt, hexToBytes, setLengthLeft, toBytes, @@ -27,7 +29,9 @@ import type { TransactionType, TypedTxData, } from './types.js' +import type { ValueOf } from '@chainsafe/ssz' import type { Common } from '@ethereumjs/common' +import type { ssz } from '@ethereumjs/util' export function checkMaxInitCodeSize(common: Common, length: number) { const maxInitCodeSize = common.param('maxInitCodeSize') @@ -305,3 +309,138 @@ export const normalizeTxParams = (txParamsFromRPC: any): TypedTxData => { return txParams } + +function getDataOrNull(elem: PrefixedHexString | null) { + if (elem === null) { + return null + } + + return hexToBytes(elem) +} + +function getQuantityOrNull(elem: PrefixedHexString | null) { + if (elem === null) { + return null + } + + return hexToBigInt(elem) +} + +export type SSZTransaction = ValueOf +export function fromPayloadJson(payloadTx: ssz.TransactionV1): SSZTransaction { + const { payload, signature } = payloadTx + return { + payload: { + type: getQuantityOrNull(payload.type), + chainId: getQuantityOrNull(payload.chainId), + nonce: getQuantityOrNull(payload.nonce), + maxFeesPerGas: payload.maxFeesPerGas + ? { + regular: getQuantityOrNull(payload.maxFeesPerGas.regular), + blob: getQuantityOrNull(payload.maxFeesPerGas.blob), + } + : null, + gas: getQuantityOrNull(payload.gas), + to: getDataOrNull(payload.to), + value: getQuantityOrNull(payload.value), + input: getDataOrNull(payload.input), + accessList: payload.accessList + ? payload.accessList.map((pal) => { + return { + address: hexToBytes(pal.address), + storageKeys: pal.storageKeys.map((sk) => hexToBytes(sk)), + } + }) + : null, + maxPriorityFeesPerGas: payload.maxPriorityFeesPerGas + ? { + regular: getQuantityOrNull(payload.maxPriorityFeesPerGas.regular), + blob: getQuantityOrNull(payload.maxPriorityFeesPerGas.blob), + } + : null, + blobVersionedHashes: payload.blobVersionedHashes?.map((vh) => hexToBytes(vh)) ?? null, + authorizationList: + payload.authorizationList?.map((al) => ({ + payload: { + magic: getQuantityOrNull(al.payload.magic), + chainId: getQuantityOrNull(al.payload.chainId), + address: getDataOrNull(al.payload.address), + nonce: getQuantityOrNull(al.payload.nonce), + }, + signature: { + secp256k1: getDataOrNull(al.signature.secp256k1), + }, + })) ?? null, + }, + signature: { + secp256k1: getDataOrNull(signature.secp256k1), + }, + } +} + +function setDataOrNull(elem: Uint8Array | null) { + if (elem === null) { + return null + } + + return bytesToHex(elem) +} + +function setQuantityOrNull(elem: bigint | null) { + if (elem === null) { + return null + } + + return bigIntToHex(elem) +} + +export function toPayloadJson(sszTx: SSZTransaction): ssz.TransactionV1 { + const { payload, signature } = sszTx + return { + payload: { + type: setQuantityOrNull(payload.type), + chainId: setQuantityOrNull(payload.chainId), + nonce: setQuantityOrNull(payload.nonce), + maxFeesPerGas: payload.maxFeesPerGas + ? { + regular: setQuantityOrNull(payload.maxFeesPerGas.regular), + blob: setQuantityOrNull(payload.maxFeesPerGas.blob), + } + : null, + gas: setQuantityOrNull(payload.gas), + to: setDataOrNull(payload.to), + value: setQuantityOrNull(payload.value), + input: setDataOrNull(payload.input), + accessList: payload.accessList + ? payload.accessList.map((pal) => { + return { + address: bytesToHex(pal.address), + storageKeys: pal.storageKeys.map((sk) => bytesToHex(sk)), + } + }) + : null, + maxPriorityFeesPerGas: payload.maxPriorityFeesPerGas + ? { + regular: setQuantityOrNull(payload.maxPriorityFeesPerGas.regular), + blob: setQuantityOrNull(payload.maxPriorityFeesPerGas.blob), + } + : null, + blobVersionedHashes: payload.blobVersionedHashes?.map((vh) => bytesToHex(vh)) ?? null, + authorizationList: + payload.authorizationList?.map((al) => ({ + payload: { + magic: setQuantityOrNull(al.payload.magic), + chainId: setQuantityOrNull(al.payload.chainId), + address: setDataOrNull(al.payload.address), + nonce: setQuantityOrNull(al.payload.nonce), + }, + signature: { + secp256k1: setDataOrNull(al.signature.secp256k1), + }, + })) ?? null, + }, + signature: { + secp256k1: setDataOrNull(signature.secp256k1), + }, + } +} diff --git a/packages/tx/test/eip6493.spec.ts b/packages/tx/test/eip6493.spec.ts new file mode 100644 index 0000000000..b529e81db0 --- /dev/null +++ b/packages/tx/test/eip6493.spec.ts @@ -0,0 +1,230 @@ +import { Hardfork, Mainnet, createCustomCommon } from '@ethereumjs/common' +import { bytesToHex, hexToBytes, ssz } from '@ethereumjs/util' +import { loadKZG } from 'kzg-wasm' +import { assert, describe, it } from 'vitest' + +import { + AccessListEIP2930Transaction, + BlobEIP4844Transaction, + FeeMarketEIP1559Transaction, + LegacyTransaction, + toPayloadJson, +} from '../src/index.js' +import { createTx, createTxFromExecutionPayloadTx } from '../src/transactionFactory.js' + +import type { Kzg } from '@ethereumjs/util' +function getLegacyTestCaseData() { + const txData = { + type: '0x0', + nonce: '0x0', + to: null, + gasLimit: '0x3d090', + gasPrice: '0xe8d4a51000', + maxPriorityFeePerGas: null, + maxFeePerGas: null, + value: '0x0', + data: '0x60608060095f395ff33373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35600143035500', + v: '0x1b', + r: '0x539', + s: '0x1b9b6eb1f0', + } + + return [ + txData, + // hash + '0xe43ec833884324f31c2e8314534d5b15233d84f32f05a05ea2a45649b587a9df', + // sender + '0x72eed28860ac985f1ec32306564b5926ea7c0b70', + // no special common required + undefined, + ] +} + +function get2930TestCaseData() { + const txData = { + type: '0x01', + data: '0x', + gasLimit: 0x62d4, + gasPrice: 0x3b9aca00, + nonce: 0x00, + to: '0xdf0a88b2b68c673713a8ec826003676f272e3573', + value: 0x01, + chainId: '0x796f6c6f763378', + accessList: [ + [ + hexToBytes('0x0000000000000000000000000000000000001337'), + [hexToBytes('0x0000000000000000000000000000000000000000000000000000000000000000')], + ], + ], + v: '0x0', + r: '0x294ac94077b35057971e6b4b06dfdf55a6fbed819133a6c1d31e187f1bca938d', + s: '0x0be950468ba1c25a5cb50e9f6d8aa13c8cd21f24ba909402775b262ac76d374d', + } + + const customChainParams = { + name: 'custom', + chainId: txData.chainId, + eips: [2930], + } + const usedCommon = createCustomCommon(customChainParams, Mainnet, { + hardfork: Hardfork.Berlin, + }) + usedCommon.setEIPs([2930]) + + return [ + txData, + // hash + '0xbbd570a3c6acc9bb7da0d5c0322fe4ea2a300db80226f7df4fef39b2d6649eec', + // sender + '0x96216849c49358b10257cb55b28ea603c874b05e', + // 2930 common + usedCommon, + ] +} + +function get1559TestCaseData() { + const txData = { + type: '0x02', + data: '0x', + gasLimit: 0x62d4, + maxFeesPerGas: 0x3b9aca00, + maxPriorityFeesPerGas: 0x1b9aca00, + nonce: 0x00, + to: '0xdf0a88b2b68c673713a8ec826003676f272e3573', + value: 0x01, + chainId: '0x796f6c6f763378', + accessList: [ + [ + hexToBytes('0x0000000000000000000000000000000000001337'), + [hexToBytes('0x0000000000000000000000000000000000000000000000000000000000000000')], + ], + ], + v: '0x0', + r: '0x294ac94077b35057971e6b4b06dfdf55a6fbed819133a6c1d31e187f1bca938d', + s: '0x0be950468ba1c25a5cb50e9f6d8aa13c8cd21f24ba909402775b262ac76d374d', + } + + const customChainParams = { + name: 'custom', + chainId: txData.chainId, + eips: [1559], + } + const usedCommon = createCustomCommon(customChainParams, Mainnet, { + hardfork: Hardfork.Berlin, + }) + usedCommon.setEIPs([1559]) + + return [ + txData, + // hash + '0x1390bffdfec7959c976754e55b1849dd7cbbdca78068cc544f2c8e8e8fe3bd8e', + // sender + '0xdcf0e8f6d5c3876912db8e06e2a690b99004b798', + // 1559 common + usedCommon, + ] +} + +function get4844TestCaseData(kzg: Kzg) { + const txData = { + type: '0x3', + nonce: '0x0', + gasPrice: null, + maxPriorityFeePerGas: '0x12a05f200', + maxFeePerGas: '0x12a05f200', + gasLimit: '0x33450', + value: '0xbc614e', + data: '0x', + v: '0x0', + r: '0x8a83833ec07806485a4ded33f24f5cea4b8d4d24dc8f357e6d446bcdae5e58a7', + s: '0x68a2ba422a50cf84c0b5fcbda32ee142196910c97198ffd99035d920c2b557f8', + to: '0xffb38a7a99e3e2335be83fc74b7faa19d5531243', + chainId: '0x28757b3', + accessList: null, + maxFeePerBlobGas: '0xb2d05e00', + blobVersionedHashes: ['0x01b0a4cdd5f55589f5c5b4d46c76704bb6ce95c0a8c09f77f197a57808dded28'], + } + + const customChainParams = { + name: 'custom', + chainId: txData.chainId, + eips: [4844], + } + const usedCommon = createCustomCommon(customChainParams, Mainnet, { + hardfork: Hardfork.Cancun, + customCrypto: { kzg }, + }) + usedCommon.setEIPs([4844]) + + return [ + txData, + // hash + '0xe5e02be0667b6d31895d1b5a8b916a6761cbc9865225c6144a3e2c50936d173e', + // sender + '0xa95d8b63835662e0d6fb0fb096994e2897072e2a', + // 4844 common + usedCommon, + ] +} + +describe('ssz <> rlp converstion', async () => { + const kzg = await loadKZG() + + const testCases = [ + ['LegacyTransaction', LegacyTransaction, ssz.ReplayableTransaction, ...getLegacyTestCaseData()], + [ + 'AccessListEIP2930Transaction', + AccessListEIP2930Transaction, + ssz.Eip2930Transaction, + ...get2930TestCaseData(), + ], + [ + 'FeeMarketEIP1559Transaction', + FeeMarketEIP1559Transaction, + ssz.Eip1559Transaction, + ...get1559TestCaseData(), + ], + [ + 'BlobEIP4844Transaction', + BlobEIP4844Transaction, + ssz.Eip4844Transaction, + ...get4844TestCaseData(kzg), + ], + ] + + for (const [txTypeName, _txType, sszType, txData, txHash, txSender, common] of testCases) { + it(`${txTypeName}`, () => { + const origTx = createTx(txData, { common }) + const calTxHash = bytesToHex(origTx.hash()) + assert.equal(calTxHash, txHash, 'transaction should be correctly loaded') + + const sszTx = origTx.sszRaw() + const sszJson = sszType.toJson(origTx.sszRaw()) + assert.equal(sszJson.signature.from, txSender, 'ssz format should be correct') + + const payloadJson = toPayloadJson(sszTx) + const payloadTx = createTxFromExecutionPayloadTx(payloadJson, { common }) + const payloadTxHash = bytesToHex(payloadTx.hash()) + assert.equal(payloadTxHash, txHash, 'transaction should be correctly loaded') + + const payloadSszJson = sszType.toJson(payloadTx.sszRaw()) + assert.equal(payloadSszJson.signature.from, txSender, 'ssz format should be correct') + }) + } + + it(`hashTree root of different transactions`, () => { + const transactions = testCases.map( + ([_txTypeName, _txType, _sszType, txData, _txHash, _txSender, common]) => { + const origTx = createTx(txData, { common }) + return origTx.sszRaw() + }, + ) + + const transactionsRoot = ssz.Transactions.hashTreeRoot(transactions) + assert.equal( + bytesToHex(transactionsRoot), + '0xe15ff0a75fc9889f4ce89afd2ae65ec570881a7ac6bf78ca664b1d04d0419e34', + 'transactions root should match', + ) + }) +}) diff --git a/packages/util/package.json b/packages/util/package.json index 6bafb0191c..819681a94f 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -92,6 +92,8 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "@chainsafe/persistent-merkle-tree": "^0.7.2", + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/rlp": "^5.0.2", "ethereum-cryptography": "^3.0.0" }, diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index d1f6dd95e7..9aeb5eaf7f 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -65,5 +65,6 @@ export * from './lock.js' export * from './mapDB.js' export * from './provider.js' export * from './request.js' +export * as ssz from './ssz.js' export * from './tasks.js' export * from './verkle.js' diff --git a/packages/util/src/ssz.ts b/packages/util/src/ssz.ts new file mode 100644 index 0000000000..ef2888803b --- /dev/null +++ b/packages/util/src/ssz.ts @@ -0,0 +1,464 @@ +import { Tree, hasher } from '@chainsafe/persistent-merkle-tree' +import { + BitArray, + BooleanType, + ByteListType, + ByteVectorType, + ContainerType, + ListCompositeType, + OptionalType, + ProfileType, + StableContainerType, + UintBigintType, + byteArrayEquals, +} from '@chainsafe/ssz' + +import type { PrefixedHexString } from './types.js' +import type { ValueOf } from '@chainsafe/ssz' + +export const MAX_CALLDATA_SIZE = 16_777_216 +export const MAX_ACCESS_LIST_STORAGE_KEYS = 524_288 +export const MAX_ACCESS_LIST_SIZE = 524_288 + +export const MAX_FEES_PER_GAS_FIELDS = 16 +export const MAX_TRANSACTION_PAYLOAD_FIELDS = 32 +export const MAX_TRANSACTION_SIGNATURE_FIELDS = 16 +export const MAX_BLOB_COMMITMENTS_PER_BLOCK = 4096 + +export const Boolean = new BooleanType() + +export const Uint8 = new UintBigintType(1) +export const Uint64 = new UintBigintType(8) +export const Uint256 = new UintBigintType(32) + +export const Bytes20 = new ByteVectorType(20) +export const Bytes32 = new ByteVectorType(32) +export const Bytes256 = new ByteVectorType(256) + +export const FeePerGas = Uint256 +export const ChainId = Uint64 +export const TransactionType = Uint8 +export const ExecutionAddress = Bytes20 + +function getFullArray(prefixVec: boolean[], maxVecLength: number): BitArray { + const fullVec = [ + ...prefixVec, + ...Array.from({ length: maxVecLength - prefixVec.length }, () => false), + ] + return BitArray.fromBoolArray(fullVec) +} + +export const FeesPerGas = new StableContainerType( + { + regular: new OptionalType(FeePerGas), + blob: new OptionalType(FeePerGas), + }, + MAX_FEES_PER_GAS_FIELDS, + { typeName: 'BasicFeesPerGas', jsonCase: 'eth2' }, +) + +export const AccessTuple = new ContainerType( + { + address: ExecutionAddress, + storageKeys: new ListCompositeType(Bytes32, MAX_ACCESS_LIST_STORAGE_KEYS), + }, + { typeName: 'AccessTuple', jsonCase: 'eth2' }, +) + +export const AccessList = new ListCompositeType(AccessTuple, MAX_ACCESS_LIST_SIZE) +export const TransactionTo = new OptionalType(ExecutionAddress) +export const TransactionInput = new ByteListType(MAX_CALLDATA_SIZE) +export const VersionedHashes = new ListCompositeType(Bytes32, MAX_BLOB_COMMITMENTS_PER_BLOCK) + +export const SECP256K1_SIGNATURE_SIZE = 65 +export const Secp256k1Signature = new ByteVectorType(SECP256K1_SIGNATURE_SIZE) + +export const MAX_EXECUTION_SIGNATURE_FIELDS = 8 +export const ExecutionSignature = new StableContainerType( + { + secp256k1: new OptionalType(Secp256k1Signature), + }, + MAX_EXECUTION_SIGNATURE_FIELDS, + { typeName: 'ExecutionSignature', jsonCase: 'eth2' }, +) +export const Secp256k1ExecutionSignature = new ProfileType( + { secp256k1: Secp256k1Signature }, + getFullArray([true], MAX_EXECUTION_SIGNATURE_FIELDS), + { typeName: 'Secp256k1ExecutionSignature', jsonCase: 'eth2' }, +) + +export const MAX_AUTHORIZATION_PAYLOAD_FIELDS = 16 +export const AuthorizationPayload = new StableContainerType( + { + magic: new OptionalType(TransactionType), + chainId: new OptionalType(ChainId), + address: new OptionalType(ExecutionAddress), + nonce: new OptionalType(Uint64), + }, + MAX_AUTHORIZATION_PAYLOAD_FIELDS, + { typeName: 'AuthorizationPayload', jsonCase: 'eth2' }, +) + +export const Authorization = new ContainerType( + { + payload: AuthorizationPayload, + signature: ExecutionSignature, + }, + { typeName: 'Authorization', jsonCase: 'eth2' }, +) + +export const MAX_AUTHORIZATION_LIST_SIZE = 65_536 +export const AuthorizationList = new ListCompositeType(Authorization, MAX_AUTHORIZATION_LIST_SIZE) + +export const TransactionPayload = new StableContainerType( + { + type: new OptionalType(TransactionType), + chainId: new OptionalType(ChainId), + nonce: new OptionalType(Uint64), + maxFeesPerGas: new OptionalType(FeesPerGas), + gas: new OptionalType(Uint64), + to: TransactionTo, + value: new OptionalType(Uint256), + input: new OptionalType(TransactionInput), + accessList: new OptionalType(AccessList), + maxPriorityFeesPerGas: new OptionalType(FeesPerGas), + blobVersionedHashes: new OptionalType(VersionedHashes), + authorizationList: new OptionalType(AuthorizationList), + }, + MAX_TRANSACTION_PAYLOAD_FIELDS, + { typeName: 'TransactionPayload', jsonCase: 'eth2' }, +) + +export const Transaction = new ContainerType( + { + payload: TransactionPayload, + signature: ExecutionSignature, + }, + { typeName: 'Transaction', jsonCase: 'eth2' }, +) + +export const BasicFeesPerGas = new ProfileType( + { regular: FeePerGas }, + getFullArray([true], MAX_FEES_PER_GAS_FIELDS), + { typeName: 'BasicFeesPerGas', jsonCase: 'eth2' }, +) + +export const BlobFeesPerGas = new ProfileType( + { + regular: FeePerGas, + blob: FeePerGas, + }, + getFullArray([true, true], MAX_FEES_PER_GAS_FIELDS), + { typeName: 'BlobFeesPerGas', jsonCase: 'eth2' }, +) + +export const ReplayableTransactionPayload = new ProfileType( + { + type: TransactionType, + nonce: Uint64, + maxFeesPerGas: BasicFeesPerGas, + gas: Uint64, + to: TransactionTo, + value: Uint256, + input: TransactionInput, + }, + getFullArray([true, false, true, true, true, true, true, true], MAX_FEES_PER_GAS_FIELDS), + { typeName: 'ReplayableTransactionPayload', jsonCase: 'eth2' }, +) + +export const ReplayableTransaction = new ContainerType( + { + payload: ReplayableTransactionPayload, + signature: Secp256k1ExecutionSignature, + }, + { typeName: 'ReplayableTransaction', jsonCase: 'eth2' }, +) + +export const LegacyTransactionPayload = new ProfileType( + { + type: TransactionType, + chainId: ChainId, + nonce: Uint64, + maxFeesPerGas: BasicFeesPerGas, + gas: Uint64, + to: TransactionTo, + value: Uint256, + input: TransactionInput, + }, + getFullArray([true, true, true, true, true, true, true, true], MAX_FEES_PER_GAS_FIELDS), + { typeName: 'LegacyTransactionPayload', jsonCase: 'eth2' }, +) + +export const LegacyTransaction = new ContainerType( + { + payload: LegacyTransactionPayload, + signature: Secp256k1ExecutionSignature, + }, + { typeName: 'LegacyTransaction', jsonCase: 'eth2' }, +) + +export const Eip2930TransactionPayload = new ProfileType( + { + type: TransactionType, + chainId: ChainId, + nonce: Uint64, + maxFeesPerGas: BasicFeesPerGas, + gas: Uint64, + to: TransactionTo, + value: Uint256, + input: TransactionInput, + accessList: AccessList, + }, + getFullArray([true, true, true, true, true, true, true, true, true], MAX_FEES_PER_GAS_FIELDS), + { typeName: 'Eip2930TransactionPayload', jsonCase: 'eth2' }, +) + +export const Eip2930Transaction = new ContainerType( + { + payload: Eip2930TransactionPayload, + signature: Secp256k1ExecutionSignature, + }, + { typeName: 'Eip2930Transaction', jsonCase: 'eth2' }, +) + +export const Eip1559TransactionPayload = new ProfileType( + { + type: TransactionType, + chainId: ChainId, + nonce: Uint64, + maxFeesPerGas: BasicFeesPerGas, + gas: Uint64, + to: TransactionTo, + value: Uint256, + input: TransactionInput, + accessList: AccessList, + maxPriorityFeesPerGas: BasicFeesPerGas, + }, + getFullArray( + [true, true, true, true, true, true, true, true, true, true], + MAX_FEES_PER_GAS_FIELDS, + ), + { typeName: 'Eip1559TransactionPayload', jsonCase: 'eth2' }, +) + +export const Eip1559Transaction = new ContainerType( + { + payload: Eip1559TransactionPayload, + signature: Secp256k1ExecutionSignature, + }, + { typeName: 'Eip1559Transaction', jsonCase: 'eth2' }, +) + +export const Eip4844TransactionPayload = new ProfileType( + { + type: TransactionType, + chainId: ChainId, + nonce: Uint64, + maxFeesPerGas: BlobFeesPerGas, + gas: Uint64, + to: ExecutionAddress, + value: Uint256, + input: TransactionInput, + accessList: AccessList, + maxPriorityFeesPerGas: BlobFeesPerGas, + blobVersionedHashes: VersionedHashes, + }, + getFullArray( + [true, true, true, true, true, true, true, true, true, true, true], + MAX_FEES_PER_GAS_FIELDS, + ), + { typeName: 'Eip4844TransactionPayload', jsonCase: 'eth2' }, +) + +export const Eip4844Transaction = new ContainerType( + { + payload: Eip4844TransactionPayload, + signature: Secp256k1ExecutionSignature, + }, + { typeName: 'Eip4844Transaction', jsonCase: 'eth2' }, +) + +const MAX_WITHDRAWALS_PER_PAYLOAD = 16 +export const Withdrawal = new ContainerType( + { + index: Uint64, + validatorIndex: Uint64, + address: ExecutionAddress, + amount: Uint64, + }, + { typeName: 'Withdrawal', jsonCase: 'eth2' }, +) +export const Withdrawals = new ListCompositeType(Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD) + +const MAX_TRANSACTIONS_PER_PAYLOAD = 1048576 +export const Transactions = new ListCompositeType(Transaction, MAX_TRANSACTIONS_PER_PAYLOAD) +export const TransactionRootsList = new ListCompositeType(Bytes32, MAX_TRANSACTIONS_PER_PAYLOAD) +export type TransactionsType = ValueOf + +const TRANSACTION_GINDEX0 = 2097152n +export function computeTransactionInclusionProof( + transactions: TransactionsType, + index: number, + fromRoots = true, +): { merkleBranch: Uint8Array[]; transactionRoot: Uint8Array } { + if (index >= transactions.length) { + throw Error(`Invalid index=${index} > transactions=${transactions.length}`) + } + + const transactionRoot = Transaction.hashTreeRoot(transactions[index]) + + let merkleBranch + if (fromRoots === true) { + const transactionRoots = transactions.map((tx) => Transaction.hashTreeRoot(tx)) + const TransactionsRootView = TransactionRootsList.toView(transactionRoots) + // transaction index is its g index in the list + merkleBranch = new Tree(TransactionsRootView.node).getSingleProof( + TRANSACTION_GINDEX0 + BigInt(index), + ) + } else { + const TransactionsView = Transactions.toView(transactions) + // transaction index is its g index in the list + merkleBranch = new Tree(TransactionsView.node).getSingleProof( + TRANSACTION_GINDEX0 + BigInt(index), + ) + } + + return { merkleBranch, transactionRoot } +} + +const TRANSACTION_PROOF_DEPTH = 21 +/** + * Verify that the given ``leaf`` is on the merkle branch ``proof`` + * starting with the given ``root``. + * + * Browser friendly version of verifyMerkleBranch + */ +export function isValidTransactionProof( + transactionRoot: Uint8Array, + proof: Uint8Array[], + index: number, + transactionsRoot: Uint8Array, +): boolean { + let value = transactionRoot + for (let i = 0; i < TRANSACTION_PROOF_DEPTH; i++) { + if (Math.floor(index / 2 ** i) % 2) { + value = hasher.digest64(proof[i], value) + } else { + value = hasher.digest64(value, proof[i]) + } + } + return byteArrayEquals(value, transactionsRoot) +} + +export type FeesPerGasV1 = { + regular: PrefixedHexString | null // Quantity 64 bytes + blob: PrefixedHexString | null // Quantity 64 bytes +} + +export type AccessTupleV1 = { + address: PrefixedHexString // DATA 20 bytes + storageKeys: PrefixedHexString[] // Data 32 bytes MAX_ACCESS_LIST_STORAGE_KEYS array +} + +export type ExecutionSignatureV1 = { + secp256k1: PrefixedHexString | null // DATA 65 bytes +} + +export type AuthorizationPayloadV1 = { + magic: PrefixedHexString | null // Quantity 1 byte, + chainId: PrefixedHexString | null // Quantity 8 bytes + address: PrefixedHexString | null // DATA 20 bytes + nonce: PrefixedHexString | null //Quantity 8 bytes +} + +export type AuthorizationV1 = { + payload: AuthorizationPayloadV1 + signature: ExecutionSignatureV1 +} + +export type TransactionPayloadV1 = { + type: PrefixedHexString | null // Quantity, 1 byte + chainId: PrefixedHexString | null // Quantity 8 bytes + nonce: PrefixedHexString | null // Quantity 8 bytes + maxFeesPerGas: FeesPerGasV1 | null + gas: PrefixedHexString | null // Quantity 8 bytes + to: PrefixedHexString | null // DATA 20 bytes + value: PrefixedHexString | null // Quantity 64 bytes + input: PrefixedHexString | null // max MAX_CALLDATA_SIZE bytes, + accessList: AccessTupleV1[] | null + maxPriorityFeesPerGas: FeesPerGasV1 | null + blobVersionedHashes: PrefixedHexString[] | null // DATA 32 bytes array + authorizationList: AuthorizationV1[] | null +} + +export type TransactionV1 = { + payload: TransactionPayloadV1 + signature: ExecutionSignatureV1 +} + +const MAX_TOPICS_PER_LOG = 4 +const MAX_LOG_DATA_SIZE = 16_777_216 +const MAX_RECEIPT_FIELDS = 32 +const MAX_LOGS_PER_RECEIPT = 2_097_152 + +export const LogTopics = new ListCompositeType(Bytes32, MAX_TOPICS_PER_LOG) +export const Log = new ContainerType( + { + address: ExecutionAddress, + topics: LogTopics, + data: new ByteListType(MAX_LOG_DATA_SIZE), + }, + { typeName: 'Log', jsonCase: 'eth2' }, +) +export const LogList = new ListCompositeType(Log, MAX_LOGS_PER_RECEIPT) +export const AuthoritiesList = new ListCompositeType(ExecutionAddress, MAX_AUTHORIZATION_LIST_SIZE) + +export const Receipt = new StableContainerType( + { + root: new OptionalType(Bytes32), + gasUsed: new OptionalType(Uint64), + contractAddress: new OptionalType(ExecutionAddress), + logs: new OptionalType(LogList), + status: new OptionalType(Boolean), + authorities: new OptionalType(AuthoritiesList), + }, + MAX_RECEIPT_FIELDS, + { typeName: 'Receipt', jsonCase: 'eth2' }, +) +export const Receipts = new ListCompositeType(Receipt, MAX_TRANSACTIONS_PER_PAYLOAD) + +export const MAX_BLOCKHEADER_FIELDS = 64 +const MAX_EXTRA_DATA_BYTES = 32 + +export const BlockHeader = new StableContainerType( + { + parentHash: new OptionalType(Bytes32), + coinbase: new OptionalType(Bytes20), + stateRoot: new OptionalType(Bytes32), + transactionsTrie: new OptionalType(Bytes32), + receiptsTrie: new OptionalType(Bytes32), + number: new OptionalType(Uint64), + gasLimits: new OptionalType(FeesPerGas), + gasUsed: new OptionalType(FeesPerGas), + timestamp: new OptionalType(Uint64), + extraData: new OptionalType(new ByteListType(MAX_EXTRA_DATA_BYTES)), + mixHash: new OptionalType(Bytes32), + baseFeePerGas: new OptionalType(FeesPerGas), + withdrawalsRoot: new OptionalType(Bytes32), + excessGas: new OptionalType(FeesPerGas), + parentBeaconBlockRoot: new OptionalType(Bytes32), + requestsRoot: new OptionalType(Bytes32), + systemLogsRoot: new OptionalType(Bytes32), + }, + MAX_BLOCKHEADER_FIELDS, + { typeName: 'BlockHeader', jsonCase: 'eth2' }, +) + +export const IVCEntry = new ContainerType( + { + prevTopicRoot: Bytes32, + number: Uint64, + logRoot: Bytes32, + }, + { typeName: 'IVCEntry', jsonCase: 'eth2' }, +) diff --git a/packages/util/test/ssz.spec.ts b/packages/util/test/ssz.spec.ts new file mode 100644 index 0000000000..1ac7457844 --- /dev/null +++ b/packages/util/test/ssz.spec.ts @@ -0,0 +1,36 @@ +import { assert, describe, it } from 'vitest' + +import { ssz } from '../src/index.js' + +const eip1559SszJson = { + payload: { + type: '2', + chain_id: '1', + nonce: '0', + max_fees_per_gas: { regular: '100' }, + gas: '30000000', + to: '0x00000000219ab540356cbb839cbe05303d7705fa', + value: '32000000000000000000', + input: + '0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001208cd4e5a69709cf8ee5b1b73d6efbf3f33bcac92fb7e4ce62b2467542fb50a72d0000000000000000000000000000000000000000000000000000000000000030ac842878bb70009552a4cfcad801d6e659c50bd50d7d03306790cb455ce7363c5b6972f0159d170f625a99b2064dbefc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020010000000000000000000000818ccb1c4eda80270b04d6df822b1e72dd83c3030000000000000000000000000000000000000000000000000000000000000060a747f75c72d0cf0d2b52504c7385b516f0523e2f0842416399f42b4aee5c6384a5674f6426b1cc3d0827886fa9b909e616f5c9f61f986013ed2b9bf37071cbae951136265b549f44e3c8e26233c0433e9124b7fd0dc86e82f9fedfc0a179d769', + access_list: [], + max_priority_fees_per_gas: { regular: '0' }, + }, + signature: { + from: '0x610adc49ecd66cbf176a8247ebd59096c031bd9f', + ecdsa_signature: + '0x5f8397122e00d9cdea67c83ec99a4694af24c3d6f25c4dde8f2fa4277d85c96754b2ea7851948fe99288049edfd8ca53c4aee79043e91afb513de0664822277900', + }, +} + +describe('profile<>stable tx container', function () { + it(`EIP 1559 tx profile<>stable conversion`, () => { + const profileSszValue = ssz.Eip1559Transaction.fromJson(eip1559SszJson) + const profileSszBytes = ssz.Eip1559Transaction.serialize(profileSszValue) + + const stableTx = ssz.Transaction.deserialize(profileSszBytes) + const stableTxJson = ssz.Transaction.toJson(stableTx) + + assert.deepEqual(stableTxJson, eip1559SszJson, 'the transaction jsons should match') + }) +}) diff --git a/packages/vm/package.json b/packages/vm/package.json index 32934c073e..54be337121 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -64,6 +64,7 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/block": "^5.3.0", "@ethereumjs/common": "^4.4.0", "@ethereumjs/evm": "^3.1.0", diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index b95fec859d..d477e5e0cc 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -2,7 +2,9 @@ import { createBlock, createSealedCliqueBlock, genRequestsTrieRoot, + genTransactionsSszRoot, genTransactionsTrieRoot, + genWithdrawalsSszRoot, genWithdrawalsTrieRoot, } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' @@ -15,29 +17,38 @@ import { BIGINT_1, BIGINT_2, GWEI_TO_WEI, - KECCAK256_RLP, TypeOutput, + bigIntToBytes, createWithdrawal, createZeroAddress, + hexToBytes, + setLengthLeft, + ssz, toBytes, toType, + utf8ToBytes, } from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak.js' import { Bloom } from './bloom/index.js' import { accumulateRequests } from './requests.js' import { + accumulateIVCLogs, accumulateParentBeaconBlockRoot, accumulateParentBlockHash, calculateMinerReward, encodeReceipt, + encodeSszReceipt, rewardAccount, } from './runBlock.js' import { runTx } from './index.js' +import type { SSZReceiptType } from './runBlock.js' import type { BuildBlockOpts, BuilderOpts, RunTxResult, SealBlockOpts } from './types.js' import type { VM } from './vm.js' import type { Block, HeaderData } from '@ethereumjs/block' +import type { Log } from '@ethereumjs/evm' import type { TypedTransaction } from '@ethereumjs/tx' import type { Withdrawal } from '@ethereumjs/util' @@ -143,10 +154,22 @@ export class BlockBuilder { * Calculates and returns the transactionsTrie for the block. */ public async transactionsTrie() { - return genTransactionsTrieRoot( - this.transactions, - new MerklePatriciaTrie({ common: this.vm.common }), - ) + return this.vm.common.isActivatedEIP(6493) + ? genTransactionsSszRoot(this.transactions) + : genTransactionsTrieRoot( + this.transactions, + new MerklePatriciaTrie({ common: this.vm.common }), + ) + } + + public async withdrawalsTrie() { + if (this.withdrawals === undefined) { + return + } + + return this.vm.common.isActivatedEIP(6493) + ? genWithdrawalsSszRoot(this.withdrawals) + : genWithdrawalsTrieRoot(this.withdrawals, new MerklePatriciaTrie({ common: this.vm.common })) } /** @@ -165,16 +188,22 @@ export class BlockBuilder { * Calculates and returns the receiptTrie for the block. */ public async receiptTrie() { - if (this.transactionResults.length === 0) { - return KECCAK256_RLP - } - const receiptTrie = new MerklePatriciaTrie({ common: this.vm.common }) - for (const [i, txResult] of this.transactionResults.entries()) { - const tx = this.transactions[i] - const encodedReceipt = encodeReceipt(txResult.receipt, tx.type) - await receiptTrie.put(RLP.encode(i), encodedReceipt) + if (this.vm.common.isActivatedEIP(6493)) { + const sszReceipts: SSZReceiptType[] = [] + for (const [i, txResult] of this.transactionResults.entries()) { + const tx = this.transactions[i] + sszReceipts.push(encodeSszReceipt(txResult.receipt, tx.type)) + } + return ssz.Receipts.hashTreeRoot(sszReceipts) + } else { + const receiptTrie = new MerklePatriciaTrie({ common: this.vm.common }) + for (const [i, txResult] of this.transactionResults.entries()) { + const tx = this.transactions[i] + const encodedReceipt = encodeReceipt(txResult.receipt, tx.type) + await receiptTrie.put(RLP.encode(i), encodedReceipt) + } + return receiptTrie.root() } - return receiptTrie.root() } /** @@ -299,6 +328,56 @@ export class BlockBuilder { this.blockStatus = { status: BuildStatus.Reverted } } + async finishBlockBuild() { + const consensusType = this.vm.common.consensusType() + + if (consensusType === ConsensusType.ProofOfWork) { + await this.rewardMiner() + } + await this.processWithdrawals() + let requests + if (this.vm.common.isActivatedEIP(7685)) { + requests = await accumulateRequests(this.vm, this.transactionResults) + } + + let systemLogs: Log[] | undefined + if (this.vm.common.isActivatedEIP(6493)) { + // decide to add individual or total reward logs + const totalPriorityReward = this.transactionResults.reduce( + (acc, elem) => acc + elem.minerValue, + BIGINT_0, + ) + const systemAddressBytes = hexToBytes('0xfffffffffffffffffffffffffffffffffffffffe') + const coinbase = + this.headerData.coinbase !== undefined + ? new Address(toBytes(this.headerData.coinbase)) + : createZeroAddress() + + const logData = { + address: systemAddressBytes, + // operation, from, to + topics: [ + keccak256(utf8ToBytes('PriorityRewards(address,uint256)')), + setLengthLeft(coinbase.toBytes(), 32), + ], + // amount be uint256 + data: setLengthLeft(bigIntToBytes(totalPriorityReward), 32), + } + + systemLogs = [[logData.address, logData.topics, logData.data]] + } + + if (this.vm.common.isActivatedEIP(6493)) { + for (const txReceipt of this.transactionReceipts) { + await accumulateIVCLogs(this.vm, txReceipt.logs) + } + + await accumulateIVCLogs(this.vm, systemLogs!) + } + + return { requests, systemLogs } + } + /** * This method constructs the finalized block, including withdrawals and any CLRequests. * It also: @@ -318,18 +397,26 @@ export class BlockBuilder { const blockOpts = this.blockOpts const consensusType = this.vm.common.consensusType() - if (consensusType === ConsensusType.ProofOfWork) { - await this.rewardMiner() + const { requests, systemLogs } = await this.finishBlockBuild() + + let requestsRoot + if (this.vm.common.isActivatedEIP(7685)) { + requestsRoot = await genRequestsTrieRoot(requests!) + } + + let systemLogsRoot + if (this.vm.common.isActivatedEIP(6493)) { + systemLogsRoot = ssz.LogList.hashTreeRoot( + systemLogs!.map((log) => ({ + address: log[0], + topics: log[1], + data: log[2], + })), + ) } - await this.processWithdrawals() const transactionsTrie = await this.transactionsTrie() - const withdrawalsRoot = this.withdrawals - ? await genWithdrawalsTrieRoot( - this.withdrawals, - new MerklePatriciaTrie({ common: this.vm.common }), - ) - : undefined + const withdrawalsRoot = await this.withdrawalsTrie() const receiptTrie = await this.receiptTrie() const logsBloom = this.logsBloom() const gasUsed = this.gasUsed @@ -341,14 +428,6 @@ export class BlockBuilder { blobGasUsed = this.blobGasUsed } - let requests - let requestsRoot - if (this.vm.common.isActivatedEIP(7685)) { - requests = await accumulateRequests(this.vm, this.transactionResults) - requestsRoot = await genRequestsTrieRoot(requests) - // Do other validations per request type - } - // get stateRoot after all the accumulateRequests etc have been done const stateRoot = await this.vm.stateManager.getStateRoot() const headerData = { @@ -363,6 +442,7 @@ export class BlockBuilder { // correct excessBlobGas should already be part of headerData used above blobGasUsed, requestsRoot, + systemLogsRoot, } if (consensusType === ConsensusType.ProofOfWork) { diff --git a/packages/vm/src/params.ts b/packages/vm/src/params.ts index 1d3a576b2c..1120a16a6f 100644 --- a/packages/vm/src/params.ts +++ b/packages/vm/src/params.ts @@ -89,4 +89,9 @@ export const paramsVM: ParamsDict = { systemAddress: '0xfffffffffffffffffffffffffffffffffffffffe', // The system address to perform operations on the consolidation requests predeploy address consolidationRequestPredeployAddress: '0x00b42dbF2194e931E80326D950320f7d9Dbeac02', // Address of the consolidations contract }, + 6493: { + systemAddress: '0xfffffffffffffffffffffffffffffffffffffffe', // The system address to perform operations on the consolidation requests predeploy address + // dummu address right now as actual will be determined with the deployment of ivc contract + ivcPredeployAddress: '0x' + '6493'.repeat(10), + }, } diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 41589f1d8c..ab06369b53 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -23,9 +23,13 @@ import { intToBytes, setLengthLeft, short, + ssz, unprefixedHexToBytes, + utf8ToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' +import { keccak256 } from 'ethereum-cryptography/keccak.js' +import { sha256 } from 'ethereum-cryptography/sha256.js' import { Bloom } from './bloom/index.js' import { emitEVMProfile } from './emitEVMProfile.js' @@ -44,10 +48,13 @@ import type { TxReceipt, } from './types.js' import type { VM } from './vm.js' +import type { ValueOf } from '@chainsafe/ssz' import type { Block } from '@ethereumjs/block' import type { Common } from '@ethereumjs/common' -import type { EVM, EVMInterface } from '@ethereumjs/evm' -import type { CLRequest, CLRequestType, PrefixedHexString } from '@ethereumjs/util' +import type { EVM, EVMInterface, Log } from '@ethereumjs/evm' +import type { PrefixedHexString } from '@ethereumjs/util' + +export type SSZReceiptType = ValueOf const debug = debugDefault('vm:block') @@ -213,10 +220,21 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise[] | undefined if (block.common.isActivatedEIP(7685)) { - requests = await accumulateRequests(vm, result.results) - requestsRoot = await genRequestsTrieRoot(requests) + requestsRoot = await genRequestsTrieRoot(result.requests!) + } + + let systemLogsRoot: Uint8Array | undefined + if (block.common.isActivatedEIP(6493)) { + // dummy for time being + const systemLogs = result.systemLogs ?? [] + systemLogsRoot = ssz.LogList.hashTreeRoot( + systemLogs!.map((log) => ({ + address: log[0], + topics: log[1], + data: log[2], + })), + ) } // Persist state @@ -242,18 +260,33 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise acc + elem.minerValue, + BIGINT_0, + ) + const systemAddressBytes = hexToBytes('0xfffffffffffffffffffffffffffffffffffffffe') + const logData = { + address: systemAddressBytes, + // operation, from, to + topics: [ + keccak256(utf8ToBytes('PriorityRewards(address,uint256)')), + setLengthLeft(block.header.coinbase.toBytes(), 32), + ], + // amount be uint256 + data: setLengthLeft(bigIntToBytes(totalPriorityReward), 32), + } + + result.systemLogs = [[logData.address, logData.topics, logData.data]] + } + + if (vm.common.isActivatedEIP(6493)) { + for (const txReceipt of result.receipts) { + await accumulateIVCLogs(vm, txReceipt.logs) + } + + await accumulateIVCLogs(vm, result.systemLogs!) + } + + return result } /** @@ -583,11 +652,6 @@ async function applyTransactions(vm: VM, block: Block, opts: RunBlockOpts) { // the total amount of gas used processing these transactions let gasUsed = BIGINT_0 - let receiptTrie: MerklePatriciaTrie | undefined = undefined - if (block.transactions.length !== 0) { - receiptTrie = new MerklePatriciaTrie({ common: vm.common }) - } - const receipts: TxReceipt[] = [] const txResults: RunTxResult[] = [] @@ -637,8 +701,6 @@ async function applyTransactions(vm: VM, block: Block, opts: RunBlockOpts) { // Add receipt to trie to later calculate receipt root receipts.push(txRes.receipt) - const encodedReceipt = encodeReceipt(txRes.receipt, tx.type) - await receiptTrie!.put(RLP.encode(txIdx), encodedReceipt) } if (enableProfiler) { @@ -646,7 +708,23 @@ async function applyTransactions(vm: VM, block: Block, opts: RunBlockOpts) { console.timeEnd(processTxsLabel) } - const receiptsRoot = receiptTrie !== undefined ? receiptTrie.root() : KECCAK256_RLP + let receiptsRoot + if (vm.common.isActivatedEIP(6493)) { + const sszReceipts: SSZReceiptType[] = [] + for (const [i, txReceipt] of receipts.entries()) { + const tx = block.transactions[i] + sszReceipts.push(encodeSszReceipt(txReceipt, tx.type)) + } + receiptsRoot = ssz.Receipts.hashTreeRoot(sszReceipts) + } else { + const receiptTrie = new MerklePatriciaTrie({ common: vm.common }) + for (const [i, txReceipt] of receipts.entries()) { + const tx = block.transactions[i] + const encodedReceipt = encodeReceipt(txReceipt, tx.type) + await receiptTrie!.put(RLP.encode(i), encodedReceipt) + } + receiptsRoot = receiptTrie.root() + } return { bloom, @@ -761,6 +839,83 @@ export function encodeReceipt(receipt: TxReceipt, txType: TransactionType) { return concatBytes(intToBytes(txType), encoded) } +export function encodeSszReceipt(receipt: TxReceipt, _txType: TransactionType) { + const sszRaw: SSZReceiptType = { + root: (receipt as PreByzantiumTxReceipt).stateRoot ?? null, + gasUsed: receipt.cumulativeBlockGasUsed, + contractAddress: receipt.contractAddress?.bytes ?? null, + logs: receipt.logs.map((log) => ({ + address: log[0], + topics: log[1], + data: log[2], + })), + status: + (receipt as PostByzantiumTxReceipt).status !== undefined + ? (receipt as PostByzantiumTxReceipt).status === 0 + ? false + : true + : null, + authorities: receipt.authorities?.map((auth) => auth.bytes) ?? null, + } + + return sszRaw +} + +export async function accumulateIVCLogs(vm: VM, logs: Log[]) { + // keep all declarations here for ease of movement and diff + const LOG_ADDRESS_STORAGE_SLOT = setLengthLeft(new Uint8Array([0]), 32) + const LOG_TOPICS_STORAGE_SLOT = setLengthLeft(new Uint8Array([1]), 32) + const LOG_ADDRESS_TOPICS_STORAGE_SLOT = setLengthLeft(new Uint8Array([2]), 32) + + const commonSHA256 = vm.common.customCrypto.sha256 ?? sha256 + const ivcContractAddress = new Address( + bigIntToAddressBytes(vm.common.param('ivcPredeployAddress')), + ) + + async function accumulateLog(key: Uint8Array, logRoot: Uint8Array) { + const prevRoot = setLengthLeft(await vm.stateManager.getStorage(ivcContractAddress, key), 32) + const newRoot = commonSHA256(concatBytes(logRoot, prevRoot)) + await vm.stateManager.putStorage(ivcContractAddress, key, newRoot) + } + + if ((await vm.stateManager.getAccount(ivcContractAddress)) === undefined) { + // store with nonce of 1 to prevent 158 cleanup + const ivcContract = new Account() + ivcContract.nonce = BIGINT_1 + await vm.stateManager.putAccount(ivcContractAddress, ivcContract) + } + + for (const log of logs) { + const sszLog = { + address: log[0], + topics: log[1], + data: log[2], + } + const logRoot = ssz.Log.hashTreeRoot(sszLog) + + // Allow eth_getLogs proof via `address` filter + // abi.encode(log.address, LOG_ADDRESS_STORAGE_SLOT) + const paddedAddress = setLengthLeft(sszLog.address, 32) + const addressKey = keccak256(concatBytes(paddedAddress, LOG_ADDRESS_STORAGE_SLOT)) + await accumulateLog(addressKey, logRoot) + + for (const topic of sszLog.topics) { + // Allow eth_getLogs proof via `topics` filter + // abi.encode(topic, LOG_TOPICS_STORAGE_SLOT) + const topicKey = keccak256(concatBytes(topic, LOG_TOPICS_STORAGE_SLOT)) + await accumulateLog(topicKey, logRoot) + + // Allow eth_getLogs proof via combined `address` + `topics` filter + // abi.encode(log.address, topic) + const addressAndTopic = keccak256(concatBytes(paddedAddress, topic)) + const addressAndTopicKey = keccak256( + concatBytes(addressAndTopic, LOG_ADDRESS_TOPICS_STORAGE_SLOT), + ) + await accumulateLog(addressAndTopicKey, logRoot) + } + } +} + /** * Apply the DAO fork changes to the VM */ diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 1036c6e30d..f360f024bc 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -8,6 +8,7 @@ import { BIGINT_0, BIGINT_1, KECCAK256_NULL, + bigIntToBytes, bytesToBigInt, bytesToHex, bytesToUnprefixedHex, @@ -16,7 +17,9 @@ import { equalsBytes, hexToBytes, publicToAddress, + setLengthLeft, short, + utf8ToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' import { keccak256 } from 'ethereum-cryptography/keccak.js' @@ -619,6 +622,22 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { ) } + if (vm.common.isActivatedEIP(6493)) { + const systemAddressBytes = hexToBytes('0xfffffffffffffffffffffffffffffffffffffffe') + const logData = { + address: systemAddressBytes, + // operation, to + topics: [keccak256(utf8ToBytes('Fee(address,uint256)')), setLengthLeft(caller.toBytes(), 32)], + // amount be uint256 + data: setLengthLeft(bigIntToBytes(actualTxCost), 32), + } + + if (results.execResult.logs === undefined) { + results.execResult.logs = [] + } + results.execResult.logs.push([logData.address, logData.topics, logData.data]) + } + // Update miner's balance let miner if (vm.common.consensusType() === ConsensusType.ProofOfAuthority) { @@ -808,6 +827,8 @@ export async function generateTxReceipt( cumulativeBlockGasUsed: cumulativeGasUsed, bitvector: txResult.bloom.bitvector, logs: txResult.execResult.logs ?? [], + contractAddress: txResult.createdAddress, + authorities: txResult.authorities, } let receipt diff --git a/packages/vm/src/types.ts b/packages/vm/src/types.ts index 0e3d7e49cd..fbbe56a60e 100644 --- a/packages/vm/src/types.ts +++ b/packages/vm/src/types.ts @@ -10,6 +10,7 @@ import type { } from '@ethereumjs/evm' import type { AccessList, TypedTransaction } from '@ethereumjs/tx' import type { + Address, BigIntLike, CLRequest, CLRequestType, @@ -34,6 +35,8 @@ export interface BaseTxReceipt { * Logs emitted */ logs: Log[] + contractAddress?: Address + authorities?: Address[] } /** @@ -349,6 +352,11 @@ export interface ApplyBlockResult { * Preimages mapping of the touched accounts from the block (see reportPreimages option) */ preimages?: Map + /** + * Any CL requests that were processed in the course of this block + */ + requests?: CLRequest[] + systemLogs?: Log[] } /** @@ -368,10 +376,6 @@ export interface RunBlockResult extends Omit { * The requestsRoot for any CL requests in the block */ requestsRoot?: Uint8Array - /** - * Any CL requests that were processed in the course of this block - */ - requests?: CLRequest[] } export interface AfterBlockEvent extends RunBlockResult { @@ -488,6 +492,8 @@ export interface RunTxResult extends EVMResult { * This is the blob gas units times the fee per blob gas for 4844 transactions */ blobGasUsed?: bigint + + authorities?: Address[] } export interface AfterTxEvent extends RunTxResult {