From 739d96c0579d1882f6bdb7dd016cb4358a68674b Mon Sep 17 00:00:00 2001 From: avalonche Date: Tue, 29 Jul 2025 03:48:31 +1000 Subject: [PATCH 1/9] Refactor payload builder to accept generic builder tx --- crates/op-rbuilder/src/builders/builder_tx.rs | 45 ++++++++++++------- .../src/builders/flashblocks/builder_tx.rs | 4 +- .../src/builders/flashblocks/payload.rs | 38 +++++++++++++--- .../src/builders/standard/builder_tx.rs | 4 +- .../src/builders/standard/payload.rs | 5 ++- .../src/flashtestations/service.rs | 1 + 6 files changed, 72 insertions(+), 25 deletions(-) diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 8ff168b14..06b9e2a23 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -17,7 +17,7 @@ use revm::{ DatabaseCommit, context::result::{EVMError, ResultAndState}, }; -use tracing::warn; +use tracing::{debug, warn}; use crate::{builders::context::OpPayloadBuilderCtx, primitives::reth::ExecutionInfo}; @@ -25,16 +25,16 @@ use crate::{builders::context::OpPayloadBuilderCtx, primitives::reth::ExecutionI pub struct BuilderTransactionCtx { pub gas_used: u64, pub da_size: u64, - pub signed_tx: Recovered, + pub signed_tx: Option>, } /// Possible error variants during construction of builder txs. #[derive(Debug, thiserror::Error)] pub enum BuilderTransactionError { - /// Builder account load fails to get builder nonce + /// Thrown when builder account load fails to get builder nonce #[error("failed to load account {0}")] AccountLoadFailed(Address), - /// Signature signing fails + /// Thrown when signature signing fails #[error("failed to sign transaction: {0}")] SigningError(secp256k1::Error), /// Unrecoverable error during evm execution. @@ -75,6 +75,7 @@ pub trait BuilderTransactions: Debug { info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, + top_of_block: bool, ) -> Result, BuilderTransactionError>; fn add_builder_txs( @@ -83,6 +84,7 @@ pub trait BuilderTransactions: Debug { info: &mut ExecutionInfo, builder_ctx: &OpPayloadBuilderCtx, db: &mut State, + top_of_block: bool, ) -> Result, BuilderTransactionError> { { let mut evm = builder_ctx @@ -91,21 +93,30 @@ pub trait BuilderTransactions: Debug { let mut invalid: HashSet
= HashSet::new(); - let builder_txs = - self.simulate_builder_txs(state_provider, info, builder_ctx, evm.db_mut())?; + let builder_txs = self.simulate_builder_txs( + state_provider, + info, + builder_ctx, + evm.db_mut(), + top_of_block, + )?; 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"); + let signed_tx = match builder_tx.signed_tx.clone() { + Some(tx) => tx, + None => continue, + }; + if invalid.contains(&signed_tx.signer()) { + debug!(target: "payload_builder", tx_hash = ?signed_tx.tx_hash(), "builder signer invalid as previous builder tx reverted"); continue; } let ResultAndState { result, state } = evm - .transact(&builder_tx.signed_tx) + .transact(&signed_tx) .map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?; if !result.is_success() { - warn!(target: "payload_builder", tx_hash = ?builder_tx.signed_tx.tx_hash(), "builder tx reverted"); - invalid.insert(builder_tx.signed_tx.signer()); + warn!(target: "payload_builder", tx_hash = ?signed_tx.tx_hash(), "builder tx reverted"); + invalid.insert(signed_tx.signer()); continue; } @@ -114,7 +125,7 @@ pub trait BuilderTransactions: Debug { info.cumulative_gas_used += gas_used; let ctx = ReceiptBuilderCtx { - tx: builder_tx.signed_tx.inner(), + tx: signed_tx.inner(), evm: &evm, result, state: &state, @@ -126,9 +137,9 @@ pub trait BuilderTransactions: Debug { evm.db_mut().commit(state); // Append sender and transaction to the respective lists - info.executed_senders.push(builder_tx.signed_tx.signer()); + info.executed_senders.push(signed_tx.signer()); info.executed_transactions - .push(builder_tx.signed_tx.clone().into_inner()); + .push(signed_tx.clone().into_inner()); } // Release the db reference by dropping evm @@ -156,8 +167,12 @@ pub trait BuilderTransactions: Debug { .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); for builder_tx in builder_txs { + let signed_tx = match builder_tx.signed_tx.clone() { + Some(tx) => tx, + None => continue, + }; let ResultAndState { state, .. } = evm - .transact(&builder_tx.signed_tx) + .transact(&signed_tx) .map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?; evm.db_mut().commit(state); diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs index be29e6aaa..a54aaaaf8 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -53,7 +53,7 @@ impl FlashblocksBuilderTx { Ok(Some(BuilderTransactionCtx { gas_used, da_size, - signed_tx, + signed_tx: Some(signed_tx), })) } None => Ok(None), @@ -122,6 +122,7 @@ impl BuilderTransactions for FlashblocksBuilderTx { info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, + top_of_block: bool, ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); @@ -147,6 +148,7 @@ impl BuilderTransactions for FlashblocksBuilderTx { info, ctx, &mut simulation_state, + top_of_block, )?; builder_txs.extend(flashtestations_builder_txs); } diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 05228f829..6c81a64fd 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -322,8 +322,19 @@ where let builder_txs = if ctx.attributes().no_tx_pool { vec![] } else { - self.builder_tx - .add_builder_txs(&state_provider, &mut info, &ctx, &mut state)? + match self.builder_tx.add_builder_txs( + &state_provider, + &mut info, + &ctx, + &mut state, + true, + ) { + Ok(builder_txs) => builder_txs, + Err(e) => { + error!(target: "payload_builder", "Error adding builder txs to fallback block: {}", e); + vec![] + } + } }; // We subtract gas limit and da limit for builder transaction from the whole limit @@ -571,8 +582,16 @@ where let flashblock_build_start_time = Instant::now(); let builder_txs = - self.builder_tx - .simulate_builder_txs(&state_provider, info, ctx, state)?; + match self + .builder_tx + .add_builder_txs(&state_provider, info, ctx, state, true) + { + Ok(builder_txs) => builder_txs, + Err(e) => { + error!(target: "payload_builder", "Error simulating builder txs: {}", e); + vec![] + } + }; let builder_tx_gas = builder_txs.iter().fold(0, |acc, tx| acc + tx.gas_used); let builder_tx_da_size: u64 = builder_txs.iter().fold(0, |acc, tx| acc + tx.da_size); @@ -636,8 +655,15 @@ where .payload_tx_simulation_gauge .set(payload_tx_simulation_time); - self.builder_tx - .add_builder_txs(&state_provider, info, ctx, state)?; + match self + .builder_tx + .add_builder_txs(&state_provider, info, ctx, state, false) + { + Ok(_) => {} + Err(e) => { + error!(target: "payload_builder", "Error simulating builder txs: {}", e); + } + }; let total_block_built_duration = Instant::now(); let build_result = build_block( diff --git a/crates/op-rbuilder/src/builders/standard/builder_tx.rs b/crates/op-rbuilder/src/builders/standard/builder_tx.rs index 23c39f3c8..426e272a0 100644 --- a/crates/op-rbuilder/src/builders/standard/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/standard/builder_tx.rs @@ -53,7 +53,7 @@ impl StandardBuilderTx { Ok(Some(BuilderTransactionCtx { gas_used, da_size, - signed_tx, + signed_tx: Some(signed_tx), })) } None => Ok(None), @@ -122,6 +122,7 @@ impl BuilderTransactions for StandardBuilderTx { info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, + top_of_block: bool, ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); let standard_builder_tx = self.simulate_builder_tx(ctx, db)?; @@ -138,6 +139,7 @@ impl BuilderTransactions for StandardBuilderTx { info, ctx, &mut simulation_state, + top_of_block, )?; builder_txs.extend(flashtestations_builder_txs); } diff --git a/crates/op-rbuilder/src/builders/standard/payload.rs b/crates/op-rbuilder/src/builders/standard/payload.rs index 52acef4b7..c510a8ba9 100644 --- a/crates/op-rbuilder/src/builders/standard/payload.rs +++ b/crates/op-rbuilder/src/builders/standard/payload.rs @@ -347,7 +347,8 @@ impl OpBuilder<'_, Txs> { // 4. if mem pool transactions are requested we execute them // gas reserved for builder tx - let builder_txs = builder_tx.simulate_builder_txs(&state_provider, &mut info, ctx, db)?; + let builder_txs = + builder_tx.simulate_builder_txs(&state_provider, &mut info, ctx, db, true)?; let builder_tx_gas = builder_txs.iter().fold(0, |acc, tx| acc + tx.gas_used); let block_gas_limit = ctx.block_gas_limit().saturating_sub(builder_tx_gas); if block_gas_limit == 0 { @@ -394,7 +395,7 @@ impl OpBuilder<'_, Txs> { } // Add builder tx to the block - builder_tx.add_builder_txs(&state_provider, &mut info, ctx, db)?; + builder_tx.add_builder_txs(&state_provider, &mut info, ctx, db, false)?; let state_merge_start_time = Instant::now(); diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index ffcd22767..71dbca86c 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -101,6 +101,7 @@ impl BuilderTransactions for Flashtestation _info: &mut ExecutionInfo, _ctx: &OpPayloadBuilderCtx, _db: &mut State, + _top_of_block: bool, ) -> Result, BuilderTransactionError> { Ok(vec![]) } From 4734ff7ae05c5d840b74a1491c9eb57ac73ccaf3 Mon Sep 17 00:00:00 2001 From: shana Date: Wed, 30 Jul 2025 01:53:55 +1000 Subject: [PATCH 2/9] Update crates/op-rbuilder/src/builders/builder_tx.rs Co-authored-by: Solar Mithril --- crates/op-rbuilder/src/builders/builder_tx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 06b9e2a23..0c42dc59d 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -31,7 +31,7 @@ pub struct BuilderTransactionCtx { /// Possible error variants during construction of builder txs. #[derive(Debug, thiserror::Error)] pub enum BuilderTransactionError { - /// Thrown when builder account load fails to get builder nonce + /// Builder account load fails to get builder nonce #[error("failed to load account {0}")] AccountLoadFailed(Address), /// Thrown when signature signing fails From 5ab24ae56507bd7bc1a852b13c6d0d2657179e11 Mon Sep 17 00:00:00 2001 From: shana Date: Wed, 30 Jul 2025 01:54:02 +1000 Subject: [PATCH 3/9] Update crates/op-rbuilder/src/builders/builder_tx.rs Co-authored-by: Solar Mithril --- crates/op-rbuilder/src/builders/builder_tx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 0c42dc59d..3305b3b51 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -34,7 +34,7 @@ pub enum BuilderTransactionError { /// Builder account load fails to get builder nonce #[error("failed to load account {0}")] AccountLoadFailed(Address), - /// Thrown when signature signing fails + /// Signature signing fails #[error("failed to sign transaction: {0}")] SigningError(secp256k1::Error), /// Unrecoverable error during evm execution. From e69f0553e9fa124f6d306394297c950c31d9948d Mon Sep 17 00:00:00 2001 From: shana Date: Wed, 30 Jul 2025 01:57:41 +1000 Subject: [PATCH 4/9] Update crates/op-rbuilder/src/builders/builder_tx.rs Co-authored-by: Solar Mithril --- crates/op-rbuilder/src/builders/builder_tx.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 3305b3b51..7fadbd42e 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -17,7 +17,7 @@ use revm::{ DatabaseCommit, context::result::{EVMError, ResultAndState}, }; -use tracing::{debug, warn}; +use tracing::warn; use crate::{builders::context::OpPayloadBuilderCtx, primitives::reth::ExecutionInfo}; @@ -101,12 +101,8 @@ pub trait BuilderTransactions: Debug { top_of_block, )?; for builder_tx in builder_txs.iter() { - let signed_tx = match builder_tx.signed_tx.clone() { - Some(tx) => tx, - None => continue, - }; - if invalid.contains(&signed_tx.signer()) { - debug!(target: "payload_builder", tx_hash = ?signed_tx.tx_hash(), "builder signer invalid as previous builder tx reverted"); + 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; } From dbe6e759962ba1e9770ea378cbae8e334c964161 Mon Sep 17 00:00:00 2001 From: avalonche Date: Wed, 10 Sep 2025 12:22:08 +1000 Subject: [PATCH 5/9] Add support for flashblocks number contract builder tx --- crates/op-rbuilder/src/args/op.rs | 11 + crates/op-rbuilder/src/builders/builder_tx.rs | 11 +- .../src/builders/flashblocks/builder_tx.rs | 208 +++++++++++++++++- .../src/builders/flashblocks/config.rs | 12 + .../src/builders/flashblocks/service.rs | 30 ++- crates/op-rbuilder/src/tests/flashblocks.rs | 10 + 6 files changed, 271 insertions(+), 11 deletions(-) diff --git a/crates/op-rbuilder/src/args/op.rs b/crates/op-rbuilder/src/args/op.rs index 760249b5a..bd860f155 100644 --- a/crates/op-rbuilder/src/args/op.rs +++ b/crates/op-rbuilder/src/args/op.rs @@ -8,6 +8,7 @@ use crate::{ flashtestations::args::FlashtestationsArgs, gas_limiter::args::GasLimiterArgs, tx_signer::Signer, }; +use alloy_primitives::Address; use anyhow::{Result, anyhow}; use clap::Parser; use reth_optimism_cli::commands::Commands; @@ -155,6 +156,16 @@ pub struct FlashblocksArgs { env = "FLASHBLOCKS_CALCULATE_STATE_ROOT" )] pub flashblocks_calculate_state_root: bool, + + /// Flashblocks number contract address + /// + /// This is the address of the contract that will be used to increment the flashblock number. + /// If set a builder tx will be added to the start of every flashblock instead of the regular builder tx. + #[arg( + long = "flashblocks.number-contract-address", + env = "FLASHBLOCK_NUMBER_CONTRACT_ADDRESS" + )] + pub flashblocks_number_contract_address: Option
, } impl Default for FlashblocksArgs { diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 7fadbd42e..c9a3b6011 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -155,7 +155,7 @@ pub trait BuilderTransactions: Debug { let state = StateProviderDatabase::new(state_provider.clone()); let mut simulation_state = State::builder() .with_database(state) - .with_bundle_prestate(db.bundle_state.clone()) + .with_cached_prestate(db.cache.clone()) .with_bundle_update() .build(); let mut evm = ctx @@ -178,3 +178,12 @@ pub trait BuilderTransactions: Debug { Ok(simulation_state) } } + +pub(super) fn get_nonce( + db: &mut State, + address: Address, +) -> Result { + db.load_cache_account(address) + .map(|acc| acc.account_info().unwrap_or_default().nonce) + .map_err(|_| BuilderTransactionError::AccountLoadFailed(address)) +} diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs index a54aaaaf8..95e0f7ba7 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -1,17 +1,25 @@ use alloy_consensus::TxEip1559; use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN}; -use alloy_evm::Database; -use alloy_primitives::{Address, TxKind}; +use alloy_evm::{Database, Evm}; +use alloy_op_evm::OpEvm; +use alloy_primitives::{Address, Bytes, TxKind, U256}; +use alloy_sol_types::{SolCall, SolError, sol}; use core::fmt::Debug; use op_alloy_consensus::OpTypedTransaction; +use op_revm::OpHaltReason; +use reth_evm::{ConfigureEvm, precompiles::PrecompilesMap}; use reth_optimism_primitives::OpTransactionSigned; use reth_primitives::Recovered; use reth_provider::StateProvider; -use reth_revm::State; +use reth_revm::{State, database::StateProviderDatabase}; +use revm::{ + context::result::{ExecutionResult, ResultAndState}, + inspector::NoOpInspector, +}; use crate::{ builders::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, builder_tx::get_nonce, context::OpPayloadBuilderCtx, flashblocks::payload::FlashblocksExtraCtx, }, flashtestations::service::FlashtestationsBuilderTx, @@ -19,6 +27,66 @@ use crate::{ tx_signer::Signer, }; +sol!( + #[sol(rpc, abi)] + interface IFlashblockNumber { + function incrementFlashblockNumber() external; + } + + /** + * @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); +); + +#[derive(Debug, thiserror::Error)] +pub(super) enum FlashblockNumberError { + #[error("non builder address: {0}")] + NonBuilderAddress(Address), + #[error("mismatched flashblock number: expected {0}, actual {1}")] + MismatchedFlashblockNumber(U256, U256), + #[error("unknown revert: {0}")] + Unknown(String), + #[error("halt: {0:?}")] + Halt(OpHaltReason), +} + +impl From for FlashblockNumberError { + fn from(value: Bytes) -> Self { + // Empty revert + if value.is_empty() { + return FlashblockNumberError::Unknown( + "Transaction reverted without reason".to_string(), + ); + } + + // Try to decode each custom error type + if let Ok(NonBuilderAddress { addr }) = NonBuilderAddress::abi_decode(&value) { + return FlashblockNumberError::NonBuilderAddress(addr); + } + + if let Ok(MismatchedFlashblockNumber { + expectedFlashblockNumber, + actualFlashblockNumber, + }) = MismatchedFlashblockNumber::abi_decode(&value) + { + return FlashblockNumberError::MismatchedFlashblockNumber( + expectedFlashblockNumber, + actualFlashblockNumber, + ); + } + + FlashblockNumberError::Unknown(hex::encode(value)) + } +} + // This will be the end of block transaction of a regular block #[derive(Debug, Clone)] pub(super) struct FlashblocksBuilderTx { @@ -156,3 +224,135 @@ impl BuilderTransactions for FlashblocksBuilderTx { Ok(builder_txs) } } + +// This will be the end of block transaction of a regular block +#[derive(Debug, Clone)] +pub(super) struct FlashblocksNumberBuilderTx { + pub signer: Option, + pub flashblock_number_address: Address, + pub flashtestations_builder_tx: Option, +} + +impl FlashblocksNumberBuilderTx { + pub(super) fn new( + signer: Option, + flashblock_number_address: Address, + flashtestations_builder_tx: Option, + ) -> Self { + Self { + signer, + flashblock_number_address, + flashtestations_builder_tx, + } + } + + fn estimate_flashblock_number_tx_gas( + &self, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + State>, + NoOpInspector, + PrecompilesMap, + >, + signer: &Signer, + nonce: u64, + ) -> Result { + let tx = self.signed_flashblock_number_tx(ctx, ctx.block_gas_limit(), nonce, signer)?; + let ResultAndState { result, .. } = match evm.transact(&tx) { + Ok(res) => res, + Err(err) => { + return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); + } + }; + + match result { + ExecutionResult::Success { gas_used, .. } => Ok(gas_used), + ExecutionResult::Revert { output, .. } => Err(BuilderTransactionError::Other( + Box::new(FlashblockNumberError::from(output)), + )), + ExecutionResult::Halt { reason, .. } => Err(BuilderTransactionError::Other(Box::new( + FlashblockNumberError::Halt(reason), + ))), + } + } + + fn signed_flashblock_number_tx( + &self, + ctx: &OpPayloadBuilderCtx, + gas_limit: u64, + nonce: u64, + signer: &Signer, + ) -> Result, secp256k1::Error> { + let calldata = IFlashblockNumber::incrementFlashblockNumberCall {}.abi_encode(); + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: ctx.chain_id(), + nonce, + gas_limit, + max_fee_per_gas: ctx.base_fee().into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(self.flashblock_number_address), + input: calldata.into(), + ..Default::default() + }); + signer.sign_tx(tx) + } +} + +impl BuilderTransactions for FlashblocksNumberBuilderTx { + fn simulate_builder_txs( + &self, + state_provider: impl StateProvider + Clone, + info: &mut ExecutionInfo, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + let mut builder_txs = Vec::::new(); + let state = StateProviderDatabase::new(state_provider.clone()); + let mut simulation_state = State::builder() + .with_database(state) + .with_cached_prestate(db.cache.clone()) + .with_bundle_update() + .build(); + + if ctx.is_last_flashblock() { + if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { + // We only include flashtestations txs in the last flashblock + let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( + state_provider, + info, + ctx, + &mut simulation_state, + )?; + builder_txs.extend(flashtestations_builder_txs); + } + } else { + // we increment the flashblock number for the next flashblock so we don't increment in the last flashblock + if let Some(signer) = &self.signer { + let mut evm = ctx + .evm_config + .evm_with_env(simulation_state, ctx.evm_env.clone()); + evm.modify_cfg(|cfg| { + cfg.disable_balance_check = true; + }); + + let nonce = get_nonce(evm.db_mut(), signer.address)?; + + let gas_used = + self.estimate_flashblock_number_tx_gas(ctx, &mut evm, signer, nonce)?; + // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer + let tx = + self.signed_flashblock_number_tx(ctx, gas_used * 64 / 63, nonce, signer)?; + let da_size = + op_alloy_flz::tx_estimated_size_fjord_bytes(tx.encoded_2718().as_slice()); + builder_txs.push(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx: tx, + }); + } + } + + Ok(builder_txs) + } +} diff --git a/crates/op-rbuilder/src/builders/flashblocks/config.rs b/crates/op-rbuilder/src/builders/flashblocks/config.rs index c852a3547..f2dca7759 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/config.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/config.rs @@ -1,3 +1,5 @@ +use alloy_primitives::Address; + use crate::{args::OpRbuilderArgs, builders::BuilderConfig}; use core::{ net::{Ipv4Addr, SocketAddr}, @@ -31,6 +33,11 @@ pub struct FlashblocksConfig { /// Should we calculate state root for each flashblock pub calculate_state_root: bool, + + /// The address of the flashblocks number contract. + /// + /// If set a builder tx will be added to the start of every flashblock instead of the regular builder tx. + pub flashblocks_number_contract_address: Option
, } impl Default for FlashblocksConfig { @@ -41,6 +48,7 @@ impl Default for FlashblocksConfig { leeway_time: Duration::from_millis(50), fixed: false, calculate_state_root: true, + flashblocks_number_contract_address: None, } } } @@ -62,12 +70,16 @@ impl TryFrom for FlashblocksConfig { let calculate_state_root = args.flashblocks.flashblocks_calculate_state_root; + let flashblocks_number_contract_address = + args.flashblocks.flashblocks_number_contract_address; + Ok(Self { ws_addr, interval, leeway_time, fixed, calculate_state_root, + flashblocks_number_contract_address, }) } } diff --git a/crates/op-rbuilder/src/builders/flashblocks/service.rs b/crates/op-rbuilder/src/builders/flashblocks/service.rs index a8b4d3ba2..46ee8ae88 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/service.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/service.rs @@ -3,7 +3,10 @@ use crate::{ builders::{ BuilderConfig, builder_tx::BuilderTransactions, - flashblocks::{builder_tx::FlashblocksBuilderTx, payload::FlashblocksExtraCtx}, + flashblocks::{ + builder_tx::{FlashblocksBuilderTx, FlashblocksNumberBuilderTx}, + payload::FlashblocksExtraCtx, + }, generator::BlockPayloadJobGenerator, }, flashtestations::service::bootstrap_flashtestations, @@ -92,10 +95,25 @@ where } else { None }; - self.spawn_payload_builder_service( - ctx, - pool, - FlashblocksBuilderTx::new(signer, flashtestations_builder_tx), - ) + + if let Some(flashblocks_number_contract_address) = + self.0.specific.flashblocks_number_contract_address + { + self.spawn_payload_builder_service( + ctx, + pool, + FlashblocksNumberBuilderTx::new( + signer, + flashblocks_number_contract_address, + flashtestations_builder_tx, + ), + ) + } else { + self.spawn_payload_builder_service( + ctx, + pool, + FlashblocksBuilderTx::new(signer, flashtestations_builder_tx), + ) + } } } diff --git a/crates/op-rbuilder/src/tests/flashblocks.rs b/crates/op-rbuilder/src/tests/flashblocks.rs index d9789457d..8204af75f 100644 --- a/crates/op-rbuilder/src/tests/flashblocks.rs +++ b/crates/op-rbuilder/src/tests/flashblocks.rs @@ -17,6 +17,7 @@ use crate::{ flashblocks_leeway_time: 100, flashblocks_fixed: false, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -55,6 +56,7 @@ async fn smoke_dynamic_base(rbuilder: LocalInstance) -> eyre::Result<()> { flashblocks_leeway_time: 100, flashblocks_fixed: false, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -93,6 +95,7 @@ async fn smoke_dynamic_unichain(rbuilder: LocalInstance) -> eyre::Result<()> { flashblocks_leeway_time: 50, flashblocks_fixed: true, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -131,6 +134,7 @@ async fn smoke_classic_unichain(rbuilder: LocalInstance) -> eyre::Result<()> { flashblocks_leeway_time: 50, flashblocks_fixed: true, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -169,6 +173,7 @@ async fn smoke_classic_base(rbuilder: LocalInstance) -> eyre::Result<()> { flashblocks_leeway_time: 100, flashblocks_fixed: false, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -214,6 +219,7 @@ async fn unichain_dynamic_with_lag(rbuilder: LocalInstance) -> eyre::Result<()> flashblocks_leeway_time: 0, flashblocks_fixed: false, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -252,6 +258,7 @@ async fn dynamic_with_full_block_lag(rbuilder: LocalInstance) -> eyre::Result<() flashblocks_leeway_time: 100, flashblocks_fixed: false, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -312,6 +319,7 @@ async fn test_flashblock_min_filtering(rbuilder: LocalInstance) -> eyre::Result< flashblocks_leeway_time: 100, flashblocks_fixed: false, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -368,6 +376,7 @@ async fn test_flashblock_max_filtering(rbuilder: LocalInstance) -> eyre::Result< flashblocks_leeway_time: 100, flashblocks_fixed: false, flashblocks_calculate_state_root: true, + flashblocks_number_contract_address: None, }, ..Default::default() })] @@ -413,6 +422,7 @@ async fn test_flashblock_min_max_filtering(rbuilder: LocalInstance) -> eyre::Res flashblocks_leeway_time: 100, flashblocks_fixed: false, flashblocks_calculate_state_root: false, + flashblocks_number_contract_address: None, }, ..Default::default() })] From 8b46f449ab302422027a77541ca5e8ab88a0503a Mon Sep 17 00:00:00 2001 From: avalonche Date: Wed, 10 Sep 2025 12:32:56 +1000 Subject: [PATCH 6/9] add docs --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 105db0739..333e19720 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,22 @@ cargo run -p op-rbuilder --bin op-rbuilder -- node \ --flashblocks.addr 127.0.0.1 # address to bind the ws that provides flashblocks ``` +#### Flashblocks Number Contract + +To enable builder tranctions to the [flashblocks number contract](https://github.com/Uniswap/flashblocks_number_contract) for contracts to integrate with flashblocks onchain, specify the address in the CLI args: + +```bash +cargo run -p op-rbuilder --bin op-rbuilder -- node \ + --chain /path/to/chain-config.json \ + --http \ + --authrpc.port 9551 \ + --authrpc.jwtsecret /path/to/jwt.hex \ + --flashblocks.enabled \ + --flashblocks.number-contract-address 0xFlashblocksNumberAddress +``` + +This will increment the flashblock number before the start of every flashblock and replace the builder tx at the end of the block. + ### Flashtestations To run op-rbuilder with flashtestations: From 01d93a099a03c1d3e56a8817dac4377b068f1eb7 Mon Sep 17 00:00:00 2001 From: avalonche Date: Fri, 12 Sep 2025 02:47:28 +1000 Subject: [PATCH 7/9] add builder tx to fallback block --- crates/op-rbuilder/src/builders/builder_tx.rs | 97 +++++++++++- .../src/builders/flashblocks/builder_tx.rs | 141 +++++------------- .../src/builders/flashblocks/payload.rs | 23 ++- .../src/builders/standard/builder_tx.rs | 92 +----------- 4 files changed, 151 insertions(+), 202 deletions(-) diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index c9a3b6011..4f01b4b0e 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -1,9 +1,12 @@ +use alloy_consensus::TxEip1559; +use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN}; use alloy_evm::Database; use alloy_primitives::{ - Address, + Address, TxKind, map::foldhash::{HashSet, HashSetExt}, }; use core::fmt::Debug; +use op_alloy_consensus::OpTypedTransaction; use op_revm::OpTransactionError; use reth_evm::{ConfigureEvm, Evm, eth::receipt_builder::ReceiptBuilderCtx}; use reth_node_api::PayloadBuilderError; @@ -19,7 +22,9 @@ use revm::{ }; use tracing::warn; -use crate::{builders::context::OpPayloadBuilderCtx, primitives::reth::ExecutionInfo}; +use crate::{ + builders::context::OpPayloadBuilderCtx, primitives::reth::ExecutionInfo, tx_signer::Signer, +}; #[derive(Debug, Clone)] pub struct BuilderTransactionCtx { @@ -179,6 +184,94 @@ pub trait BuilderTransactions: Debug { } } +#[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: &OpPayloadBuilderCtx, + db: &mut State, + ) -> 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, + })) + } + 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: &OpPayloadBuilderCtx, + db: &mut State, + signer: Signer, + gas_used: u64, + message: Vec, + ) -> Result, BuilderTransactionError> { + let nonce = db + .load_cache_account(signer.address) + .map(|acc| acc.account_info().unwrap_or_default().nonce) + .map_err(|_| BuilderTransactionError::AccountLoadFailed(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(super) fn get_nonce( db: &mut State, address: Address, diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs index 95e0f7ba7..8322526b5 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -1,5 +1,5 @@ use alloy_consensus::TxEip1559; -use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN}; +use alloy_eips::Encodable2718; use alloy_evm::{Database, Evm}; use alloy_op_evm::OpEvm; use alloy_primitives::{Address, Bytes, TxKind, U256}; @@ -19,8 +19,10 @@ use revm::{ use crate::{ builders::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, builder_tx::get_nonce, - context::OpPayloadBuilderCtx, flashblocks::payload::FlashblocksExtraCtx, + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, + builder_tx::{BuilderTxBase, get_nonce}, + context::OpPayloadBuilderCtx, + flashblocks::payload::FlashblocksExtraCtx, }, flashtestations::service::FlashtestationsBuilderTx, primitives::reth::ExecutionInfo, @@ -28,15 +30,14 @@ use crate::{ }; sol!( + // From https://github.com/Uniswap/flashblocks_number_contract/blob/main/src/FlashblockNumber.sol #[sol(rpc, abi)] interface IFlashblockNumber { function incrementFlashblockNumber() external; } - /** - * @notice Emitted when flashblock index is incremented - * @param newFlashblockIndex The new flashblock index (0-indexed within each L2 block) - */ + // @notice Emitted when flashblock index is incremented + // @param newFlashblockIndex The new flashblock index (0-indexed within each L2 block) event FlashblockIncremented(uint256 newFlashblockIndex); /// ----------------------------------------------------------------------- @@ -90,7 +91,7 @@ impl From for FlashblockNumberError { // This will be the end of block transaction of a regular block #[derive(Debug, Clone)] pub(super) struct FlashblocksBuilderTx { - pub signer: Option, + pub base_builder_tx: BuilderTxBase, pub flashtestations_builder_tx: Option, } @@ -99,88 +100,12 @@ impl FlashblocksBuilderTx { signer: Option, flashtestations_builder_tx: Option, ) -> Self { + let base_builder_tx = BuilderTxBase::new(signer); Self { - signer, + base_builder_tx, flashtestations_builder_tx, } } - - pub(super) fn simulate_builder_tx( - &self, - ctx: &OpPayloadBuilderCtx, - db: &mut State, - ) -> 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: Some(signed_tx), - })) - } - 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: &OpPayloadBuilderCtx, - db: &mut State, - signer: Signer, - gas_used: u64, - message: Vec, - ) -> Result, BuilderTransactionError> { - let nonce = db - .load_cache_account(signer.address) - .map(|acc| acc.account_info().unwrap_or_default().nonce) - .map_err(|_| BuilderTransactionError::AccountLoadFailed(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) - } } impl BuilderTransactions for FlashblocksBuilderTx { @@ -195,16 +120,15 @@ impl BuilderTransactions for FlashblocksBuilderTx { let mut builder_txs = Vec::::new(); if ctx.is_first_flashblock() { - let flashblocks_builder_tx = self.simulate_builder_tx(ctx, db)?; + let flashblocks_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; builder_txs.extend(flashblocks_builder_tx.clone()); } if ctx.is_last_flashblock() { - let flashblocks_builder_tx = self.simulate_builder_tx(ctx, db)?; + let flashblocks_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; builder_txs.extend(flashblocks_builder_tx.clone()); if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { // We only include flashtestations txs in the last flashblock - let mut simulation_state = self.simulate_builder_txs_state::( state_provider.clone(), flashblocks_builder_tx.iter().collect(), @@ -230,6 +154,7 @@ impl BuilderTransactions for FlashblocksBuilderTx { pub(super) struct FlashblocksNumberBuilderTx { pub signer: Option, pub flashblock_number_address: Address, + pub base_builder_tx: BuilderTxBase, pub flashtestations_builder_tx: Option, } @@ -239,9 +164,11 @@ impl FlashblocksNumberBuilderTx { flashblock_number_address: Address, flashtestations_builder_tx: Option, ) -> Self { + let base_builder_tx = BuilderTxBase::new(signer); Self { signer, flashblock_number_address, + base_builder_tx, flashtestations_builder_tx, } } @@ -309,23 +236,15 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); let state = StateProviderDatabase::new(state_provider.clone()); - let mut simulation_state = State::builder() + let simulation_state = State::builder() .with_database(state) .with_cached_prestate(db.cache.clone()) .with_bundle_update() .build(); - if ctx.is_last_flashblock() { - if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { - // We only include flashtestations txs in the last flashblock - let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( - state_provider, - info, - ctx, - &mut simulation_state, - )?; - builder_txs.extend(flashtestations_builder_txs); - } + if ctx.is_first_flashblock() { + let flashblocks_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; + builder_txs.extend(flashblocks_builder_tx.clone()); } else { // we increment the flashblock number for the next flashblock so we don't increment in the last flashblock if let Some(signer) = &self.signer { @@ -353,6 +272,26 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { } } + if ctx.is_last_flashblock() { + if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { + let flashblocks_builder_txs = builder_txs.clone(); + let mut simulation_state = self.simulate_builder_txs_state::( + state_provider.clone(), + flashblocks_builder_txs.iter().collect(), + ctx, + db, + )?; + // We only include flashtestations txs in the last flashblock + let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( + state_provider, + info, + ctx, + &mut simulation_state, + )?; + builder_txs.extend(flashtestations_builder_txs); + } + } + Ok(builder_txs) } } diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 6c81a64fd..0a15eb21a 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -581,17 +581,16 @@ where ); let flashblock_build_start_time = Instant::now(); - let builder_txs = - match self - .builder_tx - .add_builder_txs(&state_provider, info, ctx, state, true) - { - Ok(builder_txs) => builder_txs, - Err(e) => { - error!(target: "payload_builder", "Error simulating builder txs: {}", e); - vec![] - } - }; + let builder_txs = match self + .builder_tx + .add_builder_txs(&state_provider, info, &ctx, state, true) + { + Ok(builder_txs) => builder_txs, + Err(e) => { + error!(target: "payload_builder", "Error simulating builder txs: {}", e); + vec![] + } + }; let builder_tx_gas = builder_txs.iter().fold(0, |acc, tx| acc + tx.gas_used); let builder_tx_da_size: u64 = builder_txs.iter().fold(0, |acc, tx| acc + tx.da_size); @@ -659,7 +658,7 @@ where .builder_tx .add_builder_txs(&state_provider, info, ctx, state, false) { - Ok(_) => {} + Ok(builder_txs) => builder_txs, Err(e) => { error!(target: "payload_builder", "Error simulating builder txs: {}", e); } diff --git a/crates/op-rbuilder/src/builders/standard/builder_tx.rs b/crates/op-rbuilder/src/builders/standard/builder_tx.rs index 426e272a0..c4d154f63 100644 --- a/crates/op-rbuilder/src/builders/standard/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/standard/builder_tx.rs @@ -1,18 +1,12 @@ -use alloy_consensus::TxEip1559; -use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN}; use alloy_evm::Database; -use alloy_primitives::{Address, TxKind}; use core::fmt::Debug; -use op_alloy_consensus::OpTypedTransaction; -use reth_optimism_primitives::OpTransactionSigned; -use reth_primitives::Recovered; use reth_provider::StateProvider; use reth_revm::State; use crate::{ builders::{ BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, - context::OpPayloadBuilderCtx, + builder_tx::BuilderTxBase, context::OpPayloadBuilderCtx, }, flashtestations::service::FlashtestationsBuilderTx, primitives::reth::ExecutionInfo, @@ -22,7 +16,7 @@ use crate::{ // This will be the end of block transaction of a regular block #[derive(Debug, Clone)] pub(super) struct StandardBuilderTx { - pub signer: Option, + pub base_builder_tx: BuilderTxBase, pub flashtestations_builder_tx: Option, } @@ -31,88 +25,12 @@ impl StandardBuilderTx { signer: Option, flashtestations_builder_tx: Option, ) -> Self { + let base_builder_tx = BuilderTxBase::new(signer); Self { - signer, + base_builder_tx, flashtestations_builder_tx, } } - - pub(super) fn simulate_builder_tx( - &self, - ctx: &OpPayloadBuilderCtx, - db: &mut State, - ) -> 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: Some(signed_tx), - })) - } - 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: &OpPayloadBuilderCtx, - db: &mut State, - signer: Signer, - gas_used: u64, - message: Vec, - ) -> Result, BuilderTransactionError> { - let nonce = db - .load_cache_account(signer.address) - .map(|acc| acc.account_info().unwrap_or_default().nonce) - .map_err(|_| BuilderTransactionError::AccountLoadFailed(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) - } } impl BuilderTransactions for StandardBuilderTx { @@ -125,7 +43,7 @@ impl BuilderTransactions for StandardBuilderTx { top_of_block: bool, ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); - let standard_builder_tx = self.simulate_builder_tx(ctx, db)?; + let standard_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; builder_txs.extend(standard_builder_tx.clone()); if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { let mut simulation_state = self.simulate_builder_txs_state::<()>( From fd9e078e20abe4d5394dee5b04ff3d2f0b101dd6 Mon Sep 17 00:00:00 2001 From: avalonche Date: Thu, 18 Sep 2025 07:58:57 +1000 Subject: [PATCH 8/9] fallback to legacy builder tx --- .../src/builders/flashblocks/builder_tx.rs | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs index 8322526b5..8f751f086 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -16,6 +16,7 @@ use revm::{ context::result::{ExecutionResult, ResultAndState}, inspector::NoOpInspector, }; +use tracing::warn; use crate::{ builders::{ @@ -257,18 +258,33 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { let nonce = get_nonce(evm.db_mut(), signer.address)?; - let gas_used = - self.estimate_flashblock_number_tx_gas(ctx, &mut evm, signer, nonce)?; - // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer - let tx = - self.signed_flashblock_number_tx(ctx, gas_used * 64 / 63, nonce, signer)?; - let da_size = - op_alloy_flz::tx_estimated_size_fjord_bytes(tx.encoded_2718().as_slice()); - builder_txs.push(BuilderTransactionCtx { - gas_used, - da_size, - signed_tx: tx, - }); + let tx = match self.estimate_flashblock_number_tx_gas(ctx, &mut evm, signer, nonce) + { + Ok(gas_used) => { + // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer + let flashblocks_tx = self.signed_flashblock_number_tx( + ctx, + gas_used * 64 / 63, + nonce, + signer, + )?; + + let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( + flashblocks_tx.encoded_2718().as_slice(), + ); + Some(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx: flashblocks_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, db)? + } + }; + + builder_txs.extend(tx); } } From fc58c489f20c603e37338b5448961b3d5a4f6067 Mon Sep 17 00:00:00 2001 From: avalonche Date: Fri, 19 Sep 2025 05:45:42 +1000 Subject: [PATCH 9/9] allow top or bottom of block builder tx --- crates/op-rbuilder/src/builders/builder_tx.rs | 10 ++++-- .../src/builders/flashblocks/builder_tx.rs | 35 +++++++++++++++++-- .../src/builders/flashblocks/payload.rs | 22 ++++++------ 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 4f01b4b0e..b79200d4e 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -106,8 +106,12 @@ pub trait BuilderTransactions: Debug { top_of_block, )?; 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"); + let signed_tx = match builder_tx.signed_tx.clone() { + Some(tx) => tx, + None => continue, + }; + if invalid.contains(&signed_tx.signer()) { + warn!(target: "payload_builder", tx_hash = ?signed_tx.tx_hash(), "builder signer invalid as previous builder tx reverted"); continue; } @@ -210,7 +214,7 @@ impl BuilderTxBase { Ok(Some(BuilderTransactionCtx { gas_used, da_size, - signed_tx, + signed_tx: Some(signed_tx), })) } None => Ok(None), diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs index 8f751f086..3445bb2e5 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -127,7 +127,18 @@ impl BuilderTransactions for FlashblocksBuilderTx { if ctx.is_last_flashblock() { let flashblocks_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; - builder_txs.extend(flashblocks_builder_tx.clone()); + if let Some(tx) = flashblocks_builder_tx.clone() { + if top_of_block { + // don't commit the builder if top of block, we only return the gas used to reserve gas for the builder tx + builder_txs.push(BuilderTransactionCtx { + gas_used: tx.gas_used, + da_size: tx.da_size, + signed_tx: None, + }); + } else { + builder_txs.push(tx); + } + } if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { // We only include flashtestations txs in the last flashblock let mut simulation_state = self.simulate_builder_txs_state::( @@ -234,6 +245,7 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, + top_of_block: bool, ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); let state = StateProviderDatabase::new(state_provider.clone()); @@ -275,12 +287,28 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { Some(BuilderTransactionCtx { gas_used, da_size, - signed_tx: flashblocks_tx, + signed_tx: if top_of_block { + Some(flashblocks_tx) + } else { + None + }, // number tx at top of flashblock }) } 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, db)? + let builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; + if let Some(tx) = &builder_tx + && top_of_block + { + // don't commit the builder if top of block, we only return the gas used to reserve gas for the builder tx + Some(BuilderTransactionCtx { + gas_used: tx.gas_used, + da_size: tx.da_size, + signed_tx: None, + }) + } else { + builder_tx + } } }; @@ -303,6 +331,7 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { info, ctx, &mut simulation_state, + top_of_block, )?; builder_txs.extend(flashtestations_builder_txs); } diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 0a15eb21a..ae11a6da6 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -581,16 +581,17 @@ where ); let flashblock_build_start_time = Instant::now(); - let builder_txs = match self - .builder_tx - .add_builder_txs(&state_provider, info, &ctx, state, true) - { - Ok(builder_txs) => builder_txs, - Err(e) => { - error!(target: "payload_builder", "Error simulating builder txs: {}", e); - vec![] - } - }; + let builder_txs = + match self + .builder_tx + .add_builder_txs(&state_provider, info, ctx, state, true) + { + Ok(builder_txs) => builder_txs, + Err(e) => { + error!(target: "payload_builder", "Error simulating builder txs: {}", e); + vec![] + } + }; let builder_tx_gas = builder_txs.iter().fold(0, |acc, tx| acc + tx.gas_used); let builder_tx_da_size: u64 = builder_txs.iter().fold(0, |acc, tx| acc + tx.da_size); @@ -661,6 +662,7 @@ where Ok(builder_txs) => builder_txs, Err(e) => { error!(target: "payload_builder", "Error simulating builder txs: {}", e); + vec![] } };