From ec1b26a6fb19df03b2cb0c5dd58ca4ec0dacb230 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 21:22:43 +0800 Subject: [PATCH 1/8] Add state root calculation on payload resolve --- .../src/builders/flashblocks/payload.rs | 190 ++++++++++++++++-- .../builders/flashblocks/payload_handler.rs | 24 ++- .../src/builders/flashblocks/service.rs | 5 + 3 files changed, 191 insertions(+), 28 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index aaa74a44..1d15a689 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -30,6 +30,7 @@ use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; use reth_optimism_forks::OpHardforks; use reth_optimism_node::{OpBuiltPayload, OpPayloadBuilderAttributes}; use reth_optimism_primitives::{OpPrimitives, OpReceipt, OpTransactionSigned}; +use reth_payload_primitives::BuiltPayload; use reth_payload_util::BestPayloadTransactions; use reth_primitives_traits::RecoveredBlock; use reth_provider::{ @@ -136,9 +137,12 @@ pub(super) struct OpPayloadBuilder { pub pool: Pool, /// Node client pub client: Client, - /// Sender for sending built payloads to [`PayloadHandler`], - /// which broadcasts outgoing payloads via p2p. - pub payload_tx: mpsc::Sender, + /// Sender for sending built flashblock payloads to [`PayloadHandler`], + /// which broadcasts outgoing flashblock payloads via p2p. + pub built_fb_payload_tx: mpsc::Sender, + /// Sender for sending built full block payloads to [`PayloadHandler`], + /// which updates the engine tree state. + pub built_payload_tx: mpsc::Sender, /// WebSocket publisher for broadcasting flashblocks /// to all connected subscribers. pub ws_pub: Arc, @@ -161,7 +165,8 @@ impl OpPayloadBuilder { client: Client, config: BuilderConfig, builder_tx: BuilderTx, - payload_tx: mpsc::Sender, + built_fb_payload_tx: mpsc::Sender, + built_payload_tx: mpsc::Sender, ws_pub: Arc, metrics: Arc, ) -> Self { @@ -170,7 +175,8 @@ impl OpPayloadBuilder { evm_config, pool, client, - payload_tx, + built_fb_payload_tx, + built_payload_tx, ws_pub, config, metrics, @@ -288,7 +294,7 @@ where async fn build_payload( &self, args: BuildArguments, OpBuiltPayload>, - best_payload: BlockCell, + resolve_payload: BlockCell, ) -> Result<(), PayloadBuilderError> { let block_build_start_time = Instant::now(); let BuildArguments { @@ -368,18 +374,13 @@ where let builder_tx_da_size: u64 = builder_txs.iter().fold(0, |acc, tx| acc + tx.da_size); info.cumulative_da_bytes_used += builder_tx_da_size; - let (payload, fb_payload) = build_block( - &mut state, - &ctx, - &mut info, - !disable_state_root || ctx.attributes().no_tx_pool, // need to calculate state root for CL sync - )?; - - self.payload_tx - .send(payload.clone()) + // We should always calculate state root for fallback payload + let (fallback_payload, fb_payload) = build_block(&mut state, &ctx, &mut info, true)?; + self.built_fb_payload_tx + .send(fallback_payload.clone()) .await .map_err(PayloadBuilderError::other)?; - best_payload.set(payload); + let mut best_payload = fallback_payload.clone(); info!( target: "payload_builder", @@ -533,6 +534,15 @@ where let _entered = fb_span.enter(); if ctx.flashblock_index() > ctx.target_flashblock_count() { + self.resolve_best_payload( + &mut state, + &ctx, + &mut info, + best_payload, + fallback_payload, + &resolve_payload, + ) + .await; self.record_flashblocks_metrics( &ctx, &info, @@ -552,13 +562,22 @@ where &state_provider, &mut best_txs, &block_cancel, - &best_payload, + &mut best_payload, &fb_span, ) .await { Ok(Some(next_flashblocks_ctx)) => next_flashblocks_ctx, Ok(None) => { + self.resolve_best_payload( + &mut state, + &ctx, + &mut info, + best_payload, + fallback_payload, + &resolve_payload, + ) + .await; self.record_flashblocks_metrics( &ctx, &info, @@ -585,6 +604,15 @@ where ctx = ctx.with_cancel(fb_cancel).with_extra_ctx(next_flashblocks_ctx); }, _ = block_cancel.cancelled() => { + self.resolve_best_payload( + &mut state, + &ctx, + &mut info, + best_payload, + fallback_payload, + &resolve_payload, + ) + .await; self.record_flashblocks_metrics( &ctx, &info, @@ -610,7 +638,7 @@ where state_provider: impl reth::providers::StateProvider + Clone, best_txs: &mut NextBestFlashblocksTxs, block_cancel: &CancellationToken, - best_payload: &BlockCell, + best_payload: &mut OpBuiltPayload, span: &tracing::Span, ) -> eyre::Result> { let flashblock_index = ctx.flashblock_index(); @@ -753,11 +781,11 @@ where .ws_pub .publish(&fb_payload) .wrap_err("failed to publish flashblock via websocket")?; - self.payload_tx + self.built_fb_payload_tx .send(new_payload.clone()) .await .wrap_err("failed to send built payload to handler")?; - best_payload.set(new_payload); + *best_payload = new_payload; // Record flashblock build duration ctx.metrics @@ -802,6 +830,84 @@ where } } + async fn resolve_best_payload< + DB: Database + std::fmt::Debug + AsRef

, + P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, + >( + &self, + state: &mut State, + ctx: &OpPayloadBuilderCtx, + info: &mut ExecutionInfo, + best_payload: OpBuiltPayload, + fallback_payload: OpBuiltPayload, + resolve_payload: &BlockCell, + ) { + if resolve_payload.get().is_some() { + return; + } + + let payload = match best_payload.block().header().state_root { + B256::ZERO => { + info!(target: "payload_builder", "Resolving payload with zero state root"); + self.resolve_zero_state_root(state, ctx, info, best_payload, fallback_payload) + .await + } + _ => best_payload, + }; + resolve_payload.set(payload); + } + + async fn resolve_zero_state_root< + DB: Database + std::fmt::Debug + AsRef

, + P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, + >( + &self, + state: &mut State, + ctx: &OpPayloadBuilderCtx, + info: &ExecutionInfo, + best_payload: OpBuiltPayload, + fallback_payload: OpBuiltPayload, + ) -> OpBuiltPayload { + let Ok((state_root, trie_updates)) = calculate_state_root_only(state, ctx, info) else { + // This throws away all previously built fb payloads that has already been broadcasted + // and is not ideal. The state root calculation should be bulletproof and not fail + // under normal circumstances. + return fallback_payload; + }; + + let payload_id = best_payload.id(); + let fees = best_payload.fees(); + let executed_block = best_payload + .executed_block() + .map(|executed_block| ExecutedBlock { + recovered_block: Arc::new(executed_block.recovered_block().clone()), + execution_output: Arc::new(executed_block.execution_outcome().clone()), + hashed_state: Arc::new(executed_block.hashed_state().clone()), + trie_updates: Arc::new(trie_updates), + }); + let block = best_payload.into_sealed_block().into_block(); + let (mut header, body) = block.split(); + header.state_root = state_root; + let updated_block = alloy_consensus::Block::::new(header, body); + let sealed_block = Arc::new(updated_block.seal_slow()); + + let updated_payload = OpBuiltPayload::new(payload_id, sealed_block, fees, executed_block); + if let Err(e) = self.built_fb_payload_tx.send(updated_payload.clone()).await { + warn!( + target: "payload_builder", + error = %e, + "Failed to send updated payload" + ); + } + debug!( + target: "payload_builder", + state_root = %state_root, + "Updated payload with calculated state root" + ); + + updated_payload + } + /// Do some logging and metric recording when we stop build flashblocks fn record_flashblocks_metrics( &self, @@ -1210,3 +1316,47 @@ where fb_payload, )) } + +/// Calculates only the state root for an existing payload +fn calculate_state_root_only( + state: &mut State, + ctx: &OpPayloadBuilderCtx, + info: &ExecutionInfo, +) -> Result<(B256, TrieUpdates), PayloadBuilderError> +where + DB: Database + AsRef

, + P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, + ExtraCtx: std::fmt::Debug + Default, +{ + let state_root_start_time = Instant::now(); + let execution_outcome = ExecutionOutcome::new( + state.bundle_state.clone(), + vec![info.receipts.clone()], + ctx.block_number(), + vec![], + ); + + let state_provider = state.database.as_ref(); + let hashed_state = state_provider.hashed_post_state(execution_outcome.state()); + let state_root_updates = state + .database + .as_ref() + .state_root_with_updates(hashed_state) + .inspect_err(|err| { + warn!(target: "payload_builder", + parent_header=%ctx.parent().hash(), + %err, + "failed to calculate state root for payload" + ); + })?; + + let state_root_calculation_time = state_root_start_time.elapsed(); + ctx.metrics + .state_root_calculation_duration + .record(state_root_calculation_time); + ctx.metrics + .state_root_calculation_gauge + .set(state_root_calculation_time); + + Ok(state_root_updates) +} diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs index 96b6f683..91657184 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs @@ -29,8 +29,10 @@ use tracing::warn; /// In the case of a payload built by this node, it is broadcast to peers and an event is sent to the payload builder. /// In the case of a payload received from a peer, it is executed and if successful, an event is sent to the payload builder. pub(crate) struct PayloadHandler { - // receives new payloads built by this builder. - built_rx: mpsc::Receiver, + // receives new flashblock payloads built by this builder. + built_fb_payload_rx: mpsc::Receiver, + // receives new full block payloads built by this builder. + built_payload_rx: mpsc::Receiver, // receives incoming p2p messages from peers. p2p_rx: mpsc::Receiver, // outgoing p2p channel to broadcast new payloads to peers. @@ -50,7 +52,8 @@ where { #[allow(clippy::too_many_arguments)] pub(crate) fn new( - built_rx: mpsc::Receiver, + built_fb_payload_rx: mpsc::Receiver, + built_payload_rx: mpsc::Receiver, p2p_rx: mpsc::Receiver, p2p_tx: mpsc::Sender, payload_events_handle: tokio::sync::broadcast::Sender>, @@ -59,7 +62,8 @@ where cancel: tokio_util::sync::CancellationToken, ) -> Self { Self { - built_rx, + built_fb_payload_rx, + built_payload_rx, p2p_rx, p2p_tx, payload_events_handle, @@ -71,7 +75,8 @@ where pub(crate) async fn run(self) { let Self { - mut built_rx, + mut built_fb_payload_rx, + mut built_payload_rx, mut p2p_rx, p2p_tx, payload_events_handle, @@ -84,12 +89,15 @@ where loop { tokio::select! { - Some(payload) = built_rx.recv() => { + Some(payload) = built_fb_payload_rx.recv() => { + // ignore error here; if p2p was disabled, the channel will be closed. + let _ = p2p_tx.send(payload.into()).await; + } + Some(payload) = built_payload_rx.recv() => { + // Update engine tree state with locally built block payloads if let Err(e) = payload_events_handle.send(Events::BuiltPayload(payload.clone())) { warn!(e = ?e, "failed to send BuiltPayload event"); } - // ignore error here; if p2p was disabled, the channel will be closed. - let _ = p2p_tx.send(payload.into()).await; } Some(message) = p2p_rx.recv() => { match message { diff --git a/crates/op-rbuilder/src/builders/flashblocks/service.rs b/crates/op-rbuilder/src/builders/flashblocks/service.rs index 2c1e684b..568a10b0 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/service.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/service.rs @@ -106,6 +106,9 @@ impl FlashblocksServiceBuilder { }; let metrics = Arc::new(OpRBuilderMetrics::default()); + // Channels for built flashblock payloads + let (built_fb_payload_tx, built_fb_payload_rx) = tokio::sync::mpsc::channel(16); + // Channels for built full block payloads let (built_payload_tx, built_payload_rx) = tokio::sync::mpsc::channel(16); let ws_pub: Arc = @@ -118,6 +121,7 @@ impl FlashblocksServiceBuilder { ctx.provider().clone(), self.0.clone(), builder_tx, + built_fb_payload_tx, built_payload_tx, ws_pub.clone(), metrics.clone(), @@ -145,6 +149,7 @@ impl FlashblocksServiceBuilder { .wrap_err("failed to create flashblocks payload builder context")?; let payload_handler = PayloadHandler::new( + built_fb_payload_rx, built_payload_rx, incoming_message_rx, outgoing_message_tx, From 9d94637aeed0e20bf770a5c68699c02044187ea3 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 21:27:20 +0800 Subject: [PATCH 2/8] Fix --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 1d15a689..eada841a 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -892,7 +892,7 @@ where let sealed_block = Arc::new(updated_block.seal_slow()); let updated_payload = OpBuiltPayload::new(payload_id, sealed_block, fees, executed_block); - if let Err(e) = self.built_fb_payload_tx.send(updated_payload.clone()).await { + if let Err(e) = self.built_payload_tx.send(updated_payload.clone()).await { warn!( target: "payload_builder", error = %e, From 48ed2f319760584dfe08b7147f9f7d07cd41fc0a Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 23:13:27 +0800 Subject: [PATCH 3/8] Fix --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index eada841a..6c51149b 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -868,7 +868,8 @@ where best_payload: OpBuiltPayload, fallback_payload: OpBuiltPayload, ) -> OpBuiltPayload { - let Ok((state_root, trie_updates)) = calculate_state_root_only(state, ctx, info) else { + let Ok((state_root, trie_updates)) = calculate_state_root_on_resolve(state, ctx, info) + else { // This throws away all previously built fb payloads that has already been broadcasted // and is not ideal. The state root calculation should be bulletproof and not fail // under normal circumstances. @@ -1318,7 +1319,7 @@ where } /// Calculates only the state root for an existing payload -fn calculate_state_root_only( +fn calculate_state_root_on_resolve( state: &mut State, ctx: &OpPayloadBuilderCtx, info: &ExecutionInfo, @@ -1328,6 +1329,10 @@ where P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, ExtraCtx: std::fmt::Debug + Default, { + // Merge transitions to populate the bundle_state with all accumulated + // transition states. + state.merge_transitions(BundleRetention::Reverts); + let state_root_start_time = Instant::now(); let execution_outcome = ExecutionOutcome::new( state.bundle_state.clone(), From 701a46286a11d2545b7d840d21005650c48e57ab Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 08:29:58 +0800 Subject: [PATCH 4/8] Fix --- .../src/builders/flashblocks/payload.rs | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 6c51149b..2a44440f 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -30,7 +30,6 @@ use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; use reth_optimism_forks::OpHardforks; use reth_optimism_node::{OpBuiltPayload, OpPayloadBuilderAttributes}; use reth_optimism_primitives::{OpPrimitives, OpReceipt, OpTransactionSigned}; -use reth_payload_primitives::BuiltPayload; use reth_payload_util::BestPayloadTransactions; use reth_primitives_traits::RecoveredBlock; use reth_provider::{ @@ -868,7 +867,8 @@ where best_payload: OpBuiltPayload, fallback_payload: OpBuiltPayload, ) -> OpBuiltPayload { - let Ok((state_root, trie_updates)) = calculate_state_root_on_resolve(state, ctx, info) + let Ok((state_root, trie_updates, execution_outcome, hashed_state)) = + calculate_state_root_on_resolve(state, ctx, info) else { // This throws away all previously built fb payloads that has already been broadcasted // and is not ideal. The state root calculation should be bulletproof and not fail @@ -878,21 +878,21 @@ where let payload_id = best_payload.id(); let fees = best_payload.fees(); - let executed_block = best_payload - .executed_block() - .map(|executed_block| ExecutedBlock { - recovered_block: Arc::new(executed_block.recovered_block().clone()), - execution_output: Arc::new(executed_block.execution_outcome().clone()), - hashed_state: Arc::new(executed_block.hashed_state().clone()), - trie_updates: Arc::new(trie_updates), - }); let block = best_payload.into_sealed_block().into_block(); let (mut header, body) = block.split(); header.state_root = state_root; let updated_block = alloy_consensus::Block::::new(header, body); - let sealed_block = Arc::new(updated_block.seal_slow()); - - let updated_payload = OpBuiltPayload::new(payload_id, sealed_block, fees, executed_block); + let sealed_block = Arc::new(updated_block.clone().seal_slow()); + let recovered_block = + RecoveredBlock::new_unhashed(updated_block, info.executed_senders.clone()); + + let executed: ExecutedBlock = ExecutedBlock { + recovered_block: Arc::new(recovered_block), + execution_output: Arc::new(execution_outcome), + hashed_state: Arc::new(hashed_state), + trie_updates: Arc::new(trie_updates), + }; + let updated_payload = OpBuiltPayload::new(payload_id, sealed_block, fees, Some(executed)); if let Err(e) = self.built_payload_tx.send(updated_payload.clone()).await { warn!( target: "payload_builder", @@ -1323,18 +1323,27 @@ fn calculate_state_root_on_resolve( state: &mut State, ctx: &OpPayloadBuilderCtx, info: &ExecutionInfo, -) -> Result<(B256, TrieUpdates), PayloadBuilderError> +) -> Result< + ( + B256, + TrieUpdates, + ExecutionOutcome, + HashedPostState, + ), + PayloadBuilderError, +> where DB: Database + AsRef

, P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, ExtraCtx: std::fmt::Debug + Default, { - // Merge transitions to populate the bundle_state with all accumulated - // transition states. + // Merge transitions to get the complete state. Note that build_block() + // restores transition_state after each flashblock, keeping all transitions + // available for this final merge state.merge_transitions(BundleRetention::Reverts); let state_root_start_time = Instant::now(); - let execution_outcome = ExecutionOutcome::new( + let execution_outcome: ExecutionOutcome = ExecutionOutcome::new( state.bundle_state.clone(), vec![info.receipts.clone()], ctx.block_number(), @@ -1346,7 +1355,7 @@ where let state_root_updates = state .database .as_ref() - .state_root_with_updates(hashed_state) + .state_root_with_updates(hashed_state.clone()) .inspect_err(|err| { warn!(target: "payload_builder", parent_header=%ctx.parent().hash(), @@ -1363,5 +1372,10 @@ where .state_root_calculation_gauge .set(state_root_calculation_time); - Ok(state_root_updates) + Ok(( + state_root_updates.0, + state_root_updates.1, + execution_outcome, + hashed_state, + )) } From 33c89596ee330efcb7a89b40c5b6a6a6b6783406 Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 11:42:42 +0800 Subject: [PATCH 5/8] Fix state mismatch --- .../src/builders/flashblocks/payload.rs | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 2a44440f..907c7eb6 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -30,6 +30,7 @@ use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; use reth_optimism_forks::OpHardforks; use reth_optimism_node::{OpBuiltPayload, OpPayloadBuilderAttributes}; use reth_optimism_primitives::{OpPrimitives, OpReceipt, OpTransactionSigned}; +use reth_payload_primitives::BuiltPayload; use reth_payload_util::BestPayloadTransactions; use reth_primitives_traits::RecoveredBlock; use reth_provider::{ @@ -72,6 +73,9 @@ type NextBestFlashblocksTxs = BestFlashblocksTxs< pub(super) struct FlashblocksExecutionInfo { /// Index of the last consumed flashblock last_flashblock_index: usize, + /// Snapshot of bundle_state from the last successful build_block call. + /// Used for state root calculation when resolving payload on cancellation. + last_built_bundle_state: Option, } #[derive(Debug, Default, Clone)] @@ -848,8 +852,16 @@ where let payload = match best_payload.block().header().state_root { B256::ZERO => { info!(target: "payload_builder", "Resolving payload with zero state root"); - self.resolve_zero_state_root(state, ctx, info, best_payload, fallback_payload) + self.resolve_zero_state_root(state, ctx, info, best_payload) .await + .unwrap_or_else(|err| { + warn!( + target: "payload_builder", + error = %err, + "Failed to calculate state root, falling back to fallback payload" + ); + fallback_payload + }) } _ => best_payload, }; @@ -865,19 +877,24 @@ where ctx: &OpPayloadBuilderCtx, info: &ExecutionInfo, best_payload: OpBuiltPayload, - fallback_payload: OpBuiltPayload, - ) -> OpBuiltPayload { - let Ok((state_root, trie_updates, execution_outcome, hashed_state)) = - calculate_state_root_on_resolve(state, ctx, info) - else { - // This throws away all previously built fb payloads that has already been broadcasted - // and is not ideal. The state root calculation should be bulletproof and not fail - // under normal circumstances. - return fallback_payload; - }; + ) -> Result { + let (state_root, trie_updates, hashed_state) = + calculate_state_root_on_resolve(state, ctx, info)?; let payload_id = best_payload.id(); let fees = best_payload.fees(); + let execution_outcome = best_payload + .executed_block() + .ok_or_else(|| { + PayloadBuilderError::Other( + eyre::eyre!( + "No executed block available in best payload for payload resolution" + ) + .into(), + ) + })? + .execution_output + .clone(); let block = best_payload.into_sealed_block().into_block(); let (mut header, body) = block.split(); header.state_root = state_root; @@ -888,7 +905,7 @@ where let executed: ExecutedBlock = ExecutedBlock { recovered_block: Arc::new(recovered_block), - execution_output: Arc::new(execution_outcome), + execution_output: execution_outcome.clone(), hashed_state: Arc::new(hashed_state), trie_updates: Arc::new(trie_updates), }; @@ -906,7 +923,7 @@ where "Updated payload with calculated state root" ); - updated_payload + Ok(updated_payload) } /// Do some logging and metric recording when we stop build flashblocks @@ -1067,6 +1084,9 @@ where let untouched_transition_state = state.transition_state.clone(); let state_merge_start_time = Instant::now(); state.merge_transitions(BundleRetention::Reverts); + // Save a snapshot of the bundle_state for state root calculation on payload resolution + info.extra.last_built_bundle_state = Some(state.bundle_state.clone()); + let state_transition_merge_time = state_merge_start_time.elapsed(); ctx.metrics .state_transition_merge_duration @@ -1323,35 +1343,20 @@ fn calculate_state_root_on_resolve( state: &mut State, ctx: &OpPayloadBuilderCtx, info: &ExecutionInfo, -) -> Result< - ( - B256, - TrieUpdates, - ExecutionOutcome, - HashedPostState, - ), - PayloadBuilderError, -> +) -> Result<(B256, TrieUpdates, HashedPostState), PayloadBuilderError> where DB: Database + AsRef

, P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, ExtraCtx: std::fmt::Debug + Default, { - // Merge transitions to get the complete state. Note that build_block() - // restores transition_state after each flashblock, keeping all transitions - // available for this final merge - state.merge_transitions(BundleRetention::Reverts); - let state_root_start_time = Instant::now(); - let execution_outcome: ExecutionOutcome = ExecutionOutcome::new( - state.bundle_state.clone(), - vec![info.receipts.clone()], - ctx.block_number(), - vec![], - ); - + let bundle_state = info.extra.last_built_bundle_state.as_ref().ok_or_else(|| { + PayloadBuilderError::Other( + eyre::eyre!("No bundle state snapshot available for state root calculation").into(), + ) + })?; let state_provider = state.database.as_ref(); - let hashed_state = state_provider.hashed_post_state(execution_outcome.state()); + let hashed_state = state_provider.hashed_post_state(bundle_state); let state_root_updates = state .database .as_ref() @@ -1372,10 +1377,5 @@ where .state_root_calculation_gauge .set(state_root_calculation_time); - Ok(( - state_root_updates.0, - state_root_updates.1, - execution_outcome, - hashed_state, - )) + Ok((state_root_updates.0, state_root_updates.1, hashed_state)) } From 54955387d82aa9a41afa129030f1009bc6b134a1 Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 12:05:08 +0800 Subject: [PATCH 6/8] Fix --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 907c7eb6..e99dac35 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -905,7 +905,7 @@ where let executed: ExecutedBlock = ExecutedBlock { recovered_block: Arc::new(recovered_block), - execution_output: execution_outcome.clone(), + execution_output: execution_outcome, hashed_state: Arc::new(hashed_state), trie_updates: Arc::new(trie_updates), }; From 5d2ef89e9a20907ffd0508b08a942bc661b7314b Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 12:42:04 +0800 Subject: [PATCH 7/8] Fix --- .../src/builders/flashblocks/payload.rs | 84 +++++++------------ .../builders/flashblocks/payload_handler.rs | 2 +- 2 files changed, 33 insertions(+), 53 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index e99dac35..0fdb37a6 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -38,7 +38,9 @@ use reth_provider::{ StorageRootProvider, }; use reth_revm::{ - State, database::StateProviderDatabase, db::states::bundle_state::BundleRetention, + State, + database::StateProviderDatabase, + db::{BundleState, states::bundle_state::BundleRetention}, }; use reth_transaction_pool::TransactionPool; use reth_trie::{HashedPostState, updates::TrieUpdates}; @@ -73,9 +75,6 @@ type NextBestFlashblocksTxs = BestFlashblocksTxs< pub(super) struct FlashblocksExecutionInfo { /// Index of the last consumed flashblock last_flashblock_index: usize, - /// Snapshot of bundle_state from the last successful build_block call. - /// Used for state root calculation when resolving payload on cancellation. - last_built_bundle_state: Option, } #[derive(Debug, Default, Clone)] @@ -378,12 +377,13 @@ where info.cumulative_da_bytes_used += builder_tx_da_size; // We should always calculate state root for fallback payload - let (fallback_payload, fb_payload) = build_block(&mut state, &ctx, &mut info, true)?; + let (fallback_payload, fb_payload, bundle_state) = + build_block(&mut state, &ctx, &mut info, true)?; self.built_fb_payload_tx .send(fallback_payload.clone()) .await .map_err(PayloadBuilderError::other)?; - let mut best_payload = fallback_payload.clone(); + let mut best_payload = (fallback_payload.clone(), bundle_state); info!( target: "payload_builder", @@ -540,7 +540,6 @@ where self.resolve_best_payload( &mut state, &ctx, - &mut info, best_payload, fallback_payload, &resolve_payload, @@ -575,7 +574,6 @@ where self.resolve_best_payload( &mut state, &ctx, - &mut info, best_payload, fallback_payload, &resolve_payload, @@ -610,7 +608,6 @@ where self.resolve_best_payload( &mut state, &ctx, - &mut info, best_payload, fallback_payload, &resolve_payload, @@ -641,7 +638,7 @@ where state_provider: impl reth::providers::StateProvider + Clone, best_txs: &mut NextBestFlashblocksTxs, block_cancel: &CancellationToken, - best_payload: &mut OpBuiltPayload, + best_payload: &mut (OpBuiltPayload, BundleState), span: &tracing::Span, ) -> eyre::Result> { let flashblock_index = ctx.flashblock_index(); @@ -764,7 +761,7 @@ where ctx.metrics.invalid_built_blocks_count.increment(1); Err(err).wrap_err("failed to build payload") } - Ok((new_payload, mut fb_payload)) => { + Ok((new_payload, mut fb_payload, bundle_state)) => { fb_payload.index = flashblock_index; fb_payload.base = None; @@ -788,7 +785,7 @@ where .send(new_payload.clone()) .await .wrap_err("failed to send built payload to handler")?; - *best_payload = new_payload; + *best_payload = (new_payload, bundle_state); // Record flashblock build duration ctx.metrics @@ -840,8 +837,7 @@ where &self, state: &mut State, ctx: &OpPayloadBuilderCtx, - info: &mut ExecutionInfo, - best_payload: OpBuiltPayload, + best_payload: (OpBuiltPayload, BundleState), fallback_payload: OpBuiltPayload, resolve_payload: &BlockCell, ) { @@ -849,10 +845,10 @@ where return; } - let payload = match best_payload.block().header().state_root { + let payload = match best_payload.0.block().header().state_root { B256::ZERO => { info!(target: "payload_builder", "Resolving payload with zero state root"); - self.resolve_zero_state_root(state, ctx, info, best_payload) + self.resolve_zero_state_root(state, ctx, best_payload) .await .unwrap_or_else(|err| { warn!( @@ -863,7 +859,7 @@ where fallback_payload }) } - _ => best_payload, + _ => best_payload.0, }; resolve_payload.set(payload); } @@ -875,37 +871,28 @@ where &self, state: &mut State, ctx: &OpPayloadBuilderCtx, - info: &ExecutionInfo, - best_payload: OpBuiltPayload, + best_payload: (OpBuiltPayload, BundleState), ) -> Result { let (state_root, trie_updates, hashed_state) = - calculate_state_root_on_resolve(state, ctx, info)?; - - let payload_id = best_payload.id(); - let fees = best_payload.fees(); - let execution_outcome = best_payload - .executed_block() - .ok_or_else(|| { - PayloadBuilderError::Other( - eyre::eyre!( - "No executed block available in best payload for payload resolution" - ) + calculate_state_root_on_resolve(state, ctx, best_payload.1)?; + + let payload_id = best_payload.0.id(); + let fees = best_payload.0.fees(); + let executed_block = best_payload.0.executed_block().ok_or_else(|| { + PayloadBuilderError::Other( + eyre::eyre!("No executed block available in best payload for payload resolution") .into(), - ) - })? - .execution_output - .clone(); - let block = best_payload.into_sealed_block().into_block(); + ) + })?; + let block = best_payload.0.into_sealed_block().into_block(); let (mut header, body) = block.split(); header.state_root = state_root; let updated_block = alloy_consensus::Block::::new(header, body); - let sealed_block = Arc::new(updated_block.clone().seal_slow()); - let recovered_block = - RecoveredBlock::new_unhashed(updated_block, info.executed_senders.clone()); + let sealed_block = Arc::new(updated_block.seal_slow()); let executed: ExecutedBlock = ExecutedBlock { - recovered_block: Arc::new(recovered_block), - execution_output: execution_outcome, + recovered_block: executed_block.recovered_block.clone(), + execution_output: executed_block.execution_output.clone(), hashed_state: Arc::new(hashed_state), trie_updates: Arc::new(trie_updates), }; @@ -1074,7 +1061,7 @@ pub(super) fn build_block( ctx: &OpPayloadBuilderCtx, info: &mut ExecutionInfo, calculate_state_root: bool, -) -> Result<(OpBuiltPayload, FlashblocksPayloadV1), PayloadBuilderError> +) -> Result<(OpBuiltPayload, FlashblocksPayloadV1, BundleState), PayloadBuilderError> where DB: Database + AsRef

, P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, @@ -1084,9 +1071,6 @@ where let untouched_transition_state = state.transition_state.clone(); let state_merge_start_time = Instant::now(); state.merge_transitions(BundleRetention::Reverts); - // Save a snapshot of the bundle_state for state root calculation on payload resolution - info.extra.last_built_bundle_state = Some(state.bundle_state.clone()); - let state_transition_merge_time = state_merge_start_time.elapsed(); ctx.metrics .state_transition_merge_duration @@ -1324,7 +1308,7 @@ where }; // We clean bundle and place initial state transaction back - state.take_bundle(); + let bundle_state = state.take_bundle(); state.transition_state = untouched_transition_state; Ok(( @@ -1335,6 +1319,7 @@ where Some(executed), ), fb_payload, + bundle_state, )) } @@ -1342,7 +1327,7 @@ where fn calculate_state_root_on_resolve( state: &mut State, ctx: &OpPayloadBuilderCtx, - info: &ExecutionInfo, + bundle_state: BundleState, ) -> Result<(B256, TrieUpdates, HashedPostState), PayloadBuilderError> where DB: Database + AsRef

, @@ -1350,13 +1335,8 @@ where ExtraCtx: std::fmt::Debug + Default, { let state_root_start_time = Instant::now(); - let bundle_state = info.extra.last_built_bundle_state.as_ref().ok_or_else(|| { - PayloadBuilderError::Other( - eyre::eyre!("No bundle state snapshot available for state root calculation").into(), - ) - })?; let state_provider = state.database.as_ref(); - let hashed_state = state_provider.hashed_post_state(bundle_state); + let hashed_state = state_provider.hashed_post_state(&bundle_state); let state_root_updates = state .database .as_ref() diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs index 91657184..ed127778 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs @@ -249,7 +249,7 @@ where cancel, ); - let (built_payload, fb_payload) = crate::builders::flashblocks::payload::build_block( + let (built_payload, fb_payload, _) = crate::builders::flashblocks::payload::build_block( &mut state, &builder_ctx, &mut info, From c5117d96af6ee08e476c089ab1622695c9214f45 Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 14:15:00 +0800 Subject: [PATCH 8/8] Fix --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 0fdb37a6..04c6a576 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -888,10 +888,14 @@ where let (mut header, body) = block.split(); header.state_root = state_root; let updated_block = alloy_consensus::Block::::new(header, body); + let recovered_block = RecoveredBlock::new_unhashed( + updated_block.clone(), + executed_block.recovered_block().senders().to_vec(), + ); let sealed_block = Arc::new(updated_block.seal_slow()); let executed: ExecutedBlock = ExecutedBlock { - recovered_block: executed_block.recovered_block.clone(), + recovered_block: Arc::new(recovered_block), execution_output: executed_block.execution_output.clone(), hashed_state: Arc::new(hashed_state), trie_updates: Arc::new(trie_updates),