From ba5ada037bd4351cec3515517175e008a8fab5f3 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 13:00:27 +0800 Subject: [PATCH 01/14] Add state root calculation on payload resolve --- .../src/builders/flashblocks/payload.rs | 100 ++++++++++++++++-- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 07f1a008..29d53133 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -366,12 +366,7 @@ where ); }; - 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 - )?; + let (payload, fb_payload) = build_block(&mut state, &ctx, &mut info, true)?; self.payload_tx .send(payload.clone()) @@ -581,6 +576,7 @@ 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).await; self.record_flashblocks_metrics( &ctx, &info, @@ -701,6 +697,8 @@ where // We got block cancelled, we won't need anything from the block at this point // Caution: this assume that block cancel token only cancelled when new FCU is received if block_cancel.is_cancelled() { + self.resolve_best_payload(state, ctx, info, best_payload) + .await; self.record_flashblocks_metrics( ctx, info, @@ -824,6 +822,52 @@ 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: &BlockCell, + ) { + if let Some(payload) = best_payload.get() + && payload.block().header().state_root == B256::ZERO + { + // Calculate state root for best payload on resolve payload to prevent + // cache misses for locally built payloads. + if let Ok(state_root) = calculate_state_root_only(state, ctx, info) { + use reth_payload_primitives::BuiltPayload as _; + let payload_id = payload.id(); + let fees = payload.fees(); + let executed_block = payload.executed_block(); + + // Get the current sealed block and extract its components + let block = payload.into_sealed_block().into_block(); + let (mut header, body) = block.split(); + header.state_root = state_root; + + // Create a new sealed block with the updated header + let updated_block = + alloy_consensus::Block::::new(header, body); + let sealed_block = Arc::new(updated_block.seal_slow()); + + // Update the best payload with the calculated staet root + let updated_payload = + OpBuiltPayload::new(payload_id, sealed_block, fees, executed_block); + self.payload_tx.send(updated_payload.clone()).await.ok(); // ignore error here + best_payload.set(updated_payload); + + debug!( + target: "payload_builder", + state_root = %state_root, + "Updated payload with calculated state root" + ); + } + } + } + /// Do some logging and metric recording when we stop build flashblocks fn record_flashblocks_metrics( &self, @@ -1203,3 +1247,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 +where + DB: Database + AsRef

, + P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, + ExtraCtx: std::fmt::Debug + Default, +{ + let state_root_start_time = Instant::now(); + state.merge_transitions(BundleRetention::Reverts); + 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, _trie_output) = 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) +} From f7a393318ca8876ddd4db716cb38d461c3ce1b56 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 13:50:01 +0800 Subject: [PATCH 02/14] Fix --- .../src/builders/flashblocks/payload.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 29d53133..1b0a2bf0 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -837,11 +837,18 @@ where { // Calculate state root for best payload on resolve payload to prevent // cache misses for locally built payloads. - if let Ok(state_root) = calculate_state_root_only(state, ctx, info) { + if let Ok((state_root, trie_updates)) = calculate_state_root_only(state, ctx, info) { use reth_payload_primitives::BuiltPayload as _; let payload_id = payload.id(); let fees = payload.fees(); - let executed_block = payload.executed_block(); + let executed_block = 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), + }); // Get the current sealed block and extract its components let block = payload.into_sealed_block().into_block(); @@ -1253,7 +1260,7 @@ fn calculate_state_root_only( state: &mut State, ctx: &OpPayloadBuilderCtx, info: &ExecutionInfo, -) -> Result +) -> Result<(B256, TrieUpdates), PayloadBuilderError> where DB: Database + AsRef

, P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, @@ -1270,7 +1277,7 @@ where let state_provider = state.database.as_ref(); let hashed_state = state_provider.hashed_post_state(execution_outcome.state()); - let (state_root, _trie_output) = state + let state_root_updates = state .database .as_ref() .state_root_with_updates(hashed_state) @@ -1281,6 +1288,7 @@ where "failed to calculate state root for payload" ); })?; + let state_root_calculation_time = state_root_start_time.elapsed(); ctx.metrics .state_root_calculation_duration @@ -1289,5 +1297,5 @@ where .state_root_calculation_gauge .set(state_root_calculation_time); - Ok(state_root) + Ok(state_root_updates) } From b670c23425e727dae3fe4f9f19a8bfacbfcceb46 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 14:16:29 +0800 Subject: [PATCH 03/14] fix: update executed block to upstream version --- .../src/builders/flashblocks/payload.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 1b0a2bf0..3f914308 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -841,14 +841,15 @@ where use reth_payload_primitives::BuiltPayload as _; let payload_id = payload.id(); let fees = payload.fees(); - let executed_block = 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 executed_block = + payload + .executed_block() + .map(|executed_block| BuiltPayloadExecutedBlock { + recovered_block: executed_block.recovered_block.clone(), + execution_output: executed_block.execution_output.clone(), + hashed_state: executed_block.hashed_state.clone(), + trie_updates: Either::Left(Arc::new(trie_updates)), + }); // Get the current sealed block and extract its components let block = payload.into_sealed_block().into_block(); From 0ea5e3f1b37616b86067ebdc6435218775ebe531 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 14:17:43 +0800 Subject: [PATCH 04/14] Revert to previous logic on building fallback block --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 3f914308..8353b79a 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -366,7 +366,12 @@ where ); }; - let (payload, fb_payload) = build_block(&mut state, &ctx, &mut info, true)?; + 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()) From c3e65b2db591f28453e4f2f8a320015d82399d24 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 14:24:05 +0800 Subject: [PATCH 05/14] Remove merging transitions in statedb --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 8353b79a..bf45ba70 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -1273,7 +1273,6 @@ where ExtraCtx: std::fmt::Debug + Default, { let state_root_start_time = Instant::now(); - state.merge_transitions(BundleRetention::Reverts); let execution_outcome = ExecutionOutcome::new( state.bundle_state.clone(), vec![info.receipts.clone()], From 4e58bdb2650acc24989a8be7a4d73d90eaa0a459 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 14:28:48 +0800 Subject: [PATCH 06/14] Fix triggering resolving payload --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index bf45ba70..728c0941 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -555,6 +555,8 @@ where { Ok(Some(next_flashblocks_ctx)) => next_flashblocks_ctx, Ok(None) => { + self.resolve_best_payload(&mut state, &ctx, &mut info, &best_payload) + .await; self.record_flashblocks_metrics( &ctx, &info, @@ -702,8 +704,6 @@ where // We got block cancelled, we won't need anything from the block at this point // Caution: this assume that block cancel token only cancelled when new FCU is received if block_cancel.is_cancelled() { - self.resolve_best_payload(state, ctx, info, best_payload) - .await; self.record_flashblocks_metrics( ctx, info, From 18111e99298b3f3f86eedf78c73c0e44244baa78 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 21:40:24 +0800 Subject: [PATCH 07/14] Fixes --- .../src/builders/flashblocks/payload.rs | 179 +++++++++++------- .../builders/flashblocks/payload_handler.rs | 24 ++- .../src/builders/flashblocks/service.rs | 5 + 3 files changed, 135 insertions(+), 73 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 728c0941..d4a65589 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -30,7 +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::BuiltPayloadExecutedBlock; +use reth_payload_primitives::{BuiltPayload, BuiltPayloadExecutedBlock}; use reth_payload_util::BestPayloadTransactions; use reth_primitives_traits::RecoveredBlock; use reth_provider::{ @@ -145,9 +145,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, @@ -170,7 +173,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 { @@ -179,7 +183,8 @@ impl OpPayloadBuilder { evm_config, pool, client, - payload_tx, + built_fb_payload_tx, + built_payload_tx, ws_pub, config, metrics, @@ -297,7 +302,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 { @@ -366,18 +371,13 @@ where ); }; - 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", @@ -529,6 +529,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, @@ -548,15 +557,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) - .await; + self.resolve_best_payload( + &mut state, + &ctx, + &mut info, + best_payload, + fallback_payload, + &resolve_payload, + ) + .await; self.record_flashblocks_metrics( &ctx, &info, @@ -583,7 +599,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).await; + self.resolve_best_payload( + &mut state, + &ctx, + &mut info, + best_payload, + fallback_payload, + &resolve_payload, + ) + .await; self.record_flashblocks_metrics( &ctx, &info, @@ -609,7 +633,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(); @@ -769,11 +793,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 @@ -835,50 +859,75 @@ where state: &mut State, ctx: &OpPayloadBuilderCtx, info: &mut ExecutionInfo, - best_payload: &BlockCell, + best_payload: OpBuiltPayload, + fallback_payload: OpBuiltPayload, + resolve_payload: &BlockCell, ) { - if let Some(payload) = best_payload.get() - && payload.block().header().state_root == B256::ZERO - { - // Calculate state root for best payload on resolve payload to prevent - // cache misses for locally built payloads. - if let Ok((state_root, trie_updates)) = calculate_state_root_only(state, ctx, info) { - use reth_payload_primitives::BuiltPayload as _; - let payload_id = payload.id(); - let fees = payload.fees(); - let executed_block = - payload - .executed_block() - .map(|executed_block| BuiltPayloadExecutedBlock { - recovered_block: executed_block.recovered_block.clone(), - execution_output: executed_block.execution_output.clone(), - hashed_state: executed_block.hashed_state.clone(), - trie_updates: Either::Left(Arc::new(trie_updates)), - }); - - // Get the current sealed block and extract its components - let block = payload.into_sealed_block().into_block(); - let (mut header, body) = block.split(); - header.state_root = state_root; - - // Create a new sealed block with the updated header - let updated_block = - alloy_consensus::Block::::new(header, body); - let sealed_block = Arc::new(updated_block.seal_slow()); - - // Update the best payload with the calculated staet root - let updated_payload = - OpBuiltPayload::new(payload_id, sealed_block, fees, executed_block); - self.payload_tx.send(updated_payload.clone()).await.ok(); // ignore error here - best_payload.set(updated_payload); - - debug!( - target: "payload_builder", - state_root = %state_root, - "Updated payload with calculated state root" - ); + 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| BuiltPayloadExecutedBlock { + recovered_block: executed_block.recovered_block.clone(), + execution_output: executed_block.execution_output.clone(), + hashed_state: executed_block.hashed_state.clone(), + trie_updates: Either::Left(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_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 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 51b161ffa52b194a778b32b05a76114ff326f0f9 Mon Sep 17 00:00:00 2001 From: Niven Date: Thu, 27 Nov 2025 23:13:27 +0800 Subject: [PATCH 08/14] 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 d4a65589..4319cc08 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -889,7 +889,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. @@ -1311,7 +1312,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, @@ -1321,6 +1322,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 042b2f90add56942dd06de61cec067e8e261b9d0 Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 08:29:58 +0800 Subject: [PATCH 09/14] Fix --- .../src/builders/flashblocks/payload.rs | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 4319cc08..4c34c901 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -30,7 +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, BuiltPayloadExecutedBlock}; +use reth_payload_primitives::BuiltPayloadExecutedBlock; use reth_payload_util::BestPayloadTransactions; use reth_primitives_traits::RecoveredBlock; use reth_provider::{ @@ -889,7 +889,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 @@ -899,22 +900,21 @@ where let payload_id = best_payload.id(); let fees = best_payload.fees(); - let executed_block = - best_payload - .executed_block() - .map(|executed_block| BuiltPayloadExecutedBlock { - recovered_block: executed_block.recovered_block.clone(), - execution_output: executed_block.execution_output.clone(), - hashed_state: executed_block.hashed_state.clone(), - trie_updates: Either::Left(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 = BuiltPayloadExecutedBlock { + recovered_block: Arc::new(recovered_block), + execution_output: Arc::new(execution_outcome), + hashed_state: Either::Left(Arc::new(hashed_state)), + trie_updates: Either::Left(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", @@ -1316,18 +1316,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(), @@ -1339,7 +1348,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(), @@ -1356,5 +1365,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 edbc1131a8ba62e4f80da7b5b5103c0402f28636 Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 11:42:42 +0800 Subject: [PATCH 10/14] Fix state mismatch --- .../src/builders/flashblocks/payload.rs | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 4c34c901..2e9beee5 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -30,7 +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::BuiltPayloadExecutedBlock; +use reth_payload_primitives::{BuiltPayload, BuiltPayloadExecutedBlock}; use reth_payload_util::BestPayloadTransactions; use reth_primitives_traits::RecoveredBlock; use reth_provider::{ @@ -73,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)] @@ -870,8 +873,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, }; @@ -887,19 +898,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; @@ -910,7 +926,7 @@ where let executed = BuiltPayloadExecutedBlock { recovered_block: Arc::new(recovered_block), - execution_output: Arc::new(execution_outcome), + execution_output: execution_outcome, hashed_state: Either::Left(Arc::new(hashed_state)), trie_updates: Either::Left(Arc::new(trie_updates)), }; @@ -928,7 +944,7 @@ where "Updated payload with calculated state root" ); - updated_payload + Ok(updated_payload) } /// Do some logging and metric recording when we stop build flashblocks @@ -1089,6 +1105,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 @@ -1316,35 +1335,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() @@ -1365,10 +1369,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 5f82b81bad8f2a2ab5c04452258fdd41cfe1482a Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 12:42:04 +0800 Subject: [PATCH 11/14] 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 2e9beee5..7feff626 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)] @@ -375,12 +374,13 @@ where }; // 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", @@ -535,7 +535,6 @@ where self.resolve_best_payload( &mut state, &ctx, - &mut info, best_payload, fallback_payload, &resolve_payload, @@ -570,7 +569,6 @@ where self.resolve_best_payload( &mut state, &ctx, - &mut info, best_payload, fallback_payload, &resolve_payload, @@ -605,7 +603,6 @@ where self.resolve_best_payload( &mut state, &ctx, - &mut info, best_payload, fallback_payload, &resolve_payload, @@ -636,7 +633,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(); @@ -776,7 +773,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; @@ -800,7 +797,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 @@ -861,8 +858,7 @@ where &self, state: &mut State, ctx: &OpPayloadBuilderCtx, - info: &mut ExecutionInfo, - best_payload: OpBuiltPayload, + best_payload: (OpBuiltPayload, BundleState), fallback_payload: OpBuiltPayload, resolve_payload: &BlockCell, ) { @@ -870,10 +866,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!( @@ -884,7 +880,7 @@ where fallback_payload }) } - _ => best_payload, + _ => best_payload.0, }; resolve_payload.set(payload); } @@ -896,37 +892,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 = BuiltPayloadExecutedBlock { - 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: Either::Left(Arc::new(hashed_state)), trie_updates: Either::Left(Arc::new(trie_updates)), }; @@ -1095,7 +1082,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, @@ -1105,9 +1092,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 @@ -1316,7 +1300,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(( @@ -1327,6 +1311,7 @@ where Some(executed), ), fb_payload, + bundle_state, )) } @@ -1334,7 +1319,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

