Skip to main content

revmc_codegen/bytecode/passes/
sections.rs

1use crate::bytecode::{Bytecode, Inst, InstFlags};
2use core::fmt;
3use revm_bytecode::opcode as op;
4use revm_primitives::U256;
5
6/// A gas section tracks the total base gas cost of a sequence of instructions.
7///
8/// Gas sections end at instructions that require `gasleft` (e.g. `GAS`, `SSTORE`),
9/// branching instructions, or suspending instructions.
10#[derive(Clone, Copy, Default, PartialEq, Eq)]
11pub(crate) struct GasSection {
12    /// The total base gas cost of all instructions in the section.
13    pub(crate) gas_cost: u32,
14}
15
16impl fmt::Debug for GasSection {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        if self.is_empty() {
19            f.write_str("GasSection::EMPTY")
20        } else {
21            f.debug_struct("GasSection").field("gas_cost", &self.gas_cost).finish()
22        }
23    }
24}
25
26impl GasSection {
27    /// Returns `true` if the section is empty.
28    #[inline]
29    pub(crate) fn is_empty(self) -> bool {
30        self == Self::default()
31    }
32}
33
34/// A stack section tracks stack height requirements for a sequence of instructions.
35///
36/// Stack sections end at branching or suspending instructions, but NOT at instructions
37/// that merely require `gasleft` — those only end gas sections.
38#[derive(Clone, Copy, Default, PartialEq, Eq)]
39pub(crate) struct StackSection {
40    /// The stack height required to execute the section.
41    pub(crate) inputs: u16,
42    /// The maximum stack height growth relative to the stack height at section start.
43    pub(crate) max_growth: i16,
44}
45
46impl fmt::Debug for StackSection {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        if self.is_empty() {
49            f.write_str("StackSection::EMPTY")
50        } else {
51            f.debug_struct("StackSection")
52                .field("inputs", &self.inputs)
53                .field("max_growth", &self.max_growth)
54                .finish()
55        }
56    }
57}
58
59impl StackSection {
60    /// Returns `true` if the section is empty.
61    #[inline]
62    pub(crate) fn is_empty(self) -> bool {
63        self == Self::default()
64    }
65
66    /// Compute a stack section from an iterator of `(inputs, outputs)` pairs.
67    pub(crate) fn from_stack_io(iter: impl IntoIterator<Item = (u8, u8)>) -> Self {
68        let mut analysis = StackSectionAnalysis::default();
69        for (inp, out) in iter {
70            analysis.process(inp, out);
71        }
72        analysis.section()
73    }
74}
75
76/// Gas section analysis state.
77pub(crate) struct GasSectionAnalysis {
78    gas_cost: u64,
79    start_inst: Inst,
80}
81
82impl Default for GasSectionAnalysis {
83    fn default() -> Self {
84        Self { gas_cost: 0, start_inst: Inst::from_usize(0) }
85    }
86}
87
88impl GasSectionAnalysis {
89    /// Accumulates a base gas cost.
90    #[inline]
91    pub(crate) fn process(&mut self, base_gas: u16) {
92        self.gas_cost += base_gas as u64;
93    }
94
95    /// Accumulates extra gas (e.g. pre-computed dynamic gas for folded instructions).
96    #[inline]
97    pub(crate) fn process_extra(&mut self, gas: u64) {
98        self.gas_cost += gas;
99    }
100
101    fn save_to_reset(&mut self, bytecode: &mut Bytecode<'_>, next_section_inst: Inst) {
102        self.save_to(bytecode, next_section_inst);
103        self.reset(next_section_inst);
104    }
105
106    /// Saves the current gas section to the bytecode.
107    fn save_to(&self, bytecode: &mut Bytecode<'_>, next_section_inst: Inst) {
108        if self.start_inst >= bytecode.insts.len_idx() {
109            return;
110        }
111        let section = self.section();
112        if !section.is_empty() {
113            let _ = next_section_inst;
114            // trace!(
115            //     inst = %self.start_inst,
116            //     len = %(next_section_inst - self.start_inst).0,
117            //     ?section,
118            //     "saving gas"
119            // );
120            let mut insts = bytecode.insts[self.start_inst..].iter_mut();
121            if let Some(inst) = insts.find(|inst| !inst.is_dead_code()) {
122                inst.gas_section = section;
123            }
124        }
125    }
126
127    /// Resets the analysis for a new section.
128    pub(crate) fn reset(&mut self, inst: Inst) {
129        *self = Self { start_inst: inst, ..Default::default() };
130    }
131
132    /// Returns the current gas section.
133    pub(crate) fn section(&self) -> GasSection {
134        GasSection { gas_cost: self.gas_cost.try_into().unwrap_or(u32::MAX) }
135    }
136}
137
138/// Stack section analysis state.
139pub(crate) struct StackSectionAnalysis {
140    inputs: i32,
141    diff: i32,
142    max_growth: i32,
143    start_inst: Inst,
144}
145
146impl Default for StackSectionAnalysis {
147    fn default() -> Self {
148        Self { inputs: 0, diff: 0, max_growth: 0, start_inst: Inst::from_usize(0) }
149    }
150}
151
152impl StackSectionAnalysis {
153    /// Returns the cumulative stack delta so far.
154    #[inline]
155    pub(crate) fn diff(&self) -> i32 {
156        self.diff
157    }
158
159    /// Accumulates a single instruction's stack I/O.
160    #[inline]
161    pub(crate) fn process(&mut self, inputs: u8, outputs: u8) {
162        self.inputs = self.inputs.max(inputs as i32 - self.diff);
163        self.diff += outputs as i32 - inputs as i32;
164        self.max_growth = self.max_growth.max(self.diff);
165    }
166
167    fn save_to_reset(&mut self, bytecode: &mut Bytecode<'_>, next_section_inst: Inst) {
168        self.save_to(bytecode, next_section_inst);
169        self.reset(next_section_inst);
170    }
171
172    /// Saves the current stack section to the bytecode.
173    fn save_to(&self, bytecode: &mut Bytecode<'_>, next_section_inst: Inst) {
174        if self.start_inst >= bytecode.insts.len_idx() {
175            return;
176        }
177        let section = self.section();
178        let _ = next_section_inst;
179        // trace!(
180        //     inst = %self.start_inst,
181        //     len = %(next_section_inst - self.start_inst).0,
182        //     ?section,
183        //     "saving stack"
184        // );
185        let mut insts = bytecode.insts[self.start_inst..].iter_mut();
186        if let Some(inst) = insts.find(|inst| !inst.is_dead_code()) {
187            inst.flags |= InstFlags::STACK_SECTION_HEAD;
188            inst.stack_section = section;
189        }
190    }
191
192    /// Resets the analysis for a new section.
193    pub(crate) fn reset(&mut self, inst: Inst) {
194        *self = Self { start_inst: inst, ..Default::default() };
195    }
196
197    /// Returns the current stack section.
198    pub(crate) fn section(&self) -> StackSection {
199        StackSection {
200            inputs: self.inputs.try_into().unwrap_or(u16::MAX),
201            max_growth: self.max_growth.try_into().unwrap_or(i16::MAX),
202        }
203    }
204}
205
206/// Instruction section analysis, tracking gas and stack sections separately.
207#[derive(Default)]
208pub(crate) struct SectionsAnalysis {
209    gas: GasSectionAnalysis,
210    stack: StackSectionAnalysis,
211}
212
213impl SectionsAnalysis {
214    /// Process a single instruction.
215    pub(crate) fn process(&mut self, bytecode: &mut Bytecode<'_>, inst: Inst) {
216        // JUMPDEST starts both gas and stack sections.
217        if bytecode.inst(inst).is_reachable_jumpdest(bytecode.has_dynamic_jumps()) {
218            self.stack.save_to_reset(bytecode, inst);
219            self.gas.save_to_reset(bytecode, inst);
220        }
221
222        let data = bytecode.inst(inst);
223        let (inp, out) = data.stack_io();
224        self.stack.process(inp, out);
225        self.gas.process(data.base_gas);
226
227        // When EXP's builtin will be skipped — either because both operands are known
228        // (const_output) or because the exponent is known and handled by a peephole (≤ 2) —
229        // fold the dynamic gas into the section statically.
230        if data.opcode == op::EXP
231            && let Some(exponent) = bytecode.const_operand(inst, 1)
232            && (bytecode.const_output(inst).is_some() || exponent <= U256::from(2))
233        {
234            self.gas.process_extra(bytecode.gas_params.exp_cost(exponent));
235        }
236
237        // Instructions that require `gasleft` end only the gas section.
238        // Branching and suspending instructions end both sections.
239        let next = inst + 1;
240        if data.may_suspend() || data.is_branching() {
241            self.stack.save_to_reset(bytecode, next);
242            self.gas.save_to_reset(bytecode, next);
243        } else if data.requires_gasleft(bytecode.spec_id) {
244            self.gas.save_to_reset(bytecode, next);
245        }
246    }
247
248    /// Finishes the analysis.
249    pub(crate) fn finish(self, bytecode: &mut Bytecode<'_>) {
250        let last = bytecode.insts.len_idx() - 1;
251        self.gas.save_to(bytecode, last);
252        self.stack.save_to(bytecode, last);
253        if enabled!(tracing::Level::DEBUG) {
254            let mut max_len = 0usize;
255            let mut count = 0usize;
256            let mut section_len = 0usize;
257            for (_inst, data) in bytecode.iter_insts() {
258                section_len += 1;
259                if !data.is_stack_section_head() {
260                    continue;
261                }
262                // `section_len` includes this head; the previous section had `section_len - 1`.
263                if count > 0 {
264                    max_len = max_len.max(section_len - 1);
265                }
266                section_len = 1;
267                count += 1;
268            }
269            // Account for the last section.
270            max_len = max_len.max(section_len);
271            debug!(count, max_len, "sections");
272        }
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use crate::bytecode::{
279        Inst,
280        passes::block_analysis::tests::{analyze_asm, analyze_asm_spec, analyze_code_spec},
281    };
282    use revm_bytecode::opcode as op;
283    use revm_primitives::hardfork::SpecId;
284
285    /// Returns the gas section cost for the first non-dead instruction (the section head).
286    fn section_gas(src: &str) -> u32 {
287        let bytecode = analyze_asm(src);
288        bytecode.inst(Inst::from_usize(0)).gas_section.gas_cost
289    }
290
291    /// EXP with an asymmetric base/exponent: base is 1 byte (2), exponent is 2 bytes (256).
292    /// If the wrong operand were used for gas, the dynamic cost would be 50*1=50 instead of
293    /// 50*2=100, and the total would differ.
294    #[test]
295    fn exp_folded_gas_uses_exponent_not_base() {
296        // PUSH 256, PUSH 2, EXP, PUSH0, MSTORE, STOP
297        // Gas: PUSH(3) + PUSH(3) + EXP_base(10) + EXP_dynamic(50*2=100) + PUSH0(2) + MSTORE(3) +
298        // STOP(0)    = 3 + 3 + 10 + 100 + 2 + 3 + 0 = 121
299        let gas = section_gas("PUSH 256 PUSH 2 EXP PUSH0 MSTORE STOP");
300        assert_eq!(gas, 121, "dynamic gas must use exponent (256=2 bytes), not base (2=1 byte)");
301    }
302
303    /// EXP with zero exponent has no dynamic gas.
304    #[test]
305    fn exp_folded_gas_zero_exponent() {
306        // PUSH0(2) + PUSH(3) + EXP_base(10) + EXP_dynamic(0) + PUSH0(2) + MSTORE(3) + STOP(0) = 20
307        let gas = section_gas("PUSH 0 PUSH 2 EXP PUSH0 MSTORE STOP");
308        assert_eq!(gas, 20, "zero exponent should have no dynamic gas");
309    }
310
311    /// EXP with max exponent (32 bytes).
312    #[test]
313    fn exp_folded_gas_max_exponent() {
314        // PUSH U256::MAX (32 bytes), PUSH 2, EXP, PUSH0, MSTORE, STOP
315        // Gas: PUSH(3) + PUSH(3) + EXP_base(10) + EXP_dynamic(50*32=1600) + PUSH0(2) + MSTORE(3) +
316        // STOP(0)    = 3 + 3 + 10 + 1600 + 2 + 3 + 0 = 1621
317        let gas = section_gas(
318            "PUSH 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
319             PUSH 2 EXP PUSH0 MSTORE STOP",
320        );
321        assert_eq!(gas, 1621, "max exponent should cost 50*32=1600 dynamic gas");
322    }
323
324    /// Disabled opcodes must not contribute stack I/O or gas to the preceding section.
325    /// Otherwise the section-head underflow check fires before the disabled opcode's
326    /// `NotActivated` guard, producing a divergent `StackUnderflow`.
327    #[test]
328    fn disabled_opcode_does_not_poison_section() {
329        // CALLDATASIZE(0→1) ; TSTORE(2→0, disabled before Cancun)
330        let bytecode = analyze_asm_spec("CALLDATASIZE TSTORE", SpecId::SHANGHAI);
331        let head = bytecode.inst(Inst::from_usize(0));
332        assert_eq!(
333            head.stack_section.inputs, 0,
334            "CALLDATASIZE needs 0 inputs; disabled TSTORE must not inflate the section"
335        );
336        assert_eq!(head.gas_section.gas_cost, 2, "only CALLDATASIZE gas (2) should be charged");
337    }
338
339    /// DUPN with an invalid immediate must have stack_io = (0, 0) so that section analysis
340    /// matches translate (which emits `InvalidImmediateEncoding` and diverges).
341    /// Previously, the fallback to OPCODE_INFO's placeholder `(0, 1)` inflated the section's
342    /// cumulative diff, causing translate to underflow the virtual stack on a subsequent SSTORE.
343    #[test]
344    fn invalid_dupn_imm_does_not_inflate_section() {
345        // PUSH0, DUPN 0x5b (invalid immediate), PUSH0, SSTORE, STOP
346        // Without the fix, DUPN had stack_io=(0,1), inflating the section diff by 1.
347        // Translate would skip the +1 (goto_return!(fail)), then SSTORE's diff=-2 would
348        // underflow the virtual stack.
349        let bytecode = analyze_code_spec(
350            vec![op::PUSH0, op::DUPN, 0x5b, op::PUSH0, op::SSTORE, op::STOP],
351            SpecId::AMSTERDAM,
352        );
353        let dupn = bytecode.inst(Inst::from_usize(1));
354        assert_eq!(dupn.stack_io(), (0, 0), "DUPN with invalid immediate must have zero stack I/O");
355    }
356}