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