reth_primitives/
receipt.rs

1use alloc::{vec, vec::Vec};
2use reth_primitives_traits::InMemorySize;
3
4use alloy_consensus::{
5    Eip2718EncodableReceipt, Eip658Value, ReceiptWithBloom, RlpDecodableReceipt,
6    RlpEncodableReceipt, TxReceipt, Typed2718,
7};
8use alloy_primitives::{Bloom, Log, B256};
9use alloy_rlp::{Decodable, Encodable, Header, RlpDecodable, RlpEncodable};
10use bytes::BufMut;
11use derive_more::{DerefMut, From, IntoIterator};
12use serde::{Deserialize, Serialize};
13
14use crate::TxType;
15
16/// Retrieves gas spent by transactions as a vector of tuples (transaction index, gas used).
17pub use reth_primitives_traits::receipt::gas_spent_by_transactions;
18
19/// Receipt containing result of transaction execution.
20#[derive(
21    Clone, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable, Serialize, Deserialize,
22)]
23#[cfg_attr(any(test, feature = "reth-codec"), derive(reth_codecs::CompactZstd))]
24#[cfg_attr(any(test, feature = "reth-codec"), reth_zstd(
25    compressor = reth_zstd_compressors::RECEIPT_COMPRESSOR,
26    decompressor = reth_zstd_compressors::RECEIPT_DECOMPRESSOR
27))]
28#[rlp(trailing)]
29pub struct Receipt {
30    /// Receipt type.
31    pub tx_type: TxType,
32    /// If transaction is executed successfully.
33    ///
34    /// This is the `statusCode`
35    pub success: bool,
36    /// Gas used
37    pub cumulative_gas_used: u64,
38    /// Log send from contracts.
39    pub logs: Vec<Log>,
40    /// Deposit nonce for Optimism deposit transactions
41    #[cfg(feature = "optimism")]
42    pub deposit_nonce: Option<u64>,
43    /// Deposit receipt version for Optimism deposit transactions
44    ///
45    ///
46    /// The deposit receipt version was introduced in Canyon to indicate an update to how
47    /// receipt hashes should be computed when set. The state transition process
48    /// ensures this is only set for post-Canyon deposit transactions.
49    #[cfg(feature = "optimism")]
50    pub deposit_receipt_version: Option<u64>,
51}
52
53impl Receipt {
54    /// Calculates [`Log`]'s bloom filter. this is slow operation and [`ReceiptWithBloom`] can
55    /// be used to cache this value.
56    pub fn bloom_slow(&self) -> Bloom {
57        alloy_primitives::logs_bloom(self.logs.iter())
58    }
59
60    /// Calculates the bloom filter for the receipt and returns the [`ReceiptWithBloom`] container
61    /// type.
62    pub fn with_bloom(self) -> ReceiptWithBloom<Self> {
63        self.into()
64    }
65
66    /// Calculates the bloom filter for the receipt and returns the [`ReceiptWithBloom`]
67    /// container type.
68    pub fn with_bloom_ref(&self) -> ReceiptWithBloom<&Self> {
69        self.into()
70    }
71
72    /// Returns length of RLP-encoded receipt fields with the given [`Bloom`] without an RLP header.
73    pub fn rlp_encoded_fields_length(&self, bloom: &Bloom) -> usize {
74        let len = self.success.length() +
75            self.cumulative_gas_used.length() +
76            bloom.length() +
77            self.logs.length();
78
79        #[cfg(feature = "optimism")]
80        if self.tx_type == TxType::Deposit {
81            let mut len = len;
82
83            if let Some(deposit_nonce) = self.deposit_nonce {
84                len += deposit_nonce.length();
85            }
86            if let Some(deposit_receipt_version) = self.deposit_receipt_version {
87                len += deposit_receipt_version.length();
88            }
89
90            return len
91        }
92
93        len
94    }
95
96    /// RLP-encodes receipt fields with the given [`Bloom`] without an RLP header.
97    pub fn rlp_encode_fields(&self, bloom: &Bloom, out: &mut dyn BufMut) {
98        self.success.encode(out);
99        self.cumulative_gas_used.encode(out);
100        bloom.encode(out);
101        self.logs.encode(out);
102
103        #[cfg(feature = "optimism")]
104        if self.tx_type == TxType::Deposit {
105            if let Some(nonce) = self.deposit_nonce {
106                nonce.encode(out);
107            }
108            if let Some(version) = self.deposit_receipt_version {
109                version.encode(out);
110            }
111        }
112    }
113
114    /// Returns RLP header for inner encoding.
115    pub fn rlp_header_inner(&self, bloom: &Bloom) -> Header {
116        Header { list: true, payload_length: self.rlp_encoded_fields_length(bloom) }
117    }
118
119    fn decode_receipt_with_bloom(
120        buf: &mut &[u8],
121        tx_type: TxType,
122    ) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
123        let b = &mut &**buf;
124        let rlp_head = alloy_rlp::Header::decode(b)?;
125        if !rlp_head.list {
126            return Err(alloy_rlp::Error::UnexpectedString)
127        }
128        let started_len = b.len();
129
130        let success = Decodable::decode(b)?;
131        let cumulative_gas_used = Decodable::decode(b)?;
132        let bloom = Decodable::decode(b)?;
133        let logs = Decodable::decode(b)?;
134
135        let receipt = match tx_type {
136            #[cfg(feature = "optimism")]
137            TxType::Deposit => {
138                let remaining = |b: &[u8]| rlp_head.payload_length - (started_len - b.len()) > 0;
139                let deposit_nonce = remaining(b).then(|| Decodable::decode(b)).transpose()?;
140                let deposit_receipt_version =
141                    remaining(b).then(|| Decodable::decode(b)).transpose()?;
142
143                Self {
144                    tx_type,
145                    success,
146                    cumulative_gas_used,
147                    logs,
148                    deposit_nonce,
149                    deposit_receipt_version,
150                }
151            }
152            _ => Self {
153                tx_type,
154                success,
155                cumulative_gas_used,
156                logs,
157                #[cfg(feature = "optimism")]
158                deposit_nonce: None,
159                #[cfg(feature = "optimism")]
160                deposit_receipt_version: None,
161            },
162        };
163
164        let this = ReceiptWithBloom { receipt, logs_bloom: bloom };
165        let consumed = started_len - b.len();
166        if consumed != rlp_head.payload_length {
167            return Err(alloy_rlp::Error::ListLengthMismatch {
168                expected: rlp_head.payload_length,
169                got: consumed,
170            })
171        }
172        *buf = *b;
173        Ok(this)
174    }
175}
176
177impl Eip2718EncodableReceipt for Receipt {
178    fn eip2718_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
179        self.rlp_header_inner(bloom).length_with_payload() +
180            !matches!(self.tx_type, TxType::Legacy) as usize // account for type prefix
181    }
182
183    fn eip2718_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
184        if !matches!(self.tx_type, TxType::Legacy) {
185            out.put_u8(self.tx_type as u8);
186        }
187        self.rlp_header_inner(bloom).encode(out);
188        self.rlp_encode_fields(bloom, out);
189    }
190}
191
192impl RlpEncodableReceipt for Receipt {
193    fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
194        let mut len = self.eip2718_encoded_length_with_bloom(bloom);
195        if !matches!(self.tx_type, TxType::Legacy) {
196            len += Header {
197                list: false,
198                payload_length: self.eip2718_encoded_length_with_bloom(bloom),
199            }
200            .length();
201        }
202
203        len
204    }
205
206    fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
207        if !matches!(self.tx_type, TxType::Legacy) {
208            Header { list: false, payload_length: self.eip2718_encoded_length_with_bloom(bloom) }
209                .encode(out);
210        }
211        self.eip2718_encode_with_bloom(bloom, out);
212    }
213}
214
215impl RlpDecodableReceipt for Receipt {
216    fn rlp_decode_with_bloom(buf: &mut &[u8]) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
217        let header_buf = &mut &**buf;
218        let header = Header::decode(header_buf)?;
219
220        if header.list {
221            return Self::decode_receipt_with_bloom(buf, TxType::Legacy);
222        }
223
224        *buf = *header_buf;
225
226        let remaining = buf.len();
227        let tx_type = TxType::decode(buf)?;
228        let this = Self::decode_receipt_with_bloom(buf, tx_type)?;
229
230        if buf.len() + header.payload_length != remaining {
231            return Err(alloy_rlp::Error::UnexpectedLength);
232        }
233
234        Ok(this)
235    }
236}
237
238impl TxReceipt for Receipt {
239    type Log = Log;
240
241    fn status_or_post_state(&self) -> Eip658Value {
242        self.success.into()
243    }
244
245    fn status(&self) -> bool {
246        self.success
247    }
248
249    fn bloom(&self) -> Bloom {
250        alloy_primitives::logs_bloom(self.logs.iter())
251    }
252
253    fn cumulative_gas_used(&self) -> u128 {
254        self.cumulative_gas_used as u128
255    }
256
257    fn logs(&self) -> &[Log] {
258        &self.logs
259    }
260}
261
262impl Typed2718 for Receipt {
263    fn ty(&self) -> u8 {
264        self.tx_type as u8
265    }
266}
267
268impl reth_primitives_traits::Receipt for Receipt {}
269
270impl InMemorySize for Receipt {
271    /// Calculates a heuristic for the in-memory size of the [Receipt].
272    #[inline]
273    fn size(&self) -> usize {
274        let total_size = self.tx_type.size() +
275            core::mem::size_of::<bool>() +
276            core::mem::size_of::<u64>() +
277            self.logs.capacity() * core::mem::size_of::<Log>();
278
279        #[cfg(feature = "optimism")]
280        return total_size + 2 * core::mem::size_of::<Option<u64>>();
281        #[cfg(not(feature = "optimism"))]
282        total_size
283    }
284}
285
286/// A collection of receipts organized as a two-dimensional vector.
287#[derive(
288    Clone,
289    Debug,
290    PartialEq,
291    Eq,
292    Serialize,
293    Deserialize,
294    From,
295    derive_more::Deref,
296    DerefMut,
297    IntoIterator,
298)]
299pub struct Receipts<T = Receipt> {
300    /// A two-dimensional vector of optional `Receipt` instances.
301    pub receipt_vec: Vec<Vec<Option<T>>>,
302}
303
304impl<T> Receipts<T> {
305    /// Returns the length of the `Receipts` vector.
306    pub fn len(&self) -> usize {
307        self.receipt_vec.len()
308    }
309
310    /// Returns true if the `Receipts` vector is empty.
311    pub fn is_empty(&self) -> bool {
312        self.receipt_vec.is_empty()
313    }
314
315    /// Push a new vector of receipts into the `Receipts` collection.
316    pub fn push(&mut self, receipts: Vec<Option<T>>) {
317        self.receipt_vec.push(receipts);
318    }
319
320    /// Retrieves all recorded receipts from index and calculates the root using the given closure.
321    pub fn root_slow(&self, index: usize, f: impl FnOnce(&[&T]) -> B256) -> Option<B256> {
322        let receipts =
323            self.receipt_vec[index].iter().map(Option::as_ref).collect::<Option<Vec<_>>>()?;
324        Some(f(receipts.as_slice()))
325    }
326}
327
328impl<T> From<Vec<T>> for Receipts<T> {
329    fn from(block_receipts: Vec<T>) -> Self {
330        Self { receipt_vec: vec![block_receipts.into_iter().map(Option::Some).collect()] }
331    }
332}
333
334impl<T> FromIterator<Vec<Option<T>>> for Receipts<T> {
335    fn from_iter<I: IntoIterator<Item = Vec<Option<T>>>>(iter: I) -> Self {
336        iter.into_iter().collect::<Vec<_>>().into()
337    }
338}
339
340impl<T> Default for Receipts<T> {
341    fn default() -> Self {
342        Self { receipt_vec: Vec::new() }
343    }
344}
345
346#[cfg(any(test, feature = "arbitrary"))]
347impl<'a> arbitrary::Arbitrary<'a> for Receipt {
348    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
349        let tx_type = TxType::arbitrary(u)?;
350        let success = bool::arbitrary(u)?;
351        let cumulative_gas_used = u64::arbitrary(u)?;
352        let logs = Vec::<Log>::arbitrary(u)?;
353
354        // Only receipts for deposit transactions may contain a deposit nonce
355        #[cfg(feature = "optimism")]
356        let (deposit_nonce, deposit_receipt_version) = if tx_type == TxType::Deposit {
357            let deposit_nonce = Option::<u64>::arbitrary(u)?;
358            let deposit_nonce_version =
359                deposit_nonce.map(|_| Option::<u64>::arbitrary(u)).transpose()?.flatten();
360            (deposit_nonce, deposit_nonce_version)
361        } else {
362            (None, None)
363        };
364
365        Ok(Self {
366            tx_type,
367            success,
368            cumulative_gas_used,
369            logs,
370            #[cfg(feature = "optimism")]
371            deposit_nonce,
372            #[cfg(feature = "optimism")]
373            deposit_receipt_version,
374        })
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use alloy_eips::eip2718::Encodable2718;
382    use alloy_primitives::{address, b256, bytes, hex_literal::hex, Bytes};
383    use reth_codecs::Compact;
384
385    #[test]
386    fn test_decode_receipt() {
387        #[cfg(not(feature = "optimism"))]
388        reth_codecs::test_utils::test_decode::<Receipt>(&hex!(
389            "c428b52ffd23fc42696156b10200f034792b6a94c3850215c2fef7aea361a0c31b79d9a32652eefc0d4e2e730036061cff7344b6fc6132b50cda0ed810a991ae58ef013150c12b2522533cb3b3a8b19b7786a8b5ff1d3cdc84225e22b02def168c8858df"
390        ));
391        #[cfg(feature = "optimism")]
392        reth_codecs::test_utils::test_decode::<Receipt>(&hex!(
393            "c30328b52ffd23fc426961a00105007eb0042307705a97e503562eacf2b95060cce9de6de68386b6c155b73a9650021a49e2f8baad17f30faff5899d785c4c0873e45bc268bcf07560106424570d11f9a59e8f3db1efa4ceec680123712275f10d92c3411e1caaa11c7c5d591bc11487168e09934a9986848136da1b583babf3a7188e3aed007a1520f1cf4c1ca7d3482c6c28d37c298613c70a76940008816c4c95644579fd08471dc34732fd0f24"
394        ));
395    }
396
397    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
398    #[test]
399    fn encode_legacy_receipt() {
400        let expected = hex!("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff");
401
402        let mut data = Vec::with_capacity(expected.length());
403        let receipt = ReceiptWithBloom {
404            receipt: Receipt {
405                tx_type: TxType::Legacy,
406                cumulative_gas_used: 0x1u64,
407                logs: vec![Log::new_unchecked(
408                    address!("0000000000000000000000000000000000000011"),
409                    vec![
410                        b256!("000000000000000000000000000000000000000000000000000000000000dead"),
411                        b256!("000000000000000000000000000000000000000000000000000000000000beef"),
412                    ],
413                    bytes!("0100ff"),
414                )],
415                success: false,
416                #[cfg(feature = "optimism")]
417                deposit_nonce: None,
418                #[cfg(feature = "optimism")]
419                deposit_receipt_version: None,
420            },
421            logs_bloom: [0; 256].into(),
422        };
423
424        receipt.encode(&mut data);
425
426        // check that the rlp length equals the length of the expected rlp
427        assert_eq!(receipt.length(), expected.len());
428        assert_eq!(data, expected);
429    }
430
431    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
432    #[test]
433    fn decode_legacy_receipt() {
434        let data = hex!("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff");
435
436        // EIP658Receipt
437        let expected = ReceiptWithBloom {
438            receipt: Receipt {
439                tx_type: TxType::Legacy,
440                cumulative_gas_used: 0x1u64,
441                logs: vec![Log::new_unchecked(
442                    address!("0000000000000000000000000000000000000011"),
443                    vec![
444                        b256!("000000000000000000000000000000000000000000000000000000000000dead"),
445                        b256!("000000000000000000000000000000000000000000000000000000000000beef"),
446                    ],
447                    bytes!("0100ff"),
448                )],
449                success: false,
450                #[cfg(feature = "optimism")]
451                deposit_nonce: None,
452                #[cfg(feature = "optimism")]
453                deposit_receipt_version: None,
454            },
455            logs_bloom: [0; 256].into(),
456        };
457
458        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
459        assert_eq!(receipt, expected);
460    }
461
462    #[cfg(feature = "optimism")]
463    #[test]
464    fn decode_deposit_receipt_regolith_roundtrip() {
465        let data = hex!("b901107ef9010c0182b741b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0833d3bbf");
466
467        // Deposit Receipt (post-regolith)
468        let expected = ReceiptWithBloom {
469            receipt: Receipt {
470                tx_type: TxType::Deposit,
471                cumulative_gas_used: 46913,
472                logs: vec![],
473                success: true,
474                deposit_nonce: Some(4012991),
475                deposit_receipt_version: None,
476            },
477            logs_bloom: [0; 256].into(),
478        };
479
480        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
481        assert_eq!(receipt, expected);
482
483        let mut buf = Vec::with_capacity(data.len());
484        receipt.encode(&mut buf);
485        assert_eq!(buf, &data[..]);
486    }
487
488    #[cfg(feature = "optimism")]
489    #[test]
490    fn decode_deposit_receipt_canyon_roundtrip() {
491        let data = hex!("b901117ef9010d0182b741b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0833d3bbf01");
492
493        // Deposit Receipt (post-regolith)
494        let expected = ReceiptWithBloom {
495            receipt: Receipt {
496                tx_type: TxType::Deposit,
497                cumulative_gas_used: 46913,
498                logs: vec![],
499                success: true,
500                deposit_nonce: Some(4012991),
501                deposit_receipt_version: Some(1),
502            },
503            logs_bloom: [0; 256].into(),
504        };
505
506        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
507        assert_eq!(receipt, expected);
508
509        let mut buf = Vec::with_capacity(data.len());
510        expected.encode(&mut buf);
511        assert_eq!(buf, &data[..]);
512    }
513
514    #[test]
515    fn gigantic_receipt() {
516        let receipt = Receipt {
517            cumulative_gas_used: 16747627,
518            success: true,
519            tx_type: TxType::Legacy,
520            logs: vec![
521                Log::new_unchecked(
522                    address!("4bf56695415f725e43c3e04354b604bcfb6dfb6e"),
523                    vec![b256!("c69dc3d7ebff79e41f525be431d5cd3cc08f80eaf0f7819054a726eeb7086eb9")],
524                    Bytes::from(vec![1; 0xffffff]),
525                ),
526                Log::new_unchecked(
527                    address!("faca325c86bf9c2d5b413cd7b90b209be92229c2"),
528                    vec![b256!("8cca58667b1e9ffa004720ac99a3d61a138181963b294d270d91c53d36402ae2")],
529                    Bytes::from(vec![1; 0xffffff]),
530                ),
531            ],
532            #[cfg(feature = "optimism")]
533            deposit_nonce: None,
534            #[cfg(feature = "optimism")]
535            deposit_receipt_version: None,
536        };
537
538        let mut data = vec![];
539        receipt.to_compact(&mut data);
540        let (decoded, _) = Receipt::from_compact(&data[..], data.len());
541        assert_eq!(decoded, receipt);
542    }
543
544    #[test]
545    fn test_encode_2718_length() {
546        let receipt = ReceiptWithBloom {
547            receipt: Receipt {
548                tx_type: TxType::Eip1559,
549                success: true,
550                cumulative_gas_used: 21000,
551                logs: vec![],
552                #[cfg(feature = "optimism")]
553                deposit_nonce: None,
554                #[cfg(feature = "optimism")]
555                deposit_receipt_version: None,
556            },
557            logs_bloom: Bloom::default(),
558        };
559
560        let encoded = receipt.encoded_2718();
561        assert_eq!(
562            encoded.len(),
563            receipt.encode_2718_len(),
564            "Encoded length should match the actual encoded data length"
565        );
566
567        // Test for legacy receipt as well
568        let legacy_receipt = ReceiptWithBloom {
569            receipt: Receipt {
570                tx_type: TxType::Legacy,
571                success: true,
572                cumulative_gas_used: 21000,
573                logs: vec![],
574                #[cfg(feature = "optimism")]
575                deposit_nonce: None,
576                #[cfg(feature = "optimism")]
577                deposit_receipt_version: None,
578            },
579            logs_bloom: Bloom::default(),
580        };
581
582        let legacy_encoded = legacy_receipt.encoded_2718();
583        assert_eq!(
584            legacy_encoded.len(),
585            legacy_receipt.encode_2718_len(),
586            "Encoded length for legacy receipt should match the actual encoded data length"
587        );
588    }
589}