From 526d171563d52483200442431338cdab92282ee9 Mon Sep 17 00:00:00 2001 From: avalonche Date: Sat, 4 Oct 2025 07:25:23 +1000 Subject: [PATCH 1/2] Add permit flashtestations tx calls from builder --- crates/op-rbuilder/src/builders/builder_tx.rs | 37 +- .../src/builders/flashblocks/builder_tx.rs | 77 ++- .../src/builders/flashblocks/payload.rs | 19 +- .../src/builders/flashblocks/service.rs | 17 +- .../src/builders/standard/builder_tx.rs | 24 +- .../src/builders/standard/service.rs | 6 +- .../op-rbuilder/src/flashtestations/args.rs | 8 + .../src/flashtestations/builder_tx.rs | 455 +++++++++++++++--- crates/op-rbuilder/src/flashtestations/mod.rs | 38 ++ .../src/flashtestations/service.rs | 27 +- .../op-rbuilder/src/tests/flashtestations.rs | 373 ++++++++++---- crates/op-rbuilder/src/tests/mod.rs | 4 +- 12 files changed, 872 insertions(+), 213 deletions(-) diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 15ecc0d81..644bde589 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -55,6 +55,9 @@ pub enum BuilderTransactionError { /// Signature signing fails #[error("failed to sign transaction: {0}")] SigningError(secp256k1::Error), + /// Invalid contract data returned + #[error("invalid contract data returned {0}")] + InvalidContract(Address), /// Invalid tx errors during evm execution. #[error("invalid transaction error {0}")] InvalidTransactionError(Box), @@ -98,16 +101,17 @@ impl BuilderTransactionError { } } -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 +126,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 +149,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 +183,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>, @@ -204,16 +213,20 @@ pub trait BuilderTransactions: Debug { } #[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 +271,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, diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs index b1b169063..c642fcdb8 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -23,7 +23,7 @@ use crate::{ BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, builder_tx::{BuilderTxBase, get_nonce, log_exists}, context::OpPayloadBuilderCtx, - flashblocks::payload::FlashblocksExtraCtx, + flashblocks::payload::{FlashblocksExecutionInfo, FlashblocksExtraCtx}, }, flashtestations::builder_tx::FlashtestationsBuilderTx, primitives::reth::ExecutionInfo, @@ -64,14 +64,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 +84,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 +106,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 +141,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 { @@ -213,13 +231,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()); @@ -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 6a46e4c76..9217548c7 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 46ee8ae88..584252dfd 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/standard/builder_tx.rs b/crates/op-rbuilder/src/builders/standard/builder_tx.rs index 75a159ad7..db69906bc 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 c713b69cb..faf252b15 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 54d5d837f..cd0a5583c 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 ee5e793cf..89293f3bd 100644 --- a/crates/op-rbuilder/src/flashtestations/builder_tx.rs +++ b/crates/op-rbuilder/src/flashtestations/builder_tx.rs @@ -2,7 +2,9 @@ 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_primitives::{ + Address, B256, Bytes, Signature, TxKind, U256, keccak256, map::foldhash::HashMap, +}; use alloy_sol_types::{Error, SolCall, SolEvent, SolInterface, SolValue}; use core::fmt::Debug; use op_alloy_consensus::OpTypedTransaction; @@ -18,16 +20,17 @@ 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, + get_balance, get_nonce, log_exists, }, flashtestations::{ BlockData, FlashtestationRevertReason, IBlockBuilderPolicy::{self, BlockBuilderProofVerified}, + IERC20Permit, IFlashtestationRegistry::{self, TEEServiceRegistered}, }, primitives::reth::ExecutionInfo, @@ -45,10 +48,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 +78,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_key: Signer, + // Extra context and data + _marker: std::marker::PhantomData<(ExtraCtx, Extra)>, } #[derive(Debug, Default)] @@ -80,7 +95,18 @@ pub struct TxSimulateResult { pub logs: Vec, } -impl FlashtestationsBuilderTx { +#[derive(Debug, Default)] +pub struct SimulationSuccessResult { + pub gas_used: u64, + pub output: Bytes, + pub state_changes: HashMap, +} + +impl FlashtestationsBuilderTx +where + ExtraCtx: Debug + Default, + Extra: Debug + Default, +{ pub fn new(args: FlashtestationsBuilderTxArgs) -> Self { Self { attestation: args.attestation, @@ -93,6 +119,106 @@ 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_key: args.builder_key, + _marker: std::marker::PhantomData, + } + } + + fn sign_tx( + &self, + to: Address, + from: Signer, + gas_used: u64, + 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, + gas_limit: gas_used * 64 / 63, // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer + max_fee_per_gas: ctx.base_fee().into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(to), + input: calldata, + ..Default::default() + }); + Ok(from.sign_tx(tx)?) + } + + fn simulate_call( + &self, + contract_address: Address, + calldata: Bytes, + expected_topic: Option, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: ctx.chain_id(), + max_fee_per_gas: ctx.base_fee().into(), + gas_limit: ctx.block_gas_limit(), + to: TxKind::Call(contract_address), + input: calldata, + ..Default::default() + }); + let signed_tx = self.tee_service_signer.sign_tx(tx)?; + 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::other( + FlashtestationRevertReason::LogMismatch(contract_address, topic), + )); + } + Ok(SimulationSuccessResult { + gas_used, + output: output.into_data(), + state_changes: state, + }) + } + ExecutionResult::Revert { output, .. } => { + let revert_reason = + IFlashtestationRegistry::IFlashtestationRegistryErrors::abi_decode(&output) + .map(FlashtestationRevertReason::FlashtestationRegistry) + .or_else(|_| { + IBlockBuilderPolicy::IBlockBuilderPolicyErrors::abi_decode(&output) + .map(FlashtestationRevertReason::BlockBuilderPolicy) + }) + .unwrap_or_else(|e| { + FlashtestationRevertReason::Unknown(hex::encode(&output), e) + }); + Err(BuilderTransactionError::other(revert_reason)) + } + ExecutionResult::Halt { reason, .. } => Err(BuilderTransactionError::other( + FlashtestationRevertReason::Halt(reason), + )), } } @@ -148,7 +274,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 +332,7 @@ impl FlashtestationsBuilderTx { keccak256(&encoded) } - fn simulate_register_tee_service_tx( + fn simulate_register_tee_service_tx( &self, ctx: &OpPayloadBuilderCtx, evm: &mut OpEvm< @@ -269,20 +395,7 @@ impl FlashtestationsBuilderTx { } } - 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, @@ -345,16 +458,7 @@ impl FlashtestationsBuilderTx { } } - 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 +505,7 @@ impl FlashtestationsBuilderTx { } } - fn register_tee_service_tx( + fn register_tee_service_tx( &self, ctx: &OpPayloadBuilderCtx, evm: &mut OpEvm< @@ -418,7 +522,7 @@ 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) { + if !log_exists(&logs, &TEEServiceRegistered::SIGNATURE_HASH) { Err(BuilderTransactionError::other( FlashtestationRevertReason::LogMismatch( self.registry_address, @@ -464,7 +568,7 @@ impl FlashtestationsBuilderTx { } } - fn verify_block_proof_tx( + fn verify_block_proof_tx( &self, transactions: Vec, ctx: &OpPayloadBuilderCtx, @@ -489,7 +593,7 @@ impl FlashtestationsBuilderTx { .. } = self.simulate_verify_block_proof_tx(block_content_hash, ctx, evm)?; if success { - if !self.check_verify_block_proof_log(&logs) { + if !log_exists(&logs, &BlockBuilderProofVerified::SIGNATURE_HASH) { Err(BuilderTransactionError::other( FlashtestationRevertReason::LogMismatch( self.builder_policy_address, @@ -526,7 +630,7 @@ impl FlashtestationsBuilderTx { } } - fn set_registered( + fn set_registered( &self, state_provider: impl StateProvider + Clone, ctx: &OpPayloadBuilderCtx, @@ -541,36 +645,255 @@ 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_call(self.registry_address, calldata.into(), None, ctx, &mut evm) { + Ok(SimulationSuccessResult { output, .. }) => { + let result = + IFlashtestationRegistry::getRegistrationStatusCall::abi_decode_returns(&output) + .map_err(|_| { + BuilderTransactionError::InvalidContract(self.registry_address) + })?; + 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_call(contract_address, calldata.into(), None, ctx, evm)?; + U256::abi_decode(&output) + .map_err(|_| BuilderTransactionError::InvalidContract(contract_address)) + } + + 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_call( + self.registry_address, + struct_hash_calldata.into(), + None, + ctx, + evm, + )?; + let struct_hash = B256::abi_decode(&output) + .map_err(|_| BuilderTransactionError::InvalidContract(self.registry_address))?; + let typed_data_hash_calldata = IFlashtestationRegistry::hashTypedDataV4Call { + structHash: struct_hash, + } + .abi_encode(); + let SimulationSuccessResult { output, .. } = self.simulate_call( + self.registry_address, + typed_data_hash_calldata.into(), + None, + ctx, + evm, + )?; + let typed_data_hash = B256::abi_decode(&output) + .map_err(|_| BuilderTransactionError::InvalidContract(self.registry_address))?; + 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_call( + self.registry_address, + calldata.clone().into(), + Some(TEEServiceRegistered::SIGNATURE_HASH), + ctx, + evm, + )?; + let signed_tx = self.sign_tx( + self.registry_address, + self.builder_key, + 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_call( + self.builder_policy_address, + struct_hash_calldata.into(), + None, + ctx, + evm, + )?; + let struct_hash = B256::abi_decode(&output) + .map_err(|_| BuilderTransactionError::InvalidContract(self.builder_policy_address))?; + let typed_data_hash_calldata = IBlockBuilderPolicy::getHashedTypeDataV4Call { + structHash: struct_hash, + } + .abi_encode(); + let SimulationSuccessResult { output, .. } = self.simulate_call( + self.builder_policy_address, + typed_data_hash_calldata.into(), + None, + ctx, + evm, + )?; + let typed_data_hash = B256::abi_decode(&output) + .map_err(|_| BuilderTransactionError::InvalidContract(self.builder_policy_address))?; + 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_call( + self.builder_policy_address, + calldata.clone().into(), + Some(BlockBuilderProofVerified::SIGNATURE_HASH), + ctx, + evm, + )?; + let signed_tx = self.sign_tx( + self.builder_policy_address, + self.builder_key, + 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, + }) } } -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(); @@ -584,19 +907,41 @@ impl BuilderTransactions for Flashtestation 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 a2a506111..bf0e9309d 100644 --- a/crates/op-rbuilder/src/flashtestations/mod.rs +++ b/crates/op-rbuilder/src/flashtestations/mod.rs @@ -9,6 +9,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 +65,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 @@ -80,6 +113,11 @@ sol!( } type WorkloadId is bytes32; + + + interface IERC20Permit { + function nonces(address owner) external view returns (uint256); + } ); #[derive(Debug, thiserror::Error)] diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index 5f0871be1..b10fb22e3 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 c4e0eaed9..c3ecd7490 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 cb5646f84..17be8d09e 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"); From 75e520c99facd175f427e7090385c470ace454a6 Mon Sep 17 00:00:00 2001 From: avalonche Date: Sat, 4 Oct 2025 17:04:13 +1000 Subject: [PATCH 2/2] move simumlation calls to builder tx --- crates/op-rbuilder/src/builders/builder_tx.rs | 136 +++++++- .../src/builders/flashblocks/builder_tx.rs | 20 +- crates/op-rbuilder/src/builders/mod.rs | 4 +- .../src/flashtestations/builder_tx.rs | 301 ++++++++---------- crates/op-rbuilder/src/flashtestations/mod.rs | 21 +- 5 files changed, 277 insertions(+), 205 deletions(-) diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 644bde589..91d2c9152 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,9 +76,15 @@ pub enum BuilderTransactionError { /// Signature signing fails #[error("failed to sign transaction: {0}")] SigningError(secp256k1::Error), - /// Invalid contract data returned - #[error("invalid contract data returned {0}")] - InvalidContract(Address), + /// 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), @@ -93,12 +120,13 @@ 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 { @@ -210,6 +238,84 @@ pub trait BuilderTransactions, + 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)] @@ -322,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 c642fcdb8..7635cee8d 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,7 +21,7 @@ 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::{FlashblocksExecutionInfo, FlashblocksExtraCtx}, }, @@ -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:?}")] @@ -184,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, ), )) @@ -261,6 +260,7 @@ impl BuilderTransactions .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)?; diff --git a/crates/op-rbuilder/src/builders/mod.rs b/crates/op-rbuilder/src/builders/mod.rs index 9dbd949ce..c733d1116 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/flashtestations/builder_tx.rs b/crates/op-rbuilder/src/flashtestations/builder_tx.rs index 89293f3bd..d18b4f1f6 100644 --- a/crates/op-rbuilder/src/flashtestations/builder_tx.rs +++ b/crates/op-rbuilder/src/flashtestations/builder_tx.rs @@ -5,7 +5,7 @@ use alloy_op_evm::OpEvm; use alloy_primitives::{ Address, B256, Bytes, Signature, TxKind, U256, keccak256, map::foldhash::HashMap, }; -use alloy_sol_types::{Error, SolCall, SolEvent, SolInterface, SolValue}; +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}; @@ -24,8 +24,9 @@ use tracing::{debug, info, warn}; use crate::{ builders::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, OpPayloadBuilderCtx, - get_balance, get_nonce, log_exists, + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, + InvalidContractDataError, OpPayloadBuilderCtx, SimulationSuccessResult, get_balance, + get_nonce, }, flashtestations::{ BlockData, FlashtestationRevertReason, @@ -81,7 +82,7 @@ where // Whether to use permit for the flashtestation builder tx use_permit: bool, // Builder key for the flashtestation permit tx - builder_key: Signer, + builder_signer: Signer, // Extra context and data _marker: std::marker::PhantomData<(ExtraCtx, Extra)>, } @@ -95,13 +96,6 @@ pub struct TxSimulateResult { pub logs: Vec, } -#[derive(Debug, Default)] -pub struct SimulationSuccessResult { - pub gas_used: u64, - pub output: Bytes, - pub state_changes: HashMap, -} - impl FlashtestationsBuilderTx where ExtraCtx: Debug + Default, @@ -120,108 +114,11 @@ where registered: OnceLock::new(), enable_block_proofs: args.enable_block_proofs, use_permit: args.use_permit, - builder_key: args.builder_key, + builder_signer: args.builder_key, _marker: std::marker::PhantomData, } } - fn sign_tx( - &self, - to: Address, - from: Signer, - gas_used: u64, - 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, - gas_limit: gas_used * 64 / 63, // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer - max_fee_per_gas: ctx.base_fee().into(), - max_priority_fee_per_gas: 0, - to: TxKind::Call(to), - input: calldata, - ..Default::default() - }); - Ok(from.sign_tx(tx)?) - } - - fn simulate_call( - &self, - contract_address: Address, - calldata: Bytes, - expected_topic: Option, - ctx: &OpPayloadBuilderCtx, - evm: &mut OpEvm< - &mut State>, - NoOpInspector, - PrecompilesMap, - >, - ) -> Result { - let tx = OpTypedTransaction::Eip1559(TxEip1559 { - chain_id: ctx.chain_id(), - max_fee_per_gas: ctx.base_fee().into(), - gas_limit: ctx.block_gas_limit(), - to: TxKind::Call(contract_address), - input: calldata, - ..Default::default() - }); - let signed_tx = self.tee_service_signer.sign_tx(tx)?; - 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::other( - FlashtestationRevertReason::LogMismatch(contract_address, topic), - )); - } - Ok(SimulationSuccessResult { - gas_used, - output: output.into_data(), - state_changes: state, - }) - } - ExecutionResult::Revert { output, .. } => { - let revert_reason = - IFlashtestationRegistry::IFlashtestationRegistryErrors::abi_decode(&output) - .map(FlashtestationRevertReason::FlashtestationRegistry) - .or_else(|_| { - IBlockBuilderPolicy::IBlockBuilderPolicyErrors::abi_decode(&output) - .map(FlashtestationRevertReason::BlockBuilderPolicy) - }) - .unwrap_or_else(|e| { - FlashtestationRevertReason::Unknown(hex::encode(&output), e) - }); - Err(BuilderTransactionError::other(revert_reason)) - } - ExecutionResult::Halt { reason, .. } => Err(BuilderTransactionError::other( - FlashtestationRevertReason::Halt(reason), - )), - } - } - fn signed_funding_tx( &self, to: Address, @@ -374,8 +271,8 @@ where 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, @@ -389,7 +286,9 @@ where 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![], }), } @@ -437,8 +336,8 @@ where 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, @@ -452,7 +351,9 @@ where 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![], }), } @@ -522,12 +423,13 @@ where logs, } = self.simulate_register_tee_service_tx(ctx, evm)?; if success { - if !log_exists(&logs, &TEEServiceRegistered::SIGNATURE_HASH) { - 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)?; @@ -560,10 +462,7 @@ where 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()), ))) } } @@ -593,10 +492,13 @@ where .. } = self.simulate_verify_block_proof_tx(block_content_hash, ctx, evm)?; if success { - if !log_exists(&logs, &BlockBuilderProofVerified::SIGNATURE_HASH) { - 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, ), )) @@ -622,10 +524,7 @@ where } } else { Err(BuilderTransactionError::other(revert_reason.unwrap_or( - FlashtestationRevertReason::Unknown( - "unknown revert".into(), - Error::Other("unknown revert".into()), - ), + FlashtestationRevertReason::Unknown("unknown revert".to_string()), ))) } } @@ -651,12 +550,21 @@ where teeAddress: self.tee_service_signer.address, } .abi_encode(); - match self.simulate_call(self.registry_address, calldata.into(), None, ctx, &mut evm) { + 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) + BuilderTransactionError::InvalidContract( + self.registry_address, + InvalidContractDataError::OutputAbiDecodeError, + ) })?; if result.isValid { let _ = self.registered.set(true); @@ -682,9 +590,13 @@ where } .abi_encode(); let SimulationSuccessResult { output, .. } = - self.simulate_call(contract_address, calldata.into(), None, ctx, evm)?; - U256::abi_decode(&output) - .map_err(|_| BuilderTransactionError::InvalidContract(contract_address)) + 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( @@ -704,28 +616,36 @@ where deadline: U256::from(ctx.timestamp()), } .abi_encode(); - let SimulationSuccessResult { output, .. } = self.simulate_call( + let SimulationSuccessResult { output, .. } = self.simulate_flashtestation_call( self.registry_address, - struct_hash_calldata.into(), + struct_hash_calldata, None, ctx, evm, )?; - let struct_hash = B256::abi_decode(&output) - .map_err(|_| BuilderTransactionError::InvalidContract(self.registry_address))?; + 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_call( + let SimulationSuccessResult { output, .. } = self.simulate_flashtestation_call( self.registry_address, - typed_data_hash_calldata.into(), + typed_data_hash_calldata, None, ctx, evm, )?; - let typed_data_hash = B256::abi_decode(&output) - .map_err(|_| BuilderTransactionError::InvalidContract(self.registry_address))?; + 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) } @@ -749,17 +669,17 @@ where signature: signature.as_bytes().into(), } .abi_encode(); - let SimulationSuccessResult { gas_used, .. } = self.simulate_call( + let SimulationSuccessResult { gas_used, .. } = self.simulate_flashtestation_call( self.registry_address, - calldata.clone().into(), + calldata.clone(), Some(TEEServiceRegistered::SIGNATURE_HASH), ctx, evm, )?; let signed_tx = self.sign_tx( self.registry_address, - self.builder_key, - gas_used, + self.builder_signer, + Some(gas_used), calldata.into(), ctx, evm.db_mut(), @@ -791,28 +711,36 @@ where nonce: permit_nonce, } .abi_encode(); - let SimulationSuccessResult { output, .. } = self.simulate_call( + let SimulationSuccessResult { output, .. } = self.simulate_flashtestation_call( self.builder_policy_address, - struct_hash_calldata.into(), + struct_hash_calldata, None, ctx, evm, )?; - let struct_hash = B256::abi_decode(&output) - .map_err(|_| BuilderTransactionError::InvalidContract(self.builder_policy_address))?; + 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_call( + let SimulationSuccessResult { output, .. } = self.simulate_flashtestation_call( self.builder_policy_address, - typed_data_hash_calldata.into(), + typed_data_hash_calldata, None, ctx, evm, )?; - let typed_data_hash = B256::abi_decode(&output) - .map_err(|_| BuilderTransactionError::InvalidContract(self.builder_policy_address))?; + 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) } @@ -843,17 +771,17 @@ where eip712Sig: signature.as_bytes().into(), } .abi_encode(); - let SimulationSuccessResult { gas_used, .. } = self.simulate_call( + let SimulationSuccessResult { gas_used, .. } = self.simulate_flashtestation_call( self.builder_policy_address, - calldata.clone().into(), + calldata.clone(), Some(BlockBuilderProofVerified::SIGNATURE_HASH), ctx, evm, )?; let signed_tx = self.sign_tx( self.builder_policy_address, - self.builder_key, - gas_used, + self.builder_signer, + Some(gas_used), calldata.into(), ctx, evm.db_mut(), @@ -867,6 +795,54 @@ where 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 @@ -902,6 +878,7 @@ where .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(); diff --git a/crates/op-rbuilder/src/flashtestations/mod.rs b/crates/op-rbuilder/src/flashtestations/mod.rs index bf0e9309d..618abcc5f 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!( @@ -105,6 +103,10 @@ sol!( error EmptySourceLocators(); } + interface IERC20Permit { + function nonces(address owner) external view returns (uint256); + } + struct BlockData { bytes32 parentHash; uint256 blockNumber; @@ -113,11 +115,6 @@ sol!( } type WorkloadId is bytes32; - - - interface IERC20Permit { - function nonces(address owner) external view returns (uint256); - } ); #[derive(Debug, thiserror::Error)] @@ -126,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;