Skip to main content

revmc_codegen/bytecode/
info.rs

1use revm_bytecode::opcode as op;
2use revm_interpreter::{
3    host::DummyHost, instructions::instruction_table_gas_changes_spec, interpreter::EthInterpreter,
4};
5use revm_primitives::hardfork::SpecId;
6
7/// Opcode information.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct OpcodeInfo(u32);
10
11impl OpcodeInfo {
12    /// The unknown flag.
13    pub const UNKNOWN: u32 = 1 << 31;
14    /// The dynamic flag.
15    pub const DYNAMIC: u32 = 1 << 30;
16    /// The disabled flag.
17    pub const DISABLED: u32 = 1 << 29;
18    /// The mask for the gas cost (16 bits, max 65535).
19    pub const MASK: u32 = 0xFFFF;
20
21    /// Creates a new gas info with the given gas cost.
22    #[inline]
23    pub const fn new(gas: u16) -> Self {
24        Self(gas as u32)
25    }
26
27    /// Returns `true` if the opcode is unknown.
28    #[inline]
29    pub const fn is_unknown(self) -> bool {
30        self.0 == Self::UNKNOWN
31    }
32
33    /// Returns `true` if the gas cost is dynamic.
34    #[inline]
35    pub const fn is_dynamic(self) -> bool {
36        self.0 & Self::DYNAMIC != 0
37    }
38
39    /// Returns `true` if the opcode is known, but disabled in the current EVM
40    /// version.
41    #[inline]
42    pub const fn is_disabled(self) -> bool {
43        self.0 & Self::DISABLED != 0
44    }
45
46    /// Returns the base gas cost of the opcode.
47    ///
48    /// This may not be the final/full gas cost of the opcode as it may also have a dynamic cost.
49    #[inline]
50    pub const fn base_gas(self) -> u16 {
51        (self.0 & Self::MASK) as u16
52    }
53
54    /// Sets the unknown flag.
55    #[inline]
56    pub fn set_unknown(&mut self) {
57        self.0 = Self::UNKNOWN;
58    }
59
60    /// Sets the dynamic flag.
61    #[inline]
62    pub fn set_dynamic(&mut self) {
63        self.0 |= Self::DYNAMIC;
64    }
65
66    /// Sets the disabled flag.
67    #[inline]
68    pub fn set_disabled(&mut self) {
69        self.0 |= Self::DISABLED;
70    }
71
72    /// Sets the gas cost.
73    #[inline]
74    pub fn set_gas(&mut self, gas: u16) {
75        self.0 = (self.0 & !Self::MASK) | (gas as u32);
76    }
77}
78
79/// Returns the static info map for the given `SpecId`.
80pub fn op_info_map(spec_id: SpecId) -> &'static [OpcodeInfo; 256] {
81    use std::sync::OnceLock;
82    static MAPS: OnceLock<[[OpcodeInfo; 256]; 32]> = OnceLock::new();
83    let maps = MAPS.get_or_init(|| {
84        let mut maps = [[OpcodeInfo(OpcodeInfo::UNKNOWN); 256]; 32];
85        for (i, map) in maps.iter_mut().enumerate() {
86            if let Ok(spec) = SpecId::try_from(i as u8) {
87                *map = make_map(spec);
88            }
89        }
90        maps
91    });
92    &maps[spec_id as usize]
93}
94
95/// Opcodes with a dynamic gas component that also have a base (static) cost deducted upfront.
96/// Only the dynamic part is paid in builtins.
97const DYNAMIC_WITH_BASE_GAS: &[u8] = &[
98    op::EXP,
99    op::KECCAK256,
100    op::BALANCE,
101    op::CALLDATACOPY,
102    op::CODECOPY,
103    op::EXTCODESIZE,
104    op::EXTCODECOPY,
105    op::RETURNDATACOPY,
106    op::EXTCODEHASH,
107    op::MLOAD,
108    op::MSTORE,
109    op::MSTORE8,
110    op::MCOPY,
111    op::LOG0,
112    op::LOG1,
113    op::LOG2,
114    op::LOG3,
115    op::LOG4,
116    op::SLOAD,
117    op::CALL,
118    op::CALLCODE,
119    op::DELEGATECALL,
120    op::STATICCALL,
121    op::SELFDESTRUCT,
122];
123
124/// Opcodes whose gas cost is entirely dynamic — computed fully in builtins at runtime.
125const FULLY_DYNAMIC: &[u8] = &[op::SSTORE, op::CREATE, op::CREATE2];
126
127/// Opcodes that are gated behind a specific `SpecId`, paired with the spec they were introduced in.
128const SPEC_GATED_OPCODES: &[(u8, SpecId)] = &[
129    (op::SHL, SpecId::CONSTANTINOPLE),
130    (op::SHR, SpecId::CONSTANTINOPLE),
131    (op::SAR, SpecId::CONSTANTINOPLE),
132    (op::CLZ, SpecId::OSAKA),
133    (op::RETURNDATASIZE, SpecId::BYZANTIUM),
134    (op::RETURNDATACOPY, SpecId::BYZANTIUM),
135    (op::EXTCODEHASH, SpecId::CONSTANTINOPLE),
136    (op::CHAINID, SpecId::ISTANBUL),
137    (op::SELFBALANCE, SpecId::ISTANBUL),
138    (op::BASEFEE, SpecId::LONDON),
139    (op::BLOBHASH, SpecId::CANCUN),
140    (op::BLOBBASEFEE, SpecId::CANCUN),
141    (op::TLOAD, SpecId::CANCUN),
142    (op::TSTORE, SpecId::CANCUN),
143    (op::MCOPY, SpecId::CANCUN),
144    (op::PUSH0, SpecId::SHANGHAI),
145    (op::DELEGATECALL, SpecId::HOMESTEAD),
146    (op::CREATE2, SpecId::PETERSBURG),
147    (op::STATICCALL, SpecId::BYZANTIUM),
148    (op::REVERT, SpecId::BYZANTIUM),
149    (op::DUPN, SpecId::AMSTERDAM),
150    (op::SWAPN, SpecId::AMSTERDAM),
151    (op::EXCHANGE, SpecId::AMSTERDAM),
152    (op::SLOTNUM, SpecId::AMSTERDAM),
153];
154
155/// Opcodes present in the upstream instruction table but not supported by revmc (e.g. EOF-only).
156const UNSUPPORTED_OPCODES: &[u8] = &[];
157
158fn make_map(spec_id: SpecId) -> [OpcodeInfo; 256] {
159    let table = instruction_table_gas_changes_spec::<EthInterpreter, DummyHost>(spec_id);
160
161    let mut map = [OpcodeInfo(OpcodeInfo::UNKNOWN); 256];
162
163    for i in 0..256u16 {
164        let op = i as u8;
165
166        // Skip opcodes not defined in revm's opcode table.
167        if revm_bytecode::opcode::OpCode::new(op).is_none() {
168            continue;
169        }
170
171        // Mark opcodes not supported by revmc (e.g. EOF-only) as disabled rather than
172        // unknown, so they return `NotActivated` instead of `OpcodeNotFound` at runtime.
173        if UNSUPPORTED_OPCODES.contains(&op) {
174            map[op as usize].set_disabled();
175            continue;
176        }
177
178        let is_fully_dynamic = FULLY_DYNAMIC.contains(&op);
179        let is_dynamic_with_base = DYNAMIC_WITH_BASE_GAS.contains(&op);
180
181        // Fully dynamic opcodes have their entire gas cost handled in builtins.
182        let gas = if is_fully_dynamic {
183            0u16
184        } else {
185            let static_gas = table[op as usize].static_gas();
186            assert!(
187                static_gas <= OpcodeInfo::MASK as u64,
188                "static gas for opcode 0x{op:02X} exceeds OpcodeInfo capacity: {static_gas}"
189            );
190            static_gas as u16
191        };
192
193        let mut info = OpcodeInfo::new(gas);
194
195        if is_fully_dynamic || is_dynamic_with_base {
196            info.set_dynamic();
197        }
198
199        map[op as usize] = info;
200    }
201
202    // Apply spec-gating: mark opcodes as disabled if the current spec is before their introduction.
203    for &(op, required_spec) in SPEC_GATED_OPCODES {
204        if (spec_id as u8) < (required_spec as u8) {
205            map[op as usize].set_disabled();
206        }
207    }
208
209    map
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_clz_flags() {
218        let arrow_glacier = op_info_map(SpecId::ARROW_GLACIER);
219        let cancun = op_info_map(SpecId::CANCUN);
220        let osaka = op_info_map(SpecId::OSAKA);
221
222        let clz_ag = arrow_glacier[op::CLZ as usize];
223        let clz_cancun = cancun[op::CLZ as usize];
224        let clz_osaka = osaka[op::CLZ as usize];
225
226        eprintln!(
227            "CLZ on ARROW_GLACIER: is_unknown={}, is_disabled={}, raw={:#06x}",
228            clz_ag.is_unknown(),
229            clz_ag.is_disabled(),
230            clz_ag.0
231        );
232        eprintln!(
233            "CLZ on CANCUN: is_unknown={}, is_disabled={}, raw={:#06x}",
234            clz_cancun.is_unknown(),
235            clz_cancun.is_disabled(),
236            clz_cancun.0
237        );
238        eprintln!(
239            "CLZ on OSAKA: is_unknown={}, is_disabled={}, raw={:#06x}",
240            clz_osaka.is_unknown(),
241            clz_osaka.is_disabled(),
242            clz_osaka.0
243        );
244
245        assert!(!clz_ag.is_unknown(), "CLZ should not be unknown on pre-OSAKA");
246        assert!(clz_ag.is_disabled(), "CLZ should be disabled on ARROW_GLACIER");
247        assert!(!clz_cancun.is_unknown(), "CLZ should not be unknown on pre-OSAKA");
248        assert!(clz_cancun.is_disabled(), "CLZ should be disabled on CANCUN");
249        assert!(!clz_osaka.is_unknown(), "CLZ should not be unknown on OSAKA");
250        assert!(!clz_osaka.is_disabled(), "CLZ should not be disabled on OSAKA");
251    }
252
253    #[test]
254    fn test_gas_values() {
255        let cancun = op_info_map(SpecId::CANCUN);
256
257        // Basic arithmetic.
258        assert_eq!(cancun[op::ADD as usize].base_gas(), 3);
259        assert_eq!(cancun[op::MUL as usize].base_gas(), 5);
260        assert_eq!(cancun[op::ADDMOD as usize].base_gas(), 8);
261
262        // EXP: base 10, dynamic.
263        assert_eq!(cancun[op::EXP as usize].base_gas(), 10);
264        assert!(cancun[op::EXP as usize].is_dynamic());
265
266        // PUSH/DUP/SWAP gas.
267        assert_eq!(cancun[op::PUSH1 as usize].base_gas(), 3);
268        assert_eq!(cancun[op::DUP1 as usize].base_gas(), 3);
269        assert_eq!(cancun[op::SWAP1 as usize].base_gas(), 3);
270        assert_eq!(cancun[op::PUSH0 as usize].base_gas(), 2);
271
272        // LOG: base gas only (topic + data cost charged dynamically in builtin).
273        assert_eq!(cancun[op::LOG0 as usize].base_gas(), 375);
274        assert_eq!(cancun[op::LOG1 as usize].base_gas(), 375);
275        assert_eq!(cancun[op::LOG2 as usize].base_gas(), 375);
276        assert_eq!(cancun[op::LOG3 as usize].base_gas(), 375);
277        assert_eq!(cancun[op::LOG4 as usize].base_gas(), 375);
278        assert!(cancun[op::LOG0 as usize].is_dynamic());
279
280        // Memory ops: dynamic with base cost 3.
281        assert_eq!(cancun[op::MLOAD as usize].base_gas(), 3);
282        assert!(cancun[op::MLOAD as usize].is_dynamic());
283        assert_eq!(cancun[op::KECCAK256 as usize].base_gas(), 30);
284        assert!(cancun[op::KECCAK256 as usize].is_dynamic());
285
286        // Transient storage (Cancun).
287        assert_eq!(cancun[op::TLOAD as usize].base_gas(), 100);
288        assert!(!cancun[op::TLOAD as usize].is_disabled());
289
290        // Unknown opcode.
291        assert!(cancun[0x0C].is_unknown());
292
293        // AMSTERDAM-gated opcodes should be disabled on CANCUN.
294        assert!(cancun[op::DUPN as usize].is_disabled());
295        assert!(!cancun[op::DUPN as usize].is_unknown());
296        assert!(cancun[op::SWAPN as usize].is_disabled());
297        assert!(cancun[op::EXCHANGE as usize].is_disabled());
298        assert!(cancun[op::SLOTNUM as usize].is_disabled());
299
300        // AMSTERDAM-gated opcodes should be enabled on AMSTERDAM.
301        let amsterdam = op_info_map(SpecId::AMSTERDAM);
302        assert!(!amsterdam[op::DUPN as usize].is_disabled());
303        assert!(!amsterdam[op::SWAPN as usize].is_disabled());
304        assert!(!amsterdam[op::EXCHANGE as usize].is_disabled());
305        assert!(!amsterdam[op::SLOTNUM as usize].is_disabled());
306
307        // Spec-gated: PUSH0 disabled before Shanghai.
308        let pre_shanghai = op_info_map(SpecId::MERGE);
309        assert!(pre_shanghai[op::PUSH0 as usize].is_disabled());
310        assert!(!cancun[op::PUSH0 as usize].is_disabled());
311
312        // Dynamic-with-base-gas opcodes: base gas varies by spec.
313        let frontier = op_info_map(SpecId::FRONTIER);
314        assert_eq!(frontier[op::SLOAD as usize].base_gas(), 50);
315        assert_eq!(frontier[op::SELFDESTRUCT as usize].base_gas(), 0);
316        assert!(frontier[op::SELFDESTRUCT as usize].is_dynamic());
317        assert_eq!(frontier[op::CALL as usize].base_gas(), 40);
318        assert_eq!(frontier[op::BALANCE as usize].base_gas(), 20);
319        assert_eq!(frontier[op::EXTCODESIZE as usize].base_gas(), 20);
320        let tangerine = op_info_map(SpecId::TANGERINE);
321        assert_eq!(tangerine[op::SLOAD as usize].base_gas(), 200);
322        assert_eq!(tangerine[op::SELFDESTRUCT as usize].base_gas(), 5000);
323        assert!(tangerine[op::SELFDESTRUCT as usize].is_dynamic());
324        assert_eq!(tangerine[op::CALL as usize].base_gas(), 700);
325        assert_eq!(tangerine[op::BALANCE as usize].base_gas(), 400);
326        assert_eq!(tangerine[op::EXTCODESIZE as usize].base_gas(), 700);
327        let berlin = op_info_map(SpecId::BERLIN);
328        assert_eq!(berlin[op::SLOAD as usize].base_gas(), 100);
329        assert_eq!(berlin[op::CALL as usize].base_gas(), 100);
330        assert_eq!(berlin[op::BALANCE as usize].base_gas(), 100);
331        assert_eq!(berlin[op::EXTCODESIZE as usize].base_gas(), 100);
332    }
333}