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: 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 8ff168b14..b79200d4e 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,13 +22,15 @@ 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 { pub gas_used: u64, pub da_size: u64, - pub signed_tx: Recovered, + pub signed_tx: Option>, } /// Possible error variants during construction of builder txs. @@ -75,6 +80,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 +89,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 +98,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()) { + warn!(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 +130,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 +142,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 @@ -148,7 +164,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 @@ -156,8 +172,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); @@ -167,3 +187,100 @@ pub trait BuilderTransactions: Debug { Ok(simulation_state) } } + +#[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: 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) + } +} + +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 be29e6aaa..3445bb2e5 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -1,28 +1,98 @@ 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_eips::Encodable2718; +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 tracing::warn; use crate::{ builders::{ BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, - context::OpPayloadBuilderCtx, flashblocks::payload::FlashblocksExtraCtx, + builder_tx::{BuilderTxBase, get_nonce}, + context::OpPayloadBuilderCtx, + flashblocks::payload::FlashblocksExtraCtx, }, flashtestations::service::FlashtestationsBuilderTx, primitives::reth::ExecutionInfo, tx_signer::Signer, }; +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) + 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 { - pub signer: Option, + pub base_builder_tx: BuilderTxBase, pub flashtestations_builder_tx: Option, } @@ -31,126 +101,242 @@ 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( +impl BuilderTransactions for FlashblocksBuilderTx { + fn simulate_builder_txs( &self, - ctx: &OpPayloadBuilderCtx, + state_provider: impl StateProvider + Clone, + info: &mut ExecutionInfo, + 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, - })) + top_of_block: bool, + ) -> Result, BuilderTransactionError> { + let mut builder_txs = Vec::::new(); + + 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()); + } + + if ctx.is_last_flashblock() { + let flashblocks_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; + 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::( + state_provider.clone(), + flashblocks_builder_tx.iter().collect(), + ctx, + db, + )?; + let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( + state_provider, + info, + ctx, + &mut simulation_state, + top_of_block, + )?; + builder_txs.extend(flashtestations_builder_txs); } - None => Ok(None), } + Ok(builder_txs) } +} - 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) - } - }); +// 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 base_builder_tx: BuilderTxBase, + pub flashtestations_builder_tx: Option, +} - // 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; +impl FlashblocksNumberBuilderTx { + pub(super) fn new( + signer: Option, + 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, + } + } - // 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; + 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))); + } + }; - std::cmp::max(zero_cost + nonzero_cost + 21_000, floor_gas) + 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_builder_tx( + fn signed_flashblock_number_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))?; - + 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: gas_used, + gas_limit, 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(), + to: TxKind::Call(self.flashblock_number_address), + input: calldata.into(), ..Default::default() }); - // Sign the transaction - let builder_tx = signer - .sign_tx(tx) - .map_err(BuilderTransactionError::SigningError)?; - - Ok(builder_tx) + signer.sign_tx(tx) } } -impl BuilderTransactions for FlashblocksBuilderTx { +impl BuilderTransactions for FlashblocksNumberBuilderTx { fn simulate_builder_txs( &self, state_provider: impl StateProvider + Clone, 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()); + let simulation_state = State::builder() + .with_database(state) + .with_cached_prestate(db.cache.clone()) + .with_bundle_update() + .build(); 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()); + } 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 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: 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"); + 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 + } + } + }; + + builder_txs.extend(tx); + } } if ctx.is_last_flashblock() { - let flashblocks_builder_tx = self.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 flashblocks_builder_txs = builder_txs.clone(); let mut simulation_state = self.simulate_builder_txs_state::( state_provider.clone(), - flashblocks_builder_tx.iter().collect(), + 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, + top_of_block, )?; builder_txs.extend(flashtestations_builder_txs); } } + 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/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 05228f829..ae11a6da6 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,16 @@ 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(builder_txs) => builder_txs, + Err(e) => { + error!(target: "payload_builder", "Error simulating builder txs: {}", e); + vec![] + } + }; let total_block_built_duration = Instant::now(); let build_result = build_block( 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/builders/standard/builder_tx.rs b/crates/op-rbuilder/src/builders/standard/builder_tx.rs index 23c39f3c8..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, - })) - } - 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 { @@ -122,9 +40,10 @@ 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)?; + 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::<()>( @@ -138,6 +57,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![]) } 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() })]