reth_ethereum_primitives/
receipt.rs

1use alloc::vec::Vec;
2use alloy_consensus::{
3    Eip2718EncodableReceipt, Eip658Value, ReceiptWithBloom, RlpDecodableReceipt,
4    RlpEncodableReceipt, TxReceipt, TxType, Typed2718,
5};
6use alloy_eips::eip2718::Encodable2718;
7use alloy_primitives::{Bloom, Log, B256};
8use alloy_rlp::{BufMut, Decodable, Encodable, Header};
9use reth_primitives_traits::{proofs::ordered_trie_root_with_encoder, InMemorySize};
10use serde::{Deserialize, Serialize};
11
12/// Typed ethereum transaction receipt.
13/// Receipt containing result of transaction execution.
14#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
16#[cfg_attr(feature = "reth-codec", derive(reth_codecs::CompactZstd))]
17#[cfg_attr(feature = "reth-codec", reth_codecs::add_arbitrary_tests)]
18#[cfg_attr(feature = "reth-codec", reth_zstd(
19    compressor = reth_zstd_compressors::RECEIPT_COMPRESSOR,
20    decompressor = reth_zstd_compressors::RECEIPT_DECOMPRESSOR
21))]
22pub struct Receipt {
23    /// Receipt type.
24    pub tx_type: TxType,
25    /// If transaction is executed successfully.
26    ///
27    /// This is the `statusCode`
28    pub success: bool,
29    /// Gas used
30    pub cumulative_gas_used: u64,
31    /// Log send from contracts.
32    pub logs: Vec<Log>,
33}
34
35impl Receipt {
36    /// Returns length of RLP-encoded receipt fields with the given [`Bloom`] without an RLP header.
37    pub fn rlp_encoded_fields_length(&self, bloom: &Bloom) -> usize {
38        self.success.length() +
39            self.cumulative_gas_used.length() +
40            bloom.length() +
41            self.logs.length()
42    }
43
44    /// RLP-encodes receipt fields with the given [`Bloom`] without an RLP header.
45    pub fn rlp_encode_fields(&self, bloom: &Bloom, out: &mut dyn BufMut) {
46        self.success.encode(out);
47        self.cumulative_gas_used.encode(out);
48        bloom.encode(out);
49        self.logs.encode(out);
50    }
51
52    /// Returns RLP header for inner encoding.
53    pub fn rlp_header_inner(&self, bloom: &Bloom) -> Header {
54        Header { list: true, payload_length: self.rlp_encoded_fields_length(bloom) }
55    }
56
57    /// RLP-decodes the receipt from the provided buffer. This does not expect a type byte or
58    /// network header.
59    pub fn rlp_decode_inner(
60        buf: &mut &[u8],
61        tx_type: TxType,
62    ) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
63        let header = Header::decode(buf)?;
64        if !header.list {
65            return Err(alloy_rlp::Error::UnexpectedString);
66        }
67
68        let remaining = buf.len();
69
70        let success = Decodable::decode(buf)?;
71        let cumulative_gas_used = Decodable::decode(buf)?;
72        let logs_bloom = Decodable::decode(buf)?;
73        let logs = Decodable::decode(buf)?;
74
75        if buf.len() + header.payload_length != remaining {
76            return Err(alloy_rlp::Error::UnexpectedLength);
77        }
78
79        Ok(ReceiptWithBloom {
80            receipt: Self { cumulative_gas_used, tx_type, success, logs },
81            logs_bloom,
82        })
83    }
84
85    /// Calculates the receipt root for a header for the reference type of [Receipt].
86    ///
87    /// NOTE: Prefer `proofs::calculate_receipt_root` if you have log blooms memoized.
88    pub fn calculate_receipt_root_no_memo(receipts: &[Self]) -> B256 {
89        ordered_trie_root_with_encoder(receipts, |r, buf| r.with_bloom_ref().encode_2718(buf))
90    }
91}
92
93impl Eip2718EncodableReceipt for Receipt {
94    fn eip2718_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
95        !self.tx_type.is_legacy() as usize + self.rlp_header_inner(bloom).length_with_payload()
96    }
97
98    fn eip2718_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
99        if !self.tx_type.is_legacy() {
100            out.put_u8(self.tx_type as u8);
101        }
102        self.rlp_header_inner(bloom).encode(out);
103        self.rlp_encode_fields(bloom, out);
104    }
105}
106
107impl RlpEncodableReceipt for Receipt {
108    fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
109        let mut len = self.eip2718_encoded_length_with_bloom(bloom);
110        if !self.tx_type.is_legacy() {
111            len += Header {
112                list: false,
113                payload_length: self.eip2718_encoded_length_with_bloom(bloom),
114            }
115            .length();
116        }
117
118        len
119    }
120
121    fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
122        if !self.tx_type.is_legacy() {
123            Header { list: false, payload_length: self.eip2718_encoded_length_with_bloom(bloom) }
124                .encode(out);
125        }
126        self.eip2718_encode_with_bloom(bloom, out);
127    }
128}
129
130impl RlpDecodableReceipt for Receipt {
131    fn rlp_decode_with_bloom(buf: &mut &[u8]) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
132        let header_buf = &mut &**buf;
133        let header = Header::decode(header_buf)?;
134
135        // Legacy receipt, reuse initial buffer without advancing
136        if header.list {
137            return Self::rlp_decode_inner(buf, TxType::Legacy)
138        }
139
140        // Otherwise, advance the buffer and try decoding type flag followed by receipt
141        *buf = *header_buf;
142
143        let remaining = buf.len();
144        let tx_type = TxType::decode(buf)?;
145        let this = Self::rlp_decode_inner(buf, tx_type)?;
146
147        if buf.len() + header.payload_length != remaining {
148            return Err(alloy_rlp::Error::UnexpectedLength);
149        }
150
151        Ok(this)
152    }
153}
154
155impl TxReceipt for Receipt {
156    type Log = Log;
157
158    fn status_or_post_state(&self) -> Eip658Value {
159        self.success.into()
160    }
161
162    fn status(&self) -> bool {
163        self.success
164    }
165
166    fn bloom(&self) -> Bloom {
167        alloy_primitives::logs_bloom(self.logs())
168    }
169
170    fn cumulative_gas_used(&self) -> u64 {
171        self.cumulative_gas_used
172    }
173
174    fn logs(&self) -> &[Log] {
175        &self.logs
176    }
177}
178
179impl Typed2718 for Receipt {
180    fn ty(&self) -> u8 {
181        self.tx_type as u8
182    }
183}
184
185impl InMemorySize for Receipt {
186    fn size(&self) -> usize {
187        self.tx_type.size() +
188            core::mem::size_of::<bool>() +
189            core::mem::size_of::<u64>() +
190            self.logs.capacity() * core::mem::size_of::<Log>()
191    }
192}
193
194impl reth_primitives_traits::Receipt for Receipt {}
195
196#[cfg(feature = "serde-bincode-compat")]
197pub(super) mod serde_bincode_compat {
198    use alloc::{borrow::Cow, vec::Vec};
199    use alloy_consensus::TxType;
200    use alloy_primitives::{Log, U8};
201    use serde::{Deserialize, Deserializer, Serialize, Serializer};
202    use serde_with::{DeserializeAs, SerializeAs};
203
204    /// Bincode-compatible [`super::Receipt`] serde implementation.
205    ///
206    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
207    /// ```rust
208    /// use reth_ethereum_primitives::{serde_bincode_compat, Receipt};
209    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
210    /// use serde_with::serde_as;
211    ///
212    /// #[serde_as]
213    /// #[derive(Serialize, Deserialize)]
214    /// struct Data {
215    ///     #[serde_as(as = "serde_bincode_compat::Receipt<'_>")]
216    ///     receipt: Receipt,
217    /// }
218    /// ```
219    #[derive(Debug, Serialize, Deserialize)]
220    pub struct Receipt<'a> {
221        /// Receipt type.
222        #[serde(deserialize_with = "deserde_txtype")]
223        pub tx_type: TxType,
224        /// If transaction is executed successfully.
225        ///
226        /// This is the `statusCode`
227        pub success: bool,
228        /// Gas used
229        pub cumulative_gas_used: u64,
230        /// Log send from contracts.
231        pub logs: Cow<'a, Vec<Log>>,
232    }
233
234    /// Ensures that txtype is deserialized symmetrically as U8
235    fn deserde_txtype<'de, D>(deserializer: D) -> Result<TxType, D::Error>
236    where
237        D: Deserializer<'de>,
238    {
239        let value = U8::deserialize(deserializer)?;
240        value.to::<u8>().try_into().map_err(serde::de::Error::custom)
241    }
242
243    impl<'a> From<&'a super::Receipt> for Receipt<'a> {
244        fn from(value: &'a super::Receipt) -> Self {
245            Self {
246                tx_type: value.tx_type,
247                success: value.success,
248                cumulative_gas_used: value.cumulative_gas_used,
249                logs: Cow::Borrowed(&value.logs),
250            }
251        }
252    }
253
254    impl<'a> From<Receipt<'a>> for super::Receipt {
255        fn from(value: Receipt<'a>) -> Self {
256            Self {
257                tx_type: value.tx_type,
258                success: value.success,
259                cumulative_gas_used: value.cumulative_gas_used,
260                logs: value.logs.into_owned(),
261            }
262        }
263    }
264
265    impl SerializeAs<super::Receipt> for Receipt<'_> {
266        fn serialize_as<S>(source: &super::Receipt, serializer: S) -> Result<S::Ok, S::Error>
267        where
268            S: Serializer,
269        {
270            Receipt::<'_>::from(source).serialize(serializer)
271        }
272    }
273
274    impl<'de> DeserializeAs<'de, super::Receipt> for Receipt<'de> {
275        fn deserialize_as<D>(deserializer: D) -> Result<super::Receipt, D::Error>
276        where
277            D: Deserializer<'de>,
278        {
279            Receipt::<'_>::deserialize(deserializer).map(Into::into)
280        }
281    }
282
283    impl reth_primitives_traits::serde_bincode_compat::SerdeBincodeCompat for super::Receipt {
284        type BincodeRepr<'a> = Receipt<'a>;
285
286        fn as_repr(&self) -> Self::BincodeRepr<'_> {
287            self.into()
288        }
289
290        fn from_repr(repr: Self::BincodeRepr<'_>) -> Self {
291            repr.into()
292        }
293    }
294
295    #[cfg(test)]
296    mod tests {
297        use crate::{receipt::serde_bincode_compat, Receipt};
298        use arbitrary::Arbitrary;
299        use rand::Rng;
300        use serde::{Deserialize, Serialize};
301        use serde_with::serde_as;
302
303        #[test]
304        fn test_receipt_bincode_roundtrip() {
305            #[serde_as]
306            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
307            struct Data {
308                #[serde_as(as = "serde_bincode_compat::Receipt<'_>")]
309                reseipt: Receipt,
310            }
311
312            let mut bytes = [0u8; 1024];
313            rand::thread_rng().fill(bytes.as_mut_slice());
314            let data = Data {
315                reseipt: Receipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(),
316            };
317            let encoded = bincode::serialize(&data).unwrap();
318            let decoded: Data = bincode::deserialize(&encoded).unwrap();
319            assert_eq!(decoded, data);
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::TransactionSigned;
328    use alloy_eips::eip2718::Encodable2718;
329    use alloy_primitives::{
330        address, b256, bloom, bytes, hex_literal::hex, Address, Bytes, Log, LogData,
331    };
332    use alloy_rlp::Decodable;
333    use reth_codecs::Compact;
334    use reth_primitives_traits::proofs::{
335        calculate_receipt_root, calculate_transaction_root, calculate_withdrawals_root,
336    };
337
338    /// Ethereum full block.
339    ///
340    /// Withdrawals can be optionally included at the end of the RLP encoded message.
341    pub(crate) type Block<T = TransactionSigned> = alloy_consensus::Block<T>;
342
343    #[test]
344    fn test_decode_receipt() {
345        reth_codecs::test_utils::test_decode::<Receipt>(&hex!(
346            "c428b52ffd23fc42696156b10200f034792b6a94c3850215c2fef7aea361a0c31b79d9a32652eefc0d4e2e730036061cff7344b6fc6132b50cda0ed810a991ae58ef013150c12b2522533cb3b3a8b19b7786a8b5ff1d3cdc84225e22b02def168c8858df"
347        ));
348    }
349
350    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
351    #[test]
352    fn encode_legacy_receipt() {
353        let expected = hex!("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff");
354
355        let mut data = Vec::with_capacity(expected.length());
356        let receipt = ReceiptWithBloom {
357            receipt: Receipt {
358                tx_type: TxType::Legacy,
359                cumulative_gas_used: 0x1u64,
360                logs: vec![Log::new_unchecked(
361                    address!("0x0000000000000000000000000000000000000011"),
362                    vec![
363                        b256!("0x000000000000000000000000000000000000000000000000000000000000dead"),
364                        b256!("0x000000000000000000000000000000000000000000000000000000000000beef"),
365                    ],
366                    bytes!("0100ff"),
367                )],
368                success: false,
369            },
370            logs_bloom: [0; 256].into(),
371        };
372
373        receipt.encode(&mut data);
374
375        // check that the rlp length equals the length of the expected rlp
376        assert_eq!(receipt.length(), expected.len());
377        assert_eq!(data, expected);
378    }
379
380    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
381    #[test]
382    fn decode_legacy_receipt() {
383        let data = hex!("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff");
384
385        // EIP658Receipt
386        let expected = ReceiptWithBloom {
387            receipt: Receipt {
388                tx_type: TxType::Legacy,
389                cumulative_gas_used: 0x1u64,
390                logs: vec![Log::new_unchecked(
391                    address!("0x0000000000000000000000000000000000000011"),
392                    vec![
393                        b256!("0x000000000000000000000000000000000000000000000000000000000000dead"),
394                        b256!("0x000000000000000000000000000000000000000000000000000000000000beef"),
395                    ],
396                    bytes!("0100ff"),
397                )],
398                success: false,
399            },
400            logs_bloom: [0; 256].into(),
401        };
402
403        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
404        assert_eq!(receipt, expected);
405    }
406
407    #[test]
408    fn gigantic_receipt() {
409        let receipt = Receipt {
410            cumulative_gas_used: 16747627,
411            success: true,
412            tx_type: TxType::Legacy,
413            logs: vec![
414                Log::new_unchecked(
415                    address!("0x4bf56695415f725e43c3e04354b604bcfb6dfb6e"),
416                    vec![b256!(
417                        "0xc69dc3d7ebff79e41f525be431d5cd3cc08f80eaf0f7819054a726eeb7086eb9"
418                    )],
419                    Bytes::from(vec![1; 0xffffff]),
420                ),
421                Log::new_unchecked(
422                    address!("0xfaca325c86bf9c2d5b413cd7b90b209be92229c2"),
423                    vec![b256!(
424                        "0x8cca58667b1e9ffa004720ac99a3d61a138181963b294d270d91c53d36402ae2"
425                    )],
426                    Bytes::from(vec![1; 0xffffff]),
427                ),
428            ],
429        };
430
431        let mut data = vec![];
432        receipt.to_compact(&mut data);
433        let (decoded, _) = Receipt::from_compact(&data[..], data.len());
434        assert_eq!(decoded, receipt);
435    }
436
437    #[test]
438    fn test_encode_2718_length() {
439        let receipt = ReceiptWithBloom {
440            receipt: Receipt {
441                tx_type: TxType::Eip1559,
442                success: true,
443                cumulative_gas_used: 21000,
444                logs: vec![],
445            },
446            logs_bloom: Bloom::default(),
447        };
448
449        let encoded = receipt.encoded_2718();
450        assert_eq!(
451            encoded.len(),
452            receipt.encode_2718_len(),
453            "Encoded length should match the actual encoded data length"
454        );
455
456        // Test for legacy receipt as well
457        let legacy_receipt = ReceiptWithBloom {
458            receipt: Receipt {
459                tx_type: TxType::Legacy,
460                success: true,
461                cumulative_gas_used: 21000,
462                logs: vec![],
463            },
464            logs_bloom: Bloom::default(),
465        };
466
467        let legacy_encoded = legacy_receipt.encoded_2718();
468        assert_eq!(
469            legacy_encoded.len(),
470            legacy_receipt.encode_2718_len(),
471            "Encoded length for legacy receipt should match the actual encoded data length"
472        );
473    }
474
475    #[test]
476    fn check_transaction_root() {
477        let data = &hex!("f90262f901f9a092230ce5476ae868e98c7979cfc165a93f8b6ad1922acf2df62e340916efd49da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa02307107a867056ca33b5087e77c4174f47625e48fb49f1c70ced34890ddd88f3a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba0c598f69a5674cae9337261b669970e24abc0b46e6d284372a239ec8ccbf20b0ab901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8618203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0");
478        let block_rlp = &mut data.as_slice();
479        let block: Block = Block::decode(block_rlp).unwrap();
480
481        let tx_root = calculate_transaction_root(&block.body.transactions);
482        assert_eq!(block.transactions_root, tx_root, "Must be the same");
483    }
484
485    #[test]
486    fn check_withdrawals_root() {
487        // Single withdrawal, amount 0
488        // https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/amountIs0.json
489        let data = &hex!("f90238f90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0046119afb1ab36aaa8f66088677ed96cd62762f6d3e65642898e189fbe702d51a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a048a703da164234812273ea083e4ec3d09d028300cd325b46a6a75402e5a7ab95c0c0d9d8808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b80");
490        let block: Block = Block::decode(&mut data.as_slice()).unwrap();
491        assert!(block.body.withdrawals.is_some());
492        let withdrawals = block.body.withdrawals.as_ref().unwrap();
493        assert_eq!(withdrawals.len(), 1);
494        let withdrawals_root = calculate_withdrawals_root(withdrawals);
495        assert_eq!(block.withdrawals_root, Some(withdrawals_root));
496
497        // 4 withdrawals, identical indices
498        // https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/twoIdenticalIndex.json
499        let data = &hex!("f9028cf90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0ccf7b62d616c2ad7af862d67b9dcd2119a90cebbff8c3cd1e5d7fc99f8755774a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a0a95b9a7b58a6b3cb4001eb0be67951c5517141cb0183a255b5cae027a7b10b36c0c0f86cda808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da018094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710");
500        let block: Block = Block::decode(&mut data.as_slice()).unwrap();
501        assert!(block.body.withdrawals.is_some());
502        let withdrawals = block.body.withdrawals.as_ref().unwrap();
503        assert_eq!(withdrawals.len(), 4);
504        let withdrawals_root = calculate_withdrawals_root(withdrawals);
505        assert_eq!(block.withdrawals_root, Some(withdrawals_root));
506    }
507    #[test]
508    fn check_receipt_root_optimism() {
509        use alloy_consensus::ReceiptWithBloom;
510
511        let logs = vec![Log {
512            address: Address::ZERO,
513            data: LogData::new_unchecked(vec![], Default::default()),
514        }];
515        let bloom = bloom!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001");
516        let receipt = ReceiptWithBloom {
517            receipt: Receipt {
518                tx_type: TxType::Eip2930,
519                success: true,
520                cumulative_gas_used: 102068,
521                logs,
522            },
523            logs_bloom: bloom,
524        };
525        let receipt = vec![receipt];
526        let root = calculate_receipt_root(&receipt);
527        assert_eq!(
528            root,
529            b256!("0xfe70ae4a136d98944951b2123859698d59ad251a381abc9960fa81cae3d0d4a0")
530        );
531    }
532}