reth_ethereum_primitives/
receipt.rs

1use core::fmt::Debug;
2
3use alloc::vec::Vec;
4use alloy_consensus::{
5    Eip2718DecodableReceipt, Eip2718EncodableReceipt, Eip658Value, ReceiptEnvelope,
6    ReceiptWithBloom, RlpDecodableReceipt, RlpEncodableReceipt, TxReceipt, TxType, Typed2718,
7};
8use alloy_eips::eip2718::{Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718};
9use alloy_primitives::{Bloom, Log, B256};
10use alloy_rlp::{BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable};
11use reth_primitives_traits::{proofs::ordered_trie_root_with_encoder, InMemorySize};
12
13/// Helper trait alias with requirements for transaction type generic to be used within [`Receipt`].
14pub trait TxTy:
15    Debug
16    + Copy
17    + Eq
18    + Send
19    + Sync
20    + InMemorySize
21    + Typed2718
22    + TryFrom<u8, Error = Eip2718Error>
23    + Decodable
24    + 'static
25{
26}
27impl<T> TxTy for T where
28    T: Debug
29        + Copy
30        + Eq
31        + Send
32        + Sync
33        + InMemorySize
34        + Typed2718
35        + TryFrom<u8, Error = Eip2718Error>
36        + Decodable
37        + 'static
38{
39}
40
41/// Raw ethereum receipt.
42pub type Receipt<T = TxType> = EthereumReceipt<T>;
43
44#[cfg(feature = "rpc")]
45/// Receipt representation for RPC.
46pub type RpcReceipt<T = TxType> = EthereumReceipt<T, alloy_rpc_types_eth::Log>;
47
48/// Typed ethereum transaction receipt.
49/// Receipt containing result of transaction execution.
50#[derive(Clone, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
53#[cfg_attr(feature = "reth-codec", reth_codecs::add_arbitrary_tests(compact, rlp))]
54#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
55pub struct EthereumReceipt<T = TxType, L = Log> {
56    /// Receipt type.
57    #[cfg_attr(feature = "serde", serde(rename = "type"))]
58    pub tx_type: T,
59    /// If transaction is executed successfully.
60    ///
61    /// This is the `statusCode`
62    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity", rename = "status"))]
63    pub success: bool,
64    /// Gas used
65    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
66    pub cumulative_gas_used: u64,
67    /// Log send from contracts.
68    pub logs: Vec<L>,
69}
70
71#[cfg(feature = "rpc")]
72impl<T> Receipt<T> {
73    /// Converts the logs of the receipt to RPC logs.
74    pub fn into_rpc(
75        self,
76        next_log_index: usize,
77        meta: alloy_consensus::transaction::TransactionMeta,
78    ) -> RpcReceipt<T> {
79        let Self { tx_type, success, cumulative_gas_used, logs } = self;
80        let logs = alloy_rpc_types_eth::Log::collect_for_receipt(next_log_index, meta, logs);
81        RpcReceipt { tx_type, success, cumulative_gas_used, logs }
82    }
83}
84
85impl<T: TxTy> Receipt<T> {
86    /// Returns length of RLP-encoded receipt fields with the given [`Bloom`] without an RLP header.
87    pub fn rlp_encoded_fields_length(&self, bloom: &Bloom) -> usize {
88        self.success.length() +
89            self.cumulative_gas_used.length() +
90            bloom.length() +
91            self.logs.length()
92    }
93
94    /// RLP-encodes receipt fields with the given [`Bloom`] without an RLP header.
95    pub fn rlp_encode_fields(&self, bloom: &Bloom, out: &mut dyn BufMut) {
96        self.success.encode(out);
97        self.cumulative_gas_used.encode(out);
98        bloom.encode(out);
99        self.logs.encode(out);
100    }
101
102    /// Returns RLP header for inner encoding.
103    pub fn rlp_header_inner(&self, bloom: &Bloom) -> Header {
104        Header { list: true, payload_length: self.rlp_encoded_fields_length(bloom) }
105    }
106
107    /// RLP-decodes the receipt from the provided buffer. This does not expect a type byte or
108    /// network header.
109    pub fn rlp_decode_inner(
110        buf: &mut &[u8],
111        tx_type: T,
112    ) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
113        let header = Header::decode(buf)?;
114        if !header.list {
115            return Err(alloy_rlp::Error::UnexpectedString);
116        }
117
118        let remaining = buf.len();
119
120        let success = Decodable::decode(buf)?;
121        let cumulative_gas_used = Decodable::decode(buf)?;
122        let logs_bloom = Decodable::decode(buf)?;
123        let logs = Decodable::decode(buf)?;
124
125        if buf.len() + header.payload_length != remaining {
126            return Err(alloy_rlp::Error::UnexpectedLength);
127        }
128
129        Ok(ReceiptWithBloom {
130            receipt: Self { cumulative_gas_used, tx_type, success, logs },
131            logs_bloom,
132        })
133    }
134
135    /// Calculates the receipt root for a header for the reference type of [Receipt].
136    ///
137    /// NOTE: Prefer `proofs::calculate_receipt_root` if you have log blooms memoized.
138    pub fn calculate_receipt_root_no_memo(receipts: &[Self]) -> B256 {
139        ordered_trie_root_with_encoder(receipts, |r, buf| r.with_bloom_ref().encode_2718(buf))
140    }
141}
142
143impl<T: TxTy> Eip2718EncodableReceipt for Receipt<T> {
144    fn eip2718_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
145        !self.tx_type.is_legacy() as usize + self.rlp_header_inner(bloom).length_with_payload()
146    }
147
148    fn eip2718_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
149        if !self.tx_type.is_legacy() {
150            out.put_u8(self.tx_type.ty());
151        }
152        self.rlp_header_inner(bloom).encode(out);
153        self.rlp_encode_fields(bloom, out);
154    }
155}
156
157impl<T: TxTy> Eip2718DecodableReceipt for Receipt<T> {
158    fn typed_decode_with_bloom(ty: u8, buf: &mut &[u8]) -> Eip2718Result<ReceiptWithBloom<Self>> {
159        Ok(Self::rlp_decode_inner(buf, T::try_from(ty)?)?)
160    }
161
162    fn fallback_decode_with_bloom(buf: &mut &[u8]) -> Eip2718Result<ReceiptWithBloom<Self>> {
163        Ok(Self::rlp_decode_inner(buf, T::try_from(0)?)?)
164    }
165}
166
167impl<T: TxTy> RlpEncodableReceipt for Receipt<T> {
168    fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
169        let mut len = self.eip2718_encoded_length_with_bloom(bloom);
170        if !self.tx_type.is_legacy() {
171            len += Header {
172                list: false,
173                payload_length: self.eip2718_encoded_length_with_bloom(bloom),
174            }
175            .length();
176        }
177
178        len
179    }
180
181    fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
182        if !self.tx_type.is_legacy() {
183            Header { list: false, payload_length: self.eip2718_encoded_length_with_bloom(bloom) }
184                .encode(out);
185        }
186        self.eip2718_encode_with_bloom(bloom, out);
187    }
188}
189
190impl<T: TxTy> RlpDecodableReceipt for Receipt<T> {
191    fn rlp_decode_with_bloom(buf: &mut &[u8]) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
192        let header_buf = &mut &**buf;
193        let header = Header::decode(header_buf)?;
194
195        // Legacy receipt, reuse initial buffer without advancing
196        if header.list {
197            return Self::rlp_decode_inner(buf, T::try_from(0)?)
198        }
199
200        // Otherwise, advance the buffer and try decoding type flag followed by receipt
201        *buf = *header_buf;
202
203        let remaining = buf.len();
204        let tx_type = T::decode(buf)?;
205        let this = Self::rlp_decode_inner(buf, tx_type)?;
206
207        if buf.len() + header.payload_length != remaining {
208            return Err(alloy_rlp::Error::UnexpectedLength);
209        }
210
211        Ok(this)
212    }
213}
214
215impl<T, L> TxReceipt for EthereumReceipt<T, L>
216where
217    T: TxTy,
218    L: Send + Sync + Clone + Debug + Eq + AsRef<Log>,
219{
220    type Log = L;
221
222    fn status_or_post_state(&self) -> Eip658Value {
223        self.success.into()
224    }
225
226    fn status(&self) -> bool {
227        self.success
228    }
229
230    fn bloom(&self) -> Bloom {
231        alloy_primitives::logs_bloom(self.logs.iter().map(|l| l.as_ref()))
232    }
233
234    fn cumulative_gas_used(&self) -> u64 {
235        self.cumulative_gas_used
236    }
237
238    fn logs(&self) -> &[L] {
239        &self.logs
240    }
241
242    fn into_logs(self) -> Vec<L> {
243        self.logs
244    }
245}
246
247impl<T: TxTy> Typed2718 for Receipt<T> {
248    fn ty(&self) -> u8 {
249        self.tx_type.ty()
250    }
251}
252
253impl<T: TxTy> IsTyped2718 for Receipt<T> {
254    fn is_type(type_id: u8) -> bool {
255        <TxType as IsTyped2718>::is_type(type_id)
256    }
257}
258
259impl<T: TxTy> InMemorySize for Receipt<T> {
260    fn size(&self) -> usize {
261        size_of::<Self>() + self.logs.iter().map(|log| log.size()).sum::<usize>()
262    }
263}
264
265impl<T> From<ReceiptEnvelope<T>> for Receipt<TxType>
266where
267    T: Into<Log>,
268{
269    fn from(value: ReceiptEnvelope<T>) -> Self {
270        let value = value.into_primitives_receipt();
271        Self {
272            tx_type: value.tx_type(),
273            success: value.is_success(),
274            cumulative_gas_used: value.cumulative_gas_used(),
275            logs: value.into_logs(),
276        }
277    }
278}
279
280impl<T, L> From<EthereumReceipt<T, L>> for alloy_consensus::Receipt<L> {
281    fn from(value: EthereumReceipt<T, L>) -> Self {
282        Self {
283            status: value.success.into(),
284            cumulative_gas_used: value.cumulative_gas_used,
285            logs: value.logs,
286        }
287    }
288}
289
290impl<L> From<EthereumReceipt<TxType, L>> for ReceiptEnvelope<L>
291where
292    L: Send + Sync + Clone + Debug + Eq + AsRef<Log>,
293{
294    fn from(value: EthereumReceipt<TxType, L>) -> Self {
295        let tx_type = value.tx_type;
296        let receipt = value.into_with_bloom().map_receipt(Into::into);
297        match tx_type {
298            TxType::Legacy => Self::Legacy(receipt),
299            TxType::Eip2930 => Self::Eip2930(receipt),
300            TxType::Eip1559 => Self::Eip1559(receipt),
301            TxType::Eip4844 => Self::Eip4844(receipt),
302            TxType::Eip7702 => Self::Eip7702(receipt),
303        }
304    }
305}
306
307#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
308pub(super) mod serde_bincode_compat {
309    use alloc::{borrow::Cow, vec::Vec};
310    use alloy_consensus::TxType;
311    use alloy_eips::eip2718::Eip2718Error;
312    use alloy_primitives::{Log, U8};
313    use core::fmt::Debug;
314    use serde::{Deserialize, Deserializer, Serialize, Serializer};
315    use serde_with::{DeserializeAs, SerializeAs};
316
317    /// Bincode-compatible [`super::Receipt`] serde implementation.
318    ///
319    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
320    /// ```rust
321    /// use alloy_consensus::TxType;
322    /// use reth_ethereum_primitives::{serde_bincode_compat, Receipt};
323    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
324    /// use serde_with::serde_as;
325    ///
326    /// #[serde_as]
327    /// #[derive(Serialize, Deserialize)]
328    /// struct Data {
329    ///     #[serde_as(as = "serde_bincode_compat::Receipt<'_>")]
330    ///     receipt: Receipt<TxType>,
331    /// }
332    /// ```
333    #[derive(Debug, Serialize, Deserialize)]
334    #[serde(bound(deserialize = "T: TryFrom<u8, Error = Eip2718Error>"))]
335    pub struct Receipt<'a, T = TxType> {
336        /// Receipt type.
337        #[serde(deserialize_with = "deserde_txtype")]
338        pub tx_type: T,
339        /// If transaction is executed successfully.
340        ///
341        /// This is the `statusCode`
342        pub success: bool,
343        /// Gas used
344        pub cumulative_gas_used: u64,
345        /// Log send from contracts.
346        pub logs: Cow<'a, Vec<Log>>,
347    }
348
349    /// Ensures that txtype is deserialized symmetrically as U8
350    fn deserde_txtype<'de, D, T>(deserializer: D) -> Result<T, D::Error>
351    where
352        D: Deserializer<'de>,
353        T: TryFrom<u8, Error = Eip2718Error>,
354    {
355        U8::deserialize(deserializer)?.to::<u8>().try_into().map_err(serde::de::Error::custom)
356    }
357
358    impl<'a, T: Copy> From<&'a super::Receipt<T>> for Receipt<'a, T> {
359        fn from(value: &'a super::Receipt<T>) -> Self {
360            Self {
361                tx_type: value.tx_type,
362                success: value.success,
363                cumulative_gas_used: value.cumulative_gas_used,
364                logs: Cow::Borrowed(&value.logs),
365            }
366        }
367    }
368
369    impl<'a, T> From<Receipt<'a, T>> for super::Receipt<T> {
370        fn from(value: Receipt<'a, T>) -> Self {
371            Self {
372                tx_type: value.tx_type,
373                success: value.success,
374                cumulative_gas_used: value.cumulative_gas_used,
375                logs: value.logs.into_owned(),
376            }
377        }
378    }
379
380    impl<T: Copy + Serialize> SerializeAs<super::Receipt<T>> for Receipt<'_, T> {
381        fn serialize_as<S>(source: &super::Receipt<T>, serializer: S) -> Result<S::Ok, S::Error>
382        where
383            S: Serializer,
384        {
385            Receipt::<'_>::from(source).serialize(serializer)
386        }
387    }
388
389    impl<'de, T: TryFrom<u8, Error = Eip2718Error>> DeserializeAs<'de, super::Receipt<T>>
390        for Receipt<'de, T>
391    {
392        fn deserialize_as<D>(deserializer: D) -> Result<super::Receipt<T>, D::Error>
393        where
394            D: Deserializer<'de>,
395        {
396            Receipt::<'_, T>::deserialize(deserializer).map(Into::into)
397        }
398    }
399
400    impl<T> reth_primitives_traits::serde_bincode_compat::SerdeBincodeCompat for super::Receipt<T>
401    where
402        T: Copy + Serialize + TryFrom<u8, Error = Eip2718Error> + Debug + 'static,
403    {
404        type BincodeRepr<'a> = Receipt<'a, T>;
405
406        fn as_repr(&self) -> Self::BincodeRepr<'_> {
407            self.into()
408        }
409
410        fn from_repr(repr: Self::BincodeRepr<'_>) -> Self {
411            repr.into()
412        }
413    }
414
415    #[cfg(test)]
416    mod tests {
417        use crate::{receipt::serde_bincode_compat, Receipt};
418        use alloy_consensus::TxType;
419        use arbitrary::Arbitrary;
420        use rand::Rng;
421        use serde_with::serde_as;
422
423        #[test]
424        fn test_receipt_bincode_roundtrip() {
425            #[serde_as]
426            #[derive(Debug, PartialEq, Eq)]
427            #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
428            struct Data {
429                #[serde_as(as = "serde_bincode_compat::Receipt<'_, TxType>")]
430                receipt: Receipt<TxType>,
431            }
432
433            let mut bytes = [0u8; 1024];
434            rand::rng().fill(bytes.as_mut_slice());
435            let data = Data {
436                receipt: Receipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(),
437            };
438            let encoded = bincode::serialize(&data).unwrap();
439            let decoded: Data = bincode::deserialize(&encoded).unwrap();
440            assert_eq!(decoded, data);
441        }
442    }
443}
444
445#[cfg(feature = "reth-codec")]
446mod compact {
447    use super::*;
448    use reth_codecs::{
449        Compact,
450        __private::{modular_bitfield::prelude::*, Buf},
451    };
452
453    impl Receipt {
454        #[doc = "Used bytes by [`ReceiptFlags`]"]
455        pub const fn bitflag_encoded_bytes() -> usize {
456            1u8 as usize
457        }
458        #[doc = "Unused bits for new fields by [`ReceiptFlags`]"]
459        pub const fn bitflag_unused_bits() -> usize {
460            0u8 as usize
461        }
462    }
463
464    #[allow(non_snake_case, unused_parens)]
465    mod flags {
466        use super::*;
467
468        #[doc = "Fieldset that facilitates compacting the parent type. Used bytes: 1 | Unused bits: 0"]
469        #[bitfield]
470        #[derive(Clone, Copy, Debug, Default)]
471        pub struct ReceiptFlags {
472            pub tx_type_len: B2,
473            pub success_len: B1,
474            pub cumulative_gas_used_len: B4,
475            pub __zstd: B1,
476        }
477
478        impl ReceiptFlags {
479            #[doc = r" Deserializes this fieldset and returns it, alongside the original slice in an advanced position."]
480            pub fn from(mut buf: &[u8]) -> (Self, &[u8]) {
481                (Self::from_bytes([buf.get_u8()]), buf)
482            }
483        }
484    }
485
486    pub use flags::ReceiptFlags;
487
488    impl<T: Compact> Compact for Receipt<T> {
489        fn to_compact<B>(&self, buf: &mut B) -> usize
490        where
491            B: reth_codecs::__private::bytes::BufMut + AsMut<[u8]>,
492        {
493            let mut flags = ReceiptFlags::default();
494            let mut total_length = 0;
495            let mut buffer = reth_codecs::__private::bytes::BytesMut::new();
496
497            let tx_type_len = self.tx_type.to_compact(&mut buffer);
498            flags.set_tx_type_len(tx_type_len as u8);
499            let success_len = self.success.to_compact(&mut buffer);
500            flags.set_success_len(success_len as u8);
501            let cumulative_gas_used_len = self.cumulative_gas_used.to_compact(&mut buffer);
502            flags.set_cumulative_gas_used_len(cumulative_gas_used_len as u8);
503            self.logs.to_compact(&mut buffer);
504
505            let zstd = buffer.len() > 7;
506            if zstd {
507                flags.set___zstd(1);
508            }
509
510            let flags = flags.into_bytes();
511            total_length += flags.len() + buffer.len();
512            buf.put_slice(&flags);
513            if zstd {
514                reth_zstd_compressors::RECEIPT_COMPRESSOR.with(|compressor| {
515                    let compressed =
516                        compressor.borrow_mut().compress(&buffer).expect("Failed to compress.");
517                    buf.put(compressed.as_slice());
518                });
519            } else {
520                buf.put(buffer);
521            }
522            total_length
523        }
524
525        fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
526            let (flags, mut buf) = ReceiptFlags::from(buf);
527            if flags.__zstd() != 0 {
528                reth_zstd_compressors::RECEIPT_DECOMPRESSOR.with(|decompressor| {
529                    let decompressor = &mut decompressor.borrow_mut();
530                    let decompressed = decompressor.decompress(buf);
531                    let original_buf = buf;
532                    let mut buf: &[u8] = decompressed;
533                    let (tx_type, new_buf) = T::from_compact(buf, flags.tx_type_len() as usize);
534                    buf = new_buf;
535                    let (success, new_buf) = bool::from_compact(buf, flags.success_len() as usize);
536                    buf = new_buf;
537                    let (cumulative_gas_used, new_buf) =
538                        u64::from_compact(buf, flags.cumulative_gas_used_len() as usize);
539                    buf = new_buf;
540                    let (logs, _) = Vec::from_compact(buf, buf.len());
541                    (Self { tx_type, success, cumulative_gas_used, logs }, original_buf)
542                })
543            } else {
544                let (tx_type, new_buf) = T::from_compact(buf, flags.tx_type_len() as usize);
545                buf = new_buf;
546                let (success, new_buf) = bool::from_compact(buf, flags.success_len() as usize);
547                buf = new_buf;
548                let (cumulative_gas_used, new_buf) =
549                    u64::from_compact(buf, flags.cumulative_gas_used_len() as usize);
550                buf = new_buf;
551                let (logs, new_buf) = Vec::from_compact(buf, buf.len());
552                buf = new_buf;
553                let obj = Self { tx_type, success, cumulative_gas_used, logs };
554                (obj, buf)
555            }
556        }
557    }
558}
559
560#[cfg(feature = "reth-codec")]
561pub use compact::*;
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use crate::TransactionSigned;
567    use alloy_eips::eip2718::Encodable2718;
568    use alloy_primitives::{
569        address, b256, bloom, bytes, hex_literal::hex, Address, Bytes, Log, LogData,
570    };
571    use alloy_rlp::Decodable;
572    use reth_codecs::Compact;
573    use reth_primitives_traits::proofs::{
574        calculate_receipt_root, calculate_transaction_root, calculate_withdrawals_root,
575    };
576
577    /// Ethereum full block.
578    ///
579    /// Withdrawals can be optionally included at the end of the RLP encoded message.
580    pub(crate) type Block<T = TransactionSigned> = alloy_consensus::Block<T>;
581
582    #[test]
583    #[cfg(feature = "reth-codec")]
584    fn test_decode_receipt() {
585        reth_codecs::test_utils::test_decode::<Receipt<TxType>>(&hex!(
586            "c428b52ffd23fc42696156b10200f034792b6a94c3850215c2fef7aea361a0c31b79d9a32652eefc0d4e2e730036061cff7344b6fc6132b50cda0ed810a991ae58ef013150c12b2522533cb3b3a8b19b7786a8b5ff1d3cdc84225e22b02def168c8858df"
587        ));
588    }
589
590    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
591    #[test]
592    fn encode_legacy_receipt() {
593        let expected = hex!(
594            "f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
595        );
596
597        let mut data = Vec::with_capacity(expected.length());
598        let receipt = ReceiptWithBloom {
599            receipt: Receipt {
600                tx_type: TxType::Legacy,
601                cumulative_gas_used: 0x1u64,
602                logs: vec![Log::new_unchecked(
603                    address!("0x0000000000000000000000000000000000000011"),
604                    vec![
605                        b256!("0x000000000000000000000000000000000000000000000000000000000000dead"),
606                        b256!("0x000000000000000000000000000000000000000000000000000000000000beef"),
607                    ],
608                    bytes!("0100ff"),
609                )],
610                success: false,
611            },
612            logs_bloom: [0; 256].into(),
613        };
614
615        receipt.encode(&mut data);
616
617        // check that the rlp length equals the length of the expected rlp
618        assert_eq!(receipt.length(), expected.len());
619        assert_eq!(data, expected);
620    }
621
622    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
623    #[test]
624    fn decode_legacy_receipt() {
625        let data = hex!(
626            "f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
627        );
628
629        // EIP658Receipt
630        let expected = ReceiptWithBloom {
631            receipt: Receipt {
632                tx_type: TxType::Legacy,
633                cumulative_gas_used: 0x1u64,
634                logs: vec![Log::new_unchecked(
635                    address!("0x0000000000000000000000000000000000000011"),
636                    vec![
637                        b256!("0x000000000000000000000000000000000000000000000000000000000000dead"),
638                        b256!("0x000000000000000000000000000000000000000000000000000000000000beef"),
639                    ],
640                    bytes!("0100ff"),
641                )],
642                success: false,
643            },
644            logs_bloom: [0; 256].into(),
645        };
646
647        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
648        assert_eq!(receipt, expected);
649    }
650
651    #[test]
652    fn gigantic_receipt() {
653        let receipt = Receipt {
654            cumulative_gas_used: 16747627,
655            success: true,
656            tx_type: TxType::Legacy,
657            logs: vec![
658                Log::new_unchecked(
659                    address!("0x4bf56695415f725e43c3e04354b604bcfb6dfb6e"),
660                    vec![b256!(
661                        "0xc69dc3d7ebff79e41f525be431d5cd3cc08f80eaf0f7819054a726eeb7086eb9"
662                    )],
663                    Bytes::from(vec![1; 0xffffff]),
664                ),
665                Log::new_unchecked(
666                    address!("0xfaca325c86bf9c2d5b413cd7b90b209be92229c2"),
667                    vec![b256!(
668                        "0x8cca58667b1e9ffa004720ac99a3d61a138181963b294d270d91c53d36402ae2"
669                    )],
670                    Bytes::from(vec![1; 0xffffff]),
671                ),
672            ],
673        };
674
675        let mut data = vec![];
676        receipt.to_compact(&mut data);
677        let (decoded, _) = Receipt::<TxType>::from_compact(&data[..], data.len());
678        assert_eq!(decoded, receipt);
679    }
680
681    #[test]
682    fn test_encode_2718_length() {
683        let receipt = ReceiptWithBloom {
684            receipt: Receipt {
685                tx_type: TxType::Eip1559,
686                success: true,
687                cumulative_gas_used: 21000,
688                logs: vec![],
689            },
690            logs_bloom: Bloom::default(),
691        };
692
693        let encoded = receipt.encoded_2718();
694        assert_eq!(
695            encoded.len(),
696            receipt.encode_2718_len(),
697            "Encoded length should match the actual encoded data length"
698        );
699
700        // Test for legacy receipt as well
701        let legacy_receipt = ReceiptWithBloom {
702            receipt: Receipt {
703                tx_type: TxType::Legacy,
704                success: true,
705                cumulative_gas_used: 21000,
706                logs: vec![],
707            },
708            logs_bloom: Bloom::default(),
709        };
710
711        let legacy_encoded = legacy_receipt.encoded_2718();
712        assert_eq!(
713            legacy_encoded.len(),
714            legacy_receipt.encode_2718_len(),
715            "Encoded length for legacy receipt should match the actual encoded data length"
716        );
717    }
718
719    #[test]
720    fn check_transaction_root() {
721        let data = &hex!(
722            "f90262f901f9a092230ce5476ae868e98c7979cfc165a93f8b6ad1922acf2df62e340916efd49da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa02307107a867056ca33b5087e77c4174f47625e48fb49f1c70ced34890ddd88f3a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba0c598f69a5674cae9337261b669970e24abc0b46e6d284372a239ec8ccbf20b0ab901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8618203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0"
723        );
724        let block_rlp = &mut data.as_slice();
725        let block: Block = Block::decode(block_rlp).unwrap();
726
727        let tx_root = calculate_transaction_root(&block.body.transactions);
728        assert_eq!(block.transactions_root, tx_root, "Must be the same");
729    }
730
731    #[test]
732    fn check_withdrawals_root() {
733        // Single withdrawal, amount 0
734        // https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/amountIs0.json
735        let data = &hex!(
736            "f90238f90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0046119afb1ab36aaa8f66088677ed96cd62762f6d3e65642898e189fbe702d51a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a048a703da164234812273ea083e4ec3d09d028300cd325b46a6a75402e5a7ab95c0c0d9d8808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b80"
737        );
738        let block: Block = Block::decode(&mut data.as_slice()).unwrap();
739        assert!(block.body.withdrawals.is_some());
740        let withdrawals = block.body.withdrawals.as_ref().unwrap();
741        assert_eq!(withdrawals.len(), 1);
742        let withdrawals_root = calculate_withdrawals_root(withdrawals);
743        assert_eq!(block.withdrawals_root, Some(withdrawals_root));
744
745        // 4 withdrawals, identical indices
746        // https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/twoIdenticalIndex.json
747        let data = &hex!(
748            "f9028cf90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0ccf7b62d616c2ad7af862d67b9dcd2119a90cebbff8c3cd1e5d7fc99f8755774a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a0a95b9a7b58a6b3cb4001eb0be67951c5517141cb0183a255b5cae027a7b10b36c0c0f86cda808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da018094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710"
749        );
750        let block: Block = Block::decode(&mut data.as_slice()).unwrap();
751        assert!(block.body.withdrawals.is_some());
752        let withdrawals = block.body.withdrawals.as_ref().unwrap();
753        assert_eq!(withdrawals.len(), 4);
754        let withdrawals_root = calculate_withdrawals_root(withdrawals);
755        assert_eq!(block.withdrawals_root, Some(withdrawals_root));
756    }
757    #[test]
758    fn check_receipt_root_optimism() {
759        use alloy_consensus::ReceiptWithBloom;
760
761        let logs = vec![Log {
762            address: Address::ZERO,
763            data: LogData::new_unchecked(vec![], Default::default()),
764        }];
765        let bloom = bloom!(
766            "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
767        );
768        let receipt = ReceiptWithBloom {
769            receipt: Receipt {
770                tx_type: TxType::Eip2930,
771                success: true,
772                cumulative_gas_used: 102068,
773                logs,
774            },
775            logs_bloom: bloom,
776        };
777        let receipt = vec![receipt];
778        let root = calculate_receipt_root(&receipt);
779        assert_eq!(
780            root,
781            b256!("0xfe70ae4a136d98944951b2123859698d59ad251a381abc9960fa81cae3d0d4a0")
782        );
783    }
784
785    // Ensures that reth and alloy receipts encode to the same JSON
786    #[test]
787    #[cfg(feature = "rpc")]
788    fn test_receipt_serde() {
789        let input = r#"{"status":"0x1","cumulativeGasUsed":"0x175cc0e","logs":[{"address":"0xa18b9ca2a78660d44ab38ae72e72b18792ffe413","topics":["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925","0x000000000000000000000000e7e7d8006cbff47bc6ac2dabf592c98e97502708","0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d"],"data":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","blockHash":"0xbf9e6a368a399f996a0f0b27cab4191c028c3c99f5f76ea08a5b70b961475fcb","blockNumber":"0x164b59f","blockTimestamp":"0x68c9a713","transactionHash":"0x533aa9e57865675bb94f41aa2895c0ac81eee69686c77af16149c301e19805f1","transactionIndex":"0x14d","logIndex":"0x238","removed":false}],"logsBloom":"0x00000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000400000040000000000000004000000000000000000000000000000000000000000000020000000000000000000000000080000000000000000000000000200000020000000000000000000000000000000000000000000000000000000000000020000010000000000000000000000000000000000000000000000000000000000000","type":"0x2","transactionHash":"0x533aa9e57865675bb94f41aa2895c0ac81eee69686c77af16149c301e19805f1","transactionIndex":"0x14d","blockHash":"0xbf9e6a368a399f996a0f0b27cab4191c028c3c99f5f76ea08a5b70b961475fcb","blockNumber":"0x164b59f","gasUsed":"0xb607","effectiveGasPrice":"0x4a3ee768","from":"0xe7e7d8006cbff47bc6ac2dabf592c98e97502708","to":"0xa18b9ca2a78660d44ab38ae72e72b18792ffe413","contractAddress":null}"#;
790        let receipt: RpcReceipt = serde_json::from_str(input).unwrap();
791        let envelope: ReceiptEnvelope<alloy_rpc_types_eth::Log> =
792            serde_json::from_str(input).unwrap();
793
794        assert_eq!(envelope, receipt.clone().into());
795
796        let json_envelope = serde_json::to_value(&envelope).unwrap();
797        let json_receipt = serde_json::to_value(receipt.into_with_bloom()).unwrap();
798        assert_eq!(json_envelope, json_receipt);
799    }
800}