, @@ -1342,13 +1327,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 2d53279b269fac2bde857cb857b122e9ebb6c202 Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 28 Nov 2025 14:15:00 +0800 Subject: [PATCH 12/14] 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 7feff626..1e15e7f6 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -909,10 +909,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 = BuiltPayloadExecutedBlock { - recovered_block: executed_block.recovered_block.clone(), + recovered_block: Arc::new(recovered_block), execution_output: executed_block.execution_output.clone(), hashed_state: Either::Left(Arc::new(hashed_state)), trie_updates: Either::Left(Arc::new(trie_updates)), From 73f16eeae750fe50c757d6ef6c1ca040de4f0f8a Mon Sep 17 00:00:00 2001 From: Niven Date: Tue, 16 Dec 2025 16:47:08 +0800 Subject: [PATCH 13/14] Fix resolving payload on no tx pool flag (#45) --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 04ae5973..7ecd643e 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -439,6 +439,14 @@ where .set(info.executed_transactions.len() as f64); // return early since we don't need to build a block with transactions from the pool + self.resolve_best_payload( + &mut state, + &ctx, + best_payload, + fallback_payload, + &resolve_payload, + ) + .await; return Ok(()); } // We adjust our flashblocks timings based on time_drift if dynamic adjustment enable From 148c8f59b460b26ef3c52ec068797dc743d1e148 Mon Sep 17 00:00:00 2001 From: Niven Date: Tue, 16 Dec 2025 17:09:05 +0800 Subject: [PATCH 14/14] Resolve on error (#46) --- crates/op-rbuilder/src/builders/flashblocks/payload.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 7ecd643e..59a4672c 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -618,6 +618,14 @@ where ctx.block_number(), err ); + self.resolve_best_payload( + &mut state, + &ctx, + best_payload, + fallback_payload, + &resolve_payload, + ) + .await; return Err(PayloadBuilderError::Other(err.into())); } };