Skip to content

perf(l1): prewarming using mempool contents#6967

Open
iovoid wants to merge 27 commits into
mainfrom
perf/mempool-prewarm
Open

perf(l1): prewarming using mempool contents#6967
iovoid wants to merge 27 commits into
mainfrom
perf/mempool-prewarm

Conversation

@iovoid

@iovoid iovoid commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Motivation

Some of the transactions that will be included in the next block are already known beforehand. We can use this fact to speculatively load state to save costs during actual block execution.

Description

To ensure that malicious mempool transactions can't have an outsized negative impact we:

  • use the "block building" logic in intervals, rather than executing transactions as they arrive
  • halt the process before the block is expected to arrive

There is a known downside of making measurements more noisy (as they now depend on mempool contents), but the ~15% improvement brought by this PR is worth it.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Stateless blockchain EF-tests (zkevm bundle) skipped under Amsterdam v6.1.0

make -C tooling/ef_tests/blockchain test-stateless runs against the
tests-zkevm@v0.4.1 fixture bundle — currently the only published zkevm test
release. Those fixtures were filled against an older glamsterdam devnet and
re-execute every case under the for_amsterdam fork, so they lag the
glamsterdam-devnet v6.1.0 gas accounting this client now implements
(EIP-8037 / EIP-8038 / EIP-2780 / EIP-7976 / EIP-7981 …). Re-executing them
yields ~2790/2864 stale-gas failures ("Transaction execution unexpectedly
failed"), spread pervasively across every fork and through the EIP-8025 proof
suite, so there is no clean passing subset to keep.

Until a v6.1.0-aligned zkevm bundle is published, the entire bundle is skipped
for the stateless run via the fork_Amsterdam entry in the stateless-only
EXTRA_SKIPS (tooling/ef_tests/blockchain/tests/all.rs) — every test in this
Amsterdam-only bundle carries the [fork_Amsterdam-…] parametrization in its
test key. The skip is #[cfg(feature = "stateless")], so it does not touch
the non-stateless test-levm run. Coverage of these EIPs is retained by
test-levm, the engine EF-tests, and the state EF-tests, all of which execute
against the live v6.1.0 fixtures and pass.

Re-enable by removing the "fork_Amsterdam" skip once .fixtures_url_zkevm
points at a zkevm bundle filled for glamsterdam-devnet v6.1.0.

@github-actions github-actions Bot added L1 Ethereum client performance Block execution throughput and performance in general labels Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 587
Total lines removed: 0
Total lines changed: 587

Detailed view
+----------------------------------------+-------+------+
| File                                   | Lines | Diff |
+----------------------------------------+-------+------+
| ethrex/crates/blockchain/blockchain.rs | 3183  | +39  |
+----------------------------------------+-------+------+
| ethrex/crates/blockchain/prewarm.rs    | 486   | +486 |
+----------------------------------------+-------+------+
| ethrex/crates/networking/rpc/rpc.rs    | 1414  | +12  |
+----------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs  | 2863  | +21  |
+----------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/db/mod.rs    | 177   | +29  |
+----------------------------------------+-------+------+

@iovoid iovoid marked this pull request as ready for review July 6, 2026 21:09
@iovoid iovoid requested a review from a team as a code owner July 6, 2026 21:09
@ethrex-project-sync ethrex-project-sync Bot moved this to In Review in ethrex_l1 Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

  1. crates/blockchain/prewarm.rs drops already-warmed hashes before each delta pass, but LEVM::warm_txs rebuilds each sender group from a fresh parent-state overlay every time. That means later passes can end up executing a sender’s suffix without its already-warmed prefix, so nonce/balance-dependent txs short-circuit and never touch the state they would in real block execution. This makes the “refreshing delta” logic materially less accurate and can also burn the per-sender cap on txs that were never realistically warmable. See prewarm.rs and mod.rs. I’d either keep the sender prefix in the replay set, or carry forward a per-sender speculative overlay across delta passes.

  2. The synthetic child header used for prewarming never updates gas_limit; it stays at the parent’s value. From Amsterdam onward, intrinsic state gas depends on header.gas_limit via cost_per_state_byte, so a legitimate next block that adjusts gas limit can validate/warm a different tx set than the prewarmer did. That’s not a consensus break because the cache only stores parent-state reads, but it does make the cache handoff noticeably less faithful than the PR description suggests. See prewarm.rs and utils.rs. At minimum, I’d consider invalidating the handoff when block.header.gas_limit != parent.gas_limit, or modeling the child gas limit explicitly.

I didn’t see a direct state-corruption or fork-rule violation in the cache handoff itself; the fork guard and witness cold-start exception both look sensible. I did not run the test suite.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 2.991 ± 0.031 2.961 3.058 1.14 ± 0.01
main_levm_BubbleSort 2.625 ± 0.018 2.607 2.654 1.00
pr_revm_BubbleSort 2.979 ± 0.039 2.935 3.063 1.13 ± 0.02
pr_levm_BubbleSort 2.633 ± 0.015 2.612 2.657 1.00 ± 0.01

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 980.3 ± 7.6 971.5 992.3 1.00 ± 0.06
main_levm_ERC20Approval 978.2 ± 26.7 960.6 1052.1 1.00 ± 0.06
pr_revm_ERC20Approval 977.5 ± 53.2 951.5 1127.0 1.00
pr_levm_ERC20Approval 988.9 ± 15.6 976.4 1027.9 1.01 ± 0.06

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 135.5 ± 1.7 132.1 138.9 1.02 ± 0.03
main_levm_ERC20Mint 145.8 ± 1.6 144.9 150.2 1.10 ± 0.03
pr_revm_ERC20Mint 133.0 ± 3.5 130.4 141.2 1.00
pr_levm_ERC20Mint 147.0 ± 1.0 145.6 148.7 1.11 ± 0.03

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 236.2 ± 4.1 231.9 241.9 1.03 ± 0.02
main_levm_ERC20Transfer 248.2 ± 5.1 243.3 261.0 1.08 ± 0.03
pr_revm_ERC20Transfer 229.6 ± 2.6 226.8 234.1 1.00
pr_levm_ERC20Transfer 248.4 ± 2.6 245.1 252.4 1.08 ± 0.02

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 225.7 ± 1.2 223.6 227.9 1.00
main_levm_Factorial 259.4 ± 2.0 257.7 263.5 1.15 ± 0.01
pr_revm_Factorial 228.4 ± 11.3 224.1 260.4 1.01 ± 0.05
pr_levm_Factorial 256.1 ± 2.0 254.4 260.0 1.13 ± 0.01

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.673 ± 0.030 1.611 1.705 1.00 ± 0.03
main_levm_FactorialRecursive 9.288 ± 0.055 9.235 9.425 5.58 ± 0.14
pr_revm_FactorialRecursive 1.666 ± 0.039 1.578 1.703 1.00
pr_levm_FactorialRecursive 9.253 ± 0.042 9.190 9.300 5.55 ± 0.13

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 205.7 ± 2.0 204.6 211.3 1.00
main_levm_Fibonacci 231.5 ± 4.1 228.9 240.5 1.13 ± 0.02
pr_revm_Fibonacci 211.0 ± 16.5 204.6 258.0 1.03 ± 0.08
pr_levm_Fibonacci 231.5 ± 3.2 228.4 237.9 1.13 ± 0.02

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 879.1 ± 7.7 863.5 888.9 1.23 ± 0.01
main_levm_FibonacciRecursive 716.1 ± 5.6 710.7 727.9 1.00
pr_revm_FibonacciRecursive 883.8 ± 16.9 868.0 926.5 1.23 ± 0.03
pr_levm_FibonacciRecursive 716.4 ± 3.6 712.4 723.8 1.00 ± 0.01

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.3 ± 0.0 8.3 8.4 1.00
main_levm_ManyHashes 9.2 ± 0.1 9.1 9.4 1.10 ± 0.01
pr_revm_ManyHashes 8.6 ± 0.8 8.2 10.7 1.03 ± 0.09
pr_levm_ManyHashes 9.3 ± 0.1 9.2 9.4 1.11 ± 0.01

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 267.2 ± 9.1 260.1 287.6 1.35 ± 0.05
main_levm_MstoreBench 197.4 ± 2.5 195.1 203.1 1.00 ± 0.01
pr_revm_MstoreBench 265.6 ± 5.0 261.4 275.0 1.35 ± 0.03
pr_levm_MstoreBench 197.3 ± 1.4 195.1 199.1 1.00

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 285.1 ± 0.9 283.4 286.1 1.17 ± 0.01
main_levm_Push 243.8 ± 1.0 242.4 245.5 1.00
pr_revm_Push 285.8 ± 1.3 284.4 288.2 1.17 ± 0.01
pr_levm_Push 244.2 ± 1.2 242.8 245.9 1.00 ± 0.01

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 164.6 ± 2.8 161.5 168.9 1.65 ± 0.05
main_levm_SstoreBench_no_opt 99.8 ± 2.7 98.3 106.6 1.00
pr_revm_SstoreBench_no_opt 166.8 ± 2.7 161.6 169.0 1.67 ± 0.05
pr_levm_SstoreBench_no_opt 99.8 ± 2.7 98.6 107.5 1.00 ± 0.04

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds mempool-driven state pre-warming between block slots: after each imported block, a background worker speculatively executes the top-of-mempool transactions against the new head state, publishing the warm CachingDatabase for reuse by the next block's execution pipeline. The ~15% throughput improvement is achieved through three mechanisms: cache handoff (the pre-populated CachingDatabase is injected into execute_block_pipeline when parent hash and fork both match), persistent RocksDB population (reads during warming prime the underlying block/code caches), and trie-path warming (walking account and storage trie proofs so the merkleizer finds interior nodes hot).

  • prewarm.rs (645 lines) implements the worker lifecycle, mempool-snapshot filtering, per-sender depth capping, delta-pass deduplication, and merkle-path walking; the worker gracefully handles panics from speculative execution.
  • warm_block in levm/mod.rs is refactored to delegate to the new public warm_txs, which accepts a cancellation closure instead of an AtomicBool directly, enabling reuse from the mempool prewarmer.
  • blockchain.rs gains a PrewarmedCache mutex slot that execute_block_pipeline takes (and validates for parent hash + fork match) before constructing a cold CachingDatabase.

Confidence Score: 4/5

The core handoff mechanism (fork + parent-hash double-check, witness exclusion, cache taken out of the slot atomically) is correct and the prewarmer cannot write incorrect state into execution. All three findings are confined to warming efficiency rather than block validity.

All three findings are in the speculative, read-only warming layer: if warming is less effective than intended, block execution falls back to a cold CachingDatabase — the same path that was already working. The warmed_union pre-update and walked_accounts early-insert are logic gaps that reduce warming coverage in edge cases; they do not affect consensus or correctness.

crates/blockchain/prewarm.rs deserves the closest look — the warmed_union pre-update, walked_accounts insert-before-check, and delta successor-tx warming gaps are all there. The rest of the changed files look straightforward.

Important Files Changed

Filename Overview
crates/blockchain/prewarm.rs New 645-line module implementing the mempool prewarmer: background worker, delta-pass loop, mempool snapshot filtering, per-sender depth capping, and merkle trie path walking. Three logic issues: warmed_union pre-populated before warm_txs runs, accounts inserted into walked_accounts before their trie path is retrieved, and delta successor transactions warming against stale parent state.
crates/blockchain/blockchain.rs Adds PrewarmedCache/PrewarmedEntry types and wires the handoff slot into execute_block_pipeline; fork + parent-hash double-check before seeding execution is correct, and witness-collection path is correctly excluded from cache reuse.
crates/vm/backends/levm/mod.rs warm_block refactored to delegate tx warming to the new public warm_txs; withdrawals warming moved after warm_txs and guarded by the cancelled check; both functions share the same cfg gate so there is no feature-flag mismatch.
crates/networking/rpc/rpc.rs Spawns MempoolPrewarmer, cancels in-flight warming before each block import, and triggers a new pass only when synced and the queue is empty — the guards look correct.
crates/vm/levm/src/db/mod.rs Adds TouchedKeys struct and touched_keys_where method to CachingDatabase; implementation correctly reads both the account and storage RwLocks and applies caller-provided filters.
crates/vm/backends/mod.rs Makes VMType pub so the mempool prewarmer can reference it; trivial visibility change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BE as block_executor thread
    participant PH as PrewarmHandle
    participant W as mempool_prewarmer thread
    participant BC as Blockchain.prewarmed (Mutex)
    participant EP as execute_block_pipeline

    BE->>PH: cancel_current() — set cancel flag
    BE->>EP: execute_block_pipeline(block)
    EP->>BC: lock().take() — steal cache if parent_hash+fork match
    BC-->>EP: Some(cache) or None
    EP->>EP: seed CachingDatabase with prewarmed cache
    EP-->>BE: Ok(imported header)
    BE->>PH: trigger(imported_header)
    PH->>W: send PrewarmRequest(parent, cancel, deadline)
    W->>W: drain stale requests
    W->>W: run_pass(blockchain, pool, req)
    W->>BC: lock() — publish empty CachingDatabase
    loop every 100ms until cancel or deadline
        W->>W: filter_transactions (mempool snapshot)
        W->>W: drop_already_warmed + cap_sender_depth
        W->>W: select_warm_set (sort by tip)
        W->>W: LEVM::warm_txs (parallel per sender)
        W->>W: warm_merkle_paths (account + storage trie proofs)
    end
    Note over W,BC: cache fills with parent-state reads throughout loop
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant BE as block_executor thread
    participant PH as PrewarmHandle
    participant W as mempool_prewarmer thread
    participant BC as Blockchain.prewarmed (Mutex)
    participant EP as execute_block_pipeline

    BE->>PH: cancel_current() — set cancel flag
    BE->>EP: execute_block_pipeline(block)
    EP->>BC: lock().take() — steal cache if parent_hash+fork match
    BC-->>EP: Some(cache) or None
    EP->>EP: seed CachingDatabase with prewarmed cache
    EP-->>BE: Ok(imported header)
    BE->>PH: trigger(imported_header)
    PH->>W: send PrewarmRequest(parent, cancel, deadline)
    W->>W: drain stale requests
    W->>W: run_pass(blockchain, pool, req)
    W->>BC: lock() — publish empty CachingDatabase
    loop every 100ms until cancel or deadline
        W->>W: filter_transactions (mempool snapshot)
        W->>W: drop_already_warmed + cap_sender_depth
        W->>W: select_warm_set (sort by tip)
        W->>W: LEVM::warm_txs (parallel per sender)
        W->>W: warm_merkle_paths (account + storage trie proofs)
    end
    Note over W,BC: cache fills with parent-state reads throughout loop
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
crates/blockchain/prewarm.rs:521-538
**`warmed_union` updated before `warm_txs` runs**

`warmed_union` and `warmed_per_sender` are populated for every transaction in `warm_set` before `pool.install(|| LEVM::warm_txs(...))` executes. If `warm_txs` returns `Err` — which happens when `store.get_chain_config()` or `get_base_fee_per_blob_gas()` fails — none of those transactions were actually touched, but they are permanently excluded from the current slot's remaining delta passes by `drop_already_warmed`. The inflated `warmed_per_sender` counts also tighten the depth cap for those senders for the rest of the slot, silently reducing warming coverage with no log entry beyond the `any_err` flag at the end.

### Issue 2 of 3
crates/blockchain/prewarm.rs:484-491
**Account pre-inserted into `walked_accounts` before path is retrieved**

`walked_accounts.insert(addr, ...)` happens before the `should_stop()` guard and before the `state_trie.get_proof()` call. If `should_stop()` fires between the insert and the proof walk, the account is recorded as "already walked" in `walked_accounts`. The account filter in the next delta pass — `&|addr| !walked_accounts.contains_key(addr)` — will exclude it from `touched.accounts`, so the account trie path is never retrieved during the remainder of the slot. Moving the insert to after the `get_proof` call (and updating the subsequent storage-slot lookup accordingly) would close this window.

### Issue 3 of 3
crates/blockchain/prewarm.rs:205-216
**Delta-pass successor transactions warm against stale state**

`drop_already_warmed` removes previously-warmed hashes from the snapshot, so a delta pass can contain tx[1] for a sender whose tx[0] was warmed in an earlier pass. In `warm_txs` each sender group gets a fresh `GeneralizedDatabase` seeded from parent state (without tx[0]'s effects), causing tx[1] to fail immediately on nonce validation. The tx[1] execution path — including storage accesses that would benefit most from warming — is never explored. The comment "Ordering fidelity does not matter for warming, only membership" is accurate for account-state reads (tx[0] already cached those), but it underestimates the storage-slot coverage gap for successor transactions. A depth cap of 16 per sender makes this a bounded problem, but multi-tx senders arriving late in the slot get only their first transaction effectively warmed.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +521 to +538
if should_stop() {
return walked;
}
let _ = storage_trie.get_proof(&hash_key(&key));
walked += 1;
}
}
walked
}

