reth_optimism_txpool/
transaction.rs

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