Skip to main content

zksync_vm2/instruction_handlers/
heap_access.rs

1use primitive_types::U256;
2use zksync_vm2_interface::{opcodes, HeapId, OpcodeType, Tracer};
3
4use super::{
5    common::{boilerplate, full_boilerplate},
6    monomorphization::{match_boolean, match_reg_imm, monomorphize, parameterize},
7    ret::spontaneous_panic,
8};
9use crate::{
10    addressing_modes::{
11        Addressable, Arguments, Destination, DestinationWriter, Immediate1, Register1, Register2,
12        RegisterOrImmediate, Source,
13    },
14    fat_pointer::FatPointer,
15    instruction::ExecutionStatus,
16    page_ids::static_memory_page,
17    state::State,
18    ExecutionEnd, Instruction, VirtualMachine, World,
19};
20
21/// Dedicated heap page used to model `zk_evm` static memory.
22const STATIC_MEMORY_HEAP: HeapId = static_memory_page();
23
24pub(crate) trait HeapFromState {
25    type Read: OpcodeType;
26    type Write: OpcodeType;
27
28    fn get_heap<T, W>(state: &State<T, W>) -> HeapId;
29    fn get_heap_size<T, W>(state: &mut State<T, W>) -> &mut u32;
30}
31
32pub(crate) struct Heap;
33
34impl HeapFromState for Heap {
35    type Read = opcodes::HeapRead;
36    type Write = opcodes::HeapWrite;
37
38    fn get_heap<T, W>(state: &State<T, W>) -> HeapId {
39        state.current_frame.heap
40    }
41
42    fn get_heap_size<T, W>(state: &mut State<T, W>) -> &mut u32 {
43        &mut state.current_frame.heap_size
44    }
45}
46
47pub(crate) struct AuxHeap;
48
49impl HeapFromState for AuxHeap {
50    type Read = opcodes::AuxHeapRead;
51    type Write = opcodes::AuxHeapWrite;
52
53    fn get_heap<T, W>(state: &State<T, W>) -> HeapId {
54        state.current_frame.aux_heap
55    }
56
57    fn get_heap_size<T, W>(state: &mut State<T, W>) -> &mut u32 {
58        &mut state.current_frame.aux_heap_size
59    }
60}
61
62/// The last address to which 32 can be added without overflow.
63const LAST_ADDRESS: u32 = u32::MAX - 32;
64
65// Necessary because the obvious code compiles to a comparison of two 256-bit numbers.
66#[inline(always)]
67fn bigger_than_last_address(x: U256) -> bool {
68    x.0[0] > LAST_ADDRESS.into() || x.0[1] != 0 || x.0[2] != 0 || x.0[3] != 0
69}
70
71fn set_incremented_read_offset(
72    args: &Arguments,
73    state: &mut impl Addressable,
74    value: U256,
75    is_pointer: bool,
76) {
77    // zk_evm treats heap and static memory reads as raw offset accesses, but the
78    // incremented dst1 register still inherits src0's pointer tag. The observable
79    // case is a zero-valued panic returndata pointer: the range check accepts it,
80    // and later pointer operations must still see a pointer.
81    if is_pointer {
82        Register2::set_fat_ptr(args, state, value);
83    } else {
84        Register2::set(args, state, value);
85    }
86}
87
88fn load<T: Tracer, W: World<T>, H: HeapFromState, In: Source, const INCREMENT: bool>(
89    vm: &mut VirtualMachine<T, W>,
90    world: &mut W,
91    tracer: &mut T,
92) -> ExecutionStatus {
93    boilerplate::<H::Read, _, _>(vm, world, tracer, |vm, args| {
94        // Pointers need not be masked here even though we do not care about them being pointers.
95        // They will panic, though because they are larger than 2^32.
96        let (pointer, input_is_pointer) = In::get_with_pointer_flag(args, &mut vm.state);
97
98        if bigger_than_last_address(pointer) {
99            let _ = vm.state.use_gas(u32::MAX);
100            vm.state.current_frame.pc = spontaneous_panic();
101            return;
102        }
103
104        let address = pointer.low_u32();
105        let new_bound = address.wrapping_add(32);
106        if grow_heap::<_, _, H>(&mut vm.state, new_bound).is_err() {
107            vm.state.current_frame.pc = spontaneous_panic();
108            return;
109        }
110
111        let heap = H::get_heap(&vm.state);
112        let value = vm.state.heaps[heap].read_u256(address);
113        Register1::set(args, &mut vm.state, value);
114
115        if INCREMENT {
116            set_incremented_read_offset(args, &mut vm.state, pointer + 32, input_is_pointer);
117        }
118    })
119}
120
121fn store<T, W: World<T>, H, In, const INCREMENT: bool, const HOOKING_ENABLED: bool>(
122    vm: &mut VirtualMachine<T, W>,
123    world: &mut W,
124    tracer: &mut T,
125) -> ExecutionStatus
126where
127    T: Tracer,
128    H: HeapFromState,
129    In: Source,
130{
131    full_boilerplate::<H::Write, _, _>(vm, world, tracer, |vm, args, _, _| {
132        // Pointers need not be masked here even though we do not care about them being pointers.
133        // They will panic, though because they are larger than 2^32.
134        let (pointer, _) = In::get_with_pointer_flag(args, &mut vm.state);
135
136        if bigger_than_last_address(pointer) {
137            let _ = vm.state.use_gas(u32::MAX);
138            vm.state.current_frame.pc = spontaneous_panic();
139            return ExecutionStatus::Running;
140        }
141
142        let address = pointer.low_u32();
143        let value = Register2::get(args, &mut vm.state);
144
145        let new_bound = address.wrapping_add(32);
146        if grow_heap::<_, _, H>(&mut vm.state, new_bound).is_err() {
147            vm.state.current_frame.pc = spontaneous_panic();
148            return ExecutionStatus::Running;
149        }
150
151        let heap = H::get_heap(&vm.state);
152        vm.state.heaps.write_u256(heap, address, value);
153
154        if INCREMENT {
155            Register1::set(args, &mut vm.state, pointer + 32);
156        }
157
158        if HOOKING_ENABLED && address == vm.settings.hook_address {
159            ExecutionStatus::Stopped(ExecutionEnd::SuspendedOnHook(value.as_u32()))
160        } else {
161            ExecutionStatus::Running
162        }
163    })
164}
165
166/// Pays for more heap space. Doesn't acually grow the heap.
167/// That distinction is necessary because the bootloader gets `u32::MAX` heap for free.
168pub(crate) fn grow_heap<T, W, H: HeapFromState>(
169    state: &mut State<T, W>,
170    new_bound: u32,
171) -> Result<(), ()> {
172    if let Some(to_pay) = new_bound.checked_sub(*H::get_heap_size(state)) {
173        state.use_gas(to_pay)?;
174        *H::get_heap_size(state) = new_bound;
175    }
176
177    Ok(())
178}
179
180fn load_pointer<T: Tracer, W: World<T>, const INCREMENT: bool>(
181    vm: &mut VirtualMachine<T, W>,
182    world: &mut W,
183    tracer: &mut T,
184) -> ExecutionStatus {
185    boilerplate::<opcodes::PointerRead, _, _>(vm, world, tracer, |vm, args| {
186        let (input, input_is_pointer) = Register1::get_with_pointer_flag(args, &mut vm.state);
187        if !input_is_pointer {
188            vm.state.current_frame.pc = spontaneous_panic();
189            return;
190        }
191        let pointer = FatPointer::from(input);
192
193        // Usually, we just read zeroes instead of out-of-bounds bytes
194        // but if offset + 32 is not representable, we panic, even if we could've read some bytes.
195        // This is not a bug, this is how it must work to be backwards compatible.
196        if pointer.offset > LAST_ADDRESS {
197            vm.state.current_frame.pc = spontaneous_panic();
198            return;
199        }
200
201        let start = pointer.start + pointer.offset.min(pointer.length);
202        let end = start.saturating_add(32).min(pointer.start + pointer.length);
203
204        let value = vm.state.heaps[pointer.memory_page].read_u256_partially(start..end);
205        Register1::set(args, &mut vm.state, value);
206
207        if INCREMENT {
208            // This addition does not overflow because we checked that the offset is small enough above.
209            Register2::set_fat_ptr(args, &mut vm.state, input + 32);
210        }
211    })
212}
213
214fn load_static<T: Tracer, W: World<T>, In: Source, const INCREMENT: bool>(
215    vm: &mut VirtualMachine<T, W>,
216    world: &mut W,
217    tracer: &mut T,
218) -> ExecutionStatus {
219    boilerplate::<opcodes::StaticMemoryRead, _, _>(vm, world, tracer, |vm, args| {
220        // Static memory uses a plain 32-bit offset in src0, same as heap UMA ops.
221        // Pointer-typed values are still accepted as raw words and then validated by range check.
222        let (pointer, input_is_pointer) = In::get_with_pointer_flag(args, &mut vm.state);
223
224        if bigger_than_last_address(pointer) {
225            vm.state.current_frame.pc = spontaneous_panic();
226            return;
227        }
228
229        let address = pointer.low_u32();
230        let value = vm.state.heaps[STATIC_MEMORY_HEAP].read_u256(address);
231        Register1::set(args, &mut vm.state, value);
232
233        if INCREMENT {
234            set_incremented_read_offset(args, &mut vm.state, pointer + 32, input_is_pointer);
235        }
236    })
237}
238
239fn store_static<T, W: World<T>, In, const INCREMENT: bool>(
240    vm: &mut VirtualMachine<T, W>,
241    world: &mut W,
242    tracer: &mut T,
243) -> ExecutionStatus
244where
245    T: Tracer,
246    In: Source,
247{
248    boilerplate::<opcodes::StaticMemoryWrite, _, _>(vm, world, tracer, |vm, args| {
249        // Static memory uses a plain 32-bit offset in src0, same as heap UMA ops.
250        // Pointer-typed values are still accepted as raw words and then validated by range check.
251        let (pointer, _) = In::get_with_pointer_flag(args, &mut vm.state);
252
253        if bigger_than_last_address(pointer) {
254            vm.state.current_frame.pc = spontaneous_panic();
255            return;
256        }
257
258        let address = pointer.low_u32();
259        let value = Register2::get(args, &mut vm.state);
260        vm.state
261            .heaps
262            .write_u256(STATIC_MEMORY_HEAP, address, value);
263
264        if INCREMENT {
265            Register1::set(args, &mut vm.state, pointer + 32);
266        }
267    })
268}
269
270impl<T: Tracer, W: World<T>> Instruction<T, W> {
271    /// Creates a [`HeapRead`](opcodes::HeapRead) instruction with the provided params.
272    pub fn from_heap_read(
273        src: RegisterOrImmediate,
274        out: Register1,
275        incremented_out: Option<Register2>,
276        arguments: Arguments,
277    ) -> Self {
278        Self::from_read::<Heap>(src, out, incremented_out, arguments)
279    }
280
281    /// Creates an [`AuxHeapRead`](opcodes::AuxHeapRead) instruction with the provided params.
282    pub fn from_aux_heap_read(
283        src: RegisterOrImmediate,
284        out: Register1,
285        incremented_out: Option<Register2>,
286        arguments: Arguments,
287    ) -> Self {
288        Self::from_read::<AuxHeap>(src, out, incremented_out, arguments)
289    }
290
291    /// Creates a static memory read instruction.
292    pub fn from_static_memory_read(
293        src: RegisterOrImmediate,
294        out: Register1,
295        incremented_out: Option<Register2>,
296        arguments: Arguments,
297    ) -> Self {
298        let mut arguments = arguments.write_source(&src).write_destination(&out);
299
300        let increment = incremented_out.is_some();
301        if let Some(out2) = incremented_out {
302            out2.write_destination(&mut arguments);
303        }
304
305        Self {
306            handler: monomorphize!(load_static [T W] match_reg_imm src match_boolean increment),
307            arguments,
308        }
309    }
310
311    fn from_read<H: HeapFromState>(
312        src: RegisterOrImmediate,
313        out: Register1,
314        incremented_out: Option<Register2>,
315        arguments: Arguments,
316    ) -> Self {
317        let mut arguments = arguments.write_source(&src).write_destination(&out);
318
319        let increment = incremented_out.is_some();
320        if let Some(out2) = incremented_out {
321            out2.write_destination(&mut arguments);
322        }
323
324        Self {
325            handler: monomorphize!(load [T W H] match_reg_imm src match_boolean increment),
326            arguments,
327        }
328    }
329
330    /// Creates a [`HeapWrite`](opcodes::HeapWrite) instruction with the provided params.
331    pub fn from_heap_write(
332        src1: RegisterOrImmediate,
333        src2: Register2,
334        incremented_out: Option<Register1>,
335        arguments: Arguments,
336        should_hook: bool,
337    ) -> Self {
338        Self::from_write::<Heap>(src1, src2, incremented_out, arguments, should_hook)
339    }
340
341    /// Creates an [`AuxHeapWrite`](opcodes::AuxHeapWrite) instruction with the provided params.
342    pub fn from_aux_heap_store(
343        src1: RegisterOrImmediate,
344        src2: Register2,
345        incremented_out: Option<Register1>,
346        arguments: Arguments,
347    ) -> Self {
348        Self::from_write::<AuxHeap>(src1, src2, incremented_out, arguments, false)
349    }
350
351    /// Creates a static memory write instruction.
352    pub fn from_static_memory_write(
353        src1: RegisterOrImmediate,
354        src2: Register2,
355        incremented_out: Option<Register1>,
356        arguments: Arguments,
357    ) -> Self {
358        let increment = incremented_out.is_some();
359        Self {
360            handler: monomorphize!(store_static [T W] match_reg_imm src1 match_boolean increment),
361            arguments: arguments
362                .write_source(&src1)
363                .write_source(&src2)
364                .write_destination(&incremented_out),
365        }
366    }
367
368    fn from_write<H: HeapFromState>(
369        src1: RegisterOrImmediate,
370        src2: Register2,
371        incremented_out: Option<Register1>,
372        arguments: Arguments,
373        should_hook: bool,
374    ) -> Self {
375        let increment = incremented_out.is_some();
376        Self {
377            handler: monomorphize!(store [T W H] match_reg_imm src1 match_boolean increment match_boolean should_hook),
378            arguments: arguments
379                .write_source(&src1)
380                .write_source(&src2)
381                .write_destination(&incremented_out),
382        }
383    }
384
385    /// Creates an [`PointerRead`](opcodes::PointerRead) instruction with the provided params.
386    pub fn from_pointer_read(
387        src: Register1,
388        out: Register1,
389        incremented_out: Option<Register2>,
390        arguments: Arguments,
391    ) -> Self {
392        let increment = incremented_out.is_some();
393        Self {
394            handler: monomorphize!(load_pointer [T W] match_boolean increment),
395            arguments: arguments
396                .write_source(&src)
397                .write_destination(&out)
398                .write_destination(&incremented_out),
399        }
400    }
401}