op_revm/transaction/
abstraction.rs

1//! Optimism transaction abstraction containing the `[OpTxTr]` trait and corresponding `[OpTransaction]` type.
2use super::deposit::{DepositTransactionParts, DEPOSIT_TRANSACTION_TYPE};
3use auto_impl::auto_impl;
4use revm::{
5    context::{
6        tx::{TxEnvBuildError, TxEnvBuilder},
7        TxEnv,
8    },
9    context_interface::transaction::Transaction,
10    handler::SystemCallTx,
11    primitives::{Address, Bytes, TxKind, B256, U256},
12};
13use std::vec;
14
15/// Optimism Transaction trait.
16#[auto_impl(&, &mut, Box, Arc)]
17pub trait OpTxTr: Transaction {
18    /// Enveloped transaction bytes.
19    fn enveloped_tx(&self) -> Option<&Bytes>;
20
21    /// Source hash of the deposit transaction.
22    fn source_hash(&self) -> Option<B256>;
23
24    /// Mint of the deposit transaction
25    fn mint(&self) -> Option<u128>;
26
27    /// Whether the transaction is a system transaction
28    fn is_system_transaction(&self) -> bool;
29
30    /// Returns `true` if transaction is of type [`DEPOSIT_TRANSACTION_TYPE`].
31    fn is_deposit(&self) -> bool {
32        self.tx_type() == DEPOSIT_TRANSACTION_TYPE
33    }
34}
35
36/// Optimism transaction.
37#[derive(Clone, Debug, PartialEq, Eq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct OpTransaction<T: Transaction> {
40    /// Base transaction fields.
41    pub base: T,
42    /// An enveloped EIP-2718 typed transaction
43    ///
44    /// This is used to compute the L1 tx cost using the L1 block info, as
45    /// opposed to requiring downstream apps to compute the cost
46    /// externally.
47    pub enveloped_tx: Option<Bytes>,
48    /// Deposit transaction parts.
49    pub deposit: DepositTransactionParts,
50}
51
52impl<T: Transaction> AsRef<T> for OpTransaction<T> {
53    fn as_ref(&self) -> &T {
54        &self.base
55    }
56}
57
58impl<T: Transaction> OpTransaction<T> {
59    /// Create a new Optimism transaction.
60    pub fn new(base: T) -> Self {
61        Self {
62            base,
63            enveloped_tx: None,
64            deposit: DepositTransactionParts::default(),
65        }
66    }
67}
68
69impl OpTransaction<TxEnv> {
70    /// Create a new Optimism transaction.
71    pub fn builder() -> OpTransactionBuilder {
72        OpTransactionBuilder::new()
73    }
74}
75
76impl Default for OpTransaction<TxEnv> {
77    fn default() -> Self {
78        Self {
79            base: TxEnv::default(),
80            enveloped_tx: Some(vec![0x00].into()),
81            deposit: DepositTransactionParts::default(),
82        }
83    }
84}
85
86impl<TX: Transaction + SystemCallTx> SystemCallTx for OpTransaction<TX> {
87    fn new_system_tx_with_caller(
88        caller: Address,
89        system_contract_address: Address,
90        data: Bytes,
91    ) -> Self {
92        let mut tx = OpTransaction::new(TX::new_system_tx_with_caller(
93            caller,
94            system_contract_address,
95            data,
96        ));
97
98        tx.enveloped_tx = Some(Bytes::default());
99
100        tx
101    }
102}
103
104impl<T: Transaction> Transaction for OpTransaction<T> {
105    type AccessListItem<'a>
106        = T::AccessListItem<'a>
107    where
108        T: 'a;
109    type Authorization<'a>
110        = T::Authorization<'a>
111    where
112        T: 'a;
113
114    fn tx_type(&self) -> u8 {
115        // If this is a deposit transaction (has source_hash set), return deposit type
116        if self.deposit.source_hash != B256::ZERO {
117            DEPOSIT_TRANSACTION_TYPE
118        } else {
119            self.base.tx_type()
120        }
121    }
122
123    fn caller(&self) -> Address {
124        self.base.caller()
125    }
126
127    fn gas_limit(&self) -> u64 {
128        self.base.gas_limit()
129    }
130
131    fn value(&self) -> U256 {
132        self.base.value()
133    }
134
135    fn input(&self) -> &Bytes {
136        self.base.input()
137    }
138
139    fn nonce(&self) -> u64 {
140        self.base.nonce()
141    }
142
143    fn kind(&self) -> TxKind {
144        self.base.kind()
145    }
146
147    fn chain_id(&self) -> Option<u64> {
148        self.base.chain_id()
149    }
150
151    fn access_list(&self) -> Option<impl Iterator<Item = Self::AccessListItem<'_>>> {
152        self.base.access_list()
153    }
154
155    fn max_priority_fee_per_gas(&self) -> Option<u128> {
156        self.base.max_priority_fee_per_gas()
157    }
158
159    fn max_fee_per_gas(&self) -> u128 {
160        self.base.max_fee_per_gas()
161    }
162
163    fn gas_price(&self) -> u128 {
164        self.base.gas_price()
165    }
166
167    fn blob_versioned_hashes(&self) -> &[B256] {
168        self.base.blob_versioned_hashes()
169    }
170
171    fn max_fee_per_blob_gas(&self) -> u128 {
172        self.base.max_fee_per_blob_gas()
173    }
174
175    fn effective_gas_price(&self, base_fee: u128) -> u128 {
176        // Deposit transactions use gas_price directly
177        if self.tx_type() == DEPOSIT_TRANSACTION_TYPE {
178            return self.gas_price();
179        }
180        self.base.effective_gas_price(base_fee)
181    }
182
183    fn authorization_list_len(&self) -> usize {
184        self.base.authorization_list_len()
185    }
186
187    fn authorization_list(&self) -> impl Iterator<Item = Self::Authorization<'_>> {
188        self.base.authorization_list()
189    }
190}
191
192impl<T: Transaction> OpTxTr for OpTransaction<T> {
193    fn enveloped_tx(&self) -> Option<&Bytes> {
194        self.enveloped_tx.as_ref()
195    }
196
197    fn source_hash(&self) -> Option<B256> {
198        if self.tx_type() != DEPOSIT_TRANSACTION_TYPE {
199            return None;
200        }
201        Some(self.deposit.source_hash)
202    }
203
204    fn mint(&self) -> Option<u128> {
205        self.deposit.mint
206    }
207
208    fn is_system_transaction(&self) -> bool {
209        self.deposit.is_system_transaction
210    }
211}
212
213/// Builder for constructing [`OpTransaction`] instances
214#[derive(Default, Debug)]
215pub struct OpTransactionBuilder {
216    base: TxEnvBuilder,
217    enveloped_tx: Option<Bytes>,
218    deposit: DepositTransactionParts,
219}
220
221impl OpTransactionBuilder {
222    /// Create a new builder with default values
223    pub fn new() -> Self {
224        Self {
225            base: TxEnvBuilder::new(),
226            enveloped_tx: None,
227            deposit: DepositTransactionParts::default(),
228        }
229    }
230
231    /// Set the base transaction builder based for TxEnvBuilder.
232    pub fn base(mut self, base: TxEnvBuilder) -> Self {
233        self.base = base;
234        self
235    }
236
237    /// Set the enveloped transaction bytes.
238    pub fn enveloped_tx(mut self, enveloped_tx: Option<Bytes>) -> Self {
239        self.enveloped_tx = enveloped_tx;
240        self
241    }
242
243    /// Set the source hash of the deposit transaction.
244    pub fn source_hash(mut self, source_hash: B256) -> Self {
245        self.deposit.source_hash = source_hash;
246        self
247    }
248
249    /// Set the mint of the deposit transaction.
250    pub fn mint(mut self, mint: u128) -> Self {
251        self.deposit.mint = Some(mint);
252        self
253    }
254
255    /// Set the deposit transaction to be a system transaction.
256    pub fn is_system_transaction(mut self) -> Self {
257        self.deposit.is_system_transaction = true;
258        self
259    }
260
261    /// Set the deposit transaction to not be a system transaction.
262    pub fn not_system_transaction(mut self) -> Self {
263        self.deposit.is_system_transaction = false;
264        self
265    }
266
267    /// Set the deposit transaction to be a deposit transaction.
268    pub fn is_deposit_tx(mut self) -> Self {
269        self.base = self.base.tx_type(Some(DEPOSIT_TRANSACTION_TYPE));
270        self
271    }
272
273    /// Build the [`OpTransaction`] with default values for missing fields.
274    ///
275    /// This is useful for testing and debugging where it is not necessary to
276    /// have full [`OpTransaction`] instance.
277    ///
278    /// If the transaction is a deposit (either `tx_type == DEPOSIT_TRANSACTION_TYPE` or
279    /// `source_hash != B256::ZERO`), set the transaction type accordingly and ensure the
280    /// `enveloped_tx` is removed (`None`). For non-deposit transactions, ensure
281    /// `enveloped_tx` is set.
282    pub fn build_fill(mut self) -> OpTransaction<TxEnv> {
283        let tx_type = self.base.get_tx_type();
284        if tx_type.is_some() {
285            if tx_type == Some(DEPOSIT_TRANSACTION_TYPE) {
286                // source hash is required for deposit transactions
287                if self.deposit.source_hash == B256::ZERO {
288                    self.deposit.source_hash = B256::from([1u8; 32]);
289                }
290                // deposit transactions should not carry enveloped bytes
291                self.enveloped_tx = None;
292            } else {
293                // enveloped is required for non-deposit transactions
294                self.enveloped_tx = Some(vec![0x00].into());
295            }
296        } else if self.deposit.source_hash != B256::ZERO {
297            // if type is not set and source hash is set, set the transaction type to deposit
298            self.base = self.base.tx_type(Some(DEPOSIT_TRANSACTION_TYPE));
299            // deposit transactions should not carry enveloped bytes
300            self.enveloped_tx = None;
301        } else if self.enveloped_tx.is_none() {
302            // if type is not set and source hash is not set, set the enveloped transaction to something.
303            self.enveloped_tx = Some(vec![0x00].into());
304        }
305
306        let base = self.base.build_fill();
307
308        OpTransaction {
309            base,
310            enveloped_tx: self.enveloped_tx,
311            deposit: self.deposit,
312        }
313    }
314
315    /// Build the [`OpTransaction`] instance, return error if the transaction is not valid.
316    ///
317    pub fn build(mut self) -> Result<OpTransaction<TxEnv>, OpBuildError> {
318        let tx_type = self.base.get_tx_type();
319        if tx_type.is_some() {
320            if Some(DEPOSIT_TRANSACTION_TYPE) == tx_type {
321                // if tx type is deposit, check if source hash is set
322                if self.deposit.source_hash == B256::ZERO {
323                    return Err(OpBuildError::MissingSourceHashForDeposit);
324                }
325            } else if self.enveloped_tx.is_none() {
326                // enveloped is required for non-deposit transactions
327                return Err(OpBuildError::MissingEnvelopedTxBytes);
328            }
329        } else if self.deposit.source_hash != B256::ZERO {
330            // if type is not set and source hash is set, set the transaction type to deposit
331            self.base = self.base.tx_type(Some(DEPOSIT_TRANSACTION_TYPE));
332        } else if self.enveloped_tx.is_none() {
333            // tx is not deposit and enveloped is required
334            return Err(OpBuildError::MissingEnvelopedTxBytes);
335        }
336
337        let base = self.base.build()?;
338
339        Ok(OpTransaction {
340            base,
341            enveloped_tx: self.enveloped_tx,
342            deposit: self.deposit,
343        })
344    }
345}
346
347/// Error type for building [`TxEnv`]
348#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
349#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
350pub enum OpBuildError {
351    /// Base transaction build error
352    Base(TxEnvBuildError),
353    /// Missing enveloped transaction bytes
354    MissingEnvelopedTxBytes,
355    /// Missing source hash for deposit transaction
356    MissingSourceHashForDeposit,
357}
358
359impl From<TxEnvBuildError> for OpBuildError {
360    fn from(error: TxEnvBuildError) -> Self {
361        OpBuildError::Base(error)
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use revm::{
369        context_interface::Transaction,
370        primitives::{Address, B256},
371    };
372
373    #[test]
374    fn test_deposit_transaction_fields() {
375        let base_tx = TxEnv::builder()
376            .gas_limit(10)
377            .gas_price(100)
378            .gas_priority_fee(Some(5));
379
380        let op_tx = OpTransaction::builder()
381            .base(base_tx)
382            .enveloped_tx(None)
383            .not_system_transaction()
384            .mint(0u128)
385            .source_hash(B256::from([1u8; 32]))
386            .build()
387            .unwrap();
388        // Verify transaction type (deposit transactions should have tx_type based on OpSpecId)
389        // The tx_type is derived from the transaction structure, not set manually
390        // Verify common fields access
391        assert_eq!(op_tx.gas_limit(), 10);
392        assert_eq!(op_tx.kind(), revm::primitives::TxKind::Call(Address::ZERO));
393        // Verify gas related calculations - deposit transactions use gas_price for effective gas price
394        assert_eq!(op_tx.effective_gas_price(90), 100);
395        assert_eq!(op_tx.max_fee_per_gas(), 100);
396    }
397}