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, IsTyped2718},
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 IsTyped2718 for Receipt {
272    fn is_type(type_id: u8) -> bool {
273        <TxType as IsTyped2718>::is_type(type_id)
274    }
275}
276
277impl InMemorySize for Receipt {
278    fn size(&self) -> usize {
279        self.tx_type.size() +
280            core::mem::size_of::<bool>() +
281            core::mem::size_of::<u64>() +
282            self.logs.capacity() * core::mem::size_of::<Log>()
283    }
284}
285
286impl reth_primitives_traits::Receipt for Receipt {}
287
288#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
289pub(super) mod serde_bincode_compat {
290    use alloc::{borrow::Cow, vec::Vec};
291    use alloy_consensus::TxType;
292    use alloy_primitives::{Log, U8};
293    use serde::{Deserialize, Deserializer, Serialize, Serializer};
294    use serde_with::{DeserializeAs, SerializeAs};
295
296    /// Bincode-compatible [`super::Receipt`] serde implementation.
297    ///
298    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
299    /// ```rust
300    /// use reth_ethereum_primitives::{serde_bincode_compat, Receipt};
301    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
302    /// use serde_with::serde_as;
303    ///
304    /// #[serde_as]
305    /// #[derive(Serialize, Deserialize)]
306    /// struct Data {
307    ///     #[serde_as(as = "serde_bincode_compat::Receipt<'_>")]
308    ///     receipt: Receipt,
309    /// }
310    /// ```
311    #[derive(Debug, Serialize, Deserialize)]
312    pub struct Receipt<'a> {
313        /// Receipt type.
314        #[serde(deserialize_with = "deserde_txtype")]
315        pub tx_type: TxType,
316        /// If transaction is executed successfully.
317        ///
318        /// This is the `statusCode`
319        pub success: bool,
320        /// Gas used
321        pub cumulative_gas_used: u64,
322        /// Log send from contracts.
323        pub logs: Cow<'a, Vec<Log>>,
324    }
325
326    /// Ensures that txtype is deserialized symmetrically as U8
327    fn deserde_txtype<'de, D>(deserializer: D) -> Result<TxType, D::Error>
328    where
329        D: Deserializer<'de>,
330    {
331        let value = U8::deserialize(deserializer)?;
332        value.to::<u8>().try_into().map_err(serde::de::Error::custom)
333    }
334
335    impl<'a> From<&'a super::Receipt> for Receipt<'a> {
336        fn from(value: &'a super::Receipt) -> Self {
337            Self {
338                tx_type: value.tx_type,
339                success: value.success,
340                cumulative_gas_used: value.cumulative_gas_used,
341                logs: Cow::Borrowed(&value.logs),
342            }
343        }
344    }
345
346    impl<'a> From<Receipt<'a>> for super::Receipt {
347        fn from(value: Receipt<'a>) -> Self {
348            Self {
349                tx_type: value.tx_type,
350                success: value.success,
351                cumulative_gas_used: value.cumulative_gas_used,
352                logs: value.logs.into_owned(),
353            }
354        }
355    }
356
357    impl SerializeAs<super::Receipt> for Receipt<'_> {
358        fn serialize_as<S>(source: &super::Receipt, serializer: S) -> Result<S::Ok, S::Error>
359        where
360            S: Serializer,
361        {
362            Receipt::<'_>::from(source).serialize(serializer)
363        }
364    }
365
366    impl<'de> DeserializeAs<'de, super::Receipt> for Receipt<'de> {
367        fn deserialize_as<D>(deserializer: D) -> Result<super::Receipt, D::Error>
368        where
369            D: Deserializer<'de>,
370        {
371            Receipt::<'_>::deserialize(deserializer).map(Into::into)
372        }
373    }
374
375    impl reth_primitives_traits::serde_bincode_compat::SerdeBincodeCompat for super::Receipt {
376        type BincodeRepr<'a> = Receipt<'a>;
377
378        fn as_repr(&self) -> Self::BincodeRepr<'_> {
379            self.into()
380        }
381
382        fn from_repr(repr: Self::BincodeRepr<'_>) -> Self {
383            repr.into()
384        }
385    }
386
387    #[cfg(test)]
388    mod tests {
389        use crate::{receipt::serde_bincode_compat, Receipt};
390        use arbitrary::Arbitrary;
391        use rand::Rng;
392        use serde_with::serde_as;
393
394        #[test]
395        fn test_receipt_bincode_roundtrip() {
396            #[serde_as]
397            #[derive(Debug, PartialEq, Eq)]
398            #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
399            struct Data {
400                #[serde_as(as = "serde_bincode_compat::Receipt<'_>")]
401                reseipt: Receipt,
402            }
403
404            let mut bytes = [0u8; 1024];
405            rand::rng().fill(bytes.as_mut_slice());
406            let data = Data {
407                reseipt: Receipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(),
408            };
409            let encoded = bincode::serialize(&data).unwrap();
410            let decoded: Data = bincode::deserialize(&encoded).unwrap();
411            assert_eq!(decoded, data);
412        }
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use crate::TransactionSigned;
420    use alloy_eips::eip2718::Encodable2718;
421    use alloy_primitives::{
422        address, b256, bloom, bytes, hex_literal::hex, Address, Bytes, Log, LogData,
423    };
424    use alloy_rlp::Decodable;
425    use reth_codecs::Compact;
426    use reth_primitives_traits::proofs::{
427        calculate_receipt_root, calculate_transaction_root, calculate_withdrawals_root,
428    };
429
430    /// Ethereum full block.
431    ///
432    /// Withdrawals can be optionally included at the end of the RLP encoded message.
433    pub(crate) type Block<T = TransactionSigned> = alloy_consensus::Block<T>;
434
435    #[test]
436    fn test_decode_receipt() {
437        reth_codecs::test_utils::test_decode::<Receipt>(&hex!(
438            "c428b52ffd23fc42696156b10200f034792b6a94c3850215c2fef7aea361a0c31b79d9a32652eefc0d4e2e730036061cff7344b6fc6132b50cda0ed810a991ae58ef013150c12b2522533cb3b3a8b19b7786a8b5ff1d3cdc84225e22b02def168c8858df"
439        ));
440    }
441
442    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
443    #[test]
444    fn encode_legacy_receipt() {
445        let expected = hex!(
446            "f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
447        );
448
449        let mut data = Vec::with_capacity(expected.length());
450        let receipt = ReceiptWithBloom {
451            receipt: Receipt {
452                tx_type: TxType::Legacy,
453                cumulative_gas_used: 0x1u64,
454                logs: vec![Log::new_unchecked(
455                    address!("0x0000000000000000000000000000000000000011"),
456                    vec![
457                        b256!("0x000000000000000000000000000000000000000000000000000000000000dead"),
458                        b256!("0x000000000000000000000000000000000000000000000000000000000000beef"),
459                    ],
460                    bytes!("0100ff"),
461                )],
462                success: false,
463            },
464            logs_bloom: [0; 256].into(),
465        };
466
467        receipt.encode(&mut data);
468
469        // check that the rlp length equals the length of the expected rlp
470        assert_eq!(receipt.length(), expected.len());
471        assert_eq!(data, expected);
472    }
473
474    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
475    #[test]
476    fn decode_legacy_receipt() {
477        let data = hex!(
478            "f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
479        );
480
481        // EIP658Receipt
482        let expected = ReceiptWithBloom {
483            receipt: Receipt {
484                tx_type: TxType::Legacy,
485                cumulative_gas_used: 0x1u64,
486                logs: vec![Log::new_unchecked(
487                    address!("0x0000000000000000000000000000000000000011"),
488                    vec![
489                        b256!("0x000000000000000000000000000000000000000000000000000000000000dead"),
490                        b256!("0x000000000000000000000000000000000000000000000000000000000000beef"),
491                    ],
492                    bytes!("0100ff"),
493                )],
494                success: false,
495            },
496            logs_bloom: [0; 256].into(),
497        };
498
499        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
500        assert_eq!(receipt, expected);
501    }
502
503    #[test]
504    fn gigantic_receipt() {
505        let receipt = Receipt {
506            cumulative_gas_used: 16747627,
507            success: true,
508            tx_type: TxType::Legacy,
509            logs: vec![
510                Log::new_unchecked(
511                    address!("0x4bf56695415f725e43c3e04354b604bcfb6dfb6e"),
512                    vec![b256!(
513                        "0xc69dc3d7ebff79e41f525be431d5cd3cc08f80eaf0f7819054a726eeb7086eb9"
514                    )],
515                    Bytes::from(vec![1; 0xffffff]),
516                ),
517                Log::new_unchecked(
518                    address!("0xfaca325c86bf9c2d5b413cd7b90b209be92229c2"),
519                    vec![b256!(
520                        "0x8cca58667b1e9ffa004720ac99a3d61a138181963b294d270d91c53d36402ae2"
521                    )],
522                    Bytes::from(vec![1; 0xffffff]),
523                ),
524            ],
525        };
526
527        let mut data = vec![];
528        receipt.to_compact(&mut data);
529        let (decoded, _) = Receipt::from_compact(&data[..], data.len());
530        assert_eq!(decoded, receipt);
531    }
532
533    #[test]
534    fn test_encode_2718_length() {
535        let receipt = ReceiptWithBloom {
536            receipt: Receipt {
537                tx_type: TxType::Eip1559,
538                success: true,
539                cumulative_gas_used: 21000,
540                logs: vec![],
541            },
542            logs_bloom: Bloom::default(),
543        };
544
545        let encoded = receipt.encoded_2718();
546        assert_eq!(
547            encoded.len(),
548            receipt.encode_2718_len(),
549            "Encoded length should match the actual encoded data length"
550        );
551
552        // Test for legacy receipt as well
553        let legacy_receipt = ReceiptWithBloom {
554            receipt: Receipt {
555                tx_type: TxType::Legacy,
556                success: true,
557                cumulative_gas_used: 21000,
558                logs: vec![],
559            },
560            logs_bloom: Bloom::default(),
561        };
562
563        let legacy_encoded = legacy_receipt.encoded_2718();
564        assert_eq!(
565            legacy_encoded.len(),
566            legacy_receipt.encode_2718_len(),
567            "Encoded length for legacy receipt should match the actual encoded data length"
568        );
569    }
570
571    #[test]
572    fn check_transaction_root() {
573        let data = &hex!(
574            "f90262f901f9a092230ce5476ae868e98c7979cfc165a93f8b6ad1922acf2df62e340916efd49da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa02307107a867056ca33b5087e77c4174f47625e48fb49f1c70ced34890ddd88f3a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba0c598f69a5674cae9337261b669970e24abc0b46e6d284372a239ec8ccbf20b0ab901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8618203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0"
575        );
576        let block_rlp = &mut data.as_slice();
577        let block: Block = Block::decode(block_rlp).unwrap();
578
579        let tx_root = calculate_transaction_root(&block.body.transactions);
580        assert_eq!(block.transactions_root, tx_root, "Must be the same");
581    }
582
583    #[test]
584    fn check_withdrawals_root() {
585        // Single withdrawal, amount 0
586        // https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/amountIs0.json
587        let data = &hex!(
588            "f90238f90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0046119afb1ab36aaa8f66088677ed96cd62762f6d3e65642898e189fbe702d51a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a048a703da164234812273ea083e4ec3d09d028300cd325b46a6a75402e5a7ab95c0c0d9d8808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b80"
589        );
590        let block: Block = Block::decode(&mut data.as_slice()).unwrap();
591        assert!(block.body.withdrawals.is_some());
592        let withdrawals = block.body.withdrawals.as_ref().unwrap();
593        assert_eq!(withdrawals.len(), 1);
594        let withdrawals_root = calculate_withdrawals_root(withdrawals);
595        assert_eq!(block.withdrawals_root, Some(withdrawals_root));
596
597        // 4 withdrawals, identical indices
598        // https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/twoIdenticalIndex.json
599        let data = &hex!(
600            "f9028cf90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0ccf7b62d616c2ad7af862d67b9dcd2119a90cebbff8c3cd1e5d7fc99f8755774a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a0a95b9a7b58a6b3cb4001eb0be67951c5517141cb0183a255b5cae027a7b10b36c0c0f86cda808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da018094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710"
601        );
602        let block: Block = Block::decode(&mut data.as_slice()).unwrap();
603        assert!(block.body.withdrawals.is_some());
604        let withdrawals = block.body.withdrawals.as_ref().unwrap();
605        assert_eq!(withdrawals.len(), 4);
606        let withdrawals_root = calculate_withdrawals_root(withdrawals);
607        assert_eq!(block.withdrawals_root, Some(withdrawals_root));
608    }
609    #[test]
610    fn check_receipt_root_optimism() {
611        use alloy_consensus::ReceiptWithBloom;
612
613        let logs = vec![Log {
614            address: Address::ZERO,
615            data: LogData::new_unchecked(vec![], Default::default()),
616        }];
617        let bloom = bloom!(
618            "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
619        );
620        let receipt = ReceiptWithBloom {
621            receipt: Receipt {
622                tx_type: TxType::Eip2930,
623                success: true,
624                cumulative_gas_used: 102068,
625                logs,
626            },
627            logs_bloom: bloom,
628        };
629        let receipt = vec![receipt];
630        let root = calculate_receipt_root(&receipt);
631        assert_eq!(
632            root,
633            b256!("0xfe70ae4a136d98944951b2123859698d59ad251a381abc9960fa81cae3d0d4a0")
634        );
635    }
636}