fix: always include storage values for SLOAD and SSTORE in GethTrace#389
Conversation
mattsse
left a comment
There was a problem hiding this comment.
one question re the removed journal change check
| let reason = match op { | ||
| opcode::SLOAD => StorageChangeReason::SLOAD, | ||
| opcode::SSTORE => StorageChangeReason::SSTORE, | ||
| _ => unreachable!(), |
There was a problem hiding this comment.
I'm not sure if this is still unreachable now
There was a problem hiding this comment.
if matches!(op, opcode::SLOAD | opcode::SSTORE) on line 540 guarantees this.
mattsse
left a comment
There was a problem hiding this comment.
oh I get it now, ty
I think we need to update the snapshots now
I incorporated your feedback and split the solution in two parts:
For part 2, my understanding of // Copy a snapshot of the current storage to a new container
var storage Storage
if !l.cfg.DisableStorage && (op == vm.SLOAD || op == vm.SSTORE) {
// initialise new changed values storage container for this contract
// if not present.
if l.storage[contractAddr] == nil {
l.storage[contractAddr] = make(Storage)
}
// capture SLOAD opcodes and record the read entry in the local storage
if op == vm.SLOAD && stackLen >= 1 {
var (
address = common.Hash(stack[stackLen-1].Bytes32())
value = l.env.StateDB.GetState(contractAddr, address)
)
l.storage[contractAddr][address] = value
storage = maps.Clone(l.storage[contractAddr]) // <-- FULL CONTRACT STORAGE IS CLONED
} else if op == vm.SSTORE && stackLen >= 2 {
// capture SSTORE opcodes and record the written entry in the local storage.
var (
value = common.Hash(stack[stackLen-2].Bytes32())
address = common.Hash(stack[stackLen-1].Bytes32())
)
l.storage[contractAddr][address] = value
storage = maps.Clone(l.storage[contractAddr]) // <-- FULL CONTRACT STORAGE IS CLONED
}
}
log.Storage = storageShould I add more tests? If so, is there a test format that I can easily extend to generate some tests with |
|
I updated the snapshot writer logic to handle warmed storage slots. This also uncovered a (seemingly) erroneous "changed" error in some of the old snapshots. I stepped through all of the examples and I believe that the new values are in fact correct Addressed by 7ce652d Please let me know whether the update snapshot format is acceptable. |
| } | ||
| }) | ||
| .or_insert_with(|| { | ||
| if let Some(had_value) = change.had_value { |
There was a problem hiding this comment.
The Option in had_value is used to signal whether this was a "warm" or a "change", as per the docs:
/// Represents a storage change during execution.
///
/// This maps to evm internals:
/// [JournalEntry::StorageChanged](revm::JournalEntry::StorageChanged)
///
/// It is used to track both storage change and warm load of a storage slot. For warm load in regard
/// to EIP-2929 AccessList had_value will be None.
| value_after = num_or_hex(value_after), | ||
| )?; | ||
| match change { | ||
| StorageChange::Changed { value, had_value } => { |
There was a problem hiding this comment.
This maintains the old format
| value_after = num_or_hex(value), | ||
| )?; | ||
| } | ||
| StorageChange::Warmed { value } => { |
There was a problem hiding this comment.
This introduces a new format for "warmed" storage slots.
I explicitly didn't use the same format as changes, since it's possible to warm a storage slot that's set to zero. That would result in a confusing "0 → 0"
| │ └─ ← ret1, ret2 | ||
| ├─ storage changes: | ||
| │ @ 0: 70 → 71 | ||
| │ @ 0: 69 → 71 |
There was a problem hiding this comment.
I believe this was an error in the old logic.
Test 7 is a call to nest1:
function nest1() public {
emit Log1(number, "hi from 1");
this.nest2();
increment();
}
function nest2() public {
increment();
this.nest3();
emit Log2(number, 123, "hi from 2");
}Prior to the call, number (storage slot: 0) is 69, due to call(Counter::setNumberCall { newNumber: U256::from(69) }.abi_encode());
nest1callsnest2nest2callincrement(69 → 70)nest1callsincrement(70 → 71)
GethTrace using DebugInspectorSLOAD and SSTORE in GethTrace using DebugInspector
SLOAD and SSTORE in GethTrace using DebugInspectorSLOAD and SSTORE in GethTrace using DebugInspector
SLOAD and SSTORE in GethTrace using DebugInspectorSLOAD and SSTORE in GethTrace
mattsse
left a comment
There was a problem hiding this comment.
unsure if this should be printed like this @DaniPopes
imo having
│ @ 0: 0
isnt that useful, perhaps even wrong, because nothing changed?
There was a problem hiding this comment.
should we keep all of this as it was and simply filter for changed values ad exclude warmed? I feel like this is separate from the fix
There was a problem hiding this comment.
Reported changes now contain storage warming too, so I'll need to handle it somehow. I can filter out changes with had_value.is_none() which should always and only be true for storage warming. That leaves the snapshots as-is.
Would you prefer that?
There was a problem hiding this comment.
I'd prefer that yes
we can still make a decision on the writer later
|
@mattsse is there anything else I need to do before we can consider this for merging? |
…#9) * chore: bump revm 30.0.2 (paradigmxyz#379) bump patch * chore: bump revm 30.0.2 (paradigmxyz#380) * chore: bump to revm 33 (paradigmxyz#381) ## Motivation <!-- Explain the context and why you're making that change. What is the problem you're trying to solve? In some cases there is not a problem and this can be thought of as being the motivation for your change. --> Per: https://github.com/bluealloy/revm/releases/tag/v100 * chore: release 0.33.0 * feat: Fix geth prestate filtering for prefunded creations (paradigmxyz#383) This aims at resolving issues mentioned in paradigmxyz#382 , typically this one : <img width="1818" height="694" alt="image" src="https://github.com/user-attachments/assets/51b82944-c711-421b-8479-05095ae9bbce" /> To fix this , what i did was : - Keep track of addresses that are marked as Create and were truly empty in the database snapshot. Only these addresses are filtered from the prestate diff so prefunded contract deployments remain visible. - Introduce account_was_empty helper to avoid re-computing the emptiness check and add a regression test (prestate_diff_keeps_prefunded_created_accounts) that covers a prefunded creation vs. a true empty-address creation. cc @mattsse Please check it out if it resolves should close this one paradigmxyz/reth#19703 --------- Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de> * feat: upstream DebugInspector from reth (paradigmxyz#384) Moves `DebugInspector` from reth to revm-inspectors as requested in paradigmxyz/reth#19932. This was introduced in paradigmxyz/reth#19925 to unify debug tracing across all Geth tracer types. * chore: release 0.33.1 * test: add test for string error decode (paradigmxyz#386) * fix: use pre code if hash matches (paradigmxyz#387) this fixes an issue where we would return diffs like ``` "post": { "0x093f6c270ac22ec240f0c6fd7414ea774ca8d3e5": {}, "0x2641c2ded63a0c640629f5edf1189e0f53c06561": {}, ``` for `0x91b066df39661db7bcca1cb8bb8afd11816414408d44cbfcf6144f440d5dfe3b` ref paradigmxyz/reth#19703 however nothing changed in the account here and these should have been filtered out via: https://github.com/paradigmxyz/revm-inspectors/blob/f7503520888efc11eac3142fe7d611c519811b25/src/tracing/builder/geth.rs#L349-L349 and cleared from post via: https://github.com/alloy-rs/alloy/blob/35e20437670d987680a6715bcc173f3d357f8254/crates/rpc-types-trace/src/geth/pre_state.rs#L188-L201 the reason why this didn't work is because: the code can be None here which would make retain_change not remove the values ``` "result": { "post": { "0x093f6c270ac22ec240f0c6fd7414ea774ca8d3e5": {}, "0x2641c2ded63a0c640629f5edf1189e0f53c06561": {}, "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5": { "balance": "0xc87826afc2b88254" }, "0xbf5495efe5db9ce00f80364c8b423567e58d2110": { "storage": { "0x0000000000000000000000000000000000000000000000000000000000000035": "0x000000000000000000000000000000000000000000004b7b53b759cac06bc3b2", "0xec8c572bc2e66bfe14376dece39002d6d97d311020c59bc23a93016013e92439": "0x0000000000000000000000000000000000000000000000000f294a8cb09ad9cb" } }, "0xbfe474605fb7f1cf3693a3f21af2e329f7222462": { "balance": "0x269f4a55a240465", "nonce": 5 }, "0xf2f305d14dcd8aaef887e0428b3c9534795d0d60": { "balance": "0xe984e9959d534d3cdf" } }, "pre": { "0x093f6c270ac22ec240f0c6fd7414ea774ca8d3e5": { "balance": "0x1ea3abe47d76b5e00", "code": "..", "nonce": 1 }, "0x2641c2ded63a0c640629f5edf1189e0f53c06561": { "balance": "0x1cb3ca6ff6dc41400", "code": "..", "nonce": 1 }, "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5": { "balance": "0xc8781d351add1146", "nonce": 797368 }, "0xbf5495efe5db9ce00f80364c8b423567e58d2110": { "balance": "0x0", "code": "...", "nonce": 1, "storage": { "0x0000000000000000000000000000000000000000000000000000000000000035": "0x000000000000000000000000000000000000000000004b7b448e0f3e0fd0e9e7" } }, "0xbfe474605fb7f1cf3693a3f21af2e329f7222462": { "balance": "0x11d7cc57700ae2ed", "nonce": 4 }, "0xf2f305d14dcd8aaef887e0428b3c9534795d0d60": { "balance": "0xe975a599714e5f3cdf", "code": "...", "nonce": 1 } } }, ``` we can always use the precode here if the hashes match * chore: release 0.33.2 * feat: add support for erc7562 tracer (paradigmxyz#392) This aims to resolve this issue : paradigmxyz#391 cc @mattsse * feat: stage revm to 34.0.0 (paradigmxyz#385) stage latest main commit --------- Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de> * chore: release 0.34.0 * ci: update to tempoxyz (paradigmxyz#395) * fix: always include storage values for `SLOAD` and `SSTORE` in `GethTrace` (paradigmxyz#389) The default tracer for `debug_*` RPC methods was not reporting warmed storage values. This deviates from Get's implementation and—seemingly—the documented expectation: ```rs /// Represents a storage change during execution. /// /// This maps to evm internals: /// [JournalEntry::StorageChanged](revm::JournalEntry::StorageChanged) /// /// It is used to track both storage change and warm load of a storage slot. For warm load in regard /// to EIP-2929 AccessList had_value will be None. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct StorageChange { /// key of the storage slot pub key: U256, /// Current value of the storage slot pub value: U256, /// The previous value of the storage slot, if any pub had_value: Option<U256>, /// How this storage was accessed pub reason: StorageChangeReason, } ``` In particular, the `SLOAD` for pc `235`, `279`, and `308` are missing `"storage"`. This PR resolves the issue. For reference: [revm-inspectors.json](https://github.com/user-attachments/files/24275105/revm-inspectors.json) [geth.json](https://github.com/user-attachments/files/24275106/geth.json) * test: add prestate replay testing infrastructure (paradigmxyz#399) Add reusable tooling for debugging RPC tracing using prestate tracer responses captured from mainnet transactions. ## Changes - Add `tests/it/repro.rs` with: - `ReproTestFixture`/`TxData` types for deserializing JSON fixtures - `spec_id_from_ethereum_hardfork`/`spec_id_from_block` helpers - `build_db_from_prestate` to populate CacheDB from AccountState - `ReproContext` builder for easy test setup - Add `testdata/repro/tx-selfdestruct.json` fixture from reth's e2e test (mainnet tx 0x391f4b6a...selfdestruct via USDT withdrawal) - Add `alloy-hardforks` dev dependency for block-to-hardfork mapping ## Usage ```rust const FIXTURE: &str = include_str!("../../testdata/repro/my-fixture.json"); #[test] fn test_my_trace() { let ctx = ReproContext::load(FIXTURE); let config = PreStateConfig::default(); let mut inspector = TracingInspector::new( TracingInspectorConfig::from_geth_prestate_config(&config) ); let db = ctx.db.clone(); let mut evm = Context::mainnet() .with_db(ctx.db.clone()) .modify_cfg_chained(|cfg| cfg.spec = ctx.spec_id) .build_mainnet() .with_inspector(&mut inspector); let res = evm.inspect_tx(ctx.tx_env()).unwrap(); // ... assertions on trace results } ``` This enables writing repro tests with the flow: **raw transaction + prestate JSON → configure tracer → run → assert** Refs: https://github.com/paradigmxyz/reth/blob/main/crates/ethereum/node/tests/e2e/prestate.rs --------- Co-authored-by: Amp <amp@ampcode.com> * fix: use Default::default() for TransactionInfo for forward compatibility (paradigmxyz#401) Use `..Default::default()` pattern to ensure forward compatibility when new fields are added to `TransactionInfo`. This prepares the crate for upcoming changes to add a `block_timestamp` field. Related: ethereum/execution-apis#729 Co-authored-by: Amp <amp@ampcode.com> * chore: release 0.34.1 * feat: expose refund_counter in `CallTrace` (paradigmxyz#402) Required to be able to recover pre-refund totals from purely just the `CallTrace` for gas introspection in foundry. foundry-rs/foundry#13262 * chore: release 0.34.2 * chore: bump MSRV to 1.91 (paradigmxyz#407) Bump MSRV from 1.86 to 1.91. * feat: implement Clone for DebugInspector (paradigmxyz#406) Implement `Clone` for `DebugInspector` to enable use with `TxTracer`, which requires `Inspector: Clone`. Non-JS variants clone trivially. The `Js` variant reconstructs the `JsInspector` from its stored `code` and `config` — same approach `fuse()` already uses. * devnet2-revm: bump revm patch for release procedure (paradigmxyz#408) ## Summary This PR is part of the **revm release procedure** (`devnet2-revm` chain). It bumps revm-inspectors to revm v35.0.0. ## Changes - Bump `revm` dependency from 34.0.0 to 35.0.0 in `Cargo.toml`. - Adapt to upstream API change: `journal().precompile_addresses()` now returns a type that requires `.iter().copied().collect()` instead of `.clone()`. - Lower `gas_limit` in selfdestruct tests from 100M to 10M to comply with EIP-7825 transaction gas limit cap enforced in revm 35. ## Validation - `cargo check` - `cargo +nightly clippy --workspace --all-targets --all-features` ## Procedure Context This is an intermediate dependency bump in the revm release flow before downstream updates in `alloy-evm` and `reth`. * chore: release 0.35.0 * chore: bump revm to 36.0.0 (paradigmxyz#409) ## Summary - Bump `revm` dependency from 35.0.0 to 36.0.0 ## Test plan - [x] `cargo check` passes - [x] `cargo test` passes (all 37 tests) * chore: release 0.36.0 * fix: omit empty returnData in geth struct log trace (paradigmxyz#411) ## Summary Only include `returnData` in structLog output when the return-data buffer is non-empty, matching geth's behavior. ## Motivation [ethpandaops trace comparison report](https://investigations.ethpandaops.io/2026-01/trace-comparisons/reth/) shows reth emitting `returnData: "0x"` on **every** structLog step (~1.1M occurrences across 500 txs), while geth/erigon/nethermind/besu all omit it. Geth applies two independent gates before including `returnData`: 1. `cfg.EnableReturnData == true` (opt-in config) — we had this 2. `len(ReturnData) > 0` in `toLegacyJSON` — we were missing this ## Changes - `src/tracing/builder/geth.rs`: add `!step.returndata.is_empty()` check alongside the existing `opts.is_return_data_enabled()` gate ## Testing `cargo test --workspace` — all 24 tests pass, clippy clean. Co-Authored-By: Matthias Seitz <19890894+mattsse@users.noreply.github.com> Prompted by: mattsse Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com> * feat: add set_transaction_caller and set_transaction_target (paradigmxyz#412) Adds `set_transaction_caller` and `set_transaction_target` to `TracingInspector`, following the same pattern as `set_transaction_gas_used` / `set_transaction_gas_limit`. Custom transaction types (e.g. AA batches) may execute via a multi-call handler where the EVM's `call()` entry point doesn't reflect the actual transaction sender/recipient. This causes `callTracer` to report zeroed `from`/`to` on the root frame. These methods allow the execution layer to correct the root trace after execution. Co-Authored-By: zhygis <5236121+Zygimantass@users.noreply.github.com> Prompted by: zygis --------- Co-authored-by: zhygis <5236121+Zygimantass@users.noreply.github.com> Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com> * chore: release 0.36.1 * chore: relax `CTX` bounds on `StorageInspector` `Inspector` impl (paradigmxyz#416) Relax `CTX` bounds on `StorageInspector` `Inspector` impl as it doesn't use `Context`. * fix: eip-7708 logs (paradigmxyz#413) CallLog discarded the original Log.address during conversion and geth_empty_call_frame() reconstructed it from execution_address(). This is wrong for any log whose emitter differs from the call frame's execution context — most notably EIP-7708 system-level ETH transfer logs, which are emitted from SYSTEM_ADDRESS (0xfffffffffffffffffffffffffffffffffffffffe) rather than the executing contract. Changes src/tracing/types.rs - Add address: Address field to CallLog to preserve the original log emitter - Update From<Log> to store log.address instead of discarding it - Fix geth_empty_call_frame() to use log.address instead of self.execution_address() tests/it/geth.rs - test_geth_calltracer_logs_eip7708: value-transferring CALL to a precompile under AMSTERDAM — verifies the log address is ETH_TRANSFER_LOG_ADDRESS, not the contract or precompile address - test_geth_calltracer_logs_address_regular: regular LOG0 emission — verifies the log address is the emitting contract - test_geth_calltracer_logs_delegatecall: DELEGATECALL regression — verifies the log address is the proxy (execution context), not the implementation (bytecode) address Breaking CallLog is a public struct with a new address: Address field. Downstream consumers that construct CallLog manually will need to provide this field. Co-authored-by: Kai Yu <kai.yu@Kai-Yu-MacBook-Pro.local> * chore: bump alloy to 2.0 (paradigmxyz#421) Bump alloy dependencies to released 2.0.0. No `[patch.crates-io]` needed. Supersedes paradigmxyz#419 * chore: release 0.37.0 * tip1016: revm state-gas integration (paradigmxyz#405) ## Summary - Update revm rev to `e3223d5b38f7affb901601ed917fa7cdc466be74` (rakita/state-gas branch) for TIP-1016 dual-limit gas accounting ## Test plan - [x] `cargo check` compiles clean - [x] `cargo +nightly clippy` passes - [x] `cargo nextest run` — all 37 tests pass --------- Co-authored-by: Federico Gimenez <federico.gimenez@gmail.com> * chore: release 0.38.0 * fix: check if opcode is valid (paradigmxyz#422) TracingInspector creates OpCodes via `new_unchecked` at `start_step` to handle unknown/custom opcodes. In `step_end`, `step.op.outputs()` calls `OpCode::info()` which panics with `"opcode not found"` when the opcode byte has no entry in `OPCODE_INFO`. Guard with `is_valid()` — unknown opcodes default to 0 outputs. Prompted by: Dragan --------- Co-authored-by: rakita <13179220+rakita@users.noreply.github.com> Co-authored-by: rakita <rakita@users.noreply.github.com> * chore: release 0.38.1 * fix(tracing): always include refund counter, wire enableReturnData config (paradigmxyz#424) Two fixes based on the [ethpandaops trace comparison](https://investigations.ethpandaops.io/2026-01/trace-comparisons/): 1. Always include the refund counter in struct logs, even when zero. Previously omitted when value was 0, causing missing fields in trace output. 2. Wire `enableReturnData` from `GethDefaultTracingOptions` through to `TracingInspectorConfig::record_returndata_snapshots`. Previously this option was ignored, so returnData was never captured even when explicitly requested. Verified on a live reth archive node: - Refund: 22,476/67,028 steps → **67,028/67,028** steps - returnData with `enableReturnData: true`: 0 steps → **24,537** steps * fix(tracing): align memory encoding and returnData gating (paradigmxyz#425) Two fixes to align with the [opcode tracer spec](ethereum/execution-apis#762): 1. Memory chunks now use `0x`-prefixed `bytes32` encoding. Previously `convert_memory()` used bare `hex::encode` without the prefix. 2. `returnData` is now included on every step when `enableReturnData` is true, even when the buffer is empty. Previously skipped when empty. --------- Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de> * chore: bump revm to 38.0.0 (paradigmxyz#427) ## Summary - Bumps `revm` from `37.0.0` to `38.0.0`. ## Test plan - [ ] `cargo build` passes - [ ] `cargo test` passes - [ ] CI green * chore: release 0.39.0 * fix: use .values() instead of iter with unused key to satisfy clippy * Revert "fix: use .values() instead of iter with unused key to satisfy clippy" This reverts commit d7d6d46. * Reapply "fix: use .values() instead of iter with unused key to satisfy clippy" This reverts commit 43a426e. --------- Co-authored-by: rakita <rakita@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com> Co-authored-by: Karl Yu <43113774+0xKarl98@users.noreply.github.com> Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de> Co-authored-by: Alex Pikme <30472093+reject-i@users.noreply.github.com> Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: Wodann <Wodann@users.noreply.github.com> Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Philippe Dumonet <philippe@dumo.net> Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com> Co-authored-by: zhygis <5236121+Zygimantass@users.noreply.github.com> Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> Co-authored-by: Kai Yu <kai.yu@circle.com> Co-authored-by: Kai Yu <kai.yu@Kai-Yu-MacBook-Pro.local> Co-authored-by: Federico Gimenez <federico.gimenez@gmail.com> Co-authored-by: rakita <13179220+rakita@users.noreply.github.com> Co-authored-by: figtracer <me@figtracer.com>
The default tracer for
debug_*RPC methods was not reporting warmed storage values. This deviates from Get's implementation and—seemingly—the documented expectation:In particular, the
SLOADfor pc235,279, and308are missing"storage". This PR resolves the issue.For reference:
revm-inspectors.json
geth.json