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/// `0x2` 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 = 2;
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:
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    /// Before Byzantium (EIP-658) a receipt's first field is a 32-byte post-state
608    /// root, not a status flag. `Receipt` has no field for it, so such a receipt
609    /// cannot be decoded — the `bool` decode of field 0 rejects the 32-byte
610    /// string. This is why history backfill clamps its floor at the Byzantium
611    /// block instead of descending to genesis: pre-Byzantium receipts can never be
612    /// fetched, and trying would fail every request and penalize honest peers.
613    ///
614    /// If this test ever starts passing, `Receipt` has gained a representation for
615    /// the pre-EIP-658 form and that clamp can be revisited.
616    #[test]
617    fn pre_byzantium_receipt_cannot_be_decoded() {
618        // rlp([post_state_root(32B), cumulative_gas_used, bloom(256B), logs]) —
619        // the pre-EIP-658 consensus form.
620        // Two receipts identical except for field 0: a post-Byzantium status flag
621        // versus a pre-Byzantium 32-byte post-state root. Encoding both and
622        // decoding each isolates the failure to that field, without inspecting
623        // error text.
624        fn encode_with_first_field<T: RLPEncode>(first: &T) -> Vec<u8> {
625            let mut buf = Vec::new();
626            Encoder::new(&mut buf)
627                .encode_field(first)
628                .encode_field(&21_000u64)
629                .encode_field(&Bloom::zero())
630                .encode_field(&Vec::<Log>::new())
631                .finish();
632            buf
633        }
634
635        let post_byzantium = encode_with_first_field(&true);
636        assert!(
637            Receipt::decode_inner_with_bloom(&post_byzantium).is_ok(),
638            "a status-flag (post-Byzantium) receipt must decode"
639        );
640
641        let pre_byzantium = encode_with_first_field(&H256::from([0x11u8; 32]));
642        assert!(
643            Receipt::decode_inner_with_bloom(&pre_byzantium).is_err(),
644            "a post-state-root (pre-Byzantium) receipt must not decode"
645        );
646    }
647
648    #[test]
649    fn test_encode_decode_receipt_legacy() {
650        let receipt = Receipt {
651            tx_type: TxType::Legacy,
652            succeeded: true,
653            cumulative_gas_used: 1200,
654            logs: vec![Log {
655                address: Address::random(),
656                topics: vec![],
657                data: Bytes::from_static(b"foo"),
658            }],
659            payer: None,
660            frame_receipts: None,
661        };
662        let encoded_receipt = receipt.encode_to_vec();
663        assert_eq!(receipt, Receipt::decode(&encoded_receipt).unwrap())
664    }
665
666    #[test]
667    fn test_encode_decode_receipt_non_legacy() {
668        let receipt = Receipt {
669            tx_type: TxType::EIP4844,
670            succeeded: true,
671            cumulative_gas_used: 1500,
672            logs: vec![Log {
673                address: Address::random(),
674                topics: vec![],
675                data: Bytes::from_static(b"bar"),
676            }],
677            payer: None,
678            frame_receipts: None,
679        };
680        let encoded_receipt = receipt.encode_to_vec();
681        assert_eq!(receipt, Receipt::decode(&encoded_receipt).unwrap())
682    }
683
684    #[test]
685    fn test_encode_decode_inner_receipt_legacy() {
686        let receipt = ReceiptWithBloom {
687            tx_type: TxType::Legacy,
688            succeeded: true,
689            cumulative_gas_used: 1200,
690            bloom: Bloom::random(),
691            logs: vec![Log {
692                address: Address::random(),
693                topics: vec![],
694                data: Bytes::from_static(b"foo"),
695            }],
696        };
697        let encoded_receipt = receipt.encode_inner();
698        assert_eq!(
699            receipt,
700            ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap()
701        )
702    }
703
704    #[test]
705    fn test_encode_decode_receipt_inner_non_legacy() {
706        let receipt = ReceiptWithBloom {
707            tx_type: TxType::EIP4844,
708            succeeded: true,
709            cumulative_gas_used: 1500,
710            bloom: Bloom::random(),
711            logs: vec![Log {
712                address: Address::random(),
713                topics: vec![],
714                data: Bytes::from_static(b"bar"),
715            }],
716        };
717        let encoded_receipt = receipt.encode_inner();
718        assert_eq!(
719            receipt,
720            ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap()
721        )
722    }
723
724    #[test]
725    fn test_encode_receipt_with_bloom() {
726        let receipt = Receipt {
727            tx_type: TxType::EIP1559,
728            succeeded: true,
729            cumulative_gas_used: 1500,
730            logs: vec![Log {
731                address: Address::random(),
732                topics: vec![
733                    h256_from_hex(
734                        "e70c0d1060ffbafc84e0e18d028245de3deeb0f41ecbade6562fa657d85ae945",
735                    ),
736                    h256_from_hex(
737                        "e7e9cd61c8c6cb313324d785aa130fe50a7b9885e4d1d7700a327c5e9ae4e183",
738                    ),
739                    h256_from_hex(
740                        "666d827b9db958c08f7186f127e3d9ea6a97288bcc4b527951ce493f6e2b76c4",
741                    ),
742                    h256_from_hex(
743                        "28b4366544dccafad7b61138e9ada51706e85bb217a20cfa1c86e2648f8f369a",
744                    ),
745                    h256_from_hex(
746                        "85cf9717f65c70d71cc6175f653512c13ce7b6a9bc5d9c2b9c49b2d2d6cb9536",
747                    ),
748                ],
749                data: Bytes::from_static(b"bar"),
750            }],
751            payer: None,
752            frame_receipts: None,
753        };
754        let encoded_receipt = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
755
756        let correct_bloom = {
757            let mut bloom = Bloom::zero();
758            for log in receipt.logs {
759                bloom.accrue(BloomInput::Raw(log.address.as_ref()));
760                for topic in log.topics.iter() {
761                    bloom.accrue(BloomInput::Raw(topic.as_ref()));
762                }
763            }
764            bloom
765        };
766        let receipt_with_bloom = ReceiptWithBloom::decode_inner(&encoded_receipt).unwrap();
767        assert_eq!(receipt_with_bloom.bloom, correct_bloom);
768    }
769
770    #[test]
771    fn test_frame_receipt_rlp_roundtrip() {
772        let fr = FrameReceipt {
773            status: FRAME_RECEIPT_STATUS_SUCCESS,
774            gas_used: 21000,
775            logs: vec![Log {
776                address: Address::random(),
777                topics: vec![],
778                data: Bytes::from_static(b"test"),
779            }],
780        };
781        let encoded = fr.encode_to_vec();
782        let decoded = FrameReceipt::decode(&encoded).unwrap();
783        assert_eq!(fr, decoded);
784    }
785
786    #[test]
787    fn test_frame_receipt_skipped_status_rlp_roundtrip() {
788        // Spec line 137: status code 0x3 marks frames skipped by a failed atomic batch.
789        let fr = FrameReceipt {
790            status: FRAME_RECEIPT_STATUS_SKIPPED,
791            gas_used: 0,
792            logs: vec![],
793        };
794        let encoded = fr.encode_to_vec();
795        let decoded = FrameReceipt::decode(&encoded).unwrap();
796        assert_eq!(fr, decoded);
797        assert_eq!(decoded.status, FRAME_RECEIPT_STATUS_SKIPPED);
798    }
799
800    #[test]
801    fn test_receipt_with_frame_fields_rlp_roundtrip() {
802        // Frame receipts encode as [cumulative_gas_used, payer, [frame_receipts]]
803        // without top-level succeeded or logs. On decode, succeeded is derived
804        // from frame receipts and logs is empty.
805        let receipt = Receipt {
806            tx_type: TxType::Frame,
807            succeeded: true,
808            cumulative_gas_used: 315000,
809            logs: vec![],
810            payer: Some(Address::from_low_u64_be(0x1234)),
811            frame_receipts: Some(vec![
812                FrameReceipt {
813                    status: FRAME_RECEIPT_STATUS_SUCCESS,
814                    gas_used: 100000,
815                    logs: vec![],
816                },
817                FrameReceipt {
818                    status: FRAME_RECEIPT_STATUS_SUCCESS,
819                    gas_used: 200000,
820                    logs: vec![Log {
821                        address: Address::from_low_u64_be(0xbeef),
822                        topics: vec![],
823                        data: Bytes::from_static(b"frame2"),
824                    }],
825                },
826            ]),
827        };
828        let encoded = receipt.encode_to_vec();
829        let decoded = Receipt::decode(&encoded).unwrap();
830        assert_eq!(decoded.tx_type, TxType::Frame);
831        assert_eq!(decoded.cumulative_gas_used, 315000);
832        assert_eq!(decoded.payer, Some(Address::from_low_u64_be(0x1234)));
833        assert!(decoded.succeeded); // derived: all frame receipts succeeded
834        assert!(decoded.logs.is_empty()); // top-level logs not encoded for frame txs
835        assert_eq!(decoded.frame_receipts, receipt.frame_receipts);
836    }
837
838    #[test]
839    fn test_frame_receipt_succeeded_derived_from_frames() {
840        let receipt = Receipt {
841            tx_type: TxType::Frame,
842            succeeded: true, // will be overridden on decode
843            cumulative_gas_used: 100000,
844            logs: vec![],
845            payer: Some(Address::from_low_u64_be(0x1)),
846            frame_receipts: Some(vec![
847                FrameReceipt {
848                    status: FRAME_RECEIPT_STATUS_SUCCESS,
849                    gas_used: 50000,
850                    logs: vec![],
851                },
852                FrameReceipt {
853                    status: FRAME_RECEIPT_STATUS_FAILURE,
854                    gas_used: 50000,
855                    logs: vec![],
856                },
857            ]),
858        };
859        let encoded = receipt.encode_to_vec();
860        let decoded = Receipt::decode(&encoded).unwrap();
861        assert!(!decoded.succeeded); // one frame failed
862    }
863
864    #[test]
865    fn test_frame_receipt_skipped_disqualifies_succeeded() {
866        // A SKIPPED frame must not count as success: spec implies the tx outcome
867        // is not fully successful when any atomic batch failed.
868        let receipt = Receipt {
869            tx_type: TxType::Frame,
870            succeeded: false,
871            cumulative_gas_used: 100000,
872            logs: vec![],
873            payer: Some(Address::from_low_u64_be(0x1)),
874            frame_receipts: Some(vec![
875                FrameReceipt {
876                    status: FRAME_RECEIPT_STATUS_SUCCESS,
877                    gas_used: 50000,
878                    logs: vec![],
879                },
880                FrameReceipt {
881                    status: FRAME_RECEIPT_STATUS_SKIPPED,
882                    gas_used: 0,
883                    logs: vec![],
884                },
885            ]),
886        };
887        let encoded = receipt.encode_to_vec();
888        let decoded = Receipt::decode(&encoded).unwrap();
889        assert!(!decoded.succeeded);
890    }
891
892    #[test]
893    fn frame_receipt_storage_roundtrip_preserves_logs_and_status() {
894        let log = Log {
895            address: Address::from_low_u64_be(1),
896            topics: vec![],
897            data: Bytes::from(vec![1u8]),
898        };
899        let receipt = Receipt {
900            tx_type: TxType::Frame,
901            succeeded: true, // VM rule: no SENDER frame reverted
902            cumulative_gas_used: 50_000,
903            logs: vec![log.clone()], // aggregated frame logs
904            payer: Some(Address::from_low_u64_be(2)),
905            frame_receipts: Some(vec![
906                FrameReceipt {
907                    status: FRAME_RECEIPT_STATUS_FAILURE,
908                    gas_used: 1000,
909                    logs: vec![],
910                }, // a DEFAULT frame failed
911                FrameReceipt {
912                    status: FRAME_RECEIPT_STATUS_SUCCESS,
913                    gas_used: 2000,
914                    logs: vec![log],
915                },
916            ]),
917        };
918        // succeeded=true coexists with a FAILURE frame -> the old derive-rule would
919        // have flipped it to false; storage must keep it verbatim.
920        let decoded = Receipt::decode_storage(&receipt.encode_storage()).unwrap();
921        assert_eq!(decoded, receipt);
922    }
923
924    #[test]
925    fn nonframe_receipt_storage_roundtrip_unchanged() {
926        let r = Receipt::new(TxType::EIP1559, true, 21000, vec![]);
927        assert_eq!(Receipt::decode_storage(&r.encode_storage()).unwrap(), r);
928        // and that encode_storage matches the legacy encode_inner for non-frame:
929        assert_eq!(r.encode_storage(), r.encode_inner());
930    }
931
932    #[test]
933    fn frame_receipt_trie_encoding_is_eip8141_payload() {
934        let receipt = Receipt {
935            tx_type: TxType::Frame,
936            succeeded: true,
937            cumulative_gas_used: 100_000,
938            logs: Vec::new(),
939            payer: Some(Address::from_low_u64_be(0xBEEF)),
940            frame_receipts: Some(vec![FrameReceipt {
941                status: FRAME_RECEIPT_STATUS_SUCCESS,
942                gas_used: 21_000,
943                logs: Vec::new(),
944            }]),
945        };
946        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
947        // EIP-2718 type prefix
948        assert_eq!(encoded[0], 0x06);
949        // Spec ReceiptPayload: [cumulative_gas_used, payer, [frame_receipt, ...]]
950        let mut expected_payload = Vec::new();
951        Encoder::new(&mut expected_payload)
952            .encode_field(&100_000u64)
953            .encode_field(&Address::from_low_u64_be(0xBEEF))
954            .encode_field(receipt.frame_receipts.as_ref().unwrap())
955            .finish();
956        assert_eq!(&encoded[1..], &expected_payload[..]);
957    }
958
959    #[test]
960    fn decode_inner_with_bloom_roundtrips_nonframe() {
961        // Non-frame: encode_inner_with_bloom carries a bloom that
962        // decode_inner_with_bloom drops. Compare every field except the dropped
963        // bloom — i.e. the decoded receipt must equal the original Receipt.
964        let receipt = Receipt {
965            tx_type: TxType::EIP1559,
966            succeeded: true,
967            cumulative_gas_used: 54321,
968            logs: vec![Log {
969                address: Address::from_low_u64_be(0xabcd),
970                topics: vec![h256_from_hex(
971                    "e70c0d1060ffbafc84e0e18d028245de3deeb0f41ecbade6562fa657d85ae945",
972                )],
973                data: Bytes::from_static(b"hello"),
974            }],
975            payer: None,
976            frame_receipts: None,
977        };
978        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
979        let (decoded, rest) = Receipt::decode_inner_with_bloom(&encoded).unwrap();
980        assert!(rest.is_empty());
981        assert_eq!(decoded, receipt);
982    }
983
984    #[test]
985    fn decode_inner_with_bloom_roundtrips_legacy() {
986        let receipt = Receipt {
987            tx_type: TxType::Legacy,
988            succeeded: false,
989            cumulative_gas_used: 100,
990            logs: vec![],
991            payer: None,
992            frame_receipts: None,
993        };
994        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
995        let (decoded, rest) = Receipt::decode_inner_with_bloom(&encoded).unwrap();
996        assert!(rest.is_empty());
997        assert_eq!(decoded, receipt);
998    }
999
1000    #[test]
1001    fn decode_inner_with_bloom_roundtrips_frame_preserving_payer_and_frames() {
1002        // Frame: payer and frame_receipts MUST survive the roundtrip. succeeded
1003        // is derived from the frame statuses and logs is empty by construction.
1004        let receipt = Receipt {
1005            tx_type: TxType::Frame,
1006            succeeded: true,
1007            cumulative_gas_used: 315000,
1008            logs: vec![],
1009            payer: Some(Address::from_low_u64_be(0x1234)),
1010            frame_receipts: Some(vec![
1011                FrameReceipt {
1012                    status: FRAME_RECEIPT_STATUS_SUCCESS,
1013                    gas_used: 100000,
1014                    logs: vec![],
1015                },
1016                FrameReceipt {
1017                    status: FRAME_RECEIPT_STATUS_SUCCESS,
1018                    gas_used: 200000,
1019                    logs: vec![Log {
1020                        address: Address::from_low_u64_be(0xbeef),
1021                        topics: vec![],
1022                        data: Bytes::from_static(b"frame2"),
1023                    }],
1024                },
1025            ]),
1026        };
1027        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
1028        let (decoded, rest) = Receipt::decode_inner_with_bloom(&encoded).unwrap();
1029        assert!(rest.is_empty());
1030        assert_eq!(decoded.tx_type, TxType::Frame);
1031        assert_eq!(decoded.cumulative_gas_used, 315000);
1032        assert_eq!(decoded.payer, Some(Address::from_low_u64_be(0x1234)));
1033        assert_eq!(decoded.frame_receipts, receipt.frame_receipts);
1034        assert!(decoded.succeeded); // derived: all frame receipts succeeded
1035        assert!(decoded.logs.is_empty());
1036        // Full equality: the whole receipt round-trips.
1037        assert_eq!(decoded, receipt);
1038    }
1039
1040    #[test]
1041    fn decode_inner_with_bloom_frame_zero_payer_maps_to_none() {
1042        let receipt = Receipt {
1043            tx_type: TxType::Frame,
1044            succeeded: true,
1045            cumulative_gas_used: 21000,
1046            logs: vec![],
1047            payer: None,
1048            frame_receipts: Some(vec![FrameReceipt {
1049                status: FRAME_RECEIPT_STATUS_SUCCESS,
1050                gas_used: 21000,
1051                logs: vec![],
1052            }]),
1053        };
1054        let encoded = receipt.encode_inner_with_bloom(&ethrex_crypto::NativeCrypto);
1055        let (decoded, _) = Receipt::decode_inner_with_bloom(&encoded).unwrap();
1056        assert_eq!(decoded.payer, None);
1057        assert_eq!(decoded, receipt);
1058    }
1059}