diff --git a/crates/op-rbuilder/src/builder/assembly.rs b/crates/op-rbuilder/src/builder/assembly.rs index 1cb802be1..85e6babbf 100644 --- a/crates/op-rbuilder/src/builder/assembly.rs +++ b/crates/op-rbuilder/src/builder/assembly.rs @@ -36,9 +36,9 @@ use tracing::{debug, info, warn}; use crate::{ builder::{StateRootCalculator, payload::FlashblocksState, state_root::StateRootOutput}, evm::OpBlockEvmFactory, + execution::ExecutionInfo, hardforks::ActiveHardforks, metrics::OpRBuilderMetrics, - primitives::reth::ExecutionInfo, }; /// Pre-resolved parameters needed by `build_block`, decoupled from @@ -296,7 +296,7 @@ impl BlockAssemblyInput { pub(super) fn assemble( self, state: &mut State, - fb_state: Option<&mut FlashblocksState>, + fb_state: Option<&FlashblocksState>, info: &mut ExecutionInfo, state_root_calc: &mut StateRootCalculator, metrics: Arc, @@ -316,10 +316,7 @@ impl BlockAssemblyInput { self.check_block_number()?; - let flashblock_index_for_trace = fb_state - .as_deref() - .map(|s| s.flashblock_index()) - .unwrap_or(0); + let flashblock_index_for_trace = fb_state.map(|s| s.flashblock_index()).unwrap_or(0); // Calculate the state root (returns defaults when disabled) let state_root_start_time = Instant::now(); @@ -379,10 +376,8 @@ impl BlockAssemblyInput { let block_hash = sealed_block.hash(); - let target_flashblock_count_for_trace = fb_state - .as_deref() - .map(|s| s.target_flashblock_count()) - .unwrap_or(0); + let target_flashblock_count_for_trace = + fb_state.map(|s| s.target_flashblock_count()).unwrap_or(0); info!( target: "payload_builder", @@ -450,16 +445,13 @@ impl BlockAssemblyInput { ); // pick the new transactions from the info field and update the last flashblock index - let (new_transactions, new_receipts) = if let Some(fb_state) = fb_state { - let new_txs = fb_state.slice_new_transactions(&info.executed_transactions); - let new_receipts = fb_state.slice_new_receipts(&info.receipts); - fb_state.set_last_flashblock_tx_index(info.executed_transactions.len()); + let (new_transactions, new_receipts) = if fb_state.is_some() { + let new_txs = info.new_transactions_vec(); + let new_receipts = info.new_receipts_vec(); + info.set_last_flashblock_tx_index(); (new_txs, new_receipts) } else { - ( - info.executed_transactions.as_slice(), - info.receipts.as_slice(), - ) + (info.executed_transactions.clone(), info.receipts.clone()) }; let new_transactions_encoded: Vec = new_transactions diff --git a/crates/op-rbuilder/src/builder/builder_tx.rs b/crates/op-rbuilder/src/builder/builder_tx.rs index 3f7c03a70..57e81632d 100644 --- a/crates/op-rbuilder/src/builder/builder_tx.rs +++ b/crates/op-rbuilder/src/builder/builder_tx.rs @@ -23,8 +23,7 @@ use revm::{ use tracing::{error, trace, warn}; use crate::{ - evm::OpBlockEvmFactory, hardforks::ActiveHardforks, primitives::reth::ExecutionInfo, - tx_signer::Signer, + evm::OpBlockEvmFactory, execution::ExecutionInfo, hardforks::ActiveHardforks, tx_signer::Signer, }; #[derive(Debug, Default)] @@ -270,20 +269,18 @@ pub trait BuilderTransactions { continue; } - let tx_uncompressed_size = builder_tx.signed_tx.inner().encode_2718_len() as u64; - // Skip the builder tx if including it would push the cumulative // uncompressed block size over the configured limit. The state // changes from the simulation are dropped (we never call commit). - if let Some(limit) = ctx.max_uncompressed_block_size - && info.cumulative_uncompressed_bytes + tx_uncompressed_size > limit - { + if let Err(err) = info.check_uncompressed_size_limit( + builder_tx.signed_tx.inner().encode_2718_len() as u64, + ctx.max_uncompressed_block_size, + ) { warn!( target: "payload_builder", tx_hash = %builder_tx.signed_tx.tx_hash(), cumulative_uncompressed = info.cumulative_uncompressed_bytes, - tx_uncompressed_size, - limit, + %err, "skipping builder tx: would exceed max uncompressed block size" ); continue; @@ -293,7 +290,6 @@ pub trait BuilderTransactions { &builder_tx.signed_tx, result, state, - builder_tx.da_size, None, None, ctx.evm_factory, diff --git a/crates/op-rbuilder/src/builder/context.rs b/crates/op-rbuilder/src/builder/context.rs index ed9b7bb7f..b0c136b6e 100644 --- a/crates/op-rbuilder/src/builder/context.rs +++ b/crates/op-rbuilder/src/builder/context.rs @@ -34,10 +34,10 @@ use crate::{ cancellation::FlashblockJobCancellation, }, evm::OpBlockEvmFactory, + execution::{ExecutionInfo, TxnExecutionResult}, hardforks::ActiveHardforks, limiter::AddressLimiter, metrics::OpRBuilderMetrics, - primitives::reth::{ExecutionInfo, TxnExecutionResult}, traits::PayloadTxsBounds, }; @@ -256,17 +256,10 @@ impl OpPayloadJobCtx { } }; - let tx_da_size = if !sequencer_tx.is_deposit() { - op_alloy_flz::tx_estimated_size_fjord_bytes(sequencer_tx.encoded_2718().as_slice()) - } else { - 0 - }; - info.commit_tx( &sequencer_tx, result, state, - tx_da_size, None, depositor_nonce, &self.evm_factory, @@ -556,7 +549,6 @@ impl OpPayloadJobCtx { &tx, result, state, - tx_da_size, Some(miner_fee), None, &self.evm_factory, @@ -801,7 +793,6 @@ impl OpPayloadJobCtx { &bundle.backrun_tx, br_result, br_state, - br_tx_da_size, Some(backrun_priority_fee), None, &self.evm_factory, diff --git a/crates/op-rbuilder/src/builder/flashblocks_builder_tx.rs b/crates/op-rbuilder/src/builder/flashblocks_builder_tx.rs index 364fe7450..f17c4a4d4 100644 --- a/crates/op-rbuilder/src/builder/flashblocks_builder_tx.rs +++ b/crates/op-rbuilder/src/builder/flashblocks_builder_tx.rs @@ -19,8 +19,8 @@ use crate::{ builder_tx::{BuilderTxBase, BuilderTxEnv}, get_nonce, }, + execution::ExecutionInfo, flashtestations::builder_tx::FlashtestationsBuilderTx, - primitives::reth::ExecutionInfo, tx_signer::Signer, }; diff --git a/crates/op-rbuilder/src/builder/payload.rs b/crates/op-rbuilder/src/builder/payload.rs index 35bd31740..c226e3e1c 100644 --- a/crates/op-rbuilder/src/builder/payload.rs +++ b/crates/op-rbuilder/src/builder/payload.rs @@ -10,10 +10,10 @@ use crate::{ timing::{FlashblockScheduler, compute_slot_offset_ms}, }, evm::OpBlockEvmFactory, + execution::ExecutionInfo, hardforks::ActiveHardforks, limiter::AddressLimiter, metrics::{OpRBuilderMetrics, record_flashblock_publish_timing}, - primitives::reth::ExecutionInfo, runtime_ext::RuntimeExt, tokio_metrics::FlashblocksTaskMetrics, traits::{ClientBounds, PoolBounds}, @@ -24,7 +24,7 @@ use reth_chainspec::EthChainSpec; use reth_node_api::PayloadBuilderError; use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; use reth_optimism_node::{OpBuiltPayload, OpPayloadBuilderAttributes}; -use reth_optimism_primitives::{OpReceipt, OpTransactionSigned}; +use reth_optimism_primitives::OpTransactionSigned; use reth_payload_primitives::PayloadBuilderAttributes; use reth_payload_util::BestPayloadTransactions; use reth_provider::{ @@ -76,9 +76,6 @@ pub(super) struct FlashblocksState { da_per_batch: Option, /// DA footprint limit per flashblock da_footprint_per_batch: Option, - /// Index into ExecutionInfo tracking the last consumed flashblock - /// Used for slicing transactions/receipts per flashblock - last_flashblock_tx_index: usize, } struct FallbackBuildOutput { @@ -127,7 +124,6 @@ impl FlashblocksState { gas_per_batch: self.gas_per_batch, da_per_batch: self.da_per_batch, da_footprint_per_batch: self.da_footprint_per_batch, - last_flashblock_tx_index: self.last_flashblock_tx_index, } } @@ -215,23 +211,6 @@ impl FlashblocksState { fn target_da_footprint_for_batch(&self) -> Option { self.target_da_footprint_for_batch } - - pub(super) fn set_last_flashblock_tx_index(&mut self, index: usize) { - self.last_flashblock_tx_index = index; - } - - /// Extracts new transactions since the last flashblock - pub(super) fn slice_new_transactions<'a>( - &self, - all_transactions: &'a [OpTransactionSigned], - ) -> &'a [OpTransactionSigned] { - &all_transactions[self.last_flashblock_tx_index..] - } - - /// Extracts new receipts since the last flashblock - pub(super) fn slice_new_receipts<'a>(&self, all_receipts: &'a [OpReceipt]) -> &'a [OpReceipt] { - &all_receipts[self.last_flashblock_tx_index..] - } } /// Optimism's payload builder @@ -732,10 +711,10 @@ where let mut best_txs = FlashblockPoolTxCursor::new(&mut tx_tracker); let mut info = info; - let mut fb_state = fb_state; + let fb_state = fb_state; let result = builder.build_next_flashblock( &ctx, - &mut fb_state, + &fb_state, &mut info, &mut state, &state_provider, @@ -839,7 +818,7 @@ where fn build_fallback_block( &self, ctx: OpPayloadJobCtx, - mut fb_state: FlashblocksState, + fb_state: FlashblocksState, mut cached_reads: CachedReads, mut state_root_calc: StateRootCalculator, ) -> eyre::Result>> { @@ -878,7 +857,7 @@ where let (payload, fb_payload) = ctx.block_assembly_input()?.assemble( &mut state, - Some(&mut fb_state), + Some(&fb_state), &mut info, &mut state_root_calc, ctx.metrics.clone(), @@ -908,7 +887,7 @@ where >( &self, ctx: &OpPayloadJobCtx, - fb_state: &mut FlashblocksState, + fb_state: &FlashblocksState, info: &mut ExecutionInfo, state: &mut State, state_provider: impl reth::providers::StateProvider + Clone, @@ -992,11 +971,11 @@ where ) .wrap_err("failed to execute best transactions")?; // Extract last transactions - let new_transactions: Vec<_> = fb_state - .slice_new_transactions(&info.executed_transactions) + let new_transactions = info + .new_transactions_vec() .iter() .map(|tx| tx.tx_hash()) - .collect::>(); + .collect(); best_txs.mark_committed(new_transactions); // Remove reverted bundle txs from the pool so they aren't re-simulated in future blocks diff --git a/crates/op-rbuilder/src/builder/payload_handler.rs b/crates/op-rbuilder/src/builder/payload_handler.rs index b11edf309..3e016e902 100644 --- a/crates/op-rbuilder/src/builder/payload_handler.rs +++ b/crates/op-rbuilder/src/builder/payload_handler.rs @@ -4,9 +4,9 @@ use crate::{ syncer_config::OpPayloadSyncerConfig, }, evm::OpBlockEvmFactory, + execution::ExecutionInfo, hardforks::ActiveHardforks, metrics::OpRBuilderMetrics, - primitives::reth::ExecutionInfo, traits::ClientBounds, }; use alloy_consensus::BlockHeader; @@ -412,42 +412,21 @@ fn execute_transactions( )); } - let new_cumulative_gas = info - .cumulative_gas_used - .checked_add(tx_gas_used) - .ok_or_else(|| { - eyre::eyre!("total gas used overflowed when executing flashblock transactions") - })?; - if new_cumulative_gas > gas_limit { - bail!("flashblock exceeded gas limit when executing transactions"); + if let Err(err) = info.check_gas_limit(tx_gas_used, gas_limit) { + bail!("{err}"); } - let tx_uncompressed_size = tx_recovered.encode_2718_len() as u64; - let _new_cumulative_uncompressed = info - .cumulative_uncompressed_bytes - .checked_add(tx_uncompressed_size) - .ok_or_else(|| { - eyre::eyre!( - "total uncompressed bytes overflowed when executing flashblock transactions" - ) - })?; - if let Some(limit) = max_uncompressed_block_size - && info.cumulative_uncompressed_bytes > limit - { - bail!("flashblock exceeded max uncompressed block size when executing transactions"); + if let Err(err) = info.check_uncompressed_size_limit( + tx_recovered.encode_2718_len() as u64, + max_uncompressed_block_size, + ) { + bail!("{err}"); } - let tx_da_size = if !tx_recovered.is_deposit() { - op_alloy_flz::tx_estimated_size_fjord_bytes(tx_recovered.encoded_2718().as_slice()) - } else { - 0 - }; - info.commit_tx( &tx_recovered, result, state, - tx_da_size, None, depositor_nonce, evm_factory, diff --git a/crates/op-rbuilder/src/primitives/reth/execution.rs b/crates/op-rbuilder/src/execution.rs similarity index 71% rename from crates/op-rbuilder/src/primitives/reth/execution.rs rename to crates/op-rbuilder/src/execution.rs index dc5c12b4b..87bdbe9e9 100644 --- a/crates/op-rbuilder/src/primitives/reth/execution.rs +++ b/crates/op-rbuilder/src/execution.rs @@ -40,7 +40,6 @@ pub enum TxnExecutionResult { CoinbaseProfitTooLow, } -#[derive(Default, Debug)] pub struct ExecutionInfo { /// All executed transactions (unrecovered). pub executed_transactions: Vec, @@ -62,6 +61,10 @@ pub struct ExecutionInfo { pub optional_blob_fields: Option<(Option, Option)>, /// Reverted bundle tx hashes to remove from the pool after each flashblock. pub reverted_bundle_tx_hashes: Vec, + + /// Index tracking the last consumed flashblock. Used for slicing + /// transactions/receipts per flashblock. + last_flashblock_tx_index: usize, } impl ExecutionInfo { @@ -78,53 +81,60 @@ impl ExecutionInfo { da_footprint_scalar: None, optional_blob_fields: None, reverted_bundle_tx_hashes: Vec::new(), + last_flashblock_tx_index: 0, } } - /// Returns true if the transaction would exceed the block limits: - /// - block gas limit: ensures the transaction still fits into the block. - /// - tx DA limit: if configured, ensures the tx does not exceed the maximum allowed DA limit - /// per tx. - /// - block DA limit: if configured, ensures the transaction's DA size does not exceed the - /// maximum allowed DA limit per block. - #[expect(clippy::too_many_arguments)] - pub fn is_tx_over_limits( + pub fn check_tx_da_limit( &self, tx_da_size: u64, - block_gas_limit: u64, - tx_data_limit: Option, - block_data_limit: Option, - tx_gas_limit: u64, - block_da_footprint_limit: Option, - tx_uncompressed_size: u64, - max_uncompressed_block_size: Option, + tx_da_limit: Option, ) -> Result<(), TxnExecutionResult> { - if tx_data_limit.is_some_and(|da_limit| tx_da_size > da_limit) { + if tx_da_limit.is_some_and(|da_limit| tx_da_size > da_limit) { return Err(TxnExecutionResult::TransactionDALimitExceeded); } - let total_da_bytes_used = self.cumulative_da_bytes_used.saturating_add(tx_da_size); - if block_data_limit.is_some_and(|da_limit| total_da_bytes_used > da_limit) { + + Ok(()) + } + + pub fn check_block_da_limit( + &self, + tx_da_size: u64, + block_gas_limit: u64, + block_da_limit: Option, + block_da_footprint_limit: Option, + ) -> Result<(), TxnExecutionResult> { + let potential_da_bytes_used = self.cumulative_da_bytes_used.saturating_add(tx_da_size); + if block_da_limit.is_some_and(|da_limit| potential_da_bytes_used > da_limit) { return Err(TxnExecutionResult::BlockDALimitExceeded( self.cumulative_da_bytes_used, tx_da_size, - block_data_limit.unwrap_or_default(), + block_da_limit.unwrap_or_default(), )); } // Post Jovian: the tx DA footprint must be less than the block gas limit if let Some(da_footprint_gas_scalar) = self.da_footprint_scalar { let tx_da_footprint = - total_da_bytes_used.saturating_mul(da_footprint_gas_scalar as u64); + potential_da_bytes_used.saturating_mul(da_footprint_gas_scalar as u64); if tx_da_footprint > block_da_footprint_limit.unwrap_or(block_gas_limit) { return Err(TxnExecutionResult::BlockDALimitExceeded( - total_da_bytes_used, + potential_da_bytes_used, tx_da_size, tx_da_footprint, )); } } - if self.cumulative_gas_used + tx_gas_limit > block_gas_limit { + Ok(()) + } + + pub fn check_gas_limit( + &self, + tx_gas_limit: u64, + block_gas_limit: u64, + ) -> Result<(), TxnExecutionResult> { + if self.cumulative_gas_used.saturating_add(tx_gas_limit) > block_gas_limit { return Err(TxnExecutionResult::TransactionGasLimitExceeded( self.cumulative_gas_used, tx_gas_limit, @@ -132,12 +142,20 @@ impl ExecutionInfo { )); } + Ok(()) + } + + pub fn check_uncompressed_size_limit( + &self, + tx_uncompressed_size: u64, + max_uncompressed_block_size: Option, + ) -> Result<(), TxnExecutionResult> { // Check block uncompressed size limit if let Some(limit) = max_uncompressed_block_size { - let total = self + let potential_uncompressed_bytes = self .cumulative_uncompressed_bytes .saturating_add(tx_uncompressed_size); - if total > limit { + if potential_uncompressed_bytes > limit { return Err(TxnExecutionResult::BlockUncompressedSizeExceeded( self.cumulative_uncompressed_bytes, tx_uncompressed_size, @@ -149,19 +167,55 @@ impl ExecutionInfo { Ok(()) } + /// Returns true if the transaction would exceed the block limits: + /// - block gas limit: ensures the transaction still fits into the block. + /// - tx DA limit: if configured, ensures the tx does not exceed the maximum allowed DA limit + /// per tx. + /// - block DA limit: if configured, ensures the transaction's DA size does not exceed the + /// maximum allowed DA limit per block. + #[expect(clippy::too_many_arguments)] + pub fn is_tx_over_limits( + &self, + tx_da_size: u64, + block_gas_limit: u64, + tx_da_limit: Option, + block_da_limit: Option, + tx_gas_limit: u64, + block_da_footprint_limit: Option, + tx_uncompressed_size: u64, + max_uncompressed_block_size: Option, + ) -> Result<(), TxnExecutionResult> { + self.check_tx_da_limit(tx_da_size, tx_da_limit)?; + self.check_block_da_limit( + tx_da_size, + block_gas_limit, + block_da_limit, + block_da_footprint_limit, + )?; + self.check_gas_limit(tx_gas_limit, block_gas_limit)?; + self.check_uncompressed_size_limit(tx_uncompressed_size, max_uncompressed_block_size)?; + + Ok(()) + } + #[expect(clippy::too_many_arguments)] pub fn commit_tx>( &mut self, tx: &Recovered, execution_result: ExecutionResult, state_changes: EvmState, - tx_da_size: u64, miner_fee: Option, deposit_nonce: Option, evm_factory: &OpBlockEvmFactory, hardforks: &ActiveHardforks, evm: &mut E, ) { + let tx_da_size = if !tx.is_deposit() { + op_alloy_flz::tx_estimated_size_fjord_bytes(tx.encoded_2718().as_slice()) + } else { + 0 + }; + let gas_used = execution_result.gas_used(); self.cumulative_gas_used += gas_used; self.cumulative_da_bytes_used += tx_da_size; @@ -193,6 +247,20 @@ impl ExecutionInfo { self.executed_senders.push(tx.signer()); self.executed_transactions.push(tx.clone().into_inner()); } + + pub fn set_last_flashblock_tx_index(&mut self) { + self.last_flashblock_tx_index = self.executed_transactions.len(); + } + + /// Extracts new transactions since the last flashblock + pub fn new_transactions_vec(&self) -> Vec { + self.executed_transactions[self.last_flashblock_tx_index..].to_vec() + } + + /// Extracts new receipts since the last flashblock + pub fn new_receipts_vec(&self) -> Vec { + self.receipts[self.last_flashblock_tx_index..].to_vec() + } } fn build_receipt( @@ -231,14 +299,29 @@ fn build_receipt( #[cfg(test)] mod tests { + use alloy_primitives::U256; + use super::{ExecutionInfo, TxnExecutionResult}; + fn execution_info_with_uncompressed_bytes(cumulative_uncompressed_bytes: u64) -> ExecutionInfo { + ExecutionInfo { + executed_transactions: vec![], + executed_senders: vec![], + receipts: vec![], + cumulative_gas_used: 0, + cumulative_da_bytes_used: 0, + cumulative_uncompressed_bytes, + total_fees: U256::ZERO, + da_footprint_scalar: None, + optional_blob_fields: None, + reverted_bundle_tx_hashes: vec![], + last_flashblock_tx_index: 0, + } + } + #[test] fn tx_limit_rejects_when_uncompressed_size_exceeds_limit() { - let info = ExecutionInfo { - cumulative_uncompressed_bytes: 100, - ..Default::default() - }; + let info = execution_info_with_uncompressed_bytes(100); let result = info.is_tx_over_limits(0, 30_000_000, None, None, 21_000, None, 50, Some(149)); @@ -252,10 +335,7 @@ mod tests { #[test] fn tx_limit_allows_exact_uncompressed_size_fit() { - let info = ExecutionInfo { - cumulative_uncompressed_bytes: 100, - ..Default::default() - }; + let info = execution_info_with_uncompressed_bytes(100); let result = info.is_tx_over_limits(0, 30_000_000, None, None, 21_000, None, 50, Some(150)); diff --git a/crates/op-rbuilder/src/flashtestations/builder_tx.rs b/crates/op-rbuilder/src/flashtestations/builder_tx.rs index 0d7283e94..c2db13ea7 100644 --- a/crates/op-rbuilder/src/flashtestations/builder_tx.rs +++ b/crates/op-rbuilder/src/flashtestations/builder_tx.rs @@ -19,13 +19,13 @@ use crate::{ BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, BuilderTxEnv, SimulationSuccessResult, get_nonce, }, + execution::ExecutionInfo, flashtestations::{ BlockData, IBlockBuilderPolicy::{self, BlockBuilderProofVerified}, IERC20Permit, IFlashtestationRegistry::{self, TEEServiceRegistered}, }, - primitives::reth::ExecutionInfo, tx_signer::Signer, }; diff --git a/crates/op-rbuilder/src/lib.rs b/crates/op-rbuilder/src/lib.rs index c69ebe3d2..6f78cb138 100644 --- a/crates/op-rbuilder/src/lib.rs +++ b/crates/op-rbuilder/src/lib.rs @@ -2,6 +2,7 @@ pub mod args; pub mod backrun_bundle; pub mod builder; pub mod evm; +pub mod execution; pub mod flashtestations; pub mod hardforks; pub mod launcher; diff --git a/crates/op-rbuilder/src/primitives/mod.rs b/crates/op-rbuilder/src/primitives/mod.rs index da146d306..b9bf361b0 100644 --- a/crates/op-rbuilder/src/primitives/mod.rs +++ b/crates/op-rbuilder/src/primitives/mod.rs @@ -1,4 +1,3 @@ pub mod bundle; -pub mod reth; #[cfg(feature = "telemetry")] pub mod telemetry; diff --git a/crates/op-rbuilder/src/primitives/reth/mod.rs b/crates/op-rbuilder/src/primitives/reth/mod.rs deleted file mode 100644 index 3aa4812af..000000000 --- a/crates/op-rbuilder/src/primitives/reth/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod execution; -pub use execution::{ExecutionInfo, TxnExecutionResult};