1use alloy_primitives::{Address, B256, U256};
2use std::collections::HashMap;
3
4use crate::{l1_pricing, retryables};
5use arb_chainspec::arbos_version as arb_ver;
6
7pub const ARBOS_ADDRESS: Address = {
9 let mut bytes = [0u8; 20];
10 bytes[17] = 0x0a;
11 bytes[18] = 0x4b;
12 bytes[19] = 0x05;
13 Address::new(bytes)
14};
15
16pub const GAS_ESTIMATION_L1_PRICE_PADDING_BIPS: u64 = 11000;
18
19#[derive(Debug)]
25pub struct TxProcessor {
26 pub poster_fee: U256,
28 pub poster_gas: u64,
30 pub compute_hold_gas: u64,
32 pub delayed_inbox: bool,
34 pub top_tx_type: Option<u8>,
36 pub current_retryable: Option<B256>,
38 pub current_refund_to: Option<Address>,
40 pub scheduled_txs: Vec<Vec<u8>>,
42 pub programs_depth: HashMap<Address, usize>,
45}
46
47impl Default for TxProcessor {
48 fn default() -> Self {
49 Self {
50 poster_fee: U256::ZERO,
51 poster_gas: 0,
52 compute_hold_gas: 0,
53 delayed_inbox: false,
54 top_tx_type: None,
55 current_retryable: None,
56 current_refund_to: None,
57 scheduled_txs: Vec::new(),
58 programs_depth: HashMap::new(),
59 }
60 }
61}
62
63impl TxProcessor {
64 pub fn new(coinbase: Address) -> Self {
67 Self {
68 delayed_inbox: coinbase != l1_pricing::BATCH_POSTER_ADDRESS,
69 ..Self::default()
70 }
71 }
72
73 pub fn nonrefundable_gas(&self) -> u64 {
75 self.poster_gas
76 }
77
78 pub fn held_gas(&self) -> u64 {
80 self.compute_hold_gas
81 }
82
83 pub fn drop_tip(&self, arbos_version: u64) -> bool {
85 arbos_version != 9 || self.delayed_inbox
86 }
87
88 pub fn get_paid_gas_price(&self, arbos_version: u64, base_fee: U256, gas_price: U256) -> U256 {
90 if arbos_version != 9 {
91 base_fee
92 } else {
93 gas_price
94 }
95 }
96
97 pub fn gas_price_op(&self, arbos_version: u64, base_fee: U256, gas_price: U256) -> U256 {
99 if arbos_version >= 3 {
100 self.get_paid_gas_price(arbos_version, base_fee, gas_price)
101 } else {
102 gas_price
103 }
104 }
105
106 pub fn fill_receipt_gas_used_for_l1(&self) -> u64 {
108 self.poster_gas
109 }
110
111 pub fn push_program(&mut self, addr: Address) {
117 *self.programs_depth.entry(addr).or_insert(0) += 1;
118 }
119
120 pub fn pop_program(&mut self, addr: Address) {
122 if let Some(count) = self.programs_depth.get_mut(&addr) {
123 *count = count.saturating_sub(1);
124 if *count == 0 {
125 self.programs_depth.remove(&addr);
126 }
127 }
128 }
129
130 pub fn is_reentrant(&self, addr: &Address) -> bool {
132 self.programs_depth.get(addr).copied().unwrap_or(0) > 1
133 }
134
135 pub fn reverted_tx_hook(
149 &self,
150 tx_hash: Option<B256>,
151 pre_recorded_gas: Option<u64>,
152 is_filtered: bool,
153 ) -> RevertedTxAction {
154 let Some(_hash) = tx_hash else {
155 return RevertedTxAction::None;
156 };
157
158 if let Some(l2_gas_used) = pre_recorded_gas {
159 let adjusted_gas = l2_gas_used.saturating_sub(TX_GAS);
160 return RevertedTxAction::PreRecordedRevert {
161 gas_to_consume: adjusted_gas,
162 };
163 }
164
165 if is_filtered {
166 return RevertedTxAction::FilteredTx;
167 }
168
169 RevertedTxAction::None
170 }
171
172 pub fn set_tx_type(&mut self, tx_type: u8) {
178 self.top_tx_type = Some(tx_type);
179 }
180
181 pub fn prepare_retry_tx(&mut self, ticket_id: B256, refund_to: Address) {
189 self.current_retryable = Some(ticket_id);
190 self.current_refund_to = Some(refund_to);
191 }
192
193 pub fn gas_charging_hook(
203 &mut self,
204 gas_remaining: &mut u64,
205 intrinsic_gas: u64,
206 params: &GasChargingParams,
207 ) -> Result<(), GasChargingError> {
208 let mut gas_needed = 0u64;
209
210 if !params.base_fee.is_zero() && !params.skip_l1_charging {
211 self.poster_gas = compute_poster_gas(
212 params.poster_cost,
213 params.base_fee,
214 params.is_gas_estimation,
215 params.min_base_fee,
216 );
217 self.poster_fee = params.base_fee.saturating_mul(U256::from(self.poster_gas));
218 gas_needed = self.poster_gas;
219 }
220
221 if *gas_remaining < gas_needed {
222 return Err(GasChargingError::IntrinsicGasTooLow);
223 }
224 *gas_remaining -= gas_needed;
225
226 if !params.is_eth_call {
228 let max = if params.arbos_version < arb_ver::ARBOS_VERSION_50 {
229 params.per_block_gas_limit
230 } else {
231 params.per_tx_gas_limit.saturating_sub(intrinsic_gas)
233 };
234
235 if *gas_remaining > max {
236 self.compute_hold_gas = *gas_remaining - max;
237 *gas_remaining = max;
238 }
239 }
240
241 Ok(())
242 }
243
244 pub fn compute_end_tx_fee_distribution(
253 &self,
254 params: &EndTxNormalParams,
255 ) -> EndTxFeeDistribution {
256 let gas_used = params.gas_used;
257 let base_fee = params.base_fee;
258
259 let total_cost = base_fee.saturating_mul(U256::from(gas_used));
260 let mut compute_cost = total_cost.saturating_sub(self.poster_fee);
261 let mut poster_fee = self.poster_fee;
262
263 if total_cost < self.poster_fee {
264 tracing::error!(
265 gas_used,
266 ?base_fee,
267 poster_fee = ?self.poster_fee,
268 "total cost < poster cost"
269 );
270 poster_fee = U256::ZERO;
271 compute_cost = total_cost;
272 }
273
274 let mut infra_fee_amount = U256::ZERO;
275
276 if params.arbos_version > 4 && params.infra_fee_account != Address::ZERO {
277 let infra_fee = params.min_base_fee.min(base_fee);
278 let compute_gas = gas_used.saturating_sub(self.poster_gas);
279 infra_fee_amount = infra_fee.saturating_mul(U256::from(compute_gas));
280 compute_cost = compute_cost.saturating_sub(infra_fee_amount);
281 }
282
283 let poster_fee_destination = if params.arbos_version < 2 {
284 params.coinbase
285 } else {
286 l1_pricing::L1_PRICER_FUNDS_POOL_ADDRESS
287 };
288
289 let l1_fees_to_add = if params.arbos_version >= arb_ver::ARBOS_VERSION_10 {
290 poster_fee
291 } else {
292 U256::ZERO
293 };
294
295 let compute_gas_for_backlog = if !params.gas_price.is_zero() {
296 if gas_used > self.poster_gas {
297 gas_used - self.poster_gas
298 } else {
299 tracing::error!(
300 gas_used,
301 poster_gas = self.poster_gas,
302 "gas used < poster gas"
303 );
304 gas_used
305 }
306 } else {
307 0
308 };
309
310 EndTxFeeDistribution {
311 infra_fee_account: params.infra_fee_account,
312 infra_fee_amount,
313 network_fee_account: params.network_fee_account,
314 network_fee_amount: compute_cost,
315 poster_fee_destination,
316 poster_fee_amount: poster_fee,
317 l1_fees_to_add,
318 compute_gas_for_backlog,
319 }
320 }
321
322 pub fn end_tx_retryable<F>(
332 &self,
333 params: &EndTxRetryableParams,
334 mut burn_fn: impl FnMut(Address, U256),
335 mut transfer_fn: F,
336 ) -> EndTxRetryableResult
337 where
338 F: FnMut(Address, Address, U256) -> Result<(), ()>,
339 {
340 let effective_base_fee = params.effective_base_fee;
341 let gas_left = params.gas_left;
342 let gas_used = params.gas_used;
343
344 let gas_refund_amount = effective_base_fee.saturating_mul(U256::from(gas_left));
346 burn_fn(params.from, gas_refund_amount);
347
348 let single_gas_cost = effective_base_fee.saturating_mul(U256::from(gas_used));
349
350 let mut max_refund = params.max_refund;
351
352 if params.success {
353 refund_with_pool(
355 params.network_fee_account,
356 params.submission_fee_refund,
357 &mut max_refund,
358 params.refund_to,
359 params.from,
360 &mut transfer_fn,
361 );
362 } else {
363 take_funds(&mut max_refund, params.submission_fee_refund);
365 }
366
367 take_funds(&mut max_refund, single_gas_cost);
369
370 let mut network_refund = gas_refund_amount;
372
373 if params.arbos_version >= arb_ver::ARBOS_VERSION_11
374 && params.infra_fee_account != Address::ZERO
375 {
376 let infra_fee = params.min_base_fee.min(effective_base_fee);
377 let infra_refund_amount = infra_fee.saturating_mul(U256::from(gas_left));
378 let infra_refund = take_funds(&mut network_refund, infra_refund_amount);
379 refund_with_pool(
380 params.infra_fee_account,
381 infra_refund,
382 &mut max_refund,
383 params.refund_to,
384 params.from,
385 &mut transfer_fn,
386 );
387 }
388
389 refund_with_pool(
390 params.network_fee_account,
391 network_refund,
392 &mut max_refund,
393 params.refund_to,
394 params.from,
395 &mut transfer_fn,
396 );
397
398 if let Some(multi_cost) = params.multi_dimensional_cost {
402 let should_refund =
403 single_gas_cost > multi_cost && effective_base_fee == params.block_base_fee;
404 if should_refund {
405 let refund_amount = single_gas_cost.saturating_sub(multi_cost);
406 refund_with_pool(
407 params.network_fee_account,
408 refund_amount,
409 &mut max_refund,
410 params.refund_to,
411 params.from,
412 &mut transfer_fn,
413 );
414 }
415 }
416
417 let escrow = retryables::retryable_escrow_address(params.ticket_id);
418
419 EndTxRetryableResult {
420 compute_gas_for_backlog: gas_used,
421 should_delete_retryable: params.success,
422 should_return_value_to_escrow: !params.success,
423 escrow_address: escrow,
424 }
425 }
426}
427
428#[derive(Debug, Clone)]
434pub struct GasChargingParams {
435 pub base_fee: U256,
437 pub poster_cost: U256,
439 pub is_gas_estimation: bool,
441 pub is_eth_call: bool,
443 pub skip_l1_charging: bool,
445 pub min_base_fee: U256,
447 pub per_block_gas_limit: u64,
449 pub per_tx_gas_limit: u64,
451 pub arbos_version: u64,
453}
454
455#[derive(Debug, Clone, thiserror::Error)]
457pub enum GasChargingError {
458 #[error("intrinsic gas too low")]
459 IntrinsicGasTooLow,
460}
461
462#[derive(Debug, Clone)]
464pub struct EndTxNormalParams {
465 pub gas_used: u64,
466 pub gas_price: U256,
467 pub base_fee: U256,
468 pub coinbase: Address,
469 pub network_fee_account: Address,
470 pub infra_fee_account: Address,
471 pub min_base_fee: U256,
472 pub arbos_version: u64,
473}
474
475#[derive(Debug, Clone, Default)]
483pub struct EndTxFeeDistribution {
484 pub infra_fee_account: Address,
485 pub infra_fee_amount: U256,
486 pub network_fee_account: Address,
487 pub network_fee_amount: U256,
488 pub poster_fee_destination: Address,
489 pub poster_fee_amount: U256,
490 pub l1_fees_to_add: U256,
491 pub compute_gas_for_backlog: u64,
492}
493
494#[derive(Debug, Clone)]
496pub struct EndTxRetryableParams {
497 pub gas_left: u64,
498 pub gas_used: u64,
499 pub effective_base_fee: U256,
500 pub from: Address,
501 pub refund_to: Address,
502 pub max_refund: U256,
503 pub submission_fee_refund: U256,
504 pub ticket_id: B256,
505 pub value: U256,
506 pub success: bool,
507 pub network_fee_account: Address,
508 pub infra_fee_account: Address,
509 pub min_base_fee: U256,
510 pub arbos_version: u64,
511 pub multi_dimensional_cost: Option<U256>,
514 pub block_base_fee: U256,
518}
519
520#[derive(Debug, Clone)]
527pub struct EndTxRetryableResult {
528 pub compute_gas_for_backlog: u64,
529 pub should_delete_retryable: bool,
530 pub should_return_value_to_escrow: bool,
531 pub escrow_address: Address,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq)]
536pub enum RevertedTxAction {
537 None,
539 PreRecordedRevert { gas_to_consume: u64 },
541 FilteredTx,
543}
544
545#[derive(Debug, Clone)]
547pub struct SubmitRetryableParams {
548 pub ticket_id: B256,
549 pub from: Address,
550 pub fee_refund_addr: Address,
551 pub deposit_value: U256,
552 pub retry_value: U256,
553 pub gas_fee_cap: U256,
554 pub gas: u64,
555 pub max_submission_fee: U256,
556 pub retry_data_len: usize,
557 pub l1_base_fee: U256,
558 pub effective_base_fee: U256,
559 pub current_time: u64,
560 pub balance_after_mint: U256,
562 pub infra_fee_account: Address,
563 pub min_base_fee: U256,
564 pub arbos_version: u64,
565}
566
567#[derive(Debug, Clone, Default)]
582pub struct SubmitRetryableFees {
583 pub submission_fee: U256,
585 pub submission_fee_refund: U256,
587 pub escrow: Address,
589 pub timeout: u64,
591 pub can_pay_for_gas: bool,
593 pub gas_cost: U256,
595 pub infra_cost: U256,
597 pub network_cost: U256,
599 pub gas_price_refund: U256,
601 pub gas_cost_refund: U256,
603 pub available_refund: U256,
605 pub withheld_submission_fee: U256,
607 pub error: Option<String>,
609}
610
611pub const TX_GAS: u64 = 21_000;
613
614pub fn take_funds(pool: &mut U256, take: U256) -> U256 {
621 if *pool < take {
622 let old = *pool;
623 *pool = U256::ZERO;
624 old
625 } else {
626 *pool -= take;
627 take
628 }
629}
630
631pub fn compute_poster_gas(
634 poster_cost: U256,
635 base_fee: U256,
636 is_gas_estimation: bool,
637 min_gas_price: U256,
638) -> u64 {
639 if base_fee.is_zero() {
640 return 0;
641 }
642
643 let adjusted_base_fee = if is_gas_estimation {
644 let adjusted = base_fee * U256::from(7) / U256::from(8);
646 if adjusted < min_gas_price {
647 min_gas_price
648 } else {
649 adjusted
650 }
651 } else {
652 base_fee
653 };
654
655 let padded_cost = if is_gas_estimation {
656 poster_cost * U256::from(GAS_ESTIMATION_L1_PRICE_PADDING_BIPS) / U256::from(10000)
657 } else {
658 poster_cost
659 };
660
661 if adjusted_base_fee.is_zero() {
662 return 0;
663 }
664
665 let gas = padded_cost / adjusted_base_fee;
666 gas.try_into().unwrap_or(u64::MAX)
667}
668
669pub fn get_poster_gas(
675 tx_data: &[u8],
676 l1_base_fee: U256,
677 l2_base_fee: U256,
678 _arbos_version: u64,
679) -> (u64, u64) {
680 if l2_base_fee.is_zero() || l1_base_fee.is_zero() {
681 return (0, 0);
682 }
683
684 let calldata_units = tx_data_non_zero_count(tx_data) * 16 + tx_data_zero_count(tx_data) * 4;
685
686 let l1_cost = U256::from(calldata_units) * l1_base_fee;
687 let poster_gas = l1_cost / l2_base_fee;
688 let poster_gas_u64: u64 = poster_gas.try_into().unwrap_or(u64::MAX);
689
690 (poster_gas_u64, calldata_units as u64)
691}
692
693fn refund_with_pool<F>(
698 refund_from: Address,
699 amount: U256,
700 max_refund: &mut U256,
701 refund_to: Address,
702 from: Address,
703 transfer_fn: &mut F,
704) where
705 F: FnMut(Address, Address, U256) -> Result<(), ()>,
706{
707 let to_refund_addr = take_funds(max_refund, amount);
708 let _ = transfer_fn(refund_from, refund_to, to_refund_addr);
709 let remainder = amount.saturating_sub(to_refund_addr);
710 let _ = transfer_fn(refund_from, from, remainder);
711}
712
713pub fn compute_retryable_gas_split(
717 gas: u64,
718 effective_base_fee: U256,
719 infra_fee_account: Address,
720 min_base_fee: U256,
721 arbos_version: u64,
722) -> (U256, U256) {
723 let gas_cost = effective_base_fee.saturating_mul(U256::from(gas));
724 let mut network_cost = gas_cost;
725 let mut infra_cost = U256::ZERO;
726
727 if arbos_version >= arb_ver::ARBOS_VERSION_11 && infra_fee_account != Address::ZERO {
728 let infra_fee = min_base_fee.min(effective_base_fee);
729 infra_cost = infra_fee.saturating_mul(U256::from(gas));
730 infra_cost = take_funds(&mut network_cost, infra_cost);
731 }
732
733 (infra_cost, network_cost)
734}
735
736pub fn compute_submit_retryable_fees(params: &SubmitRetryableParams) -> SubmitRetryableFees {
742 let submission_fee =
743 retryables::retryable_submission_fee(params.retry_data_len, params.l1_base_fee);
744
745 let escrow = retryables::retryable_escrow_address(params.ticket_id);
746 let timeout = params.current_time + retryables::RETRYABLE_LIFETIME_SECONDS;
747
748 if params.balance_after_mint < params.max_submission_fee {
750 return SubmitRetryableFees {
751 submission_fee,
752 escrow,
753 timeout,
754 error: Some(format!(
755 "insufficient funds for max submission fee: have {} want {}",
756 params.balance_after_mint, params.max_submission_fee,
757 )),
758 ..Default::default()
759 };
760 }
761
762 if params.max_submission_fee < submission_fee {
764 return SubmitRetryableFees {
765 submission_fee,
766 escrow,
767 timeout,
768 error: Some(format!(
769 "max submission fee {} is less than actual {}",
770 params.max_submission_fee, submission_fee,
771 )),
772 ..Default::default()
773 };
774 }
775
776 let mut available_refund = params.deposit_value;
778 take_funds(&mut available_refund, params.retry_value);
779 let withheld_submission_fee = take_funds(&mut available_refund, submission_fee);
780 let submission_fee_refund = take_funds(
782 &mut available_refund,
783 params.max_submission_fee.saturating_sub(submission_fee),
784 );
785
786 let max_gas_cost = params.gas_fee_cap.saturating_mul(U256::from(params.gas));
788 let fee_cap_too_low = params.gas_fee_cap < params.effective_base_fee;
789
790 let mut balance_after_deductions = params
794 .balance_after_mint
795 .saturating_sub(submission_fee)
796 .saturating_sub(params.retry_value);
797 if params.fee_refund_addr != params.from {
798 balance_after_deductions = balance_after_deductions.saturating_sub(submission_fee_refund);
799 }
800
801 let can_pay_for_gas =
802 !fee_cap_too_low && params.gas >= TX_GAS && balance_after_deductions >= max_gas_cost;
803
804 let (infra_cost, network_cost) = compute_retryable_gas_split(
806 params.gas,
807 params.effective_base_fee,
808 params.infra_fee_account,
809 params.min_base_fee,
810 params.arbos_version,
811 );
812 let gas_cost = params
813 .effective_base_fee
814 .saturating_mul(U256::from(params.gas));
815
816 let gas_cost_refund = if !can_pay_for_gas {
818 take_funds(&mut available_refund, max_gas_cost)
819 } else {
820 U256::ZERO
821 };
822
823 let gas_price_refund = if params.gas_fee_cap > params.effective_base_fee {
825 (params.gas_fee_cap - params.effective_base_fee).saturating_mul(U256::from(params.gas))
826 } else {
827 U256::ZERO
828 };
829
830 let mut gas_price_refund_actual = U256::ZERO;
833
834 if can_pay_for_gas {
835 let withheld_gas_funds = take_funds(&mut available_refund, gas_cost);
837 gas_price_refund_actual = take_funds(&mut available_refund, gas_price_refund);
838 available_refund = available_refund
840 .saturating_add(withheld_gas_funds)
841 .saturating_add(withheld_submission_fee);
842 }
843
844 SubmitRetryableFees {
845 submission_fee,
846 submission_fee_refund,
847 escrow,
848 timeout,
849 can_pay_for_gas,
850 gas_cost,
851 infra_cost,
852 network_cost,
853 gas_price_refund: gas_price_refund_actual,
854 gas_cost_refund,
855 available_refund,
856 withheld_submission_fee,
857 error: None,
858 }
859}
860
861fn tx_data_non_zero_count(data: &[u8]) -> usize {
862 data.iter().filter(|&&b| b != 0).count()
863}
864
865fn tx_data_zero_count(data: &[u8]) -> usize {
866 data.iter().filter(|&&b| b == 0).count()
867}