#[cfg(test)]
mod tests {
use super::*;
use ethrex_common::Address;
use ethrex_common::types::{EIP1559Transaction, MempoolTransaction, Transaction};
use rustc_hash::FxHashMap;

fn make_tx(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 warmed_union updated before warm_txs runs

warmed_union and warmed_per_sender are populated for every transaction in warm_set before pool.install(|| LEVM::warm_txs(...)) executes. If warm_txs returns Err — which happens when store.get_chain_config() or get_base_fee_per_blob_gas() fails — none of those transactions were actually touched, but they are permanently excluded from the current slot's remaining delta passes by drop_already_warmed. The inflated warmed_per_sender counts also tighten the depth cap for those senders for the rest of the slot, silently reducing warming coverage with no log entry beyond the any_err flag at the end.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/prewarm.rs
Line: 521-538

Comment:
**`warmed_union` updated before `warm_txs` runs**

`warmed_union` and `warmed_per_sender` are populated for every transaction in `warm_set` before `pool.install(|| LEVM::warm_txs(...))` executes. If `warm_txs` returns `Err` — which happens when `store.get_chain_config()` or `get_base_fee_per_blob_gas()` fails — none of those transactions were actually touched, but they are permanently excluded from the current slot's remaining delta passes by `drop_already_warmed`. The inflated `warmed_per_sender` counts also tighten the depth cap for those senders for the rest of the slot, silently reducing warming coverage with no log entry beyond the `any_err` flag at the end.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +484 to +491
for (addr, storage_root) in touched.accounts {
let hashed = keccak(addr.to_fixed_bytes());
walked_accounts.insert(addr, (hashed, storage_root));
if should_stop() {
return walked;
}
let _ = state_trie.get_proof(hashed.as_bytes());
walked += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Account pre-inserted into walked_accounts before path is retrieved

walked_accounts.insert(addr, ...) happens before the should_stop() guard and before the state_trie.get_proof() call. If should_stop() fires between the insert and the proof walk, the account is recorded as "already walked" in walked_accounts. The account filter in the next delta pass — &|addr| !walked_accounts.contains_key(addr) — will exclude it from touched.accounts, so the account trie path is never retrieved during the remainder of the slot. Moving the insert to after the get_proof call (and updating the subsequent storage-slot lookup accordingly) would close this window.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/prewarm.rs
Line: 484-491

Comment:
**Account pre-inserted into `walked_accounts` before path is retrieved**

`walked_accounts.insert(addr, ...)` happens before the `should_stop()` guard and before the `state_trie.get_proof()` call. If `should_stop()` fires between the insert and the proof walk, the account is recorded as "already walked" in `walked_accounts`. The account filter in the next delta pass — `&|addr| !walked_accounts.contains_key(addr)` — will exclude it from `touched.accounts`, so the account trie path is never retrieved during the remainder of the slot. Moving the insert to after the `get_proof` call (and updating the subsequent storage-slot lookup accordingly) would close this window.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +205 to +216
.ok()?;
let (sender, receiver) = mpsc::channel::<PrewarmRequest>();
std::thread::Builder::new()
.name("mempool_prewarmer".to_string())
.spawn(move || {
while let Ok(mut req) = receiver.recv() {
// Drain to the newest request; stale ones are already cancelled.
while let Ok(next) = receiver.try_recv() {
req = next;
}
// A panic inside a pass (speculating on arbitrary
// mempool txs) must not kill this loop: the worker is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Delta-pass successor transactions warm against stale state

drop_already_warmed removes previously-warmed hashes from the snapshot, so a delta pass can contain tx[1] for a sender whose tx[0] was warmed in an earlier pass. In warm_txs each sender group gets a fresh GeneralizedDatabase seeded from parent state (without tx[0]'s effects), causing tx[1] to fail immediately on nonce validation. The tx[1] execution path — including storage accesses that would benefit most from warming — is never explored. The comment "Ordering fidelity does not matter for warming, only membership" is accurate for account-state reads (tx[0] already cached those), but it underestimates the storage-slot coverage gap for successor transactions. A depth cap of 16 per sender makes this a bounded problem, but multi-tx senders arriving late in the slot get only their first transaction effectively warmed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/prewarm.rs
Line: 205-216

Comment:
**Delta-pass successor transactions warm against stale state**

`drop_already_warmed` removes previously-warmed hashes from the snapshot, so a delta pass can contain tx[1] for a sender whose tx[0] was warmed in an earlier pass. In `warm_txs` each sender group gets a fresh `GeneralizedDatabase` seeded from parent state (without tx[0]'s effects), causing tx[1] to fail immediately on nonce validation. The tx[1] execution path — including storage accesses that would benefit most from warming — is never explored. The comment "Ordering fidelity does not matter for warming, only membership" is accurate for account-state reads (tx[0] already cached those), but it underestimates the storage-slot coverage gap for successor transactions. A depth cap of 16 per sender makes this a bounded problem, but multi-tx senders arriving late in the slot get only their first transaction effectively warmed.

How can I resolve this? If you propose a fix, please make it concise.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Benchmark Block Execution Results Comparison Against Main

Command Mean [s] Min [s] Max [s] Relative
base 60.878 ± 0.138 60.665 61.109 1.00
head 60.947 ± 0.119 60.764 61.108 1.00 ± 0.00

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client performance Block execution throughput and performance in general

Projects

Status: In Review
Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant