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 self.drop_tip_with_collect(arbos_version, false)
86 }
87
88 pub fn drop_tip_with_collect(&self, arbos_version: u64, collect_tips_enabled: bool) -> bool {
94 if self.delayed_inbox {
95 return true;
96 }
97 if arbos_version == 9 {
98 return false;
99 }
100 if arbos_version < 60 {
101 return true;
102 }
103 !collect_tips_enabled
104 }
105
106 pub fn get_paid_gas_price(&self, arbos_version: u64, base_fee: U256, gas_price: U256) -> U256 {
108 self.get_paid_gas_price_with_collect(arbos_version, base_fee, gas_price, false)
109 }
110
111 pub fn get_paid_gas_price_with_collect(
113 &self,
114 arbos_version: u64,
115 base_fee: U256,
116 gas_price: U256,
117 collect_tips_enabled: bool,
118 ) -> U256 {
119 if !self.drop_tip_with_collect(arbos_version, collect_tips_enabled) {
121 gas_price
122 } else {
123 base_fee
124 }
125 }
126
127 pub fn gas_price_op(&self, arbos_version: u64, base_fee: U256, gas_price: U256) -> U256 {
129 self.gas_price_op_with_collect(arbos_version, base_fee, gas_price, false)
130 }
131
132 pub fn gas_price_op_with_collect(
134 &self,
135 arbos_version: u64,
136 base_fee: U256,
137 gas_price: U256,
138 collect_tips_enabled: bool,
139 ) -> U256 {
140 if arbos_version >= 3 {
141 self.get_paid_gas_price_with_collect(
142 arbos_version,
143 base_fee,
144 gas_price,
145 collect_tips_enabled,
146 )
147 } else {
148 gas_price
149 }
150 }
151
152 pub fn fill_receipt_gas_used_for_l1(&self) -> u64 {
154 self.poster_gas
155 }
156
157 pub fn push_program(&mut self, addr: Address) {
163 *self.programs_depth.entry(addr).or_insert(0) += 1;
164 }
165
166 pub fn pop_program(&mut self, addr: Address) {
168 if let Some(count) = self.programs_depth.get_mut(&addr) {
169 *count = count.saturating_sub(1);
170 if *count == 0 {
171 self.programs_depth.remove(&addr);
172 }
173 }
174 }
175
176 pub fn is_reentrant(&self, addr: &Address) -> bool {
178 self.programs_depth.get(addr).copied().unwrap_or(0) > 1
179 }
180
181 pub fn reverted_tx_hook(
195 &self,
196 tx_hash: Option<B256>,
197 pre_recorded_gas: Option<u64>,
198 is_filtered: bool,
199 ) -> RevertedTxAction {
200 let Some(_hash) = tx_hash else {
201 return RevertedTxAction::None;
202 };
203
204 if let Some(l2_gas_used) = pre_recorded_gas {
205 let adjusted_gas = l2_gas_used.saturating_sub(TX_GAS);
206 return RevertedTxAction::PreRecordedRevert {
207 gas_to_consume: adjusted_gas,
208 };
209 }
210
211 if is_filtered {
212 return RevertedTxAction::FilteredTx;
213 }
214
215 RevertedTxAction::None
216 }
217
218 pub fn set_tx_type(&mut self, tx_type: u8) {
224 self.top_tx_type = Some(tx_type);
225 }
226
227 pub fn prepare_retry_tx(&mut self, ticket_id: B256, refund_to: Address) {
235 self.current_retryable = Some(ticket_id);
236 self.current_refund_to = Some(refund_to);
237 }
238
239 pub fn gas_charging_hook(
249 &mut self,
250 gas_remaining: &mut u64,
251 intrinsic_gas: u64,
252 params: &GasChargingParams,
253 ) -> Result<(), GasChargingError> {
254 let mut gas_needed = 0u64;
255
256 if !params.base_fee.is_zero() && !params.skip_l1_charging {
257 self.poster_gas = compute_poster_gas(
258 params.poster_cost,
259 params.base_fee,
260 params.is_gas_estimation,
261 params.min_base_fee,
262 );
263 self.poster_fee = params.base_fee.saturating_mul(U256::from(self.poster_gas));
264 gas_needed = self.poster_gas;
265 }
266
267 if *gas_remaining < gas_needed {
268 return Err(GasChargingError::IntrinsicGasTooLow);
269 }
270 *gas_remaining -= gas_needed;
271
272 if !params.is_eth_call {
274 let max = if params.arbos_version < arb_ver::ARBOS_VERSION_50 {
275 params.per_block_gas_limit
276 } else {
277 params.per_tx_gas_limit.saturating_sub(intrinsic_gas)
279 };
280
281 if *gas_remaining > max {
282 self.compute_hold_gas = *gas_remaining - max;
283 *gas_remaining = max;
284 }
285 }
286
287 Ok(())
288 }
289
290 pub fn compute_end_tx_fee_distribution(
299 &self,
300 params: &EndTxNormalParams,
301 ) -> EndTxFeeDistribution {
302 let gas_used = params.gas_used;
303 let base_fee = params.base_fee;
304
305 let compute_gas = gas_used.saturating_sub(self.poster_gas);
310 let mut compute_cost = base_fee.saturating_mul(U256::from(compute_gas));
311 let poster_fee = self.poster_fee;
312
313 let mut infra_fee_amount = U256::ZERO;
314
315 if params.arbos_version > 4 && params.infra_fee_account != Address::ZERO {
316 let infra_fee = params.min_base_fee.min(base_fee);
317 infra_fee_amount = infra_fee.saturating_mul(U256::from(compute_gas));
318 compute_cost = compute_cost.saturating_sub(infra_fee_amount);
319 }
320
321 let poster_fee_destination = if params.arbos_version < 2 {
322 params.coinbase
323 } else {
324 l1_pricing::L1_PRICER_FUNDS_POOL_ADDRESS
325 };
326
327 let l1_fees_to_add = if params.arbos_version >= arb_ver::ARBOS_VERSION_10 {
328 poster_fee
329 } else {
330 U256::ZERO
331 };
332
333 let compute_gas_for_backlog = if !params.gas_price.is_zero() {
334 if gas_used > self.poster_gas {
335 gas_used - self.poster_gas
336 } else {
337 tracing::error!(
338 gas_used,
339 poster_gas = self.poster_gas,
340 "gas used < poster gas"
341 );
342 gas_used
343 }
344 } else {
345 0
346 };
347
348 EndTxFeeDistribution {
349 infra_fee_account: params.infra_fee_account,
350 infra_fee_amount,
351 network_fee_account: params.network_fee_account,
352 network_fee_amount: compute_cost,
353 poster_fee_destination,
354 poster_fee_amount: poster_fee,
355 l1_fees_to_add,
356 compute_gas_for_backlog,
357 }
358 }
359
360 pub fn end_tx_retryable<F>(
370 &self,
371 params: &EndTxRetryableParams,
372 mut burn_fn: impl FnMut(Address, U256),
373 mut transfer_fn: F,
374 ) -> EndTxRetryableResult
375 where
376 F: FnMut(Address, Address, U256) -> Result<(), ()>,
377 {
378 let effective_base_fee = params.effective_base_fee;
379 let gas_left = params.gas_left;
380 let gas_used = params.gas_used;
381
382 let gas_refund_amount = effective_base_fee.saturating_mul(U256::from(gas_left));
383 burn_fn(params.from, gas_refund_amount);
384
385 let single_gas_cost = effective_base_fee.saturating_mul(U256::from(gas_used));
386
387 let mut max_refund = params.max_refund;
388
389 if params.success {
390 refund_with_pool(
391 params.network_fee_account,
392 params.submission_fee_refund,
393 &mut max_refund,
394 params.refund_to,
395 params.from,
396 &mut transfer_fn,
397 );
398 } else {
399 take_funds(&mut max_refund, params.submission_fee_refund);
400 }
401
402 take_funds(&mut max_refund, single_gas_cost);
403
404 let mut network_refund = gas_refund_amount;
405
406 if params.arbos_version >= arb_ver::ARBOS_VERSION_11
407 && params.infra_fee_account != Address::ZERO
408 {
409 let infra_fee = params.min_base_fee.min(effective_base_fee);
410 let infra_refund_amount = infra_fee.saturating_mul(U256::from(gas_left));
411 let infra_refund = take_funds(&mut network_refund, infra_refund_amount);
412 refund_with_pool(
413 params.infra_fee_account,
414 infra_refund,
415 &mut max_refund,
416 params.refund_to,
417 params.from,
418 &mut transfer_fn,
419 );
420 }
421
422 refund_with_pool(
423 params.network_fee_account,
424 network_refund,
425 &mut max_refund,
426 params.refund_to,
427 params.from,
428 &mut transfer_fn,
429 );
430
431 if let Some(multi_cost) = params.multi_dimensional_cost {
435 let should_refund =
436 single_gas_cost > multi_cost && effective_base_fee == params.block_base_fee;
437 if should_refund {
438 let refund_amount = single_gas_cost.saturating_sub(multi_cost);
439 refund_with_pool(
440 params.network_fee_account,
441 refund_amount,
442 &mut max_refund,
443 params.refund_to,
444 params.from,
445 &mut transfer_fn,
446 );
447 }
448 }
449
450 let escrow = retryables::retryable_escrow_address(params.ticket_id);
451
452 EndTxRetryableResult {
453 compute_gas_for_backlog: gas_used,
454 should_delete_retryable: params.success,
455 should_return_value_to_escrow: !params.success,
456 escrow_address: escrow,
457 }
458 }
459}
460
461#[derive(Debug, Clone)]
467pub struct GasChargingParams {
468 pub base_fee: U256,
470 pub poster_cost: U256,
472 pub is_gas_estimation: bool,
474 pub is_eth_call: bool,
476 pub skip_l1_charging: bool,
478 pub min_base_fee: U256,
480 pub per_block_gas_limit: u64,
482 pub per_tx_gas_limit: u64,
484 pub arbos_version: u64,
486}
487
488#[derive(Debug, Clone, thiserror::Error)]
490pub enum GasChargingError {
491 #[error("intrinsic gas too low")]
492 IntrinsicGasTooLow,
493}
494
495#[derive(Debug, Clone)]
497pub struct EndTxNormalParams {
498 pub gas_used: u64,
499 pub gas_price: U256,
500 pub base_fee: U256,
501 pub coinbase: Address,
502 pub network_fee_account: Address,
503 pub infra_fee_account: Address,
504 pub min_base_fee: U256,
505 pub arbos_version: u64,
506}
507
508#[derive(Debug, Clone, Default)]
516pub struct EndTxFeeDistribution {
517 pub infra_fee_account: Address,
518 pub infra_fee_amount: U256,
519 pub network_fee_account: Address,
520 pub network_fee_amount: U256,
521 pub poster_fee_destination: Address,
522 pub poster_fee_amount: U256,
523 pub l1_fees_to_add: U256,
524 pub compute_gas_for_backlog: u64,
525}
526
527#[derive(Debug, Clone)]
529pub struct EndTxRetryableParams {
530 pub gas_left: u64,
531 pub gas_used: u64,
532 pub effective_base_fee: U256,
533 pub from: Address,
534 pub refund_to: Address,
535 pub max_refund: U256,
536 pub submission_fee_refund: U256,
537 pub ticket_id: B256,
538 pub value: U256,
539 pub success: bool,
540 pub network_fee_account: Address,
541 pub infra_fee_account: Address,
542 pub min_base_fee: U256,
543 pub arbos_version: u64,
544 pub multi_dimensional_cost: Option<U256>,
547 pub block_base_fee: U256,
551}
552
553#[derive(Debug, Clone)]
560pub struct EndTxRetryableResult {
561 pub compute_gas_for_backlog: u64,
562 pub should_delete_retryable: bool,
563 pub should_return_value_to_escrow: bool,
564 pub escrow_address: Address,
565}
566
567#[derive(Debug, Clone, PartialEq, Eq)]
569pub enum RevertedTxAction {
570 None,
572 PreRecordedRevert { gas_to_consume: u64 },
574 FilteredTx,
576}
577
578#[derive(Debug, Clone)]
580pub struct SubmitRetryableParams {
581 pub ticket_id: B256,
582 pub from: Address,
583 pub fee_refund_addr: Address,
584 pub deposit_value: U256,
585 pub retry_value: U256,
586 pub gas_fee_cap: U256,
587 pub gas: u64,
588 pub max_submission_fee: U256,
589 pub retry_data_len: usize,
590 pub l1_base_fee: U256,
591 pub effective_base_fee: U256,
592 pub current_time: u64,
593 pub balance_after_mint: U256,
595 pub infra_fee_account: Address,
596 pub min_base_fee: U256,
597 pub arbos_version: u64,
598}
599
600#[derive(Debug, Clone, Default)]
615pub struct SubmitRetryableFees {
616 pub submission_fee: U256,
618 pub submission_fee_refund: U256,
620 pub escrow: Address,
622 pub timeout: u64,
624 pub can_pay_for_gas: bool,
626 pub gas_cost: U256,
628 pub infra_cost: U256,
630 pub network_cost: U256,
632 pub gas_price_refund: U256,
634 pub gas_cost_refund: U256,
636 pub available_refund: U256,
638 pub withheld_submission_fee: U256,
640 pub error: Option<String>,
642}
643
644pub const TX_GAS: u64 = 21_000;
646
647pub fn take_funds(pool: &mut U256, take: U256) -> U256 {
654 if *pool < take {
655 let old = *pool;
656 *pool = U256::ZERO;
657 old
658 } else {
659 *pool -= take;
660 take
661 }
662}
663
664pub fn compute_poster_gas(
667 poster_cost: U256,
668 base_fee: U256,
669 is_gas_estimation: bool,
670 min_gas_price: U256,
671) -> u64 {
672 if base_fee.is_zero() {
673 return 0;
674 }
675
676 let adjusted_base_fee = if is_gas_estimation {
677 let adjusted = base_fee * U256::from(7) / U256::from(8);
679 if adjusted < min_gas_price {
680 min_gas_price
681 } else {
682 adjusted
683 }
684 } else {
685 base_fee
686 };
687
688 let padded_cost = if is_gas_estimation {
689 poster_cost * U256::from(GAS_ESTIMATION_L1_PRICE_PADDING_BIPS) / U256::from(10000)
690 } else {
691 poster_cost
692 };
693
694 if adjusted_base_fee.is_zero() {
695 return 0;
696 }
697
698 let gas = padded_cost / adjusted_base_fee;
699 gas.try_into().unwrap_or(u64::MAX)
700}
701
702pub fn get_poster_gas(
708 tx_data: &[u8],
709 l1_base_fee: U256,
710 l2_base_fee: U256,
711 _arbos_version: u64,
712) -> (u64, u64) {
713 if l2_base_fee.is_zero() || l1_base_fee.is_zero() {
714 return (0, 0);
715 }
716
717 let calldata_units = tx_data_non_zero_count(tx_data) * 16 + tx_data_zero_count(tx_data) * 4;
718
719 let l1_cost = U256::from(calldata_units) * l1_base_fee;
720 let poster_gas = l1_cost / l2_base_fee;
721 let poster_gas_u64: u64 = poster_gas.try_into().unwrap_or(u64::MAX);
722
723 (poster_gas_u64, calldata_units as u64)
724}
725
726fn refund_with_pool<F>(
731 refund_from: Address,
732 amount: U256,
733 max_refund: &mut U256,
734 refund_to: Address,
735 from: Address,
736 transfer_fn: &mut F,
737) where
738 F: FnMut(Address, Address, U256) -> Result<(), ()>,
739{
740 let to_refund_addr = take_funds(max_refund, amount);
741 let _ = transfer_fn(refund_from, refund_to, to_refund_addr);
742 let remainder = amount.saturating_sub(to_refund_addr);
743 let _ = transfer_fn(refund_from, from, remainder);
744}
745
746pub fn compute_retryable_gas_split(
750 gas: u64,
751 effective_base_fee: U256,
752 infra_fee_account: Address,
753 min_base_fee: U256,
754 arbos_version: u64,
755) -> (U256, U256) {
756 let gas_cost = effective_base_fee.saturating_mul(U256::from(gas));
757 let mut network_cost = gas_cost;
758 let mut infra_cost = U256::ZERO;
759
760 if arbos_version >= arb_ver::ARBOS_VERSION_11 && infra_fee_account != Address::ZERO {
761 let infra_fee = min_base_fee.min(effective_base_fee);
762 infra_cost = infra_fee.saturating_mul(U256::from(gas));
763 infra_cost = take_funds(&mut network_cost, infra_cost);
764 }
765
766 (infra_cost, network_cost)
767}
768
769pub fn compute_submit_retryable_fees(params: &SubmitRetryableParams) -> SubmitRetryableFees {
775 let submission_fee =
776 retryables::retryable_submission_fee(params.retry_data_len, params.l1_base_fee);
777
778 let escrow = retryables::retryable_escrow_address(params.ticket_id);
779 let timeout = params.current_time + retryables::RETRYABLE_LIFETIME_SECONDS;
780
781 if params.balance_after_mint < params.max_submission_fee {
783 return SubmitRetryableFees {
784 submission_fee,
785 escrow,
786 timeout,
787 error: Some(format!(
788 "insufficient funds for max submission fee: have {} want {}",
789 params.balance_after_mint, params.max_submission_fee,
790 )),
791 ..Default::default()
792 };
793 }
794
795 if params.max_submission_fee < submission_fee {
797 return SubmitRetryableFees {
798 submission_fee,
799 escrow,
800 timeout,
801 error: Some(format!(
802 "max submission fee {} is less than actual {}",
803 params.max_submission_fee, submission_fee,
804 )),
805 ..Default::default()
806 };
807 }
808
809 let mut available_refund = params.deposit_value;
811 take_funds(&mut available_refund, params.retry_value);
812 let withheld_submission_fee = take_funds(&mut available_refund, submission_fee);
813 let submission_fee_refund = take_funds(
815 &mut available_refund,
816 params.max_submission_fee.saturating_sub(submission_fee),
817 );
818
819 let max_gas_cost = params.gas_fee_cap.saturating_mul(U256::from(params.gas));
821 let fee_cap_too_low = params.gas_fee_cap < params.effective_base_fee;
822
823 let mut balance_after_deductions = params
827 .balance_after_mint
828 .saturating_sub(submission_fee)
829 .saturating_sub(params.retry_value);
830 if params.fee_refund_addr != params.from {
831 balance_after_deductions = balance_after_deductions.saturating_sub(submission_fee_refund);
832 }
833
834 let can_pay_for_gas =
835 !fee_cap_too_low && params.gas >= TX_GAS && balance_after_deductions >= max_gas_cost;
836
837 let (infra_cost, network_cost) = compute_retryable_gas_split(
839 params.gas,
840 params.effective_base_fee,
841 params.infra_fee_account,
842 params.min_base_fee,
843 params.arbos_version,
844 );
845 let gas_cost = params
846 .effective_base_fee
847 .saturating_mul(U256::from(params.gas));
848
849 let gas_cost_refund = if !can_pay_for_gas {
851 take_funds(&mut available_refund, max_gas_cost)
852 } else {
853 U256::ZERO
854 };
855
856 let gas_price_refund = if params.gas_fee_cap > params.effective_base_fee {
858 (params.gas_fee_cap - params.effective_base_fee).saturating_mul(U256::from(params.gas))
859 } else {
860 U256::ZERO
861 };
862
863 let mut gas_price_refund_actual = U256::ZERO;
866
867 if can_pay_for_gas {
868 let withheld_gas_funds = take_funds(&mut available_refund, gas_cost);
870 gas_price_refund_actual = take_funds(&mut available_refund, gas_price_refund);
871 available_refund = available_refund
873 .saturating_add(withheld_gas_funds)
874 .saturating_add(withheld_submission_fee);
875 }
876
877 SubmitRetryableFees {
878 submission_fee,
879 submission_fee_refund,
880 escrow,
881 timeout,
882 can_pay_for_gas,
883 gas_cost,
884 infra_cost,
885 network_cost,
886 gas_price_refund: gas_price_refund_actual,
887 gas_cost_refund,
888 available_refund,
889 withheld_submission_fee,
890 error: None,
891 }
892}
893
894fn tx_data_non_zero_count(data: &[u8]) -> usize {
895 data.iter().filter(|&&b| b != 0).count()
896}
897
898fn tx_data_zero_count(data: &[u8]) -> usize {
899 data.iter().filter(|&&b| b == 0).count()
900}