Skip to main content

alloy_consensus/receipt/
envelope.rs

1use crate::{Eip658Value, Receipt, ReceiptWithBloom, TxReceipt, TxType};
2use alloc::vec::Vec;
3use alloy_eips::{
4    eip2718::{
5        Decodable2718, Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718, EIP1559_TX_TYPE_ID,
6        EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID, LEGACY_TX_TYPE_ID,
7    },
8    Typed2718,
9};
10use alloy_primitives::{Bloom, Log};
11use alloy_rlp::{BufMut, Decodable, Encodable};
12use core::fmt;
13
14/// Receipt envelope, as defined in [EIP-2718].
15///
16/// Represents untagged legacy receipts and typed EIP-2718 variants. Binary decoding rejects a
17/// literal `0x00` type prefix; legacy receipts use the untagged fallback encoding. JSON `type: 0x0`
18/// is accepted as legacy.
19///
20/// Transaction receipt payloads are specified in their respective EIPs.
21///
22/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
23#[derive(Clone, Debug, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25#[cfg_attr(feature = "serde", serde(tag = "type"))]
26#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
27#[doc(alias = "TransactionReceiptEnvelope", alias = "TxReceiptEnvelope")]
28pub enum ReceiptEnvelope<T = Log> {
29    /// Receipt envelope with no type flag.
30    #[cfg_attr(feature = "serde", serde(rename = "0x0", alias = "0x00"))]
31    Legacy(ReceiptWithBloom<Receipt<T>>),
32    /// Receipt envelope with type flag 1, containing a [EIP-2930] receipt.
33    ///
34    /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930
35    #[cfg_attr(feature = "serde", serde(rename = "0x1", alias = "0x01"))]
36    Eip2930(ReceiptWithBloom<Receipt<T>>),
37    /// Receipt envelope with type flag 2, containing a [EIP-1559] receipt.
38    ///
39    /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559
40    #[cfg_attr(feature = "serde", serde(rename = "0x2", alias = "0x02"))]
41    Eip1559(ReceiptWithBloom<Receipt<T>>),
42    /// Receipt envelope with type flag 3, containing a [EIP-4844] receipt.
43    ///
44    /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844
45    #[cfg_attr(feature = "serde", serde(rename = "0x3", alias = "0x03"))]
46    Eip4844(ReceiptWithBloom<Receipt<T>>),
47    /// Receipt envelope with type flag 4, containing a [EIP-7702] receipt.
48    ///
49    /// [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
50    #[cfg_attr(feature = "serde", serde(rename = "0x4", alias = "0x04"))]
51    Eip7702(ReceiptWithBloom<Receipt<T>>),
52}
53
54/// Deserializes a receipt, treating a missing `type` field as [`TxType::Legacy`].
55///
56/// The `type` field is required by the JSON-RPC specification, but some
57/// Ethereum-compatible nodes omit it entirely. A receipt without a type flag is a
58/// pre-[EIP-2718] receipt, which is unambiguously legacy, so it is accepted rather
59/// than rejected.
60///
61/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
62#[cfg(feature = "serde")]
63impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for ReceiptEnvelope<T> {
64    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
65        #[derive(serde::Deserialize)]
66        struct ReceiptEnvelopeHelper<T> {
67            #[serde(default, rename = "type", with = "alloy_serde::quantity::opt")]
68            ty: Option<u8>,
69            #[serde(flatten)]
70            receipt: ReceiptWithBloom<Receipt<T>>,
71        }
72
73        let helper = ReceiptEnvelopeHelper::<T>::deserialize(deserializer)?;
74        let ty = TxType::try_from(helper.ty.unwrap_or(LEGACY_TX_TYPE_ID))
75            .map_err(serde::de::Error::custom)?;
76        Ok(Self::from_typed(ty, helper.receipt))
77    }
78}
79
80impl<T> ReceiptEnvelope<T> {
81    /// Creates the envelope for a given type and receipt.
82    pub fn from_typed<R>(tx_type: TxType, receipt: R) -> Self
83    where
84        R: Into<ReceiptWithBloom<Receipt<T>>>,
85    {
86        match tx_type {
87            TxType::Legacy => Self::Legacy(receipt.into()),
88            TxType::Eip2930 => Self::Eip2930(receipt.into()),
89            TxType::Eip1559 => Self::Eip1559(receipt.into()),
90            TxType::Eip4844 => Self::Eip4844(receipt.into()),
91            TxType::Eip7702 => Self::Eip7702(receipt.into()),
92        }
93    }
94
95    /// Converts the receipt's log type by applying a function to each log.
96    ///
97    /// Returns the receipt with the new log type.
98    pub fn map_logs<U>(self, f: impl FnMut(T) -> U) -> ReceiptEnvelope<U> {
99        match self {
100            Self::Legacy(r) => ReceiptEnvelope::Legacy(r.map_logs(f)),
101            Self::Eip2930(r) => ReceiptEnvelope::Eip2930(r.map_logs(f)),
102            Self::Eip1559(r) => ReceiptEnvelope::Eip1559(r.map_logs(f)),
103            Self::Eip4844(r) => ReceiptEnvelope::Eip4844(r.map_logs(f)),
104            Self::Eip7702(r) => ReceiptEnvelope::Eip7702(r.map_logs(f)),
105        }
106    }
107
108    /// Converts a [`ReceiptEnvelope`] with a custom log type into a [`ReceiptEnvelope`] with the
109    /// primitives [`Log`] type by converting the logs.
110    ///
111    /// This is useful if log types that embed the primitives log type, e.g. the log receipt rpc
112    /// type.
113    pub fn into_primitives_receipt(self) -> ReceiptEnvelope<Log>
114    where
115        T: Into<Log>,
116    {
117        self.map_logs(Into::into)
118    }
119
120    /// Return the [`TxType`] of the inner receipt.
121    #[doc(alias = "transaction_type")]
122    pub const fn tx_type(&self) -> TxType {
123        match self {
124            Self::Legacy(_) => TxType::Legacy,
125            Self::Eip2930(_) => TxType::Eip2930,
126            Self::Eip1559(_) => TxType::Eip1559,
127            Self::Eip4844(_) => TxType::Eip4844,
128            Self::Eip7702(_) => TxType::Eip7702,
129        }
130    }
131
132    /// Return true if the transaction was successful.
133    pub const fn is_success(&self) -> bool {
134        self.status()
135    }
136
137    /// Returns the success status of the receipt's transaction.
138    pub const fn status(&self) -> bool {
139        self.as_receipt().unwrap().status.coerce_status()
140    }
141
142    /// Returns the cumulative gas used at this receipt.
143    pub const fn cumulative_gas_used(&self) -> u64 {
144        self.as_receipt().unwrap().cumulative_gas_used
145    }
146
147    /// Return the receipt logs.
148    pub fn logs(&self) -> &[T] {
149        &self.as_receipt().unwrap().logs
150    }
151
152    /// Consumes the type and returns the logs.
153    pub fn into_logs(self) -> Vec<T> {
154        self.into_receipt().logs
155    }
156
157    /// Return the receipt's bloom.
158    pub const fn logs_bloom(&self) -> &Bloom {
159        &self.as_receipt_with_bloom().unwrap().logs_bloom
160    }
161
162    /// Return the inner receipt with bloom. Currently this is infallible,
163    /// however, future receipt types may be added.
164    pub const fn as_receipt_with_bloom(&self) -> Option<&ReceiptWithBloom<Receipt<T>>> {
165        match self {
166            Self::Legacy(t)
167            | Self::Eip2930(t)
168            | Self::Eip1559(t)
169            | Self::Eip4844(t)
170            | Self::Eip7702(t) => Some(t),
171        }
172    }
173
174    /// Return the mutable inner receipt with bloom. Currently this is
175    /// infallible, however, future receipt types may be added.
176    pub const fn as_receipt_with_bloom_mut(&mut self) -> Option<&mut ReceiptWithBloom<Receipt<T>>> {
177        match self {
178            Self::Legacy(t)
179            | Self::Eip2930(t)
180            | Self::Eip1559(t)
181            | Self::Eip4844(t)
182            | Self::Eip7702(t) => Some(t),
183        }
184    }
185
186    /// Consumes the type and returns the underlying [`Receipt`].
187    pub fn into_receipt(self) -> Receipt<T> {
188        match self {
189            Self::Legacy(t)
190            | Self::Eip2930(t)
191            | Self::Eip1559(t)
192            | Self::Eip4844(t)
193            | Self::Eip7702(t) => t.receipt,
194        }
195    }
196
197    /// Return the inner receipt. Currently this is infallible, however, future
198    /// receipt types may be added.
199    pub const fn as_receipt(&self) -> Option<&Receipt<T>> {
200        match self {
201            Self::Legacy(t)
202            | Self::Eip2930(t)
203            | Self::Eip1559(t)
204            | Self::Eip4844(t)
205            | Self::Eip7702(t) => Some(&t.receipt),
206        }
207    }
208}
209
210impl<T> TxReceipt for ReceiptEnvelope<T>
211where
212    T: Clone + fmt::Debug + PartialEq + Eq + Send + Sync,
213{
214    type Log = T;
215
216    fn status_or_post_state(&self) -> Eip658Value {
217        self.as_receipt().unwrap().status
218    }
219
220    fn status(&self) -> bool {
221        self.as_receipt().unwrap().status.coerce_status()
222    }
223
224    /// Return the receipt's bloom.
225    fn bloom(&self) -> Bloom {
226        self.as_receipt_with_bloom().unwrap().logs_bloom
227    }
228
229    fn bloom_cheap(&self) -> Option<Bloom> {
230        Some(self.bloom())
231    }
232
233    /// Returns the cumulative gas used at this receipt.
234    fn cumulative_gas_used(&self) -> u64 {
235        self.as_receipt().unwrap().cumulative_gas_used
236    }
237
238    /// Return the receipt logs.
239    fn logs(&self) -> &[T] {
240        &self.as_receipt().unwrap().logs
241    }
242
243    fn into_logs(self) -> Vec<Self::Log>
244    where
245        Self::Log: Clone,
246    {
247        self.into_receipt().logs
248    }
249}
250
251impl ReceiptEnvelope {
252    /// Get the length of the inner receipt in the 2718 encoding.
253    pub fn inner_length(&self) -> usize {
254        self.as_receipt_with_bloom().unwrap().length()
255    }
256
257    /// Calculate the length of the rlp payload of the network encoded receipt.
258    pub fn rlp_payload_length(&self) -> usize {
259        let length = self.as_receipt_with_bloom().unwrap().length();
260        match self {
261            Self::Legacy(_) => length,
262            _ => length + 1,
263        }
264    }
265}
266
267impl Encodable for ReceiptEnvelope {
268    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
269        self.network_encode(out)
270    }
271
272    fn length(&self) -> usize {
273        self.network_len()
274    }
275}
276
277impl Decodable for ReceiptEnvelope {
278    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
279        Self::network_decode(buf)
280            .map_or_else(|_| Err(alloy_rlp::Error::Custom("Unexpected type")), Ok)
281    }
282}
283
284impl Typed2718 for ReceiptEnvelope {
285    fn ty(&self) -> u8 {
286        match self {
287            Self::Legacy(_) => LEGACY_TX_TYPE_ID,
288            Self::Eip2930(_) => EIP2930_TX_TYPE_ID,
289            Self::Eip1559(_) => EIP1559_TX_TYPE_ID,
290            Self::Eip4844(_) => EIP4844_TX_TYPE_ID,
291            Self::Eip7702(_) => EIP7702_TX_TYPE_ID,
292        }
293    }
294}
295
296impl IsTyped2718 for ReceiptEnvelope {
297    fn is_type(type_id: u8) -> bool {
298        <TxType as IsTyped2718>::is_type(type_id)
299    }
300}
301
302impl Encodable2718 for ReceiptEnvelope {
303    fn encode_2718_len(&self) -> usize {
304        self.inner_length() + !self.is_legacy() as usize
305    }
306
307    fn encode_2718(&self, out: &mut dyn BufMut) {
308        match self.type_flag() {
309            None => {}
310            Some(ty) => out.put_u8(ty),
311        }
312        self.as_receipt_with_bloom().unwrap().encode(out);
313    }
314}
315
316impl Decodable2718 for ReceiptEnvelope {
317    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
318        let receipt = Decodable::decode(buf)?;
319        match ty.try_into().map_err(|_| alloy_rlp::Error::Custom("Unexpected type"))? {
320            TxType::Eip2930 => Ok(Self::Eip2930(receipt)),
321            TxType::Eip1559 => Ok(Self::Eip1559(receipt)),
322            TxType::Eip4844 => Ok(Self::Eip4844(receipt)),
323            TxType::Eip7702 => Ok(Self::Eip7702(receipt)),
324            TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
325        }
326    }
327
328    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
329        Ok(Self::Legacy(Decodable::decode(buf)?))
330    }
331}
332
333#[cfg(any(test, feature = "arbitrary"))]
334impl<'a, T> arbitrary::Arbitrary<'a> for ReceiptEnvelope<T>
335where
336    T: arbitrary::Arbitrary<'a>,
337{
338    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
339        let receipt = ReceiptWithBloom::<Receipt<T>>::arbitrary(u)?;
340
341        match u.int_in_range(0..=4)? {
342            0 => Ok(Self::Legacy(receipt)),
343            1 => Ok(Self::Eip2930(receipt)),
344            2 => Ok(Self::Eip1559(receipt)),
345            3 => Ok(Self::Eip4844(receipt)),
346            4 => Ok(Self::Eip7702(receipt)),
347            _ => unreachable!(),
348        }
349    }
350}
351
352/// Bincode-compatible [`ReceiptEnvelope`] serde implementation.
353#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
354pub(crate) mod serde_bincode_compat {
355    use crate::{Receipt, ReceiptWithBloom, TxType};
356    use alloc::borrow::Cow;
357    use alloy_primitives::{Bloom, Log, U8};
358    use serde::{Deserialize, Deserializer, Serialize, Serializer};
359    use serde_with::{DeserializeAs, SerializeAs};
360
361    /// Bincode-compatible [`super::ReceiptEnvelope`] serde implementation.
362    ///
363    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
364    /// ```rust
365    /// use alloy_consensus::{serde_bincode_compat, ReceiptEnvelope};
366    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
367    /// use serde_with::serde_as;
368    ///
369    /// #[serde_as]
370    /// #[derive(Serialize, Deserialize)]
371    /// struct Data<T: Serialize + DeserializeOwned + Clone + 'static> {
372    ///     #[serde_as(as = "serde_bincode_compat::ReceiptEnvelope<'_, T>")]
373    ///     receipt: ReceiptEnvelope<T>,
374    /// }
375    /// ```
376    #[derive(Debug, Serialize, Deserialize)]
377    pub struct ReceiptEnvelope<'a, T: Clone = Log> {
378        #[serde(deserialize_with = "deserde_txtype")]
379        tx_type: TxType,
380        success: bool,
381        cumulative_gas_used: u64,
382        logs_bloom: Cow<'a, Bloom>,
383        logs: Cow<'a, [T]>,
384    }
385
386    /// Ensures that txtype is deserialized symmetrically as U8
387    fn deserde_txtype<'de, D>(deserializer: D) -> Result<TxType, D::Error>
388    where
389        D: Deserializer<'de>,
390    {
391        let value = U8::deserialize(deserializer)?;
392        value.to::<u8>().try_into().map_err(serde::de::Error::custom)
393    }
394
395    impl<'a, T: Clone> From<&'a super::ReceiptEnvelope<T>> for ReceiptEnvelope<'a, T> {
396        fn from(value: &'a super::ReceiptEnvelope<T>) -> Self {
397            Self {
398                tx_type: value.tx_type(),
399                success: value.status(),
400                cumulative_gas_used: value.cumulative_gas_used(),
401                logs_bloom: Cow::Borrowed(value.logs_bloom()),
402                logs: Cow::Borrowed(value.logs()),
403            }
404        }
405    }
406
407    impl<'a, T: Clone> From<ReceiptEnvelope<'a, T>> for super::ReceiptEnvelope<T> {
408        fn from(value: ReceiptEnvelope<'a, T>) -> Self {
409            let ReceiptEnvelope { tx_type, success, cumulative_gas_used, logs_bloom, logs } = value;
410            let receipt = ReceiptWithBloom {
411                receipt: Receipt {
412                    status: success.into(),
413                    cumulative_gas_used,
414                    logs: logs.into_owned(),
415                },
416                logs_bloom: logs_bloom.into_owned(),
417            };
418            match tx_type {
419                TxType::Legacy => Self::Legacy(receipt),
420                TxType::Eip2930 => Self::Eip2930(receipt),
421                TxType::Eip1559 => Self::Eip1559(receipt),
422                TxType::Eip4844 => Self::Eip4844(receipt),
423                TxType::Eip7702 => Self::Eip7702(receipt),
424            }
425        }
426    }
427
428    impl<T: Serialize + Clone> SerializeAs<super::ReceiptEnvelope<T>> for ReceiptEnvelope<'_, T> {
429        fn serialize_as<S>(
430            source: &super::ReceiptEnvelope<T>,
431            serializer: S,
432        ) -> Result<S::Ok, S::Error>
433        where
434            S: Serializer,
435        {
436            ReceiptEnvelope::<'_, T>::from(source).serialize(serializer)
437        }
438    }
439
440    impl<'de, T: Deserialize<'de> + Clone> DeserializeAs<'de, super::ReceiptEnvelope<T>>
441        for ReceiptEnvelope<'de, T>
442    {
443        fn deserialize_as<D>(deserializer: D) -> Result<super::ReceiptEnvelope<T>, D::Error>
444        where
445            D: Deserializer<'de>,
446        {
447            ReceiptEnvelope::<'_, T>::deserialize(deserializer).map(Into::into)
448        }
449    }
450
451    #[cfg(test)]
452    mod tests {
453        use super::super::{serde_bincode_compat, ReceiptEnvelope};
454        use alloy_primitives::Log;
455        use arbitrary::Arbitrary;
456        use bincode::config;
457        use rand::Rng;
458        use serde::{Deserialize, Serialize};
459        use serde_with::serde_as;
460
461        #[test]
462        fn test_receipt_envelope_bincode_roundtrip() {
463            #[serde_as]
464            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
465            struct Data {
466                #[serde_as(as = "serde_bincode_compat::ReceiptEnvelope<'_>")]
467                transaction: ReceiptEnvelope<Log>,
468            }
469
470            let mut bytes = [0u8; 1024];
471            rand::thread_rng().fill(bytes.as_mut_slice());
472            let mut data = Data {
473                transaction: ReceiptEnvelope::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
474                    .unwrap(),
475            };
476
477            // ensure we have proper roundtrip data
478            data.transaction.as_receipt_with_bloom_mut().unwrap().receipt.status = true.into();
479
480            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
481            let (decoded, _) =
482                bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
483            assert_eq!(decoded, data);
484        }
485    }
486}
487
488#[cfg(test)]
489mod test {
490    use crate::{Receipt, ReceiptEnvelope, TxType};
491    use alloy_primitives::Log;
492
493    #[cfg(feature = "serde")]
494    #[test]
495    fn deser_pre658_receipt_envelope() {
496        use crate::Receipt;
497        use alloy_primitives::b256;
498
499        let receipt = super::ReceiptWithBloom::<Receipt<()>> {
500            receipt: super::Receipt {
501                status: super::Eip658Value::PostState(b256!(
502                    "284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10"
503                )),
504                cumulative_gas_used: 0,
505                logs: Default::default(),
506            },
507            logs_bloom: Default::default(),
508        };
509
510        let json = serde_json::to_string(&receipt).unwrap();
511
512        println!("Serialized {json}");
513
514        let receipt: super::ReceiptWithBloom<Receipt<()>> = serde_json::from_str(&json).unwrap();
515
516        assert_eq!(
517            receipt.receipt.status,
518            super::Eip658Value::PostState(b256!(
519                "284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10"
520            ))
521        );
522    }
523
524    #[cfg(feature = "serde")]
525    #[test]
526    fn deser_receipt_envelope_without_type() {
527        let inner = super::ReceiptWithBloom::<Receipt<()>> {
528            receipt: Receipt {
529                status: super::Eip658Value::Eip658(true),
530                cumulative_gas_used: 0xc3b68,
531                logs: Default::default(),
532            },
533            logs_bloom: Default::default(),
534        };
535        let mut json = serde_json::to_value(&inner).unwrap();
536        assert!(json.get("type").is_none());
537
538        let envelope: ReceiptEnvelope<()> = serde_json::from_value(json.clone()).unwrap();
539        assert_eq!(envelope, ReceiptEnvelope::Legacy(inner.clone()));
540
541        // An explicit type flag is still honored.
542        json["type"] = "0x2".into();
543        let envelope: ReceiptEnvelope<()> = serde_json::from_value(json.clone()).unwrap();
544        assert_eq!(envelope, ReceiptEnvelope::Eip1559(inner));
545
546        // An unknown type flag is still rejected.
547        json["type"] = "0x7f".into();
548        serde_json::from_value::<ReceiptEnvelope<()>>(json).unwrap_err();
549    }
550
551    #[test]
552    fn convert_envelope() {
553        let receipt = Receipt::<Log>::default();
554        let _envelope = ReceiptEnvelope::from_typed(TxType::Eip7702, receipt);
555    }
556}