Skip to main content

reth_eth_wire_types/
receipts.rs

1//! Implements the `GetReceipts` and `Receipts` message types.
2
3use alloc::vec::Vec;
4use alloy_consensus::{ReceiptWithBloom, RlpDecodableReceipt, RlpEncodableReceipt, TxReceipt};
5use alloy_primitives::B256;
6use alloy_rlp::{RlpDecodableWrapper, RlpEncodableWrapper};
7use derive_more::{Deref, IntoIterator};
8use reth_codecs_derive::add_arbitrary_tests;
9use reth_ethereum_primitives::Receipt;
10
11/// A request for transaction receipts from the given block hashes.
12#[derive(
13    Clone,
14    Debug,
15    PartialEq,
16    Eq,
17    RlpEncodableWrapper,
18    RlpDecodableWrapper,
19    Default,
20    Deref,
21    IntoIterator,
22)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
25#[add_arbitrary_tests(rlp)]
26pub struct GetReceipts(
27    /// The block hashes to request receipts for.
28    pub Vec<B256>,
29);
30
31/// Eth/70 `GetReceipts` request payload that supports partial receipt queries.
32///
33/// When used with eth/70, the request id is carried by the surrounding
34/// [`crate::message::RequestPair`], and the on-wire shape is the flattened list
35/// `firstBlockReceiptIndex, [blockhash₁, ...]`.
36///
37/// See also [eip-7975](https://eips.ethereum.org/EIPS/eip-7975)
38#[derive(Clone, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
41pub struct GetReceipts70 {
42    /// Index into the receipts of the first requested block hash.
43    pub first_block_receipt_index: u64,
44    /// The block hashes to request receipts for.
45    pub block_hashes: Vec<B256>,
46}
47
48impl alloy_rlp::Encodable for GetReceipts70 {
49    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
50        self.first_block_receipt_index.encode(out);
51        self.block_hashes.encode(out);
52    }
53
54    fn length(&self) -> usize {
55        self.first_block_receipt_index.length() + self.block_hashes.length()
56    }
57}
58
59impl alloy_rlp::Decodable for GetReceipts70 {
60    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
61        let first_block_receipt_index = u64::decode(buf)?;
62        let block_hashes = Vec::<B256>::decode(buf)?;
63        Ok(Self { first_block_receipt_index, block_hashes })
64    }
65}
66
67/// The response to [`GetReceipts`], containing receipt lists that correspond to each block
68/// requested.
69#[derive(Clone, Debug, PartialEq, Eq, Default, Deref, IntoIterator)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
72#[add_arbitrary_tests(rlp)]
73pub struct Receipts<T = Receipt>(
74    /// Each receipt hash should correspond to a block hash in the request.
75    pub Vec<Vec<ReceiptWithBloom<T>>>,
76);
77
78impl<T: RlpEncodableReceipt> alloy_rlp::Encodable for Receipts<T> {
79    #[inline]
80    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
81        self.0.encode(out)
82    }
83    #[inline]
84    fn length(&self) -> usize {
85        self.0.length()
86    }
87}
88
89impl<T: RlpDecodableReceipt> alloy_rlp::Decodable for Receipts<T> {
90    #[inline]
91    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
92        alloy_rlp::Decodable::decode(buf).map(Self)
93    }
94}
95
96/// Eth/69 receipt response type that removes bloom filters from the protocol.
97///
98/// This is effectively a subset of [`Receipts`].
99#[derive(
100    Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Deref, IntoIterator,
101)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
104#[add_arbitrary_tests(rlp)]
105pub struct Receipts69<T = Receipt>(pub Vec<Vec<T>>);
106
107impl<T: TxReceipt> Receipts69<T> {
108    /// Encodes all receipts with the bloom filter.
109    ///
110    /// Eth/69 omits bloom filters on the wire, while some internal callers
111    /// (and legacy APIs) still operate on [`Receipts`] with
112    /// [`ReceiptWithBloom`]. This helper reconstructs the bloom locally from
113    /// each receipt's logs so the older API can be used on top of eth/69 data.
114    ///
115    /// Note: This is an expensive operation that recalculates the bloom for
116    /// every receipt.
117    pub fn into_with_bloom(self) -> Receipts<T> {
118        Receipts(
119            self.into_iter()
120                .map(|receipts| receipts.into_iter().map(|r| r.into_with_bloom()).collect())
121                .collect(),
122        )
123    }
124}
125
126impl<T: TxReceipt> From<Receipts69<T>> for Receipts<T> {
127    fn from(receipts: Receipts69<T>) -> Self {
128        receipts.into_with_bloom()
129    }
130}
131
132/// Eth/70 `Receipts` response payload.
133///
134/// This is used in conjunction with [`crate::message::RequestPair`] to encode the full wire
135/// message `[request-id, lastBlockIncomplete, [[receipt₁, receipt₂], ...]]`.
136#[derive(Clone, Debug, PartialEq, Eq)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
139pub struct Receipts70<T = Receipt> {
140    /// Whether the receipts list for the last block is incomplete.
141    pub last_block_incomplete: bool,
142    /// Receipts grouped by block.
143    pub receipts: Vec<Vec<T>>,
144}
145
146impl<T> alloy_rlp::Encodable for Receipts70<T>
147where
148    T: alloy_rlp::Encodable,
149{
150    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
151        self.last_block_incomplete.encode(out);
152        self.receipts.encode(out);
153    }
154
155    fn length(&self) -> usize {
156        self.last_block_incomplete.length() + self.receipts.length()
157    }
158}
159
160impl<T> alloy_rlp::Decodable for Receipts70<T>
161where
162    T: alloy_rlp::Decodable,
163{
164    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
165        let last_block_incomplete = bool::decode(buf)?;
166        let receipts = Vec::<Vec<T>>::decode(buf)?;
167        Ok(Self { last_block_incomplete, receipts })
168    }
169}
170
171impl<T: TxReceipt> Receipts70<T> {
172    /// Encodes all receipts with the bloom filter.
173    ///
174    /// Just like eth/69, eth/70 does not transmit bloom filters over the wire.
175    /// When higher layers still expect the older bloom-bearing [`Receipts`]
176    /// type, this helper converts the eth/70 payload into that shape by
177    /// recomputing the bloom locally from the contained receipts.
178    ///
179    /// Note: This is an expensive operation that recalculates the bloom for
180    /// every receipt.
181    pub fn into_with_bloom(self) -> Receipts<T> {
182        // Reuse the eth/69 helper, since both variants carry the same
183        // receipt list shape (only eth/70 adds request metadata).
184        Receipts69(self.receipts).into_with_bloom()
185    }
186}
187
188impl<T: TxReceipt> From<Receipts70<T>> for Receipts<T> {
189    fn from(receipts: Receipts70<T>) -> Self {
190        receipts.into_with_bloom()
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::{message::RequestPair, GetReceipts, Receipts};
198    use alloy_consensus::TxType;
199    use alloy_primitives::{hex, Log};
200    use alloy_rlp::{Decodable, Encodable};
201
202    #[test]
203    fn roundtrip_eip1559() {
204        let receipts = Receipts(vec![vec![ReceiptWithBloom {
205            receipt: Receipt { tx_type: TxType::Eip1559, ..Default::default() },
206            logs_bloom: Default::default(),
207        }]]);
208
209        let mut out = vec![];
210        receipts.encode(&mut out);
211
212        let mut out = out.as_slice();
213        let decoded = Receipts::decode(&mut out).unwrap();
214
215        assert_eq!(receipts, decoded);
216    }
217
218    #[test]
219    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
220    fn encode_get_receipts() {
221        let expected = hex!(
222            "f847820457f842a000000000000000000000000000000000000000000000000000000000deadc0dea000000000000000000000000000000000000000000000000000000000feedbeef"
223        );
224        let mut data = vec![];
225        let request = RequestPair {
226            request_id: 1111,
227            message: GetReceipts(vec![
228                hex!("00000000000000000000000000000000000000000000000000000000deadc0de").into(),
229                hex!("00000000000000000000000000000000000000000000000000000000feedbeef").into(),
230            ]),
231        };
232        request.encode(&mut data);
233        assert_eq!(data, expected);
234    }
235
236    #[test]
237    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
238    fn decode_get_receipts() {
239        let data = hex!(
240            "f847820457f842a000000000000000000000000000000000000000000000000000000000deadc0dea000000000000000000000000000000000000000000000000000000000feedbeef"
241        );
242        let request = RequestPair::<GetReceipts>::decode(&mut &data[..]).unwrap();
243        assert_eq!(
244            request,
245            RequestPair {
246                request_id: 1111,
247                message: GetReceipts(vec![
248                    hex!("00000000000000000000000000000000000000000000000000000000deadc0de").into(),
249                    hex!("00000000000000000000000000000000000000000000000000000000feedbeef").into(),
250                ]),
251            }
252        );
253    }
254
255    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
256    #[test]
257    fn encode_receipts() {
258        let expected = hex!(
259            "f90172820457f9016cf90169f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
260        );
261        let mut data = vec![];
262        let request = RequestPair {
263            request_id: 1111,
264            message: Receipts(vec![vec![
265                ReceiptWithBloom {
266                    receipt: Receipt {
267                        tx_type: TxType::Legacy,
268                        cumulative_gas_used: 0x1u64,
269                        logs: vec![
270                            Log::new_unchecked(
271                                hex!("0000000000000000000000000000000000000011").into(),
272                                vec![
273                                    hex!("000000000000000000000000000000000000000000000000000000000000dead").into(),
274                                    hex!("000000000000000000000000000000000000000000000000000000000000beef").into(),
275                                ],
276                                hex!("0100ff")[..].into(),
277                            ),
278                        ],
279                        success: false,
280                    },
281                    logs_bloom: hex!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(),
282                },
283            ]]),
284        };
285        request.encode(&mut data);
286        assert_eq!(data, expected);
287    }
288
289    // Test vector from: https://eips.ethereum.org/EIPS/eip-2481
290    #[test]
291    fn decode_receipts() {
292        let data = hex!(
293            "f90172820457f9016cf90169f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
294        );
295        let request = RequestPair::<Receipts>::decode(&mut &data[..]).unwrap();
296        assert_eq!(
297            request,
298            RequestPair {
299                request_id: 1111,
300                message: Receipts(vec![
301                    vec![
302                        ReceiptWithBloom {
303                            receipt: Receipt {
304                                tx_type: TxType::Legacy,
305                                cumulative_gas_used: 0x1u64,
306                                logs: vec![
307                                    Log::new_unchecked(
308                                        hex!("0000000000000000000000000000000000000011").into(),
309                                        vec![
310                                            hex!("000000000000000000000000000000000000000000000000000000000000dead").into(),
311                                            hex!("000000000000000000000000000000000000000000000000000000000000beef").into(),
312                                        ],
313                                        hex!("0100ff")[..].into(),
314                                    ),
315                                ],
316                                success: false,
317                            },
318                            logs_bloom: hex!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(),
319                        },
320                    ],
321                ]),
322            }
323        );
324    }
325
326    #[test]
327    fn decode_receipts_69() {
328        let data = hex!("0xf9026605f90262f9025fc60201826590c0c7800183013cd9c0c702018301a2a5c0c7010183027a36c0c702018302e03ec0c7010183034646c0c702018303ac30c0c78001830483b8c0c702018304e9a2c0c780018305c17fc0c7020183062769c0c7800183068d71c0c702018306f35bc0c702018307cb77c0c701018308a382c0c7020183097ab6c0c78080830b0156c0c70101830b6740c0c70201830bcd48c0c70101830c32f6c0c70101830c98e0c0c70201830cfecac0c70201830d64b4c0c70280830dca9ec0c70101830e30a6c0c70201830f080dc0c70201830f6e15c0c78080830fd41dc0c702018310abbac0c701018310fdc2c0c7020183116370c0c780018311c95ac0c7010183122f44c0c701808312952ec0c7020183136c7dc0c70201831443c0c0c702018314a9c8c0c7020183150f94c0c7018083169634c0c7020183176d68c0c702808317d370c0c70201831838c4c0c701808319bf64c0c70201831a256cc0c78080831bac0cc0c70201831c11d8c0c70201831c77c2c0c78080831cdd34c0c70201831db57bc0c70101831e8d07c0c70101831ef2d3c0c70201831fcb37c0c70180832030e5c0c70201832096cfc0c701018320fcb9c0c70201832162c1c0c702018321c8abc0c7020183229ffac0c70201832305c6c0c7028083236bcec0c702808323d1d6c0c702018324a91cc0c7020183250f06c0c70201832574d2c0c7020183264c15c0c70201832723b6c0c70201832789a0c0c702018327ef8ac0c7020183285574c0c702018328bb40c0c702018329212ac0c7028083298714c0c70201832a5e4ec0c70201832ac438c0c70201832b9b72c0c70201832c017ac0");
329
330        let request = RequestPair::<Receipts69>::decode(&mut &data[..]).unwrap();
331        assert_eq!(
332            request.message.0[0][0],
333            Receipt {
334                tx_type: TxType::Eip1559,
335                success: true,
336                cumulative_gas_used: 26000,
337                logs: vec![],
338            }
339        );
340
341        let encoded = alloy_rlp::encode(&request);
342        assert_eq!(encoded, data);
343    }
344
345    #[test]
346    fn encode_get_receipts70_inline_shape() {
347        let req = RequestPair {
348            request_id: 1111,
349            message: GetReceipts70 {
350                first_block_receipt_index: 0,
351                block_hashes: vec![
352                    hex!("00000000000000000000000000000000000000000000000000000000deadc0de").into(),
353                    hex!("00000000000000000000000000000000000000000000000000000000feedbeef").into(),
354                ],
355            },
356        };
357
358        let mut out = vec![];
359        req.encode(&mut out);
360
361        let mut buf = out.as_slice();
362        let header = alloy_rlp::Header::decode(&mut buf).unwrap();
363        let payload_start = buf.len();
364        let request_id = u64::decode(&mut buf).unwrap();
365        let first_block_receipt_index = u64::decode(&mut buf).unwrap();
366        let block_hashes = Vec::<B256>::decode(&mut buf).unwrap();
367
368        assert!(buf.is_empty(), "buffer not fully consumed");
369        assert_eq!(request_id, 1111);
370        assert_eq!(first_block_receipt_index, 0);
371        assert_eq!(block_hashes.len(), 2);
372        // ensure payload length matches header
373        assert_eq!(payload_start - buf.len(), header.payload_length);
374
375        let mut buf = out.as_slice();
376        let decoded = RequestPair::<GetReceipts70>::decode(&mut buf).unwrap();
377        assert!(buf.is_empty(), "buffer not fully consumed on decode");
378        assert_eq!(decoded, req);
379    }
380
381    #[test]
382    fn encode_receipts70_inline_shape() {
383        let payload: Receipts70<Receipt> =
384            Receipts70 { last_block_incomplete: true, receipts: vec![vec![Receipt::default()]] };
385
386        let resp = RequestPair { request_id: 7, message: payload };
387
388        let mut out = vec![];
389        resp.encode(&mut out);
390
391        let mut buf = out.as_slice();
392        let header = alloy_rlp::Header::decode(&mut buf).unwrap();
393        let payload_start = buf.len();
394        let request_id = u64::decode(&mut buf).unwrap();
395        let last_block_incomplete = bool::decode(&mut buf).unwrap();
396        let receipts = Vec::<Vec<Receipt>>::decode(&mut buf).unwrap();
397
398        assert!(buf.is_empty(), "buffer not fully consumed");
399        assert_eq!(payload_start - buf.len(), header.payload_length);
400        assert_eq!(request_id, 7);
401        assert!(last_block_incomplete);
402        assert_eq!(receipts.len(), 1);
403        assert_eq!(receipts[0].len(), 1);
404
405        let mut buf = out.as_slice();
406        let decoded = RequestPair::<Receipts70>::decode(&mut buf).unwrap();
407        assert!(buf.is_empty(), "buffer not fully consumed on decode");
408        assert_eq!(decoded, resp);
409    }
410}