reth_optimism_txpool/
transaction.rs

1use crate::{conditional::MaybeConditionalTransaction, interop::MaybeInteropTransaction};
2use alloy_consensus::{
3    transaction::Recovered, BlobTransactionSidecar, BlobTransactionValidationError, Typed2718,
4};
5use alloy_eips::{eip2930::AccessList, eip7702::SignedAuthorization};
6use alloy_primitives::{Address, Bytes, TxHash, TxKind, B256, U256};
7use alloy_rpc_types_eth::erc4337::TransactionConditional;
8use c_kzg::KzgSettings;
9use core::fmt::Debug;
10use reth_optimism_primitives::OpTransactionSigned;
11use reth_primitives_traits::{InMemorySize, SignedTransaction};
12use reth_transaction_pool::{
13    EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction,
14};
15use std::sync::{
16    atomic::{AtomicU64, Ordering},
17    Arc, OnceLock,
18};
19
20/// Marker for no-interop transactions
21pub(crate) const NO_INTEROP_TX: u64 = 0;
22
23/// Pool transaction for OP.
24///
25/// This type wraps the actual transaction and caches values that are frequently used by the pool.
26/// For payload building this lazily tracks values that are required during payload building:
27///  - Estimated compressed size of this transaction
28#[derive(Debug, Clone, derive_more::Deref)]
29pub struct OpPooledTransaction<
30    Cons = OpTransactionSigned,
31    Pooled = op_alloy_consensus::OpPooledTransaction,
32> {
33    #[deref]
34    inner: EthPooledTransaction<Cons>,
35    /// The estimated size of this transaction, lazily computed.
36    estimated_tx_compressed_size: OnceLock<u64>,
37    /// The pooled transaction type.
38    _pd: core::marker::PhantomData<Pooled>,
39
40    /// Optional conditional attached to this transaction.
41    conditional: Option<Box<TransactionConditional>>,
42
43    /// Optional interop deadline attached to this transaction.
44    interop: Arc<AtomicU64>,
45
46    /// Cached EIP-2718 encoded bytes of the transaction, lazily computed.
47    encoded_2718: OnceLock<Bytes>,
48}
49
50impl<Cons: SignedTransaction, Pooled> OpPooledTransaction<Cons, Pooled> {
51    /// Create new instance of [Self].
52    pub fn new(transaction: Recovered<Cons>, encoded_length: usize) -> Self {
53        Self {
54            inner: EthPooledTransaction::new(transaction, encoded_length),
55            estimated_tx_compressed_size: Default::default(),
56            conditional: None,
57            interop: Arc::new(AtomicU64::new(NO_INTEROP_TX)),
58            _pd: core::marker::PhantomData,
59            encoded_2718: Default::default(),
60        }
61    }
62
63    /// Returns the estimated compressed size of a transaction in bytes scaled by 1e6.
64    /// This value is computed based on the following formula:
65    /// `max(minTransactionSize, intercept + fastlzCoef*fastlzSize)`
66    /// Uses cached EIP-2718 encoded bytes to avoid recomputing the encoding for each estimation.
67    pub fn estimated_compressed_size(&self) -> u64 {
68        *self
69            .estimated_tx_compressed_size
70            .get_or_init(|| op_alloy_flz::tx_estimated_size_fjord(self.encoded_2718()))
71    }
72
73    /// Returns lazily computed EIP-2718 encoded bytes of the transaction.
74    pub fn encoded_2718(&self) -> &Bytes {
75        self.encoded_2718.get_or_init(|| self.inner.transaction().encoded_2718().into())
76    }
77
78    /// Conditional setter.
79    pub fn with_conditional(mut self, conditional: TransactionConditional) -> Self {
80        self.conditional = Some(Box::new(conditional));
81        self
82    }
83}
84
85impl<Cons, Pooled> MaybeConditionalTransaction for OpPooledTransaction<Cons, Pooled> {
86    fn set_conditional(&mut self, conditional: TransactionConditional) {
87        self.conditional = Some(Box::new(conditional))
88    }
89
90    fn conditional(&self) -> Option<&TransactionConditional> {
91        self.conditional.as_deref()
92    }
93}
94
95impl<Cons, Pooled> MaybeInteropTransaction for OpPooledTransaction<Cons, Pooled> {
96    fn set_interop_deadline(&self, deadline: u64) {
97        self.interop.store(deadline, Ordering::Relaxed);
98    }
99
100    fn interop_deadline(&self) -> Option<u64> {
101        let interop = self.interop.load(Ordering::Relaxed);
102        if interop > NO_INTEROP_TX {
103            return Some(interop)
104        }
105        None
106    }
107}
108
109impl<Cons, Pooled> PoolTransaction for OpPooledTransaction<Cons, Pooled>
110where
111    Cons: SignedTransaction + From<Pooled>,
112    Pooled: SignedTransaction + TryFrom<Cons, Error: core::error::Error>,
113{
114    type TryFromConsensusError = <Pooled as TryFrom<Cons>>::Error;
115    type Consensus = Cons;
116    type Pooled = Pooled;
117
118    fn clone_into_consensus(&self) -> Recovered<Self::Consensus> {
119        self.inner.transaction().clone()
120    }
121
122    fn into_consensus(self) -> Recovered<Self::Consensus> {
123        self.inner.transaction
124    }
125
126    fn from_pooled(tx: Recovered<Self::Pooled>) -> Self {
127        let encoded_len = tx.encode_2718_len();
128        Self::new(tx.convert(), encoded_len)
129    }
130
131    fn hash(&self) -> &TxHash {
132        self.inner.transaction.tx_hash()
133    }
134
135    fn sender(&self) -> Address {
136        self.inner.transaction.signer()
137    }
138
139    fn sender_ref(&self) -> &Address {
140        self.inner.transaction.signer_ref()
141    }
142
143    fn cost(&self) -> &U256 {
144        &self.inner.cost
145    }
146
147    fn encoded_length(&self) -> usize {
148        self.inner.encoded_length
149    }
150}
151
152impl<Cons: Typed2718, Pooled> Typed2718 for OpPooledTransaction<Cons, Pooled> {
153    fn ty(&self) -> u8 {
154        self.inner.ty()
155    }
156}
157
158impl<Cons: InMemorySize, Pooled> InMemorySize for OpPooledTransaction<Cons, Pooled> {
159    fn size(&self) -> usize {
160        self.inner.size()
161    }
162}
163
164impl<Cons, Pooled> alloy_consensus::Transaction for OpPooledTransaction<Cons, Pooled>
165where
166    Cons: alloy_consensus::Transaction,
167    Pooled: Debug + Send + Sync + 'static,
168{
169    fn chain_id(&self) -> Option<u64> {
170        self.inner.chain_id()
171    }
172
173    fn nonce(&self) -> u64 {
174        self.inner.nonce()
175    }
176
177    fn gas_limit(&self) -> u64 {
178        self.inner.gas_limit()
179    }
180
181    fn gas_price(&self) -> Option<u128> {
182        self.inner.gas_price()
183    }
184
185    fn max_fee_per_gas(&self) -> u128 {
186        self.inner.max_fee_per_gas()
187    }
188
189    fn max_priority_fee_per_gas(&self) -> Option<u128> {
190        self.inner.max_priority_fee_per_gas()
191    }
192
193    fn max_fee_per_blob_gas(&self) -> Option<u128> {
194        self.inner.max_fee_per_blob_gas()
195    }
196
197    fn priority_fee_or_price(&self) -> u128 {
198        self.inner.priority_fee_or_price()
199    }
200
201    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
202        self.inner.effective_gas_price(base_fee)
203    }
204
205    fn is_dynamic_fee(&self) -> bool {
206        self.inner.is_dynamic_fee()
207    }
208
209    fn kind(&self) -> TxKind {
210        self.inner.kind()
211    }
212
213    fn is_create(&self) -> bool {
214        self.inner.is_create()
215    }
216
217    fn value(&self) -> U256 {
218        self.inner.value()
219    }
220
221    fn input(&self) -> &Bytes {
222        self.inner.input()
223    }
224
225    fn access_list(&self) -> Option<&AccessList> {
226        self.inner.access_list()
227    }
228
229    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
230        self.inner.blob_versioned_hashes()
231    }
232
233    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
234        self.inner.authorization_list()
235    }
236}
237
238impl<Cons, Pooled> EthPoolTransaction for OpPooledTransaction<Cons, Pooled>
239where
240    Cons: SignedTransaction + From<Pooled>,
241    Pooled: SignedTransaction + TryFrom<Cons>,
242    <Pooled as TryFrom<Cons>>::Error: core::error::Error,
243{
244    fn take_blob(&mut self) -> EthBlobTransactionSidecar {
245        EthBlobTransactionSidecar::None
246    }
247
248    fn try_into_pooled_eip4844(
249        self,
250        _sidecar: Arc<BlobTransactionSidecar>,
251    ) -> Option<Recovered<Self::Pooled>> {
252        None
253    }
254
255    fn try_from_eip4844(
256        _tx: Recovered<Self::Consensus>,
257        _sidecar: BlobTransactionSidecar,
258    ) -> Option<Self> {
259        None
260    }
261
262    fn validate_blob(
263        &self,
264        _sidecar: &BlobTransactionSidecar,
265        _settings: &KzgSettings,
266    ) -> Result<(), BlobTransactionValidationError> {
267        Err(BlobTransactionValidationError::NotBlobTransaction(self.ty()))
268    }
269}
270
271/// Helper trait to provide payload builder with access to conditionals and encoded bytes of
272/// transaction.
273pub trait OpPooledTx:
274    MaybeConditionalTransaction + MaybeInteropTransaction + PoolTransaction
275{
276}
277impl<T> OpPooledTx for T where
278    T: MaybeConditionalTransaction + MaybeInteropTransaction + PoolTransaction
279{
280}
281
282#[cfg(test)]
283mod tests {
284    use crate::{OpPooledTransaction, OpTransactionValidator};
285    use alloy_consensus::transaction::Recovered;
286    use alloy_eips::eip2718::Encodable2718;
287    use alloy_primitives::{TxKind, U256};
288    use op_alloy_consensus::TxDeposit;
289    use reth_optimism_chainspec::OP_MAINNET;
290    use reth_optimism_primitives::OpTransactionSigned;
291    use reth_provider::test_utils::MockEthProvider;
292    use reth_transaction_pool::{
293        blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder, TransactionOrigin,
294        TransactionValidationOutcome,
295    };
296    #[tokio::test]
297    async fn validate_optimism_transaction() {
298        let client = MockEthProvider::default().with_chain_spec(OP_MAINNET.clone());
299        let validator = EthTransactionValidatorBuilder::new(client)
300            .no_shanghai()
301            .no_cancun()
302            .build(InMemoryBlobStore::default());
303        let validator = OpTransactionValidator::new(validator);
304
305        let origin = TransactionOrigin::External;
306        let signer = Default::default();
307        let deposit_tx = TxDeposit {
308            source_hash: Default::default(),
309            from: signer,
310            to: TxKind::Create,
311            mint: None,
312            value: U256::ZERO,
313            gas_limit: 0,
314            is_system_transaction: false,
315            input: Default::default(),
316        };
317        let signed_tx: OpTransactionSigned = deposit_tx.into();
318        let signed_recovered = Recovered::new_unchecked(signed_tx, signer);
319        let len = signed_recovered.encode_2718_len();
320        let pooled_tx: OpPooledTransaction = OpPooledTransaction::new(signed_recovered, len);
321        let outcome = validator.validate_one(origin, pooled_tx).await;
322
323        let err = match outcome {
324            TransactionValidationOutcome::Invalid(_, err) => err,
325            _ => panic!("Expected invalid transaction"),
326        };
327        assert_eq!(err.to_string(), "transaction type not supported");
328    }
329}