Skip to main content

zksync_vm2/
vm.rs

1use std::fmt;
2
3use primitive_types::{H160, U256};
4use zksync_vm2_interface::{opcodes::TypeLevelCallingMode, CallingMode, HeapId, Tracer};
5
6use crate::{
7    callframe::{Callframe, FrameRemnant},
8    decommit::{materialize_decommit_page, u256_into_address},
9    instruction::ExecutionStatus,
10    page_ids::{aux_heap_page_from_base, code_page_from_base, heap_page_from_base},
11    stack::StackPool,
12    state::{State, StateSnapshot},
13    world_diff::{ExternalSnapshot, Snapshot, WorldDiff},
14    ExecutionEnd, Program, World,
15};
16
17/// [`VirtualMachine`] settings.
18#[derive(Debug, Clone)]
19pub struct Settings {
20    /// Bytecode hash of the default account abstraction contract.
21    pub default_aa_code_hash: [u8; 32],
22    /// Bytecode hash of the EVM interpreter.
23    pub evm_interpreter_code_hash: [u8; 32],
24    /// Writing to this address in the bootloader's heap suspends execution
25    pub hook_address: u32,
26}
27
28/// High-performance out-of-circuit EraVM implementation.
29#[derive(Debug)]
30pub struct VirtualMachine<T, W> {
31    pub(crate) world_diff: WorldDiff,
32    pub(crate) state: State<T, W>,
33    pub(crate) settings: Settings,
34    pub(crate) stack_pool: StackPool,
35    pub(crate) snapshot: Option<VmSnapshot>,
36}
37
38impl<T: Tracer, W: World<T>> VirtualMachine<T, W> {
39    /// Creates a new VM instance.
40    pub fn new(
41        address: H160,
42        program: Program<T, W>,
43        caller: H160,
44        calldata: &[u8],
45        gas: u32,
46        settings: Settings,
47    ) -> Self {
48        let world_diff = WorldDiff::default();
49        let world_before_this_frame = world_diff.snapshot();
50        let mut stack_pool = StackPool::default();
51
52        Self {
53            world_diff,
54            state: State::new(
55                address,
56                caller,
57                calldata,
58                gas,
59                program,
60                world_before_this_frame,
61                stack_pool.get(),
62            ),
63            settings,
64            stack_pool,
65            snapshot: None,
66        }
67    }
68
69    /// Pre-reserve dynamic heap-group capacity to suppress mid-execution
70    /// Vec doublings inside `Heaps`. Hint with the worst-case far-call
71    /// count estimable from the witness.
72    pub fn reserve_dynamic_heap_capacity(&mut self, n: usize) {
73        self.state.heaps.reserve_dynamic_groups(n);
74    }
75
76    /// Provides a reference to the [`World`] diff accumulated by VM execution so far.
77    pub fn world_diff(&self) -> &WorldDiff {
78        &self.world_diff
79    }
80
81    /// Provides a mutable reference to the [`World`] diff accumulated by VM execution so far.
82    ///
83    /// It is unsound to mutate [`WorldDiff`] in the middle of VM execution in the general case; thus, this method should only be used in tests.
84    #[doc(hidden)]
85    pub fn world_diff_mut(&mut self) -> &mut WorldDiff {
86        &mut self.world_diff
87    }
88
89    /// Manually warms up a decommit of the code with the provided `code_hash`, materializing its
90    /// heap page exactly like the [`Decommit`](zksync_vm2_interface::opcodes::Decommit) opcode
91    /// handler does. Returns `true` if this was a fresh decommit (i.e., the code wasn't decommitted
92    /// previously in the same VM run).
93    ///
94    /// Unlike [`WorldDiff::decommit_opcode`], this records the decommit in the VM state (assigning a
95    /// reusable page), so a subsequent `decommit` opcode on the same hash is recognized as
96    /// already-decommitted and refunded — matching legacy `zk_evm` `execute_decommit` semantics.
97    ///
98    /// This is intended for execution-verification setup / tests; it can break VM operation if
99    /// called in the middle of execution.
100    #[doc(hidden)]
101    pub fn manually_decommit(&mut self, world: &mut W, tracer: &mut T, code_hash: U256) -> bool {
102        let (code, is_fresh) = self.world_diff.decommit_opcode(world, tracer, code_hash);
103        if is_fresh {
104            // Materialize into a fresh code page rather than reusing the current frame's heap, so
105            // manual decommits performed between frames (e.g. against the bootloader frame) don't
106            // clobber live heap data.
107            let base_page = self.state.allocate_base_page();
108            materialize_decommit_page(self, code_hash, &code, code_page_from_base(base_page));
109        }
110        is_fresh
111    }
112
113    /// Runs this VM with the specified [`World`] and [`Tracer`] until an end of execution due to a hook, or an error.
114    pub fn run(&mut self, world: &mut W, tracer: &mut T) -> ExecutionEnd {
115        unsafe {
116            loop {
117                if let ExecutionStatus::Stopped(end) =
118                    ((*self.state.current_frame.pc).handler)(self, world, tracer)
119                {
120                    return end;
121                }
122            }
123        }
124    }
125
126    /// Returns how much of the extra gas limit is left and the stop reason,
127    /// unless the extra gas limit was exceeded.
128    ///
129    /// Needed to support account validation gas limit.
130    /// We cannot simply reduce the available gas, as contracts might behave differently
131    /// depending on remaining gas.
132    pub fn resume_with_additional_gas_limit(
133        &mut self,
134        world: &mut W,
135        tracer: &mut T,
136        gas_limit: u32,
137    ) -> Option<(u32, ExecutionEnd)> {
138        let minimum_gas = self.state.total_unspent_gas().saturating_sub(gas_limit);
139
140        let end = unsafe {
141            loop {
142                if let ExecutionStatus::Stopped(end) =
143                    ((*self.state.current_frame.pc).handler)(self, world, tracer)
144                {
145                    break end;
146                }
147
148                if self.state.total_unspent_gas() < minimum_gas {
149                    return None;
150                }
151            }
152        };
153
154        self.state
155            .total_unspent_gas()
156            .checked_sub(minimum_gas)
157            .map(|left| (left, end))
158    }
159
160    /// Creates a VM snapshot. The snapshot can then be rolled back to, or discarded.
161    ///
162    /// # Panics
163    ///
164    /// - Panics if called outside the initial (bootloader) callframe.
165    /// - Panics if this VM already has a snapshot.
166    pub fn make_snapshot(&mut self) {
167        assert!(self.snapshot.is_none(), "VM already has a snapshot");
168        assert!(
169            self.state.previous_frames.is_empty(),
170            "Snapshotting is only allowed in the bootloader"
171        );
172
173        self.snapshot = Some(VmSnapshot {
174            world_snapshot: self.world_diff.external_snapshot(),
175            state_snapshot: self.state.snapshot(),
176        });
177    }
178
179    /// Returns the VM to the state it was in when [`Self::make_snapshot()`] was called.
180    ///
181    /// # Panics
182    ///
183    /// - Panics if this VM doesn't hold a snapshot.
184    /// - Panics if called outside the initial (bootloader) callframe.
185    pub fn rollback(&mut self) {
186        assert!(
187            self.state.previous_frames.is_empty(),
188            "Rolling back is only allowed in the bootloader"
189        );
190
191        let snapshot = self
192            .snapshot
193            .take()
194            .expect("`rollback()` called without a snapshot");
195        self.world_diff.external_rollback(snapshot.world_snapshot);
196        self.state.rollback(snapshot.state_snapshot, |heap| {
197            self.world_diff.is_decommit_page_pinned(heap)
198        });
199        self.delete_history();
200    }
201
202    /// Pops a [previously made](Self::make_snapshot()) snapshot without rolling back to it. This effectively commits
203    /// all changes made up to this point, so that they cannot be rolled back.
204    ///
205    /// # Panics
206    ///
207    /// - Panics if called outside the initial (bootloader) callframe.
208    pub fn pop_snapshot(&mut self) {
209        assert!(
210            self.state.previous_frames.is_empty(),
211            "Popping a snapshot is only allowed in the bootloader"
212        );
213        self.snapshot = None;
214        self.delete_history();
215        self.reclaim_bootloader_returndata_heaps();
216    }
217
218    /// Frees the returndata heaps that accumulated on the bootloader frame while
219    /// the just-committed transaction(s) executed.
220    ///
221    /// Every far-call keeps the heap holding its returndata alive and bubbles it
222    /// up to the caller (see [`Self::pop_frame`]); heaps forwarded all the way to
223    /// the bootloader frame — which never pops during a batch — otherwise live
224    /// until the VM is dropped, so they accumulate across every transaction (the
225    /// dominant heap-memory consumer on large batches).
226    ///
227    /// This is the safe point to release them: the callstack is unwound to the
228    /// bootloader (`previous_frames` is empty), the external snapshot has just
229    /// been discarded (`self.snapshot` is `None`) and history deleted, so no
230    /// rollback can reference these heaps; and a committed transaction's
231    /// returndata is dead once the bootloader moves on. Decommit-pinned code
232    /// pages (shared across transactions by hash) are kept — the same predicate
233    /// [`Self::pop_frame`] uses. Freed pages return to the `PagePool` and are
234    /// reused by the next transaction, so peak page usage stays at roughly one
235    /// transaction's worth instead of growing with the transaction count.
236    fn reclaim_bootloader_returndata_heaps(&mut self) {
237        // `kept` is owned after the take, so the `retain` closure can borrow
238        // `self.world_diff`/`self.state.heaps` without conflicting with the
239        // borrow of the field it compacts. Retain drops the deallocated heaps
240        // in place, avoiding a second allocation. Reordering is irrelevant here:
241        // there is no live snapshot to consume the tail ordering (see the len()
242        // snapshot / tail-drain rollback path in `Callframe`).
243        let mut kept = std::mem::take(&mut self.state.current_frame.heaps_i_am_keeping_alive);
244        kept.retain(|&heap| {
245            let pinned = self.world_diff.is_decommit_page_pinned(heap);
246            if !pinned {
247                self.state.heaps.deallocate(heap);
248            }
249            pinned
250        });
251        self.state.current_frame.heaps_i_am_keeping_alive = kept;
252    }
253
254    /// This must only be called when it is known that the VM cannot be rolled back,
255    /// so there must not be any external snapshots and the callstack
256    /// should ideally be empty, though in practice it sometimes contains
257    /// a near call inside the bootloader.
258    fn delete_history(&mut self) {
259        self.world_diff.delete_history();
260        self.state.delete_history();
261    }
262}
263
264impl<T: Tracer, W> VirtualMachine<T, W> {
265    #[allow(clippy::too_many_arguments)]
266    pub(crate) fn push_frame<M: TypeLevelCallingMode>(
267        &mut self,
268        code_address: H160,
269        program: Program<T, W>,
270        gas: u32,
271        exception_handler: u16,
272        is_static: bool,
273        is_evm_blob_format: bool,
274        calldata_heap: HeapId,
275        world_before_this_frame: Snapshot,
276    ) {
277        let base_page = self.state.allocate_base_page();
278        let heap_page = heap_page_from_base(base_page);
279        let aux_heap_page = aux_heap_page_from_base(base_page);
280        self.state.heaps.allocate_at(heap_page);
281        self.state.heaps.allocate_at(aux_heap_page);
282
283        let mut new_frame = Callframe::new(
284            if M::VALUE == CallingMode::Delegate {
285                self.state.current_frame.address
286            } else {
287                code_address
288            },
289            code_address,
290            match M::VALUE {
291                CallingMode::Normal => self.state.current_frame.address,
292                CallingMode::Delegate => self.state.current_frame.caller,
293                CallingMode::Mimic => u256_into_address(self.state.registers[15]),
294            },
295            program,
296            self.stack_pool.get(),
297            heap_page,
298            aux_heap_page,
299            calldata_heap,
300            gas,
301            exception_handler,
302            if M::VALUE == CallingMode::Delegate {
303                self.state.current_frame.context_u128
304            } else {
305                self.state.context_u128
306            },
307            is_static,
308            is_evm_blob_format,
309            world_before_this_frame,
310        );
311        self.state.context_u128 = 0;
312
313        std::mem::swap(&mut new_frame, &mut self.state.current_frame);
314        self.state.previous_frames.push(new_frame);
315    }
316
317    pub(crate) fn pop_frame(&mut self, heap_to_keep: Option<HeapId>) -> Option<FrameRemnant> {
318        let mut frame = self.state.previous_frames.pop()?;
319
320        for &heap in [
321            self.state.current_frame.heap,
322            self.state.current_frame.aux_heap,
323        ]
324        .iter()
325        .chain(&self.state.current_frame.heaps_i_am_keeping_alive)
326        {
327            if Some(heap) != heap_to_keep && !self.world_diff.is_decommit_page_pinned(heap) {
328                self.state.heaps.deallocate(heap);
329            }
330        }
331
332        std::mem::swap(&mut self.state.current_frame, &mut frame);
333        let Callframe {
334            exception_handler,
335            world_before_this_frame,
336            stack,
337            ..
338        } = frame;
339
340        self.stack_pool.recycle(stack);
341
342        self.state
343            .current_frame
344            .heaps_i_am_keeping_alive
345            .extend(heap_to_keep);
346
347        Some(FrameRemnant {
348            exception_handler,
349            snapshot: world_before_this_frame,
350        })
351    }
352
353    pub(crate) fn start_new_tx(&mut self) {
354        self.state.transaction_number = self.state.transaction_number.wrapping_add(1);
355        self.world_diff.clear_transient_storage();
356    }
357}
358
359impl<T: fmt::Debug, W: fmt::Debug> VirtualMachine<T, W> {
360    /// Dumps an opaque representation of the current VM state.
361    #[doc(hidden)] // should only be used in tests
362    pub fn dump_state(&self) -> impl PartialEq + fmt::Debug {
363        self.state.clone()
364    }
365}
366
367/// Snapshot of a [`VirtualMachine`].
368#[derive(Debug)]
369pub(crate) struct VmSnapshot {
370    world_snapshot: ExternalSnapshot,
371    state_snapshot: StateSnapshot,
372}