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/// Frame receipt status codes (EIP-8141).
16/// `0x3` is reserved for frames skipped due to a failed atomic batch.
17pub const FRAME_RECEIPT_STATUS_FAILURE: u8 = 0;
18pub const FRAME_RECEIPT_STATUS_SUCCESS: u8 = 1;
19pub const FRAME_RECEIPT_STATUS_SKIPPED: u8 = 3;
20
21/// Per-frame execution result within a frame transaction (EIP-8141)
22#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
23pub struct FrameReceipt {
24    pub status: u8,
25    pub gas_used: u64,
26    pub logs: Vec<Log>,
27}
28
29impl RLPEncode for FrameReceipt {
30    fn encode(&self, buf: &mut dyn bytes::BufMut) {
31        Encoder::new(buf)
32            .encode_field(&self.status)
33            .encode_field(&self.gas_used)
34            .encode_field(&self.logs)
35            .finish();
36    }
37}
38
39impl RLPDecode for FrameReceipt {
40    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
41        let decoder = Decoder::new(rlp)?;
42        let (status, decoder) = decoder.decode_field("status")?;
43        let (gas_used, decoder) = decoder.decode_field("gas_used")?;
44        let (logs, decoder) = decoder.decode_field("logs")?;
45        Ok((
46            FrameReceipt {
47                status,
48                gas_used,
49                logs,
50            },
51            decoder.finish()?,
52        ))
53    }
54}
55
56/// Result of a transaction
57#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
58pub struct Receipt {
59    pub tx_type: TxType,
60    pub succeeded: bool,
61    /// Cumulative gas used by this and all previous transactions in the block.
62    /// This is always post-refund gas.
63    /// Note: Block-level gas accounting (pre-refund for EIP-7778) uses BlockExecutionResult::block_gas_used.
64    pub cumulative_gas_used: u64,
65    pub logs: Vec<Log>,
66    /// For frame transactions: the address that paid for gas (set by APPROVE)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub payer: Option<Address>,
69    /// For frame transactions: per-frame execution results
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub frame_receipts: Option<Vec<FrameReceipt>>,
72}
73
74impl Receipt {
75    pub fn new(tx_type: TxType, succeeded: bool, cumulative_gas_used: u64, logs: Vec<Log>) -> Self {
76        Self {
77            tx_type,
78            succeeded,
79            cumulative_gas_used,
80            logs,
81            payer: None,
82            frame_receipts: None,
83        }
84    }
85
86    pub fn encode_inner(&self) -> Vec<u8> {
87        let mut encoded_data = vec![];
88        let tx_type: u8 = self.tx_type as u8;
89        if self.tx_type == TxType::Frame {
90            // EIP-8141 receipt: [tx_type, cumulative_gas_used, payer, [frame_receipt, ...]]
91            // No top-level succeeded or logs fields.
92            let empty_frame_receipts = Vec::new();
93            Encoder::new(&mut encoded_data)
94                .encode_field(&tx_type)
95                .encode_field(&self.cumulative_gas_used)
96                .encode_field(&self.payer.unwrap_or_default())
97                .encode_field(
98                    self.frame_receipts
99                        .as_ref()
100                        .unwrap_or(&empty_frame_receipts),
101                )
102                .finish();
103        } else {
104            Encoder::new(&mut encoded_data)
105                .encode_field(&tx_type)
106                .encode_field(&self.succeeded)
107                .encode_field(&self.cumulative_gas_used)
108                .encode_field(&self.logs)
109                .finish();
110        }
111        encoded_data
112    }
113
114    /// Full-fidelity INTERNAL storage encoding. NOT a wire/consensus format:
115    /// the receipts trie uses `encode_inner_with_bloom` and P2P uses the
116    /// `RLPEncode` impl (`encode_inner`). For frame receipts this additionally
117    /// persists `succeeded` and the aggregated top-level `logs` (needed by
118    /// eth_getLogs / eth_getTransactionReceipt), which the consensus layout
119    /// intentionally omits. Non-frame receipts reuse the existing layout so
120    /// databases written before this change stay readable.
121    pub fn encode_storage(&self) -> Vec<u8> {
122        if self.tx_type == TxType::Frame {
123            let mut buf = vec![];
124            let empty_frame_receipts = Vec::new();
125            Encoder::new(&mut buf)
126                .encode_field(&(self.tx_type as u8))
127                .encode_field(&self.succeeded)
128                .encode_field(&self.cumulative_gas_used)
129                .encode_field(&self.logs)
130                .encode_field(&self.payer.unwrap_or_default())
131                .encode_field(
132                    self.frame_receipts
133                        .as_ref()
134                        .unwrap_or(&empty_frame_receipts),
135                )
136                .finish();
137            buf
138        } else {
139            self.encode_inner()
140        }
141    }
142
143    /// Inverse of `encode_storage`.
144    pub fn decode_storage(rlp: &[u8]) -> Result<Receipt, RLPDecodeError> {
145        // Peek the tx-type (first field) to choose the layout.
146        let (tx_type_byte, _): (u8, _) = Decoder::new(rlp)?.decode_field("tx-type")?;
147        if TxType::from_u8(tx_type_byte) != Some(TxType::Frame) {
148            // Non-frame receipts use the standard consensus decode (unchanged
149            // layout) — old databases remain readable.
150            return Receipt::decode(rlp);
151        }
152        let decoder = Decoder::new(rlp)?;
153        let (_, decoder): (u8, _) = decoder.decode_field("tx-type")?;
154        let (succeeded, decoder) = decoder.decode_field("succeeded")?;
155        let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
156        let (logs, decoder) = decoder.decode_field("logs")?;
157        let (payer, decoder): (Address, _) = decoder.decode_field("payer")?;
158        let (frame_receipts, decoder): (Vec<FrameReceipt>, _) =
159            decoder.decode_field("frame_receipts")?;
160        decoder.finish()?;
161        Ok(Receipt {
162            tx_type: TxType::Frame,
163            succeeded,
164            cumulative_gas_used,
165            logs,
166            payer: if payer == Address::zero() {
167                None
168            } else {
169                Some(payer)
170            },
171            frame_receipts: Some(frame_receipts),
172        })
173    }
174
175    pub fn encode_inner_with_bloom(&self, crypto: &dyn Crypto) -> Vec<u8> {
176        self.encode_inner_with_precomputed_bloom(bloom_from_logs(&self.logs, crypto))
177    }
178
179    /// Like [`Self::encode_inner_with_bloom`] but takes an already-computed bloom, so
180    /// callers that also need the bloom for other purposes (e.g. the aggregate header
181    /// `logs_bloom`) don't recompute it.
182    pub fn encode_inner_with_precomputed_bloom(&self, bloom: Bloom) -> Vec<u8> {
183        // Bloom is already 256 bytes, so we preallocate at least that much plus some,
184        // to avoid multiple small allocations.
185        let mut encode_buf = Vec::with_capacity(512);
186        if self.tx_type != TxType::Legacy {
187            encode_buf.push(self.tx_type as u8);
188        }
189        if self.tx_type == TxType::Frame {
190            // EIP-8141 ReceiptPayload (spec lines 178-185):
191            // [cumulative_gas_used, payer, [frame_receipt, ...]]
192            // No succeeded, no bloom, no top-level logs.
193            let empty_frame_receipts = Vec::new();
194            Encoder::new(&mut encode_buf)
195                .encode_field(&self.cumulative_gas_used)
196                .encode_field(&self.payer.unwrap_or_default())
197                .encode_field(
198                    self.frame_receipts
199                        .as_ref()
200                        .unwrap_or(&empty_frame_receipts),
201                )
202                .finish();
203        } else {
204            Encoder::new(&mut encode_buf)
205                .encode_field(&self.succeeded)
206                .encode_field(&self.cumulative_gas_used)
207                .encode_field(&bloom)
208                .encode_field(&self.logs)
209                .finish();
210        }
211        encode_buf
212    }
213
214    /// Inverse of [`Receipt::encode_inner_with_bloom`].
215    ///
216    /// Parses a receipt from its consensus / trie byte form, i.e. the raw
217    /// `tx_type || rlp(payload)` (typed) or `rlp(payload)` (legacy) produced by
218    /// `encode_inner_with_bloom`. Returns the decoded receipt and the remaining
219    /// bytes after the consumed payload.
220    ///
221    /// For non-frame receipts the layout is `[succeeded, cumulative_gas_used,
222    /// bloom, logs]`; the bloom is dropped because [`Receipt`] does not store it.
223    /// For frame receipts (tx_type 0x06) the layout is the EIP-8141
224    /// `[cumulative_gas_used, payer, [frame_receipt, ...]]`: there is no
225    /// top-level `succeeded` (it is derived from the frame statuses, identical
226    /// to the `RLPDecode for Receipt` frame branch) and no top-level `logs`.
227    pub fn decode_inner_with_bloom(rlp: &[u8]) -> Result<(Receipt, &[u8]), RLPDecodeError> {
228        // Determine the tx type from the optional 1-byte type prefix. A leading
229        // byte < 0x7f denotes the EIP-2718 type; otherwise it is a legacy
230        // receipt (whose payload starts directly with an RLP list header).
231        let (tx_type, payload) = match rlp.first() {
232            Some(byte) if *byte < 0x7f => {
233                let Some(tx_type) = TxType::from_u8(*byte) else {
234                    return Err(RLPDecodeError::Custom(format!(
235                        "Invalid transaction type: {byte}"
236                    )));
237                };
238                (tx_type, &rlp[1..])
239            }
240            _ => (TxType::Legacy, rlp),
241        };
242
243        if tx_type == TxType::Frame {
244            let decoder = Decoder::new(payload)?;
245            let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
246            let (payer, decoder): (Address, _) = decoder.decode_field("payer")?;
247            let (frame_receipts, decoder): (Vec<FrameReceipt>, _) =
248                decoder.decode_field("frame_receipts")?;
249            let remaining = decoder.finish_unchecked();
250            let payer = if payer == Address::zero() {
251                None
252            } else {
253                Some(payer)
254            };
255            // Derive succeeded from frame receipts: true iff every frame's status
256            // is SUCCESS (same rule as `RLPDecode for Receipt`).
257            let succeeded = frame_receipts
258                .iter()
259                .all(|fr| fr.status == FRAME_RECEIPT_STATUS_SUCCESS);
260            Ok((
261                Receipt {
262                    tx_type,
263                    succeeded,
264                    cumulative_gas_used,
265                    logs: Vec::new(),
266                    payer,
267                    frame_receipts: Some(frame_receipts),
268                },
269                remaining,
270            ))
271        } else {
272            let decoder = Decoder::new(payload)?;
273            let (succeeded, decoder) = decoder.decode_field("succeeded")?;
274            let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
275            // Decode and discard the bloom: it is derivable from the logs and is
276            // not stored on `Receipt`.
277            let (_bloom, decoder): (Bloom, _) = decoder.decode_field("bloom")?;
278            let (logs, decoder) = decoder.decode_field("logs")?;
279            let remaining = decoder.finish_unchecked();
280            Ok((
281                Receipt {
282                    tx_type,
283                    succeeded,
284                    cumulative_gas_used,
285                    logs,
286                    payer: None,
287                    frame_receipts: None,
288                },
289                remaining,
290            ))
291        }
292    }
293}
294
295pub fn bloom_from_logs(logs: &[Log], crypto: &dyn Crypto) -> Bloom {
296    let mut bloom = Bloom::zero();
297    for log in logs {
298        let address_hash = crypto.keccak256(log.address.as_bytes());
299        bloom.accrue(BloomInput::Hash(&address_hash));
300        for topic in log.topics.iter() {
301            let topic_hash = crypto.keccak256(topic.as_bytes());
302            bloom.accrue(BloomInput::Hash(&topic_hash));
303        }
304    }
305    bloom
306}
307
308impl RLPEncode for Receipt {
309    fn encode(&self, buf: &mut dyn bytes::BufMut) {
310        let encoded_inner = self.encode_inner();
311        buf.put_slice(&encoded_inner);
312    }
313}
314
315impl RLPDecode for Receipt {
316    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
317        let decoder = Decoder::new(rlp)?;
318        let (tx_type, decoder): (u8, _) = decoder.decode_field("tx-type")?;
319
320        let Some(tx_type) = TxType::from_u8(tx_type) else {
321            return Err(RLPDecodeError::Custom(
322                "Invalid transaction type".to_string(),
323            ));
324        };
325
326        if tx_type == TxType::Frame {
327            // EIP-8141 receipt: [tx_type, cumulative_gas_used, payer, [frame_receipt, ...]]
328            let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
329            let (payer, decoder): (Address, _) = decoder.decode_field("payer")?;
330            let (frame_receipts, decoder): (Vec<FrameReceipt>, _) =
331                decoder.decode_field("frame_receipts")?;
332            let payer = if payer == Address::zero() {
333                None
334            } else {
335                Some(payer)
336            };
337            // Derive succeeded from frame receipts: true iff every frame's status is SUCCESS.
338            // Any FAILURE or SKIPPED frame disqualifies the transaction from `succeeded`.
339            let succeeded = frame_receipts
340                .iter()
341                .all(|fr| fr.status == FRAME_RECEIPT_STATUS_SUCCESS);
342            Ok((
343                Receipt {
344                    tx_type,
345                    succeeded,
346                    cumulative_gas_used,
347                    logs: Vec::new(),
348                    payer,
349                    frame_receipts: Some(frame_receipts),
350                },
351                decoder.finish()?,
352            ))
353        } else {
354            let (succeeded, decoder) = decoder.decode_field("succeeded")?;
355            let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
356            let (logs, decoder) = decoder.decode_field("logs")?;
357            Ok((
358                Receipt {
359                    tx_type,
360                    succeeded,
361                    cumulative_gas_used,
362                    logs,
363                    payer: None,
364                    frame_receipts: None,
365                },
366                decoder.finish()?,
367            ))
368        }
369    }
370}
371
372/// Result of a transaction
373#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
374pub struct ReceiptWithBloom {
375    pub tx_type: TxType,
376    pub succeeded: bool,
377    /// Cumulative gas used by this and all previous transactions in the block.
378    /// This is always post-refund gas.
379    /// Note: Block-level gas accounting (pre-refund for EIP-7778) uses BlockExecutionResult::block_gas_used.
380    pub cumulative_gas_used: u64,
381    pub bloom: Bloom,
382    pub logs: Vec<Log>,
383}
384
385impl ReceiptWithBloom {
386    pub fn new(tx_type: TxType, succeeded: bool, cumulative_gas_used: u64, logs: Vec<Log>) -> Self {
387        Self {
388            tx_type,
389            succeeded,
390            cumulative_gas_used,
391            bloom: bloom_from_logs(&logs, &ethrex_crypto::NativeCrypto),
392            logs,
393        }
394    }
395
396    // By reading the typed transactions EIP, and some geth code:
397    // - https://eips.ethereum.org/EIPS/eip-2718
398    // - https://github.com/ethereum/go-ethereum/blob/330190e476e2a2de4aac712551629a4134f802d5/core/types/receipt.go#L143
399    // We've noticed the are some subtleties around encoding receipts and transactions.
400    // First, `encode_inner` will encode a receipt according
401    // to the RLP of its fields, if typed, the RLP of the fields
402    // is padded with the byte representing this type.
403    // For P2P messages, receipts are re-encoded as bytes
404    // (see the `encode` implementation for receipt).
405    // For debug and computing receipt roots, the expected
406    // RLP encodings are the ones returned by `encode_inner`.
407    // On some documentations, this is also called the `consensus-encoding`
408    // for a receipt.
409
410    /// Encodes Receipts in the following formats:
411    /// A) Legacy receipts: rlp(receipt)
412    /// B) Non legacy receipts: tx_type | rlp(receipt).
413    pub fn encode_inner(&self) -> Vec<u8> {
414        let mut encode_buff = match self.tx_type {
415            TxType::Legacy => {
416                vec![]
417            }
418            _ => {
419                vec![self.tx_type as u8]
420            }
421        };
422        Encoder::new(&mut encode_buff)
423            .encode_field(&self.succeeded)
424            .encode_field(&self.cumulative_gas_used)
425            .encode_field(&self.bloom)
426            .encode_field(&self.logs)
427            .finish();
428        encode_buff
429    }
430
431    /// Decodes Receipts in the following formats:
432    /// A) Legacy receipts: rlp(receipt)
433    /// B) Non legacy receipts: tx_type | rlp(receipt).
434    pub fn decode_inner(rlp: &[u8]) -> Result<Self, RLPDecodeError> {
435        // Obtain TxType
436        let (tx_type, rlp) = match rlp.first() {
437            Some(tx_type) if *tx_type < 0x7f => {
438                let tx_type = match tx_type {
439                    0x0 => TxType::Legacy,
440                    0x1 => TxType::EIP2930,
441                    0x2 => TxType::EIP1559,
442                    0x3 => TxType::EIP4844,
443                    0x4 => TxType::EIP7702,
444                    0x6 => TxType::Frame,
445                    0x7d => TxType::FeeToken,
446                    0x7e => TxType::Privileged,
447                    ty => {
448                        return Err(RLPDecodeError::Custom(format!(
449                            "Invalid transaction type: {ty}"
450                        )));
451                    }
452                };
453                (tx_type, &rlp[1..])
454            }
455            _ => (TxType::Legacy, rlp),
456        };
457        let decoder = Decoder::new(rlp)?;
458        let (succeeded, decoder) = decoder.decode_field("succeeded")?;
459        let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
460        let (bloom, decoder) = decoder.decode_field("bloom")?;
461        let (logs, decoder) = decoder.decode_field("logs")?;
462        decoder.finish()?;
463
464        Ok(Self {
465            tx_type,
466            succeeded,
467            cumulative_gas_used,
468            bloom,
469            logs,
470        })
471    }
472}
473
474impl RLPEncode for ReceiptWithBloom {
475    /// Receipts can be encoded in the following formats:
476    /// A) Legacy receipts: rlp(receipt)
477    /// B) Non legacy receipts: rlp(Bytes(tx_type | rlp(receipt))).
478    fn encode(&self, buf: &mut dyn bytes::BufMut) {
479        match self.tx_type {
480            TxType::Legacy => {
481                let legacy_encoded = self.encode_inner();
482                buf.put_slice(&legacy_encoded);
483            }
484            _ => {
485                let typed_recepipt_encoded = self.encode_inner();
486                let bytes = Bytes::from(typed_recepipt_encoded);
487                bytes.encode(buf);
488            }
489        };
490    }
491}
492
493impl RLPDecode for ReceiptWithBloom {
494    /// Receipts can be encoded in the following formats:
495    /// A) Legacy receipts: rlp(receipt)
496    /// B) Non legacy receipts: rlp(Bytes(tx_type | rlp(receipt))).
497    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
498        // The minimum size for a ReceiptWithBloom is > 256 bytes (due to the Bloom type field) meaning that it is safe
499        // to check for bytes prefix to diferenticate between legacy receipts and non-legacy receipt payloads
500        let (tx_type, rlp) = if is_encoded_as_bytes(rlp)? {
501            let payload = get_rlp_bytes_item_payload(rlp)?;
502            let tx_type = match payload.first().ok_or(RLPDecodeError::InvalidLength)? {
503                0x0 => TxType::Legacy,
504                0x1 => TxType::EIP2930,
505                0x2 => TxType::EIP1559,
506                0x3 => TxType::EIP4844,
507                0x4 => TxType::EIP7702,
508                0x6 => TxType::Frame,
509                0x7d => TxType::FeeToken,
510                0x7e => TxType::Privileged,
511                ty => {
512                    return Err(RLPDecodeError::Custom(format!(
513                        "Invalid transaction type: {ty}"
514                    )));
515                }
516            };
517            (tx_type, &payload[1..])
518        } else {
519            (TxType::Legacy, rlp)
520        };
521
522        let decoder = Decoder::new(rlp)?;
523        let (succeeded, decoder) = decoder.decode_field("succeeded")?;
524        let (cumulative_gas_used, decoder) = decoder.decode_field("cumulative_gas_used")?;
525        let (bloom, decoder) = decoder.decode_field("bloom")?;
526        let (logs, decoder) = decoder.decode_field("logs")?;
527
528        Ok((
529            ReceiptWithBloom {
530                tx_type,
531                succeeded,
532                cumulative_gas_used,
533                bloom,
534                logs,
535            },
536            decoder.finish()?,
537        ))
538    }
539}
540
541impl From<&Receipt> for ReceiptWithBloom {
542    fn from(receipt: &Receipt) -> Self {
543        Self {
544            tx_type: receipt.tx_type,
545            succeeded: receipt.succeeded,
546            cumulative_gas_used: receipt.cumulative_gas_used,
547            bloom: bloom_from_logs(&receipt.logs, &ethrex_crypto::NativeCrypto),
548            logs: receipt.logs.clone(),
549        }
550    }
551}
552
553impl From<&ReceiptWithBloom> for Receipt {
554    fn from(receipt: &ReceiptWithBloom) -> Self {
555        Self {
556            tx_type: receipt.tx_type,
557            succeeded: receipt.succeeded,
558            cumulative_gas_used: receipt.cumulative_gas_used,
559            logs: receipt.logs.clone(),
560            payer: None,
561            frame_receipts: None,
562        }
563    }
564}
565
566/// Data record produced during the execution of a transaction.
567#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
568pub struct Log {
569    pub address: Address,
570    pub topics: Vec<H256>,
571    pub data: Bytes,
572}
573
574impl RLPEncode for Log {
575    fn encode(&self, buf: &mut dyn bytes::BufMut) {
576        Encoder::new(buf)
577            .encode_field(&self.address)
578            .encode_field(&self.topics)
579            .encode_field(&self.data)
580            .finish();
581    }
582}
583
584impl RLPDecode for Log {
585    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
586        let decoder = Decoder::new(rlp)?;
587        let (address, decoder) = decoder.decode_field("address")?;
588        let (topics, decoder) = decoder.decode_field("topics")?;
589        let (data, decoder) = decoder.decode_field("data")?;
590        let log = Log {
591            address,
592            topics,
593            data,
594        };
595        Ok((log, decoder.finish()?))
596    }
597}
598
599#[cfg(test)]
600mod test {
601    use super::*;
602
603    fn h256_from_hex(s: &str) -> H256 {
604        H256::from_slice(&hex::decode(s).unwrap())
605    }
606
607    #[test]
608    fn test_encode_decode_receipt_legacy() {
609        let receipt = Receipt {
610            tx_type: TxType::Legacy,
611            succeeded: true,
612            cumulative_gas_used: 1200,
613            logs: vec![Log {
614                address: Address::random(),
615                topics: vec![],
616                data: Bytes::from_static(b"foo"),
617            }],
618            payer: None,
619            frame_receipts: None,
620        };
621        let encoded_receipt = receipt.encode_to_vec();
622        assert_eq!(receipt, Receipt::decode(&encoded_receipt).unwrap())
623    }
624
625    #[test]
626    fn test_encode_decode_receipt_non_legacy() {
627        let receipt = Receipt {
628            tx_type: TxType::EIP4844,
629            succeeded: true,
630            cumulative_gas_used: 1500,
631            logs: vec![Log {
632                address: Address::random(),
633                topics: vec![],
634                data: Bytes::from_static(b"bar"),
635            }],
636            payer: None,
637            frame_receipts: None,
638        };
639        let encoded_receipt = receipt.encode_to_vec();
640        assert_eq!(receipt, Receipt::decode(&encoded_receipt).unwrap())
641    }
642
643    #[test]
644    fn test_encode_decode_inner_receipt_legacy() {
645        let receipt = ReceiptWithBloom {
646            tx_type: TxType::Legacy,
647            succeeded: true,
648            cumulative_gas_used: 1200,
649            bloom: Bloom::random(),
650            logs: vec![Log {
651                address: Address::random(),
652                topics: vec![],
653                data: Bytes::from_static(b"foo"),
654            }],
655        };
656        let encoded_receipt = receipt.encode_inner();
657        assert_eq!(
658            receipt,
659            ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap()
660        )
661    }
662
663    #[test]
664    fn test_encode_decode_receipt_inner_non_legacy() {
665        let receipt = ReceiptWithBloom {
666            tx_type: TxType::EIP4844,
667            succeeded: true,
668            cumulative_gas_used: 1500,
669            bloom: Bloom::random(),
670            logs: vec![Log {
671                address: Address::random(),
672                topics: vec![],
673                data: Bytes::from_static(b"bar"),
674            }],
675        };
676        let encoded_receipt = receipt.encode_inner();
677        assert_eq!(
678            receipt,
679            ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap()
680        )
681    }
682
683    #[test]
684    fn test_encode_receipt_with_bloom() {
685        let receipt = Receipt {
686            tx_type: TxType::EIP1559,
687            succeeded: true,
688            cumulative_gas_used: 1500,
689            logs: vec![Log {
690                address: Address::random(),
691                topics: vec![
692                    h256_from_hex(
693                        "e70c0d1060ffbafc84e0e18d028245de3deeb0f41ecbade6562fa657d85ae945",
694                    ),
695                    h256_from_hex(
696                        "e7e9cd61c8c6cb313324d785aa130fe50a7b9885e4d1d7700a327c5e9ae4e183",
697                    ),
698                    h256_from_hex(
699                        "666d827b9db958c08f7186f127e3d9ea6a97288bcc4b527951ce493f6e2b76c4",
700                    ),
701                    h256_from_hex(
702                        "28b4366544dccafad7b61138e9ada51706e85bb217a20cfa1c86e2648f8f369a",
703                    ),
704                    h256_from_hex(
705                        "85cf9717f65c70d71cc6175f653512c13ce7b6a9bc5d9c2b9c49b2d2d6cb9536",
706                    ),
707                ],
708                data: Bytes::from_static(b"bar"),
709            }],
710            payer: None,
711            frame_receipts: None,
712        };
713        let encoded_receipt = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
714
715        let correct_bloom = {
716            let mut bloom = Bloom::zero();
717            for log in receipt.logs {
718                bloom.accrue(BloomInput::Raw(log.address.as_ref()));
719                for topic in log.topics.iter() {
720                    bloom.accrue(BloomInput::Raw(topic.as_ref()));
721                }
722            }
723            bloom
724        };
725        let receipt_with_bloom = ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap();
726        assert_eq!(receipt_with_bloom.bloom, correct_bloom);
727    }
728
729    #[test]
730    fn test_frame_receipt_rlp_roundtrip() {
731        let fr = FrameReceipt {
732            status: FRAME_RECEIPT_STATUS_SUCCESS,
733            gas_used: 21000,
734            logs: vec![Log {
735                address: Address::random(),
736                topics: vec![],
737                data: Bytes::from_static(b"test"),
738            }],
739        };
740        let encoded = fr.encode_to_vec();
741        let decoded = FrameReceipt::decode(&encoded).unwrap();
742        assert_eq!(fr, decoded);
743    }
744
745    #[test]
746    fn test_frame_receipt_skipped_status_rlp_roundtrip() {
747        // Spec line 137: status code 0x3 marks frames skipped by a failed atomic batch.
748        let fr = FrameReceipt {
749            status: FRAME_RECEIPT_STATUS_SKIPPED,
750            gas_used: 0,
751            logs: vec![],
752        };
753        let encoded = fr.encode_to_vec();
754        let decoded = FrameReceipt::decode(&encoded).unwrap();
755        assert_eq!(fr, decoded);
756        assert_eq!(decoded.status, FRAME_RECEIPT_STATUS_SKIPPED);
757    }
758
759    #[test]
760    fn test_receipt_with_frame_fields_rlp_roundtrip() {
761        // Frame receipts encode as [cumulative_gas_used, payer, [frame_receipts]]
762        // without top-level succeeded or logs. On decode, succeeded is derived
763        // from frame receipts and logs is empty.
764        let receipt = Receipt {
765            tx_type: TxType::Frame,
766            succeeded: true,
767            cumulative_gas_used: 315000,
768            logs: vec![],
769            payer: Some(Address::from_low_u64_be(0x1234)),
770            frame_receipts: Some(vec![
771                FrameReceipt {
772                    status: FRAME_RECEIPT_STATUS_SUCCESS,
773                    gas_used: 100000,
774                    logs: vec![],
775                },
776                FrameReceipt {
777                    status: FRAME_RECEIPT_STATUS_SUCCESS,
778                    gas_used: 200000,
779                    logs: vec![Log {
780                        address: Address::from_low_u64_be(0xbeef),
781                        topics: vec![],
782                        data: Bytes::from_static(b"frame2"),
783                    }],
784                },
785            ]),
786        };
787        let encoded = receipt.encode_to_vec();
788        let decoded = Receipt::decode(&encoded).unwrap();
789        assert_eq!(decoded.tx_type, TxType::Frame);
790        assert_eq!(decoded.cumulative_gas_used, 315000);
791        assert_eq!(decoded.payer, Some(Address::from_low_u64_be(0x1234)));
792        assert!(decoded.succeeded); // derived: all frame receipts succeeded
793        assert!(decoded.logs.is_empty()); // top-level logs not encoded for frame txs
794        assert_eq!(decoded.frame_receipts, receipt.frame_receipts);
795    }
796
797    #[test]
798    fn test_frame_receipt_succeeded_derived_from_frames() {
799        let receipt = Receipt {
800            tx_type: TxType::Frame,
801            succeeded: true, // will be overridden on decode
802            cumulative_gas_used: 100000,
803            logs: vec![],
804            payer: Some(Address::from_low_u64_be(0x1)),
805            frame_receipts: Some(vec![
806                FrameReceipt {
807                    status: FRAME_RECEIPT_STATUS_SUCCESS,
808                    gas_used: 50000,
809                    logs: vec![],
810                },
811                FrameReceipt {
812                    status: FRAME_RECEIPT_STATUS_FAILURE,
813                    gas_used: 50000,
814                    logs: vec![],
815                },
816            ]),
817        };
818        let encoded = receipt.encode_to_vec();
819        let decoded = Receipt::decode(&encoded).unwrap();
820        assert!(!decoded.succeeded); // one frame failed
821    }
822
823    #[test]
824    fn test_frame_receipt_skipped_disqualifies_succeeded() {
825        // A SKIPPED frame must not count as success: spec implies the tx outcome
826        // is not fully successful when any atomic batch failed.
827        let receipt = Receipt {
828            tx_type: TxType::Frame,
829            succeeded: false,
830            cumulative_gas_used: 100000,
831            logs: vec![],
832            payer: Some(Address::from_low_u64_be(0x1)),
833            frame_receipts: Some(vec![
834                FrameReceipt {
835                    status: FRAME_RECEIPT_STATUS_SUCCESS,
836                    gas_used: 50000,
837                    logs: vec![],
838                },
839                FrameReceipt {
840                    status: FRAME_RECEIPT_STATUS_SKIPPED,
841                    gas_used: 0,
842                    logs: vec![],
843                },
844            ]),
845        };
846        let encoded = receipt.encode_to_vec();
847        let decoded = Receipt::decode(&encoded).unwrap();
848        assert!(!decoded.succeeded);
849    }
850
851    #[test]
852    fn frame_receipt_storage_roundtrip_preserves_logs_and_status() {
853        let log = Log {
854            address: Address::from_low_u64_be(1),
855            topics: vec![],
856            data: Bytes::from(vec![1u8]),
857        };
858        let receipt = Receipt {
859            tx_type: TxType::Frame,
860            succeeded: true, // VM rule: no SENDER frame reverted
861            cumulative_gas_used: 50_000,
862            logs: vec![log.clone()], // aggregated frame logs
863            payer: Some(Address::from_low_u64_be(2)),
864            frame_receipts: Some(vec![
865                FrameReceipt {
866                    status: FRAME_RECEIPT_STATUS_FAILURE,
867                    gas_used: 1000,
868                    logs: vec![],
869                }, // a DEFAULT frame failed
870                FrameReceipt {
871                    status: FRAME_RECEIPT_STATUS_SUCCESS,
872                    gas_used: 2000,
873                    logs: vec![log],
874                },
875            ]),
876        };
877        // succeeded=true coexists with a FAILURE frame -> the old derive-rule would
878        // have flipped it to false; storage must keep it verbatim.
879        let decoded = Receipt::decode_storage(&receipt.encode_storage()).unwrap();
880        assert_eq!(decoded, receipt);
881    }
882
883    #[test]
884    fn nonframe_receipt_storage_roundtrip_unchanged() {
885        let r = Receipt::new(TxType::EIP1559, true, 21000, vec![]);
886        assert_eq!(Receipt::decode_storage(&r.encode_storage()).unwrap(), r);
887        // and that encode_storage matches the legacy encode_inner for non-frame:
888        assert_eq!(r.encode_storage(), r.encode_inner());
889    }
890
891    #[test]
892    fn frame_receipt_trie_encoding_is_eip8141_payload() {
893        let receipt = Receipt {
894            tx_type: TxType::Frame,
895            succeeded: true,
896            cumulative_gas_used: 100_000,
897            logs: Vec::new(),
898            payer: Some(Address::from_low_u64_be(0xBEEF)),
899            frame_receipts: Some(vec![FrameReceipt {
900                status: FRAME_RECEIPT_STATUS_SUCCESS,
901                gas_used: 21_000,
902                logs: Vec::new(),
903            }]),
904        };
905        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
906        // EIP-2718 type prefix
907        assert_eq!(encoded[0], 0x06);
908        // Spec ReceiptPayload: [cumulative_gas_used, payer, [frame_receipt, ...]]
909        let mut expected_payload = Vec::new();
910        Encoder::new(&mut expected_payload)
911            .encode_field(&100_000u64)
912            .encode_field(&Address::from_low_u64_be(0xBEEF))
913            .encode_field(receipt.frame_receipts.as_ref().unwrap())
914            .finish();
915        assert_eq!(&encoded[1..], &expected_payload[..]);
916    }
917
918    #[test]
919    fn decode_inner_with_bloom_roundtrips_nonframe() {
920        // Non-frame: encode_inner_with_bloom carries a bloom that
921        // decode_inner_with_bloom drops. Compare every field except the dropped
922        // bloom — i.e. the decoded receipt must equal the original Receipt.
923        let receipt = Receipt {
924            tx_type: TxType::EIP1559,
925            succeeded: true,
926            cumulative_gas_used: 54321,
927            logs: vec![Log {
928                address: Address::from_low_u64_be(0xabcd),
929                topics: vec![h256_from_hex(
930                    "e70c0d1060ffbafc84e0e18d028245de3deeb0f41ecbade6562fa657d85ae945",
931                )],
932                data: Bytes::from_static(b"hello"),
933            }],
934            payer: None,
935            frame_receipts: None,
936        };
937        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
938        let (decoded, rest) = Receipt::decode_inner_with_bloom(&encoded).unwrap();
939        assert!(rest.is_empty());
940        assert_eq!(decoded, receipt);
941    }
942
943    #[test]
944    fn decode_inner_with_bloom_roundtrips_legacy() {
945        let receipt = Receipt {
946            tx_type: TxType::Legacy,
947            succeeded: false,
948            cumulative_gas_used: 100,
949            logs: vec![],
950            payer: None,
951            frame_receipts: None,
952        };
953        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
954        let (decoded, rest) = Receipt::decode_inner_with_bloom(&encoded).unwrap();
955        assert!(rest.is_empty());
956        assert_eq!(decoded, receipt);
957    }
958
959    #[test]
960    fn decode_inner_with_bloom_roundtrips_frame_preserving_payer_and_frames() {
961        // Frame: payer and frame_receipts MUST survive the roundtrip. succeeded
962        // is derived from the frame statuses and logs is empty by construction.
963        let receipt = Receipt {
964            tx_type: TxType::Frame,
965            succeeded: true,
966            cumulative_gas_used: 315000,
967            logs: vec![],
968            payer: Some(Address::from_low_u64_be(0x1234)),
969            frame_receipts: Some(vec![
970                FrameReceipt {
971                    status: FRAME_RECEIPT_STATUS_SUCCESS,
972                    gas_used: 100000,
973                    logs: vec![],
974                },
975                FrameReceipt {
976                    status: FRAME_RECEIPT_STATUS_SUCCESS,
977                    gas_used: 200000,
978                    logs: vec![Log {
979                        address: Address::from_low_u64_be(0xbeef),
980                        topics: vec![],
981                        data: Bytes::from_static(b"frame2"),
982                    }],
983                },
984            ]),
985        };
986        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
987        let (decoded, rest) = Receipt::decode_inner_with_bloom(&encoded).unwrap();
988        assert!(rest.is_empty());
989        assert_eq!(decoded.tx_type, TxType::Frame);
990        assert_eq!(decoded.cumulative_gas_used, 315000);
991        assert_eq!(decoded.payer, Some(Address::from_low_u64_be(0x1234)));
992        assert_eq!(decoded.frame_receipts, receipt.frame_receipts);
993        assert!(decoded.succeeded); // derived: all frame receipts succeeded
994        assert!(decoded.logs.is_empty());
995        // Full equality: the whole receipt round-trips.
996        assert_eq!(decoded, receipt);
997    }
998
999    #[test]
1000    fn decode_inner_with_bloom_frame_zero_payer_maps_to_none() {
1001        let receipt = Receipt {
1002            tx_type: TxType::Frame,
1003            succeeded: true,
1004            cumulative_gas_used: 21000,
1005            logs: vec![],
1006            payer: None,
1007            frame_receipts: Some(vec![FrameReceipt {
1008                status: FRAME_RECEIPT_STATUS_SUCCESS,
1009                gas_used: 21000,
1010                logs: vec![],
1011            }]),
1012        };
1013        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
1014        let (decoded, _) = Receipt::decode_inner_with_bloom(&encoded).unwrap();
1015        assert_eq!(decoded.payer, None);
1016        assert_eq!(decoded, receipt);
1017    }
1018}