diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 15ecc0d8..91d2c915 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -1,14 +1,18 @@ -use alloy_consensus::TxEip1559; +use alloy_consensus::{Transaction, TxEip1559}; use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN}; use alloy_evm::Database; +use alloy_op_evm::OpEvm; use alloy_primitives::{ - Address, B256, Log, TxKind, U256, - map::foldhash::{HashSet, HashSetExt}, + Address, B256, Bytes, TxKind, U256, + map::foldhash::{HashMap, 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 op_revm::{OpHaltReason, OpTransactionError}; +use reth_evm::{ + ConfigureEvm, Evm, EvmError, eth::receipt_builder::ReceiptBuilderCtx, + precompiles::PrecompilesMap, +}; use reth_node_api::PayloadBuilderError; use reth_optimism_primitives::OpTransactionSigned; use reth_primitives::Recovered; @@ -16,7 +20,9 @@ use reth_provider::{ProviderError, StateProvider}; use reth_revm::{State, database::StateProviderDatabase}; use revm::{ DatabaseCommit, - context::result::{EVMError, ResultAndState}, + context::result::{EVMError, ExecutionResult, ResultAndState}, + inspector::NoOpInspector, + state::Account, }; use tracing::warn; @@ -24,6 +30,13 @@ use crate::{ builders::context::OpPayloadBuilderCtx, primitives::reth::ExecutionInfo, tx_signer::Signer, }; +#[derive(Debug, Default)] +pub struct SimulationSuccessResult { + pub gas_used: u64, + pub output: Bytes, + pub state_changes: HashMap, +} + #[derive(Debug, Clone)] pub struct BuilderTransactionCtx { pub gas_used: u64, @@ -46,6 +59,14 @@ impl BuilderTransactionCtx { } } +#[derive(Debug, thiserror::Error)] +pub enum InvalidContractDataError { + #[error("did not find expected log {0:?} in emitted logs")] + InvalidLogs(B256), + #[error("could not decode output from contract call")] + OutputAbiDecodeError, +} + /// Possible error variants during construction of builder txs. #[derive(Debug, thiserror::Error)] pub enum BuilderTransactionError { @@ -55,6 +76,15 @@ pub enum BuilderTransactionError { /// Signature signing fails #[error("failed to sign transaction: {0}")] SigningError(secp256k1::Error), + /// Invalid contract errors indicating the contract is incorrect + #[error("contract {0} may be incorrect, invalid contract data: {1}")] + InvalidContract(Address, InvalidContractDataError), + /// Transaction halted execution + #[error("transaction halted {0:?}")] + TransactionHalted(OpHaltReason), + /// Transaction reverted + #[error("transaction reverted {0}")] + TransactionReverted(Box), /// Invalid tx errors during evm execution. #[error("invalid transaction error {0}")] InvalidTransactionError(Box), @@ -90,24 +120,26 @@ impl From for PayloadBuilderError { } impl BuilderTransactionError { - pub fn other(error: E) -> Self - where - E: core::error::Error + Send + Sync + 'static, - { + pub fn other(error: impl core::error::Error + Send + Sync + 'static) -> Self { BuilderTransactionError::Other(Box::new(error)) } + + pub fn msg(msg: impl core::fmt::Display) -> Self { + Self::Other(msg.to_string().into()) + } } -pub trait BuilderTransactions: Debug { - fn simulate_builder_txs( +pub trait BuilderTransactions { + fn simulate_builder_txs( &self, state_provider: impl StateProvider + Clone, info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, + top_of_block: bool, ) -> Result, BuilderTransactionError>; - fn add_builder_txs( + fn add_builder_txs( &self, state_provider: impl StateProvider + Clone, info: &mut ExecutionInfo, @@ -122,8 +154,13 @@ 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 builder_tx.is_top_of_block != top_of_block { // don't commit tx if the buidler tx is not being added in the intended @@ -140,7 +177,7 @@ pub trait BuilderTransactions: Debug { .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"); + warn!(target: "payload_builder", tx_hash = ?builder_tx.signed_tx.tx_hash(), result = ?result, "builder tx reverted"); invalid.insert(builder_tx.signed_tx.signer()); continue; } @@ -174,7 +211,7 @@ pub trait BuilderTransactions: Debug { } } - fn simulate_builder_txs_state( + fn simulate_builder_txs_state( &self, state_provider: impl StateProvider + Clone, builder_txs: Vec<&BuilderTransactionCtx>, @@ -201,19 +238,101 @@ pub trait BuilderTransactions: Debug { Ok(simulation_state) } + + fn sign_tx( + &self, + to: Address, + from: Signer, + gas_used: Option, + calldata: Bytes, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + let nonce = get_nonce(db, from.address)?; + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: ctx.chain_id(), + nonce, + // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer + gas_limit: gas_used + .map(|gas| gas * 64 / 63) + .unwrap_or(ctx.block_gas_limit()), + max_fee_per_gas: ctx.base_fee().into(), + to: TxKind::Call(to), + input: calldata, + ..Default::default() + }); + Ok(from.sign_tx(tx)?) + } + + fn simulate_call( + &self, + signed_tx: Recovered, + expected_topic: Option, + revert_handler: impl FnOnce(Bytes) -> BuilderTransactionError, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let ResultAndState { result, state } = match evm.transact(&signed_tx) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + return Err(BuilderTransactionError::InvalidTransactionError(Box::new( + err, + ))); + } else { + return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); + } + } + }; + + match result { + ExecutionResult::Success { + logs, + gas_used, + output, + .. + } => { + if let Some(topic) = expected_topic + && !logs.iter().any(|log| log.topics().first() == Some(&topic)) + { + return Err(BuilderTransactionError::InvalidContract( + signed_tx.to().unwrap_or_default(), + InvalidContractDataError::InvalidLogs(topic), + )); + } + Ok(SimulationSuccessResult { + gas_used, + output: output.into_data(), + state_changes: state, + }) + } + ExecutionResult::Revert { output, .. } => Err(revert_handler(output)), + ExecutionResult::Halt { reason, .. } => Err(BuilderTransactionError::other( + BuilderTransactionError::TransactionHalted(reason), + )), + } + } } #[derive(Debug, Clone)] -pub(super) struct BuilderTxBase { +pub(super) struct BuilderTxBase { pub signer: Option, + _marker: std::marker::PhantomData, } -impl BuilderTxBase { +impl BuilderTxBase { pub(super) fn new(signer: Option) -> Self { - Self { signer } + Self { + signer, + _marker: std::marker::PhantomData, + } } - pub(super) fn simulate_builder_tx( + pub(super) fn simulate_builder_tx( &self, ctx: &OpPayloadBuilderCtx, db: &mut State, @@ -258,7 +377,7 @@ impl BuilderTxBase { std::cmp::max(zero_cost + nonzero_cost + 21_000, floor_gas) } - fn signed_builder_tx( + fn signed_builder_tx( &self, ctx: &OpPayloadBuilderCtx, db: &mut State, @@ -309,7 +428,3 @@ pub fn get_balance( .map(|acc| acc.account_info().unwrap_or_default().balance) .map_err(|_| BuilderTransactionError::AccountLoadFailed(address)) } - -pub fn log_exists(logs: &[Log], topic: &B256) -> bool { - logs.iter().any(|log| log.topics().first() == Some(topic)) -} diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs index b1b16906..7635cee8 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -2,7 +2,7 @@ use alloy_consensus::TxEip1559; use alloy_eips::Encodable2718; use alloy_evm::{Database, Evm}; use alloy_op_evm::OpEvm; -use alloy_primitives::{Address, B256, TxKind}; +use alloy_primitives::{Address, TxKind}; use alloy_sol_types::{Error, SolCall, SolEvent, SolInterface, sol}; use core::fmt::Debug; use op_alloy_consensus::OpTypedTransaction; @@ -21,9 +21,9 @@ use tracing::warn; use crate::{ builders::{ BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, - builder_tx::{BuilderTxBase, get_nonce, log_exists}, + builder_tx::{BuilderTxBase, get_nonce}, context::OpPayloadBuilderCtx, - flashblocks::payload::FlashblocksExtraCtx, + flashblocks::payload::{FlashblocksExecutionInfo, FlashblocksExtraCtx}, }, flashtestations::builder_tx::FlashtestationsBuilderTx, primitives::reth::ExecutionInfo, @@ -53,8 +53,6 @@ sol!( pub(super) enum FlashblockNumberError { #[error("flashblocks number contract tx reverted: {0:?}")] Revert(IFlashblockNumber::IFlashblockNumberErrors), - #[error("contract may be invalid, mismatch in log emitted: expected {0:?}")] - LogMismatch(B256), #[error("unknown revert: {0} err: {1}")] Unknown(String, Error), #[error("halt: {0:?}")] @@ -64,14 +62,17 @@ pub(super) enum FlashblockNumberError { // This will be the end of block transaction of a regular block #[derive(Debug, Clone)] pub(super) struct FlashblocksBuilderTx { - pub base_builder_tx: BuilderTxBase, - pub flashtestations_builder_tx: Option, + pub base_builder_tx: BuilderTxBase, + pub flashtestations_builder_tx: + Option>, } impl FlashblocksBuilderTx { pub(super) fn new( signer: Option, - flashtestations_builder_tx: Option, + flashtestations_builder_tx: Option< + FlashtestationsBuilderTx, + >, ) -> Self { let base_builder_tx = BuilderTxBase::new(signer); Self { @@ -81,13 +82,14 @@ impl FlashblocksBuilderTx { } } -impl BuilderTransactions for FlashblocksBuilderTx { - fn simulate_builder_txs( +impl BuilderTransactions for FlashblocksBuilderTx { + fn simulate_builder_txs( &self, state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, + info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, + top_of_block: bool, ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); @@ -102,19 +104,30 @@ impl BuilderTransactions for FlashblocksBuilderTx { 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::( + let mut simulation_state = self.simulate_builder_txs_state( state_provider.clone(), - base_tx.iter().collect(), + base_tx + .iter() + .filter(|tx| tx.is_top_of_block == top_of_block) + .collect(), ctx, db, )?; - let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( + // We only include flashtestations txs in the last flashblock + match flashtestations_builder_tx.simulate_builder_txs( state_provider, info, ctx, &mut simulation_state, - )?; - builder_txs.extend(flashtestations_builder_txs); + top_of_block, + ) { + Ok(flashtestations_builder_txs) => { + builder_txs.extend(flashtestations_builder_txs) + } + Err(e) => { + warn!(target: "flashtestations", error = ?e, "failed to add flashtestations builder tx") + } + } } } Ok(builder_txs) @@ -126,15 +139,18 @@ 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, + pub base_builder_tx: BuilderTxBase, + pub flashtestations_builder_tx: + Option>, } impl FlashblocksNumberBuilderTx { pub(super) fn new( signer: Option, flashblock_number_address: Address, - flashtestations_builder_tx: Option, + flashtestations_builder_tx: Option< + FlashtestationsBuilderTx, + >, ) -> Self { let base_builder_tx = BuilderTxBase::new(signer); Self { @@ -166,14 +182,15 @@ impl FlashblocksNumberBuilderTx { match result { ExecutionResult::Success { gas_used, logs, .. } => { - if log_exists( - &logs, - &IFlashblockNumber::FlashblockIncremented::SIGNATURE_HASH, - ) { + if logs.iter().any(|log| { + log.topics().first() + == Some(&IFlashblockNumber::FlashblockIncremented::SIGNATURE_HASH) + }) { Ok(gas_used) } else { - Err(BuilderTransactionError::other( - FlashblockNumberError::LogMismatch( + Err(BuilderTransactionError::InvalidContract( + self.flashblock_number_address, + crate::builders::InvalidContractDataError::InvalidLogs( IFlashblockNumber::FlashblockIncremented::SIGNATURE_HASH, ), )) @@ -213,13 +230,16 @@ impl FlashblocksNumberBuilderTx { } } -impl BuilderTransactions for FlashblocksNumberBuilderTx { - fn simulate_builder_txs( +impl BuilderTransactions + for FlashblocksNumberBuilderTx +{ + fn simulate_builder_txs( &self, state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, + 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()); @@ -240,6 +260,7 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { .evm_with_env(simulation_state, ctx.evm_env.clone()); evm.modify_cfg(|cfg| { cfg.disable_balance_check = true; + cfg.disable_block_gas_limit = true; }); let nonce = get_nonce(evm.db_mut(), signer.address)?; @@ -280,20 +301,30 @@ 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::( + let mut simulation_state = self.simulate_builder_txs_state( state_provider.clone(), - flashblocks_builder_txs.iter().collect(), + flashblocks_builder_txs + .iter() + .filter(|tx| tx.is_top_of_block == top_of_block) + .collect(), ctx, db, )?; // We only include flashtestations txs in the last flashblock - let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( + match flashtestations_builder_tx.simulate_builder_txs( state_provider, info, ctx, &mut simulation_state, - )?; - builder_txs.extend(flashtestations_builder_txs); + top_of_block, + ) { + Ok(flashtestations_builder_txs) => { + builder_txs.extend(flashtestations_builder_txs) + } + Err(e) => { + warn!(target: "flashtestations", error = ?e, "failed to add flashtestations builder tx") + } + } } } diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 6a46e4c7..9217548c 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -67,13 +67,13 @@ type NextBestFlashblocksTxs = BestFlashblocksTxs< >, >; -#[derive(Debug, Default)] -struct ExtraExecutionInfo { +#[derive(Debug, Default, Clone)] +pub(super) struct FlashblocksExecutionInfo { /// Index of the last consumed flashblock pub last_flashblock_index: usize, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct FlashblocksExtraCtx { /// Current flashblock index flashblock_index: u64, @@ -211,7 +211,7 @@ impl OpPayloadBuilder where Pool: PoolBounds, Client: ClientBounds, - BuilderTx: BuilderTransactions + Send + Sync, + BuilderTx: BuilderTransactions + Send + Sync, { /// Constructs an Optimism payload from the transactions sent via the /// Payload attributes by the sequencer. If the `no_tx_pool` argument is passed in @@ -542,7 +542,7 @@ where >( &self, ctx: &mut OpPayloadBuilderCtx, - info: &mut ExecutionInfo, + info: &mut ExecutionInfo, state: &mut State, state_provider: impl reth::providers::StateProvider + Clone, best_txs: &mut NextBestFlashblocksTxs, @@ -756,7 +756,7 @@ where fn record_flashblocks_metrics( &self, ctx: &OpPayloadBuilderCtx, - info: &ExecutionInfo, + info: &ExecutionInfo, flashblocks_per_block: u64, span: &tracing::Span, message: &str, @@ -872,7 +872,8 @@ impl PayloadBuilder for OpPayloadBuilder + Clone + Send + Sync, + BuilderTx: + BuilderTransactions + Clone + Send + Sync, { type Attributes = OpPayloadBuilderAttributes; type BuiltPayload = OpBuiltPayload; @@ -896,7 +897,7 @@ struct FlashblocksMetadata { fn execute_pre_steps( state: &mut State, ctx: &OpPayloadBuilderCtx, -) -> Result, PayloadBuilderError> +) -> Result, PayloadBuilderError> where DB: Database + std::fmt::Debug, ExtraCtx: std::fmt::Debug + Default, @@ -916,7 +917,7 @@ where fn build_block( state: &mut State, ctx: &OpPayloadBuilderCtx, - info: &mut ExecutionInfo, + info: &mut ExecutionInfo, calculate_state_root: bool, ) -> Result<(OpBuiltPayload, FlashblocksPayloadV1), PayloadBuilderError> where diff --git a/crates/op-rbuilder/src/builders/flashblocks/service.rs b/crates/op-rbuilder/src/builders/flashblocks/service.rs index 46ee8ae8..584252df 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/service.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/service.rs @@ -5,7 +5,7 @@ use crate::{ builder_tx::BuilderTransactions, flashblocks::{ builder_tx::{FlashblocksBuilderTx, FlashblocksNumberBuilderTx}, - payload::FlashblocksExtraCtx, + payload::{FlashblocksExecutionInfo, FlashblocksExtraCtx}, }, generator::BlockPayloadJobGenerator, }, @@ -32,7 +32,12 @@ impl FlashblocksServiceBuilder { where Node: NodeBounds, Pool: PoolBounds, - BuilderTx: BuilderTransactions + Unpin + Clone + Send + Sync + 'static, + BuilderTx: BuilderTransactions + + Unpin + + Clone + + Send + + Sync + + 'static, { let once_lock = Arc::new(std::sync::OnceLock::new()); @@ -84,8 +89,12 @@ where _: OpEvmConfig, ) -> eyre::Result::Payload>> { let signer = self.0.builder_signer; - let flashtestations_builder_tx = if self.0.flashtestations_config.flashtestations_enabled { - match bootstrap_flashtestations(self.0.flashtestations_config.clone(), ctx).await { + let flashtestations_builder_tx = if let Some(builder_key) = signer + && self.0.flashtestations_config.flashtestations_enabled + { + match bootstrap_flashtestations(self.0.flashtestations_config.clone(), builder_key, ctx) + .await + { Ok(builder_tx) => Some(builder_tx), Err(e) => { tracing::warn!(error = %e, "Failed to bootstrap flashtestations, builder will not include flashtestations txs"); diff --git a/crates/op-rbuilder/src/builders/mod.rs b/crates/op-rbuilder/src/builders/mod.rs index 9dbd949c..c733d111 100644 --- a/crates/op-rbuilder/src/builders/mod.rs +++ b/crates/op-rbuilder/src/builders/mod.rs @@ -22,8 +22,8 @@ mod generator; mod standard; pub use builder_tx::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, get_balance, get_nonce, - log_exists, + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, InvalidContractDataError, + SimulationSuccessResult, get_balance, get_nonce, }; pub use context::OpPayloadBuilderCtx; pub use flashblocks::FlashblocksBuilder; diff --git a/crates/op-rbuilder/src/builders/standard/builder_tx.rs b/crates/op-rbuilder/src/builders/standard/builder_tx.rs index 75a159ad..db69906b 100644 --- a/crates/op-rbuilder/src/builders/standard/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/standard/builder_tx.rs @@ -2,6 +2,7 @@ use alloy_evm::Database; use core::fmt::Debug; use reth_provider::StateProvider; use reth_revm::State; +use tracing::warn; use crate::{ builders::{ @@ -34,30 +35,39 @@ impl StandardBuilderTx { } impl BuilderTransactions for StandardBuilderTx { - fn simulate_builder_txs( + fn simulate_builder_txs( &self, state_provider: impl StateProvider + Clone, - info: &mut ExecutionInfo, + 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.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::<()>( + let mut simulation_state = self.simulate_builder_txs_state( state_provider.clone(), - standard_builder_tx.iter().collect(), + standard_builder_tx + .iter() + .filter(|tx| tx.is_top_of_block == top_of_block) + .collect(), ctx, db, )?; - let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( + match flashtestations_builder_tx.simulate_builder_txs( state_provider, info, ctx, &mut simulation_state, - )?; - builder_txs.extend(flashtestations_builder_txs); + top_of_block, + ) { + Ok(flashtestations_builder_txs) => builder_txs.extend(flashtestations_builder_txs), + Err(e) => { + warn!(target: "flashtestations", error = ?e, "failed to add flashtestations builder tx") + } + } } Ok(builder_txs) } diff --git a/crates/op-rbuilder/src/builders/standard/service.rs b/crates/op-rbuilder/src/builders/standard/service.rs index c713b69c..faf252b1 100644 --- a/crates/op-rbuilder/src/builders/standard/service.rs +++ b/crates/op-rbuilder/src/builders/standard/service.rs @@ -72,8 +72,10 @@ where evm_config: OpEvmConfig, ) -> eyre::Result::Payload>> { let signer = self.0.builder_signer; - let flashtestations_builder_tx = if self.0.flashtestations_config.flashtestations_enabled { - match bootstrap_flashtestations::(self.0.flashtestations_config.clone(), ctx) + let flashtestations_builder_tx = if let Some(builder_key) = signer + && self.0.flashtestations_config.flashtestations_enabled + { + match bootstrap_flashtestations(self.0.flashtestations_config.clone(), builder_key, ctx) .await { Ok(builder_tx) => Some(builder_tx), diff --git a/crates/op-rbuilder/src/flashtestations/args.rs b/crates/op-rbuilder/src/flashtestations/args.rs index 54d5d837..cd0a5583 100644 --- a/crates/op-rbuilder/src/flashtestations/args.rs +++ b/crates/op-rbuilder/src/flashtestations/args.rs @@ -92,6 +92,14 @@ pub struct FlashtestationsArgs { default_value = "1" )] pub builder_proof_version: u8, + + /// Use permit for the flashtestation builder tx + #[arg( + long = "flashtestations.use-permit", + env = "FLASHTESTATIONS_USE_PERMIT", + default_value = "false" + )] + pub flashtestations_use_permit: bool, } impl Default for FlashtestationsArgs { diff --git a/crates/op-rbuilder/src/flashtestations/builder_tx.rs b/crates/op-rbuilder/src/flashtestations/builder_tx.rs index ee5e793c..d18b4f1f 100644 --- a/crates/op-rbuilder/src/flashtestations/builder_tx.rs +++ b/crates/op-rbuilder/src/flashtestations/builder_tx.rs @@ -2,8 +2,10 @@ use alloy_consensus::TxEip1559; use alloy_eips::Encodable2718; use alloy_evm::Database; use alloy_op_evm::OpEvm; -use alloy_primitives::{Address, B256, Bytes, TxKind, U256, keccak256, map::foldhash::HashMap}; -use alloy_sol_types::{Error, SolCall, SolEvent, SolInterface, SolValue}; +use alloy_primitives::{ + Address, B256, Bytes, Signature, TxKind, U256, keccak256, map::foldhash::HashMap, +}; +use alloy_sol_types::{SolCall, SolEvent, SolInterface, SolValue}; use core::fmt::Debug; use op_alloy_consensus::OpTypedTransaction; use reth_evm::{ConfigureEvm, Evm, EvmError, precompiles::PrecompilesMap}; @@ -18,16 +20,18 @@ use revm::{ state::Account, }; use std::sync::OnceLock; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use crate::{ builders::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, OpPayloadBuilderCtx, - get_balance, get_nonce, + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, + InvalidContractDataError, OpPayloadBuilderCtx, SimulationSuccessResult, get_balance, + get_nonce, }, flashtestations::{ BlockData, FlashtestationRevertReason, IBlockBuilderPolicy::{self, BlockBuilderProofVerified}, + IERC20Permit, IFlashtestationRegistry::{self, TEEServiceRegistered}, }, primitives::reth::ExecutionInfo, @@ -45,10 +49,16 @@ pub struct FlashtestationsBuilderTxArgs { pub builder_proof_version: u8, pub enable_block_proofs: bool, pub registered: bool, + pub use_permit: bool, + pub builder_key: Signer, } #[derive(Debug, Clone)] -pub struct FlashtestationsBuilderTx { +pub struct FlashtestationsBuilderTx +where + ExtraCtx: Debug + Default, + Extra: Debug + Default, +{ // Attestation for the builder attestation: Vec, // Extra registration data for the builder @@ -69,6 +79,12 @@ pub struct FlashtestationsBuilderTx { registered: OnceLock, // Whether block proofs are enabled enable_block_proofs: bool, + // Whether to use permit for the flashtestation builder tx + use_permit: bool, + // Builder key for the flashtestation permit tx + builder_signer: Signer, + // Extra context and data + _marker: std::marker::PhantomData<(ExtraCtx, Extra)>, } #[derive(Debug, Default)] @@ -80,7 +96,11 @@ pub struct TxSimulateResult { pub logs: Vec, } -impl FlashtestationsBuilderTx { +impl FlashtestationsBuilderTx +where + ExtraCtx: Debug + Default, + Extra: Debug + Default, +{ pub fn new(args: FlashtestationsBuilderTxArgs) -> Self { Self { attestation: args.attestation, @@ -93,6 +113,9 @@ impl FlashtestationsBuilderTx { builder_proof_version: args.builder_proof_version, registered: OnceLock::new(), enable_block_proofs: args.enable_block_proofs, + use_permit: args.use_permit, + builder_signer: args.builder_key, + _marker: std::marker::PhantomData, } } @@ -148,7 +171,7 @@ impl FlashtestationsBuilderTx { self.tee_service_signer.sign_tx(tx) } - fn signed_block_builder_proof_tx( + fn signed_block_builder_proof_tx( &self, block_content_hash: B256, ctx: &OpPayloadBuilderCtx, @@ -206,7 +229,7 @@ impl FlashtestationsBuilderTx { keccak256(&encoded) } - fn simulate_register_tee_service_tx( + fn simulate_register_tee_service_tx( &self, ctx: &OpPayloadBuilderCtx, evm: &mut OpEvm< @@ -248,8 +271,8 @@ impl FlashtestationsBuilderTx { let revert_reason = IFlashtestationRegistry::IFlashtestationRegistryErrors::abi_decode(&output) .map(FlashtestationRevertReason::FlashtestationRegistry) - .unwrap_or_else(|e| { - FlashtestationRevertReason::Unknown(hex::encode(output), e) + .unwrap_or_else(|_| { + FlashtestationRevertReason::Unknown(hex::encode(output)) }); Ok(TxSimulateResult { gas_used, @@ -263,26 +286,15 @@ impl FlashtestationsBuilderTx { gas_used: 0, success: false, state_changes: state, - revert_reason: Some(FlashtestationRevertReason::Halt(reason)), + revert_reason: Some(FlashtestationRevertReason::Unknown(format!( + "block proof transaction halted {reason:?}" + ))), logs: vec![], }), } } - fn check_tee_address_registered_log(&self, logs: &[Log], address: Address) -> bool { - for log in logs { - if log.topics().first() == Some(&TEEServiceRegistered::SIGNATURE_HASH) { - if let Ok(decoded) = TEEServiceRegistered::decode_log(log) { - if decoded.teeAddress == address { - return true; - } - }; - } - } - false - } - - fn simulate_verify_block_proof_tx( + fn simulate_verify_block_proof_tx( &self, block_content_hash: B256, ctx: &OpPayloadBuilderCtx, @@ -324,8 +336,8 @@ impl FlashtestationsBuilderTx { let revert_reason = IBlockBuilderPolicy::IBlockBuilderPolicyErrors::abi_decode(&output) .map(FlashtestationRevertReason::BlockBuilderPolicy) - .unwrap_or_else(|e| { - FlashtestationRevertReason::Unknown(hex::encode(output), e) + .unwrap_or_else(|_| { + FlashtestationRevertReason::Unknown(hex::encode(output)) }); Ok(TxSimulateResult { gas_used, @@ -339,22 +351,15 @@ impl FlashtestationsBuilderTx { gas_used: 0, success: false, state_changes: state, - revert_reason: Some(FlashtestationRevertReason::Halt(reason)), + revert_reason: Some(FlashtestationRevertReason::Unknown(format!( + "register tee transaction halted {reason:?}" + ))), logs: vec![], }), } } - fn check_verify_block_proof_log(&self, logs: &[Log]) -> bool { - for log in logs { - if log.topics().first() == Some(&BlockBuilderProofVerified::SIGNATURE_HASH) { - return true; - } - } - false - } - - fn fund_tee_service_tx( + fn fund_tee_service_tx( &self, ctx: &OpPayloadBuilderCtx, evm: &mut OpEvm< @@ -401,7 +406,7 @@ impl FlashtestationsBuilderTx { } } - fn register_tee_service_tx( + fn register_tee_service_tx( &self, ctx: &OpPayloadBuilderCtx, evm: &mut OpEvm< @@ -418,12 +423,13 @@ impl FlashtestationsBuilderTx { logs, } = self.simulate_register_tee_service_tx(ctx, evm)?; if success { - if !self.check_tee_address_registered_log(&logs, self.tee_service_signer.address) { - Err(BuilderTransactionError::other( - FlashtestationRevertReason::LogMismatch( - self.registry_address, - TEEServiceRegistered::SIGNATURE_HASH, - ), + if !logs + .iter() + .any(|log| log.topics().first() == Some(&TEEServiceRegistered::SIGNATURE_HASH)) + { + Err(BuilderTransactionError::InvalidContract( + self.registry_address, + InvalidContractDataError::InvalidLogs(TEEServiceRegistered::SIGNATURE_HASH), )) } else { let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; @@ -456,15 +462,12 @@ impl FlashtestationsBuilderTx { Ok((None, true)) } else { Err(BuilderTransactionError::other(revert_reason.unwrap_or( - FlashtestationRevertReason::Unknown( - "unknown revert".into(), - Error::Other("unknown revert".into()), - ), + FlashtestationRevertReason::Unknown("unknown revert".to_string()), ))) } } - fn verify_block_proof_tx( + fn verify_block_proof_tx( &self, transactions: Vec, ctx: &OpPayloadBuilderCtx, @@ -489,10 +492,13 @@ impl FlashtestationsBuilderTx { .. } = self.simulate_verify_block_proof_tx(block_content_hash, ctx, evm)?; if success { - if !self.check_verify_block_proof_log(&logs) { - Err(BuilderTransactionError::other( - FlashtestationRevertReason::LogMismatch( - self.builder_policy_address, + if !logs + .iter() + .any(|log| log.topics().first() == Some(&BlockBuilderProofVerified::SIGNATURE_HASH)) + { + Err(BuilderTransactionError::InvalidContract( + self.builder_policy_address, + InvalidContractDataError::InvalidLogs( BlockBuilderProofVerified::SIGNATURE_HASH, ), )) @@ -518,15 +524,12 @@ impl FlashtestationsBuilderTx { } } else { Err(BuilderTransactionError::other(revert_reason.unwrap_or( - FlashtestationRevertReason::Unknown( - "unknown revert".into(), - Error::Other("unknown revert".into()), - ), + FlashtestationRevertReason::Unknown("unknown revert".to_string()), ))) } } - fn set_registered( + fn set_registered( &self, state_provider: impl StateProvider + Clone, ctx: &OpPayloadBuilderCtx, @@ -541,36 +544,332 @@ impl FlashtestationsBuilderTx { .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); evm.modify_cfg(|cfg| { cfg.disable_balance_check = true; + cfg.disable_nonce_check = true; }); - match self.register_tee_service_tx(ctx, &mut evm) { - Ok((_, registered)) => { - if registered { - let _ = self.registered.set(registered); + let calldata = IFlashtestationRegistry::getRegistrationStatusCall { + teeAddress: self.tee_service_signer.address, + } + .abi_encode(); + match self.simulate_flashtestation_call( + self.registry_address, + calldata, + None, + ctx, + &mut evm, + ) { + Ok(SimulationSuccessResult { output, .. }) => { + let result = + IFlashtestationRegistry::getRegistrationStatusCall::abi_decode_returns(&output) + .map_err(|_| { + BuilderTransactionError::InvalidContract( + self.registry_address, + InvalidContractDataError::OutputAbiDecodeError, + ) + })?; + if result.isValid { + let _ = self.registered.set(true); } Ok(()) } - Err(e) => Err(BuilderTransactionError::other(e)), + Err(e) => Err(e), + } + } + + fn get_permit_nonce( + &self, + contract_address: Address, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let calldata = IERC20Permit::noncesCall { + owner: self.tee_service_signer.address, + } + .abi_encode(); + let SimulationSuccessResult { output, .. } = + self.simulate_flashtestation_call(contract_address, calldata, None, ctx, evm)?; + U256::abi_decode(&output).map_err(|_| { + BuilderTransactionError::InvalidContract( + contract_address, + InvalidContractDataError::OutputAbiDecodeError, + ) + }) + } + + fn registration_permit_signature( + &self, + permit_nonce: U256, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let struct_hash_calldata = IFlashtestationRegistry::computeStructHashCall { + rawQuote: self.attestation.clone().into(), + extendedRegistrationData: self.extra_registration_data.clone(), + nonce: permit_nonce, + deadline: U256::from(ctx.timestamp()), + } + .abi_encode(); + let SimulationSuccessResult { output, .. } = self.simulate_flashtestation_call( + self.registry_address, + struct_hash_calldata, + None, + ctx, + evm, + )?; + let struct_hash = B256::abi_decode(&output).map_err(|_| { + BuilderTransactionError::InvalidContract( + self.registry_address, + InvalidContractDataError::OutputAbiDecodeError, + ) + })?; + let typed_data_hash_calldata = IFlashtestationRegistry::hashTypedDataV4Call { + structHash: struct_hash, + } + .abi_encode(); + let SimulationSuccessResult { output, .. } = self.simulate_flashtestation_call( + self.registry_address, + typed_data_hash_calldata, + None, + ctx, + evm, + )?; + let typed_data_hash = B256::abi_decode(&output).map_err(|_| { + BuilderTransactionError::InvalidContract( + self.registry_address, + InvalidContractDataError::OutputAbiDecodeError, + ) + })?; + let signature = self.tee_service_signer.sign_message(typed_data_hash)?; + Ok(signature) + } + + fn signed_registration_permit_tx( + &self, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let permit_nonce = self.get_permit_nonce(self.registry_address, ctx, evm)?; + let signature = self.registration_permit_signature(permit_nonce, ctx, evm)?; + let calldata = IFlashtestationRegistry::permitRegisterTEEServiceCall { + rawQuote: self.attestation.clone().into(), + extendedRegistrationData: self.extra_registration_data.clone(), + nonce: permit_nonce, + deadline: U256::from(ctx.timestamp()), + signature: signature.as_bytes().into(), + } + .abi_encode(); + let SimulationSuccessResult { gas_used, .. } = self.simulate_flashtestation_call( + self.registry_address, + calldata.clone(), + Some(TEEServiceRegistered::SIGNATURE_HASH), + ctx, + evm, + )?; + let signed_tx = self.sign_tx( + self.registry_address, + self.builder_signer, + Some(gas_used), + calldata.into(), + ctx, + evm.db_mut(), + )?; + let da_size = + op_alloy_flz::tx_estimated_size_fjord_bytes(signed_tx.encoded_2718().as_slice()); + Ok(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx, + is_top_of_block: false, + }) + } + + fn block_proof_permit_signature( + &self, + permit_nonce: U256, + block_content_hash: B256, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let struct_hash_calldata = IBlockBuilderPolicy::computeStructHashCall { + version: self.builder_proof_version, + blockContentHash: block_content_hash, + nonce: permit_nonce, + } + .abi_encode(); + let SimulationSuccessResult { output, .. } = self.simulate_flashtestation_call( + self.builder_policy_address, + struct_hash_calldata, + None, + ctx, + evm, + )?; + let struct_hash = B256::abi_decode(&output).map_err(|_| { + BuilderTransactionError::InvalidContract( + self.builder_policy_address, + InvalidContractDataError::OutputAbiDecodeError, + ) + })?; + let typed_data_hash_calldata = IBlockBuilderPolicy::getHashedTypeDataV4Call { + structHash: struct_hash, + } + .abi_encode(); + let SimulationSuccessResult { output, .. } = self.simulate_flashtestation_call( + self.builder_policy_address, + typed_data_hash_calldata, + None, + ctx, + evm, + )?; + let typed_data_hash = B256::abi_decode(&output).map_err(|_| { + BuilderTransactionError::InvalidContract( + self.builder_policy_address, + InvalidContractDataError::OutputAbiDecodeError, + ) + })?; + let signature = self.tee_service_signer.sign_message(typed_data_hash)?; + Ok(signature) + } + + fn signed_block_proof_permit_tx( + &self, + transactions: Vec, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let permit_nonce = self.get_permit_nonce(self.builder_policy_address, ctx, evm)?; + let block_content_hash = Self::compute_block_content_hash( + transactions.clone(), + ctx.parent_hash(), + ctx.block_number(), + ctx.timestamp(), + ); + let signature = + self.block_proof_permit_signature(permit_nonce, block_content_hash, ctx, evm)?; + let calldata = IBlockBuilderPolicy::permitVerifyBlockBuilderProofCall { + blockContentHash: block_content_hash, + nonce: U256::from(permit_nonce), + version: self.builder_proof_version, + eip712Sig: signature.as_bytes().into(), } + .abi_encode(); + let SimulationSuccessResult { gas_used, .. } = self.simulate_flashtestation_call( + self.builder_policy_address, + calldata.clone(), + Some(BlockBuilderProofVerified::SIGNATURE_HASH), + ctx, + evm, + )?; + let signed_tx = self.sign_tx( + self.builder_policy_address, + self.builder_signer, + Some(gas_used), + calldata.into(), + ctx, + evm.db_mut(), + )?; + let da_size = + op_alloy_flz::tx_estimated_size_fjord_bytes(signed_tx.encoded_2718().as_slice()); + Ok(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx, + is_top_of_block: false, + }) + } + + fn simulate_flashtestation_call( + &self, + contract_address: Address, + calldata: Vec, + expected_topic: Option, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let signed_tx = self.sign_tx( + contract_address, + self.builder_signer, + None, + calldata.into(), + ctx, + evm.db_mut(), + )?; + let revert_handler = if contract_address == self.registry_address { + Self::handle_registry_reverts + } else { + Self::handle_block_builder_policy_reverts + }; + self.simulate_call(signed_tx, expected_topic, revert_handler, evm) + } + + fn handle_registry_reverts(revert_output: Bytes) -> BuilderTransactionError { + let revert_reason = + IFlashtestationRegistry::IFlashtestationRegistryErrors::abi_decode(&revert_output) + .map(FlashtestationRevertReason::FlashtestationRegistry) + .unwrap_or_else(|_| { + FlashtestationRevertReason::Unknown(hex::encode(revert_output)) + }); + BuilderTransactionError::TransactionReverted(Box::new(revert_reason)) + } + + fn handle_block_builder_policy_reverts(revert_output: Bytes) -> BuilderTransactionError { + let revert_reason = + IBlockBuilderPolicy::IBlockBuilderPolicyErrors::abi_decode(&revert_output) + .map(FlashtestationRevertReason::BlockBuilderPolicy) + .unwrap_or_else(|_| { + FlashtestationRevertReason::Unknown(hex::encode(revert_output)) + }); + BuilderTransactionError::TransactionReverted(Box::new(revert_reason)) } } -impl BuilderTransactions for FlashtestationsBuilderTx { - fn simulate_builder_txs( +impl BuilderTransactions + for FlashtestationsBuilderTx +where + ExtraCtx: Debug + Default, + Extra: Debug + Default, +{ + fn simulate_builder_txs( &self, state_provider: impl StateProvider + Clone, info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, + _top_of_block: bool, ) -> Result, BuilderTransactionError> { // set registered simulating against the committed state if !self.registered.get().unwrap_or(&false) { - self.set_registered(state_provider.clone(), ctx)?; + self.set_registered(state_provider.clone(), ctx).inspect_err(|e| { + warn!(target: "flashtestations", error = ?e, "failed to check if builder tee address is registered"); + })?; } 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(); @@ -579,24 +878,47 @@ impl BuilderTransactions for Flashtestation .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); evm.modify_cfg(|cfg| { cfg.disable_balance_check = true; + cfg.disable_block_gas_limit = true; }); let mut builder_txs = Vec::::new(); if !self.registered.get().unwrap_or(&false) { - info!(target: "flashtestations", "tee service not registered yet, attempting to register"); - builder_txs.extend(self.fund_tee_service_tx(ctx, &mut evm)?); - let (register_tx, _) = self.register_tee_service_tx(ctx, &mut evm)?; - builder_txs.extend(register_tx); + if self.use_permit { + info!(target: "flashtestations", "tee service not registered yet, adding permit registration tx"); + let register_tx = self.signed_registration_permit_tx(ctx, &mut evm)?; + builder_txs.push(register_tx); + } else { + builder_txs.extend(self.fund_tee_service_tx(ctx, &mut evm)?); + let (register_tx, _) = self.register_tee_service_tx(ctx, &mut evm)?; + builder_txs.extend(register_tx); + } } + // don't return on error for block proof as previous txs in builder_txs will not be returned if self.enable_block_proofs { - // add verify block proof tx - builder_txs.extend(self.verify_block_proof_tx( - info.executed_transactions.clone(), - ctx, - &mut evm, - )?); + if self.use_permit { + debug!(target: "flashtestations", "adding permit verify block proof tx"); + match self.signed_block_proof_permit_tx( + info.executed_transactions.clone(), + ctx, + &mut evm, + ) { + Ok(block_proof_tx) => builder_txs.push(block_proof_tx), + Err(e) => { + warn!(target: "flashtestations", error = ?e, "failed to add permit block proof transaction") + } + } + } else { + // add verify block proof tx + match self.verify_block_proof_tx(info.executed_transactions.clone(), ctx, &mut evm) + { + Ok(block_proof_tx) => builder_txs.extend(block_proof_tx), + Err(e) => { + warn!(target: "flashtestations", error = ?e, "failed to add block proof transaction") + } + }; + } } Ok(builder_txs) } diff --git a/crates/op-rbuilder/src/flashtestations/mod.rs b/crates/op-rbuilder/src/flashtestations/mod.rs index a2a50611..618abcc5 100644 --- a/crates/op-rbuilder/src/flashtestations/mod.rs +++ b/crates/op-rbuilder/src/flashtestations/mod.rs @@ -1,6 +1,4 @@ -use alloy_primitives::{Address, B256}; -use alloy_sol_types::{Error, sol}; -use op_revm::OpHaltReason; +use alloy_sol_types::sol; // https://github.com/flashbots/flashtestations/commit/7cc7f68492fe672a823dd2dead649793aac1f216 sol!( @@ -9,6 +7,25 @@ sol!( interface IFlashtestationRegistry { function registerTEEService(bytes calldata rawQuote, bytes calldata extendedRegistrationData) external; + function permitRegisterTEEService( + bytes calldata rawQuote, + bytes calldata extendedRegistrationData, + uint256 nonce, + uint256 deadline, + bytes calldata signature + ) external payable; + + function computeStructHash( + bytes calldata rawQuote, + bytes calldata extendedRegistrationData, + uint256 nonce, + uint256 deadline + ) external pure returns (bytes32); + + function hashTypedDataV4(bytes32 structHash) external view returns (bytes32); + + function getRegistrationStatus(address teeAddress) external view returns (bool isValid, bytes32 quoteHash); + /// @notice Emitted when a TEE service is registered /// @param teeAddress The address of the TEE service /// @param rawQuote The raw quote from the TEE device @@ -46,6 +63,20 @@ sol!( interface IBlockBuilderPolicy { function verifyBlockBuilderProof(uint8 version, bytes32 blockContentHash) external; + function permitVerifyBlockBuilderProof( + uint8 version, + bytes32 blockContentHash, + uint256 nonce, + bytes calldata eip712Sig + ) external; + + function computeStructHash(uint8 version, bytes32 blockContentHash, uint256 nonce) + external + pure + returns (bytes32); + + function getHashedTypeDataV4(bytes32 structHash) external view returns (bytes32); + /// @notice Emitted when a block builder proof is successfully verified /// @param caller The address that called the verification function (TEE address) /// @param workloadId The workload identifier of the TEE @@ -72,6 +103,10 @@ sol!( error EmptySourceLocators(); } + interface IERC20Permit { + function nonces(address owner) external view returns (uint256); + } + struct BlockData { bytes32 parentHash; uint256 blockNumber; @@ -88,12 +123,8 @@ pub enum FlashtestationRevertReason { FlashtestationRegistry(IFlashtestationRegistry::IFlashtestationRegistryErrors), #[error("block builder policy error: {0:?}")] BlockBuilderPolicy(IBlockBuilderPolicy::IBlockBuilderPolicyErrors), - #[error("contract {0:?} may be invalid, mismatch in log emitted: expected {1:?}")] - LogMismatch(Address, B256), - #[error("unknown revert: {0} err: {1}")] - Unknown(String, Error), - #[error("halt: {0:?}")] - Halt(OpHaltReason), + #[error("unknown revert {0}")] + Unknown(String), } pub mod args; diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index 5f0871be..b10fb22e 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -2,24 +2,27 @@ use alloy_primitives::{Bytes, keccak256}; use reth_node_builder::BuilderContext; use tracing::{info, warn}; -use crate::{ - flashtestations::builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, - traits::NodeBounds, - tx_signer::{Signer, generate_ethereum_keypair, generate_key_from_seed}, -}; - use super::{ args::FlashtestationsArgs, attestation::{AttestationConfig, get_attestation_provider}, tx_manager::TxManager, }; +use crate::{ + flashtestations::builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, + traits::NodeBounds, + tx_signer::{Signer, generate_ethereum_keypair, generate_key_from_seed}, +}; +use std::fmt::Debug; -pub async fn bootstrap_flashtestations( +pub async fn bootstrap_flashtestations( args: FlashtestationsArgs, + builder_key: Signer, ctx: &BuilderContext, -) -> eyre::Result +) -> eyre::Result> where Node: NodeBounds, + ExtraCtx: Debug + Default, + Extra: Debug + Default, { let (private_key, public_key, address) = if args.debug { info!("Flashtestations debug mode enabled, generating debug key"); @@ -77,7 +80,11 @@ where info!(target: "flashtestations", "requesting TDX attestation"); let attestation = attestation_provider.get_attestation(report_data).await?; - let (tx_manager, registered) = if let Some(rpc_url) = args.rpc_url { + // TODO: support permit with an external rpc, skip this step if using permit signatures + // since the permit txs are signed by the builder key and will result in nonce issues + let (tx_manager, registered) = if let Some(rpc_url) = args.rpc_url + && !args.flashtestations_use_permit + { let tx_manager = TxManager::new( tee_service_signer, funding_key, @@ -114,6 +121,8 @@ where builder_proof_version: args.builder_proof_version, enable_block_proofs: args.enable_block_proofs, registered, + use_permit: args.flashtestations_use_permit, + builder_key, }); ctx.task_executor() diff --git a/crates/op-rbuilder/src/tests/flashtestations.rs b/crates/op-rbuilder/src/tests/flashtestations.rs index c4e0eaed..c3ecd749 100644 --- a/crates/op-rbuilder/src/tests/flashtestations.rs +++ b/crates/op-rbuilder/src/tests/flashtestations.rs @@ -11,7 +11,7 @@ use crate::{ tests::{ BLOCK_BUILDER_POLICY_ADDRESS, BundleOpts, ChainDriver, ChainDriverExt, FLASHBLOCKS_NUMBER_ADDRESS, FLASHTESTATION_REGISTRY_ADDRESS, LocalInstance, - MOCK_DCAP_ADDRESS, TEE_DEBUG_ADDRESS, TransactionBuilderExt, + MOCK_DCAP_ADDRESS, TEE_DEBUG_ADDRESS, TransactionBuilderExt, builder_signer, flashblocks_number_contract::FlashblocksNumber, flashtestation_registry::FlashtestationRegistry, flashtestations_signer, }, @@ -33,45 +33,7 @@ use crate::{ async fn test_flashtestations_registrations(rbuilder: LocalInstance) -> eyre::Result<()> { let driver = rbuilder.driver().await?; let provider = rbuilder.provider().await?; - setup_flashtestation_contracts(&driver, &provider, true).await?; - let block: alloy_rpc_types_eth::Block = - driver.build_new_block_with_current_timestamp(None).await?; - // check the builder tx, funding tx and registration tx is in the block - let num_txs = block.transactions.len(); - assert!(num_txs >= 3, "Expected at least 3 transactions in block"); - println!( - "block transactions {:#?}", - &block.transactions.clone().into_transactions_vec() - ); - let last_3_txs = &block.transactions.into_transactions_vec()[num_txs - 3..]; - // Check builder tx - assert_eq!( - last_3_txs[0].to(), - Some(Address::ZERO), - "builder tx should send to zero address" - ); - // Check funding tx - assert_eq!( - last_3_txs[1].to(), - Some(TEE_DEBUG_ADDRESS), - "funding tx should send to tee address" - ); - assert!( - last_3_txs[1] - .value() - .eq(&rbuilder.args().flashtestations.funding_amount), - "funding tx should have correct amount" - ); - // Check registration tx - assert_eq!( - last_3_txs[2].to(), - Some(FLASHTESTATION_REGISTRY_ADDRESS), - "registration tx should call registry" - ); - let contract = FlashtestationRegistry::new(FLASHTESTATION_REGISTRY_ADDRESS, provider.clone()); - let result = contract.getRegistration(TEE_DEBUG_ADDRESS).call().await?; - assert!(result._1.isValid, "The tee key is not registered"); - + setup_flashtestation_contracts(&driver, &provider, true, true).await?; // check builder does not try to register again let block = driver.build_new_block_with_current_timestamp(None).await?; let num_txs = block.transactions.len(); @@ -102,20 +64,12 @@ async fn test_flashtestations_registrations(rbuilder: LocalInstance) -> eyre::Re async fn test_flashtestations_block_proofs(rbuilder: LocalInstance) -> eyre::Result<()> { let driver = rbuilder.driver().await?; let provider = rbuilder.provider().await?; - setup_flashtestation_contracts(&driver, &provider, true).await?; - driver.build_new_block_with_current_timestamp(None).await?; - - // check registered - let contract = FlashtestationRegistry::new(FLASHTESTATION_REGISTRY_ADDRESS, provider.clone()); - let result = contract.getRegistration(TEE_DEBUG_ADDRESS).call().await?; - assert!(result._1.isValid, "The tee key is not registered"); - + setup_flashtestation_contracts(&driver, &provider, true, true).await?; // check that only the builder tx and block proof is in the block let (tx_hash, block) = driver.build_new_block_with_valid_transaction().await?; let txs = block.transactions.into_transactions_vec(); - if_flashblocks!( - assert_eq!(txs.len(), 5, "Expected at 4 transactions in block"); // deposit + valid tx + 2 builder tx + end of block proof + assert_eq!(txs.len(), 5, "Expected 5 transactions in block"); // deposit + valid tx + 2 builder tx + end of block proof // Check builder tx assert_eq!( txs[1].to(), @@ -124,7 +78,7 @@ async fn test_flashtestations_block_proofs(rbuilder: LocalInstance) -> eyre::Res ); ); if_standard!( - assert_eq!(txs.len(), 4, "Expected at 4 transactions in block"); // deposit + valid tx + builder tx + end of block proof + assert_eq!(txs.len(), 4, "Expected 4 transactions in block"); // deposit + valid tx + builder tx + end of block proof ); let last_3_txs = &txs[txs.len() - 3..]; // Check valid transaction @@ -148,6 +102,116 @@ async fn test_flashtestations_block_proofs(rbuilder: LocalInstance) -> eyre::Res Ok(()) } +#[rb_test(args = OpRbuilderArgs { + chain_block_time: 1000, + enable_revert_protection: true, + flashtestations: FlashtestationsArgs { + flashtestations_enabled: true, + registry_address: Some(FLASHTESTATION_REGISTRY_ADDRESS), + builder_policy_address: Some(BLOCK_BUILDER_POLICY_ADDRESS), + funding_key: Some(flashtestations_signer()), + debug: true, + enable_block_proofs: true, + ..Default::default() + }, + ..Default::default() +})] +async fn test_flashtestations_invalid_quote(rbuilder: LocalInstance) -> eyre::Result<()> { + let driver = rbuilder.driver().await?; + let provider = rbuilder.provider().await?; + setup_flashtestation_contracts(&driver, &provider, false, true).await?; + // verify not registered + let contract = FlashtestationRegistry::new(FLASHTESTATION_REGISTRY_ADDRESS, provider.clone()); + let result = contract + .getRegistrationStatus(TEE_DEBUG_ADDRESS) + .call() + .await?; + assert!( + !result.isValid, + "The tee key is registered for invalid quote" + ); + // check that only regular builder tx is in the block + let (tx_hash, block) = driver.build_new_block_with_valid_transaction().await?; + let txs = block.transactions.into_transactions_vec(); + + if_flashblocks!( + assert_eq!(txs.len(), 4, "Expected 4 transactions in block"); // deposit + valid tx + 2 builder tx + end of block proof + // Check builder tx + assert_eq!( + txs[1].to(), + Some(Address::ZERO), + "builder tx should send to zero address" + ); + ); + if_standard!( + assert_eq!(txs.len(), 3, "Expected 3 transactions in block"); // deposit + valid tx + builder tx + end of block proof + ); + let last_txs = &txs[txs.len() - 2..]; + // Check user transaction + assert_eq!( + last_txs[0].inner.tx_hash(), + tx_hash, + "tx hash for user transaction should match" + ); + // Check builder tx + assert_eq!( + last_txs[1].to(), + Some(Address::ZERO), + "builder tx should send to zero address" + ); + Ok(()) +} + +#[rb_test(args = OpRbuilderArgs { + chain_block_time: 1000, + enable_revert_protection: true, + flashtestations: FlashtestationsArgs { + flashtestations_enabled: true, + registry_address: Some(FLASHTESTATION_REGISTRY_ADDRESS), + builder_policy_address: Some(BLOCK_BUILDER_POLICY_ADDRESS), + funding_key: Some(flashtestations_signer()), + debug: true, + enable_block_proofs: true, + ..Default::default() + }, + ..Default::default() +})] +async fn test_flashtestations_unauthorized_workload(rbuilder: LocalInstance) -> eyre::Result<()> { + let driver = rbuilder.driver().await?; + let provider = rbuilder.provider().await?; + setup_flashtestation_contracts(&driver, &provider, true, false).await?; + // check that only the regular builder tx is in the block + let (tx_hash, block) = driver.build_new_block_with_valid_transaction().await?; + let txs = block.transactions.into_transactions_vec(); + + if_flashblocks!( + assert_eq!(txs.len(), 4, "Expected 4 transactions in block"); // deposit + valid tx + 2 builder tx + end of block proof + // Check builder tx + assert_eq!( + txs[1].to(), + Some(Address::ZERO), + "builder tx should send to zero address" + ); + ); + if_standard!( + assert_eq!(txs.len(), 3, "Expected 3 transactions in block"); // deposit + valid tx + builder tx + end of block proof + ); + let last_txs = &txs[txs.len() - 2..]; + // Check user transaction + assert_eq!( + last_txs[0].inner.tx_hash(), + tx_hash, + "tx hash for user transaction should match" + ); + // Check builder tx + assert_eq!( + last_txs[1].to(), + Some(Address::ZERO), + "builder tx should send to zero address" + ); + Ok(()) +} + #[rb_test(flashblocks, args = OpRbuilderArgs { chain_block_time: 1000, enable_revert_protection: true, @@ -170,7 +234,7 @@ async fn test_flashtestations_with_number_contract(rbuilder: LocalInstance) -> e let driver = rbuilder.driver().await?; let provider = rbuilder.provider().await?; setup_flashblock_number_contract(&driver, &provider, true).await?; - setup_flashtestation_contracts(&driver, &provider, true).await?; + setup_flashtestation_contracts(&driver, &provider, true, true).await?; let tx = driver .create_transaction() .random_valid_transfer() @@ -178,9 +242,9 @@ async fn test_flashtestations_with_number_contract(rbuilder: LocalInstance) -> e .send() .await?; let block = driver.build_new_block_with_current_timestamp(None).await?; - // 1 deposit tx, 1 fallback builder tx, 4 flashblocks number tx, valid tx, funding tx, registration tx, block proof + // 1 deposit tx, 1 fallback builder tx, 4 flashblocks number tx, valid tx, block proof let txs = block.transactions.into_transactions_vec(); - assert_eq!(txs.len(), 10, "Expected at 10 transactions in block"); + assert_eq!(txs.len(), 8, "Expected at 8 transactions in block"); // Check builder tx assert_eq!( txs[1].to(), @@ -192,7 +256,8 @@ async fn test_flashtestations_with_number_contract(rbuilder: LocalInstance) -> e assert_eq!( txs[i].to(), Some(FLASHBLOCKS_NUMBER_ADDRESS), - "builder tx should send to flashblocks number contract" + "builder tx should send to flashblocks number contract at index {}", + i ); } // check regular tx @@ -201,24 +266,138 @@ async fn test_flashtestations_with_number_contract(rbuilder: LocalInstance) -> e *tx.tx_hash(), "bundle tx was not in block" ); - // check funding, registration and block proof tx + // check block proof tx assert_eq!( txs[7].to(), - Some(TEE_DEBUG_ADDRESS), - "funding tx should send to tee address" + Some(BLOCK_BUILDER_POLICY_ADDRESS), + "block proof tx should call block policy address" ); + // Verify flashblock number incremented correctly + let contract = FlashblocksNumber::new(FLASHBLOCKS_NUMBER_ADDRESS, provider.clone()); + let current_number = contract.getFlashblockNumber().call().await?; + assert!( + current_number.gt(&U256::from(8)), // contract deployments incremented the number but we built at least 2 full blocks + "Flashblock number not incremented" + ); + Ok(()) +} + +#[rb_test(args = OpRbuilderArgs { + chain_block_time: 1000, + enable_revert_protection: true, + flashtestations: FlashtestationsArgs { + flashtestations_enabled: true, + registry_address: Some(FLASHTESTATION_REGISTRY_ADDRESS), + builder_policy_address: Some(BLOCK_BUILDER_POLICY_ADDRESS), + funding_key: Some(flashtestations_signer()), + debug: true, + flashtestations_use_permit: true, + ..Default::default() + }, + ..Default::default() +})] +async fn test_flashtestations_permit_registration(rbuilder: LocalInstance) -> eyre::Result<()> { + let driver = rbuilder.driver().await?; + let provider = rbuilder.provider().await?; + setup_flashtestation_contracts(&driver, &provider, true, true).await?; + // check builder does not try to register again + let block = driver.build_new_block_with_current_timestamp(None).await?; + let num_txs = block.transactions.len(); + if_flashblocks!( + assert!(num_txs == 3, "Expected at 3 transactions in block"); // deposit + 2 builder tx + ); + if_standard!( + assert!(num_txs == 2, "Expected at 2 transactions in block"); // deposit + builder tx + ); + // check that the tee signer did not send any transactions + let balance = provider.get_balance(TEE_DEBUG_ADDRESS).await?; + assert!(balance.is_zero()); + let nonce = provider.get_transaction_count(TEE_DEBUG_ADDRESS).await?; + assert_eq!(nonce, 0); + Ok(()) +} + +#[rb_test(args = OpRbuilderArgs { + chain_block_time: 1000, + enable_revert_protection: true, + flashtestations: FlashtestationsArgs { + flashtestations_enabled: true, + registry_address: Some(FLASHTESTATION_REGISTRY_ADDRESS), + builder_policy_address: Some(BLOCK_BUILDER_POLICY_ADDRESS), + funding_key: Some(flashtestations_signer()), + debug: true, + enable_block_proofs: true, + flashtestations_use_permit: true, + ..Default::default() + }, + ..Default::default() +})] +async fn test_flashtestations_permit_block_proof(rbuilder: LocalInstance) -> eyre::Result<()> { + let driver = rbuilder.driver().await?; + let provider = rbuilder.provider().await?; + setup_flashtestation_contracts(&driver, &provider, true, true).await?; + // check builder does not try to register again + let block = driver.build_new_block_with_current_timestamp(None).await?; + let num_txs = block.transactions.len(); + if_flashblocks!( + assert!(num_txs == 4, "Expected at 4 transactions in block"); // deposit + 2 builder tx + ); + if_standard!( + assert!(num_txs == 3, "Expected at 3 transactions in block"); // deposit + builder tx + ); + let last_2_txs = &block.transactions.into_transactions_vec()[num_txs - 2..]; + // Check builder tx assert_eq!( - txs[8].to(), - Some(FLASHTESTATION_REGISTRY_ADDRESS), - "registration tx should call registry" + last_2_txs[0].to(), + Some(Address::ZERO), + "builder tx should send to zero address" ); + // check builder proof assert_eq!( - txs[9].to(), + last_2_txs[1].to(), Some(BLOCK_BUILDER_POLICY_ADDRESS), - "block proof tx should call block policy address" + "builder tx should send to flashtestations builder policy address" ); + assert_eq!( + last_2_txs[1].from(), + builder_signer().address, + "block proof tx should come from builder address" + ); + // check that the tee signer did not send any transactions + let balance = provider.get_balance(TEE_DEBUG_ADDRESS).await?; + assert!(balance.is_zero()); + let nonce = provider.get_transaction_count(TEE_DEBUG_ADDRESS).await?; + assert_eq!(nonce, 0); + + Ok(()) +} - // add a user transaciton to ensure the flashblock number builder tx is top of block +#[rb_test(flashblocks, args = OpRbuilderArgs { + chain_block_time: 1000, + enable_revert_protection: true, + flashblocks: FlashblocksArgs { + flashblocks_number_contract_address: Some(FLASHBLOCKS_NUMBER_ADDRESS), + ..Default::default() + }, + flashtestations: FlashtestationsArgs { + flashtestations_enabled: true, + registry_address: Some(FLASHTESTATION_REGISTRY_ADDRESS), + builder_policy_address: Some(BLOCK_BUILDER_POLICY_ADDRESS), + funding_key: Some(flashtestations_signer()), + debug: true, + flashtestations_use_permit: true, + enable_block_proofs: true, + ..Default::default() + }, + ..Default::default() +})] +async fn test_flashtestations_permit_with_flashblocks_number_contract( + rbuilder: LocalInstance, +) -> eyre::Result<()> { + let driver = rbuilder.driver().await?; + let provider = rbuilder.provider().await?; + setup_flashtestation_contracts(&driver, &provider, true, true).await?; + setup_flashblock_number_contract(&driver, &provider, true).await?; let tx = driver .create_transaction() .random_valid_transfer() @@ -226,44 +405,47 @@ async fn test_flashtestations_with_number_contract(rbuilder: LocalInstance) -> e .send() .await?; let block = driver.build_new_block_with_current_timestamp(None).await?; - // check the flashblocks number tx and block proof is in the block + // check the builder tx, funding tx and registration tx is in the block + let num_txs = block.transactions.len(); let txs = block.transactions.into_transactions_vec(); - assert_eq!(txs.len(), 8, "Expected at 5 transactions in block"); + // // 1 deposit tx, 1 regular builder tx, 4 flashblocks number tx, 1 user tx, 1 block proof tx + assert_eq!(num_txs, 8, "Expected 8 transactions in block"); // Check builder tx assert_eq!( txs[1].to(), Some(Address::ZERO), - "fallback builder tx should send to zero address" + "builder tx should send to zero address" ); // flashblocks number contract for i in 2..6 { assert_eq!( txs[i].to(), Some(FLASHBLOCKS_NUMBER_ADDRESS), - "builder tx should send to flashblocks number contract" + "builder tx should send to flashblocks number contract at index {}", + i ); } // user tx assert_eq!( txs[6].tx_hash(), *tx.tx_hash(), - "bundle tx was not in block" + "user tx should be in correct position in block" ); - // block proof assert_eq!( txs[7].to(), Some(BLOCK_BUILDER_POLICY_ADDRESS), - "block proof tx should call block policy address" + "builder tx should send verify block builder proof" ); - - let contract = FlashtestationRegistry::new(FLASHTESTATION_REGISTRY_ADDRESS, provider.clone()); - let result = contract.getRegistration(TEE_DEBUG_ADDRESS).call().await?; - assert!(result._1.isValid, "The tee key is not registered"); + // check that the tee signer did not send any transactions + let balance = provider.get_balance(TEE_DEBUG_ADDRESS).await?; + assert!(balance.is_zero()); + let nonce = provider.get_transaction_count(TEE_DEBUG_ADDRESS).await?; + assert_eq!(nonce, 0); // Verify flashblock number incremented correctly let contract = FlashblocksNumber::new(FLASHBLOCKS_NUMBER_ADDRESS, provider.clone()); let current_number = contract.getFlashblockNumber().call().await?; assert!( - current_number.gt(&U256::from(8)), // contract deployments incremented the number but we built at least 2 full blocks + current_number.gt(&U256::from(4)), // contract deployments incremented the number but we built at least 1 full block "Flashblock number not incremented" ); Ok(()) @@ -272,6 +454,7 @@ async fn test_flashtestations_with_number_contract(rbuilder: LocalInstance) -> e async fn setup_flashtestation_contracts( driver: &ChainDriver, provider: &RootProvider, + add_quote: bool, authorize_workload: bool, ) -> eyre::Result<()> { // deploy the mock contract and register a mock quote @@ -282,15 +465,6 @@ async fn setup_flashtestation_contracts( .send() .await?; - // Add test quote - let mock_quote_tx = driver - .create_transaction() - .add_mock_quote() - .with_to(MOCK_DCAP_ADDRESS) - .with_bundle(BundleOpts::default()) - .send() - .await?; - // deploy the flashtestations registry contract let flashtestations_registry_tx = driver .create_transaction() @@ -328,6 +502,30 @@ async fn setup_flashtestation_contracts( // include the deployment and initialization in a block driver.build_new_block_with_current_timestamp(None).await?; + if add_quote { + // Add test quote + let mock_quote_tx = driver + .create_transaction() + .add_mock_quote() + .with_to(MOCK_DCAP_ADDRESS) + .with_bundle(BundleOpts::default()) + .send() + .await?; + driver.build_new_block_with_current_timestamp(None).await?; + provider + .get_transaction_receipt(*mock_quote_tx.tx_hash()) + .await? + .expect("add mock quote not mined"); + // verify registered + let contract = + FlashtestationRegistry::new(FLASHTESTATION_REGISTRY_ADDRESS, provider.clone()); + let result = contract + .getRegistrationStatus(TEE_DEBUG_ADDRESS) + .call() + .await?; + assert!(result.isValid, "The tee key is not registered"); + } + if authorize_workload { // add the workload id to the block builder policy let add_workload = driver @@ -357,11 +555,6 @@ async fn setup_flashtestation_contracts( mock_dcap_address, MOCK_DCAP_ADDRESS, "mock dcap contract address mismatch" ); - // verify mock quote added - provider - .get_transaction_receipt(*mock_quote_tx.tx_hash()) - .await? - .expect("add mock quote not mined"); // verify flashtestations registry contract deployment let receipt = provider .get_transaction_receipt(*flashtestations_registry_tx.tx_hash()) diff --git a/crates/op-rbuilder/src/tests/mod.rs b/crates/op-rbuilder/src/tests/mod.rs index cb5646f8..17be8d09 100644 --- a/crates/op-rbuilder/src/tests/mod.rs +++ b/crates/op-rbuilder/src/tests/mod.rs @@ -35,7 +35,7 @@ const MOCK_DCAP_ADDRESS: alloy_primitives::Address = alloy_primitives::address!("700b6a60ce7eaaea56f065753d8dcb9653dbad35"); #[cfg(test)] const FLASHTESTATION_REGISTRY_ADDRESS: alloy_primitives::Address = - alloy_primitives::address!("b19b36b1456e65e3a6d514d3f715f204bd59f431"); + alloy_primitives::address!("a15bb66138824a1c7167f5e85b957d04dd34e468"); #[cfg(test)] const BLOCK_BUILDER_POLICY_ADDRESS: alloy_primitives::Address = - alloy_primitives::address!("e1aa25618fa0c7a1cfdab5d6b456af611873b629"); + alloy_primitives::address!("8ce361602b935680e8dec218b820ff5056beb7af");