Skip to main content

zksync_vm2/instruction_handlers/
ret.rs

1use primitive_types::U256;
2use zksync_vm2_interface::{
3    opcodes::{self, Normal, Panic, Revert, TypeLevelReturnType},
4    ReturnType, Tracer,
5};
6
7use super::{
8    common::full_boilerplate,
9    far_call::get_calldata,
10    monomorphization::{match_boolean, monomorphize, parameterize},
11};
12use crate::{
13    addressing_modes::{Arguments, Immediate1, Register1, Source, INVALID_INSTRUCTION_COST},
14    callframe::FrameRemnant,
15    instruction::{ExecutionEnd, ExecutionStatus},
16    mode_requirements::ModeRequirements,
17    page_ids::base_page_from_heap,
18    predication::Flags,
19    tracing::VmAndWorld,
20    Instruction, Predicate, VirtualMachine, World,
21};
22
23fn naked_ret<T: Tracer, W: World<T>, RT: TypeLevelReturnType, const TO_LABEL: bool>(
24    vm: &mut VirtualMachine<T, W>,
25    args: &Arguments,
26) -> ExecutionStatus {
27    let mut return_type = RT::VALUE;
28    let near_call_leftover_gas = vm.state.current_frame.gas;
29
30    let (snapshot, leftover_gas) = if let Some(FrameRemnant {
31        exception_handler,
32        snapshot,
33    }) = vm.state.current_frame.pop_near_call()
34    {
35        if TO_LABEL {
36            let pc = Immediate1::get_u16(args);
37            vm.state.current_frame.set_pc_from_u16(pc);
38        } else if return_type.is_failure() {
39            vm.state.current_frame.set_pc_from_u16(exception_handler);
40        }
41
42        (snapshot, near_call_leftover_gas)
43    } else {
44        let (raw_abi, is_pointer) = Register1::get_with_pointer_flag(args, &mut vm.state);
45        let return_value_or_panic = if return_type == ReturnType::Panic {
46            // A panic forwards no returndata, but the return-ABI pointer must still be resolved for
47            // its heap-growth cost: a fresh-heap pointer whose `start + length` overflows `u32`
48            // grows the heap to `u32::MAX`, draining the frame's gas. Passing `already_failed = true`
49            // charges exactly that penalty while discarding the (unused) returndata pointer. This
50            // mirrors the proving circuit and post-#217 zk_evm; see `get_calldata`.
51            get_calldata(raw_abi, is_pointer, vm, true);
52            None
53        } else {
54            let result = get_calldata(raw_abi, is_pointer, vm, false).filter(|pointer| {
55                if vm.state.current_frame.is_kernel {
56                    true
57                } else {
58                    // Non-kernel returndata forwarding must be unidirectional: callers may pass
59                    // pointers down the stack, but callees must not forward pointers to older pages.
60                    // This mirrors zk_evm's restriction based on base memory page checks.
61                    pointer.memory_page.as_u32() >= base_page_from_heap(vm.state.current_frame.heap)
62                        && pointer.memory_page != vm.state.current_frame.calldata_heap
63                }
64            });
65
66            if result.is_none() {
67                return_type = ReturnType::Panic;
68            }
69            result
70        };
71
72        let leftover_gas = vm.state.current_frame.gas;
73
74        let Some(FrameRemnant {
75            exception_handler,
76            snapshot,
77        }) = vm.pop_frame(
78            return_value_or_panic
79                .as_ref()
80                .map(|pointer| pointer.memory_page),
81            return_value_or_panic
82                .as_ref()
83                .map(|pointer| (pointer.start, pointer.length)),
84        )
85        else {
86            // The initial frame is not rolled back, even if it fails.
87            // It is the caller's job to clean up when the execution as a whole fails because
88            // the caller may take external snapshots while the VM is in the initial frame and
89            // these would break were the initial frame to be rolled back.
90
91            // But to continue execution would be nonsensical and can cause UB because there
92            // is no next instruction after a panic arising from some other instruction.
93            vm.state.current_frame.pc = invalid_instruction();
94
95            return if let Some(return_value) = return_value_or_panic {
96                let output = vm.state.heaps[return_value.memory_page]
97                    .read_range_big_endian(
98                        return_value.start..return_value.start + return_value.length,
99                    )
100                    .clone();
101                if return_type == ReturnType::Revert {
102                    ExecutionStatus::Stopped(ExecutionEnd::Reverted(output))
103                } else {
104                    ExecutionStatus::Stopped(ExecutionEnd::ProgramFinished(output))
105                }
106            } else {
107                ExecutionStatus::Stopped(ExecutionEnd::Panicked)
108            };
109        };
110
111        vm.state.set_context_u128(0);
112        vm.state.registers = [U256::zero(); 16];
113
114        if let Some(return_value) = return_value_or_panic {
115            vm.state.registers[1] = return_value.into_u256();
116        }
117        vm.state.register_pointer_flags = 2;
118
119        if return_type.is_failure() {
120            vm.state.current_frame.set_pc_from_u16(exception_handler);
121        }
122
123        (snapshot, leftover_gas)
124    };
125
126    if return_type.is_failure() {
127        vm.world_diff.append_rollback_logs(&snapshot);
128        vm.world_diff.rollback(snapshot);
129    }
130
131    vm.state.flags = Flags::new(return_type == ReturnType::Panic, false, false);
132    vm.state.current_frame.gas += leftover_gas;
133
134    ExecutionStatus::Running
135}
136
137fn ret<T: Tracer, W: World<T>, RT: TypeLevelReturnType, const TO_LABEL: bool>(
138    vm: &mut VirtualMachine<T, W>,
139    world: &mut W,
140    tracer: &mut T,
141) -> ExecutionStatus {
142    full_boilerplate::<opcodes::Ret<RT>, _, _>(vm, world, tracer, |vm, args, _, _| {
143        naked_ret::<T, W, RT, TO_LABEL>(vm, args)
144    })
145}
146
147/// Turn the current instruction into a panic at no extra cost. (Great value, I know.)
148///
149/// Call this when:
150/// - gas runs out when paying for the fixed cost of an instruction
151/// - causing side effects in a static context
152/// - using privileged instructions while not in a system call
153/// - the far call stack overflows
154///
155/// For all other panics, point the instruction pointer at [PANIC] instead.
156pub(crate) fn free_panic<T: Tracer, W: World<T>>(
157    vm: &mut VirtualMachine<T, W>,
158    world: &mut W,
159    tracer: &mut T,
160) -> ExecutionStatus {
161    tracer.before_instruction::<opcodes::Ret<Panic>, _>(&mut VmAndWorld { vm, world });
162    // A spontaneous panic has no return ABI: these empty args encode source register r0, so
163    // naked_ret's return-ABI resolution reads zero and charges no heap growth. (args are otherwise
164    // only consulted for the jump label when TO_LABEL is set, which it isn't here.)
165    naked_ret::<T, W, Panic, false>(
166        vm,
167        &Arguments::new(Predicate::Always, 0, ModeRequirements::none()),
168    )
169    .merge_tracer(tracer.after_instruction::<opcodes::Ret<Panic>, _>(&mut VmAndWorld { vm, world }))
170}
171
172fn invalid<T: Tracer, W: World<T>>(
173    vm: &mut VirtualMachine<T, W>,
174    world: &mut W,
175    tracer: &mut T,
176) -> ExecutionStatus {
177    vm.state.current_frame.gas = 0;
178    free_panic(vm, world, tracer)
179}
180
181trait GenericStatics<T, W> {
182    const PANIC: Instruction<T, W>;
183    const INVALID: Instruction<T, W>;
184}
185
186impl<T: Tracer, W: World<T>> GenericStatics<T, W> for () {
187    const PANIC: Instruction<T, W> = Instruction::from_spontaneous_panic();
188    const INVALID: Instruction<T, W> = Instruction::from_invalid();
189}
190
191// The following functions return references that live for 'static.
192// They aren't marked as such because returning any lifetime is more ergonomic.
193
194/// Point the program counter at this instruction when a panic occurs during the logic of and instruction.
195pub(crate) fn spontaneous_panic<'a, T: Tracer, W: World<T>>() -> &'a Instruction<T, W> {
196    &<()>::PANIC
197}
198
199/// Panics, burning all available gas.
200pub(crate) fn invalid_instruction<'a, T: Tracer, W: World<T>>() -> &'a Instruction<T, W> {
201    &<()>::INVALID
202}
203
204pub(crate) const RETURN_COST: u32 = 5;
205
206/// Variations of [`Ret`](opcodes::Ret) instructions.
207impl<T: Tracer, W: World<T>> Instruction<T, W> {
208    /// Creates a normal [`Ret`](opcodes::Ret) instruction with the provided params.
209    pub fn from_ret(src1: Register1, label: Option<Immediate1>, arguments: Arguments) -> Self {
210        let to_label = label.is_some();
211        Self {
212            handler: monomorphize!(ret [T W Normal] match_boolean to_label),
213            arguments: arguments.write_source(&src1).write_source(&label),
214        }
215    }
216
217    /// Creates a revert [`Ret`](opcodes::Ret) instruction with the provided params.
218    pub fn from_revert(src1: Register1, label: Option<Immediate1>, arguments: Arguments) -> Self {
219        let to_label = label.is_some();
220        Self {
221            handler: monomorphize!(ret [T W Revert] match_boolean to_label),
222            arguments: arguments.write_source(&src1).write_source(&label),
223        }
224    }
225
226    /// Creates a panic [`Ret`](opcodes::Ret) instruction with the provided params.
227    ///
228    /// `src1` carries the return-ABI register. Even though a panic forwards no returndata, the
229    /// register is still resolved for its heap-growth cost (see `naked_ret`).
230    pub fn from_panic(src1: Register1, label: Option<Immediate1>, arguments: Arguments) -> Self {
231        let to_label = label.is_some();
232        Self {
233            handler: monomorphize!(ret [T W Panic] match_boolean to_label),
234            arguments: arguments.write_source(&src1).write_source(&label),
235        }
236    }
237
238    /// Creates the instruction that is executed when anonther instruction encounters
239    /// an error.
240    pub(crate) const fn from_spontaneous_panic() -> Self {
241        Self {
242            handler: ret::<T, W, Panic, false>,
243            arguments: Arguments::new(Predicate::Always, RETURN_COST, ModeRequirements::none()),
244        }
245    }
246
247    /// Creates a *invalid* instruction that will panic by draining all gas.
248    pub const fn from_invalid() -> Self {
249        Self {
250            handler: invalid,
251            arguments: Arguments::new(
252                Predicate::Always,
253                INVALID_INSTRUCTION_COST,
254                ModeRequirements::none(),
255            ),
256        }
257    }
258}