From af149a01e6ce0c62d4b8a6acf9481e807ac24a8f Mon Sep 17 00:00:00 2001 From: Joonatan Saarhelo Date: Thu, 19 Dec 2024 10:21:30 +0000 Subject: [PATCH] feat: vm2 account validation (#2863) Implements an account validation gas limit and the validation tracer for vm2, along with better tests for validation. Instead of a second gas limit like in vm_latest, the normal gas limit is used. Unfortunately this means that the VM is not safe to use in the sequencer until we forbid the use of gasleft. I didn't do it here because it requires something like taint analysis and could break existing contracts that didn't know that gasleft is forbidden. --------- Co-authored-by: Alex Ostrovski Co-authored-by: Alex Ostrovski --- core/lib/contracts/src/lib.rs | 2 +- core/lib/multivm/src/tracers/mod.rs | 7 +- core/lib/multivm/src/tracers/validator/mod.rs | 12 +- .../src/tracers/validator/vm_latest/mod.rs | 12 +- core/lib/multivm/src/versions/shadow/mod.rs | 2 +- .../testonly/account_validation_rules.rs | 59 ++ .../src/versions/testonly/l1_messenger.rs | 4 +- core/lib/multivm/src/versions/testonly/mod.rs | 5 +- .../src/versions/testonly/require_eip712.rs | 41 +- .../src/versions/testonly/tester/mod.rs | 21 + .../multivm/src/versions/vm_fast/bytecode.rs | 2 +- core/lib/multivm/src/versions/vm_fast/hook.rs | 6 +- core/lib/multivm/src/versions/vm_fast/mod.rs | 8 +- .../vm_fast/tests/account_validation_rules.rs | 7 + .../multivm/src/versions/vm_fast/tests/mod.rs | 39 +- .../circuits.rs} | 0 .../evm_deploy.rs} | 6 +- .../src/versions/vm_fast/tracers/mod.rs | 86 +++ .../versions/vm_fast/tracers/validation.rs | 296 ++++++++++ core/lib/multivm/src/versions/vm_fast/vm.rs | 528 ++++++++++-------- .../tests/account_validation_rules.rs | 8 + .../src/versions/vm_latest/tests/mod.rs | 36 +- core/lib/multivm/src/vm_instance.rs | 62 +- .../validation-rule-breaker.sol | 149 +++++ core/lib/test_contracts/src/contracts.rs | 6 + core/lib/vm_executor/src/batch/factory.rs | 4 +- core/lib/vm_executor/src/oneshot/mod.rs | 137 +++-- core/lib/vm_interface/src/types/tracer.rs | 6 +- core/lib/vm_interface/src/utils/shadow.rs | 28 +- core/lib/vm_interface/src/vm.rs | 1 - core/node/eth_watch/src/tests/mod.rs | 14 +- .../src/tree_data_fetcher/provider/tests.rs | 2 +- core/tests/vm-benchmark/src/vm.rs | 7 +- 33 files changed, 1220 insertions(+), 383 deletions(-) create mode 100644 core/lib/multivm/src/versions/testonly/account_validation_rules.rs create mode 100644 core/lib/multivm/src/versions/vm_fast/tests/account_validation_rules.rs rename core/lib/multivm/src/versions/vm_fast/{circuits_tracer.rs => tracers/circuits.rs} (100%) rename core/lib/multivm/src/versions/vm_fast/{evm_deploy_tracer.rs => tracers/evm_deploy.rs} (94%) create mode 100644 core/lib/multivm/src/versions/vm_fast/tracers/mod.rs create mode 100644 core/lib/multivm/src/versions/vm_fast/tracers/validation.rs create mode 100644 core/lib/multivm/src/versions/vm_latest/tests/account_validation_rules.rs create mode 100644 core/lib/test_contracts/contracts/custom-account/validation-rule-breaker.sol diff --git a/core/lib/contracts/src/lib.rs b/core/lib/contracts/src/lib.rs index e60fdb0e59f3..4669fc2b86b8 100644 --- a/core/lib/contracts/src/lib.rs +++ b/core/lib/contracts/src/lib.rs @@ -180,7 +180,7 @@ pub fn l1_messenger_contract() -> Contract { /// Reads bytecode from the path RELATIVE to the Cargo workspace location. pub fn read_bytecode(relative_path: impl AsRef + std::fmt::Debug) -> Vec { - read_bytecode_from_path(relative_path).expect("Exists") + read_bytecode_from_path(relative_path).expect("Failed to open file") } pub fn eth_contract() -> Contract { diff --git a/core/lib/multivm/src/tracers/mod.rs b/core/lib/multivm/src/tracers/mod.rs index 35224d993a17..b888c3730118 100644 --- a/core/lib/multivm/src/tracers/mod.rs +++ b/core/lib/multivm/src/tracers/mod.rs @@ -1,6 +1,9 @@ pub use self::{ - call_tracer::CallTracer, multivm_dispatcher::TracerDispatcher, prestate_tracer::PrestateTracer, - storage_invocation::StorageInvocations, validator::ValidationTracer, + call_tracer::CallTracer, + multivm_dispatcher::TracerDispatcher, + prestate_tracer::PrestateTracer, + storage_invocation::StorageInvocations, + validator::{ValidationTracer, TIMESTAMP_ASSERTER_FUNCTION_SELECTOR}, }; mod call_tracer; diff --git a/core/lib/multivm/src/tracers/validator/mod.rs b/core/lib/multivm/src/tracers/validator/mod.rs index 88249467a575..c1dd311d9fd3 100644 --- a/core/lib/multivm/src/tracers/validator/mod.rs +++ b/core/lib/multivm/src/tracers/validator/mod.rs @@ -5,6 +5,7 @@ use std::{ }; use once_cell::sync::OnceCell; +pub use vm_latest::TIMESTAMP_ASSERTER_FUNCTION_SELECTOR; use zksync_system_constants::{ ACCOUNT_CODE_STORAGE_ADDRESS, BOOTLOADER_ADDRESS, CONTRACT_DEPLOYER_ADDRESS, L2_BASE_TOKEN_ADDRESS, MSG_VALUE_SIMULATOR_ADDRESS, SYSTEM_CONTEXT_ADDRESS, @@ -13,10 +14,7 @@ use zksync_types::{ address_to_u256, u256_to_h256, vm::VmVersion, web3::keccak256, AccountTreeId, Address, StorageKey, H256, U256, }; -use zksync_vm_interface::{ - tracer::{TimestampAsserterParams, ValidationTraces}, - L1BatchEnv, -}; +use zksync_vm_interface::tracer::{TimestampAsserterParams, ValidationTraces}; use self::types::{NewTrustedValidationItems, ValidationTracerMode}; use crate::{ @@ -54,7 +52,7 @@ pub struct ValidationTracer { computational_gas_limit: u32, timestamp_asserter_params: Option, vm_version: VmVersion, - l1_batch_env: L1BatchEnv, + l1_batch_timestamp: u64, pub result: Arc>, pub traces: Arc>, _marker: PhantomData H>, @@ -65,7 +63,7 @@ type ValidationRoundResult = Result ValidationTracer { const MAX_ALLOWED_SLOT_OFFSET: u32 = 127; - pub fn new(params: ValidationParams, vm_version: VmVersion, l1_batch_env: L1BatchEnv) -> Self { + pub fn new(params: ValidationParams, vm_version: VmVersion, l1_batch_timestamp: u64) -> Self { Self { validation_mode: ValidationTracerMode::NoValidation, auxilary_allowed_slots: Default::default(), @@ -83,7 +81,7 @@ impl ValidationTracer { result: Arc::new(OnceCell::new()), traces: Arc::new(Mutex::new(ValidationTraces::default())), _marker: Default::default(), - l1_batch_env, + l1_batch_timestamp, } } diff --git a/core/lib/multivm/src/tracers/validator/vm_latest/mod.rs b/core/lib/multivm/src/tracers/validator/vm_latest/mod.rs index 3c819384137f..5588dd144e95 100644 --- a/core/lib/multivm/src/tracers/validator/vm_latest/mod.rs +++ b/core/lib/multivm/src/tracers/validator/vm_latest/mod.rs @@ -1,6 +1,6 @@ use zk_evm_1_5_0::{ tracing::{BeforeExecutionData, VmLocalStateData}, - zkevm_opcode_defs::{ContextOpcode, FarCallABI, LogOpcode, Opcode}, + zkevm_opcode_defs::{ContextOpcode, FarCallABI, LogOpcode, Opcode, RetOpcode}, }; use zksync_system_constants::KECCAK256_PRECOMPILE_ADDRESS; use zksync_types::{ @@ -116,8 +116,7 @@ impl ValidationTracer { // using self.l1_batch_env.timestamp is ok here because the tracer is always // used in a oneshot execution mode if end - < self.l1_batch_env.timestamp - + params.min_time_till_end.as_secs() + < self.l1_batch_timestamp + params.min_time_till_end.as_secs() { return Err( ViolatedValidationRule::TimestampAssertionCloseToRangeEnd, @@ -168,6 +167,13 @@ impl ValidationTracer { }); } } + + Opcode::Ret(RetOpcode::Panic) + if state.vm_local_state.callstack.current.ergs_remaining == 0 => + { + // Actual gas limit was reached, not the validation gas limit. + return Err(ViolatedValidationRule::TookTooManyComputationalGas(0)); + } _ => {} } diff --git a/core/lib/multivm/src/versions/shadow/mod.rs b/core/lib/multivm/src/versions/shadow/mod.rs index a335d0fe5906..1ad5bdba5a7b 100644 --- a/core/lib/multivm/src/versions/shadow/mod.rs +++ b/core/lib/multivm/src/versions/shadow/mod.rs @@ -25,7 +25,7 @@ use crate::{ mod tests; type ReferenceVm = vm_latest::Vm, HistoryEnabled>; -type ShadowedFastVm = crate::vm_instance::ShadowedFastVm; +type ShadowedFastVm = crate::vm_instance::ShadowedFastVm; fn hash_block(block_env: L2BlockEnv, tx_hashes: &[H256]) -> H256 { let mut hasher = L2BlockHasher::new( diff --git a/core/lib/multivm/src/versions/testonly/account_validation_rules.rs b/core/lib/multivm/src/versions/testonly/account_validation_rules.rs new file mode 100644 index 000000000000..b2beb050ad41 --- /dev/null +++ b/core/lib/multivm/src/versions/testonly/account_validation_rules.rs @@ -0,0 +1,59 @@ +use assert_matches::assert_matches; +use zksync_test_contracts::TestContract; +use zksync_types::{u256_to_h256, AccountTreeId, Address, StorageKey}; +use zksync_vm_interface::tracer::ViolatedValidationRule; + +use super::{ + get_empty_storage, require_eip712::make_aa_transaction, tester::VmTesterBuilder, + ContractToDeploy, TestedVm, TestedVmForValidation, +}; +use crate::interface::TxExecutionMode; + +/// Checks that every limitation imposed on account validation results in an appropriate error. +/// The actual misbehavior cases are found in "validation-rule-breaker.sol". +pub(crate) fn test_account_validation_rules() { + assert_matches!(test_rule::(0), None); + assert_matches!( + test_rule::(1), + Some(ViolatedValidationRule::TouchedDisallowedStorageSlots(_, _)) + ); + assert_matches!( + test_rule::(2), + Some(ViolatedValidationRule::CalledContractWithNoCode(_)) + ); + assert_matches!(test_rule::(3), None); + assert_matches!( + test_rule::(4), + Some(ViolatedValidationRule::TookTooManyComputationalGas(_)) + ) +} + +fn test_rule(rule: u32) -> Option { + let aa_address = Address::repeat_byte(0x10); + let beneficiary_address = Address::repeat_byte(0x20); + + // Set the type of misbehaviour of the AA contract + let mut storage_with_rule_break_set = get_empty_storage(); + storage_with_rule_break_set.set_value( + StorageKey::new(AccountTreeId::new(aa_address), u256_to_h256(0.into())), + u256_to_h256(rule.into()), + ); + + let bytecode = TestContract::validation_test().bytecode.to_vec(); + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_custom_contracts(vec![ + ContractToDeploy::account(bytecode, aa_address).funded() + ]) + .with_storage(storage_with_rule_break_set) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_rich_accounts(1) + .build::(); + + let private_account = vm.rich_accounts[0].clone(); + + vm.vm.run_validation( + make_aa_transaction(aa_address, beneficiary_address, &private_account), + 55, + ) +} diff --git a/core/lib/multivm/src/versions/testonly/l1_messenger.rs b/core/lib/multivm/src/versions/testonly/l1_messenger.rs index daf07b2750f7..5d602b1e7d66 100644 --- a/core/lib/multivm/src/versions/testonly/l1_messenger.rs +++ b/core/lib/multivm/src/versions/testonly/l1_messenger.rs @@ -99,9 +99,7 @@ pub(crate) fn test_rollup_da_output_hash_match() { // Firstly, deploy tx. It should publish the bytecode of the "test contract" let counter_bytecode = TestContract::counter().bytecode; - let tx = account - .get_deploy_tx(&counter_bytecode, None, TxType::L2) - .tx; + let tx = account.get_deploy_tx(counter_bytecode, None, TxType::L2).tx; // We do not use compression here, to have the bytecode published in full. let (_, result) = vm .vm diff --git a/core/lib/multivm/src/versions/testonly/mod.rs b/core/lib/multivm/src/versions/testonly/mod.rs index a0f08546197c..5ab13df87337 100644 --- a/core/lib/multivm/src/versions/testonly/mod.rs +++ b/core/lib/multivm/src/versions/testonly/mod.rs @@ -24,12 +24,15 @@ use zksync_vm_interface::{ pubdata::PubdataBuilder, L1BatchEnv, L2BlockEnv, SystemEnv, TxExecutionMode, }; -pub(super) use self::tester::{TestedVm, VmTester, VmTesterBuilder}; +pub(super) use self::tester::{ + validation_params, TestedVm, TestedVmForValidation, VmTester, VmTesterBuilder, +}; use crate::{ interface::storage::InMemoryStorage, pubdata_builders::RollupPubdataBuilder, vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT, }; +pub(super) mod account_validation_rules; pub(super) mod block_tip; pub(super) mod bootloader; pub(super) mod bytecode_publishing; diff --git a/core/lib/multivm/src/versions/testonly/require_eip712.rs b/core/lib/multivm/src/versions/testonly/require_eip712.rs index 7a934c570aea..2f17a3e7823e 100644 --- a/core/lib/multivm/src/versions/testonly/require_eip712.rs +++ b/core/lib/multivm/src/versions/testonly/require_eip712.rs @@ -1,6 +1,6 @@ use ethabi::Token; use zksync_eth_signer::TransactionParameters; -use zksync_test_contracts::TestContract; +use zksync_test_contracts::{Account, TestContract}; use zksync_types::{ fee::Fee, l2::L2Tx, transaction_request::TransactionRequest, Address, Eip712Domain, Execute, L2ChainId, Nonce, Transaction, U256, @@ -30,7 +30,6 @@ pub(crate) fn test_require_eip712() { .with_rich_accounts(1) .build::(); assert_eq!(vm.get_eth_balance(beneficiary_address), U256::from(0)); - let chain_id: u32 = 270; let mut private_account = vm.rich_accounts[0].clone(); // First, let's set the owners of the AA account to the `private_address`. @@ -97,7 +96,30 @@ pub(crate) fn test_require_eip712() { vm.get_eth_balance(private_account.address) ); - // // Now send the 'classic' EIP712 transaction + // Now send the 'classic' EIP712 transaction + + let transaction: Transaction = + make_aa_transaction(aa_address, beneficiary_address, &private_account).into(); + vm.vm.push_transaction(transaction); + vm.vm.execute(InspectExecutionMode::OneTx); + + assert_eq!( + vm.get_eth_balance(beneficiary_address), + U256::from(916375026) + ); + assert_eq!( + private_account_balance, + vm.get_eth_balance(private_account.address) + ); +} + +pub(crate) fn make_aa_transaction( + aa_address: Address, + beneficiary_address: Address, + private_account: &Account, +) -> L2Tx { + let chain_id: u32 = 270; + let tx_712 = L2Tx::new( Some(beneficiary_address), vec![], @@ -130,16 +152,5 @@ pub(crate) fn test_require_eip712() { let mut l2_tx = L2Tx::from_request(aa_txn_request, 100000, false).unwrap(); l2_tx.set_input(encoded_tx, aa_hash); - let transaction: Transaction = l2_tx.into(); - vm.vm.push_transaction(transaction); - vm.vm.execute(InspectExecutionMode::OneTx); - - assert_eq!( - vm.get_eth_balance(beneficiary_address), - U256::from(916375026) - ); - assert_eq!( - private_account_balance, - vm.get_eth_balance(private_account.address) - ); + l2_tx } diff --git a/core/lib/multivm/src/versions/testonly/tester/mod.rs b/core/lib/multivm/src/versions/testonly/tester/mod.rs index d3cf2d6f782f..c29f2dbbf8f3 100644 --- a/core/lib/multivm/src/versions/testonly/tester/mod.rs +++ b/core/lib/multivm/src/versions/testonly/tester/mod.rs @@ -3,6 +3,7 @@ use std::{collections::HashSet, fmt, rc::Rc}; use zksync_contracts::BaseSystemContracts; use zksync_test_contracts::{Account, TestContract, TxType}; use zksync_types::{ + l2::L2Tx, utils::{deployed_address_create, storage_key_for_eth_balance}, writes::StateDiffRecord, Address, L1BatchNumber, StorageKey, Transaction, H256, U256, @@ -14,6 +15,7 @@ use crate::{ interface::{ pubdata::{PubdataBuilder, PubdataInput}, storage::{InMemoryStorage, StoragePtr, StorageView}, + tracer::{ValidationParams, ViolatedValidationRule}, CurrentExecutionState, InspectExecutionMode, L1BatchEnv, L2BlockEnv, SystemEnv, TxExecutionMode, VmExecutionResultAndLogs, VmFactory, VmInterfaceExt, VmInterfaceHistoryEnabled, @@ -231,3 +233,22 @@ pub(crate) trait TestedVm: /// Returns pubdata input. fn pubdata_input(&self) -> PubdataInput; } + +pub(crate) trait TestedVmForValidation { + fn run_validation(&mut self, tx: L2Tx, timestamp: u64) -> Option; +} + +pub(crate) fn validation_params(tx: &L2Tx, system: &SystemEnv) -> ValidationParams { + let user_address = tx.common_data.initiator_address; + let paymaster_address = tx.common_data.paymaster_params.paymaster; + ValidationParams { + user_address, + paymaster_address, + trusted_slots: Default::default(), + trusted_addresses: Default::default(), + // field `trustedAddress` of ValidationRuleBreaker + trusted_address_slots: [(Address::repeat_byte(0x10), 2.into())].into(), + computational_gas_limit: system.default_validation_computational_gas_limit, + timestamp_asserter_params: None, + } +} diff --git a/core/lib/multivm/src/versions/vm_fast/bytecode.rs b/core/lib/multivm/src/versions/vm_fast/bytecode.rs index 4dc52951c16c..aec5ed9ae301 100644 --- a/core/lib/multivm/src/versions/vm_fast/bytecode.rs +++ b/core/lib/multivm/src/versions/vm_fast/bytecode.rs @@ -7,7 +7,7 @@ use crate::{ utils::bytecode, }; -impl Vm { +impl Vm { /// Checks the last transaction has successfully published compressed bytecodes and returns `true` if there is at least one is still unknown. pub(crate) fn has_unpublished_bytecodes(&mut self) -> bool { self.bootloader_state diff --git a/core/lib/multivm/src/versions/vm_fast/hook.rs b/core/lib/multivm/src/versions/vm_fast/hook.rs index 8d385f94f3e1..b138c6d496d9 100644 --- a/core/lib/multivm/src/versions/vm_fast/hook.rs +++ b/core/lib/multivm/src/versions/vm_fast/hook.rs @@ -1,8 +1,8 @@ -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub(crate) enum Hook { AccountValidationEntered, PaymasterValidationEntered, - AccountValidationExited, + ValidationExited, ValidationStepEnded, TxHasEnded, DebugLog, @@ -22,7 +22,7 @@ impl Hook { match hook { 0 => Hook::AccountValidationEntered, 1 => Hook::PaymasterValidationEntered, - 2 => Hook::AccountValidationExited, + 2 => Hook::ValidationExited, 3 => Hook::ValidationStepEnded, 4 => Hook::TxHasEnded, 5 => Hook::DebugLog, diff --git a/core/lib/multivm/src/versions/vm_fast/mod.rs b/core/lib/multivm/src/versions/vm_fast/mod.rs index 840653b63b08..dca575138553 100644 --- a/core/lib/multivm/src/versions/vm_fast/mod.rs +++ b/core/lib/multivm/src/versions/vm_fast/mod.rs @@ -1,19 +1,21 @@ pub use zksync_vm2::interface; pub(crate) use self::version::FastVmVersion; -pub use self::vm::Vm; +pub use self::{ + tracers::{FullValidationTracer, ValidationTracer}, + vm::Vm, +}; mod bootloader_state; mod bytecode; -mod circuits_tracer; mod events; -mod evm_deploy_tracer; mod glue; mod hook; mod initial_bootloader_memory; mod refund; #[cfg(test)] mod tests; +mod tracers; mod transaction_data; mod utils; mod version; diff --git a/core/lib/multivm/src/versions/vm_fast/tests/account_validation_rules.rs b/core/lib/multivm/src/versions/vm_fast/tests/account_validation_rules.rs new file mode 100644 index 000000000000..fa4b634a22f6 --- /dev/null +++ b/core/lib/multivm/src/versions/vm_fast/tests/account_validation_rules.rs @@ -0,0 +1,7 @@ +use super::TestedFastVm; +use crate::versions::testonly::account_validation_rules::test_account_validation_rules; + +#[test] +fn test_account_validation_rules_fast() { + test_account_validation_rules::>(); +} diff --git a/core/lib/multivm/src/versions/vm_fast/tests/mod.rs b/core/lib/multivm/src/versions/vm_fast/tests/mod.rs index 2093d0ec496f..e148444922ba 100644 --- a/core/lib/multivm/src/versions/vm_fast/tests/mod.rs +++ b/core/lib/multivm/src/versions/vm_fast/tests/mod.rs @@ -1,22 +1,25 @@ use std::{any::Any, collections::HashSet, fmt, rc::Rc}; use zksync_types::{ - h256_to_u256, writes::StateDiffRecord, StorageKey, Transaction, H160, H256, U256, + h256_to_u256, l2::L2Tx, writes::StateDiffRecord, StorageKey, Transaction, H160, H256, U256, }; -use zksync_vm2::interface::{Event, HeapId, StateInterface}; +use zksync_vm2::interface::{Event, HeapId, StateInterface, Tracer}; use zksync_vm_interface::{ pubdata::{PubdataBuilder, PubdataInput}, storage::ReadStorage, - CurrentExecutionState, L2BlockEnv, VmExecutionMode, VmExecutionResultAndLogs, VmInterface, + tracer::ViolatedValidationRule, + CurrentExecutionState, InspectExecutionMode, L2BlockEnv, VmExecutionMode, + VmExecutionResultAndLogs, VmInterface, }; -use super::{circuits_tracer::CircuitsTracer, Vm}; +use super::{FullValidationTracer, ValidationTracer, Vm}; use crate::{ interface::storage::{ImmutableStorageView, InMemoryStorage}, - versions::testonly::TestedVm, - vm_fast::evm_deploy_tracer::{DynamicBytecodes, EvmDeployTracer}, + versions::testonly::{validation_params, TestedVm, TestedVmForValidation}, + vm_fast::tracers::WithBuiltinTracers, }; +mod account_validation_rules; mod block_tip; mod bootloader; mod bytecode_publishing; @@ -80,7 +83,13 @@ impl PartialEq for VmStateDump { } } -impl TestedVm for Vm> { +pub(crate) type TestedFastVm = Vm, Tr, Val>; + +impl TestedVm for TestedFastVm +where + Tr: 'static + Tracer + Default + fmt::Debug, + Val: 'static + ValidationTracer + fmt::Debug, +{ type StateDump = VmStateDump; fn dump_state(&self) -> Self::StateDump { @@ -126,13 +135,9 @@ impl TestedVm for Vm> { } fn manually_decommit(&mut self, code_hash: H256) -> bool { - let mut tracer = ( - ((), CircuitsTracer::default()), - EvmDeployTracer::new(DynamicBytecodes::default()), - ); let (_, is_fresh) = self.inner.world_diff_mut().decommit_opcode( &mut self.world, - &mut tracer, + &mut WithBuiltinTracers::mock(), h256_to_u256(code_hash), ); is_fresh @@ -174,3 +179,13 @@ impl TestedVm for Vm> { self.bootloader_state.get_pubdata_information().clone() } } + +impl TestedVmForValidation for Vm, (), FullValidationTracer> { + fn run_validation(&mut self, tx: L2Tx, timestamp: u64) -> Option { + let validation_params = validation_params(&tx, &self.system_env); + self.push_transaction(tx.into()); + let mut tracer = ((), FullValidationTracer::new(validation_params, timestamp)); + self.inspect(&mut tracer, InspectExecutionMode::OneTx); + tracer.1.validation_error() + } +} diff --git a/core/lib/multivm/src/versions/vm_fast/circuits_tracer.rs b/core/lib/multivm/src/versions/vm_fast/tracers/circuits.rs similarity index 100% rename from core/lib/multivm/src/versions/vm_fast/circuits_tracer.rs rename to core/lib/multivm/src/versions/vm_fast/tracers/circuits.rs diff --git a/core/lib/multivm/src/versions/vm_fast/evm_deploy_tracer.rs b/core/lib/multivm/src/versions/vm_fast/tracers/evm_deploy.rs similarity index 94% rename from core/lib/multivm/src/versions/vm_fast/evm_deploy_tracer.rs rename to core/lib/multivm/src/versions/vm_fast/tracers/evm_deploy.rs index c443c99ccf9a..0fbf9dec7215 100644 --- a/core/lib/multivm/src/versions/vm_fast/evm_deploy_tracer.rs +++ b/core/lib/multivm/src/versions/vm_fast/tracers/evm_deploy.rs @@ -8,14 +8,14 @@ use zksync_vm2::interface::{ CallframeInterface, CallingMode, GlobalStateInterface, Opcode, OpcodeType, ShouldStop, Tracer, }; -use super::utils::read_fat_pointer; +use crate::vm_fast::utils::read_fat_pointer; /// Container for dynamic bytecodes added by [`EvmDeployTracer`]. #[derive(Debug, Clone, Default)] -pub(super) struct DynamicBytecodes(Rc>>>); +pub(crate) struct DynamicBytecodes(Rc>>>); impl DynamicBytecodes { - pub(super) fn map(&self, hash: U256, f: impl FnOnce(&[u8]) -> R) -> Option { + pub(crate) fn map(&self, hash: U256, f: impl FnOnce(&[u8]) -> R) -> Option { self.0.borrow().get(&hash).map(|code| f(code)) } diff --git a/core/lib/multivm/src/versions/vm_fast/tracers/mod.rs b/core/lib/multivm/src/versions/vm_fast/tracers/mod.rs new file mode 100644 index 000000000000..3d9602536743 --- /dev/null +++ b/core/lib/multivm/src/versions/vm_fast/tracers/mod.rs @@ -0,0 +1,86 @@ +//! Tracers for the fast VM. + +use zksync_vm2::interface::{CycleStats, GlobalStateInterface, OpcodeType, ShouldStop, Tracer}; + +pub(super) use self::evm_deploy::DynamicBytecodes; +pub use self::validation::{FullValidationTracer, ValidationTracer}; +use self::{circuits::CircuitsTracer, evm_deploy::EvmDeployTracer}; +use crate::interface::CircuitStatistic; + +mod circuits; +mod evm_deploy; +mod validation; + +#[derive(Debug)] +pub(super) struct WithBuiltinTracers { + pub external: Ext, + pub validation: Val, + circuits: CircuitsTracer, + evm_deploy_tracer: EvmDeployTracer, +} + +impl WithBuiltinTracers { + pub(super) fn new(external: Tr, validation: Val, dynamic_bytecodes: DynamicBytecodes) -> Self { + Self { + external, + validation, + circuits: CircuitsTracer::default(), + evm_deploy_tracer: EvmDeployTracer::new(dynamic_bytecodes), + } + } + + pub(super) fn circuit_statistic(&self) -> CircuitStatistic { + self.circuits.circuit_statistic() + } +} + +#[cfg(test)] +impl WithBuiltinTracers { + pub(super) fn mock() -> Self { + Self::new(Tr::default(), Val::default(), DynamicBytecodes::default()) + } +} + +impl Tracer for WithBuiltinTracers { + #[inline(always)] + fn before_instruction(&mut self, state: &mut S) { + self.validation.before_instruction::(state); + self.external.before_instruction::(state); + self.circuits.before_instruction::(state); + self.evm_deploy_tracer.before_instruction::(state); + } + + #[inline(always)] + fn after_instruction( + &mut self, + state: &mut S, + ) -> ShouldStop { + if matches!( + self.validation.after_instruction::(state), + ShouldStop::Stop + ) { + return ShouldStop::Stop; + } + if matches!( + self.external.after_instruction::(state), + ShouldStop::Stop + ) { + return ShouldStop::Stop; + } + if matches!( + self.circuits.after_instruction::(state), + ShouldStop::Stop + ) { + return ShouldStop::Stop; + } + self.evm_deploy_tracer.after_instruction::(state) + } + + #[inline(always)] + fn on_extra_prover_cycles(&mut self, stats: CycleStats) { + self.validation.on_extra_prover_cycles(stats); + self.external.on_extra_prover_cycles(stats); + self.circuits.on_extra_prover_cycles(stats); + self.evm_deploy_tracer.on_extra_prover_cycles(stats); + } +} diff --git a/core/lib/multivm/src/versions/vm_fast/tracers/validation.rs b/core/lib/multivm/src/versions/vm_fast/tracers/validation.rs new file mode 100644 index 000000000000..52b0a4747b7d --- /dev/null +++ b/core/lib/multivm/src/versions/vm_fast/tracers/validation.rs @@ -0,0 +1,296 @@ +use std::collections::HashSet; + +use zk_evm_1_3_1::address_to_u256; +use zksync_types::{ + u256_to_address, Address, ACCOUNT_CODE_STORAGE_ADDRESS, BOOTLOADER_ADDRESS, + CONTRACT_DEPLOYER_ADDRESS, KECCAK256_PRECOMPILE_ADDRESS, L2_BASE_TOKEN_ADDRESS, + MSG_VALUE_SIMULATOR_ADDRESS, SYSTEM_CONTEXT_ADDRESS, U256, +}; +use zksync_vm2::interface::{ + CallframeInterface, GlobalStateInterface, Opcode::*, OpcodeType, ReturnType::*, ShouldStop, + Tracer, +}; +use zksync_vm_interface::tracer::{ + TimestampAsserterParams, ValidationParams, ValidationTraces, ViolatedValidationRule, +}; + +use crate::{tracers::TIMESTAMP_ASSERTER_FUNCTION_SELECTOR, vm_fast::utils::read_fat_pointer}; + +/// [`Tracer`] used for account validation per [EIP-4337] and [EIP-7562]. +/// +/// [EIP-4337]: https://eips.ethereum.org/EIPS/eip-4337 +/// [EIP-7562]: https://eips.ethereum.org/EIPS/eip-7562 +pub trait ValidationTracer: Tracer + Default { + /// Should the execution stop after validation is complete? + const STOP_AFTER_VALIDATION: bool; + /// Hook called when account validation is entered. + fn account_validation_entered(&mut self); + /// Hook called when account validation is exited. + fn validation_exited(&mut self); +} + +impl ValidationTracer for () { + const STOP_AFTER_VALIDATION: bool = false; + fn account_validation_entered(&mut self) {} + fn validation_exited(&mut self) {} +} + +/// Account abstraction exposes a chain to denial of service attacks because someone who fails to +/// authenticate does not pay for the failed transaction. Otherwise, people could empty other's +/// wallets for free! +/// +/// If some address repeatedly posts transactions that validate during preliminary checks but fail +/// to validate during the actual execution, that address is considered a spammer. However, when +/// the spam comes from multiple addresses, that doesn't work. +/// +/// We want to ensure that a spammer has to pay for every account that fails validation. This is +/// achieved by limiting what the code of a custom account is allowed to do. If we allowed access +/// to things like time, a validation that fails in the sequencer could be crafted for free, so we +/// don't. +/// +/// However, we want to give access to storage. A spammer has to pay for changing storage but +/// could change just one storage slot to invalidate transactions from many accounts. To prevent +/// that, we make sure that the storage slots accessed by different accounts are disjoint by only +/// allowing access to storage in the account itself and slots derived from the account's address. +/// +/// Our rules are an extension of the rules are outlined in [EIP-7562]. +/// +/// This tracer enforces the rules by checking what the code does at runtime, even though the +/// properties checked are supposed to always hold for a well-written custom account. Proving +/// that a contract adheres to the rules ahead of time would be challenging or even impossible, +/// considering that contracts that the code depends on may get upgraded. +/// +/// [EIP-7562]: https://eips.ethereum.org/EIPS/eip-7562 +#[derive(Debug, Default)] +pub struct FullValidationTracer { + in_validation: bool, + add_return_value_to_allowed_slots: bool, + + slots_obtained_via_keccak: HashSet, + trusted_addresses: HashSet
, + + user_address: Address, + trusted_storage: HashSet<(Address, U256)>, + /// These location's values are added to [Self::trusted_addresses] to support upgradeable proxies. + storage_containing_trusted_addresses: HashSet<(Address, U256)>, + timestamp_asserter_params: Option, + l1_batch_timestamp: u64, + + validation_error: Option, + traces: ValidationTraces, +} + +impl ValidationTracer for FullValidationTracer { + const STOP_AFTER_VALIDATION: bool = true; + + fn account_validation_entered(&mut self) { + self.in_validation = true; + } + + fn validation_exited(&mut self) { + self.in_validation = false; + } +} + +impl Tracer for FullValidationTracer { + fn before_instruction(&mut self, state: &mut S) { + if !self.in_validation { + return; + } + + match OP::VALUE { + // Out of gas once means out of gas for the whole validation, as the EIP forbids handling out of gas errors + Ret(Panic) if state.current_frame().gas() == 0 => { + self.set_error(ViolatedValidationRule::TookTooManyComputationalGas(0)) + } + + ContextMeta => self.set_error(ViolatedValidationRule::TouchedDisallowedContext), + + StorageRead => { + let address = state.current_frame().address(); + let caller = state.current_frame().caller(); + + // Can unwrap because the instruction pointer does not point to a panic instruction + let pc = state.current_frame().program_counter().unwrap(); + let word = pc / 4; + let part = pc % 4; + let instruction = + state.current_frame().read_contract_code(word).0[3 - part as usize]; + let slot = state.read_register((instruction >> 16) as u8 & 0b1111).0; + + if self + .storage_containing_trusted_addresses + .contains(&(address, slot)) + { + self.trusted_addresses + .insert(u256_to_address(&state.get_storage(address, slot))); + } else if !self.is_valid_storage_read( + address, + caller, + slot, + state.get_storage(address, slot), + ) { + self.set_error(ViolatedValidationRule::TouchedDisallowedStorageSlots( + address, slot, + )) + } + } + + _ => {} + } + } + + fn after_instruction( + &mut self, + state: &mut S, + ) -> ShouldStop { + if !self.in_validation { + return ShouldStop::Continue; + } + + if self.validation_error.is_some() { + return ShouldStop::Stop; + } + + match OP::VALUE { + FarCall(_) => { + // Intercept calls to keccak, whitelist storage slots corresponding to the hash + let code_address = state.current_frame().code_address(); + if code_address == KECCAK256_PRECOMPILE_ADDRESS { + let calldata = read_fat_pointer(state, state.read_register(1).0); + if calldata.len() != 64 { + return ShouldStop::Continue; + } + + // Solidity mappings store values at the keccak256 hash of `key ++ slot_of_mapping` + let (key, mapping) = calldata.split_at(32); + + let mapping_is_allowed = + self.slots_obtained_via_keccak.contains(&mapping.into()); + + if U256::from(key) == address_to_u256(&self.user_address) || mapping_is_allowed + { + self.add_return_value_to_allowed_slots = true; + } + } else if code_address != self.user_address + && state + .get_storage(ACCOUNT_CODE_STORAGE_ADDRESS, address_to_u256(&code_address)) + .is_zero() + { + self.set_error(ViolatedValidationRule::CalledContractWithNoCode( + code_address, + )); + return ShouldStop::Stop; + } + + if let Some(ref params) = self.timestamp_asserter_params { + if code_address == params.address { + let calldata = read_fat_pointer(state, state.read_register(1).0); + if calldata.len() == 68 + && calldata[..4] == TIMESTAMP_ASSERTER_FUNCTION_SELECTOR + { + // start and end need to be capped to u64::MAX to avoid overflow + let start = U256::from_big_endian( + &calldata[calldata.len() - 64..calldata.len() - 32], + ) + .try_into() + .unwrap_or(u64::MAX); + let end = U256::from_big_endian(&calldata[calldata.len() - 32..]) + .try_into() + .unwrap_or(u64::MAX); + + // using self.l1_batch_env.timestamp is ok here because the tracer is always + // used in a oneshot execution mode + if end < self.l1_batch_timestamp + params.min_time_till_end.as_secs() { + self.set_error( + ViolatedValidationRule::TimestampAssertionCloseToRangeEnd, + ); + return ShouldStop::Stop; + } + + self.traces.apply_timestamp_asserter_range(start..end); + } + } + } + } + Ret(kind) => { + if self.add_return_value_to_allowed_slots && kind == Normal { + let return_value = read_fat_pointer(state, state.read_register(1).0); + self.slots_obtained_via_keccak + .insert(return_value.as_slice().into()); + } + self.add_return_value_to_allowed_slots = false; + } + _ => {} + } + + ShouldStop::Continue + } +} + +impl FullValidationTracer { + pub fn new(params: ValidationParams, l1_batch_timestamp: u64) -> Self { + let ValidationParams { + user_address, + trusted_slots, + trusted_addresses, + trusted_address_slots, + timestamp_asserter_params, + .. + } = params; + Self { + user_address, + trusted_storage: trusted_slots, + trusted_addresses, + storage_containing_trusted_addresses: trusted_address_slots, + l1_batch_timestamp, + timestamp_asserter_params, + + ..Self::default() + } + } + + fn is_valid_storage_read( + &self, + address: Address, + caller: Address, + slot: U256, + value: U256, + ) -> bool { + // allow reading own slots + address == self.user_address + // allow reading slot + || slot == address_to_u256(&self.user_address) + || self.slots_obtained_via_keccak.contains(&slot) + // some storage locations are always allowed + || self.trusted_addresses.contains(&address) + || self.trusted_storage.contains(&(address, slot)) + // certain system contracts are allowed to transfer ETH + || address == L2_BASE_TOKEN_ADDRESS + && (caller == MSG_VALUE_SIMULATOR_ADDRESS + || caller == CONTRACT_DEPLOYER_ADDRESS + || caller == BOOTLOADER_ADDRESS) + // allow getting chain_id + || address == SYSTEM_CONTEXT_ADDRESS && slot == U256::zero() + // allow reading code hashes of existing contracts + || address == ACCOUNT_CODE_STORAGE_ADDRESS && !value.is_zero() + // allow TimestampAsserter to do its job + || self.timestamp_asserter_params.as_ref() + .map(|p| p.address == caller) + .unwrap_or_default() + } + + fn set_error(&mut self, error: ViolatedValidationRule) { + if self.validation_error.is_none() { + self.validation_error = Some(error); + } + } + + pub fn validation_error(&self) -> Option { + self.validation_error.clone() + } + + pub fn traces(&self) -> ValidationTraces { + self.traces.clone() + } +} diff --git a/core/lib/multivm/src/versions/vm_fast/vm.rs b/core/lib/multivm/src/versions/vm_fast/vm.rs index c935b1c0e7f5..e2310c254e96 100644 --- a/core/lib/multivm/src/versions/vm_fast/vm.rs +++ b/core/lib/multivm/src/versions/vm_fast/vm.rs @@ -27,10 +27,9 @@ use zksync_vm2::{ use super::{ bootloader_state::{BootloaderState, BootloaderStateSnapshot}, bytecode::compress_bytecodes, - circuits_tracer::CircuitsTracer, - evm_deploy_tracer::{DynamicBytecodes, EvmDeployTracer}, hook::Hook, initial_bootloader_memory::bootloader_initial_memory, + tracers::{DynamicBytecodes, ValidationTracer, WithBuiltinTracers}, transaction_data::TransactionData, }; use crate::{ @@ -58,8 +57,6 @@ use crate::{ VmVersion, }; -type FullTracer = ((Tr, CircuitsTracer), EvmDeployTracer); - #[derive(Debug)] struct VmRunResult { execution_result: ExecutionResult, @@ -85,15 +82,18 @@ impl VmRunResult { } } +type InnerVm = + VirtualMachine, World>>; + /// Fast VM wrapper. /// -/// The wrapper is parametric by the storage and tracer types. Besides the [`Tracer`] trait, a tracer must have `'static` lifetime -/// and implement [`Default`] (the latter is necessary to complete batches). [`CircuitsTracer`] is currently always enabled; -/// you don't need to specify it explicitly. -pub struct Vm { - pub(super) world: World>, - pub(super) inner: VirtualMachine, World>>, - gas_for_account_validation: u32, +/// The wrapper is parametric by the storage and tracer types. Besides the [`Tracer`] trait, the tracer must implement [`Default`] +/// (the latter is necessary to complete batches). Validation is encapsulated in a separate type param. It should be set to `()` +/// for "standard" validation (not stopping after validation; no validation-specific checks), or [`FullValidationTracer`](super::FullValidationTracer) +/// for full validation (stopping after validation; validation-specific checks). +pub struct Vm { + pub(super) world: World>, + pub(super) inner: InnerVm, pub(super) bootloader_state: BootloaderState, pub(super) batch_env: L1BatchEnv, pub(super) system_env: SystemEnv, @@ -103,7 +103,7 @@ pub struct Vm { enforced_state_diffs: Option>, } -impl Vm { +impl Vm { pub fn custom(batch_env: L1BatchEnv, system_env: SystemEnv, storage: S) -> Self { let vm_version: FastVmVersion = VmVersion::from(system_env.version) .try_into() @@ -161,7 +161,6 @@ impl Vm { let mut this = Self { world: World::new(storage, program_cache), inner, - gas_for_account_validation: system_env.default_validation_computational_gas_limit, bootloader_state: BootloaderState::new( system_env.execution_mode, bootloader_memory.clone(), @@ -179,13 +178,218 @@ impl Vm { this } + 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) + .map(|word| self.read_word_from_bootloader_heap(word as usize)) + .collect::>() + .try_into() + .unwrap() + } + + fn get_tx_result(&self) -> U256 { + let tx_idx = self.bootloader_state.current_tx(); + let slot = get_result_success_first_slot(self.vm_version.into()) as usize + tx_idx; + self.read_word_from_bootloader_heap(slot) + } + + fn get_debug_log(&self) -> (String, String) { + let hook_params = self.get_hook_params(); + let mut msg = u256_to_h256(hook_params[0]).as_bytes().to_vec(); + // Trim 0 byte padding at the end. + while msg.last() == Some(&0) { + msg.pop(); + } + + let data = hook_params[1]; + let msg = String::from_utf8(msg).expect("Invalid debug message"); + + // For long data, it is better to use hex-encoding for greater readability + let data_str = if data > U256::from(u64::MAX) { + format!("0x{data:x}") + } else { + data.to_string() + }; + (msg, data_str) + } + + /// Should only be used when the bootloader is executing (e.g., when handling hooks). + pub(crate) fn read_word_from_bootloader_heap(&self, word: usize) -> U256 { + let start_address = word as u32 * 32; + self.inner.read_heap_u256(HeapId::FIRST, start_address) + } + + fn read_bytes_from_heap(&self, ptr: FatPointer) -> Vec { + assert_eq!(ptr.offset, 0); + (ptr.start..ptr.start + ptr.length) + .map(|addr| self.inner.read_heap_byte(ptr.memory_page, addr)) + .collect() + } + + pub(crate) fn has_previous_far_calls(&mut self) -> bool { + let callframe_count = self.inner.number_of_callframes(); + (1..callframe_count).any(|i| !self.inner.callframe(i).is_near_call()) + } + + /// Should only be used when the bootloader is executing (e.g., when handling hooks). + pub(crate) fn write_to_bootloader_heap( + &mut self, + memory: impl IntoIterator, + ) { + assert!( + !self.has_previous_far_calls(), + "Cannot write to bootloader heap when not in root call frame" + ); + + for (slot, value) in memory { + let start_address = slot as u32 * 32; + self.inner + .write_heap_u256(HeapId::FIRST, start_address, value); + } + } + + pub(crate) fn insert_bytecodes<'a>(&mut self, bytecodes: impl IntoIterator) { + for code in bytecodes { + let hash = BytecodeHash::for_bytecode(code).value_u256(); + self.world.bytecode_cache.insert(hash, code.into()); + } + } + + pub(crate) fn push_transaction_inner( + &mut self, + tx: zksync_types::Transaction, + refund: u64, + with_compression: bool, + ) { + let tx: TransactionData = tx.into(); + let overhead = tx.overhead_gas(); + + self.insert_bytecodes(tx.factory_deps.iter().map(|dep| &dep[..])); + + let compressed_bytecodes = if is_l1_tx_type(tx.tx_type) || !with_compression { + // L1 transactions do not need compression + vec![] + } else { + compress_bytecodes(&tx.factory_deps, |hash| { + self.inner + .world_diff() + .get_storage_state() + .get(&(KNOWN_CODES_STORAGE_ADDRESS, h256_to_u256(hash))) + .map(|x| !x.is_zero()) + .unwrap_or_else(|| self.world.storage.is_bytecode_known(&hash)) + }) + }; + + let trusted_ergs_limit = tx.trusted_ergs_limit(); + + let memory = self.bootloader_state.push_tx( + tx, + overhead, + refund, + compressed_bytecodes, + trusted_ergs_limit, + self.system_env.chain_id, + ); + + self.write_to_bootloader_heap(memory); + } + + #[cfg(test)] + pub(super) fn enforce_state_diffs(&mut self, diffs: Vec) { + self.enforced_state_diffs = Some(diffs); + } + + fn compute_state_diffs(&mut self) -> Vec { + #[cfg(test)] + if let Some(enforced_diffs) = self.enforced_state_diffs.take() { + return enforced_diffs; + } + + let storage = &mut self.world.storage; + let diffs = + self.inner + .world_diff() + .get_storage_changes() + .map(move |((address, key), change)| { + let storage_key = + StorageKey::new(AccountTreeId::new(address), u256_to_h256(key)); + StateDiffRecord { + address, + key, + derived_key: LogQuery::derive_final_address_for_params(&address, &key), + enumeration_index: storage + .get_enumeration_index(&storage_key) + .unwrap_or_default(), + initial_value: change.before, + final_value: change.after, + } + }); + diffs + .filter(|diff| diff.address != L1_MESSENGER_ADDRESS) + .collect() + } + + pub(crate) fn decommitted_hashes(&self) -> impl Iterator + '_ { + self.inner.world_diff().decommitted_hashes() + } + + pub(super) fn gas_remaining(&mut self) -> u32 { + self.inner.current_frame().gas() + } + + // visible for testing + pub(super) fn get_current_execution_state(&self) -> CurrentExecutionState { + let world_diff = self.inner.world_diff(); + let vm = &self.inner; + let events = merge_events(vm.events(), self.batch_env.number); + + let user_l2_to_l1_logs = extract_l2tol1logs_from_l1_messenger(&events) + .into_iter() + .map(Into::into) + .map(UserL2ToL1Log) + .collect(); + + CurrentExecutionState { + events, + deduplicated_storage_logs: world_diff + .get_storage_changes() + .map(|((address, key), change)| StorageLog { + key: StorageKey::new(AccountTreeId::new(address), u256_to_h256(key)), + value: u256_to_h256(change.after), + kind: StorageLogKind::RepeatedWrite, // Initialness doesn't matter here + }) + .collect(), + used_contract_hashes: self.decommitted_hashes().collect(), + system_logs: vm.l2_to_l1_logs().map(GlueInto::glue_into).collect(), + user_l2_to_l1_logs, + storage_refunds: world_diff.storage_refunds().to_vec(), + pubdata_costs: world_diff.pubdata_costs().to_vec(), + } + } +} + +struct AccountValidationGasSplit { + gas_given: u32, + gas_hidden: u32, +} + +impl Vm +where + S: ReadStorage, + Tr: Tracer + Default, + Val: ValidationTracer, +{ fn run( &mut self, execution_mode: VmExecutionMode, - tracer: &mut FullTracer, + tracer: &mut WithBuiltinTracers, track_refunds: bool, pubdata_builder: Option<&dyn PubdataBuilder>, ) -> VmRunResult { + let mut gas_left_for_account_validation = + self.system_env.default_validation_computational_gas_limit; + let mut account_validation_gas_split = None; + let mut refunds = Refunds { gas_refunded: 0, operator_suggested_refund: 0, @@ -227,10 +431,47 @@ impl Vm { } }; - match Hook::from_u32(hook) { - Hook::AccountValidationEntered | Hook::AccountValidationExited => { - // TODO (PLA-908): implement account validation + let hook = Hook::from_u32(hook); + match hook { + Hook::AccountValidationEntered => { + assert!( + account_validation_gas_split.is_none(), + "Account validation can't be nested" + ); + tracer.validation.account_validation_entered(); + + let gas = self.gas_remaining(); + let gas_given = gas.min(gas_left_for_account_validation); + account_validation_gas_split = Some(AccountValidationGasSplit { + gas_given, + gas_hidden: gas - gas_given, + }); + // As long as gasleft is allowed during account validation, + // the VM must not be used in the sequencer because a malicious + // account cause proving failure by checking if gasleft > 100k + self.inner.current_frame().set_gas(gas_given); + } + + Hook::ValidationExited => { + tracer.validation.validation_exited(); + + if let Some(AccountValidationGasSplit { + gas_given, + gas_hidden, + }) = account_validation_gas_split.take() + { + let gas_left = self.inner.current_frame().gas(); + gas_left_for_account_validation -= gas_given - gas_left; + self.inner.current_frame().set_gas(gas_left + gas_hidden); + } } + + Hook::ValidationStepEnded => { + if Val::STOP_AFTER_VALIDATION { + break (ExecutionResult::Success { output: vec![] }, true); + } + } + Hook::TxHasEnded => { if let VmExecutionMode::OneTx = execution_mode { // The bootloader may invoke `TxHasEnded` hook without posting a tx result previously. One case when this can happen @@ -355,6 +596,10 @@ impl Vm { state_diffs: self.compute_state_diffs(), }; + // Save the pubdata for the future initial bootloader memory building + self.bootloader_state + .set_pubdata_input(pubdata_input.clone()); + // Apply the pubdata to the current memory let mut memory_to_apply = vec![]; @@ -365,12 +610,9 @@ impl Vm { self.system_env.version, ); self.write_to_bootloader_heap(memory_to_apply); - - // Save the pubdata for the future initial bootloader memory building - self.bootloader_state.set_pubdata_input(pubdata_input); } - Hook::PaymasterValidationEntered | Hook::ValidationStepEnded => { /* unused */ } + Hook::PaymasterValidationEntered => { /* unused */ } Hook::DebugLog => { let (log, log_arg) = self.get_debug_log(); let last_tx = self.bootloader_state.last_l2_block().txs.last(); @@ -391,198 +633,9 @@ impl Vm { } } - 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) - .map(|word| self.read_word_from_bootloader_heap(word as usize)) - .collect::>() - .try_into() - .unwrap() - } - - fn get_tx_result(&self) -> U256 { - let tx_idx = self.bootloader_state.current_tx(); - let slot = get_result_success_first_slot(self.vm_version.into()) as usize + tx_idx; - self.read_word_from_bootloader_heap(slot) - } - - fn get_debug_log(&self) -> (String, String) { - let hook_params = self.get_hook_params(); - let mut msg = u256_to_h256(hook_params[0]).as_bytes().to_vec(); - // Trim 0 byte padding at the end. - while msg.last() == Some(&0) { - msg.pop(); - } - - let data = hook_params[1]; - let msg = String::from_utf8(msg).expect("Invalid debug message"); - - // For long data, it is better to use hex-encoding for greater readability - let data_str = if data > U256::from(u64::MAX) { - format!("0x{data:x}") - } else { - data.to_string() - }; - (msg, data_str) - } - - /// Should only be used when the bootloader is executing (e.g., when handling hooks). - pub(crate) fn read_word_from_bootloader_heap(&self, word: usize) -> U256 { - let start_address = word as u32 * 32; - self.inner.read_heap_u256(HeapId::FIRST, start_address) - } - - fn read_bytes_from_heap(&self, ptr: FatPointer) -> Vec { - assert_eq!(ptr.offset, 0); - (ptr.start..ptr.start + ptr.length) - .map(|addr| self.inner.read_heap_byte(ptr.memory_page, addr)) - .collect() - } - - pub(crate) fn has_previous_far_calls(&mut self) -> bool { - let callframe_count = self.inner.number_of_callframes(); - (1..callframe_count).any(|i| !self.inner.callframe(i).is_near_call()) - } - - /// Should only be used when the bootloader is executing (e.g., when handling hooks). - pub(crate) fn write_to_bootloader_heap( - &mut self, - memory: impl IntoIterator, - ) { - assert!( - !self.has_previous_far_calls(), - "Cannot write to bootloader heap when not in root call frame" - ); - - for (slot, value) in memory { - let start_address = slot as u32 * 32; - self.inner - .write_heap_u256(HeapId::FIRST, start_address, value); - } - } - - pub(crate) fn insert_bytecodes<'a>(&mut self, bytecodes: impl IntoIterator) { - for code in bytecodes { - let hash = BytecodeHash::for_bytecode(code).value_u256(); - self.world.bytecode_cache.insert(hash, code.into()); - } - } - - pub(crate) fn push_transaction_inner( - &mut self, - tx: zksync_types::Transaction, - refund: u64, - with_compression: bool, - ) { - let tx: TransactionData = tx.into(); - let overhead = tx.overhead_gas(); - - self.insert_bytecodes(tx.factory_deps.iter().map(|dep| &dep[..])); - - let compressed_bytecodes = if is_l1_tx_type(tx.tx_type) || !with_compression { - // L1 transactions do not need compression - vec![] - } else { - compress_bytecodes(&tx.factory_deps, |hash| { - self.inner - .world_diff() - .get_storage_state() - .get(&(KNOWN_CODES_STORAGE_ADDRESS, h256_to_u256(hash))) - .map(|x| !x.is_zero()) - .unwrap_or_else(|| self.world.storage.is_bytecode_known(&hash)) - }) - }; - - let trusted_ergs_limit = tx.trusted_ergs_limit(); - - let memory = self.bootloader_state.push_tx( - tx, - overhead, - refund, - compressed_bytecodes, - trusted_ergs_limit, - self.system_env.chain_id, - ); - - self.write_to_bootloader_heap(memory); - } - - #[cfg(test)] - pub(super) fn enforce_state_diffs(&mut self, diffs: Vec) { - self.enforced_state_diffs = Some(diffs); - } - - fn compute_state_diffs(&mut self) -> Vec { - #[cfg(test)] - if let Some(enforced_diffs) = self.enforced_state_diffs.take() { - return enforced_diffs; - } - - let storage = &mut self.world.storage; - let diffs = - self.inner - .world_diff() - .get_storage_changes() - .map(move |((address, key), change)| { - let storage_key = - StorageKey::new(AccountTreeId::new(address), u256_to_h256(key)); - StateDiffRecord { - address, - key, - derived_key: LogQuery::derive_final_address_for_params(&address, &key), - enumeration_index: storage - .get_enumeration_index(&storage_key) - .unwrap_or_default(), - initial_value: change.before, - final_value: change.after, - } - }); - diffs - .filter(|diff| diff.address != L1_MESSENGER_ADDRESS) - .collect() - } - - pub(crate) fn decommitted_hashes(&self) -> impl Iterator + '_ { - self.inner.world_diff().decommitted_hashes() - } - - pub(super) fn gas_remaining(&mut self) -> u32 { - self.inner.current_frame().gas() - } - - // visible for testing - pub(super) fn get_current_execution_state(&self) -> CurrentExecutionState { - let world_diff = self.inner.world_diff(); - let vm = &self.inner; - let events = merge_events(vm.events(), self.batch_env.number); - - let user_l2_to_l1_logs = extract_l2tol1logs_from_l1_messenger(&events) - .into_iter() - .map(Into::into) - .map(UserL2ToL1Log) - .collect(); - - CurrentExecutionState { - events, - deduplicated_storage_logs: world_diff - .get_storage_changes() - .map(|((address, key), change)| StorageLog { - key: StorageKey::new(AccountTreeId::new(address), u256_to_h256(key)), - value: u256_to_h256(change.after), - kind: StorageLogKind::RepeatedWrite, // Initialness doesn't matter here - }) - .collect(), - used_contract_hashes: self.decommitted_hashes().collect(), - system_logs: vm.l2_to_l1_logs().map(GlueInto::glue_into).collect(), - user_l2_to_l1_logs, - storage_refunds: world_diff.storage_refunds().to_vec(), - pubdata_costs: world_diff.pubdata_costs().to_vec(), - } - } - pub(crate) fn inspect_inner( &mut self, - tracer: &mut Tr, + tracer: &mut (Tr, Val), execution_mode: VmExecutionMode, pubdata_builder: Option<&dyn PubdataBuilder>, ) -> VmExecutionResultAndLogs { @@ -595,19 +648,18 @@ impl Vm { let start = self.inner.world_diff().snapshot(); let gas_before = self.gas_remaining(); + let (external, validation) = mem::take(tracer); + let mut full_tracer = + WithBuiltinTracers::new(external, validation, self.world.dynamic_bytecodes.clone()); - let mut full_tracer = ( - (mem::take(tracer), CircuitsTracer::default()), - EvmDeployTracer::new(self.world.dynamic_bytecodes.clone()), - ); let result = self.run( execution_mode, &mut full_tracer, track_refunds, pubdata_builder, ); - let ((external_tracer, circuits_tracer), _) = full_tracer; - *tracer = external_tracer; // place the tracer back + let circuit_statistic = full_tracer.circuit_statistic(); + *tracer = (full_tracer.external, full_tracer.validation); let ignore_world_diff = matches!(execution_mode, VmExecutionMode::OneTx) && result.should_ignore_vm_logs(); @@ -680,7 +732,7 @@ impl Vm { gas_remaining, computational_gas_used: gas_used, // since 1.5.0, this always has the same value as `gas_used` pubdata_published: result.pubdata_published, - circuit_statistic: circuits_tracer.circuit_statistic(), + circuit_statistic, contracts_used: 0, cycles_used: 0, total_log_queries: 0, @@ -691,10 +743,11 @@ impl Vm { } } -impl VmFactory> for Vm, Tr> +impl VmFactory> for Vm, Tr, Val> where S: ReadStorage, - Tr: Tracer + Default + 'static, + Tr: Tracer + Default, + Val: ValidationTracer, { fn new( batch_env: L1BatchEnv, @@ -706,8 +759,13 @@ where } } -impl VmInterface for Vm { - type TracerDispatcher = Tr; +impl VmInterface for Vm +where + S: ReadStorage, + Tr: Tracer + Default, + Val: ValidationTracer, +{ + type TracerDispatcher = (Tr, Val); fn push_transaction(&mut self, tx: Transaction) -> PushTransactionResult<'_> { self.push_transaction_inner(tx, 0, true); @@ -730,7 +788,7 @@ impl VmInterface for Vm { fn inspect_transaction_with_bytecode_compression( &mut self, tracer: &mut Self::TracerDispatcher, - tx: zksync_types::Transaction, + tx: Transaction, with_compression: bool, ) -> (BytecodeCompressionResult<'_>, VmExecutionResultAndLogs) { self.push_transaction_inner(tx, 0, with_compression); @@ -753,7 +811,7 @@ impl VmInterface for Vm { fn finish_batch(&mut self, pubdata_builder: Rc) -> FinishedL1Batch { let result = self.inspect_inner( - &mut Tr::default(), + &mut Default::default(), VmExecutionMode::Batch, Some(pubdata_builder.as_ref()), ); @@ -782,10 +840,14 @@ impl VmInterface for Vm { #[derive(Debug)] struct VmSnapshot { bootloader_snapshot: BootloaderStateSnapshot, - gas_for_account_validation: u32, } -impl VmInterfaceHistoryEnabled for Vm { +impl VmInterfaceHistoryEnabled for Vm +where + S: ReadStorage, + Tr: Tracer + Default, + Val: ValidationTracer, +{ fn make_snapshot(&mut self) { assert!( self.snapshot.is_none(), @@ -795,19 +857,16 @@ impl VmInterfaceHistoryEnabled f self.inner.make_snapshot(); self.snapshot = Some(VmSnapshot { bootloader_snapshot: self.bootloader_state.get_snapshot(), - gas_for_account_validation: self.gas_for_account_validation, }); } fn rollback_to_the_latest_snapshot(&mut self) { let VmSnapshot { bootloader_snapshot, - gas_for_account_validation, } = self.snapshot.take().expect("no snapshots to rollback to"); self.inner.rollback(); self.bootloader_state.apply_snapshot(bootloader_snapshot); - self.gas_for_account_validation = gas_for_account_validation; } fn pop_snapshot_no_rollback(&mut self) { @@ -816,19 +875,18 @@ impl VmInterfaceHistoryEnabled f } } -impl VmTrackingContracts for Vm { +impl VmTrackingContracts for Vm +where + Self: VmInterface, +{ fn used_contract_hashes(&self) -> Vec { self.decommitted_hashes().map(u256_to_h256).collect() } } -impl fmt::Debug for Vm { +impl fmt::Debug for Vm { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Vm") - .field( - "gas_for_account_validation", - &self.gas_for_account_validation, - ) .field("bootloader_state", &self.bootloader_state) .field("storage", &self.world.storage) .field("program_cache", &self.world.program_cache) diff --git a/core/lib/multivm/src/versions/vm_latest/tests/account_validation_rules.rs b/core/lib/multivm/src/versions/vm_latest/tests/account_validation_rules.rs new file mode 100644 index 000000000000..8ee6c06c1c69 --- /dev/null +++ b/core/lib/multivm/src/versions/vm_latest/tests/account_validation_rules.rs @@ -0,0 +1,8 @@ +use crate::{ + versions::testonly::account_validation_rules::test_account_validation_rules, vm_latest::Vm, +}; + +#[test] +fn test_account_validation_rules_legacy() { + test_account_validation_rules::>(); +} diff --git a/core/lib/multivm/src/versions/vm_latest/tests/mod.rs b/core/lib/multivm/src/versions/vm_latest/tests/mod.rs index aac3b1655b3a..0a89ddb0bf50 100644 --- a/core/lib/multivm/src/versions/vm_latest/tests/mod.rs +++ b/core/lib/multivm/src/versions/vm_latest/tests/mod.rs @@ -1,6 +1,7 @@ use std::{ collections::{HashMap, HashSet}, rc::Rc, + sync::Arc, }; use zk_evm_1_5_0::{ @@ -9,19 +10,24 @@ use zk_evm_1_5_0::{ zkevm_opcode_defs::{ContractCodeSha256Format, VersionedHashLen32}, }; use zksync_types::{ - bytecode::BytecodeHash, writes::StateDiffRecord, StorageKey, StorageValue, Transaction, H256, - U256, + bytecode::BytecodeHash, l2::L2Tx, vm::VmVersion, writes::StateDiffRecord, StorageKey, + StorageValue, Transaction, H256, U256, }; +use zksync_vm_interface::VmInterface; -use super::{HistoryEnabled, Vm}; +use super::{HistoryEnabled, ToTracerPointer, Vm}; use crate::{ interface::{ pubdata::{PubdataBuilder, PubdataInput}, storage::{InMemoryStorage, ReadStorage, StorageView, WriteStorage}, + tracer::ViolatedValidationRule, CurrentExecutionState, L2BlockEnv, VmExecutionMode, VmExecutionResultAndLogs, }, + tracers::ValidationTracer, utils::bytecode::bytes_to_be_words, - versions::testonly::{filter_out_base_system_contracts, TestedVm}, + versions::testonly::{ + filter_out_base_system_contracts, validation_params, TestedVm, TestedVmForValidation, + }, vm_latest::{ constants::BOOTLOADER_HEAP_PAGE, old_vm::{event_sink::InMemoryEventSink, history_recorder::HistoryRecorder}, @@ -36,6 +42,7 @@ mod bootloader; mod default_aa; // TODO - fix this test // `mod invalid_bytecode;` +mod account_validation_rules; mod block_tip; mod bytecode_publishing; mod call_tracer; @@ -195,6 +202,27 @@ impl TestedVm for TestedLatestVm { } } +impl TestedVmForValidation for TestedLatestVm { + fn run_validation(&mut self, tx: L2Tx, timestamp: u64) -> Option { + let validation_params = validation_params(&tx, &self.system_env); + self.push_transaction(tx.into()); + + let tracer = ValidationTracer::::new( + validation_params, + VmVersion::Vm1_5_0IncreasedBootloaderMemory, + timestamp, + ); + let mut failures = tracer.get_result(); + + self.inspect_inner( + &mut tracer.into_tracer_pointer().into(), + VmExecutionMode::OneTx, + None, + ); + Arc::make_mut(&mut failures).take() + } +} + #[derive(Clone, Debug)] pub(crate) struct ModifiedKeysMap(HashMap); diff --git a/core/lib/multivm/src/vm_instance.rs b/core/lib/multivm/src/vm_instance.rs index 9de99a7eb116..97af38ea0347 100644 --- a/core/lib/multivm/src/vm_instance.rs +++ b/core/lib/multivm/src/vm_instance.rs @@ -1,7 +1,6 @@ use std::{mem, rc::Rc}; use zksync_types::{vm::VmVersion, ProtocolVersionId, Transaction}; -use zksync_vm2::interface::Tracer; use zksync_vm_interface::{pubdata::PubdataBuilder, InspectExecutionMode}; use crate::{ @@ -14,8 +13,8 @@ use crate::{ VmMemoryMetrics, }, tracers::TracerDispatcher, - vm_fast::FastVmVersion, - vm_latest::HistoryEnabled, + vm_fast::{self, interface::Tracer, FastVmVersion}, + vm_latest::{self, HistoryEnabled}, }; /// Enumeration encompassing all supported legacy VM versions. @@ -35,7 +34,7 @@ pub enum LegacyVmInstance { VmBoojumIntegration(crate::vm_boojum_integration::Vm, H>), Vm1_4_1(crate::vm_1_4_1::Vm, H>), Vm1_4_2(crate::vm_1_4_2::Vm, H>), - Vm1_5_0(crate::vm_latest::Vm, H>), + Vm1_5_0(vm_latest::Vm, H>), } macro_rules! dispatch_legacy_vm { @@ -191,29 +190,29 @@ impl LegacyVmInstance { Self::Vm1_4_2(vm) } VmVersion::Vm1_5_0SmallBootloaderMemory => { - let vm = crate::vm_latest::Vm::new_with_subversion( + let vm = vm_latest::Vm::new_with_subversion( l1_batch_env, system_env, storage_view, - crate::vm_latest::MultiVmSubversion::SmallBootloaderMemory, + vm_latest::MultiVmSubversion::SmallBootloaderMemory, ); Self::Vm1_5_0(vm) } VmVersion::Vm1_5_0IncreasedBootloaderMemory => { - let vm = crate::vm_latest::Vm::new_with_subversion( + let vm = vm_latest::Vm::new_with_subversion( l1_batch_env, system_env, storage_view, - crate::vm_latest::MultiVmSubversion::IncreasedBootloaderMemory, + vm_latest::MultiVmSubversion::IncreasedBootloaderMemory, ); Self::Vm1_5_0(vm) } VmVersion::VmGateway => { - let vm = crate::vm_latest::Vm::new_with_subversion( + let vm = vm_latest::Vm::new_with_subversion( l1_batch_env, system_env, storage_view, - crate::vm_latest::MultiVmSubversion::Gateway, + vm_latest::MultiVmSubversion::Gateway, ); Self::Vm1_5_0(vm) } @@ -227,19 +226,19 @@ impl LegacyVmInstance { } /// Fast VM shadowed by the latest legacy VM. -pub type ShadowedFastVm = ShadowVm< +pub type ShadowedFastVm = ShadowVm< S, - crate::vm_latest::Vm, HistoryEnabled>, - crate::vm_fast::Vm, Tr>, + vm_latest::Vm, HistoryEnabled>, + vm_fast::Vm, Tr, Val>, >; /// Fast VM variants. #[derive(Debug)] -pub enum FastVmInstance { +pub enum FastVmInstance { /// Fast VM running in isolation. - Fast(crate::vm_fast::Vm, Tr>), + Fast(vm_fast::Vm, Tr, Val>), /// Fast VM shadowed by the latest legacy VM. - Shadowed(ShadowedFastVm), + Shadowed(ShadowedFastVm), } macro_rules! dispatch_fast_vm { @@ -251,10 +250,15 @@ macro_rules! dispatch_fast_vm { }; } -impl VmInterface for FastVmInstance { +impl VmInterface for FastVmInstance +where + S: ReadStorage, + Tr: Tracer + Default, + Val: vm_fast::ValidationTracer, +{ type TracerDispatcher = ( - crate::vm_latest::TracerDispatcher, HistoryEnabled>, - Tr, + vm_latest::TracerDispatcher, HistoryEnabled>, + (Tr, Val), ); fn push_transaction(&mut self, tx: Transaction) -> PushTransactionResult<'_> { @@ -299,8 +303,11 @@ impl VmInterface for FastVmInsta } } -impl VmInterfaceHistoryEnabled - for FastVmInstance +impl VmInterfaceHistoryEnabled for FastVmInstance +where + S: ReadStorage, + Tr: Tracer + Default, + Val: vm_fast::ValidationTracer, { fn make_snapshot(&mut self) { dispatch_fast_vm!(self.make_snapshot()); @@ -315,18 +322,19 @@ impl VmInterfaceHistoryEnabled } } -impl FastVmInstance { +impl FastVmInstance +where + S: ReadStorage, + Tr: Tracer + Default, + Val: vm_fast::ValidationTracer, +{ /// Creates an isolated fast VM. pub fn fast( l1_batch_env: L1BatchEnv, system_env: SystemEnv, storage_view: StoragePtr>, ) -> Self { - Self::Fast(crate::vm_fast::Vm::new( - l1_batch_env, - system_env, - storage_view, - )) + Self::Fast(vm_fast::Vm::new(l1_batch_env, system_env, storage_view)) } /// Creates a shadowed fast VM. diff --git a/core/lib/test_contracts/contracts/custom-account/validation-rule-breaker.sol b/core/lib/test_contracts/contracts/custom-account/validation-rule-breaker.sol new file mode 100644 index 000000000000..45961f705be7 --- /dev/null +++ b/core/lib/test_contracts/contracts/custom-account/validation-rule-breaker.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +pragma solidity ^0.8.0; + +import "./Constants.sol"; +import "./TransactionHelper.sol"; + +import "./SystemContractsCaller.sol"; + +import "./interfaces/IAccount.sol"; + +contract ValidationRuleBreaker is IAccount { + using TransactionHelper for Transaction; + + uint32 public typeOfRuleBreak; + address public trustedAddress = address(0x800a); + + constructor() { + typeOfRuleBreak = 0; + } + + function setTypeOfRuleBreak(uint32 _typeOfRuleBreak) external { + typeOfRuleBreak = _typeOfRuleBreak; + } + + function validateTransaction( + bytes32 _txHash, + bytes32 _suggestedSignedTxHash, + Transaction calldata _transaction + ) external payable override returns (bytes4 magic) { + // By default we consider the transaction as successful + magic = VALIDATION_SUCCESS_MAGIC; + + if (typeOfRuleBreak == 1) { + // The balance of another account may not be read + // I'm writing assertions because otherwise the compiler would optimize away the access + require(BOOTLOADER_FORMAL_ADDRESS.balance != 0); + } else if (typeOfRuleBreak == 2) { + // May not call an EOA + address(1234567890).call(""); + } else if (typeOfRuleBreak == 3) { + // This should succeed because a trustedAddress is marked as a slot that grants access to the address it contains + require(trustedAddress == address(0x800a)); + require(BOOTLOADER_FORMAL_ADDRESS.balance != 0); + } else if (typeOfRuleBreak == 4) { + // This should still fail; EIP-4337 defines out of gas as an immediate failure + address(this).call( + abi.encodeWithSignature("_runOutOfGasButCatchThePanic()") + ); + } + + _validateTransaction(_suggestedSignedTxHash, _transaction); + } + + function _runOutOfGasButCatchThePanic() external { + address(this).call( + abi.encodeWithSignature("_runOutOfGasButCatchThePanic()") + ); + } + + function _validateTransaction( + bytes32 _suggestedSignedTxHash, + Transaction calldata _transaction + ) internal { + SystemContractsCaller.systemCallWithPropagatedRevert( + uint32(gasleft()), + address(NONCE_HOLDER_SYSTEM_CONTRACT), + 0, + abi.encodeCall( + INonceHolder.incrementMinNonceIfEquals, + (_transaction.nonce) + ) + ); + } + + function executeTransaction( + bytes32, + bytes32, + Transaction calldata _transaction + ) external payable override { + _execute(_transaction); + } + + function executeTransactionFromOutside( + Transaction calldata _transaction + ) external payable override { + _validateTransaction(bytes32(0), _transaction); + _execute(_transaction); + } + + function _execute(Transaction calldata _transaction) internal { + address to = address(uint160(_transaction.to)); + uint256 value = _transaction.reserved[1]; + bytes memory data = _transaction.data; + + if (to == address(DEPLOYER_SYSTEM_CONTRACT)) { + // We allow calling ContractDeployer with any calldata + SystemContractsCaller.systemCallWithPropagatedRevert( + uint32(gasleft()), + to, + uint128(_transaction.reserved[1]), // By convention, reserved[1] is `value` + _transaction.data + ); + } else { + bool success; + assembly { + success := call( + gas(), + to, + value, + add(data, 0x20), + mload(data), + 0, + 0 + ) + } + require(success); + } + } + + // Here, the user pays the bootloader for the transaction + function payForTransaction( + bytes32, + bytes32, + Transaction calldata _transaction + ) external payable { + bool success = _transaction.payToTheBootloader(); + require(success, "Failed to pay the fee to the operator"); + } + + // Here, the user should prepare for the transaction to be paid for by a paymaster + // Here, the account should set the allowance for the smart contracts + function prepareForPaymaster( + bytes32, + bytes32, + Transaction calldata _transaction + ) external payable { + _transaction.processPaymasterInput(); + } + + fallback() external payable { + // fallback of default AA shouldn't be called by bootloader under no circumstances + assert(msg.sender != BOOTLOADER_FORMAL_ADDRESS); + + // If the contract is called directly, behave like an EOA + } + + receive() external payable {} +} diff --git a/core/lib/test_contracts/src/contracts.rs b/core/lib/test_contracts/src/contracts.rs index 36d758c46de2..a997f70f6870 100644 --- a/core/lib/test_contracts/src/contracts.rs +++ b/core/lib/test_contracts/src/contracts.rs @@ -115,6 +115,12 @@ impl TestContract { &CONTRACT } + pub fn validation_test() -> &'static Self { + static CONTRACT: Lazy = + Lazy::new(|| TestContract::new(raw::custom_account::ValidationRuleBreaker)); + &CONTRACT + } + /// Returns a contract testing precompiles. pub fn precompiles_test() -> &'static Self { static CONTRACT: Lazy = diff --git a/core/lib/vm_executor/src/batch/factory.rs b/core/lib/vm_executor/src/batch/factory.rs index 76ef244401bd..9797e1681032 100644 --- a/core/lib/vm_executor/src/batch/factory.rs +++ b/core/lib/vm_executor/src/batch/factory.rs @@ -37,7 +37,7 @@ pub trait BatchTracer: fmt::Debug + 'static + Send + Sealed { const TRACE_CALLS: bool; /// Tracer for the fast VM. #[doc(hidden)] - type Fast: vm_fast::interface::Tracer + Default + 'static; + type Fast: vm_fast::interface::Tracer + Default; } impl Sealed for () {} @@ -228,7 +228,7 @@ impl BatchVm { with_compression, ), Self::Fast(vm) => { - let mut tracer = (legacy_tracer.into(), ::default()); + let mut tracer = (legacy_tracer.into(), Default::default()); vm.inspect_transaction_with_bytecode_compression(&mut tracer, tx, with_compression) } }; diff --git a/core/lib/vm_executor/src/oneshot/mod.rs b/core/lib/vm_executor/src/oneshot/mod.rs index 0dfdb67bff52..e52a88e3e9c5 100644 --- a/core/lib/vm_executor/src/oneshot/mod.rs +++ b/core/lib/vm_executor/src/oneshot/mod.rs @@ -17,9 +17,9 @@ use once_cell::sync::OnceCell; use zksync_multivm::{ interface::{ executor::{OneshotExecutor, TransactionValidator}, - storage::{ReadStorage, StorageView, StorageWithOverrides}, + storage::{ReadStorage, StorageView, StorageWithOverrides, WriteStorage}, tracer::{ValidationError, ValidationParams, ValidationTraces}, - utils::{DivergenceHandler, ShadowVm}, + utils::{DivergenceHandler, ShadowMut, ShadowVm}, Call, ExecutionResult, InspectExecutionMode, OneshotEnv, OneshotTracingParams, OneshotTransactionExecutionResult, StoredL2BlockEnv, TxExecutionArgs, TxExecutionMode, VmFactory, VmInterface, @@ -27,9 +27,10 @@ use zksync_multivm::{ is_supported_by_fast_vm, tracers::{CallTracer, StorageInvocations, TracerDispatcher, ValidationTracer}, utils::adjust_pubdata_price_for_tx, + vm_fast, vm_latest::{HistoryDisabled, HistoryEnabled}, zk_evm_latest::ethereum_types::U256, - FastVmInstance, HistoryMode, LegacyVmInstance, MultiVmTracer, + FastVmInstance, HistoryMode, LegacyVmInstance, MultiVmTracer, VmVersion, }; use zksync_types::{ block::pack_block_info, @@ -180,7 +181,11 @@ where let l1_batch_env = env.l1_batch.clone(); let sandbox = VmSandbox { - fast_vm_mode: FastVmMode::Old, + fast_vm_mode: if !is_supported_by_fast_vm(env.system.version) { + FastVmMode::Old // the fast VM doesn't support old protocol versions + } else { + self.fast_vm_mode + }, panic_on_divergence: self.panic_on_divergence, storage, env, @@ -189,31 +194,35 @@ where }; tokio::task::spawn_blocking(move || { - let validation_tracer = ValidationTracer::::new( - validation_params, - sandbox.env.system.version.into(), - l1_batch_env, - ); - let mut validation_result = validation_tracer.get_result(); - let validation_traces = validation_tracer.get_traces(); - let tracers = vec![validation_tracer.into_tracer_pointer()]; - - let exec_result = sandbox.execute_in_vm(|vm, transaction| { - let Vm::Legacy(vm) = vm else { - unreachable!("Fast VM is never used for validation yet"); - }; - vm.push_transaction(transaction); - vm.inspect(&mut tracers.into(), InspectExecutionMode::OneTx) - }); - let validation_result = Arc::make_mut(&mut validation_result) - .take() - .map_or(Ok(()), Err); - - match (exec_result.result, validation_result) { - (_, Err(violated_rule)) => Err(ValidationError::ViolatedRule(violated_rule)), - (ExecutionResult::Halt { reason }, _) => Err(ValidationError::FailedTx(reason)), - _ => Ok(validation_traces.lock().unwrap().clone()), - } + let version = sandbox.env.system.version.into(); + let batch_timestamp = l1_batch_env.timestamp; + + sandbox.execute_in_vm(|vm, transaction| match vm { + Vm::Legacy(vm) => { + vm.push_transaction(transaction); + validate_legacy(vm, version, validation_params, batch_timestamp) + } + + Vm::Fast(FastVmInstance::Fast(vm)) => { + vm.push_transaction(transaction); + validate_fast(vm, validation_params, batch_timestamp) + } + + Vm::Fast(FastVmInstance::Shadowed(vm)) => { + vm.push_transaction(transaction); + vm.get_custom_mut("validation result", |vm| match vm { + ShadowMut::Main(vm) => validate_legacy::<_, HistoryEnabled>( + vm, + version, + validation_params.clone(), + batch_timestamp, + ), + ShadowMut::Shadow(vm) => { + validate_fast(vm, validation_params.clone(), batch_timestamp) + } + }) + } + }) }) .await .context("VM execution panicked") @@ -221,12 +230,12 @@ where } #[derive(Debug)] -enum Vm { +enum Vm { Legacy(LegacyVmInstance), - Fast(FastVmInstance), + Fast(FastVmInstance), } -impl Vm { +impl Vm { fn inspect_transaction_with_bytecode_compression( &mut self, missed_storage_invocation_limit: usize, @@ -252,7 +261,7 @@ impl Vm { missed_storage_invocation_limit, None, ); - let mut full_tracer = (legacy_tracers.into(), ()); + let mut full_tracer = (legacy_tracers.into(), ((), ())); vm.inspect_transaction_with_bytecode_compression( &mut full_tracer, tx, @@ -282,6 +291,57 @@ impl Vm { } } +fn validate_fast( + vm: &mut vm_fast::Vm, + validation_params: ValidationParams, + batch_timestamp: u64, +) -> Result { + let validation = vm_fast::FullValidationTracer::new(validation_params, batch_timestamp); + let mut tracer = ((), validation); + let result_and_logs = vm.inspect(&mut tracer, InspectExecutionMode::OneTx); + if let Some(violation) = tracer.1.validation_error() { + return Err(ValidationError::ViolatedRule(violation)); + } + + match result_and_logs.result { + ExecutionResult::Halt { reason } => Err(ValidationError::FailedTx(reason)), + ExecutionResult::Revert { .. } => { + unreachable!("Revert can only happen at the end of a transaction") + } + ExecutionResult::Success { .. } => Ok(tracer.1.traces()), + } +} + +fn validate_legacy( + vm: &mut impl VmInterface>>, + version: VmVersion, + validation_params: ValidationParams, + batch_timestamp: u64, +) -> Result +where + S: WriteStorage, + H: 'static + HistoryMode, + ValidationTracer: MultiVmTracer, +{ + let validation_tracer = ValidationTracer::::new(validation_params, version, batch_timestamp); + let mut validation_result = validation_tracer.get_result(); + let validation_traces = validation_tracer.get_traces(); + let validation_tracer: Box> = validation_tracer.into_tracer_pointer(); + let tracers = TracerDispatcher::from(validation_tracer); + + let exec_result = vm.inspect(&mut tracers.into(), InspectExecutionMode::OneTx); + + let validation_result = Arc::make_mut(&mut validation_result) + .take() + .map_or(Ok(()), Err); + + match (exec_result.result, validation_result) { + (_, Err(violated_rule)) => Err(ValidationError::ViolatedRule(violated_rule)), + (ExecutionResult::Halt { reason }, _) => Err(ValidationError::FailedTx(reason)), + _ => Ok(validation_traces.lock().unwrap().clone()), + } +} + /// Full parameters necessary to instantiate a VM for oneshot execution. #[derive(Debug)] struct VmSandbox { @@ -342,11 +402,14 @@ impl VmSandbox { } } - /// This method is blocking. - fn execute_in_vm( + fn execute_in_vm( mut self, - action: impl FnOnce(&mut Vm>, Transaction) -> T, - ) -> T { + action: impl FnOnce(&mut Vm, Tr, Val>, Transaction) -> T, + ) -> T + where + Tr: vm_fast::interface::Tracer + Default, + Val: vm_fast::ValidationTracer, + { Self::setup_storage( &mut self.storage, &self.execution_args, diff --git a/core/lib/vm_interface/src/types/tracer.rs b/core/lib/vm_interface/src/types/tracer.rs index 1c3f65f443ef..168834f28cef 100644 --- a/core/lib/vm_interface/src/types/tracer.rs +++ b/core/lib/vm_interface/src/types/tracer.rs @@ -71,7 +71,7 @@ pub struct TimestampAsserterParams { } /// Rules that can be violated when validating a transaction. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum ViolatedValidationRule { /// The transaction touched disallowed storage slots during validation. TouchedDisallowedStorageSlots(Address, U256), @@ -112,7 +112,7 @@ impl fmt::Display for ViolatedValidationRule { } /// Errors returned when validating a transaction. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum ValidationError { /// VM execution was halted during validation. FailedTx(Halt), @@ -124,7 +124,7 @@ pub enum ValidationError { /// For instance, the `timestamp_asserter_range` represent the range within which the transaction might make /// assertions on `block.timestamp`. This information is crucial for the caller, as expired transactions should /// be excluded from the mempool. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct ValidationTraces { pub timestamp_asserter_range: Option>, } diff --git a/core/lib/vm_interface/src/utils/shadow.rs b/core/lib/vm_interface/src/utils/shadow.rs index 0883971f4de8..d6a6d16c77a0 100644 --- a/core/lib/vm_interface/src/utils/shadow.rs +++ b/core/lib/vm_interface/src/utils/shadow.rs @@ -13,6 +13,7 @@ use super::dump::{DumpingVm, VmDump}; use crate::{ pubdata::PubdataBuilder, storage::{ReadStorage, StoragePtr, StorageView}, + tracer::{ValidationError, ValidationTraces}, BytecodeCompressionResult, CurrentExecutionState, FinishedL1Batch, InspectExecutionMode, L1BatchEnv, L2BlockEnv, PushTransactionResult, SystemEnv, VmExecutionResultAndLogs, VmFactory, VmInterface, VmInterfaceHistoryEnabled, VmTrackingContracts, @@ -224,6 +225,14 @@ impl CheckDivergence for FinishedL1Batch { } } +impl CheckDivergence for Result { + fn check_divergence(&self, other: &Self) -> DivergenceErrors { + let mut errors = DivergenceErrors::new(); + errors.check_match("validation result", self, other); + errors + } +} + /// Shadowed VM that executes 2 VMs for each operation and compares their outputs. /// /// If a divergence is detected, the VM state is dumped using [a pluggable handler](Self::set_dump_handler()), @@ -238,7 +247,6 @@ impl ShadowVm where S: ReadStorage, Main: VmTrackingContracts, - Shadow: VmInterface, { /// Sets the divergence handler to be used by this VM. pub fn set_divergence_handler(&mut self, handler: DivergenceHandler) { @@ -247,6 +255,18 @@ where } } + /// Dumps the current VM state. + pub fn dump_state(&self) -> VmDump { + self.main.dump_state() + } +} + +impl ShadowVm +where + S: ReadStorage, + Main: VmTrackingContracts, + Shadow: VmInterface, +{ /// Mutable ref is not necessary, but it automatically drops potential borrows. fn report(&mut self, err: DivergenceErrors) { self.report_shared(err); @@ -260,11 +280,6 @@ where .report(err, self.main.dump_state()); } - /// Dumps the current VM state. - pub fn dump_state(&self) -> VmDump { - self.main.dump_state() - } - /// Gets the specified value from both the main and shadow VM, checking whether it matches on both. pub fn get(&self, name: &str, mut action: impl FnMut(ShadowRef<'_, Main, Shadow>) -> R) -> R where @@ -330,7 +345,6 @@ impl ShadowVm where S: ReadStorage, Main: VmFactory> + VmTrackingContracts, - Shadow: VmInterface, { /// Creates a VM with a custom shadow storage. pub fn with_custom_shadow( diff --git a/core/lib/vm_interface/src/vm.rs b/core/lib/vm_interface/src/vm.rs index 2c25d729e318..f347bb54f550 100644 --- a/core/lib/vm_interface/src/vm.rs +++ b/core/lib/vm_interface/src/vm.rs @@ -22,7 +22,6 @@ use crate::{ }; pub trait VmInterface { - /// Lifetime is used to be able to define `Option<&mut _>` as a dispatcher. type TracerDispatcher: Default; /// Pushes a transaction to bootloader memory for future execution with bytecode compression (if it's supported by the VM). diff --git a/core/node/eth_watch/src/tests/mod.rs b/core/node/eth_watch/src/tests/mod.rs index df91074beb18..0b34a34ab63f 100644 --- a/core/node/eth_watch/src/tests/mod.rs +++ b/core/node/eth_watch/src/tests/mod.rs @@ -480,7 +480,7 @@ async fn test_batch_root_processor_from_genesis() { .await .unwrap() .unwrap(); - let proof1 = hex::encode(&bincode::serialize(&proof1).unwrap()); + let proof1 = hex::encode(bincode::serialize(&proof1).unwrap()); assert_eq!(proof1, "000000000600000000000000420000000000000030783030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303042000000000000003078303030303030303030303030303030303030303030303030303030303030303530303030303030303030303030303030303030303030303030303030303030334200000000000000307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030316639420000000000000030783031303230303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303042000000000000003078303932343932386331333737613663663234633339633264343666386562396466323365383131623236646333353237653534383339366664346531373362314200000000000000307833373561356266393039636230323134336533363935636136353865303634316537333961613539306630303034646261393335373263343463646239643264"); sl_client.set_last_finalized_block_number(11).await; @@ -492,7 +492,7 @@ async fn test_batch_root_processor_from_genesis() { .await .unwrap() .unwrap(); - let proof2 = hex::encode(&bincode::serialize(&proof2).unwrap()); + let proof2 = hex::encode(bincode::serialize(&proof2).unwrap()); assert_eq!(proof2, "0100000007000000000000004200000000000000307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303031420000000000000030783130613265663736653730396433313862343539626534396631653864376630326437313230663262353031626330616664646439333566316138313363363742000000000000003078303030303030303030303030303030303030303030303030303030303030303930303030303030303030303030303030303030303030303030303030303030334200000000000000307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030316639420000000000000030783031303230303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303042000000000000003078303932343932386331333737613663663234633339633264343666386562396466323365383131623236646333353237653534383339366664346531373362314200000000000000307861333738613230636132376237616533303731643162643763326164613030343639616263353765343239646436663438613833303932646237303539613138"); let proof3 = connection @@ -501,7 +501,7 @@ async fn test_batch_root_processor_from_genesis() { .await .unwrap() .unwrap(); - let proof3 = hex::encode(&bincode::serialize(&proof3).unwrap()); + let proof3 = hex::encode(bincode::serialize(&proof3).unwrap()); assert_eq!(proof3, "02000000080000000000000042000000000000003078303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030324200000000000000307834363730306234643430616335633335616632633232646461323738376139316562353637623036633932346138666238616539613035623230633038633231420000000000000030786530633333333066363734623662326435373866393538613164626436366631363464303638623062623561396662303737656361303133393736666461366642000000000000003078303030303030303030303030303030303030303030303030303030303030306230303030303030303030303030303030303030303030303030303030303030334200000000000000307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030316639420000000000000030783031303230303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303042000000000000003078303932343932386331333737613663663234633339633264343666386562396466323365383131623236646333353237653534383339366664346531373362314200000000000000307833373561356266393039636230323134336533363935636136353865303634316537333961613539306630303034646261393335373263343463646239643264"); } @@ -567,7 +567,7 @@ async fn test_batch_root_processor_restart() { .await .unwrap() .unwrap(); - let proof = hex::encode(&bincode::serialize(&proof).unwrap()); + let proof = hex::encode(bincode::serialize(&proof).unwrap()); assert_eq!(proof, "02000000080000000000000042000000000000003078303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030324200000000000000307834363730306234643430616335633335616632633232646461323738376139316562353637623036633932346138666238616539613035623230633038633231420000000000000030786530633333333066363734623662326435373866393538613164626436366631363464303638623062623561396662303737656361303133393736666461366642000000000000003078303030303030303030303030303030303030303030303030303030303030306230303030303030303030303030303030303030303030303030303030303030334200000000000000307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030316639420000000000000030783031303230303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303042000000000000003078303932343932386331333737613663663234633339633264343666386562396466323365383131623236646333353237653534383339366664346531373362314200000000000000307833373561356266393039636230323134336533363935636136353865303634316537333961613539306630303034646261393335373263343463646239643264"); let proof = connection @@ -576,7 +576,7 @@ async fn test_batch_root_processor_restart() { .await .unwrap() .unwrap(); - let proof = hex::encode(&bincode::serialize(&proof).unwrap()); + let proof = hex::encode(bincode::serialize(&proof).unwrap()); assert_eq!(proof, "02000000080000000000000042000000000000003078303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030334200000000000000307837623765373735373139343639366666393634616233353837393131373362636337663735356132656161393334653935373061636533393139383435313265420000000000000030786530633333333066363734623662326435373866393538613164626436366631363464303638623062623561396662303737656361303133393736666461366642000000000000003078303030303030303030303030303030303030303030303030303030303030306430303030303030303030303030303030303030303030303030303030303030334200000000000000307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030316639420000000000000030783031303230303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303042000000000000003078303932343932386331333737613663663234633339633264343666386562396466323365383131623236646333353237653534383339366664346531373362314200000000000000307835353063313735316338653764626166633839303939326634353532333636663064643565623665343362653535353936386264616338633732656466316261"); let proof = connection @@ -585,7 +585,7 @@ async fn test_batch_root_processor_restart() { .await .unwrap() .unwrap(); - let proof = hex::encode(&bincode::serialize(&proof).unwrap()); + let proof = hex::encode(bincode::serialize(&proof).unwrap()); assert_eq!(proof, "030000000900000000000000420000000000000030783030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303442000000000000003078303235663065363031353230366661626364326263613930316432633438396536336263356564346231356266356330633963363066396531363735383564614200000000000000307863633463343165646230633230333133343862323932623736386539626163316565386339326330396566386133323737633265636534303963313264383661420000000000000030783533656463316635616437396335393939626435373864666331333566396335316562643766616661343538356236346637316431356232646365316237323842000000000000003078303030303030303030303030303030303030303030303030303030303030306530303030303030303030303030303030303030303030303030303030303030334200000000000000307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030316639420000000000000030783031303230303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303042000000000000003078303932343932386331333737613663663234633339633264343666386562396466323365383131623236646333353237653534383339366664346531373362314200000000000000307833373561356266393039636230323134336533363935636136353865303634316537333961613539306630303034646261393335373263343463646239643264"); let proof = connection @@ -594,7 +594,7 @@ async fn test_batch_root_processor_restart() { .await .unwrap() .unwrap(); - let proof = hex::encode(&bincode::serialize(&proof).unwrap()); + let proof = hex::encode(bincode::serialize(&proof).unwrap()); assert_eq!(proof, "030000000900000000000000420000000000000030783030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303542000000000000003078323465653435363834376535373364313635613832333634306632303834383139636331613865333433316562633635633865363064333435343266313637324200000000000000307863633463343165646230633230333133343862323932623736386539626163316565386339326330396566386133323737633265636534303963313264383661420000000000000030783533656463316635616437396335393939626435373864666331333566396335316562643766616661343538356236346637316431356232646365316237323842000000000000003078303030303030303030303030303030303030303030303030303030303030306530303030303030303030303030303030303030303030303030303030303030334200000000000000307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030316639420000000000000030783031303230303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303042000000000000003078303932343932386331333737613663663234633339633264343666386562396466323365383131623236646333353237653534383339366664346531373362314200000000000000307833373561356266393039636230323134336533363935636136353865303634316537333961613539306630303034646261393335373263343463646239643264"); } diff --git a/core/node/node_sync/src/tree_data_fetcher/provider/tests.rs b/core/node/node_sync/src/tree_data_fetcher/provider/tests.rs index 75bf96092335..e8c855359390 100644 --- a/core/node/node_sync/src/tree_data_fetcher/provider/tests.rs +++ b/core/node/node_sync/src/tree_data_fetcher/provider/tests.rs @@ -418,7 +418,7 @@ async fn using_different_settlement_layers() { .batch_details(number, &get_last_l2_block(&mut storage, number).await) .await .unwrap() - .expect(&format!("no root hash for batch #{number}")); + .unwrap_or_else(|err| panic!("no root hash for batch #{number}: {err:?}")); assert_eq!(root_hash, H256::repeat_byte(number.0 as u8)); let past_l1_batch = provider.past_l1_batch.unwrap(); diff --git a/core/tests/vm-benchmark/src/vm.rs b/core/tests/vm-benchmark/src/vm.rs index e69e7ca1e909..a4b61ee54809 100644 --- a/core/tests/vm-benchmark/src/vm.rs +++ b/core/tests/vm-benchmark/src/vm.rs @@ -113,12 +113,11 @@ impl CountInstructions for Fast { } let (system_env, l1_batch_env) = test_env(); - let mut vm = - vm_fast::Vm::<_, InstructionCount>::custom(l1_batch_env, system_env, &*STORAGE); + let mut vm = vm_fast::Vm::custom(l1_batch_env, system_env, &*STORAGE); vm.push_transaction(tx.clone()); - let mut tracer = InstructionCount(0); + let mut tracer = (InstructionCount(0), ()); vm.inspect(&mut tracer, InspectExecutionMode::OneTx); - tracer.0 + tracer.0 .0 } }