From 3d5d189fa75ec85a7b48ef81c850d1bd8536830d Mon Sep 17 00:00:00 2001 From: Niven Date: Wed, 3 Dec 2025 11:40:08 +0800 Subject: [PATCH 1/5] Add external payload validation logic inside fb payload handler (#26) --- .../builders/flashblocks/payload_handler.rs | 87 +++++++++++++------ 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs index 96b6f683..3d4148c4 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs @@ -14,11 +14,13 @@ use reth_basic_payload_builder::PayloadConfig; use reth_evm::FromRecoveredTx; use reth_node_builder::Events; use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_consensus::OpBeaconConsensus; use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; use reth_optimism_node::{OpEngineTypes, OpPayloadBuilderAttributes}; use reth_optimism_payload_builder::OpBuiltPayload; use reth_optimism_primitives::{OpReceipt, OpTransactionSigned}; use reth_payload_builder::EthPayloadBuilderAttributes; +use reth_primitives_traits::SealedHeader; use rollup_boost::FlashblocksPayloadV1; use std::sync::Arc; use tokio::sync::mpsc; @@ -94,30 +96,33 @@ where Some(message) = p2p_rx.recv() => { match message { Message::OpBuiltPayload(payload) => { - let payload: OpBuiltPayload = payload.into(); + let external_payload: OpBuiltPayload = payload.into(); let ctx = ctx.clone(); let client = client.clone(); let payload_events_handle = payload_events_handle.clone(); let cancel = cancel.clone(); - // execute the flashblock on a thread where blocking is acceptable, - // as it's potentially a heavy operation + // Validating and executing the built full payload on a thread where blocking + // is acceptable, as there should only be one flashblock builder at any point in time. tokio::task::spawn_blocking(move || { let res = execute_flashblock( - payload, + &external_payload, ctx, client, cancel, ); - match res { - Ok((payload, _)) => { - tracing::info!(hash = payload.block().hash().to_string(), block_number = payload.block().header().number, "successfully executed received flashblock"); - if let Err(e) = payload_events_handle.send(Events::BuiltPayload(payload)) { + // For X Layer + match res.and_then(|(built_payload, _)| { + tracing::info!(hash = built_payload.block().hash().to_string(), block_number = built_payload.block().header().number, "successfully executed received flashblock"); + validate_post_execute(&built_payload, &external_payload).map(|_| built_payload) + }) { + Ok(built_payload) => { + if let Err(e) = payload_events_handle.send(Events::BuiltPayload(built_payload)) { warn!(e = ?e, "failed to send BuiltPayload event on synced block"); } } Err(e) => { - tracing::error!(error = ?e, "failed to execute received flashblock"); + tracing::error!(error = ?e, "failed to received external full payload from flashblock builder"); } } }); @@ -131,7 +136,7 @@ where } fn execute_flashblock( - payload: OpBuiltPayload, + payload: &OpBuiltPayload, ctx: OpPayloadSyncerCtx, client: Client, cancel: tokio_util::sync::CancellationToken, @@ -154,6 +159,11 @@ where .wrap_err("failed to get parent header")? .ok_or_else(|| eyre::eyre!("parent header not found"))?; + // For X Layer, validate header and parent relationship before execution + let chain_spec = client.chain_spec(); + validate_pre_execution(payload, &parent_header, parent_hash, chain_spec.clone()) + .wrap_err("pre-execution validation failed")?; + let state_provider = client .state_by_block_hash(parent_hash) .wrap_err("failed to get state for parent hash")?; @@ -163,7 +173,6 @@ where .with_bundle_update() .build(); - let chain_spec = client.chain_spec(); let timestamp = payload.block().header().timestamp(); let block_env_attributes = OpNextBlockEnvAttributes { timestamp, @@ -282,7 +291,7 @@ fn execute_transactions( is_canyon_active: bool, is_regolith_active: bool, ) -> eyre::Result<()> { - use alloy_evm::{Evm as _, EvmError as _}; + use alloy_evm::Evm as _; use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; use reth_evm::ConfigureEvm as _; use reth_primitives_traits::SignerRecoverable as _; @@ -333,19 +342,7 @@ fn execute_transactions( } }; - let ResultAndState { result, state } = match evm.transact_raw(executable_tx) { - Ok(res) => res, - Err(err) => { - if let Some(err) = err.as_invalid_tx_err() { - // TODO: what invalid txs are allowed in the block? - // reverting txs should be allowed (?) but not straight up invalid ones - tracing::error!(error = %err, "skipping invalid transaction in flashblock"); - continue; - } - return Err(err).wrap_err("failed to execute flashblock transaction"); - } - }; - + let ResultAndState { result, state } = evm.transact_raw(executable_tx)?; if let Some(max_gas_per_txn) = max_gas_per_txn && result.gas_used() > max_gas_per_txn { @@ -436,6 +433,46 @@ fn build_receipt( } } +/// Validates the payload header and its relationship with the parent before execution. +/// This performs consensus rule validation including: +/// - Header field validation (timestamp, gas limit, etc.) +/// - Parent relationship validation (block number increment, timestamp progression) +fn validate_pre_execution( + payload: &OpBuiltPayload, + parent_header: &reth_primitives_traits::Header, + parent_hash: alloy_primitives::B256, + chain_spec: Arc, +) -> eyre::Result<()> { + use reth::consensus::HeaderValidator; + + let consensus = OpBeaconConsensus::new(chain_spec); + let parent_sealed = SealedHeader::new(parent_header.clone(), parent_hash); + + // Validate incoming header + consensus + .validate_header(payload.block().sealed_header()) + .wrap_err("header validation failed")?; + + // Validate incoming header against parent + consensus + .validate_header_against_parent(payload.block().sealed_header(), &parent_sealed) + .wrap_err("header validation against parent failed")?; + + Ok(()) +} + +fn validate_post_execute( + built_payload: &OpBuiltPayload, + external_payload: &OpBuiltPayload, +) -> eyre::Result<()> { + if built_payload.block().hash() != external_payload.block().hash() { + return Err(eyre::eyre!( + "validation failed, built payload hash mismatch" + )); + } + Ok(()) +} + fn is_canyon_active(chain_spec: &OpChainSpec, timestamp: u64) -> bool { use reth_optimism_chainspec::OpHardforks as _; chain_spec.is_canyon_active_at_timestamp(timestamp) From a3e84e01dbb5bbcbe175f653c43c49536fc1986e Mon Sep 17 00:00:00 2001 From: Niven Date: Wed, 3 Dec 2025 11:58:33 +0800 Subject: [PATCH 2/5] Remove post execution validation as default already does the validation (#27) --- .../builders/flashblocks/payload_handler.rs | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs index 3d4148c4..3b8178a1 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs @@ -96,33 +96,30 @@ where Some(message) = p2p_rx.recv() => { match message { Message::OpBuiltPayload(payload) => { - let external_payload: OpBuiltPayload = payload.into(); + let payload: OpBuiltPayload = payload.into(); let ctx = ctx.clone(); let client = client.clone(); let payload_events_handle = payload_events_handle.clone(); let cancel = cancel.clone(); - // Validating and executing the built full payload on a thread where blocking - // is acceptable, as there should only be one flashblock builder at any point in time. + // execute the built full payload on a thread where blocking is acceptable, + // as there should only be one flashblock builder at any point in time. tokio::task::spawn_blocking(move || { let res = execute_flashblock( - &external_payload, + payload, ctx, client, cancel, ); - // For X Layer - match res.and_then(|(built_payload, _)| { - tracing::info!(hash = built_payload.block().hash().to_string(), block_number = built_payload.block().header().number, "successfully executed received flashblock"); - validate_post_execute(&built_payload, &external_payload).map(|_| built_payload) - }) { - Ok(built_payload) => { - if let Err(e) = payload_events_handle.send(Events::BuiltPayload(built_payload)) { + match res { + Ok((payload, _)) => { + tracing::info!(hash = payload.block().hash().to_string(), block_number = payload.block().header().number, "successfully executed received flashblock"); + if let Err(e) = payload_events_handle.send(Events::BuiltPayload(payload)) { warn!(e = ?e, "failed to send BuiltPayload event on synced block"); } } Err(e) => { - tracing::error!(error = ?e, "failed to received external full payload from flashblock builder"); + tracing::error!(error = ?e, "failed to execute received flashblock"); } } }); @@ -136,7 +133,7 @@ where } fn execute_flashblock( - payload: &OpBuiltPayload, + payload: OpBuiltPayload, ctx: OpPayloadSyncerCtx, client: Client, cancel: tokio_util::sync::CancellationToken, @@ -161,7 +158,7 @@ where // For X Layer, validate header and parent relationship before execution let chain_spec = client.chain_spec(); - validate_pre_execution(payload, &parent_header, parent_hash, chain_spec.clone()) + validate_pre_execution(&payload, &parent_header, parent_hash, chain_spec.clone()) .wrap_err("pre-execution validation failed")?; let state_provider = client @@ -461,18 +458,6 @@ fn validate_pre_execution( Ok(()) } -fn validate_post_execute( - built_payload: &OpBuiltPayload, - external_payload: &OpBuiltPayload, -) -> eyre::Result<()> { - if built_payload.block().hash() != external_payload.block().hash() { - return Err(eyre::eyre!( - "validation failed, built payload hash mismatch" - )); - } - Ok(()) -} - fn is_canyon_active(chain_spec: &OpChainSpec, timestamp: u64) -> bool { use reth_optimism_chainspec::OpHardforks as _; chain_spec.is_canyon_active_at_timestamp(timestamp) From dc8799be197c5ae8dfe56cea9e6dbf8ba019f666 Mon Sep 17 00:00:00 2001 From: Niven Date: Wed, 3 Dec 2025 12:28:26 +0800 Subject: [PATCH 3/5] Revert comments (#28) --- crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs index 3b8178a1..21c0062a 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs @@ -103,7 +103,7 @@ where let cancel = cancel.clone(); // execute the built full payload on a thread where blocking is acceptable, - // as there should only be one flashblock builder at any point in time. + // as it's potentially a heavy operation tokio::task::spawn_blocking(move || { let res = execute_flashblock( payload, From 9a10999bf90f36abb9b94b76ffe4a9023a26fd6e Mon Sep 17 00:00:00 2001 From: Niven Date: Wed, 3 Dec 2025 16:35:17 +0800 Subject: [PATCH 4/5] Fix execution txs logic on external payload validation (#31) --- .../src/builders/flashblocks/ctx.rs | 11 ++ .../builders/flashblocks/payload_handler.rs | 143 ++++++------------ 2 files changed, 58 insertions(+), 96 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/ctx.rs b/crates/op-rbuilder/src/builders/flashblocks/ctx.rs index 16d1a71c..c29a4ee1 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/ctx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/ctx.rs @@ -9,6 +9,7 @@ use reth_basic_payload_builder::PayloadConfig; use reth_evm::EvmEnv; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; +use reth_optimism_forks::OpHardforks; use reth_optimism_payload_builder::{ OpPayloadBuilderAttributes, config::{OpDAConfig, OpGasLimitConfig}, @@ -59,6 +60,16 @@ impl OpPayloadSyncerCtx { self.max_gas_per_txn } + /// Returns true if regolith is active for the payload. + pub(super) fn is_regolith_active(&self, timestamp: u64) -> bool { + self.chain_spec.is_regolith_active_at_timestamp(timestamp) + } + + /// Returns true if canyon is active for the payload. + pub(super) fn is_canyon_active(&self, timestamp: u64) -> bool { + self.chain_spec.is_canyon_active_at_timestamp(timestamp) + } + pub(super) fn into_op_payload_builder_ctx( self, payload_config: PayloadConfig>, diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs index 21c0062a..0723dc72 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs @@ -8,14 +8,12 @@ use crate::{ use alloy_evm::eth::receipt_builder::ReceiptBuilderCtx; use alloy_primitives::B64; use eyre::{WrapErr as _, bail}; -use op_alloy_consensus::OpTxEnvelope; use reth::revm::{State, database::StateProviderDatabase}; use reth_basic_payload_builder::PayloadConfig; -use reth_evm::FromRecoveredTx; use reth_node_builder::Events; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_consensus::OpBeaconConsensus; -use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; +use reth_optimism_evm::OpNextBlockEnvAttributes; use reth_optimism_node::{OpEngineTypes, OpPayloadBuilderAttributes}; use reth_optimism_payload_builder::OpBuiltPayload; use reth_optimism_primitives::{OpReceipt, OpTransactionSigned}; @@ -228,15 +226,13 @@ where ); execute_transactions( + &ctx, &mut info, &mut state, payload.block().body().transactions.clone(), payload.block().header().gas_used, - ctx.evm_config(), + timestamp, evm_env.clone(), - ctx.max_gas_per_txn(), - is_canyon_active(&chain_spec, timestamp), - is_regolith_active(&chain_spec, timestamp), ) .wrap_err("failed to execute best transactions")?; @@ -278,77 +274,55 @@ where #[allow(clippy::too_many_arguments)] fn execute_transactions( + ctx: &OpPayloadSyncerCtx, info: &mut ExecutionInfo, state: &mut State, txs: Vec, gas_limit: u64, - evm_config: &reth_optimism_evm::OpEvmConfig, + timestamp: u64, evm_env: alloy_evm::EvmEnv, - max_gas_per_txn: Option, - is_canyon_active: bool, - is_regolith_active: bool, ) -> eyre::Result<()> { use alloy_evm::Evm as _; - use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; use reth_evm::ConfigureEvm as _; - use reth_primitives_traits::SignerRecoverable as _; - use revm::{ - DatabaseCommit as _, - context::{TxEnv, result::ResultAndState}, - }; + use reth_primitives_traits::SignedTransaction; + use revm::{DatabaseCommit as _, context::result::ResultAndState}; - let mut evm = evm_config.evm_with_env(&mut *state, evm_env); + let mut evm = ctx.evm_config().evm_with_env(&mut *state, evm_env); for tx in txs { - let sender = tx - .recover_signer() - .wrap_err("failed to recover tx signer")?; - let tx_env = TxEnv::from_recovered_tx(&tx, sender); - let executable_tx = match tx { - OpTxEnvelope::Deposit(ref tx) => { - let deposit = DepositTransactionParts { - mint: Some(tx.mint), - source_hash: tx.source_hash, - is_system_transaction: tx.is_system_transaction, - }; - OpTransaction { - base: tx_env, - enveloped_tx: None, - deposit, - } - } - OpTxEnvelope::Legacy(_) => { - let mut tx = OpTransaction::new(tx_env); - tx.enveloped_tx = Some(vec![0x00].into()); - tx - } - OpTxEnvelope::Eip2930(_) => { - let mut tx = OpTransaction::new(tx_env); - tx.enveloped_tx = Some(vec![0x00].into()); - tx - } - OpTxEnvelope::Eip1559(_) => { - let mut tx = OpTransaction::new(tx_env); - tx.enveloped_tx = Some(vec![0x00].into()); - tx - } - OpTxEnvelope::Eip7702(_) => { - let mut tx = OpTransaction::new(tx_env); - tx.enveloped_tx = Some(vec![0x00].into()); - tx - } - }; + // Convert to recovered transaction + let tx_recovered = tx + .try_clone_into_recovered() + .wrap_err("failed to recover tx")?; + let sender = tx_recovered.signer(); + + // Cache the depositor account prior to the state transition for the deposit nonce. + // + // Note that this *only* needs to be done post-regolith hardfork, as deposit nonces + // were not introduced in Bedrock. In addition, regular transactions don't have deposit + // nonces, so we don't need to touch the DB for those. + let depositor_nonce = (ctx.is_regolith_active(timestamp) && tx_recovered.is_deposit()) + .then(|| { + evm.db_mut() + .load_cache_account(sender) + .map(|acc| acc.account_info().unwrap_or_default().nonce) + }) + .transpose() + .wrap_err("failed to get depositor nonce")?; - let ResultAndState { result, state } = evm.transact_raw(executable_tx)?; - if let Some(max_gas_per_txn) = max_gas_per_txn - && result.gas_used() > max_gas_per_txn + let ResultAndState { result, state } = evm + .transact(&tx_recovered) + .wrap_err("failed to execute transaction")?; + + let tx_gas_used = result.gas_used(); + if let Some(max_gas_per_txn) = ctx.max_gas_per_txn() + && tx_gas_used > max_gas_per_txn { return Err(eyre::eyre!( "transaction exceeded max gas per txn limit in flashblock" )); } - let tx_gas_used = result.gas_used(); info.cumulative_gas_used = info .cumulative_gas_used .checked_add(tx_gas_used) @@ -359,16 +333,7 @@ fn execute_transactions( bail!("flashblock exceeded gas limit when executing transactions"); } - let depositor_nonce = (is_regolith_active && tx.is_deposit()) - .then(|| { - evm.db_mut() - .load_cache_account(sender) - .map(|acc| acc.account_info().unwrap_or_default().nonce) - }) - .transpose() - .wrap_err("failed to get depositor nonce")?; - - let ctx = ReceiptBuilderCtx { + let receipt_ctx = ReceiptBuilderCtx { tx: &tx, evm: &evm, result, @@ -376,12 +341,8 @@ fn execute_transactions( cumulative_gas_used: info.cumulative_gas_used, }; - info.receipts.push(build_receipt( - evm_config, - ctx, - depositor_nonce, - is_canyon_active, - )); + info.receipts + .push(build_receipt(ctx, receipt_ctx, depositor_nonce, timestamp)); evm.db_mut().commit(state); @@ -394,26 +355,26 @@ fn execute_transactions( } fn build_receipt( - evm_config: &OpEvmConfig, - ctx: ReceiptBuilderCtx<'_, OpTransactionSigned, E>, + ctx: &OpPayloadSyncerCtx, + receipt_ctx: ReceiptBuilderCtx<'_, OpTransactionSigned, E>, deposit_nonce: Option, - is_canyon_active: bool, + timestamp: u64, ) -> OpReceipt { use alloy_consensus::Eip658Value; use alloy_op_evm::block::receipt_builder::OpReceiptBuilder as _; use op_alloy_consensus::OpDepositReceipt; use reth_evm::ConfigureEvm as _; - let receipt_builder = evm_config.block_executor_factory().receipt_builder(); - match receipt_builder.build_receipt(ctx) { + let receipt_builder = ctx.evm_config().block_executor_factory().receipt_builder(); + match receipt_builder.build_receipt(receipt_ctx) { Ok(receipt) => receipt, - Err(ctx) => { + Err(receipt_ctx) => { let receipt = alloy_consensus::Receipt { // Success flag was added in `EIP-658: Embedding transaction status code // in receipts`. - status: Eip658Value::Eip658(ctx.result.is_success()), - cumulative_gas_used: ctx.cumulative_gas_used, - logs: ctx.result.into_logs(), + status: Eip658Value::Eip658(receipt_ctx.result.is_success()), + cumulative_gas_used: receipt_ctx.cumulative_gas_used, + logs: receipt_ctx.result.into_logs(), }; receipt_builder.build_deposit_receipt(OpDepositReceipt { @@ -424,7 +385,7 @@ fn build_receipt( // when set. The state transition process ensures // this is only set for post-Canyon deposit // transactions. - deposit_receipt_version: is_canyon_active.then_some(1), + deposit_receipt_version: ctx.is_canyon_active(timestamp).then_some(1), }) } } @@ -457,13 +418,3 @@ fn validate_pre_execution( Ok(()) } - -fn is_canyon_active(chain_spec: &OpChainSpec, timestamp: u64) -> bool { - use reth_optimism_chainspec::OpHardforks as _; - chain_spec.is_canyon_active_at_timestamp(timestamp) -} - -fn is_regolith_active(chain_spec: &OpChainSpec, timestamp: u64) -> bool { - use reth_optimism_chainspec::OpHardforks as _; - chain_spec.is_regolith_active_at_timestamp(timestamp) -} From cba132d02e1fe52a43688cc0ca3fb33e52214561 Mon Sep 17 00:00:00 2001 From: Niven Date: Wed, 3 Dec 2025 17:10:58 +0800 Subject: [PATCH 5/5] Revert comment --- crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs index 0723dc72..1e6e9b14 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs @@ -100,7 +100,7 @@ where let payload_events_handle = payload_events_handle.clone(); let cancel = cancel.clone(); - // execute the built full payload on a thread where blocking is acceptable, + // execute the flashblock on a thread where blocking is acceptable, // as it's potentially a heavy operation tokio::task::spawn_blocking(move || { let res = execute_flashblock(