From 7bb5ed377719227f5c9861231e110dd9a5bb2ac0 Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Mon, 3 Feb 2025 14:15:22 +0200 Subject: [PATCH] feat(vm): Allow caching signature verification (#3505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ Allows caching signature verification (more precisely, `ecrecover` output for L2 transactions using the default AA) in the fast VM. ## Why ❔ Signature verification takes ~50% of the execution time for "simple" transactions (e.g., base token and ERC-20 token transfers), so caching it may improve performance. ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [x] Tests for the changes have been added / updated. - [x] Documentation comments have been added / updated. - [x] Code has been formatted via `zkstack dev fmt` and `zkstack dev lint`. --- core/Cargo.lock | 4 +- core/Cargo.toml | 2 +- core/lib/multivm/src/versions/vm_fast/mod.rs | 1 + .../src/versions/vm_fast/tests/precompiles.rs | 45 +++- core/lib/multivm/src/versions/vm_fast/vm.rs | 201 +++------------- .../lib/multivm/src/versions/vm_fast/world.rs | 219 ++++++++++++++++++ .../src/versions/vm_latest/bootloader/mod.rs | 2 +- .../versions/vm_latest/bootloader/state.rs | 11 +- .../src/versions/vm_latest/bootloader/tx.rs | 36 ++- .../versions/vm_latest/implementation/tx.rs | 2 +- .../vm_latest/types/transaction_data.rs | 31 ++- core/lib/multivm/src/vm_instance.rs | 11 + core/lib/types/src/transaction_request.rs | 11 +- core/lib/vm_executor/src/batch/factory.rs | 19 ++ core/lib/vm_interface/src/utils/shadow.rs | 5 + core/tests/vm-benchmark/benches/batch.rs | 4 +- .../vm-benchmark/benches/instructions.rs | 4 +- core/tests/vm-benchmark/benches/oneshot.rs | 5 +- core/tests/vm-benchmark/src/lib.rs | 5 +- core/tests/vm-benchmark/src/vm.rs | 22 ++ prover/Cargo.lock | 4 +- 21 files changed, 435 insertions(+), 209 deletions(-) create mode 100644 core/lib/multivm/src/versions/vm_fast/world.rs diff --git a/core/Cargo.lock b/core/Cargo.lock index 1983a856f713..cd783f5f3928 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -13087,7 +13087,7 @@ dependencies = [ [[package]] name = "zksync_vm2" version = "0.2.1" -source = "git+https://github.com/matter-labs/vm2.git?rev=457d8a7eea9093af9440662e33e598c13ba41633#457d8a7eea9093af9440662e33e598c13ba41633" +source = "git+https://github.com/matter-labs/vm2.git?rev=3841f5a430288a63c8207853eca11560bf7a5712#3841f5a430288a63c8207853eca11560bf7a5712" dependencies = [ "enum_dispatch", "primitive-types", @@ -13099,7 +13099,7 @@ dependencies = [ [[package]] name = "zksync_vm2_interface" version = "0.2.1" -source = "git+https://github.com/matter-labs/vm2.git?rev=457d8a7eea9093af9440662e33e598c13ba41633#457d8a7eea9093af9440662e33e598c13ba41633" +source = "git+https://github.com/matter-labs/vm2.git?rev=3841f5a430288a63c8207853eca11560bf7a5712#3841f5a430288a63c8207853eca11560bf7a5712" dependencies = [ "primitive-types", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index e636e1079f13..f737007fbc65 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -244,7 +244,7 @@ fflonk = "=0.30.13" bellman = { package = "zksync_bellman", version = "=0.30.13" } # New VM; pinned to a specific commit because of instability -zksync_vm2 = { git = "https://github.com/matter-labs/vm2.git", rev = "457d8a7eea9093af9440662e33e598c13ba41633" } +zksync_vm2 = { git = "https://github.com/matter-labs/vm2.git", rev = "3841f5a430288a63c8207853eca11560bf7a5712" } # Consensus dependencies. zksync_concurrency = "=0.8.0" diff --git a/core/lib/multivm/src/versions/vm_fast/mod.rs b/core/lib/multivm/src/versions/vm_fast/mod.rs index 291961d3312a..dddb94406e8c 100644 --- a/core/lib/multivm/src/versions/vm_fast/mod.rs +++ b/core/lib/multivm/src/versions/vm_fast/mod.rs @@ -15,3 +15,4 @@ mod tracers; mod utils; mod version; mod vm; +mod world; diff --git a/core/lib/multivm/src/versions/vm_fast/tests/precompiles.rs b/core/lib/multivm/src/versions/vm_fast/tests/precompiles.rs index ccf1463979cd..80d73f38c9dc 100644 --- a/core/lib/multivm/src/versions/vm_fast/tests/precompiles.rs +++ b/core/lib/multivm/src/versions/vm_fast/tests/precompiles.rs @@ -1,5 +1,12 @@ +use circuit_sequencer_api::geometry_config::ProtocolGeometry; +use zksync_types::Execute; + use crate::{ - versions::testonly::precompiles::{test_ecrecover, test_keccak, test_sha256}, + interface::{InspectExecutionMode, TxExecutionMode, VmInterface, VmInterfaceExt}, + versions::testonly::{ + precompiles::{test_ecrecover, test_keccak, test_sha256}, + VmTesterBuilder, + }, vm_fast::Vm, }; @@ -17,3 +24,39 @@ fn sha256() { fn ecrecover() { test_ecrecover::>(); } + +#[test] +fn caching_ecrecover_result() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_rich_accounts(1) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .build::>(); + vm.vm.skip_signature_verification(); + + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Some(account.address), + calldata: vec![], + value: 0.into(), + factory_deps: vec![], + }, + None, + ); + vm.vm.push_transaction(tx); + + assert!(vm.vm.world.precompiles.expected_ecrecover_call.is_some()); + assert_eq!(vm.vm.world.precompiles.expected_calls.get(), 0); + + let exec_result = vm.vm.execute(InspectExecutionMode::OneTx); + assert!(!exec_result.result.is_failed(), "{exec_result:#?}"); + assert_eq!(vm.vm.world.precompiles.expected_calls.get(), 1); + + // Cycle stats should still be produced for the cached call + let ecrecover_count = exec_result.statistics.circuit_statistic.ecrecover + * ProtocolGeometry::V1_5_0 + .config() + .cycles_per_ecrecover_circuit as f32; + assert!((ecrecover_count - 1.0).abs() < 1e-4, "{ecrecover_count}"); +} diff --git a/core/lib/multivm/src/versions/vm_fast/vm.rs b/core/lib/multivm/src/versions/vm_fast/vm.rs index 5065b8a7c67d..5c7b5a534f60 100644 --- a/core/lib/multivm/src/versions/vm_fast/vm.rs +++ b/core/lib/multivm/src/versions/vm_fast/vm.rs @@ -3,30 +3,21 @@ use std::{collections::HashMap, fmt, mem, rc::Rc}; use zk_evm_1_5_0::{ aux_structures::LogQuery, zkevm_opcode_defs::system_params::INITIAL_FRAME_FORMAL_EH_LOCATION, }; -use zksync_contracts::SystemContractCode; use zksync_types::{ - bytecode::BytecodeHash, - h256_to_u256, - l1::is_l1_tx_type, - l2_to_l1_log::UserL2ToL1Log, - u256_to_h256, - utils::key_for_eth_balance, - writes::{ - compression::compress_with_best_strategy, StateDiffRecord, BYTES_PER_DERIVED_KEY, - BYTES_PER_ENUMERATION_INDEX, - }, - AccountTreeId, StorageKey, StorageLog, StorageLogKind, StorageLogWithPreviousValue, - Transaction, BOOTLOADER_ADDRESS, H160, H256, KNOWN_CODES_STORAGE_ADDRESS, L1_MESSENGER_ADDRESS, - L2_BASE_TOKEN_ADDRESS, U256, + bytecode::BytecodeHash, h256_to_u256, l1::is_l1_tx_type, l2_to_l1_log::UserL2ToL1Log, + u256_to_h256, writes::StateDiffRecord, AccountTreeId, StorageKey, StorageLog, StorageLogKind, + StorageLogWithPreviousValue, Transaction, BOOTLOADER_ADDRESS, H160, H256, + KNOWN_CODES_STORAGE_ADDRESS, L1_MESSENGER_ADDRESS, U256, }; use zksync_vm2::{ interface::{CallframeInterface, HeapId, StateInterface, Tracer}, - ExecutionEnd, FatPointer, Program, Settings, StorageSlot, VirtualMachine, + ExecutionEnd, FatPointer, Settings, VirtualMachine, }; use super::{ bytecode::compress_bytecodes, - tracers::{DynamicBytecodes, ValidationTracer, WithBuiltinTracers}, + tracers::{ValidationTracer, WithBuiltinTracers}, + world::World, }; use crate::{ glue::GlueInto, @@ -99,6 +90,7 @@ pub struct Vm { pub(super) system_env: SystemEnv, snapshot: Option, vm_version: FastVmVersion, + skip_signature_verification: bool, #[cfg(test)] enforced_state_diffs: Option>, } @@ -171,6 +163,7 @@ impl Vm { batch_env, snapshot: None, vm_version, + skip_signature_verification: false, #[cfg(test)] enforced_state_diffs: None, }; @@ -178,6 +171,10 @@ impl Vm { this } + pub fn skip_signature_verification(&mut self) { + self.skip_signature_verification = true; + } + fn get_hook_params(&self) -> [U256; 3] { (get_vm_hook_params_start_position(self.vm_version.into()) ..get_vm_hook_params_start_position(self.vm_version.into()) + VM_HOOK_PARAMS_COUNT) @@ -281,8 +278,8 @@ impl Vm { }; let trusted_ergs_limit = tx.trusted_ergs_limit(); - - let memory = self.bootloader_state.push_tx( + let tx_origin = tx.from; + let (memory, ecrecover_call) = self.bootloader_state.push_tx( tx, overhead, refund, @@ -290,8 +287,19 @@ impl Vm { trusted_ergs_limit, self.system_env.chain_id, ); - self.write_to_bootloader_heap(memory); + + // The expected `ecrecover` call params *must* be reset on each transaction. + // We only set call params for transactions using the default AA. Other AAs may expect another + // address returned from `ecrecover`, or may not invoke `ecrecover` at all during validation + // (both of which would make caching a security hazard). + let needs_default_aa_check = self.skip_signature_verification && ecrecover_call.is_some(); + self.world.precompiles.expected_ecrecover_call = + if needs_default_aa_check && self.world.has_default_aa(&tx_origin) { + ecrecover_call + } else { + None + }; } #[cfg(test)] @@ -889,163 +897,10 @@ impl fmt::Debug for Vm) -> fmt::Result { f.debug_struct("Vm") .field("bootloader_state", &self.bootloader_state) - .field("storage", &self.world.storage) - .field("program_cache", &self.world.program_cache) + .field("world", &self.world) .field("batch_env", &self.batch_env) .field("system_env", &self.system_env) .field("snapshot", &self.snapshot.as_ref().map(|_| ())) .finish() } } - -#[derive(Debug)] -pub(crate) struct World { - pub(crate) storage: S, - dynamic_bytecodes: DynamicBytecodes, - program_cache: HashMap>, - pub(crate) bytecode_cache: HashMap>, -} - -impl World { - fn new(storage: S, program_cache: HashMap>) -> Self { - Self { - storage, - dynamic_bytecodes: DynamicBytecodes::default(), - program_cache, - bytecode_cache: HashMap::default(), - } - } - - fn convert_system_contract_code( - code: &SystemContractCode, - is_bootloader: bool, - ) -> (U256, Program) { - ( - h256_to_u256(code.hash), - Program::new(&code.code, is_bootloader), - ) - } - - fn decommit_dynamic_bytecodes( - &self, - candidate_hashes: impl Iterator, - ) -> HashMap> { - let bytecodes = candidate_hashes.filter_map(|hash| { - let bytecode = self - .dynamic_bytecodes - .map(h256_to_u256(hash), <[u8]>::to_vec)?; - Some((hash, bytecode)) - }); - bytecodes.collect() - } -} - -impl zksync_vm2::StorageInterface for World { - fn read_storage(&mut self, contract: H160, key: U256) -> StorageSlot { - let key = &StorageKey::new(AccountTreeId::new(contract), u256_to_h256(key)); - let value = U256::from_big_endian(self.storage.read_value(key).as_bytes()); - // `is_write_initial` value can be true even if the slot has previously been written to / has non-zero value! - // This can happen during oneshot execution (i.e., executing a single transaction) since it emulates - // execution starting in the middle of a batch in the general case. Hence, a slot that was first written to in the batch - // must still be considered an initial write by the refund logic. - let is_write_initial = self.storage.is_write_initial(key); - StorageSlot { - value, - is_write_initial, - } - } - - fn read_storage_value(&mut self, contract: H160, key: U256) -> U256 { - let key = &StorageKey::new(AccountTreeId::new(contract), u256_to_h256(key)); - U256::from_big_endian(self.storage.read_value(key).as_bytes()) - } - - fn cost_of_writing_storage(&mut self, slot: StorageSlot, new_value: U256) -> u32 { - if slot.value == new_value { - return 0; - } - - // Since we need to publish the state diffs onchain, for each of the updated storage slot - // we basically need to publish the following pair: `()`. - // For key we use the following optimization: - // - The first time we publish it, we use 32 bytes. - // Then, we remember a 8-byte id for this slot and assign it to it. We call this initial write. - // - The second time we publish it, we will use the 4/5 byte representation of this 8-byte instead of the 32 - // bytes of the entire key. - // For value compression, we use a metadata byte which holds the length of the value and the operation from the - // previous state to the new state, and the compressed value. The maximum for this is 33 bytes. - // Total bytes for initial writes then becomes 65 bytes and repeated writes becomes 38 bytes. - let compressed_value_size = compress_with_best_strategy(slot.value, new_value).len() as u32; - - if slot.is_write_initial { - (BYTES_PER_DERIVED_KEY as u32) + compressed_value_size - } else { - (BYTES_PER_ENUMERATION_INDEX as u32) + compressed_value_size - } - } - - fn is_free_storage_slot(&self, contract: &H160, key: &U256) -> bool { - contract == &zksync_system_constants::SYSTEM_CONTEXT_ADDRESS - || contract == &L2_BASE_TOKEN_ADDRESS - && u256_to_h256(*key) == key_for_eth_balance(&BOOTLOADER_ADDRESS) - } -} - -/// It may look like that an append-only cache for EVM bytecodes / `Program`s can lead to the following scenario: -/// -/// 1. A transaction deploys an EVM bytecode with hash `H`, then reverts. -/// 2. A following transaction in the same VM run queries a bytecode with hash `H` and gets it. -/// -/// This would be incorrect behavior because bytecode deployments must be reverted along with transactions. -/// -/// In reality, this cannot happen because both `decommit()` and `decommit_code()` calls perform storage-based checks -/// before a decommit: -/// -/// - `decommit_code()` is called from the `CodeOracle` system contract, which checks that the decommitted bytecode is known. -/// - `decommit()` is called during far calls, which obtains address -> bytecode hash mapping beforehand. -/// -/// Thus, if storage is reverted correctly, additional EVM bytecodes occupy the cache, but are unreachable. -impl zksync_vm2::World for World { - fn decommit(&mut self, hash: U256) -> Program { - self.program_cache - .entry(hash) - .or_insert_with(|| { - let cached = self - .bytecode_cache - .get(&hash) - .map(|code| Program::new(code, false)) - .or_else(|| { - self.dynamic_bytecodes - .map(hash, |code| Program::new(code, false)) - }); - - if let Some(cached) = cached { - cached - } else { - let code = self - .storage - .load_factory_dep(u256_to_h256(hash)) - .unwrap_or_else(|| { - panic!("VM tried to decommit nonexistent bytecode: {hash:?}"); - }); - let program = Program::new(&code, false); - self.bytecode_cache.insert(hash, code); - program - } - }) - .clone() - } - - fn decommit_code(&mut self, hash: U256) -> Vec { - self.decommit(hash) - .code_page() - .as_ref() - .iter() - .flat_map(|u| { - let mut buffer = [0u8; 32]; - u.to_big_endian(&mut buffer); - buffer - }) - .collect() - } -} diff --git a/core/lib/multivm/src/versions/vm_fast/world.rs b/core/lib/multivm/src/versions/vm_fast/world.rs new file mode 100644 index 000000000000..85cd642391b5 --- /dev/null +++ b/core/lib/multivm/src/versions/vm_fast/world.rs @@ -0,0 +1,219 @@ +use std::collections::HashMap; + +use zk_evm_1_5_0::zkevm_opcode_defs::ECRECOVER_INNER_FUNCTION_PRECOMPILE_ADDRESS; +use zksync_contracts::SystemContractCode; +use zksync_system_constants::{BOOTLOADER_ADDRESS, L2_BASE_TOKEN_ADDRESS}; +use zksync_types::{ + address_to_u256, get_code_key, h256_to_u256, u256_to_h256, + utils::key_for_eth_balance, + writes::{ + compression::compress_with_best_strategy, BYTES_PER_DERIVED_KEY, + BYTES_PER_ENUMERATION_INDEX, + }, + AccountTreeId, Address, StorageKey, H160, H256, U256, +}; +use zksync_vm2::{ + interface::{CycleStats, Tracer}, + precompiles::{LegacyPrecompiles, PrecompileMemoryReader, PrecompileOutput, Precompiles}, + Program, StorageSlot, +}; + +use super::tracers::DynamicBytecodes; +use crate::{interface::storage::ReadStorage, vm_latest::bootloader::EcRecoverCall}; + +#[derive(Debug)] +pub(super) struct World { + pub(super) storage: S, + pub(super) dynamic_bytecodes: DynamicBytecodes, + program_cache: HashMap>, + pub(super) bytecode_cache: HashMap>, + pub(super) precompiles: OptimizedPrecompiles, +} + +impl World { + pub(super) fn new(storage: S, program_cache: HashMap>) -> Self { + Self { + storage, + dynamic_bytecodes: DynamicBytecodes::default(), + program_cache, + bytecode_cache: HashMap::default(), + precompiles: OptimizedPrecompiles::default(), + } + } + + pub(super) fn convert_system_contract_code( + code: &SystemContractCode, + is_bootloader: bool, + ) -> (U256, Program) { + ( + h256_to_u256(code.hash), + Program::new(&code.code, is_bootloader), + ) + } + + pub(super) fn decommit_dynamic_bytecodes( + &self, + candidate_hashes: impl Iterator, + ) -> HashMap> { + let bytecodes = candidate_hashes.filter_map(|hash| { + let bytecode = self + .dynamic_bytecodes + .map(h256_to_u256(hash), <[u8]>::to_vec)?; + Some((hash, bytecode)) + }); + bytecodes.collect() + } + + /// Checks whether the specified `address` uses the default AA. + pub(super) fn has_default_aa(&mut self, address: &Address) -> bool { + // The code storage slot is always read during tx validation / execution anyway. + self.storage.read_value(&get_code_key(address)).is_zero() + } +} + +impl zksync_vm2::StorageInterface for World { + fn read_storage(&mut self, contract: H160, key: U256) -> StorageSlot { + let key = &StorageKey::new(AccountTreeId::new(contract), u256_to_h256(key)); + let value = U256::from_big_endian(self.storage.read_value(key).as_bytes()); + // `is_write_initial` value can be true even if the slot has previously been written to / has non-zero value! + // This can happen during oneshot execution (i.e., executing a single transaction) since it emulates + // execution starting in the middle of a batch in the general case. Hence, a slot that was first written to in the batch + // must still be considered an initial write by the refund logic. + let is_write_initial = self.storage.is_write_initial(key); + StorageSlot { + value, + is_write_initial, + } + } + + fn read_storage_value(&mut self, contract: H160, key: U256) -> U256 { + let key = &StorageKey::new(AccountTreeId::new(contract), u256_to_h256(key)); + U256::from_big_endian(self.storage.read_value(key).as_bytes()) + } + + fn cost_of_writing_storage(&mut self, slot: StorageSlot, new_value: U256) -> u32 { + if slot.value == new_value { + return 0; + } + + // Since we need to publish the state diffs onchain, for each of the updated storage slot + // we basically need to publish the following pair: `()`. + // For key we use the following optimization: + // - The first time we publish it, we use 32 bytes. + // Then, we remember a 8-byte id for this slot and assign it to it. We call this initial write. + // - The second time we publish it, we will use the 4/5 byte representation of this 8-byte instead of the 32 + // bytes of the entire key. + // For value compression, we use a metadata byte which holds the length of the value and the operation from the + // previous state to the new state, and the compressed value. The maximum for this is 33 bytes. + // Total bytes for initial writes then becomes 65 bytes and repeated writes becomes 38 bytes. + let compressed_value_size = compress_with_best_strategy(slot.value, new_value).len() as u32; + + if slot.is_write_initial { + (BYTES_PER_DERIVED_KEY as u32) + compressed_value_size + } else { + (BYTES_PER_ENUMERATION_INDEX as u32) + compressed_value_size + } + } + + fn is_free_storage_slot(&self, contract: &H160, key: &U256) -> bool { + contract == &zksync_system_constants::SYSTEM_CONTEXT_ADDRESS + || contract == &L2_BASE_TOKEN_ADDRESS + && u256_to_h256(*key) == key_for_eth_balance(&BOOTLOADER_ADDRESS) + } +} + +/// It may look like that an append-only cache for EVM bytecodes / `Program`s can lead to the following scenario: +/// +/// 1. A transaction deploys an EVM bytecode with hash `H`, then reverts. +/// 2. A following transaction in the same VM run queries a bytecode with hash `H` and gets it. +/// +/// This would be incorrect behavior because bytecode deployments must be reverted along with transactions. +/// +/// In reality, this cannot happen because both `decommit()` and `decommit_code()` calls perform storage-based checks +/// before a decommit: +/// +/// - `decommit_code()` is called from the `CodeOracle` system contract, which checks that the decommitted bytecode is known. +/// - `decommit()` is called during far calls, which obtains address -> bytecode hash mapping beforehand. +/// +/// Thus, if storage is reverted correctly, additional EVM bytecodes occupy the cache, but are unreachable. +impl zksync_vm2::World for World { + fn decommit(&mut self, hash: U256) -> Program { + self.program_cache + .entry(hash) + .or_insert_with(|| { + let cached = self + .bytecode_cache + .get(&hash) + .map(|code| Program::new(code, false)) + .or_else(|| { + self.dynamic_bytecodes + .map(hash, |code| Program::new(code, false)) + }); + + if let Some(cached) = cached { + cached + } else { + let code = self + .storage + .load_factory_dep(u256_to_h256(hash)) + .unwrap_or_else(|| { + panic!("VM tried to decommit nonexistent bytecode: {hash:?}"); + }); + let program = Program::new(&code, false); + self.bytecode_cache.insert(hash, code); + program + } + }) + .clone() + } + + fn decommit_code(&mut self, hash: U256) -> Vec { + self.decommit(hash) + .code_page() + .as_ref() + .iter() + .flat_map(|u| { + let mut buffer = [0u8; 32]; + u.to_big_endian(&mut buffer); + buffer + }) + .collect() + } + + fn precompiles(&self) -> &impl Precompiles { + &self.precompiles + } +} + +/// Precompiles implementation that may shortcut an `ecrecover` call made during L2 transaction validation. +#[derive(Debug, Default)] +pub(super) struct OptimizedPrecompiles { + pub(super) expected_ecrecover_call: Option, + #[cfg(test)] + pub(super) expected_calls: std::cell::Cell, +} + +impl Precompiles for OptimizedPrecompiles { + fn call_precompile( + &self, + address_low: u16, + memory: PrecompileMemoryReader<'_>, + aux_input: u64, + ) -> PrecompileOutput { + if address_low == ECRECOVER_INNER_FUNCTION_PRECOMPILE_ADDRESS { + if let Some(call) = &self.expected_ecrecover_call { + let memory_input = memory.clone().assume_offset_in_words(); + if call.input.iter().copied().eq(memory_input) { + // Return the predetermined address instead of ECDSA recovery + #[cfg(test)] + self.expected_calls.set(self.expected_calls.get() + 1); + // By convention, the recovered address is left-padded to a 32-byte word and is preceded + // by the success marker. + return PrecompileOutput::from([U256::one(), address_to_u256(&call.output)]) + .with_cycle_stats(CycleStats::EcRecover(1)); + } + } + } + LegacyPrecompiles.call_precompile(address_low, memory, aux_input) + } +} diff --git a/core/lib/multivm/src/versions/vm_latest/bootloader/mod.rs b/core/lib/multivm/src/versions/vm_latest/bootloader/mod.rs index 02f67f322f0c..8f4a75a1ca44 100644 --- a/core/lib/multivm/src/versions/vm_latest/bootloader/mod.rs +++ b/core/lib/multivm/src/versions/vm_latest/bootloader/mod.rs @@ -1,5 +1,5 @@ -pub(crate) use self::snapshot::BootloaderStateSnapshot; pub use self::state::BootloaderState; +pub(crate) use self::{snapshot::BootloaderStateSnapshot, tx::EcRecoverCall}; mod init; mod l2_block; diff --git a/core/lib/multivm/src/versions/vm_latest/bootloader/state.rs b/core/lib/multivm/src/versions/vm_latest/bootloader/state.rs index 0b65d961d3ac..4b3b33fe0557 100644 --- a/core/lib/multivm/src/versions/vm_latest/bootloader/state.rs +++ b/core/lib/multivm/src/versions/vm_latest/bootloader/state.rs @@ -4,7 +4,10 @@ use once_cell::sync::OnceCell; use zksync_types::{vm::VmVersion, L2ChainId, ProtocolVersionId, U256}; use zksync_vm_interface::pubdata::PubdataBuilder; -use super::{tx::BootloaderTx, utils::apply_pubdata_to_memory}; +use super::{ + tx::{BootloaderTx, EcRecoverCall}, + utils::apply_pubdata_to_memory, +}; use crate::{ interface::{ pubdata::PubdataInput, BootloaderMemory, CompressedBytecodeInfo, L2BlockEnv, @@ -119,9 +122,9 @@ impl BootloaderState { compressed_bytecodes: Vec, trusted_ergs_limit: U256, chain_id: L2ChainId, - ) -> BootloaderMemory { + ) -> (BootloaderMemory, Option) { let tx_offset = self.free_tx_offset(); - let bootloader_tx = BootloaderTx::new( + let (bootloader_tx, ecrecover_call) = BootloaderTx::new( tx, predefined_refund, predefined_overhead, @@ -146,7 +149,7 @@ impl BootloaderState { self.compressed_bytecodes_encoding += compressed_bytecode_size; self.free_tx_offset = tx_offset + bootloader_tx.encoded_len(); self.last_mut_l2_block().push_tx(bootloader_tx); - memory + (memory, ecrecover_call) } pub(crate) fn last_l2_block(&self) -> &BootloaderL2Block { diff --git a/core/lib/multivm/src/versions/vm_latest/bootloader/tx.rs b/core/lib/multivm/src/versions/vm_latest/bootloader/tx.rs index 00c7ae43c58f..5ec80fc9105a 100644 --- a/core/lib/multivm/src/versions/vm_latest/bootloader/tx.rs +++ b/core/lib/multivm/src/versions/vm_latest/bootloader/tx.rs @@ -1,7 +1,20 @@ -use zksync_types::{L2ChainId, H256, U256}; +use zksync_types::{Address, L2ChainId, H256, U256}; use crate::{interface::CompressedBytecodeInfo, vm_latest::types::TransactionData}; +#[derive(Debug)] +pub(crate) struct EcRecoverCall { + /// `ecrecover` input obtained by concatenating: + /// + /// - 32-byte signed tx hash + /// - `signature.v` (0 or 1), represented as a big-endian 32-byte word + /// - `signature.r` (32 bytes) + /// - `signature.s` (32 bytes) + pub input: [u8; 128], + /// Expected call output (= transaction initiator address). + pub output: Address, +} + /// Information about tx necessary for execution in bootloader. #[derive(Debug, Clone)] pub(crate) struct BootloaderTx { @@ -29,9 +42,21 @@ impl BootloaderTx { compressed_bytecodes: Vec, offset: usize, chain_id: L2ChainId, - ) -> Self { - let hash = tx.tx_hash(chain_id); - Self { + ) -> (Self, Option) { + let (signed_hash, hash) = tx.signed_and_tx_hashes(chain_id); + let expected_ecrecover_call = signed_hash.and_then(|signed_hash| { + let mut input = [0_u8; 128]; + input[..32].copy_from_slice(signed_hash.as_bytes()); + let (v, r_and_s) = tx.parse_signature()?; + input[63] = v as u8; + input[64..].copy_from_slice(r_and_s); + Some(EcRecoverCall { + input, + output: tx.from, + }) + }); + + let this = Self { hash, encoded: tx.into_tokens(), compressed_bytecodes, @@ -39,7 +64,8 @@ impl BootloaderTx { gas_overhead: predefined_overhead, trusted_gas_limit, offset, - } + }; + (this, expected_ecrecover_call) } pub(super) fn encoded_len(&self) -> usize { diff --git a/core/lib/multivm/src/versions/vm_latest/implementation/tx.rs b/core/lib/multivm/src/versions/vm_latest/implementation/tx.rs index 02adcc3cdad8..6f6a49d50cc4 100644 --- a/core/lib/multivm/src/versions/vm_latest/implementation/tx.rs +++ b/core/lib/multivm/src/versions/vm_latest/implementation/tx.rs @@ -40,7 +40,7 @@ impl Vm { let trusted_ergs_limit = tx.trusted_ergs_limit(); - let memory = self.bootloader_state.push_tx( + let (memory, _) = self.bootloader_state.push_tx( tx, predefined_overhead, predefined_refund, diff --git a/core/lib/multivm/src/versions/vm_latest/types/transaction_data.rs b/core/lib/multivm/src/versions/vm_latest/types/transaction_data.rs index 33f923414eb3..f0236404cbc7 100644 --- a/core/lib/multivm/src/versions/vm_latest/types/transaction_data.rs +++ b/core/lib/multivm/src/versions/vm_latest/types/transaction_data.rs @@ -234,9 +234,9 @@ impl TransactionData { U256::from(TX_MAX_COMPUTE_GAS_LIMIT).min(self.gas_limit) } - pub(crate) fn tx_hash(&self, chain_id: L2ChainId) -> H256 { + pub(crate) fn signed_and_tx_hashes(&self, chain_id: L2ChainId) -> (Option, H256) { if is_l1_tx_type(self.tx_type) { - return self.canonical_l1_tx_hash().unwrap(); + return (None, self.canonical_l1_tx_hash()); } let l2_tx: L2Tx = self.clone().try_into().unwrap(); @@ -244,27 +244,34 @@ impl TransactionData { transaction_request.chain_id = Some(chain_id.as_u64()); // It is assumed that the `TransactionData` always has all the necessary components to recover the hash. - transaction_request - .get_tx_hash() - .expect("Could not recover L2 transaction hash") + let (signed_hash, tx_hash) = transaction_request + .get_signed_and_tx_hashes() + .expect("Could not recover L2 transaction hash"); + (Some(signed_hash), tx_hash) } - fn canonical_l1_tx_hash(&self) -> Result { + fn canonical_l1_tx_hash(&self) -> H256 { use zksync_types::web3::keccak256; - if !is_l1_tx_type(self.tx_type) { - return Err(TxHashCalculationError::CannotCalculateL1HashForL2Tx); - } - let encoded_bytes = self.clone().abi_encode(); + H256(keccak256(&encoded_bytes)) + } - Ok(H256(keccak256(&encoded_bytes))) + pub(crate) fn parse_signature(&self) -> Option<(bool, &[u8])> { + if self.signature.len() != 65 { + return None; + } + let v = match self.signature[64] { + 27 => false, + 28 => true, + _ => return None, + }; + Some((v, &self.signature[..64])) } } #[derive(Debug, Clone, Copy)] pub(crate) enum TxHashCalculationError { - CannotCalculateL1HashForL2Tx, CannotCalculateL2HashForL1Tx, } diff --git a/core/lib/multivm/src/vm_instance.rs b/core/lib/multivm/src/vm_instance.rs index 97af38ea0347..7004e2d7d2a8 100644 --- a/core/lib/multivm/src/vm_instance.rs +++ b/core/lib/multivm/src/vm_instance.rs @@ -345,6 +345,17 @@ where ) -> Self { Self::Shadowed(ShadowedFastVm::new(l1_batch_env, system_env, storage_view)) } + + pub fn skip_signature_verification(&mut self) { + match self { + Self::Fast(vm) => vm.skip_signature_verification(), + Self::Shadowed(vm) => { + if let Some(shadow_vm) = vm.shadow_mut() { + shadow_vm.skip_signature_verification(); + } + } + } + } } /// Checks whether the protocol version is supported by the fast VM. diff --git a/core/lib/types/src/transaction_request.rs b/core/lib/types/src/transaction_request.rs index db66c6955bda..fcf1d9fdcab6 100644 --- a/core/lib/types/src/transaction_request.rs +++ b/core/lib/types/src/transaction_request.rs @@ -738,12 +738,17 @@ impl TransactionRequest { } pub fn get_tx_hash(&self) -> Result { + Ok(self.get_signed_and_tx_hashes()?.0) + } + + pub fn get_signed_and_tx_hashes(&self) -> Result<(H256, H256), SerializationTransactionError> { let signed_message = self.get_default_signed_message()?; - if let Some(hash) = self.get_tx_hash_with_signed_message(signed_message)? { - return Ok(hash); + if let Some(tx_hash) = self.get_tx_hash_with_signed_message(signed_message)? { + return Ok((signed_message, tx_hash)); } let signature = self.get_packed_signature()?; - Ok(H256(keccak256(&self.get_signed_bytes(&signature)?))) + let tx_hash = H256(keccak256(&self.get_signed_bytes(&signature)?)); + Ok((signed_message, tx_hash)) } fn recover_default_signer( diff --git a/core/lib/vm_executor/src/batch/factory.rs b/core/lib/vm_executor/src/batch/factory.rs index 5e375d1f3062..64afde12b9ed 100644 --- a/core/lib/vm_executor/src/batch/factory.rs +++ b/core/lib/vm_executor/src/batch/factory.rs @@ -89,6 +89,7 @@ pub struct MainBatchExecutorFactory { optional_bytecode_compression: bool, fast_vm_mode: FastVmMode, observe_storage_metrics: bool, + skip_signature_verification: bool, divergence_handler: Option, _tracer: PhantomData, } @@ -99,6 +100,7 @@ impl MainBatchExecutorFactory { optional_bytecode_compression, fast_vm_mode: FastVmMode::Old, observe_storage_metrics: false, + skip_signature_verification: false, divergence_handler: None, _tracer: PhantomData, } @@ -125,6 +127,15 @@ impl MainBatchExecutorFactory { tracing::info!("Set VM divergence handler"); self.divergence_handler = Some(handler); } + + /// Skips signature verification for L2 transactions. + /// + /// # Important + /// + /// This is only safe to enable if transaction signatures are checked in some other way beforehand! + pub fn skip_signature_verification(&mut self) { + self.skip_signature_verification = true; + } } impl BatchExecutorFactory @@ -144,6 +155,7 @@ impl BatchExecutorFactory optional_bytecode_compression: self.optional_bytecode_compression, fast_vm_mode: self.fast_vm_mode, observe_storage_metrics: self.observe_storage_metrics, + skip_signature_verification: self.skip_signature_verification, divergence_handler: self.divergence_handler.clone(), commands: commands_receiver, _storage: PhantomData, @@ -294,6 +306,7 @@ struct CommandReceiver { optional_bytecode_compression: bool, fast_vm_mode: FastVmMode, observe_storage_metrics: bool, + skip_signature_verification: bool, divergence_handler: Option, commands: mpsc::Receiver, _storage: PhantomData, @@ -317,6 +330,12 @@ impl CommandReceiver { storage_view.clone(), self.fast_vm_mode, ); + + if self.skip_signature_verification { + if let BatchVm::Fast(vm) = &mut vm { + vm.skip_signature_verification(); + } + } let mut batch_finished = false; let mut prev_storage_stats = StorageViewStats::default(); diff --git a/core/lib/vm_interface/src/utils/shadow.rs b/core/lib/vm_interface/src/utils/shadow.rs index e4a7aa51f78c..2f2e33a41829 100644 --- a/core/lib/vm_interface/src/utils/shadow.rs +++ b/core/lib/vm_interface/src/utils/shadow.rs @@ -320,6 +320,11 @@ where pub fn dump_state(&self) -> VmDump { self.main.dump_state() } + + /// Accesses the shadow VM if it's available (the instance is dropped after finding a divergence). + pub fn shadow_mut(&mut self) -> Option<&mut Shadow> { + Some(&mut self.shadow.get_mut().as_mut()?.vm) + } } impl ShadowVm diff --git a/core/tests/vm-benchmark/benches/batch.rs b/core/tests/vm-benchmark/benches/batch.rs index f4151c39a6f8..4e8b4e4027ba 100644 --- a/core/tests/vm-benchmark/benches/batch.rs +++ b/core/tests/vm-benchmark/benches/batch.rs @@ -20,7 +20,8 @@ use vm_benchmark::{ criterion::{is_test_mode, BenchmarkGroup, BenchmarkId, CriterionExt, MeteredTime}, get_deploy_tx_with_gas_limit, get_erc20_deploy_tx, get_erc20_transfer_tx, get_heavy_load_test_tx, get_load_test_deploy_tx, get_load_test_tx, get_realistic_load_test_tx, - get_transfer_tx, BenchmarkingVm, BenchmarkingVmFactory, Bytecode, Fast, Legacy, LoadTestParams, + get_transfer_tx, BenchmarkingVm, BenchmarkingVmFactory, Bytecode, Fast, FastNoSignatures, + Legacy, LoadTestParams, }; use zksync_types::Transaction; @@ -197,6 +198,7 @@ criterion_group!( .with_measurement(MeteredTime::new("fill_bootloader")); targets = bench_fill_bootloader::, bench_fill_bootloader::, + bench_fill_bootloader::, bench_fill_bootloader:: ); criterion_main!(benches); diff --git a/core/tests/vm-benchmark/benches/instructions.rs b/core/tests/vm-benchmark/benches/instructions.rs index 654dfef71b29..f327fbb1e7ee 100644 --- a/core/tests/vm-benchmark/benches/instructions.rs +++ b/core/tests/vm-benchmark/benches/instructions.rs @@ -4,7 +4,8 @@ use std::{env, sync::mpsc}; use vise::{Gauge, LabeledFamily, Metrics}; use vm_benchmark::{ - criterion::PrometheusRuntime, BenchmarkingVm, BenchmarkingVmFactory, Fast, Legacy, BYTECODES, + criterion::PrometheusRuntime, BenchmarkingVm, BenchmarkingVmFactory, Fast, FastNoSignatures, + Legacy, BYTECODES, }; use yab::{ reporter::{BenchmarkOutput, BenchmarkReporter, Reporter}, @@ -200,6 +201,7 @@ fn benchmarks(bencher: &mut Bencher) { .add_reporter(ComparisonReporter::default()); } benchmarks_for_vm::(bencher); + benchmarks_for_vm::(bencher); benchmarks_for_vm::(bencher); } diff --git a/core/tests/vm-benchmark/benches/oneshot.rs b/core/tests/vm-benchmark/benches/oneshot.rs index 58a90af4981f..417e7e8e8414 100644 --- a/core/tests/vm-benchmark/benches/oneshot.rs +++ b/core/tests/vm-benchmark/benches/oneshot.rs @@ -4,7 +4,8 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use vm_benchmark::{ criterion::{BenchmarkGroup, CriterionExt, MeteredTime}, get_heavy_load_test_tx, get_load_test_deploy_tx, get_load_test_tx, get_realistic_load_test_tx, - BenchmarkingVm, BenchmarkingVmFactory, Fast, Legacy, LoadTestParams, BYTECODES, + BenchmarkingVm, BenchmarkingVmFactory, Fast, FastNoSignatures, Legacy, LoadTestParams, + BYTECODES, }; use zksync_types::Transaction; @@ -83,9 +84,11 @@ criterion_group!( .with_measurement(MeteredTime::new("criterion")); targets = benches_in_folder::, benches_in_folder::, + benches_in_folder::, benches_in_folder::, benches_in_folder::, bench_load_test::, + bench_load_test::, bench_load_test:: ); criterion_main!(benches); diff --git a/core/tests/vm-benchmark/src/lib.rs b/core/tests/vm-benchmark/src/lib.rs index 8f43f61b28b6..99fe68a394e0 100644 --- a/core/tests/vm-benchmark/src/lib.rs +++ b/core/tests/vm-benchmark/src/lib.rs @@ -6,7 +6,10 @@ pub use crate::{ get_heavy_load_test_tx, get_load_test_deploy_tx, get_load_test_tx, get_realistic_load_test_tx, get_transfer_tx, LoadTestParams, }, - vm::{BenchmarkingVm, BenchmarkingVmFactory, CountInstructions, Fast, Legacy, VmLabel}, + vm::{ + BenchmarkingVm, BenchmarkingVmFactory, CountInstructions, Fast, FastNoSignatures, Legacy, + VmLabel, + }, }; pub mod criterion; diff --git a/core/tests/vm-benchmark/src/vm.rs b/core/tests/vm-benchmark/src/vm.rs index a4b61ee54809..284d0fadc67f 100644 --- a/core/tests/vm-benchmark/src/vm.rs +++ b/core/tests/vm-benchmark/src/vm.rs @@ -36,6 +36,7 @@ static STORAGE: Lazy = Lazy::new(|| { #[derive(Debug, Clone, Copy)] pub enum VmLabel { Fast, + FastNoSignatures, Legacy, } @@ -44,6 +45,7 @@ impl VmLabel { pub const fn as_str(self) -> &'static str { match self { Self::Fast => "fast", + Self::FastNoSignatures => "fast_no_sigs", Self::Legacy => "legacy", } } @@ -52,6 +54,7 @@ impl VmLabel { pub const fn as_suffix(self) -> &'static str { match self { Self::Fast => "", + Self::FastNoSignatures => "/no_sigs", Self::Legacy => "/legacy", } } @@ -121,6 +124,25 @@ impl CountInstructions for Fast { } } +#[derive(Debug)] +pub struct FastNoSignatures; + +impl BenchmarkingVmFactory for FastNoSignatures { + const LABEL: VmLabel = VmLabel::FastNoSignatures; + + type Instance = vm_fast::Vm<&'static InMemoryStorage>; + + fn create( + batch_env: L1BatchEnv, + system_env: SystemEnv, + storage: &'static InMemoryStorage, + ) -> Self::Instance { + let mut vm = vm_fast::Vm::custom(batch_env, system_env, storage); + vm.skip_signature_verification(); + vm + } +} + /// Factory for the legacy VM (latest version). #[derive(Debug)] pub struct Legacy; diff --git a/prover/Cargo.lock b/prover/Cargo.lock index bc52f1ab2905..c27a7fef2c97 100644 --- a/prover/Cargo.lock +++ b/prover/Cargo.lock @@ -9037,7 +9037,7 @@ dependencies = [ [[package]] name = "zksync_vm2" version = "0.2.1" -source = "git+https://github.com/matter-labs/vm2.git?rev=457d8a7eea9093af9440662e33e598c13ba41633#457d8a7eea9093af9440662e33e598c13ba41633" +source = "git+https://github.com/matter-labs/vm2.git?rev=3841f5a430288a63c8207853eca11560bf7a5712#3841f5a430288a63c8207853eca11560bf7a5712" dependencies = [ "enum_dispatch", "primitive-types", @@ -9049,7 +9049,7 @@ dependencies = [ [[package]] name = "zksync_vm2_interface" version = "0.2.1" -source = "git+https://github.com/matter-labs/vm2.git?rev=457d8a7eea9093af9440662e33e598c13ba41633#457d8a7eea9093af9440662e33e598c13ba41633" +source = "git+https://github.com/matter-labs/vm2.git?rev=3841f5a430288a63c8207853eca11560bf7a5712#3841f5a430288a63c8207853eca11560bf7a5712" dependencies = [ "primitive-types", ]