From 6a3f27f6afe240b93632681005de7fed5db98132 Mon Sep 17 00:00:00 2001 From: Ash Kunda <18058966+akundaz@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:01:14 -0400 Subject: [PATCH 1/2] refactor: builder transactions --- crates/op-rbuilder/src/builder/builder_tx.rs | 695 ------------------ .../op-rbuilder/src/builder/builder_tx/env.rs | 21 + .../src/builder/builder_tx/impls/claim.rs | 108 +++ .../builder_tx/impls/flashblock_number.rs | 216 ++++++ .../src/builder/builder_tx/impls/mod.rs | 5 + .../op-rbuilder/src/builder/builder_tx/mod.rs | 14 + .../src/builder/builder_tx/producer.rs | 92 +++ .../src/builder/builder_tx/schedule.rs | 427 +++++++++++ .../op-rbuilder/src/builder/builder_tx/sim.rs | 194 +++++ .../src/builder/continuous/candidate_loop.rs | 90 ++- .../src/builder/continuous/interval.rs | 14 +- .../src/builder/continuous/publish.rs | 4 +- .../src/builder/flashblocks_builder_tx.rs | 352 --------- crates/op-rbuilder/src/builder/mod.rs | 6 +- crates/op-rbuilder/src/builder/payload.rs | 122 ++- crates/op-rbuilder/src/builder/service.rs | 115 +-- .../src/flashtestations/builder_tx.rs | 125 ++-- 17 files changed, 1323 insertions(+), 1277 deletions(-) delete mode 100644 crates/op-rbuilder/src/builder/builder_tx.rs create mode 100644 crates/op-rbuilder/src/builder/builder_tx/env.rs create mode 100644 crates/op-rbuilder/src/builder/builder_tx/impls/claim.rs create mode 100644 crates/op-rbuilder/src/builder/builder_tx/impls/flashblock_number.rs create mode 100644 crates/op-rbuilder/src/builder/builder_tx/impls/mod.rs create mode 100644 crates/op-rbuilder/src/builder/builder_tx/mod.rs create mode 100644 crates/op-rbuilder/src/builder/builder_tx/producer.rs create mode 100644 crates/op-rbuilder/src/builder/builder_tx/schedule.rs create mode 100644 crates/op-rbuilder/src/builder/builder_tx/sim.rs delete mode 100644 crates/op-rbuilder/src/builder/flashblocks_builder_tx.rs diff --git a/crates/op-rbuilder/src/builder/builder_tx.rs b/crates/op-rbuilder/src/builder/builder_tx.rs deleted file mode 100644 index 082b4f4a..00000000 --- a/crates/op-rbuilder/src/builder/builder_tx.rs +++ /dev/null @@ -1,695 +0,0 @@ -use alloy_consensus::TxEip1559; -use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN}; -use alloy_evm::{Database, rpc::TryIntoTxEnv}; -use alloy_op_evm::{OpEvm, OpTx}; -use alloy_primitives::{Address, B256, BlockHash, Bytes, TxKind, U256, map::HashSet}; -use alloy_sol_types::{ContractError, Revert, SolCall, SolError, SolInterface}; -use core::fmt::Debug; -use op_alloy_consensus::OpTypedTransaction; -use op_alloy_rpc_types::OpTransactionRequest; -use op_revm::{OpHaltReason, OpTransactionError}; -use reth_evm::{Evm, EvmError, InvalidTxError, precompiles::PrecompilesMap}; -use reth_node_api::PayloadBuilderError; -use reth_optimism_primitives::OpTransactionSigned; -use reth_primitives_traits::Recovered; -use reth_provider::{ProviderError, StateProvider}; -use reth_revm::{State, database::StateProviderDatabase}; -use reth_rpc_api::eth::EthTxEnvError; -use revm::{ - DatabaseCommit, DatabaseRef, - context::result::{EVMError, ExecutionResult, ResultAndState}, - inspector::NoOpInspector, -}; -use tracing::{error, trace, warn}; - -use crate::{ - evm::OpBlockEvmFactory, hardforks::ActiveHardforks, primitives::reth::ExecutionInfo, - tx_signer::Signer, -}; - -#[derive(Debug, Default)] -pub struct SimulationSuccessResult { - pub gas_used: u64, - pub output: T::Return, - pub state_changes: revm::state::EvmState, -} - -#[derive(Debug, Clone)] -pub struct BuilderTransactionCtx { - pub gas_used: u64, - pub da_size: u64, - pub signed_tx: Recovered, - // whether the transaction should be a top of block or - // bottom of block transaction - pub is_top_of_block: bool, -} - -impl BuilderTransactionCtx { - pub fn set_top_of_block(mut self) -> Self { - self.is_top_of_block = true; - self - } - - pub fn set_bottom_of_block(mut self) -> Self { - self.is_top_of_block = false; - self - } -} - -#[derive(Debug, thiserror::Error)] -pub enum InvalidContractDataError { - #[error("did not find expected logs expected {0:?} but got {1:?}")] - InvalidLogs(Vec, Vec), - #[error("could not decode output from contract call")] - OutputAbiDecodeError, -} - -/// Possible error variants during construction of builder txs. -#[derive(Debug, thiserror::Error)] -pub enum BuilderTransactionError { - /// Builder account load fails to get builder nonce - #[error("failed to load account {0}")] - AccountLoadFailed(Address), - /// Signature signing fails - #[error("failed to sign transaction: {0}")] - SigningError(secp256k1::Error), - /// Invalid contract errors indicating the contract is incorrect - #[error("contract {0} may be incorrect, invalid contract data: {1}")] - InvalidContract(Address, InvalidContractDataError), - /// Transaction halted execution - #[error("transaction to {0} halted {1:?}")] - TransactionHalted(Address, OpHaltReason), - /// Transaction reverted - #[error("transaction to {0} reverted {1}")] - TransactionReverted(Address, Revert), - /// Invalid tx errors during evm execution. - #[error("invalid transaction error {0}")] - InvalidTransactionError(Box), - /// Unrecoverable error during evm execution. - #[error("evm execution error {0}")] - EvmExecutionError(Box), - /// Any other builder transaction errors. - #[error(transparent)] - Other(Box), -} - -impl From for BuilderTransactionError { - fn from(error: secp256k1::Error) -> Self { - BuilderTransactionError::SigningError(error) - } -} - -impl From> for BuilderTransactionError { - fn from(error: EVMError) -> Self { - BuilderTransactionError::EvmExecutionError(Box::new(error)) - } -} - -impl From for BuilderTransactionError { - fn from(error: EthTxEnvError) -> Self { - BuilderTransactionError::EvmExecutionError(Box::new(error)) - } -} - -impl From for PayloadBuilderError { - fn from(error: BuilderTransactionError) -> Self { - match error { - BuilderTransactionError::EvmExecutionError(e) => { - PayloadBuilderError::EvmExecutionError(e) - } - _ => PayloadBuilderError::other(error), - } - } -} - -impl BuilderTransactionError { - pub fn other(error: impl core::error::Error + Send + Sync + 'static) -> Self { - BuilderTransactionError::Other(Box::new(error)) - } - - pub fn msg(msg: impl core::fmt::Display) -> Self { - Self::Other(msg.to_string().into()) - } -} - -/// Minimal environment needed by [`BuilderTransactions`] so that -/// builder-transaction code does not depend on the full payload-job context. -pub struct BuilderTxEnv<'a> { - pub evm_factory: &'a OpBlockEvmFactory, - pub hardforks: &'a ActiveHardforks, - pub base_fee: u64, - pub block_number: u64, - pub block_gas_limit: u64, - pub parent_hash: BlockHash, - pub max_uncompressed_block_size: Option, -} - -impl BuilderTxEnv<'_> { - pub fn chain_id(&self) -> u64 { - self.hardforks.chain_id() - } -} - -pub trait BuilderTransactions { - // Simulates and returns the signed builder transactions. The simulation modifies and commit - // changes to the db so call new_simulation_state to simulate on a new copy of the state - #[expect(clippy::too_many_arguments)] - fn simulate_builder_txs( - &self, - state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, - ctx: &BuilderTxEnv<'_>, - db: &mut State, - top_of_block: bool, - is_first_flashblock: bool, - is_last_flashblock: bool, - ) -> Result, BuilderTransactionError>; - - #[expect(clippy::too_many_arguments)] - fn simulate_builder_txs_with_state_copy( - &self, - state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, - ctx: &BuilderTxEnv<'_>, - db: &State, - top_of_block: bool, - is_first_flashblock: bool, - is_last_flashblock: bool, - ) -> Result, BuilderTransactionError> { - let mut simulation_state = self.new_simulation_state(state_provider.clone(), db); - self.simulate_builder_txs( - state_provider, - info, - ctx, - &mut simulation_state, - top_of_block, - is_first_flashblock, - is_last_flashblock, - ) - } - - #[expect(clippy::too_many_arguments)] - fn add_builder_txs( - &self, - state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, - ctx: &BuilderTxEnv<'_>, - db: &mut State, - top_of_block: bool, - is_first_flashblock: bool, - is_last_flashblock: bool, - ) -> Result, BuilderTransactionError> { - let builder_txs = self.simulate_builder_txs_with_state_copy( - state_provider, - info, - ctx, - db, - top_of_block, - is_first_flashblock, - is_last_flashblock, - )?; - - let mut evm = ctx.evm_factory.evm(&mut *db); - - let mut invalid = HashSet::new(); - - for builder_tx in builder_txs.iter() { - if builder_tx.is_top_of_block != top_of_block { - // don't commit tx if the buidler tx is not being added in the intended - // position in the block - continue; - } - if invalid.contains(&builder_tx.signed_tx.signer()) { - warn!( - target: "payload_builder", - tx_hash = %builder_tx.signed_tx.tx_hash(), - "builder signer invalid as previous builder tx reverted" - ); - continue; - } - - let ResultAndState { result, state } = match evm.transact(&builder_tx.signed_tx) { - Ok(res) => res, - Err(err) => { - if let Some(err) = err.as_invalid_tx_err() { - if err.is_nonce_too_low() { - // if the nonce is too low, we can skip this transaction - trace!( - target: "payload_builder", - error = %err, - tx_hash = %builder_tx.signed_tx.tx_hash(), - "skipping nonce too low builder transaction" - ); - } else { - // if the transaction is invalid, we can skip it and all of its - // descendants - trace!( - target: "payload_builder", - error = %err, - tx_hash = %builder_tx.signed_tx.tx_hash(), - "skipping invalid builder transaction and its descendants" - ); - invalid.insert(builder_tx.signed_tx.signer()); - } - - continue; - } - // this is an error that we should treat as fatal for this attempt - return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); - } - }; - - if !result.is_success() { - warn!( - target: "payload_builder", - tx_hash = %builder_tx.signed_tx.tx_hash(), - result = ?result, - "builder tx reverted" - ); - invalid.insert(builder_tx.signed_tx.signer()); - 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 - { - warn!( - target: "payload_builder", - tx_hash = %builder_tx.signed_tx.tx_hash(), - cumulative_uncompressed = info.cumulative_uncompressed_bytes, - tx_uncompressed_size, - limit, - "skipping builder tx: would exceed max uncompressed block size" - ); - continue; - } - - info.commit_tx( - &builder_tx.signed_tx, - result, - state, - builder_tx.da_size, - None, - None, - ctx.evm_factory, - ctx.hardforks, - &mut evm, - ); - } - - Ok(builder_txs) - } - - // Creates a copy of the state to simulate against - fn new_simulation_state( - &self, - state_provider: impl StateProvider, - db: &State, - ) -> State> { - let state = StateProviderDatabase::new(state_provider); - - State::builder() - .with_database(state) - .with_cached_prestate(db.cache.clone()) - .with_bundle_update() - .build() - } - - fn sign_tx( - &self, - to: Address, - from: Signer, - gas_used: u64, - calldata: Bytes, - ctx: &BuilderTxEnv<'_>, - db: impl DatabaseRef, - ) -> Result, BuilderTransactionError> { - let nonce = get_nonce(db, from.address)?; - // Create the EIP-1559 transaction - let tx = OpTypedTransaction::Eip1559(TxEip1559 { - chain_id: ctx.chain_id(), - nonce, - // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer - gas_limit: gas_used * 64 / 63, - max_fee_per_gas: ctx.base_fee.into(), - to: TxKind::Call(to), - input: calldata, - ..Default::default() - }); - Ok(from.sign_tx(tx)?) - } - - fn commit_txs( - &self, - signed_txs: Vec>, - ctx: &BuilderTxEnv<'_>, - db: &mut State, - ) -> Result<(), BuilderTransactionError> { - let mut evm = ctx.evm_factory.evm(&mut *db); - for signed_tx in signed_txs { - let ResultAndState { state, .. } = evm - .transact(&signed_tx) - .map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?; - evm.db_mut().commit(state) - } - Ok(()) - } - - fn simulate_call( - &self, - tx: OpTransactionRequest, - expected_logs: Vec, - evm: &mut OpEvm, - ) -> Result, BuilderTransactionError> { - let evm_env = alloy_evm::EvmEnv::new(evm.cfg_env().clone(), evm.block().clone()); - let tx_env: revm::context::TxEnv = tx.as_ref().clone().try_into_tx_env(&evm_env)?; - let to = tx_env.kind.into_to().unwrap_or_default(); - let op_tx = OpTx(op_revm::OpTransaction { - base: tx_env, - enveloped_tx: Some(Bytes::new()), - deposit: Default::default(), - }); - - let ResultAndState { result, state } = match evm.transact(op_tx) { - Ok(res) => res, - Err(err) => { - if err.is_invalid_tx_err() { - return Err(BuilderTransactionError::InvalidTransactionError(Box::new( - err, - ))); - } else { - return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); - } - } - }; - let gas_used = result.tx_gas_used(); - - match result { - ExecutionResult::Success { output, logs, .. } => { - let topics: HashSet = logs - .into_iter() - .flat_map(|log| log.topics().to_vec()) - .collect(); - if !expected_logs - .iter() - .all(|expected_topic| topics.contains(expected_topic)) - { - return Err(BuilderTransactionError::InvalidContract( - to, - InvalidContractDataError::InvalidLogs( - expected_logs, - topics.into_iter().collect(), - ), - )); - } - let return_output = T::abi_decode_returns(&output.into_data()).map_err(|_| { - BuilderTransactionError::InvalidContract( - to, - InvalidContractDataError::OutputAbiDecodeError, - ) - })?; - Ok(SimulationSuccessResult:: { - gas_used, - output: return_output, - state_changes: state, - }) - } - ExecutionResult::Revert { output, .. } => { - let revert = ContractError::::abi_decode(&output) - .map(|reason| Revert::from(format!("{reason:?}"))) - .or_else(|_| Revert::abi_decode(&output)) - .unwrap_or_else(|_| { - Revert::from(format!("unknown revert: {}", hex::encode(&output))) - }); - Err(BuilderTransactionError::TransactionReverted(to, revert)) - } - ExecutionResult::Halt { reason, .. } => { - Err(BuilderTransactionError::TransactionHalted(to, reason)) - } - } - } -} - -#[derive(Debug, Clone)] -pub(super) struct BuilderTxBase { - pub signer: Option, -} - -impl BuilderTxBase { - pub(super) fn new(signer: Option) -> Self { - Self { signer } - } - - pub(super) fn simulate_builder_tx( - &self, - ctx: &BuilderTxEnv<'_>, - db: impl DatabaseRef, - ) -> Result, BuilderTransactionError> { - match self.signer { - Some(signer) => { - let message: Vec = format!("Block Number: {}", ctx.block_number).into_bytes(); - let gas_used = self.estimate_builder_tx_gas(&message); - let signed_tx = self.signed_builder_tx(ctx, db, signer, gas_used, message)?; - let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( - signed_tx.encoded_2718().as_slice(), - ); - Ok(Some(BuilderTransactionCtx { - gas_used, - da_size, - signed_tx, - is_top_of_block: false, - })) - } - None => Ok(None), - } - } - - fn estimate_builder_tx_gas(&self, input: &[u8]) -> u64 { - // Count zero and non-zero bytes - let (zero_bytes, nonzero_bytes) = input.iter().fold((0, 0), |(zeros, nonzeros), &byte| { - if byte == 0 { - (zeros + 1, nonzeros) - } else { - (zeros, nonzeros + 1) - } - }); - - // Calculate gas cost (4 gas per zero byte, 16 gas per non-zero byte) - let zero_cost = zero_bytes * 4; - let nonzero_cost = nonzero_bytes * 16; - - // Tx gas should be not less than floor gas https://eips.ethereum.org/EIPS/eip-7623 - let tokens_in_calldata = zero_bytes + nonzero_bytes * 4; - let floor_gas = 21_000 + tokens_in_calldata * TOTAL_COST_FLOOR_PER_TOKEN; - - std::cmp::max(zero_cost + nonzero_cost + 21_000, floor_gas) - } - - fn signed_builder_tx( - &self, - ctx: &BuilderTxEnv<'_>, - db: impl DatabaseRef, - signer: Signer, - gas_used: u64, - message: Vec, - ) -> Result, BuilderTransactionError> { - let nonce = get_nonce(db, signer.address)?; - - // Create the EIP-1559 transaction - let tx = OpTypedTransaction::Eip1559(TxEip1559 { - chain_id: ctx.chain_id(), - nonce, - gas_limit: gas_used, - max_fee_per_gas: ctx.base_fee.into(), - max_priority_fee_per_gas: 0, - to: TxKind::Call(Address::ZERO), - // Include the message as part of the transaction data - input: message.into(), - ..Default::default() - }); - // Sign the transaction - let builder_tx = signer - .sign_tx(tx) - .map_err(BuilderTransactionError::SigningError)?; - - Ok(builder_tx) - } -} - -pub fn get_nonce(db: impl DatabaseRef, address: Address) -> Result { - db.basic_ref(address) - .map(|acc| acc.unwrap_or_default().nonce) - .map_err(|_| BuilderTransactionError::AccountLoadFailed(address)) -} - -pub fn get_balance( - db: impl DatabaseRef, - address: Address, -) -> Result { - db.basic_ref(address) - .map(|acc| acc.unwrap_or_default().balance) - .map_err(|_| BuilderTransactionError::AccountLoadFailed(address)) -} - -/// Adjust batch gas/DA/uncompressed limits to reserve capacity for bottom-of-block builder txs. -/// Returns the adjusted `max_uncompressed_block_size`. -pub(crate) fn reserve_builder_tx_budget( - builder_txs: &[BuilderTransactionCtx], - target_gas: &mut u64, - target_da: &mut Option, - target_da_footprint: &mut Option, - da_footprint_scalar: Option, - ctx_max_uncompressed: Option, - cumulative_uncompressed: u64, -) -> Option { - let bottom_txs = builder_txs.iter().filter(|tx| !tx.is_top_of_block); - - let mut builder_tx_gas: u64 = 0; - let mut builder_tx_da_size: u64 = 0; - let mut builder_tx_uncompressed_size: u64 = 0; - for tx in bottom_txs { - builder_tx_gas += tx.gas_used; - builder_tx_da_size += tx.da_size; - builder_tx_uncompressed_size += tx.signed_tx.inner().encode_2718_len() as u64; - } - - *target_gas = target_gas.saturating_sub(builder_tx_gas); - - if let Some(da_limit) = target_da.as_mut() { - *da_limit = da_limit.saturating_sub(builder_tx_da_size); - } - - if let (Some(footprint), Some(scalar)) = (target_da_footprint.as_mut(), da_footprint_scalar) { - *footprint = footprint.saturating_sub(builder_tx_da_size.saturating_mul(u64::from(scalar))); - } - - let max_uncompressed = - ctx_max_uncompressed.map(|limit| limit.saturating_sub(builder_tx_uncompressed_size)); - if let Some(limit) = ctx_max_uncompressed - && cumulative_uncompressed >= limit.saturating_sub(builder_tx_uncompressed_size) - { - error!( - target: "payload_builder", - current_uncompressed = cumulative_uncompressed, - reserved_builder_tx_uncompressed = builder_tx_uncompressed_size, - limit, - "Builder tx uncompressed size subtraction caused max_uncompressed_block_size to be 0. \ - No transaction would be included." - ); - } - - max_uncompressed -} - -#[cfg(test)] -mod tests { - use super::*; - - fn mock_builder_tx(gas_used: u64, da_size: u64, top_of_block: bool) -> BuilderTransactionCtx { - let signer = Signer::random(); - let signed = signer - .sign_tx(OpTypedTransaction::Legacy(Default::default())) - .unwrap(); - BuilderTransactionCtx { - gas_used, - da_size, - signed_tx: signed, - is_top_of_block: top_of_block, - } - } - - #[test] - fn test_reserve_budget_empty_txs() { - let mut gas = 1_000_000; - let mut da = Some(10_000u64); - let mut footprint = Some(50_000u64); - - let result = reserve_builder_tx_budget( - &[], - &mut gas, - &mut da, - &mut footprint, - Some(1), - Some(100_000), - 0, - ); - - assert_eq!(gas, 1_000_000, "gas unchanged"); - assert_eq!(da, Some(10_000), "da unchanged"); - assert_eq!(footprint, Some(50_000), "footprint unchanged"); - assert_eq!(result, Some(100_000), "uncompressed unchanged"); - } - - #[test] - fn test_reserve_budget_only_bottom_txs_counted() { - let txs = vec![ - mock_builder_tx(100_000, 500, true), // top-of-block — should be ignored - mock_builder_tx(50_000, 200, false), // bottom — counted - ]; - let mut gas = 1_000_000; - let mut da = Some(10_000u64); - let mut footprint = None; - - reserve_builder_tx_budget(&txs, &mut gas, &mut da, &mut footprint, None, None, 0); - - assert_eq!(gas, 950_000, "only bottom tx gas subtracted"); - assert_eq!(da, Some(9_800), "only bottom tx da subtracted"); - } - - #[test] - fn test_reserve_budget_saturating_sub() { - let txs = vec![mock_builder_tx(2_000_000, 50_000, false)]; - let mut gas = 1_000_000; - let mut da = Some(10_000u64); - let mut footprint = Some(100u64); - - reserve_builder_tx_budget(&txs, &mut gas, &mut da, &mut footprint, Some(1), None, 0); - - assert_eq!(gas, 0, "gas saturates at 0"); - assert_eq!(da, Some(0), "da saturates at 0"); - assert_eq!(footprint, Some(0), "footprint saturates at 0"); - } - - #[test] - fn test_reserve_budget_da_none_untouched() { - let txs = vec![mock_builder_tx(100, 200, false)]; - let mut gas = 1_000; - let mut da: Option = None; - let mut footprint: Option = None; - - reserve_builder_tx_budget(&txs, &mut gas, &mut da, &mut footprint, None, None, 0); - - assert_eq!(gas, 900); - assert_eq!(da, None, "None da stays None"); - assert_eq!(footprint, None, "None footprint stays None"); - } - - #[test] - fn test_reserve_budget_uncompressed_limit_reduction() { - let txs = vec![mock_builder_tx(0, 0, false)]; - // The tx has some encoded size even with default fields - let mut gas = 1_000_000; - - let result = reserve_builder_tx_budget( - &txs, - &mut gas, - &mut None, - &mut None, - None, - Some(1_000_000), - 0, - ); - - // Result should be Some(1_000_000 - encoded_size) - assert!(result.is_some()); - assert!( - result.unwrap() < 1_000_000, - "uncompressed limit reduced by tx encoded size" - ); - } -} diff --git a/crates/op-rbuilder/src/builder/builder_tx/env.rs b/crates/op-rbuilder/src/builder/builder_tx/env.rs new file mode 100644 index 00000000..d2b33c89 --- /dev/null +++ b/crates/op-rbuilder/src/builder/builder_tx/env.rs @@ -0,0 +1,21 @@ +use alloy_primitives::BlockHash; + +use crate::{evm::OpBlockEvmFactory, hardforks::ActiveHardforks}; + +/// Minimal environment needed by [`super::producer::BuilderTxProducer`] so that +/// builder-transaction code does not depend on the full payload-job context. +pub struct BuilderTxEnv<'a> { + pub evm_factory: &'a OpBlockEvmFactory, + pub hardforks: &'a ActiveHardforks, + pub base_fee: u64, + pub block_number: u64, + pub block_gas_limit: u64, + pub parent_hash: BlockHash, + pub max_uncompressed_block_size: Option, +} + +impl BuilderTxEnv<'_> { + pub fn chain_id(&self) -> u64 { + self.hardforks.chain_id() + } +} diff --git a/crates/op-rbuilder/src/builder/builder_tx/impls/claim.rs b/crates/op-rbuilder/src/builder/builder_tx/impls/claim.rs new file mode 100644 index 00000000..d7fea7ab --- /dev/null +++ b/crates/op-rbuilder/src/builder/builder_tx/impls/claim.rs @@ -0,0 +1,108 @@ +use alloy_consensus::TxEip1559; +use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN}; +use alloy_primitives::{Address, TxKind}; +use op_alloy_consensus::OpTypedTransaction; +use reth_optimism_primitives::OpTransactionSigned; +use reth_primitives_traits::Recovered; +use reth_provider::StateProvider; +use revm::DatabaseRef; +use std::sync::Arc; + +use crate::{ + builder::builder_tx::{ + BuilderTxEnv, BuilderTxError, BuilderTxProducer, SimulatedBuilderTx, SimulationState, + get_nonce, + }, + primitives::reth::ExecutionInfo, + tx_signer::Signer, +}; + +/// Builder transaction producer for the block claim: a tx from the builder's +/// address carrying a "Block Number: {}" message, publicly indicating we built +/// the block. +#[derive(Debug, Clone, derive_more::Constructor)] +pub(crate) struct ClaimBuilderTx { + pub signer: Option, +} + +impl BuilderTxProducer for ClaimBuilderTx { + fn simulate_builder_txs( + &self, + _state_provider: Arc, + _info: &ExecutionInfo, + env: &BuilderTxEnv<'_>, + sim_state: &mut SimulationState, + ) -> Result, BuilderTxError> { + Ok(self + .simulate_builder_tx(env, &mut *sim_state)? + .into_iter() + .collect()) + } +} + +impl ClaimBuilderTx { + fn simulate_builder_tx( + &self, + env: &BuilderTxEnv<'_>, + db: impl DatabaseRef, + ) -> Result, BuilderTxError> { + let Some(signer) = self.signer else { + return Ok(None); + }; + + let message: Vec = format!("Block Number: {}", env.block_number).into_bytes(); + let gas_used = self.estimate_builder_tx_gas(&message); + let signed_tx = self.signed_builder_tx(env, db, signer, gas_used, message)?; + let da_size = + op_alloy_flz::tx_estimated_size_fjord_bytes(signed_tx.encoded_2718().as_slice()); + + Ok(Some(SimulatedBuilderTx { + gas_used, + da_size, + signed_tx, + })) + } + + fn estimate_builder_tx_gas(&self, input: &[u8]) -> u64 { + let (zero_bytes, nonzero_bytes) = input.iter().fold((0, 0), |(zeros, nonzeros), &byte| { + if byte == 0 { + (zeros + 1, nonzeros) + } else { + (zeros, nonzeros + 1) + } + }); + + let zero_cost = zero_bytes * 4; + let nonzero_cost = nonzero_bytes * 16; + + let tokens_in_calldata = zero_bytes + nonzero_bytes * 4; + let floor_gas = 21_000 + tokens_in_calldata * TOTAL_COST_FLOOR_PER_TOKEN; + + std::cmp::max(zero_cost + nonzero_cost + 21_000, floor_gas) + } + + fn signed_builder_tx( + &self, + env: &BuilderTxEnv<'_>, + db: impl DatabaseRef, + signer: Signer, + gas_used: u64, + message: Vec, + ) -> Result, BuilderTxError> { + let nonce = get_nonce(db, signer.address)?; + + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: env.chain_id(), + nonce, + gas_limit: gas_used, + max_fee_per_gas: env.base_fee.into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(Address::ZERO), + input: message.into(), + ..Default::default() + }); + let builder_tx = signer.sign_tx(tx).map_err(BuilderTxError::SigningError)?; + + Ok(builder_tx) + } +} diff --git a/crates/op-rbuilder/src/builder/builder_tx/impls/flashblock_number.rs b/crates/op-rbuilder/src/builder/builder_tx/impls/flashblock_number.rs new file mode 100644 index 00000000..4c506962 --- /dev/null +++ b/crates/op-rbuilder/src/builder/builder_tx/impls/flashblock_number.rs @@ -0,0 +1,216 @@ +use alloy_eips::Encodable2718; +use alloy_evm::{Database, Evm}; +use alloy_op_evm::OpEvm; +use alloy_primitives::{Address, B256, Signature, U256}; +use alloy_rpc_types_eth::TransactionInput; +use alloy_sol_types::{SolCall, SolEvent, sol}; +use core::fmt::Debug; +use op_alloy_rpc_types::OpTransactionRequest; +use reth_evm::precompiles::PrecompilesMap; +use reth_provider::StateProvider; +use revm::{DatabaseRef, context_interface::Cfg as _, inspector::NoOpInspector}; +use std::sync::Arc; +use tracing::warn; + +use crate::{ + builder::builder_tx::{ + BuilderTxEnv, BuilderTxError, BuilderTxProducer, SimulatedBuilderTx, SimulationState, + SimulationSuccessResult, get_nonce, sign_tx, simulate_call, + }, + primitives::reth::ExecutionInfo, + tx_signer::Signer, +}; + +sol!( + // From https://github.com/Uniswap/flashblocks_number_contract/blob/main/src/FlashblockNumber.sol + #[sol(rpc, abi)] + #[derive(Debug)] + interface IFlashblockNumber { + uint256 public flashblockNumber; + + function incrementFlashblockNumber() external; + + function permitIncrementFlashblockNumber(uint256 currentFlashblockNumber, bytes memory signature) external; + + function computeStructHash(uint256 currentFlashblockNumber) external pure returns (bytes32); + + function hashTypedDataV4(bytes32 structHash) external view returns (bytes32); + + + // @notice Emitted when flashblock index is incremented + // @param newFlashblockIndex The new flashblock index (0-indexed within each L2 block) + event FlashblockIncremented(uint256 newFlashblockIndex); + + /// ----------------------------------------------------------------------- + /// Errors + /// ----------------------------------------------------------------------- + error NonBuilderAddress(address addr); + error MismatchedFlashblockNumber(uint256 expectedFlashblockNumber, uint256 actualFlashblockNumber); + } +); + +/// Builder transaction that increments the on-chain flashblock number contract. +/// Scheduled for non-first flashblocks via +/// [`crate::builder::builder_tx::ScheduledBuilderTx`]; the block claim tx is a +/// separate schedule entry for the first flashblock. +#[derive(Debug, Clone)] +pub(crate) struct FlashblockNumberBuilderTx { + pub signer: Signer, + pub flashblock_number_address: Address, + pub use_permit: bool, + /// TEE signer used only when `use_permit` is true to co-sign the permit. + pub tee_signer: Option, +} + +impl FlashblockNumberBuilderTx { + pub(crate) fn new( + signer: Signer, + flashblock_number_address: Address, + use_permit: bool, + tee_signer: Option, + ) -> Self { + Self { + signer, + flashblock_number_address, + use_permit, + tee_signer, + } + } + + fn signed_increment_flashblocks_tx( + &self, + env: &BuilderTxEnv<'_>, + evm: &mut OpEvm, + ) -> Result { + let calldata = IFlashblockNumber::incrementFlashblockNumberCall {}; + self.increment_flashblocks_tx(calldata, env, evm) + } + + fn increment_flashblocks_permit_signature( + &self, + flashtestations_signer: &Signer, + current_flashblock_number: U256, + env: &BuilderTxEnv<'_>, + evm: &mut OpEvm, + ) -> Result { + let struct_hash_calldata = IFlashblockNumber::computeStructHashCall { + currentFlashblockNumber: current_flashblock_number, + }; + let SimulationSuccessResult { output, .. } = + self.simulate_flashblocks_readonly_call(struct_hash_calldata, env, evm)?; + let typed_data_hash_calldata = + IFlashblockNumber::hashTypedDataV4Call { structHash: output }; + let SimulationSuccessResult { output, .. } = + self.simulate_flashblocks_readonly_call(typed_data_hash_calldata, env, evm)?; + let signature = flashtestations_signer.sign_message(output)?; + Ok(signature) + } + + fn signed_increment_flashblocks_permit_tx( + &self, + flashtestations_signer: &Signer, + env: &BuilderTxEnv<'_>, + evm: &mut OpEvm, + ) -> Result { + let current_flashblock_calldata = IFlashblockNumber::flashblockNumberCall {}; + let SimulationSuccessResult { output, .. } = + self.simulate_flashblocks_readonly_call(current_flashblock_calldata, env, evm)?; + let signature = + self.increment_flashblocks_permit_signature(flashtestations_signer, output, env, evm)?; + let calldata = IFlashblockNumber::permitIncrementFlashblockNumberCall { + currentFlashblockNumber: output, + signature: signature.as_bytes().into(), + }; + self.increment_flashblocks_tx(calldata, env, evm) + } + + fn increment_flashblocks_tx( + &self, + calldata: T, + env: &BuilderTxEnv<'_>, + evm: &mut OpEvm, + ) -> Result { + let SimulationSuccessResult { gas_used, .. } = self.simulate_flashblocks_call( + calldata.clone(), + vec![IFlashblockNumber::FlashblockIncremented::SIGNATURE_HASH], + env, + evm, + )?; + let signed_tx = sign_tx( + self.flashblock_number_address, + self.signer, + gas_used, + calldata.abi_encode().into(), + env, + evm.db_mut(), + )?; + let da_size = + op_alloy_flz::tx_estimated_size_fjord_bytes(signed_tx.encoded_2718().as_slice()); + Ok(SimulatedBuilderTx { + signed_tx, + gas_used, + da_size, + }) + } + + fn simulate_flashblocks_readonly_call( + &self, + calldata: T, + env: &BuilderTxEnv<'_>, + evm: &mut OpEvm, + ) -> Result, BuilderTxError> { + self.simulate_flashblocks_call(calldata, vec![], env, evm) + } + + fn simulate_flashblocks_call( + &self, + calldata: T, + expected_logs: Vec, + env: &BuilderTxEnv<'_>, + evm: &mut OpEvm, + ) -> Result, BuilderTxError> { + let simulation_gas_limit = env.block_gas_limit.min(evm.cfg_env().tx_gas_limit_cap()); + let tx_req = OpTransactionRequest::default() + .gas_limit(simulation_gas_limit) + .max_fee_per_gas(env.base_fee.into()) + .to(self.flashblock_number_address) + .from(self.signer.address) + .nonce(get_nonce(evm.db(), self.signer.address)?) + .input(TransactionInput::new(calldata.abi_encode().into())); + simulate_call::(tx_req, expected_logs, evm) + } +} + +impl BuilderTxProducer for FlashblockNumberBuilderTx { + fn simulate_builder_txs( + &self, + _state_provider: Arc, + _info: &ExecutionInfo, + env: &BuilderTxEnv<'_>, + sim_state: &mut SimulationState, + ) -> Result, BuilderTxError> { + let mut evm = env.evm_factory.evm(&mut *sim_state); + evm.modify_cfg(|cfg| { + cfg.disable_balance_check = true; + cfg.disable_block_gas_limit = true; + }); + + let num_tx = if let Some(tee_signer) = &self.tee_signer + && self.use_permit + { + self.signed_increment_flashblocks_permit_tx(tee_signer, env, &mut evm) + } else { + self.signed_increment_flashblocks_tx(env, &mut evm) + }; + + let tx = num_tx.inspect_err(|e| { + warn!( + target: "builder_tx", + error = %e, + "flashblocks number contract tx simulation failed" + ) + })?; + + Ok(vec![tx]) + } +} diff --git a/crates/op-rbuilder/src/builder/builder_tx/impls/mod.rs b/crates/op-rbuilder/src/builder/builder_tx/impls/mod.rs new file mode 100644 index 00000000..c105c38f --- /dev/null +++ b/crates/op-rbuilder/src/builder/builder_tx/impls/mod.rs @@ -0,0 +1,5 @@ +mod claim; +mod flashblock_number; + +pub(crate) use claim::ClaimBuilderTx; +pub(crate) use flashblock_number::FlashblockNumberBuilderTx; diff --git a/crates/op-rbuilder/src/builder/builder_tx/mod.rs b/crates/op-rbuilder/src/builder/builder_tx/mod.rs new file mode 100644 index 00000000..e58d23cb --- /dev/null +++ b/crates/op-rbuilder/src/builder/builder_tx/mod.rs @@ -0,0 +1,14 @@ +mod env; +mod impls; +mod producer; +mod schedule; +mod sim; + +pub use env::BuilderTxEnv; +pub use producer::{BuilderTxError, BuilderTxProducer, SimulatedBuilderTx}; +pub use schedule::{BuilderTxPosition, BuilderTxSchedule, ScheduledBuilderTx}; +pub use sim::{SimulationState, SimulationSuccessResult, get_nonce, sign_tx, simulate_call}; + +pub(crate) use schedule::reserve_builder_tx_budget; + +pub(super) use impls::{ClaimBuilderTx, FlashblockNumberBuilderTx}; diff --git a/crates/op-rbuilder/src/builder/builder_tx/producer.rs b/crates/op-rbuilder/src/builder/builder_tx/producer.rs new file mode 100644 index 00000000..a07b88b2 --- /dev/null +++ b/crates/op-rbuilder/src/builder/builder_tx/producer.rs @@ -0,0 +1,92 @@ +use alloy_primitives::{Address, B256}; +use alloy_sol_types::Revert; +use op_revm::OpHaltReason; +use reth_node_api::PayloadBuilderError; +use reth_optimism_primitives::OpTransactionSigned; +use reth_primitives_traits::Recovered; +use reth_provider::StateProvider; +use std::sync::Arc; + +use crate::primitives::reth::ExecutionInfo; + +use super::{env::BuilderTxEnv, sim::SimulationState}; + +#[derive(Debug, Clone)] +pub struct SimulatedBuilderTx { + pub gas_used: u64, + pub da_size: u64, + pub signed_tx: Recovered, +} + +#[derive(Debug, thiserror::Error)] +pub enum InvalidContractDataError { + #[error("did not find expected logs expected {0:?} but got {1:?}")] + InvalidLogs(Vec, Vec), + #[error("could not decode output from contract call")] + OutputAbiDecodeError, +} + +/// Possible error variants during construction of builder txs. +#[derive(Debug, thiserror::Error)] +pub enum BuilderTxError { + #[error("failed to load account {0}")] + AccountLoadFailed(Address), + #[error("failed to sign transaction: {0}")] + SigningError(secp256k1::Error), + #[error("contract {0} may be incorrect, invalid contract data: {1}")] + InvalidContract(Address, InvalidContractDataError), + #[error("transaction to {0} halted {1:?}")] + TransactionHalted(Address, OpHaltReason), + #[error("transaction to {0} reverted {1}")] + TransactionReverted(Address, Revert), + #[error("invalid transaction error {0}")] + InvalidTransactionError(Box), + #[error("evm execution error {0}")] + EvmExecutionError(Box), + #[error(transparent)] + Other(Box), +} + +impl From for BuilderTxError { + fn from(error: secp256k1::Error) -> Self { + BuilderTxError::SigningError(error) + } +} + +impl From for PayloadBuilderError { + fn from(error: BuilderTxError) -> Self { + match error { + BuilderTxError::EvmExecutionError(e) => PayloadBuilderError::EvmExecutionError(e), + _ => PayloadBuilderError::other(error), + } + } +} + +impl BuilderTxError { + pub fn other(error: impl core::error::Error + Send + Sync + 'static) -> Self { + BuilderTxError::Other(Box::new(error)) + } + + pub fn msg(msg: impl core::fmt::Display) -> Self { + Self::Other(msg.to_string().into()) + } +} + +pub trait BuilderTxProducer: Send + Sync { + /// Simulates and returns the signed builder transactions. + /// + /// The simulation commits changes to `sim_state`, which is a copy of the + /// real block state. The caller is responsible for creating `sim_state` via + /// [`super::sim::new_simulation_state`] and then re-executing the returned + /// transactions against the real database. + /// + /// Positioning is declared at scheduling time via + /// [`super::schedule::ScheduledBuilderTx`] and is not passed here. + fn simulate_builder_txs( + &self, + state_provider: Arc, + info: &ExecutionInfo, + env: &BuilderTxEnv<'_>, + sim_state: &mut SimulationState, + ) -> Result, BuilderTxError>; +} diff --git a/crates/op-rbuilder/src/builder/builder_tx/schedule.rs b/crates/op-rbuilder/src/builder/builder_tx/schedule.rs new file mode 100644 index 00000000..c36a0218 --- /dev/null +++ b/crates/op-rbuilder/src/builder/builder_tx/schedule.rs @@ -0,0 +1,427 @@ +use alloy_eips::Encodable2718; +use alloy_evm::Database; +use alloy_primitives::map::HashSet; +use reth_evm::{Evm, EvmError, InvalidTxError}; +use reth_provider::StateProvider; +use reth_revm::State; +use revm::context::result::ResultAndState; +use std::sync::Arc; +use tracing::{error, trace, warn}; + +use crate::{builder::builder_tx::sim::commit_tx, primitives::reth::ExecutionInfo}; + +use super::{ + env::BuilderTxEnv, + producer::{BuilderTxError, BuilderTxProducer, SimulatedBuilderTx}, + sim::new_simulation_state, +}; + +/// Where in the block build sequence a builder transaction runs. +/// +/// Each variant corresponds to a distinct dispatch point. The block builder +/// calls the matching [`BuilderTxSchedule`] method at each point without +/// passing any position flags. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BuilderTxPosition { + /// After sequencer transactions and before user transactions. + TopOfBlock, + /// Before user transactions in each flashblock. + TopOfFlashblock, + /// After user transactions in the last flashblock. + BottomOfBlock, +} + +/// Schedule entry pairing a producer with its dispatch position. +#[derive(derive_more::Constructor)] +pub struct ScheduledBuilderTx { + source: Arc, + position: BuilderTxPosition, +} + +impl std::fmt::Debug for ScheduledBuilderTx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ScheduledBuilderTx") + .field("position", &self.position) + .finish_non_exhaustive() + } +} + +/// The set of builder-transaction producers, each scheduled at a named point +/// in the block build sequence. +/// +/// Each method corresponds to a dispatch point. The block builder calls the +/// right method at the right moment without passing position flags. +#[derive(Debug, Default)] +pub struct BuilderTxSchedule { + scheduled: Vec, +} + +impl BuilderTxSchedule { + pub fn new(scheduled: Vec) -> Self { + Self { scheduled } + } + + pub fn commit_top_of_block( + &self, + state_provider: Arc, + info: &mut ExecutionInfo, + env: &BuilderTxEnv<'_>, + db: &mut State, + ) -> Result<(), BuilderTxError> { + commit_builder_txs( + &self.scheduled, + state_provider, + info, + env, + db, + BuilderTxPosition::TopOfBlock, + ) + } + + pub fn commit_top_of_flashblock( + &self, + state_provider: Arc, + info: &mut ExecutionInfo, + env: &BuilderTxEnv<'_>, + db: &mut State, + ) -> Result<(), BuilderTxError> { + commit_builder_txs( + &self.scheduled, + state_provider, + info, + env, + db, + BuilderTxPosition::TopOfFlashblock, + ) + } + + pub fn estimate_bottom_of_block( + &self, + state_provider: Arc, + info: &ExecutionInfo, + env: &BuilderTxEnv<'_>, + db: &State, + ) -> Vec { + simulate_scheduled( + &self.scheduled, + state_provider, + info, + env, + db, + BuilderTxPosition::BottomOfBlock, + ) + } + + pub fn commit_bottom_of_block( + &self, + state_provider: Arc, + info: &mut ExecutionInfo, + env: &BuilderTxEnv<'_>, + db: &mut State, + ) -> Result<(), BuilderTxError> { + commit_builder_txs( + &self.scheduled, + state_provider, + info, + env, + db, + BuilderTxPosition::BottomOfBlock, + ) + } +} + +/// Simulate the scheduled builder transactions matching `position` without +/// committing to any real state. +/// +/// Iterates `scheduled` in order with a shared simulation state (a throwaway +/// copy of `db`'s cache), committing each entry's txs to it so later entries +/// see correct nonces. The returned txs carry gas and DA estimates suitable +/// for `reserve_builder_tx_budget`. +pub(crate) fn simulate_scheduled( + scheduled: &[ScheduledBuilderTx], + state_provider: Arc, + info: &ExecutionInfo, + env: &BuilderTxEnv<'_>, + db: &State, + position: BuilderTxPosition, +) -> Vec { + let mut sim_state = new_simulation_state(state_provider.clone(), db); + + scheduled + .iter() + .filter(|scheduled| scheduled.position == position) + .flat_map(|scheduled| { + let txs = scheduled + .source + .simulate_builder_txs(state_provider.clone(), info, env, &mut sim_state) + .inspect_err(|e| { + error!( + target: "payload_builder", + error = %e, + position = ?position, + "Error simulating builder txs" + ); + }) + .unwrap_or_default(); + + txs.iter().for_each(|tx| { + if let Err(e) = commit_tx(tx.signed_tx.clone(), env, &mut sim_state) { + warn!( + target: "payload_builder", + error = %e, + "failed to commit builder txs to simulation state" + ); + } + }); + + txs + }) + .collect() +} + +/// Simulate builder transactions and commit the returned ones to the real `db`. +pub(crate) fn commit_builder_txs( + scheduled: &[ScheduledBuilderTx], + state_provider: Arc, + info: &mut ExecutionInfo, + env: &BuilderTxEnv<'_>, + db: &mut State, + position: BuilderTxPosition, +) -> Result<(), BuilderTxError> { + let builder_txs = simulate_scheduled(scheduled, state_provider, info, env, db, position); + + let mut evm = env.evm_factory.evm(&mut *db); + + let mut invalid = HashSet::new(); + + for builder_tx in builder_txs.iter() { + if invalid.contains(&builder_tx.signed_tx.signer()) { + warn!( + target: "payload_builder", + tx_hash = %builder_tx.signed_tx.tx_hash(), + "builder signer invalid as previous builder tx reverted" + ); + continue; + } + + let ResultAndState { result, state } = match evm.transact(&builder_tx.signed_tx) { + Ok(res) => res, + Err(err) => { + if let Some(err) = err.as_invalid_tx_err() { + if err.is_nonce_too_low() { + trace!( + target: "payload_builder", + error = %err, + tx_hash = %builder_tx.signed_tx.tx_hash(), + "skipping nonce too low builder transaction" + ); + } else { + trace!( + target: "payload_builder", + error = %err, + tx_hash = %builder_tx.signed_tx.tx_hash(), + "skipping invalid builder transaction and its descendants" + ); + invalid.insert(builder_tx.signed_tx.signer()); + } + continue; + } + return Err(BuilderTxError::EvmExecutionError(Box::new(err))); + } + }; + + if !result.is_success() { + warn!( + target: "payload_builder", + tx_hash = %builder_tx.signed_tx.tx_hash(), + result = ?result, + "builder tx reverted" + ); + invalid.insert(builder_tx.signed_tx.signer()); + continue; + } + + let tx_uncompressed_size = builder_tx.signed_tx.inner().encode_2718_len() as u64; + + if let Some(limit) = env.max_uncompressed_block_size + && info.cumulative_uncompressed_bytes + tx_uncompressed_size > limit + { + warn!( + target: "payload_builder", + tx_hash = %builder_tx.signed_tx.tx_hash(), + cumulative_uncompressed = info.cumulative_uncompressed_bytes, + tx_uncompressed_size, + limit, + "skipping builder tx: would exceed max uncompressed block size" + ); + continue; + } + + info.commit_tx( + &builder_tx.signed_tx, + result, + state, + builder_tx.da_size, + None, + None, + env.evm_factory, + env.hardforks, + &mut evm, + ); + } + + Ok(()) +} + +/// Adjust batch gas/DA/uncompressed limits to reserve capacity for the provided builder txs. +/// `builder_txs` should be the bottom-of-block txs that have not yet been committed. +/// Returns the adjusted `max_uncompressed_block_size`. +pub(crate) fn reserve_builder_tx_budget( + builder_txs: &[SimulatedBuilderTx], + target_gas: &mut u64, + target_da: &mut Option, + target_da_footprint: &mut Option, + da_footprint_scalar: Option, + env_max_uncompressed: Option, + cumulative_uncompressed: u64, +) -> Option { + let mut builder_tx_gas: u64 = 0; + let mut builder_tx_da_size: u64 = 0; + let mut builder_tx_uncompressed_size: u64 = 0; + for tx in builder_txs { + builder_tx_gas += tx.gas_used; + builder_tx_da_size += tx.da_size; + builder_tx_uncompressed_size += tx.signed_tx.inner().encode_2718_len() as u64; + } + + *target_gas = target_gas.saturating_sub(builder_tx_gas); + + if let Some(da_limit) = target_da.as_mut() { + *da_limit = da_limit.saturating_sub(builder_tx_da_size); + } + + if let (Some(footprint), Some(scalar)) = (target_da_footprint.as_mut(), da_footprint_scalar) { + *footprint = footprint.saturating_sub(builder_tx_da_size.saturating_mul(u64::from(scalar))); + } + + let max_uncompressed = + env_max_uncompressed.map(|limit| limit.saturating_sub(builder_tx_uncompressed_size)); + if let Some(limit) = env_max_uncompressed + && cumulative_uncompressed >= limit.saturating_sub(builder_tx_uncompressed_size) + { + error!( + target: "payload_builder", + current_uncompressed = cumulative_uncompressed, + reserved_builder_tx_uncompressed = builder_tx_uncompressed_size, + limit, + "Builder tx uncompressed size subtraction caused max_uncompressed_block_size to be 0. \ + No transaction would be included." + ); + } + + max_uncompressed +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tx_signer::Signer; + use op_alloy_consensus::OpTypedTransaction; + + fn mock_builder_tx(gas_used: u64, da_size: u64) -> SimulatedBuilderTx { + let signer = Signer::random(); + let signed = signer + .sign_tx(OpTypedTransaction::Legacy(Default::default())) + .unwrap(); + SimulatedBuilderTx { + gas_used, + da_size, + signed_tx: signed, + } + } + + #[test] + fn test_reserve_budget_empty_txs() { + let mut gas = 1_000_000; + let mut da = Some(10_000u64); + let mut footprint = Some(50_000u64); + + let result = reserve_builder_tx_budget( + &[], + &mut gas, + &mut da, + &mut footprint, + Some(1), + Some(100_000), + 0, + ); + + assert_eq!(gas, 1_000_000, "gas unchanged"); + assert_eq!(da, Some(10_000), "da unchanged"); + assert_eq!(footprint, Some(50_000), "footprint unchanged"); + assert_eq!(result, Some(100_000), "uncompressed unchanged"); + } + + #[test] + fn test_reserve_budget_bottom_txs_counted() { + let txs = vec![mock_builder_tx(50_000, 200)]; + let mut gas = 1_000_000; + let mut da = Some(10_000u64); + let mut footprint = None; + + reserve_builder_tx_budget(&txs, &mut gas, &mut da, &mut footprint, None, None, 0); + + assert_eq!(gas, 950_000, "bottom tx gas subtracted"); + assert_eq!(da, Some(9_800), "bottom tx da subtracted"); + } + + #[test] + fn test_reserve_budget_saturating_sub() { + let txs = vec![mock_builder_tx(2_000_000, 50_000)]; + let mut gas = 1_000_000; + let mut da = Some(10_000u64); + let mut footprint = Some(100u64); + + reserve_builder_tx_budget(&txs, &mut gas, &mut da, &mut footprint, Some(1), None, 0); + + assert_eq!(gas, 0, "gas saturates at 0"); + assert_eq!(da, Some(0), "da saturates at 0"); + assert_eq!(footprint, Some(0), "footprint saturates at 0"); + } + + #[test] + fn test_reserve_budget_da_none_untouched() { + let txs = vec![mock_builder_tx(100, 200)]; + let mut gas = 1_000; + let mut da: Option = None; + let mut footprint: Option = None; + + reserve_builder_tx_budget(&txs, &mut gas, &mut da, &mut footprint, None, None, 0); + + assert_eq!(gas, 900); + assert_eq!(da, None, "None da stays None"); + assert_eq!(footprint, None, "None footprint stays None"); + } + + #[test] + fn test_reserve_budget_uncompressed_limit_reduction() { + let txs = vec![mock_builder_tx(0, 0)]; + let mut gas = 1_000_000; + + let result = reserve_builder_tx_budget( + &txs, + &mut gas, + &mut None, + &mut None, + None, + Some(1_000_000), + 0, + ); + + assert!(result.is_some()); + assert!( + result.unwrap() < 1_000_000, + "uncompressed limit reduced by tx encoded size" + ); + } +} diff --git a/crates/op-rbuilder/src/builder/builder_tx/sim.rs b/crates/op-rbuilder/src/builder/builder_tx/sim.rs new file mode 100644 index 00000000..f3e40305 --- /dev/null +++ b/crates/op-rbuilder/src/builder/builder_tx/sim.rs @@ -0,0 +1,194 @@ +use alloy_consensus::TxEip1559; +use alloy_evm::{Database, rpc::TryIntoTxEnv}; +use alloy_op_evm::{OpEvm, OpTx}; +use alloy_primitives::{Address, B256, Bytes, TxKind, map::HashSet}; +use alloy_sol_types::{ContractError, Revert, SolCall, SolError, SolInterface}; +use core::fmt::Debug; +use op_alloy_consensus::OpTypedTransaction; +use op_alloy_rpc_types::OpTransactionRequest; +use op_revm::OpTransactionError; +use reth_evm::{Evm, EvmError, InvalidTxError, precompiles::PrecompilesMap}; +use reth_optimism_primitives::OpTransactionSigned; +use reth_primitives_traits::Recovered; +use reth_provider::{ProviderError, StateProvider}; +use reth_revm::{State, database::StateProviderDatabase}; +use reth_rpc_api::eth::EthTxEnvError; +use revm::{ + DatabaseCommit, DatabaseRef, + context::result::{EVMError, ExecutionResult, ResultAndState}, + inspector::NoOpInspector, +}; +use std::sync::Arc; +use tracing::trace; + +use crate::tx_signer::Signer; + +use super::{ + env::BuilderTxEnv, + producer::{BuilderTxError, InvalidContractDataError}, +}; + +#[derive(Debug, Default)] +pub struct SimulationSuccessResult { + pub gas_used: u64, + pub output: T::Return, + pub state_changes: revm::state::EvmState, +} + +/// The concrete state type used for simulating builder transactions. +pub type SimulationState = State>>; + +/// Creates a copy of the state backed by `state_provider` to simulate against. +pub(super) fn new_simulation_state( + state_provider: Arc, + db: &State, +) -> SimulationState { + let state = StateProviderDatabase::new(state_provider); + + State::builder() + .with_database(state) + .with_cached_prestate(db.cache.clone()) + .with_bundle_update() + .build() +} + +pub fn sign_tx( + to: Address, + from: Signer, + gas_used: u64, + calldata: Bytes, + env: &BuilderTxEnv<'_>, + db: impl DatabaseRef, +) -> Result, BuilderTxError> { + let nonce = get_nonce(db, from.address)?; + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: env.chain_id(), + nonce, + // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer + gas_limit: gas_used * 64 / 63, + max_fee_per_gas: env.base_fee.into(), + to: TxKind::Call(to), + input: calldata, + ..Default::default() + }); + Ok(from.sign_tx(tx)?) +} + +/// Commit a signed transaction to the state. +/// +/// Transactions whose nonce is too low are skipped rather than treated as +/// errors: an implementor of [`super::producer::BuilderTxProducer`] may commit +/// its own txs' state during simulation (e.g. to chain nonces between its own +/// txs), so re-executing those here is expected to no-op. +pub(super) fn commit_tx( + signed_tx: Recovered, + env: &BuilderTxEnv<'_>, + db: &mut State, +) -> Result<(), BuilderTxError> { + let mut evm = env.evm_factory.evm(&mut *db); + + let ResultAndState { state, .. } = match evm.transact(&signed_tx) { + Ok(res) => res, + Err(err) => { + if let Some(invalid) = err.as_invalid_tx_err() + && invalid.is_nonce_too_low() + { + trace!( + target: "payload_builder", + tx_hash = %signed_tx.tx_hash(), + "skipping already-committed builder transaction" + ); + return Ok(()); + } else { + return Err(BuilderTxError::EvmExecutionError(Box::new(err))); + } + } + }; + evm.db_mut().commit(state); + + Ok(()) +} + +pub fn simulate_call( + tx: OpTransactionRequest, + expected_logs: Vec, + evm: &mut OpEvm, +) -> Result, BuilderTxError> { + let evm_env = alloy_evm::EvmEnv::new(evm.cfg_env().clone(), evm.block().clone()); + let tx_env: revm::context::TxEnv = tx.as_ref().clone().try_into_tx_env(&evm_env)?; + let to = tx_env.kind.into_to().unwrap_or_default(); + let op_tx = OpTx(op_revm::OpTransaction { + base: tx_env, + enveloped_tx: Some(Bytes::new()), + deposit: Default::default(), + }); + + let ResultAndState { result, state } = match evm.transact(op_tx) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + return Err(BuilderTxError::InvalidTransactionError(Box::new(err))); + } else { + return Err(BuilderTxError::EvmExecutionError(Box::new(err))); + } + } + }; + let gas_used = result.tx_gas_used(); + + match result { + ExecutionResult::Success { output, logs, .. } => { + let topics: HashSet = logs + .into_iter() + .flat_map(|log| log.topics().to_vec()) + .collect(); + if !expected_logs + .iter() + .all(|expected_topic| topics.contains(expected_topic)) + { + return Err(BuilderTxError::InvalidContract( + to, + InvalidContractDataError::InvalidLogs( + expected_logs, + topics.into_iter().collect(), + ), + )); + } + let return_output = T::abi_decode_returns(&output.into_data()).map_err(|_| { + BuilderTxError::InvalidContract(to, InvalidContractDataError::OutputAbiDecodeError) + })?; + Ok(SimulationSuccessResult:: { + gas_used, + output: return_output, + state_changes: state, + }) + } + ExecutionResult::Revert { output, .. } => { + let revert = ContractError::::abi_decode(&output) + .map(|reason| Revert::from(format!("{reason:?}"))) + .or_else(|_| Revert::abi_decode(&output)) + .unwrap_or_else(|_| { + Revert::from(format!("unknown revert: {}", hex::encode(&output))) + }); + Err(BuilderTxError::TransactionReverted(to, revert)) + } + ExecutionResult::Halt { reason, .. } => Err(BuilderTxError::TransactionHalted(to, reason)), + } +} + +pub fn get_nonce(db: impl DatabaseRef, address: Address) -> Result { + db.basic_ref(address) + .map(|acc| acc.unwrap_or_default().nonce) + .map_err(|_| BuilderTxError::AccountLoadFailed(address)) +} + +impl From> for BuilderTxError { + fn from(error: EVMError) -> Self { + BuilderTxError::EvmExecutionError(Box::new(error)) + } +} + +impl From for BuilderTxError { + fn from(error: EthTxEnvError) -> Self { + BuilderTxError::EvmExecutionError(Box::new(error)) + } +} diff --git a/crates/op-rbuilder/src/builder/continuous/candidate_loop.rs b/crates/op-rbuilder/src/builder/continuous/candidate_loop.rs index 248b0c16..6df29b9e 100644 --- a/crates/op-rbuilder/src/builder/continuous/candidate_loop.rs +++ b/crates/op-rbuilder/src/builder/continuous/candidate_loop.rs @@ -5,7 +5,7 @@ use super::{ use crate::{ builder::{ best_txs::{FlashblockPoolTxCursor, FlashblockTxTracker}, - builder_tx::{BuilderTransactions, reserve_builder_tx_budget}, + builder_tx::reserve_builder_tx_budget, context::OpPayloadJobCtx, payload::{FlashblocksState, OpPayloadBuilder}, state_root::StateRootCalculator, @@ -19,13 +19,13 @@ use op_alloy_rpc_types_engine::OpFlashblockPayload; use reth_optimism_node::OpBuiltPayload; use reth_payload_util::BestPayloadTransactions; use reth_provider::{ - HashedPostStateProvider, ProviderError, StateRootProvider, StorageRootProvider, + HashedPostStateProvider, ProviderError, StateProvider, StateRootProvider, StorageRootProvider, }; use reth_revm::State; use revm::Database; use std::{ mem, - sync::atomic::Ordering, + sync::{Arc, atomic::Ordering}, time::{Duration, Instant}, }; use tokio_util::sync::CancellationToken; @@ -37,11 +37,10 @@ use tracing::{error, field, info, metadata::Level, span, warn}; // clones state, executes txs, seals a flashblock, and writes the new // best into [`SharedBest`] until its per-interval cancel fires. -impl OpPayloadBuilder +impl OpPayloadBuilder where Pool: PoolBounds + 'static, Client: ClientBounds + 'static, - BuilderTx: BuilderTransactions + Send + Sync + 'static, { #[expect(clippy::too_many_arguments)] fn build_empty_flashblock_candidate< @@ -53,20 +52,19 @@ where fb_state: &mut FlashblocksState, info: &mut ExecutionInfo, state: &mut State, - state_provider: impl reth::providers::StateProvider + Clone, + state_provider: Arc, state_root_calc: &mut StateRootCalculator, target_da_for_batch: Option, target_da_footprint_for_batch: Option, ) -> eyre::Result<(FlashblocksState, OpBuiltPayload, OpFlashblockPayload)> { - if let Err(e) = self.builder_tx().add_builder_txs( - &state_provider, - info, - &ctx.builder_tx_env(), - state, - false, - fb_state.is_first_flashblock(), - fb_state.is_last_flashblock(), - ) { + if fb_state.is_last_flashblock() + && let Err(e) = self.builder_tx_schedule().commit_bottom_of_block( + state_provider, + info, + &ctx.builder_tx_env(), + state, + ) + { error!( target: "payload_builder", "Error adding bottom builder txs to empty flashblock candidate: {}", @@ -107,7 +105,7 @@ where fb_state: &mut FlashblocksState, info: &mut ExecutionInfo, state: &mut State, - state_provider: impl reth::providers::StateProvider + Clone, + state_provider: Arc, tx_tracker: &mut FlashblockTxTracker, state_root_calc: &mut StateRootCalculator, block_cancel: &CancellationToken, @@ -131,24 +129,41 @@ where "continuous: starting candidate loop", ); - let builder_txs = self - .builder_tx() - .add_builder_txs( - &state_provider, + let schedule = self.builder_tx_schedule(); + if fb_state.is_first_flashblock() { + schedule + .commit_top_of_block(state_provider.clone(), info, &ctx.builder_tx_env(), state) + .inspect_err( + |e| error!(target: "payload_builder", "Error simulating builder txs: {}", e), + ) + .ok(); + } else { + schedule + .commit_top_of_flashblock( + state_provider.clone(), + info, + &ctx.builder_tx_env(), + state, + ) + .inspect_err( + |e| error!(target: "payload_builder", "Error simulating builder txs: {}", e), + ) + .ok(); + } + + let bottom_builder_txs_estimate = if fb_state.is_last_flashblock() { + self.builder_tx_schedule().estimate_bottom_of_block( + state_provider.clone(), info, &ctx.builder_tx_env(), state, - true, - fb_state.is_first_flashblock(), - fb_state.is_last_flashblock(), - ) - .inspect_err( - |e| error!(target: "payload_builder", "Error simulating builder txs: {}", e), ) - .unwrap_or_default(); + } else { + vec![] + }; let max_uncompressed_block_size = reserve_builder_tx_budget( - &builder_txs, + &bottom_builder_txs_estimate, &mut target_gas_for_batch, &mut target_da_for_batch, &mut target_da_footprint_for_batch, @@ -190,7 +205,7 @@ where &mut empty_candidate_fb_state, &mut empty_candidate_info, state, - &state_provider, + state_provider.clone(), &mut empty_candidate_state_root_calc, target_da_for_batch, target_da_footprint_for_batch, @@ -325,15 +340,14 @@ where break; } - if let Err(e) = self.builder_tx().add_builder_txs( - &state_provider, - &mut sim_info, - &ctx.builder_tx_env(), - state, - false, - sim_fb_state.is_first_flashblock(), - sim_fb_state.is_last_flashblock(), - ) { + if sim_fb_state.is_last_flashblock() + && let Err(e) = self.builder_tx_schedule().commit_bottom_of_block( + state_provider.clone(), + &mut sim_info, + &ctx.builder_tx_env(), + state, + ) + { error!(target: "payload_builder", "Error adding bottom builder txs: {}", e); } diff --git a/crates/op-rbuilder/src/builder/continuous/interval.rs b/crates/op-rbuilder/src/builder/continuous/interval.rs index 03bc9c99..5bf2517f 100644 --- a/crates/op-rbuilder/src/builder/continuous/interval.rs +++ b/crates/op-rbuilder/src/builder/continuous/interval.rs @@ -4,7 +4,6 @@ use super::{ }; use crate::{ builder::{ - builder_tx::BuilderTransactions, cancellation::{FlashblockJobCancellation, PayloadJobCancellation}, payload::{BuildProgress, BuildState, JobDeps, OpPayloadBuilder, PayloadBuildStats}, }, @@ -12,8 +11,9 @@ use crate::{ }; use alloy_primitives::B256; use reth_node_api::PayloadBuilderError; +use reth_provider::StateProvider; use reth_revm::{State, database::StateProviderDatabase}; -use std::{ops::ControlFlow, time::Instant}; +use std::{ops::ControlFlow, sync::Arc, time::Instant}; use tokio::sync::{mpsc, oneshot}; use tracing::{debug, field, metadata::Level, span}; @@ -23,11 +23,10 @@ use tracing::{debug, field, metadata::Level, span}; // trigger channel. On each trigger it advances one interval; on cancel // or end-of-block it returns `Ok(())`. -impl OpPayloadBuilder +impl OpPayloadBuilder where Pool: PoolBounds + 'static, Client: ClientBounds + 'static, - BuilderTx: BuilderTransactions + Send + Sync + 'static, { /// Entrypoint to build a single payload in continuous mode, it should be called soon after FCU. /// It handles the top-level loop: sets up state then iterates flashblock intervals. @@ -162,9 +161,10 @@ where let _enter = fb_span.enter(); let base_state = base_state; - let state_provider = builder.client().state_by_block_hash(parent_hash)?; + let state_provider: Arc = + Arc::from(builder.client().state_by_block_hash(parent_hash)?); let mut state_db = State::builder() - .with_database(StateProviderDatabase::new(&state_provider)) + .with_database(StateProviderDatabase::new(state_provider.clone())) .with_cached_prestate(base_state.cache) .with_bundle_update() .build(); @@ -181,7 +181,7 @@ where &mut fb_state, &mut info, &mut state_db, - &state_provider, + state_provider.clone(), &mut tx_tracker, &mut state_root_calc, &block_cancel, diff --git a/crates/op-rbuilder/src/builder/continuous/publish.rs b/crates/op-rbuilder/src/builder/continuous/publish.rs index ad41c68e..e9dd0655 100644 --- a/crates/op-rbuilder/src/builder/continuous/publish.rs +++ b/crates/op-rbuilder/src/builder/continuous/publish.rs @@ -7,7 +7,6 @@ use super::{ }; use crate::{ builder::{ - builder_tx::BuilderTransactions, cancellation::FlashblockJobCancellation, context::OpPayloadJobCtx, payload::{ @@ -95,11 +94,10 @@ impl CandidateLogEvent { } } -impl OpPayloadBuilder +impl OpPayloadBuilder where Pool: PoolBounds + 'static, Client: ClientBounds + 'static, - BuilderTx: BuilderTransactions + Send + Sync + 'static, { /// Publish a candidate flashblock immediately. The context is only used for /// publish timing metadata, so this can run before awaiting the build task. diff --git a/crates/op-rbuilder/src/builder/flashblocks_builder_tx.rs b/crates/op-rbuilder/src/builder/flashblocks_builder_tx.rs deleted file mode 100644 index 008cc5c0..00000000 --- a/crates/op-rbuilder/src/builder/flashblocks_builder_tx.rs +++ /dev/null @@ -1,352 +0,0 @@ -use alloy_eips::Encodable2718; -use alloy_evm::{Database, Evm}; -use alloy_op_evm::OpEvm; -use alloy_primitives::{Address, B256, Signature, U256}; -use alloy_rpc_types_eth::TransactionInput; -use alloy_sol_types::{SolCall, SolEvent, sol}; -use core::fmt::Debug; -use op_alloy_rpc_types::OpTransactionRequest; -use reth_evm::precompiles::PrecompilesMap; -use reth_provider::StateProvider; -use reth_revm::State; -use revm::{DatabaseRef, context_interface::Cfg as _, inspector::NoOpInspector}; -use tracing::warn; - -use crate::{ - builder::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, - SimulationSuccessResult, - builder_tx::{BuilderTxBase, BuilderTxEnv}, - get_nonce, - }, - flashtestations::builder_tx::FlashtestationsBuilderTx, - primitives::reth::ExecutionInfo, - tx_signer::Signer, -}; - -sol!( - // From https://github.com/Uniswap/flashblocks_number_contract/blob/main/src/FlashblockNumber.sol - #[sol(rpc, abi)] - #[derive(Debug)] - interface IFlashblockNumber { - uint256 public flashblockNumber; - - function incrementFlashblockNumber() external; - - function permitIncrementFlashblockNumber(uint256 currentFlashblockNumber, bytes memory signature) external; - - function computeStructHash(uint256 currentFlashblockNumber) external pure returns (bytes32); - - function hashTypedDataV4(bytes32 structHash) external view returns (bytes32); - - - // @notice Emitted when flashblock index is incremented - // @param newFlashblockIndex The new flashblock index (0-indexed within each L2 block) - event FlashblockIncremented(uint256 newFlashblockIndex); - - /// ----------------------------------------------------------------------- - /// Errors - /// ----------------------------------------------------------------------- - error NonBuilderAddress(address addr); - error MismatchedFlashblockNumber(uint256 expectedFlashblockNumber, uint256 actualFlashblockNumber); - } -); - -// This will be the end of block transaction of a regular block -#[derive(Debug, Clone)] -pub(super) struct FlashblocksBuilderTx { - pub base_builder_tx: BuilderTxBase, - pub flashtestations_builder_tx: Option, -} - -impl FlashblocksBuilderTx { - pub(super) fn new( - signer: Option, - flashtestations_builder_tx: Option, - ) -> Self { - let base_builder_tx = BuilderTxBase::new(signer); - Self { - base_builder_tx, - flashtestations_builder_tx, - } - } -} - -impl BuilderTransactions for FlashblocksBuilderTx { - fn simulate_builder_txs( - &self, - state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, - ctx: &BuilderTxEnv<'_>, - db: &mut State, - top_of_block: bool, - is_first_flashblock: bool, - is_last_flashblock: bool, - ) -> Result, BuilderTransactionError> { - let mut builder_txs = Vec::::new(); - - if is_first_flashblock { - let flashblocks_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, &mut *db)?; - builder_txs.extend(flashblocks_builder_tx); - } - - if is_last_flashblock { - let base_tx = self.base_builder_tx.simulate_builder_tx(ctx, &mut *db)?; - builder_txs.extend(base_tx.clone()); - - if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { - // Commit state that is included to get the correct nonce - if let Some(builder_tx) = base_tx { - self.commit_txs(vec![builder_tx.signed_tx], ctx, &mut *db)?; - } - // We only include flashtestations txs in the last flashblock - match flashtestations_builder_tx.simulate_builder_txs( - state_provider, - info, - ctx, - db, - top_of_block, - is_first_flashblock, - is_last_flashblock, - ) { - Ok(flashtestations_builder_txs) => { - builder_txs.extend(flashtestations_builder_txs) - } - Err(e) => { - warn!( - target: "flashtestations", - error = %e, - "failed to add flashtestations builder tx" - ) - } - } - } - } - Ok(builder_txs) - } -} - -// This will be the end of block transaction of a regular block -#[derive(Debug, Clone)] -pub(super) struct FlashblocksNumberBuilderTx { - pub signer: Signer, - pub flashblock_number_address: Address, - pub use_permit: bool, - pub base_builder_tx: BuilderTxBase, - pub flashtestations_builder_tx: Option, -} - -impl FlashblocksNumberBuilderTx { - pub(super) fn new( - signer: Signer, - flashblock_number_address: Address, - use_permit: bool, - flashtestations_builder_tx: Option, - ) -> Self { - let base_builder_tx = BuilderTxBase::new(Some(signer)); - Self { - signer, - flashblock_number_address, - use_permit, - base_builder_tx, - flashtestations_builder_tx, - } - } - - fn signed_increment_flashblocks_tx( - &self, - ctx: &BuilderTxEnv<'_>, - evm: &mut OpEvm, - ) -> Result { - let calldata = IFlashblockNumber::incrementFlashblockNumberCall {}; - self.increment_flashblocks_tx(calldata, ctx, evm) - } - - fn increment_flashblocks_permit_signature( - &self, - flashtestations_signer: &Signer, - current_flashblock_number: U256, - ctx: &BuilderTxEnv<'_>, - evm: &mut OpEvm, - ) -> Result { - let struct_hash_calldata = IFlashblockNumber::computeStructHashCall { - currentFlashblockNumber: current_flashblock_number, - }; - let SimulationSuccessResult { output, .. } = - self.simulate_flashblocks_readonly_call(struct_hash_calldata, ctx, evm)?; - let typed_data_hash_calldata = - IFlashblockNumber::hashTypedDataV4Call { structHash: output }; - let SimulationSuccessResult { output, .. } = - self.simulate_flashblocks_readonly_call(typed_data_hash_calldata, ctx, evm)?; - let signature = flashtestations_signer.sign_message(output)?; - Ok(signature) - } - - fn signed_increment_flashblocks_permit_tx( - &self, - flashtestations_signer: &Signer, - ctx: &BuilderTxEnv<'_>, - evm: &mut OpEvm, - ) -> Result { - let current_flashblock_calldata = IFlashblockNumber::flashblockNumberCall {}; - let SimulationSuccessResult { output, .. } = - self.simulate_flashblocks_readonly_call(current_flashblock_calldata, ctx, evm)?; - let signature = - self.increment_flashblocks_permit_signature(flashtestations_signer, output, ctx, evm)?; - let calldata = IFlashblockNumber::permitIncrementFlashblockNumberCall { - currentFlashblockNumber: output, - signature: signature.as_bytes().into(), - }; - self.increment_flashblocks_tx(calldata, ctx, evm) - } - - fn increment_flashblocks_tx( - &self, - calldata: T, - ctx: &BuilderTxEnv<'_>, - evm: &mut OpEvm, - ) -> Result { - let SimulationSuccessResult { gas_used, .. } = self.simulate_flashblocks_call( - calldata.clone(), - vec![IFlashblockNumber::FlashblockIncremented::SIGNATURE_HASH], - ctx, - evm, - )?; - let signed_tx = self.sign_tx( - self.flashblock_number_address, - self.signer, - gas_used, - calldata.abi_encode().into(), - ctx, - evm.db_mut(), - )?; - let da_size = - op_alloy_flz::tx_estimated_size_fjord_bytes(signed_tx.encoded_2718().as_slice()); - Ok(BuilderTransactionCtx { - signed_tx, - gas_used, - da_size, - is_top_of_block: true, - }) - } - - fn simulate_flashblocks_readonly_call( - &self, - calldata: T, - ctx: &BuilderTxEnv<'_>, - evm: &mut OpEvm, - ) -> Result, BuilderTransactionError> { - self.simulate_flashblocks_call(calldata, vec![], ctx, evm) - } - - fn simulate_flashblocks_call( - &self, - calldata: T, - expected_logs: Vec, - ctx: &BuilderTxEnv<'_>, - evm: &mut OpEvm, - ) -> Result, BuilderTransactionError> { - let simulation_gas_limit = ctx.block_gas_limit.min(evm.cfg_env().tx_gas_limit_cap()); - let tx_req = OpTransactionRequest::default() - .gas_limit(simulation_gas_limit) - .max_fee_per_gas(ctx.base_fee.into()) - .to(self.flashblock_number_address) - .from(self.signer.address) // use tee key as signer for simulations - .nonce(get_nonce(evm.db(), self.signer.address)?) - .input(TransactionInput::new(calldata.abi_encode().into())); - self.simulate_call::( - tx_req, - expected_logs, - evm, - ) - } -} - -impl BuilderTransactions for FlashblocksNumberBuilderTx { - fn simulate_builder_txs( - &self, - state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, - ctx: &BuilderTxEnv<'_>, - db: &mut State, - top_of_block: bool, - is_first_flashblock: bool, - is_last_flashblock: bool, - ) -> Result, BuilderTransactionError> { - let mut builder_txs = Vec::::new(); - - if is_first_flashblock { - // fallback block builder tx - builder_txs.extend(self.base_builder_tx.simulate_builder_tx(ctx, &mut *db)?); - } else { - // we increment the flashblock number for the next flashblock so we don't increment in the last flashblock - let mut evm = ctx.evm_factory.evm(&mut *db); - evm.modify_cfg(|cfg| { - cfg.disable_balance_check = true; - cfg.disable_block_gas_limit = true; - }); - - let flashblocks_num_tx = if let Some(flashtestations) = &self.flashtestations_builder_tx - && self.use_permit - { - self.signed_increment_flashblocks_permit_tx( - flashtestations.tee_signer(), - ctx, - &mut evm, - ) - } else { - self.signed_increment_flashblocks_tx(ctx, &mut evm) - }; - - let tx = match flashblocks_num_tx { - Ok(tx) => Some(tx), - Err(e) => { - warn!( - target: "builder_tx", - error = %e, - "flashblocks number contract tx simulation failed, defaulting to fallback builder tx" - ); - self.base_builder_tx - .simulate_builder_tx(ctx, &mut *db)? - .map(|tx| tx.set_top_of_block()) - } - }; - - builder_txs.extend(tx); - } - - if is_last_flashblock - && let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx - { - // Commit state that should be included to compute the correct nonce - let flashblocks_builder_txs = builder_txs - .iter() - .filter(|tx| tx.is_top_of_block == top_of_block) - .map(|tx| tx.signed_tx.clone()) - .collect(); - self.commit_txs(flashblocks_builder_txs, ctx, &mut *db)?; - - // We only include flashtestations txs in the last flashblock - match flashtestations_builder_tx.simulate_builder_txs( - state_provider, - info, - ctx, - db, - top_of_block, - is_first_flashblock, - is_last_flashblock, - ) { - Ok(flashtestations_builder_txs) => builder_txs.extend(flashtestations_builder_txs), - Err(e) => { - warn!( - target: "flashtestations", - error = %e, - "failed to add flashtestations builder tx" - ) - } - } - } - - Ok(builder_txs) - } -} diff --git a/crates/op-rbuilder/src/builder/mod.rs b/crates/op-rbuilder/src/builder/mod.rs index 535cc04c..18d48c7e 100644 --- a/crates/op-rbuilder/src/builder/mod.rs +++ b/crates/op-rbuilder/src/builder/mod.rs @@ -16,7 +16,6 @@ pub(crate) mod cancellation; mod config; mod context; mod continuous; -mod flashblocks_builder_tx; mod generator; mod p2p; mod payload; @@ -28,8 +27,9 @@ mod timing; mod wspub; pub use builder_tx::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, BuilderTxEnv, - InvalidContractDataError, SimulationSuccessResult, get_balance, get_nonce, + BuilderTxEnv, BuilderTxError, BuilderTxPosition, BuilderTxProducer, BuilderTxSchedule, + ScheduledBuilderTx, SimulatedBuilderTx, SimulationState, SimulationSuccessResult, get_nonce, + sign_tx, simulate_call, }; pub use config::FlashblocksConfig; pub use context::OpPayloadJobCtx; diff --git a/crates/op-rbuilder/src/builder/payload.rs b/crates/op-rbuilder/src/builder/payload.rs index cd9882be..788e8909 100644 --- a/crates/op-rbuilder/src/builder/payload.rs +++ b/crates/op-rbuilder/src/builder/payload.rs @@ -3,7 +3,7 @@ use crate::{ builder::{ BuilderConfig, best_txs::{FlashblockPoolTxCursor, FlashblockTxTracker}, - builder_tx::{BuilderTransactions, reserve_builder_tx_budget}, + builder_tx::{BuilderTxSchedule, reserve_builder_tx_budget}, cancellation::{CancellationReason, FlashblockJobCancellation, PayloadJobCancellation}, context::{OpPayloadBuilderCtx, OpPayloadJobCtx}, generator::{BuildArguments, PayloadBuilder}, @@ -29,6 +29,7 @@ use reth_optimism_payload_builder::OpPayloadAttrs; use reth_optimism_primitives::{OpReceipt, OpTransactionSigned}; use reth_payload_builder::PayloadId; use reth_payload_util::BestPayloadTransactions; +use reth_provider::StateProvider; use reth_revm::{ State, cached::CachedReads, @@ -337,10 +338,6 @@ struct FlashblockMeta { } impl FlashblockMeta { - fn is_first(&self) -> bool { - self.flashblock_index == 0 - } - fn is_last(&self) -> bool { self.flashblock_index == self.target_flashblock_count } @@ -348,12 +345,12 @@ impl FlashblockMeta { /// Optimism's payload builder #[derive(Debug)] -pub(crate) struct OpPayloadBuilder { - inner: Arc>, +pub(crate) struct OpPayloadBuilder { + inner: Arc>, } #[derive(Debug)] -pub(crate) struct OpPayloadBuilderInner { +pub(crate) struct OpPayloadBuilderInner { /// Builder context builder_ctx: Arc, /// The transaction pool @@ -371,8 +368,8 @@ pub(crate) struct OpPayloadBuilderInner { ws_pub: WebSocketPublisher, /// System configuration for the builder config: BuilderConfig, - /// The end of builder transaction type - builder_tx: BuilderTx, + /// Builder-transaction producers scheduled at named dispatch points. + builder_tx_schedule: BuilderTxSchedule, /// Tokio task metrics for monitoring spawned tasks task_metrics: Arc, /// Monotonic epoch that advances on pool mutations. @@ -381,7 +378,7 @@ pub(crate) struct OpPayloadBuilderInner { executor: Runtime, } -impl OpPayloadBuilderInner { +impl OpPayloadBuilderInner { pub(crate) fn pool(&self) -> &Pool { &self.pool } @@ -402,8 +399,8 @@ impl OpPayloadBuilderInner { &self.builder_ctx.metrics } - pub(crate) fn builder_tx(&self) -> &BuilderTx { - &self.builder_tx + pub(crate) fn builder_tx_schedule(&self) -> &BuilderTxSchedule { + &self.builder_tx_schedule } pub(crate) fn pool_change_epoch(&self) -> &AtomicU64 { @@ -415,15 +412,15 @@ impl OpPayloadBuilderInner { } } -impl Deref for OpPayloadBuilder { - type Target = OpPayloadBuilderInner; +impl Deref for OpPayloadBuilder { + type Target = OpPayloadBuilderInner; fn deref(&self) -> &Self::Target { self.inner.as_ref() } } -impl Clone for OpPayloadBuilder { +impl Clone for OpPayloadBuilder { fn clone(&self) -> Self { Self { inner: Arc::clone(&self.inner), @@ -431,7 +428,7 @@ impl Clone for OpPayloadBuilder OpPayloadBuilder +impl OpPayloadBuilder where Client: ClientBounds, { @@ -441,7 +438,7 @@ where pool: Pool, client: Client, config: BuilderConfig, - builder_tx: BuilderTx, + builder_tx_schedule: BuilderTxSchedule, built_fb_payload_tx: mpsc::Sender, built_payload_tx: mpsc::Sender, ws_pub: WebSocketPublisher, @@ -478,7 +475,7 @@ where built_payload_tx, ws_pub, config, - builder_tx, + builder_tx_schedule, task_metrics, pool_change_epoch, executor, @@ -499,11 +496,10 @@ async fn wait_for_trigger( } } -impl OpPayloadBuilder +impl OpPayloadBuilder where Pool: PoolBounds + 'static, Client: ClientBounds + 'static, - BuilderTx: BuilderTransactions + Send + Sync + 'static, { fn get_op_payload_job_ctx( &self, @@ -1003,8 +999,9 @@ where mut cached_reads: CachedReads, mut state_root_calc: StateRootCalculator, ) -> eyre::Result>> { - let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?; - let db = StateProviderDatabase::new(&state_provider); + let state_provider: Arc = + Arc::from(self.client.state_by_block_hash(ctx.parent().hash())?); + let db = StateProviderDatabase::new(state_provider.clone()); let sequencer_tx_start_time = Instant::now(); let mut state = State::builder() @@ -1017,24 +1014,20 @@ where ctx.metrics.sequencer_tx_duration.record(sequencer_tx_time); ctx.metrics.sequencer_tx_gauge.set(sequencer_tx_time); - // We add first builder tx right after deposits - if !ctx.attributes().no_tx_pool { - let flashblock = fb_state.meta(); - if let Err(e) = self.builder_tx.add_builder_txs( - &state_provider, + // Add the top-of-block builder txs right after sequencer txs + if !ctx.attributes().no_tx_pool + && let Err(e) = self.builder_tx_schedule.commit_top_of_block( + state_provider.clone(), &mut info, &ctx.builder_tx_env(), &mut state, - false, - flashblock.is_first(), - flashblock.is_last(), - ) { - error!( - target: "payload_builder", - "Error adding builder txs to fallback block: {}", - e - ); - } + ) + { + error!( + target: "payload_builder", + error = %e, + "Error adding builder txs to fallback block", + ); } let (payload, fb_payload) = ctx.block_assembly_input()?.assemble( @@ -1164,9 +1157,10 @@ where mut state_root_calc, } = build_state; - let state_provider = self.client.state_by_block_hash(parent_hash)?; + let state_provider: Arc = + Arc::from(self.client.state_by_block_hash(parent_hash)?); let mut evm_state = State::builder() - .with_database(StateProviderDatabase::new(&state_provider)) + .with_database(StateProviderDatabase::new(state_provider.clone())) .with_cached_prestate(cache) .with_bundle_update() .build(); @@ -1192,26 +1186,31 @@ where let flashblock_build_start_time = Instant::now(); let flashblock = fb_state.meta(); - let builder_txs = self - .builder_tx - .add_builder_txs( - &state_provider, + self.builder_tx_schedule + .commit_top_of_flashblock( + state_provider.clone(), &mut info, &ctx.builder_tx_env(), &mut evm_state, - true, - flashblock.is_first(), - flashblock.is_last(), ) .inspect_err( |e| error!(target: "payload_builder", error = %e, "Error simulating builder txs"), ) - .unwrap_or_default(); + .ok(); + + let bottom_builder_txs_estimate = if flashblock.is_last() { + self.builder_tx_schedule.estimate_bottom_of_block( + state_provider.clone(), + &info, + &ctx.builder_tx_env(), + &evm_state, + ) + } else { + vec![] + }; - // only reserve builder tx gas / da size that has not been committed yet - // committed builder txs would have counted towards the gas / da used let max_uncompressed_block_size = reserve_builder_tx_budget( - &builder_txs, + &bottom_builder_txs_estimate, &mut target_gas_for_batch, &mut target_da_for_batch, &mut target_da_footprint_for_batch, @@ -1289,16 +1288,14 @@ where .payload_transaction_simulation_gauge .set(payload_transaction_simulation_time); - let flashblock = fb_state.meta(); - if let Err(e) = self.builder_tx.add_builder_txs( - &state_provider, - &mut info, - &ctx.builder_tx_env(), - &mut evm_state, - false, - flashblock.is_first(), - flashblock.is_last(), - ) { + if flashblock.is_last() + && let Err(e) = self.builder_tx_schedule.commit_bottom_of_block( + state_provider.clone(), + &mut info, + &ctx.builder_tx_env(), + &mut evm_state, + ) + { error!(target: "payload_builder", error = %e, "Error simulating builder txs"); } @@ -1436,11 +1433,10 @@ where } #[async_trait::async_trait] -impl PayloadBuilder for OpPayloadBuilder +impl PayloadBuilder for OpPayloadBuilder where Pool: PoolBounds + 'static, Client: ClientBounds + 'static, - BuilderTx: BuilderTransactions + Send + Sync + 'static, { type Attributes = OpPayloadAttrs; type BuiltPayload = OpBuiltPayload; diff --git a/crates/op-rbuilder/src/builder/service.rs b/crates/op-rbuilder/src/builder/service.rs index 4002edd4..8091d2b5 100644 --- a/crates/op-rbuilder/src/builder/service.rs +++ b/crates/op-rbuilder/src/builder/service.rs @@ -1,9 +1,8 @@ use super::payload::OpPayloadBuilder; use crate::{ builder::{ - BuilderConfig, - builder_tx::BuilderTransactions, - flashblocks_builder_tx::{FlashblocksBuilderTx, FlashblocksNumberBuilderTx}, + BuilderConfig, BuilderTxPosition, BuilderTxSchedule, ScheduledBuilderTx, + builder_tx::{ClaimBuilderTx, FlashblockNumberBuilderTx}, generator::BlockPayloadJobGenerator, p2p::{AGENT_VERSION, FLASHBLOCKS_STREAM_PROTOCOL, Message}, payload_handler::PayloadHandler, @@ -35,16 +34,15 @@ use tracing::{error, info, warn}; pub struct FlashblocksServiceBuilder(pub BuilderConfig); impl FlashblocksServiceBuilder { - fn spawn_payload_builder_service_internal( + fn spawn_payload_builder_service_internal( self, ctx: &BuilderContext, pool: Pool, - builder_tx: BuilderTx, + builder_tx_schedule: BuilderTxSchedule, ) -> eyre::Result::Payload>> where Node: NodeBounds, Pool: PoolBounds, - BuilderTx: BuilderTransactions + Unpin + Clone + Send + Sync + 'static, { let flashblocks_config = &self.0.flashblocks_config; @@ -145,7 +143,7 @@ impl FlashblocksServiceBuilder { pool, ctx.provider().clone(), self.0.clone(), - builder_tx, + builder_tx_schedule, built_fb_payload_tx, built_payload_tx, ws_pub, @@ -201,6 +199,63 @@ impl FlashblocksServiceBuilder { info!(target: "payload_builder", "Flashblocks payload builder service started"); Ok(payload_builder_handle) } + + async fn build_builder_tx_schedule(&self) -> BuilderTxSchedule { + let mut scheduled = vec![]; + + let Some(signer) = self.0.builder_signer else { + return BuilderTxSchedule::new(scheduled); + }; + + let flashtestations_builder_tx = if self.0.flashtestations_config.flashtestations_enabled { + bootstrap_flashtestations(self.0.flashtestations_config.clone(), signer) + .await + .inspect_err(|e| warn!( + target: "payload_builder", + error = %e, + "Failed to bootstrap flashtestations, builder will not include flashtestations txs" + )) + .ok() + } else { + None + }; + + scheduled.push(ScheduledBuilderTx::new( + Arc::new(ClaimBuilderTx::new(Some(signer))), + BuilderTxPosition::TopOfBlock, + )); + + if let Some(flashblocks_number_contract_address) = + self.0.flashblocks_config.number_contract_address + { + let use_permit = self.0.flashblocks_config.number_contract_use_permit; + let tee_signer = flashtestations_builder_tx + .as_ref() + .map(|ft| *ft.tee_signer()); + + scheduled.push(ScheduledBuilderTx::new( + Arc::new(FlashblockNumberBuilderTx::new( + signer, + flashblocks_number_contract_address, + use_permit, + tee_signer, + )), + BuilderTxPosition::TopOfFlashblock, + )); + } + + // Flashtestations runs at bottom-of-block on the last flashblock, after all other txs. + if self.0.flashtestations_config.flashtestations_enabled + && let Some(flashtestations_builder_tx) = flashtestations_builder_tx + { + scheduled.push(ScheduledBuilderTx::new( + Arc::new(flashtestations_builder_tx), + BuilderTxPosition::BottomOfBlock, + )); + } + + BuilderTxSchedule::new(scheduled) + } } impl PayloadServiceBuilder for FlashblocksServiceBuilder @@ -214,49 +269,7 @@ where pool: Pool, _: OpEvmConfig, ) -> eyre::Result::Payload>> { - let signer = self.0.builder_signer; - let flashtestations_builder_tx = if let Some(builder_key) = signer - && self.0.flashtestations_config.flashtestations_enabled - { - match bootstrap_flashtestations(self.0.flashtestations_config.clone(), builder_key) - .await - { - Ok(builder_tx) => Some(builder_tx), - Err(e) => { - warn!( - target: "payload_builder", - error = %e, - "Failed to bootstrap flashtestations, builder will not include flashtestations txs" - ); - None - } - } - } else { - None - }; - - let flashblocks_config = &self.0.flashblocks_config; - if let Some(builder_signer) = signer - && let Some(flashblocks_number_contract_address) = - flashblocks_config.number_contract_address - { - let use_permit = flashblocks_config.number_contract_use_permit; - self.spawn_payload_builder_service_internal( - ctx, - pool, - FlashblocksNumberBuilderTx::new( - builder_signer, - flashblocks_number_contract_address, - use_permit, - flashtestations_builder_tx, - ), - ) - } else { - self.spawn_payload_builder_service_internal( - ctx, - pool, - FlashblocksBuilderTx::new(signer, flashtestations_builder_tx), - ) - } + let builder_tx_schedule = self.build_builder_tx_schedule().await; + self.spawn_payload_builder_service_internal(ctx, pool, builder_tx_schedule) } } diff --git a/crates/op-rbuilder/src/flashtestations/builder_tx.rs b/crates/op-rbuilder/src/flashtestations/builder_tx.rs index 99ed46e3..df69f550 100644 --- a/crates/op-rbuilder/src/flashtestations/builder_tx.rs +++ b/crates/op-rbuilder/src/flashtestations/builder_tx.rs @@ -16,8 +16,8 @@ use tracing::{debug, info, warn}; use crate::{ builder::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, BuilderTxEnv, - SimulationSuccessResult, get_nonce, + BuilderTxEnv, BuilderTxError, BuilderTxProducer, SimulatedBuilderTx, SimulationState, + SimulationSuccessResult, get_nonce, sign_tx, simulate_call, }, flashtestations::{ BlockData, @@ -116,14 +116,14 @@ impl FlashtestationsBuilderTx { fn set_registered( &self, - state_provider: impl StateProvider + Clone, - ctx: &BuilderTxEnv<'_>, - ) -> Result<(), BuilderTransactionError> { + state_provider: Arc, + env: &BuilderTxEnv<'_>, + ) -> Result<(), BuilderTxError> { let mut simulation_state = State::builder() .with_database(StateProviderDatabase::new(state_provider)) .with_bundle_update() .build(); - let mut evm = ctx.evm_factory.evm(&mut simulation_state); + let mut evm = env.evm_factory.evm(&mut simulation_state); evm.modify_cfg(|cfg| { cfg.disable_balance_check = true; cfg.disable_nonce_check = true; @@ -132,7 +132,7 @@ impl FlashtestationsBuilderTx { teeAddress: self.tee_service_signer.address, }; let SimulationSuccessResult { output, .. } = - self.flashtestations_contract_read(self.registry_address, calldata, ctx, &mut evm)?; + self.flashtestations_contract_read(self.registry_address, calldata, env, &mut evm)?; if output.isValid { self.registered .store(true, std::sync::atomic::Ordering::SeqCst); @@ -143,33 +143,33 @@ impl FlashtestationsBuilderTx { fn get_permit_nonce( &self, contract_address: Address, - ctx: &BuilderTxEnv<'_>, + env: &BuilderTxEnv<'_>, evm: &mut OpEvm, - ) -> Result { + ) -> Result { let calldata = IERC20Permit::noncesCall { owner: self.tee_service_signer.address, }; let SimulationSuccessResult { output, .. } = - self.flashtestations_contract_read(contract_address, calldata, ctx, evm)?; + self.flashtestations_contract_read(contract_address, calldata, env, evm)?; Ok(output) } fn registration_permit_signature( &self, permit_nonce: U256, - ctx: &BuilderTxEnv<'_>, + env: &BuilderTxEnv<'_>, evm: &mut OpEvm, - ) -> Result { + ) -> Result { let struct_hash_calldata = IFlashtestationRegistry::computeStructHashCall { rawQuote: self.attestation.clone().into(), extendedRegistrationData: self.extra_registration_data.clone(), nonce: permit_nonce, - deadline: U256::from(ctx.hardforks.timestamp), + deadline: U256::from(env.hardforks.timestamp), }; let SimulationSuccessResult { output, .. } = self.flashtestations_contract_read( self.registry_address, struct_hash_calldata, - ctx, + env, evm, )?; let typed_data_hash_calldata = @@ -177,7 +177,7 @@ impl FlashtestationsBuilderTx { let SimulationSuccessResult { output, .. } = self.flashtestations_contract_read( self.registry_address, typed_data_hash_calldata, - ctx, + env, evm, )?; let signature = self.tee_service_signer.sign_message(output)?; @@ -186,16 +186,16 @@ impl FlashtestationsBuilderTx { fn signed_registration_permit_tx( &self, - ctx: &BuilderTxEnv<'_>, + env: &BuilderTxEnv<'_>, evm: &mut OpEvm<&mut State, NoOpInspector, PrecompilesMap>, - ) -> Result { - let permit_nonce = self.get_permit_nonce(self.registry_address, ctx, evm)?; - let signature = self.registration_permit_signature(permit_nonce, ctx, evm)?; + ) -> Result { + let permit_nonce = self.get_permit_nonce(self.registry_address, env, evm)?; + let signature = self.registration_permit_signature(permit_nonce, env, evm)?; let calldata = IFlashtestationRegistry::permitRegisterTEEServiceCall { rawQuote: self.attestation.clone().into(), extendedRegistrationData: self.extra_registration_data.clone(), nonce: permit_nonce, - deadline: U256::from(ctx.hardforks.timestamp), + deadline: U256::from(env.hardforks.timestamp), signature: signature.as_bytes().into(), }; let SimulationSuccessResult { @@ -206,26 +206,25 @@ impl FlashtestationsBuilderTx { self.registry_address, calldata.clone(), vec![TEEServiceRegistered::SIGNATURE_HASH], - ctx, + env, evm, )?; - let signed_tx = self.sign_tx( + let signed_tx = sign_tx( self.registry_address, self.builder_signer, gas_used, calldata.abi_encode().into(), - ctx, + env, evm.db(), )?; let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes(signed_tx.encoded_2718().as_slice()); // commit the register transaction state so the block proof transaction can succeed evm.db_mut().commit(state_changes); - Ok(BuilderTransactionCtx { + Ok(SimulatedBuilderTx { gas_used, da_size, signed_tx, - is_top_of_block: false, }) } @@ -233,9 +232,9 @@ impl FlashtestationsBuilderTx { &self, permit_nonce: U256, block_content_hash: B256, - ctx: &BuilderTxEnv<'_>, + env: &BuilderTxEnv<'_>, evm: &mut OpEvm, - ) -> Result { + ) -> Result { let struct_hash_calldata = IBlockBuilderPolicy::computeStructHashCall { version: self.builder_proof_version, blockContentHash: block_content_hash, @@ -244,7 +243,7 @@ impl FlashtestationsBuilderTx { let SimulationSuccessResult { output, .. } = self.flashtestations_contract_read( self.builder_policy_address, struct_hash_calldata, - ctx, + env, evm, )?; let typed_data_hash_calldata = @@ -252,7 +251,7 @@ impl FlashtestationsBuilderTx { let SimulationSuccessResult { output, .. } = self.flashtestations_contract_read( self.builder_policy_address, typed_data_hash_calldata, - ctx, + env, evm, )?; let signature = self.tee_service_signer.sign_message(output)?; @@ -262,18 +261,18 @@ impl FlashtestationsBuilderTx { fn signed_block_proof_permit_tx( &self, transactions: &[OpTransactionSigned], - ctx: &BuilderTxEnv<'_>, + env: &BuilderTxEnv<'_>, evm: &mut OpEvm, - ) -> Result { - let permit_nonce = self.get_permit_nonce(self.builder_policy_address, ctx, evm)?; + ) -> Result { + let permit_nonce = self.get_permit_nonce(self.builder_policy_address, env, evm)?; let block_content_hash = Self::compute_block_content_hash( transactions, - ctx.parent_hash, - ctx.block_number, - ctx.hardforks.timestamp, + env.parent_hash, + env.block_number, + env.hardforks.timestamp, ); let signature = - self.block_proof_permit_signature(permit_nonce, block_content_hash, ctx, evm)?; + self.block_proof_permit_signature(permit_nonce, block_content_hash, env, evm)?; let calldata = IBlockBuilderPolicy::permitVerifyBlockBuilderProofCall { blockContentHash: block_content_hash, nonce: permit_nonce, @@ -284,24 +283,23 @@ impl FlashtestationsBuilderTx { self.builder_policy_address, calldata.clone(), vec![BlockBuilderProofVerified::SIGNATURE_HASH], - ctx, + env, evm, )?; - let signed_tx = self.sign_tx( + let signed_tx = sign_tx( self.builder_policy_address, self.builder_signer, gas_used, calldata.abi_encode().into(), - ctx, + env, evm.db(), )?; let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes(signed_tx.encoded_2718().as_slice()); - Ok(BuilderTransactionCtx { + Ok(SimulatedBuilderTx { gas_used, da_size, signed_tx, - is_top_of_block: false, }) } @@ -309,10 +307,10 @@ impl FlashtestationsBuilderTx { &self, contract_address: Address, calldata: T, - ctx: &BuilderTxEnv<'_>, + env: &BuilderTxEnv<'_>, evm: &mut OpEvm, - ) -> Result, BuilderTransactionError> { - self.flashtestations_call(contract_address, calldata, vec![], ctx, evm) + ) -> Result, BuilderTxError> { + self.flashtestations_call(contract_address, calldata, vec![], env, evm) } fn flashtestations_call( @@ -320,74 +318,71 @@ impl FlashtestationsBuilderTx { contract_address: Address, calldata: T, expected_topics: Vec, - ctx: &BuilderTxEnv<'_>, + env: &BuilderTxEnv<'_>, evm: &mut OpEvm, - ) -> Result, BuilderTransactionError> { - let simulation_gas_limit = ctx.block_gas_limit.min(evm.cfg_env().tx_gas_limit_cap()); + ) -> Result, BuilderTxError> { + let simulation_gas_limit = env.block_gas_limit.min(evm.cfg_env().tx_gas_limit_cap()); let tx_req = OpTransactionRequest::default() .gas_limit(simulation_gas_limit) - .max_fee_per_gas(ctx.base_fee.into()) + .max_fee_per_gas(env.base_fee.into()) .to(contract_address) .from(self.builder_signer.address) .nonce(get_nonce(evm.db(), self.builder_signer.address)?) .input(TransactionInput::new(calldata.abi_encode().into())); if contract_address == self.registry_address { - self.simulate_call::( + simulate_call::( tx_req, expected_topics, evm, ) } else if contract_address == self.builder_policy_address { - self.simulate_call::( + simulate_call::( tx_req, expected_topics, evm, ) } else { - Err(BuilderTransactionError::msg( + Err(BuilderTxError::msg( "invalid contract address for flashtestations", )) } } } -impl BuilderTransactions for FlashtestationsBuilderTx { +impl BuilderTxProducer for FlashtestationsBuilderTx { fn simulate_builder_txs( &self, - state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, - ctx: &BuilderTxEnv<'_>, - db: &mut State, - _top_of_block: bool, - _is_first_flashblock: bool, - _is_last_flashblock: bool, - ) -> Result, BuilderTransactionError> { + state_provider: Arc, + info: &ExecutionInfo, + env: &BuilderTxEnv<'_>, + sim_state: &mut SimulationState, + ) -> Result, BuilderTxError> { // set registered by simulating against the committed state if !self.registered.load(std::sync::atomic::Ordering::SeqCst) { - self.set_registered(state_provider, ctx)?; + self.set_registered(state_provider, env)?; } - let mut evm = ctx.evm_factory.evm(&mut *db); + let mut evm = env.evm_factory.evm(&mut *sim_state); evm.modify_cfg(|cfg| { cfg.disable_balance_check = true; cfg.disable_block_gas_limit = true; }); - let mut builder_txs = Vec::::new(); + let mut builder_txs = Vec::::new(); if !self.registered.load(std::sync::atomic::Ordering::SeqCst) { info!( target: "flashtestations", "tee service not registered yet, attempting to register" ); - let register_tx = self.signed_registration_permit_tx(ctx, &mut evm)?; + let register_tx = self.signed_registration_permit_tx(env, &mut evm)?; builder_txs.push(register_tx); } // don't return on error for block proof as previous txs in builder_txs will not be returned if self.enable_block_proofs { debug!(target: "flashtestations", "adding permit verify block proof tx"); - match self.signed_block_proof_permit_tx(&info.executed_transactions, ctx, &mut evm) { + match self.signed_block_proof_permit_tx(&info.executed_transactions, env, &mut evm) { Ok(block_proof_tx) => builder_txs.push(block_proof_tx), Err(e) => { warn!( From 1e5b6fc15e3f51368f46833a8186879eab615c58 Mon Sep 17 00:00:00 2001 From: Ash Kunda <18058966+akundaz@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:04:09 -0400 Subject: [PATCH 2/2] revert behavior changes --- .../builder_tx/impls/flashblock_number.rs | 59 +++++++++++-------- crates/op-rbuilder/src/builder/service.rs | 5 ++ 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/crates/op-rbuilder/src/builder/builder_tx/impls/flashblock_number.rs b/crates/op-rbuilder/src/builder/builder_tx/impls/flashblock_number.rs index 4c506962..e5971c73 100644 --- a/crates/op-rbuilder/src/builder/builder_tx/impls/flashblock_number.rs +++ b/crates/op-rbuilder/src/builder/builder_tx/impls/flashblock_number.rs @@ -14,8 +14,8 @@ use tracing::warn; use crate::{ builder::builder_tx::{ - BuilderTxEnv, BuilderTxError, BuilderTxProducer, SimulatedBuilderTx, SimulationState, - SimulationSuccessResult, get_nonce, sign_tx, simulate_call, + BuilderTxEnv, BuilderTxError, BuilderTxProducer, ClaimBuilderTx, SimulatedBuilderTx, + SimulationState, SimulationSuccessResult, get_nonce, sign_tx, simulate_call, }, primitives::reth::ExecutionInfo, tx_signer::Signer, @@ -184,33 +184,42 @@ impl FlashblockNumberBuilderTx { impl BuilderTxProducer for FlashblockNumberBuilderTx { fn simulate_builder_txs( &self, - _state_provider: Arc, - _info: &ExecutionInfo, + state_provider: Arc, + info: &ExecutionInfo, env: &BuilderTxEnv<'_>, sim_state: &mut SimulationState, ) -> Result, BuilderTxError> { - let mut evm = env.evm_factory.evm(&mut *sim_state); - evm.modify_cfg(|cfg| { - cfg.disable_balance_check = true; - cfg.disable_block_gas_limit = true; - }); - - let num_tx = if let Some(tee_signer) = &self.tee_signer - && self.use_permit - { - self.signed_increment_flashblocks_permit_tx(tee_signer, env, &mut evm) - } else { - self.signed_increment_flashblocks_tx(env, &mut evm) + let flashblock_number_tx = { + let mut evm = env.evm_factory.evm(&mut *sim_state); + evm.modify_cfg(|cfg| { + cfg.disable_balance_check = true; + cfg.disable_block_gas_limit = true; + }); + + if let Some(tee_signer) = &self.tee_signer + && self.use_permit + { + self.signed_increment_flashblocks_permit_tx(tee_signer, env, &mut evm) + } else { + self.signed_increment_flashblocks_tx(env, &mut evm) + } }; - let tx = num_tx.inspect_err(|e| { - warn!( - target: "builder_tx", - error = %e, - "flashblocks number contract tx simulation failed" - ) - })?; - - Ok(vec![tx]) + match flashblock_number_tx { + Ok(tx) => Ok(vec![tx]), + Err(e) => { + warn!( + target: "builder_tx", + error = %e, + "flashblocks number contract tx simulation failed, falling back to claim tx" + ); + ClaimBuilderTx::new(Some(self.signer)).simulate_builder_txs( + state_provider, + info, + env, + sim_state, + ) + } + } } } diff --git a/crates/op-rbuilder/src/builder/service.rs b/crates/op-rbuilder/src/builder/service.rs index 8091d2b5..7ad94783 100644 --- a/crates/op-rbuilder/src/builder/service.rs +++ b/crates/op-rbuilder/src/builder/service.rs @@ -242,6 +242,11 @@ impl FlashblocksServiceBuilder { )), BuilderTxPosition::TopOfFlashblock, )); + } else { + scheduled.push(ScheduledBuilderTx::new( + Arc::new(ClaimBuilderTx::new(Some(signer))), + BuilderTxPosition::BottomOfBlock, + )); } // Flashtestations runs at bottom-of-block on the last flashblock, after all other txs.