alloy_consensus/receipt/
envelope.rs

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