perf(l1): prewarming using mempool contents#6967
Conversation
This reverts commit 0f151ed.
|
Lines of code reportTotal lines added: Detailed view |
# Conflicts: # CHANGELOG.md
🤖 Codex Code Review
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 |
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
|
Greptile SummaryThis 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
Confidence Score: 4/5The 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.
|
| 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
%%{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
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
| 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( |
There was a problem hiding this 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.
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.| 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; |
There was a problem hiding this 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.
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.| .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 |
There was a problem hiding this 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.
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.
Benchmark Block Execution Results Comparison Against Main
|
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:
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.