From f070f5713f1ba58c33e9cdf39cfe79e6d35f48db Mon Sep 17 00:00:00 2001 From: avalonche Date: Tue, 17 Mar 2026 09:16:53 -0700 Subject: [PATCH 1/2] fix: gate incremental trie behind flag and use cumulative prefix sets Adds `--flashblocks.enable-incremental-state-root` flag (default false) to gate incremental trie state root calculation. When enabled, cumulative prefix sets from all prior flashblocks are carried forward so the trie walker re-visits every modified path, preventing stale cached hashes from reverted storage slots. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/op-rbuilder/src/args/op.rs | 10 ++++ crates/op-rbuilder/src/builder/config.rs | 9 ++++ crates/op-rbuilder/src/builder/payload.rs | 58 +++++++++++++++++++---- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/crates/op-rbuilder/src/args/op.rs b/crates/op-rbuilder/src/args/op.rs index d30961c5..e5c718d8 100644 --- a/crates/op-rbuilder/src/args/op.rs +++ b/crates/op-rbuilder/src/args/op.rs @@ -127,6 +127,16 @@ pub struct FlashblocksArgs { )] pub flashblocks_disable_state_root: bool, + /// Whether to enable incremental state root calculation for flashblocks. + /// When enabled, flashblocks reuse cached trie nodes from the previous flashblock + /// instead of performing a full state root calculation each time. + #[arg( + long = "flashblocks.enable-incremental-state-root", + default_value = "false", + env = "FLASHBLOCKS_ENABLE_INCREMENTAL_STATE_ROOT" + )] + pub flashblocks_enable_incremental_state_root: bool, + /// Flashblocks number contract address /// /// This is the address of the contract that will be used to increment the flashblock number. diff --git a/crates/op-rbuilder/src/builder/config.rs b/crates/op-rbuilder/src/builder/config.rs index 6b1566e6..6905da8d 100644 --- a/crates/op-rbuilder/src/builder/config.rs +++ b/crates/op-rbuilder/src/builder/config.rs @@ -21,6 +21,11 @@ pub struct FlashblocksConfig { /// Should we disable state root calculation for each flashblock pub disable_state_root: bool, + /// Whether to enable incremental state root calculation. + /// When true, flashblocks reuse cached trie nodes from the previous flashblock. + /// When false (default), every flashblock computes its state root from scratch. + pub enable_incremental_state_root: bool, + /// The address of the flashblocks number contract. /// /// If set a builder tx will be added to the start of every flashblock instead of the regular builder tx. @@ -62,6 +67,7 @@ impl Default for FlashblocksConfig { ws_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 1111), interval: Duration::from_millis(250), disable_state_root: false, + enable_incremental_state_root: false, number_contract_address: None, number_contract_use_permit: false, send_offset_ms: 0, @@ -97,6 +103,9 @@ impl TryFrom for FlashblocksConfig { ws_addr, interval, disable_state_root, + enable_incremental_state_root: args + .flashblocks + .flashblocks_enable_incremental_state_root, number_contract_address, number_contract_use_permit, send_offset_ms: args.flashblocks.flashblocks_send_offset_ms, diff --git a/crates/op-rbuilder/src/builder/payload.rs b/crates/op-rbuilder/src/builder/payload.rs index 26e8b27b..1a228287 100644 --- a/crates/op-rbuilder/src/builder/payload.rs +++ b/crates/op-rbuilder/src/builder/payload.rs @@ -46,7 +46,7 @@ use reth_revm::{ State, database::StateProviderDatabase, db::states::bundle_state::BundleRetention, }; use reth_transaction_pool::TransactionPool; -use reth_trie::{HashedPostState, TrieInput, updates::TrieUpdates}; +use reth_trie::{HashedPostState, TrieInput, prefix_set::TriePrefixSetsMut, updates::TrieUpdates}; use revm::Database; use std::{collections::BTreeMap, sync::Arc, time::Instant}; use tokio::sync::mpsc; @@ -104,19 +104,33 @@ pub(super) struct FlashblocksState { da_footprint_per_batch: Option, /// Whether to disable state root calculation for each flashblock disable_state_root: bool, + /// Whether to enable incremental state root calculation using cached trie nodes + enable_incremental_state_root: bool, /// Index into ExecutionInfo tracking the last consumed flashblock /// Used for slicing transactions/receipts per flashblock last_flashblock_tx_index: usize, /// Cached trie updates from previous flashblock for incremental state root calculation. /// None only for the first flashblock; populated after each subsequent state root calculation. prev_trie_updates: Option>, + /// Cumulative prefix sets from all previous flashblocks in this block. + /// Extended into the current flashblock's prefix sets so the trie walker re-visits + /// every path that was modified in earlier flashblocks. Without this, reverted storage + /// slots can leave stale cached hashes in the incremental trie (the walker skips + /// subtrees whose prefix isn't covered, using the cached hash which reflects the + /// pre-revert value). + cumulative_prefix_sets: Option, } impl FlashblocksState { - fn new(target_flashblock_count: u64, disable_state_root: bool) -> Self { + fn new( + target_flashblock_count: u64, + disable_state_root: bool, + enable_incremental_state_root: bool, + ) -> Self { Self { target_flashblock_count, disable_state_root, + enable_incremental_state_root, ..Default::default() } } @@ -138,8 +152,10 @@ impl FlashblocksState { da_per_batch: self.da_per_batch, da_footprint_per_batch: self.da_footprint_per_batch, disable_state_root: self.disable_state_root, + enable_incremental_state_root: self.enable_incremental_state_root, last_flashblock_tx_index: self.last_flashblock_tx_index, prev_trie_updates: self.prev_trie_updates.clone(), + cumulative_prefix_sets: self.cumulative_prefix_sets.clone(), } } @@ -419,6 +435,8 @@ where ); let disable_state_root = self.config.flashblocks_config.disable_state_root; + let enable_incremental_state_root = + self.config.flashblocks_config.enable_incremental_state_root; let ctx = self .get_op_payload_builder_ctx(config.clone(), block_cancel.clone()) .map_err(|e| PayloadBuilderError::Other(e.into()))?; @@ -429,6 +447,7 @@ where .flashblocks_config .flashblocks_per_block(self.config.block_time), disable_state_root, + enable_incremental_state_root, ); let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?; @@ -1078,14 +1097,21 @@ where let mut state_root = B256::ZERO; let mut hashed_state = HashedPostState::default(); let mut trie_updates_to_cache: Option> = None; + let mut prefix_sets_to_cache: Option = None; if calculate_state_root { let state_provider = state.database.as_ref(); // prev_trie_updates is None for the first flashblock. + let enable_incremental = fb_state + .as_deref() + .is_some_and(|s| s.enable_incremental_state_root); let prev_trie = fb_state .as_deref() .and_then(|s| s.prev_trie_updates.clone()); + let prev_cumulative_prefix_sets = fb_state + .as_deref() + .and_then(|s| s.cumulative_prefix_sets.clone()); let flashblock_index = fb_state .as_deref() .map(|s| s.flashblock_index()) @@ -1094,7 +1120,9 @@ where hashed_state = state_provider.hashed_post_state(&state.bundle_state); let trie_output; - (state_root, trie_output) = if let Some(prev_trie) = prev_trie { + (state_root, trie_output) = if let Some(prev_trie) = prev_trie + && enable_incremental + { // Incremental path: Use cached trie from previous flashblock debug!( target: "payload_builder", @@ -1102,11 +1130,21 @@ where "Using incremental state root calculation with cached trie" ); - let trie_input = TrieInput::new( - (*prev_trie).clone(), - hashed_state.clone(), - hashed_state.construct_prefix_sets(), // Don't freeze - need TriePrefixSetsMut - ); + // Extend current prefix sets with cumulative prefix sets from all + // prior flashblocks. This ensures the trie walker re-visits every + // path that was modified in earlier flashblocks, even if the slot + // reverted (disappeared from cumulative HashedPostState). Without + // this, reverted slots leave stale cached hashes in the branch + // nodes returned by InMemoryTrieCursor. + let mut prefix_sets = hashed_state.construct_prefix_sets(); + if let Some(prev_sets) = prev_cumulative_prefix_sets { + prefix_sets.extend(prev_sets); + } + // Cache the cumulative prefix sets for the next flashblock + prefix_sets_to_cache = Some(prefix_sets.clone()); + + let trie_input = + TrieInput::new((*prev_trie).clone(), hashed_state.clone(), prefix_sets); state_provider .state_root_from_nodes_with_updates(trie_input) @@ -1118,6 +1156,9 @@ where "Using full state root calculation" ); + // Cache prefix sets for the next flashblock's incremental path + prefix_sets_to_cache = Some(hashed_state.construct_prefix_sets()); + state .database .as_ref() @@ -1270,6 +1311,7 @@ where if let Some(updates) = trie_updates_to_cache.take() { fb_state.prev_trie_updates = Some(updates); } + fb_state.cumulative_prefix_sets = prefix_sets_to_cache; let new_txs = fb_state.slice_new_transactions(&info.executed_transactions); let new_receipts = fb_state.slice_new_receipts(&info.receipts); fb_state.set_last_flashblock_tx_index(info.executed_transactions.len()); From b77df4d1c5184bad81f51c3687f89f4bdbffd2dc Mon Sep 17 00:00:00 2001 From: avalonche Date: Tue, 17 Mar 2026 09:19:39 -0700 Subject: [PATCH 2/2] fix: remove cumulative prefix sets, keep only the incremental flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip the cumulative_prefix_sets plumbing from FlashblocksState and payload builder — this PR now only gates incremental trie behind the --flashblocks.enable-incremental-state-root flag. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/op-rbuilder/src/builder/payload.rs | 38 ++++------------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/crates/op-rbuilder/src/builder/payload.rs b/crates/op-rbuilder/src/builder/payload.rs index 1a228287..6ad3cc5e 100644 --- a/crates/op-rbuilder/src/builder/payload.rs +++ b/crates/op-rbuilder/src/builder/payload.rs @@ -46,7 +46,7 @@ use reth_revm::{ State, database::StateProviderDatabase, db::states::bundle_state::BundleRetention, }; use reth_transaction_pool::TransactionPool; -use reth_trie::{HashedPostState, TrieInput, prefix_set::TriePrefixSetsMut, updates::TrieUpdates}; +use reth_trie::{HashedPostState, TrieInput, updates::TrieUpdates}; use revm::Database; use std::{collections::BTreeMap, sync::Arc, time::Instant}; use tokio::sync::mpsc; @@ -112,13 +112,6 @@ pub(super) struct FlashblocksState { /// Cached trie updates from previous flashblock for incremental state root calculation. /// None only for the first flashblock; populated after each subsequent state root calculation. prev_trie_updates: Option>, - /// Cumulative prefix sets from all previous flashblocks in this block. - /// Extended into the current flashblock's prefix sets so the trie walker re-visits - /// every path that was modified in earlier flashblocks. Without this, reverted storage - /// slots can leave stale cached hashes in the incremental trie (the walker skips - /// subtrees whose prefix isn't covered, using the cached hash which reflects the - /// pre-revert value). - cumulative_prefix_sets: Option, } impl FlashblocksState { @@ -155,7 +148,6 @@ impl FlashblocksState { enable_incremental_state_root: self.enable_incremental_state_root, last_flashblock_tx_index: self.last_flashblock_tx_index, prev_trie_updates: self.prev_trie_updates.clone(), - cumulative_prefix_sets: self.cumulative_prefix_sets.clone(), } } @@ -1097,7 +1089,6 @@ where let mut state_root = B256::ZERO; let mut hashed_state = HashedPostState::default(); let mut trie_updates_to_cache: Option> = None; - let mut prefix_sets_to_cache: Option = None; if calculate_state_root { let state_provider = state.database.as_ref(); @@ -1109,9 +1100,6 @@ where let prev_trie = fb_state .as_deref() .and_then(|s| s.prev_trie_updates.clone()); - let prev_cumulative_prefix_sets = fb_state - .as_deref() - .and_then(|s| s.cumulative_prefix_sets.clone()); let flashblock_index = fb_state .as_deref() .map(|s| s.flashblock_index()) @@ -1130,21 +1118,11 @@ where "Using incremental state root calculation with cached trie" ); - // Extend current prefix sets with cumulative prefix sets from all - // prior flashblocks. This ensures the trie walker re-visits every - // path that was modified in earlier flashblocks, even if the slot - // reverted (disappeared from cumulative HashedPostState). Without - // this, reverted slots leave stale cached hashes in the branch - // nodes returned by InMemoryTrieCursor. - let mut prefix_sets = hashed_state.construct_prefix_sets(); - if let Some(prev_sets) = prev_cumulative_prefix_sets { - prefix_sets.extend(prev_sets); - } - // Cache the cumulative prefix sets for the next flashblock - prefix_sets_to_cache = Some(prefix_sets.clone()); - - let trie_input = - TrieInput::new((*prev_trie).clone(), hashed_state.clone(), prefix_sets); + let trie_input = TrieInput::new( + (*prev_trie).clone(), + hashed_state.clone(), + hashed_state.construct_prefix_sets(), + ); state_provider .state_root_from_nodes_with_updates(trie_input) @@ -1156,9 +1134,6 @@ where "Using full state root calculation" ); - // Cache prefix sets for the next flashblock's incremental path - prefix_sets_to_cache = Some(hashed_state.construct_prefix_sets()); - state .database .as_ref() @@ -1311,7 +1286,6 @@ where if let Some(updates) = trie_updates_to_cache.take() { fb_state.prev_trie_updates = Some(updates); } - fb_state.cumulative_prefix_sets = prefix_sets_to_cache; let new_txs = fb_state.slice_new_transactions(&info.executed_transactions); let new_receipts = fb_state.slice_new_receipts(&info.receipts); fb_state.set_last_flashblock_tx_index(info.executed_transactions.len());