Skip to main content

alloy_consensus/transaction/
pooled.rs

1//! Defines the exact transaction variant that is allowed to be propagated over the eth p2p
2//! protocol.
3
4use super::EthereumTxEnvelope;
5use crate::{error::ValueError, Signed, TxEip4844, TxEip4844Variant, TxEip4844WithSidecar};
6use alloy_eips::eip7594::{
7    BlobTransactionSidecarEip7594, BlobTransactionSidecarVariant, Encodable7594,
8};
9
10/// Pooled transaction format for Osaka and later.
11///
12/// This can contain an [EIP-7594] blob transaction with its cell-proof sidecar or any non-blob
13/// signed Ethereum transaction. For cross-fork [EIP-4844]/[EIP-7594] handling, use
14/// `EthereumTxEnvelope<TxEip4844WithSidecar<BlobTransactionSidecarVariant>>` instead.
15///
16/// The difference between this and the [`EthereumTxEnvelope<TxEip4844Variant<T>>`] is that this
17/// type always requires the [`TxEip4844WithSidecar`] variant, because EIP-4844 transaction can only
18/// be propagated with the sidecar over p2p.
19///
20/// After the Osaka upgrade (EIP-7594), the blob sidecar uses
21/// [`BlobTransactionSidecarEip7594`] which replaces single KZG proofs with cell proofs
22/// for PeerDAS data availability sampling.
23///
24/// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844
25/// [EIP-7594]: https://eips.ethereum.org/EIPS/eip-7594
26pub type PooledTransaction =
27    EthereumTxEnvelope<TxEip4844WithSidecar<BlobTransactionSidecarEip7594>>;
28
29impl EthereumTxEnvelope<TxEip4844WithSidecar<BlobTransactionSidecarEip7594>> {
30    /// Clears EIP-7594 blob payloads while retaining commitments and cell proofs.
31    ///
32    /// This prepares the transaction for inclusion in an eth/72 `PooledTransactions` response as
33    /// specified by [EIP-8070]. This has no effect on non-blob transactions.
34    ///
35    /// [EIP-8070]: https://eips.ethereum.org/EIPS/eip-8070
36    pub fn clear_eip7594_blobs(&mut self) {
37        if let Self::Eip4844(tx) = self {
38            tx.tx_mut().sidecar.clear_eip7594_blobs();
39        }
40    }
41}
42
43impl EthereumTxEnvelope<TxEip4844WithSidecar<BlobTransactionSidecarVariant>> {
44    /// Clears EIP-7594 blob payloads while retaining commitments and cell proofs.
45    ///
46    /// This prepares the transaction for inclusion in an eth/72 `PooledTransactions` response as
47    /// specified by [EIP-8070]. This has no effect on non-blob transactions or EIP-4844 sidecars.
48    ///
49    /// [EIP-8070]: https://eips.ethereum.org/EIPS/eip-8070
50    pub fn clear_eip7594_blobs(&mut self) {
51        if let Self::Eip4844(tx) = self {
52            tx.tx_mut().sidecar.clear_eip7594_blobs();
53        }
54    }
55}
56
57impl<T: Encodable7594> EthereumTxEnvelope<TxEip4844WithSidecar<T>> {
58    /// Converts the transaction into [`EthereumTxEnvelope<TxEip4844Variant<T>>`].
59    pub fn into_envelope(self) -> EthereumTxEnvelope<TxEip4844Variant<T>> {
60        match self {
61            Self::Legacy(tx) => tx.into(),
62            Self::Eip2930(tx) => tx.into(),
63            Self::Eip1559(tx) => tx.into(),
64            Self::Eip7702(tx) => tx.into(),
65            Self::Eip4844(tx) => tx.into(),
66        }
67    }
68}
69
70impl<T: Encodable7594> TryFrom<Signed<TxEip4844Variant<T>>>
71    for EthereumTxEnvelope<TxEip4844WithSidecar<T>>
72{
73    type Error = ValueError<Signed<TxEip4844Variant<T>>>;
74
75    fn try_from(value: Signed<TxEip4844Variant<T>>) -> Result<Self, Self::Error> {
76        let (value, signature, hash) = value.into_parts();
77        match value {
78            tx @ TxEip4844Variant::TxEip4844(_) => Err(ValueError::new_static(
79                Signed::new_unchecked(tx, signature, hash),
80                "pooled transaction requires 4844 sidecar",
81            )),
82            TxEip4844Variant::TxEip4844WithSidecar(tx) => {
83                Ok(Signed::new_unchecked(tx, signature, hash).into())
84            }
85        }
86    }
87}
88
89impl<T: Encodable7594> TryFrom<EthereumTxEnvelope<TxEip4844Variant<T>>>
90    for EthereumTxEnvelope<TxEip4844WithSidecar<T>>
91{
92    type Error = ValueError<EthereumTxEnvelope<TxEip4844Variant<T>>>;
93
94    fn try_from(value: EthereumTxEnvelope<TxEip4844Variant<T>>) -> Result<Self, Self::Error> {
95        value.try_into_pooled()
96    }
97}
98
99impl<T: Encodable7594> TryFrom<EthereumTxEnvelope<TxEip4844>>
100    for EthereumTxEnvelope<TxEip4844WithSidecar<T>>
101{
102    type Error = ValueError<EthereumTxEnvelope<TxEip4844>>;
103
104    fn try_from(value: EthereumTxEnvelope<TxEip4844>) -> Result<Self, Self::Error> {
105        value.try_into_pooled()
106    }
107}
108
109impl<T: Encodable7594> From<EthereumTxEnvelope<TxEip4844WithSidecar<T>>>
110    for EthereumTxEnvelope<TxEip4844Variant<T>>
111{
112    fn from(tx: EthereumTxEnvelope<TxEip4844WithSidecar<T>>) -> Self {
113        tx.into_envelope()
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::Transaction;
121    use alloy_eips::{
122        eip4844::{Blob, Bytes48},
123        eip7594::CELLS_PER_EXT_BLOB,
124        Decodable2718, Encodable2718,
125    };
126    use alloy_primitives::{address, hex, Bytes, Signature, U256};
127    use alloy_rlp::Decodable;
128    use std::path::PathBuf;
129
130    fn eip7594_sidecar() -> BlobTransactionSidecarEip7594 {
131        BlobTransactionSidecarEip7594::new(
132            vec![Blob::repeat_byte(0x01)],
133            vec![Bytes48::repeat_byte(0x02)],
134            vec![Bytes48::repeat_byte(0x03); CELLS_PER_EXT_BLOB],
135        )
136    }
137
138    fn signature() -> Signature {
139        Signature::new(U256::from(1), U256::from(2), false)
140    }
141
142    #[test]
143    fn clear_eip7594_blobs_from_pooled_envelopes() {
144        let sidecar = eip7594_sidecar();
145        let commitments = sidecar.commitments.clone();
146        let cell_proofs = sidecar.cell_proofs.clone();
147        let tx = TxEip4844WithSidecar::from_tx_and_sidecar(TxEip4844::default(), sidecar);
148        let mut pooled: PooledTransaction = Signed::new_unhashed(tx, signature()).into();
149
150        pooled.clear_eip7594_blobs();
151
152        let sidecar = &pooled.as_eip4844().unwrap().tx().sidecar;
153        assert!(sidecar.blobs.is_empty());
154        assert_eq!(sidecar.commitments, commitments);
155        assert_eq!(sidecar.cell_proofs, cell_proofs);
156
157        let tx = TxEip4844WithSidecar::from_tx_and_sidecar(
158            TxEip4844::default(),
159            BlobTransactionSidecarVariant::Eip7594(eip7594_sidecar()),
160        );
161        let mut pooled = EthereumTxEnvelope::Eip4844(Signed::new_unhashed(tx, signature()));
162
163        pooled.clear_eip7594_blobs();
164
165        let sidecar = pooled.as_eip4844().unwrap().tx().sidecar.as_eip7594().unwrap();
166        assert!(sidecar.blobs.is_empty());
167        assert_eq!(sidecar.commitments, commitments);
168        assert_eq!(sidecar.cell_proofs, cell_proofs);
169    }
170
171    #[test]
172    fn invalid_legacy_pooled_decoding_input_too_short() {
173        let input_too_short = [
174            // this should fail because the payload length is longer than expected
175            &hex!("d90b0280808bc5cd028083c5cdfd9e407c56565656")[..],
176            // these should fail decoding
177            //
178            // The `c1` at the beginning is a list header, and the rest is a valid legacy
179            // transaction, BUT the payload length of the list header is 1, and the payload is
180            // obviously longer than one byte.
181            &hex!("c10b02808083c5cd028883c5cdfd9e407c56565656"),
182            &hex!("c10b0280808bc5cd028083c5cdfd9e407c56565656"),
183            // this one is 19 bytes, and the buf is long enough, but the transaction will not
184            // consume that many bytes.
185            &hex!("d40b02808083c5cdeb8783c5acfd9e407c5656565656"),
186            &hex!("d30102808083c5cd02887dc5cdfd9e64fd9e407c56"),
187        ];
188
189        for hex_data in &input_too_short {
190            let input_rlp = &mut &hex_data[..];
191            let res = PooledTransaction::decode(input_rlp);
192
193            assert!(
194                res.is_err(),
195                "expected err after decoding rlp input: {:x?}",
196                Bytes::copy_from_slice(hex_data)
197            );
198
199            // this is a legacy tx so we can attempt the same test with decode_enveloped
200            let input_rlp = &mut &hex_data[..];
201            let res = PooledTransaction::decode_2718(input_rlp);
202
203            assert!(
204                res.is_err(),
205                "expected err after decoding enveloped rlp input: {:x?}",
206                Bytes::copy_from_slice(hex_data)
207            );
208        }
209    }
210
211    // <https://holesky.etherscan.io/tx/0x7f60faf8a410a80d95f7ffda301d5ab983545913d3d789615df3346579f6c849>
212    #[test]
213    fn decode_eip1559_enveloped() {
214        let data = hex!("02f903d382426882ba09832dc6c0848674742682ed9694714b6a4ea9b94a8a7d9fd362ed72630688c8898c80b90364492d24749189822d8512430d3f3ff7a2ede675ac08265c08e2c56ff6fdaa66dae1cdbe4a5d1d7809f3e99272d067364e597542ac0c369d69e22a6399c3e9bee5da4b07e3f3fdc34c32c3d88aa2268785f3e3f8086df0934b10ef92cfffc2e7f3d90f5e83302e31382e302d64657600000000000000000000000000000000000000000000569e75fc77c1a856f6daaf9e69d8a9566ca34aa47f9133711ce065a571af0cfd000000000000000000000000e1e210594771824dad216568b91c9cb4ceed361c00000000000000000000000000000000000000000000000000000000000546e00000000000000000000000000000000000000000000000000000000000e4e1c00000000000000000000000000000000000000000000000000000000065d6750c00000000000000000000000000000000000000000000000000000000000f288000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cf600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000f1628e56fa6d8c50e5b984a58c0df14de31c7b857ce7ba499945b99252976a93d06dcda6776fc42167fbe71cb59f978f5ef5b12577a90b132d14d9c6efa528076f0161d7bf03643cfc5490ec5084f4a041db7f06c50bd97efa08907ba79ddcac8b890f24d12d8db31abbaaf18985d54f400449ee0559a4452afe53de5853ce090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000c080a01428023fc54a27544abc421d5d017b9a7c5936ad501cbdecd0d9d12d04c1a033a0753104bbf1c87634d6ff3f0ffa0982710612306003eb022363b57994bdef445a"
215);
216
217        let res = PooledTransaction::decode_2718(&mut &data[..]).unwrap();
218        assert_eq!(res.to(), Some(address!("714b6a4ea9b94a8a7d9fd362ed72630688c8898c")));
219    }
220
221    #[test]
222    fn legacy_valid_pooled_decoding() {
223        // d3 <- payload length, d3 - c0 = 0x13 = 19
224        // 0b <- nonce
225        // 02 <- gas_price
226        // 80 <- gas_limit
227        // 80 <- to (Create)
228        // 83 c5cdeb <- value
229        // 87 83c5acfd9e407c <- input
230        // 56 <- v (eip155, so modified with a chain id)
231        // 56 <- r
232        // 56 <- s
233        let data = &hex!("d30b02808083c5cdeb8783c5acfd9e407c565656")[..];
234
235        let input_rlp = &mut &data[..];
236        let res = PooledTransaction::decode(input_rlp);
237        assert!(res.is_ok());
238        assert!(input_rlp.is_empty());
239
240        // we can also decode_enveloped
241        let res = PooledTransaction::decode_2718(&mut &data[..]);
242        assert!(res.is_ok());
243    }
244
245    #[test]
246    fn decode_encode_raw_4844_rlp() {
247        // Test data is in legacy EIP-4844 RLP format, so use BlobTransactionSidecarVariant
248        // which can decode both EIP-4844 and EIP-7594.
249        type VariantPooledTransaction = EthereumTxEnvelope<TxEip4844WithSidecar>;
250
251        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/4844rlp");
252        let dir = std::fs::read_dir(path).expect("Unable to read folder");
253        for entry in dir {
254            let entry = entry.unwrap();
255            let content = std::fs::read_to_string(entry.path()).unwrap();
256            let raw = hex::decode(content.trim()).unwrap();
257            let tx = VariantPooledTransaction::decode_2718(&mut raw.as_ref())
258                .map_err(|err| {
259                    panic!("Failed to decode transaction: {:?} {:?}", err, entry.path());
260                })
261                .unwrap();
262            // We want to test only EIP-4844 transactions
263            assert!(tx.is_eip4844());
264            let encoded = tx.encoded_2718();
265            assert_eq!(encoded.as_slice(), &raw[..], "{:?}", entry.path());
266        }
267    }
268
269    #[test]
270    #[cfg(feature = "kzg")]
271    fn convert_to_eip7594() {
272        // Test data is in legacy EIP-4844 RLP format, so use BlobTransactionSidecarVariant
273        // which can decode both EIP-4844 and EIP-7594.
274        type VariantPooledTransaction = EthereumTxEnvelope<TxEip4844WithSidecar>;
275
276        let kzg_settings = alloy_eips::eip4844::env_settings::EnvKzgSettings::default();
277        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/4844rlp");
278        let dir = std::fs::read_dir(path).expect("Unable to read folder");
279        for entry in dir {
280            let entry = entry.unwrap();
281            let content = std::fs::read_to_string(entry.path()).unwrap();
282            let raw = hex::decode(content.trim()).unwrap();
283            let VariantPooledTransaction::Eip4844(tx) =
284                VariantPooledTransaction::decode_2718(&mut raw.as_ref())
285                    .map_err(|err| {
286                        panic!("Failed to decode transaction: {:?} {:?}", err, entry.path());
287                    })
288                    .unwrap()
289            else {
290                panic!("Expected EIP-4844 transaction");
291            };
292            let tx = tx.into_parts().0;
293            assert!(!tx.sidecar.blobs().is_empty());
294            assert!(tx.validate_blob(kzg_settings.get()).is_ok());
295
296            let tx = tx
297                .try_map_sidecar(|sidecar| {
298                    sidecar.try_convert_into_eip7594_with_settings(kzg_settings.get())
299                })
300                .unwrap();
301
302            assert!(!tx.sidecar.blobs().is_empty());
303            assert!(tx.validate_blob(kzg_settings.get()).is_ok());
304        }
305    }
306
307    /// Tests that `PooledTransaction` (with [`BlobTransactionSidecarEip7594`]) can encode and
308    /// decode EIP-7594 blob transactions round-trip.
309    #[test]
310    #[cfg(feature = "kzg")]
311    fn pooled_transaction_eip7594_roundtrip() {
312        // Decode legacy 4844 test data, convert sidecar to EIP-7594, then verify
313        // PooledTransaction roundtrips correctly with the new sidecar format.
314        type VariantPooledTransaction = EthereumTxEnvelope<TxEip4844WithSidecar>;
315
316        let kzg_settings = alloy_eips::eip4844::env_settings::EnvKzgSettings::default();
317        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/4844rlp");
318        let dir = std::fs::read_dir(path).expect("Unable to read folder");
319        for entry in dir {
320            let entry = entry.unwrap();
321            let content = std::fs::read_to_string(entry.path()).unwrap();
322            let raw = hex::decode(content.trim()).unwrap();
323            let VariantPooledTransaction::Eip4844(tx) =
324                VariantPooledTransaction::decode_2718(&mut raw.as_ref())
325                    .map_err(|err| {
326                        panic!("Failed to decode transaction: {:?} {:?}", err, entry.path());
327                    })
328                    .unwrap()
329            else {
330                panic!("Expected EIP-4844 transaction");
331            };
332
333            // Convert the sidecar from EIP-4844 to EIP-7594
334            let (tx_with_sidecar, sig, hash) = tx.into_parts();
335            let tx_eip7594 = tx_with_sidecar
336                .try_map_sidecar(|sidecar| {
337                    sidecar.try_into_eip7594_with_settings(kzg_settings.get())
338                })
339                .unwrap();
340
341            // Build a PooledTransaction (EIP-7594) and roundtrip encode/decode
342            let pooled_tx: PooledTransaction = Signed::new_unchecked(tx_eip7594, sig, hash).into();
343            assert!(pooled_tx.is_eip4844());
344
345            let encoded = pooled_tx.encoded_2718();
346            let decoded = PooledTransaction::decode_2718(&mut encoded.as_ref()).unwrap();
347            assert_eq!(pooled_tx, decoded);
348        }
349    }
350}