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