arb_primitives/
receipt.rs

1use alloc::vec::Vec;
2
3use alloy_consensus::{
4    Eip658Value, Eip2718EncodableReceipt, Receipt as AlloyReceipt, TxReceipt, Typed2718,
5};
6use alloy_eips::{Decodable2718, Encodable2718};
7use alloy_primitives::{Bloom, Log};
8use alloy_rlp::{Decodable, Encodable};
9use arb_alloy_consensus::tx::ArbTxType;
10use reth_primitives_traits::InMemorySize;
11
12use crate::multigas::MultiGas;
13
14/// Arbitrum receipt: wraps the per-type receipt kind with L1 gas metadata.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ArbReceipt {
17    pub kind: ArbReceiptKind,
18    /// Gas units used for L1 calldata posting (poster gas).
19    /// Populated by the block executor after receipt construction.
20    pub gas_used_for_l1: u64,
21    /// Multi-dimensional gas usage breakdown.
22    /// Populated when multi-gas tracking is enabled (ArbOS v60+).
23    pub multi_gas_used: MultiGas,
24}
25
26impl ArbReceipt {
27    /// Create a new receipt with no L1 gas usage (filled in later).
28    pub fn new(kind: ArbReceiptKind) -> Self {
29        Self {
30            kind,
31            gas_used_for_l1: 0,
32            multi_gas_used: MultiGas::zero(),
33        }
34    }
35
36    pub fn with_gas_used_for_l1(mut self, gas: u64) -> Self {
37        self.gas_used_for_l1 = gas;
38        self
39    }
40}
41
42/// Trait for setting Arbitrum-specific fields on a receipt after construction.
43pub trait SetArbReceiptFields {
44    fn set_gas_used_for_l1(&mut self, gas: u64);
45    fn set_multi_gas_used(&mut self, multi_gas: MultiGas);
46}
47
48impl SetArbReceiptFields for ArbReceipt {
49    fn set_gas_used_for_l1(&mut self, gas: u64) {
50        self.gas_used_for_l1 = gas;
51    }
52
53    fn set_multi_gas_used(&mut self, multi_gas: MultiGas) {
54        self.multi_gas_used = multi_gas;
55    }
56}
57
58/// Per-type receipt variants matching the Arbitrum transaction type.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum ArbReceiptKind {
61    Legacy(AlloyReceipt),
62    Eip1559(AlloyReceipt),
63    Eip2930(AlloyReceipt),
64    Eip7702(AlloyReceipt),
65    Deposit(ArbDepositReceipt),
66    Unsigned(AlloyReceipt),
67    Contract(AlloyReceipt),
68    Retry(AlloyReceipt),
69    SubmitRetryable(AlloyReceipt),
70    Internal(AlloyReceipt),
71}
72
73/// Deposit receipts carry a success status, with no gas and no logs.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ArbDepositReceipt {
76    /// Whether the deposit succeeded. A filtered deposit fails.
77    pub status: bool,
78}
79
80impl Default for ArbDepositReceipt {
81    fn default() -> Self {
82        Self { status: true }
83    }
84}
85
86impl ArbDepositReceipt {
87    pub const fn new(status: bool) -> Self {
88        Self { status }
89    }
90}
91
92// ---------------------------------------------------------------------------
93// ArbReceiptKind — inherent methods (encoding internals)
94// ---------------------------------------------------------------------------
95
96impl ArbReceiptKind {
97    pub const fn arb_tx_type(&self) -> ArbTxType {
98        match self {
99            Self::Legacy(_) | Self::Eip2930(_) | Self::Eip1559(_) | Self::Eip7702(_) => {
100                ArbTxType::ArbitrumLegacyTx
101            }
102            Self::Deposit(_) => ArbTxType::ArbitrumDepositTx,
103            Self::Unsigned(_) => ArbTxType::ArbitrumUnsignedTx,
104            Self::Contract(_) => ArbTxType::ArbitrumContractTx,
105            Self::Retry(_) => ArbTxType::ArbitrumRetryTx,
106            Self::SubmitRetryable(_) => ArbTxType::ArbitrumSubmitRetryableTx,
107            Self::Internal(_) => ArbTxType::ArbitrumInternalTx,
108        }
109    }
110
111    /// Returns the underlying `AlloyReceipt` for variants that wrap one.
112    ///
113    /// Returns `None` for [`ArbReceiptKind::Deposit`], which has no inner
114    /// receipt body (deposits always succeed with zero gas and no logs).
115    pub const fn as_receipt(&self) -> Option<&AlloyReceipt> {
116        match self {
117            Self::Legacy(r)
118            | Self::Eip2930(r)
119            | Self::Eip1559(r)
120            | Self::Eip7702(r)
121            | Self::Unsigned(r)
122            | Self::Contract(r)
123            | Self::Retry(r)
124            | Self::SubmitRetryable(r)
125            | Self::Internal(r) => Some(r),
126            Self::Deposit(_) => None,
127        }
128    }
129
130    fn rlp_encoded_fields_length(&self, bloom: &Bloom) -> usize {
131        match self {
132            Self::Legacy(r)
133            | Self::Eip2930(r)
134            | Self::Eip1559(r)
135            | Self::Eip7702(r)
136            | Self::Unsigned(r)
137            | Self::Contract(r)
138            | Self::Retry(r)
139            | Self::SubmitRetryable(r)
140            | Self::Internal(r) => r.rlp_encoded_fields_length_with_bloom(bloom),
141            Self::Deposit(d) => {
142                Eip658Value::Eip658(d.status).length()
143                    + 0u64.length()
144                    + bloom.length()
145                    + Vec::<Log>::new().length()
146            }
147        }
148    }
149
150    fn rlp_encode_fields(&self, bloom: &Bloom, out: &mut dyn alloy_rlp::bytes::BufMut) {
151        match self {
152            Self::Legacy(r)
153            | Self::Eip2930(r)
154            | Self::Eip1559(r)
155            | Self::Eip7702(r)
156            | Self::Unsigned(r)
157            | Self::Contract(r)
158            | Self::Retry(r)
159            | Self::SubmitRetryable(r)
160            | Self::Internal(r) => r.rlp_encode_fields_with_bloom(bloom, out),
161            Self::Deposit(d) => {
162                Eip658Value::Eip658(d.status).encode(out);
163                (0u64).encode(out);
164                bloom.encode(out);
165                let logs: Vec<Log> = Vec::new();
166                logs.encode(out);
167            }
168        }
169    }
170
171    fn rlp_header_inner(&self, bloom: &Bloom) -> alloy_rlp::Header {
172        alloy_rlp::Header {
173            list: true,
174            payload_length: self.rlp_encoded_fields_length(bloom),
175        }
176    }
177
178    fn rlp_encode_fields_without_bloom(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
179        match self {
180            Self::Legacy(r)
181            | Self::Eip2930(r)
182            | Self::Eip1559(r)
183            | Self::Eip7702(r)
184            | Self::Unsigned(r)
185            | Self::Contract(r)
186            | Self::Retry(r)
187            | Self::SubmitRetryable(r)
188            | Self::Internal(r) => {
189                r.status.encode(out);
190                r.cumulative_gas_used.encode(out);
191                r.logs.encode(out);
192            }
193            Self::Deposit(d) => {
194                Eip658Value::Eip658(d.status).encode(out);
195                (0u64).encode(out);
196                let logs: Vec<Log> = Vec::new();
197                logs.encode(out);
198            }
199        }
200    }
201
202    fn rlp_encoded_fields_length_without_bloom(&self) -> usize {
203        match self {
204            Self::Legacy(r)
205            | Self::Eip2930(r)
206            | Self::Eip1559(r)
207            | Self::Eip7702(r)
208            | Self::Unsigned(r)
209            | Self::Contract(r)
210            | Self::Retry(r)
211            | Self::SubmitRetryable(r)
212            | Self::Internal(r) => {
213                r.status.length() + r.cumulative_gas_used.length() + r.logs.length()
214            }
215            Self::Deposit(d) => {
216                Eip658Value::Eip658(d.status).length()
217                    + (0u64).length()
218                    + Vec::<Log>::new().length()
219            }
220        }
221    }
222
223    fn rlp_header_inner_without_bloom(&self) -> alloy_rlp::Header {
224        alloy_rlp::Header {
225            list: true,
226            payload_length: self.rlp_encoded_fields_length_without_bloom(),
227        }
228    }
229
230    fn rlp_decode_inner(
231        buf: &mut &[u8],
232        tx_type: ArbTxType,
233    ) -> alloy_rlp::Result<alloy_consensus::ReceiptWithBloom<ArbReceipt>> {
234        match tx_type {
235            ArbTxType::ArbitrumDepositTx => {
236                let header = alloy_rlp::Header::decode(buf)?;
237                if !header.list {
238                    return Err(alloy_rlp::Error::UnexpectedString);
239                }
240                let remaining = buf.len();
241                let status: Eip658Value = alloy_rlp::Decodable::decode(buf)?;
242                let _cumu: u64 = alloy_rlp::Decodable::decode(buf)?;
243                let logs_bloom: Bloom = alloy_rlp::Decodable::decode(buf)?;
244                let _logs: Vec<Log> = alloy_rlp::Decodable::decode(buf)?;
245                if buf.len() + header.payload_length != remaining {
246                    return Err(alloy_rlp::Error::UnexpectedLength);
247                }
248                Ok(alloy_consensus::ReceiptWithBloom {
249                    receipt: ArbReceipt::new(ArbReceiptKind::Deposit(ArbDepositReceipt::new(
250                        status.coerce_status(),
251                    ))),
252                    logs_bloom,
253                })
254            }
255            _ => {
256                let alloy_consensus::ReceiptWithBloom {
257                    receipt,
258                    logs_bloom,
259                } = <AlloyReceipt as alloy_consensus::RlpDecodableReceipt>::rlp_decode_with_bloom(
260                    buf,
261                )?;
262                Ok(alloy_consensus::ReceiptWithBloom {
263                    receipt: ArbReceipt::new(ArbReceiptKind::Legacy(receipt)),
264                    logs_bloom,
265                })
266            }
267        }
268    }
269
270    fn rlp_decode_inner_without_bloom(
271        buf: &mut &[u8],
272        tx_type: ArbTxType,
273    ) -> alloy_rlp::Result<ArbReceipt> {
274        let header = alloy_rlp::Header::decode(buf)?;
275        if !header.list {
276            return Err(alloy_rlp::Error::UnexpectedString);
277        }
278        let remaining = buf.len();
279        let status: Eip658Value = alloy_rlp::Decodable::decode(buf)?;
280        let cumulative_gas_used: u64 = alloy_rlp::Decodable::decode(buf)?;
281        let logs: Vec<Log> = alloy_rlp::Decodable::decode(buf)?;
282        if buf.len() + header.payload_length != remaining {
283            return Err(alloy_rlp::Error::UnexpectedLength);
284        }
285        let receipt = AlloyReceipt {
286            status,
287            cumulative_gas_used,
288            logs,
289        };
290        let kind = match tx_type {
291            ArbTxType::ArbitrumDepositTx => {
292                ArbReceiptKind::Deposit(ArbDepositReceipt::new(receipt.status.coerce_status()))
293            }
294            ArbTxType::ArbitrumUnsignedTx => ArbReceiptKind::Unsigned(receipt),
295            ArbTxType::ArbitrumContractTx => ArbReceiptKind::Contract(receipt),
296            ArbTxType::ArbitrumRetryTx => ArbReceiptKind::Retry(receipt),
297            ArbTxType::ArbitrumSubmitRetryableTx => ArbReceiptKind::SubmitRetryable(receipt),
298            ArbTxType::ArbitrumInternalTx => ArbReceiptKind::Internal(receipt),
299            ArbTxType::ArbitrumLegacyTx => ArbReceiptKind::Legacy(receipt),
300        };
301        Ok(ArbReceipt::new(kind))
302    }
303}
304
305// ---------------------------------------------------------------------------
306// InMemorySize
307// ---------------------------------------------------------------------------
308
309impl InMemorySize for ArbReceipt {
310    fn size(&self) -> usize {
311        core::mem::size_of::<u64>() // gas_used_for_l1
312            + core::mem::size_of::<MultiGas>() // multi_gas_used
313    }
314}
315
316// ---------------------------------------------------------------------------
317// TxReceipt — delegate to kind
318// ---------------------------------------------------------------------------
319
320impl TxReceipt for ArbReceipt {
321    type Log = Log;
322
323    fn status_or_post_state(&self) -> Eip658Value {
324        match self.kind.as_receipt() {
325            Some(r) => r.status_or_post_state(),
326            None => match &self.kind {
327                ArbReceiptKind::Deposit(d) => Eip658Value::Eip658(d.status),
328                _ => Eip658Value::Eip658(true),
329            },
330        }
331    }
332
333    fn status(&self) -> bool {
334        match self.kind.as_receipt() {
335            Some(r) => r.status(),
336            None => match &self.kind {
337                ArbReceiptKind::Deposit(d) => d.status,
338                _ => true,
339            },
340        }
341    }
342
343    fn bloom(&self) -> Bloom {
344        match self.kind.as_receipt() {
345            Some(r) => r.bloom(),
346            None => Bloom::ZERO,
347        }
348    }
349
350    fn cumulative_gas_used(&self) -> u64 {
351        match self.kind.as_receipt() {
352            Some(r) => r.cumulative_gas_used(),
353            None => 0,
354        }
355    }
356
357    fn logs(&self) -> &[Self::Log] {
358        match self.kind.as_receipt() {
359            Some(r) => r.logs(),
360            None => &[],
361        }
362    }
363
364    fn into_logs(self) -> Vec<Self::Log> {
365        match self.kind {
366            ArbReceiptKind::Legacy(r)
367            | ArbReceiptKind::Eip2930(r)
368            | ArbReceiptKind::Eip1559(r)
369            | ArbReceiptKind::Eip7702(r)
370            | ArbReceiptKind::Unsigned(r)
371            | ArbReceiptKind::Contract(r)
372            | ArbReceiptKind::Retry(r)
373            | ArbReceiptKind::SubmitRetryable(r)
374            | ArbReceiptKind::Internal(r) => r.logs,
375            ArbReceiptKind::Deposit(_) => Vec::new(),
376        }
377    }
378}
379
380// ---------------------------------------------------------------------------
381// Typed2718 — delegate to kind
382// ---------------------------------------------------------------------------
383
384impl Typed2718 for ArbReceipt {
385    fn is_legacy(&self) -> bool {
386        matches!(self.kind, ArbReceiptKind::Legacy(_))
387    }
388
389    fn ty(&self) -> u8 {
390        match &self.kind {
391            ArbReceiptKind::Legacy(_) => 0x00,
392            ArbReceiptKind::Eip2930(_) => 0x01,
393            ArbReceiptKind::Eip1559(_) => 0x02,
394            ArbReceiptKind::Eip7702(_) => 0x04,
395            ArbReceiptKind::Deposit(_) => 0x64,
396            ArbReceiptKind::Unsigned(_) => 0x65,
397            ArbReceiptKind::Contract(_) => 0x66,
398            ArbReceiptKind::Retry(_) => 0x68,
399            ArbReceiptKind::SubmitRetryable(_) => 0x69,
400            ArbReceiptKind::Internal(_) => 0x6A,
401        }
402    }
403}
404
405// ---------------------------------------------------------------------------
406// Eip2718EncodableReceipt — consensus encoding (no gas_used_for_l1)
407// ---------------------------------------------------------------------------
408
409impl Eip2718EncodableReceipt for ArbReceipt {
410    fn eip2718_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
411        let inner_len = self.kind.rlp_header_inner(bloom).length_with_payload();
412        if !self.is_legacy() {
413            1 + inner_len
414        } else {
415            inner_len
416        }
417    }
418
419    fn eip2718_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn alloy_rlp::bytes::BufMut) {
420        if !self.is_legacy() {
421            out.put_u8(self.ty());
422        }
423        self.kind.rlp_header_inner(bloom).encode(out);
424        self.kind.rlp_encode_fields(bloom, out);
425    }
426}
427
428// ---------------------------------------------------------------------------
429// RlpEncodableReceipt
430// ---------------------------------------------------------------------------
431
432impl alloy_consensus::RlpEncodableReceipt for ArbReceipt {
433    fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize {
434        let mut len = self.eip2718_encoded_length_with_bloom(bloom);
435        if !self.is_legacy() {
436            len += alloy_rlp::Header {
437                list: false,
438                payload_length: self.eip2718_encoded_length_with_bloom(bloom),
439            }
440            .length();
441        }
442        len
443    }
444
445    fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn alloy_rlp::bytes::BufMut) {
446        if !self.is_legacy() {
447            alloy_rlp::Header {
448                list: false,
449                payload_length: self.eip2718_encoded_length_with_bloom(bloom),
450            }
451            .encode(out);
452        }
453        self.eip2718_encode_with_bloom(bloom, out);
454    }
455}
456
457// ---------------------------------------------------------------------------
458// RlpDecodableReceipt
459// ---------------------------------------------------------------------------
460
461impl alloy_consensus::RlpDecodableReceipt for ArbReceipt {
462    fn rlp_decode_with_bloom(
463        buf: &mut &[u8],
464    ) -> alloy_rlp::Result<alloy_consensus::ReceiptWithBloom<Self>> {
465        let header_buf = &mut &**buf;
466        let header = alloy_rlp::Header::decode(header_buf)?;
467        if header.list {
468            return ArbReceiptKind::rlp_decode_inner(buf, ArbTxType::ArbitrumLegacyTx);
469        }
470        *buf = *header_buf;
471        let remaining = buf.len();
472        let ty = u8::decode(buf)?;
473        let tx_type = ArbTxType::from_u8(ty)
474            .map_err(|_| alloy_rlp::Error::Custom("unexpected arb receipt tx type"))?;
475        let this = ArbReceiptKind::rlp_decode_inner(buf, tx_type)?;
476        if buf.len() + header.payload_length != remaining {
477            return Err(alloy_rlp::Error::UnexpectedLength);
478        }
479        Ok(this)
480    }
481}
482
483// ---------------------------------------------------------------------------
484// Encodable2718 / Decodable2718
485// ---------------------------------------------------------------------------
486
487impl Encodable2718 for ArbReceipt {
488    fn encode_2718_len(&self) -> usize {
489        let type_len = if self.is_legacy() { 0 } else { 1 };
490        type_len
491            + self
492                .kind
493                .rlp_header_inner_without_bloom()
494                .length_with_payload()
495    }
496
497    fn encode_2718(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
498        if !self.is_legacy() {
499            out.put_u8(self.ty());
500        }
501        self.kind.rlp_header_inner_without_bloom().encode(out);
502        self.kind.rlp_encode_fields_without_bloom(out);
503    }
504}
505
506impl Decodable2718 for ArbReceipt {
507    fn typed_decode(ty: u8, buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Self> {
508        // Standard EVM types use their raw type byte in 2718 encoding but map to
509        // specific ArbReceiptKind variants. ArbTxType only covers Arbitrum-specific
510        // types (0x64+), so handle standard EVM types here first.
511        match ty {
512            0x01 => {
513                return Self::decode_standard_receipt(buf, ArbReceiptKind::Eip2930);
514            }
515            0x02 => {
516                return Self::decode_standard_receipt(buf, ArbReceiptKind::Eip1559);
517            }
518            0x04 => {
519                return Self::decode_standard_receipt(buf, ArbReceiptKind::Eip7702);
520            }
521            _ => {}
522        }
523        let tx_type = ArbTxType::from_u8(ty)
524            .map_err(|_| alloy_eips::eip2718::Eip2718Error::UnexpectedType(ty))?;
525        Ok(ArbReceiptKind::rlp_decode_inner_without_bloom(
526            buf, tx_type,
527        )?)
528    }
529
530    fn fallback_decode(buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Self> {
531        Ok(ArbReceiptKind::rlp_decode_inner_without_bloom(
532            buf,
533            ArbTxType::ArbitrumLegacyTx,
534        )?)
535    }
536}
537
538impl ArbReceipt {
539    /// Decode a standard EVM receipt (EIP-2930, EIP-1559, EIP-7702) from RLP.
540    fn decode_standard_receipt(
541        buf: &mut &[u8],
542        wrap: impl FnOnce(AlloyReceipt) -> ArbReceiptKind,
543    ) -> alloy_eips::eip2718::Eip2718Result<Self> {
544        let header = alloy_rlp::Header::decode(buf)?;
545        if !header.list {
546            return Err(alloy_rlp::Error::UnexpectedString.into());
547        }
548        let remaining = buf.len();
549        let status: Eip658Value = alloy_rlp::Decodable::decode(buf)?;
550        let cumulative_gas_used: u64 = alloy_rlp::Decodable::decode(buf)?;
551        let logs: Vec<Log> = alloy_rlp::Decodable::decode(buf)?;
552        if buf.len() + header.payload_length != remaining {
553            return Err(alloy_rlp::Error::UnexpectedLength.into());
554        }
555        Ok(ArbReceipt::new(wrap(AlloyReceipt {
556            status,
557            cumulative_gas_used,
558            logs,
559        })))
560    }
561}
562
563// ---------------------------------------------------------------------------
564// Encodable / Decodable (RLP) — network encoding
565// ---------------------------------------------------------------------------
566
567impl alloy_rlp::Encodable for ArbReceipt {
568    fn encode(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
569        self.network_encode(out);
570    }
571
572    fn length(&self) -> usize {
573        self.network_len()
574    }
575}
576
577impl alloy_rlp::Decodable for ArbReceipt {
578    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
579        Ok(Self::network_decode(buf)?)
580    }
581}
582
583// ---------------------------------------------------------------------------
584// serde
585// ---------------------------------------------------------------------------
586
587impl serde::Serialize for ArbReceipt {
588    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
589    where
590        S: serde::Serializer,
591    {
592        use serde::ser::SerializeStruct;
593        let mut state = serializer.serialize_struct("ArbReceipt", 5)?;
594        state.serialize_field("status", &self.status())?;
595        state.serialize_field("cumulative_gas_used", &self.cumulative_gas_used())?;
596        state.serialize_field("ty", &self.ty())?;
597        state.serialize_field("gas_used_for_l1", &self.gas_used_for_l1)?;
598        state.serialize_field("multi_gas_used", &self.multi_gas_used)?;
599        state.end()
600    }
601}
602
603impl<'de> serde::Deserialize<'de> for ArbReceipt {
604    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
605    where
606        D: serde::Deserializer<'de>,
607    {
608        #[derive(serde::Deserialize)]
609        struct Helper {
610            status: bool,
611            cumulative_gas_used: u64,
612            #[serde(default)]
613            ty: u8,
614            #[serde(default)]
615            gas_used_for_l1: u64,
616            #[serde(default)]
617            multi_gas_used: MultiGas,
618        }
619        let helper = Helper::deserialize(deserializer)?;
620        let kind = if helper.ty == 0x64 {
621            ArbReceiptKind::Deposit(ArbDepositReceipt::new(helper.status))
622        } else {
623            let receipt = AlloyReceipt {
624                status: Eip658Value::Eip658(helper.status),
625                cumulative_gas_used: helper.cumulative_gas_used,
626                logs: Vec::new(),
627            };
628            ArbReceiptKind::Legacy(receipt)
629        };
630        Ok(ArbReceipt {
631            kind,
632            gas_used_for_l1: helper.gas_used_for_l1,
633            multi_gas_used: helper.multi_gas_used,
634        })
635    }
636}
637
638// ---------------------------------------------------------------------------
639// RlpBincode — required by SerdeBincodeCompat
640// ---------------------------------------------------------------------------
641
642impl reth_primitives_traits::serde_bincode_compat::RlpBincode for ArbReceipt {}
643
644// ---------------------------------------------------------------------------
645// Compact — storage encoding (includes gas_used_for_l1)
646// ---------------------------------------------------------------------------
647
648impl reth_codecs::Compact for ArbReceipt {
649    fn to_compact<B>(&self, buf: &mut B) -> usize
650    where
651        B: bytes::BufMut + AsMut<[u8]>,
652    {
653        // Encode the receipt body via 2718.
654        let mut encoded = Vec::new();
655        self.encode_2718(&mut encoded);
656        let len = encoded.len() as u32;
657        buf.put_u32(len);
658        buf.put_slice(&encoded);
659        // Append gas_used_for_l1 for storage.
660        buf.put_u64(self.gas_used_for_l1);
661        // Append multi_gas_used (8 dimensions + total + refund = 10 u64s).
662        for i in 0..crate::multigas::NUM_RESOURCE_KIND {
663            buf.put_u64(
664                self.multi_gas_used.get(
665                    crate::multigas::ResourceKind::from_u8(i as u8)
666                        .unwrap_or(crate::multigas::ResourceKind::Unknown),
667                ),
668            );
669        }
670        buf.put_u64(self.multi_gas_used.total());
671        buf.put_u64(self.multi_gas_used.refund());
672        0
673    }
674
675    fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
676        use bytes::Buf;
677        let mut slice = buf;
678        let receipt_len = slice.get_u32() as usize;
679        let receipt_bytes = &slice[..receipt_len];
680        slice = &slice[receipt_len..];
681
682        let mut rbuf = receipt_bytes;
683        let mut receipt = Self::network_decode(&mut rbuf).unwrap_or_else(|_| {
684            ArbReceipt::new(ArbReceiptKind::Legacy(AlloyReceipt {
685                status: Eip658Value::Eip658(false),
686                cumulative_gas_used: 0,
687                logs: Vec::new(),
688            }))
689        });
690
691        // Bytes after the length-prefixed body, bounded by the on-disk `len` so
692        // trailing data from a larger blob is left for the caller.
693        let mut tail = len.saturating_sub(4 + receipt_len).min(slice.len());
694
695        if tail >= 8 {
696            receipt.gas_used_for_l1 = slice.get_u64();
697            tail -= 8;
698        }
699
700        // The multi-gas section is `[gas: K x u64][total][refund]`. K was
701        // NUM_RESOURCE_KIND at write time and may differ from the current value,
702        // so derive it from the section length instead of assuming it — receipts
703        // written under a smaller or larger K decode without over-reading.
704        if tail >= 16 {
705            let kinds_on_disk = (tail - 16) / 8;
706            let store = kinds_on_disk.min(crate::multigas::NUM_RESOURCE_KIND);
707            let mut gas = [0u64; crate::multigas::NUM_RESOURCE_KIND];
708            for slot in gas.iter_mut().take(store) {
709                *slot = slice.get_u64();
710            }
711            // Consume any kinds beyond what this build's array holds (a tail
712            // written under a larger NUM_RESOURCE_KIND) to keep the cursor aligned.
713            for _ in store..kinds_on_disk {
714                slice.advance(8);
715            }
716            let total = slice.get_u64();
717            let refund = slice.get_u64();
718            receipt.multi_gas_used = MultiGas::from_raw(gas, total, refund);
719        }
720
721        (receipt, slice)
722    }
723}
724
725// ---------------------------------------------------------------------------
726// Compress / Decompress — delegates to Compact for database storage
727// ---------------------------------------------------------------------------
728
729impl reth_db_api::table::Compress for ArbReceipt {
730    type Compressed = Vec<u8>;
731
732    fn compress_to_buf<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) {
733        let _ = reth_codecs::Compact::to_compact(self, buf);
734    }
735}
736
737impl reth_db_api::table::Decompress for ArbReceipt {
738    fn decompress(value: &[u8]) -> Result<Self, reth_db_api::DatabaseError> {
739        let (obj, _) = reth_codecs::Compact::from_compact(value, value.len());
740        Ok(obj)
741    }
742}
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747
748    fn alloy_receipt() -> AlloyReceipt {
749        AlloyReceipt {
750            status: Eip658Value::Eip658(true),
751            cumulative_gas_used: 21_000,
752            logs: Vec::new(),
753        }
754    }
755
756    #[test]
757    fn as_receipt_returns_none_for_deposit() {
758        let kind = ArbReceiptKind::Deposit(ArbDepositReceipt::default());
759        assert!(kind.as_receipt().is_none());
760    }
761
762    #[test]
763    fn as_receipt_returns_some_for_non_deposit() {
764        let kind = ArbReceiptKind::Legacy(alloy_receipt());
765        assert!(kind.as_receipt().is_some());
766    }
767
768    #[test]
769    fn tx_receipt_trait_methods_handle_deposit() {
770        let receipt = ArbReceipt::new(ArbReceiptKind::Deposit(ArbDepositReceipt::default()));
771        assert!(receipt.status());
772        assert_eq!(receipt.status_or_post_state(), Eip658Value::Eip658(true));
773        assert_eq!(receipt.bloom(), Bloom::ZERO);
774        assert_eq!(receipt.cumulative_gas_used(), 0);
775        assert!(receipt.logs().is_empty());
776    }
777
778    #[test]
779    fn tx_receipt_trait_methods_delegate_for_non_deposit() {
780        let receipt = ArbReceipt::new(ArbReceiptKind::Legacy(alloy_receipt()));
781        assert!(receipt.status());
782        assert_eq!(receipt.cumulative_gas_used(), 21_000);
783    }
784
785    const NUM: usize = crate::multigas::NUM_RESOURCE_KIND;
786
787    /// Encode a receipt with an arbitrary `gas.len()` multi-gas width, mirroring
788    /// `to_compact` so tests can replay tails written under a different
789    /// `NUM_RESOURCE_KIND`.
790    fn compact_with_kinds(receipt: &ArbReceipt, gas: &[u64], total: u64, refund: u64) -> Vec<u8> {
791        use bytes::BufMut;
792        let mut body = Vec::new();
793        receipt.encode_2718(&mut body);
794        let mut buf = Vec::new();
795        buf.put_u32(body.len() as u32);
796        buf.put_slice(&body);
797        buf.put_u64(receipt.gas_used_for_l1);
798        for &g in gas {
799            buf.put_u64(g);
800        }
801        buf.put_u64(total);
802        buf.put_u64(refund);
803        buf
804    }
805
806    fn decode(buf: &[u8]) -> ArbReceipt {
807        let (receipt, rest) = <ArbReceipt as reth_codecs::Compact>::from_compact(buf, buf.len());
808        assert!(rest.is_empty(), "{} trailing bytes left", rest.len());
809        receipt
810    }
811
812    #[test]
813    fn from_compact_round_trips_current_kinds() {
814        let mut receipt = ArbReceipt::new(ArbReceiptKind::Eip1559(alloy_receipt()));
815        receipt.gas_used_for_l1 = 4242;
816        let gas: [u64; NUM] = std::array::from_fn(|i| (i as u64 + 1) * 10);
817        receipt.multi_gas_used = MultiGas::from_raw(gas, 999, 7);
818        let mut buf = Vec::new();
819        reth_codecs::Compact::to_compact(&receipt, &mut buf);
820        let decoded = decode(&buf);
821        assert_eq!(decoded.gas_used_for_l1, 4242);
822        assert_eq!(decoded.multi_gas_used, receipt.multi_gas_used);
823    }
824
825    #[test]
826    fn from_compact_pads_receipt_with_fewer_kinds() {
827        // An 8-kind, 80-byte tail written before NUM_RESOURCE_KIND grew — the
828        // case that previously over-read and panicked.
829        let mut receipt = ArbReceipt::new(ArbReceiptKind::Eip1559(alloy_receipt()));
830        receipt.gas_used_for_l1 = 11;
831        let gas: Vec<u64> = (1..=8).map(|n| n * 100).collect();
832        let decoded = decode(&compact_with_kinds(&receipt, &gas, 800, 5));
833        let mut expected = [0u64; NUM];
834        expected[..gas.len()].copy_from_slice(&gas);
835        assert_eq!(decoded.gas_used_for_l1, 11);
836        assert_eq!(decoded.multi_gas_used, MultiGas::from_raw(expected, 800, 5));
837    }
838
839    // Canonical Arbitrum Sepolia block 1 auto-redeem retry tx (0x873c5ee3…);
840    // fields fetched from a real node. Proves our typed-tx hash preimage matches.
841    #[test]
842    fn canonical_block1_retry_tx_hash() {
843        use alloy_primitives::{Bytes, U256, address, b256, hex, keccak256};
844        use arb_alloy_consensus::tx::ArbRetryTx;
845        let tx = ArbRetryTx {
846            chain_id: U256::from(421614u64),
847            nonce: 0,
848            from: address!("b8787d8f23e176a5d32135d746b69886e03313be"),
849            gas_fee_cap: U256::from(0x5f5e100u64),
850            gas: 100_000,
851            to: Some(address!("3fab184622dc19b6109349b94811493bf2a45362")),
852            value: U256::from(0x2386f26fc10000u64),
853            data: Bytes::new(),
854            ticket_id: b256!("13cb79b086a427f3db7ebe6ec2bb90a806a3b0368ecee6020144f352e37dbdf6"),
855            refund_to: address!("11155ca9bbf7be58e27f3309e629c847996b43c8"),
856            max_refund: U256::from(0xb0e85efeab8u64),
857            submission_fee_refund: U256::from(0x1f6377d4ab8u64),
858        };
859        let mut enc = Vec::new();
860        enc.push(ArbTxType::ArbitrumRetryTx.as_u8());
861        alloy_rlp::Encodable::encode(&tx, &mut enc);
862        assert_eq!(
863            keccak256(&enc),
864            b256!("873c5ee3092c40336006808e249293bf5f4cb3235077a74cac9cafa7cf73cb8b"),
865            "retry-tx hash mismatch; our preimage = 0x{}",
866            hex::encode(&enc)
867        );
868    }
869
870    // Canonical Arbitrum Sepolia block 1 SubmitRetryable (0x13cb79b0…) = the
871    // ticketId that feeds the retry tx. Proves our type-0x69 hash preimage.
872    #[test]
873    fn canonical_block1_submit_retryable_hash() {
874        use alloy_primitives::{Bytes, U256, address, b256, hex, keccak256};
875        use arb_alloy_consensus::tx::ArbSubmitRetryableTx;
876        let tx = ArbSubmitRetryableTx {
877            chain_id: U256::from(421614u64),
878            request_id: b256!("0000000000000000000000000000000000000000000000000000000000000001"),
879            from: address!("b8787d8f23e176a5d32135d746b69886e03313be"),
880            l1_base_fee: U256::from(0x5bd57bd9u64),
881            deposit_value: U256::from(0x23e3dbb7b88ab8u64),
882            gas_fee_cap: U256::from(0x3b9aca00u64),
883            gas: 100_000,
884            retry_to: Some(address!("3fab184622dc19b6109349b94811493bf2a45362")),
885            retry_value: U256::from(0x2386f26fc10000u64),
886            beneficiary: address!("11155ca9bbf7be58e27f3309e629c847996b43c8"),
887            max_submission_fee: U256::from(0x1f6377d4ab8u64),
888            fee_refund_addr: address!("11155ca9bbf7be58e27f3309e629c847996b43c8"),
889            retry_data: Bytes::new(),
890        };
891        let mut enc = Vec::new();
892        enc.push(ArbTxType::ArbitrumSubmitRetryableTx.as_u8());
893        alloy_rlp::Encodable::encode(&tx, &mut enc);
894        assert_eq!(
895            keccak256(&enc),
896            b256!("13cb79b086a427f3db7ebe6ec2bb90a806a3b0368ecee6020144f352e37dbdf6"),
897            "submit-retryable hash mismatch; our preimage = 0x{}",
898            hex::encode(&enc)
899        );
900    }
901
902    #[test]
903    fn from_compact_truncates_receipt_with_more_kinds() {
904        // A tail written under a larger NUM_RESOURCE_KIND: extra kinds are
905        // consumed (no over-read) but dropped.
906        let mut receipt = ArbReceipt::new(ArbReceiptKind::Eip1559(alloy_receipt()));
907        receipt.gas_used_for_l1 = 22;
908        let gas: Vec<u64> = (1..=(NUM as u64 + 1)).map(|n| n * 100).collect();
909        let decoded = decode(&compact_with_kinds(&receipt, &gas, 1000, 9));
910        let mut expected = [0u64; NUM];
911        expected.copy_from_slice(&gas[..NUM]);
912        assert_eq!(
913            decoded.multi_gas_used,
914            MultiGas::from_raw(expected, 1000, 9)
915        );
916    }
917}