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()
})]