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, serde::Deserialize))]
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
55impl<T> ReceiptEnvelope<T> {
56    /// Creates the envelope for a given type and receipt.
57    pub fn from_typed<R>(tx_type: TxType, receipt: R) -> Self
58    where
59        R: Into<ReceiptWithBloom<Receipt<T>>>,
60    {
61        match tx_type {
62            TxType::Legacy => Self::Legacy(receipt.into()),
63            TxType::Eip2930 => Self::Eip2930(receipt.into()),
64            TxType::Eip1559 => Self::Eip1559(receipt.into()),
65            TxType::Eip4844 => Self::Eip4844(receipt.into()),
66            TxType::Eip7702 => Self::Eip7702(receipt.into()),
67        }
68    }
69
70    /// Converts the receipt's log type by applying a function to each log.
71    ///
72    /// Returns the receipt with the new log type.
73    pub fn map_logs<U>(self, f: impl FnMut(T) -> U) -> ReceiptEnvelope<U> {
74        match self {
75            Self::Legacy(r) => ReceiptEnvelope::Legacy(r.map_logs(f)),
76            Self::Eip2930(r) => ReceiptEnvelope::Eip2930(r.map_logs(f)),
77            Self::Eip1559(r) => ReceiptEnvelope::Eip1559(r.map_logs(f)),
78            Self::Eip4844(r) => ReceiptEnvelope::Eip4844(r.map_logs(f)),
79            Self::Eip7702(r) => ReceiptEnvelope::Eip7702(r.map_logs(f)),
80        }
81    }
82
83    /// Converts a [`ReceiptEnvelope`] with a custom log type into a [`ReceiptEnvelope`] with the
84    /// primitives [`Log`] type by converting the logs.
85    ///
86    /// This is useful if log types that embed the primitives log type, e.g. the log receipt rpc
87    /// type.
88    pub fn into_primitives_receipt(self) -> ReceiptEnvelope<Log>
89    where
90        T: Into<Log>,
91    {
92        self.map_logs(Into::into)
93    }
94
95    /// Return the [`TxType`] of the inner receipt.
96    #[doc(alias = "transaction_type")]
97    pub const fn tx_type(&self) -> TxType {
98        match self {
99            Self::Legacy(_) => TxType::Legacy,
100            Self::Eip2930(_) => TxType::Eip2930,
101            Self::Eip1559(_) => TxType::Eip1559,
102            Self::Eip4844(_) => TxType::Eip4844,
103            Self::Eip7702(_) => TxType::Eip7702,
104        }
105    }
106
107    /// Return true if the transaction was successful.
108    pub const fn is_success(&self) -> bool {
109        self.status()
110    }
111
112    /// Returns the success status of the receipt's transaction.
113    pub const fn status(&self) -> bool {
114        self.as_receipt().unwrap().status.coerce_status()
115    }
116
117    /// Returns the cumulative gas used at this receipt.
118    pub const fn cumulative_gas_used(&self) -> u64 {
119        self.as_receipt().unwrap().cumulative_gas_used
120    }
121
122    /// Return the receipt logs.
123    pub fn logs(&self) -> &[T] {
124        &self.as_receipt().unwrap().logs
125    }
126
127    /// Consumes the type and returns the logs.
128    pub fn into_logs(self) -> Vec<T> {
129        self.into_receipt().logs
130    }
131
132    /// Return the receipt's bloom.
133    pub const fn logs_bloom(&self) -> &Bloom {
134        &self.as_receipt_with_bloom().unwrap().logs_bloom
135    }
136
137    /// Return the inner receipt with bloom. Currently this is infallible,
138    /// however, future receipt types may be added.
139    pub const fn as_receipt_with_bloom(&self) -> Option<&ReceiptWithBloom<Receipt<T>>> {
140        match self {
141            Self::Legacy(t)
142            | Self::Eip2930(t)
143            | Self::Eip1559(t)
144            | Self::Eip4844(t)
145            | Self::Eip7702(t) => Some(t),
146        }
147    }
148
149    /// Return the mutable inner receipt with bloom. Currently this is
150    /// infallible, however, future receipt types may be added.
151    pub const fn as_receipt_with_bloom_mut(&mut self) -> Option<&mut ReceiptWithBloom<Receipt<T>>> {
152        match self {
153            Self::Legacy(t)
154            | Self::Eip2930(t)
155            | Self::Eip1559(t)
156            | Self::Eip4844(t)
157            | Self::Eip7702(t) => Some(t),
158        }
159    }
160
161    /// Consumes the type and returns the underlying [`Receipt`].
162    pub fn into_receipt(self) -> Receipt<T> {
163        match self {
164            Self::Legacy(t)
165            | Self::Eip2930(t)
166            | Self::Eip1559(t)
167            | Self::Eip4844(t)
168            | Self::Eip7702(t) => t.receipt,
169        }
170    }
171
172    /// Return the inner receipt. Currently this is infallible, however, future
173    /// receipt types may be added.
174    pub const fn as_receipt(&self) -> Option<&Receipt<T>> {
175        match self {
176            Self::Legacy(t)
177            | Self::Eip2930(t)
178            | Self::Eip1559(t)
179            | Self::Eip4844(t)
180            | Self::Eip7702(t) => Some(&t.receipt),
181        }
182    }
183}
184
185impl<T> TxReceipt for ReceiptEnvelope<T>
186where
187    T: Clone + fmt::Debug + PartialEq + Eq + Send + Sync,
188{
189    type Log = T;
190
191    fn status_or_post_state(&self) -> Eip658Value {
192        self.as_receipt().unwrap().status
193    }
194
195    fn status(&self) -> bool {
196        self.as_receipt().unwrap().status.coerce_status()
197    }
198
199    /// Return the receipt's bloom.
200    fn bloom(&self) -> Bloom {
201        self.as_receipt_with_bloom().unwrap().logs_bloom
202    }
203
204    fn bloom_cheap(&self) -> Option<Bloom> {
205        Some(self.bloom())
206    }
207
208    /// Returns the cumulative gas used at this receipt.
209    fn cumulative_gas_used(&self) -> u64 {
210        self.as_receipt().unwrap().cumulative_gas_used
211    }
212
213    /// Return the receipt logs.
214    fn logs(&self) -> &[T] {
215        &self.as_receipt().unwrap().logs
216    }
217
218    fn into_logs(self) -> Vec<Self::Log>
219    where
220        Self::Log: Clone,
221    {
222        self.into_receipt().logs
223    }
224}
225
226impl ReceiptEnvelope {
227    /// Get the length of the inner receipt in the 2718 encoding.
228    pub fn inner_length(&self) -> usize {
229        self.as_receipt_with_bloom().unwrap().length()
230    }
231
232    /// Calculate the length of the rlp payload of the network encoded receipt.
233    pub fn rlp_payload_length(&self) -> usize {
234        let length = self.as_receipt_with_bloom().unwrap().length();
235        match self {
236            Self::Legacy(_) => length,
237            _ => length + 1,
238        }
239    }
240}
241
242impl Encodable for ReceiptEnvelope {
243    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
244        self.network_encode(out)
245    }
246
247    fn length(&self) -> usize {
248        self.network_len()
249    }
250}
251
252impl Decodable for ReceiptEnvelope {
253    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
254        Self::network_decode(buf)
255            .map_or_else(|_| Err(alloy_rlp::Error::Custom("Unexpected type")), Ok)
256    }
257}
258
259impl Typed2718 for ReceiptEnvelope {
260    fn ty(&self) -> u8 {
261        match self {
262            Self::Legacy(_) => LEGACY_TX_TYPE_ID,
263            Self::Eip2930(_) => EIP2930_TX_TYPE_ID,
264            Self::Eip1559(_) => EIP1559_TX_TYPE_ID,
265            Self::Eip4844(_) => EIP4844_TX_TYPE_ID,
266            Self::Eip7702(_) => EIP7702_TX_TYPE_ID,
267        }
268    }
269}
270
271impl IsTyped2718 for ReceiptEnvelope {
272    fn is_type(type_id: u8) -> bool {
273        <TxType as IsTyped2718>::is_type(type_id)
274    }
275}
276
277impl Encodable2718 for ReceiptEnvelope {
278    fn encode_2718_len(&self) -> usize {
279        self.inner_length() + !self.is_legacy() as usize
280    }
281
282    fn encode_2718(&self, out: &mut dyn BufMut) {
283        match self.type_flag() {
284            None => {}
285            Some(ty) => out.put_u8(ty),
286        }
287        self.as_receipt_with_bloom().unwrap().encode(out);
288    }
289}
290
291impl Decodable2718 for ReceiptEnvelope {
292    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
293        let receipt = Decodable::decode(buf)?;
294        match ty.try_into().map_err(|_| alloy_rlp::Error::Custom("Unexpected type"))? {
295            TxType::Eip2930 => Ok(Self::Eip2930(receipt)),
296            TxType::Eip1559 => Ok(Self::Eip1559(receipt)),
297            TxType::Eip4844 => Ok(Self::Eip4844(receipt)),
298            TxType::Eip7702 => Ok(Self::Eip7702(receipt)),
299            TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
300        }
301    }
302
303    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
304        Ok(Self::Legacy(Decodable::decode(buf)?))
305    }
306}
307
308#[cfg(any(test, feature = "arbitrary"))]
309impl<'a, T> arbitrary::Arbitrary<'a> for ReceiptEnvelope<T>
310where
311    T: arbitrary::Arbitrary<'a>,
312{
313    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
314        let receipt = ReceiptWithBloom::<Receipt<T>>::arbitrary(u)?;
315
316        match u.int_in_range(0..=4)? {
317            0 => Ok(Self::Legacy(receipt)),
318            1 => Ok(Self::Eip2930(receipt)),
319            2 => Ok(Self::Eip1559(receipt)),
320            3 => Ok(Self::Eip4844(receipt)),
321            4 => Ok(Self::Eip7702(receipt)),
322            _ => unreachable!(),
323        }
324    }
325}
326
327/// Bincode-compatible [`ReceiptEnvelope`] serde implementation.
328#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
329pub(crate) mod serde_bincode_compat {
330    use crate::{Receipt, ReceiptWithBloom, TxType};
331    use alloc::borrow::Cow;
332    use alloy_primitives::{Bloom, Log, U8};
333    use serde::{Deserialize, Deserializer, Serialize, Serializer};
334    use serde_with::{DeserializeAs, SerializeAs};
335
336    /// Bincode-compatible [`super::ReceiptEnvelope`] serde implementation.
337    ///
338    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
339    /// ```rust
340    /// use alloy_consensus::{serde_bincode_compat, ReceiptEnvelope};
341    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
342    /// use serde_with::serde_as;
343    ///
344    /// #[serde_as]
345    /// #[derive(Serialize, Deserialize)]
346    /// struct Data<T: Serialize + DeserializeOwned + Clone + 'static> {
347    ///     #[serde_as(as = "serde_bincode_compat::ReceiptEnvelope<'_, T>")]
348    ///     receipt: ReceiptEnvelope<T>,
349    /// }
350    /// ```
351    #[derive(Debug, Serialize, Deserialize)]
352    pub struct ReceiptEnvelope<'a, T: Clone = Log> {
353        #[serde(deserialize_with = "deserde_txtype")]
354        tx_type: TxType,
355        success: bool,
356        cumulative_gas_used: u64,
357        logs_bloom: Cow<'a, Bloom>,
358        logs: Cow<'a, [T]>,
359    }
360
361    /// Ensures that txtype is deserialized symmetrically as U8
362    fn deserde_txtype<'de, D>(deserializer: D) -> Result<TxType, D::Error>
363    where
364        D: Deserializer<'de>,
365    {
366        let value = U8::deserialize(deserializer)?;
367        value.to::<u8>().try_into().map_err(serde::de::Error::custom)
368    }
369
370    impl<'a, T: Clone> From<&'a super::ReceiptEnvelope<T>> for ReceiptEnvelope<'a, T> {
371        fn from(value: &'a super::ReceiptEnvelope<T>) -> Self {
372            Self {
373                tx_type: value.tx_type(),
374                success: value.status(),
375                cumulative_gas_used: value.cumulative_gas_used(),
376                logs_bloom: Cow::Borrowed(value.logs_bloom()),
377                logs: Cow::Borrowed(value.logs()),
378            }
379        }
380    }
381
382    impl<'a, T: Clone> From<ReceiptEnvelope<'a, T>> for super::ReceiptEnvelope<T> {
383        fn from(value: ReceiptEnvelope<'a, T>) -> Self {
384            let ReceiptEnvelope { tx_type, success, cumulative_gas_used, logs_bloom, logs } = value;
385            let receipt = ReceiptWithBloom {
386                receipt: Receipt {
387                    status: success.into(),
388                    cumulative_gas_used,
389                    logs: logs.into_owned(),
390                },
391                logs_bloom: logs_bloom.into_owned(),
392            };
393            match tx_type {
394                TxType::Legacy => Self::Legacy(receipt),
395                TxType::Eip2930 => Self::Eip2930(receipt),
396                TxType::Eip1559 => Self::Eip1559(receipt),
397                TxType::Eip4844 => Self::Eip4844(receipt),
398                TxType::Eip7702 => Self::Eip7702(receipt),
399            }
400        }
401    }
402
403    impl<T: Serialize + Clone> SerializeAs<super::ReceiptEnvelope<T>> for ReceiptEnvelope<'_, T> {
404        fn serialize_as<S>(
405            source: &super::ReceiptEnvelope<T>,
406            serializer: S,
407        ) -> Result<S::Ok, S::Error>
408        where
409            S: Serializer,
410        {
411            ReceiptEnvelope::<'_, T>::from(source).serialize(serializer)
412        }
413    }
414
415    impl<'de, T: Deserialize<'de> + Clone> DeserializeAs<'de, super::ReceiptEnvelope<T>>
416        for ReceiptEnvelope<'de, T>
417    {
418        fn deserialize_as<D>(deserializer: D) -> Result<super::ReceiptEnvelope<T>, D::Error>
419        where
420            D: Deserializer<'de>,
421        {
422            ReceiptEnvelope::<'_, T>::deserialize(deserializer).map(Into::into)
423        }
424    }
425
426    #[cfg(test)]
427    mod tests {
428        use super::super::{serde_bincode_compat, ReceiptEnvelope};
429        use alloy_primitives::Log;
430        use arbitrary::Arbitrary;
431        use bincode::config;
432        use rand::Rng;
433        use serde::{Deserialize, Serialize};
434        use serde_with::serde_as;
435
436        #[test]
437        fn test_receipt_envelope_bincode_roundtrip() {
438            #[serde_as]
439            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
440            struct Data {
441                #[serde_as(as = "serde_bincode_compat::ReceiptEnvelope<'_>")]
442                transaction: ReceiptEnvelope<Log>,
443            }
444
445            let mut bytes = [0u8; 1024];
446            rand::thread_rng().fill(bytes.as_mut_slice());
447            let mut data = Data {
448                transaction: ReceiptEnvelope::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
449                    .unwrap(),
450            };
451
452            // ensure we have proper roundtrip data
453            data.transaction.as_receipt_with_bloom_mut().unwrap().receipt.status = true.into();
454
455            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
456            let (decoded, _) =
457                bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
458            assert_eq!(decoded, data);
459        }
460    }
461}
462
463#[cfg(test)]
464mod test {
465    use crate::{Receipt, ReceiptEnvelope, TxType};
466    use alloy_primitives::Log;
467
468    #[cfg(feature = "serde")]
469    #[test]
470    fn deser_pre658_receipt_envelope() {
471        use crate::Receipt;
472        use alloy_primitives::b256;
473
474        let receipt = super::ReceiptWithBloom::<Receipt<()>> {
475            receipt: super::Receipt {
476                status: super::Eip658Value::PostState(b256!(
477                    "284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10"
478                )),
479                cumulative_gas_used: 0,
480                logs: Default::default(),
481            },
482            logs_bloom: Default::default(),
483        };
484
485        let json = serde_json::to_string(&receipt).unwrap();
486
487        println!("Serialized {json}");
488
489        let receipt: super::ReceiptWithBloom<Receipt<()>> = serde_json::from_str(&json).unwrap();
490
491        assert_eq!(
492            receipt.receipt.status,
493            super::Eip658Value::PostState(b256!(
494                "284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10"
495            ))
496        );
497    }
498
499    #[test]
500    fn convert_envelope() {
501        let receipt = Receipt::<Log>::default();
502        let _envelope = ReceiptEnvelope::from_typed(TxType::Eip7702, receipt);
503    }
504}