From 0b958e75ef45e1619761a8046533cf6b16e39615 Mon Sep 17 00:00:00 2001 From: Niven Date: Fri, 16 Jan 2026 16:27:23 +0800 Subject: [PATCH] Add async state root calculation --- crates/op-rbuilder/src/args/op.rs | 8 + crates/op-rbuilder/src/builders/context.rs | 2 +- .../src/builders/flashblocks/config.rs | 9 + .../src/builders/flashblocks/payload.rs | 254 ++++++++++-------- .../src/builders/flashblocks/service.rs | 1 + 5 files changed, 163 insertions(+), 111 deletions(-) diff --git a/crates/op-rbuilder/src/args/op.rs b/crates/op-rbuilder/src/args/op.rs index c5136854e..8a67ed325 100644 --- a/crates/op-rbuilder/src/args/op.rs +++ b/crates/op-rbuilder/src/args/op.rs @@ -167,6 +167,14 @@ pub struct FlashblocksArgs { )] pub flashblocks_disable_rollup_boost: bool, + /// Whether to disable async state root calculation on full payload resolution + #[arg( + long = "flashblocks.disable-async-calculate-state-root", + default_value = "false", + env = "FLASHBLOCKS_DISABLE_ASYNC_CALCULATE_STATE_ROOT" + )] + pub flashblocks_disable_async_calculate_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/builders/context.rs b/crates/op-rbuilder/src/builders/context.rs index 0aee26162..e2b9ef391 100644 --- a/crates/op-rbuilder/src/builders/context.rs +++ b/crates/op-rbuilder/src/builders/context.rs @@ -48,7 +48,7 @@ use crate::{ }; /// Container type that holds all necessities to build a new payload. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct OpPayloadBuilderCtx { /// The type that knows how to perform system calls and configure the evm. pub evm_config: OpEvmConfig, diff --git a/crates/op-rbuilder/src/builders/flashblocks/config.rs b/crates/op-rbuilder/src/builders/flashblocks/config.rs index 23254b66f..c9778695f 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/config.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/config.rs @@ -37,6 +37,9 @@ pub struct FlashblocksConfig { /// Should we disable running builder in rollup boost mode pub disable_rollup_boost: bool, + /// Should we disable async state root calculation on full payload resolution + pub disable_async_calculate_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. @@ -87,6 +90,7 @@ impl Default for FlashblocksConfig { fixed: false, disable_state_root: false, disable_rollup_boost: false, + disable_async_calculate_state_root: false, number_contract_address: None, number_contract_use_permit: false, build_at_interval_end: false, @@ -122,6 +126,10 @@ impl TryFrom for FlashblocksConfig { let disable_rollup_boost = args.flashblocks.flashblocks_disable_rollup_boost; + let disable_async_calculate_state_root = args + .flashblocks + .flashblocks_disable_async_calculate_state_root; + let number_contract_address = args.flashblocks.flashblocks_number_contract_address; let number_contract_use_permit = args.flashblocks.flashblocks_number_contract_use_permit; @@ -133,6 +141,7 @@ impl TryFrom for FlashblocksConfig { fixed, disable_state_root, disable_rollup_boost, + disable_async_calculate_state_root, number_contract_address, number_contract_use_permit, build_at_interval_end: args.flashblocks.flashblocks_build_at_interval_end, diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 32f1a46bc..43c51516b 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -16,14 +16,14 @@ use alloy_consensus::{ BlockBody, EMPTY_OMMER_ROOT_HASH, Header, constants::EMPTY_WITHDRAWALS, proofs, }; use alloy_eips::{Encodable2718, eip7685::EMPTY_REQUESTS_HASH, merge::BEACON_NONCE}; -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{Address, B256, BlockHash, U256}; use core::time::Duration; use eyre::WrapErr as _; use op_alloy_rpc_types_engine::{ OpFlashblockPayload, OpFlashblockPayloadBase, OpFlashblockPayloadDelta, OpFlashblockPayloadMetadata, }; -use reth::payload::PayloadBuilderAttributes; +use reth::{payload::PayloadBuilderAttributes, tasks::TaskSpawner}; use reth_basic_payload_builder::BuildOutcome; use reth_chain_state::ExecutedBlock; use reth_chainspec::EthChainSpec; @@ -33,7 +33,7 @@ use reth_optimism_consensus::{calculate_receipt_root_no_memo_optimism, isthmus}; 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_optimism_primitives::{OpReceipt, OpTransactionSigned}; use reth_payload_primitives::BuiltPayload; use reth_payload_util::BestPayloadTransactions; @@ -56,7 +56,7 @@ use std::{ sync::Arc, time::Instant, }; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, oneshot}; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, metadata::Level, span, warn}; @@ -173,13 +173,15 @@ impl OpPayloadBuilderCtx { /// Optimism's payload builder #[derive(Debug, Clone)] -pub(super) struct OpPayloadBuilder { +pub(super) struct OpPayloadBuilder { /// The type responsible for creating the evm. pub evm_config: OpEvmConfig, /// The transaction pool pub pool: Pool, /// Node client pub client: Client, + /// Task executor + pub task_executor: Tasks, /// Sender for sending built flashblock payloads to [`PayloadHandler`], /// which broadcasts outgoing flashblock payloads via p2p. pub built_fb_payload_tx: mpsc::Sender, @@ -199,13 +201,14 @@ pub(super) struct OpPayloadBuilder { pub address_gas_limiter: AddressGasLimiter, } -impl OpPayloadBuilder { +impl OpPayloadBuilder { /// `OpPayloadBuilder` constructor. #[allow(clippy::too_many_arguments)] pub(super) fn new( evm_config: OpEvmConfig, pool: Pool, client: Client, + task_executor: Tasks, config: BuilderConfig, builder_tx: BuilderTx, built_fb_payload_tx: mpsc::Sender, @@ -218,6 +221,7 @@ impl OpPayloadBuilder { evm_config, pool, client, + task_executor, built_fb_payload_tx, built_payload_tx, ws_pub, @@ -229,12 +233,13 @@ impl OpPayloadBuilder { } } -impl reth_basic_payload_builder::PayloadBuilder - for OpPayloadBuilder +impl reth_basic_payload_builder::PayloadBuilder + for OpPayloadBuilder where Pool: Clone + Send + Sync, Client: Clone + Send + Sync, BuilderTx: Clone + Send + Sync, + Tasks: Clone + Send + Sync, { type Attributes = OpPayloadBuilderAttributes; type BuiltPayload = OpBuiltPayload; @@ -257,11 +262,12 @@ where } } -impl OpPayloadBuilder +impl OpPayloadBuilder where Pool: PoolBounds, Client: ClientBounds, BuilderTx: BuilderTransactions + Send + Sync, + Tasks: TaskSpawner + Clone + Unpin + 'static, { fn get_op_payload_builder_ctx( &self, @@ -453,14 +459,8 @@ 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; + self.resolve_best_payload(&ctx, best_payload, fallback_payload, &resolve_payload) + .await; return Ok(()); } // We adjust our flashblocks timings based on time the fcu block building signal arrived @@ -619,7 +619,6 @@ where }, _ = block_cancel.cancelled() => { self.resolve_best_payload( - &mut state, &ctx, best_payload, fallback_payload, @@ -649,14 +648,8 @@ where let _entered = fb_span.enter(); if ctx.flashblock_index() > ctx.target_flashblock_count() { - self.resolve_best_payload( - &mut state, - &ctx, - best_payload, - fallback_payload, - &resolve_payload, - ) - .await; + self.resolve_best_payload(&ctx, best_payload, fallback_payload, &resolve_payload) + .await; self.record_flashblocks_metrics( &ctx, &info, @@ -684,7 +677,6 @@ where Ok(Some(next_flashblocks_ctx)) => next_flashblocks_ctx, Ok(None) => { self.resolve_best_payload( - &mut state, &ctx, best_payload, fallback_payload, @@ -709,7 +701,6 @@ where err ); self.resolve_best_payload( - &mut state, &ctx, best_payload, fallback_payload, @@ -954,12 +945,8 @@ where } } - async fn resolve_best_payload< - DB: Database + std::fmt::Debug + AsRef

