feat(flashblocks): State root computation via PayloadProcessor sparse trie task#2247
feat(flashblocks): State root computation via PayloadProcessor sparse trie task#2247
Conversation
mw2000
commented
Apr 16, 2026
- Replace inline synchronous state root computation with reth's PayloadProcessor sparse trie task, computing state roots in parallel during block building
- PayloadProcessor, ChangesetCache, and TreeConfig are stored on OpPayloadBuilder and reused across blocks so the sparse trie stays warm
- Falls back to synchronous computation on task failure
- Rewrite state_root benchmark to measure residual wait time (finish → root) with busy-wait simulation of 200ms inter-flashblock delays
…trics Expand ClientBounds with DatabaseProviderFactory, StateReader, and 'static to support OverlayStateProviderFactory and StateProviderBuilder needed by the upcoming PayloadProcessor integration. Add state_root_task_* metrics (started, completed, error counts and duration histogram) for observability.
…rocessor Spawn reth's PayloadProcessor to compute state roots in parallel with transaction execution via the sparse trie task. Per-tx state diffs are fed through OnStateHook and the final root is awaited at finalization, with a synchronous fallback on task failure. PayloadProcessor, ChangesetCache, and TreeConfig are stored on OpPayloadBuilder and reused across blocks so the sparse trie stays warm when consecutive blocks share the same parent state root.
…wait Rewrite the state root benchmark to measure residual wait time — the duration from dropping the state hook to receiving the computed root. Uses busy-wait spin loops (not thread::sleep) to simulate 200ms inter-flashblock execution delays without OS scheduling artifacts. Includes warm sparse trie (cold-start warmup block), 50k base-state accounts, and per-flashblock intermediate root measurements.
🟡 Heimdall Review Status
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
| let start_time = std::time::Instant::now(); | ||
|
|
||
| // Signal that all state updates are done. | ||
| state_hook.finish(); | ||
|
|
||
| let state_provider = state.database.as_ref(); | ||
| let hashed_state = state_provider.hashed_post_state(&state.bundle_state); |
There was a problem hiding this comment.
Potential correctness issue: hashed_state computed before merge_transitions
hashed_post_state(&state.bundle_state) is called here, before build_block calls state.merge_transitions(BundleRetention::Reverts). In reth's State model, individual commit() calls push changes into transition_state, and merge_transitions() merges them into bundle_state. If there are un-merged transitions at this point, the hashed_state won't reflect all state changes.
This hashed_state is then passed to build_block and stored in BuiltPayloadExecutedBlock, which the engine uses for state persistence. If it's stale (pre-merge), the engine could persist an inconsistent view.
Consider calling state.merge_transitions(BundleRetention::Reverts) before computing the hashed state, or moving this computation to after build_block merges transitions. Alternatively, verify that all transitions are already merged into bundle_state by this point (i.e., no pending transitions exist).
| if let Some(rx) = state_root_handle { | ||
| match rx.recv() { |
There was a problem hiding this comment.
Blocking recv() on a tokio worker thread
rx.recv() is std::sync::mpsc::Receiver::recv(), which blocks the current thread. This is called from finalize_payload, which is invoked from async fn build_payload. Blocking a tokio worker thread while waiting for the sparse trie task can stall other tasks on the same runtime, especially under load.
Consider using tokio::sync::oneshot instead of std::sync::mpsc for StateRootHandle, or wrapping this in tokio::task::spawn_blocking / tokio::task::block_in_place.
| let env = reth_engine_tree::tree::ExecutionEnv { | ||
| evm_env: Default::default(), | ||
| hash: B256::ZERO, | ||
| parent_hash, | ||
| parent_state_root, | ||
| transaction_count: 0, | ||
| withdrawals: None, | ||
| }; |
There was a problem hiding this comment.
evm_env: Default::default() and hash: B256::ZERO — these are placeholder values for the ExecutionEnv since the builder manages its own execution loop. This works because the empty tx list means the processor won't actually execute anything with these fields. However, this coupling to internal PayloadProcessor behavior is fragile — if PayloadProcessor::spawn starts validating or using these fields (e.g., for logging, caching, or consistency checks), this will silently produce wrong results. A brief // SAFETY: or // INVARIANT: comment documenting why the defaults are correct would help future readers.
| pub config: BuilderConfig, | ||
| /// Reusable payload processor — warm sparse trie persisted across blocks. | ||
| #[debug(skip)] | ||
| payload_processor: Arc<std::sync::Mutex<PayloadProcessor<BaseEvmConfig>>>, |
There was a problem hiding this comment.
Arc<std::sync::Mutex<PayloadProcessor>> — the lock is correctly scoped (dropped before any .await), but since OpPayloadBuilder is Clone (and payload_processor is Arc), concurrent build_payload calls would contend on this lock. If the service guarantees single-active-builder at a time, that's fine, but worth a comment noting the exclusivity assumption.
|
|
||
| // PayloadProcessor is reused across blocks for warm sparse trie. | ||
| let evm_config = BaseEvmConfig::optimism(ctx.chain_spec()); | ||
| let tree_config = TreeConfig::default(); |
There was a problem hiding this comment.
TreeConfig::default() — the benchmark uses TreeConfig::default().with_disable_sparse_trie_cache_pruning(true), but production here uses the plain default (which has pruning enabled). If the benchmark is meant to reflect production, one of these should change. If cache pruning is intentionally enabled in production but disabled in benchmarks to keep the trie warm, that's fine but worth documenting the discrepancy.
Review SummaryThis PR integrates reth's Correctness concern (high)
Concurrency concerns (medium)
Robustness / maintainability (low)
Trait bound expansion (note)
|