feat(l1): implement eth_simulateV1 RPC method#6951
Conversation
…e ETH-transfer logs. Environment gains disable_nonce_check (eth_simulateV1 with validation=false behaves like eth_call; the account nonce still increments), disable_eoa_check (simulations allow contract senders; the EIP-3607 check is skipped unconditionally in simulation contexts), and trace_eth_transfers (emit informational ETH-transfer logs from the execution-apis sentinel address 0xeeee...eeee when tracing pre-Amsterdam). The five EIP-7708 transfer-log emission sites now route through VM::eth_transfer_log_address, which encodes the precedence: Amsterdam+ consensus logs from SYSTEM_ADDRESS (self-transfers excluded per the EIP), otherwise traceTransfers logs from TRACE_TRANSFER_ADDRESS (self-transfers included; CALLCODE excluded since its value never leaves the caller). create_eth_transfer_log takes the emitter address explicitly. Pre-Cancun selfdestructs gain a trace-only emission site. Consensus behavior is unchanged: all sites still emit from SYSTEM_ADDRESS with identical conditions on Amsterdam+.
…ulateV1. Evm::execute_tx_simulate mirrors execute_tx but returns SimulationTxError, which keeps TxValidationError variants structured instead of stringifying them through From<VMError> for EvmError — the RPC layer needs the variant to map nonce/basefee/funds/intrinsic-gas failures to their spec error codes and abort the whole request (validation failures are request-level errors in eth_simulateV1, unlike reverts which are per-call results). It applies the simulation relaxations: EOA-sender check always skipped, nonce check skipped unless validation:true, unpriced blob fees zeroed, traceTransfers flag. setup_env/setup_env_with_config now return VMError (callers auto-convert), so the base-fee-too-low validation raised while computing the effective gas price survives structurally. Evm::get_account_nonce reads through the live EVM cache for per-block nonce auto-fill. SimTxConfig, SimulationTxError, TxValidationError, TxResult, ExecutionReport and TRACE_TRANSFER_ADDRESS are re-exported from ethrex_vm for the blockchain-crate engine.
…te chaining. The overlay accumulates every change since the base block (state overrides materialized as AccountUpdates plus each simulated block's execution results) and is the single source of truth for both reads and per-block state roots. SimulationVmDatabase serves reads overlay-first with fallback to a StoreVmDatabase opened at the base block, which is how simulated block N+1 observes block N's results without committing anything. BLOCKHASH resolves real ancestors through the inner database and previously simulated blocks from the overlay's hash map. The overlay needs its own merge (AccountUpdate::merge is not enough): an incoming removal voids everything accumulated for the address, and re-creation over a tombstone folds into removed_storage + info because the trie application (apply_account_updates_from_trie_batch) short-circuits on removed and would drop the new account info. Also fixes test compilation on the base branch: Code::bytecode became private after the merge with main, so the mock database and the overlay tests now use the public accessors. The shared test mock moved to a test_mock_db module reused by both wrapper test suites.
Simulates a chain of up to 256 blocks on top of a base block without committing anything, mirroring geth's simulator semantics (verified against internal/ethapi/simulate.go and the execution-apis eth_simulateV1 test corpus): - sanitize_blocks resolves block numbers/timestamps (defaults parent+1 and parent+12), validates strict ordering, fills numbering gaps with empty blocks that are part of the response, and bounds the simulated height at base+256. - Headers inherit coinbase/gasLimit from the (post-override) parent; prevRandao/difficulty default to zero; baseFeePerGas is EIP-1559-derived only with validation:true and zero otherwise, so zero-priced calls run like eth_call; the blob-fee market rolls forward in both modes. - Calls become real transactions (variant chosen by fields, EIP-1559 by default) with zeroed signatures, auto-filled nonces read from the live simulated state and remaining-block-gas defaults; senders travel out-of-band. An explicit gas above the remaining block gas aborts with the block-gas-limit error; a 50M cumulative request budget (geth's RPCGasCap default) clamps call gas otherwise. - Tx validation failures abort the whole request (structured, for spec error codes); reverts/halts are per-call results and the tx stays in the block, with revert data separated from returnData. - Pre-block system calls (EIP-4788/EIP-2935) run for every simulated block including gap-filled ones, which is also what makes BLOCKHASH observable in-EVM; EIP-7685 requests are extracted per block; withdrawals overrides credit balances and land in the body/withdrawalsRoot. - State chains through SimulationOverlay; per-block state roots re-apply the cumulative updates onto the base block's trie (storage tries must open at committed roots). traceTransfers sentinel logs stay out of receipts and the logs bloom but are kept in per-call results. Not modeled yet (documented): EIP-8037 2D gas accounting and EIP-7928 BAL headers on Amsterdam chains, and maxUsedGas reports pre-refund gas rather than geth's in-flight peak.
EthSimulateRequest parses the {blockStateCalls, traceTransfers, validation,
returnFullTransactions} payload (strict: unknown keys and malformed payloads
map to -32602, which ethrex's generic BadParams would report as -32000) with
an optional block parameter defaulting to latest, converts overrides into
engine types, and runs Blockchain::simulate_v1 inside spawn_blocking with a
5s wall-clock cap, following the debug_traceCall scaffolding.
Each result block serializes exactly like eth_getBlockByHash (RpcBlock is
flattened; returnFullTransactions switches the untagged body representation)
extended with calls: status/returnData/gasUsed/maxUsedGas plus logs carrying
per-block logIndex, tx hash/index, block hash/number and blockTimestamp —
maxUsedGas and blockTimestamp are required by the execution-apis response
schema. Failed calls keep logs: [] and carry revert data in error.data with
code 3 (-32015 for other halts). Request-level failures map to the spec
error-code table (-38010..-38026) through the new code-agnostic
RpcErr::EthSimulate variant.
BlockOverrideSet gains the eth_simulateV1 field spellings as serde aliases
(feeRecipient/prevRandao/blobBaseFee) — the derived Deserialize silently
dropped them before — plus the spec's withdrawals list, and exposes the
blobBaseFee inversion so the engine receives excess_blob_gas directly.
Ports eleven scenarios from the execution-apis eth_simulateV1 test corpus to the in-memory fixtures/genesis/l1.json chain, asserting response JSON paths (the hive fixtures use a pre-merge genesis ethrex cannot import, which is also why the CI rpc-compat pin predates the corpus): simple transfer with a balance override, empty block entry, block-number order (-38020), timestamp order (-38021), explicit gas above block limit (-38015), missing base fee under validation (-38012), insufficient funds (-38014), traceTransfers synthetic log shape (address/topics/data/blockTimestamp, excluded from the logs bloom), cross-block state carry-over with chained parent hashes, gap blocks in the response with 12s timestamp steps, full transaction objects, and block overrides reflected in the result header. RpcTransaction::build_with_sender constructs full transaction objects from the engine's out-of-band senders, since simulated transactions carry zeroed signatures that cannot be recovered.
# Conflicts: # crates/blockchain/vm.rs
|
Lines of code reportTotal lines added: Detailed view |
Benchmark Results ComparisonNo significant difference was registered for any benchmark run. Detailed ResultsBenchmark Results: BubbleSort
Benchmark Results: ERC20Approval
Benchmark Results: ERC20Mint
Benchmark Results: ERC20Transfer
Benchmark Results: Factorial
Benchmark Results: FactorialRecursive
Benchmark Results: Fibonacci
Benchmark Results: FibonacciRecursive
Benchmark Results: ManyHashes
Benchmark Results: MstoreBench
Benchmark Results: Push
Benchmark Results: SstoreBench_no_opt
|
Some simulators (anvil among them) skip nonce enforcement even with validation: true, so a nonce-gapped call passes and only reverts get caught. ethrex follows geth: with validation on, an explicit nonce above the account nonce aborts the request with -38011 and a stale nonce with -38010, while the same gapped call without validation succeeds (the reference corpus' transaction-too-high-nonce case — explicit nonces are ignored there, like eth_call). The tests pin all four behaviors, with fees covering the real base fee that validation mode derives so the nonce check is the one that fires. Known precedence divergence, deliberately untouched: when a call violates both fees/balance and nonce, ethrex's pre-execution hook reports the fee or balance error first while geth checks the nonce first; the hook order is shared with consensus execution and is not worth forking for an error-code tiebreak.
Motivation
eth_simulateV1is the multi-block successor toeth_callbatching (execution-apisethSimulate); wallets, simulators and MEV tooling increasingly depend on it. Builds on the State/Block Override Sets from #6660 (this PR is stacked on that branch).Description
Simulates up to 256 chained blocks on top of a base block without committing anything to the store:
blockStateCallsentry carries block overrides (number/time/gasLimit/feeRecipient/prevRandao/baseFeePerGas/blobBaseFee/difficulty/withdrawals), state overrides (reusing feat(l1,l2): add State Override Set and Block Override Set to RPC simulation methods #6660'sStateOverrideSet), and a list of call objects executed as real transactions (variant chosen by fields, EIP-1559 default; auto-filled nonces from the live simulated state; remaining-block-gas defaults; zeroed signatures with senders carried out-of-band).SimulationOverlayis the single source of truth for both reads (SimulationVmDatabase) and per-block state roots (re-applying the cumulative account updates onto the base block's trie), so block N+1 observes block N's execution results.BLOCKHASHresolves real ancestors and previously simulated blocks; EIP-4788/EIP-2935 system calls run per simulated block; EIP-7685 requests are extracted per block.validation: trueenforces nonces and a real EIP-1559 base fee; the default behaves likeeth_call(zero base fee, relaxed checks).traceTransfersemits synthetic ERC-20Transferlogs from0xeeee…eeeefor every ETH transfer, kept out of receipts and the logs bloom.returnFullTransactionsswitches between tx hashes and full objects.eth_getBlockByHashoutput with real state/transactions/receipts roots, extended with per-call results (status,returnData,gasUsed,maxUsedGas,logswith per-blocklogIndexandblockTimestamp,errorwith revert data). Request-level failures use the spec error-code table (-38010..-38026); tx validation failures abort the request while reverts/halts are per-call results with the tx included in the block, matching geth.internal/ethapi/simulate.goand the execution-apiseth_simulateV1test corpus (91.iofiles); ~11 scenarios are ported as integration tests against the in-memory store. A 50M cumulative gas budget (geth'sRPCGasCapdefault) bounds the request.Known gaps, documented in
docs/roadmaps/forks-roadmap.md:movePrecompileToAddressis parsed but not yet enforced (follow-up); Amsterdam EIP-8037 2D gas / EIP-7928 BAL headers are not modeled;maxUsedGasreports pre-refund gas rather than geth's in-flight peak. The hiverpc-compatCI pin predates theeth_simulateV1corpus (newer fixtures use a pre-merge genesis), so CI does not exercise the method yet — conformance is covered by the ported scenarios.Also fixes test compilation on the base branch (
Code::bytecodebecame private after the merge with main; the vm.rs test helpers now use the public accessors).How to Test
Or against a live node:
Expected: two simulated blocks chained by
parentHash, both calls"status": "0x1", aTransferlog from0xeeee…eeeeper call, and the second block'sminerset to0x…beef.Checklist
STORE_SCHEMA_VERSION(crates/storage/lib.rs) if the PR includes breaking changes to theStorerequiring a re-sync. — N/A, no Store schema changes.Closes #6212