arb_primitives/
signed_tx.rs

1use alloc::vec::Vec;
2use core::{
3    hash::{Hash, Hasher},
4    ops::Deref,
5};
6
7use alloy_consensus::{
8    SignableTransaction, Transaction as ConsensusTx, TxLegacy, Typed2718,
9    transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx, TxHashRef},
10};
11use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718};
12use alloy_primitives::{Address, B256, Bytes, Signature, TxHash, TxKind, U256, keccak256};
13use alloy_rlp::{Decodable, Encodable};
14use arb_alloy_consensus::tx::{
15    ArbContractTx, ArbDepositTx, ArbInternalTx, ArbRetryTx, ArbSubmitRetryableTx, ArbTxType,
16    ArbUnsignedTx,
17};
18use reth_primitives_traits::{
19    InMemorySize, SignedTransaction,
20    crypto::secp256k1::{recover_signer, recover_signer_unchecked},
21};
22
23/// Internal ArbOS address used as sender for internal transactions.
24const ARBOS_ADDRESS: Address =
25    alloy_primitives::address!("00000000000000000000000000000000000A4B05");
26
27/// Retryable precompile address (0x6e).
28const RETRYABLE_ADDRESS: Address =
29    alloy_primitives::address!("000000000000000000000000000000000000006e");
30
31/// Wraps all supported transaction types (standard Ethereum + Arbitrum-specific).
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum ArbTypedTransaction {
34    Deposit(ArbDepositTx),
35    Unsigned(ArbUnsignedTx),
36    Contract(ArbContractTx),
37    Retry(ArbRetryTx),
38    SubmitRetryable(ArbSubmitRetryableTx),
39    Internal(ArbInternalTx),
40
41    Legacy(TxLegacy),
42    Eip2930(alloy_consensus::TxEip2930),
43    Eip1559(alloy_consensus::TxEip1559),
44    Eip4844(alloy_consensus::TxEip4844),
45    Eip7702(alloy_consensus::TxEip7702),
46}
47
48/// Discriminant for transaction type classification.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum ArbTxTypeLocal {
51    Deposit,
52    Unsigned,
53    Contract,
54    Retry,
55    SubmitRetryable,
56    Internal,
57    Legacy,
58    Eip2930,
59    Eip1559,
60    Eip4844,
61    Eip7702,
62}
63
64impl ArbTxTypeLocal {
65    /// Convert to the EIP-2718 type byte.
66    pub fn as_u8(self) -> u8 {
67        match self {
68            Self::Legacy => 0x00,
69            Self::Eip2930 => 0x01,
70            Self::Eip1559 => 0x02,
71            Self::Eip4844 => 0x03,
72            Self::Eip7702 => 0x04,
73            Self::Deposit => ArbTxType::ArbitrumDepositTx.as_u8(),
74            Self::Unsigned => ArbTxType::ArbitrumUnsignedTx.as_u8(),
75            Self::Contract => ArbTxType::ArbitrumContractTx.as_u8(),
76            Self::Retry => ArbTxType::ArbitrumRetryTx.as_u8(),
77            Self::SubmitRetryable => ArbTxType::ArbitrumSubmitRetryableTx.as_u8(),
78            Self::Internal => ArbTxType::ArbitrumInternalTx.as_u8(),
79        }
80    }
81}
82
83impl Typed2718 for ArbTxTypeLocal {
84    fn is_legacy(&self) -> bool {
85        matches!(self, Self::Legacy)
86    }
87
88    fn ty(&self) -> u8 {
89        self.as_u8()
90    }
91}
92
93impl alloy_consensus::TransactionEnvelope for ArbTransactionSigned {
94    type TxType = ArbTxTypeLocal;
95
96    fn tx_type(&self) -> Self::TxType {
97        match &self.transaction {
98            ArbTypedTransaction::Legacy(_) => ArbTxTypeLocal::Legacy,
99            ArbTypedTransaction::Eip2930(_) => ArbTxTypeLocal::Eip2930,
100            ArbTypedTransaction::Eip1559(_) => ArbTxTypeLocal::Eip1559,
101            ArbTypedTransaction::Eip4844(_) => ArbTxTypeLocal::Eip4844,
102            ArbTypedTransaction::Eip7702(_) => ArbTxTypeLocal::Eip7702,
103            ArbTypedTransaction::Deposit(_) => ArbTxTypeLocal::Deposit,
104            ArbTypedTransaction::Unsigned(_) => ArbTxTypeLocal::Unsigned,
105            ArbTypedTransaction::Contract(_) => ArbTxTypeLocal::Contract,
106            ArbTypedTransaction::Retry(_) => ArbTxTypeLocal::Retry,
107            ArbTypedTransaction::SubmitRetryable(_) => ArbTxTypeLocal::SubmitRetryable,
108            ArbTypedTransaction::Internal(_) => ArbTxTypeLocal::Internal,
109        }
110    }
111}
112
113/// Signed Arbitrum transaction with lazy hash caching.
114#[derive(Clone, Debug, Eq)]
115pub struct ArbTransactionSigned {
116    hash: reth_primitives_traits::sync::OnceLock<TxHash>,
117    signature: Signature,
118    transaction: ArbTypedTransaction,
119    input_cache: reth_primitives_traits::sync::OnceLock<Bytes>,
120    sender_cache: reth_primitives_traits::sync::OnceLock<Address>,
121    /// Cached poster calldata units, packed as `(level as u64) << 56 | (units &
122    /// 0x00FF_FFFF_FFFF_FFFF)`. Brotli compression level is stable across a block, so this
123    /// cache avoids repeated brotli compression of the same tx bytes within that block.
124    poster_units_cache: reth_primitives_traits::sync::OnceLock<u64>,
125}
126
127impl Deref for ArbTransactionSigned {
128    type Target = ArbTypedTransaction;
129    fn deref(&self) -> &Self::Target {
130        &self.transaction
131    }
132}
133
134impl ArbTransactionSigned {
135    pub fn new(transaction: ArbTypedTransaction, signature: Signature, hash: B256) -> Self {
136        Self {
137            hash: hash.into(),
138            signature,
139            transaction,
140            input_cache: Default::default(),
141            sender_cache: Default::default(),
142            poster_units_cache: Default::default(),
143        }
144    }
145
146    pub fn new_unhashed(transaction: ArbTypedTransaction, signature: Signature) -> Self {
147        Self {
148            hash: Default::default(),
149            signature,
150            transaction,
151            input_cache: Default::default(),
152            sender_cache: Default::default(),
153            poster_units_cache: Default::default(),
154        }
155    }
156
157    /// Construct from a signed Ethereum envelope (standard tx types only).
158    pub fn from_envelope(
159        envelope: alloy_consensus::EthereumTxEnvelope<alloy_consensus::TxEip4844>,
160    ) -> Self {
161        use alloy_consensus::EthereumTxEnvelope;
162        match envelope {
163            EthereumTxEnvelope::Legacy(signed) => {
164                let (tx, sig, hash) = signed.into_parts();
165                Self::new(ArbTypedTransaction::Legacy(tx), sig, hash)
166            }
167            EthereumTxEnvelope::Eip2930(signed) => {
168                let (tx, sig, hash) = signed.into_parts();
169                Self::new(ArbTypedTransaction::Eip2930(tx), sig, hash)
170            }
171            EthereumTxEnvelope::Eip1559(signed) => {
172                let (tx, sig, hash) = signed.into_parts();
173                Self::new(ArbTypedTransaction::Eip1559(tx), sig, hash)
174            }
175            EthereumTxEnvelope::Eip4844(signed) => {
176                let (tx, sig, hash) = signed.into_parts();
177                Self::new(ArbTypedTransaction::Eip4844(tx), sig, hash)
178            }
179            EthereumTxEnvelope::Eip7702(signed) => {
180                let (tx, sig, hash) = signed.into_parts();
181                Self::new(ArbTypedTransaction::Eip7702(tx), sig, hash)
182            }
183        }
184    }
185
186    pub const fn signature(&self) -> &Signature {
187        &self.signature
188    }
189
190    /// Returns the inner typed transaction.
191    pub fn inner(&self) -> &ArbTypedTransaction {
192        &self.transaction
193    }
194
195    /// Consume self and return (transaction, signature, hash).
196    pub fn split(self) -> (ArbTypedTransaction, Signature, B256) {
197        let hash = *self.hash.get_or_init(|| self.compute_hash());
198        (self.transaction, self.signature, hash)
199    }
200
201    pub const fn tx_type(&self) -> ArbTxTypeLocal {
202        match &self.transaction {
203            ArbTypedTransaction::Deposit(_) => ArbTxTypeLocal::Deposit,
204            ArbTypedTransaction::Unsigned(_) => ArbTxTypeLocal::Unsigned,
205            ArbTypedTransaction::Contract(_) => ArbTxTypeLocal::Contract,
206            ArbTypedTransaction::Retry(_) => ArbTxTypeLocal::Retry,
207            ArbTypedTransaction::SubmitRetryable(_) => ArbTxTypeLocal::SubmitRetryable,
208            ArbTypedTransaction::Internal(_) => ArbTxTypeLocal::Internal,
209            ArbTypedTransaction::Legacy(_) => ArbTxTypeLocal::Legacy,
210            ArbTypedTransaction::Eip2930(_) => ArbTxTypeLocal::Eip2930,
211            ArbTypedTransaction::Eip1559(_) => ArbTxTypeLocal::Eip1559,
212            ArbTypedTransaction::Eip4844(_) => ArbTxTypeLocal::Eip4844,
213            ArbTypedTransaction::Eip7702(_) => ArbTxTypeLocal::Eip7702,
214        }
215    }
216
217    fn compute_hash(&self) -> B256 {
218        keccak256(self.encoded_2718())
219    }
220
221    fn zero_sig() -> Signature {
222        Signature::new(U256::ZERO, U256::ZERO, false)
223    }
224
225    /// Returns the cached poster calldata units for the given brotli compression level,
226    /// computing them via `compute` on first call. The cache is valid for a single level
227    /// — if called with a different level, returns the freshly computed value without
228    /// updating the cache (callers should avoid mixing levels per-tx).
229    pub fn poster_units_cached<F: FnOnce() -> u64>(&self, level: u64, compute: F) -> u64 {
230        if let Some(&entry) = self.poster_units_cache.get() {
231            let (cached_level, cached_units) = unpack_poster_units(entry);
232            if cached_level == level {
233                return cached_units;
234            }
235            return compute();
236        }
237        let units = compute();
238        let _ = self.poster_units_cache.set(pack_poster_units(level, units));
239        units
240    }
241}
242
243#[inline]
244fn pack_poster_units(level: u64, units: u64) -> u64 {
245    ((level & 0xFF) << 56) | (units & 0x00FF_FFFF_FFFF_FFFF)
246}
247
248#[inline]
249fn unpack_poster_units(packed: u64) -> (u64, u64) {
250    let level = (packed >> 56) & 0xFF;
251    let units = packed & 0x00FF_FFFF_FFFF_FFFF;
252    (level, units)
253}
254
255// ---------------------------------------------------------------------------
256// Hash / PartialEq — identity by tx hash
257// ---------------------------------------------------------------------------
258
259impl Hash for ArbTransactionSigned {
260    fn hash<H: Hasher>(&self, state: &mut H) {
261        self.tx_hash().hash(state)
262    }
263}
264
265impl PartialEq for ArbTransactionSigned {
266    fn eq(&self, other: &Self) -> bool {
267        self.tx_hash() == other.tx_hash()
268    }
269}
270
271impl InMemorySize for ArbTransactionSigned {
272    fn size(&self) -> usize {
273        core::mem::size_of::<TxHash>() + core::mem::size_of::<Signature>()
274    }
275}
276
277// ---------------------------------------------------------------------------
278// TxHashRef — lazy hash initialization
279// ---------------------------------------------------------------------------
280
281impl TxHashRef for ArbTransactionSigned {
282    fn tx_hash(&self) -> &TxHash {
283        self.hash.get_or_init(|| self.compute_hash())
284    }
285}
286
287// ---------------------------------------------------------------------------
288// SignedTransaction
289// ---------------------------------------------------------------------------
290
291impl SignedTransaction for ArbTransactionSigned {
292    fn recalculate_hash(&self) -> B256 {
293        keccak256(self.encoded_2718())
294    }
295}
296
297// ---------------------------------------------------------------------------
298// SignerRecoverable
299// ---------------------------------------------------------------------------
300
301impl ArbTransactionSigned {
302    fn recover_signer_inner(
303        &self,
304        strict: bool,
305    ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
306        match &self.transaction {
307            ArbTypedTransaction::Deposit(tx) => Ok(tx.from),
308            ArbTypedTransaction::Unsigned(tx) => Ok(tx.from),
309            ArbTypedTransaction::Contract(tx) => Ok(tx.from),
310            ArbTypedTransaction::Retry(tx) => Ok(tx.from),
311            ArbTypedTransaction::SubmitRetryable(tx) => Ok(tx.from),
312            ArbTypedTransaction::Internal(_) => Ok(ARBOS_ADDRESS),
313            ArbTypedTransaction::Legacy(tx) => {
314                let mut buf = Vec::new();
315                tx.encode_for_signing(&mut buf);
316                if strict {
317                    recover_signer(&self.signature, keccak256(&buf))
318                } else {
319                    recover_signer_unchecked(&self.signature, keccak256(&buf))
320                }
321            }
322            ArbTypedTransaction::Eip2930(tx) => {
323                let mut buf = Vec::new();
324                tx.encode_for_signing(&mut buf);
325                if strict {
326                    recover_signer(&self.signature, keccak256(&buf))
327                } else {
328                    recover_signer_unchecked(&self.signature, keccak256(&buf))
329                }
330            }
331            ArbTypedTransaction::Eip1559(tx) => {
332                let mut buf = Vec::new();
333                tx.encode_for_signing(&mut buf);
334                if strict {
335                    recover_signer(&self.signature, keccak256(&buf))
336                } else {
337                    recover_signer_unchecked(&self.signature, keccak256(&buf))
338                }
339            }
340            ArbTypedTransaction::Eip4844(tx) => {
341                let mut buf = Vec::new();
342                tx.encode_for_signing(&mut buf);
343                if strict {
344                    recover_signer(&self.signature, keccak256(&buf))
345                } else {
346                    recover_signer_unchecked(&self.signature, keccak256(&buf))
347                }
348            }
349            ArbTypedTransaction::Eip7702(tx) => {
350                let mut buf = Vec::new();
351                tx.encode_for_signing(&mut buf);
352                if strict {
353                    recover_signer(&self.signature, keccak256(&buf))
354                } else {
355                    recover_signer_unchecked(&self.signature, keccak256(&buf))
356                }
357            }
358        }
359    }
360}
361
362impl alloy_consensus::transaction::SignerRecoverable for ArbTransactionSigned {
363    fn recover_signer(
364        &self,
365    ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
366        if let Some(addr) = self.sender_cache.get() {
367            return Ok(*addr);
368        }
369        let addr = self.recover_signer_inner(true)?;
370        let _ = self.sender_cache.set(addr);
371        Ok(addr)
372    }
373
374    fn recover_signer_unchecked(
375        &self,
376    ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
377        if let Some(addr) = self.sender_cache.get() {
378            return Ok(*addr);
379        }
380        let addr = self.recover_signer_inner(false)?;
381        let _ = self.sender_cache.set(addr);
382        Ok(addr)
383    }
384}
385
386// ---------------------------------------------------------------------------
387// Typed2718
388// ---------------------------------------------------------------------------
389
390impl Typed2718 for ArbTransactionSigned {
391    fn is_legacy(&self) -> bool {
392        matches!(self.transaction, ArbTypedTransaction::Legacy(_))
393    }
394
395    fn ty(&self) -> u8 {
396        match &self.transaction {
397            ArbTypedTransaction::Legacy(_) => 0u8,
398            ArbTypedTransaction::Deposit(_) => ArbTxType::ArbitrumDepositTx.as_u8(),
399            ArbTypedTransaction::Unsigned(_) => ArbTxType::ArbitrumUnsignedTx.as_u8(),
400            ArbTypedTransaction::Contract(_) => ArbTxType::ArbitrumContractTx.as_u8(),
401            ArbTypedTransaction::Retry(_) => ArbTxType::ArbitrumRetryTx.as_u8(),
402            ArbTypedTransaction::SubmitRetryable(_) => ArbTxType::ArbitrumSubmitRetryableTx.as_u8(),
403            ArbTypedTransaction::Internal(_) => ArbTxType::ArbitrumInternalTx.as_u8(),
404            ArbTypedTransaction::Eip2930(_) => 0x01,
405            ArbTypedTransaction::Eip1559(_) => 0x02,
406            ArbTypedTransaction::Eip4844(_) => 0x03,
407            ArbTypedTransaction::Eip7702(_) => 0x04,
408        }
409    }
410}
411
412// ---------------------------------------------------------------------------
413// IsTyped2718
414// ---------------------------------------------------------------------------
415
416impl IsTyped2718 for ArbTransactionSigned {
417    fn is_type(type_id: u8) -> bool {
418        // Standard Ethereum types.
419        matches!(type_id, 0x01..=0x04) || ArbTxType::from_u8(type_id).is_ok()
420    }
421}
422
423// ---------------------------------------------------------------------------
424// Encodable2718
425// ---------------------------------------------------------------------------
426
427impl Encodable2718 for ArbTransactionSigned {
428    fn type_flag(&self) -> Option<u8> {
429        if self.is_legacy() {
430            None
431        } else {
432            Some(self.ty())
433        }
434    }
435
436    fn encode_2718_len(&self) -> usize {
437        match &self.transaction {
438            ArbTypedTransaction::Legacy(tx) => tx.eip2718_encoded_length(&self.signature),
439            ArbTypedTransaction::Deposit(tx) => tx.length() + 1,
440            ArbTypedTransaction::Unsigned(tx) => tx.length() + 1,
441            ArbTypedTransaction::Contract(tx) => tx.length() + 1,
442            ArbTypedTransaction::Retry(tx) => tx.length() + 1,
443            ArbTypedTransaction::SubmitRetryable(tx) => tx.length() + 1,
444            ArbTypedTransaction::Internal(tx) => tx.length() + 1,
445            ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encoded_length(&self.signature),
446            ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encoded_length(&self.signature),
447            ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encoded_length(&self.signature),
448            ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encoded_length(&self.signature),
449        }
450    }
451
452    fn encode_2718(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
453        match &self.transaction {
454            ArbTypedTransaction::Legacy(tx) => tx.eip2718_encode(&self.signature, out),
455            ArbTypedTransaction::Deposit(tx) => {
456                out.put_u8(ArbTxType::ArbitrumDepositTx.as_u8());
457                tx.encode(out);
458            }
459            ArbTypedTransaction::Unsigned(tx) => {
460                out.put_u8(ArbTxType::ArbitrumUnsignedTx.as_u8());
461                tx.encode(out);
462            }
463            ArbTypedTransaction::Contract(tx) => {
464                out.put_u8(ArbTxType::ArbitrumContractTx.as_u8());
465                tx.encode(out);
466            }
467            ArbTypedTransaction::Retry(tx) => {
468                out.put_u8(ArbTxType::ArbitrumRetryTx.as_u8());
469                tx.encode(out);
470            }
471            ArbTypedTransaction::SubmitRetryable(tx) => {
472                out.put_u8(ArbTxType::ArbitrumSubmitRetryableTx.as_u8());
473                tx.encode(out);
474            }
475            ArbTypedTransaction::Internal(tx) => {
476                out.put_u8(ArbTxType::ArbitrumInternalTx.as_u8());
477                tx.encode(out);
478            }
479            ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encode(&self.signature, out),
480            ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encode(&self.signature, out),
481            ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encode(&self.signature, out),
482            ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encode(&self.signature, out),
483        }
484    }
485}
486
487// ---------------------------------------------------------------------------
488// Decodable2718
489// ---------------------------------------------------------------------------
490
491impl Decodable2718 for ArbTransactionSigned {
492    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
493        // Try Arbitrum-specific types first.
494        if let Ok(kind) = ArbTxType::from_u8(ty) {
495            return Ok(match kind {
496                ArbTxType::ArbitrumDepositTx => {
497                    let tx = ArbDepositTx::decode(buf)?;
498                    Self::new_unhashed(ArbTypedTransaction::Deposit(tx), Self::zero_sig())
499                }
500                ArbTxType::ArbitrumUnsignedTx => {
501                    let tx = ArbUnsignedTx::decode(buf)?;
502                    Self::new_unhashed(ArbTypedTransaction::Unsigned(tx), Self::zero_sig())
503                }
504                ArbTxType::ArbitrumContractTx => {
505                    let tx = ArbContractTx::decode(buf)?;
506                    Self::new_unhashed(ArbTypedTransaction::Contract(tx), Self::zero_sig())
507                }
508                ArbTxType::ArbitrumRetryTx => {
509                    let tx = ArbRetryTx::decode(buf)?;
510                    Self::new_unhashed(ArbTypedTransaction::Retry(tx), Self::zero_sig())
511                }
512                ArbTxType::ArbitrumSubmitRetryableTx => {
513                    let tx = ArbSubmitRetryableTx::decode(buf)?;
514                    Self::new_unhashed(ArbTypedTransaction::SubmitRetryable(tx), Self::zero_sig())
515                }
516                ArbTxType::ArbitrumInternalTx => {
517                    let tx = ArbInternalTx::decode(buf)?;
518                    Self::new_unhashed(ArbTypedTransaction::Internal(tx), Self::zero_sig())
519                }
520                ArbTxType::ArbitrumLegacyTx => return Err(Eip2718Error::UnexpectedType(0x78)),
521            });
522        }
523
524        // Standard Ethereum typed transactions.
525        match alloy_consensus::TxType::try_from(ty).map_err(|_| Eip2718Error::UnexpectedType(ty))? {
526            alloy_consensus::TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
527            alloy_consensus::TxType::Eip2930 => {
528                let (tx, sig) = alloy_consensus::TxEip2930::rlp_decode_with_signature(buf)?;
529                Ok(Self::new_unhashed(ArbTypedTransaction::Eip2930(tx), sig))
530            }
531            alloy_consensus::TxType::Eip1559 => {
532                let (tx, sig) = alloy_consensus::TxEip1559::rlp_decode_with_signature(buf)?;
533                Ok(Self::new_unhashed(ArbTypedTransaction::Eip1559(tx), sig))
534            }
535            alloy_consensus::TxType::Eip4844 => {
536                let (tx, sig) = alloy_consensus::TxEip4844::rlp_decode_with_signature(buf)?;
537                Ok(Self::new_unhashed(ArbTypedTransaction::Eip4844(tx), sig))
538            }
539            alloy_consensus::TxType::Eip7702 => {
540                let (tx, sig) = alloy_consensus::TxEip7702::rlp_decode_with_signature(buf)?;
541                Ok(Self::new_unhashed(ArbTypedTransaction::Eip7702(tx), sig))
542            }
543        }
544    }
545
546    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
547        let (tx, sig, hash) = TxLegacy::rlp_decode_signed(buf)?.into_parts();
548        let signed_tx = Self::new_unhashed(ArbTypedTransaction::Legacy(tx), sig);
549        signed_tx.hash.get_or_init(|| hash);
550        Ok(signed_tx)
551    }
552}
553
554// ---------------------------------------------------------------------------
555// Encodable / Decodable (RLP network encoding)
556// ---------------------------------------------------------------------------
557
558impl Encodable for ArbTransactionSigned {
559    fn encode(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
560        self.network_encode(out);
561    }
562    fn length(&self) -> usize {
563        let mut payload_length = self.encode_2718_len();
564        if !self.is_legacy() {
565            payload_length += alloy_rlp::Header {
566                list: false,
567                payload_length,
568            }
569            .length();
570        }
571        payload_length
572    }
573}
574
575impl Decodable for ArbTransactionSigned {
576    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
577        Self::network_decode(buf).map_err(Into::into)
578    }
579}
580
581// ---------------------------------------------------------------------------
582// Transaction (alloy_consensus::Transaction)
583// ---------------------------------------------------------------------------
584
585impl ConsensusTx for ArbTransactionSigned {
586    fn chain_id(&self) -> Option<u64> {
587        match &self.transaction {
588            ArbTypedTransaction::Legacy(tx) => tx.chain_id,
589            ArbTypedTransaction::Deposit(tx) => Some(tx.chain_id.to::<u64>()),
590            ArbTypedTransaction::Unsigned(tx) => Some(tx.chain_id.to::<u64>()),
591            ArbTypedTransaction::Contract(tx) => Some(tx.chain_id.to::<u64>()),
592            ArbTypedTransaction::Retry(tx) => Some(tx.chain_id.to::<u64>()),
593            ArbTypedTransaction::SubmitRetryable(tx) => Some(tx.chain_id.to::<u64>()),
594            ArbTypedTransaction::Internal(tx) => Some(tx.chain_id.to::<u64>()),
595            ArbTypedTransaction::Eip2930(tx) => Some(tx.chain_id),
596            ArbTypedTransaction::Eip1559(tx) => Some(tx.chain_id),
597            ArbTypedTransaction::Eip4844(tx) => Some(tx.chain_id),
598            ArbTypedTransaction::Eip7702(tx) => Some(tx.chain_id),
599        }
600    }
601
602    fn nonce(&self) -> u64 {
603        match &self.transaction {
604            ArbTypedTransaction::Legacy(tx) => tx.nonce,
605            ArbTypedTransaction::Deposit(_) => 0,
606            ArbTypedTransaction::Unsigned(tx) => tx.nonce,
607            ArbTypedTransaction::Contract(_) => 0,
608            ArbTypedTransaction::Retry(tx) => tx.nonce,
609            ArbTypedTransaction::SubmitRetryable(_) => 0,
610            ArbTypedTransaction::Internal(_) => 0,
611            ArbTypedTransaction::Eip2930(tx) => tx.nonce,
612            ArbTypedTransaction::Eip1559(tx) => tx.nonce,
613            ArbTypedTransaction::Eip4844(tx) => tx.nonce,
614            ArbTypedTransaction::Eip7702(tx) => tx.nonce,
615        }
616    }
617
618    fn gas_limit(&self) -> u64 {
619        match &self.transaction {
620            ArbTypedTransaction::Legacy(tx) => tx.gas_limit,
621            ArbTypedTransaction::Deposit(_) => 0,
622            ArbTypedTransaction::Unsigned(tx) => tx.gas,
623            ArbTypedTransaction::Contract(tx) => tx.gas,
624            ArbTypedTransaction::Retry(tx) => tx.gas,
625            ArbTypedTransaction::SubmitRetryable(tx) => tx.gas,
626            ArbTypedTransaction::Internal(_) => 0,
627            ArbTypedTransaction::Eip2930(tx) => tx.gas_limit,
628            ArbTypedTransaction::Eip1559(tx) => tx.gas_limit,
629            ArbTypedTransaction::Eip4844(tx) => tx.gas_limit,
630            ArbTypedTransaction::Eip7702(tx) => tx.gas_limit,
631        }
632    }
633
634    fn gas_price(&self) -> Option<u128> {
635        match &self.transaction {
636            ArbTypedTransaction::Legacy(tx) => Some(tx.gas_price),
637            ArbTypedTransaction::Eip2930(tx) => Some(tx.gas_price),
638            _ => None,
639        }
640    }
641
642    fn max_fee_per_gas(&self) -> u128 {
643        match &self.transaction {
644            ArbTypedTransaction::Legacy(tx) => tx.gas_price,
645            ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
646            ArbTypedTransaction::Unsigned(tx) => tx.gas_fee_cap.to::<u128>(),
647            ArbTypedTransaction::Contract(tx) => tx.gas_fee_cap.to::<u128>(),
648            ArbTypedTransaction::Retry(tx) => tx.gas_fee_cap.to::<u128>(),
649            ArbTypedTransaction::SubmitRetryable(tx) => tx.gas_fee_cap.to::<u128>(),
650            ArbTypedTransaction::Eip1559(tx) => tx.max_fee_per_gas,
651            ArbTypedTransaction::Eip4844(tx) => tx.max_fee_per_gas,
652            ArbTypedTransaction::Eip7702(tx) => tx.max_fee_per_gas,
653            _ => 0,
654        }
655    }
656
657    fn max_priority_fee_per_gas(&self) -> Option<u128> {
658        match &self.transaction {
659            ArbTypedTransaction::Eip1559(tx) => Some(tx.max_priority_fee_per_gas),
660            ArbTypedTransaction::Eip4844(tx) => Some(tx.max_priority_fee_per_gas),
661            ArbTypedTransaction::Eip7702(tx) => Some(tx.max_priority_fee_per_gas),
662            // Legacy / 2930 / Arbitrum-internal types have no priority fee.
663            _ => None,
664        }
665    }
666
667    fn max_fee_per_blob_gas(&self) -> Option<u128> {
668        match &self.transaction {
669            ArbTypedTransaction::Eip4844(tx) => Some(tx.max_fee_per_blob_gas),
670            _ => None,
671        }
672    }
673
674    fn priority_fee_or_price(&self) -> u128 {
675        match self.max_priority_fee_per_gas() {
676            Some(p) => p,
677            None => self.gas_price().unwrap_or(0),
678        }
679    }
680
681    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
682        let bf = base_fee.unwrap_or(0) as u128;
683        match &self.transaction {
684            ArbTypedTransaction::Legacy(tx) => tx.gas_price,
685            ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
686            ArbTypedTransaction::Eip1559(tx) => core::cmp::min(
687                tx.max_fee_per_gas,
688                bf.saturating_add(tx.max_priority_fee_per_gas),
689            ),
690            ArbTypedTransaction::Eip7702(tx) => core::cmp::min(
691                tx.max_fee_per_gas,
692                bf.saturating_add(tx.max_priority_fee_per_gas),
693            ),
694            ArbTypedTransaction::Eip4844(tx) => core::cmp::min(
695                tx.max_fee_per_gas,
696                bf.saturating_add(tx.max_priority_fee_per_gas),
697            ),
698            // Arbitrum-internal types: gas price is determined elsewhere.
699            _ => bf,
700        }
701    }
702
703    fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
704        let bf = base_fee as u128;
705        match &self.transaction {
706            ArbTypedTransaction::Eip1559(tx) => Some(core::cmp::min(
707                tx.max_priority_fee_per_gas,
708                tx.max_fee_per_gas.saturating_sub(bf),
709            )),
710            ArbTypedTransaction::Eip7702(tx) => Some(core::cmp::min(
711                tx.max_priority_fee_per_gas,
712                tx.max_fee_per_gas.saturating_sub(bf),
713            )),
714            ArbTypedTransaction::Eip4844(tx) => Some(core::cmp::min(
715                tx.max_priority_fee_per_gas,
716                tx.max_fee_per_gas.saturating_sub(bf),
717            )),
718            _ => None,
719        }
720    }
721
722    fn is_dynamic_fee(&self) -> bool {
723        !matches!(
724            self.transaction,
725            ArbTypedTransaction::Legacy(_) | ArbTypedTransaction::Eip2930(_)
726        )
727    }
728
729    fn kind(&self) -> TxKind {
730        match &self.transaction {
731            ArbTypedTransaction::Legacy(tx) => tx.to,
732            ArbTypedTransaction::Deposit(tx) => TxKind::Call(tx.to),
733            ArbTypedTransaction::Unsigned(tx) => match tx.to {
734                Some(to) => TxKind::Call(to),
735                None => TxKind::Create,
736            },
737            ArbTypedTransaction::Contract(tx) => match tx.to {
738                Some(to) => TxKind::Call(to),
739                None => TxKind::Create,
740            },
741            ArbTypedTransaction::Retry(tx) => match tx.to {
742                Some(to) => TxKind::Call(to),
743                None => TxKind::Create,
744            },
745            ArbTypedTransaction::SubmitRetryable(_) => TxKind::Call(RETRYABLE_ADDRESS),
746            ArbTypedTransaction::Internal(_) => TxKind::Call(ARBOS_ADDRESS),
747            ArbTypedTransaction::Eip2930(tx) => tx.to,
748            ArbTypedTransaction::Eip1559(tx) => tx.to,
749            ArbTypedTransaction::Eip4844(tx) => TxKind::Call(tx.to),
750            ArbTypedTransaction::Eip7702(tx) => TxKind::Call(tx.to),
751        }
752    }
753
754    fn is_create(&self) -> bool {
755        matches!(self.kind(), TxKind::Create)
756    }
757
758    fn value(&self) -> U256 {
759        match &self.transaction {
760            ArbTypedTransaction::Legacy(tx) => tx.value,
761            ArbTypedTransaction::Deposit(tx) => tx.value,
762            ArbTypedTransaction::Unsigned(tx) => tx.value,
763            ArbTypedTransaction::Contract(tx) => tx.value,
764            ArbTypedTransaction::Retry(tx) => tx.value,
765            ArbTypedTransaction::SubmitRetryable(tx) => tx.retry_value,
766            ArbTypedTransaction::Internal(_) => U256::ZERO,
767            ArbTypedTransaction::Eip2930(tx) => tx.value,
768            ArbTypedTransaction::Eip1559(tx) => tx.value,
769            ArbTypedTransaction::Eip4844(tx) => tx.value,
770            ArbTypedTransaction::Eip7702(tx) => tx.value,
771        }
772    }
773
774    fn input(&self) -> &Bytes {
775        match &self.transaction {
776            ArbTypedTransaction::Legacy(tx) => &tx.input,
777            ArbTypedTransaction::Deposit(_) => self.input_cache.get_or_init(Bytes::new),
778            ArbTypedTransaction::Unsigned(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
779            ArbTypedTransaction::Contract(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
780            ArbTypedTransaction::Retry(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
781            ArbTypedTransaction::SubmitRetryable(tx) => self.input_cache.get_or_init(|| {
782                let sel = arb_alloy_predeploys::selector(
783                    arb_alloy_predeploys::SIG_RETRY_SUBMIT_RETRYABLE,
784                );
785                let mut out = Vec::with_capacity(4 + tx.retry_data.len());
786                out.extend_from_slice(&sel);
787                out.extend_from_slice(&tx.retry_data);
788                Bytes::from(out)
789            }),
790            ArbTypedTransaction::Internal(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
791            ArbTypedTransaction::Eip2930(tx) => &tx.input,
792            ArbTypedTransaction::Eip1559(tx) => &tx.input,
793            ArbTypedTransaction::Eip4844(tx) => &tx.input,
794            ArbTypedTransaction::Eip7702(tx) => &tx.input,
795        }
796    }
797
798    fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> {
799        match &self.transaction {
800            ArbTypedTransaction::Eip2930(tx) => Some(&tx.access_list),
801            ArbTypedTransaction::Eip1559(tx) => Some(&tx.access_list),
802            ArbTypedTransaction::Eip4844(tx) => Some(&tx.access_list),
803            ArbTypedTransaction::Eip7702(tx) => Some(&tx.access_list),
804            _ => None,
805        }
806    }
807
808    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
809        None
810    }
811
812    fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
813        match &self.transaction {
814            ArbTypedTransaction::Eip7702(tx) => Some(&tx.authorization_list),
815            _ => None,
816        }
817    }
818}
819
820// ---------------------------------------------------------------------------
821// serde — serialize via 2718 encoding
822// ---------------------------------------------------------------------------
823
824impl serde::Serialize for ArbTransactionSigned {
825    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
826    where
827        S: serde::Serializer,
828    {
829        use serde::ser::SerializeStruct;
830        let mut state = serializer.serialize_struct("ArbTransactionSigned", 2)?;
831        state.serialize_field("signature", &self.signature)?;
832        state.serialize_field("hash", self.tx_hash())?;
833        state.end()
834    }
835}
836
837impl<'de> serde::Deserialize<'de> for ArbTransactionSigned {
838    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
839    where
840        D: serde::Deserializer<'de>,
841    {
842        #[derive(serde::Deserialize)]
843        struct Helper {
844            signature: Signature,
845            #[serde(default)]
846            transaction_encoded_2718: Option<alloy_primitives::Bytes>,
847        }
848        let helper = Helper::deserialize(deserializer)?;
849        if let Some(encoded) = helper.transaction_encoded_2718 {
850            let mut slice: &[u8] = encoded.as_ref();
851            let parsed = Self::network_decode(&mut slice).map_err(serde::de::Error::custom)?;
852            Ok(parsed)
853        } else {
854            // Fallback: return a default-like empty tx (legacy with zero fields).
855            Ok(Self::new_unhashed(
856                ArbTypedTransaction::Legacy(TxLegacy::default()),
857                helper.signature,
858            ))
859        }
860    }
861}
862
863// ---------------------------------------------------------------------------
864// RlpBincode — required by SerdeBincodeCompat
865// ---------------------------------------------------------------------------
866
867impl reth_primitives_traits::serde_bincode_compat::RlpBincode for ArbTransactionSigned {}
868
869// ---------------------------------------------------------------------------
870// Compact — required by MaybeCompact when reth-codec feature is active
871// ---------------------------------------------------------------------------
872
873impl reth_codecs::Compact for ArbTransactionSigned {
874    fn to_compact<B>(&self, buf: &mut B) -> usize
875    where
876        B: bytes::BufMut + AsMut<[u8]>,
877    {
878        // Simple approach: encode via 2718 and prefix with length.
879        let encoded = self.encoded_2718();
880        let len = encoded.len() as u32;
881        buf.put_u32(len);
882        buf.put_slice(&encoded);
883        // Signature
884        let sig_bytes = self.signature.as_bytes();
885        buf.put_slice(&sig_bytes);
886        0
887    }
888
889    fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
890        use bytes::Buf;
891        let mut slice = buf;
892        let tx_len = slice.get_u32() as usize;
893        let tx_bytes = &slice[..tx_len];
894        slice = &slice[tx_len..];
895
896        let mut tx_buf = tx_bytes;
897        let tx = Self::network_decode(&mut tx_buf).unwrap_or_else(|_| {
898            Self::new_unhashed(
899                ArbTypedTransaction::Legacy(TxLegacy::default()),
900                Signature::new(U256::ZERO, U256::ZERO, false),
901            )
902        });
903
904        // Read signature (65 bytes)
905        if slice.len() >= 65 {
906            let _sig_bytes = &slice[..65];
907            slice = &slice[65..];
908        }
909
910        (tx, slice)
911    }
912}
913
914// ---------------------------------------------------------------------------
915// Compress / Decompress — delegates to Compact for database storage
916// ---------------------------------------------------------------------------
917
918impl reth_db_api::table::Compress for ArbTransactionSigned {
919    type Compressed = Vec<u8>;
920
921    fn compress_to_buf<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) {
922        let _ = reth_codecs::Compact::to_compact(self, buf);
923    }
924}
925
926impl reth_db_api::table::Decompress for ArbTransactionSigned {
927    fn decompress(value: &[u8]) -> Result<Self, reth_db_api::DatabaseError> {
928        let (obj, _) = reth_codecs::Compact::from_compact(value, value.len());
929        Ok(obj)
930    }
931}
932
933// ---------------------------------------------------------------------------
934// Arbitrum transaction data extraction
935// ---------------------------------------------------------------------------
936
937/// Data extracted from a SubmitRetryable transaction for processing.
938#[derive(Debug, Clone)]
939pub struct SubmitRetryableInfo {
940    pub from: Address,
941    pub deposit_value: U256,
942    pub retry_value: U256,
943    pub gas_fee_cap: U256,
944    pub gas: u64,
945    pub retry_to: Option<Address>,
946    pub retry_data: Vec<u8>,
947    pub beneficiary: Address,
948    pub max_submission_fee: U256,
949    pub fee_refund_addr: Address,
950    pub l1_base_fee: U256,
951    pub request_id: B256,
952}
953
954/// Data extracted from a RetryTx transaction for processing.
955#[derive(Debug, Clone)]
956pub struct RetryTxInfo {
957    pub from: Address,
958    pub ticket_id: B256,
959    pub refund_to: Address,
960    pub gas_fee_cap: U256,
961    pub max_refund: U256,
962    pub submission_fee_refund: U256,
963}
964
965/// Trait for extracting Arbitrum-specific transaction data beyond the
966/// standard `Transaction` trait.
967pub trait ArbTransactionExt {
968    fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
969        None
970    }
971    fn retry_tx_info(&self) -> Option<RetryTxInfo> {
972        None
973    }
974    /// Compute or return cached poster calldata units for the given brotli level.
975    /// Default impl calls `compute` every time; `ArbTransactionSigned` caches the result.
976    fn poster_units_for(&self, _level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
977        compute()
978    }
979}
980
981impl ArbTransactionExt for ArbTransactionSigned {
982    fn poster_units_for(&self, level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
983        if let Some(&entry) = self.poster_units_cache.get() {
984            let (cached_level, cached_units) = unpack_poster_units(entry);
985            if cached_level == level {
986                return cached_units;
987            }
988            return compute();
989        }
990        let units = compute();
991        let _ = self.poster_units_cache.set(pack_poster_units(level, units));
992        units
993    }
994
995    fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
996        match &self.transaction {
997            ArbTypedTransaction::SubmitRetryable(tx) => Some(SubmitRetryableInfo {
998                from: tx.from,
999                deposit_value: tx.deposit_value,
1000                retry_value: tx.retry_value,
1001                gas_fee_cap: tx.gas_fee_cap,
1002                gas: tx.gas,
1003                retry_to: tx.retry_to,
1004                retry_data: tx.retry_data.to_vec(),
1005                beneficiary: tx.beneficiary,
1006                max_submission_fee: tx.max_submission_fee,
1007                fee_refund_addr: tx.fee_refund_addr,
1008                l1_base_fee: tx.l1_base_fee,
1009                request_id: tx.request_id,
1010            }),
1011            _ => None,
1012        }
1013    }
1014
1015    fn retry_tx_info(&self) -> Option<RetryTxInfo> {
1016        match &self.transaction {
1017            ArbTypedTransaction::Retry(tx) => Some(RetryTxInfo {
1018                from: tx.from,
1019                ticket_id: tx.ticket_id,
1020                refund_to: tx.refund_to,
1021                gas_fee_cap: tx.gas_fee_cap,
1022                max_refund: tx.max_refund,
1023                submission_fee_refund: tx.submission_fee_refund,
1024            }),
1025            _ => None,
1026        }
1027    }
1028}
1029
1030/// Standard Ethereum transaction envelopes don't carry retryable data.
1031impl<T> ArbTransactionExt for alloy_consensus::EthereumTxEnvelope<T> {}
1032
1033#[cfg(test)]
1034mod tests {
1035    use super::*;
1036
1037    #[test]
1038    fn roundtrip_unsigned_tx() {
1039        let tx = ArbUnsignedTx {
1040            chain_id: U256::from(42161u64),
1041            from: alloy_primitives::address!("00000000000000000000000000000000000000aa"),
1042            nonce: 7,
1043            gas_fee_cap: U256::from(1_000_000u64),
1044            gas: 21000,
1045            to: Some(alloy_primitives::address!(
1046                "00000000000000000000000000000000000000bb"
1047            )),
1048            value: U256::from(123u64),
1049            data: Vec::new().into(),
1050        };
1051
1052        let mut enc = Vec::with_capacity(1 + tx.length());
1053        enc.push(ArbTxType::ArbitrumUnsignedTx.as_u8());
1054        tx.encode(&mut enc);
1055
1056        let signed =
1057            ArbTransactionSigned::decode_2718_exact(enc.as_slice()).expect("typed decode ok");
1058        assert_eq!(signed.tx_type(), ArbTxTypeLocal::Unsigned);
1059        assert_eq!(signed.chain_id(), Some(42161));
1060        assert_eq!(signed.nonce(), 7);
1061        assert_eq!(signed.gas_limit(), 21000);
1062        assert_eq!(signed.value(), U256::from(123u64));
1063    }
1064
1065    #[test]
1066    fn deposit_tx_has_zero_gas() {
1067        let tx = ArbDepositTx {
1068            chain_id: U256::from(42161u64),
1069            l1_request_id: B256::ZERO,
1070            from: Address::ZERO,
1071            to: Address::ZERO,
1072            value: U256::from(100u64),
1073        };
1074
1075        let signed = ArbTransactionSigned::new_unhashed(
1076            ArbTypedTransaction::Deposit(tx),
1077            ArbTransactionSigned::zero_sig(),
1078        );
1079
1080        assert_eq!(signed.gas_limit(), 0);
1081        assert_eq!(signed.nonce(), 0);
1082        assert_eq!(signed.tx_type(), ArbTxTypeLocal::Deposit);
1083    }
1084}