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
29pub(crate) const NO_INTEROP_TX: u64 = 0;
31
32#[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 estimated_tx_compressed_size: OnceLock<u64>,
46 _pd: core::marker::PhantomData<Pooled>,
48
49 conditional: Option<Box<TransactionConditional>>,
51
52 interop: Arc<AtomicU64>,
54
55 encoded_2718: OnceLock<Bytes>,
57}
58
59impl<Cons: SignedTransaction, Pooled> OpPooledTransaction<Cons, Pooled> {
60 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 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 pub fn encoded_2718(&self) -> &Bytes {
84 self.encoded_2718.get_or_init(|| self.inner.transaction().encoded_2718().into())
85 }
86
87 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
291pub trait OpPooledTx:
294 MaybeConditionalTransaction + MaybeInteropTransaction + PoolTransaction + DataAvailabilitySized
295{
296 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}