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,
20 crypto::secp256k1::{recover_signer, recover_signer_unchecked},
21};
22
23const ARBOS_ADDRESS: Address =
25 alloy_primitives::address!("00000000000000000000000000000000000A4B05");
26
27const RETRYABLE_ADDRESS: Address =
29 alloy_primitives::address!("000000000000000000000000000000000000006e");
30
31#[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#[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 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#[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 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 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 pub fn inner(&self) -> &ArbTypedTransaction {
192 &self.transaction
193 }
194
195 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 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
255impl 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
277impl TxHashRef for ArbTransactionSigned {
282 fn tx_hash(&self) -> &TxHash {
283 self.hash.get_or_init(|| self.compute_hash())
284 }
285}
286
287impl ArbTransactionSigned {
292 fn recover_signer_inner(
293 &self,
294 strict: bool,
295 ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
296 match &self.transaction {
297 ArbTypedTransaction::Deposit(tx) => Ok(tx.from),
298 ArbTypedTransaction::Unsigned(tx) => Ok(tx.from),
299 ArbTypedTransaction::Contract(tx) => Ok(tx.from),
300 ArbTypedTransaction::Retry(tx) => Ok(tx.from),
301 ArbTypedTransaction::SubmitRetryable(tx) => Ok(tx.from),
302 ArbTypedTransaction::Internal(_) => Ok(ARBOS_ADDRESS),
303 ArbTypedTransaction::Legacy(tx) => {
304 let mut buf = Vec::new();
305 tx.encode_for_signing(&mut buf);
306 if strict {
307 recover_signer(&self.signature, keccak256(&buf))
308 } else {
309 recover_signer_unchecked(&self.signature, keccak256(&buf))
310 }
311 }
312 ArbTypedTransaction::Eip2930(tx) => {
313 let mut buf = Vec::new();
314 tx.encode_for_signing(&mut buf);
315 if strict {
316 recover_signer(&self.signature, keccak256(&buf))
317 } else {
318 recover_signer_unchecked(&self.signature, keccak256(&buf))
319 }
320 }
321 ArbTypedTransaction::Eip1559(tx) => {
322 let mut buf = Vec::new();
323 tx.encode_for_signing(&mut buf);
324 if strict {
325 recover_signer(&self.signature, keccak256(&buf))
326 } else {
327 recover_signer_unchecked(&self.signature, keccak256(&buf))
328 }
329 }
330 ArbTypedTransaction::Eip4844(tx) => {
331 let mut buf = Vec::new();
332 tx.encode_for_signing(&mut buf);
333 if strict {
334 recover_signer(&self.signature, keccak256(&buf))
335 } else {
336 recover_signer_unchecked(&self.signature, keccak256(&buf))
337 }
338 }
339 ArbTypedTransaction::Eip7702(tx) => {
340 let mut buf = Vec::new();
341 tx.encode_for_signing(&mut buf);
342 if strict {
343 recover_signer(&self.signature, keccak256(&buf))
344 } else {
345 recover_signer_unchecked(&self.signature, keccak256(&buf))
346 }
347 }
348 }
349 }
350}
351
352impl alloy_consensus::transaction::SignerRecoverable for ArbTransactionSigned {
353 fn recover_signer(
354 &self,
355 ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
356 if let Some(addr) = self.sender_cache.get() {
357 return Ok(*addr);
358 }
359 let addr = self.recover_signer_inner(true)?;
360 let _ = self.sender_cache.set(addr);
361 Ok(addr)
362 }
363
364 fn recover_signer_unchecked(
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(false)?;
371 let _ = self.sender_cache.set(addr);
372 Ok(addr)
373 }
374}
375
376impl Typed2718 for ArbTransactionSigned {
381 fn is_legacy(&self) -> bool {
382 matches!(self.transaction, ArbTypedTransaction::Legacy(_))
383 }
384
385 fn ty(&self) -> u8 {
386 match &self.transaction {
387 ArbTypedTransaction::Legacy(_) => 0u8,
388 ArbTypedTransaction::Deposit(_) => ArbTxType::ArbitrumDepositTx.as_u8(),
389 ArbTypedTransaction::Unsigned(_) => ArbTxType::ArbitrumUnsignedTx.as_u8(),
390 ArbTypedTransaction::Contract(_) => ArbTxType::ArbitrumContractTx.as_u8(),
391 ArbTypedTransaction::Retry(_) => ArbTxType::ArbitrumRetryTx.as_u8(),
392 ArbTypedTransaction::SubmitRetryable(_) => ArbTxType::ArbitrumSubmitRetryableTx.as_u8(),
393 ArbTypedTransaction::Internal(_) => ArbTxType::ArbitrumInternalTx.as_u8(),
394 ArbTypedTransaction::Eip2930(_) => 0x01,
395 ArbTypedTransaction::Eip1559(_) => 0x02,
396 ArbTypedTransaction::Eip4844(_) => 0x03,
397 ArbTypedTransaction::Eip7702(_) => 0x04,
398 }
399 }
400}
401
402impl IsTyped2718 for ArbTransactionSigned {
407 fn is_type(type_id: u8) -> bool {
408 matches!(type_id, 0x01..=0x04) || ArbTxType::from_u8(type_id).is_ok()
410 }
411}
412
413impl Encodable2718 for ArbTransactionSigned {
418 fn type_flag(&self) -> Option<u8> {
419 if self.is_legacy() {
420 None
421 } else {
422 Some(self.ty())
423 }
424 }
425
426 fn encode_2718_len(&self) -> usize {
427 match &self.transaction {
428 ArbTypedTransaction::Legacy(tx) => tx.eip2718_encoded_length(&self.signature),
429 ArbTypedTransaction::Deposit(tx) => tx.length() + 1,
430 ArbTypedTransaction::Unsigned(tx) => tx.length() + 1,
431 ArbTypedTransaction::Contract(tx) => tx.length() + 1,
432 ArbTypedTransaction::Retry(tx) => tx.length() + 1,
433 ArbTypedTransaction::SubmitRetryable(tx) => tx.length() + 1,
434 ArbTypedTransaction::Internal(tx) => tx.length() + 1,
435 ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encoded_length(&self.signature),
436 ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encoded_length(&self.signature),
437 ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encoded_length(&self.signature),
438 ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encoded_length(&self.signature),
439 }
440 }
441
442 fn encode_2718(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
443 match &self.transaction {
444 ArbTypedTransaction::Legacy(tx) => tx.eip2718_encode(&self.signature, out),
445 ArbTypedTransaction::Deposit(tx) => {
446 out.put_u8(ArbTxType::ArbitrumDepositTx.as_u8());
447 tx.encode(out);
448 }
449 ArbTypedTransaction::Unsigned(tx) => {
450 out.put_u8(ArbTxType::ArbitrumUnsignedTx.as_u8());
451 tx.encode(out);
452 }
453 ArbTypedTransaction::Contract(tx) => {
454 out.put_u8(ArbTxType::ArbitrumContractTx.as_u8());
455 tx.encode(out);
456 }
457 ArbTypedTransaction::Retry(tx) => {
458 out.put_u8(ArbTxType::ArbitrumRetryTx.as_u8());
459 tx.encode(out);
460 }
461 ArbTypedTransaction::SubmitRetryable(tx) => {
462 out.put_u8(ArbTxType::ArbitrumSubmitRetryableTx.as_u8());
463 tx.encode(out);
464 }
465 ArbTypedTransaction::Internal(tx) => {
466 out.put_u8(ArbTxType::ArbitrumInternalTx.as_u8());
467 tx.encode(out);
468 }
469 ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encode(&self.signature, out),
470 ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encode(&self.signature, out),
471 ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encode(&self.signature, out),
472 ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encode(&self.signature, out),
473 }
474 }
475}
476
477impl Decodable2718 for ArbTransactionSigned {
482 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
483 if let Ok(kind) = ArbTxType::from_u8(ty) {
485 return Ok(match kind {
486 ArbTxType::ArbitrumDepositTx => {
487 let tx = ArbDepositTx::decode(buf)?;
488 Self::new_unhashed(ArbTypedTransaction::Deposit(tx), Self::zero_sig())
489 }
490 ArbTxType::ArbitrumUnsignedTx => {
491 let tx = ArbUnsignedTx::decode(buf)?;
492 Self::new_unhashed(ArbTypedTransaction::Unsigned(tx), Self::zero_sig())
493 }
494 ArbTxType::ArbitrumContractTx => {
495 let tx = ArbContractTx::decode(buf)?;
496 Self::new_unhashed(ArbTypedTransaction::Contract(tx), Self::zero_sig())
497 }
498 ArbTxType::ArbitrumRetryTx => {
499 let tx = ArbRetryTx::decode(buf)?;
500 Self::new_unhashed(ArbTypedTransaction::Retry(tx), Self::zero_sig())
501 }
502 ArbTxType::ArbitrumSubmitRetryableTx => {
503 let tx = ArbSubmitRetryableTx::decode(buf)?;
504 Self::new_unhashed(ArbTypedTransaction::SubmitRetryable(tx), Self::zero_sig())
505 }
506 ArbTxType::ArbitrumInternalTx => {
507 let tx = ArbInternalTx::decode(buf)?;
508 Self::new_unhashed(ArbTypedTransaction::Internal(tx), Self::zero_sig())
509 }
510 ArbTxType::ArbitrumLegacyTx => return Err(Eip2718Error::UnexpectedType(0x78)),
511 });
512 }
513
514 match alloy_consensus::TxType::try_from(ty).map_err(|_| Eip2718Error::UnexpectedType(ty))? {
516 alloy_consensus::TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
517 alloy_consensus::TxType::Eip2930 => {
518 let (tx, sig) = alloy_consensus::TxEip2930::rlp_decode_with_signature(buf)?;
519 Ok(Self::new_unhashed(ArbTypedTransaction::Eip2930(tx), sig))
520 }
521 alloy_consensus::TxType::Eip1559 => {
522 let (tx, sig) = alloy_consensus::TxEip1559::rlp_decode_with_signature(buf)?;
523 Ok(Self::new_unhashed(ArbTypedTransaction::Eip1559(tx), sig))
524 }
525 alloy_consensus::TxType::Eip4844 => {
526 let (tx, sig) = alloy_consensus::TxEip4844::rlp_decode_with_signature(buf)?;
527 Ok(Self::new_unhashed(ArbTypedTransaction::Eip4844(tx), sig))
528 }
529 alloy_consensus::TxType::Eip7702 => {
530 let (tx, sig) = alloy_consensus::TxEip7702::rlp_decode_with_signature(buf)?;
531 Ok(Self::new_unhashed(ArbTypedTransaction::Eip7702(tx), sig))
532 }
533 }
534 }
535
536 fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
537 let (tx, sig, hash) = TxLegacy::rlp_decode_signed(buf)?.into_parts();
538 let signed_tx = Self::new_unhashed(ArbTypedTransaction::Legacy(tx), sig);
539 signed_tx.hash.get_or_init(|| hash);
540 Ok(signed_tx)
541 }
542}
543
544impl Encodable for ArbTransactionSigned {
549 fn encode(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
550 self.network_encode(out);
551 }
552 fn length(&self) -> usize {
553 let mut payload_length = self.encode_2718_len();
554 if !self.is_legacy() {
555 payload_length += alloy_rlp::Header {
556 list: false,
557 payload_length,
558 }
559 .length();
560 }
561 payload_length
562 }
563}
564
565impl Decodable for ArbTransactionSigned {
566 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
567 Self::network_decode(buf).map_err(Into::into)
568 }
569}
570
571impl ConsensusTx for ArbTransactionSigned {
576 fn chain_id(&self) -> Option<u64> {
577 match &self.transaction {
578 ArbTypedTransaction::Legacy(tx) => tx.chain_id,
579 ArbTypedTransaction::Deposit(tx) => Some(tx.chain_id.to::<u64>()),
580 ArbTypedTransaction::Unsigned(tx) => Some(tx.chain_id.to::<u64>()),
581 ArbTypedTransaction::Contract(tx) => Some(tx.chain_id.to::<u64>()),
582 ArbTypedTransaction::Retry(tx) => Some(tx.chain_id.to::<u64>()),
583 ArbTypedTransaction::SubmitRetryable(tx) => Some(tx.chain_id.to::<u64>()),
584 ArbTypedTransaction::Internal(tx) => Some(tx.chain_id.to::<u64>()),
585 ArbTypedTransaction::Eip2930(tx) => Some(tx.chain_id),
586 ArbTypedTransaction::Eip1559(tx) => Some(tx.chain_id),
587 ArbTypedTransaction::Eip4844(tx) => Some(tx.chain_id),
588 ArbTypedTransaction::Eip7702(tx) => Some(tx.chain_id),
589 }
590 }
591
592 fn nonce(&self) -> u64 {
593 match &self.transaction {
594 ArbTypedTransaction::Legacy(tx) => tx.nonce,
595 ArbTypedTransaction::Deposit(_) => 0,
596 ArbTypedTransaction::Unsigned(tx) => tx.nonce,
597 ArbTypedTransaction::Contract(_) => 0,
598 ArbTypedTransaction::Retry(tx) => tx.nonce,
599 ArbTypedTransaction::SubmitRetryable(_) => 0,
600 ArbTypedTransaction::Internal(_) => 0,
601 ArbTypedTransaction::Eip2930(tx) => tx.nonce,
602 ArbTypedTransaction::Eip1559(tx) => tx.nonce,
603 ArbTypedTransaction::Eip4844(tx) => tx.nonce,
604 ArbTypedTransaction::Eip7702(tx) => tx.nonce,
605 }
606 }
607
608 fn gas_limit(&self) -> u64 {
609 match &self.transaction {
610 ArbTypedTransaction::Legacy(tx) => tx.gas_limit,
611 ArbTypedTransaction::Deposit(_) => 0,
612 ArbTypedTransaction::Unsigned(tx) => tx.gas,
613 ArbTypedTransaction::Contract(tx) => tx.gas,
614 ArbTypedTransaction::Retry(tx) => tx.gas,
615 ArbTypedTransaction::SubmitRetryable(tx) => tx.gas,
616 ArbTypedTransaction::Internal(_) => 0,
617 ArbTypedTransaction::Eip2930(tx) => tx.gas_limit,
618 ArbTypedTransaction::Eip1559(tx) => tx.gas_limit,
619 ArbTypedTransaction::Eip4844(tx) => tx.gas_limit,
620 ArbTypedTransaction::Eip7702(tx) => tx.gas_limit,
621 }
622 }
623
624 fn gas_price(&self) -> Option<u128> {
625 match &self.transaction {
626 ArbTypedTransaction::Legacy(tx) => Some(tx.gas_price),
627 ArbTypedTransaction::Eip2930(tx) => Some(tx.gas_price),
628 _ => None,
629 }
630 }
631
632 fn max_fee_per_gas(&self) -> u128 {
633 match &self.transaction {
634 ArbTypedTransaction::Legacy(tx) => tx.gas_price,
635 ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
636 ArbTypedTransaction::Unsigned(tx) => tx.gas_fee_cap.to::<u128>(),
637 ArbTypedTransaction::Contract(tx) => tx.gas_fee_cap.to::<u128>(),
638 ArbTypedTransaction::Retry(tx) => tx.gas_fee_cap.to::<u128>(),
639 ArbTypedTransaction::SubmitRetryable(tx) => tx.gas_fee_cap.to::<u128>(),
640 ArbTypedTransaction::Eip1559(tx) => tx.max_fee_per_gas,
641 ArbTypedTransaction::Eip4844(tx) => tx.max_fee_per_gas,
642 ArbTypedTransaction::Eip7702(tx) => tx.max_fee_per_gas,
643 _ => 0,
644 }
645 }
646
647 fn max_priority_fee_per_gas(&self) -> Option<u128> {
648 match &self.transaction {
649 ArbTypedTransaction::Eip1559(tx) => Some(tx.max_priority_fee_per_gas),
650 ArbTypedTransaction::Eip4844(tx) => Some(tx.max_priority_fee_per_gas),
651 ArbTypedTransaction::Eip7702(tx) => Some(tx.max_priority_fee_per_gas),
652 _ => None,
654 }
655 }
656
657 fn max_fee_per_blob_gas(&self) -> Option<u128> {
658 match &self.transaction {
659 ArbTypedTransaction::Eip4844(tx) => Some(tx.max_fee_per_blob_gas),
660 _ => None,
661 }
662 }
663
664 fn priority_fee_or_price(&self) -> u128 {
665 match self.max_priority_fee_per_gas() {
666 Some(p) => p,
667 None => self.gas_price().unwrap_or(0),
668 }
669 }
670
671 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
672 let bf = base_fee.unwrap_or(0) as u128;
673 match &self.transaction {
674 ArbTypedTransaction::Legacy(tx) => tx.gas_price,
675 ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
676 ArbTypedTransaction::Eip1559(tx) => core::cmp::min(
677 tx.max_fee_per_gas,
678 bf.saturating_add(tx.max_priority_fee_per_gas),
679 ),
680 ArbTypedTransaction::Eip7702(tx) => core::cmp::min(
681 tx.max_fee_per_gas,
682 bf.saturating_add(tx.max_priority_fee_per_gas),
683 ),
684 ArbTypedTransaction::Eip4844(tx) => core::cmp::min(
685 tx.max_fee_per_gas,
686 bf.saturating_add(tx.max_priority_fee_per_gas),
687 ),
688 _ => bf,
690 }
691 }
692
693 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
694 let bf = base_fee as u128;
695 match &self.transaction {
696 ArbTypedTransaction::Eip1559(tx) => Some(core::cmp::min(
697 tx.max_priority_fee_per_gas,
698 tx.max_fee_per_gas.saturating_sub(bf),
699 )),
700 ArbTypedTransaction::Eip7702(tx) => Some(core::cmp::min(
701 tx.max_priority_fee_per_gas,
702 tx.max_fee_per_gas.saturating_sub(bf),
703 )),
704 ArbTypedTransaction::Eip4844(tx) => Some(core::cmp::min(
705 tx.max_priority_fee_per_gas,
706 tx.max_fee_per_gas.saturating_sub(bf),
707 )),
708 _ => None,
709 }
710 }
711
712 fn is_dynamic_fee(&self) -> bool {
713 !matches!(
714 self.transaction,
715 ArbTypedTransaction::Legacy(_) | ArbTypedTransaction::Eip2930(_)
716 )
717 }
718
719 fn kind(&self) -> TxKind {
720 match &self.transaction {
721 ArbTypedTransaction::Legacy(tx) => tx.to,
722 ArbTypedTransaction::Deposit(tx) => TxKind::Call(tx.to),
723 ArbTypedTransaction::Unsigned(tx) => match tx.to {
724 Some(to) => TxKind::Call(to),
725 None => TxKind::Create,
726 },
727 ArbTypedTransaction::Contract(tx) => match tx.to {
728 Some(to) => TxKind::Call(to),
729 None => TxKind::Create,
730 },
731 ArbTypedTransaction::Retry(tx) => match tx.to {
732 Some(to) => TxKind::Call(to),
733 None => TxKind::Create,
734 },
735 ArbTypedTransaction::SubmitRetryable(_) => TxKind::Call(RETRYABLE_ADDRESS),
736 ArbTypedTransaction::Internal(_) => TxKind::Call(ARBOS_ADDRESS),
737 ArbTypedTransaction::Eip2930(tx) => tx.to,
738 ArbTypedTransaction::Eip1559(tx) => tx.to,
739 ArbTypedTransaction::Eip4844(tx) => TxKind::Call(tx.to),
740 ArbTypedTransaction::Eip7702(tx) => TxKind::Call(tx.to),
741 }
742 }
743
744 fn is_create(&self) -> bool {
745 matches!(self.kind(), TxKind::Create)
746 }
747
748 fn value(&self) -> U256 {
749 match &self.transaction {
750 ArbTypedTransaction::Legacy(tx) => tx.value,
751 ArbTypedTransaction::Deposit(tx) => tx.value,
752 ArbTypedTransaction::Unsigned(tx) => tx.value,
753 ArbTypedTransaction::Contract(tx) => tx.value,
754 ArbTypedTransaction::Retry(tx) => tx.value,
755 ArbTypedTransaction::SubmitRetryable(tx) => tx.retry_value,
756 ArbTypedTransaction::Internal(_) => U256::ZERO,
757 ArbTypedTransaction::Eip2930(tx) => tx.value,
758 ArbTypedTransaction::Eip1559(tx) => tx.value,
759 ArbTypedTransaction::Eip4844(tx) => tx.value,
760 ArbTypedTransaction::Eip7702(tx) => tx.value,
761 }
762 }
763
764 fn input(&self) -> &Bytes {
765 match &self.transaction {
766 ArbTypedTransaction::Legacy(tx) => &tx.input,
767 ArbTypedTransaction::Deposit(_) => self.input_cache.get_or_init(Bytes::new),
768 ArbTypedTransaction::Unsigned(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
769 ArbTypedTransaction::Contract(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
770 ArbTypedTransaction::Retry(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
771 ArbTypedTransaction::SubmitRetryable(tx) => self.input_cache.get_or_init(|| {
772 let sel = arb_alloy_predeploys::selector(
773 arb_alloy_predeploys::SIG_RETRY_SUBMIT_RETRYABLE,
774 );
775 let mut out = Vec::with_capacity(4 + tx.retry_data.len());
776 out.extend_from_slice(&sel);
777 out.extend_from_slice(&tx.retry_data);
778 Bytes::from(out)
779 }),
780 ArbTypedTransaction::Internal(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
781 ArbTypedTransaction::Eip2930(tx) => &tx.input,
782 ArbTypedTransaction::Eip1559(tx) => &tx.input,
783 ArbTypedTransaction::Eip4844(tx) => &tx.input,
784 ArbTypedTransaction::Eip7702(tx) => &tx.input,
785 }
786 }
787
788 fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> {
789 match &self.transaction {
790 ArbTypedTransaction::Eip2930(tx) => Some(&tx.access_list),
791 ArbTypedTransaction::Eip1559(tx) => Some(&tx.access_list),
792 ArbTypedTransaction::Eip4844(tx) => Some(&tx.access_list),
793 ArbTypedTransaction::Eip7702(tx) => Some(&tx.access_list),
794 _ => None,
795 }
796 }
797
798 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
799 None
800 }
801
802 fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
803 match &self.transaction {
804 ArbTypedTransaction::Eip7702(tx) => Some(&tx.authorization_list),
805 _ => None,
806 }
807 }
808}
809
810impl serde::Serialize for ArbTransactionSigned {
815 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
816 where
817 S: serde::Serializer,
818 {
819 use serde::ser::SerializeStruct;
820 let mut state = serializer.serialize_struct("ArbTransactionSigned", 2)?;
821 state.serialize_field("signature", &self.signature)?;
822 state.serialize_field("hash", self.tx_hash())?;
823 state.end()
824 }
825}
826
827impl<'de> serde::Deserialize<'de> for ArbTransactionSigned {
828 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
829 where
830 D: serde::Deserializer<'de>,
831 {
832 #[derive(serde::Deserialize)]
833 struct Helper {
834 signature: Signature,
835 #[serde(default)]
836 transaction_encoded_2718: Option<alloy_primitives::Bytes>,
837 }
838 let helper = Helper::deserialize(deserializer)?;
839 if let Some(encoded) = helper.transaction_encoded_2718 {
840 let mut slice: &[u8] = encoded.as_ref();
841 let parsed = Self::network_decode(&mut slice).map_err(serde::de::Error::custom)?;
842 Ok(parsed)
843 } else {
844 Ok(Self::new_unhashed(
846 ArbTypedTransaction::Legacy(TxLegacy::default()),
847 helper.signature,
848 ))
849 }
850 }
851}
852
853impl reth_codecs::Compact for ArbTransactionSigned {
858 fn to_compact<B>(&self, buf: &mut B) -> usize
859 where
860 B: bytes::BufMut + AsMut<[u8]>,
861 {
862 let encoded = self.encoded_2718();
864 let len = encoded.len() as u32;
865 buf.put_u32(len);
866 buf.put_slice(&encoded);
867 let sig_bytes = self.signature.as_bytes();
869 buf.put_slice(&sig_bytes);
870 0
871 }
872
873 fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
874 use bytes::Buf;
875 let mut slice = buf;
876 let tx_len = slice.get_u32() as usize;
877 let tx_bytes = &slice[..tx_len];
878 slice = &slice[tx_len..];
879
880 let mut tx_buf = tx_bytes;
881 let tx = Self::network_decode(&mut tx_buf).unwrap_or_else(|_| {
882 Self::new_unhashed(
883 ArbTypedTransaction::Legacy(TxLegacy::default()),
884 Signature::new(U256::ZERO, U256::ZERO, false),
885 )
886 });
887
888 if slice.len() >= 65 {
890 let _sig_bytes = &slice[..65];
891 slice = &slice[65..];
892 }
893
894 (tx, slice)
895 }
896}
897
898impl reth_db_api::table::Compress for ArbTransactionSigned {
903 type Compressed = Vec<u8>;
904
905 fn compress_to_buf<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) {
906 let _ = reth_codecs::Compact::to_compact(self, buf);
907 }
908}
909
910impl reth_db_api::table::Decompress for ArbTransactionSigned {
911 fn decompress(value: &[u8]) -> Result<Self, reth_codecs::DecompressError> {
912 let (obj, _) = reth_codecs::Compact::from_compact(value, value.len());
913 Ok(obj)
914 }
915}
916
917#[derive(Debug, Clone)]
923pub struct SubmitRetryableInfo {
924 pub from: Address,
925 pub deposit_value: U256,
926 pub retry_value: U256,
927 pub gas_fee_cap: U256,
928 pub gas: u64,
929 pub retry_to: Option<Address>,
930 pub retry_data: Vec<u8>,
931 pub beneficiary: Address,
932 pub max_submission_fee: U256,
933 pub fee_refund_addr: Address,
934 pub l1_base_fee: U256,
935 pub request_id: B256,
936}
937
938#[derive(Debug, Clone)]
940pub struct RetryTxInfo {
941 pub from: Address,
942 pub ticket_id: B256,
943 pub refund_to: Address,
944 pub gas_fee_cap: U256,
945 pub max_refund: U256,
946 pub submission_fee_refund: U256,
947}
948
949pub trait ArbTransactionExt {
952 fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
953 None
954 }
955 fn retry_tx_info(&self) -> Option<RetryTxInfo> {
956 None
957 }
958 fn poster_units_for(&self, _level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
961 compute()
962 }
963}
964
965impl ArbTransactionExt for ArbTransactionSigned {
966 fn poster_units_for(&self, level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
967 if let Some(&entry) = self.poster_units_cache.get() {
968 let (cached_level, cached_units) = unpack_poster_units(entry);
969 if cached_level == level {
970 return cached_units;
971 }
972 return compute();
973 }
974 let units = compute();
975 let _ = self.poster_units_cache.set(pack_poster_units(level, units));
976 units
977 }
978
979 fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
980 match &self.transaction {
981 ArbTypedTransaction::SubmitRetryable(tx) => Some(SubmitRetryableInfo {
982 from: tx.from,
983 deposit_value: tx.deposit_value,
984 retry_value: tx.retry_value,
985 gas_fee_cap: tx.gas_fee_cap,
986 gas: tx.gas,
987 retry_to: tx.retry_to,
988 retry_data: tx.retry_data.to_vec(),
989 beneficiary: tx.beneficiary,
990 max_submission_fee: tx.max_submission_fee,
991 fee_refund_addr: tx.fee_refund_addr,
992 l1_base_fee: tx.l1_base_fee,
993 request_id: tx.request_id,
994 }),
995 _ => None,
996 }
997 }
998
999 fn retry_tx_info(&self) -> Option<RetryTxInfo> {
1000 match &self.transaction {
1001 ArbTypedTransaction::Retry(tx) => Some(RetryTxInfo {
1002 from: tx.from,
1003 ticket_id: tx.ticket_id,
1004 refund_to: tx.refund_to,
1005 gas_fee_cap: tx.gas_fee_cap,
1006 max_refund: tx.max_refund,
1007 submission_fee_refund: tx.submission_fee_refund,
1008 }),
1009 _ => None,
1010 }
1011 }
1012}
1013
1014impl<T> ArbTransactionExt for alloy_consensus::EthereumTxEnvelope<T> {}
1016
1017#[cfg(test)]
1018mod tests {
1019 use super::*;
1020
1021 #[test]
1022 fn roundtrip_unsigned_tx() {
1023 let tx = ArbUnsignedTx {
1024 chain_id: U256::from(42161u64),
1025 from: alloy_primitives::address!("00000000000000000000000000000000000000aa"),
1026 nonce: 7,
1027 gas_fee_cap: U256::from(1_000_000u64),
1028 gas: 21000,
1029 to: Some(alloy_primitives::address!(
1030 "00000000000000000000000000000000000000bb"
1031 )),
1032 value: U256::from(123u64),
1033 data: Vec::new().into(),
1034 };
1035
1036 let mut enc = Vec::with_capacity(1 + tx.length());
1037 enc.push(ArbTxType::ArbitrumUnsignedTx.as_u8());
1038 tx.encode(&mut enc);
1039
1040 let signed =
1041 ArbTransactionSigned::decode_2718_exact(enc.as_slice()).expect("typed decode ok");
1042 assert_eq!(signed.tx_type(), ArbTxTypeLocal::Unsigned);
1043 assert_eq!(signed.chain_id(), Some(42161));
1044 assert_eq!(signed.nonce(), 7);
1045 assert_eq!(signed.gas_limit(), 21000);
1046 assert_eq!(signed.value(), U256::from(123u64));
1047 }
1048
1049 #[test]
1050 fn deposit_tx_has_zero_gas() {
1051 let tx = ArbDepositTx {
1052 chain_id: U256::from(42161u64),
1053 l1_request_id: B256::ZERO,
1054 from: Address::ZERO,
1055 to: Address::ZERO,
1056 value: U256::from(100u64),
1057 };
1058
1059 let signed = ArbTransactionSigned::new_unhashed(
1060 ArbTypedTransaction::Deposit(tx),
1061 ArbTransactionSigned::zero_sig(),
1062 );
1063
1064 assert_eq!(signed.gas_limit(), 0);
1065 assert_eq!(signed.nonce(), 0);
1066 assert_eq!(signed.tx_type(), ArbTxTypeLocal::Deposit);
1067 }
1068}