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