Skip to main content

alloy_consensus_any/receipt/
envelope.rs

1use alloc::vec::Vec;
2use alloy_consensus::{Eip658Value, Receipt, ReceiptWithBloom, TxReceipt};
3use alloy_eips::{
4    eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718},
5    Typed2718,
6};
7use alloy_primitives::{bytes::BufMut, Bloom, Log};
8use alloy_rlp::{Decodable, Encodable};
9use core::fmt;
10
11/// Receipt envelope, as defined in [EIP-2718].
12///
13/// Represents untagged legacy receipts and typed EIP-2718 variants. Binary decoding rejects a
14/// literal `0x00` type prefix; type ID 0 uses the untagged legacy encoding.
15///
16/// Transaction receipt payloads are specified in their respective EIPs.
17///
18/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
19#[derive(Clone, Debug, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[doc(alias = "AnyTransactionReceiptEnvelope", alias = "AnyTxReceiptEnvelope")]
22pub struct AnyReceiptEnvelope<T = Log> {
23    /// The receipt envelope.
24    #[cfg_attr(feature = "serde", serde(flatten))]
25    pub inner: ReceiptWithBloom<Receipt<T>>,
26    /// The transaction type.
27    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
28    pub r#type: u8,
29}
30
31impl<T> AnyReceiptEnvelope<T> {
32    /// Returns whether this is a legacy receipt (type 0)
33    pub const fn is_legacy(&self) -> bool {
34        self.r#type == 0
35    }
36}
37
38impl<T: Encodable> AnyReceiptEnvelope<T> {
39    /// Calculate the length of the rlp payload of the network encoded receipt.
40    pub fn rlp_payload_length(&self) -> usize {
41        let length = self.inner.length();
42        if self.is_legacy() {
43            length
44        } else {
45            length + 1
46        }
47    }
48}
49
50impl<T> AnyReceiptEnvelope<T> {
51    /// Return true if the transaction was successful.
52    ///
53    /// ## Note
54    ///
55    /// This method may not accurately reflect the status of the transaction
56    /// for transactions before [EIP-658].
57    ///
58    /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658
59    pub const fn is_success(&self) -> bool {
60        self.status()
61    }
62
63    /// Returns the success status of the receipt's transaction.
64    ///
65    /// ## Note
66    ///
67    /// This method may not accurately reflect the status of the transaction
68    /// for transactions before [EIP-658].
69    ///
70    /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658
71    pub const fn status(&self) -> bool {
72        self.inner.receipt.status.coerce_status()
73    }
74
75    /// Return the receipt's bloom.
76    pub const fn bloom(&self) -> Bloom {
77        self.inner.logs_bloom
78    }
79
80    /// Return a reference to the receipt's bloom.
81    pub const fn bloom_ref(&self) -> &Bloom {
82        &self.inner.logs_bloom
83    }
84
85    /// Returns the cumulative gas used at this receipt.
86    pub const fn cumulative_gas_used(&self) -> u64 {
87        self.inner.receipt.cumulative_gas_used
88    }
89
90    /// Return the receipt logs.
91    pub fn logs(&self) -> &[T] {
92        &self.inner.receipt.logs
93    }
94}
95
96impl<T> TxReceipt for AnyReceiptEnvelope<T>
97where
98    T: Clone + fmt::Debug + PartialEq + Eq + Send + Sync,
99{
100    type Log = T;
101
102    fn status_or_post_state(&self) -> Eip658Value {
103        self.inner.receipt.status
104    }
105
106    fn status(&self) -> bool {
107        self.status()
108    }
109
110    fn bloom(&self) -> Bloom {
111        self.bloom()
112    }
113
114    fn cumulative_gas_used(&self) -> u64 {
115        self.cumulative_gas_used()
116    }
117
118    fn logs(&self) -> &[T] {
119        Self::logs(self)
120    }
121
122    fn into_logs(self) -> Vec<Self::Log> {
123        self.inner.receipt.logs
124    }
125}
126
127impl Typed2718 for AnyReceiptEnvelope {
128    fn ty(&self) -> u8 {
129        self.r#type
130    }
131}
132
133impl Encodable2718 for AnyReceiptEnvelope {
134    fn encode_2718_len(&self) -> usize {
135        self.inner.length() + !self.is_legacy() as usize
136    }
137
138    fn encode_2718(&self, out: &mut dyn BufMut) {
139        match self.type_flag() {
140            None => {}
141            Some(ty) => out.put_u8(ty),
142        }
143        self.inner.encode(out);
144    }
145}
146
147impl Decodable2718 for AnyReceiptEnvelope {
148    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
149        // Legacy receipts are untagged: `encode_2718` never emits a `0x00` type byte, so a
150        // literal `0x00` prefix must be rejected rather than decoded as legacy, which would not
151        // round-trip. Untagged legacy receipts are handled by `fallback_decode`.
152        if ty == 0 {
153            return Err(Eip2718Error::UnexpectedType(ty));
154        }
155        let receipt = Decodable::decode(buf)?;
156        Ok(Self { inner: receipt, r#type: ty })
157    }
158
159    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
160        let receipt = Decodable::decode(buf)?;
161        Ok(Self { inner: receipt, r#type: 0 })
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use alloc::vec;
169    use alloy_rlp::Header;
170
171    fn envelope(ty: u8) -> AnyReceiptEnvelope {
172        AnyReceiptEnvelope { inner: Default::default(), r#type: ty }
173    }
174
175    /// Wraps an EIP-2718 payload in the RLP string header used by the network encoding of typed
176    /// receipts.
177    fn network_framed(payload: &[u8]) -> Vec<u8> {
178        let mut out = Vec::new();
179        Header { list: false, payload_length: payload.len() }.encode(&mut out);
180        out.extend_from_slice(payload);
181        out
182    }
183
184    #[test]
185    fn legacy_receipt_roundtrips_untagged() {
186        let envelope = envelope(0);
187        let encoded = envelope.encoded_2718();
188        assert!(encoded[0] >= 0xc0, "sanity: legacy receipts are encoded as a bare RLP list");
189        // The network encoding of a legacy receipt is the bare list as well, without a header.
190        let mut network = Vec::new();
191        envelope.network_encode(&mut network);
192        assert_eq!(network, encoded);
193
194        assert_eq!(AnyReceiptEnvelope::decode_2718_exact(&encoded).unwrap(), envelope);
195        assert_eq!(AnyReceiptEnvelope::network_decode(&mut encoded.as_slice()).unwrap(), envelope);
196    }
197
198    #[test]
199    fn typed_receipt_roundtrips() {
200        // A non-Ethereum type id, since this envelope is the catch-all for unknown networks.
201        let envelope = envelope(0x7e);
202        let encoded = envelope.encoded_2718();
203        assert_eq!(encoded[0], 0x7e, "sanity: typed receipts are prefixed with their type byte");
204        let mut network = Vec::new();
205        envelope.network_encode(&mut network);
206        assert_eq!(network, network_framed(&encoded));
207
208        assert_eq!(AnyReceiptEnvelope::decode_2718_exact(&encoded).unwrap(), envelope);
209        assert_eq!(AnyReceiptEnvelope::network_decode(&mut network.as_slice()).unwrap(), envelope);
210    }
211
212    #[test]
213    fn tagged_legacy_receipt_is_rejected() {
214        // Per EIP-2718, legacy receipts are untagged and `0x00` is not an assigned type. The
215        // encoder never emits a `0x00` prefix, so accepting one on decode would re-encode the
216        // receipt differently from the bytes that were decoded.
217        let mut tagged = vec![0x00];
218        tagged.extend_from_slice(&envelope(0).encoded_2718());
219        let tagged_network = network_framed(&tagged);
220
221        assert!(matches!(
222            AnyReceiptEnvelope::decode_2718_exact(&tagged),
223            Err(Eip2718Error::UnexpectedType(0))
224        ));
225        assert!(matches!(
226            AnyReceiptEnvelope::network_decode(&mut tagged_network.as_slice()),
227            Err(Eip2718Error::UnexpectedType(0))
228        ));
229    }
230}