diff --git a/packages/block/test/testdata/payload-kaustinen.json b/packages/block/test/testdata/payload-kaustinen.json index 9c98a50678..e7492350d0 100644 --- a/packages/block/test/testdata/payload-kaustinen.json +++ b/packages/block/test/testdata/payload-kaustinen.json @@ -242,9 +242,7 @@ } ], "verkleProof": { - "otherStems": [ - "0x9444524c2261a7086dd332bb05d79b1c63e74913a97b7b85c680368b19db0e" - ], + "otherStems": ["0x9444524c2261a7086dd332bb05d79b1c63e74913a97b7b85c680368b19db0e"], "depthExtensionPresent": "0x0a101209120808", "commitmentsByPath": [ "0x1ba24c38c9aff43cf2793ed22a2fdd04282d558bcfad33589b5e4697c0662e45", @@ -284,4 +282,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index b801b44260..65734e5a01 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -327,6 +327,14 @@ export interface ConfigOptions { snapAvailabilityDepth?: bigint snapTransitionSafeDepth?: bigint + /** + * Save account keys preimages in the meta db (default: false) + */ + savePreimages?: boolean + + /** + * Enables stateless verkle block execution (default: false) + */ statelessVerkle?: boolean } @@ -431,6 +439,7 @@ export class Config { // Defaulting to false as experimental as of now public readonly enableSnapSync: boolean public readonly useStringValueTrieDB: boolean + public readonly savePreimages: boolean public readonly statelessVerkle: boolean @@ -477,6 +486,7 @@ export class Config { this.debugCode = options.debugCode ?? Config.DEBUGCODE_DEFAULT this.mine = options.mine ?? false this.isSingleNode = options.isSingleNode ?? false + this.savePreimages = options.savePreimages ?? false if (options.vmProfileBlocks !== undefined || options.vmProfileTxs !== undefined) { this.vmProfilerOpts = { diff --git a/packages/client/src/execution/preimage.ts b/packages/client/src/execution/preimage.ts new file mode 100644 index 0000000000..53ef200415 --- /dev/null +++ b/packages/client/src/execution/preimage.ts @@ -0,0 +1,27 @@ +import { DBKey, MetaDBManager } from '../util/metaDBManager' + +/** + * The `PreImagesManager` saves the preimages of hashed keys. This is necessary for the Verkle transition. + * A "PreImage" of a hash is whatever the input is to the hashed function. So, if one calls `keccak256(X)` with + * output `Y` then `X` is the preimage of `Y`. It thus serves to recover the input to the trapdoor hash function, + * which would otherwise not be feasible. + */ +export class PreimagesManager extends MetaDBManager { + /** + * Returns the preimage for a given hashed key + * @param key the hashed key + * @returns the preimage of the hashed key + */ + async getPreimage(key: Uint8Array): Promise { + return this.get(DBKey.Preimage, key) + } + + /** + * Saves a preimage to the db for a given hashed key. + * @param key The hashed key + * @param preimage The preimage to save + */ + async savePreimage(key: Uint8Array, preimage: Uint8Array) { + await this.put(DBKey.Preimage, key, preimage) + } +} diff --git a/packages/client/src/execution/vmexecution.ts b/packages/client/src/execution/vmexecution.ts index 3335965083..7a852451f6 100644 --- a/packages/client/src/execution/vmexecution.ts +++ b/packages/client/src/execution/vmexecution.ts @@ -12,7 +12,15 @@ import { StatelessVerkleStateManager, } from '@ethereumjs/statemanager' import { Trie } from '@ethereumjs/trie' -import { BIGINT_0, BIGINT_1, Lock, ValueEncoding, bytesToHex, equalsBytes } from '@ethereumjs/util' +import { + BIGINT_0, + BIGINT_1, + Lock, + ValueEncoding, + bytesToHex, + equalsBytes, + hexToBytes, +} from '@ethereumjs/util' import { VM } from '@ethereumjs/vm' import { Event } from '../types' @@ -21,6 +29,7 @@ import { debugCodeReplayBlock } from '../util/debug' import { Execution } from './execution' import { LevelDB } from './level' +import { PreimagesManager } from './preimage' import { ReceiptsManager } from './receipt' import type { ExecutionOptions } from './execution' @@ -50,6 +59,7 @@ export class VMExecution extends Execution { public chainStatus: ChainStatus | null = null public receiptsManager?: ReceiptsManager + public preimagesManager?: PreimagesManager private pendingReceipts?: Map private vmPromise?: Promise @@ -94,25 +104,34 @@ export class VMExecution extends Execution { ;(this.vm as any).blockchain = this.chain.blockchain } - if (this.metaDB && this.config.saveReceipts) { - this.receiptsManager = new ReceiptsManager({ - chain: this.chain, - config: this.config, - metaDB: this.metaDB, - }) - this.pendingReceipts = new Map() - this.chain.blockchain.events.addListener( - 'deletedCanonicalBlocks', - async (blocks, resolve) => { - // Once a block gets deleted from the chain, delete the receipts also - for (const block of blocks) { - await this.receiptsManager?.deleteReceipts(block) - } - if (resolve !== undefined) { - resolve() + if (this.metaDB) { + if (this.config.saveReceipts) { + this.receiptsManager = new ReceiptsManager({ + chain: this.chain, + config: this.config, + metaDB: this.metaDB, + }) + this.pendingReceipts = new Map() + this.chain.blockchain.events.addListener( + 'deletedCanonicalBlocks', + async (blocks, resolve) => { + // Once a block gets deleted from the chain, delete the receipts also + for (const block of blocks) { + await this.receiptsManager?.deleteReceipts(block) + } + if (resolve !== undefined) { + resolve() + } } - } - ) + ) + } + if (this.config.savePreimages) { + this.preimagesManager = new PreimagesManager({ + chain: this.chain, + config: this.config, + metaDB: this.metaDB, + }) + } } } @@ -407,8 +426,18 @@ export class VMExecution extends Execution { if (skipHeaderValidation) { skipBlockchain = true } + const reportPreimages = this.config.savePreimages - const result = await vm.runBlock({ clearCache, ...opts, skipHeaderValidation }) + const result = await vm.runBlock({ + clearCache, + ...opts, + skipHeaderValidation, + reportPreimages, + }) + + if (this.config.savePreimages && result.preimages !== undefined) { + await this.savePreimages(result.preimages) + } receipts = result.receipts } if (receipts !== undefined) { @@ -438,6 +467,14 @@ export class VMExecution extends Execution { return true } + async savePreimages(preimages: Map) { + if (this.preimagesManager !== undefined) { + for (const [key, preimage] of preimages) { + await this.preimagesManager.savePreimage(hexToBytes(key), preimage) + } + } + } + /** * Sets the chain to a new head block. * Should only be used after {@link VMExecution.runWithoutSetHead} @@ -677,6 +714,7 @@ export class VMExecution extends Execution { clearCache, skipBlockValidation, skipHeaderValidation: true, + reportPreimages: this.config.savePreimages, }) const afterTS = Date.now() const diffSec = Math.round((afterTS - beforeTS) / 1000) @@ -691,6 +729,9 @@ export class VMExecution extends Execution { } await this.receiptsManager?.saveReceipts(block, result.receipts) + if (this.config.savePreimages && result.preimages !== undefined) { + await this.savePreimages(result.preimages) + } txCounter += block.transactions.length // set as new head block diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 98aa1be088..e551c49e1e 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -163,6 +163,7 @@ export interface ClientOpts { vmProfileTxs?: boolean loadBlocksFromRlp?: string pruneEngineCache?: boolean + savePreimages?: boolean verkleGenesisStateRoot?: Uint8Array statelessVerkle?: boolean engineNewpayloadMaxExecute?: number diff --git a/packages/client/src/util/metaDBManager.ts b/packages/client/src/util/metaDBManager.ts index ede8a4820c..eae48c6c52 100644 --- a/packages/client/src/util/metaDBManager.ts +++ b/packages/client/src/util/metaDBManager.ts @@ -20,6 +20,7 @@ export enum DBKey { SkeletonBlockHashToNumber, SkeletonStatus, SkeletonUnfinalizedBlockByHash, + Preimage, } export interface MetaDBManagerOptions { diff --git a/packages/client/test/rpc/engine/preimages.spec.ts b/packages/client/test/rpc/engine/preimages.spec.ts new file mode 100644 index 0000000000..e5c8350a96 --- /dev/null +++ b/packages/client/test/rpc/engine/preimages.spec.ts @@ -0,0 +1,267 @@ +import { Block, BlockHeader } from '@ethereumjs/block' +import { TransactionFactory } from '@ethereumjs/tx' +import { + Withdrawal, + bytesToHex, + equalsBytes, + hexToBytes, + intToBytes, + intToHex, + setLengthRight, +} from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak.js' +import * as td from 'testdouble' +import { assert, describe, it } from 'vitest' + +import { blockToExecutionPayload } from '../../../src/rpc/modules/index.js' +import blocks from '../../testdata/blocks/kaustinen2.json' +import genesisJSON from '../../testdata/geth-genesis/kaustinen2.json' +import { getRpcClient, setupChain } from '../helpers.js' + +import type { Common } from '@ethereumjs/common' +import type { PrefixedHexString } from '@ethereumjs/util' +import type { HttpClient } from 'jayson/promise' + +const genesisStateRoot = '0x78026f1e4f2ff57c340634f844f47cb241beef4c965be86a483c855793e4b07d' +const genesisBlockHash = '0x76a519ccb8a2b12d733ad7d88e2d5f4a11d6dc6ca320edccd3b8a3e9081ca1b3' + +const originalValidate = (BlockHeader as any).prototype._consensusFormatValidation +BlockHeader.prototype['_consensusFormatValidation'] = () => {} //stub + +async function genBlockWithdrawals(blockNumber: number) { + // if block 1, bundle 0 withdrawals + const withdrawals = + blockNumber === 1 + ? [] + : Array.from({ length: 8 }, (_v, i) => { + const withdrawalIndex = blockNumber * 16 + i + + // just return a withdrawal based on withdrawalIndex + return { + index: intToHex(withdrawalIndex), + validatorIndex: intToHex(withdrawalIndex), + address: bytesToHex(setLengthRight(intToBytes(withdrawalIndex), 20)), + amount: intToHex(withdrawalIndex), + } + }) + const withdrawalsRoot = bytesToHex( + await Block.genWithdrawalsTrieRoot(withdrawals.map(Withdrawal.fromWithdrawalData)) + ) + + return { withdrawals, withdrawalsRoot } +} + +async function runBlock( + { common, rpc }: { common: Common; rpc: HttpClient }, + runData: { + parentHash: PrefixedHexString + transactions: PrefixedHexString[] + blockNumber: PrefixedHexString + stateRoot: PrefixedHexString + receiptTrie: PrefixedHexString + gasUsed: PrefixedHexString + coinbase: PrefixedHexString + } +) { + const { transactions, parentHash, blockNumber, stateRoot, receiptTrie, gasUsed, coinbase } = + runData + const txs = [] + for (const [index, serializedTx] of transactions.entries()) { + try { + const tx = TransactionFactory.fromSerializedData(hexToBytes(serializedTx), { + common, + }) + txs.push(tx) + } catch (error) { + const validationError = `Invalid tx at index ${index}: ${error}` + throw validationError + } + } + const transactionsTrie = bytesToHex(await Block.genTransactionsTrieRoot(txs)) + + const { withdrawals, withdrawalsRoot } = await genBlockWithdrawals(Number(blockNumber)) + + const headerData = { + parentHash, + number: blockNumber, + withdrawalsRoot, + transactionsTrie, + stateRoot, + receiptTrie, + gasUsed, + coinbase, + } + const blockData = { header: headerData, transactions: txs, withdrawals } + const executeBlock = Block.fromBlockData(blockData, { common }) + const executePayload = blockToExecutionPayload(executeBlock, BigInt(0)).executionPayload + const res = await rpc.request('engine_newPayloadV2', [executePayload]) + assert.equal(res.result.status, 'VALID', 'valid status should be received') + return executePayload +} + +describe(`valid verkle network setup`, async () => { + // unschedule verkle + const unschedulePragueJson = { + ...genesisJSON, + config: { ...genesisJSON.config, pragueTime: undefined }, + } + const { server, chain, common, execution } = await setupChain( + unschedulePragueJson, + 'post-merge', + { + engine: true, + savePreimages: true, + } + ) + ;(chain.blockchain as any).validateHeader = () => {} + + const rpc = getRpcClient(server) + it('genesis should be correctly setup', async () => { + const res = await rpc.request('eth_getBlockByNumber', ['0x0', false]) + + const block0 = res.result + assert.equal(block0.hash, genesisBlockHash) + assert.equal(block0.stateRoot, genesisStateRoot) + }) + + // build some testcases uses some transactions from kaustinen2 which have + // normal txs, contract fail, contract success tx, although kaustinen2 + // is verkle, but we run the tests in the merkle (pre-verkle) setup + // + // withdrawals are generated and bundled using genBlockWithdrawals util + // and for block1 are coded to return no withdrawals + // + // third consideration is for feerecipient which are added here as random + // coinbase addrs + const testCases = [ + { + name: 'block 1 no txs', + blockData: { + transactions: [] as string[], + blockNumber: '0x01', + stateRoot: '0x78026f1e4f2ff57c340634f844f47cb241beef4c965be86a483c855793e4b07d', + receiptTrie: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + gasUsed: '0x0', + coinbase: '0x78026f1e4f2ff57c340634f844f47cb241beef4c', + }, + preimages: [ + // coinbase + '0x78026f1e4f2ff57c340634f844f47cb241beef4c', + // no withdrawals + // no tx accesses + ], + }, + { + name: 'block 2 having kaustinen2 block 12 txs', + blockData: { + transactions: blocks.block12.execute.transactions, + blockNumber: '0x02', + stateRoot: '0xa86d54279c8faebed72e112310b29115d3600e8cc6ff2a2e4466a788b8776ad9', + receiptTrie: '0xd95b673818fa493deec414e01e610d97ee287c9421c8eff4102b1647c1a184e4', + gasUsed: '0xa410', + coinbase: '0x9da2abca45e494476a21c49982619ee038b68556', + }, + // add preimages for addresses accessed in txs + preimages: [ + // coinbase + '0x9da2abca45e494476a21c49982619ee038b68556', + // withdrawals + '0x2000000000000000000000000000000000000000', + '0x2100000000000000000000000000000000000000', + '0x2200000000000000000000000000000000000000', + '0x2300000000000000000000000000000000000000', + '0x2400000000000000000000000000000000000000', + '0x2500000000000000000000000000000000000000', + '0x2600000000000000000000000000000000000000', + '0x2700000000000000000000000000000000000000', + // txs + '0x7e454a14b8e7528465eef86f0dc1da4f235d9d79', + '0x6177843db3138ae69679a54b95cf345ed759450d', + '0x687704db07e902e9a8b3754031d168d46e3d586e', + ], + }, + { + name: 'block 3 no txs with just withdrawals but zero coinbase', + blockData: { + transactions: [] as string[], + blockNumber: '0x03', + stateRoot: '0xe4538f9d7531eb76e82edf7480e4578bc2be5f454ab02db4d9db6187dfa1f9ca', + receiptTrie: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + gasUsed: '0x0', + coinbase: '0x0000000000000000000000000000000000000000', + }, + preimages: [ + // coinbase + '0x0000000000000000000000000000000000000000', + // withdrawals, + '0x3000000000000000000000000000000000000000', + '0x3100000000000000000000000000000000000000', + '0x3200000000000000000000000000000000000000', + '0x3300000000000000000000000000000000000000', + '0x3400000000000000000000000000000000000000', + '0x3500000000000000000000000000000000000000', + '0x3600000000000000000000000000000000000000', + '0x3700000000000000000000000000000000000000', + // no txs + ], + }, + { + name: 'block 4 with kaustinen block13 txs and withdrawals', + blockData: { + transactions: blocks.block13.execute.transactions, + blockNumber: '0x04', + stateRoot: '0x57e675e1d6b2ab5d65601e81658de1468afad77752a271a48364dcefda856614', + receiptTrie: '0x6a0be0e8208f625225e43681258eb9901ed753e2656f0cd6c0a3971fada5f190', + gasUsed: '0x3c138', + coinbase: '0xa874386cdb13f6cb3b974d1097b25116e67fc21e', + }, + preimages: [ + // coinbase + '0xa874386cdb13f6cb3b974d1097b25116e67fc21e', + // withdrawals + '0x4000000000000000000000000000000000000000', + '0x4100000000000000000000000000000000000000', + '0x4200000000000000000000000000000000000000', + '0x4300000000000000000000000000000000000000', + '0x4400000000000000000000000000000000000000', + '0x4500000000000000000000000000000000000000', + '0x4600000000000000000000000000000000000000', + '0x4700000000000000000000000000000000000000', + // txs + '0x687704db07e902e9a8b3754031d168d46e3d586e', + '0xdfd66120239099c0a9ad623a5af2b26ea6b4fb8d', + '0xbbbbde4ca27f83fc18aa108170547ff57675936a', + '0x6177843db3138ae69679a54b95cf345ed759450d', + '0x2971704d406f9efef2f4a36ea2a40aa994922fb9', + '0xad692144cd452a8a85bf683b2a80bb22aab2f9ed', + ], + }, + ] as const + + let parentHash = genesisBlockHash + for (const testCase of testCases) { + const { name, blockData, preimages } = testCase + it(`run ${name}`, async () => { + const { blockHash } = await runBlock({ common, rpc }, { ...blockData, parentHash }) + // check the preimages are in the preimage manager + for (const preimage of preimages) { + const preimageBytes = hexToBytes(preimage) + const savedPreimage = await execution.preimagesManager!.getPreimage( + keccak256(preimageBytes) + ) + assert.isNotNull(savedPreimage, `Missing preimage for ${preimage}`) + assert.ok( + savedPreimage !== null && equalsBytes(savedPreimage, preimageBytes), + `Incorrect preimage for ${preimage}` + ) + } + parentHash = blockHash + }) + } + + it(`reset TD`, () => { + server.close() + BlockHeader.prototype['_consensusFormatValidation'] = originalValidate + td.reset() + }) +}) diff --git a/packages/client/test/rpc/helpers.ts b/packages/client/test/rpc/helpers.ts index 0c637e44ec..1ff4c267ff 100644 --- a/packages/client/test/rpc/helpers.ts +++ b/packages/client/test/rpc/helpers.ts @@ -53,6 +53,7 @@ type createClientArgs = { opened: boolean genesisState: GenesisState genesisStateRoot: Uint8Array + savePreimages: boolean } export function startRPC( methods: any, @@ -97,6 +98,7 @@ export async function createClient(clientOpts: Partial = {}) { txLookupLimit: clientOpts.txLookupLimit, accountCache: 10000, storageCache: 1000, + savePreimages: clientOpts.savePreimages, }) const blockchain = clientOpts.blockchain ?? mockBlockchain() diff --git a/packages/client/test/sim/beaconsync.md b/packages/client/test/sim/beaconsync.md index 4c9077beef..c4d989e265 100644 --- a/packages/client/test/sim/beaconsync.md +++ b/packages/client/test/sim/beaconsync.md @@ -26,6 +26,7 @@ BEACON_SYNC=true NETWORK=mainnet NETWORKID=1337903 ELCLIENT=geth npx vitest run ``` or just + ```bash rm -rf ./datadir; DEBUG=ethjs,client:* BEACON_SYNC=true NETWORK=mainnet NETWORKID=1337903 ELCLIENT=geth npx vitest run test/sim/beaconsync.spec.ts -``` \ No newline at end of file +``` diff --git a/packages/common/README.md b/packages/common/README.md index 39e001a127..e0df248eed 100644 --- a/packages/common/README.md +++ b/packages/common/README.md @@ -79,14 +79,14 @@ console.log(`EIP 4844 is active -- ${c.isActivatedEIP(4844)}`) ### Custom Cryptography Primitives (WASM) -All EthereumJS packages use cryptographic primitives from the audited `ethereum-cryptography` library by default. -These primitves, including `keccak256`, `sha256`, and elliptic curve signature methods, are all written in native -Javascript and therefore have the potential downside of being less performant than alternative cryptography modules -written in other languages and then compiled to WASM. If cryptography performance is a bottleneck in your usage of -the EthereumJS libraries, you can provide your own primitives to the `Common` constructor and they will be used in -place of the defaults. Depending on how your preferred primitives are implemented, you may need to write wrapper +All EthereumJS packages use cryptographic primitives from the audited `ethereum-cryptography` library by default. +These primitves, including `keccak256`, `sha256`, and elliptic curve signature methods, are all written in native +Javascript and therefore have the potential downside of being less performant than alternative cryptography modules +written in other languages and then compiled to WASM. If cryptography performance is a bottleneck in your usage of +the EthereumJS libraries, you can provide your own primitives to the `Common` constructor and they will be used in +place of the defaults. Depending on how your preferred primitives are implemented, you may need to write wrapper methods around them so they conform to the interface exposed by the [`common.customCrypto` property](./src/types.ts). -See the implementation of this in the [`@etheruemjs/client`](../client/bin/cli.ts#L810) using `@polkadot/wasm-crypto` +See the implementation of this in the [`@etheruemjs/client`](../client/bin/cli.ts#L810) using `@polkadot/wasm-crypto` for an example of how this is done for each available cryptographic primitive. Note: replacing native JS crypto primitives with WASM based libraries comes with new security assumptions (additional external dependencies, unauditability of WASM code). It is therefore recommended to evaluate your usage context before applying! @@ -119,8 +119,8 @@ main() ### Example 2: KZG -The KZG library used for EIP-4844 Blob Transactions is initialized by `common` under the `common.customCrypto` property -and is then used throughout the `Ethereumjs` stack wherever KZG cryptography is required. Below is an example of how +The KZG library used for EIP-4844 Blob Transactions is initialized by `common` under the `common.customCrypto` property +and is then used throughout the `Ethereumjs` stack wherever KZG cryptography is required. Below is an example of how to initalize (assuming you are using the `c-kzg` package as your KZG cryptography library). ```ts diff --git a/packages/common/src/interfaces.ts b/packages/common/src/interfaces.ts index 3be20bb916..eecd212a0c 100644 --- a/packages/common/src/interfaces.ts +++ b/packages/common/src/interfaces.ts @@ -83,6 +83,7 @@ export interface StateManagerInterface { getProof?(address: Address, storageSlots: Uint8Array[]): Promise hasStateRoot(root: Uint8Array): Promise // only used in client shallowCopy(downlevelCaches?: boolean): StateManagerInterface + getAppliedKey?(address: Uint8Array): Uint8Array } export interface EVMStateManagerInterface extends StateManagerInterface { diff --git a/packages/evm/src/journal.ts b/packages/evm/src/journal.ts index dab81ff1b7..8e52d8ac6f 100644 --- a/packages/evm/src/journal.ts +++ b/packages/evm/src/journal.ts @@ -2,9 +2,11 @@ import { Hardfork } from '@ethereumjs/common' import { Address, RIPEMD160_ADDRESS_STRING, + bytesToHex, bytesToUnprefixedHex, stripHexPrefix, toBytes, + unprefixedHexToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' @@ -14,6 +16,7 @@ import type { Debugger } from 'debug' const { debug: createDebugLogger } = debugDefault type AddressString = string +type HashString = string type SlotString = string type WarmSlots = Set @@ -44,6 +47,7 @@ export class Journal { private journalHeight: JournalHeight public accessList?: Map> + public preimages?: Map constructor(stateManager: EVMStateManagerInterface, common: Common) { // Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables @@ -69,6 +73,14 @@ export class Journal { this.accessList = new Map() } + /** + * Clears the internal `preimages` map, and marks this journal to start reporting + * the images (hashed addresses) of the accounts that have been accessed + */ + startReportingPreimages() { + this.preimages = new Map() + } + async putAccount(address: Address, account: Account | undefined) { this.touchAddress(address) return this.stateManager.putAccount(address, account) @@ -85,6 +97,18 @@ export class Journal { } private touchAccount(address: string) { + // If preimages are being reported, add the address to the preimages map + if (this.preimages !== undefined) { + const bytesAddress = unprefixedHexToBytes(address) + if (this.stateManager.getAppliedKey === undefined) { + throw new Error( + 'touchAccount: stateManager.getAppliedKey can not be undefined if preimage storing is enabled' + ) + } + const hashedKey = this.stateManager.getAppliedKey(bytesAddress) + this.preimages.set(bytesToHex(hashedKey), bytesAddress) + } + if (!this.touched.has(address)) { this.touched.add(address) const diffArr = this.journalDiff[this.journalDiff.length - 1][1] @@ -164,7 +188,7 @@ export class Journal { } /** - * Removes accounts form the state trie that have been touched, + * Removes accounts from the state trie that have been touched, * as defined in EIP-161 (https://eips.ethereum.org/EIPS/eip-161). * Also cleanups any other internal fields */ @@ -183,6 +207,7 @@ export class Journal { } this.cleanJournal() delete this.accessList + delete this.preimages } addAlwaysWarmAddress(addressStr: string, addToAccessList: boolean = false) { diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 3881a06355..19477478e6 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -150,9 +150,11 @@ export interface EVMInterface { putAccount(address: Address, account: Account): Promise deleteAccount(address: Address): Promise accessList?: Map> + preimages?: Map addAlwaysWarmAddress(address: string, addToAccessList?: boolean): void addAlwaysWarmSlot(address: string, slot: string, addToAccessList?: boolean): void startReportingAccessList(): void + startReportingPreimages(): void } stateManager: EVMStateManagerInterface precompiles: Map diff --git a/packages/statemanager/src/rpcStateManager.ts b/packages/statemanager/src/rpcStateManager.ts index 07266efe01..676e545dad 100644 --- a/packages/statemanager/src/rpcStateManager.ts +++ b/packages/statemanager/src/rpcStateManager.ts @@ -367,6 +367,16 @@ export class RPCStateManager implements EVMStateManagerInterface { return proof } + /** + * Returns the applied key for a given address + * Used for saving preimages + * @param address - The address to return the applied key + * @returns {Uint8Array} - The applied key (e.g. hashed address) + */ + getAppliedKey(address: Uint8Array): Uint8Array { + return this.keccakFunction(address) + } + /** * Checkpoints the current state of the StateManager instance. * State changes that follow can then be committed by calling diff --git a/packages/statemanager/src/stateManager.ts b/packages/statemanager/src/stateManager.ts index 6b449c6fce..647e8cf3cf 100644 --- a/packages/statemanager/src/stateManager.ts +++ b/packages/statemanager/src/stateManager.ts @@ -1150,4 +1150,14 @@ export class DefaultStateManager implements EVMStateManagerInterface { this._storageCache?.clear() this._codeCache?.clear() } + + /** + * Returns the applied key for a given address + * Used for saving preimages + * @param address - The address to return the applied key + * @returns {Uint8Array} - The applied key (e.g. hashed address) + */ + getAppliedKey(address: Uint8Array): Uint8Array { + return this._trie['appliedKey'](address) + } } diff --git a/packages/statemanager/src/statelessVerkleStateManager.ts b/packages/statemanager/src/statelessVerkleStateManager.ts index 37c4e629dd..20d73fc5b3 100644 --- a/packages/statemanager/src/statelessVerkleStateManager.ts +++ b/packages/statemanager/src/statelessVerkleStateManager.ts @@ -887,4 +887,8 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { generateCanonicalGenesis(_initState: any): Promise { return Promise.resolve() } + + getAppliedKey(_: Uint8Array): Uint8Array { + throw Error('not implemented') + } } diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 31d4aaa3c5..689f18885c 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -27,10 +27,12 @@ import { Bloom } from './bloom/index.js' import type { AfterBlockEvent, + ApplyBlockResult, PostByzantiumTxReceipt, PreByzantiumTxReceipt, RunBlockOpts, RunBlockResult, + RunTxResult, TxReceipt, } from './types.js' import type { VM } from './vm.js' @@ -157,7 +159,7 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promise> + let result: ApplyBlockResult try { result = await applyBlock.bind(this)(block, opts) @@ -272,6 +274,7 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promise { // Validate block if (opts.skipBlockValidation !== true) { if (block.header.gasLimit >= BigInt('0x8000000000000000')) { @@ -364,8 +367,36 @@ async function applyBlock(this: VM, block: Block, opts: RunBlockOpts) { console.time(withdrawalsRewardsCommitLabel) } + // Add txResult preimages to the blockResults preimages + // Also add the coinbase preimage + + if (opts.reportPreimages === true) { + if (this.evm.stateManager.getAppliedKey === undefined) { + throw new Error( + 'applyBlock: evm.stateManager.getAppliedKey can not be undefined if reportPreimages is true' + ) + } + blockResults.preimages.set( + bytesToHex(this.evm.stateManager.getAppliedKey(block.header.coinbase.toBytes())), + block.header.coinbase.toBytes() + ) + for (const txResult of blockResults.results) { + if (txResult.preimages !== undefined) { + for (const [key, preimage] of txResult.preimages) { + blockResults.preimages.set(key, preimage) + } + } + } + } + if (this.common.isActivatedEIP(4895)) { + if (opts.reportPreimages === true) this.evm.journal.startReportingPreimages() await assignWithdrawals.bind(this)(block) + if (opts.reportPreimages === true && this.evm.journal.preimages !== undefined) { + for (const [key, preimage] of this.evm.journal.preimages) { + blockResults.preimages.set(key, preimage) + } + } await this.evm.journal.cleanup() } // Pay ommers and miners @@ -431,8 +462,8 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { receiptTrie = new Trie({ common: this.common }) } - const receipts = [] - const txResults = [] + const receipts: TxReceipt[] = [] + const txResults: RunTxResult[] = [] /* * Process transactions @@ -453,7 +484,7 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { } // Run the tx through the VM - const { skipBalance, skipNonce, skipHardForkValidation } = opts + const { skipBalance, skipNonce, skipHardForkValidation, reportPreimages } = opts const txRes = await this.runTx({ tx, @@ -462,6 +493,7 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { skipNonce, skipHardForkValidation, blockGasUsed: gasUsed, + reportPreimages, }) txResults.push(txRes) if (this.DEBUG) { @@ -493,6 +525,7 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { return { bloom, gasUsed, + preimages: new Map(), receiptsRoot, receipts, results: txResults, diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 8a0ccd995a..c4ceaed3cb 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -128,6 +128,10 @@ export async function runTx(this: VM, opts: RunTxOpts): Promise { this.evm.journal.startReportingAccessList() } + if (opts.reportPreimages === true) { + this.evm.journal.startReportingPreimages() + } + await this.evm.journal.checkpoint() if (this.DEBUG) { debug('-'.repeat(100)) @@ -684,6 +688,10 @@ async function _runTx(this: VM, opts: RunTxOpts): Promise { console.time(journalCacheCleanUpLabel) } + if (opts.reportPreimages === true && this.evm.journal.preimages !== undefined) { + results.preimages = this.evm.journal.preimages + } + await this.evm.journal.cleanup() state.originalStorageCache.clear() diff --git a/packages/vm/src/types.ts b/packages/vm/src/types.ts index 0e7c5b9258..768774abd4 100644 --- a/packages/vm/src/types.ts +++ b/packages/vm/src/types.ts @@ -274,12 +274,30 @@ export interface RunBlockOpts { * Default: `false` (HF is set to whatever default HF is set by the {@link Common} instance) */ setHardfork?: boolean | BigIntLike + + /** + * If true, adds a hashedKey -> preimages mapping of all touched accounts + * to the `RunTxResult` returned. + */ + reportPreimages?: boolean } /** - * Result of {@link runBlock} + * Result of {@link applyBlock} */ -export interface RunBlockResult { +export interface ApplyBlockResult { + /** + * The Bloom filter + */ + bloom: Bloom + /** + * The gas used after executing the block + */ + gasUsed: bigint + /** + * The receipt root after executing the block + */ + receiptsRoot: Uint8Array /** * Receipts generated for transactions in the block */ @@ -289,21 +307,23 @@ export interface RunBlockResult { */ results: RunTxResult[] /** - * The stateRoot after executing the block + * Preimages mapping of the touched accounts from the block (see reportPreimages option) */ - stateRoot: Uint8Array + preimages?: Map +} + +/** + * Result of {@link runBlock} + */ +export interface RunBlockResult extends Omit { /** - * The gas used after executing the block + * The stateRoot after executing the block */ - gasUsed: bigint + stateRoot: Uint8Array /** * The bloom filter of the LOGs (events) after executing the block */ logsBloom: Uint8Array - /** - * The receipt root after executing the block - */ - receiptsRoot: Uint8Array } export interface AfterBlockEvent extends RunBlockResult { @@ -328,6 +348,7 @@ export interface RunTxOpts { * If true, skips the nonce check */ skipNonce?: boolean + /** * Skip balance checks if true. Adds transaction cost to balance to ensure execution doesn't fail. */ @@ -357,6 +378,12 @@ export interface RunTxOpts { */ reportAccessList?: boolean + /** + * If true, adds a hashedKey -> preimages mapping of all touched accounts + * to the `RunTxResult` returned. + */ + reportPreimages?: boolean + /** * To obtain an accurate tx receipt input the block gas used up until this tx. */ @@ -399,6 +426,11 @@ export interface RunTxResult extends EVMResult { */ accessList?: AccessList + /** + * Preimages mapping of the touched accounts from the tx (see `reportPreimages` option) + */ + preimages?: Map + /** * The value that accrues to the miner by this transaction */ diff --git a/packages/vm/test/api/runTx.spec.ts b/packages/vm/test/api/runTx.spec.ts index 3101cebedd..2b664cef1c 100644 --- a/packages/vm/test/api/runTx.spec.ts +++ b/packages/vm/test/api/runTx.spec.ts @@ -13,6 +13,8 @@ import { Address, KECCAK256_NULL, MAX_INTEGER, + bytesToHex, + equalsBytes, hexToBytes, initKZG, zeros, @@ -317,6 +319,24 @@ describe('runTx() -> API parameter usage/data errors', () => { assert.deepEqual(res.accessList, []) }) + it('simple run (reportPreimages option)', async () => { + const vm = await VM.create({ common }) + + const tx = getTransaction(vm.common, 0, true) + + const caller = tx.getSenderAddress() + const acc = createAccount() + await vm.stateManager.putAccount(caller, acc) + + const res = await vm.runTx({ tx, reportPreimages: true }) + + const hashedCallerKey = vm.stateManager.getAppliedKey!(caller.bytes) + + const retrievedPreimage = res.preimages?.get(bytesToHex(hashedCallerKey)) + + assert.ok(retrievedPreimage !== undefined && equalsBytes(retrievedPreimage, caller.bytes)) + }) + it('run without signature', async () => { for (const txType of TRANSACTION_TYPES) { const vm = await VM.create({ common })