op_alloy_consensus/transaction/
pooled.rs

1//! Defines the exact transaction variants that are allowed to be propagated over the eth p2p
2//! protocol in op.
3
4use crate::{OpTxEnvelope, OpTxType};
5use alloy_consensus::{
6    SignableTransaction, Signed, Transaction, TxEip7702, TxEnvelope, Typed2718,
7    error::ValueError,
8    transaction::{RlpEcdsaDecodableTx, TxEip1559, TxEip2930, TxLegacy},
9};
10use alloy_eips::{
11    eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718},
12    eip2930::AccessList,
13    eip7702::SignedAuthorization,
14};
15use alloy_primitives::{B256, Bytes, ChainId, Signature, TxHash, TxKind, U256, bytes};
16use alloy_rlp::{Decodable, Encodable, Header};
17use core::hash::{Hash, Hasher};
18
19/// All possible transactions that can be included in a response to `GetPooledTransactions`.
20/// A response to `GetPooledTransactions`. This can include a typed signed transaction, but cannot
21/// include a deposit transaction or EIP-4844 transaction.
22///
23/// The difference between this and the [`OpTxEnvelope`] is that this type does not have the deposit
24/// transaction variant, which is not expected to be pooled.
25#[derive(Clone, Debug, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27#[cfg_attr(all(any(test, feature = "arbitrary"), feature = "k256"), derive(arbitrary::Arbitrary))]
28pub enum OpPooledTransaction {
29    /// An untagged [`TxLegacy`].
30    Legacy(Signed<TxLegacy>),
31    /// A [`TxEip2930`] transaction tagged with type 1.
32    Eip2930(Signed<TxEip2930>),
33    /// A [`TxEip1559`] transaction tagged with type 2.
34    Eip1559(Signed<TxEip1559>),
35    /// A [`TxEip7702`] transaction tagged with type 4.
36    Eip7702(Signed<TxEip7702>),
37}
38
39impl OpPooledTransaction {
40    /// Heavy operation that returns the signature hash over rlp encoded transaction. It is only
41    /// for signature signing or signer recovery.
42    pub fn signature_hash(&self) -> B256 {
43        match self {
44            Self::Legacy(tx) => tx.signature_hash(),
45            Self::Eip2930(tx) => tx.signature_hash(),
46            Self::Eip1559(tx) => tx.signature_hash(),
47            Self::Eip7702(tx) => tx.signature_hash(),
48        }
49    }
50
51    /// Reference to transaction hash. Used to identify transaction.
52    pub fn hash(&self) -> &TxHash {
53        match self {
54            Self::Legacy(tx) => tx.hash(),
55            Self::Eip2930(tx) => tx.hash(),
56            Self::Eip1559(tx) => tx.hash(),
57            Self::Eip7702(tx) => tx.hash(),
58        }
59    }
60
61    /// Returns the signature of the transaction.
62    pub const fn signature(&self) -> &Signature {
63        match self {
64            Self::Legacy(tx) => tx.signature(),
65            Self::Eip2930(tx) => tx.signature(),
66            Self::Eip1559(tx) => tx.signature(),
67            Self::Eip7702(tx) => tx.signature(),
68        }
69    }
70
71    /// The length of the 2718 encoded envelope in network format. This is the
72    /// length of the header + the length of the type flag and inner encoding.
73    fn network_len(&self) -> usize {
74        let mut payload_length = self.encode_2718_len();
75        if !self.is_legacy() {
76            payload_length += Header { list: false, payload_length }.length();
77        }
78
79        payload_length
80    }
81
82    /// Recover the signer of the transaction.
83    #[cfg(feature = "k256")]
84    pub fn recover_signer(
85        &self,
86    ) -> Result<alloy_primitives::Address, alloy_primitives::SignatureError> {
87        match self {
88            Self::Legacy(tx) => tx.recover_signer(),
89            Self::Eip2930(tx) => tx.recover_signer(),
90            Self::Eip1559(tx) => tx.recover_signer(),
91            Self::Eip7702(tx) => tx.recover_signer(),
92        }
93    }
94
95    /// This encodes the transaction _without_ the signature, and is only suitable for creating a
96    /// hash intended for signing.
97    pub fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) {
98        match self {
99            Self::Legacy(tx) => tx.tx().encode_for_signing(out),
100            Self::Eip2930(tx) => tx.tx().encode_for_signing(out),
101            Self::Eip1559(tx) => tx.tx().encode_for_signing(out),
102            Self::Eip7702(tx) => tx.tx().encode_for_signing(out),
103        }
104    }
105
106    /// Converts the transaction into the ethereum [`TxEnvelope`].
107    pub fn into_envelope(self) -> TxEnvelope {
108        match self {
109            Self::Legacy(tx) => tx.into(),
110            Self::Eip2930(tx) => tx.into(),
111            Self::Eip1559(tx) => tx.into(),
112            Self::Eip7702(tx) => tx.into(),
113        }
114    }
115
116    /// Converts the transaction into the optimism [`OpTxEnvelope`].
117    pub fn into_op_envelope(self) -> OpTxEnvelope {
118        match self {
119            Self::Legacy(tx) => tx.into(),
120            Self::Eip2930(tx) => tx.into(),
121            Self::Eip1559(tx) => tx.into(),
122            Self::Eip7702(tx) => tx.into(),
123        }
124    }
125
126    /// Returns the [`TxLegacy`] variant if the transaction is a legacy transaction.
127    pub const fn as_legacy(&self) -> Option<&TxLegacy> {
128        match self {
129            Self::Legacy(tx) => Some(tx.tx()),
130            _ => None,
131        }
132    }
133
134    /// Returns the [`TxEip2930`] variant if the transaction is an EIP-2930 transaction.
135    pub const fn as_eip2930(&self) -> Option<&TxEip2930> {
136        match self {
137            Self::Eip2930(tx) => Some(tx.tx()),
138            _ => None,
139        }
140    }
141
142    /// Returns the [`TxEip1559`] variant if the transaction is an EIP-1559 transaction.
143    pub const fn as_eip1559(&self) -> Option<&TxEip1559> {
144        match self {
145            Self::Eip1559(tx) => Some(tx.tx()),
146            _ => None,
147        }
148    }
149
150    /// Returns the [`TxEip7702`] variant if the transaction is an EIP-7702 transaction.
151    pub const fn as_eip7702(&self) -> Option<&TxEip7702> {
152        match self {
153            Self::Eip7702(tx) => Some(tx.tx()),
154            _ => None,
155        }
156    }
157}
158
159impl From<Signed<TxLegacy>> for OpPooledTransaction {
160    fn from(v: Signed<TxLegacy>) -> Self {
161        Self::Legacy(v)
162    }
163}
164
165impl From<Signed<TxEip2930>> for OpPooledTransaction {
166    fn from(v: Signed<TxEip2930>) -> Self {
167        Self::Eip2930(v)
168    }
169}
170
171impl From<Signed<TxEip1559>> for OpPooledTransaction {
172    fn from(v: Signed<TxEip1559>) -> Self {
173        Self::Eip1559(v)
174    }
175}
176
177impl From<Signed<TxEip7702>> for OpPooledTransaction {
178    fn from(v: Signed<TxEip7702>) -> Self {
179        Self::Eip7702(v)
180    }
181}
182
183impl From<OpPooledTransaction> for alloy_consensus::transaction::PooledTransaction {
184    fn from(value: OpPooledTransaction) -> Self {
185        match value {
186            OpPooledTransaction::Legacy(tx) => tx.into(),
187            OpPooledTransaction::Eip2930(tx) => tx.into(),
188            OpPooledTransaction::Eip1559(tx) => tx.into(),
189            OpPooledTransaction::Eip7702(tx) => tx.into(),
190        }
191    }
192}
193
194impl Hash for OpPooledTransaction {
195    fn hash<H: Hasher>(&self, state: &mut H) {
196        self.trie_hash().hash(state);
197    }
198}
199
200impl Encodable for OpPooledTransaction {
201    /// This encodes the transaction _with_ the signature, and an rlp header.
202    ///
203    /// For legacy transactions, it encodes the transaction data:
204    /// `rlp(tx-data)`
205    ///
206    /// For EIP-2718 typed transactions, it encodes the transaction type followed by the rlp of the
207    /// transaction:
208    /// `rlp(tx-type || rlp(tx-data))`
209    fn encode(&self, out: &mut dyn bytes::BufMut) {
210        self.network_encode(out);
211    }
212
213    fn length(&self) -> usize {
214        self.network_len()
215    }
216}
217
218impl Decodable for OpPooledTransaction {
219    /// Decodes an enveloped [`OpPooledTransaction`].
220    ///
221    /// CAUTION: this expects that `buf` is `rlp(tx_type || rlp(tx-data))`
222    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
223        Ok(Self::network_decode(buf)?)
224    }
225}
226
227impl Encodable2718 for OpPooledTransaction {
228    fn type_flag(&self) -> Option<u8> {
229        match self {
230            Self::Legacy(_) => None,
231            Self::Eip2930(_) => Some(0x01),
232            Self::Eip1559(_) => Some(0x02),
233            Self::Eip7702(_) => Some(0x04),
234        }
235    }
236
237    fn encode_2718_len(&self) -> usize {
238        match self {
239            Self::Legacy(tx) => tx.eip2718_encoded_length(),
240            Self::Eip2930(tx) => tx.eip2718_encoded_length(),
241            Self::Eip1559(tx) => tx.eip2718_encoded_length(),
242            Self::Eip7702(tx) => tx.eip2718_encoded_length(),
243        }
244    }
245
246    fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
247        match self {
248            Self::Legacy(tx) => tx.eip2718_encode(out),
249            Self::Eip2930(tx) => tx.eip2718_encode(out),
250            Self::Eip1559(tx) => tx.eip2718_encode(out),
251            Self::Eip7702(tx) => tx.eip2718_encode(out),
252        }
253    }
254
255    fn trie_hash(&self) -> B256 {
256        *self.hash()
257    }
258}
259
260impl Decodable2718 for OpPooledTransaction {
261    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
262        match ty.try_into().map_err(|_| alloy_rlp::Error::Custom("unexpected tx type"))? {
263            OpTxType::Eip2930 => Ok(TxEip2930::rlp_decode_signed(buf)?.into()),
264            OpTxType::Eip1559 => Ok(TxEip1559::rlp_decode_signed(buf)?.into()),
265            OpTxType::Eip7702 => Ok(TxEip7702::rlp_decode_signed(buf)?.into()),
266            OpTxType::Legacy => Err(Eip2718Error::UnexpectedType(OpTxType::Legacy.into())),
267            OpTxType::Deposit => Err(Eip2718Error::UnexpectedType(OpTxType::Deposit.into())),
268        }
269    }
270
271    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
272        TxLegacy::rlp_decode_signed(buf).map(Into::into).map_err(Into::into)
273    }
274}
275
276impl Transaction for OpPooledTransaction {
277    fn chain_id(&self) -> Option<ChainId> {
278        match self {
279            Self::Legacy(tx) => tx.tx().chain_id(),
280            Self::Eip2930(tx) => tx.tx().chain_id(),
281            Self::Eip1559(tx) => tx.tx().chain_id(),
282            Self::Eip7702(tx) => tx.tx().chain_id(),
283        }
284    }
285
286    fn nonce(&self) -> u64 {
287        match self {
288            Self::Legacy(tx) => tx.tx().nonce(),
289            Self::Eip2930(tx) => tx.tx().nonce(),
290            Self::Eip1559(tx) => tx.tx().nonce(),
291            Self::Eip7702(tx) => tx.tx().nonce(),
292        }
293    }
294
295    fn gas_limit(&self) -> u64 {
296        match self {
297            Self::Legacy(tx) => tx.tx().gas_limit(),
298            Self::Eip2930(tx) => tx.tx().gas_limit(),
299            Self::Eip1559(tx) => tx.tx().gas_limit(),
300            Self::Eip7702(tx) => tx.tx().gas_limit(),
301        }
302    }
303
304    fn gas_price(&self) -> Option<u128> {
305        match self {
306            Self::Legacy(tx) => tx.tx().gas_price(),
307            Self::Eip2930(tx) => tx.tx().gas_price(),
308            Self::Eip1559(tx) => tx.tx().gas_price(),
309            Self::Eip7702(tx) => tx.tx().gas_price(),
310        }
311    }
312
313    fn max_fee_per_gas(&self) -> u128 {
314        match self {
315            Self::Legacy(tx) => tx.tx().max_fee_per_gas(),
316            Self::Eip2930(tx) => tx.tx().max_fee_per_gas(),
317            Self::Eip1559(tx) => tx.tx().max_fee_per_gas(),
318            Self::Eip7702(tx) => tx.tx().max_fee_per_gas(),
319        }
320    }
321
322    fn max_priority_fee_per_gas(&self) -> Option<u128> {
323        match self {
324            Self::Legacy(tx) => tx.tx().max_priority_fee_per_gas(),
325            Self::Eip2930(tx) => tx.tx().max_priority_fee_per_gas(),
326            Self::Eip1559(tx) => tx.tx().max_priority_fee_per_gas(),
327            Self::Eip7702(tx) => tx.tx().max_priority_fee_per_gas(),
328        }
329    }
330
331    fn max_fee_per_blob_gas(&self) -> Option<u128> {
332        match self {
333            Self::Legacy(tx) => tx.tx().max_fee_per_blob_gas(),
334            Self::Eip2930(tx) => tx.tx().max_fee_per_blob_gas(),
335            Self::Eip1559(tx) => tx.tx().max_fee_per_blob_gas(),
336            Self::Eip7702(tx) => tx.tx().max_fee_per_blob_gas(),
337        }
338    }
339
340    fn priority_fee_or_price(&self) -> u128 {
341        match self {
342            Self::Legacy(tx) => tx.tx().priority_fee_or_price(),
343            Self::Eip2930(tx) => tx.tx().priority_fee_or_price(),
344            Self::Eip1559(tx) => tx.tx().priority_fee_or_price(),
345            Self::Eip7702(tx) => tx.tx().priority_fee_or_price(),
346        }
347    }
348
349    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
350        match self {
351            Self::Legacy(tx) => tx.tx().effective_gas_price(base_fee),
352            Self::Eip2930(tx) => tx.tx().effective_gas_price(base_fee),
353            Self::Eip1559(tx) => tx.tx().effective_gas_price(base_fee),
354            Self::Eip7702(tx) => tx.tx().effective_gas_price(base_fee),
355        }
356    }
357
358    fn is_dynamic_fee(&self) -> bool {
359        match self {
360            Self::Legacy(tx) => tx.tx().is_dynamic_fee(),
361            Self::Eip2930(tx) => tx.tx().is_dynamic_fee(),
362            Self::Eip1559(tx) => tx.tx().is_dynamic_fee(),
363            Self::Eip7702(tx) => tx.tx().is_dynamic_fee(),
364        }
365    }
366
367    fn kind(&self) -> TxKind {
368        match self {
369            Self::Legacy(tx) => tx.tx().kind(),
370            Self::Eip2930(tx) => tx.tx().kind(),
371            Self::Eip1559(tx) => tx.tx().kind(),
372            Self::Eip7702(tx) => tx.tx().kind(),
373        }
374    }
375
376    fn is_create(&self) -> bool {
377        match self {
378            Self::Legacy(tx) => tx.tx().is_create(),
379            Self::Eip2930(tx) => tx.tx().is_create(),
380            Self::Eip1559(tx) => tx.tx().is_create(),
381            Self::Eip7702(tx) => tx.tx().is_create(),
382        }
383    }
384
385    fn value(&self) -> U256 {
386        match self {
387            Self::Legacy(tx) => tx.tx().value(),
388            Self::Eip2930(tx) => tx.tx().value(),
389            Self::Eip1559(tx) => tx.tx().value(),
390            Self::Eip7702(tx) => tx.tx().value(),
391        }
392    }
393
394    fn input(&self) -> &Bytes {
395        match self {
396            Self::Legacy(tx) => tx.tx().input(),
397            Self::Eip2930(tx) => tx.tx().input(),
398            Self::Eip1559(tx) => tx.tx().input(),
399            Self::Eip7702(tx) => tx.tx().input(),
400        }
401    }
402
403    fn access_list(&self) -> Option<&AccessList> {
404        match self {
405            Self::Legacy(tx) => tx.tx().access_list(),
406            Self::Eip2930(tx) => tx.tx().access_list(),
407            Self::Eip1559(tx) => tx.tx().access_list(),
408            Self::Eip7702(tx) => tx.tx().access_list(),
409        }
410    }
411
412    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
413        match self {
414            Self::Legacy(tx) => tx.tx().blob_versioned_hashes(),
415            Self::Eip2930(tx) => tx.tx().blob_versioned_hashes(),
416            Self::Eip1559(tx) => tx.tx().blob_versioned_hashes(),
417            Self::Eip7702(tx) => tx.tx().blob_versioned_hashes(),
418        }
419    }
420
421    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
422        match self {
423            Self::Legacy(tx) => tx.tx().authorization_list(),
424            Self::Eip2930(tx) => tx.tx().authorization_list(),
425            Self::Eip1559(tx) => tx.tx().authorization_list(),
426            Self::Eip7702(tx) => tx.tx().authorization_list(),
427        }
428    }
429}
430
431impl Typed2718 for OpPooledTransaction {
432    fn ty(&self) -> u8 {
433        match self {
434            Self::Legacy(tx) => tx.tx().ty(),
435            Self::Eip2930(tx) => tx.tx().ty(),
436            Self::Eip1559(tx) => tx.tx().ty(),
437            Self::Eip7702(tx) => tx.tx().ty(),
438        }
439    }
440}
441
442impl From<OpPooledTransaction> for TxEnvelope {
443    fn from(tx: OpPooledTransaction) -> Self {
444        tx.into_envelope()
445    }
446}
447
448impl From<OpPooledTransaction> for OpTxEnvelope {
449    fn from(tx: OpPooledTransaction) -> Self {
450        tx.into_op_envelope()
451    }
452}
453
454impl TryFrom<OpTxEnvelope> for OpPooledTransaction {
455    type Error = ValueError<OpTxEnvelope>;
456
457    fn try_from(value: OpTxEnvelope) -> Result<Self, Self::Error> {
458        value.try_into_pooled()
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465    use alloy_primitives::{address, hex};
466    use bytes::Bytes;
467
468    #[test]
469    fn invalid_legacy_pooled_decoding_input_too_short() {
470        let input_too_short = [
471            // this should fail because the payload length is longer than expected
472            &hex!("d90b0280808bc5cd028083c5cdfd9e407c56565656")[..],
473            // these should fail decoding
474            //
475            // The `c1` at the beginning is a list header, and the rest is a valid legacy
476            // transaction, BUT the payload length of the list header is 1, and the payload is
477            // obviously longer than one byte.
478            &hex!("c10b02808083c5cd028883c5cdfd9e407c56565656"),
479            &hex!("c10b0280808bc5cd028083c5cdfd9e407c56565656"),
480            // this one is 19 bytes, and the buf is long enough, but the transaction will not
481            // consume that many bytes.
482            &hex!("d40b02808083c5cdeb8783c5acfd9e407c5656565656"),
483            &hex!("d30102808083c5cd02887dc5cdfd9e64fd9e407c56"),
484        ];
485
486        for hex_data in &input_too_short {
487            let input_rlp = &mut &hex_data[..];
488            let res = OpPooledTransaction::decode(input_rlp);
489
490            assert!(
491                res.is_err(),
492                "expected err after decoding rlp input: {:x?}",
493                Bytes::copy_from_slice(hex_data)
494            );
495
496            // this is a legacy tx so we can attempt the same test with decode_enveloped
497            let input_rlp = &mut &hex_data[..];
498            let res = OpPooledTransaction::decode_2718(input_rlp);
499
500            assert!(
501                res.is_err(),
502                "expected err after decoding enveloped rlp input: {:x?}",
503                Bytes::copy_from_slice(hex_data)
504            );
505        }
506    }
507
508    // <https://holesky.etherscan.io/tx/0x7f60faf8a410a80d95f7ffda301d5ab983545913d3d789615df3346579f6c849>
509    #[test]
510    fn decode_eip1559_enveloped() {
511        let data = hex!(
512            "02f903d382426882ba09832dc6c0848674742682ed9694714b6a4ea9b94a8a7d9fd362ed72630688c8898c80b90364492d24749189822d8512430d3f3ff7a2ede675ac08265c08e2c56ff6fdaa66dae1cdbe4a5d1d7809f3e99272d067364e597542ac0c369d69e22a6399c3e9bee5da4b07e3f3fdc34c32c3d88aa2268785f3e3f8086df0934b10ef92cfffc2e7f3d90f5e83302e31382e302d64657600000000000000000000000000000000000000000000569e75fc77c1a856f6daaf9e69d8a9566ca34aa47f9133711ce065a571af0cfd000000000000000000000000e1e210594771824dad216568b91c9cb4ceed361c00000000000000000000000000000000000000000000000000000000000546e00000000000000000000000000000000000000000000000000000000000e4e1c00000000000000000000000000000000000000000000000000000000065d6750c00000000000000000000000000000000000000000000000000000000000f288000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cf600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000f1628e56fa6d8c50e5b984a58c0df14de31c7b857ce7ba499945b99252976a93d06dcda6776fc42167fbe71cb59f978f5ef5b12577a90b132d14d9c6efa528076f0161d7bf03643cfc5490ec5084f4a041db7f06c50bd97efa08907ba79ddcac8b890f24d12d8db31abbaaf18985d54f400449ee0559a4452afe53de5853ce090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000c080a01428023fc54a27544abc421d5d017b9a7c5936ad501cbdecd0d9d12d04c1a033a0753104bbf1c87634d6ff3f0ffa0982710612306003eb022363b57994bdef445a"
513        );
514
515        let res = OpPooledTransaction::decode_2718(&mut &data[..]).unwrap();
516        assert_eq!(res.to(), Some(address!("714b6a4ea9b94a8a7d9fd362ed72630688c8898c")));
517    }
518
519    #[test]
520    fn legacy_valid_pooled_decoding() {
521        // d3 <- payload length, d3 - c0 = 0x13 = 19
522        // 0b <- nonce
523        // 02 <- gas_price
524        // 80 <- gas_limit
525        // 80 <- to (Create)
526        // 83 c5cdeb <- value
527        // 87 83c5acfd9e407c <- input
528        // 56 <- v (eip155, so modified with a chain id)
529        // 56 <- r
530        // 56 <- s
531        let data = &hex!("d30b02808083c5cdeb8783c5acfd9e407c565656")[..];
532
533        let input_rlp = &mut &data[..];
534        let res = OpPooledTransaction::decode(input_rlp);
535        assert!(res.is_ok());
536        assert!(input_rlp.is_empty());
537
538        // we can also decode_enveloped
539        let res = OpPooledTransaction::decode_2718(&mut &data[..]);
540        assert!(res.is_ok());
541    }
542}