1use std::collections::HashMap;
2
3use alloy_primitives::{Address, B256, U256};
4use arb_chainspec::arbos_version as arb_ver;
5
6use crate::{l1_pricing, retryables, util::BalanceError};
7
8pub const ARBOS_ADDRESS: Address = {
10 let mut bytes = [0u8; 20];
11 bytes[17] = 0x0a;
12 bytes[18] = 0x4b;
13 bytes[19] = 0x05;
14 Address::new(bytes)
15};
16
17pub const GAS_ESTIMATION_L1_PRICE_PADDING_BIPS: u64 = 11000;
19
20#[derive(Debug)]
26pub struct TxProcessor {
27 pub poster_fee: U256,
29 pub poster_gas: u64,
31 pub compute_hold_gas: u64,
33 pub delayed_inbox: bool,
35 pub top_tx_type: Option<u8>,
37 pub current_retryable: Option<B256>,
39 pub current_refund_to: Option<Address>,
41 pub scheduled_txs: Vec<Vec<u8>>,
43 pub programs_depth: HashMap<Address, usize>,
46}
47
48impl Default for TxProcessor {
49 fn default() -> Self {
50 Self {
51 poster_fee: U256::ZERO,
52 poster_gas: 0,
53 compute_hold_gas: 0,
54 delayed_inbox: false,
55 top_tx_type: None,
56 current_retryable: None,
57 current_refund_to: None,
58 scheduled_txs: Vec::new(),
59 programs_depth: HashMap::new(),
60 }
61 }
62}
63
64impl TxProcessor {
65 pub fn new(coinbase: Address) -> Self {
68 Self {
69 delayed_inbox: coinbase != l1_pricing::BATCH_POSTER_ADDRESS,
70 ..Self::default()
71 }
72 }
73
74 pub fn nonrefundable_gas(&self) -> u64 {
76 self.poster_gas
77 }
78
79 pub fn held_gas(&self) -> u64 {
81 self.compute_hold_gas
82 }
83
84 pub fn drop_tip(&self, arbos_version: u64) -> bool {
86 self.drop_tip_with_collect(arbos_version, false)
87 }
88
89 pub fn drop_tip_with_collect(&self, arbos_version: u64, collect_tips_enabled: bool) -> bool {
95 if self.delayed_inbox {
96 return true;
97 }
98 if arbos_version == 9 {
99 return false;
100 }
101 if arbos_version < 60 {
102 return true;
103 }
104 !collect_tips_enabled
105 }
106
107 pub fn get_paid_gas_price(&self, arbos_version: u64, base_fee: U256, gas_price: U256) -> U256 {
109 self.get_paid_gas_price_with_collect(arbos_version, base_fee, gas_price, false)
110 }
111
112 pub fn get_paid_gas_price_with_collect(
114 &self,
115 arbos_version: u64,
116 base_fee: U256,
117 gas_price: U256,
118 collect_tips_enabled: bool,
119 ) -> U256 {
120 if !self.drop_tip_with_collect(arbos_version, collect_tips_enabled) {
122 gas_price
123 } else {
124 base_fee
125 }
126 }
127
128 pub fn gas_price_op(&self, arbos_version: u64, base_fee: U256, gas_price: U256) -> U256 {
130 self.gas_price_op_with_collect(arbos_version, base_fee, gas_price, false)
131 }
132
133 pub fn gas_price_op_with_collect(
135 &self,
136 arbos_version: u64,
137 base_fee: U256,
138 gas_price: U256,
139 collect_tips_enabled: bool,
140 ) -> U256 {
141 if arbos_version >= 3 {
142 self.get_paid_gas_price_with_collect(
143 arbos_version,
144 base_fee,
145 gas_price,
146 collect_tips_enabled,
147 )
148 } else {
149 gas_price
150 }
151 }
152
153 pub fn fill_receipt_gas_used_for_l1(&self) -> u64 {
155 self.poster_gas
156 }
157
158 pub fn push_program(&mut self, addr: Address) {
164 *self.programs_depth.entry(addr).or_insert(0) += 1;
165 }
166
167 pub fn pop_program(&mut self, addr: Address) {
169 if let Some(count) = self.programs_depth.get_mut(&addr) {
170 *count = count.saturating_sub(1);
171 if *count == 0 {
172 self.programs_depth.remove(&addr);
173 }
174 }
175 }
176
177 pub fn is_reentrant(&self, addr: &Address) -> bool {
179 self.programs_depth.get(addr).copied().unwrap_or(0) > 1
180 }
181
182 pub fn reverted_tx_hook(
196 &self,
197 tx_hash: Option<B256>,
198 pre_recorded_gas: Option<u64>,
199 is_filtered: bool,
200 ) -> RevertedTxAction {
201 let Some(hash) = tx_hash else {
202 return RevertedTxAction::None;
203 };
204
205 let l2_gas_used = pre_recorded_gas.or_else(|| crate::reverted_tx_gas::lookup(hash));
206 if let Some(g) = l2_gas_used {
207 let adjusted_gas = g.saturating_sub(TX_GAS);
208 return RevertedTxAction::PreRecordedRevert {
209 gas_to_consume: adjusted_gas,
210 };
211 }
212
213 if is_filtered {
214 return RevertedTxAction::FilteredTx;
215 }
216
217 RevertedTxAction::None
218 }
219
220 pub fn set_tx_type(&mut self, tx_type: u8) {
226 self.top_tx_type = Some(tx_type);
227 }
228
229 pub fn prepare_retry_tx(&mut self, ticket_id: B256, refund_to: Address) {
237 self.current_retryable = Some(ticket_id);
238 self.current_refund_to = Some(refund_to);
239 }
240
241 pub fn gas_charging_hook(
251 &mut self,
252 gas_remaining: &mut u64,
253 intrinsic_gas: u64,
254 params: &GasChargingParams,
255 ) -> Result<(), GasChargingError> {
256 let mut gas_needed = 0u64;
257
258 if !params.base_fee.is_zero() && !params.skip_l1_charging {
259 self.poster_gas = compute_poster_gas(
260 params.poster_cost,
261 params.base_fee,
262 params.is_gas_estimation,
263 params.min_base_fee,
264 );
265 self.poster_fee = params.base_fee.saturating_mul(U256::from(self.poster_gas));
266 gas_needed = self.poster_gas;
267 }
268
269 if *gas_remaining < gas_needed {
270 return Err(GasChargingError::IntrinsicGasTooLow);
271 }
272 *gas_remaining -= gas_needed;
273
274 if !params.is_eth_call {
276 let max = if params.arbos_version < arb_ver::ARBOS_VERSION_50 {
277 params.per_block_gas_limit
278 } else {
279 params.per_tx_gas_limit.saturating_sub(intrinsic_gas)
281 };
282
283 if *gas_remaining > max {
284 self.compute_hold_gas = *gas_remaining - max;
285 *gas_remaining = max;
286 }
287 }
288
289 Ok(())
290 }
291
292 pub fn compute_end_tx_fee_distribution(
301 &self,
302 params: &EndTxNormalParams,
303 ) -> EndTxFeeDistribution {
304 let gas_used = params.gas_used;
305 let base_fee = params.base_fee;
306
307 let compute_gas = gas_used.saturating_sub(self.poster_gas);
312 let mut compute_cost = base_fee.saturating_mul(U256::from(compute_gas));
313 let poster_fee = self.poster_fee;
314
315 let mut infra_fee_amount = U256::ZERO;
316
317 if params.arbos_version > 4 && params.infra_fee_account != Address::ZERO {
318 let infra_fee = params.min_base_fee.min(base_fee);
319 infra_fee_amount = infra_fee.saturating_mul(U256::from(compute_gas));
320 compute_cost = compute_cost.saturating_sub(infra_fee_amount);
321 }
322
323 let poster_fee_destination = if params.arbos_version < 2 {
324 params.coinbase
325 } else {
326 l1_pricing::L1_PRICER_FUNDS_POOL_ADDRESS
327 };
328
329 let l1_fees_to_add = if params.arbos_version >= arb_ver::ARBOS_VERSION_10 {
330 poster_fee
331 } else {
332 U256::ZERO
333 };
334
335 let compute_gas_for_backlog = if !params.gas_price.is_zero() {
336 if gas_used > self.poster_gas {
337 gas_used - self.poster_gas
338 } else {
339 tracing::error!(
340 gas_used,
341 poster_gas = self.poster_gas,
342 "gas used < poster gas"
343 );
344 gas_used
345 }
346 } else {
347 0
348 };
349
350 EndTxFeeDistribution {
351 infra_fee_account: params.infra_fee_account,
352 infra_fee_amount,
353 network_fee_account: params.network_fee_account,
354 network_fee_amount: compute_cost,
355 poster_fee_destination,
356 poster_fee_amount: poster_fee,
357 l1_fees_to_add,
358 compute_gas_for_backlog,
359 }
360 }
361
362 pub fn end_tx_retryable<F>(
372 &self,
373 params: &EndTxRetryableParams,
374 mut burn_fn: impl FnMut(Address, U256),
375 mut transfer_fn: F,
376 ) -> EndTxRetryableResult
377 where
378 F: FnMut(Address, Address, U256) -> Result<(), BalanceError>,
379 {
380 let effective_base_fee = params.effective_base_fee;
381 let gas_left = params.gas_left;
382 let gas_used = params.gas_used;
383
384 let gas_refund_amount = effective_base_fee.saturating_mul(U256::from(gas_left));
385 burn_fn(params.from, gas_refund_amount);
386
387 let single_gas_cost = effective_base_fee.saturating_mul(U256::from(gas_used));
388
389 let mut max_refund = params.max_refund;
390
391 if params.success {
392 refund_with_pool(
393 params.network_fee_account,
394 params.submission_fee_refund,
395 &mut max_refund,
396 params.refund_to,
397 params.from,
398 &mut transfer_fn,
399 );
400 } else {
401 take_funds(&mut max_refund, params.submission_fee_refund);
402 }
403
404 take_funds(&mut max_refund, single_gas_cost);
405
406 let mut network_refund = gas_refund_amount;
407
408 if params.arbos_version >= arb_ver::ARBOS_VERSION_11
409 && params.infra_fee_account != Address::ZERO
410 {
411 let infra_fee = params.min_base_fee.min(effective_base_fee);
412 let infra_refund_amount = infra_fee.saturating_mul(U256::from(gas_left));
413 let infra_refund = take_funds(&mut network_refund, infra_refund_amount);
414 refund_with_pool(
415 params.infra_fee_account,
416 infra_refund,
417 &mut max_refund,
418 params.refund_to,
419 params.from,
420 &mut transfer_fn,
421 );
422 }
423
424 refund_with_pool(
425 params.network_fee_account,
426 network_refund,
427 &mut max_refund,
428 params.refund_to,
429 params.from,
430 &mut transfer_fn,
431 );
432
433 if let Some(multi_cost) = params.multi_dimensional_cost {
437 let should_refund =
438 single_gas_cost > multi_cost && effective_base_fee == params.block_base_fee;
439 if should_refund {
440 let refund_amount = single_gas_cost.saturating_sub(multi_cost);
441 refund_with_pool(
442 params.network_fee_account,
443 refund_amount,
444 &mut max_refund,
445 params.refund_to,
446 params.from,
447 &mut transfer_fn,
448 );
449 }
450 }
451
452 let escrow = retryables::retryable_escrow_address(params.ticket_id);
453
454 EndTxRetryableResult {
455 compute_gas_for_backlog: gas_used,
456 should_delete_retryable: params.success,
457 should_return_value_to_escrow: !params.success,
458 escrow_address: escrow,
459 }
460 }
461}
462
463#[derive(Debug, Clone)]
469pub struct GasChargingParams {
470 pub base_fee: U256,
472 pub poster_cost: U256,
474 pub is_gas_estimation: bool,
476 pub is_eth_call: bool,
478 pub skip_l1_charging: bool,
480 pub min_base_fee: U256,
482 pub per_block_gas_limit: u64,
484 pub per_tx_gas_limit: u64,
486 pub arbos_version: u64,
488}
489
490#[derive(Debug, Clone, thiserror::Error)]
492pub enum GasChargingError {
493 #[error("intrinsic gas too low")]
494 IntrinsicGasTooLow,
495}
496
497#[derive(Debug, Clone)]
499pub struct EndTxNormalParams {
500 pub gas_used: u64,
501 pub gas_price: U256,
502 pub base_fee: U256,
503 pub coinbase: Address,
504 pub network_fee_account: Address,
505 pub infra_fee_account: Address,
506 pub min_base_fee: U256,
507 pub arbos_version: u64,
508}
509
510#[derive(Debug, Clone, Default)]
518pub struct EndTxFeeDistribution {
519 pub infra_fee_account: Address,
520 pub infra_fee_amount: U256,
521 pub network_fee_account: Address,
522 pub network_fee_amount: U256,
523 pub poster_fee_destination: Address,
524 pub poster_fee_amount: U256,
525 pub l1_fees_to_add: U256,
526 pub compute_gas_for_backlog: u64,
527}
528
529#[derive(Debug, Clone)]
531pub struct EndTxRetryableParams {
532 pub gas_left: u64,
533 pub gas_used: u64,
534 pub effective_base_fee: U256,
535 pub from: Address,
536 pub refund_to: Address,
537 pub max_refund: U256,
538 pub submission_fee_refund: U256,
539 pub ticket_id: B256,
540 pub value: U256,
541 pub success: bool,
542 pub network_fee_account: Address,
543 pub infra_fee_account: Address,
544 pub min_base_fee: U256,
545 pub arbos_version: u64,
546 pub multi_dimensional_cost: Option<U256>,
549 pub block_base_fee: U256,
553}
554
555#[derive(Debug, Clone)]
562pub struct EndTxRetryableResult {
563 pub compute_gas_for_backlog: u64,
564 pub should_delete_retryable: bool,
565 pub should_return_value_to_escrow: bool,
566 pub escrow_address: Address,
567}
568
569#[derive(Debug, Clone, PartialEq, Eq)]
571pub enum RevertedTxAction {
572 None,
574 PreRecordedRevert { gas_to_consume: u64 },
576 FilteredTx,
578}
579
580#[derive(Debug, Clone)]
582pub struct SubmitRetryableParams {
583 pub ticket_id: B256,
584 pub from: Address,
585 pub fee_refund_addr: Address,
586 pub deposit_value: U256,
587 pub retry_value: U256,
588 pub gas_fee_cap: U256,
589 pub gas: u64,
590 pub max_submission_fee: U256,
591 pub retry_data_len: usize,
592 pub l1_base_fee: U256,
593 pub effective_base_fee: U256,
594 pub current_time: u64,
595 pub balance_after_mint: U256,
597 pub infra_fee_account: Address,
598 pub min_base_fee: U256,
599 pub arbos_version: u64,
600}
601
602#[derive(Debug, Clone, Default)]
617pub struct SubmitRetryableFees {
618 pub submission_fee: U256,
620 pub submission_fee_refund: U256,
622 pub escrow: Address,
624 pub timeout: u64,
626 pub can_pay_for_gas: bool,
628 pub gas_cost: U256,
630 pub infra_cost: U256,
632 pub network_cost: U256,
634 pub gas_price_refund: U256,
636 pub gas_cost_refund: U256,
638 pub available_refund: U256,
640 pub withheld_submission_fee: U256,
642 pub error: Option<String>,
644}
645
646pub const TX_GAS: u64 = 21_000;
648
649pub fn take_funds(pool: &mut U256, take: U256) -> U256 {
656 if *pool < take {
657 let old = *pool;
658 *pool = U256::ZERO;
659 old
660 } else {
661 *pool -= take;
662 take
663 }
664}
665
666pub fn compute_poster_gas(
669 poster_cost: U256,
670 base_fee: U256,
671 is_gas_estimation: bool,
672 min_gas_price: U256,
673) -> u64 {
674 if base_fee.is_zero() {
675 return 0;
676 }
677
678 let adjusted_base_fee = if is_gas_estimation {
679 let adjusted = base_fee * U256::from(7) / U256::from(8);
681 if adjusted < min_gas_price {
682 min_gas_price
683 } else {
684 adjusted
685 }
686 } else {
687 base_fee
688 };
689
690 let padded_cost = if is_gas_estimation {
691 poster_cost * U256::from(GAS_ESTIMATION_L1_PRICE_PADDING_BIPS) / U256::from(10000)
692 } else {
693 poster_cost
694 };
695
696 if adjusted_base_fee.is_zero() {
697 return 0;
698 }
699
700 let gas = padded_cost / adjusted_base_fee;
701 gas.try_into().unwrap_or(u64::MAX)
702}
703
704pub fn get_poster_gas(
710 tx_data: &[u8],
711 l1_base_fee: U256,
712 l2_base_fee: U256,
713 _arbos_version: u64,
714) -> (u64, u64) {
715 if l2_base_fee.is_zero() || l1_base_fee.is_zero() {
716 return (0, 0);
717 }
718
719 let calldata_units = tx_data_non_zero_count(tx_data) * 16 + tx_data_zero_count(tx_data) * 4;
720
721 let l1_cost = U256::from(calldata_units) * l1_base_fee;
722 let poster_gas = l1_cost / l2_base_fee;
723 let poster_gas_u64: u64 = poster_gas.try_into().unwrap_or(u64::MAX);
724
725 (poster_gas_u64, calldata_units as u64)
726}
727
728fn refund_with_pool<F>(
733 refund_from: Address,
734 amount: U256,
735 max_refund: &mut U256,
736 refund_to: Address,
737 from: Address,
738 transfer_fn: &mut F,
739) where
740 F: FnMut(Address, Address, U256) -> Result<(), BalanceError>,
741{
742 let to_refund_addr = take_funds(max_refund, amount);
743 let _ = transfer_fn(refund_from, refund_to, to_refund_addr);
748 let remainder = amount.saturating_sub(to_refund_addr);
749 let _ = transfer_fn(refund_from, from, remainder);
750}
751
752pub fn compute_retryable_gas_split(
756 gas: u64,
757 effective_base_fee: U256,
758 infra_fee_account: Address,
759 min_base_fee: U256,
760 arbos_version: u64,
761) -> (U256, U256) {
762 let gas_cost = effective_base_fee.saturating_mul(U256::from(gas));
763 let mut network_cost = gas_cost;
764 let mut infra_cost = U256::ZERO;
765
766 if arbos_version >= arb_ver::ARBOS_VERSION_11 && infra_fee_account != Address::ZERO {
767 let infra_fee = min_base_fee.min(effective_base_fee);
768 infra_cost = infra_fee.saturating_mul(U256::from(gas));
769 infra_cost = take_funds(&mut network_cost, infra_cost);
770 }
771
772 (infra_cost, network_cost)
773}
774
775pub fn compute_submit_retryable_fees(params: &SubmitRetryableParams) -> SubmitRetryableFees {
781 let submission_fee =
782 retryables::retryable_submission_fee(params.retry_data_len, params.l1_base_fee);
783
784 let escrow = retryables::retryable_escrow_address(params.ticket_id);
785 let timeout = params.current_time + retryables::RETRYABLE_LIFETIME_SECONDS;
786
787 if params.balance_after_mint < params.max_submission_fee {
789 return SubmitRetryableFees {
790 submission_fee,
791 escrow,
792 timeout,
793 error: Some(format!(
794 "insufficient funds for max submission fee: have {} want {}",
795 params.balance_after_mint, params.max_submission_fee,
796 )),
797 ..Default::default()
798 };
799 }
800
801 if params.max_submission_fee < submission_fee {
803 return SubmitRetryableFees {
804 submission_fee,
805 escrow,
806 timeout,
807 error: Some(format!(
808 "max submission fee {} is less than actual {}",
809 params.max_submission_fee, submission_fee,
810 )),
811 ..Default::default()
812 };
813 }
814
815 let mut available_refund = params.deposit_value;
817 take_funds(&mut available_refund, params.retry_value);
818 let withheld_submission_fee = take_funds(&mut available_refund, submission_fee);
819 let submission_fee_refund = take_funds(
821 &mut available_refund,
822 params.max_submission_fee.saturating_sub(submission_fee),
823 );
824
825 let max_gas_cost = params.gas_fee_cap.saturating_mul(U256::from(params.gas));
827 let fee_cap_too_low = params.gas_fee_cap < params.effective_base_fee;
828
829 let mut balance_after_deductions = params
833 .balance_after_mint
834 .saturating_sub(submission_fee)
835 .saturating_sub(params.retry_value);
836 if params.fee_refund_addr != params.from {
837 balance_after_deductions = balance_after_deductions.saturating_sub(submission_fee_refund);
838 }
839
840 let can_pay_for_gas =
841 !fee_cap_too_low && params.gas >= TX_GAS && balance_after_deductions >= max_gas_cost;
842
843 let (infra_cost, network_cost) = compute_retryable_gas_split(
845 params.gas,
846 params.effective_base_fee,
847 params.infra_fee_account,
848 params.min_base_fee,
849 params.arbos_version,
850 );
851 let gas_cost = params
852 .effective_base_fee
853 .saturating_mul(U256::from(params.gas));
854
855 let gas_cost_refund = if !can_pay_for_gas {
857 take_funds(&mut available_refund, max_gas_cost)
858 } else {
859 U256::ZERO
860 };
861
862 let gas_price_refund = if params.gas_fee_cap > params.effective_base_fee {
864 (params.gas_fee_cap - params.effective_base_fee).saturating_mul(U256::from(params.gas))
865 } else {
866 U256::ZERO
867 };
868
869 let mut gas_price_refund_actual = U256::ZERO;
872
873 if can_pay_for_gas {
874 let withheld_gas_funds = take_funds(&mut available_refund, gas_cost);
876 gas_price_refund_actual = take_funds(&mut available_refund, gas_price_refund);
877 available_refund = available_refund
879 .saturating_add(withheld_gas_funds)
880 .saturating_add(withheld_submission_fee);
881 }
882
883 SubmitRetryableFees {
884 submission_fee,
885 submission_fee_refund,
886 escrow,
887 timeout,
888 can_pay_for_gas,
889 gas_cost,
890 infra_cost,
891 network_cost,
892 gas_price_refund: gas_price_refund_actual,
893 gas_cost_refund,
894 available_refund,
895 withheld_submission_fee,
896 error: None,
897 }
898}
899
900fn tx_data_non_zero_count(data: &[u8]) -> usize {
901 data.iter().filter(|&&b| b != 0).count()
902}
903
904fn tx_data_zero_count(data: &[u8]) -> usize {
905 data.iter().filter(|&&b| b == 0).count()
906}
907
908#[cfg(test)]
909mod block1_retryable_repro {
910 use alloy_primitives::{Address, Bytes, U256, address, b256, keccak256};
911 use arb_alloy_consensus::tx::{ArbRetryTx, ArbTxType};
912
913 use super::{SubmitRetryableParams, compute_submit_retryable_fees};
914
915 #[test]
918 fn canonical_block1_auto_redeem_hash() {
919 let params = SubmitRetryableParams {
920 ticket_id: b256!("13cb79b086a427f3db7ebe6ec2bb90a806a3b0368ecee6020144f352e37dbdf6"),
921 from: address!("b8787d8f23e176a5d32135d746b69886e03313be"),
922 fee_refund_addr: address!("11155ca9bbf7be58e27f3309e629c847996b43c8"),
923 deposit_value: U256::from(0x23e3dbb7b88ab8u64),
924 retry_value: U256::from(0x2386f26fc10000u64),
925 gas_fee_cap: U256::from(0x3b9aca00u64),
926 gas: 100_000,
927 max_submission_fee: U256::from(0x1f6377d4ab8u64),
928 retry_data_len: 0,
929 l1_base_fee: U256::from(0x5bd57bd9u64),
930 effective_base_fee: U256::from(0x5f5e100u64),
931 current_time: 0,
932 balance_after_mint: U256::from(1_000_000_000_000_000_000u64),
933 infra_fee_account: Address::ZERO,
934 min_base_fee: U256::ZERO,
935 arbos_version: 10,
936 };
937 let fees = compute_submit_retryable_fees(¶ms);
938 assert!(fees.can_pay_for_gas, "expected auto-redeem path");
939 let retry = ArbRetryTx {
940 chain_id: U256::from(421614u64),
941 nonce: 0,
942 from: params.from,
943 gas_fee_cap: params.effective_base_fee,
944 gas: params.gas,
945 to: Some(address!("3fab184622dc19b6109349b94811493bf2a45362")),
946 value: params.retry_value,
947 data: Bytes::new(),
948 ticket_id: params.ticket_id,
949 refund_to: params.fee_refund_addr,
950 max_refund: fees.available_refund,
951 submission_fee_refund: fees.submission_fee,
952 };
953 let mut enc = Vec::new();
954 enc.push(ArbTxType::ArbitrumRetryTx.as_u8());
955 alloy_rlp::Encodable::encode(&retry, &mut enc);
956 assert_eq!(
957 keccak256(&enc),
958 b256!("873c5ee3092c40336006808e249293bf5f4cb3235077a74cac9cafa7cf73cb8b"),
959 "mismatch: available_refund=0x{:x} submission_fee=0x{:x}",
960 fees.available_refund,
961 fees.submission_fee
962 );
963 }
964}