arb_evm/multi_gas/
intrinsic.rs

1//! Transaction-level intrinsic gas split, mirroring Nitro's `IntrinsicMultiGas`.
2//!
3//! The intrinsic cost is charged before any opcode runs, so the inspector never
4//! sees it; it is classified here from the transaction fields and added to the
5//! per-tx accumulator.
6
7use arb_primitives::multigas::{MultiGas, ResourceKind};
8
9const TX_GAS: u64 = 21_000;
10const TX_GAS_CREATE: u64 = 53_000;
11const TX_DATA_NONZERO: u64 = 16; // TxDataNonZeroGasEIP2028
12const TX_DATA_ZERO: u64 = 4; // TxDataZeroGas
13const INIT_CODE_WORD: u64 = 2; // InitCodeWordGas
14const ACCESS_LIST_ADDRESS: u64 = 2_400; // TxAccessListAddressGas
15const ACCESS_LIST_KEY: u64 = 1_900; // TxAccessListStorageKeyGas
16const AUTH_ACCOUNT: u64 = 25_000; // CallNewAccountGas (EIP-7702)
17
18/// Transaction fields needed to split the intrinsic gas.
19#[derive(Debug, Clone, Copy)]
20pub struct IntrinsicInput {
21    pub is_create: bool,
22    pub zero_bytes: u64,
23    pub nonzero_bytes: u64,
24    pub init_code_words: u64,
25    pub access_list_addresses: u64,
26    pub access_list_keys: u64,
27    pub auth_list_len: u64,
28}
29
30/// Split the intrinsic gas across resource kinds: base and init-code words are
31/// computation, calldata bytes are L2 calldata, access-list entries are storage
32/// reads, and EIP-7702 authorizations are storage growth.
33pub fn intrinsic_multigas(input: IntrinsicInput) -> MultiGas {
34    let base = if input.is_create {
35        TX_GAS_CREATE
36    } else {
37        TX_GAS
38    };
39    let init_code = if input.is_create {
40        input.init_code_words * INIT_CODE_WORD
41    } else {
42        0
43    };
44    let calldata = input.nonzero_bytes * TX_DATA_NONZERO + input.zero_bytes * TX_DATA_ZERO;
45    let access = input.access_list_addresses * ACCESS_LIST_ADDRESS
46        + input.access_list_keys * ACCESS_LIST_KEY;
47    let auth = input.auth_list_len * AUTH_ACCOUNT;
48
49    MultiGas::from_pairs(&[
50        (ResourceKind::Computation, base + init_code),
51        (ResourceKind::L2Calldata, calldata),
52        (ResourceKind::StorageAccessRead, access),
53        (ResourceKind::StorageGrowth, auth),
54    ])
55}
56
57#[cfg(test)]
58mod tests {
59    use arb_primitives::multigas::ResourceKind::*;
60
61    use super::*;
62
63    fn input() -> IntrinsicInput {
64        IntrinsicInput {
65            is_create: false,
66            zero_bytes: 0,
67            nonzero_bytes: 0,
68            init_code_words: 0,
69            access_list_addresses: 0,
70            access_list_keys: 0,
71            auth_list_len: 0,
72        }
73    }
74
75    #[test]
76    fn bare_call() {
77        let mg = intrinsic_multigas(input());
78        assert_eq!(mg.get(Computation), 21_000);
79        assert_eq!(mg.single_gas(), 21_000);
80    }
81
82    #[test]
83    fn calldata_goes_to_l2_calldata() {
84        let mg = intrinsic_multigas(IntrinsicInput {
85            zero_bytes: 5,
86            nonzero_bytes: 10,
87            ..input()
88        });
89        assert_eq!(mg.get(Computation), 21_000);
90        assert_eq!(mg.get(L2Calldata), 10 * 16 + 5 * 4);
91        assert_eq!(mg.single_gas(), 21_000 + 180);
92    }
93
94    #[test]
95    fn creation_base_and_init_words() {
96        let mg = intrinsic_multigas(IntrinsicInput {
97            is_create: true,
98            init_code_words: 7,
99            ..input()
100        });
101        assert_eq!(mg.get(Computation), 53_000 + 7 * 2);
102    }
103
104    #[test]
105    fn access_list_is_storage_read() {
106        let mg = intrinsic_multigas(IntrinsicInput {
107            access_list_addresses: 2,
108            access_list_keys: 3,
109            ..input()
110        });
111        assert_eq!(mg.get(StorageAccessRead), 2 * 2_400 + 3 * 1_900);
112    }
113
114    #[test]
115    fn auth_list_is_storage_growth() {
116        let mg = intrinsic_multigas(IntrinsicInput {
117            auth_list_len: 2,
118            ..input()
119        });
120        assert_eq!(mg.get(StorageGrowth), 2 * 25_000);
121    }
122}