Skip to main content

zksync_vm2/
vm.rs

1use std::fmt;
2
3use primitive_types::H160;
4use zksync_vm2_interface::{opcodes::TypeLevelCallingMode, CallingMode, HeapId, Tracer};
5
6use crate::{
7    callframe::{Callframe, FrameRemnant},
8    decommit::u256_into_address,
9    instruction::ExecutionStatus,
10    page_ids::{aux_heap_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    /// Provides a reference to the [`World`] diff accumulated by VM execution so far.
70    pub fn world_diff(&self) -> &WorldDiff {
71        &self.world_diff
72    }
73
74    /// Provides a mutable reference to the [`World`] diff accumulated by VM execution so far.
75    ///
76    /// 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.
77    #[doc(hidden)]
78    pub fn world_diff_mut(&mut self) -> &mut WorldDiff {
79        &mut self.world_diff
80    }
81
82    /// Runs this VM with the specified [`World`] and [`Tracer`] until an end of execution due to a hook, or an error.
83    pub fn run(&mut self, world: &mut W, tracer: &mut T) -> ExecutionEnd {
84        unsafe {
85            loop {
86                if let ExecutionStatus::Stopped(end) =
87                    ((*self.state.current_frame.pc).handler)(self, world, tracer)
88                {
89                    return end;
90                }
91            }
92        }
93    }
94
95    /// Returns how much of the extra gas limit is left and the stop reason,
96    /// unless the extra gas limit was exceeded.
97    ///
98    /// Needed to support account validation gas limit.
99    /// We cannot simply reduce the available gas, as contracts might behave differently
100    /// depending on remaining gas.
101    pub fn resume_with_additional_gas_limit(
102        &mut self,
103        world: &mut W,
104        tracer: &mut T,
105        gas_limit: u32,
106    ) -> Option<(u32, ExecutionEnd)> {
107        let minimum_gas = self.state.total_unspent_gas().saturating_sub(gas_limit);
108
109        let end = unsafe {
110            loop {
111                if let ExecutionStatus::Stopped(end) =
112                    ((*self.state.current_frame.pc).handler)(self, world, tracer)
113                {
114                    break end;
115                }
116
117                if self.state.total_unspent_gas() < minimum_gas {
118                    return None;
119                }
120            }
121        };
122
123        self.state
124            .total_unspent_gas()
125            .checked_sub(minimum_gas)
126            .map(|left| (left, end))
127    }
128
129    /// Creates a VM snapshot. The snapshot can then be rolled back to, or discarded.
130    ///
131    /// # Panics
132    ///
133    /// - Panics if called outside the initial (bootloader) callframe.
134    /// - Panics if this VM already has a snapshot.
135    pub fn make_snapshot(&mut self) {
136        assert!(self.snapshot.is_none(), "VM already has a snapshot");
137        assert!(
138            self.state.previous_frames.is_empty(),
139            "Snapshotting is only allowed in the bootloader"
140        );
141
142        self.snapshot = Some(VmSnapshot {
143            world_snapshot: self.world_diff.external_snapshot(),
144            state_snapshot: self.state.snapshot(),
145        });
146    }
147
148    /// Returns the VM to the state it was in when [`Self::make_snapshot()`] was called.
149    ///
150    /// # Panics
151    ///
152    /// - Panics if this VM doesn't hold a snapshot.
153    /// - Panics if called outside the initial (bootloader) callframe.
154    pub fn rollback(&mut self) {
155        assert!(
156            self.state.previous_frames.is_empty(),
157            "Rolling back is only allowed in the bootloader"
158        );
159
160        let snapshot = self
161            .snapshot
162            .take()
163            .expect("`rollback()` called without a snapshot");
164        self.world_diff.external_rollback(snapshot.world_snapshot);
165        self.state.rollback(snapshot.state_snapshot, |heap| {
166            self.world_diff.is_decommit_page_pinned(heap)
167        });
168        self.delete_history();
169    }
170
171    /// Pops a [previously made](Self::make_snapshot()) snapshot without rolling back to it. This effectively commits
172    /// all changes made up to this point, so that they cannot be rolled back.
173    ///
174    /// # Panics
175    ///
176    /// - Panics if called outside the initial (bootloader) callframe.
177    pub fn pop_snapshot(&mut self) {
178        assert!(
179            self.state.previous_frames.is_empty(),
180            "Popping a snapshot is only allowed in the bootloader"
181        );
182        self.snapshot = None;
183        self.delete_history();
184    }
185
186    /// This must only be called when it is known that the VM cannot be rolled back,
187    /// so there must not be any external snapshots and the callstack
188    /// should ideally be empty, though in practice it sometimes contains
189    /// a near call inside the bootloader.
190    fn delete_history(&mut self) {
191        self.world_diff.delete_history();
192        self.state.delete_history();
193    }
194}
195
196impl<T: Tracer, W> VirtualMachine<T, W> {
197    #[allow(clippy::too_many_arguments)]
198    pub(crate) fn push_frame<M: TypeLevelCallingMode>(
199        &mut self,
200        code_address: H160,
201        program: Program<T, W>,
202        gas: u32,
203        exception_handler: u16,
204        is_static: bool,
205        is_evm_blob_format: bool,
206        calldata_heap: HeapId,
207        world_before_this_frame: Snapshot,
208    ) {
209        let base_page = self.state.allocate_base_page();
210        let heap_page = heap_page_from_base(base_page);
211        let aux_heap_page = aux_heap_page_from_base(base_page);
212        self.state.heaps.allocate_at(heap_page);
213        self.state.heaps.allocate_at(aux_heap_page);
214
215        let mut new_frame = Callframe::new(
216            if M::VALUE == CallingMode::Delegate {
217                self.state.current_frame.address
218            } else {
219                code_address
220            },
221            code_address,
222            match M::VALUE {
223                CallingMode::Normal => self.state.current_frame.address,
224                CallingMode::Delegate => self.state.current_frame.caller,
225                CallingMode::Mimic => u256_into_address(self.state.registers[15]),
226            },
227            program,
228            self.stack_pool.get(),
229            heap_page,
230            aux_heap_page,
231            calldata_heap,
232            gas,
233            exception_handler,
234            if M::VALUE == CallingMode::Delegate {
235                self.state.current_frame.context_u128
236            } else {
237                self.state.context_u128
238            },
239            is_static,
240            is_evm_blob_format,
241            world_before_this_frame,
242        );
243        self.state.context_u128 = 0;
244
245        std::mem::swap(&mut new_frame, &mut self.state.current_frame);
246        self.state.previous_frames.push(new_frame);
247    }
248
249    pub(crate) fn pop_frame(&mut self, heap_to_keep: Option<HeapId>) -> Option<FrameRemnant> {
250        let mut frame = self.state.previous_frames.pop()?;
251
252        for &heap in [
253            self.state.current_frame.heap,
254            self.state.current_frame.aux_heap,
255        ]
256        .iter()
257        .chain(&self.state.current_frame.heaps_i_am_keeping_alive)
258        {
259            if Some(heap) != heap_to_keep && !self.world_diff.is_decommit_page_pinned(heap) {
260                self.state.heaps.deallocate(heap);
261            }
262        }
263
264        std::mem::swap(&mut self.state.current_frame, &mut frame);
265        let Callframe {
266            exception_handler,
267            world_before_this_frame,
268            stack,
269            ..
270        } = frame;
271
272        self.stack_pool.recycle(stack);
273
274        self.state
275            .current_frame
276            .heaps_i_am_keeping_alive
277            .extend(heap_to_keep);
278
279        Some(FrameRemnant {
280            exception_handler,
281            snapshot: world_before_this_frame,
282        })
283    }
284
285    pub(crate) fn start_new_tx(&mut self) {
286        self.state.transaction_number = self.state.transaction_number.wrapping_add(1);
287        self.world_diff.clear_transient_storage();
288    }
289}
290
291impl<T: fmt::Debug, W: fmt::Debug> VirtualMachine<T, W> {
292    /// Dumps an opaque representation of the current VM state.
293    #[doc(hidden)] // should only be used in tests
294    pub fn dump_state(&self) -> impl PartialEq + fmt::Debug {
295        self.state.clone()
296    }
297}
298
299/// Snapshot of a [`VirtualMachine`].
300#[derive(Debug)]
301pub(crate) struct VmSnapshot {
302    world_snapshot: ExternalSnapshot,
303    state_snapshot: StateSnapshot,
304}