arb_primitives/
signed_tx.rs

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