Skip to main content

alloy_consensus/receipt/
receipt2.rs

1use crate::{
2    proofs::ordered_trie_root_with_encoder,
3    receipt::{
4        Eip2718DecodableReceipt, Eip2718EncodableReceipt, Eip658Value, RlpDecodableReceipt,
5        RlpEncodableReceipt, TxReceipt,
6    },
7    InMemorySize, ReceiptEnvelope, ReceiptWithBloom, TxType,
8};
9use alloc::vec::Vec;
10use alloy_eips::{
11    eip2718::{Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718},
12    Typed2718,
13};
14use alloy_primitives::{Bloom, Log, B256};
15use alloy_rlp::{BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable};
16use core::fmt::Debug;
17
18/// Helper trait alias with requirements for transaction type generic to be used within
19/// [`EthereumReceipt`].
20pub trait TxTy:
21    Debug
22    + Copy
23    + Eq
24    + Send
25    + Sync
26    + InMemorySize
27    + Typed2718
28    + TryFrom<u8, Error = Eip2718Error>
29    + Decodable
30    + 'static
31{
32}
33
34impl<T> TxTy for T where
35    T: Debug
36        + Copy
37        + Eq
38        + Send
39        + Sync
40        + InMemorySize
41        + Typed2718
42        + TryFrom<u8, Error = Eip2718Error>
43        + Decodable
44        + 'static
45{
46}
47
48/// Typed ethereum transaction receipt.
49/// Receipt containing result of transaction execution.
50#[derive(Clone, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
53#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
54pub struct EthereumReceipt<T = TxType, L = Log> {
55    /// Receipt type.
56    #[cfg_attr(feature = "serde", serde(rename = "type"))]
57    pub tx_type: T,
58    /// If transaction is executed successfully.
59    ///
60    /// This is the `statusCode`
61    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity", rename = "status"))]
62    pub success: bool,
63    /// Gas used
64    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
65    pub cumulative_gas_used: u64,
66    /// Log send from contracts.
67    pub logs: Vec<L>,
68}
69
70impl<T: TxTy> EthereumReceipt<T> {
71    /// Returns length of RLP-encoded receipt fields with the given [`Bloom`] without an RLP header.
72    pub fn rlp_encoded_fields_length(&self, bloom: &Bloom) -> usize {
73        self.success.length()
74            + self.cumulative_gas_used.length()
75            + bloom.length()
76            + self.logs.length()
77    }
78
79    /// RLP-encodes receipt fields with the given [`Bloom`] without an RLP header.
80    pub fn rlp_encode_fields(&self, bloom: &Bloom, out: &mut dyn BufMut) {
81        self.success.encode(out);
82        self.cumulative_gas_used.encode(out);
83        bloom.encode(out);
84        self.logs.encode(out);
85    }
86
87    /// Returns RLP header for this receipt encoding with the given [`Bloom`].
88    pub fn rlp_header_inner(&self, bloom: &Bloom) -> Header {
89        Header { list: true, payload_length: self.rlp_encoded_fields_length(bloom) }
90    }
91
92    /// RLP-decodes receipt and [`Bloom`] into [`ReceiptWithBloom`] instance.
93    pub fn rlp_decode_inner(
94        buf: &mut &[u8],
95        tx_type: T,
96    ) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
97        let header = Header::decode(buf)?;
98        if !header.list {
99            return Err(alloy_rlp::Error::UnexpectedString);
100        }
101
102        let remaining = buf.len();
103
104        let success = Decodable::decode(buf)?;
105        let cumulative_gas_used = Decodable::decode(buf)?;
106        let logs_bloom = Decodable::decode(buf)?;
107        let logs = Decodable::decode(buf)?;
108
109        let this = ReceiptWithBloom {
110            receipt: Self { tx_type, success, cumulative_gas_used, logs },
111            logs_bloom,
112        };
113
114        if buf.len() + header.payload_length != remaining {
115            return Err(alloy_rlp::Error::UnexpectedLength);
116        }
117
118        Ok(this)
119    }
120
121    /// Calculates the receipt root for a list of receipts.
122    ///
123    /// NOTE: Prefer [`crate::proofs::calculate_receipt_root`] if you have log blooms memoized.
124    pub fn calculate_receipt_root_no_memo(receipts: &[Self]) -> B256 {
125        ordered_trie_root_with_encoder(receipts, |r, buf| r.with_bloom_ref().encode_2718(buf))
126    }
127}
128
129impl<T: TxTy> Eip2718EncodableReceipt for EthereumReceipt<T> {
130    fn eip2718_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
131        !self.tx_type.is_legacy() as usize + self.rlp_header_inner(bloom).length_with_payload()
132    }
133
134    fn eip2718_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
135        if !self.tx_type.is_legacy() {
136            out.put_u8(self.tx_type.ty());
137        }
138        self.rlp_header_inner(bloom).encode(out);
139        self.rlp_encode_fields(bloom, out);
140    }
141}
142
143impl<T: TxTy> Eip2718DecodableReceipt for EthereumReceipt<T> {
144    fn typed_decode_with_bloom(ty: u8, buf: &mut &[u8]) -> Eip2718Result<ReceiptWithBloom<Self>> {
145        Ok(Self::rlp_decode_inner(buf, T::try_from(ty)?)?)
146    }
147
148    fn fallback_decode_with_bloom(buf: &mut &[u8]) -> Eip2718Result<ReceiptWithBloom<Self>> {
149        Ok(Self::rlp_decode_inner(buf, T::try_from(0)?)?)
150    }
151}
152
153impl<T: TxTy> RlpEncodableReceipt for EthereumReceipt<T> {
154    fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
155        let payload_length = self.eip2718_encoded_length_with_bloom(bloom);
156
157        if !self.tx_type.is_legacy() {
158            payload_length + Header { list: false, payload_length }.length()
159        } else {
160            payload_length
161        }
162    }
163
164    fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) {
165        if !self.tx_type.is_legacy() {
166            Header { list: false, payload_length: self.eip2718_encoded_length_with_bloom(bloom) }
167                .encode(out);
168        }
169        self.eip2718_encode_with_bloom(bloom, out);
170    }
171}
172
173impl<T: TxTy> RlpDecodableReceipt for EthereumReceipt<T> {
174    fn rlp_decode_with_bloom(buf: &mut &[u8]) -> alloy_rlp::Result<ReceiptWithBloom<Self>> {
175        let header_buf = &mut &**buf;
176        let header = Header::decode(header_buf)?;
177
178        // Legacy receipt, reuse initial buffer without advancing
179        if header.list {
180            return Self::rlp_decode_inner(buf, T::try_from(0)?);
181        }
182
183        // Otherwise, advance the buffer and try decoding type flag followed by receipt
184        *buf = *header_buf;
185
186        let remaining = buf.len();
187
188        let tx_type = T::decode(buf)?;
189        let this = Self::rlp_decode_inner(buf, tx_type)?;
190
191        if buf.len() + header.payload_length != remaining {
192            return Err(alloy_rlp::Error::UnexpectedLength);
193        }
194
195        Ok(this)
196    }
197}
198
199impl<T, L> TxReceipt for EthereumReceipt<T, L>
200where
201    T: TxTy,
202    L: Send + Sync + Clone + Debug + Eq + AsRef<Log>,
203{
204    type Log = L;
205
206    fn status_or_post_state(&self) -> Eip658Value {
207        self.success.into()
208    }
209
210    fn status(&self) -> bool {
211        self.success
212    }
213
214    fn bloom(&self) -> Bloom {
215        alloy_primitives::logs_bloom(self.logs.iter().map(|l| l.as_ref()))
216    }
217
218    fn cumulative_gas_used(&self) -> u64 {
219        self.cumulative_gas_used
220    }
221
222    fn logs(&self) -> &[L] {
223        &self.logs
224    }
225
226    fn into_logs(self) -> Vec<L> {
227        self.logs
228    }
229}
230
231impl<T: TxTy> Typed2718 for EthereumReceipt<T> {
232    fn ty(&self) -> u8 {
233        self.tx_type.ty()
234    }
235}
236
237impl<T: TxTy + IsTyped2718> IsTyped2718 for EthereumReceipt<T> {
238    fn is_type(type_id: u8) -> bool {
239        <T as IsTyped2718>::is_type(type_id)
240    }
241}
242
243impl<T: TxTy> InMemorySize for EthereumReceipt<T> {
244    fn size(&self) -> usize {
245        core::mem::size_of::<Self>() + self.logs.iter().map(|log| log.size()).sum::<usize>()
246    }
247}
248
249impl<T> From<ReceiptEnvelope<T>> for EthereumReceipt<TxType>
250where
251    T: Into<Log>,
252{
253    fn from(value: ReceiptEnvelope<T>) -> Self {
254        let value = value.into_primitives_receipt();
255        Self {
256            tx_type: value.tx_type(),
257            success: value.is_success(),
258            cumulative_gas_used: value.cumulative_gas_used(),
259            logs: value.into_logs(),
260        }
261    }
262}
263
264impl<T, L> From<EthereumReceipt<T, L>> for crate::Receipt<L> {
265    fn from(value: EthereumReceipt<T, L>) -> Self {
266        Self {
267            status: value.success.into(),
268            cumulative_gas_used: value.cumulative_gas_used,
269            logs: value.logs,
270        }
271    }
272}
273
274impl<L> From<EthereumReceipt<TxType, L>> for ReceiptEnvelope<L>
275where
276    L: Send + Sync + Clone + Debug + Eq + AsRef<Log>,
277{
278    fn from(value: EthereumReceipt<TxType, L>) -> Self {
279        let tx_type = value.tx_type;
280        let receipt = value.into_with_bloom().map_receipt(Into::into);
281        match tx_type {
282            TxType::Legacy => Self::Legacy(receipt),
283            TxType::Eip2930 => Self::Eip2930(receipt),
284            TxType::Eip1559 => Self::Eip1559(receipt),
285            TxType::Eip4844 => Self::Eip4844(receipt),
286            TxType::Eip7702 => Self::Eip7702(receipt),
287        }
288    }
289}
290
291#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
292pub(crate) mod serde_bincode_compat {
293    use alloc::{borrow::Cow, vec::Vec};
294    use alloy_eips::eip2718::Eip2718Error;
295    use alloy_primitives::{Log, U8};
296    use core::fmt::Debug;
297    use serde::{Deserialize, Deserializer, Serialize, Serializer};
298    use serde_with::{DeserializeAs, SerializeAs};
299
300    /// Bincode-compatible [`super::EthereumReceipt`] serde implementation.
301    ///
302    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
303    /// ```rust
304    /// use alloy_consensus::{serde_bincode_compat, EthereumReceipt, TxType};
305    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
306    /// use serde_with::serde_as;
307    ///
308    /// #[serde_as]
309    /// #[derive(Serialize, Deserialize)]
310    /// struct Data {
311    ///     #[serde_as(as = "serde_bincode_compat::EthereumReceipt<'_>")]
312    ///     receipt: EthereumReceipt<TxType>,
313    /// }
314    /// ```
315    #[derive(Debug, Serialize, Deserialize)]
316    #[serde(bound(deserialize = "T: TryFrom<u8, Error = Eip2718Error>"))]
317    pub struct EthereumReceipt<'a, T = crate::TxType> {
318        /// Receipt type.
319        #[serde(deserialize_with = "deserde_txtype")]
320        pub tx_type: T,
321        /// If transaction is executed successfully.
322        ///
323        /// This is the `statusCode`
324        pub success: bool,
325        /// Gas used
326        pub cumulative_gas_used: u64,
327        /// Log send from contracts.
328        pub logs: Cow<'a, Vec<Log>>,
329    }
330
331    /// Ensures that txtype is deserialized symmetrically as U8
332    fn deserde_txtype<'de, D, T>(deserializer: D) -> Result<T, D::Error>
333    where
334        D: Deserializer<'de>,
335        T: TryFrom<u8, Error = Eip2718Error>,
336    {
337        U8::deserialize(deserializer)?.to::<u8>().try_into().map_err(serde::de::Error::custom)
338    }
339
340    impl<'a, T: Copy> From<&'a super::EthereumReceipt<T>> for EthereumReceipt<'a, T> {
341        fn from(value: &'a super::EthereumReceipt<T>) -> Self {
342            Self {
343                tx_type: value.tx_type,
344                success: value.success,
345                cumulative_gas_used: value.cumulative_gas_used,
346                logs: Cow::Borrowed(&value.logs),
347            }
348        }
349    }
350
351    impl<'a, T> From<EthereumReceipt<'a, T>> for super::EthereumReceipt<T> {
352        fn from(value: EthereumReceipt<'a, T>) -> Self {
353            Self {
354                tx_type: value.tx_type,
355                success: value.success,
356                cumulative_gas_used: value.cumulative_gas_used,
357                logs: value.logs.into_owned(),
358            }
359        }
360    }
361
362    impl<T: Copy + Serialize> SerializeAs<super::EthereumReceipt<T>> for EthereumReceipt<'_, T> {
363        fn serialize_as<S>(
364            source: &super::EthereumReceipt<T>,
365            serializer: S,
366        ) -> Result<S::Ok, S::Error>
367        where
368            S: Serializer,
369        {
370            EthereumReceipt::<'_>::from(source).serialize(serializer)
371        }
372    }
373
374    impl<'de, T: TryFrom<u8, Error = Eip2718Error>> DeserializeAs<'de, super::EthereumReceipt<T>>
375        for EthereumReceipt<'de, T>
376    {
377        fn deserialize_as<D>(deserializer: D) -> Result<super::EthereumReceipt<T>, D::Error>
378        where
379            D: Deserializer<'de>,
380        {
381            EthereumReceipt::<'_, T>::deserialize(deserializer).map(Into::into)
382        }
383    }
384
385    #[cfg(test)]
386    mod tests {
387        use crate::TxType;
388        use arbitrary::Arbitrary;
389        use bincode::config;
390        use rand::Rng;
391        use serde_with::serde_as;
392
393        use super::super::EthereumReceipt;
394
395        #[test]
396        fn test_ethereum_receipt_bincode_roundtrip() {
397            #[serde_as]
398            #[derive(Debug, PartialEq, Eq)]
399            #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
400            struct Data {
401                #[serde_as(as = "super::EthereumReceipt<'_, TxType>")]
402                receipt: EthereumReceipt<TxType>,
403            }
404
405            let mut bytes = [0u8; 1024];
406            rand::thread_rng().fill(bytes.as_mut_slice());
407            let data = Data {
408                receipt: EthereumReceipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
409                    .unwrap(),
410            };
411
412            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
413            let (decoded, _): (Data, _) =
414                bincode::serde::decode_from_slice(&encoded, config::legacy()).unwrap();
415            assert_eq!(decoded, data);
416        }
417    }
418}