Skip to main content

alloy_consensus/receipt/
receipts.rs

1use crate::receipt::{
2    Eip2718DecodableReceipt, Eip2718EncodableReceipt, Eip658Value, RlpDecodableReceipt,
3    RlpEncodableReceipt, TxReceipt,
4};
5use alloc::{vec, vec::Vec};
6use alloy_eips::{
7    eip2718::{Eip2718Result, Encodable2718},
8    Decodable2718, Typed2718,
9};
10use alloy_primitives::{Bloom, Log};
11use alloy_rlp::{BufMut, Decodable, Encodable, Header};
12use core::fmt;
13
14/// Receipt containing result of transaction execution.
15#[derive(Clone, Debug, Default, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
17#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
18#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
19#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
20#[doc(alias = "TransactionReceipt", alias = "TxReceipt")]
21pub struct Receipt<T = Log> {
22    /// If transaction is executed successfully.
23    ///
24    /// This is the `statusCode`
25    #[cfg_attr(feature = "serde", serde(flatten))]
26    pub status: Eip658Value,
27    /// Gas used
28    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
29    pub cumulative_gas_used: u64,
30    /// Log send from contracts.
31    pub logs: Vec<T>,
32}
33
34impl<T> Receipt<T> {
35    /// Converts the receipt's log type by applying a function to each log.
36    ///
37    /// Returns the receipt with the new log type
38    pub fn map_logs<U>(self, f: impl FnMut(T) -> U) -> Receipt<U> {
39        let Self { status, cumulative_gas_used, logs } = self;
40        Receipt { status, cumulative_gas_used, logs: logs.into_iter().map(f).collect() }
41    }
42}
43
44impl<T> Receipt<T>
45where
46    T: AsRef<Log>,
47{
48    /// Calculates [`Log`]'s bloom filter. This is slow operation and
49    /// [`ReceiptWithBloom`] can be used to cache this value.
50    pub fn bloom_slow(&self) -> Bloom {
51        self.logs.iter().map(AsRef::as_ref).collect()
52    }
53
54    /// Calculates the bloom filter for the receipt and returns the
55    /// [`ReceiptWithBloom`] container type.
56    pub fn with_bloom(self) -> ReceiptWithBloom<Self> {
57        ReceiptWithBloom { logs_bloom: self.bloom_slow(), receipt: self }
58    }
59}
60
61impl<T> Receipt<T>
62where
63    T: Into<Log>,
64{
65    /// Converts a [`Receipt`] with a custom log type into a [`Receipt`] with the primitives [`Log`]
66    /// type by converting the logs.
67    ///
68    /// This is useful if log types that embed the primitives log type, e.g. the log receipt rpc
69    /// type.
70    pub fn into_primitives_receipt(self) -> Receipt<Log> {
71        self.map_logs(Into::into)
72    }
73}
74
75impl<T> TxReceipt for Receipt<T>
76where
77    T: AsRef<Log> + Clone + fmt::Debug + PartialEq + Eq + Send + Sync,
78{
79    type Log = T;
80
81    fn status_or_post_state(&self) -> Eip658Value {
82        self.status
83    }
84
85    fn status(&self) -> bool {
86        self.status.coerce_status()
87    }
88
89    fn bloom(&self) -> Bloom {
90        self.bloom_slow()
91    }
92
93    fn cumulative_gas_used(&self) -> u64 {
94        self.cumulative_gas_used
95    }
96
97    fn logs(&self) -> &[Self::Log] {
98        &self.logs
99    }
100
101    fn into_logs(self) -> Vec<Self::Log>
102    where
103        Self::Log: Clone,
104    {
105        self.logs
106    }
107}
108
109impl<T: Encodable> Receipt<T> {
110    /// Returns length of RLP-encoded receipt fields with the given [`Bloom`] without an RLP header.
111    pub fn rlp_encoded_fields_length_with_bloom(&self, bloom: &Bloom) -> usize {
112        self.status.length()
113            + self.cumulative_gas_used.length()
114            + bloom.length()
115            + self.logs.length()
116    }
117
118    /// RLP-encodes receipt fields with the given [`Bloom`] without an RLP header.
119    pub fn rlp_encode_fields_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
120        self.status.encode(out);
121        self.cumulative_gas_used.encode(out);
122        bloom.encode(out);
123        self.logs.encode(out);
124    }
125
126    /// Returns RLP header for this receipt encoding with the given [`Bloom`].
127    pub fn rlp_header_with_bloom(&self, bloom: &Bloom) -> Header {
128        Header { list: true, payload_length: self.rlp_encoded_fields_length_with_bloom(bloom) }
129    }
130}
131
132impl<T: Encodable> RlpEncodableReceipt for Receipt<T> {
133    fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
134        self.rlp_header_with_bloom(bloom).length_with_payload()
135    }
136
137    fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
138        self.rlp_header_with_bloom(bloom).encode(out);
139        self.rlp_encode_fields_with_bloom(bloom, out);
140    }
141}
142
143impl<T: Decodable> Receipt<T> {
144    /// RLP-decodes receipt's field with a [`Bloom`].
145    ///
146    /// Does not expect an RLP header.
147    pub fn rlp_decode_fields_with_bloom(
148        buf: &mut &[u8],
149    ) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
150        let status = Decodable::decode(buf)?;
151        let cumulative_gas_used = Decodable::decode(buf)?;
152        let logs_bloom = Decodable::decode(buf)?;
153        let logs = Decodable::decode(buf)?;
154
155        Ok(ReceiptWithBloom { receipt: Self { status, cumulative_gas_used, logs }, logs_bloom })
156    }
157}
158
159impl<T: Decodable> RlpDecodableReceipt for Receipt<T> {
160    fn rlp_decode_with_bloom(buf: &mut &[u8]) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
161        let header = Header::decode(buf)?;
162        if !header.list {
163            return Err(alloy_rlp::Error::UnexpectedString);
164        }
165
166        let remaining = buf.len();
167
168        let this = Self::rlp_decode_fields_with_bloom(buf)?;
169
170        if buf.len() + header.payload_length != remaining {
171            return Err(alloy_rlp::Error::UnexpectedLength);
172        }
173
174        Ok(this)
175    }
176}
177
178impl<T> From<ReceiptWithBloom<Self>> for Receipt<T> {
179    /// Consume the structure, returning only the receipt
180    fn from(receipt_with_bloom: ReceiptWithBloom<Self>) -> Self {
181        receipt_with_bloom.receipt
182    }
183}
184
185/// A collection of receipts organized as a two-dimensional vector.
186#[derive(
187    Clone,
188    Debug,
189    PartialEq,
190    Eq,
191    derive_more::Deref,
192    derive_more::DerefMut,
193    derive_more::From,
194    derive_more::IntoIterator,
195)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197pub struct Receipts<T> {
198    /// A two-dimensional vector of [`Receipt`] instances.
199    pub receipt_vec: Vec<Vec<T>>,
200}
201
202impl<T> Receipts<T> {
203    /// Returns the length of the [`Receipts`] vector.
204    pub const fn len(&self) -> usize {
205        self.receipt_vec.len()
206    }
207
208    /// Returns `true` if the [`Receipts`] vector is empty.
209    pub const fn is_empty(&self) -> bool {
210        self.receipt_vec.is_empty()
211    }
212
213    /// Push a new vector of receipts into the [`Receipts`] collection.
214    pub fn push(&mut self, receipts: Vec<T>) {
215        self.receipt_vec.push(receipts);
216    }
217}
218
219impl<T> From<Vec<T>> for Receipts<T> {
220    fn from(block_receipts: Vec<T>) -> Self {
221        Self { receipt_vec: vec![block_receipts] }
222    }
223}
224
225impl<T> FromIterator<Vec<T>> for Receipts<T> {
226    fn from_iter<I: IntoIterator<Item = Vec<T>>>(iter: I) -> Self {
227        Self { receipt_vec: iter.into_iter().collect() }
228    }
229}
230
231impl<T: Encodable> Encodable for Receipts<T> {
232    fn encode(&self, out: &mut dyn BufMut) {
233        self.receipt_vec.encode(out)
234    }
235
236    fn length(&self) -> usize {
237        self.receipt_vec.length()
238    }
239}
240
241impl<T: Decodable> Decodable for Receipts<T> {
242    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
243        Ok(Self { receipt_vec: Decodable::decode(buf)? })
244    }
245}
246
247impl<T> Default for Receipts<T> {
248    fn default() -> Self {
249        Self { receipt_vec: Default::default() }
250    }
251}
252
253/// A receipt paired with a precomputed logs bloom.
254///
255/// Consistency is not verified or automatically maintained. Prefer [`Receipt::with_bloom`] or
256/// [`From<R>`](From) to calculate it, and recompute it after changing bloom-relevant log addresses
257/// or topics.
258#[derive(Clone, Debug, Default, PartialEq, Eq)]
259#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
260#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
261#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
262#[doc(alias = "TransactionReceiptWithBloom", alias = "TxReceiptWithBloom")]
263pub struct ReceiptWithBloom<T = Receipt<Log>> {
264    #[cfg_attr(feature = "serde", serde(flatten))]
265    /// The receipt.
266    pub receipt: T,
267    /// The bloom filter.
268    pub logs_bloom: Bloom,
269}
270
271impl<R> TxReceipt for ReceiptWithBloom<R>
272where
273    R: TxReceipt,
274{
275    type Log = R::Log;
276
277    fn status_or_post_state(&self) -> Eip658Value {
278        self.receipt.status_or_post_state()
279    }
280
281    fn status(&self) -> bool {
282        self.receipt.status()
283    }
284
285    fn bloom(&self) -> Bloom {
286        self.logs_bloom
287    }
288
289    fn bloom_cheap(&self) -> Option<Bloom> {
290        Some(self.logs_bloom)
291    }
292
293    fn cumulative_gas_used(&self) -> u64 {
294        self.receipt.cumulative_gas_used()
295    }
296
297    fn logs(&self) -> &[Self::Log] {
298        self.receipt.logs()
299    }
300
301    fn into_logs(self) -> Vec<Self::Log>
302    where
303        Self::Log: Clone,
304    {
305        self.receipt.into_logs()
306    }
307}
308
309impl<R> From<R> for ReceiptWithBloom<R>
310where
311    R: TxReceipt,
312{
313    fn from(receipt: R) -> Self {
314        let logs_bloom = receipt.bloom();
315        Self { logs_bloom, receipt }
316    }
317}
318
319impl<R> ReceiptWithBloom<R> {
320    /// Converts the receipt type while reusing the existing bloom without validation.
321    pub fn map_receipt<U>(self, f: impl FnOnce(R) -> U) -> ReceiptWithBloom<U> {
322        let Self { receipt, logs_bloom } = self;
323        ReceiptWithBloom { receipt: f(receipt), logs_bloom }
324    }
325
326    /// Creates a receipt with a caller-supplied bloom without verifying consistency.
327    pub const fn new(receipt: R, logs_bloom: Bloom) -> Self {
328        Self { receipt, logs_bloom }
329    }
330
331    /// Consume the structure, returning the receipt and the bloom filter
332    pub fn into_components(self) -> (R, Bloom) {
333        (self.receipt, self.logs_bloom)
334    }
335
336    /// Returns a reference to the bloom.
337    pub const fn bloom_ref(&self) -> &Bloom {
338        &self.logs_bloom
339    }
340}
341
342impl<L> ReceiptWithBloom<Receipt<L>> {
343    /// Converts the receipt's log type while reusing the existing bloom.
344    ///
345    /// Use this only when the mapping preserves bloom-relevant log addresses and topics.
346    pub fn map_logs<U>(self, f: impl FnMut(L) -> U) -> ReceiptWithBloom<Receipt<U>> {
347        let Self { receipt, logs_bloom } = self;
348        ReceiptWithBloom { receipt: receipt.map_logs(f), logs_bloom }
349    }
350
351    /// Converts a [`ReceiptWithBloom`] with a custom log type into a [`ReceiptWithBloom`] with the
352    /// primitives [`Log`] type by converting the logs.
353    ///
354    /// This is useful if log types that embed the primitives log type, e.g. the log receipt rpc
355    /// type.
356    pub fn into_primitives_receipt(self) -> ReceiptWithBloom<Receipt<Log>>
357    where
358        L: Into<Log>,
359    {
360        self.map_logs(Into::into)
361    }
362}
363
364impl<R: RlpEncodableReceipt> Encodable for ReceiptWithBloom<R> {
365    fn encode(&self, out: &mut dyn BufMut) {
366        self.receipt.rlp_encode_with_bloom(&self.logs_bloom, out);
367    }
368
369    fn length(&self) -> usize {
370        self.receipt.rlp_encoded_length_with_bloom(&self.logs_bloom)
371    }
372}
373
374impl<R: RlpDecodableReceipt> Decodable for ReceiptWithBloom<R> {
375    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
376        R::rlp_decode_with_bloom(buf)
377    }
378}
379
380impl<R: Typed2718> Typed2718 for ReceiptWithBloom<R> {
381    fn ty(&self) -> u8 {
382        self.receipt.ty()
383    }
384}
385
386impl<R> Encodable2718 for ReceiptWithBloom<R>
387where
388    R: Eip2718EncodableReceipt + Send + Sync,
389{
390    fn encode_2718_len(&self) -> usize {
391        self.receipt.eip2718_encoded_length_with_bloom(&self.logs_bloom)
392    }
393
394    fn encode_2718(&self, out: &mut dyn BufMut) {
395        self.receipt.eip2718_encode_with_bloom(&self.logs_bloom, out);
396    }
397}
398
399impl<R> Decodable2718 for ReceiptWithBloom<R>
400where
401    R: Eip2718DecodableReceipt,
402{
403    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
404        R::typed_decode_with_bloom(ty, buf)
405    }
406
407    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
408        R::fallback_decode_with_bloom(buf)
409    }
410}
411
412#[cfg(any(test, feature = "arbitrary"))]
413impl<'a, R> arbitrary::Arbitrary<'a> for ReceiptWithBloom<R>
414where
415    R: arbitrary::Arbitrary<'a>,
416{
417    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
418        Ok(Self { receipt: R::arbitrary(u)?, logs_bloom: Bloom::arbitrary(u)? })
419    }
420}
421
422#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
423pub(crate) mod serde_bincode_compat {
424    use alloc::borrow::Cow;
425    use serde::{Deserialize, Deserializer, Serialize, Serializer};
426    use serde_with::{DeserializeAs, SerializeAs};
427
428    /// Bincode-compatible [`super::Receipt`] serde implementation.
429    ///
430    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
431    /// ```rust
432    /// use alloy_consensus::{serde_bincode_compat, Receipt};
433    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
434    /// use serde_with::serde_as;
435    ///
436    /// #[serde_as]
437    /// #[derive(Serialize, Deserialize)]
438    /// struct Data<T: Serialize + DeserializeOwned + Clone + 'static> {
439    ///     #[serde_as(as = "serde_bincode_compat::Receipt<'_, T>")]
440    ///     receipt: Receipt<T>,
441    /// }
442    /// ```
443    #[derive(Debug, Serialize, Deserialize)]
444    pub struct Receipt<'a, T: Clone = alloy_primitives::Log> {
445        logs: Cow<'a, [T]>,
446        status: bool,
447        cumulative_gas_used: u64,
448    }
449
450    impl<'a, T: Clone> From<&'a super::Receipt<T>> for Receipt<'a, T> {
451        fn from(value: &'a super::Receipt<T>) -> Self {
452            Self {
453                logs: Cow::Borrowed(&value.logs),
454                // OP has no post state root variant
455                status: value.status.coerce_status(),
456                cumulative_gas_used: value.cumulative_gas_used,
457            }
458        }
459    }
460
461    impl<'a, T: Clone> From<Receipt<'a, T>> for super::Receipt<T> {
462        fn from(value: Receipt<'a, T>) -> Self {
463            Self {
464                status: value.status.into(),
465                cumulative_gas_used: value.cumulative_gas_used,
466                logs: value.logs.into_owned(),
467            }
468        }
469    }
470
471    impl<T: Serialize + Clone> SerializeAs<super::Receipt<T>> for Receipt<'_, T> {
472        fn serialize_as<S>(source: &super::Receipt<T>, serializer: S) -> Result<S::Ok, S::Error>
473        where
474            S: Serializer,
475        {
476            Receipt::<'_, T>::from(source).serialize(serializer)
477        }
478    }
479
480    impl<'de, T: Deserialize<'de> + Clone> DeserializeAs<'de, super::Receipt<T>> for Receipt<'de, T> {
481        fn deserialize_as<D>(deserializer: D) -> Result<super::Receipt<T>, D::Error>
482        where
483            D: Deserializer<'de>,
484        {
485            Receipt::<'_, T>::deserialize(deserializer).map(Into::into)
486        }
487    }
488
489    #[cfg(test)]
490    mod tests {
491        use super::super::{serde_bincode_compat, Receipt};
492        use alloy_primitives::Log;
493        use arbitrary::Arbitrary;
494        use bincode::config;
495        use rand::Rng;
496        use serde::{de::DeserializeOwned, Deserialize, Serialize};
497        use serde_with::serde_as;
498
499        #[test]
500        fn test_receipt_bincode_roundtrip() {
501            #[serde_as]
502            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
503            struct Data<T: Serialize + DeserializeOwned + Clone + 'static> {
504                #[serde_as(as = "serde_bincode_compat::Receipt<'_,T>")]
505                receipt: Receipt<T>,
506            }
507
508            let mut bytes = [0u8; 1024];
509            rand::thread_rng().fill(bytes.as_mut_slice());
510            let mut data = Data {
511                receipt: Receipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(),
512            };
513            // ensure we don't have an invalid poststate variant
514            data.receipt.status = data.receipt.status.coerce_status().into();
515
516            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
517            let (decoded, _) =
518                bincode::serde::decode_from_slice::<Data<Log>, _>(&encoded, config::legacy())
519                    .unwrap();
520            assert_eq!(decoded, data);
521        }
522    }
523}
524
525#[cfg(test)]
526mod test {
527    use super::*;
528    use crate::ReceiptEnvelope;
529    use alloy_rlp::{Decodable, Encodable};
530
531    const fn assert_tx_receipt<T: TxReceipt>() {}
532
533    #[test]
534    const fn assert_receipt() {
535        assert_tx_receipt::<Receipt>();
536        assert_tx_receipt::<ReceiptWithBloom<Receipt>>();
537    }
538
539    #[cfg(feature = "serde")]
540    #[test]
541    fn root_vs_status() {
542        let receipt = super::Receipt::<()> {
543            status: super::Eip658Value::Eip658(true),
544            cumulative_gas_used: 0,
545            logs: Vec::new(),
546        };
547
548        let json = serde_json::to_string(&receipt).unwrap();
549        assert_eq!(json, r#"{"status":"0x1","cumulativeGasUsed":"0x0","logs":[]}"#);
550
551        let receipt = super::Receipt::<()> {
552            status: super::Eip658Value::PostState(Default::default()),
553            cumulative_gas_used: 0,
554            logs: Vec::new(),
555        };
556
557        let json = serde_json::to_string(&receipt).unwrap();
558        assert_eq!(
559            json,
560            r#"{"root":"0x0000000000000000000000000000000000000000000000000000000000000000","cumulativeGasUsed":"0x0","logs":[]}"#
561        );
562    }
563
564    #[cfg(feature = "serde")]
565    #[test]
566    fn deser_pre658() {
567        use alloy_primitives::b256;
568
569        let json = r#"{"root":"0x284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10","cumulativeGasUsed":"0x0","logs":[]}"#;
570
571        let receipt: super::Receipt<()> = serde_json::from_str(json).unwrap();
572
573        assert_eq!(
574            receipt.status,
575            super::Eip658Value::PostState(b256!(
576                "284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10"
577            ))
578        );
579    }
580
581    #[test]
582    fn roundtrip_encodable_eip1559() {
583        let receipts =
584            Receipts { receipt_vec: vec![vec![ReceiptEnvelope::Eip1559(Default::default())]] };
585
586        let mut out = vec![];
587        receipts.encode(&mut out);
588
589        let mut out = out.as_slice();
590        let decoded = Receipts::<ReceiptEnvelope>::decode(&mut out).unwrap();
591
592        assert_eq!(receipts, decoded);
593    }
594}