Skip to main content

ethrex_common/types/
receipt.rs

1use bytes::Bytes;
2use ethereum_types::{Address, Bloom, BloomInput, H256};
3use ethrex_crypto::Crypto;
4use ethrex_rlp::{
5    decode::{RLPDecode, get_rlp_bytes_item_payload, is_encoded_as_bytes},
6    encode::RLPEncode,
7    error::RLPDecodeError,
8    structs::{Decoder, Encoder},
9};
10use serde::{Deserialize, Serialize};
11
12use crate::types::TxType;
13pub type Index = u64;
14
15/// Result of a transaction
16#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
17pub struct Receipt {
18    pub tx_type: TxType,
19    pub succeeded: bool,
20    /// Cumulative gas used by this and all previous transactions in the block.
21    /// This is always post-refund gas.
22    /// Note: Block-level gas accounting (pre-refund for EIP-7778) uses BlockExecutionResult::block_gas_used.
23    pub cumulative_gas_used: u64,
24    pub logs: Vec<Log>,
25}
26
27impl Receipt {
28    pub fn new(tx_type: TxType, succeeded: bool, cumulative_gas_used: u64, logs: Vec<Log>) -> Self {
29        Self {
30            tx_type,
31            succeeded,
32            cumulative_gas_used,
33            logs,
34        }
35    }
36
37    pub fn encode_inner(&self) -> Vec<u8> {
38        let mut encoded_data = vec![];
39        let tx_type: u8 = self.tx_type as u8;
40        Encoder::new(&mut encoded_data)
41            .encode_field(&tx_type)
42            .encode_field(&self.succeeded)
43            .encode_field(&self.cumulative_gas_used)
44            .encode_field(&self.logs)
45            .finish();
46        encoded_data
47    }
48
49    pub fn encode_inner_with_bloom(&self, crypto: &dyn Crypto) -> Vec<u8> {
50        self.encode_inner_with_precomputed_bloom(bloom_from_logs(&self.logs, crypto))
51    }
52
53    /// Like [`Self::encode_inner_with_bloom`] but takes an already-computed bloom, so
54    /// callers that also need the bloom for other purposes (e.g. the aggregate header
55    /// `logs_bloom`) don't recompute it.
56    pub fn encode_inner_with_precomputed_bloom(&self, bloom: Bloom) -> Vec<u8> {
57        // Bloom is already 256 bytes, so we preallocate at least that much plus some,
58        // to avoid multiple small allocations.
59        let mut encode_buf = Vec::with_capacity(512);
60        if self.tx_type != TxType::Legacy {
61            encode_buf.push(self.tx_type as u8);
62        }
63        Encoder::new(&mut encode_buf)
64            .encode_field(&self.succeeded)
65            .encode_field(&self.cumulative_gas_used)
66            .encode_field(&bloom)
67            .encode_field(&self.logs)
68            .finish();
69        encode_buf
70    }
71}
72
73pub fn bloom_from_logs(logs: &[Log], crypto: &dyn Crypto) -> Bloom {
74    let mut bloom = Bloom::zero();
75    for log in logs {
76        let address_hash = crypto.keccak256(log.address.as_bytes());
77        bloom.accrue(BloomInput::Hash(&address_hash));
78        for topic in log.topics.iter() {
79            let topic_hash = crypto.keccak256(topic.as_bytes());
80            bloom.accrue(BloomInput::Hash(&topic_hash));
81        }
82    }
83    bloom
84}
85
86impl RLPEncode for Receipt {
87    fn encode(&self, buf: &mut dyn bytes::BufMut) {
88        let encoded_inner = self.encode_inner();
89        buf.put_slice(&encoded_inner);
90    }
91}
92
93impl RLPDecode for Receipt {
94    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
95        let decoder = Decoder::new(rlp)?;
96        let (tx_type, decoder): (u8, _) = decoder.decode_field("tx-type")?;
97        let (succeeded, decoder) = decoder.decode_field("succeeded")?;
98        let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
99        let (logs, decoder) = decoder.decode_field("logs")?;
100
101        let Some(tx_type) = TxType::from_u8(tx_type) else {
102            return Err(RLPDecodeError::Custom(
103                "Invalid transaction type".to_string(),
104            ));
105        };
106
107        Ok((
108            Receipt {
109                tx_type,
110                succeeded,
111                cumulative_gas_used,
112                logs,
113            },
114            decoder.finish()?,
115        ))
116    }
117}
118
119/// Result of a transaction
120#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
121pub struct ReceiptWithBloom {
122    pub tx_type: TxType,
123    pub succeeded: bool,
124    /// Cumulative gas used by this and all previous transactions in the block.
125    /// This is always post-refund gas.
126    /// Note: Block-level gas accounting (pre-refund for EIP-7778) uses BlockExecutionResult::block_gas_used.
127    pub cumulative_gas_used: u64,
128    pub bloom: Bloom,
129    pub logs: Vec<Log>,
130}
131
132impl ReceiptWithBloom {
133    pub fn new(tx_type: TxType, succeeded: bool, cumulative_gas_used: u64, logs: Vec<Log>) -> Self {
134        Self {
135            tx_type,
136            succeeded,
137            cumulative_gas_used,
138            bloom: bloom_from_logs(&logs, &ethrex_crypto::NativeCrypto),
139            logs,
140        }
141    }
142
143    // By reading the typed transactions EIP, and some geth code:
144    // - https://eips.ethereum.org/EIPS/eip-2718
145    // - https://github.com/ethereum/go-ethereum/blob/330190e476e2a2de4aac712551629a4134f802d5/core/types/receipt.go#L143
146    // We've noticed the are some subtleties around encoding receipts and transactions.
147    // First, `encode_inner` will encode a receipt according
148    // to the RLP of its fields, if typed, the RLP of the fields
149    // is padded with the byte representing this type.
150    // For P2P messages, receipts are re-encoded as bytes
151    // (see the `encode` implementation for receipt).
152    // For debug and computing receipt roots, the expected
153    // RLP encodings are the ones returned by `encode_inner`.
154    // On some documentations, this is also called the `consensus-encoding`
155    // for a receipt.
156
157    /// Encodes Receipts in the following formats:
158    /// A) Legacy receipts: rlp(receipt)
159    /// B) Non legacy receipts: tx_type | rlp(receipt).
160    pub fn encode_inner(&self) -> Vec<u8> {
161        let mut encode_buff = match self.tx_type {
162            TxType::Legacy => {
163                vec![]
164            }
165            _ => {
166                vec![self.tx_type as u8]
167            }
168        };
169        Encoder::new(&mut encode_buff)
170            .encode_field(&self.succeeded)
171            .encode_field(&self.cumulative_gas_used)
172            .encode_field(&self.bloom)
173            .encode_field(&self.logs)
174            .finish();
175        encode_buff
176    }
177
178    /// Decodes Receipts in the following formats:
179    /// A) Legacy receipts: rlp(receipt)
180    /// B) Non legacy receipts: tx_type | rlp(receipt).
181    pub fn decode_inner(rlp: &[u8]) -> Result<Self, RLPDecodeError> {
182        // Obtain TxType
183        let (tx_type, rlp) = match rlp.first() {
184            Some(tx_type) if *tx_type < 0x7f => {
185                let tx_type = match tx_type {
186                    0x0 => TxType::Legacy,
187                    0x1 => TxType::EIP2930,
188                    0x2 => TxType::EIP1559,
189                    0x3 => TxType::EIP4844,
190                    0x4 => TxType::EIP7702,
191                    0x7d => TxType::FeeToken,
192                    0x7e => TxType::Privileged,
193                    ty => {
194                        return Err(RLPDecodeError::Custom(format!(
195                            "Invalid transaction type: {ty}"
196                        )));
197                    }
198                };
199                (tx_type, &rlp[1..])
200            }
201            _ => (TxType::Legacy, rlp),
202        };
203        let decoder = Decoder::new(rlp)?;
204        let (succeeded, decoder) = decoder.decode_field("succeeded")?;
205        let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
206        let (bloom, decoder) = decoder.decode_field("bloom")?;
207        let (logs, decoder) = decoder.decode_field("logs")?;
208        decoder.finish()?;
209
210        Ok(Self {
211            tx_type,
212            succeeded,
213            cumulative_gas_used,
214            bloom,
215            logs,
216        })
217    }
218}
219
220impl RLPEncode for ReceiptWithBloom {
221    /// Receipts can be encoded in the following formats:
222    /// A) Legacy receipts: rlp(receipt)
223    /// B) Non legacy receipts: rlp(Bytes(tx_type | rlp(receipt))).
224    fn encode(&self, buf: &mut dyn bytes::BufMut) {
225        match self.tx_type {
226            TxType::Legacy => {
227                let legacy_encoded = self.encode_inner();
228                buf.put_slice(&legacy_encoded);
229            }
230            _ => {
231                let typed_recepipt_encoded = self.encode_inner();
232                let bytes = Bytes::from(typed_recepipt_encoded);
233                bytes.encode(buf);
234            }
235        };
236    }
237}
238
239impl RLPDecode for ReceiptWithBloom {
240    /// Receipts can be encoded in the following formats:
241    /// A) Legacy receipts: rlp(receipt)
242    /// B) Non legacy receipts: rlp(Bytes(tx_type | rlp(receipt))).
243    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
244        // The minimum size for a ReceiptWithBloom is > 256 bytes (due to the Bloom type field) meaning that it is safe
245        // to check for bytes prefix to diferenticate between legacy receipts and non-legacy receipt payloads
246        let (tx_type, rlp) = if is_encoded_as_bytes(rlp)? {
247            let payload = get_rlp_bytes_item_payload(rlp)?;
248            let tx_type = match payload.first().ok_or(RLPDecodeError::InvalidLength)? {
249                0x0 => TxType::Legacy,
250                0x1 => TxType::EIP2930,
251                0x2 => TxType::EIP1559,
252                0x3 => TxType::EIP4844,
253                0x4 => TxType::EIP7702,
254                0x7d => TxType::FeeToken,
255                0x7e => TxType::Privileged,
256                ty => {
257                    return Err(RLPDecodeError::Custom(format!(
258                        "Invalid transaction type: {ty}"
259                    )));
260                }
261            };
262            (tx_type, &payload[1..])
263        } else {
264            (TxType::Legacy, rlp)
265        };
266
267        let decoder = Decoder::new(rlp)?;
268        let (succeeded, decoder) = decoder.decode_field("succeeded")?;
269        let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
270        let (bloom, decoder) = decoder.decode_field("bloom")?;
271        let (logs, decoder) = decoder.decode_field("logs")?;
272
273        Ok((
274            ReceiptWithBloom {
275                tx_type,
276                succeeded,
277                cumulative_gas_used,
278                bloom,
279                logs,
280            },
281            decoder.finish()?,
282        ))
283    }
284}
285
286impl From<&Receipt> for ReceiptWithBloom {
287    fn from(receipt: &Receipt) -> Self {
288        Self {
289            tx_type: receipt.tx_type,
290            succeeded: receipt.succeeded,
291            cumulative_gas_used: receipt.cumulative_gas_used,
292            bloom: bloom_from_logs(&receipt.logs, &ethrex_crypto::NativeCrypto),
293            logs: receipt.logs.clone(),
294        }
295    }
296}
297
298impl From<&ReceiptWithBloom> for Receipt {
299    fn from(receipt: &ReceiptWithBloom) -> Self {
300        Self {
301            tx_type: receipt.tx_type,
302            succeeded: receipt.succeeded,
303            cumulative_gas_used: receipt.cumulative_gas_used,
304            logs: receipt.logs.clone(),
305        }
306    }
307}
308
309/// Data record produced during the execution of a transaction.
310#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
311pub struct Log {
312    pub address: Address,
313    pub topics: Vec<H256>,
314    pub data: Bytes,
315}
316
317impl RLPEncode for Log {
318    fn encode(&self, buf: &mut dyn bytes::BufMut) {
319        Encoder::new(buf)
320            .encode_field(&self.address)
321            .encode_field(&self.topics)
322            .encode_field(&self.data)
323            .finish();
324    }
325}
326
327impl RLPDecode for Log {
328    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
329        let decoder = Decoder::new(rlp)?;
330        let (address, decoder) = decoder.decode_field("address")?;
331        let (topics, decoder) = decoder.decode_field("topics")?;
332        let (data, decoder) = decoder.decode_field("data")?;
333        let log = Log {
334            address,
335            topics,
336            data,
337        };
338        Ok((log, decoder.finish()?))
339    }
340}
341
342#[cfg(test)]
343mod test {
344    use super::*;
345
346    fn h256_from_hex(s: &str) -> H256 {
347        H256::from_slice(&hex::decode(s).unwrap())
348    }
349
350    #[test]
351    fn test_encode_decode_receipt_legacy() {
352        let receipt = Receipt {
353            tx_type: TxType::Legacy,
354            succeeded: true,
355            cumulative_gas_used: 1200,
356            logs: vec![Log {
357                address: Address::random(),
358                topics: vec![],
359                data: Bytes::from_static(b"foo"),
360            }],
361        };
362        let encoded_receipt = receipt.encode_to_vec();
363        assert_eq!(receipt, Receipt::decode(&encoded_receipt).unwrap())
364    }
365
366    #[test]
367    fn test_encode_decode_receipt_non_legacy() {
368        let receipt = Receipt {
369            tx_type: TxType::EIP4844,
370            succeeded: true,
371            cumulative_gas_used: 1500,
372            logs: vec![Log {
373                address: Address::random(),
374                topics: vec![],
375                data: Bytes::from_static(b"bar"),
376            }],
377        };
378        let encoded_receipt = receipt.encode_to_vec();
379        assert_eq!(receipt, Receipt::decode(&encoded_receipt).unwrap())
380    }
381
382    #[test]
383    fn test_encode_decode_inner_receipt_legacy() {
384        let receipt = ReceiptWithBloom {
385            tx_type: TxType::Legacy,
386            succeeded: true,
387            cumulative_gas_used: 1200,
388            bloom: Bloom::random(),
389            logs: vec![Log {
390                address: Address::random(),
391                topics: vec![],
392                data: Bytes::from_static(b"foo"),
393            }],
394        };
395        let encoded_receipt = receipt.encode_inner();
396        assert_eq!(
397            receipt,
398            ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap()
399        )
400    }
401
402    #[test]
403    fn test_encode_decode_receipt_inner_non_legacy() {
404        let receipt = ReceiptWithBloom {
405            tx_type: TxType::EIP4844,
406            succeeded: true,
407            cumulative_gas_used: 1500,
408            bloom: Bloom::random(),
409            logs: vec![Log {
410                address: Address::random(),
411                topics: vec![],
412                data: Bytes::from_static(b"bar"),
413            }],
414        };
415        let encoded_receipt = receipt.encode_inner();
416        assert_eq!(
417            receipt,
418            ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap()
419        )
420    }
421
422    #[test]
423    fn test_encode_receipt_with_bloom() {
424        let receipt = Receipt {
425            tx_type: TxType::EIP1559,
426            succeeded: true,
427            cumulative_gas_used: 1500,
428            logs: vec![Log {
429                address: Address::random(),
430                topics: vec![
431                    h256_from_hex(
432                        "e70c0d1060ffbafc84e0e18d028245de3deeb0f41ecbade6562fa657d85ae945",
433                    ),
434                    h256_from_hex(
435                        "e7e9cd61c8c6cb313324d785aa130fe50a7b9885e4d1d7700a327c5e9ae4e183",
436                    ),
437                    h256_from_hex(
438                        "666d827b9db958c08f7186f127e3d9ea6a97288bcc4b527951ce493f6e2b76c4",
439                    ),
440                    h256_from_hex(
441                        "28b4366544dccafad7b61138e9ada51706e85bb217a20cfa1c86e2648f8f369a",
442                    ),
443                    h256_from_hex(
444                        "85cf9717f65c70d71cc6175f653512c13ce7b6a9bc5d9c2b9c49b2d2d6cb9536",
445                    ),
446                ],
447                data: Bytes::from_static(b"bar"),
448            }],
449        };
450        let encoded_receipt = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
451
452        let correct_bloom = {
453            let mut bloom = Bloom::zero();
454            for log in receipt.logs {
455                bloom.accrue(BloomInput::Raw(log.address.as_ref()));
456                for topic in log.topics.iter() {
457                    bloom.accrue(BloomInput::Raw(topic.as_ref()));
458                }
459            }
460            bloom
461        };
462        let receipt_with_bloom = ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap();
463        assert_eq!(receipt_with_bloom.bloom, correct_bloom);
464    }
465}