, - P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, - >( + async fn resolve_best_payload( &self, - state: &mut State, ctx: &OpPayloadBuilderCtx, best_payload: (OpBuiltPayload, BundleState), fallback_payload: OpBuiltPayload, @@ -971,76 +958,73 @@ where 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, best_payload) - .await - .unwrap_or_else(|err| { + let (tx, rx) = oneshot::channel(); + + // Get the fallback payload for payload resolution. Note that if async state root calculation is + // enabled, we use the fallback payload as the best payload contains empty state root since zero + // state root payloads are acceptable. + let fallback_payload_for_resolve = + if self.config.specific.disable_async_calculate_state_root { + // If async state root calculation is disabled, we use the fallback payload with state + // root calcuated to ensure the full payload is valid + fallback_payload.clone() + } else { + best_payload.0.clone() + }; + + let state_root_ctx = CalculateStateRootContext { + best_payload, + parent_hash: ctx.parent().hash(), + built_payload_tx: self.built_payload_tx.clone(), + metrics: self.metrics.clone(), + }; + + // Async calculate state root + match self.client.state_by_block_hash(ctx.parent().hash()) { + Ok(state_provider) => { + self.task_executor.spawn(Box::pin(async move { + let payload_with_state_root = resolve_zero_state_root(state_root_ctx, state_provider).await.unwrap_or_else(|err| { + warn!( + target: "payload_builder", + error = %err, + "Failed to calculate state root, falling back to fallback payload" + ); + // Use the fallback payload with state root calculated on failures as a safety + // net so that the full built payload is assured to be a valid. + fallback_payload + }); + + // Send the payload if aync SR calculation is disabled + let _ = tx.send(payload_with_state_root); + })); + + if self.config.specific.disable_async_calculate_state_root { + rx.await.unwrap_or_else(|_| { + warn!( + target: "payload_builder", + "Failed to calculate state root, state root task stopped. falling back to fallback payload" + ); + fallback_payload_for_resolve + }) + } else { + fallback_payload_for_resolve + } + } + Err(err) => { warn!( target: "payload_builder", error = %err, - "Failed to calculate state root, falling back to fallback payload" + "Failed to get state provider for parent block for SR calculation. falling back to fallback payload" ); - fallback_payload - }) + fallback_payload_for_resolve + } + } } _ => best_payload.0, }; 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, - best_payload: (OpBuiltPayload, BundleState), - ) -> Result { - let (state_root, trie_updates, hashed_state) = - 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(), - ) - })?; - 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 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: Arc::new(recovered_block), - execution_output: executed_block.execution_output.clone(), - 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", - error = %e, - "Failed to send updated payload" - ); - } - debug!( - target: "payload_builder", - state_root = %state_root, - "Updated payload with calculated state root" - ); - - Ok(updated_payload) - } - /// Do some logging and metric recording when we stop build flashblocks fn record_flashblocks_metrics( &self, @@ -1292,12 +1276,14 @@ where } #[async_trait::async_trait] -impl PayloadBuilder for OpPayloadBuilder +impl PayloadBuilder + for OpPayloadBuilder where Pool: PoolBounds, Client: ClientBounds, BuilderTx: BuilderTransactions + Clone + Send + Sync, + Tasks: TaskSpawner + Clone + Unpin + 'static, { type Attributes = OpPayloadBuilderAttributes; type BuiltPayload = OpBuiltPayload; @@ -1599,27 +1585,75 @@ where )) } +struct CalculateStateRootContext { + best_payload: (OpBuiltPayload, BundleState), + parent_hash: BlockHash, + built_payload_tx: mpsc::Sender, + metrics: Arc, +} + +async fn resolve_zero_state_root( + ctx: CalculateStateRootContext, + state_provider: Box, +) -> Result { + let (state_root, trie_updates, hashed_state) = + calculate_state_root_on_resolve(&ctx, state_provider)?; + + let payload_id = ctx.best_payload.0.id(); + let fees = ctx.best_payload.0.fees(); + let executed_block = ctx.best_payload.0.executed_block().ok_or_else(|| { + PayloadBuilderError::Other( + eyre::eyre!("No executed block available in best payload for payload resolution") + .into(), + ) + })?; + let block = ctx.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 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 { + 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), + }; + let updated_payload = OpBuiltPayload::new(payload_id, sealed_block, fees, Some(executed)); + + // Send full built payload with state root calculated to pre-warm local engine state tree + if let Err(e) = ctx.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" + ); + + Ok(updated_payload) +} + /// Calculates only the state root for an existing payload -fn calculate_state_root_on_resolve( - state: &mut State, - ctx: &OpPayloadBuilderCtx, - bundle_state: BundleState, -) -> Result<(B256, TrieUpdates, HashedPostState), PayloadBuilderError> -where - DB: Database + AsRef

, - P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, - ExtraCtx: std::fmt::Debug + Default, -{ +fn calculate_state_root_on_resolve( + ctx: &CalculateStateRootContext, + state_provider: Box, +) -> Result<(B256, TrieUpdates, HashedPostState), PayloadBuilderError> { let state_root_start_time = Instant::now(); - let state_provider = state.database.as_ref(); - let hashed_state = state_provider.hashed_post_state(&bundle_state); - let state_root_updates = state - .database - .as_ref() + let hashed_state = state_provider.hashed_post_state(&ctx.best_payload.1); + let state_root_updates = state_provider .state_root_with_updates(hashed_state.clone()) .inspect_err(|err| { warn!(target: "payload_builder", - parent_header=%ctx.parent().hash(), + parent_header=%ctx.parent_hash, %err, "failed to calculate state root for payload" ); diff --git a/crates/op-rbuilder/src/builders/flashblocks/service.rs b/crates/op-rbuilder/src/builders/flashblocks/service.rs index 7cfff2888..18ad85053 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/service.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/service.rs @@ -119,6 +119,7 @@ impl FlashblocksServiceBuilder { OpEvmConfig::optimism(ctx.chain_spec()), pool, ctx.provider().clone(), + ctx.task_executor().clone(), self.0.clone(), builder_tx, built_fb_payload_tx,