diff --git a/crates/client/flashblocks/Cargo.toml b/crates/client/flashblocks/Cargo.toml index 832e4d95..69e794ff 100644 --- a/crates/client/flashblocks/Cargo.toml +++ b/crates/client/flashblocks/Cargo.toml @@ -50,7 +50,6 @@ alloy-provider.workspace = true alloy-rpc-types.workspace = true alloy-consensus.workspace = true alloy-primitives.workspace = true -alloy-op-evm.workspace = true alloy-rpc-types-eth.workspace = true alloy-rpc-types-engine.workspace = true @@ -96,6 +95,7 @@ base-flashblocks = { path = ".", features = ["test-utils"] } rstest.workspace = true rand.workspace = true eyre.workspace = true +revm = { workspace = true, features = ["std"] } reth-db.workspace = true once_cell.workspace = true reth-provider.workspace = true diff --git a/crates/client/flashblocks/src/error.rs b/crates/client/flashblocks/src/error.rs index 8574125f..3af9b0db 100644 --- a/crates/client/flashblocks/src/error.rs +++ b/crates/client/flashblocks/src/error.rs @@ -78,6 +78,10 @@ pub enum ExecutionError { /// Failed to load cache account for depositor. #[error("failed to load cache account for deposit transaction sender")] DepositAccountLoad, + + /// Failed to build RPC receipt. + #[error("failed to build RPC receipt: {0}")] + RpcReceiptBuild(String), } impl From for ExecutionError { @@ -86,6 +90,12 @@ impl From for ExecutionError { } } +impl From for ExecutionError { + fn from(err: crate::ReceiptBuildError) -> Self { + Self::RpcReceiptBuild(err.to_string()) + } +} + /// Errors related to pending blocks construction. #[derive(Debug, Clone, Eq, PartialEq, Error)] pub enum BuildError { @@ -124,6 +134,12 @@ impl From for StateProcessorError { } } +impl From for StateProcessorError { + fn from(err: crate::ReceiptBuildError) -> Self { + Self::Execution(ExecutionError::from(err)) + } +} + /// A type alias for `Result`. pub type Result = std::result::Result; diff --git a/crates/client/flashblocks/src/lib.rs b/crates/client/flashblocks/src/lib.rs index ad3a288f..30931992 100644 --- a/crates/client/flashblocks/src/lib.rs +++ b/crates/client/flashblocks/src/lib.rs @@ -32,6 +32,9 @@ pub use traits::{FlashblocksAPI, FlashblocksReceiver, PendingBlocksAPI}; mod state_builder; pub use state_builder::{ExecutedPendingTransaction, PendingStateBuilder}; +mod receipt_builder; +pub use receipt_builder::{ReceiptBuildError, UnifiedReceiptBuilder}; + mod validation; pub use validation::{ CanonicalBlockReconciler, FlashblockSequenceValidator, ReconciliationStrategy, diff --git a/crates/client/flashblocks/src/processor.rs b/crates/client/flashblocks/src/processor.rs index d31786d8..3310c9a3 100644 --- a/crates/client/flashblocks/src/processor.rs +++ b/crates/client/flashblocks/src/processor.rs @@ -421,7 +421,6 @@ where prev_pending_blocks.clone(), l1_block_info, state_overrides, - *evm_config.block_executor_factory().receipt_builder(), ); for (idx, (transaction, sender)) in txs_with_senders.into_iter().enumerate() { diff --git a/crates/client/flashblocks/src/receipt_builder.rs b/crates/client/flashblocks/src/receipt_builder.rs new file mode 100644 index 00000000..2fa1781b --- /dev/null +++ b/crates/client/flashblocks/src/receipt_builder.rs @@ -0,0 +1,339 @@ +//! Unified receipt builder for Optimism transactions. +//! +//! This module provides a receipt builder that handles both deposit and non-deposit +//! transactions seamlessly, without requiring error handling at the call site. + +use alloy_consensus::{Eip658Value, Receipt, transaction::Recovered}; +use op_alloy_consensus::{OpDepositReceipt, OpTxEnvelope, OpTxType}; +use reth_evm::Evm; +use reth_optimism_chainspec::OpHardforks; +use reth_optimism_primitives::OpReceipt; +use revm::{Database, context::result::ExecutionResult}; + +/// Error type for receipt building operations. +#[derive(Debug, thiserror::Error)] +pub enum ReceiptBuildError { + /// Failed to load the deposit sender's account from the database. + #[error("failed to load deposit account")] + DepositAccountLoad, +} + +/// A unified receipt builder that handles both deposit and non-deposit transactions +/// seamlessly without requiring error handling at the call site. +/// +/// This builder automatically handles the deposit receipt case internally, +/// eliminating the need for callers to implement the try-catch pattern typically +/// required when using `build_receipt` followed by `build_deposit_receipt`. +/// +/// # Example +/// +/// ```ignore +/// let builder = UnifiedReceiptBuilder::new(chain_spec); +/// let receipt = builder.build(&mut evm, &transaction, result, cumulative_gas_used, timestamp)?; +/// ``` +#[derive(Debug, Clone)] +pub struct UnifiedReceiptBuilder { + chain_spec: C, +} + +impl UnifiedReceiptBuilder { + /// Creates a new unified receipt builder with the given chain specification. + pub const fn new(chain_spec: C) -> Self { + Self { chain_spec } + } + + /// Returns a reference to the chain specification. + pub const fn chain_spec(&self) -> &C { + &self.chain_spec + } +} + +impl UnifiedReceiptBuilder { + /// Builds a receipt for any transaction type, handling deposit receipts internally. + /// + /// This method builds either a regular receipt or a deposit receipt based on + /// the transaction type. For deposit transactions, it automatically fetches + /// the required deposit-specific data (nonce and receipt version). + /// + /// # Arguments + /// + /// * `evm` - Mutable reference to the EVM, used for database access + /// * `transaction` - The recovered transaction to build a receipt for + /// * `result` - The execution result + /// * `cumulative_gas_used` - Cumulative gas used up to and including this transaction + /// * `timestamp` - The block timestamp, used to determine active hardforks + /// + /// # Returns + /// + /// Returns the built receipt on success, or a [`ReceiptBuildError`] if the deposit + /// account could not be loaded from the database. + pub fn build( + &self, + evm: &mut E, + transaction: &Recovered, + result: ExecutionResult, + cumulative_gas_used: u64, + timestamp: u64, + ) -> Result + where + E: Evm, + E::DB: Database, + { + let tx_type = transaction.tx_type(); + + // Build the inner receipt from the execution result + let receipt = Receipt { + status: Eip658Value::Eip658(result.is_success()), + cumulative_gas_used, + logs: result.into_logs(), + }; + + if tx_type == OpTxType::Deposit { + // Handle deposit transaction + let is_canyon_active = self.chain_spec.is_canyon_active_at_timestamp(timestamp); + let is_regolith_active = self.chain_spec.is_regolith_active_at_timestamp(timestamp); + + // Fetch deposit nonce if Regolith is active + let deposit_nonce = if is_regolith_active { + Some( + evm.db_mut() + .basic(transaction.signer()) + .map_err(|_| ReceiptBuildError::DepositAccountLoad)? + .map(|acc| acc.nonce) + .unwrap_or_default(), + ) + } else { + None + }; + + Ok(OpReceipt::Deposit(OpDepositReceipt { + inner: receipt, + deposit_nonce, + deposit_receipt_version: is_canyon_active.then_some(1), + })) + } else { + // Handle non-deposit transaction + Ok(match tx_type { + OpTxType::Legacy => OpReceipt::Legacy(receipt), + OpTxType::Eip2930 => OpReceipt::Eip2930(receipt), + OpTxType::Eip1559 => OpReceipt::Eip1559(receipt), + OpTxType::Eip7702 => OpReceipt::Eip7702(receipt), + OpTxType::Deposit => unreachable!(), + }) + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use alloy_consensus::Header; + use alloy_primitives::{Address, Log, LogData, TxKind, address}; + use op_alloy_consensus::TxDeposit; + use reth_evm::{ConfigureEvm, op_revm::OpHaltReason}; + use reth_optimism_chainspec::OpChainSpecBuilder; + use reth_optimism_evm::OpEvmConfig; + use revm::database::InMemoryDB; + + use super::*; + + fn create_legacy_tx() -> Recovered { + let tx = alloy_consensus::TxLegacy { + chain_id: Some(1), + nonce: 0, + gas_price: 1000000000, + gas_limit: 21000, + to: TxKind::Call(Address::ZERO), + value: alloy_primitives::U256::ZERO, + input: alloy_primitives::Bytes::new(), + }; + let envelope = OpTxEnvelope::Legacy(alloy_consensus::Signed::new_unchecked( + tx, + alloy_primitives::Signature::test_signature(), + alloy_primitives::B256::ZERO, + )); + Recovered::new_unchecked(envelope, Address::ZERO) + } + + fn create_deposit_tx() -> Recovered { + let deposit = TxDeposit { + source_hash: alloy_primitives::B256::ZERO, + from: address!("0x1234567890123456789012345678901234567890"), + to: TxKind::Call(Address::ZERO), + mint: 0, + value: alloy_primitives::U256::ZERO, + gas_limit: 21000, + is_system_transaction: false, + input: alloy_primitives::Bytes::new(), + }; + let sealed = alloy_consensus::Sealed::new_unchecked(deposit, alloy_primitives::B256::ZERO); + let envelope = OpTxEnvelope::Deposit(sealed); + Recovered::new_unchecked(envelope, address!("0x1234567890123456789012345678901234567890")) + } + + fn create_success_result() -> ExecutionResult { + ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Stop, + gas_used: 21000, + gas_refunded: 0, + logs: vec![Log { + address: Address::ZERO, + data: LogData::new_unchecked(vec![], alloy_primitives::Bytes::new()), + }], + output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()), + } + } + + #[test] + fn test_unified_receipt_builder_creation() { + let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().build()); + let builder = UnifiedReceiptBuilder::new(chain_spec.clone()); + assert!(Arc::ptr_eq(builder.chain_spec(), &chain_spec)); + } + + #[test] + fn test_receipt_from_success_result() { + let result: ExecutionResult = create_success_result(); + let receipt = Receipt { + status: Eip658Value::Eip658(result.is_success()), + cumulative_gas_used: 21000, + logs: result.into_logs(), + }; + assert!(receipt.status.coerce_status()); + assert_eq!(receipt.cumulative_gas_used, 21000); + assert_eq!(receipt.logs.len(), 1); + } + + #[test] + fn test_receipt_from_revert_result() { + let result: ExecutionResult = + ExecutionResult::Revert { gas_used: 10000, output: alloy_primitives::Bytes::new() }; + let receipt = Receipt { + status: Eip658Value::Eip658(result.is_success()), + cumulative_gas_used: 10000, + logs: result.into_logs(), + }; + assert!(!receipt.status.coerce_status()); + assert_eq!(receipt.cumulative_gas_used, 10000); + assert!(receipt.logs.is_empty()); + } + + #[test] + fn test_op_receipt_legacy_variant() { + let receipt = + Receipt { status: Eip658Value::Eip658(true), cumulative_gas_used: 21000, logs: vec![] }; + let op_receipt = OpReceipt::Legacy(receipt); + assert!(matches!(op_receipt, OpReceipt::Legacy(_))); + } + + #[test] + fn test_op_receipt_deposit_variant() { + let receipt = + Receipt { status: Eip658Value::Eip658(true), cumulative_gas_used: 21000, logs: vec![] }; + let op_receipt = OpReceipt::Deposit(OpDepositReceipt { + inner: receipt, + deposit_nonce: Some(1), + deposit_receipt_version: Some(1), + }); + assert!(matches!(op_receipt, OpReceipt::Deposit(_))); + if let OpReceipt::Deposit(deposit) = op_receipt { + assert_eq!(deposit.deposit_nonce, Some(1)); + assert_eq!(deposit.deposit_receipt_version, Some(1)); + } + } + + /// Helper to create an EVM instance for testing + fn create_test_evm( + chain_spec: Arc, + db: &mut InMemoryDB, + ) -> impl Evm + '_ { + let evm_config = OpEvmConfig::optimism(chain_spec); + let header = Header::default(); + let evm_env = evm_config.evm_env(&header).expect("failed to create evm env"); + evm_config.evm_with_env(db, evm_env) + } + + #[test] + fn test_build_legacy_receipt() { + let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().build()); + let mut db = InMemoryDB::default(); + let mut evm = create_test_evm(chain_spec.clone(), &mut db); + + let builder = UnifiedReceiptBuilder::new(chain_spec); + let tx = create_legacy_tx(); + let result = create_success_result(); + + let receipt = builder.build(&mut evm, &tx, result, 21000, 0).expect("build should succeed"); + + assert!(matches!(receipt, OpReceipt::Legacy(_))); + if let OpReceipt::Legacy(inner) = receipt { + assert!(inner.status.coerce_status()); + assert_eq!(inner.cumulative_gas_used, 21000); + } + } + + #[test] + fn test_build_deposit_receipt() { + let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().build()); + let mut db = InMemoryDB::default(); + let mut evm = create_test_evm(chain_spec.clone(), &mut db); + + let builder = UnifiedReceiptBuilder::new(chain_spec); + let tx = create_deposit_tx(); + let result = create_success_result(); + + let receipt = builder.build(&mut evm, &tx, result, 21000, 0).expect("build should succeed"); + + assert!(matches!(receipt, OpReceipt::Deposit(_))); + if let OpReceipt::Deposit(deposit) = receipt { + assert!(deposit.inner.status.coerce_status()); + assert_eq!(deposit.inner.cumulative_gas_used, 21000); + } + } + + #[test] + fn test_build_deposit_receipt_with_canyon_active() { + // Canyon activates deposit_receipt_version + let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().build()); + let mut db = InMemoryDB::default(); + let mut evm = create_test_evm(chain_spec.clone(), &mut db); + + let builder = UnifiedReceiptBuilder::new(chain_spec.clone()); + let tx = create_deposit_tx(); + let result = create_success_result(); + + // Use a timestamp after Canyon activation (Base mainnet Canyon: 1704992401) + let canyon_timestamp = 1704992401 + 1000; + let receipt = builder + .build(&mut evm, &tx, result, 21000, canyon_timestamp) + .expect("build should succeed"); + + if let OpReceipt::Deposit(deposit) = receipt { + assert_eq!(deposit.deposit_receipt_version, Some(1)); + } else { + panic!("Expected deposit receipt"); + } + } + + #[test] + fn test_build_failed_transaction_receipt() { + let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().build()); + let mut db = InMemoryDB::default(); + let mut evm = create_test_evm(chain_spec.clone(), &mut db); + + let builder = UnifiedReceiptBuilder::new(chain_spec); + let tx = create_legacy_tx(); + let result: ExecutionResult = + ExecutionResult::Revert { gas_used: 10000, output: alloy_primitives::Bytes::new() }; + + let receipt = builder.build(&mut evm, &tx, result, 10000, 0).expect("build should succeed"); + + if let OpReceipt::Legacy(inner) = receipt { + assert!(!inner.status.coerce_status()); // Failed transaction + assert_eq!(inner.cumulative_gas_used, 10000); + } else { + panic!("Expected legacy receipt"); + } + } +} diff --git a/crates/client/flashblocks/src/state_builder.rs b/crates/client/flashblocks/src/state_builder.rs index 262b9420..bb980465 100644 --- a/crates/client/flashblocks/src/state_builder.rs +++ b/crates/client/flashblocks/src/state_builder.rs @@ -1,26 +1,22 @@ use std::sync::Arc; use alloy_consensus::{ - Block, Eip658Value, Header, TxReceipt, + Block, Header, TxReceipt, transaction::{Recovered, TransactionMeta}, }; -use alloy_op_evm::block::receipt_builder::OpReceiptBuilder; use alloy_primitives::B256; use alloy_rpc_types::TransactionTrait; use alloy_rpc_types_eth::state::StateOverride; -use op_alloy_consensus::{OpDepositReceipt, OpTxEnvelope}; +use op_alloy_consensus::OpTxEnvelope; use op_alloy_rpc_types::{OpTransactionReceipt, Transaction}; -use reth_evm::{ - Evm, FromRecoveredTx, eth::receipt_builder::ReceiptBuilderCtx, op_revm::L1BlockInfo, -}; +use reth_evm::{Evm, FromRecoveredTx, op_revm::L1BlockInfo}; use reth_optimism_chainspec::OpHardforks; -use reth_optimism_evm::OpRethReceiptBuilder; use reth_optimism_primitives::OpPrimitives; use reth_optimism_rpc::OpReceiptBuilder as OpRpcReceiptBuilder; use reth_rpc_convert::transaction::ConvertReceiptInput; use revm::{Database, DatabaseCommit, context::result::ResultAndState, state::EvmState}; -use crate::{ExecutionError, PendingBlocks, StateProcessorError}; +use crate::{ExecutionError, PendingBlocks, StateProcessorError, UnifiedReceiptBuilder}; /// Represents the result of executing or fetching a cached pending transaction. #[derive(Debug, Clone)] @@ -42,8 +38,7 @@ pub struct PendingStateBuilder { evm: E, pending_block: Block, l1_block_info: L1BlockInfo, - chain_spec: ChainSpec, - receipt_builder: OpRethReceiptBuilder, + receipt_builder: UnifiedReceiptBuilder, prev_pending_blocks: Option>, state_overrides: StateOverride, @@ -64,7 +59,6 @@ where prev_pending_blocks: Option>, l1_block_info: L1BlockInfo, state_overrides: StateOverride, - receipt_builder: OpRethReceiptBuilder, ) -> Self { Self { pending_block, @@ -74,8 +68,7 @@ where prev_pending_blocks, l1_block_info, state_overrides, - chain_spec, - receipt_builder, + receipt_builder: UnifiedReceiptBuilder::new(chain_spec), } } @@ -195,45 +188,15 @@ where .checked_add(gas_used) .ok_or(ExecutionError::GasOverflow)?; - let is_canyon_active = - self.chain_spec.is_canyon_active_at_timestamp(self.pending_block.timestamp); - - let is_regolith_active = - self.chain_spec.is_regolith_active_at_timestamp(self.pending_block.timestamp); - - let receipt = match self.receipt_builder.build_receipt(ReceiptBuilderCtx { - tx: &transaction, - evm: &mut self.evm, + // Build receipt using the unified receipt builder - handles both + // deposit and non-deposit transactions seamlessly + let receipt = self.receipt_builder.build( + &mut self.evm, + &transaction, result, - state: &state, - cumulative_gas_used: self.cumulative_gas_used, - }) { - Ok(receipt) => receipt, - Err(ctx) => { - // This is a deposit transaction, so build the receipt from the context - let receipt = alloy_consensus::Receipt { - status: Eip658Value::Eip658(ctx.result.is_success()), - cumulative_gas_used: ctx.cumulative_gas_used, - logs: ctx.result.into_logs(), - }; - - let deposit_nonce = (is_regolith_active && transaction.is_deposit()) - .then(|| { - self.evm - .db_mut() - .basic(transaction.signer()) - .map(|acc| acc.unwrap_or_default().nonce) - }) - .transpose() - .map_err(|_| ExecutionError::DepositAccountLoad)?; - - self.receipt_builder.build_deposit_receipt(OpDepositReceipt { - inner: receipt, - deposit_nonce, - deposit_receipt_version: is_canyon_active.then_some(1), - }) - } - }; + self.cumulative_gas_used, + self.pending_block.timestamp, + )?; let meta = TransactionMeta { tx_hash, @@ -254,10 +217,13 @@ where meta, }; - let op_receipt = - OpRpcReceiptBuilder::new(&self.chain_spec, input, &mut self.l1_block_info) - .unwrap() - .build(); + let op_receipt = OpRpcReceiptBuilder::new( + self.receipt_builder.chain_spec(), + input, + &mut self.l1_block_info, + ) + .map_err(|e| ExecutionError::RpcReceiptBuild(e.to_string()))? + .build(); self.next_log_index += receipt.logs().len(); let (deposit_receipt_version, deposit_nonce) = if transaction.is_deposit() {