diff --git a/Cargo.lock b/Cargo.lock index 686a988d6..dce94b2b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6308,9 +6308,9 @@ dependencies = [ "futures-util", "hex", "http", - "jsonrpsee 0.26.0", - "jsonrpsee-core 0.26.0", - "jsonrpsee-types 0.26.0", + "jsonrpsee", + "jsonrpsee-core", + "jsonrpsee-types", "k256", "macros", "metrics", diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index b79200d4e..f3f3241f8 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -2,7 +2,7 @@ use alloy_consensus::TxEip1559; use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN}; use alloy_evm::Database; use alloy_primitives::{ - Address, TxKind, + Address, TxKind, U256, map::foldhash::{HashSet, HashSetExt}, }; use core::fmt::Debug; @@ -30,7 +30,10 @@ use crate::{ pub struct BuilderTransactionCtx { pub gas_used: u64, pub da_size: u64, - pub signed_tx: Option>, + pub signed_tx: Recovered, + // whether the transaction should be a top of block or + // bottom of block transaction + pub is_top_of_block: bool, } /// Possible error variants during construction of builder txs. @@ -80,7 +83,6 @@ pub trait BuilderTransactions: Debug { info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, - top_of_block: bool, ) -> Result, BuilderTransactionError>; fn add_builder_txs( @@ -98,30 +100,26 @@ 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(), - top_of_block, - )?; + let builder_txs = + self.simulate_builder_txs(state_provider, info, builder_ctx, evm.db_mut())?; for builder_tx in builder_txs.iter() { - let signed_tx = match builder_tx.signed_tx.clone() { - Some(tx) => tx, - None => continue, - }; - if invalid.contains(&signed_tx.signer()) { - warn!(target: "payload_builder", tx_hash = ?signed_tx.tx_hash(), "builder signer invalid as previous builder tx reverted"); + 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 + // position in the block + continue; + } + if invalid.contains(&builder_tx.signed_tx.signer()) { + warn!(target: "payload_builder", tx_hash = ?builder_tx.signed_tx.tx_hash(), "builder signer invalid as previous builder tx reverted"); continue; } let ResultAndState { result, state } = evm - .transact(&signed_tx) + .transact(&builder_tx.signed_tx) .map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?; if !result.is_success() { - warn!(target: "payload_builder", tx_hash = ?signed_tx.tx_hash(), "builder tx reverted"); - invalid.insert(signed_tx.signer()); + warn!(target: "payload_builder", tx_hash = ?builder_tx.signed_tx.tx_hash(), "builder tx reverted"); + invalid.insert(builder_tx.signed_tx.signer()); continue; } @@ -130,7 +128,7 @@ pub trait BuilderTransactions: Debug { info.cumulative_gas_used += gas_used; let ctx = ReceiptBuilderCtx { - tx: signed_tx.inner(), + tx: builder_tx.signed_tx.inner(), evm: &evm, result, state: &state, @@ -142,9 +140,9 @@ pub trait BuilderTransactions: Debug { evm.db_mut().commit(state); // Append sender and transaction to the respective lists - info.executed_senders.push(signed_tx.signer()); + info.executed_senders.push(builder_tx.signed_tx.signer()); info.executed_transactions - .push(signed_tx.clone().into_inner()); + .push(builder_tx.signed_tx.clone().into_inner()); } // Release the db reference by dropping evm @@ -172,12 +170,8 @@ pub trait BuilderTransactions: Debug { .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); for builder_tx in builder_txs { - let signed_tx = match builder_tx.signed_tx.clone() { - Some(tx) => tx, - None => continue, - }; let ResultAndState { state, .. } = evm - .transact(&signed_tx) + .transact(&builder_tx.signed_tx) .map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?; evm.db_mut().commit(state); @@ -214,7 +208,8 @@ impl BuilderTxBase { Ok(Some(BuilderTransactionCtx { gas_used, da_size, - signed_tx: Some(signed_tx), + signed_tx, + is_top_of_block: false, })) } None => Ok(None), @@ -276,7 +271,7 @@ impl BuilderTxBase { } } -pub(super) fn get_nonce( +pub(crate) fn get_nonce( db: &mut State, address: Address, ) -> Result { @@ -284,3 +279,12 @@ pub(super) fn get_nonce( .map(|acc| acc.account_info().unwrap_or_default().nonce) .map_err(|_| BuilderTransactionError::AccountLoadFailed(address)) } + +pub(crate) fn get_balance( + db: &mut State, + address: Address, +) -> Result { + db.load_cache_account(address) + .map(|acc| acc.account_info().unwrap_or_default().balance) + .map_err(|_| BuilderTransactionError::AccountLoadFailed(address)) +} diff --git a/crates/op-rbuilder/src/builders/context.rs b/crates/op-rbuilder/src/builders/context.rs index 9604ced71..63f1256d1 100644 --- a/crates/op-rbuilder/src/builders/context.rs +++ b/crates/op-rbuilder/src/builders/context.rs @@ -2,7 +2,7 @@ use alloy_consensus::{Eip658Value, Transaction, conditional::BlockConditionalAtt use alloy_eips::Typed2718; use alloy_evm::Database; use alloy_op_evm::block::receipt_builder::OpReceiptBuilder; -use alloy_primitives::{Bytes, U256}; +use alloy_primitives::{BlockHash, Bytes, U256}; use alloy_rpc_types_eth::Withdrawals; use core::fmt::Debug; use op_alloy_consensus::OpDepositReceipt; @@ -75,41 +75,51 @@ pub struct OpPayloadBuilderCtx { impl OpPayloadBuilderCtx { /// Returns the parent block the payload will be build on. - pub(super) fn parent(&self) -> &SealedHeader { + pub(crate) fn parent(&self) -> &SealedHeader { &self.config.parent_header } + /// Returns the parent hash + pub(crate) fn parent_hash(&self) -> BlockHash { + self.parent().hash() + } + + /// Returns the timestamp + pub(crate) fn timestamp(&self) -> u64 { + self.attributes().timestamp() + } + /// Returns the builder attributes. - pub(super) const fn attributes(&self) -> &OpPayloadBuilderAttributes { + pub(crate) const fn attributes(&self) -> &OpPayloadBuilderAttributes { &self.config.attributes } /// Returns the withdrawals if shanghai is active. - pub(super) fn withdrawals(&self) -> Option<&Withdrawals> { + pub(crate) fn withdrawals(&self) -> Option<&Withdrawals> { self.chain_spec .is_shanghai_active_at_timestamp(self.attributes().timestamp()) .then(|| &self.attributes().payload_attributes.withdrawals) } /// Returns the block gas limit to target. - pub(super) fn block_gas_limit(&self) -> u64 { + pub(crate) fn block_gas_limit(&self) -> u64 { self.attributes() .gas_limit .unwrap_or(self.evm_env.block_env.gas_limit) } /// Returns the block number for the block. - pub(super) fn block_number(&self) -> u64 { + pub(crate) fn block_number(&self) -> u64 { as_u64_saturated!(self.evm_env.block_env.number) } /// Returns the current base fee - pub(super) fn base_fee(&self) -> u64 { + pub(crate) fn base_fee(&self) -> u64 { self.evm_env.block_env.basefee } /// Returns the current blob gas price. - pub(super) fn get_blob_gasprice(&self) -> Option { + pub(crate) fn get_blob_gasprice(&self) -> Option { self.evm_env .block_env .blob_gasprice() @@ -119,7 +129,7 @@ impl OpPayloadBuilderCtx { /// Returns the blob fields for the header. /// /// This will always return `Some(0)` after ecotone. - pub(super) fn blob_fields(&self) -> (Option, Option) { + pub(crate) fn blob_fields(&self) -> (Option, Option) { // OP doesn't support blobs/EIP-4844. // https://specs.optimism.io/protocol/exec-engine.html#ecotone-disable-blob-transactions // Need [Some] or [None] based on hardfork to match block hash. @@ -133,7 +143,7 @@ impl OpPayloadBuilderCtx { /// Returns the extra data for the block. /// /// After holocene this extracts the extradata from the paylpad - pub(super) fn extra_data(&self) -> Result { + pub(crate) fn extra_data(&self) -> Result { if self.is_holocene_active() { self.attributes() .get_holocene_extra_data( @@ -148,47 +158,47 @@ impl OpPayloadBuilderCtx { } /// Returns the current fee settings for transactions from the mempool - pub(super) fn best_transaction_attributes(&self) -> BestTransactionsAttributes { + pub(crate) fn best_transaction_attributes(&self) -> BestTransactionsAttributes { BestTransactionsAttributes::new(self.base_fee(), self.get_blob_gasprice()) } /// Returns the unique id for this payload job. - pub(super) fn payload_id(&self) -> PayloadId { + pub(crate) fn payload_id(&self) -> PayloadId { self.attributes().payload_id() } /// Returns true if regolith is active for the payload. - pub(super) fn is_regolith_active(&self) -> bool { + pub(crate) fn is_regolith_active(&self) -> bool { self.chain_spec .is_regolith_active_at_timestamp(self.attributes().timestamp()) } /// Returns true if ecotone is active for the payload. - pub(super) fn is_ecotone_active(&self) -> bool { + pub(crate) fn is_ecotone_active(&self) -> bool { self.chain_spec .is_ecotone_active_at_timestamp(self.attributes().timestamp()) } /// Returns true if canyon is active for the payload. - pub(super) fn is_canyon_active(&self) -> bool { + pub(crate) fn is_canyon_active(&self) -> bool { self.chain_spec .is_canyon_active_at_timestamp(self.attributes().timestamp()) } /// Returns true if holocene is active for the payload. - pub(super) fn is_holocene_active(&self) -> bool { + pub(crate) fn is_holocene_active(&self) -> bool { self.chain_spec .is_holocene_active_at_timestamp(self.attributes().timestamp()) } /// Returns true if isthmus is active for the payload. - pub(super) fn is_isthmus_active(&self) -> bool { + pub(crate) fn is_isthmus_active(&self) -> bool { self.chain_spec .is_isthmus_active_at_timestamp(self.attributes().timestamp()) } /// Returns the chain id - pub(super) fn chain_id(&self) -> u64 { + pub(crate) fn chain_id(&self) -> u64 { self.chain_spec.chain_id() } } diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs index 3445bb2e5..2db279c8c 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -2,8 +2,8 @@ use alloy_consensus::TxEip1559; use alloy_eips::Encodable2718; use alloy_evm::{Database, Evm}; use alloy_op_evm::OpEvm; -use alloy_primitives::{Address, Bytes, TxKind, U256}; -use alloy_sol_types::{SolCall, SolError, sol}; +use alloy_primitives::{Address, TxKind}; +use alloy_sol_types::{Error, SolCall, SolInterface, sol}; use core::fmt::Debug; use op_alloy_consensus::OpTypedTransaction; use op_revm::OpHaltReason; @@ -25,7 +25,7 @@ use crate::{ context::OpPayloadBuilderCtx, flashblocks::payload::FlashblocksExtraCtx, }, - flashtestations::service::FlashtestationsBuilderTx, + flashtestations::builder_tx::FlashtestationsBuilderTx, primitives::reth::ExecutionInfo, tx_signer::Signer, }; @@ -33,62 +33,32 @@ use crate::{ sol!( // From https://github.com/Uniswap/flashblocks_number_contract/blob/main/src/FlashblockNumber.sol #[sol(rpc, abi)] + #[derive(Debug)] interface IFlashblockNumber { function incrementFlashblockNumber() external; - } - // @notice Emitted when flashblock index is incremented - // @param newFlashblockIndex The new flashblock index (0-indexed within each L2 block) - event FlashblockIncremented(uint256 newFlashblockIndex); + // @notice Emitted when flashblock index is incremented + // @param newFlashblockIndex The new flashblock index (0-indexed within each L2 block) + event FlashblockIncremented(uint256 newFlashblockIndex); - /// ----------------------------------------------------------------------- - /// Errors - /// ----------------------------------------------------------------------- - error NonBuilderAddress(address addr); - error MismatchedFlashblockNumber(uint256 expectedFlashblockNumber, uint256 actualFlashblockNumber); + /// ----------------------------------------------------------------------- + /// Errors + /// ----------------------------------------------------------------------- + error NonBuilderAddress(address addr); + error MismatchedFlashblockNumber(uint256 expectedFlashblockNumber, uint256 actualFlashblockNumber); + } ); #[derive(Debug, thiserror::Error)] pub(super) enum FlashblockNumberError { - #[error("non builder address: {0}")] - NonBuilderAddress(Address), - #[error("mismatched flashblock number: expected {0}, actual {1}")] - MismatchedFlashblockNumber(U256, U256), - #[error("unknown revert: {0}")] - Unknown(String), + #[error("flashblocks number contract error: {0:?}")] + Revert(IFlashblockNumber::IFlashblockNumberErrors), + #[error("unknown revert: {0} err: {1}")] + Unknown(String, Error), #[error("halt: {0:?}")] Halt(OpHaltReason), } -impl From for FlashblockNumberError { - fn from(value: Bytes) -> Self { - // Empty revert - if value.is_empty() { - return FlashblockNumberError::Unknown( - "Transaction reverted without reason".to_string(), - ); - } - - // Try to decode each custom error type - if let Ok(NonBuilderAddress { addr }) = NonBuilderAddress::abi_decode(&value) { - return FlashblockNumberError::NonBuilderAddress(addr); - } - - if let Ok(MismatchedFlashblockNumber { - expectedFlashblockNumber, - actualFlashblockNumber, - }) = MismatchedFlashblockNumber::abi_decode(&value) - { - return FlashblockNumberError::MismatchedFlashblockNumber( - expectedFlashblockNumber, - actualFlashblockNumber, - ); - } - - FlashblockNumberError::Unknown(hex::encode(value)) - } -} - // This will be the end of block transaction of a regular block #[derive(Debug, Clone)] pub(super) struct FlashblocksBuilderTx { @@ -116,7 +86,6 @@ impl BuilderTransactions for FlashblocksBuilderTx { info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, - top_of_block: bool, ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); @@ -126,24 +95,14 @@ impl BuilderTransactions for FlashblocksBuilderTx { } if ctx.is_last_flashblock() { - let flashblocks_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; - if let Some(tx) = flashblocks_builder_tx.clone() { - if top_of_block { - // don't commit the builder if top of block, we only return the gas used to reserve gas for the builder tx - builder_txs.push(BuilderTransactionCtx { - gas_used: tx.gas_used, - da_size: tx.da_size, - signed_tx: None, - }); - } else { - builder_txs.push(tx); - } - } + let base_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; + builder_txs.extend(base_tx.clone()); + if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { // We only include flashtestations txs in the last flashblock let mut simulation_state = self.simulate_builder_txs_state::( state_provider.clone(), - flashblocks_builder_tx.iter().collect(), + base_tx.iter().collect(), ctx, db, )?; @@ -152,7 +111,6 @@ impl BuilderTransactions for FlashblocksBuilderTx { info, ctx, &mut simulation_state, - top_of_block, )?; builder_txs.extend(flashtestations_builder_txs); } @@ -206,9 +164,13 @@ impl FlashblocksNumberBuilderTx { match result { ExecutionResult::Success { gas_used, .. } => Ok(gas_used), - ExecutionResult::Revert { output, .. } => Err(BuilderTransactionError::Other( - Box::new(FlashblockNumberError::from(output)), - )), + ExecutionResult::Revert { output, .. } => { + Err(BuilderTransactionError::Other(Box::new( + IFlashblockNumber::IFlashblockNumberErrors::abi_decode(&output) + .map(FlashblockNumberError::Revert) + .unwrap_or_else(|e| FlashblockNumberError::Unknown(hex::encode(output), e)), + ))) + } ExecutionResult::Halt { reason, .. } => Err(BuilderTransactionError::Other(Box::new( FlashblockNumberError::Halt(reason), ))), @@ -245,7 +207,6 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, - top_of_block: bool, ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); let state = StateProviderDatabase::new(state_provider.clone()); @@ -274,7 +235,7 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { { Ok(gas_used) => { // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer - let flashblocks_tx = self.signed_flashblock_number_tx( + let signed_tx = self.signed_flashblock_number_tx( ctx, gas_used * 64 / 63, nonce, @@ -282,33 +243,18 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { )?; let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( - flashblocks_tx.encoded_2718().as_slice(), + signed_tx.encoded_2718().as_slice(), ); Some(BuilderTransactionCtx { gas_used, da_size, - signed_tx: if top_of_block { - Some(flashblocks_tx) - } else { - None - }, // number tx at top of flashblock + signed_tx, + is_top_of_block: true, // number tx at top of flashblock }) } Err(e) => { warn!(target: "builder_tx", error = ?e, "Flashblocks number contract tx simulation failed, defaulting to fallback builder tx"); - let builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; - if let Some(tx) = &builder_tx - && top_of_block - { - // don't commit the builder if top of block, we only return the gas used to reserve gas for the builder tx - Some(BuilderTransactionCtx { - gas_used: tx.gas_used, - da_size: tx.da_size, - signed_tx: None, - }) - } else { - builder_tx - } + self.base_builder_tx.simulate_builder_tx(ctx, db)? } }; @@ -331,7 +277,6 @@ impl BuilderTransactions for FlashblocksNumberBuilderTx { info, ctx, &mut simulation_state, - top_of_block, )?; builder_txs.extend(flashtestations_builder_txs); } diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index ae11a6da6..44fa2bfb4 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -52,7 +52,7 @@ use std::{ }; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, metadata::Level, span, warn}; +use tracing::{Span, debug, error, info, metadata::Level, span, warn}; type NextBestFlashblocksTxs = BestFlashblocksTxs< ::Transaction, @@ -89,6 +89,8 @@ pub struct FlashblocksExtraCtx { da_per_batch: Option, /// Whether to calculate the state root for each flashblock calculate_state_root: bool, + /// Cancel token for the flashblock + flashblock_cancel: CancellationToken, } impl OpPayloadBuilderCtx { @@ -277,6 +279,7 @@ where .next_evm_env(&config.parent_header, &block_env_attributes) .map_err(PayloadBuilderError::other)?; + let mut fb_cancel = block_cancel.child_token(); let mut ctx = OpPayloadBuilderCtx:: { evm_config: self.evm_config.clone(), chain_spec: self.client.chain_spec(), @@ -296,6 +299,7 @@ where gas_per_batch: 0, da_per_batch: None, calculate_state_root, + flashblock_cancel: fb_cancel.clone(), }, max_gas_per_txn: self.config.max_gas_per_txn, address_gas_limiter: self.address_gas_limiter.clone(), @@ -327,7 +331,7 @@ where &mut info, &ctx, &mut state, - true, + false, ) { Ok(builder_txs) => builder_txs, Err(e) => { @@ -419,7 +423,7 @@ where if let Some(da_limit) = da_per_batch { // We error if we can't insert any tx aside from builder tx in flashblock if da_limit / 2 < builder_tx_da_size { - error!( + warn!( "Builder tx da size subtraction caused max_da_block_size to be 0. No transaction would be included." ); } @@ -442,11 +446,9 @@ where )); let interval = self.config.specific.interval; let (tx, mut rx) = mpsc::channel((self.config.flashblocks_per_block() + 1) as usize); - let mut fb_cancel = block_cancel.child_token(); - ctx.cancel = fb_cancel.clone(); tokio::spawn({ - let block_cancel = block_cancel.clone(); + let block_cancel = ctx.cancel.clone(); async move { let mut timer = tokio::time::interval_at( @@ -483,7 +485,7 @@ where // Process flashblocks in a blocking loop loop { let fb_span = if span.is_none() { - tracing::Span::none() + Span::none() } else { span!( parent: &span, @@ -500,9 +502,7 @@ where &mut state, &state_provider, &mut best_txs, - &block_cancel, &best_payload, - &fb_span, ) { Ok(()) => {} Err(err) => { @@ -519,7 +519,7 @@ where tokio::select! { Some(fb_cancel) = rx.recv() => { - ctx.cancel = fb_cancel; + ctx.extra_ctx.flashblock_cancel = fb_cancel; }, _ = block_cancel.cancelled() => { self.record_flashblocks_metrics( @@ -535,7 +535,6 @@ where } } - #[allow(clippy::too_many_arguments)] fn build_next_flashblock< DB: Database + std::fmt::Debug + AsRef

, P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, @@ -546,9 +545,7 @@ where state: &mut State, state_provider: impl reth::providers::StateProvider + Clone, best_txs: &mut NextBestFlashblocksTxs, - block_cancel: &CancellationToken, best_payload: &BlockCell, - span: &tracing::Span, ) -> Result<(), PayloadBuilderError> { // fallback block is index 0, so we need to increment here ctx.increment_flashblock_index(); @@ -636,14 +633,7 @@ where // We got block cancelled, we won't need anything from the block at this point // Caution: this assume that block cancel token only cancelled when new FCU is received - if block_cancel.is_cancelled() { - self.record_flashblocks_metrics( - ctx, - info, - ctx.target_flashblock_count(), - span, - "Payload building complete, channel closed or job cancelled", - ); + if ctx.cancel.is_cancelled() { return Ok(()); } @@ -696,14 +686,7 @@ where // If main token got canceled in here that means we received get_payload and we should drop everything and now update best_payload // To ensure that we will return same blocks as rollup-boost (to leverage caches) - if block_cancel.is_cancelled() { - self.record_flashblocks_metrics( - ctx, - info, - ctx.target_flashblock_count(), - span, - "Payload building complete, channel closed or job cancelled", - ); + if ctx.cancel.is_cancelled() { return Ok(()); } let flashblock_byte_size = self diff --git a/crates/op-rbuilder/src/builders/mod.rs b/crates/op-rbuilder/src/builders/mod.rs index 8dcde8eb5..5bb05b45e 100644 --- a/crates/op-rbuilder/src/builders/mod.rs +++ b/crates/op-rbuilder/src/builders/mod.rs @@ -21,7 +21,9 @@ mod flashblocks; mod generator; mod standard; -pub use builder_tx::{BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions}; +pub use builder_tx::{ + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, get_balance, get_nonce, +}; pub use context::OpPayloadBuilderCtx; pub use flashblocks::FlashblocksBuilder; pub use standard::StandardBuilder; diff --git a/crates/op-rbuilder/src/builders/standard/builder_tx.rs b/crates/op-rbuilder/src/builders/standard/builder_tx.rs index c4d154f63..75a159ad7 100644 --- a/crates/op-rbuilder/src/builders/standard/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/standard/builder_tx.rs @@ -8,7 +8,7 @@ use crate::{ BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, builder_tx::BuilderTxBase, context::OpPayloadBuilderCtx, }, - flashtestations::service::FlashtestationsBuilderTx, + flashtestations::builder_tx::FlashtestationsBuilderTx, primitives::reth::ExecutionInfo, tx_signer::Signer, }; @@ -40,7 +40,6 @@ impl BuilderTransactions for StandardBuilderTx { info: &mut ExecutionInfo, ctx: &OpPayloadBuilderCtx, db: &mut State, - top_of_block: bool, ) -> Result, BuilderTransactionError> { let mut builder_txs = Vec::::new(); let standard_builder_tx = self.base_builder_tx.simulate_builder_tx(ctx, db)?; @@ -57,7 +56,6 @@ impl BuilderTransactions for StandardBuilderTx { info, ctx, &mut simulation_state, - top_of_block, )?; builder_txs.extend(flashtestations_builder_txs); } diff --git a/crates/op-rbuilder/src/builders/standard/payload.rs b/crates/op-rbuilder/src/builders/standard/payload.rs index c510a8ba9..0c0e5de07 100644 --- a/crates/op-rbuilder/src/builders/standard/payload.rs +++ b/crates/op-rbuilder/src/builders/standard/payload.rs @@ -347,8 +347,7 @@ impl OpBuilder<'_, Txs> { // 4. if mem pool transactions are requested we execute them // gas reserved for builder tx - let builder_txs = - builder_tx.simulate_builder_txs(&state_provider, &mut info, ctx, db, true)?; + let builder_txs = builder_tx.add_builder_txs(&state_provider, &mut info, ctx, db, true)?; let builder_tx_gas = builder_txs.iter().fold(0, |acc, tx| acc + tx.gas_used); let block_gas_limit = ctx.block_gas_limit().saturating_sub(builder_tx_gas); if block_gas_limit == 0 { diff --git a/crates/op-rbuilder/src/flashtestations/attestation.rs b/crates/op-rbuilder/src/flashtestations/attestation.rs index 534dfcb15..4ad73cca1 100644 --- a/crates/op-rbuilder/src/flashtestations/attestation.rs +++ b/crates/op-rbuilder/src/flashtestations/attestation.rs @@ -47,6 +47,7 @@ impl AttestationProvider for RemoteAttestationProvider { } } +#[allow(clippy::if_same_then_else)] pub fn get_attestation_provider( config: AttestationConfig, ) -> Box { diff --git a/crates/op-rbuilder/src/flashtestations/builder_tx.rs b/crates/op-rbuilder/src/flashtestations/builder_tx.rs new file mode 100644 index 000000000..eeb48dda2 --- /dev/null +++ b/crates/op-rbuilder/src/flashtestations/builder_tx.rs @@ -0,0 +1,581 @@ +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::{SolCall, SolEvent, SolInterface, SolValue}; +use core::fmt::Debug; +use op_alloy_consensus::OpTypedTransaction; +use reth_evm::{ConfigureEvm, Evm, EvmError, precompiles::PrecompilesMap}; +use reth_optimism_primitives::OpTransactionSigned; +use reth_primitives::{Log, Recovered}; +use reth_provider::StateProvider; +use reth_revm::{State, database::StateProviderDatabase}; +use revm::{ + DatabaseCommit, + context::result::{ExecutionResult, ResultAndState}, + inspector::NoOpInspector, + state::Account, +}; +use std::sync::OnceLock; +use tracing::{debug, info, warn}; + +use crate::{ + builders::{ + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, OpPayloadBuilderCtx, + get_balance, get_nonce, + }, + flashtestations::{ + BlockData, FlashtestationRevertReason, + IBlockBuilderPolicy::{self, BlockBuilderProofVerified}, + IFlashtestationRegistry::{self, TEEServiceRegistered}, + }, + primitives::reth::ExecutionInfo, + tx_signer::Signer, +}; + +pub struct FlashtestationsBuilderTxArgs { + pub attestation: Vec, + pub extra_registration_data: Bytes, + pub tee_service_signer: Signer, + pub funding_key: Signer, + pub funding_amount: U256, + pub registry_address: Address, + pub builder_policy_address: Address, + pub builder_proof_version: u8, + pub enable_block_proofs: bool, + pub registered: bool, +} + +#[derive(Debug, Clone)] +pub struct FlashtestationsBuilderTx { + // Attestation for the builder + attestation: Vec, + // Extra registration data for the builder + extra_registration_data: Bytes, + // TEE service generated key + tee_service_signer: Signer, + // Funding key for the TEE signer + funding_key: Signer, + // Funding amount for the TEE signer + funding_amount: U256, + // Registry address for the attestation + registry_address: Address, + // Builder policy address for the block builder proof + builder_policy_address: Address, + // Builder proof version + builder_proof_version: u8, + // Whether the workload and address has been registered + registered: OnceLock, + // Whether block proofs are enabled + enable_block_proofs: bool, +} + +#[derive(Debug, Default)] +pub struct TxSimulateResult { + pub gas_used: u64, + pub success: bool, + pub state_changes: HashMap, + pub revert_reason: Option, + pub logs: Vec, +} + +impl FlashtestationsBuilderTx { + pub fn new(args: FlashtestationsBuilderTxArgs) -> Self { + Self { + attestation: args.attestation, + extra_registration_data: args.extra_registration_data, + tee_service_signer: args.tee_service_signer, + funding_key: args.funding_key, + funding_amount: args.funding_amount, + registry_address: args.registry_address, + builder_policy_address: args.builder_policy_address, + builder_proof_version: args.builder_proof_version, + registered: OnceLock::new(), + enable_block_proofs: args.enable_block_proofs, + } + } + + fn signed_funding_tx( + &self, + to: Address, + from: Signer, + amount: U256, + base_fee: u64, + chain_id: u64, + nonce: u64, + ) -> Result, secp256k1::Error> { + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id, + nonce, + gas_limit: 21000, + max_fee_per_gas: base_fee.into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(to), + value: amount, + ..Default::default() + }); + from.sign_tx(tx) + } + + fn signed_register_tee_service_tx( + &self, + attestation: Vec, + gas_limit: u64, + base_fee: u64, + chain_id: u64, + nonce: u64, + ) -> Result, secp256k1::Error> { + let quote_bytes = Bytes::from(attestation); + let calldata = IFlashtestationRegistry::registerTEEServiceCall { + rawQuote: quote_bytes, + extendedRegistrationData: self.extra_registration_data.clone(), + } + .abi_encode(); + + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas: base_fee.into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(self.registry_address), + input: calldata.into(), + ..Default::default() + }); + self.tee_service_signer.sign_tx(tx) + } + + fn signed_block_builder_proof_tx( + &self, + block_content_hash: B256, + ctx: &OpPayloadBuilderCtx, + gas_limit: u64, + nonce: u64, + ) -> Result, secp256k1::Error> { + let calldata = IBlockBuilderPolicy::verifyBlockBuilderProofCall { + version: self.builder_proof_version, + blockContentHash: block_content_hash, + } + .abi_encode(); + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: ctx.chain_id(), + nonce, + gas_limit, + max_fee_per_gas: ctx.base_fee().into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(self.builder_policy_address), + input: calldata.into(), + ..Default::default() + }); + self.tee_service_signer.sign_tx(tx) + } + + /// Computes the block content hash according to the formula: + /// keccak256(abi.encode(parentHash, blockNumber, timestamp, transactionHashes)) + /// https://github.com/flashbots/rollup-boost/blob/main/specs/flashtestations.md#block-building-process + fn compute_block_content_hash( + transactions: Vec, + parent_hash: B256, + block_number: u64, + timestamp: u64, + ) -> B256 { + // Create ordered list of transaction hashes + let transaction_hashes: Vec = transactions + .iter() + .map(|tx| { + // RLP encode the transaction and hash it + let mut encoded = Vec::new(); + tx.encode_2718(&mut encoded); + keccak256(&encoded) + }) + .collect(); + + // Create struct and ABI encode + let block_data = BlockData { + parentHash: parent_hash, + blockNumber: U256::from(block_number), + timestamp: U256::from(timestamp), + transactionHashes: transaction_hashes, + }; + + let encoded = block_data.abi_encode(); + keccak256(&encoded) + } + + fn simulate_register_tee_service_tx( + &self, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; + + let register_tx = self.signed_register_tee_service_tx( + self.attestation.clone(), + ctx.block_gas_limit(), + ctx.base_fee(), + ctx.chain_id(), + nonce, + )?; + let ResultAndState { result, state } = match evm.transact(®ister_tx) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + warn!(target: "flashtestations", %err, "register tee service tx failed"); + return Ok(TxSimulateResult::default()); + } else { + return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); + } + } + }; + match result { + ExecutionResult::Success { gas_used, logs, .. } => Ok(TxSimulateResult { + gas_used, + success: true, + state_changes: state, + revert_reason: None, + logs, + }), + ExecutionResult::Revert { output, gas_used } => { + let revert_reason = + IFlashtestationRegistry::IFlashtestationRegistryErrors::abi_decode(&output) + .map(FlashtestationRevertReason::FlashtestationRegistry) + .unwrap_or_else(|e| { + FlashtestationRevertReason::Unknown(hex::encode(output), e) + }); + Ok(TxSimulateResult { + gas_used, + success: false, + state_changes: state, + revert_reason: Some(revert_reason), + logs: vec![], + }) + } + ExecutionResult::Halt { reason, .. } => Ok(TxSimulateResult { + gas_used: 0, + success: false, + state_changes: state, + revert_reason: Some(FlashtestationRevertReason::Halt(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( + &self, + block_content_hash: B256, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; + + let verify_block_proof_tx = self.signed_block_builder_proof_tx( + block_content_hash, + ctx, + ctx.block_gas_limit(), + nonce, + )?; + let ResultAndState { result, state } = match evm.transact(&verify_block_proof_tx) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + warn!(target: "flashtestations", %err, "verify block proof tx failed"); + return Ok(TxSimulateResult::default()); + } else { + return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); + } + } + }; + match result { + ExecutionResult::Success { gas_used, logs, .. } => Ok(TxSimulateResult { + gas_used, + success: true, + state_changes: state, + revert_reason: None, + logs, + }), + ExecutionResult::Revert { output, gas_used } => { + let revert_reason = + IBlockBuilderPolicy::IBlockBuilderPolicyErrors::abi_decode(&output) + .map(FlashtestationRevertReason::BlockBuilderPolicy) + .unwrap_or_else(|e| { + FlashtestationRevertReason::Unknown(hex::encode(output), e) + }); + Ok(TxSimulateResult { + gas_used, + success: false, + state_changes: state, + revert_reason: Some(revert_reason), + logs: vec![], + }) + } + ExecutionResult::Halt { reason, .. } => Ok(TxSimulateResult { + gas_used: 0, + success: false, + state_changes: state, + revert_reason: Some(FlashtestationRevertReason::Halt(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( + &self, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result, BuilderTransactionError> { + let balance = get_balance(evm.db_mut(), self.tee_service_signer.address)?; + if balance.is_zero() { + let funding_nonce = get_nonce(evm.db_mut(), self.funding_key.address)?; + let funding_tx = self.signed_funding_tx( + self.tee_service_signer.address, + self.funding_key, + self.funding_amount, + ctx.base_fee(), + ctx.chain_id(), + funding_nonce, + )?; + let da_size = + op_alloy_flz::tx_estimated_size_fjord_bytes(funding_tx.encoded_2718().as_slice()); + let ResultAndState { state, .. } = match evm.transact(&funding_tx) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + warn!(target: "flashtestations", %err, "funding tx failed"); + return Ok(None); + } else { + return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); + } + } + }; + info!(target: "flashtestations", block_number = ctx.block_number(), tx_hash = ?funding_tx.tx_hash(), "adding funding tx to builder txs"); + evm.db_mut().commit(state); + Ok(Some(BuilderTransactionCtx { + gas_used: 21000, + da_size, + signed_tx: funding_tx, + is_top_of_block: true, + })) + } else { + Ok(None) + } + } + + fn register_tee_service_tx( + &self, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result<(Option, bool), BuilderTransactionError> { + let TxSimulateResult { + gas_used, + success, + state_changes, + revert_reason, + logs, + } = self.simulate_register_tee_service_tx(ctx, evm)?; + if success { + if !self.check_tee_address_registered_log(&logs, self.tee_service_signer.address) { + warn!(target: "flashtestations", "transaction did not emit TEEServiceRegistered log, FlashtestationRegistry contract address may be incorrect"); + Ok((None, false)) + } else { + let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; + let register_tx = self.signed_register_tee_service_tx( + self.attestation.clone(), + gas_used * 64 / 63, // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer + ctx.base_fee(), + ctx.chain_id(), + nonce, + )?; + let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( + register_tx.encoded_2718().as_slice(), + ); + info!(target: "flashtestations", block_number = ctx.block_number(), tx_hash = ?register_tx.tx_hash(), "adding register tee tx to builder txs"); + evm.db_mut().commit(state_changes); + Ok(( + Some(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx: register_tx, + is_top_of_block: true, + }), + false, + )) + } + } else if let Some(FlashtestationRevertReason::FlashtestationRegistry( + IFlashtestationRegistry::IFlashtestationRegistryErrors::TEEServiceAlreadyRegistered(_), + )) = revert_reason + { + Ok((None, true)) + } else { + warn!(target: "flashtestations", reason = ?revert_reason, "register tee service tx failed"); + Ok((None, false)) + } + } + + fn verify_block_proof_tx( + &self, + transactions: Vec, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result, BuilderTransactionError> { + let block_content_hash = Self::compute_block_content_hash( + transactions.clone(), + ctx.parent_hash(), + ctx.block_number(), + ctx.timestamp(), + ); + + let TxSimulateResult { + gas_used, + success, + revert_reason, + logs, + .. + } = self.simulate_verify_block_proof_tx(block_content_hash, ctx, evm)?; + if success { + if !self.check_verify_block_proof_log(&logs) { + warn!(target: "flashtestations", "transaction did not emit BlockBuilderProofVerified log, BlockBuilderPolicy contract address may be incorrect"); + Ok(None) + } else { + let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; + // Due to EIP-150, only 63/64 of available gas is forwarded to external calls so need to add a buffer + let verify_block_proof_tx = self.signed_block_builder_proof_tx( + block_content_hash, + ctx, + gas_used * 64 / 63, + nonce, + )?; + let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( + verify_block_proof_tx.encoded_2718().as_slice(), + ); + debug!(target: "flashtestations", block_number = ctx.block_number(), tx_hash = ?verify_block_proof_tx.tx_hash(), "adding verify block proof tx to builder txs"); + Ok(Some(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx: verify_block_proof_tx, + is_top_of_block: false, + })) + } + } else { + warn!(target: "flashtestations", reason = ?revert_reason, "verify block proof tx failed, falling back to standard builder tx"); + Ok(None) + } + } + + fn set_registered( + &self, + state_provider: impl StateProvider + Clone, + ctx: &OpPayloadBuilderCtx, + ) { + let state = StateProviderDatabase::new(state_provider.clone()); + let mut simulation_state = State::builder() + .with_database(state) + .with_bundle_update() + .build(); + let mut evm = ctx + .evm_config + .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); + evm.modify_cfg(|cfg| { + cfg.disable_balance_check = true; + }); + match self.register_tee_service_tx(ctx, &mut evm) { + Ok((_, registered)) => { + if let Err(e) = self.registered.set(registered) { + warn!(target: "flashtestations", error = ?e, "error setting builder registered"); + } + } + Err(e) => { + warn!(target: "flashtestations", error = ?e, "simulation error when checking if registered"); + } + } + } +} + +impl BuilderTransactions for FlashtestationsBuilderTx { + fn simulate_builder_txs( + &self, + state_provider: impl StateProvider + Clone, + info: &mut ExecutionInfo, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + let state = StateProviderDatabase::new(state_provider.clone()); + let mut simulation_state = State::builder() + .with_database(state) + .with_bundle_prestate(db.bundle_state.clone()) + .with_bundle_update() + .build(); + + let mut evm = ctx + .evm_config + .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); + evm.modify_cfg(|cfg| { + cfg.disable_balance_check = 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); + self.set_registered(state_provider, ctx); + } + + 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, + )?); + } + Ok(builder_txs) + } +} diff --git a/crates/op-rbuilder/src/flashtestations/mod.rs b/crates/op-rbuilder/src/flashtestations/mod.rs index a5b16e734..a5070be09 100644 --- a/crates/op-rbuilder/src/flashtestations/mod.rs +++ b/crates/op-rbuilder/src/flashtestations/mod.rs @@ -1,4 +1,99 @@ +use alloy_sol_types::{Error, sol}; +use op_revm::OpHaltReason; + +sol!( + #[sol(rpc, abi)] + #[derive(Debug)] + interface IFlashtestationRegistry { + function registerTEEService(bytes calldata rawQuote, bytes calldata extendedRegistrationData) external; + + /// @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 + /// @param alreadyExists Whether the TEE service is already registered + event TEEServiceRegistered(address indexed teeAddress, bytes rawQuote, bool alreadyExists); + + /// @notice Emitted when the attestation contract is the 0x0 address + error InvalidAttestationContract(); + /// @notice Emitted when the signature is expired because the deadline has passed + error ExpiredSignature(uint256 deadline); + /// @notice Emitted when the quote is invalid according to the Automata DCAP Attestation contract + error InvalidQuote(bytes output); + /// @notice Emitted when the report data length is too short + error InvalidReportDataLength(uint256 length); + /// @notice Emitted when the registration data hash does not match the expected hash + error InvalidRegistrationDataHash(bytes32 expected, bytes32 received); + /// @notice Emitted when the byte size is exceeded + error ByteSizeExceeded(uint256 size); + /// @notice Emitted when the TEE service is already registered when registering + error TEEServiceAlreadyRegistered(address teeAddress); + /// @notice Emitted when the signer doesn't match the TEE address + error SignerMustMatchTEEAddress(address signer, address teeAddress); + /// @notice Emitted when the TEE service is not registered + error TEEServiceNotRegistered(address teeAddress); + /// @notice Emitted when the TEE service is already invalid when trying to invalidate a TEE registration + error TEEServiceAlreadyInvalid(address teeAddress); + /// @notice Emitted when the TEE service is still valid when trying to invalidate a TEE registration + error TEEIsStillValid(address teeAddress); + /// @notice Emitted when the nonce is invalid when verifying a signature + error InvalidNonce(uint256 expected, uint256 provided); + } + + #[sol(rpc, abi)] + #[derive(Debug)] + interface IBlockBuilderPolicy { + function verifyBlockBuilderProof(uint8 version, bytes32 blockContentHash) external; + + /// @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 + /// @param version The flashtestation protocol version used + /// @param blockContentHash The hash of the block content + /// @param commitHash The git commit hash associated with the workload + event BlockBuilderProofVerified( + address caller, bytes32 workloadId, uint8 version, bytes32 blockContentHash, string commitHash + ); + + /// @notice Emitted when the registry is the 0x0 address + error InvalidRegistry(); + /// @notice Emitted when a workload to be added is already in the policy + error WorkloadAlreadyInPolicy(); + /// @notice Emitted when a workload to be removed is not in the policy + error WorkloadNotInPolicy(); + /// @notice Emitted when the address is not in the approvedWorkloads mapping + error UnauthorizedBlockBuilder(address caller); + /// @notice Emitted when the nonce is invalid + error InvalidNonce(uint256 expected, uint256 provided); + /// @notice Emitted when the commit hash is empty + error EmptyCommitHash(); + /// @notice Emitted when the source locators array is empty + error EmptySourceLocators(); + } + + struct BlockData { + bytes32 parentHash; + uint256 blockNumber; + uint256 timestamp; + bytes32[] transactionHashes; + } + + type WorkloadId is bytes32; +); + +#[derive(Debug, thiserror::Error)] +pub enum FlashtestationRevertReason { + #[error("flashtestation registry error: {0:?}")] + FlashtestationRegistry(IFlashtestationRegistry::IFlashtestationRegistryErrors), + #[error("block builder policy error: {0:?}")] + BlockBuilderPolicy(IBlockBuilderPolicy::IBlockBuilderPolicyErrors), + #[error("unknown revert: {0} err: {1}")] + Unknown(String, Error), + #[error("halt: {0:?}")] + Halt(OpHaltReason), +} + pub mod args; pub mod attestation; +pub mod builder_tx; pub mod service; pub mod tx_manager; diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index 40f126521..ac948921c 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -1,113 +1,19 @@ -use std::sync::Arc; - -use alloy_primitives::U256; +use alloy_primitives::{Bytes, keccak256}; use reth_node_builder::BuilderContext; -use reth_provider::StateProvider; -use reth_revm::State; -use revm::Database; -use std::fmt::Debug; use tracing::{info, warn}; use crate::{ - builders::{ - BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, OpPayloadBuilderCtx, - }, - primitives::reth::ExecutionInfo, + flashtestations::builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, traits::NodeBounds, - tx_signer::{Signer, generate_ethereum_keypair}, + tx_signer::{Signer, generate_ethereum_keypair, generate_key_from_seed}, }; use super::{ args::FlashtestationsArgs, - attestation::{AttestationConfig, AttestationProvider, get_attestation_provider}, + attestation::{AttestationConfig, get_attestation_provider}, tx_manager::TxManager, }; -#[derive(Clone)] -pub struct FlashtestationsService { - // Attestation provider generating attestations - attestation_provider: Arc>, - // Handles the onchain attestation and TEE block building proofs - tx_manager: TxManager, - // TEE service generated key - tee_service_signer: Signer, - // Funding amount for the TEE signer - funding_amount: U256, -} - -// TODO: FlashtestationsService error types -impl FlashtestationsService { - pub fn new(args: FlashtestationsArgs) -> Self { - let (private_key, public_key, address) = generate_ethereum_keypair(); - let tee_service_signer = Signer { - address, - pubkey: public_key, - secret: private_key, - }; - - let attestation_provider = Arc::new(get_attestation_provider(AttestationConfig { - debug: args.debug, - quote_provider: args.quote_provider, - })); - - let tx_manager = TxManager::new( - tee_service_signer, - args.funding_key - .expect("funding key required when flashtestations enabled"), - args.rpc_url - .expect("external rpc url required when flashtestations enabled"), - args.registry_address - .expect("registry address required when flashtestations enabled"), - args.builder_policy_address - .expect("builder policy address required when flashtestations enabled"), - args.builder_proof_version, - ); - - Self { - attestation_provider, - tx_manager, - tee_service_signer, - funding_amount: args.funding_amount, - } - } - - pub async fn bootstrap(&self) -> eyre::Result<()> { - // Prepare report data with public key (64 bytes, no 0x04 prefix) - let mut report_data = [0u8; 64]; - let pubkey_uncompressed = self.tee_service_signer.pubkey.serialize_uncompressed(); - report_data.copy_from_slice(&pubkey_uncompressed[1..65]); // Skip 0x04 prefix - - // Request TDX attestation - info!(target: "flashtestations", "requesting TDX attestation"); - let attestation = self.attestation_provider.get_attestation(report_data)?; - - // Submit report onchain by registering the key of the tee service - self.tx_manager - .fund_and_register_tee_service(attestation, self.funding_amount) - .await - } - - pub async fn clean_up(&self) -> eyre::Result<()> { - self.tx_manager.clean_up().await - } -} - -#[derive(Debug, Clone)] -pub struct FlashtestationsBuilderTx {} - -impl BuilderTransactions for FlashtestationsBuilderTx { - fn simulate_builder_txs( - &self, - _state_provider: impl StateProvider + Clone, - _info: &mut ExecutionInfo, - _ctx: &OpPayloadBuilderCtx, - _db: &mut State, - _top_of_block: bool, - ) -> Result, BuilderTransactionError> { - Ok(vec![]) - } -} - pub async fn bootstrap_flashtestations( args: FlashtestationsArgs, ctx: &BuilderContext, @@ -115,74 +21,119 @@ pub async fn bootstrap_flashtestations( where Node: NodeBounds, { - info!("Flashtestations enabled"); - - let flashtestations_service = FlashtestationsService::new(args.clone()); - // Generates new key and registers the attestation onchain - flashtestations_service.bootstrap().await?; + let (private_key, public_key, address) = if args.debug { + info!("Flashtestations debug mode enabled, generating debug key"); + // Generate deterministic key for debugging purposes + generate_key_from_seed(&args.debug_tee_key_seed) + } else { + generate_ethereum_keypair() + }; + + info!("Flashtestations key generated: {}", address); + + let tee_service_signer = Signer { + address, + pubkey: public_key, + secret: private_key, + }; + + let funding_key = args + .funding_key + .expect("funding key required when flashtestations enabled"); + let registry_address = args + .registry_address + .expect("registry address required when flashtestations enabled"); + let builder_policy_address = args + .builder_policy_address + .expect("builder policy address required when flashtestations enabled"); + + let attestation_provider = get_attestation_provider(AttestationConfig { + debug: args.debug, + quote_provider: args.quote_provider, + }); + + // Prepare report data: + // - TEE address (20 bytes) at reportData[0:20] + // - Extended registration data hash (32 bytes) at reportData[20:52] + // - Total: 52 bytes, padded to 64 bytes with zeros + + // Extract TEE address as 20 bytes + let tee_address_bytes: [u8; 20] = tee_service_signer.address.into(); + + // Calculate keccak256 hash of empty bytes (32 bytes) + let ext_data = Bytes::from(b""); + let ext_data_hash = keccak256(&ext_data); + + // Create 64-byte report data array + let mut report_data = [0u8; 64]; + + // Copy TEE address (20 bytes) to positions 0-19 + report_data[0..20].copy_from_slice(&tee_address_bytes); + + // Copy extended registration data hash (32 bytes) to positions 20-51 + report_data[20..52].copy_from_slice(ext_data_hash.as_ref()); + + // Request TDX attestation + info!(target: "flashtestations", "requesting TDX attestation"); + let attestation = attestation_provider.get_attestation(report_data)?; + + let (tx_manager, registered) = if let Some(rpc_url) = args.rpc_url { + let tx_manager = TxManager::new( + tee_service_signer, + funding_key, + rpc_url.clone(), + registry_address, + ); + // Submit report onchain by registering the key of the tee service + match tx_manager + .fund_and_register_tee_service( + attestation.clone(), + ext_data.clone(), + args.funding_amount, + ) + .await + { + Ok(_) => (Some(tx_manager), true), + Err(e) => { + warn!(error = %e, "Failed to register tee service via rpc"); + (Some(tx_manager), false) + } + } + } else { + (None, false) + }; + + let flashtestations_builder_tx = FlashtestationsBuilderTx::new(FlashtestationsBuilderTxArgs { + attestation, + extra_registration_data: ext_data, + tee_service_signer, + funding_key, + funding_amount: args.funding_amount, + registry_address, + builder_policy_address, + builder_proof_version: args.builder_proof_version, + enable_block_proofs: args.enable_block_proofs, + registered, + }); - let flashtestations_clone = flashtestations_service.clone(); ctx.task_executor() .spawn_critical_with_graceful_shutdown_signal( "flashtestations clean up task", |shutdown| { Box::pin(async move { let graceful_guard = shutdown.await; - if let Err(e) = flashtestations_clone.clean_up().await { - warn!( - error = %e, - "Failed to complete clean up for flashtestations service", - ) - }; + if let Some(tx_manager) = tx_manager { + if let Err(e) = tx_manager.clean_up().await { + warn!( + error = %e, + "Failed to complete clean up for flashtestations service", + ); + } + } drop(graceful_guard) }) }, ); - Ok(FlashtestationsBuilderTx {}) -} - -#[cfg(test)] -mod tests { - use alloy_primitives::Address; - use secp256k1::{PublicKey, Secp256k1, SecretKey}; - use sha3::{Digest, Keccak256}; - - use crate::tx_signer::public_key_to_address; - - /// Derives Ethereum address from report data using the same logic as the Solidity contract - fn derive_ethereum_address_from_report_data(pubkey_64_bytes: &[u8]) -> Address { - // This exactly matches the Solidity implementation: - // address(uint160(uint256(keccak256(reportData)))) - - // Step 1: keccak256(reportData) - let hash = Keccak256::digest(pubkey_64_bytes); - - // Step 2: Take last 20 bytes (same as uint256 -> uint160 conversion) - let mut address_bytes = [0u8; 20]; - address_bytes.copy_from_slice(&hash[12..32]); - - Address::from(address_bytes) - } - - #[test] - fn test_address_derivation_matches() { - // Test that our manual derivation is correct - let secp = Secp256k1::new(); - let private_key = SecretKey::from_slice(&[0x01; 32]).unwrap(); - let public_key = PublicKey::from_secret_key(&secp, &private_key); - - // Get address using our implementation - let our_address = public_key_to_address(&public_key); - - // Get address using our manual derivation (matching Solidity) - let pubkey_bytes = public_key.serialize_uncompressed(); - let report_data = &pubkey_bytes[1..65]; // Skip 0x04 prefix - let manual_address = derive_ethereum_address_from_report_data(report_data); - - assert_eq!( - our_address, manual_address, - "Address derivation should match" - ); - } + Ok(flashtestations_builder_tx) } diff --git a/crates/op-rbuilder/src/flashtestations/tx_manager.rs b/crates/op-rbuilder/src/flashtestations/tx_manager.rs index 9eabdd28a..d2c0cc968 100644 --- a/crates/op-rbuilder/src/flashtestations/tx_manager.rs +++ b/crates/op-rbuilder/src/flashtestations/tx_manager.rs @@ -1,41 +1,34 @@ -use alloy_consensus::TxEip1559; -use alloy_eips::Encodable2718; +use alloy_json_rpc::RpcError; use alloy_network::ReceiptResponse; -use alloy_primitives::{Address, B256, Bytes, TxHash, TxKind, U256, keccak256}; +use alloy_primitives::{Address, Bytes, TxHash, TxKind, U256}; use alloy_rpc_types_eth::TransactionRequest; -use alloy_transport::TransportResult; -use op_alloy_consensus::OpTypedTransaction; -use reth_optimism_node::OpBuiltPayload; -use reth_optimism_primitives::OpTransactionSigned; -use reth_primitives::Recovered; +use alloy_sol_types::SolCall; +use alloy_transport::{TransportError, TransportErrorKind, TransportResult}; +use k256::ecdsa; use std::time::Duration; -use alloy_provider::{PendingTransactionBuilder, Provider, ProviderBuilder}; +use alloy_provider::{ + PendingTransactionBuilder, PendingTransactionError, Provider, ProviderBuilder, +}; use alloy_signer_local::PrivateKeySigner; -use alloy_sol_types::{SolCall, SolValue, sol}; use op_alloy_network::Optimism; -use tracing::{debug, error, info}; - -use crate::tx_signer::Signer; - -sol!( - #[sol(rpc, abi)] - interface IFlashtestationRegistry { - function registerTEEService(bytes calldata rawQuote) external; - } - - #[sol(rpc, abi)] - interface IBlockBuilderPolicy { - function verifyBlockBuilderProof(uint8 version, bytes32 blockContentHash) external; - } - - struct BlockData { - bytes32 parentHash; - uint256 blockNumber; - uint256 timestamp; - bytes32[] transactionHashes; - } -); +use tracing::{debug, error, info, warn}; + +use crate::{flashtestations::IFlashtestationRegistry, tx_signer::Signer}; + +#[derive(Debug, thiserror::Error)] +pub enum TxManagerError { + #[error("rpc error: {0}")] + RpcError(#[from] TransportError), + #[error("tx reverted: {0}")] + TxReverted(TxHash), + #[error("error checking tx confirmation: {0}")] + TxConfirmationError(PendingTransactionError), + #[error("tx rpc error: {0}")] + TxRpcError(RpcError), + #[error("signer error: {0}")] + SignerError(ecdsa::Error), +} #[derive(Debug, Clone)] pub struct TxManager { @@ -43,8 +36,6 @@ pub struct TxManager { funding_signer: Signer, rpc_url: String, registry_address: Address, - builder_policy_address: Address, - builder_proof_version: u8, } impl TxManager { @@ -53,21 +44,23 @@ impl TxManager { funding_signer: Signer, rpc_url: String, registry_address: Address, - builder_policy_address: Address, - builder_proof_version: u8, ) -> Self { Self { tee_service_signer, funding_signer, rpc_url, registry_address, - builder_policy_address, - builder_proof_version, } } - pub async fn fund_address(&self, from: Signer, to: Address, amount: U256) -> eyre::Result<()> { - let funding_wallet = PrivateKeySigner::from_bytes(&from.secret.secret_bytes().into())?; + pub async fn fund_address( + &self, + from: Signer, + to: Address, + amount: U256, + ) -> Result<(), TxManagerError> { + let funding_wallet = PrivateKeySigner::from_bytes(&from.secret.secret_bytes().into()) + .map_err(TxManagerError::SignerError)?; let provider = ProviderBuilder::new() .disable_recommended_fillers() .fetch_chain_id() @@ -91,36 +84,41 @@ impl TxManager { match Self::process_pending_tx(provider.send_transaction(funding_tx.into()).await).await { Ok(tx_hash) => { info!(target: "flashtestations", tx_hash = %tx_hash, "funding transaction confirmed successfully"); + Ok(()) } Err(e) => { - error!(target: "flashtestations", error = %e, "funding transaction failed"); - return Err(e); + warn!(target: "flashtestations", error = %e, "funding transaction failed"); + Err(e) } } - - Ok(()) } pub async fn fund_and_register_tee_service( &self, attestation: Vec, + extra_registration_data: Bytes, funding_amount: U256, - ) -> eyre::Result<()> { + ) -> Result<(), TxManagerError> { info!(target: "flashtestations", "funding TEE address at {}", self.tee_service_signer.address); self.fund_address( self.funding_signer, self.tee_service_signer.address, funding_amount, ) - .await?; + .await + .unwrap_or_else(|e| { + warn!(target: "flashtestations", error = %e, "Failed to fund TEE address, attempting to register without funding"); + }); let quote_bytes = Bytes::from(attestation); let wallet = - PrivateKeySigner::from_bytes(&self.tee_service_signer.secret.secret_bytes().into())?; + PrivateKeySigner::from_bytes(&self.tee_service_signer.secret.secret_bytes().into()) + .map_err(TxManagerError::SignerError)?; let provider = ProviderBuilder::new() .disable_recommended_fillers() .fetch_chain_id() .with_gas_estimation() + .with_cached_nonce_management() .wallet(wallet) .network::() .connect(self.rpc_url.as_str()) @@ -128,62 +126,29 @@ impl TxManager { info!(target: "flashtestations", "submitting quote to registry at {}", self.registry_address); - // TODO: add retries let calldata = IFlashtestationRegistry::registerTEEServiceCall { rawQuote: quote_bytes, + extendedRegistrationData: extra_registration_data, } .abi_encode(); let tx = TransactionRequest { from: Some(self.tee_service_signer.address), to: Some(TxKind::Call(self.registry_address)), - // gas: Some(10_000_000), // Set gas limit manually as the contract is gas heavy - nonce: Some(0), input: calldata.into(), ..Default::default() }; match Self::process_pending_tx(provider.send_transaction(tx.into()).await).await { Ok(tx_hash) => { info!(target: "flashtestations", tx_hash = %tx_hash, "attestation transaction confirmed successfully"); - Ok(()) } Err(e) => { - error!(target: "flashtestations", error = %e, "attestation transaction failed to be sent"); - Err(e) + warn!(target: "flashtestations", error = %e, "attestation transaction failed to be sent"); } } + Ok(()) } - pub fn signed_block_builder_proof( - &self, - payload: OpBuiltPayload, - gas_limit: u64, - base_fee: u64, - chain_id: u64, - nonce: u64, - ) -> Result, secp256k1::Error> { - let block_content_hash = Self::compute_block_content_hash(payload); - - info!(target: "flashtestations", block_content_hash = ?block_content_hash, "submitting block builder proof transaction"); - let calldata = IBlockBuilderPolicy::verifyBlockBuilderProofCall { - version: self.builder_proof_version, - blockContentHash: block_content_hash, - } - .abi_encode(); - // Create the EIP-1559 transaction - let tx = OpTypedTransaction::Eip1559(TxEip1559 { - chain_id, - nonce, - gas_limit, - max_fee_per_gas: base_fee.into(), - max_priority_fee_per_gas: 0, - to: TxKind::Call(self.builder_policy_address), - input: calldata.into(), - ..Default::default() - }); - self.tee_service_signer.sign_tx(tx) - } - - pub async fn clean_up(&self) -> eyre::Result<()> { + pub async fn clean_up(&self) -> Result<(), TxManagerError> { info!(target: "flashtestations", "sending funds back from TEE generated key to funding address"); let provider = ProviderBuilder::new() .disable_recommended_fillers() @@ -202,7 +167,7 @@ impl TxManager { self.funding_signer.address, balance.saturating_sub(gas_cost), ) - .await? + .await?; } Ok(()) } @@ -210,7 +175,7 @@ impl TxManager { /// Processes a pending transaction and logs whether the transaction succeeded or not async fn process_pending_tx( pending_tx_result: TransportResult>, - ) -> eyre::Result { + ) -> Result { match pending_tx_result { Ok(pending_tx) => { let tx_hash = *pending_tx.tx_hash(); @@ -226,42 +191,13 @@ impl TxManager { if receipt.status() { Ok(receipt.transaction_hash()) } else { - Err(eyre::eyre!("Transaction reverted: {}", tx_hash)) + Err(TxManagerError::TxReverted(tx_hash)) } } - Err(e) => Err(e.into()), + Err(e) => Err(TxManagerError::TxConfirmationError(e)), } } - Err(e) => Err(e.into()), + Err(e) => Err(TxManagerError::TxRpcError(e)), } } - - /// Computes the block content hash according to the formula: - /// keccak256(abi.encode(parentHash, blockNumber, timestamp, transactionHashes)) - fn compute_block_content_hash(payload: OpBuiltPayload) -> B256 { - let block = payload.block(); - let body = block.clone().into_body(); - let transactions = body.transactions(); - - // Create ordered list of transaction hashes - let transaction_hashes: Vec = transactions - .map(|tx| { - // RLP encode the transaction and hash it - let mut encoded = Vec::new(); - tx.encode_2718(&mut encoded); - keccak256(&encoded) - }) - .collect(); - - // Create struct and ABI encode - let block_data = BlockData { - parentHash: block.parent_hash, - blockNumber: U256::from(block.number), - timestamp: U256::from(block.timestamp), - transactionHashes: transaction_hashes, - }; - - let encoded = block_data.abi_encode(); - keccak256(&encoded) - } }