From 30dc88a05d99dae5786c683ac0fa23034d79d4b6 Mon Sep 17 00:00:00 2001 From: harkamal Date: Thu, 13 Jun 2024 23:51:10 +0530 Subject: [PATCH 1/4] common,util,tx: implement aip 6493 stable container txs debug and fix the legacy ssz encoding decoding add a spec test for legacy ssz encoding decoding add the ssztx boilerplate to other tx types implement sszRaw value for 2930 tx add 2930 spec test and debug/fix ssz encoding/decoding add the ssz encoding decoding to 1559 tx add eip 1559 testcase and get it working add 4844 ssz encoding decoding add eip 4844 testcase and get it working define block transactions ssz type and test ssz transactionsRoot handle ssz roots for transactions and withdrawals in block when 6493 activated handle the roots gen in the build block fix the transaction stable container update the execution payload serialization deserialization for 6493 add 6493 hardfork for the testing/devnet refactor the transaction factory ssz tx deserialization add ssz profile<>stablecontaiber conversion spec test add eip6493 support to common debug and fix the block transaction withdrawal root comparision by removing null keccak hash hardcoding enhance eip6493 tx test by testing transaction factory deserialization which uses stable container add client eip6493 end to end spec and fix the payload generation refactor tx serialization deserializion with respect to execution/beacon payload add, debug and fix the transactionv1 or hex transactions validator and debug/fix the newpayloadeip6493 spec test add 6493 to electra for kurtosis testing console log error for debugging console log error for debugging txpool fix attempt add more descriptive checks for nulloroptional add more descriptive checks for nulloroptional log full error debug and fix handling of replay vs legacy tx w.r.t. v/ypartity and confirm via spec test build fix dev and add transaction inclusion proof to the getTransactionX apis workaround to get the proof since stable container impl for proof seems buggy and breaking refactor the proof format based on feedback debug, discuss and fix the signature packing scheme add hack to schedule 6493 on prague in cli for stablecontainer devnets debug and fix newpayload eip6493 spec debug rebase and spec fixes in tx utils debug and fix block build fix the vm build debug and get 6493 end to end client spec working rebase 4844 fixes add ssz blockheader type and update the blockhash to use when ssz activated debug and update client spec with ssz blockhash update ssz field to receiptstrie updates after discussion with etan update test update and proagate ssz signature scheme as well as authroization list check and remove an invalid failing spec test impl ssz receipts and dev modify the receipts rooting work debug and fix the client 6493 end to end spec test accumulate logs into an ivc contract for advance proofing capabilities add ivc spec test in end to end client spec and debug and fix it modify the ivc accumulator code for smartcontract comaptible compute and add log proofing capabilities by multiple filters move the log accumulation to post executions move the ivc log processing to pre 7685 to allow adding transfer logs for cl withdrawals add and process systemslogs root to header for allowing system logs verification debug and fix issues introduced by systemlogs field and fix the 6493 client spec add log for the combined miner/priority reward and debug/fix/validate client eip6493 spec simplify log based on discussion with etan and make changes remove console trace update ssz to stablecontainer released version fix missing sz update propagate, serve and save system logs and modify, debug and fix client end to end spec test add systemlogs root add transfer logs generate and add receipt proof to jsonrpc api added and verified receipt proof check in the test --- package-lock.json | 110 ++++ packages/block/package.json | 1 + packages/block/src/block/block.ts | 44 +- packages/block/src/block/constructors.ts | 45 +- packages/block/src/from-beacon-payload.ts | 106 +++- packages/block/src/header/header.ts | 69 ++- packages/block/src/helpers.ts | 19 +- packages/block/src/index.ts | 2 + packages/block/src/types.ts | 7 +- packages/block/test/eip4895block.spec.ts | 11 - packages/client/bin/utils.ts | 12 + packages/client/src/execution/receipt.ts | 33 +- packages/client/src/execution/vmexecution.ts | 22 +- packages/client/src/miner/pendingBlock.ts | 46 +- packages/client/src/rpc/helpers.ts | 19 +- .../client/src/rpc/modules/engine/engine.ts | 531 ++++++++++-------- .../src/rpc/modules/engine/validators.ts | 45 +- packages/client/src/rpc/modules/eth.ts | 87 ++- packages/client/src/rpc/validation.ts | 39 ++ packages/client/src/service/txpool.ts | 3 + packages/client/src/types.ts | 1 + packages/client/src/util/metaDBManager.ts | 1 + .../test/rpc/engine/newPayloadEip6493.spec.ts | 275 +++++++++ packages/common/src/eips.ts | 10 + packages/common/src/enums.ts | 1 + packages/common/src/hardforks.ts | 8 + packages/common/src/utils.ts | 2 + packages/evm/src/evm.ts | 53 +- packages/tx/package.json | 1 + packages/tx/src/1559/constructors.ts | 54 +- packages/tx/src/1559/tx.ts | 41 +- packages/tx/src/2930/constructors.ts | 51 +- packages/tx/src/2930/tx.ts | 42 +- packages/tx/src/4844/constructors.ts | 51 +- packages/tx/src/4844/tx.ts | 45 +- packages/tx/src/7702/tx.ts | 11 +- packages/tx/src/index.ts | 1 + packages/tx/src/legacy/constructors.ts | 41 +- packages/tx/src/legacy/tx.ts | 49 ++ packages/tx/src/transactionFactory.ts | 70 ++- packages/tx/src/types.ts | 10 + packages/tx/src/util.ts | 139 +++++ packages/tx/test/eip6493.spec.ts | 230 ++++++++ packages/util/package.json | 2 + packages/util/src/index.ts | 1 + packages/util/src/ssz.ts | 498 ++++++++++++++++ packages/util/test/ssz.spec.ts | 36 ++ packages/vm/package.json | 1 + packages/vm/src/buildBlock.ts | 133 ++++- packages/vm/src/index.ts | 2 +- packages/vm/src/params.ts | 5 + packages/vm/src/runBlock.ts | 185 +++++- packages/vm/src/runTx.ts | 21 + packages/vm/src/types.ts | 10 + 54 files changed, 2970 insertions(+), 362 deletions(-) create mode 100644 packages/client/test/rpc/engine/newPayloadEip6493.spec.ts create mode 100644 packages/tx/test/eip6493.spec.ts create mode 100644 packages/util/src/ssz.ts create mode 100644 packages/util/test/ssz.spec.ts diff --git a/package-lock.json b/package-lock.json index a9861f53258..9352f7d9be2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -682,6 +682,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", @@ -695,6 +763,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", @@ -16506,6 +16611,7 @@ "version": "6.0.0-alpha.1", "license": "MPL-2.0", "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/common": "^5.0.0-alpha.1", "@ethereumjs/mpt": "^7.0.0-alpha.1", "@ethereumjs/rlp": "^6.0.0-alpha.1", @@ -16927,6 +17033,7 @@ "version": "6.0.0-alpha.1", "license": "MPL-2.0", "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/common": "^5.0.0-alpha.1", "@ethereumjs/rlp": "^6.0.0-alpha.1", "@ethereumjs/util": "^10.0.0-alpha.1", @@ -17020,6 +17127,8 @@ "version": "10.0.0-alpha.1", "license": "MPL-2.0", "dependencies": { + "@chainsafe/persistent-merkle-tree": "^0.7.2", + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/rlp": "^6.0.0-alpha.1", "ethereum-cryptography": "^3.0.0" }, @@ -17062,6 +17171,7 @@ "version": "9.0.0-alpha.1", "license": "MPL-2.0", "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/block": "^6.0.0-alpha.1", "@ethereumjs/common": "^5.0.0-alpha.1", "@ethereumjs/evm": "^4.0.0-alpha.1", diff --git a/packages/block/package.json b/packages/block/package.json index 4b7cef9f8dd..cc296118721 100644 --- a/packages/block/package.json +++ b/packages/block/package.json @@ -42,6 +42,7 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/common": "^5.0.0-alpha.1", "@ethereumjs/rlp": "^6.0.0-alpha.1", "@ethereumjs/mpt": "^7.0.0-alpha.1", diff --git a/packages/block/src/block/block.ts b/packages/block/src/block/block.ts index 454e2c40227..7f2a3d0a819 100644 --- a/packages/block/src/block/block.ts +++ b/packages/block/src/block/block.ts @@ -18,6 +18,7 @@ import { sha256 } from 'ethereum-cryptography/sha256' // 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 { genTransactionsTrieRoot, genWithdrawalsTrieRoot, @@ -199,10 +200,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 })) } /** @@ -211,16 +211,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 } @@ -319,7 +313,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) } @@ -409,6 +405,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 @@ -418,19 +420,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 } @@ -498,7 +491,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 = { @@ -522,6 +517,7 @@ export class Block { parentBeaconBlockRoot: header.parentBeaconBlockRoot, requestsHash: header.requestsHash, executionWitness: this.executionWitness, + systemLogsRoot: this.common.isActivatedEIP(6493) ? header.systemLogsRoot : undefined, } return executionPayload diff --git a/packages/block/src/block/constructors.ts b/packages/block/src/block/constructors.ts index 86920243db5..309076c3b25 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' @@ -21,7 +22,12 @@ import { } from '@ethereumjs/util' import { generateCliqueBlockExtraData } from '../consensus/clique.js' -import { genTransactionsTrieRoot, genWithdrawalsTrieRoot } from '../helpers.js' +import { + genTransactionsSszRoot, + genTransactionsTrieRoot, + genWithdrawalsSszRoot, + genWithdrawalsTrieRoot, +} from '../helpers.js' import { Block, createBlockHeader, @@ -41,6 +47,7 @@ import type { JSONRPCBlock, WithdrawalsBytes, } from '../types.js' +import type { Common } from '@ethereumjs/common' import type { TypedTransaction } from '@ethereumjs/tx' import type { EthersProvider, PrefixedHexString, WithdrawalBytes } from '@ethereumjs/util' @@ -322,7 +329,7 @@ export const createBlockFromJSONRPCProvider = async ( */ export async function createBlockFromExecutionPayload( payload: ExecutionPayload, - opts?: BlockOptions, + opts: BlockOptions & { common: Common }, ): Promise { const { blockNumber: number, @@ -335,11 +342,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}` @@ -347,13 +367,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 header: HeaderData = { @@ -393,7 +414,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 e83ea9f7807..4ae1282a285 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 @@ -10,7 +15,53 @@ type BeaconWithdrawal = { amount: 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 @@ -26,12 +77,13 @@ 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 parent_beacon_block_root?: PrefixedHexString requests_hash?: PrefixedHexString + system_logs_root?: PrefixedHexString // the casing of VerkleExecutionWitness remains same camel case for now execution_witness?: VerkleExecutionWitness } @@ -97,6 +149,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, @@ -111,7 +205,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) { @@ -136,6 +230,10 @@ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJSON): E executionPayload.requestsHash = payload.requests_hash } + 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 e0fe7f88337..3f528a243cb 100644 --- a/packages/block/src/header/header.ts +++ b/packages/block/src/header/header.ts @@ -17,6 +17,7 @@ import { createZeroAddress, equalsBytes, hexToBytes, + ssz, toType, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' @@ -30,6 +31,9 @@ import { computeBlobGasPrice } 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 @@ -62,6 +66,7 @@ export class BlockHeader { public readonly excessBlobGas?: bigint public readonly parentBeaconBlockRoot?: Uint8Array public readonly requestsHash?: Uint8Array + public readonly systemLogsRoot?: Uint8Array public readonly common: Common @@ -165,6 +170,7 @@ export class BlockHeader { // Note: as of devnet-4 we stub the null SHA256 hash, but for devnet5 this will actually // be the correct hash for empty requests. requestsHash: this.common.isActivatedEIP(7685) ? SHA256_NULL : undefined, + systemLogsRoot: this.common.isActivatedEIP(6493) ? KECCAK256_RLP : undefined, } const baseFeePerGas = @@ -180,6 +186,8 @@ export class BlockHeader { hardforkDefaults.parentBeaconBlockRoot const requestsHash = toType(headerData.requestsHash, TypeOutput.Uint8Array) ?? hardforkDefaults.requestsHash + 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') @@ -211,6 +219,10 @@ export class BlockHeader { throw new Error('requestsHash 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 @@ -232,6 +244,7 @@ export class BlockHeader { this.excessBlobGas = excessBlobGas this.parentBeaconBlockRoot = parentBeaconBlockRoot this.requestsHash = requestsHash + this.systemLogsRoot = systemLogsRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -352,6 +365,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) + } + } } /** @@ -619,21 +639,63 @@ export class BlockHeader { if (this.common.isActivatedEIP(7685)) { rawItems.push(this.requestsHash!) } + 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() } /** @@ -761,6 +823,9 @@ export class BlockHeader { if (this.common.isActivatedEIP(7685)) { JSONDict.requestsHash = bytesToHex(this.requestsHash!) } + 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 bd200f03d36..a5641d827bb 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, concatBytes, isHexString, toType } from '@ethereumjs/util' +import { BIGINT_0, BIGINT_1, TypeOutput, concatBytes, isHexString, ssz, toType } from '@ethereumjs/util' import type { BlockHeaderBytes, HeaderData } from './types.js' import type { Common } from '@ethereumjs/common' import type { TypedTransaction } from '@ethereumjs/tx' import type { CLRequest, CLRequestType, PrefixedHexString, Withdrawal } from '@ethereumjs/util' +import type { ValueOf } from '@chainsafe/ssz' + +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, requestsHash, + 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, requestsHash, + systemLogsRoot, } } @@ -145,6 +150,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 @@ -161,6 +171,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 e3745e9e7c3..029747cd01c 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 { genRequestsRoot, + genTransactionsSszRoot, genTransactionsTrieRoot, + genWithdrawalsSszRoot, genWithdrawalsTrieRoot, getDifficulty, valuesArrayToHeaderData, diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 5efee2bedf9..23b96042113 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -11,6 +11,7 @@ import type { VerkleExecutionWitness, WithdrawalBytes, WithdrawalData, + ssz, } from '@ethereumjs/util' /** @@ -109,6 +110,7 @@ export interface HeaderData { excessBlobGas?: BigIntLike parentBeaconBlockRoot?: BytesLike requestsHash?: BytesLike + systemLogsRoot?: BytesLike } /** @@ -192,6 +194,7 @@ export interface JSONHeader { excessBlobGas?: PrefixedHexString parentBeaconBlockRoot?: PrefixedHexString requestsHash?: PrefixedHexString + systemLogsRoot?: PrefixedHexString } /* @@ -226,6 +229,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 requestsHash?: PrefixedHexString // If EIP-7685 is enabled for this block, returns the requests root + systemLogsRoot?: PrefixedHexString } export type WithdrawalV1 = { @@ -250,7 +254,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 @@ -258,4 +262,5 @@ export type ExecutionPayload = { requestsHash?: PrefixedHexString // VerkleExecutionWitness is already a hex serialized object executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available + systemLogsRoot?: PrefixedHexString } diff --git a/packages/block/test/eip4895block.spec.ts b/packages/block/test/eip4895block.spec.ts index 98d2baed05a..5ec4c0f90ec 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/utils.ts b/packages/client/bin/utils.ts index 5959851d3f6..64a8d2e4da6 100644 --- a/packages/client/bin/utils.ts +++ b/packages/client/bin/utils.ts @@ -129,6 +129,12 @@ export function getArgs(): ClientOpts { 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', @@ -734,6 +740,12 @@ export async function generateClientConfig(args: ClientOpts) { // 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/execution/receipt.ts b/packages/client/src/execution/receipt.ts index b560b60c0f0..5027d4cadcf 100644 --- a/packages/client/src/execution/receipt.ts +++ b/packages/client/src/execution/receipt.ts @@ -43,8 +43,8 @@ type GetReceiptByTxHashReturn = [ type GetLogsReturn = { log: Log block: Block - tx: TypedTransaction - txIndex: number + tx?: TypedTransaction + txIndex?: number logIndex: number }[] @@ -112,6 +112,15 @@ export class ReceiptsManager extends MetaDBManager { void this.updateIndex(IndexOperation.Delete, IndexType.TxHash, block) } + async saveSystemLogs(block: Block, logs: Log[]) { + const encoded = this.rlp(RlpConvert.Encode, RlpType.Logs, logs) + await this.put(DBKey.SystemLogs, block.hash(), encoded) + } + + async deleteSystemLogs(block: Block) { + await this.delete(DBKey.SystemLogs, block.hash()) + } + /** * Returns receipts for given blockHash * @param blockHash the block hash @@ -152,6 +161,13 @@ export class ReceiptsManager extends MetaDBManager { return receipts } + async getSystemLogs(blockHash: Uint8Array): Promise { + const encoded = await this.get(DBKey.SystemLogs, blockHash) + if (!encoded) return [] + const systemLogs = this.rlp(RlpConvert.Decode, RlpType.Logs, encoded as unknown as rlpLog[]) + return systemLogs + } + /** * Returns receipt by tx hash with additional metadata for the JSON RPC response, or null if not found * @param txHash the tx hash @@ -183,7 +199,8 @@ export class ReceiptsManager extends MetaDBManager { for (let i = from.header.number; i <= to.header.number; i++) { const block = await this.chain.getBlock(i) const receipts = await this.getReceipts(block.hash()) - if (receipts.length === 0) continue + const systemLogs = await this.getSystemLogs(block.hash()) + if (receipts.length === 0 && systemLogs.length === 0) continue let logs: GetLogsReturn = [] let logIndex = 0 for (const [receiptIndex, receipt] of receipts.entries()) { @@ -197,6 +214,16 @@ export class ReceiptsManager extends MetaDBManager { })), ) } + + // push system logs + logs.push( + ...systemLogs.map((log) => ({ + log, + block, + logIndex: logIndex++, + })), + ) + if (addresses && addresses.length > 0) { logs = logs.filter((l) => addresses.some((a) => equalsBytes(a, l.log[0]))) } diff --git a/packages/client/src/execution/vmexecution.ts b/packages/client/src/execution/vmexecution.ts index b921e547346..8f1bb66b4ad 100644 --- a/packages/client/src/execution/vmexecution.ts +++ b/packages/client/src/execution/vmexecution.ts @@ -40,6 +40,7 @@ import { ReceiptsManager } from './receipt.js' import type { ExecutionOptions } from './execution.js' import type { Block } from '@ethereumjs/block' +import type { Log } from '@ethereumjs/evm' import type { PrefixedHexString } from '@ethereumjs/util' import type { RunBlockOpts, TxReceipt, VM } from '@ethereumjs/vm' @@ -68,7 +69,7 @@ export class VMExecution extends Execution { public receiptsManager?: ReceiptsManager public preimagesManager?: PreimagesManager - private pendingReceipts?: Map + private pendingReceipts?: Map private vmPromise?: Promise /** Maximally tolerated block time before giving a warning on console */ @@ -126,6 +127,7 @@ export class VMExecution extends Execution { // Once a block gets deleted from the chain, delete the receipts also for (const block of blocks) { await this.receiptsManager?.deleteReceipts(block) + await this.receiptsManager?.deleteSystemLogs(block) } if (resolve !== undefined) { resolve() @@ -369,7 +371,7 @@ export class VMExecution extends Execution { */ async runWithoutSetHead( opts: RunBlockOpts & { parentBlock?: Block }, - receipts?: TxReceipt[], + receiptsAndSystemLogs?: { receipts: TxReceipt[]; systemLogs?: Log[] }, blocking: boolean = false, skipBlockchain: boolean = false, ): Promise { @@ -386,7 +388,7 @@ export class VMExecution extends Execution { this.running = true const { block, root } = opts - if (receipts === undefined) { + if (receiptsAndSystemLogs === undefined) { // Check if we need to pass flag to clear statemanager cache or not const prevVMStateRoot = await this.vm.stateManager.getStateRoot() // If root is not provided its mean to be run on the same set state @@ -452,11 +454,11 @@ export class VMExecution extends Execution { if (this.config.savePreimages && result.preimages !== undefined) { await this.savePreimages(result.preimages) } - receipts = result.receipts + receiptsAndSystemLogs = { receipts: result.receipts, systemLogs: result.systemLogs } } - if (receipts !== undefined) { + if (receiptsAndSystemLogs !== undefined) { // Save receipts - this.pendingReceipts?.set(bytesToHex(block.hash()), receipts) + this.pendingReceipts?.set(bytesToHex(block.hash()), receiptsAndSystemLogs) } if (!skipBlockchain) { @@ -520,9 +522,13 @@ export class VMExecution extends Execution { // skip emitting the chain update event as we will manually do it await this.chain.putBlocks(blocks, true, true) for (const block of blocks) { - const receipts = this.pendingReceipts?.get(bytesToHex(block.hash())) - if (receipts) { + const receiptsAndSystemLogs = this.pendingReceipts?.get(bytesToHex(block.hash())) + if (receiptsAndSystemLogs) { + const { receipts, systemLogs } = receiptsAndSystemLogs await this.receiptsManager?.saveReceipts(block, receipts) + if (systemLogs !== undefined) { + await this.receiptsManager?.saveSystemLogs(block, systemLogs) + } this.pendingReceipts?.delete(bytesToHex(block.hash())) } } diff --git a/packages/client/src/miner/pendingBlock.ts b/packages/client/src/miner/pendingBlock.ts index 4d9e8deb2e0..611a52154b1 100644 --- a/packages/client/src/miner/pendingBlock.ts +++ b/packages/client/src/miner/pendingBlock.ts @@ -18,6 +18,7 @@ import { keccak256 } from 'ethereum-cryptography/keccak' import type { Config } from '../config.js' import type { TxPool } from '../service/txpool.js' import type { Block, HeaderData } from '@ethereumjs/block' +import type { Log } from '@ethereumjs/evm' import type { TypedTransaction } from '@ethereumjs/tx' import type { CLRequest, CLRequestType, PrefixedHexString, WithdrawalData } from '@ethereumjs/util' import type { BlockBuilder, TxReceipt, VM } from '@ethereumjs/vm' @@ -199,10 +200,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})`, ) @@ -243,7 +249,7 @@ export class PendingBlock { | void | [ block: Block, - receipts: TxReceipt[], + receiptsAndSystemLogs: { receipts: TxReceipt[]; systemLogs?: Log[] }, value: bigint, blobs?: BlobsBundle, requests?: CLRequest[], @@ -259,7 +265,7 @@ export class PendingBlock { if (blockStatus.status === BuildStatus.Build) { return [ blockStatus.block, - builder.transactionReceipts, + { receipts: builder.transactionReceipts, systemLogs: builder.systemLogs }, builder.minerValue, this.blobsBundles.get(payloadId), ] @@ -279,10 +285,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) => @@ -307,10 +318,16 @@ export class PendingBlock { block.transactions.length }${withdrawalsStr}${blobsStr} skippedByAddErrors=${skippedByAddErrors} hash=${bytesToHex( block.hash(), - )}`, + )} systemLogs=${builder.systemLogs ? builder.systemLogs.length : undefined}`, ) - return [block, builder.transactionReceipts, builder.minerValue, blobs, requests] + return [ + block, + { receipts: builder.transactionReceipts, systemLogs: builder.systemLogs }, + builder.minerValue, + blobs, + requests, + ] } private async addTransactions(builder: BlockBuilder, txs: TypedTransaction[]) { @@ -334,6 +351,7 @@ export class PendingBlock { blockFull = true // Falls through default: + console.log({ addTxResult }) skippedByAddErrors++ } index++ @@ -356,6 +374,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 @@ -372,8 +391,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 0fee280adb8..f13700f1990 100644 --- a/packages/client/src/rpc/helpers.ts +++ b/packages/client/src/rpc/helpers.ts @@ -37,7 +37,16 @@ export function callWithStackTrace(handler: Function, debug: boolean): RPCMethod /** * 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, @@ -62,6 +71,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 31796bcad5f..9ed2f182e2d 100644 --- a/packages/client/src/rpc/modules/engine/engine.ts +++ b/packages/client/src/rpc/modules/engine/engine.ts @@ -38,6 +38,7 @@ import { executionPayloadV1FieldValidators, executionPayloadV2FieldValidators, executionPayloadV3FieldValidators, + executionPayloadV4FieldValidators, forkchoiceFieldValidators, payloadAttributesFieldValidatorsV1, payloadAttributesFieldValidatorsV2, @@ -206,7 +207,7 @@ export class Engine { callWithStackTrace(this.newPayloadV4.bind(this), this._rpcDebug), 4, [ - [validators.object(executionPayloadV3FieldValidators)], + [validators.object(executionPayloadV4FieldValidators)], [validators.array(validators.bytes32)], [validators.bytes32], [validators.array(validators.hex)], @@ -341,6 +342,7 @@ export class Engine { * valid block in the branch defined by payload and its ancestors * 3. validationError: String|null - validation error message */ + private async newPayload( params: [ ExecutionPayload, @@ -348,6 +350,19 @@ export class Engine { (Bytes32 | null)?, (PrefixedHexString[] | null)?, ], + ): Promise { + return this.newPayloadWrap(params).catch(e=>{ + console.log('newPayload', e); + throw e; + }) + } + private async newPayloadWrap( + params: [ + ExecutionPayload, + (Bytes32[] | null)?, + (Bytes32 | null)?, + (PrefixedHexString[] | null)?, + ], ): Promise { const [payload, blobVersionedHashes, parentBeaconBlockRoot, executionRequests] = params if (this.config.synchronized) { @@ -388,9 +403,7 @@ export class Engine { ) response = { status: Status.INVALID, latestValidHash, validationError } } - // skip marking the block invalid as this is more of a data issue from CL - return response - } + const { parentHash, blockHash } = payload /** * Stats and hardfork updates @@ -417,14 +430,19 @@ export class Engine { (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 + * 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, @@ -434,26 +452,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 && @@ -471,9 +612,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 && @@ -504,215 +659,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 } /** @@ -1305,17 +1388,22 @@ export class Engine { } // The third arg returned is the minerValue which we will use to // value the block - const [block, receipts, value, blobs, requests] = built + const [block, receiptsAndSystemLogs, value, blobs, requests] = built // do a blocking call even if execution might be busy for the moment and skip putting // it into chain till CL confirms with full data via new payload like versioned hashes // parent beacon block root - const executed = await this.execution.runWithoutSetHead({ block }, receipts, true, true) + const executed = await this.execution.runWithoutSetHead( + { block }, + receiptsAndSystemLogs, + true, + true, + ) if (!executed) { 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 */ @@ -1359,6 +1447,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/validators.ts b/packages/client/src/rpc/modules/engine/validators.ts index 9aae69fe2a2..528046dc6a5 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, @@ -26,6 +64,11 @@ export const executionPayloadV3FieldValidators = { excessBlobGas: validators.uint64, } +export const executionPayloadV4FieldValidators = { + ...executionPayloadV3FieldValidators, + systemLogsRoot: validators.bytes32, +} + export const forkchoiceFieldValidators = { headBlockHash: validators.blockHash, safeBlockHash: validators.blockHash, diff --git a/packages/client/src/rpc/modules/eth.ts b/packages/client/src/rpc/modules/eth.ts index 5eb5070897e..b7f3d297be1 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 { @@ -36,6 +37,7 @@ import { type PreByzantiumTxReceipt, type TxReceipt, type VM, + encodeSszReceipt, runBlock, runTx, } from '@ethereumjs/vm' @@ -90,6 +92,11 @@ type JSONRPCReceipt = { blobGasUsed?: string // QUANTITY, blob gas consumed by transaction (if blob transaction) blobGasPrice?: string // QUAntity, blob gas price for block including this transaction (if blob transaction) type: string // QUANTITY, transaction type + inclusionProof?: { + merkleBranch: string[] + receiptsRoot: string + receiptRoot: string + } } type JSONRPCLog = { removed: boolean // TAG - true when the log was removed, due to a chain reorganization. false if it's a valid log. @@ -153,6 +160,7 @@ const toJSONRPCBlock = async ( excessBlobGas: header.excessBlobGas, parentBeaconBlockRoot: header.parentBeaconBlockRoot, requestsHash: header.requestsHash, + systemLogsRoot: header.systemLogsRoot, } } @@ -191,6 +199,11 @@ const toJSONRPCReceipt = async ( contractAddress?: Address, blobGasUsed?: bigint, blobGasPrice?: bigint, + inclusionProof?: { + merkleBranch: Uint8Array[] + receiptsRoot: Uint8Array + receiptRoot: Uint8Array + }, ): Promise => ({ transactionHash: bytesToHex(tx.hash()), transactionIndex: intToHex(txIndex), @@ -217,6 +230,14 @@ const toJSONRPCReceipt = async ( blobGasUsed: blobGasUsed !== undefined ? bigIntToHex(blobGasUsed) : undefined, blobGasPrice: blobGasPrice !== undefined ? bigIntToHex(blobGasPrice) : undefined, type: intToHex(tx.type), + inclusionProof: + inclusionProof !== undefined + ? { + merkleBranch: inclusionProof.merkleBranch.map((elem) => bytesToHex(elem)), + receiptsRoot: bytesToHex(inclusionProof.receiptsRoot), + receiptRoot: bytesToHex(inclusionProof.receiptRoot), + } + : undefined, }) const calculateRewards = async ( @@ -803,7 +824,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, @@ -828,7 +860,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, @@ -849,8 +892,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) } /** @@ -944,6 +999,10 @@ export class Eth { skipBlockValidation: true, }) + const sszReceipts = runBlockResult.receipts.map((txReceipt, index) => + encodeSszReceipt(txReceipt, block.transactions[index].type), + ) + const receipts = await Promise.all( result.map(async (r, i) => { const tx = block.transactions[i] @@ -959,6 +1018,14 @@ export class Eth { block.header.baseFeePerGas! : (tx as LegacyTx).gasPrice + let inclusionProof = undefined + if (block.common.isActivatedEIP(6493)) { + inclusionProof = inclusionProof = { + receiptsRoot: block.header.receiptTrie, + ...ssz.computeReceiptInclusionProof(sszReceipts, i), + } + } + return toJSONRPCReceipt( r, totalGasSpent, @@ -970,6 +1037,7 @@ export class Eth { createdAddress, blobGasUsed, blobGasPrice, + inclusionProof, ) }), ) @@ -1021,6 +1089,18 @@ export class Eth { const { totalGasSpent, createdAddress } = runBlockResult.results[txIndex] const { blobGasPrice, blobGasUsed } = runBlockResult.receipts[txIndex] as EIP4844BlobTxReceipt + + const sszReceipts = runBlockResult.receipts.map((txReceipt, index) => + encodeSszReceipt(txReceipt, block.transactions[index].type), + ) + let inclusionProof = undefined + if (block.common.isActivatedEIP(6493)) { + inclusionProof = inclusionProof = { + receiptsRoot: block.header.receiptTrie, + ...ssz.computeReceiptInclusionProof(sszReceipts, txIndex), + } + } + return toJSONRPCReceipt( receipt, totalGasSpent, @@ -1032,6 +1112,7 @@ export class Eth { createdAddress, blobGasUsed, blobGasPrice, + inclusionProof, ) } diff --git a/packages/client/src/rpc/validation.ts b/packages/client/src/rpc/validation.ts index dcde5ebb7c4..60fe634f307 100644 --- a/packages/client/src/rpc/validation.ts +++ b/packages/client/src/rpc/validation.ts @@ -187,6 +187,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) }, @@ -607,6 +610,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 @@ -771,6 +792,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 fe2d36c176f..ebd45d3f8af 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 24040e15135..37fa9ffefa4 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -79,6 +79,7 @@ export interface ClientOpts { gethGenesis?: string trustedSetup?: string mergeForkIdPostMerge?: boolean + eip6493AtPrague?: boolean bootnodes?: string | string[] port?: number extIP?: string diff --git a/packages/client/src/util/metaDBManager.ts b/packages/client/src/util/metaDBManager.ts index a553cc8c445..701f62d1b5d 100644 --- a/packages/client/src/util/metaDBManager.ts +++ b/packages/client/src/util/metaDBManager.ts @@ -21,6 +21,7 @@ export enum DBKey { SkeletonStatus, SkeletonUnfinalizedBlockByHash, Preimage, + SystemLogs, } export interface MetaDBManagerOptions { 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 00000000000..12897be6460 --- /dev/null +++ b/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts @@ -0,0 +1,275 @@ +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', + ) + + // check system logs available + res = await rpc.request('eth_getLogs', [{ blockHash: executionPayload.blockHash }]) + assert.equal(res.result.length, 6, '6 logs should be found including system logs') + const systemLogs = res.result[5]! + assert.equal(systemLogs.transactionHash, null, 'last log should be system log') + assert.equal( + systemLogs.topics[0], + '0x5dfe9c0fd3043bb299f97cfece428f0396cf8b7890c525756e4ea5c0ff7d61b2', + 'priority reward topic', + ) + assert.equal( + systemLogs.topics[1].includes(executionPayload.feeRecipient.slice(2)), + true, + 'fee recipient in topic', + ) + + const txHash = res.result[4].transactionHash + res = await rpc.request('eth_getTransactionReceipt', [txHash]) + assert.ok(res.result.inclusionProof !== undefined, 'recepit should have inclusion proof') + }) +}) + +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 1c6e31aab00..a0b7cbe1a01 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 8f4f673d577..13262fea405 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', Verkle = 'verkle', } diff --git a/packages/common/src/hardforks.ts b/packages/common/src/hardforks.ts index fd11ca6876b..e324e189477 100644 --- a/packages/common/src/hardforks.ts +++ b/packages/common/src/hardforks.ts @@ -163,6 +163,14 @@ export const hardforksDict: HardforksDict = { osaka: { eips: [663, 3540, 3670, 4200, 4750, 5450, 6206, 7069, 7480, 7620, 7692, 7698], }, + /** + * 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 551847425c2..1cd08c8c2a7 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -148,6 +148,8 @@ 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 }, [Hardfork.Verkle]: { name: 'verkleTime', postMerge: true, isTimestamp: true }, } diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index c59bc28bdb9..ee01e67ef5e 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -14,10 +14,14 @@ import { equalsBytes, generateAddress, generateAddress2, + hexToBytes, + setLengthLeft, short, + utf8ToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' import { EventEmitter } from 'eventemitter3' +import { keccak256 } from 'ethereum-cryptography/keccak.js' import { FORMAT } from './eof/constants.js' import { isEOF } from './eof/util.js' @@ -53,6 +57,7 @@ import type { AsyncDynamicGasHandler, SyncDynamicGasHandler } from './opcodes/ga import type { OpHandler, OpcodeList, OpcodeMap } from './opcodes/index.js' import type { CustomPrecompile, PrecompileFunc } from './precompiles/index.js' import type { VerkleAccessWitness } from './verkleAccessWitness.js' +import type { Log } from './types.js' import type { Common, StateManagerInterface } from '@ethereumjs/common' const debug = debugDefault('evm:evm') @@ -86,6 +91,7 @@ export class EVM implements EVMInterface { Hardfork.Shanghai, Hardfork.Cancun, Hardfork.Prague, + Hardfork.Eip6493, Hardfork.Osaka, Hardfork.Verkle, ] @@ -183,8 +189,8 @@ 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, - 7002, 7069, 7251, 7480, 7516, 7620, 7685, 7691, 7692, 7698, 7702, 7709, + 3855, 3860, 4200, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6493, 6780, + 6800, 7002, 7069, 7251, 7480, 7516, 7620, 7685, 7691, 7692, 7698, 7702, 7709, ] for (const eip of this.common.eips()) { @@ -263,6 +269,7 @@ export class EVM implements EVMInterface { protected async _executeCall(message: MessageWithTo): Promise { let gasLimit = message.gasLimit const fromAddress = message.caller + let logs: Log[] | undefined = undefined if (this.common.isActivatedEIP(6800)) { if (message.accessWitness === undefined) { @@ -343,6 +350,22 @@ export class EVM implements EVMInterface { if (!message.delegatecall) { try { await this._addToBalance(toAccount, message) + if (this.common.isActivatedEIP(6493) && message.value > 0) { + const systemAddressBytes = hexToBytes('0xfffffffffffffffffffffffffffffffffffffffe') + const logData = { + address: systemAddressBytes, + // operation, to + topics: [ + keccak256(utf8ToBytes('Transfer(address,address,uint256)')), + setLengthLeft(fromAddress.toBytes(), 32), + setLengthLeft(message.to.toBytes(), 32), + ], + // amount be uint256 + data: setLengthLeft(bigIntToBytes(message.value), 32), + } + logs = logs ?? [] + logs.push([logData.address, logData.topics, logData.data]) + } } catch (e: any) { errorMessage = e } @@ -370,6 +393,7 @@ export class EVM implements EVMInterface { executionGasUsed: message.gasLimit - gasLimit, exceptionError: errorMessage, // Only defined if addToBalance failed returnValue: new Uint8Array(0), + logs, }, } } @@ -409,6 +433,9 @@ export class EVM implements EVMInterface { } result.executionGasUsed += message.gasLimit - gasLimit + if (this.common.isActivatedEIP(6493) && logs !== undefined) { + result.logs = [...(logs ?? []), ...(result.logs ?? [])] + } return { execResult: result, @@ -418,6 +445,7 @@ export class EVM implements EVMInterface { protected async _executeCreate(message: Message): Promise { let gasLimit = message.gasLimit const fromAddress = message.caller + let logs: Log[] | undefined = undefined if (this.common.isActivatedEIP(6800)) { if (message.depth === 0) { @@ -530,6 +558,22 @@ export class EVM implements EVMInterface { let errorMessage try { await this._addToBalance(toAccount, message as MessageWithTo) + if (this.common.isActivatedEIP(6493) && message.value > 0) { + const systemAddressBytes = hexToBytes('0xfffffffffffffffffffffffffffffffffffffffe') + const logData = { + address: systemAddressBytes, + // operation, to + topics: [ + keccak256(utf8ToBytes('Transfer(address,address,uint256)')), + setLengthLeft(fromAddress.toBytes(), 32), + setLengthLeft(message.to.toBytes(), 32), + ], + // amount be uint256 + data: setLengthLeft(bigIntToBytes(message.value), 32), + } + logs = logs ?? [] + logs.push([logData.address, logData.topics, logData.data]) + } } catch (e: any) { errorMessage = e } @@ -580,6 +624,7 @@ export class EVM implements EVMInterface { gasRefund: message.gasRefund, exceptionError: errorMessage, // only defined if addToBalance failed returnValue: new Uint8Array(0), + logs, }, } } @@ -740,6 +785,10 @@ export class EVM implements EVMInterface { this.postMessageCleanup() } + if (this.common.isActivatedEIP(6493) && logs !== undefined) { + result.logs = [...(logs ?? []), ...(result.logs ?? [])] + } + return { createdAddress: message.to, execResult: result, diff --git a/packages/tx/package.json b/packages/tx/package.json index adf1596e703..65e01957a56 100644 --- a/packages/tx/package.json +++ b/packages/tx/package.json @@ -49,6 +49,7 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/common": "^5.0.0-alpha.1", "@ethereumjs/rlp": "^6.0.0-alpha.1", "@ethereumjs/util": "^10.0.0-alpha.1", diff --git a/packages/tx/src/1559/constructors.ts b/packages/tx/src/1559/constructors.ts index 623659832e9..7663bb6855d 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 00423967c81..a66ccf9b727 100644 --- a/packages/tx/src/1559/tx.ts +++ b/packages/tx/src/1559/tx.ts @@ -5,6 +5,7 @@ import { bigIntToHex, bigIntToUnpaddedBytes, bytesToBigInt, + setLengthLeft, toBytes, } from '@ethereumjs/util' @@ -14,7 +15,7 @@ import * as EIP2930 from '../capabilities/eip2930.js' import * as Legacy from '../capabilities/legacy.js' import { getBaseJSON, sharedConstructor, valueBoundaryCheck } from '../features/util.js' import { TransactionType } from '../types.js' -import { AccessLists } from '../util.js' +import { AccessLists, toPayloadJson } from '../util.js' import { createFeeMarket1559Tx } from './constructors.js' @@ -25,6 +26,8 @@ import type { TxValuesArray as AllTypesTxValuesArray, Capability, JSONTx, + SSZTransactionV1, + SSZTransactionType, TransactionCache, TransactionInterface, TxOptions, @@ -232,6 +235,38 @@ export class FeeMarket1559Tx implements TransactionInterface ({ 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. * @@ -341,6 +376,10 @@ export class FeeMarket1559Tx implements TransactionInterface /** * 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 f827d2518ff..93424c70de3 100644 --- a/packages/tx/src/2930/tx.ts +++ b/packages/tx/src/2930/tx.ts @@ -4,6 +4,7 @@ import { bigIntToHex, bigIntToUnpaddedBytes, bytesToBigInt, + setLengthLeft, toBytes, } from '@ethereumjs/util' @@ -12,7 +13,7 @@ import * as EIP2930 from '../capabilities/eip2930.js' import * as Legacy from '../capabilities/legacy.js' import { getBaseJSON, sharedConstructor, valueBoundaryCheck } from '../features/util.js' import { TransactionType } from '../types.js' -import { AccessLists } from '../util.js' +import { AccessLists, toPayloadJson } from '../util.js' import { createAccessList2930Tx } from './constructors.js' @@ -23,6 +24,8 @@ import type { TxValuesArray as AllTypesTxValuesArray, Capability, JSONTx, + SSZTransactionV1, + SSZTransactionType, TransactionCache, TransactionInterface, TxOptions, @@ -208,6 +211,39 @@ export class AccessList2930Tx implements TransactionInterface ({ 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. * @@ -315,6 +351,10 @@ export class AccessList2930Tx implements TransactionInterface 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 f8082b51485..985e3b1d7fb 100644 --- a/packages/tx/src/4844/tx.ts +++ b/packages/tx/src/4844/tx.ts @@ -7,6 +7,7 @@ import { bigIntToUnpaddedBytes, bytesToBigInt, hexToBytes, + setLengthLeft, toBytes, toType, } from '@ethereumjs/util' @@ -17,7 +18,7 @@ import * as EIP2930 from '../capabilities/eip2930.js' import * as Legacy from '../capabilities/legacy.js' import { getBaseJSON, sharedConstructor, valueBoundaryCheck } from '../features/util.js' import { TransactionType } from '../types.js' -import { AccessLists, validateNotArray } from '../util.js' +import { AccessLists, validateNotArray, toPayloadJson } from '../util.js' import { createBlob4844Tx } from './constructors.js' @@ -28,6 +29,8 @@ import type { TxValuesArray as AllTypesTxValuesArray, Capability, JSONTx, + SSZTransactionV1, + SSZTransactionType, TransactionCache, TransactionInterface, TxOptions, @@ -295,6 +298,42 @@ export class Blob4844Tx implements TransactionInterface ({ 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. * @@ -388,6 +427,10 @@ export class Blob4844Tx implements TransactionInterface +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 a576f4f7ff7..89b010ea558 100644 --- a/packages/tx/src/legacy/tx.ts +++ b/packages/tx/src/legacy/tx.ts @@ -1,11 +1,16 @@ 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' @@ -15,6 +20,7 @@ import * as Legacy from '../capabilities/legacy.js' import { getBaseJSON, sharedConstructor, valueBoundaryCheck } from '../features/util.js' import { paramsTx } from '../index.js' import { Capability, TransactionType } from '../types.js' +import { toPayloadJson } from '../util.js' import { createLegacyTx } from './constructors.js' @@ -22,6 +28,8 @@ import type { TxData as AllTypesTxData, TxValuesArray as AllTypesTxValuesArray, JSONTx, + SSZTransactionV1, + SSZTransactionType, TransactionCache, TransactionInterface, TxOptions, @@ -218,6 +226,43 @@ export class LegacyTx implements TransactionInterface { ] } + 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. * @@ -373,6 +418,10 @@ export class LegacyTx implements TransactionInterface { return baseJSON } + toExecutionPayloadTx(): SSZTransactionV1 { + return toPayloadJson(this.sszRaw()) + } + getValidationErrors(): string[] { return Legacy.getValidationErrors(this) } diff --git a/packages/tx/src/transactionFactory.ts b/packages/tx/src/transactionFactory.ts index a9d2153ec7e..59d988f0f3a 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 16f442da16c..51a62390adf 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -13,6 +13,11 @@ import type { BytesLike, PrefixedHexString, } from '@ethereumjs/util' +import type { ValueOf } from '@chainsafe/ssz' +import type { ssz } from '@ethereumjs/util' + +export type SSZTransactionType = ValueOf + /** * Can be used in conjunction with {@link Transaction[TransactionType].supports} * to query on tx capabilities @@ -204,6 +209,7 @@ export interface TransactionInterface { 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 00000000000..b529e81db09 --- /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 fb3513904b3..84f42114d89 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -86,6 +86,8 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "@chainsafe/persistent-merkle-tree": "^0.7.2", + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/rlp": "^6.0.0-alpha.1", "ethereum-cryptography": "^3.0.0" }, diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 50316890362..cdf1943161c 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -64,5 +64,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 00000000000..281ee13334d --- /dev/null +++ b/packages/util/src/ssz.ts @@ -0,0 +1,498 @@ +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 ReceiptRootsList = new ListCompositeType(Bytes32, MAX_TRANSACTIONS_PER_PAYLOAD) +export type ReceiptsType = ValueOf + +export const MAX_BLOCKHEADER_FIELDS = 64 +const MAX_EXTRA_DATA_BYTES = 32 + +export function computeReceiptInclusionProof( + receipts: ReceiptsType, + index: number, + fromRoots = true, +): { merkleBranch: Uint8Array[]; receiptRoot: Uint8Array } { + if (index >= receipts.length) { + throw Error(`Invalid index=${index} > receipts=${receipts.length}`) + } + + const receiptRoot = Receipt.hashTreeRoot(receipts[index]) + + let merkleBranch + if (fromRoots === true) { + const receiptRoots = receipts.map((tx) => Receipt.hashTreeRoot(tx)) + const ReceiptsRootView = ReceiptRootsList.toView(receiptRoots) + // transaction index is its g index in the list + merkleBranch = new Tree(ReceiptsRootView.node).getSingleProof( + // same gindex as transaction + TRANSACTION_GINDEX0 + BigInt(index), + ) + } else { + const ReceiptsView = Receipts.toView(receipts) + // transaction index is its g index in the list + merkleBranch = new Tree(ReceiptsView.node).getSingleProof( + // same gindex as transaction + TRANSACTION_GINDEX0 + BigInt(index), + ) + } + + return { merkleBranch, receiptRoot } +} + +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 00000000000..1ac7457844a --- /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 31e0fc95873..48aabafefdb 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -56,6 +56,7 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "@chainsafe/ssz": "^0.18.0", "@ethereumjs/block": "^6.0.0-alpha.1", "@ethereumjs/common": "^5.0.0-alpha.1", "@ethereumjs/evm": "^4.0.0-alpha.1", diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index 7d893c8e4b2..e155d016912 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -2,7 +2,9 @@ import { createBlock, createSealedCliqueBlock, genRequestsRoot, + genTransactionsSszRoot, genTransactionsTrieRoot, + genWithdrawalsSszRoot, genWithdrawalsTrieRoot, } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' @@ -15,30 +17,39 @@ import { BIGINT_1, BIGINT_2, GWEI_TO_WEI, - KECCAK256_RLP, TypeOutput, + bigIntToBytes, createWithdrawal, createZeroAddress, + hexToBytes, + setLengthLeft, + ssz, toBytes, toType, + utf8ToBytes, } from '@ethereumjs/util' import { sha256 } from 'ethereum-cryptography/sha256' +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' @@ -76,6 +87,7 @@ export class BlockBuilder { private checkpointed = false private blockStatus: BlockStatus = { status: BuildStatus.Pending } + systemLogs?: Log[] get transactionReceipts() { return this.transactionResults.map((result) => result.receipt) } @@ -144,10 +156,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 })) } /** @@ -166,16 +190,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() } /** @@ -300,6 +330,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: @@ -319,18 +399,22 @@ 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() + this.systemLogs = systemLogs + + 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 @@ -342,11 +426,9 @@ export class BlockBuilder { blobGasUsed = this.blobGasUsed } - let requests let requestsHash if (this.vm.common.isActivatedEIP(7685)) { const sha256Function = this.vm.common.customCrypto.sha256 ?? sha256 - requests = await accumulateRequests(this.vm, this.transactionResults) requestsHash = genRequestsRoot(requests, sha256Function) } @@ -364,6 +446,7 @@ export class BlockBuilder { // correct excessBlobGas should already be part of headerData used above blobGasUsed, requestsHash, + systemLogsRoot, } if (consensusType === ConsensusType.ProofOfWork) { diff --git a/packages/vm/src/index.ts b/packages/vm/src/index.ts index f284760a753..29f3a3d434d 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -3,7 +3,7 @@ export { BlockBuilder, BuildStatus } from './buildBlock.js' export { buildBlock } from './buildBlock.js' export * from './constructors.js' export * from './params.js' -export { encodeReceipt } from './runBlock.js' +export { encodeReceipt, encodeSszReceipt } from './runBlock.js' export { runBlock } from './runBlock.js' export { runTx } from './runTx.js' export * from './types.js' diff --git a/packages/vm/src/params.ts b/packages/vm/src/params.ts index 34b22fda30f..02f4f869de2 100644 --- a/packages/vm/src/params.ts +++ b/packages/vm/src/params.ts @@ -105,4 +105,9 @@ export const paramsVM: ParamsDict = { maxBlobGasPerBlock: 1179648, // The max blob gas allowable per block blobGasPriceUpdateFraction: 5007716, // The denominator used in the exponential when calculating a blob gas price }, + 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 0d8c1f122f1..a011b03dbef 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -1,6 +1,6 @@ import { createBlock, genRequestsRoot } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' -import { type EVM, type EVMInterface, VerkleAccessWitness } from '@ethereumjs/evm' +import { type EVM, type EVMInterface, VerkleAccessWitness, Log } from '@ethereumjs/evm' import { MerklePatriciaTrie } from '@ethereumjs/mpt' import { RLP } from '@ethereumjs/rlp' import { StatelessVerkleStateManager, verifyVerkleStateProof } from '@ethereumjs/statemanager' @@ -23,10 +23,13 @@ import { intToBytes, setLengthLeft, short, + ssz, unprefixedHexToBytes, + utf8ToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' -import { sha256 } from 'ethereum-cryptography/sha256' +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' @@ -45,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 { CLRequest, CLRequestType, PrefixedHexString } from '@ethereumjs/util' +export type SSZReceiptType = ValueOf + const debug = debugDefault('vm:block') const parentBeaconBlockRootAddress = createAddressFromString( @@ -223,11 +229,22 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise[] | undefined if (block.common.isActivatedEIP(7685)) { const sha256Function = vm.common.customCrypto.sha256 ?? sha256 - requests = await accumulateRequests(vm, result.results) - requestsHash = genRequestsRoot(requests, sha256Function) + requestsHash = genRequestsRoot(result.requests!, sha256Function) + } + + 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 @@ -253,13 +270,28 @@ 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 } /** @@ -609,11 +678,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[] = [] @@ -663,8 +727,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) { @@ -672,7 +734,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, @@ -794,6 +872,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 fa4d6565a7c..daa3516d638 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -13,6 +13,7 @@ import { MAX_UINT64, SECP256K1_ORDER_DIV_2, bigIntMax, + bigIntToBytes, bytesToBigInt, bytesToHex, bytesToUnprefixedHex, @@ -21,7 +22,9 @@ import { equalsBytes, hexToBytes, publicToAddress, + setLengthLeft, short, + utf8ToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' import { keccak256 } from 'ethereum-cryptography/keccak.js' @@ -679,6 +682,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) { @@ -884,6 +903,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 2ac8228baff..ef398a18d93 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[] } /** @@ -488,6 +496,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 { From 0cf90f8cf2a9f992dc73bc9021b4fa54750e72a4 Mon Sep 17 00:00:00 2001 From: harkamal Date: Sun, 26 Jan 2025 17:35:38 +0530 Subject: [PATCH 2/4] rebase, lint and build fixes --- packages/block/src/block/block.ts | 8 +- packages/block/src/header/header.ts | 4 +- packages/block/src/helpers.ts | 12 +- packages/client/bin/utils.ts | 2 +- .../client/src/rpc/modules/engine/engine.ts | 509 ++++++++---------- packages/evm/src/evm.ts | 4 +- packages/tx/src/1559/tx.ts | 2 +- packages/tx/src/2930/tx.ts | 2 +- packages/tx/src/4844/tx.ts | 4 +- packages/tx/src/7702/tx.ts | 4 +- packages/tx/src/legacy/tx.ts | 2 +- packages/tx/src/types.ts | 6 +- packages/util/src/ssz.ts | 2 +- packages/vm/src/buildBlock.ts | 4 +- packages/vm/src/runBlock.ts | 5 +- 15 files changed, 253 insertions(+), 317 deletions(-) diff --git a/packages/block/src/block/block.ts b/packages/block/src/block/block.ts index 7f2a3d0a819..49cefd3c20f 100644 --- a/packages/block/src/block/block.ts +++ b/packages/block/src/block/block.ts @@ -2,13 +2,7 @@ import { ConsensusType } from '@ethereumjs/common' import { MerklePatriciaTrie } from '@ethereumjs/mpt' import { RLP } from '@ethereumjs/rlp' import { Blob4844Tx, Capability } from '@ethereumjs/tx' -import { - BIGINT_0, - KECCAK256_RLP, - KECCAK256_RLP_ARRAY, - bytesToHex, - equalsBytes, -} from '@ethereumjs/util' +import { BIGINT_0, KECCAK256_RLP_ARRAY, bytesToHex, equalsBytes } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { sha256 } from 'ethereum-cryptography/sha256' diff --git a/packages/block/src/header/header.ts b/packages/block/src/header/header.ts index 3f528a243cb..924946d961d 100644 --- a/packages/block/src/header/header.ts +++ b/packages/block/src/header/header.ts @@ -186,7 +186,7 @@ export class BlockHeader { hardforkDefaults.parentBeaconBlockRoot const requestsHash = toType(headerData.requestsHash, TypeOutput.Uint8Array) ?? hardforkDefaults.requestsHash - const systemLogsRoot = + const systemLogsRoot = toType(headerData.systemLogsRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.systemLogsRoot if (!this.common.isActivatedEIP(1559) && baseFeePerGas !== undefined) { @@ -669,7 +669,7 @@ export class BlockHeader { withdrawalsRoot: this.withdrawalsRoot ?? null, excessGas: { regular: null, blob: this.excessBlobGas ?? null }, parentBeaconBlockRoot: this.parentBeaconBlockRoot ?? null, - requestsRoot: this.requestsRoot ?? null, + requestsHash: this.requestsHash ?? null, systemLogsRoot: this.systemLogsRoot ?? null, } diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index a5641d827bb..d306695e23a 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -1,13 +1,21 @@ import { MerklePatriciaTrie } from '@ethereumjs/mpt' import { RLP } from '@ethereumjs/rlp' import { Blob4844Tx } from '@ethereumjs/tx' -import { BIGINT_0, BIGINT_1, TypeOutput, concatBytes, isHexString, ssz, toType } from '@ethereumjs/util' +import { + BIGINT_0, + BIGINT_1, + TypeOutput, + concatBytes, + isHexString, + ssz, + toType, +} from '@ethereumjs/util' import type { BlockHeaderBytes, HeaderData } from './types.js' +import type { ValueOf } from '@chainsafe/ssz' import type { Common } from '@ethereumjs/common' import type { TypedTransaction } from '@ethereumjs/tx' import type { CLRequest, CLRequestType, PrefixedHexString, Withdrawal } from '@ethereumjs/util' -import type { ValueOf } from '@chainsafe/ssz' export type SSZTransactionType = ValueOf /** diff --git a/packages/client/bin/utils.ts b/packages/client/bin/utils.ts index 64a8d2e4da6..c83b3946809 100644 --- a/packages/client/bin/utils.ts +++ b/packages/client/bin/utils.ts @@ -745,7 +745,7 @@ export async function generateClientConfig(args: ClientOpts) { 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/rpc/modules/engine/engine.ts b/packages/client/src/rpc/modules/engine/engine.ts index 9ed2f182e2d..308aa29372e 100644 --- a/packages/client/src/rpc/modules/engine/engine.ts +++ b/packages/client/src/rpc/modules/engine/engine.ts @@ -351,11 +351,12 @@ export class Engine { (PrefixedHexString[] | null)?, ], ): Promise { - return this.newPayloadWrap(params).catch(e=>{ - console.log('newPayload', e); - throw e; + return this.newPayloadWrap(params).catch((e) => { + console.log('newPayload', e) + throw e }) } + private async newPayloadWrap( params: [ ExecutionPayload, @@ -403,7 +404,9 @@ export class Engine { ) response = { status: Status.INVALID, latestValidHash, validationError } } - const { parentHash, blockHash } = payload + // skip marking the block invalid as this is more of a data issue from CL + return response + } /** * Stats and hardfork updates @@ -430,19 +433,14 @@ export class Engine { (await this.chain.getBlock(hexToBytes(parentHash as PrefixedHexString))) /** - * Validate blob versioned hashes in the context of EIP-4844 blob transactions + * 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)) { - 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) + try { + headBlock.validateBlobTransactions(parent.header) + } catch (error: any) { + const validationError = `Invalid 4844 transactions: ${error}` const latestValidHash = await validHash( hexToBytes(parentHash as PrefixedHexString), this.chain, @@ -452,149 +450,26 @@ 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 } /** - * Stats and hardfork updates + * Check for executed parent */ - 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))) - - /** - * 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 + 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) - // 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 && @@ -612,23 +487,9 @@ export class Engine { return response } - 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 - } - + /** + * Invalid execution + */ if ( this.execution.chainStatus?.status === ExecStatus.INVALID && optimisticLookup && @@ -659,143 +520,215 @@ export class Engine { } } - /** - * 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 + 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, } + return response + } - blocks.push(headBlock) + this.remoteBlocks.set(bytesToUnprefixedHex(headBlock.hash()), 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 } - return response - } else { - this.executedBlocks.set(bytesToUnprefixedHex(block.hash()), block) - } - } - } - } catch (error) { + // 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 const latestValidHash = await validHash( - headBlock.header.parentHash, + 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 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 } + 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 } return response } else { - throw { - code: INTERNAL_ERROR, - message: errorMsg, - } + this.executedBlocks.set(bytesToUnprefixedHex(block.hash()), block) } } - - 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 } + } catch (error) { + const latestValidHash = await validHash( + headBlock.header.parentHash, + this.chain, + this.chainCache, + ) - const response = { - status: Status.VALID, - latestValidHash: bytesToHex(headBlock.hash()), - 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 { + throw { + code: INTERNAL_ERROR, + message: errorMsg, + } + } } + + 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 - } catch (e) { - console.log('newPayload', e) - throw e } + + const response = { + status: Status.VALID, + latestValidHash: bytesToHex(headBlock.hash()), + validationError: null, + } + return response } /** diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index ee01e67ef5e..9e2cbf5663f 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -20,8 +20,8 @@ import { utf8ToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' -import { EventEmitter } from 'eventemitter3' import { keccak256 } from 'ethereum-cryptography/keccak.js' +import { EventEmitter } from 'eventemitter3' import { FORMAT } from './eof/constants.js' import { isEOF } from './eof/util.js' @@ -56,8 +56,8 @@ import type { MessageWithTo } from './message.js' import type { AsyncDynamicGasHandler, SyncDynamicGasHandler } from './opcodes/gas.js' import type { OpHandler, OpcodeList, OpcodeMap } from './opcodes/index.js' import type { CustomPrecompile, PrecompileFunc } from './precompiles/index.js' -import type { VerkleAccessWitness } from './verkleAccessWitness.js' import type { Log } from './types.js' +import type { VerkleAccessWitness } from './verkleAccessWitness.js' import type { Common, StateManagerInterface } from '@ethereumjs/common' const debug = debugDefault('evm:evm') diff --git a/packages/tx/src/1559/tx.ts b/packages/tx/src/1559/tx.ts index a66ccf9b727..6e91c171383 100644 --- a/packages/tx/src/1559/tx.ts +++ b/packages/tx/src/1559/tx.ts @@ -26,8 +26,8 @@ import type { TxValuesArray as AllTypesTxValuesArray, Capability, JSONTx, - SSZTransactionV1, SSZTransactionType, + SSZTransactionV1, TransactionCache, TransactionInterface, TxOptions, diff --git a/packages/tx/src/2930/tx.ts b/packages/tx/src/2930/tx.ts index 93424c70de3..8d325f29f29 100644 --- a/packages/tx/src/2930/tx.ts +++ b/packages/tx/src/2930/tx.ts @@ -24,8 +24,8 @@ import type { TxValuesArray as AllTypesTxValuesArray, Capability, JSONTx, - SSZTransactionV1, SSZTransactionType, + SSZTransactionV1, TransactionCache, TransactionInterface, TxOptions, diff --git a/packages/tx/src/4844/tx.ts b/packages/tx/src/4844/tx.ts index 985e3b1d7fb..bcb4f413462 100644 --- a/packages/tx/src/4844/tx.ts +++ b/packages/tx/src/4844/tx.ts @@ -18,7 +18,7 @@ import * as EIP2930 from '../capabilities/eip2930.js' import * as Legacy from '../capabilities/legacy.js' import { getBaseJSON, sharedConstructor, valueBoundaryCheck } from '../features/util.js' import { TransactionType } from '../types.js' -import { AccessLists, validateNotArray, toPayloadJson } from '../util.js' +import { AccessLists, toPayloadJson, validateNotArray } from '../util.js' import { createBlob4844Tx } from './constructors.js' @@ -29,8 +29,8 @@ import type { TxValuesArray as AllTypesTxValuesArray, Capability, JSONTx, - SSZTransactionV1, SSZTransactionType, + SSZTransactionV1, TransactionCache, TransactionInterface, TxOptions, diff --git a/packages/tx/src/7702/tx.ts b/packages/tx/src/7702/tx.ts index 4ff066c5a9b..c48397dcfa6 100644 --- a/packages/tx/src/7702/tx.ts +++ b/packages/tx/src/7702/tx.ts @@ -14,7 +14,7 @@ import * as EIP7702 from '../capabilities/eip7702.js' import * as Legacy from '../capabilities/legacy.js' import { getBaseJSON, sharedConstructor, valueBoundaryCheck } from '../features/util.js' import { TransactionType } from '../types.js' -import { AccessLists, AuthorizationLists, validateNotArray, toPayloadJson } from '../util.js' +import { AccessLists, AuthorizationLists, toPayloadJson, validateNotArray } from '../util.js' import { createEOACode7702Tx } from './constructors.js' @@ -27,8 +27,8 @@ import type { AuthorizationListBytes, Capability, JSONTx, - SSZTransactionV1, SSZTransactionType, + SSZTransactionV1, TransactionCache, TransactionInterface, TxOptions, diff --git a/packages/tx/src/legacy/tx.ts b/packages/tx/src/legacy/tx.ts index 89b010ea558..5afbd46756d 100644 --- a/packages/tx/src/legacy/tx.ts +++ b/packages/tx/src/legacy/tx.ts @@ -28,8 +28,8 @@ import type { TxData as AllTypesTxData, TxValuesArray as AllTypesTxValuesArray, JSONTx, - SSZTransactionV1, SSZTransactionType, + SSZTransactionV1, TransactionCache, TransactionInterface, TxOptions, diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index 51a62390adf..8b02b8baa58 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -5,6 +5,7 @@ import type { AccessList2930Tx } from './2930/tx.js' import type { Blob4844Tx } from './4844/tx.js' import type { EOACode7702Tx } from './7702/tx.js' import type { LegacyTx } from './legacy/tx.js' +import type { ValueOf } from '@chainsafe/ssz' import type { Common, Hardfork, ParamsDict } from '@ethereumjs/common' import type { Address, @@ -12,9 +13,8 @@ import type { BigIntLike, BytesLike, PrefixedHexString, + ssz, } from '@ethereumjs/util' -import type { ValueOf } from '@chainsafe/ssz' -import type { ssz } from '@ethereumjs/util' export type SSZTransactionType = ValueOf @@ -559,7 +559,7 @@ export interface JSONTx { yParity?: PrefixedHexString } -export type SSZTransactionV1 = ssz.TransactionV1; +export type SSZTransactionV1 = ssz.TransactionV1 export type JSONBlobTxNetworkWrapper = JSONTx & { blobs: PrefixedHexString[] diff --git a/packages/util/src/ssz.ts b/packages/util/src/ssz.ts index 281ee13334d..febeb02e41d 100644 --- a/packages/util/src/ssz.ts +++ b/packages/util/src/ssz.ts @@ -481,7 +481,7 @@ export const BlockHeader = new StableContainerType( withdrawalsRoot: new OptionalType(Bytes32), excessGas: new OptionalType(FeesPerGas), parentBeaconBlockRoot: new OptionalType(Bytes32), - requestsRoot: new OptionalType(Bytes32), + requestsHash: new OptionalType(Bytes32), systemLogsRoot: new OptionalType(Bytes32), }, MAX_BLOCKHEADER_FIELDS, diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index e155d016912..4fcd302dca4 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -28,8 +28,8 @@ import { toType, utf8ToBytes, } from '@ethereumjs/util' -import { sha256 } from 'ethereum-cryptography/sha256' import { keccak256 } from 'ethereum-cryptography/keccak.js' +import { sha256 } from 'ethereum-cryptography/sha256' import { Bloom } from './bloom/index.js' import { accumulateRequests } from './requests.js' @@ -429,7 +429,7 @@ export class BlockBuilder { let requestsHash if (this.vm.common.isActivatedEIP(7685)) { const sha256Function = this.vm.common.customCrypto.sha256 ?? sha256 - requestsHash = genRequestsRoot(requests, sha256Function) + requestsHash = genRequestsRoot(requests!, sha256Function) } // get stateRoot after all the accumulateRequests etc have been done diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index a011b03dbef..0186438242e 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -1,6 +1,6 @@ import { createBlock, genRequestsRoot } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' -import { type EVM, type EVMInterface, VerkleAccessWitness, Log } from '@ethereumjs/evm' +import { type EVM, type EVMInterface, VerkleAccessWitness } from '@ethereumjs/evm' import { MerklePatriciaTrie } from '@ethereumjs/mpt' import { RLP } from '@ethereumjs/rlp' import { StatelessVerkleStateManager, verifyVerkleStateProof } from '@ethereumjs/statemanager' @@ -51,7 +51,8 @@ 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 { CLRequest, CLRequestType, PrefixedHexString } from '@ethereumjs/util' +import type { Log } from '@ethereumjs/evm' +import type { PrefixedHexString } from '@ethereumjs/util' export type SSZReceiptType = ValueOf From 078620819757df2a76532112d74660a9d7852d4e Mon Sep 17 00:00:00 2001 From: harkamal Date: Mon, 27 Jan 2025 22:05:04 +0530 Subject: [PATCH 3/4] update and debug end to end client test and apply fixes --- packages/block/src/header/header.ts | 2 +- .../client/src/rpc/modules/engine/engine.ts | 3 ++- .../client/src/rpc/modules/engine/types.ts | 1 + .../test/rpc/engine/newPayloadEip6493.spec.ts | 21 +++++++------------ 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/block/src/header/header.ts b/packages/block/src/header/header.ts index 924946d961d..27ae25347c3 100644 --- a/packages/block/src/header/header.ts +++ b/packages/block/src/header/header.ts @@ -656,7 +656,7 @@ export class BlockHeader { number: this.number, gasLimits: { regular: this.gasLimit, - blob: this.common.isActivatedEIP(4844) ? this.common.param('maxblobGasPerBlock') : null, + blob: this.common.isActivatedEIP(4844) ? this.common.param('maxBlobGasPerBlock') : null, }, gasUsed: { regular: this.gasUsed, blob: this.blobGasUsed ?? null }, timestamp: this.timestamp, diff --git a/packages/client/src/rpc/modules/engine/engine.ts b/packages/client/src/rpc/modules/engine/engine.ts index 308aa29372e..1d47c62eeb2 100644 --- a/packages/client/src/rpc/modules/engine/engine.ts +++ b/packages/client/src/rpc/modules/engine/engine.ts @@ -58,6 +58,7 @@ import type { ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, + ExecutionPayloadV4, ForkchoiceResponseV1, ForkchoiceStateV1, PayloadAttributes, @@ -838,7 +839,7 @@ export class Engine { } async newPayloadV4( - params: [ExecutionPayloadV3, Bytes32[], Bytes32, Bytes32[]], + params: [ExecutionPayloadV4, Bytes32[], Bytes32, Bytes32[]], ): Promise { const pragueTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Prague) const ts = parseInt(params[0].timestamp) diff --git a/packages/client/src/rpc/modules/engine/types.ts b/packages/client/src/rpc/modules/engine/types.ts index 8563a7c59a7..2eebd62c33f 100644 --- a/packages/client/src/rpc/modules/engine/types.ts +++ b/packages/client/src/rpc/modules/engine/types.ts @@ -27,6 +27,7 @@ export type ExecutionPayloadV1 = ExecutionPayload export type ExecutionPayloadV2 = ExecutionPayloadV1 & { withdrawals: WithdrawalV1[] } // parentBeaconBlockRoot comes separate in new payloads and needs to be added to payload data export type ExecutionPayloadV3 = ExecutionPayloadV2 & { excessBlobGas: Uint64; blobGasUsed: Uint64 } +export type ExecutionPayloadV4 = ExecutionPayloadV3 & { systemLogsRoot: Bytes32} export type ForkchoiceStateV1 = { headBlockHash: Bytes32 diff --git a/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts b/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts index 12897be6460..27a6da8b447 100644 --- a/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts @@ -57,21 +57,18 @@ describe(`${method}: call with executionPayloadV4`, () => { withdrawals: [], blobGasUsed: '0x0', excessBlobGas: '0x0', - depositRequests: [], - withdrawalRequests: [], - consolidationRequests: [], systemLogsRoot: '0x3850240388ff8bed46a8631179e63ad67e28c343be54906cfaec0c3a2d95e71e', receiptsRoot: '0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1', parentHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', - stateRoot: '0x9d95c5098ef0f1b45fef49659318055ac4f06dc6601d7baf3656a391381981e3', - blockHash: '0x390042a0aefa4a11387652e215dd698a45dc5698d152ee0270a162e697420352', + stateRoot: '0xc3cc6a86d4db9edbc48b107ff140a9a184312c1daae0c079438e0d0aa88270ac', + blockHash: '0x49d123d84781969ee07cef6bfd08c4380e7eddf7cd304d1b5207194ef3d36301', } let res res = await rpc.request(`eth_getBlockByNumber`, ['0x0', false]) assert.equal(res.result.hash, validForkChoiceState.headBlockHash) - res = await rpc.request(method, [validBlock, [], parentBeaconBlockRoot]) + res = await rpc.request(method, [validBlock, [], parentBeaconBlockRoot, []]) console.log(res) assert.equal(res.result.status, 'VALID') @@ -114,20 +111,16 @@ describe(`${method}: call with executionPayloadV4`, () => { }) res = await rpc.request('engine_getPayloadV4', [payloadId]) - const { executionPayload } = res.result + const { executionPayload,executionRequests } = 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', + executionRequests.length === 1, + 'executionRequests should have 1 deposit request', ) console.log(executionPayload) - res = await rpc.request(method, [executionPayload, [], parentBeaconBlockRoot]) + res = await rpc.request(method, [executionPayload, [], parentBeaconBlockRoot, executionRequests]) assert.equal(res.result.status, 'VALID') const newBlockHashHex = executionPayload.blockHash From 46b669f9eb1f2aae8f3daa80ed2b65990cc06ebf Mon Sep 17 00:00:00 2001 From: harkamal Date: Mon, 27 Jan 2025 22:07:24 +0530 Subject: [PATCH 4/4] lint --- packages/client/src/rpc/modules/engine/types.ts | 2 +- .../test/rpc/engine/newPayloadEip6493.spec.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/client/src/rpc/modules/engine/types.ts b/packages/client/src/rpc/modules/engine/types.ts index 2eebd62c33f..1fa74242ea9 100644 --- a/packages/client/src/rpc/modules/engine/types.ts +++ b/packages/client/src/rpc/modules/engine/types.ts @@ -27,7 +27,7 @@ export type ExecutionPayloadV1 = ExecutionPayload export type ExecutionPayloadV2 = ExecutionPayloadV1 & { withdrawals: WithdrawalV1[] } // parentBeaconBlockRoot comes separate in new payloads and needs to be added to payload data export type ExecutionPayloadV3 = ExecutionPayloadV2 & { excessBlobGas: Uint64; blobGasUsed: Uint64 } -export type ExecutionPayloadV4 = ExecutionPayloadV3 & { systemLogsRoot: Bytes32} +export type ExecutionPayloadV4 = ExecutionPayloadV3 & { systemLogsRoot: Bytes32 } export type ForkchoiceStateV1 = { headBlockHash: Bytes32 diff --git a/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts b/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts index 27a6da8b447..787a27be84a 100644 --- a/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadEip6493.spec.ts @@ -111,16 +111,18 @@ describe(`${method}: call with executionPayloadV4`, () => { }) res = await rpc.request('engine_getPayloadV4', [payloadId]) - const { executionPayload,executionRequests } = res.result + const { executionPayload, executionRequests } = res.result assert.ok(executionPayload.transactions.length === 2, 'two transactions should have been added') - assert.ok( - executionRequests.length === 1, - 'executionRequests should have 1 deposit request', - ) + assert.ok(executionRequests.length === 1, 'executionRequests should have 1 deposit request') console.log(executionPayload) - res = await rpc.request(method, [executionPayload, [], parentBeaconBlockRoot, executionRequests]) + res = await rpc.request(method, [ + executionPayload, + [], + parentBeaconBlockRoot, + executionRequests, + ]) assert.equal(res.result.status, 'VALID') const newBlockHashHex = executionPayload.blockHash