reth_optimism_primitives/
receipt.rs

1use alloc::vec::Vec;
2use alloy_consensus::{
3    Eip2718EncodableReceipt, Eip658Value, Receipt, ReceiptWithBloom, RlpDecodableReceipt,
4    RlpEncodableReceipt, TxReceipt, Typed2718,
5};
6use alloy_eips::{
7    eip2718::{Eip2718Result, IsTyped2718},
8    Decodable2718, Encodable2718,
9};
10use alloy_primitives::{Bloom, Log};
11use alloy_rlp::{BufMut, Decodable, Encodable, Header};
12use op_alloy_consensus::{OpDepositReceipt, OpReceipt, OpTxType};
13use reth_primitives_traits::InMemorySize;
14
15/// Trait for deposit receipt.
16pub trait DepositReceipt: reth_primitives_traits::Receipt {
17    /// Converts a `Receipt` into a mutable Optimism deposit receipt.
18    fn as_deposit_receipt_mut(&mut self) -> Option<&mut OpDepositReceipt>;
19
20    /// Extracts an Optimism deposit receipt from `Receipt`.
21    fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt>;
22}
23
24impl DepositReceipt for OpReceipt {
25    fn as_deposit_receipt_mut(&mut self) -> Option<&mut OpDepositReceipt> {
26        match self {
27            Self::Deposit(receipt) => Some(receipt),
28            _ => None,
29        }
30    }
31
32    fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt> {
33        match self {
34            Self::Deposit(receipt) => Some(receipt),
35            _ => None,
36        }
37    }
38}
39
40#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
41pub(super) mod serde_bincode_compat {
42    use serde::{Deserialize, Deserializer, Serialize, Serializer};
43    use serde_with::{DeserializeAs, SerializeAs};
44
45    /// Bincode-compatible [`super::OpReceipt`] serde implementation.
46    ///
47    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
48    /// ```rust
49    /// use reth_optimism_primitives::OpReceipt;
50    /// use reth_primitives_traits::serde_bincode_compat::SerdeBincodeCompat;
51    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
52    /// use serde_with::serde_as;
53    ///
54    /// #[serde_as]
55    /// #[derive(Serialize, Deserialize)]
56    /// struct Data {
57    ///     #[serde_as(
58    ///         as = "reth_primitives_traits::serde_bincode_compat::BincodeReprFor<'_, OpReceipt>"
59    ///     )]
60    ///     receipt: OpReceipt,
61    /// }
62    /// ```
63    #[allow(rustdoc::private_doc_tests)]
64    #[derive(Debug, Serialize, Deserialize)]
65    pub enum OpReceipt<'a> {
66        /// Legacy receipt
67        Legacy(alloy_consensus::serde_bincode_compat::Receipt<'a, alloy_primitives::Log>),
68        /// EIP-2930 receipt
69        Eip2930(alloy_consensus::serde_bincode_compat::Receipt<'a, alloy_primitives::Log>),
70        /// EIP-1559 receipt
71        Eip1559(alloy_consensus::serde_bincode_compat::Receipt<'a, alloy_primitives::Log>),
72        /// EIP-7702 receipt
73        Eip7702(alloy_consensus::serde_bincode_compat::Receipt<'a, alloy_primitives::Log>),
74        /// Deposit receipt
75        Deposit(
76            op_alloy_consensus::serde_bincode_compat::OpDepositReceipt<'a, alloy_primitives::Log>,
77        ),
78    }
79
80    impl<'a> From<&'a super::OpReceipt> for OpReceipt<'a> {
81        fn from(value: &'a super::OpReceipt) -> Self {
82            match value {
83                super::OpReceipt::Legacy(receipt) => Self::Legacy(receipt.into()),
84                super::OpReceipt::Eip2930(receipt) => Self::Eip2930(receipt.into()),
85                super::OpReceipt::Eip1559(receipt) => Self::Eip1559(receipt.into()),
86                super::OpReceipt::Eip7702(receipt) => Self::Eip7702(receipt.into()),
87                super::OpReceipt::Deposit(receipt) => Self::Deposit(receipt.into()),
88            }
89        }
90    }
91
92    impl<'a> From<OpReceipt<'a>> for super::OpReceipt {
93        fn from(value: OpReceipt<'a>) -> Self {
94            match value {
95                OpReceipt::Legacy(receipt) => Self::Legacy(receipt.into()),
96                OpReceipt::Eip2930(receipt) => Self::Eip2930(receipt.into()),
97                OpReceipt::Eip1559(receipt) => Self::Eip1559(receipt.into()),
98                OpReceipt::Eip7702(receipt) => Self::Eip7702(receipt.into()),
99                OpReceipt::Deposit(receipt) => Self::Deposit(receipt.into()),
100            }
101        }
102    }
103
104    impl SerializeAs<super::OpReceipt> for OpReceipt<'_> {
105        fn serialize_as<S>(source: &super::OpReceipt, serializer: S) -> Result<S::Ok, S::Error>
106        where
107            S: Serializer,
108        {
109            OpReceipt::<'_>::from(source).serialize(serializer)
110        }
111    }
112
113    impl<'de> DeserializeAs<'de, super::OpReceipt> for OpReceipt<'de> {
114        fn deserialize_as<D>(deserializer: D) -> Result<super::OpReceipt, D::Error>
115        where
116            D: Deserializer<'de>,
117        {
118            OpReceipt::<'_>::deserialize(deserializer).map(Into::into)
119        }
120    }
121
122    #[cfg(test)]
123    mod tests {
124        use crate::{receipt::serde_bincode_compat, OpReceipt};
125        use arbitrary::Arbitrary;
126        use rand::Rng;
127        use serde::{Deserialize, Serialize};
128        use serde_with::serde_as;
129
130        #[test]
131        fn test_tx_bincode_roundtrip() {
132            #[serde_as]
133            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
134            struct Data {
135                #[serde_as(as = "serde_bincode_compat::OpReceipt<'_>")]
136                receipt: OpReceipt,
137            }
138
139            let mut bytes = [0u8; 1024];
140            rand::rng().fill(bytes.as_mut_slice());
141            let mut data = Data {
142                receipt: OpReceipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(),
143            };
144            let success = data.receipt.as_receipt_mut().status.coerce_status();
145            // // ensure we don't have an invalid poststate variant
146            data.receipt.as_receipt_mut().status = success.into();
147
148            let encoded = bincode::serialize(&data).unwrap();
149            let decoded: Data = bincode::deserialize(&encoded).unwrap();
150            assert_eq!(decoded, data);
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use alloy_eips::eip2718::Encodable2718;
159    use alloy_primitives::{address, b256, bytes, hex_literal::hex, Bytes};
160    use alloy_rlp::Encodable;
161    use reth_codecs::Compact;
162
163    #[test]
164    fn test_decode_receipt() {
165        reth_codecs::test_utils::test_decode::<OpReceipt>(&hex!(
166            "c30328b52ffd23fc426961a00105007eb0042307705a97e503562eacf2b95060cce9de6de68386b6c155b73a9650021a49e2f8baad17f30faff5899d785c4c0873e45bc268bcf07560106424570d11f9a59e8f3db1efa4ceec680123712275f10d92c3411e1caaa11c7c5d591bc11487168e09934a9986848136da1b583babf3a7188e3aed007a1520f1cf4c1ca7d3482c6c28d37c298613c70a76940008816c4c95644579fd08471dc34732fd0f24"
167        ));
168    }
169
170    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
171    #[test]
172    fn encode_legacy_receipt() {
173        let expected = hex!(
174            "f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
175        );
176
177        let mut data = Vec::with_capacity(expected.length());
178        let receipt = ReceiptWithBloom {
179            receipt: OpReceipt::Legacy(Receipt::<Log> {
180                status: Eip658Value::Eip658(false),
181                cumulative_gas_used: 0x1,
182                logs: vec![Log::new_unchecked(
183                    address!("0x0000000000000000000000000000000000000011"),
184                    vec![
185                        b256!("0x000000000000000000000000000000000000000000000000000000000000dead"),
186                        b256!("0x000000000000000000000000000000000000000000000000000000000000beef"),
187                    ],
188                    bytes!("0100ff"),
189                )],
190            }),
191            logs_bloom: [0; 256].into(),
192        };
193
194        receipt.encode(&mut data);
195
196        // check that the rlp length equals the length of the expected rlp
197        assert_eq!(receipt.length(), expected.len());
198        assert_eq!(data, expected);
199    }
200
201    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
202    #[test]
203    fn decode_legacy_receipt() {
204        let data = hex!(
205            "f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
206        );
207
208        // EIP658Receipt
209        let expected = ReceiptWithBloom {
210            receipt: OpReceipt::Legacy(Receipt::<Log> {
211                status: Eip658Value::Eip658(false),
212                cumulative_gas_used: 0x1,
213                logs: vec![Log::new_unchecked(
214                    address!("0x0000000000000000000000000000000000000011"),
215                    vec![
216                        b256!("0x000000000000000000000000000000000000000000000000000000000000dead"),
217                        b256!("0x000000000000000000000000000000000000000000000000000000000000beef"),
218                    ],
219                    bytes!("0100ff"),
220                )],
221            }),
222            logs_bloom: [0; 256].into(),
223        };
224
225        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
226        assert_eq!(receipt, expected);
227    }
228
229    #[test]
230    fn decode_deposit_receipt_regolith_roundtrip() {
231        let data = hex!(
232            "b901107ef9010c0182b741b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0833d3bbf"
233        );
234
235        // Deposit Receipt (post-regolith)
236        let expected = ReceiptWithBloom {
237            receipt: OpReceipt::Deposit(OpDepositReceipt {
238                inner: Receipt::<Log> {
239                    status: Eip658Value::Eip658(true),
240                    cumulative_gas_used: 46913,
241                    logs: vec![],
242                },
243                deposit_nonce: Some(4012991),
244                deposit_receipt_version: None,
245            }),
246            logs_bloom: [0; 256].into(),
247        };
248
249        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
250        assert_eq!(receipt, expected);
251
252        let mut buf = Vec::with_capacity(data.len());
253        receipt.encode(&mut buf);
254        assert_eq!(buf, &data[..]);
255    }
256
257    #[test]
258    fn decode_deposit_receipt_canyon_roundtrip() {
259        let data = hex!(
260            "b901117ef9010d0182b741b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0833d3bbf01"
261        );
262
263        // Deposit Receipt (post-canyon)
264        let expected = ReceiptWithBloom {
265            receipt: OpReceipt::Deposit(OpDepositReceipt {
266                inner: Receipt::<Log> {
267                    status: Eip658Value::Eip658(true),
268                    cumulative_gas_used: 46913,
269                    logs: vec![],
270                },
271                deposit_nonce: Some(4012991),
272                deposit_receipt_version: Some(1),
273            }),
274            logs_bloom: [0; 256].into(),
275        };
276
277        let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap();
278        assert_eq!(receipt, expected);
279
280        let mut buf = Vec::with_capacity(data.len());
281        expected.encode(&mut buf);
282        assert_eq!(buf, &data[..]);
283    }
284
285    #[test]
286    fn gigantic_receipt() {
287        let receipt = OpReceipt::Legacy(Receipt::<Log> {
288            status: Eip658Value::Eip658(true),
289            cumulative_gas_used: 16747627,
290            logs: vec![
291                Log::new_unchecked(
292                    address!("0x4bf56695415f725e43c3e04354b604bcfb6dfb6e"),
293                    vec![b256!(
294                        "0xc69dc3d7ebff79e41f525be431d5cd3cc08f80eaf0f7819054a726eeb7086eb9"
295                    )],
296                    Bytes::from(vec![1; 0xffffff]),
297                ),
298                Log::new_unchecked(
299                    address!("0xfaca325c86bf9c2d5b413cd7b90b209be92229c2"),
300                    vec![b256!(
301                        "0x8cca58667b1e9ffa004720ac99a3d61a138181963b294d270d91c53d36402ae2"
302                    )],
303                    Bytes::from(vec![1; 0xffffff]),
304                ),
305            ],
306        });
307
308        let mut data = vec![];
309        receipt.to_compact(&mut data);
310        let (decoded, _) = OpReceipt::from_compact(&data[..], data.len());
311        assert_eq!(decoded, receipt);
312    }
313
314    #[test]
315    fn test_encode_2718_length() {
316        let receipt = ReceiptWithBloom {
317            receipt: OpReceipt::Eip1559(Receipt::<Log> {
318                status: Eip658Value::Eip658(true),
319                cumulative_gas_used: 21000,
320                logs: vec![],
321            }),
322            logs_bloom: Bloom::default(),
323        };
324
325        let encoded = receipt.encoded_2718();
326        assert_eq!(
327            encoded.len(),
328            receipt.encode_2718_len(),
329            "Encoded length should match the actual encoded data length"
330        );
331
332        // Test for legacy receipt as well
333        let legacy_receipt = ReceiptWithBloom {
334            receipt: OpReceipt::Legacy(Receipt::<Log> {
335                status: Eip658Value::Eip658(true),
336                cumulative_gas_used: 21000,
337                logs: vec![],
338            }),
339            logs_bloom: Bloom::default(),
340        };
341
342        let legacy_encoded = legacy_receipt.encoded_2718();
343        assert_eq!(
344            legacy_encoded.len(),
345            legacy_receipt.encode_2718_len(),
346            "Encoded length for legacy receipt should match the actual encoded data length"
347        );
348    }
349}