Skip to main content

zksync_vm2/instruction_handlers/
far_call.rs

1use primitive_types::U256;
2use zkevm_opcode_defs::{system_params::MSG_VALUE_SIMULATOR_ADDITIVE_COST, ADDRESS_MSG_VALUE};
3use zksync_vm2_interface::{
4    opcodes::{FarCall, TypeLevelCallingMode},
5    Tracer,
6};
7
8use super::{
9    common::full_boilerplate,
10    heap_access::grow_heap,
11    monomorphization::{match_boolean, monomorphize, parameterize},
12    AuxHeap, Heap,
13};
14use crate::{
15    addressing_modes::{Arguments, Immediate1, Register1, Register2, Source},
16    decommit::{is_kernel, materialize_decommit_page, u256_into_address},
17    fat_pointer::FatPointer,
18    instruction::ExecutionStatus,
19    page_ids::code_page_from_base,
20    predication::Flags,
21    Instruction, Program, VirtualMachine, World,
22};
23
24/// A call to another contract.
25///
26/// First, the code of the called contract is fetched and a fat pointer is created
27/// or and existing one is forwarded. Costs for decommitting and memory growth are paid
28/// at this point.
29///
30/// A new stack frame is pushed. At most 63/64 of the *remaining* gas is passed to the called contract.
31///
32/// Even though all errors happen before the new stack frame, they cause a panic in the new frame,
33/// not in the caller!
34fn far_call<T, W, M, const IS_STATIC: bool, const IS_SHARD: bool>(
35    vm: &mut VirtualMachine<T, W>,
36    world: &mut W,
37    tracer: &mut T,
38) -> ExecutionStatus
39where
40    T: Tracer,
41    W: World<T>,
42    M: TypeLevelCallingMode,
43{
44    full_boilerplate::<FarCall<M>, _, _>(vm, world, tracer, |vm, args, world, tracer| {
45        let (raw_abi, raw_abi_is_pointer) = Register1::get_with_pointer_flag(args, &mut vm.state);
46
47        let address_mask: U256 = U256::MAX >> (256 - 160);
48        let destination_address = Register2::get(args, &mut vm.state) & address_mask;
49        let exception_handler = Immediate1::get_u16(args);
50
51        let mut abi = get_far_call_arguments(raw_abi);
52        abi.is_constructor_call = abi.is_constructor_call && vm.state.current_frame.is_kernel;
53        abi.is_system_call =
54            abi.is_system_call && is_kernel(u256_into_address(destination_address));
55
56        let mut mandated_gas =
57            if abi.is_system_call && destination_address == ADDRESS_MSG_VALUE.into() {
58                MSG_VALUE_SIMULATOR_ADDITIVE_COST
59            } else {
60                0
61            };
62        let new_base_page = vm.state.next_base_page();
63
64        let fallible_part = (|| {
65            let shard_call_failed = IS_SHARD && abi.shard_id != 0;
66
67            let (maybe_calldata, decommit_result) = if shard_call_failed {
68                // calldata has to be constructed even if we already know we will panic because
69                // overflowing start + length makes the heap resize even when already panicking.
70                (get_calldata(raw_abi, raw_abi_is_pointer, vm, true), None)
71            } else {
72                let decommit_result = vm.world_diff.decommit(
73                    world,
74                    tracer,
75                    destination_address,
76                    vm.settings.default_aa_code_hash,
77                    vm.settings.evm_interpreter_code_hash,
78                    abi.is_constructor_call,
79                    vm.state.transaction_number,
80                );
81
82                // calldata has to be constructed even if we already know we will panic because
83                // overflowing start + length makes the heap resize even when already panicking.
84                let already_failed = decommit_result.is_none();
85                let maybe_calldata = get_calldata(raw_abi, raw_abi_is_pointer, vm, already_failed);
86                (maybe_calldata, decommit_result)
87            };
88
89            // mandated gas is passed even if it means transferring more than the 63/64 rule allows
90            if let Some(gas_left) = vm.state.current_frame.gas.checked_sub(mandated_gas) {
91                vm.state.current_frame.gas = gas_left;
92            } else {
93                // If the gas is insufficient, the rest is burned
94                vm.state.current_frame.gas = 0;
95                mandated_gas = 0;
96                return None;
97            }
98
99            if shard_call_failed {
100                return None;
101            }
102            let calldata = maybe_calldata?;
103            let (unpaid_decommit, is_evm, is_evm_blob_format) = decommit_result?;
104            let code_hash = unpaid_decommit.code_key();
105            let should_materialize = unpaid_decommit.should_materialize();
106            let program = vm.world_diff.pay_for_decommit(
107                world,
108                tracer,
109                unpaid_decommit,
110                &mut vm.state.current_frame.gas,
111            )?;
112
113            if should_materialize {
114                // TODO: The interfaces that `World` provide exposes either a parsed program OR bytes,
115                // so converting back to bytes here feels like a more reasonable choice; though probably
116                // a more optimal approach is possible if we rework interfaces either for the `World` or
117                // for heap instantiation.
118                let code = program_to_bytes(&program);
119                materialize_decommit_page(vm, code_hash, &code, code_page_from_base(new_base_page));
120            }
121
122            Some((calldata, program, is_evm, is_evm_blob_format))
123        })();
124
125        let maximum_gas = vm.state.current_frame.gas / 64 * 63;
126        let normally_passed_gas = abi.gas_to_pass.min(maximum_gas);
127        vm.state.current_frame.gas -= normally_passed_gas;
128        let new_frame_gas = normally_passed_gas + mandated_gas;
129
130        // A far call pushes a new frame and returns from it in the next instruction if it panics.
131        let (calldata, program, is_evm_interpreter, is_evm_blob_format) = fallible_part
132            .unwrap_or_else(|| (U256::zero().into(), Program::new_panicking(), false, false));
133
134        let new_frame_is_static = IS_STATIC || vm.state.current_frame.is_static;
135        vm.push_frame::<M>(
136            u256_into_address(destination_address),
137            program,
138            new_frame_gas,
139            exception_handler,
140            new_frame_is_static && !is_evm_interpreter,
141            is_evm_blob_format,
142            calldata.memory_page,
143            vm.world_diff.snapshot(),
144        );
145
146        vm.state.flags = Flags::new(false, false, false);
147
148        if abi.is_system_call {
149            // r3 to r12 are kept but they lose their pointer flags
150            vm.state.registers[13] = U256::zero();
151            vm.state.registers[14] = U256::zero();
152            vm.state.registers[15] = U256::zero();
153        } else {
154            vm.state.registers = [U256::zero(); 16];
155        }
156
157        // Only r1 is a pointer
158        vm.state.register_pointer_flags = 2;
159        vm.state.registers[1] = calldata.into_u256();
160
161        let is_static_call_to_evm_interpreter = new_frame_is_static && is_evm_interpreter;
162        let call_type = (u8::from(is_static_call_to_evm_interpreter) << 2)
163            | (u8::from(abi.is_system_call) << 1)
164            | u8::from(abi.is_constructor_call);
165
166        vm.state.registers[2] = call_type.into();
167
168        ExecutionStatus::Running
169    })
170}
171
172#[derive(Debug)]
173pub(crate) struct FarCallABI {
174    pub(crate) gas_to_pass: u32,
175    pub(crate) shard_id: u8,
176    pub(crate) is_constructor_call: bool,
177    pub(crate) is_system_call: bool,
178}
179
180#[allow(clippy::cast_possible_truncation)] // intentional
181fn get_far_call_arguments(abi: U256) -> FarCallABI {
182    let gas_to_pass = abi.0[3] as u32;
183    let settings = (abi.0[3] >> 32) as u32;
184    let [_, shard_id, constructor_call_byte, system_call_byte] = settings.to_le_bytes();
185
186    FarCallABI {
187        gas_to_pass,
188        shard_id,
189        is_constructor_call: constructor_call_byte != 0,
190        is_system_call: system_call_byte != 0,
191    }
192}
193
194fn program_to_bytes<T, W>(program: &Program<T, W>) -> Vec<u8> {
195    let mut result = Vec::with_capacity(program.code_page().len() * 32);
196    #[allow(clippy::explicit_iter_loop)] // `.iter()` is required in this case
197    for word in program.code_page().iter() {
198        let mut bytes = [0u8; 32];
199        word.to_big_endian(&mut bytes);
200        result.extend_from_slice(&bytes);
201    }
202    result
203}
204
205/// Forms a new fat pointer or narrows an existing one, as dictated by the ABI.
206///
207/// This function needs to be called even if we already know we will panic because
208/// overflowing start + length makes the heap resize even when already panicking.
209pub(crate) fn get_calldata<T, W>(
210    raw_abi: U256,
211    is_pointer: bool,
212    vm: &mut VirtualMachine<T, W>,
213    already_failed: bool,
214) -> Option<FatPointer> {
215    let mut pointer = FatPointer::from(raw_abi);
216    #[allow(clippy::cast_possible_truncation)]
217    // intentional: the source is encoded in the lower byte of the extracted value
218    let raw_source = (raw_abi.0[3] >> 32) as u8;
219
220    match FatPointerSource::from_abi(raw_source) {
221        FatPointerSource::ForwardFatPointer => {
222            if !is_pointer || pointer.offset > pointer.length {
223                return None;
224            }
225
226            pointer.narrow();
227        }
228        FatPointerSource::MakeNewPointer(target) => {
229            let mut grow = |size| {
230                match target {
231                    FatPointerTarget::ToHeap => {
232                        grow_heap::<_, _, Heap>(&mut vm.state, size).ok()?;
233                        pointer.memory_page = vm.state.current_frame.heap;
234                    }
235                    FatPointerTarget::ToAuxHeap => {
236                        grow_heap::<_, _, AuxHeap>(&mut vm.state, size).ok()?;
237                        pointer.memory_page = vm.state.current_frame.aux_heap;
238                    }
239                }
240                Some(())
241            };
242
243            // A pointer whose start + length > u32::MAX always causes the heap to grow,
244            // even if it doesn't fullfill any other validity criteria.
245            if let Some(bound) = pointer.start.checked_add(pointer.length) {
246                if is_pointer || pointer.offset != 0 || already_failed {
247                    return None;
248                }
249                grow(bound)?;
250            } else {
251                grow(u32::MAX);
252                return None;
253            }
254        }
255    }
256
257    Some(pointer)
258}
259
260#[derive(Debug)]
261enum FatPointerSource {
262    MakeNewPointer(FatPointerTarget),
263    ForwardFatPointer,
264}
265
266#[derive(Debug)]
267enum FatPointerTarget {
268    ToHeap,
269    ToAuxHeap,
270}
271
272impl FatPointerSource {
273    const fn from_abi(value: u8) -> Self {
274        match value {
275            1 => Self::ForwardFatPointer,
276            2 => Self::MakeNewPointer(FatPointerTarget::ToAuxHeap),
277            _ => Self::MakeNewPointer(FatPointerTarget::ToHeap), // default
278        }
279    }
280}
281
282impl FatPointer {
283    fn narrow(&mut self) {
284        self.start += self.offset;
285        self.length -= self.offset;
286        self.offset = 0;
287    }
288}
289
290impl<T: Tracer, W: World<T>> Instruction<T, W> {
291    /// Creates a [`FarCall`] instruction with the provided mode and params.
292    pub fn from_far_call<M: TypeLevelCallingMode>(
293        src1: Register1,
294        src2: Register2,
295        error_handler: Immediate1,
296        is_static: bool,
297        is_shard: bool,
298        arguments: Arguments,
299    ) -> Self {
300        Self {
301            handler: monomorphize!(far_call [T W M] match_boolean is_static match_boolean is_shard),
302            arguments: arguments
303                .write_source(&src1)
304                .write_source(&src2)
305                .write_source(&error_handler),
306        }
307    }
308}