diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 00000000..f70a2298 --- /dev/null +++ b/.clippy.toml @@ -0,0 +1,2 @@ +# Configures additional identifiers excempt from `clippy::doc_markdown` lint +doc-valid-idents = ["EraVM", "ZKsync", ".."] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 128a53b9..8a7fa3c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ on: env: CARGO_TERM_COLOR: always + # Nightly Rust necessary for building docs. + RUST_NIGHTLY_VERSION: nightly-2024-08-01 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -32,7 +34,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Install cargo-afl - run: cargo install cargo-afl --version=^0.15 --force + run: cargo install cargo-afl --version=^0.15 --locked --force - name: Build project run: | @@ -65,3 +67,34 @@ jobs: run: | AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \ cargo afl fuzz -i tests/afl-fuzz/in -o tests/afl-fuzz/out -V 60 target/debug/zksync_vm2_afl_fuzz + + document: + needs: + - build_and_test + if: github.event_name == 'push' && github.ref_type == 'branch' + permissions: + contents: write + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_NIGHTLY_VERSION }} + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Build docs + run: | + cargo clean --doc && \ + cargo rustdoc -p zksync_vm2_interface -- -Z unstable-options --enable-index-page && \ + cargo rustdoc -p zksync_vm2 -- -Z unstable-options --enable-index-page + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: target/doc + single-commit: true diff --git a/Cargo.toml b/Cargo.toml index 70428c3a..f8d759c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,16 @@ zk_evm = { git = "https://github.com/matter-labs/era-zk_evm.git", rev = "b7caa02 # Dependencies within the workspace zksync_vm2_interface = { version = "0.1.0", path = "crates/vm2-interface" } zksync_vm2 = { version = "0.1.0", path = "crates/vm2" } + +[workspace.lints.rust] +missing_docs = "warn" +missing_debug_implementations = "warn" +unreachable_pub = "warn" + +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +must_use_candidate = "allow" +module_name_repetitions = "allow" +inline_always = "allow" +struct_field_names = "allow" diff --git a/README.md b/README.md index 397d459a..5b238fa4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # High-Performance ZKsync Era VM (EraVM) +[![Build Status](https://github.com/matter-labs/vm2/actions/workflows/ci.yml/badge.svg)](https://github.com/matter-labs/vm2/actions) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/matter-labs/vm2#license) + A high-performance rewrite of the out-of-circuit VM for ZKsync Era (aka EraVM). See [Era docs](https://github.com/matter-labs/zksync-era/tree/main/docs/specs/zk_evm) for the VM overview and formal specification. diff --git a/crates/vm2-interface/Cargo.toml b/crates/vm2-interface/Cargo.toml index 38a5bece..8f43907b 100644 --- a/crates/vm2-interface/Cargo.toml +++ b/crates/vm2-interface/Cargo.toml @@ -13,3 +13,6 @@ categories.workspace = true [dependencies] primitive-types.workspace = true + +[lints] +workspace = true diff --git a/crates/vm2-interface/README.md b/crates/vm2-interface/README.md index dd251884..01735e67 100644 --- a/crates/vm2-interface/README.md +++ b/crates/vm2-interface/README.md @@ -1,5 +1,11 @@ # Stable Interface for ZKsync Era VM +[![Build Status](https://github.com/matter-labs/vm2/actions/workflows/ci.yml/badge.svg)](https://github.com/matter-labs/vm2/actions) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/matter-labs/vm2#license) + +**Documentation:** +[![crate docs (main)](https://img.shields.io/badge/main-yellow.svg?label=docs)](https://matter-labs.github.io/vm2/zksync_vm2_interface/) + This library provides a stable interface for EraVM. It defines an interface for tracers that will never change but may be extended. ## License diff --git a/crates/vm2-interface/src/lib.rs b/crates/vm2-interface/src/lib.rs index 1a83f2f6..5e1a8fb0 100644 --- a/crates/vm2-interface/src/lib.rs +++ b/crates/vm2-interface/src/lib.rs @@ -15,14 +15,14 @@ //! With any sane design it would be trivial to take a tracer written for version 1 and //! update it to work with version 2. However, then it can no longer be used with VM1. //! -//! This exact thing caused us a lot of trouble when we put many versions of zk_evm in multivm. +//! This exact thing caused us a lot of trouble when we put many versions of `zk_evm` in `multivm`. //! //! ## How do I add a new feature to the interface? //! //! Do not change the existing traits. In fact, you should delete existing code in the new //! version that you publish and import it from the previous version instead. //! -//! This is how you would add a new method to StateInterface and a new opcode. +//! This is how you would add a new method to [`StateInterface`] and a new opcode. //! //! ``` //! # use zksync_vm2_interface as zksync_vm2_interface_v1; diff --git a/crates/vm2-interface/src/state_interface.rs b/crates/vm2-interface/src/state_interface.rs index 1b5d6b70..a5bc7fee 100644 --- a/crates/vm2-interface/src/state_interface.rs +++ b/crates/vm2-interface/src/state_interface.rs @@ -1,106 +1,164 @@ use primitive_types::{H160, U256}; +/// Public interface of the VM state. Encompasses both read and write methods. pub trait StateInterface { + /// Reads a register with the specified zero-based index. Returns a value together with a pointer flag. fn read_register(&self, register: u8) -> (U256, bool); + /// Sets a register with the specified zero-based index fn set_register(&mut self, register: u8, value: U256, is_pointer: bool); + /// Returns a mutable handle to the current call frame. fn current_frame(&mut self) -> impl CallframeInterface + '_; + /// Returns the total number of call frames. fn number_of_callframes(&self) -> usize; + /// Returns a mutable handle to a call frame with the specified index, where /// zero is the current frame, one is the frame before that etc. fn callframe(&mut self, n: usize) -> impl CallframeInterface + '_; - fn read_heap_byte(&self, heap: HeapId, index: u32) -> u8; - /// Reads an entire `U256` word in the big-endian order from the specified heap page / `index` + /// Reads a single byte from the specified heap at the specified 0-based offset. + fn read_heap_byte(&self, heap: HeapId, offset: u32) -> u8; + /// Reads an entire `U256` word in the big-endian order from the specified heap / `offset` /// (which is the index of the most significant byte of the read value). - fn read_heap_u256(&self, heap: HeapId, index: u32) -> U256; - /// Writes an entire `U256` word in the big-endian order to the specified heap page at the specified `index` + fn read_heap_u256(&self, heap: HeapId, offset: u32) -> U256; + /// Writes an entire `U256` word in the big-endian order to the specified heap at the specified `offset` /// (which is the index of the most significant byte of the written value). - fn write_heap_u256(&mut self, heap: HeapId, index: u32, value: U256); + fn write_heap_u256(&mut self, heap: HeapId, offset: u32, value: U256); + /// Returns current execution flags. fn flags(&self) -> Flags; + /// Sets current execution flags. fn set_flags(&mut self, flags: Flags); + /// Returns the currently set 0-based transaction number. fn transaction_number(&self) -> u16; + /// Sets the current transaction number. fn set_transaction_number(&mut self, value: u16); + /// Returns the value of the context register. fn context_u128_register(&self) -> u128; + /// Sets the value of the context register. fn set_context_u128_register(&mut self, value: u128); + /// Iterates over storage slots read or written during VM execution. fn get_storage_state(&self) -> impl Iterator; - + /// Iterates over all transient storage slots set during VM execution. fn get_transient_storage_state(&self) -> impl Iterator; + /// Gets value of the specified transient storage slot. fn get_transient_storage(&self, address: H160, slot: U256) -> U256; + /// Sets value of the specified transient storage slot. fn write_transient_storage(&mut self, address: H160, slot: U256, value: U256); + /// Iterates over events emitted during VM execution. fn events(&self) -> impl Iterator; + /// Iterates over L2-to-L1 logs emitted during VM execution. fn l2_to_l1_logs(&self) -> impl Iterator; + /// Gets the current amount of published pubdata. fn pubdata(&self) -> i32; + /// Sets the current amount of published pubdata. fn set_pubdata(&mut self, value: i32); } +/// VM execution flags. See the EraVM reference for more details. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Flags { + /// "Less than" flag. pub less_than: bool, + /// "Equal" flag. pub equal: bool, + /// "Greater than" flag. pub greater: bool, } +/// Public interface of an EraVM call frame. pub trait CallframeInterface { + /// Address of the storage context associated with this frame. For delegate calls, this address is inherited from the calling contract; + /// otherwise, it's the same as [`Self::code_address()`]. fn address(&self) -> H160; + /// Sets the address of the executing contract. fn set_address(&mut self, address: H160); + /// Address of the contract being executed. fn code_address(&self) -> H160; + /// Sets the address of the contract being executed. Does not cause the contract at the specified address get loaded per se, just updates + /// the value used internally by the VM (e.g., returned by the [`CodeAddress`](crate::opcodes::CodeAddress) opcode). fn set_code_address(&mut self, address: H160); + /// Address of the calling contract. Respects delegate and mimic calls. fn caller(&self) -> H160; + /// Sets the address of the calling contract. fn set_caller(&mut self, address: H160); - /// During panic and arbitrary code execution this returns None. + /// Returns the current program counter (i.e., 0-based index of the instruction being executed). + /// During panic this returns `None`. fn program_counter(&self) -> Option; - + /// Sets the program counter. /// The VM will execute an invalid instruction if you jump out of the program. fn set_program_counter(&mut self, value: u16); + /// Returns the program counter that the parent frame should continue from if this frame fails. fn exception_handler(&self) -> u16; + /// Sets the exception handler as specified [above](Self::exception_handler()). fn set_exception_handler(&mut self, value: u16); + /// Checks whether the call is static. fn is_static(&self) -> bool; + /// Checks whether the call is executed in kernel mode. fn is_kernel(&self) -> bool; + /// Returns the remaining amount of gas. fn gas(&self) -> u32; + /// Sets the remaining amount of gas. fn set_gas(&mut self, new_gas: u32); + /// Additional gas provided for the duration of this callframe. fn stipend(&self) -> u32; + /// Returns the context value for this call. This context is accessible via [`ContextU128`](crate::opcodes::ContextU128) opcode. fn context_u128(&self) -> u128; + /// Sets the context value for this call. fn set_context_u128(&mut self, value: u128); + /// Checks whether this frame corresponds to a near call. fn is_near_call(&self) -> bool; + /// Reads the specified stack slot. Returns a value together with a pointer flag. fn read_stack(&self, index: u16) -> (U256, bool); + /// Sets the value and pointer flag for the specified stack slot. fn write_stack(&mut self, index: u16, value: U256, is_pointer: bool); + /// Returns the stack pointer. fn stack_pointer(&self) -> u16; + /// Sets the stack pointer. fn set_stack_pointer(&mut self, value: u16); + /// Returns ID of the main heap used in this call. fn heap(&self) -> HeapId; + /// Returns the main heap boundary (number of paid bytes). fn heap_bound(&self) -> u32; + /// Sets the main heap boundary. fn set_heap_bound(&mut self, value: u32); + /// Returns ID of the auxiliary heap used in this call. fn aux_heap(&self) -> HeapId; + /// Returns the auxiliary heap boundary (number of paid bytes). fn aux_heap_bound(&self) -> u32; + /// Sets the auxiliary heap boundary. fn set_aux_heap_bound(&mut self, value: u32); - fn read_code_page(&self, slot: u16) -> U256; + /// Reads a word from the bytecode of the executing contract. + fn read_contract_code(&self, slot: u16) -> U256; } -/// Identifier of a VM heap page. +/// Identifier of a VM heap. +/// +/// EraVM docs sometimes refer to heaps as *heap pages*; docs in these crate don't to avoid confusion with internal heap structure. #[derive(Copy, Clone, PartialEq, Debug)] pub struct HeapId(u32); impl HeapId { - /// Identifier of the calldata heap page used by the first executed program (i.e., the bootloader). + /// Identifier of the calldata heap used by the first executed program (i.e., the bootloader). pub const FIRST_CALLDATA: Self = Self(1); - /// Identifier of the heap page used by the first executed program (i.e., the bootloader). + /// Identifier of the heap used by the first executed program (i.e., the bootloader). pub const FIRST: Self = Self(2); - /// Identifier of the auxiliary heap page used by the first executed program (i.e., the bootloader) + /// Identifier of the auxiliary heap used by the first executed program (i.e., the bootloader) pub const FIRST_AUX: Self = Self(3); /// Only for dealing with external data structures, never use internally. @@ -109,33 +167,49 @@ impl HeapId { Self(value) } + /// Converts this ID to an integer value. pub const fn as_u32(self) -> u32 { self.0 } } +/// Event emitted by EraVM. +/// /// There is no address field because nobody is interested in events that don't come -/// from the event writer, so we simply do not record events coming frome anywhere else. -#[derive(Clone, PartialEq, Debug)] +/// from the event writer, so we simply do not record events coming from anywhere else. +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Event { + /// Event key. pub key: U256, + /// Event value. pub value: U256, + /// Is this event first in a chain of events? pub is_first: bool, + /// Shard identifier (currently, always set to 0). pub shard_id: u8, + /// 0-based index of a transaction that has emitted this event. pub tx_number: u16, } -#[derive(Debug)] +/// L2-to-L1 log emitted by EraVM. +#[derive(Debug, Clone, Copy, PartialEq)] pub struct L2ToL1Log { + /// Log key. pub key: U256, + /// Log value. pub value: U256, + /// Is this a service log? pub is_service: bool, + /// Address of the contract that has emitted this log. pub address: H160, + /// Shard identifier (currently, always set to 0). pub shard_id: u8, + /// 0-based index of a transaction that has emitted this event. pub tx_number: u16, } #[cfg(test)] +#[derive(Debug)] pub struct DummyState; #[cfg(test)] @@ -212,11 +286,11 @@ impl StateInterface for DummyState { unimplemented!() } - fn events(&self) -> impl Iterator { + fn events(&self) -> impl Iterator { std::iter::empty() } - fn l2_to_l1_logs(&self) -> impl Iterator { + fn l2_to_l1_logs(&self) -> impl Iterator { std::iter::empty() } @@ -319,7 +393,7 @@ impl CallframeInterface for DummyState { unimplemented!() } - fn heap(&self) -> crate::HeapId { + fn heap(&self) -> HeapId { unimplemented!() } @@ -331,7 +405,7 @@ impl CallframeInterface for DummyState { unimplemented!() } - fn aux_heap(&self) -> crate::HeapId { + fn aux_heap(&self) -> HeapId { unimplemented!() } @@ -343,7 +417,7 @@ impl CallframeInterface for DummyState { unimplemented!() } - fn read_code_page(&self, _: u16) -> U256 { + fn read_contract_code(&self, _: u16) -> U256 { unimplemented!() } } diff --git a/crates/vm2-interface/src/tracer_interface.rs b/crates/vm2-interface/src/tracer_interface.rs index 3aa75306..22a8b44b 100644 --- a/crates/vm2-interface/src/tracer_interface.rs +++ b/crates/vm2-interface/src/tracer_interface.rs @@ -48,54 +48,87 @@ macro_rules! forall_simple_opcodes { macro_rules! pub_struct { ($x:ident) => { + #[doc = concat!("`", stringify!($x), "` opcode.")] + #[derive(Debug)] pub struct $x; }; } +/// EraVM opcodes. pub mod opcodes { use std::marker::PhantomData; use super::{CallingMode, ReturnType}; forall_simple_opcodes!(pub_struct); + + /// `FarCall` group of opcodes distinguished by the calling mode (normal, delegate, or mimic). + #[derive(Debug)] pub struct FarCall(PhantomData); + + /// `Ret` group of opcodes distinguished by the return type (normal, panic, or revert). + #[derive(Debug)] pub struct Ret(PhantomData); + /// Normal [`Ret`]urn mode / [`FarCall`] mode. + #[derive(Debug)] pub struct Normal; + + /// Delegate [`FarCall`] mode. + #[derive(Debug)] pub struct Delegate; + + /// Mimic [`FarCall`] mode. + #[derive(Debug)] pub struct Mimic; + + /// Revert [`Ret`]urn mode. + #[derive(Debug)] pub struct Revert; + + /// Panic [`Ret`]urn mode. + #[derive(Debug)] pub struct Panic; + /// Calling mode for the [`FarCall`] opcodes. pub trait TypeLevelCallingMode { + /// Constant corresponding to this mode allowing to easily `match` it. const VALUE: CallingMode; } impl TypeLevelCallingMode for Normal { const VALUE: CallingMode = CallingMode::Normal; } + impl TypeLevelCallingMode for Delegate { const VALUE: CallingMode = CallingMode::Delegate; } + impl TypeLevelCallingMode for Mimic { const VALUE: CallingMode = CallingMode::Mimic; } + /// Return type for the [`Ret`] opcodes. pub trait TypeLevelReturnType { + /// Constant corresponding to this return type allowing to easily `match` it. const VALUE: ReturnType; } impl TypeLevelReturnType for Normal { const VALUE: ReturnType = ReturnType::Normal; } + impl TypeLevelReturnType for Revert { const VALUE: ReturnType = ReturnType::Revert; } + impl TypeLevelReturnType for Panic { const VALUE: ReturnType = ReturnType::Panic; } } +/// All supported EraVM opcodes in a single enumeration. +#[allow(missing_docs)] #[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] pub enum Opcode { Nop, @@ -143,27 +176,38 @@ pub enum Opcode { TransientStorageWrite, } +/// All supported calling modes for [`FarCall`](opcodes::FarCall) opcode. #[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] pub enum CallingMode { + /// Normal calling mode. Normal, + /// Delegate calling mode (similar to `delegatecall` in EVM). Delegate, + /// Mimic calling mode (can only be used by system contracts; allows to emulate `eth_call` semantics while retaining the bootloader). Mimic, } +/// All supported return types for the [`Ret`](opcodes::Ret) opcode. #[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] pub enum ReturnType { + /// Normal return. Normal, + /// Revert (e.g., a result of a Solidity `revert`). Revert, + /// Panic, i.e. a non-revert abnormal control flow termination (e.g., out of gas). Panic, } impl ReturnType { + /// Checks if this return type is [normal](Self::Normal). pub fn is_failure(&self) -> bool { *self != ReturnType::Normal } } +/// Trait mapping opcodes as types to the corresponding variants of the [`Opcode`] enum. pub trait OpcodeType { + /// `Opcode` variant corresponding to this opcode type. const VALUE: Opcode; } @@ -185,16 +229,17 @@ impl OpcodeType for opcodes::Ret { const VALUE: Opcode = Opcode::Ret(T::VALUE); } -/// Implement this for a type that holds the state of your tracer. +/// EraVM instruction tracer. /// -/// [Tracer::before_instruction] is called just before the actual instruction is executed. -/// If the instruction is skipped, `before_instruction` will be called with [Nop](opcodes::Nop). -/// [Tracer::after_instruction] is called once the instruction is executed and the program +/// [`Self::before_instruction()`] is called just before the actual instruction is executed. +/// If the instruction is skipped, `before_instruction` will be called with [`Nop`](opcodes::Nop). +/// [`Self::after_instruction()`] is called once the instruction is executed and the program /// counter has advanced. /// /// # Examples /// /// Here `FarCallCounter` counts the number of far calls. +/// /// ``` /// # use zksync_vm2_interface::{Tracer, StateInterface, OpcodeType, Opcode}; /// struct FarCallCounter(usize); @@ -209,23 +254,41 @@ impl OpcodeType for opcodes::Ret { /// } /// ``` pub trait Tracer { + /// Executes logic before an instruction handler. + /// + /// The default implementation does nothing. fn before_instruction(&mut self, _state: &mut S) {} + /// Executes logic after an instruction handler. + /// + /// The default implementation does nothing. fn after_instruction(&mut self, _state: &mut S) {} + /// Provides cycle statistics for "complex" instructions from the prover perspective (mostly precompile calls). + /// + /// The default implementation does nothing. fn on_extra_prover_cycles(&mut self, _stats: CycleStats) {} } +/// Cycle statistics emitted by the VM and supplied to [`Tracer::on_extra_prover_cycles()`]. #[derive(Debug, Clone, Copy)] pub enum CycleStats { + /// Call to the `keccak256` precompile with the specified number of hash cycles. Keccak256(u32), + /// Call to the `sha256` precompile with the specified number of hash cycles. Sha256(u32), + /// Call to the `ecrecover` precompile with the specified number of hash cycles. EcRecover(u32), - Secp256k1Verify(u32), + /// Call to the `secp256r1_verify` precompile with the specified number of hash cycles. + Secp256r1Verify(u32), + /// Decommitting an opcode. Decommit(u32), + /// Reading a slot from the VM storage. StorageRead, + /// Writing a slot to the VM storage. StorageWrite, } +/// No-op tracer implementation. impl Tracer for () {} // Multiple tracers can be combined by building a linked list out of tuples. diff --git a/crates/vm2/Cargo.toml b/crates/vm2/Cargo.toml index 4134bb30..5b0b3299 100644 --- a/crates/vm2/Cargo.toml +++ b/crates/vm2/Cargo.toml @@ -27,6 +27,9 @@ anyhow = { workspace = true, optional = true } divan.workspace = true proptest.workspace = true +[lints] +workspace = true + [[bench]] name = "nested_near_call" harness = false diff --git a/crates/vm2/README.md b/crates/vm2/README.md index 1c693c4f..42db8f9d 100644 --- a/crates/vm2/README.md +++ b/crates/vm2/README.md @@ -1,5 +1,11 @@ # High-Performance ZKsync Era VM +[![Build Status](https://github.com/matter-labs/vm2/actions/workflows/ci.yml/badge.svg)](https://github.com/matter-labs/vm2/actions) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/matter-labs/vm2#license) + +**Documentation:** +[![crate docs (main)](https://img.shields.io/badge/main-yellow.svg?label=docs)](https://matter-labs.github.io/vm2/zksync_vm2/) + A high-performance rewrite of the out-of-circuit VM for ZKsync Era. ## License diff --git a/crates/vm2/benches/nested_near_call.rs b/crates/vm2/benches/nested_near_call.rs index 8119108a..4ba17608 100644 --- a/crates/vm2/benches/nested_near_call.rs +++ b/crates/vm2/benches/nested_near_call.rs @@ -1,9 +1,10 @@ +//! Low-level benchmarks. + use divan::{black_box, Bencher}; use zkevm_opcode_defs::ethereum_types::Address; use zksync_vm2::{ addressing_modes::{Arguments, Immediate1, Immediate2, Register, Register1, Register2}, - initial_decommit, - testworld::TestWorld, + testonly::{initial_decommit, TestWorld}, Instruction, ModeRequirements, Predicate::Always, Program, Settings, VirtualMachine, @@ -11,7 +12,7 @@ use zksync_vm2::{ #[divan::bench] fn nested_near_call(bencher: Bencher) { - let program = Program::new( + let program = Program::from_raw( vec![Instruction::from_near_call( // zero means pass all gas Register1(Register::new(0)), @@ -22,7 +23,7 @@ fn nested_near_call(bencher: Bencher) { vec![], ); - let address = Address::from_low_u64_be(0xabe123ff); + let address = Address::from_low_u64_be(0x_abe1_23ff); bencher.bench(|| { let mut world = TestWorld::new(&[(address, program.clone())]); @@ -31,7 +32,7 @@ fn nested_near_call(bencher: Bencher) { address, program, Address::zero(), - vec![], + &[], 10_000_000, Settings { default_aa_code_hash: [0; 32], @@ -46,13 +47,13 @@ fn nested_near_call(bencher: Bencher) { #[divan::bench] fn nested_near_call_with_storage_write(bencher: Bencher) { - let program = Program::new( + let program = Program::from_raw( vec![ Instruction::from_ergs_left( Register1(Register::new(1)), Arguments::new(Always, 5, ModeRequirements::none()), ), - Instruction::from_sstore( + Instruction::from_storage_write( // always use same storage slot to get a warm write discount Register1(Register::new(0)), Register2(Register::new(1)), @@ -69,7 +70,7 @@ fn nested_near_call_with_storage_write(bencher: Bencher) { vec![], ); - let address = Address::from_low_u64_be(0xabe123ff); + let address = Address::from_low_u64_be(0x_abe1_23ff); bencher.bench(|| { let mut world = TestWorld::new(&[(address, program.clone())]); @@ -78,7 +79,7 @@ fn nested_near_call_with_storage_write(bencher: Bencher) { address, program, Address::zero(), - vec![], + &[], 80_000_000, Settings { default_aa_code_hash: [0; 32], diff --git a/crates/vm2/src/addressing_modes.rs b/crates/vm2/src/addressing_modes.rs index 0e257a5a..543bdd4e 100644 --- a/crates/vm2/src/addressing_modes.rs +++ b/crates/vm2/src/addressing_modes.rs @@ -29,7 +29,7 @@ pub(crate) trait Source { ) -> (U256, bool) { let (mut value, is_pointer) = Self::get_with_pointer_flag(args, state); if is_pointer && !state.in_kernel_mode() { - erase_fat_pointer_metadata(&mut value) + erase_fat_pointer_metadata(&mut value); } (value, is_pointer && state.in_kernel_mode()) } @@ -69,7 +69,7 @@ pub(crate) trait SourceWriter { impl SourceWriter for Option { fn write_source(&self, args: &mut Arguments) { if let Some(x) = self { - x.write_source(args) + x.write_source(args); } } } @@ -82,12 +82,12 @@ pub(crate) trait DestinationWriter { impl DestinationWriter for Option { fn write_destination(&self, args: &mut Arguments) { if let Some(x) = self { - x.write_destination(args) + x.write_destination(args); } } } -/// Argument provided to an instruction in an EraVM bytecode. +/// Arguments provided to an instruction in an EraVM bytecode. // It is important for performance that this fits into 8 bytes. #[derive(Debug)] pub struct Arguments { @@ -99,12 +99,14 @@ pub struct Arguments { static_gas_cost: u8, } -pub(crate) const L1_MESSAGE_COST: u32 = 156250; -pub(crate) const SSTORE_COST: u32 = 5511; -pub(crate) const SLOAD_COST: u32 = 2008; -pub(crate) const INVALID_INSTRUCTION_COST: u32 = 4294967295; +pub(crate) const L1_MESSAGE_COST: u32 = 156_250; +pub(crate) const SSTORE_COST: u32 = 5_511; +pub(crate) const SLOAD_COST: u32 = 2_008; +pub(crate) const INVALID_INSTRUCTION_COST: u32 = 4_294_967_295; impl Arguments { + /// Creates arguments from the provided info. + #[allow(clippy::missing_panics_doc)] // never panics on properly created inputs pub const fn new( predicate: Predicate, gas_cost: u32, @@ -124,6 +126,7 @@ impl Arguments { } } + #[allow(clippy::cast_possible_truncation)] // checked const fn encode_static_gas_cost(x: u32) -> u8 { match x { L1_MESSAGE_COST => 1, @@ -133,7 +136,7 @@ impl Arguments { 1..=4 => panic!("Reserved gas cost values overlap with actual gas costs"), x => { if x > u8::MAX as u32 { - panic!("Gas cost doesn't fit into 8 bits") + panic!("Gas cost doesn't fit into 8 bits"); } else { x as u8 } @@ -221,7 +224,7 @@ impl Destination for Register1 { impl DestinationWriter for Register1 { fn write_destination(&self, args: &mut Arguments) { - args.destination_registers.set_register1(self.0) + args.destination_registers.set_register1(self.0); } } @@ -237,7 +240,7 @@ impl Destination for Register2 { impl DestinationWriter for Register2 { fn write_destination(&self, args: &mut Arguments) { - args.destination_registers.set_register2(self.0) + args.destination_registers.set_register2(self.0); } } @@ -251,9 +254,21 @@ pub struct Immediate1(pub u16); #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct Immediate2(pub u16); +impl Immediate1 { + pub(crate) fn get_u16(args: &Arguments) -> u16 { + args.immediate1 + } +} + +impl Immediate2 { + pub(crate) fn get_u16(args: &Arguments) -> u16 { + args.immediate2 + } +} + impl Source for Immediate1 { fn get(args: &Arguments, _state: &mut impl Addressable) -> U256 { - U256([args.immediate1 as u64, 0, 0, 0]) + U256([args.immediate1.into(), 0, 0, 0]) } } @@ -265,7 +280,7 @@ impl SourceWriter for Immediate1 { impl Source for Immediate2 { fn get(args: &Arguments, _state: &mut impl Addressable) -> U256 { - U256([args.immediate2 as u64, 0, 0, 0]) + U256([args.immediate2.into(), 0, 0, 0]) } } @@ -280,7 +295,9 @@ impl SourceWriter for Immediate2 { #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct RegisterAndImmediate { + /// Immediate value. pub immediate: u16, + /// Register spec. pub register: Register, } @@ -301,7 +318,7 @@ impl DestinationWriter for T { fn write_destination(&self, args: &mut Arguments) { args.immediate2 = self.inner().immediate; args.destination_registers - .set_register1(self.inner().register) + .set_register1(self.inner().register); } } @@ -348,6 +365,7 @@ pub(crate) fn destination_stack_address(args: &Arguments, state: &mut impl Addre /// Computes register + immediate (mod 2^16). /// Stack addresses are always in that remainder class anyway. +#[allow(clippy::cast_possible_truncation)] fn compute_stack_address(state: &mut impl Addressable, register: Register, immediate: u16) -> u16 { (register.value(state).low_u32() as u16).wrapping_add(immediate) } @@ -444,7 +462,7 @@ impl Source for CodePage { state .code_page() .get(address as usize) - .cloned() + .copied() .unwrap_or(U256::zero()) } } @@ -455,27 +473,31 @@ pub struct Register(u8); impl Register { /// Creates a register with the specified 0-based index. + /// + /// # Panics + /// + /// Panics if `n >= 16`; EraVM has 16 registers. pub const fn new(n: u8) -> Self { assert!(n < 16, "EraVM has 16 registers"); Self(n) } - fn value(&self, state: &mut impl Addressable) -> U256 { + fn value(self, state: &mut impl Addressable) -> U256 { unsafe { *state.registers().get_unchecked(self.0 as usize) } } - fn pointer_flag(&self, state: &mut impl Addressable) -> bool { + fn pointer_flag(self, state: &mut impl Addressable) -> bool { *state.register_pointer_flags() & (1 << self.0) != 0 } - fn set(&self, state: &mut impl Addressable, value: U256) { + fn set(self, state: &mut impl Addressable, value: U256) { if self.0 != 0 { unsafe { *state.registers().get_unchecked_mut(self.0 as usize) = value }; *state.register_pointer_flags() &= !(1 << self.0); } } - fn set_ptr(&self, state: &mut impl Addressable, value: U256) { + fn set_ptr(self, state: &mut impl Addressable, value: U256) { if self.0 != 0 { unsafe { *state.registers().get_unchecked_mut(self.0 as usize) = value }; *state.register_pointer_flags() |= 1 << self.0; @@ -485,6 +507,7 @@ impl Register { #[cfg(feature = "arbitrary")] impl<'a> Arbitrary<'a> for Register { + #[allow(clippy::cast_possible_truncation)] // false positive: the value is <16 fn arbitrary(u: &mut Unstructured<'a>) -> Result { Ok(Register(u.choose_index(16)? as u8)) } @@ -510,16 +533,22 @@ impl PackedRegisters { } } -/// All supported addressing modes for source arguments. +/// All supported addressing modes for the first source argument. #[enum_dispatch(SourceWriter)] #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub enum AnySource { + /// Register mode. Register1, + /// Immediate mode. Immediate1, + /// Absolute stack addressing. AbsoluteStack, + /// Relative stack addressing. RelativeStack, + /// Relative stack addressing that updates the stack pointer on access. AdvanceStackPointer, + /// Addressing into the code page of the executing contract. CodePage, } @@ -528,7 +557,9 @@ pub enum AnySource { #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub enum RegisterOrImmediate { + /// Register mode. Register1, + /// Immediate mode. Immediate1, } @@ -548,13 +579,17 @@ impl TryFrom for RegisterOrImmediate { } } -/// All supported addressing modes for destination arguments. +/// All supported addressing modes for the first destination argument. #[enum_dispatch(DestinationWriter)] #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub enum AnyDestination { + /// Register mode. Register1, + /// Absolute stack addressing. AbsoluteStack, + /// Relative stack addressing. RelativeStack, + /// Relative stack addressing that updates the stack pointer on access. AdvanceStackPointer, } diff --git a/crates/vm2/src/bitset.rs b/crates/vm2/src/bitset.rs index ad1b4f8d..49614dcc 100644 --- a/crates/vm2/src/bitset.rs +++ b/crates/vm2/src/bitset.rs @@ -4,19 +4,19 @@ pub(crate) struct Bitset([u64; 1 << 10]); impl Bitset { #[inline(always)] - pub fn get(&self, i: u16) -> bool { + pub(crate) fn get(&self, i: u16) -> bool { let (slot, bit) = slot_and_bit(i); self.0[slot] & bit != 0 } #[inline(always)] - pub fn set(&mut self, i: u16) { + pub(crate) fn set(&mut self, i: u16) { let (slot, bit) = slot_and_bit(i); self.0[slot] |= bit; } #[inline(always)] - pub fn clear(&mut self, i: u16) { + pub(crate) fn clear(&mut self, i: u16) { let (slot, bit) = slot_and_bit(i); self.0[slot] &= !bit; } @@ -24,7 +24,7 @@ impl Bitset { #[inline(always)] fn slot_and_bit(i: u16) -> (usize, u64) { - ((i >> 6) as usize, 1u64 << (i & 0b111111)) + ((i >> 6) as usize, 1u64 << (i & 0b_0011_1111)) } impl Default for Bitset { diff --git a/crates/vm2/src/callframe.rs b/crates/vm2/src/callframe.rs index e48e597b..a83c6aa7 100644 --- a/crates/vm2/src/callframe.rs +++ b/crates/vm2/src/callframe.rs @@ -1,3 +1,5 @@ +use std::{mem, ptr}; + use primitive_types::H160; use zkevm_opcode_defs::system_params::{NEW_FRAME_MEMORY_STIPEND, NEW_KERNEL_FRAME_MEMORY_STIPEND}; use zksync_vm2_interface::HeapId; @@ -133,17 +135,30 @@ impl Callframe { }) } + /// Gets a raw inferred program counter. This value can be garbage if the frame is on an invalid instruction or free panic. + #[allow(clippy::cast_possible_wrap)] // false positive: `Instruction` isn't that large + pub(crate) fn get_raw_pc(&self) -> isize { + // We cannot use `<*const _>::offset_from` because `self.pc` isn't guaranteed to be allocated within `self.program` + // (invalid instructions and free panics aren't). + let offset_in_bytes = + self.pc as isize - ptr::from_ref(self.program.instruction(0).unwrap()) as isize; + offset_in_bytes / mem::size_of::>() as isize + } + + // TODO: can overflow / underflow after an invalid instruction or free panic. Ordinarily, this will lead to VM termination (for an invalid instruction) + // or the callframe getting popped (for a panic), but it's still technically possible to invoke this method afterwards in certain cases (e.g., in the bootloader callframe). + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] pub(crate) fn get_pc_as_u16(&self) -> u16 { - unsafe { self.pc.offset_from(self.program.instruction(0).unwrap()) as u16 } + self.get_raw_pc() as u16 } /// Sets the next instruction to execute to the instruction at the given index. /// If the index is out of bounds, the invalid instruction is used. - pub fn set_pc_from_u16(&mut self, index: u16) { + pub(crate) fn set_pc_from_u16(&mut self, index: u16) { self.pc = self .program .instruction(index) - .unwrap_or(invalid_instruction()) + .unwrap_or_else(invalid_instruction); } /// The total amount of gas in this frame, including gas currently inaccessible because of a near call. diff --git a/crates/vm2/src/decode.rs b/crates/vm2/src/decode.rs index 53602200..329a570f 100644 --- a/crates/vm2/src/decode.rs +++ b/crates/vm2/src/decode.rs @@ -1,43 +1,30 @@ use zkevm_opcode_defs::{ decoding::{EncodingModeProduction, VmEncodingMode}, ImmMemHandlerFlags, Opcode, - Operand::*, + Operand::{Full, RegOnly, RegOrImm}, RegOrImmFlags, FAR_CALL_SHARD_FLAG_IDX, FAR_CALL_STATIC_FLAG_IDX, FIRST_MESSAGE_FLAG_IDX, RET_TO_LABEL_BIT_IDX, SET_FLAGS_FLAG_IDX, SWAP_OPERANDS_FLAG_IDX_FOR_ARITH_OPCODES, SWAP_OPERANDS_FLAG_IDX_FOR_PTR_OPCODE, UMA_INCREMENT_FLAG_IDX, }; -use zksync_vm2_interface::{opcodes, Tracer}; +use zksync_vm2_interface::{ + opcodes::{ + self, Add, And, Div, Mul, Or, PointerAdd, PointerPack, PointerShrink, PointerSub, + RotateLeft, RotateRight, ShiftLeft, ShiftRight, Sub, Xor, + }, + Tracer, +}; use crate::{ addressing_modes::{ AbsoluteStack, AdvanceStackPointer, AnyDestination, AnySource, Arguments, CodePage, Immediate1, Immediate2, Register, Register1, Register2, RegisterAndImmediate, - RelativeStack, Source, SourceWriter, - }, - instruction::{jump_to_beginning, ExecutionEnd, ExecutionStatus}, - instruction_handlers::{ - Add, And, Div, Mul, Or, PointerAdd, PointerPack, PointerShrink, PointerSub, RotateLeft, - RotateRight, ShiftLeft, ShiftRight, Sub, Xor, + RelativeStack, SourceWriter, }, + instruction::{ExecutionEnd, ExecutionStatus}, mode_requirements::ModeRequirements, Instruction, Predicate, VirtualMachine, World, }; -pub fn decode_program>( - raw: &[u64], - is_bootloader: bool, -) -> Vec> { - raw.iter() - .take(1 << 16) - .map(|i| decode(*i, is_bootloader)) - .chain(std::iter::once(if raw.len() >= 1 << 16 { - jump_to_beginning() - } else { - Instruction::from_invalid() - })) - .collect() -} - fn unimplemented_instruction(variant: Opcode) -> Instruction { let mut arguments = Arguments::new(Predicate::Always, 0, ModeRequirements::none()); let variant_as_number: u16 = unsafe { std::mem::transmute(variant) }; @@ -54,27 +41,25 @@ fn unimplemented_handler( _: &mut T, ) -> ExecutionStatus { let variant: Opcode = unsafe { - std::mem::transmute( - Immediate1::get(&(*vm.state.current_frame.pc).arguments, &mut vm.state).low_u32() - as u16, - ) + std::mem::transmute(Immediate1::get_u16(&(*vm.state.current_frame.pc).arguments)) }; - eprintln!("Unimplemented instruction: {:?}!", variant); + eprintln!("Unimplemented instruction: {variant:?}"); ExecutionStatus::Stopped(ExecutionEnd::Panicked) } +#[allow(clippy::too_many_lines)] pub(crate) fn decode>(raw: u64, is_bootloader: bool) -> Instruction { let (parsed, _) = EncodingModeProduction::parse_preliminary_variant_and_absolute_number(raw); let predicate = match parsed.condition { - zkevm_opcode_defs::Condition::Always => crate::Predicate::Always, - zkevm_opcode_defs::Condition::Gt => crate::Predicate::IfGT, - zkevm_opcode_defs::Condition::Lt => crate::Predicate::IfLT, - zkevm_opcode_defs::Condition::Eq => crate::Predicate::IfEQ, - zkevm_opcode_defs::Condition::Ge => crate::Predicate::IfGE, - zkevm_opcode_defs::Condition::Le => crate::Predicate::IfLE, - zkevm_opcode_defs::Condition::Ne => crate::Predicate::IfNotEQ, - zkevm_opcode_defs::Condition::GtOrLt => crate::Predicate::IfGtOrLT, + zkevm_opcode_defs::Condition::Always => Predicate::Always, + zkevm_opcode_defs::Condition::Gt => Predicate::IfGT, + zkevm_opcode_defs::Condition::Lt => Predicate::IfLT, + zkevm_opcode_defs::Condition::Eq => Predicate::IfEQ, + zkevm_opcode_defs::Condition::Ge => Predicate::IfGE, + zkevm_opcode_defs::Condition::Le => Predicate::IfLE, + zkevm_opcode_defs::Condition::Ne => Predicate::IfNotEQ, + zkevm_opcode_defs::Condition::GtOrLt => Predicate::IfGTOrLT, }; let arguments = Arguments::new( predicate, @@ -128,7 +113,7 @@ pub(crate) fn decode>(raw: u64, is_bootloader: bool) -> I src1, src2, out, - $snd, + &$snd, arguments, parsed.variant.flags[SWAP_OPERANDS_FLAG_IDX_FOR_ARITH_OPCODES], parsed.variant.flags[SET_FLAGS_FLAG_IDX], @@ -248,13 +233,13 @@ pub(crate) fn decode>(raw: u64, is_bootloader: bool) -> I } } Opcode::Log(x) => match x { - zkevm_opcode_defs::LogOpcode::StorageRead => Instruction::from_sload( + zkevm_opcode_defs::LogOpcode::StorageRead => Instruction::from_storage_read( src1.try_into().unwrap(), out.try_into().unwrap(), arguments, ), zkevm_opcode_defs::LogOpcode::TransientStorageRead => { - Instruction::from_sload_transient( + Instruction::from_transient_storage_read( src1.try_into().unwrap(), out.try_into().unwrap(), arguments, @@ -262,11 +247,11 @@ pub(crate) fn decode>(raw: u64, is_bootloader: bool) -> I } zkevm_opcode_defs::LogOpcode::StorageWrite => { - Instruction::from_sstore(src1.try_into().unwrap(), src2, arguments) + Instruction::from_storage_write(src1.try_into().unwrap(), src2, arguments) } zkevm_opcode_defs::LogOpcode::TransientStorageWrite => { - Instruction::from_sstore_transient(src1.try_into().unwrap(), src2, arguments) + Instruction::from_transient_storage_write(src1.try_into().unwrap(), src2, arguments) } zkevm_opcode_defs::LogOpcode::ToL1Message => Instruction::from_l2_to_l1_message( @@ -297,20 +282,20 @@ pub(crate) fn decode>(raw: u64, is_bootloader: bool) -> I Opcode::UMA(x) => { let increment = parsed.variant.flags[UMA_INCREMENT_FLAG_IDX]; match x { - zkevm_opcode_defs::UMAOpcode::HeapRead => Instruction::from_heap_load( + zkevm_opcode_defs::UMAOpcode::HeapRead => Instruction::from_heap_read( src1.try_into().unwrap(), out.try_into().unwrap(), increment.then_some(out2), arguments, ), - zkevm_opcode_defs::UMAOpcode::HeapWrite => Instruction::from_heap_store( + zkevm_opcode_defs::UMAOpcode::HeapWrite => Instruction::from_heap_write( src1.try_into().unwrap(), src2, increment.then_some(out.try_into().unwrap()), arguments, is_bootloader, ), - zkevm_opcode_defs::UMAOpcode::AuxHeapRead => Instruction::from_aux_heap_load( + zkevm_opcode_defs::UMAOpcode::AuxHeapRead => Instruction::from_aux_heap_read( src1.try_into().unwrap(), out.try_into().unwrap(), increment.then_some(out2), @@ -322,7 +307,7 @@ pub(crate) fn decode>(raw: u64, is_bootloader: bool) -> I increment.then_some(out.try_into().unwrap()), arguments, ), - zkevm_opcode_defs::UMAOpcode::FatPointerRead => Instruction::from_load_pointer( + zkevm_opcode_defs::UMAOpcode::FatPointerRead => Instruction::from_pointer_read( src1.try_into().unwrap(), out.try_into().unwrap(), increment.then_some(out2), diff --git a/crates/vm2/src/decommit.rs b/crates/vm2/src/decommit.rs index b7aa3796..a4083e7f 100644 --- a/crates/vm2/src/decommit.rs +++ b/crates/vm2/src/decommit.rs @@ -17,7 +17,7 @@ impl WorldDiff { is_constructor_call: bool, ) -> Option<(UnpaidDecommit, bool)> { let deployer_system_contract_address = - Address::from_low_u64_be(DEPLOYER_SYSTEM_CONTRACT_ADDRESS_LOW as u64); + Address::from_low_u64_be(DEPLOYER_SYSTEM_CONTRACT_ADDRESS_LOW.into()); let mut is_evm = false; @@ -79,7 +79,7 @@ impl WorldDiff { 0 } else { let code_length_in_words = u16::from_be_bytes([code_info[2], code_info[3]]); - code_length_in_words as u32 * zkevm_opcode_defs::ERGS_PER_CODE_WORD_DECOMMITTMENT + u32::from(code_length_in_words) * zkevm_opcode_defs::ERGS_PER_CODE_WORD_DECOMMITTMENT }; Some((UnpaidDecommit { cost, code_key }, is_evm)) @@ -97,8 +97,9 @@ impl WorldDiff { let is_new = self.decommitted_hashes.insert(code_hash, true) != Some(true); let code = world.decommit_code(code_hash); if is_new { - // Decommitter can process two words per cycle - tracer.on_extra_prover_cycles(CycleStats::Decommit((code.len() as u32 + 63) / 64)); + let code_len = u32::try_from(code.len()).expect("bytecode length overflow"); + // Decommitter can process two words per cycle, hence division by 2 * 32 = 64. + tracer.on_extra_prover_cycles(CycleStats::Decommit(code_len.div_ceil(64))); } (code, is_new) } @@ -123,48 +124,22 @@ impl WorldDiff { let decommit = world.decommit(decommit.code_key); if is_new { - tracer.on_extra_prover_cycles(CycleStats::Decommit( - (decommit.code_page().len() as u32 + 1) / 2, - )); + let code_len_in_words = + u32::try_from(decommit.code_page().len()).expect("bytecode length overflow"); + // Decommitter can process two words per cycle. + tracer.on_extra_prover_cycles(CycleStats::Decommit(code_len_in_words.div_ceil(2))); } Some(decommit) } } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub(crate) struct UnpaidDecommit { cost: u32, code_key: U256, } -/// May be used to load code when the VM first starts up. -/// Doesn't check for any errors. -/// Doesn't cost anything but also doesn't make the code free in future decommits. -#[doc(hidden)] // should be used only in low-level testing / benches -pub fn initial_decommit>(world: &mut W, address: H160) -> Program { - let deployer_system_contract_address = - Address::from_low_u64_be(DEPLOYER_SYSTEM_CONTRACT_ADDRESS_LOW as u64); - let code_info = world - .read_storage(deployer_system_contract_address, address_into_u256(address)) - .unwrap_or_default(); - - let mut code_info_bytes = [0; 32]; - code_info.to_big_endian(&mut code_info_bytes); - - code_info_bytes[1] = 0; - let code_key: U256 = U256::from_big_endian(&code_info_bytes); - - world.decommit(code_key) -} - -#[doc(hidden)] // should be used only in low-level testing / benches -pub fn address_into_u256(address: H160) -> U256 { - let mut buffer = [0; 32]; - buffer[12..].copy_from_slice(address.as_bytes()); - U256::from_big_endian(&buffer) -} - pub(crate) fn u256_into_address(source: U256) -> H160 { let mut result = H160::zero(); let mut bytes = [0; 32]; diff --git a/crates/vm2/src/fat_pointer.rs b/crates/vm2/src/fat_pointer.rs index 8e7b9075..410557b0 100644 --- a/crates/vm2/src/fat_pointer.rs +++ b/crates/vm2/src/fat_pointer.rs @@ -1,3 +1,5 @@ +use std::ptr; + use primitive_types::U256; use zksync_vm2_interface::HeapId; @@ -5,30 +7,32 @@ use zksync_vm2_interface::HeapId; #[derive(Debug)] #[repr(C)] pub struct FatPointer { + /// Additional pointer offset inside the `start..(start + length)` range. pub offset: u32, + /// ID of the heap this points to. pub memory_page: HeapId, + /// 0-based index of the pointer start byte at the `memory` page. pub start: u32, + /// Length of the pointed slice in bytes. pub length: u32, } #[cfg(target_endian = "little")] impl From<&mut U256> for &mut FatPointer { fn from(value: &mut U256) -> Self { - unsafe { &mut *(value as *mut U256).cast() } + unsafe { &mut *ptr::from_mut(value).cast() } } } #[cfg(target_endian = "little")] impl From for FatPointer { fn from(value: U256) -> Self { - unsafe { - let ptr: *const FatPointer = (&value as *const U256).cast(); - ptr.read() - } + unsafe { std::mem::transmute(value.low_u128()) } } } impl FatPointer { + /// Converts this pointer into a `U256` word. #[cfg(target_endian = "little")] pub fn into_u256(self) -> U256 { U256::zero() + unsafe { std::mem::transmute::(self) } diff --git a/crates/vm2/src/heap.rs b/crates/vm2/src/heap.rs index 7d852850..ce6c68ca 100644 --- a/crates/vm2/src/heap.rs +++ b/crates/vm2/src/heap.rs @@ -138,7 +138,7 @@ impl Heap { /// Needed only by tracers pub(crate) fn read_byte(&self, address: u32) -> u8 { let (page, offset) = address_to_page_offset(address); - self.page(page).map(|page| page.0[offset]).unwrap_or(0) + self.page(page).map_or(0, |page| page.0[offset]) } fn page(&self, idx: usize) -> Option<&HeapPage> { @@ -217,7 +217,8 @@ impl Heaps { } fn allocate_inner(&mut self, memory: &[u8]) -> HeapId { - let id = HeapId::from_u32_unchecked(self.heaps.len() as u32); + let id = u32::try_from(self.heaps.len()).expect("heap ID overflow"); + let id = HeapId::from_u32_unchecked(id); self.heaps .push(Heap::from_bytes(memory, &mut self.pagepool)); id @@ -230,7 +231,7 @@ impl Heaps { } } - pub fn write_u256(&mut self, heap: HeapId, start_address: u32, value: U256) { + pub(crate) fn write_u256(&mut self, heap: HeapId, start_address: u32, value: U256) { if heap == HeapId::FIRST { let prev_value = self[heap].read_u256(start_address); self.bootloader_heap_rollback_info @@ -329,6 +330,7 @@ impl PagePool { } #[cfg(test)] +#[allow(clippy::cast_possible_truncation)] mod tests { use super::*; diff --git a/crates/vm2/src/instruction.rs b/crates/vm2/src/instruction.rs index 261b5e05..530ae002 100644 --- a/crates/vm2/src/instruction.rs +++ b/crates/vm2/src/instruction.rs @@ -1,10 +1,11 @@ use std::fmt; -use crate::{ - addressing_modes::Arguments, mode_requirements::ModeRequirements, vm::VirtualMachine, Predicate, -}; +use crate::{addressing_modes::Arguments, vm::VirtualMachine}; -#[doc(hidden)] // should only be used for low-level testing / benchmarking +/// Single EraVM instruction (an opcode + [`Arguments`]). +/// +/// Managing instructions is warranted for low-level tests; prefer using [`Program`](crate::Program)s to decode instructions +/// from EraVM bytecodes. pub struct Instruction { pub(crate) handler: Handler, pub(crate) arguments: Arguments, @@ -27,28 +28,15 @@ pub(crate) enum ExecutionStatus { Stopped(ExecutionEnd), } +/// VM stop reason returned from [`VirtualMachine::run()`]. #[derive(Debug, PartialEq)] pub enum ExecutionEnd { + /// The executed program has finished and returned the specified data. ProgramFinished(Vec), + /// The executed program has reverted returning the specified data. Reverted(Vec), + /// The executed program has panicked. Panicked, - - /// Returned when the bootloader writes to the heap location [crate::Settings::hook_address] + /// Returned when the bootloader writes to the heap location specified by [`hook_address`](crate::Settings.hook_address). SuspendedOnHook(u32), } - -pub(crate) fn jump_to_beginning() -> Instruction { - Instruction { - handler: jump_to_beginning_handler, - arguments: Arguments::new(Predicate::Always, 0, ModeRequirements::none()), - } -} -fn jump_to_beginning_handler( - vm: &mut VirtualMachine, - _: &mut W, - _: &mut T, -) -> ExecutionStatus { - let first_instruction = vm.state.current_frame.program.instruction(0).unwrap(); - vm.state.current_frame.pc = first_instruction; - ExecutionStatus::Running -} diff --git a/crates/vm2/src/instruction_handlers/binop.rs b/crates/vm2/src/instruction_handlers/binop.rs index 871e0343..a1ec08ae 100644 --- a/crates/vm2/src/instruction_handlers/binop.rs +++ b/crates/vm2/src/instruction_handlers/binop.rs @@ -4,7 +4,12 @@ use zksync_vm2_interface::{ OpcodeType, Tracer, }; -use super::{common::boilerplate, monomorphization::*}; +use super::{ + common::boilerplate, + monomorphization::{ + match_boolean, match_destination, match_source, monomorphize, parameterize, + }, +}; use crate::{ addressing_modes::{ AbsoluteStack, Addressable, AdvanceStackPointer, AnyDestination, AnySource, Arguments, @@ -13,22 +18,20 @@ use crate::{ }, instruction::{ExecutionStatus, Instruction}, predication::Flags, - VirtualMachine, + VirtualMachine, World, }; -fn binop< +fn binop( + vm: &mut VirtualMachine, + world: &mut W, + tracer: &mut T, +) -> ExecutionStatus +where T: Tracer, - W, Op: Binop, In1: Source, Out: Destination, - const SWAP: bool, - const SET_FLAGS: bool, ->( - vm: &mut VirtualMachine, - world: &mut W, - tracer: &mut T, -) -> ExecutionStatus { +{ boilerplate::(vm, world, tracer, |vm, args| { let a = In1::get(args, &mut vm.state); let b = Register2::get(args, &mut vm.state); @@ -104,7 +107,7 @@ impl Binop for Xor { impl Binop for ShiftLeft { #[inline(always)] fn perform(a: &U256, b: &U256) -> (U256, (), Flags) { - let result = *a << b.low_u32() as u8; + let result = *a << (b.low_u32() % 256); (result, (), Flags::new(false, result.is_zero(), false)) } type Out2 = (); @@ -113,7 +116,7 @@ impl Binop for ShiftLeft { impl Binop for ShiftRight { #[inline(always)] fn perform(a: &U256, b: &U256) -> (U256, (), Flags) { - let result = *a >> b.low_u32() as u8; + let result = *a >> (b.low_u32() % 256); (result, (), Flags::new(false, result.is_zero(), false)) } type Out2 = (); @@ -122,8 +125,8 @@ impl Binop for ShiftRight { impl Binop for RotateLeft { #[inline(always)] fn perform(a: &U256, b: &U256) -> (U256, (), Flags) { - let shift = b.low_u32() as u8; - let result = *a << shift | *a >> (256 - shift as u16); + let shift = b.low_u32() % 256; + let result = *a << shift | *a >> (256 - shift); (result, (), Flags::new(false, result.is_zero(), false)) } type Out2 = (); @@ -132,8 +135,8 @@ impl Binop for RotateLeft { impl Binop for RotateRight { #[inline(always)] fn perform(a: &U256, b: &U256) -> (U256, (), Flags) { - let shift = b.low_u32() as u8; - let result = *a >> shift | *a << (256 - shift as u16); + let shift = b.low_u32() % 256; + let result = *a >> shift | *a << (256 - shift); (result, (), Flags::new(false, result.is_zero(), false)) } type Out2 = (); @@ -189,15 +192,15 @@ impl Binop for Mul { impl Binop for Div { fn perform(a: &U256, b: &U256) -> (U256, Self::Out2, Flags) { - if *b != U256::zero() { + if b.is_zero() { + (U256::zero(), U256::zero(), Flags::new(true, false, false)) + } else { let (quotient, remainder) = a.div_mod(*b); ( quotient, remainder, Flags::new(false, quotient.is_zero(), remainder.is_zero()), ) - } else { - (U256::zero(), U256::zero(), Flags::new(true, false, false)) } } type Out2 = U256; @@ -205,8 +208,7 @@ impl Binop for Div { macro_rules! from_binop { ($name:ident <$binop:ty>) => { - #[doc = concat!("Creates `", stringify!($binop), "` instruction with the provided params.")] - #[inline(always)] + #[doc = concat!("Creates [`", stringify!($binop), "`] instruction with the provided params.")] pub fn $name( src1: AnySource, src2: Register2, @@ -215,13 +217,12 @@ macro_rules! from_binop { swap: bool, set_flags: bool, ) -> Self { - Self::from_binop::<$binop>(src1, src2, out, (), arguments, swap, set_flags) + Self::from_binop::<$binop>(src1, src2, out, &(), arguments, swap, set_flags) } }; ($name:ident <$binop:ty, $out2: ty>) => { - #[doc = concat!("Creates `", stringify!($binop), "` instruction with the provided params.")] - #[inline(always)] + #[doc = concat!("Creates [`", stringify!($binop), "`] instruction with the provided params.")] pub fn $name( src1: AnySource, src2: Register2, @@ -231,18 +232,18 @@ macro_rules! from_binop { swap: bool, set_flags: bool, ) -> Self { - Self::from_binop::<$binop>(src1, src2, out, out2, arguments, swap, set_flags) + Self::from_binop::<$binop>(src1, src2, out, &out2, arguments, swap, set_flags) } }; } -impl Instruction { - #[inline(always)] +/// Instructions for binary operations. +impl> Instruction { pub(crate) fn from_binop( src1: AnySource, src2: Register2, out: AnyDestination, - out2: ::Destination, + out2: &::Destination, arguments: Arguments, swap: bool, set_flags: bool, @@ -253,7 +254,7 @@ impl Instruction { .write_source(&src1) .write_source(&src2) .write_destination(&out) - .write_destination(&out2), + .write_destination(out2), } } diff --git a/crates/vm2/src/instruction_handlers/context.rs b/crates/vm2/src/instruction_handlers/context.rs index 679ef97e..53461104 100644 --- a/crates/vm2/src/instruction_handlers/context.rs +++ b/crates/vm2/src/instruction_handlers/context.rs @@ -1,4 +1,4 @@ -use primitive_types::U256; +use primitive_types::{H160, U256}; use zkevm_opcode_defs::VmMetaParameters; use zksync_vm2_interface::{ opcodes::{self, Caller, CodeAddress, ContextU128, ErgsLeft, This, SP}, @@ -8,20 +8,29 @@ use zksync_vm2_interface::{ use super::common::boilerplate; use crate::{ addressing_modes::{Arguments, Destination, Register1, Source}, - decommit::address_into_u256, instruction::ExecutionStatus, state::State, Instruction, VirtualMachine, }; -fn context( +pub(crate) fn address_into_u256(address: H160) -> U256 { + let mut buffer = [0; 32]; + buffer[12..].copy_from_slice(address.as_bytes()); + U256::from_big_endian(&buffer) +} + +fn context( vm: &mut VirtualMachine, world: &mut W, tracer: &mut T, -) -> ExecutionStatus { +) -> ExecutionStatus +where + T: Tracer, + Op: ContextOp, +{ boilerplate::(vm, world, tracer, |vm, args| { let result = Op::get(&vm.state); - Register1::set(args, &mut vm.state, result) + Register1::set(args, &mut vm.state, result); }) } @@ -49,7 +58,7 @@ impl ContextOp for CodeAddress { impl ContextOp for ErgsLeft { fn get(state: &State) -> U256 { - U256([state.current_frame.gas as u64, 0, 0, 0]) + U256([state.current_frame.gas.into(), 0, 0, 0]) } } @@ -79,7 +88,10 @@ fn context_meta( code_shard_id: 0, // This field is actually pubdata! aux_field_0: if vm.state.current_frame.is_kernel { - vm.world_diff.pubdata.0 as u32 + #[allow(clippy::cast_sign_loss)] // wrapping conversion is intentional + { + vm.world_diff.pubdata.0 as u32 + } } else { 0 }, @@ -121,6 +133,7 @@ fn aux_mutating( }) } +/// Context-related instructions. impl Instruction { fn from_context(out: Register1, arguments: Arguments) -> Self { Self { @@ -129,42 +142,61 @@ impl Instruction { } } + /// Creates a [`This`] instruction with the provided params. pub fn from_this(out: Register1, arguments: Arguments) -> Self { Self::from_context::(out, arguments) } + + /// Creates a [`Caller`] instruction with the provided params. pub fn from_caller(out: Register1, arguments: Arguments) -> Self { Self::from_context::(out, arguments) } + + /// Creates a [`CodeAddress`] instruction with the provided params. pub fn from_code_address(out: Register1, arguments: Arguments) -> Self { Self::from_context::(out, arguments) } + + /// Creates an [`ErgsLeft`] instruction with the provided params. pub fn from_ergs_left(out: Register1, arguments: Arguments) -> Self { Self::from_context::(out, arguments) } + + /// Creates a [`ContextU128`] instruction with the provided params. pub fn from_context_u128(out: Register1, arguments: Arguments) -> Self { Self::from_context::(out, arguments) } + + /// Creates an [`SP`] instruction with the provided params. pub fn from_context_sp(out: Register1, arguments: Arguments) -> Self { Self::from_context::(out, arguments) } + + /// Creates a [`ContextMeta`](opcodes::ContextMeta) instruction with the provided params. pub fn from_context_meta(out: Register1, arguments: Arguments) -> Self { Self { handler: context_meta, arguments: arguments.write_destination(&out), } } + + /// Creates a [`SetContextU128`](opcodes::SetContextU128) instruction with the provided params. pub fn from_set_context_u128(src: Register1, arguments: Arguments) -> Self { Self { handler: set_context_u128, arguments: arguments.write_source(&src), } } + + /// Creates an [`IncrementTxNumber`](opcodes::IncrementTxNumber) instruction with the provided params. pub fn from_increment_tx_number(arguments: Arguments) -> Self { Self { handler: increment_tx_number, arguments, } } + + /// Creates an [`AuxMutating0`](opcodes::AuxMutating0) instruction with the provided params. pub fn from_aux_mutating(arguments: Arguments) -> Self { Self { handler: aux_mutating, diff --git a/crates/vm2/src/instruction_handlers/decommit.rs b/crates/vm2/src/instruction_handlers/decommit.rs index 13b0ed1b..430f420e 100644 --- a/crates/vm2/src/instruction_handlers/decommit.rs +++ b/crates/vm2/src/instruction_handlers/decommit.rs @@ -53,6 +53,7 @@ fn decommit>( } impl> Instruction { + /// Creates a [`Decommit`](opcodes::Decommit) instruction with the provided params. pub fn from_decommit( abi: Register1, burn: Register2, diff --git a/crates/vm2/src/instruction_handlers/event.rs b/crates/vm2/src/instruction_handlers/event.rs index 71bc44a3..dc3d44e3 100644 --- a/crates/vm2/src/instruction_handlers/event.rs +++ b/crates/vm2/src/instruction_handlers/event.rs @@ -1,12 +1,11 @@ use primitive_types::H160; use zkevm_opcode_defs::ADDRESS_EVENT_WRITER; -use zksync_vm2_interface::{opcodes, Tracer}; +use zksync_vm2_interface::{opcodes, Event, L2ToL1Log, Tracer}; use super::common::boilerplate_ext; use crate::{ addressing_modes::{Arguments, Immediate1, Register1, Register2, Source}, instruction::ExecutionStatus, - world_diff::{Event, L2ToL1Log}, Instruction, VirtualMachine, }; @@ -16,7 +15,7 @@ fn event( tracer: &mut T, ) -> ExecutionStatus { boilerplate_ext::(vm, world, tracer, |vm, args, _, _| { - if vm.state.current_frame.address == H160::from_low_u64_be(ADDRESS_EVENT_WRITER as u64) { + if vm.state.current_frame.address == H160::from_low_u64_be(ADDRESS_EVENT_WRITER.into()) { let key = Register1::get(args, &mut vm.state); let value = Register2::get(args, &mut vm.state); let is_first = Immediate1::get(args, &mut vm.state).low_u32() == 1; @@ -53,6 +52,7 @@ fn l2_to_l1( } impl Instruction { + /// Creates an [`Event`](opcodes::Event) instruction with the provided params. pub fn from_event( key: Register1, value: Register2, @@ -68,6 +68,7 @@ impl Instruction { } } + /// Creates an [`L2ToL1Message`](opcodes::L2ToL1Message) instruction with the provided params. pub fn from_l2_to_l1_message( key: Register1, value: Register2, diff --git a/crates/vm2/src/instruction_handlers/far_call.rs b/crates/vm2/src/instruction_handlers/far_call.rs index 556c728d..1fc80552 100644 --- a/crates/vm2/src/instruction_handlers/far_call.rs +++ b/crates/vm2/src/instruction_handlers/far_call.rs @@ -11,6 +11,7 @@ use zksync_vm2_interface::{ use super::{ common::boilerplate_ext, heap_access::grow_heap, + monomorphization::{match_boolean, monomorphize, parameterize}, ret::{panic_from_failed_far_call, RETURN_COST}, AuxHeap, Heap, }; @@ -33,23 +34,22 @@ use crate::{ /// /// Even though all errors happen before the new stack frame, they cause a panic in the new frame, /// not in the caller! -fn far_call< - T: Tracer, - W: World, - M: TypeLevelCallingMode, - const IS_STATIC: bool, - const IS_SHARD: bool, ->( +fn far_call( vm: &mut VirtualMachine, world: &mut W, tracer: &mut T, -) -> ExecutionStatus { +) -> ExecutionStatus +where + T: Tracer, + W: World, + M: TypeLevelCallingMode, +{ boilerplate_ext::, _, _>(vm, world, tracer, |vm, args, world, tracer| { let (raw_abi, raw_abi_is_pointer) = Register1::get_with_pointer_flag(args, &mut vm.state); let address_mask: U256 = U256::MAX >> (256 - 160); let destination_address = Register2::get(args, &mut vm.state) & address_mask; - let exception_handler = Immediate1::get(args, &mut vm.state).low_u32() as u16; + let exception_handler = Immediate1::get_u16(args); let mut abi = get_far_call_arguments(raw_abi); abi.is_constructor_call = abi.is_constructor_call && vm.state.current_frame.is_kernel; @@ -158,14 +158,16 @@ fn far_call< }) } +#[derive(Debug)] pub(crate) struct FarCallABI { - pub gas_to_pass: u32, - pub shard_id: u8, - pub is_constructor_call: bool, - pub is_system_call: bool, + pub(crate) gas_to_pass: u32, + pub(crate) shard_id: u8, + pub(crate) is_constructor_call: bool, + pub(crate) is_system_call: bool, } -pub(crate) fn get_far_call_arguments(abi: U256) -> FarCallABI { +#[allow(clippy::cast_possible_truncation)] // intentional +fn get_far_call_arguments(abi: U256) -> FarCallABI { let gas_to_pass = abi.0[3] as u32; let settings = (abi.0[3] >> 32) as u32; let [_, shard_id, constructor_call_byte, system_call_byte] = settings.to_le_bytes(); @@ -182,15 +184,18 @@ pub(crate) fn get_far_call_arguments(abi: U256) -> FarCallABI { /// /// This function needs to be called even if we already know we will panic because /// overflowing start + length makes the heap resize even when already panicking. -pub(crate) fn get_far_call_calldata( +pub(crate) fn get_far_call_calldata( raw_abi: U256, is_pointer: bool, vm: &mut VirtualMachine, already_failed: bool, ) -> Option { let mut pointer = FatPointer::from(raw_abi); + #[allow(clippy::cast_possible_truncation)] + // intentional: the source is encoded in the lower byte of the extracted value + let raw_source = (raw_abi.0[3] >> 32) as u8; - match FatPointerSource::from_abi((raw_abi.0[3] >> 32) as u8) { + match FatPointerSource::from_abi(raw_source) { FatPointerSource::ForwardFatPointer => { if !is_pointer || pointer.offset > pointer.length || already_failed { return None; @@ -204,11 +209,11 @@ pub(crate) fn get_far_call_calldata( return None; } match target { - ToHeap => { + FatPointerTarget::ToHeap => { grow_heap::<_, _, Heap>(&mut vm.state, bound).ok()?; pointer.memory_page = vm.state.current_frame.heap; } - ToAuxHeap => { + FatPointerTarget::ToAuxHeap => { grow_heap::<_, _, AuxHeap>(&mut vm.state, bound).ok()?; pointer.memory_page = vm.state.current_frame.aux_heap; } @@ -218,10 +223,10 @@ pub(crate) fn get_far_call_calldata( // TODO PLA-974 revert to not growing the heap on failure as soon as zk_evm is fixed let bound = u32::MAX; match target { - ToHeap => { + FatPointerTarget::ToHeap => { grow_heap::<_, _, Heap>(&mut vm.state, bound).ok()?; } - ToAuxHeap => { + FatPointerTarget::ToAuxHeap => { grow_heap::<_, _, AuxHeap>(&mut vm.state, bound).ok()?; } } @@ -233,23 +238,24 @@ pub(crate) fn get_far_call_calldata( Some(pointer) } +#[derive(Debug)] enum FatPointerSource { MakeNewPointer(FatPointerTarget), ForwardFatPointer, } + +#[derive(Debug)] enum FatPointerTarget { ToHeap, ToAuxHeap, } -use FatPointerTarget::*; impl FatPointerSource { - pub const fn from_abi(value: u8) -> Self { + const fn from_abi(value: u8) -> Self { match value { - 0 => Self::MakeNewPointer(ToHeap), 1 => Self::ForwardFatPointer, - 2 => Self::MakeNewPointer(ToAuxHeap), - _ => Self::MakeNewPointer(ToHeap), // default + 2 => Self::MakeNewPointer(FatPointerTarget::ToAuxHeap), + _ => Self::MakeNewPointer(FatPointerTarget::ToHeap), // default } } } @@ -262,9 +268,8 @@ impl FatPointer { } } -use super::monomorphization::*; - impl> Instruction { + /// Creates a [`FarCall`] instruction with the provided mode and params. pub fn from_far_call( src1: Register1, src2: Register2, diff --git a/crates/vm2/src/instruction_handlers/heap_access.rs b/crates/vm2/src/instruction_handlers/heap_access.rs index 70aba1ea..0fbd2655 100644 --- a/crates/vm2/src/instruction_handlers/heap_access.rs +++ b/crates/vm2/src/instruction_handlers/heap_access.rs @@ -1,7 +1,10 @@ use primitive_types::U256; -use zksync_vm2_interface::{opcodes, OpcodeType, Tracer}; +use zksync_vm2_interface::{opcodes, HeapId, OpcodeType, Tracer}; -use super::common::{boilerplate, full_boilerplate}; +use super::{ + common::{boilerplate, full_boilerplate}, + monomorphization::{match_boolean, match_reg_imm, monomorphize, parameterize}, +}; use crate::{ addressing_modes::{ Arguments, Destination, DestinationWriter, Immediate1, Register1, Register2, @@ -10,7 +13,7 @@ use crate::{ fat_pointer::FatPointer, instruction::ExecutionStatus, state::State, - ExecutionEnd, HeapId, Instruction, VirtualMachine, + ExecutionEnd, Instruction, VirtualMachine, }; pub(crate) trait HeapFromState { @@ -91,23 +94,21 @@ fn load( Register1::set(args, &mut vm.state, value); if INCREMENT { - Register2::set(args, &mut vm.state, pointer + 32) + Register2::set(args, &mut vm.state, pointer + 32); } }) } -fn store< - T: Tracer, - W, - H: HeapFromState, - In: Source, - const INCREMENT: bool, - const HOOKING_ENABLED: bool, ->( +fn store( vm: &mut VirtualMachine, world: &mut W, tracer: &mut T, -) -> ExecutionStatus { +) -> ExecutionStatus +where + T: Tracer, + H: HeapFromState, + In: Source, +{ full_boilerplate::(vm, world, tracer, |vm, args, _, _| { // Pointers need not be masked here even though we do not care about them being pointers. // They will panic, though because they are larger than 2^32. @@ -134,7 +135,7 @@ fn store< vm.state.heaps.write_u256(heap, address, value); if INCREMENT { - Register1::set(args, &mut vm.state, pointer + 32) + Register1::set(args, &mut vm.state, pointer + 32); } if HOOKING_ENABLED && address == vm.settings.hook_address { @@ -146,8 +147,8 @@ fn store< } /// Pays for more heap space. Doesn't acually grow the heap. -/// That distinction is necessary because the bootloader gets u32::MAX heap for free. -pub(crate) fn grow_heap( +/// That distinction is necessary because the bootloader gets `u32::MAX` heap for free. +pub(crate) fn grow_heap( state: &mut State, new_bound: u32, ) -> Result<(), ()> { @@ -190,36 +191,33 @@ fn load_pointer( if INCREMENT { // This addition does not overflow because we checked that the offset is small enough above. - Register2::set_fat_ptr(args, &mut vm.state, input + 32) + Register2::set_fat_ptr(args, &mut vm.state, input + 32); } }) } -use super::monomorphization::*; - impl Instruction { - #[inline(always)] - pub fn from_heap_load( + /// Creates a [`HeapRead`](opcodes::HeapRead) instruction with the provided params. + pub fn from_heap_read( src: RegisterOrImmediate, out: Register1, incremented_out: Option, arguments: Arguments, ) -> Self { - Self::from_load::(src, out, incremented_out, arguments) + Self::from_read::(src, out, incremented_out, arguments) } - #[inline(always)] - pub fn from_aux_heap_load( + /// Creates an [`AuxHeapRead`](opcodes::AuxHeapRead) instruction with the provided params. + pub fn from_aux_heap_read( src: RegisterOrImmediate, out: Register1, incremented_out: Option, arguments: Arguments, ) -> Self { - Self::from_load::(src, out, incremented_out, arguments) + Self::from_read::(src, out, incremented_out, arguments) } - #[inline(always)] - fn from_load( + fn from_read( src: RegisterOrImmediate, out: Register1, incremented_out: Option, @@ -238,29 +236,28 @@ impl Instruction { } } - #[inline(always)] - pub fn from_heap_store( + /// Creates a [`HeapWrite`](opcodes::HeapWrite) instruction with the provided params. + pub fn from_heap_write( src1: RegisterOrImmediate, src2: Register2, incremented_out: Option, arguments: Arguments, should_hook: bool, ) -> Self { - Self::from_store::(src1, src2, incremented_out, arguments, should_hook) + Self::from_write::(src1, src2, incremented_out, arguments, should_hook) } - #[inline(always)] + /// Creates an [`AuxHeapWrite`](opcodes::AuxHeapWrite) instruction with the provided params. pub fn from_aux_heap_store( src1: RegisterOrImmediate, src2: Register2, incremented_out: Option, arguments: Arguments, ) -> Self { - Self::from_store::(src1, src2, incremented_out, arguments, false) + Self::from_write::(src1, src2, incremented_out, arguments, false) } - #[inline(always)] - fn from_store( + fn from_write( src1: RegisterOrImmediate, src2: Register2, incremented_out: Option, @@ -277,8 +274,8 @@ impl Instruction { } } - #[inline(always)] - pub fn from_load_pointer( + /// Creates an [`PointerRead`](opcodes::PointerRead) instruction with the provided params. + pub fn from_pointer_read( src: Register1, out: Register1, incremented_out: Option, diff --git a/crates/vm2/src/instruction_handlers/jump.rs b/crates/vm2/src/instruction_handlers/jump.rs index 3ca7a557..7bbb1c93 100644 --- a/crates/vm2/src/instruction_handlers/jump.rs +++ b/crates/vm2/src/instruction_handlers/jump.rs @@ -1,6 +1,9 @@ use zksync_vm2_interface::{opcodes, Tracer}; -use super::common::boilerplate; +use super::{ + common::boilerplate, + monomorphization::{match_source, monomorphize, parameterize}, +}; use crate::{ addressing_modes::{ AbsoluteStack, AdvanceStackPointer, AnySource, Arguments, CodePage, Destination, @@ -16,6 +19,7 @@ fn jump( tracer: &mut T, ) -> ExecutionStatus { boilerplate::(vm, world, tracer, |vm, args| { + #[allow(clippy::cast_possible_truncation)] // intentional let target = In::get(args, &mut vm.state).low_u32() as u16; let next_instruction = vm.state.current_frame.get_pc_as_u16(); @@ -25,9 +29,8 @@ fn jump( }) } -use super::monomorphization::*; - impl Instruction { + /// Creates a [`Jump`](opcodes::Jump) instruction with the provided params. pub fn from_jump(source: AnySource, destination: Register1, arguments: Arguments) -> Self { Self { handler: monomorphize!(jump [T W] match_source source), diff --git a/crates/vm2/src/instruction_handlers/mod.rs b/crates/vm2/src/instruction_handlers/mod.rs index 9b62fabe..9a27ea29 100644 --- a/crates/vm2/src/instruction_handlers/mod.rs +++ b/crates/vm2/src/instruction_handlers/mod.rs @@ -1,9 +1,5 @@ -pub use zksync_vm2_interface::opcodes::{ - Add, And, Div, Mul, Or, PointerAdd, PointerPack, PointerShrink, PointerSub, RotateLeft, - RotateRight, ShiftLeft, ShiftRight, Sub, Xor, -}; - pub(crate) use self::{ + context::address_into_u256, heap_access::{AuxHeap, Heap}, ret::{invalid_instruction, RETURN_COST}, }; diff --git a/crates/vm2/src/instruction_handlers/near_call.rs b/crates/vm2/src/instruction_handlers/near_call.rs index 16c3a19d..ac4a6378 100644 --- a/crates/vm2/src/instruction_handlers/near_call.rs +++ b/crates/vm2/src/instruction_handlers/near_call.rs @@ -14,9 +14,9 @@ fn near_call( tracer: &mut T, ) -> ExecutionStatus { boilerplate::(vm, world, tracer, |vm, args| { - let gas_to_pass = Register1::get(args, &mut vm.state).0[0] as u32; - let destination = Immediate1::get(args, &mut vm.state); - let error_handler = Immediate2::get(args, &mut vm.state); + let gas_to_pass = Register1::get(args, &mut vm.state).low_u32(); + let destination = Immediate1::get_u16(args); + let error_handler = Immediate2::get_u16(args); let new_frame_gas = if gas_to_pass == 0 { vm.state.current_frame.gas @@ -25,19 +25,18 @@ fn near_call( }; vm.state.current_frame.push_near_call( new_frame_gas, - error_handler.low_u32() as u16, + error_handler, vm.world_diff.snapshot(), ); vm.state.flags = Flags::new(false, false, false); - vm.state - .current_frame - .set_pc_from_u16(destination.low_u32() as u16); + vm.state.current_frame.set_pc_from_u16(destination); }) } impl Instruction { + /// Creates a [`NearCall`](opcodes::NearCall) instruction with the provided params. pub fn from_near_call( gas: Register1, destination: Immediate1, diff --git a/crates/vm2/src/instruction_handlers/nop.rs b/crates/vm2/src/instruction_handlers/nop.rs index 7349ae20..d1ec55be 100644 --- a/crates/vm2/src/instruction_handlers/nop.rs +++ b/crates/vm2/src/instruction_handlers/nop.rs @@ -24,6 +24,7 @@ fn nop( } impl Instruction { + /// Creates a [`Nop`](opcodes::Nop) instruction with the provided params. pub fn from_nop( pop: AdvanceStackPointer, push: AdvanceStackPointer, diff --git a/crates/vm2/src/instruction_handlers/pointer.rs b/crates/vm2/src/instruction_handlers/pointer.rs index c78a92d7..e224ec19 100644 --- a/crates/vm2/src/instruction_handlers/pointer.rs +++ b/crates/vm2/src/instruction_handlers/pointer.rs @@ -4,7 +4,12 @@ use zksync_vm2_interface::{ OpcodeType, Tracer, }; -use super::common::boilerplate; +use super::{ + common::boilerplate, + monomorphization::{ + match_boolean, match_destination, match_source, monomorphize, parameterize, + }, +}; use crate::{ addressing_modes::{ AbsoluteStack, AdvanceStackPointer, AnyDestination, AnySource, Arguments, CodePage, @@ -47,7 +52,7 @@ fn ptr }) } -pub trait PtrOp: OpcodeType { +pub(crate) trait PtrOp: OpcodeType { fn perform(in1: U256, in2: U256) -> Option; } @@ -102,11 +107,29 @@ impl PtrOp for PointerShrink { } } -use super::monomorphization::*; +macro_rules! from_ptr_op { + ($name:ident <$binop:ty>) => { + #[doc = concat!("Creates a [`", stringify!($binop), "`] instruction with the provided params.")] + pub fn $name( + src1: AnySource, + src2: Register2, + out: AnyDestination, + arguments: Arguments, + swap: bool, + ) -> Self { + Self::from_ptr::<$binop>(src1, src2, out, arguments, swap) + } + }; +} +/// Pointer-related instructions. impl Instruction { - #[inline(always)] - pub fn from_ptr( + from_ptr_op!(from_pointer_add); + from_ptr_op!(from_pointer_sub); + from_ptr_op!(from_pointer_pack); + from_ptr_op!(from_pointer_shrink); + + pub(crate) fn from_ptr( src1: AnySource, src2: Register2, out: AnyDestination, diff --git a/crates/vm2/src/instruction_handlers/precompiles.rs b/crates/vm2/src/instruction_handlers/precompiles.rs index 9d88ed71..4ad5e7f0 100644 --- a/crates/vm2/src/instruction_handlers/precompiles.rs +++ b/crates/vm2/src/instruction_handlers/precompiles.rs @@ -1,3 +1,4 @@ +use primitive_types::{H160, U256}; use zk_evm_abstractions::{ aux::Timestamp, precompiles::{ @@ -37,7 +38,11 @@ fn precompile_call( vm.state.current_frame.pc = &*vm.panic; return; }; - vm.world_diff.pubdata.0 += aux_data.extra_pubdata_cost as i32; + + #[allow(clippy::cast_possible_wrap)] + { + vm.world_diff.pubdata.0 += aux_data.extra_pubdata_cost as i32; + } let mut abi = PrecompileCallABI::from_u256(Register1::get(args, &mut vm.state)); if abi.memory_page_to_read == 0 { @@ -54,9 +59,9 @@ fn precompile_call( tx_number_in_block: Default::default(), aux_byte: Default::default(), shard_id: Default::default(), - address: Default::default(), - read_value: Default::default(), - written_value: Default::default(), + address: H160::default(), + read_value: U256::default(), + written_value: U256::default(), rw_flag: Default::default(), rollback: Default::default(), is_service: Default::default(), @@ -65,6 +70,9 @@ fn precompile_call( let address_bytes = vm.state.current_frame.address.0; let address_low = u16::from_le_bytes([address_bytes[19], address_bytes[18]]); let heaps = &mut vm.state.heaps; + + #[allow(clippy::cast_possible_truncation)] + // if we're having `> u32::MAX` cycles, we've got larger issues match address_low { KECCAK256_ROUND_FUNCTION_PRECOMPILE_ADDRESS => { tracer.on_extra_prover_cycles(CycleStats::Keccak256( @@ -82,7 +90,7 @@ fn precompile_call( )); } SECP256R1_VERIFY_PRECOMPILE_ADDRESS => { - tracer.on_extra_prover_cycles(CycleStats::Secp256k1Verify( + tracer.on_extra_prover_cycles(CycleStats::Secp256r1Verify( secp256r1_verify_function::<_, false>(0, query, heaps).0 as u32, )); } @@ -131,6 +139,7 @@ impl Memory for Heaps { } impl Instruction { + /// Creates a [`PrecompileCall`](opcodes::PrecompileCall) instruction with the provided params. pub fn from_precompile_call( abi: Register1, burn: Register2, diff --git a/crates/vm2/src/instruction_handlers/ret.rs b/crates/vm2/src/instruction_handlers/ret.rs index f423bf8f..8f523d5d 100644 --- a/crates/vm2/src/instruction_handlers/ret.rs +++ b/crates/vm2/src/instruction_handlers/ret.rs @@ -4,7 +4,11 @@ use zksync_vm2_interface::{ ReturnType, Tracer, }; -use super::{common::full_boilerplate, far_call::get_far_call_calldata, monomorphization::*}; +use super::{ + common::full_boilerplate, + far_call::get_far_call_calldata, + monomorphization::{match_boolean, monomorphize, parameterize}, +}; use crate::{ addressing_modes::{Arguments, Immediate1, Register1, Source, INVALID_INSTRUCTION_COST}, callframe::FrameRemnant, @@ -27,10 +31,10 @@ fn naked_ret( }) = vm.state.current_frame.pop_near_call() { if TO_LABEL { - let pc = Immediate1::get(args, &mut vm.state).low_u32() as u16; + let pc = Immediate1::get_u16(args); vm.state.current_frame.set_pc_from_u16(pc); } else if return_type.is_failure() { - vm.state.current_frame.set_pc_from_u16(exception_handler) + vm.state.current_frame.set_pc_from_u16(exception_handler); } (snapshot, near_call_leftover_gas) @@ -79,7 +83,7 @@ fn naked_ret( .read_range_big_endian( return_value.start..return_value.start + return_value.length, ) - .to_vec(); + .clone(); if return_type == ReturnType::Revert { ExecutionStatus::Stopped(ExecutionEnd::Reverted(output)) } else { @@ -99,7 +103,7 @@ fn naked_ret( vm.state.register_pointer_flags = 2; if return_type.is_failure() { - vm.state.current_frame.set_pc_from_u16(exception_handler) + vm.state.current_frame.set_pc_from_u16(exception_handler); } (snapshot, leftover_gas) @@ -140,7 +144,7 @@ pub(crate) fn free_panic( ) -> ExecutionStatus { tracer.before_instruction::, _>(vm); // args aren't used for panics unless TO_LABEL - let result = naked_ret::( + let result = naked_ret::( vm, &Arguments::new(Predicate::Always, 0, ModeRequirements::none()), ); @@ -159,14 +163,10 @@ pub(crate) fn panic_from_failed_far_call( // Gas is already subtracted in the far call code. // No need to roll back, as no changes are made in this "frame". - vm.state.set_context_u128(0); - vm.state.registers = [U256::zero(); 16]; vm.state.register_pointer_flags = 2; - vm.state.flags = Flags::new(true, false, false); - vm.state.current_frame.set_pc_from_u16(exception_handler); tracer.after_instruction::, _>(vm); @@ -175,14 +175,16 @@ pub(crate) fn panic_from_failed_far_call( /// Panics, burning all available gas. static INVALID_INSTRUCTION: Instruction<(), ()> = Instruction::from_invalid(); -pub fn invalid_instruction<'a, T, W>() -> &'a Instruction { +pub(crate) fn invalid_instruction<'a, T, W>() -> &'a Instruction { // Safety: the handler of an invalid instruction is never read. - unsafe { &*(&INVALID_INSTRUCTION as *const Instruction<(), ()>).cast() } + unsafe { &*std::ptr::addr_of!(INVALID_INSTRUCTION).cast() } } pub(crate) const RETURN_COST: u32 = 5; +/// Variations of [`Ret`](opcodes::Ret) instructions. impl Instruction { + /// Creates a normal [`Ret`](opcodes::Ret) instruction with the provided params. pub fn from_ret(src1: Register1, label: Option, arguments: Arguments) -> Self { let to_label = label.is_some(); Self { @@ -190,6 +192,8 @@ impl Instruction { arguments: arguments.write_source(&src1).write_source(&label), } } + + /// Creates a revert [`Ret`](opcodes::Ret) instruction with the provided params. pub fn from_revert(src1: Register1, label: Option, arguments: Arguments) -> Self { let to_label = label.is_some(); Self { @@ -197,6 +201,8 @@ impl Instruction { arguments: arguments.write_source(&src1).write_source(&label), } } + + /// Creates a panic [`Ret`](opcodes::Ret) instruction with the provided params. pub fn from_panic(label: Option, arguments: Arguments) -> Self { let to_label = label.is_some(); Self { @@ -205,6 +211,7 @@ impl Instruction { } } + /// Creates a *invalid* instruction that will panic by draining all gas. pub const fn from_invalid() -> Self { Self { // This field is never read because the instruction fails at the gas cost stage. diff --git a/crates/vm2/src/instruction_handlers/storage.rs b/crates/vm2/src/instruction_handlers/storage.rs index ff8fd208..07eaeb62 100644 --- a/crates/vm2/src/instruction_handlers/storage.rs +++ b/crates/vm2/src/instruction_handlers/storage.rs @@ -76,32 +76,40 @@ fn sload_transient( } impl> Instruction { - #[inline(always)] - pub fn from_sstore(src1: Register1, src2: Register2, arguments: Arguments) -> Self { + /// Creates a [`StorageWrite`](opcodes::StorageWrite) instruction with the provided params. + pub fn from_storage_write(src1: Register1, src2: Register2, arguments: Arguments) -> Self { Self { handler: sstore, arguments: arguments.write_source(&src1).write_source(&src2), } } - #[inline(always)] - pub fn from_sstore_transient(src1: Register1, src2: Register2, arguments: Arguments) -> Self { + /// Creates a [`TransientStorageWrite`](opcodes::TransientStorageWrite) instruction with the provided params. + pub fn from_transient_storage_write( + src1: Register1, + src2: Register2, + arguments: Arguments, + ) -> Self { Self { handler: sstore_transient, arguments: arguments.write_source(&src1).write_source(&src2), } } - #[inline(always)] - pub fn from_sload(src: Register1, dst: Register1, arguments: Arguments) -> Self { + /// Creates a [`StorageRead`](opcodes::StorageRead) instruction with the provided params. + pub fn from_storage_read(src: Register1, dst: Register1, arguments: Arguments) -> Self { Self { handler: sload, arguments: arguments.write_source(&src).write_destination(&dst), } } - #[inline(always)] - pub fn from_sload_transient(src: Register1, dst: Register1, arguments: Arguments) -> Self { + /// Creates a [`TransientStorageRead`](opcodes::TransientStorageRead) instruction with the provided params. + pub fn from_transient_storage_read( + src: Register1, + dst: Register1, + arguments: Arguments, + ) -> Self { Self { handler: sload_transient, arguments: arguments.write_source(&src).write_destination(&dst), diff --git a/crates/vm2/src/lib.rs b/crates/vm2/src/lib.rs index a8333bfc..338bfdab 100644 --- a/crates/vm2/src/lib.rs +++ b/crates/vm2/src/lib.rs @@ -1,30 +1,31 @@ +//! # High-Performance ZKsync Era VM +//! +//! This crate provides high-performance [`VirtualMachine`] for ZKsync Era. + use std::hash::{DefaultHasher, Hash, Hasher}; use primitive_types::{H160, U256}; -pub use zksync_vm2_interface::{ - CallframeInterface, CycleStats, HeapId, Opcode, OpcodeType, ReturnType, StateInterface, Tracer, -}; +pub use zksync_vm2_interface as interface; +use zksync_vm2_interface::Tracer; // Re-export missing modules if single instruction testing is enabled #[cfg(feature = "single_instruction_test")] pub(crate) use self::single_instruction_test::{heap, program, stack}; pub use self::{ - decommit::{address_into_u256, initial_decommit}, fat_pointer::FatPointer, instruction::{ExecutionEnd, Instruction}, mode_requirements::ModeRequirements, predication::Predicate, program::Program, - vm::{Settings, VirtualMachine, VmSnapshot as Snapshot}, - world_diff::{Event, L2ToL1Log, WorldDiff}, + vm::{Settings, VirtualMachine}, + world_diff::{Snapshot, StorageChange, WorldDiff}, }; -// FIXME: revise visibility pub mod addressing_modes; #[cfg(not(feature = "single_instruction_test"))] mod bitset; mod callframe; -pub mod decode; +mod decode; mod decommit; mod fat_pointer; #[cfg(not(feature = "single_instruction_test"))] @@ -41,21 +42,18 @@ pub mod single_instruction_test; #[cfg(not(feature = "single_instruction_test"))] mod stack; mod state; -pub mod testworld; +pub mod testonly; +#[cfg(all(test, not(feature = "single_instruction_test")))] +mod tests; mod tracing; mod vm; mod world_diff; -pub trait World: StorageInterface + Sized { - /// This will be called *every* time a contract is called. Caching and decoding is - /// the world implementor's job. - fn decommit(&mut self, hash: U256) -> Program; - - fn decommit_code(&mut self, hash: U256) -> Vec; -} - +/// VM storage access operations. pub trait StorageInterface { - /// There is no write_storage; [WorldDiff::get_storage_changes] gives a list of all storage changes. + /// Reads the specified slot from the storage. + /// + /// There is no write counterpart; [`WorldDiff::get_storage_changes()`] gives a list of all storage changes. fn read_storage(&mut self, contract: H160, key: U256) -> Option; /// Computes the cost of writing a storage slot. @@ -65,6 +63,19 @@ pub trait StorageInterface { fn is_free_storage_slot(&self, contract: &H160, key: &U256) -> bool; } +/// Encapsulates VM interaction with the external world. This includes VM storage and decomitting (loading) bytecodes +/// for execution. +pub trait World: StorageInterface + Sized { + /// Loads a bytecode with the specified hash. + /// + /// This method will be called *every* time a contract is called. Caching and decoding is + /// the world implementor's job. + fn decommit(&mut self, hash: U256) -> Program; + + /// Loads bytecode bytes for the `decommit` opcode. + fn decommit_code(&mut self, hash: U256) -> Vec; +} + /// Deterministic (across program runs and machines) hash that can be used for `Debug` implementations /// to concisely represent large amounts of data. #[cfg_attr(feature = "single_instruction_test", allow(dead_code))] // Currently used entirely in types overridden by `single_instruction_test` feature diff --git a/crates/vm2/src/mode_requirements.rs b/crates/vm2/src/mode_requirements.rs index 5ba2c8f6..918ad6ba 100644 --- a/crates/vm2/src/mode_requirements.rs +++ b/crates/vm2/src/mode_requirements.rs @@ -1,17 +1,20 @@ -#[derive(Debug)] +/// VM execution mode requirements (kernel only, not in static call) that can be placed on instructions. +#[derive(Debug, Clone, Copy)] pub struct ModeRequirements(pub(crate) u8); impl ModeRequirements { + /// Creates new requirements. pub const fn new(kernel_only: bool, cannot_use_in_static: bool) -> Self { Self((kernel_only as u8) | ((cannot_use_in_static as u8) << 1)) } + /// Creates default requirements that always hold. pub const fn none() -> Self { Self::new(false, false) } - pub fn met(&self, is_kernel: bool, is_static: bool) -> bool { - let enabled_modes = (is_kernel as u8) | ((!is_static as u8) << 1); + pub(crate) fn met(self, is_kernel: bool, is_static: bool) -> bool { + let enabled_modes = u8::from(is_kernel) | (u8::from(!is_static) << 1); enabled_modes & self.0 == self.0 } } diff --git a/crates/vm2/src/predication.rs b/crates/vm2/src/predication.rs index 64893b03..1ac16006 100644 --- a/crates/vm2/src/predication.rs +++ b/crates/vm2/src/predication.rs @@ -4,43 +4,46 @@ const GT_BIT: u8 = 1 << 2; const ALWAYS_BIT: u8 = 1 << 3; #[derive(Debug, Clone, PartialEq)] -pub struct Flags(u8); +pub(crate) struct Flags(u8); impl Flags { - pub fn new(lt_of: bool, eq: bool, gt: bool) -> Self { - Flags(lt_of as u8 | ((eq as u8) << 1) | ((gt as u8) << 2) | ALWAYS_BIT) + pub(crate) fn new(lt_of: bool, eq: bool, gt: bool) -> Self { + Flags(u8::from(lt_of) | (u8::from(eq) << 1) | (u8::from(gt) << 2) | ALWAYS_BIT) } } -/// Predicate encoded so that comparing it to flags is efficient -#[derive(Copy, Clone, Debug, Hash)] +/// Predicate for an instruction. Encoded so that comparing it to flags is efficient. +#[derive(Copy, Clone, Debug, Default, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[repr(u8)] pub enum Predicate { + /// Always execute the associated instruction. + #[default] Always = ALWAYS_BIT, + /// Execute the associated instruction if the "greater than" execution flag is set. IfGT = GT_BIT, + /// Execute the associated instruction if the "equal" execution flag is set. IfEQ = EQ_BIT, + /// Execute the associated instruction if the "less than" execution flag is set. IfLT = LT_BIT, + /// Execute the associated instruction if either of "greater than" or "equal" execution flags are set. IfGE = GT_BIT | EQ_BIT, + /// Execute the associated instruction if either of "less than" or "equal" execution flags are set. IfLE = LT_BIT | EQ_BIT, + /// Execute the associated instruction if the "equal" execution flag is not set. IfNotEQ = EQ_BIT << 4 | ALWAYS_BIT, - IfGtOrLT = GT_BIT | LT_BIT, + /// Execute the associated instruction if either of "less than" or "greater than" execution flags are set. + IfGTOrLT = GT_BIT | LT_BIT, } impl Predicate { #[inline(always)] - pub fn satisfied(&self, flags: &Flags) -> bool { - let bits = *self as u8; + pub(crate) fn satisfied(self, flags: &Flags) -> bool { + let bits = self as u8; bits & flags.0 != 0 && (bits >> 4) & flags.0 == 0 } } -impl Default for Predicate { - fn default() -> Self { - Self::Always - } -} - #[cfg(feature = "single_instruction_test")] impl From<&Flags> for zk_evm::flags::Flags { fn from(flags: &Flags) -> Self { diff --git a/crates/vm2/src/program.rs b/crates/vm2/src/program.rs index 93ebc8fd..7a2b0dc0 100644 --- a/crates/vm2/src/program.rs +++ b/crates/vm2/src/program.rs @@ -1,8 +1,12 @@ use std::{fmt, sync::Arc}; use primitive_types::U256; +use zksync_vm2_interface::Tracer; -use crate::{hash_for_debugging, Instruction}; +use crate::{ + addressing_modes::Arguments, decode::decode, hash_for_debugging, instruction::ExecutionStatus, + Instruction, ModeRequirements, Predicate, VirtualMachine, World, +}; /// Compiled EraVM bytecode. /// @@ -47,19 +51,58 @@ impl fmt::Debug for Program { } } -impl Program { - pub fn new(instructions: Vec>, code_page: Vec) -> Self { +impl> Program { + /// Creates a new program. + #[allow(clippy::missing_panics_doc)] // false positive + pub fn new(bytecode: &[u8], enable_hooks: bool) -> Self { + let instructions = decode_program( + &bytecode + .chunks_exact(8) + .map(|chunk| u64::from_be_bytes(chunk.try_into().unwrap())) + .collect::>(), + enable_hooks, + ); + let code_page = bytecode + .chunks_exact(32) + .map(U256::from_big_endian) + .collect::>(); Self { + instructions: instructions.into(), code_page: code_page.into(), + } + } + + /// Creates a new program from `U256` words. + pub fn from_words(bytecode_words: Vec, enable_hooks: bool) -> Self { + let instructions = decode_program( + &bytecode_words + .iter() + .flat_map(|x| x.0.into_iter().rev()) + .collect::>(), + enable_hooks, + ); + Self { + instructions: instructions.into(), + code_page: bytecode_words.into(), + } + } + + #[doc(hidden)] // should only be used in low-level tests / benchmarks + pub fn from_raw(instructions: Vec>, code_page: Vec) -> Self { + Self { instructions: instructions.into(), + code_page: code_page.into(), } } +} - pub fn instruction(&self, n: u16) -> Option<&Instruction> { +impl Program { + pub(crate) fn instruction(&self, n: u16) -> Option<&Instruction> { self.instructions.get::(n.into()) } - pub fn code_page(&self) -> &Arc<[U256]> { + /// Returns a reference to the code page of this program. + pub fn code_page(&self) -> &[U256] { &self.code_page } } @@ -75,3 +118,37 @@ impl PartialEq for Program { && Arc::ptr_eq(&self.instructions, &other.instructions) } } + +/// Wraparound instruction placed at the end of programs exceeding `1 << 16` instructions to simulate the 16-bit program counter overflowing. +/// Does not invoke tracers because it is an implementation detail, not an actual instruction. +fn jump_to_beginning() -> Instruction { + Instruction { + handler: jump_to_beginning_handler, + arguments: Arguments::new(Predicate::Always, 0, ModeRequirements::none()), + } +} + +fn jump_to_beginning_handler( + vm: &mut VirtualMachine, + _: &mut W, + _: &mut T, +) -> ExecutionStatus { + let first_instruction = vm.state.current_frame.program.instruction(0).unwrap(); + vm.state.current_frame.pc = first_instruction; + ExecutionStatus::Running +} + +fn decode_program>( + raw: &[u64], + is_bootloader: bool, +) -> Vec> { + raw.iter() + .take(1 << 16) + .map(|i| decode(*i, is_bootloader)) + .chain(std::iter::once(if raw.len() >= 1 << 16 { + jump_to_beginning() + } else { + Instruction::from_invalid() + })) + .collect() +} diff --git a/crates/vm2/src/rollback.rs b/crates/vm2/src/rollback.rs index 9764bd22..6344a7ef 100644 --- a/crates/vm2/src/rollback.rs +++ b/crates/vm2/src/rollback.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; /// A trait for things that can be rolled back to snapshots pub(crate) trait Rollback { @@ -15,7 +15,7 @@ pub(crate) struct RollbackableMap { } impl RollbackableMap { - pub fn insert(&mut self, key: K, value: V) -> Option { + pub(crate) fn insert(&mut self, key: K, value: V) -> Option { let old_value = self.map.insert(key.clone(), value); self.old_entries.push((key, old_value.clone())); old_value @@ -66,14 +66,14 @@ impl AsRef> for RollbackableMap { #[derive(Debug, Default)] pub(crate) struct RollbackableSet { - map: BTreeMap, + map: BTreeSet, old_entries: Vec, } impl RollbackableSet { /// Adds `key` to the set and returns if it was added (not present earlier). - pub fn add(&mut self, key: T) -> bool { - let is_new = self.map.insert(key.clone(), ()).is_none(); + pub(crate) fn add(&mut self, key: T) -> bool { + let is_new = self.map.insert(key.clone()); if is_new { self.old_entries.push(key); } @@ -99,8 +99,8 @@ impl Rollback for RollbackableSet { } } -impl AsRef> for RollbackableSet { - fn as_ref(&self) -> &BTreeMap { +impl AsRef> for RollbackableSet { + fn as_ref(&self) -> &BTreeSet { &self.map } } @@ -113,7 +113,7 @@ pub(crate) struct RollbackableLog { impl Default for RollbackableLog { fn default() -> Self { Self { - entries: Default::default(), + entries: Vec::default(), } } } @@ -126,15 +126,15 @@ impl Rollback for RollbackableLog { } fn rollback(&mut self, snapshot: Self::Snapshot) { - self.entries.truncate(snapshot) + self.entries.truncate(snapshot); } fn delete_history(&mut self) {} } impl RollbackableLog { - pub fn push(&mut self, entry: T) { - self.entries.push(entry) + pub(crate) fn push(&mut self, entry: T) { + self.entries.push(entry); } pub(crate) fn logs_after(&self, snapshot: as Rollback>::Snapshot) -> &[T] { @@ -150,7 +150,7 @@ impl AsRef<[T]> for RollbackableLog { /// Rollbackable Plain Old Data simply stores copies of itself in snapshots. #[derive(Debug, Default, Copy, Clone)] -pub(crate) struct RollbackablePod(pub T); +pub(crate) struct RollbackablePod(pub(crate) T); impl Rollback for RollbackablePod { type Snapshot = T; @@ -160,7 +160,7 @@ impl Rollback for RollbackablePod { } fn rollback(&mut self, snapshot: Self::Snapshot) { - self.0 = snapshot + self.0 = snapshot; } fn delete_history(&mut self) {} diff --git a/crates/vm2/src/single_instruction_test/callframe.rs b/crates/vm2/src/single_instruction_test/callframe.rs index c1f5b76a..e8caccb8 100644 --- a/crates/vm2/src/single_instruction_test/callframe.rs +++ b/crates/vm2/src/single_instruction_test/callframe.rs @@ -1,12 +1,11 @@ use arbitrary::Arbitrary; use primitive_types::H160; use zkevm_opcode_defs::EVM_SIMULATOR_STIPEND; -use zksync_vm2_interface::Tracer; +use zksync_vm2_interface::{HeapId, Tracer}; use super::stack::{Stack, StackPool}; use crate::{ - callframe::Callframe, decommit::is_kernel, predication::Flags, HeapId, Program, World, - WorldDiff, + callframe::Callframe, decommit::is_kernel, predication::Flags, Program, World, WorldDiff, }; impl<'a> Arbitrary<'a> for Flags { @@ -25,6 +24,8 @@ impl<'a, T: Tracer, W: World> Arbitrary<'a> for Callframe { // zk_evm considers smaller pages to be older // vm2 doesn't care about the order // but the calldata heap must be different from the heap and aux heap + #[allow(clippy::range_minus_one)] + // cannot use exclusive range because of `int_in_range()` signature let calldata_heap = HeapId::from_u32_unchecked(u.int_in_range(0..=base_page - 1)?); let program: Program = u.arbitrary()?; @@ -65,11 +66,7 @@ impl<'a, T: Tracer, W: World> Arbitrary<'a> for Callframe { } impl Callframe { - pub fn raw_first_instruction(&self) -> u64 { - self.program.raw_first_instruction - } - - pub fn dummy() -> Self { + pub(crate) fn dummy() -> Self { Self { address: H160::zero(), code_address: H160::zero(), @@ -95,3 +92,9 @@ impl Callframe { } } } + +impl Callframe { + pub(crate) fn raw_first_instruction(&self) -> u64 { + self.program.raw_first_instruction + } +} diff --git a/crates/vm2/src/single_instruction_test/heap.rs b/crates/vm2/src/single_instruction_test/heap.rs index fb143ffc..1fe841b3 100644 --- a/crates/vm2/src/single_instruction_test/heap.rs +++ b/crates/vm2/src/single_instruction_test/heap.rs @@ -12,6 +12,7 @@ pub struct Heap { pub(crate) write: Option<(u32, U256)>, } +#[allow(clippy::unused_self)] // to align signatures with real implementation impl Heap { fn write_u256(&mut self, start_address: u32, value: U256) { assert!(self.write.is_none()); @@ -57,6 +58,7 @@ pub struct Heaps { pub(crate) read: MockRead, } +#[allow(clippy::unused_self)] // to align signatures with real implementation impl Heaps { pub(crate) fn new(_: &[u8]) -> Self { unimplemented!("Should use arbitrary heap, not fresh heap in testing.") diff --git a/crates/vm2/src/single_instruction_test/into_zk_evm.rs b/crates/vm2/src/single_instruction_test/into_zk_evm.rs index 0411815c..9829007f 100644 --- a/crates/vm2/src/single_instruction_test/into_zk_evm.rs +++ b/crates/vm2/src/single_instruction_test/into_zk_evm.rs @@ -15,7 +15,7 @@ use zkevm_opcode_defs::{decoding::EncodingModeProduction, TRANSIENT_STORAGE_AUX_ use zksync_vm2_interface::Tracer; use super::{stack::Stack, state_to_zk_evm::vm2_state_to_zk_evm_state, MockWorld}; -use crate::{StorageInterface, VirtualMachine}; +use crate::{StorageInterface, VirtualMachine, World}; type ZkEvmState = VmState< MockWorldWrapper, @@ -28,7 +28,10 @@ type ZkEvmState = VmState< EncodingModeProduction, >; -pub fn vm2_to_zk_evm(vm: &VirtualMachine, world: MockWorld) -> ZkEvmState { +pub fn vm2_to_zk_evm>( + vm: &VirtualMachine, + world: MockWorld, +) -> ZkEvmState { let mut event_sink = InMemoryEventSink::new(); event_sink.start_frame(zk_evm::aux_structures::Timestamp(0)); @@ -88,10 +91,8 @@ pub struct MockMemory { heap_write: Option, } -// One arbitrary heap value is not enough for zk_evm -// because it reads two U256s to read one U256. #[derive(Debug, Copy, Clone)] -pub struct ExpectedHeapValue { +pub(crate) struct ExpectedHeapValue { heap: u32, start_index: u32, value: [u8; 32], @@ -99,6 +100,7 @@ pub struct ExpectedHeapValue { impl ExpectedHeapValue { /// Returns a new U256 that contains data from the heap value and zero elsewhere. + /// One arbitrary heap value is not enough for `zk_evm` because it reads two U256s to read one U256. fn partially_overlapping_u256(&self, start: u32) -> U256 { let mut read = [0; 32]; for i in 0..32 { @@ -121,6 +123,7 @@ impl Memory for MockMemory { ) -> zk_evm::aux_structures::MemoryQuery { match query.location.memory_type { MemoryType::Stack => { + #[allow(clippy::cast_possible_truncation)] // intentional let slot = query.location.index.0 as u16; if query.rw_flag { self.stack.set(slot, query.value); @@ -174,7 +177,7 @@ impl Memory for MockMemory { query.value = self .code_page .get(query.location.index.0 as usize) - .cloned() + .copied() .unwrap_or_default(); query } @@ -196,10 +199,7 @@ impl Storage for MockWorldWrapper { &mut self, _: u32, mut query: zk_evm::aux_structures::LogQuery, - ) -> ( - zk_evm::aux_structures::LogQuery, - zk_evm::aux_structures::PubdataCost, - ) { + ) -> (zk_evm::aux_structures::LogQuery, PubdataCost) { if query.rw_flag { (query, PubdataCost(0)) } else { @@ -234,12 +234,12 @@ impl DecommittmentProcessor for MockDecommitter { Ok(partial_query) } - fn decommit_into_memory( + fn decommit_into_memory( &mut self, _: u32, _partial_query: zk_evm::aux_structures::DecommittmentQuery, _memory: &mut M, - ) -> anyhow::Result>> { + ) -> anyhow::Result>> { Ok(None) } } @@ -286,7 +286,7 @@ impl tracing::Tracer for NoTracer { pub struct NoOracle; impl PrecompilesProcessor for NoOracle { - fn execute_precompile( + fn execute_precompile( &mut self, _: u32, _: zk_evm::aux_structures::LogQuery, @@ -333,7 +333,7 @@ impl VmWitnessTracer<8, EncodingModeProduction> for NoOracle { &mut self, _: u32, _: zk_evm::aux_structures::LogQuery, - _: zk_evm::aux_structures::PubdataCost, + _: PubdataCost, ) { } @@ -344,7 +344,7 @@ impl VmWitnessTracer<8, EncodingModeProduction> for NoOracle { &mut self, _: u32, _: zk_evm::aux_structures::DecommittmentQuery, - _: Vec, + _: Vec, ) { } diff --git a/crates/vm2/src/single_instruction_test/mock_array.rs b/crates/vm2/src/single_instruction_test/mock_array.rs index 4b9eff18..8e267c63 100644 --- a/crates/vm2/src/single_instruction_test/mock_array.rs +++ b/crates/vm2/src/single_instruction_test/mock_array.rs @@ -3,20 +3,20 @@ use std::{cell::Cell, fmt::Debug}; use arbitrary::Arbitrary; #[derive(Clone, Debug)] -pub struct MockRead { +pub(crate) struct MockRead { pub(crate) value_read: T, index_read: Cell>, } impl MockRead { - pub fn new(value: T) -> Self { + pub(crate) fn new(value: T) -> Self { Self { value_read: value, index_read: Cell::new(None), } } - pub fn get(&self, index: I) -> &T { + pub(crate) fn get(&self, index: I) -> &T { if let Some(previous_index) = self.index_read.get() { assert_eq!(previous_index, index); } @@ -25,7 +25,7 @@ impl MockRead { &self.value_read } - pub fn get_mut(&mut self, index: I) -> &mut T { + pub(crate) fn get_mut(&mut self, index: I) -> &mut T { if let Some(previous_index) = self.index_read.get() { assert_eq!(previous_index, index); } @@ -34,7 +34,7 @@ impl MockRead { &mut self.value_read } - pub fn read_that_happened(&self) -> Option<(I, T)> { + pub(crate) fn read_that_happened(&self) -> Option<(I, T)> { self.index_read .get() .map(|index| (index, self.value_read.clone())) diff --git a/crates/vm2/src/single_instruction_test/mod.rs b/crates/vm2/src/single_instruction_test/mod.rs index 4db72b28..527a05b8 100644 --- a/crates/vm2/src/single_instruction_test/mod.rs +++ b/crates/vm2/src/single_instruction_test/mod.rs @@ -3,7 +3,9 @@ //! It would be wasteful to randomly generate the whole heap. Instead, we only generate //! the part of the heap that is actually accessed, which is at most 32 bytes! //! -//! The same kind of mocking in applied to stack memory, the program, the world and callstack. +//! The same kind of mocking is applied to stack memory, the program, the world and the callstack. + +#![allow(missing_docs)] pub use self::{ into_zk_evm::{add_heap_to_zk_evm, vm2_to_zk_evm, NoTracer}, diff --git a/crates/vm2/src/single_instruction_test/print_mock_info.rs b/crates/vm2/src/single_instruction_test/print_mock_info.rs index a2285d74..57eba599 100644 --- a/crates/vm2/src/single_instruction_test/print_mock_info.rs +++ b/crates/vm2/src/single_instruction_test/print_mock_info.rs @@ -13,9 +13,9 @@ impl VirtualMachine { } impl State { - pub fn print_mock_info(&self) { - if let Some((heapid, heap)) = self.heaps.read.read_that_happened() { - println!("Heap: {:?}", heapid); + pub(crate) fn print_mock_info(&self) { + if let Some((heap_id, heap)) = self.heaps.read.read_that_happened() { + println!("Heap: {heap_id:?}"); if let Some((address, value)) = heap.read.read_that_happened() { println!(" {value:?} read from {address:?}"); } @@ -35,7 +35,7 @@ impl State { } impl Callframe { - pub fn print_mock_info(&self) { + pub(crate) fn print_mock_info(&self) { if let Some((address, (value, tag))) = self.stack.read_that_happened() { println!(" {value:?} (is_pointer: {tag}) read from stack address {address}",); } diff --git a/crates/vm2/src/single_instruction_test/stack.rs b/crates/vm2/src/single_instruction_test/stack.rs index 6618bd39..ff642928 100644 --- a/crates/vm2/src/single_instruction_test/stack.rs +++ b/crates/vm2/src/single_instruction_test/stack.rs @@ -1,9 +1,9 @@ use primitive_types::U256; +use zksync_vm2_interface::HeapId; use super::{ mock_array::MockRead, validation::is_valid_tagged_value, vm::arbitrary_register_value, }; -use crate::HeapId; #[derive(PartialEq, Debug, Clone)] pub struct Stack { @@ -13,6 +13,7 @@ pub struct Stack { pointer_tag_written: bool, } +#[allow(clippy::unused_self)] // to align signatures with real implementation impl Stack { pub(crate) fn new_arbitrary( u: &mut arbitrary::Unstructured, diff --git a/crates/vm2/src/single_instruction_test/state_to_zk_evm.rs b/crates/vm2/src/single_instruction_test/state_to_zk_evm.rs index c0b9409e..8db4d6c2 100644 --- a/crates/vm2/src/single_instruction_test/state_to_zk_evm.rs +++ b/crates/vm2/src/single_instruction_test/state_to_zk_evm.rs @@ -6,7 +6,6 @@ use zk_evm::{ vm_state::{execution_stack::CallStackEntry, Callstack, PrimitiveValue, VmLocalState}, }; use zkevm_opcode_defs::decoding::EncodingModeProduction; -use zksync_vm2_interface::Tracer; use crate::{ callframe::{Callframe, NearCallFrame}, @@ -14,7 +13,7 @@ use crate::{ Instruction, }; -pub(crate) fn vm2_state_to_zk_evm_state( +pub(crate) fn vm2_state_to_zk_evm_state( state: &State, panic: &Instruction, ) -> VmLocalState<8, EncodingModeProduction> { diff --git a/crates/vm2/src/single_instruction_test/universal_state.rs b/crates/vm2/src/single_instruction_test/universal_state.rs index 4cd5e898..3f8f3b5f 100644 --- a/crates/vm2/src/single_instruction_test/universal_state.rs +++ b/crates/vm2/src/single_instruction_test/universal_state.rs @@ -18,7 +18,7 @@ pub struct UniversalVmState { } #[derive(PartialEq, Debug)] -pub struct UniversalVmFrame { +pub(crate) struct UniversalVmFrame { address: H160, caller: H160, code_address: H160, diff --git a/crates/vm2/src/single_instruction_test/validation.rs b/crates/vm2/src/single_instruction_test/validation.rs index 8ed4d411..50f719de 100644 --- a/crates/vm2/src/single_instruction_test/validation.rs +++ b/crates/vm2/src/single_instruction_test/validation.rs @@ -14,10 +14,10 @@ pub(crate) fn is_valid_tagged_value((value, is_pointer): (U256, bool)) -> bool { impl State { pub(crate) fn is_valid(&self) -> bool { self.current_frame.is_valid() - && self.previous_frames.iter().all(|frame| frame.is_valid()) - && (0..16).all(|i| { + && self.previous_frames.iter().all(Callframe::is_valid) + && (0_u16..16).all(|i| { is_valid_tagged_value(( - self.registers[i as usize], + self.registers[usize::from(i)], self.register_pointer_flags & (1 << i) != 0, )) }) diff --git a/crates/vm2/src/single_instruction_test/vm.rs b/crates/vm2/src/single_instruction_test/vm.rs index b3435d4c..9a67c744 100644 --- a/crates/vm2/src/single_instruction_test/vm.rs +++ b/crates/vm2/src/single_instruction_test/vm.rs @@ -1,11 +1,11 @@ use arbitrary::Arbitrary; use primitive_types::U256; -use zksync_vm2_interface::Tracer; +use zksync_vm2_interface::{HeapId, Tracer}; use super::{heap::Heaps, stack::StackPool}; use crate::{ addressing_modes::Arguments, callframe::Callframe, fat_pointer::FatPointer, state::State, - HeapId, Instruction, ModeRequirements, Predicate, Settings, VirtualMachine, World, + Instruction, ModeRequirements, Predicate, Settings, VirtualMachine, World, WorldDiff, }; impl VirtualMachine { @@ -58,7 +58,7 @@ impl<'a, T: Tracer, W: World> Arbitrary<'a> for VirtualMachine { current_frame.heap.as_u32() - 2, )?; *register = value; - register_pointer_flags |= (is_pointer as u16) << i; + register_pointer_flags |= u16::from(is_pointer) << i; } let heaps = Heaps::from_id(current_frame.heap, u)?; @@ -77,19 +77,20 @@ impl<'a, T: Tracer, W: World> Arbitrary<'a> for VirtualMachine { context_u128: u.arbitrary()?, }, settings: u.arbitrary()?, - world_diff: Default::default(), + world_diff: WorldDiff::default(), stack_pool: StackPool {}, panic: Box::new(Instruction::from_panic( None, Arguments::new(Predicate::Always, 5, ModeRequirements::none()), )), + snapshot: None, }) } } /// Generates a pointer or non-pointer value. /// The pointers always point to the calldata heap or a heap larger than the base page. -/// This is because heap < base_page in zk_evm means the same as heap == calldata_heap in vm2. +/// This is because `heap < base_page` in `zk_evm` means the same as `heap == calldata_heap` in vm2. pub(crate) fn arbitrary_register_value( u: &mut arbitrary::Unstructured, calldata_heap: HeapId, diff --git a/crates/vm2/src/stack.rs b/crates/vm2/src/stack.rs index 9ae5d909..847056e3 100644 --- a/crates/vm2/src/stack.rs +++ b/crates/vm2/src/stack.rs @@ -19,8 +19,9 @@ const NUMBER_OF_DIRTY_AREAS: usize = 64; const DIRTY_AREA_SIZE: usize = (1 << 16) / NUMBER_OF_DIRTY_AREAS; impl Stack { + #[allow(clippy::cast_ptr_alignment)] // aligned per `Stack` layout pub(crate) fn new() -> Box { - unsafe { Box::from_raw(alloc_zeroed(Layout::new::()).cast::()) } + unsafe { Box::from_raw(alloc_zeroed(Layout::new::()).cast()) } } #[inline(always)] @@ -39,14 +40,14 @@ impl Stack { fn zero(&mut self) { for i in 0..NUMBER_OF_DIRTY_AREAS { if self.dirty_areas & (1 << i) != 0 { - for slot in self.slots[i * DIRTY_AREA_SIZE..(i + 1) * DIRTY_AREA_SIZE].iter_mut() { + for slot in &mut self.slots[i * DIRTY_AREA_SIZE..(i + 1) * DIRTY_AREA_SIZE] { *slot = U256::zero(); } } } self.dirty_areas = 0; - self.pointer_flags = Default::default(); + self.pointer_flags = Bitset::default(); } #[inline(always)] @@ -111,17 +112,14 @@ pub(crate) struct StackPool { } impl StackPool { - pub fn get(&mut self) -> Box { - self.stacks - .pop() - .map(|mut s| { - s.zero(); - s - }) - .unwrap_or_else(Stack::new) + pub(crate) fn get(&mut self) -> Box { + self.stacks.pop().map_or_else(Stack::new, |mut s| { + s.zero(); + s + }) } - pub fn recycle(&mut self, stack: Box) { + pub(crate) fn recycle(&mut self, stack: Box) { self.stacks.push(stack); } } diff --git a/crates/vm2/src/state.rs b/crates/vm2/src/state.rs index 8d61067f..5b8d7edd 100644 --- a/crates/vm2/src/state.rs +++ b/crates/vm2/src/state.rs @@ -42,7 +42,7 @@ impl State { memory_page: HeapId::FIRST_CALLDATA, offset: 0, start: 0, - length: calldata.len() as u32, + length: u32::try_from(calldata.len()).expect("calldata length overflow"), } .into_u256(); @@ -92,7 +92,7 @@ impl State { + self .previous_frames .iter() - .map(|frame| frame.contained_gas()) + .map(Callframe::contained_gas) .sum::() } @@ -177,6 +177,7 @@ impl Addressable for State { fn registers(&mut self) -> &mut [U256; 16] { &mut self.registers } + fn register_pointer_flags(&mut self) -> &mut u16 { &mut self.register_pointer_flags } @@ -184,9 +185,11 @@ impl Addressable for State { fn read_stack(&mut self, slot: u16) -> U256 { self.current_frame.stack.get(slot) } + fn write_stack(&mut self, slot: u16, value: U256) { - self.current_frame.stack.set(slot, value) + self.current_frame.stack.set(slot, value); } + fn stack_pointer(&mut self) -> &mut u16 { &mut self.current_frame.sp } @@ -194,11 +197,13 @@ impl Addressable for State { fn read_stack_pointer_flag(&mut self, slot: u16) -> bool { self.current_frame.stack.get_pointer_flag(slot) } + fn set_stack_pointer_flag(&mut self, slot: u16) { - self.current_frame.stack.set_pointer_flag(slot) + self.current_frame.stack.set_pointer_flag(slot); } + fn clear_stack_pointer_flag(&mut self, slot: u16) { - self.current_frame.stack.clear_pointer_flag(slot) + self.current_frame.stack.clear_pointer_flag(slot); } fn code_page(&self) -> &[U256] { diff --git a/crates/vm2/src/testworld.rs b/crates/vm2/src/testonly.rs similarity index 63% rename from crates/vm2/src/testworld.rs rename to crates/vm2/src/testonly.rs index 0bb0e3c4..88a932b1 100644 --- a/crates/vm2/src/testworld.rs +++ b/crates/vm2/src/testonly.rs @@ -1,3 +1,5 @@ +//! Test-only tools for EraVM. + use std::{ collections::{hash_map::DefaultHasher, BTreeMap}, hash::{Hash, Hasher}, @@ -9,15 +11,21 @@ use zkevm_opcode_defs::{ }; use zksync_vm2_interface::Tracer; -use crate::{address_into_u256, Program, StorageInterface, World}; +use crate::{instruction_handlers::address_into_u256, Program, StorageInterface, World}; +/// Test [`World`] implementation. #[derive(Debug)] pub struct TestWorld { - pub address_to_hash: BTreeMap, - pub hash_to_contract: BTreeMap>, + pub(crate) address_to_hash: BTreeMap, + pub(crate) hash_to_contract: BTreeMap>, } -impl TestWorld { +impl TestWorld { + /// Creates a test world with the provided programs. + /// + /// # Panics + /// + /// Panics if the provided `Program`s are malformed. pub fn new(contracts: &[(Address, Program)]) -> Self { let mut address_to_hash = BTreeMap::new(); let mut hash_to_contract = BTreeMap::new(); @@ -29,7 +37,9 @@ impl TestWorld { let mut code_info_bytes = [0; 32]; code_info_bytes[24..].copy_from_slice(&hasher.finish().to_be_bytes()); - code_info_bytes[2..=3].copy_from_slice(&(code.code_page().len() as u16).to_be_bytes()); + let code_len = u16::try_from(code.code_page().len()) + .expect("code length must not exceed u16::MAX"); + code_info_bytes[2..=3].copy_from_slice(&code_len.to_be_bytes()); code_info_bytes[0] = 1; let hash = U256::from_big_endian(&code_info_bytes); @@ -68,7 +78,7 @@ impl World for TestWorld { impl StorageInterface for TestWorld { fn read_storage(&mut self, contract: H160, key: U256) -> Option { let deployer_system_contract_address = - Address::from_low_u64_be(DEPLOYER_SYSTEM_CONTRACT_ADDRESS_LOW as u64); + Address::from_low_u64_be(DEPLOYER_SYSTEM_CONTRACT_ADDRESS_LOW.into()); if contract == deployer_system_contract_address { Some( @@ -90,3 +100,23 @@ impl StorageInterface for TestWorld { false } } + +/// May be used to load code when the VM first starts up. +/// Doesn't check for any errors. +/// Doesn't cost anything but also doesn't make the code free in future decommits. +#[doc(hidden)] // should be used only in low-level testing / benches +pub fn initial_decommit>(world: &mut W, address: H160) -> Program { + let deployer_system_contract_address = + Address::from_low_u64_be(DEPLOYER_SYSTEM_CONTRACT_ADDRESS_LOW.into()); + let code_info = world + .read_storage(deployer_system_contract_address, address_into_u256(address)) + .unwrap_or_default(); + + let mut code_info_bytes = [0; 32]; + code_info.to_big_endian(&mut code_info_bytes); + + code_info_bytes[1] = 0; + let code_key: U256 = U256::from_big_endian(&code_info_bytes); + + world.decommit(code_key) +} diff --git a/crates/vm2/src/tests/bytecode_behaviour.rs b/crates/vm2/src/tests/bytecode_behaviour.rs new file mode 100644 index 00000000..decc1e3e --- /dev/null +++ b/crates/vm2/src/tests/bytecode_behaviour.rs @@ -0,0 +1,37 @@ +use zkevm_opcode_defs::ethereum_types::Address; +use zksync_vm2_interface::{CallframeInterface, StateInterface}; + +use crate::{ + testonly::{initial_decommit, TestWorld}, + ExecutionEnd, Program, Settings, VirtualMachine, +}; + +#[test] +fn call_to_invalid_address() { + // A far call should make a new frame, even if the address is invalid. + // Thus, setting the error handler to the call instruction itself should + // result in an infinite loop. + + let address = Address::from_low_u64_be(0x_1234_5678_90ab_cdef); + let bytecode = include_bytes!("bytecodes/call_far"); + let mut world = TestWorld::new(&[(address, Program::new(bytecode, false))]); + let program = initial_decommit(&mut world, address); + + let mut vm = VirtualMachine::new( + address, + program, + Address::zero(), + &[], + 10000, + Settings { + default_aa_code_hash: [0; 32], + evm_interpreter_code_hash: [0; 32], + hook_address: 0, + }, + ); + assert!(matches!( + vm.run(&mut world, &mut ()), + ExecutionEnd::Panicked + )); + assert_eq!(vm.current_frame().gas(), 0); +} diff --git a/crates/vm2/tests/bytecodes/call_far b/crates/vm2/src/tests/bytecodes/call_far similarity index 100% rename from crates/vm2/tests/bytecodes/call_far rename to crates/vm2/src/tests/bytecodes/call_far diff --git a/crates/vm2/tests/far_call_decommitment.rs b/crates/vm2/src/tests/far_call_decommitment.rs similarity index 93% rename from crates/vm2/tests/far_call_decommitment.rs rename to crates/vm2/src/tests/far_call_decommitment.rs index 4b749b9d..2e8b506f 100644 --- a/crates/vm2/tests/far_call_decommitment.rs +++ b/crates/vm2/src/tests/far_call_decommitment.rs @@ -1,18 +1,16 @@ -#![cfg(not(feature = "single_instruction_test"))] - use std::collections::HashSet; use primitive_types::{H160, U256}; use zkevm_opcode_defs::ethereum_types::Address; -use zksync_vm2::{ +use zksync_vm2_interface::{opcodes, CallframeInterface, StateInterface}; + +use crate::{ addressing_modes::{ Arguments, CodePage, Immediate1, Register, Register1, Register2, RegisterAndImmediate, }, - initial_decommit, - testworld::TestWorld, + testonly::{initial_decommit, TestWorld}, ExecutionEnd, Instruction, ModeRequirements, Predicate, Program, Settings, VirtualMachine, }; -use zksync_vm2_interface::{opcodes, CallframeInterface, StateInterface}; const GAS_TO_PASS: u32 = 10_000; const LARGE_BYTECODE_LEN: usize = 10_000; @@ -29,7 +27,7 @@ fn create_test_world() -> TestWorld<()> { let mut abi = U256::zero(); abi.0[3] = GAS_TO_PASS.into(); - let main_program = Program::new( + let main_program = Program::from_raw( vec![ // 0..=2: Prepare and execute far call Instruction::from_add( @@ -65,7 +63,7 @@ fn create_test_world() -> TestWorld<()> { Arguments::new(Predicate::Always, 200, ModeRequirements::none()), ), // 3: Hook (0) - Instruction::from_heap_store( + Instruction::from_heap_write( Register1(r0).into(), Register2(r0), None, @@ -88,7 +86,7 @@ fn create_test_world() -> TestWorld<()> { vec![abi, CALLED_ADDRESS.to_low_u64_be().into()], ); - let called_program = Program::new( + let called_program = Program::from_raw( vec![ Instruction::from_add( Register1(r0).into(), @@ -122,7 +120,7 @@ fn test() { MAIN_ADDRESS, main_program, Address::zero(), - vec![], + &[], initial_gas, Settings { default_aa_code_hash: [0; 32], @@ -134,7 +132,7 @@ fn test() { let result = vm.run(&mut world, &mut ()); let remaining_gas = vm.current_frame().gas(); assert_eq!(result, ExecutionEnd::SuspendedOnHook(0)); - let expected_decommit_cost = LARGE_BYTECODE_LEN as u32 * 4; + let expected_decommit_cost = u32::try_from(LARGE_BYTECODE_LEN).unwrap() * 4; assert!( remaining_gas < initial_gas - expected_decommit_cost, "{remaining_gas}" @@ -158,7 +156,7 @@ fn test_with_initial_out_of_gas_error() { MAIN_ADDRESS, main_program, Address::zero(), - vec![], + &[], 10_000, Settings { default_aa_code_hash: [0; 32], @@ -185,7 +183,7 @@ fn test_with_initial_out_of_gas_error() { let result = vm.run(&mut world, &mut ()); let remaining_gas = vm.current_frame().gas(); assert_eq!(result, ExecutionEnd::SuspendedOnHook(0)); - let expected_decommit_cost = LARGE_BYTECODE_LEN as u32 * 4; + let expected_decommit_cost = u32::try_from(LARGE_BYTECODE_LEN).unwrap() * 4; assert!( remaining_gas < initial_gas - expected_decommit_cost, "{remaining_gas}" diff --git a/crates/vm2/src/tests/mod.rs b/crates/vm2/src/tests/mod.rs new file mode 100644 index 00000000..b11f2893 --- /dev/null +++ b/crates/vm2/src/tests/mod.rs @@ -0,0 +1,6 @@ +//! Low-level VM tests. + +mod bytecode_behaviour; +mod far_call_decommitment; +mod panic; +mod stipend; diff --git a/crates/vm2/tests/panic.proptest-regressions b/crates/vm2/src/tests/panic.proptest-regressions similarity index 100% rename from crates/vm2/tests/panic.proptest-regressions rename to crates/vm2/src/tests/panic.proptest-regressions diff --git a/crates/vm2/tests/panic.rs b/crates/vm2/src/tests/panic.rs similarity index 86% rename from crates/vm2/tests/panic.rs rename to crates/vm2/src/tests/panic.rs index 272ac366..b7a4cfdd 100644 --- a/crates/vm2/tests/panic.rs +++ b/crates/vm2/src/tests/panic.rs @@ -1,11 +1,9 @@ -#![cfg(not(feature = "single_instruction_test"))] - use proptest::prelude::*; use zkevm_opcode_defs::ethereum_types::Address; -use zksync_vm2::{ + +use crate::{ addressing_modes::{Arguments, Immediate1, Immediate2, Register, Register1}, - initial_decommit, - testworld::TestWorld, + testonly::{initial_decommit, TestWorld}, ExecutionEnd, Instruction, ModeRequirements, Predicate, Program, Settings, VirtualMachine, }; @@ -29,9 +27,9 @@ proptest! { )); } - let program = Program::new(instructions, vec![]); + let program = Program::from_raw(instructions, vec![]); - let address = Address::from_low_u64_be(0x1234567890abcdef); + let address = Address::from_low_u64_be(0x_1234_5678_90ab_cdef); let mut world = TestWorld::new(&[(address, program)]); let program = initial_decommit(&mut world, address); @@ -39,7 +37,7 @@ proptest! { address, program, Address::zero(), - vec![], + &[], 1000, Settings { default_aa_code_hash: [0; 32], diff --git a/crates/vm2/tests/stipend.rs b/crates/vm2/src/tests/stipend.rs similarity index 90% rename from crates/vm2/tests/stipend.rs rename to crates/vm2/src/tests/stipend.rs index 3f9e133a..d76b1fbb 100644 --- a/crates/vm2/tests/stipend.rs +++ b/crates/vm2/src/tests/stipend.rs @@ -1,17 +1,15 @@ -#![cfg(not(feature = "single_instruction_test"))] - use primitive_types::U256; use zkevm_opcode_defs::ethereum_types::Address; -use zksync_vm2::{ - address_into_u256, +use zksync_vm2_interface::{opcodes, CallframeInterface, StateInterface}; + +use crate::{ addressing_modes::{ Arguments, CodePage, Immediate1, Register, Register1, Register2, RegisterAndImmediate, }, - initial_decommit, - testworld::TestWorld, + instruction_handlers::address_into_u256, + testonly::{initial_decommit, TestWorld}, ExecutionEnd, Instruction, ModeRequirements, Predicate, Program, Settings, VirtualMachine, }; -use zksync_vm2_interface::{opcodes, CallframeInterface, StateInterface}; const INITIAL_GAS: u32 = 1000; @@ -20,11 +18,11 @@ fn test_scenario(gas_to_pass: u32) -> (ExecutionEnd, u32) { let r1 = Register::new(1); let r2 = Register::new(2); - let ethereum_address = 0xeeeeee; + let ethereum_address = 0x_00ee_eeee; let mut abi = U256::zero(); - abi.0[3] = gas_to_pass as u64; + abi.0[3] = gas_to_pass.into(); - let main_program = Program::new( + let main_program = Program::from_raw( vec![ Instruction::from_add( CodePage(RegisterAndImmediate { @@ -68,7 +66,7 @@ fn test_scenario(gas_to_pass: u32) -> (ExecutionEnd, u32) { vec![abi, ethereum_address.into()], ); - let interpreter = Program::new( + let interpreter = Program::from_raw( vec![ Instruction::from_add( Register1(r0).into(), @@ -87,8 +85,8 @@ fn test_scenario(gas_to_pass: u32) -> (ExecutionEnd, u32) { vec![], ); - let main_address = Address::from_low_u64_be(0xfeddeadbeef); - let interpreter_address = Address::from_low_u64_be(0x1234567890abcdef); + let main_address = Address::from_low_u64_be(0x_0fed_dead_beef); + let interpreter_address = Address::from_low_u64_be(0x_1234_5678_90ab_cdef); let mut world = TestWorld::new(&[ (interpreter_address, interpreter), (main_address, main_program), @@ -107,7 +105,7 @@ fn test_scenario(gas_to_pass: u32) -> (ExecutionEnd, u32) { main_address, program, Address::zero(), - vec![], + &[], INITIAL_GAS, Settings { default_aa_code_hash: [0; 32], diff --git a/crates/vm2/src/tracing.rs b/crates/vm2/src/tracing.rs index ddf454ff..40940f08 100644 --- a/crates/vm2/src/tracing.rs +++ b/crates/vm2/src/tracing.rs @@ -1,7 +1,9 @@ use std::cmp::Ordering; use primitive_types::{H160, U256}; -use zksync_vm2_interface::*; +use zksync_vm2_interface::{ + CallframeInterface, Event, Flags, HeapId, L2ToL1Log, StateInterface, Tracer, +}; use crate::{ callframe::{Callframe, NearCallFrame}, @@ -10,7 +12,7 @@ use crate::{ VirtualMachine, }; -impl StateInterface for VirtualMachine { +impl StateInterface for VirtualMachine { fn read_register(&self, register: u8) -> (U256, bool) { ( self.state.registers[register as usize], @@ -130,28 +132,15 @@ impl StateInterface for VirtualMachine { fn write_transient_storage(&mut self, address: H160, slot: U256, value: U256) { self.world_diff - .write_transient_storage(address, slot, value) + .write_transient_storage(address, slot, value); } fn events(&self) -> impl Iterator { - self.world_diff.events().iter().map(|event| Event { - key: event.key, - value: event.value, - is_first: event.is_first, - shard_id: event.shard_id, - tx_number: event.tx_number, - }) + self.world_diff.events().iter().copied() } fn l2_to_l1_logs(&self) -> impl Iterator { - self.world_diff.l2_to_l1_logs().iter().map(|log| L2ToL1Log { - address: log.address, - key: log.key, - value: log.value, - is_service: log.is_service, - shard_id: log.shard_id, - tx_number: log.tx_number, - }) + self.world_diff.l2_to_l1_logs().iter().copied() } fn pubdata(&self) -> i32 { @@ -168,7 +157,7 @@ struct CallframeWrapper<'a, T, W> { near_call: Option, } -impl CallframeInterface for CallframeWrapper<'_, T, W> { +impl CallframeInterface for CallframeWrapper<'_, T, W> { fn address(&self) -> H160 { self.frame.address } @@ -254,7 +243,7 @@ impl CallframeInterface for CallframeWrapper<'_, T, W> { self.frame.aux_heap_size = value; } - fn read_code_page(&self, slot: u16) -> U256 { + fn read_contract_code(&self, slot: u16) -> U256 { self.frame.program.code_page()[slot as usize] } @@ -296,15 +285,17 @@ impl CallframeInterface for CallframeWrapper<'_, T, W> { } } + // we don't expect the VM to run on 16-bit machines, and sign loss / wrap is checked + #[allow( + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::cast_possible_wrap + )] fn program_counter(&self) -> Option { if let Some(call) = self.near_call_on_top() { Some(call.previous_frame_pc) } else { - let offset = unsafe { - self.frame - .pc - .offset_from(self.frame.program.instruction(0).unwrap()) - }; + let offset = self.frame.get_raw_pc(); if offset < 0 || offset > u16::MAX as isize || self.frame.program.instruction(offset as u16).is_none() @@ -370,16 +361,19 @@ impl CallframeWrapper<'_, T, W> { mod test { use primitive_types::H160; use zkevm_opcode_defs::ethereum_types::Address; - use zksync_vm2_interface::HeapId; + use zksync_vm2_interface::opcodes; use super::*; - use crate::{initial_decommit, testworld::TestWorld, Instruction, Program, VirtualMachine}; + use crate::{ + testonly::{initial_decommit, TestWorld}, + Instruction, Program, VirtualMachine, + }; #[test] fn callframe_picking() { - let program = Program::new(vec![Instruction::from_invalid()], vec![]); + let program = Program::from_raw(vec![Instruction::from_invalid()], vec![]); - let address = Address::from_low_u64_be(0x1234567890abcdef); + let address = Address::from_low_u64_be(0x_1234_5678_90ab_cdef); let mut world = TestWorld::new(&[(address, program)]); let program = initial_decommit(&mut world, address); @@ -387,7 +381,7 @@ mod test { address, program.clone(), Address::zero(), - vec![], + &[], 1000, crate::Settings { default_aa_code_hash: [0; 32], diff --git a/crates/vm2/src/vm.rs b/crates/vm2/src/vm.rs index 1a82e4de..2a8f2d63 100644 --- a/crates/vm2/src/vm.rs +++ b/crates/vm2/src/vm.rs @@ -12,13 +12,15 @@ use crate::{ stack::StackPool, state::{State, StateSnapshot}, world_diff::{ExternalSnapshot, Snapshot, WorldDiff}, - ExecutionEnd, Instruction, ModeRequirements, Predicate, Program, + ExecutionEnd, Instruction, ModeRequirements, Predicate, Program, World, }; /// [`VirtualMachine`] settings. #[derive(Debug, Clone)] pub struct Settings { + /// Bytecode hash of the default account abstraction contract. pub default_aa_code_hash: [u8; 32], + /// Bytecode hash of the EVM interpreter. pub evm_interpreter_code_hash: [u8; 32], /// Writing to this address in the bootloader's heap suspends execution pub hook_address: u32, @@ -34,14 +36,16 @@ pub struct VirtualMachine { /// Instruction that is jumped to when things go wrong while executing another. /// Boxed, so the pointer isn't invalidated by moves. pub(crate) panic: Box>, + pub(crate) snapshot: Option, } -impl VirtualMachine { +impl> VirtualMachine { + /// Creates a new VM instance. pub fn new( address: H160, program: Program, caller: H160, - calldata: Vec, + calldata: &[u8], gas: u32, settings: Settings, ) -> Self { @@ -54,7 +58,7 @@ impl VirtualMachine { state: State::new( address, caller, - &calldata, + calldata, gas, program, world_before_this_frame, @@ -66,22 +70,24 @@ impl VirtualMachine { None, Arguments::new(Predicate::Always, RETURN_COST, ModeRequirements::none()), )), + snapshot: None, } } - /// Provides a reference to the [`World`](crate::World) diff accumulated by VM execution so far. + /// Provides a reference to the [`World`] diff accumulated by VM execution so far. pub fn world_diff(&self) -> &WorldDiff { &self.world_diff } - /// Provides a mutable reference to the [`World`](crate::World) diff accumulated by VM execution so far. + /// Provides a mutable reference to the [`World`] diff accumulated by VM execution so far. /// - /// 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. + /// 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. #[doc(hidden)] pub fn world_diff_mut(&mut self) -> &mut WorldDiff { &mut self.world_diff } + /// Runs this VM with the specified [`World`] and [`Tracer`] until an end of execution due to a hook, or an error. pub fn run(&mut self, world: &mut W, tracer: &mut T) -> ExecutionEnd { unsafe { loop { @@ -128,48 +134,73 @@ impl VirtualMachine { .map(|left| (left, end)) } - /// Returns a compact representation of the VM's current state, - /// including pending side effects like storage changes and emitted events. - /// [`Self::rollback()`] can be used to return the VM to this state. + /// Creates a VM snapshot. The snapshot can then be rolled back to, or discarded. /// /// # Panics /// - /// Calling this function outside the initial callframe is not allowed. - pub fn snapshot(&self) -> VmSnapshot { + /// - Panics if called outside the initial (bootloader) callframe. + /// - Panics if this VM already has a snapshot. + pub fn make_snapshot(&mut self) { + assert!(self.snapshot.is_none(), "VM already has a snapshot"); assert!( self.state.previous_frames.is_empty(), - "Snapshotting is only allowed in the bootloader!" + "Snapshotting is only allowed in the bootloader" ); - VmSnapshot { + + self.snapshot = Some(VmSnapshot { world_snapshot: self.world_diff.external_snapshot(), state_snapshot: self.state.snapshot(), - } + }); } - /// Returns the VM to the state it was in when the snapshot was created. + /// Returns the VM to the state it was in when [`Self::make_snapshot()`] was called. /// /// # Panics /// - /// - Rolling back snapshots in anything but LIFO order may panic. - /// - Rolling back outside the initial callframe will panic. - pub fn rollback(&mut self, snapshot: VmSnapshot) { + /// - Panics if this VM doesn't hold a snapshot. + /// - Panics if called outside the initial (bootloader) callframe. + pub fn rollback(&mut self) { assert!( self.state.previous_frames.is_empty(), - "Rolling back is only allowed in the bootloader!" + "Rolling back is only allowed in the bootloader" ); + + let snapshot = self + .snapshot + .take() + .expect("`rollback()` called without a snapshot"); self.world_diff.external_rollback(snapshot.world_snapshot); self.state.rollback(snapshot.state_snapshot); + self.delete_history(); + } + + /// Pops a [previously made](Self::make_snapshot()) snapshot without rolling back to it. This effectively commits + /// all changes made up to this point, so that they cannot be rolled back. + /// + /// # Panics + /// + /// - Panics if called outside the initial (bootloader) callframe. + pub fn pop_snapshot(&mut self) { + assert!( + self.state.previous_frames.is_empty(), + "Popping a snapshot is only allowed in the bootloader" + ); + self.snapshot = None; + self.delete_history(); } /// This must only be called when it is known that the VM cannot be rolled back, /// so there must not be any external snapshots and the callstack /// should ideally be empty, though in practice it sometimes contains /// a near call inside the bootloader. - pub fn delete_history(&mut self) { + fn delete_history(&mut self) { self.world_diff.delete_history(); self.state.delete_history(); } +} +// Private methods. We don't constrain `T` and `W` to ease potential refactoring. +impl VirtualMachine { #[allow(clippy::too_many_arguments)] pub(crate) fn push_frame( &mut self, @@ -254,7 +285,7 @@ impl VirtualMachine { pub(crate) fn start_new_tx(&mut self) { self.state.transaction_number = self.state.transaction_number.wrapping_add(1); - self.world_diff.clear_transient_storage() + self.world_diff.clear_transient_storage(); } } @@ -268,7 +299,7 @@ impl VirtualMachine { /// Snapshot of a [`VirtualMachine`]. #[derive(Debug)] -pub struct VmSnapshot { +pub(crate) struct VmSnapshot { world_snapshot: ExternalSnapshot, state_snapshot: StateSnapshot, } diff --git a/crates/vm2/src/world_diff.rs b/crates/vm2/src/world_diff.rs index 8973d475..948def92 100644 --- a/crates/vm2/src/world_diff.rs +++ b/crates/vm2/src/world_diff.rs @@ -5,7 +5,7 @@ use zkevm_opcode_defs::system_params::{ STORAGE_ACCESS_COLD_READ_COST, STORAGE_ACCESS_COLD_WRITE_COST, STORAGE_ACCESS_WARM_READ_COST, STORAGE_ACCESS_WARM_WRITE_COST, }; -use zksync_vm2_interface::{CycleStats, Tracer}; +use zksync_vm2_interface::{CycleStats, Event, L2ToL1Log, Tracer}; use crate::{ rollback::{Rollback, RollbackableLog, RollbackableMap, RollbackablePod, RollbackableSet}, @@ -39,7 +39,7 @@ pub struct WorldDiff { } #[derive(Debug)] -pub struct ExternalSnapshot { +pub(crate) struct ExternalSnapshot { internal_snapshot: Snapshot, pub(crate) decommitted_hashes: as Rollback>::Snapshot, read_storage_slots: as Rollback>::Snapshot, @@ -48,27 +48,6 @@ pub struct ExternalSnapshot { pubdata_costs: as Rollback>::Snapshot, } -/// There is no address field because nobody is interested in events that don't come -/// from the event writer, so we simply do not record events coming frome anywhere else. -#[derive(Clone, PartialEq, Debug)] -pub struct Event { - pub key: U256, - pub value: U256, - pub is_first: bool, - pub shard_id: u8, - pub tx_number: u16, -} - -#[derive(Debug)] -pub struct L2ToL1Log { - pub key: U256, - pub value: U256, - pub is_service: bool, - pub address: H160, - pub shard_id: u8, - pub tx_number: u16, -} - impl WorldDiff { /// Returns the storage slot's value and a refund based on its hot/cold status. pub(crate) fn read_storage( @@ -157,41 +136,48 @@ impl WorldDiff { .insert((contract, key), update_cost) .unwrap_or(0); - let refund = if !self.written_storage_slots.add((contract, key)) { - WARM_WRITE_REFUND - } else { + let refund = if self.written_storage_slots.add((contract, key)) { tracer.on_extra_prover_cycles(CycleStats::StorageWrite); - if !self.read_storage_slots.add((contract, key)) { - COLD_WRITE_AFTER_WARM_READ_REFUND - } else { + if self.read_storage_slots.add((contract, key)) { 0 + } else { + COLD_WRITE_AFTER_WARM_READ_REFUND } + } else { + WARM_WRITE_REFUND }; - let pubdata_cost = (update_cost as i32) - (prepaid as i32); - self.pubdata.0 += pubdata_cost; - self.storage_refunds.push(refund); - self.pubdata_costs.push(pubdata_cost); + #[allow(clippy::cast_possible_wrap)] + { + let pubdata_cost = (update_cost as i32) - (prepaid as i32); + self.pubdata.0 += pubdata_cost; + self.storage_refunds.push(refund); + self.pubdata_costs.push(pubdata_cost); + } refund } - pub fn pubdata(&self) -> i32 { + pub(crate) fn pubdata(&self) -> i32 { self.pubdata.0 } + /// Returns recorded refunds for all storage operations. pub fn storage_refunds(&self) -> &[u32] { self.storage_refunds.as_ref() } + /// Returns recorded pubdata costs for all storage operations. pub fn pubdata_costs(&self) -> &[i32] { self.pubdata_costs.as_ref() } + #[doc(hidden)] // duplicates `StateInterface::get_storage_state()`, but we use random access in some places pub fn get_storage_state(&self) -> &BTreeMap<(H160, U256), U256> { self.storage_changes.as_ref() } + /// Gets changes for all touched storage slots. pub fn get_storage_changes( &self, ) -> impl Iterator, U256))> + '_ { @@ -207,6 +193,7 @@ impl WorldDiff { }) } + /// Gets changes for storage slots touched after the specified `snapshot` was created. pub fn get_storage_changes_after( &self, snapshot: &Snapshot, @@ -242,7 +229,7 @@ impl WorldDiff { .insert((contract, key), value); } - pub fn get_transient_storage_state(&self) -> &BTreeMap<(H160, U256), U256> { + pub(crate) fn get_transient_storage_state(&self) -> &BTreeMap<(H160, U256), U256> { self.transient_storage_changes.as_ref() } @@ -250,10 +237,11 @@ impl WorldDiff { self.events.push(event); } - pub fn events(&self) -> &[Event] { + pub(crate) fn events(&self) -> &[Event] { self.events.as_ref() } + /// Returns events emitted after the specified `snapshot` was created. pub fn events_after(&self, snapshot: &Snapshot) -> &[Event] { self.events.logs_after(snapshot.events) } @@ -262,10 +250,11 @@ impl WorldDiff { self.l2_to_l1_logs.push(log); } - pub fn l2_to_l1_logs(&self) -> &[L2ToL1Log] { + pub(crate) fn l2_to_l1_logs(&self) -> &[L2ToL1Log] { self.l2_to_l1_logs.as_ref() } + /// Returns L2-to-L1 logs emitted after the specified `snapshot` was created. pub fn l2_to_l1_logs_after(&self, snapshot: &Snapshot) -> &[L2ToL1Log] { self.l2_to_l1_logs.logs_after(snapshot.l2_to_l1_logs) } @@ -276,9 +265,7 @@ impl WorldDiff { self.decommitted_hashes.as_ref().keys().copied() } - /// Get a snapshot for selecting which logs [Self::events_after] & Co output. - /// The snapshot can't be used for rolling back the VM because the method for - /// that is private. Use [crate::VirtualMachine::snapshot] for that instead. + /// Get a snapshot for selecting which logs & co. to output using [`Self::events_after()`] and other methods. pub fn snapshot(&self) -> Snapshot { Snapshot { storage_changes: self.storage_changes.snapshot(), @@ -290,6 +277,7 @@ impl WorldDiff { } } + #[allow(clippy::needless_pass_by_value)] // intentional: we require a snapshot to be rolled back to no more than once pub(crate) fn rollback(&mut self, snapshot: Snapshot) { self.storage_changes.rollback(snapshot.storage_changes); self.paid_changes.rollback(snapshot.paid_changes); @@ -347,10 +335,12 @@ impl WorldDiff { } pub(crate) fn clear_transient_storage(&mut self) { - self.transient_storage_changes = Default::default(); + self.transient_storage_changes = RollbackableMap::default(); } } +/// Opaque snapshot of a [`WorldDiff`] output by its [eponymous method](WorldDiff::snapshot()). +/// Can be provided to [`WorldDiff::events_after()`] etc. to get data after the snapshot was created. #[derive(Clone, PartialEq, Debug)] pub struct Snapshot { storage_changes: as Rollback>::Snapshot, @@ -361,11 +351,14 @@ pub struct Snapshot { pubdata: as Rollback>::Snapshot, } +/// Change in a single storage slot. #[derive(Debug, PartialEq)] pub struct StorageChange { + /// Value before the slot was written to. `None` if the slot was not written to previously. pub before: Option, + /// Value written to the slot. pub after: U256, - /// `true` if the slot is not set in the World. + /// `true` if the slot is not set in the [`World`](crate::World). /// A write may be initial even if it isn't the first write to a slot! pub is_initial: bool, } @@ -447,10 +440,10 @@ mod tests { .collect::>(); for (key, value) in second_changes { let initial = initial_values.get(&key).copied(); - if initial.unwrap_or_default() != value { - combined.insert(key, (initial, value)); - } else { + if initial.unwrap_or_default() == value { combined.remove(&key); + } else { + combined.insert(key, (initial, value)); } } diff --git a/crates/vm2/tests/bytecode_behaviour.rs b/crates/vm2/tests/bytecode_behaviour.rs deleted file mode 100644 index a924b83c..00000000 --- a/crates/vm2/tests/bytecode_behaviour.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![cfg(not(feature = "single_instruction_test"))] - -use primitive_types::U256; -use zkevm_opcode_defs::ethereum_types::Address; -use zksync_vm2::{ - decode::decode_program, initial_decommit, testworld::TestWorld, ExecutionEnd, Program, - Settings, VirtualMachine, World, -}; -use zksync_vm2_interface::{CallframeInterface, StateInterface, Tracer}; - -fn program_from_file>(filename: &str) -> Program { - let blob = std::fs::read(filename).unwrap(); - Program::new( - decode_program( - &blob - .chunks_exact(8) - .map(|chunk| u64::from_be_bytes(chunk.try_into().unwrap())) - .collect::>(), - false, - ), - blob.chunks_exact(32) - .map(U256::from_big_endian) - .collect::>(), - ) -} - -#[test] -fn call_to_invalid_address() { - // A far call should make a new frame, even if the address is invalid. - // Thus, setting the error handler to the call instruction itself should - // result in an infinite loop. - - let address = Address::from_low_u64_be(0x1234567890abcdef); - let mut world = TestWorld::new(&[(address, program_from_file("tests/bytecodes/call_far"))]); - let program = initial_decommit(&mut world, address); - - let mut vm = VirtualMachine::new( - address, - program, - Address::zero(), - vec![], - 10000, - Settings { - default_aa_code_hash: [0; 32], - evm_interpreter_code_hash: [0; 32], - hook_address: 0, - }, - ); - assert!(matches!( - vm.run(&mut world, &mut ()), - ExecutionEnd::Panicked - )); - assert_eq!(vm.current_frame().gas(), 0); -}