diff --git a/.github/workflows/ci-core-reusable.yml b/.github/workflows/ci-core-reusable.yml index 36564600d832..7b559cf48124 100644 --- a/.github/workflows/ci-core-reusable.yml +++ b/.github/workflows/ci-core-reusable.yml @@ -63,7 +63,11 @@ jobs: run: ci_run yarn l1-contracts test - name: Rust unit tests - run: ci_run zk test rust + run: | + ci_run zk test rust + # Benchmarks are not tested by `cargo nextest` unless specified explicitly, and even then `criterion` harness is incompatible + # with how `cargo nextest` runs tests. Thus, we run criterion-based benchmark tests manually. + ci_run zk f cargo test --release -p vm-benchmark --bench criterion --bench fill_bootloader loadtest: runs-on: [matterlabs-ci-runner] diff --git a/Cargo.lock b/Cargo.lock index ce20580b3856..5dfbc235926b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1979,6 +1979,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "era_vm" +version = "0.1.0" +source = "git+https://github.com/lambdaclass/era_vm.git?branch=main#d3f6a983870b3807b65e33e1269569f7d38b4634" +dependencies = [ + "hex", + "lazy_static", + "primitive-types", + "rocksdb", + "thiserror", + "zk_evm_abstractions 1.5.1", + "zkevm_opcode_defs 1.5.0", +] + [[package]] name = "errno" version = "0.3.9" @@ -6559,18 +6573,18 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", @@ -7282,8 +7296,10 @@ version = "0.1.0" dependencies = [ "criterion", "iai", + "rand 0.8.5", "tokio", "vise", + "zksync_types", "zksync_vlog", "zksync_vm_benchmark_harness", ] @@ -7924,6 +7940,18 @@ dependencies = [ "zkevm_opcode_defs 0.150.0", ] +[[package]] +name = "zk_evm_abstractions" +version = "1.5.1" +source = "git+https://github.com/matter-labs/era-zk_evm_abstractions.git?branch=v1.5.1#eecb28df7d6dbcb728d4e9106933d288e4cb267e" +dependencies = [ + "anyhow", + "num_enum 0.6.1", + "serde", + "static_assertions", + "zkevm_opcode_defs 1.5.0", +] + [[package]] name = "zkevm_circuits" version = "0.140.2" @@ -8047,6 +8075,22 @@ dependencies = [ "sha3 0.10.8", ] +[[package]] +name = "zkevm_opcode_defs" +version = "1.5.0" +source = "git+https://github.com/matter-labs/era-zkevm_opcode_defs.git?branch=v1.5.1#9c470e3dbb093c4878b04b61e4d9459d94b41d45" +dependencies = [ + "bitflags 2.6.0", + "blake2 0.10.6", + "ethereum-types", + "k256 0.13.3", + "lazy_static", + "p256", + "serde", + "sha2 0.10.8", + "sha3 0.10.8", +] + [[package]] name = "zksync_base_token_adjuster" version = "0.1.0" @@ -8922,6 +8966,7 @@ dependencies = [ "circuit_sequencer_api 0.141.1", "circuit_sequencer_api 0.142.0", "circuit_sequencer_api 0.150.2-rc.3", + "era_vm", "ethabi", "hex", "itertools 0.10.5", @@ -9746,6 +9791,7 @@ dependencies = [ name = "zksync_vm_benchmark_harness" version = "0.1.0" dependencies = [ + "assert_matches", "once_cell", "zk_evm 0.133.0", "zksync_contracts", diff --git a/Cargo.toml b/Cargo.toml index 06bd6669b679..cbd50131d063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,7 +172,7 @@ strum = "0.26" tempfile = "3.0.2" test-casing = "0.1.2" test-log = "0.2.15" -thiserror = "1" +thiserror = "1.0.62" thread_local = "1.1" tikv-jemallocator = "0.5" tiny-keccak = "2" @@ -298,3 +298,6 @@ zksync_contract_verification_server = { version = "0.1.0", path = "core/node/con zksync_node_api_server = { version = "0.1.0", path = "core/node/api_server" } zksync_tee_verifier_input_producer = { version = "0.1.0", path = "core/node/tee_verifier_input_producer" } zksync_base_token_adjuster = { version = "0.1.0", path = "core/node/base_token_adjuster" } + +[patch.crates-io] +iai = { git = "https://github.com/lambdaclass/iai", branch = "fix/valgrind-forward-compatibility" } diff --git a/core/lib/multivm/Cargo.toml b/core/lib/multivm/Cargo.toml index fc35f152ae19..f2d4dc58af86 100644 --- a/core/lib/multivm/Cargo.toml +++ b/core/lib/multivm/Cargo.toml @@ -17,6 +17,8 @@ zk_evm_1_4_0.workspace = true zk_evm_1_3_3.workspace = true zk_evm_1_3_1.workspace = true vm2.workspace = true +era_vm = {path = "../../../../"} +# era_vm = {git = "https://github.com/lambdaclass/era_vm.git", branch = "main"} circuit_sequencer_api_1_3_3.workspace = true circuit_sequencer_api_1_4_0.workspace = true diff --git a/core/lib/multivm/src/glue/tracers/mod.rs b/core/lib/multivm/src/glue/tracers/mod.rs index 7aa792ef1f71..432d569979b8 100644 --- a/core/lib/multivm/src/glue/tracers/mod.rs +++ b/core/lib/multivm/src/glue/tracers/mod.rs @@ -30,7 +30,7 @@ //! - Add this trait as a trait bound for `T` in `MultiVMTracer` implementation. //! - Implement the trait for `T` with a bound to `VmTracer` for a specific version. -use zksync_state::WriteStorage; +use zksync_state::{ImmutableStorageView, WriteStorage}; use crate::{tracers::old_tracers::OldTracers, HistoryMode}; @@ -44,6 +44,7 @@ pub trait MultiVMTracer: + IntoVm1_4_1IntegrationTracer + IntoVm1_4_2IntegrationTracer + IntoOldVmTracer + + IntoEraVmTracer { fn into_tracer_pointer(self) -> MultiVmTracerPointer where @@ -57,6 +58,10 @@ pub trait IntoLatestTracer { fn latest(&self) -> crate::vm_latest::TracerPointer; } +pub trait IntoEraVmTracer { + fn into_era_vm(&self) -> Box>; +} + pub trait IntoVmVirtualBlocksTracer { fn vm_virtual_blocks( &self, @@ -106,6 +111,17 @@ where } } +impl IntoEraVmTracer for T +where + S: WriteStorage, + H: HistoryMode, + T: crate::era_vm::tracers::traits::VmTracer + Clone + 'static, +{ + fn into_era_vm(&self) -> Box> { + Box::new(self.clone()) + } +} + impl IntoVmVirtualBlocksTracer for T where S: WriteStorage, @@ -180,6 +196,7 @@ where + IntoVmBoojumIntegrationTracer + IntoVm1_4_1IntegrationTracer + IntoVm1_4_2IntegrationTracer - + IntoOldVmTracer, + + IntoOldVmTracer + + IntoEraVmTracer, { } diff --git a/core/lib/multivm/src/lib.rs b/core/lib/multivm/src/lib.rs index 08b077ce3eab..b4ec4a385829 100644 --- a/core/lib/multivm/src/lib.rs +++ b/core/lib/multivm/src/lib.rs @@ -12,8 +12,8 @@ pub use crate::{ tracers::{MultiVMTracer, MultiVmTracerPointer}, }, versions::{ - vm_1_3_2, vm_1_4_1, vm_1_4_2, vm_boojum_integration, vm_fast, vm_latest, vm_m5, vm_m6, - vm_refunds_enhancement, vm_virtual_blocks, + era_vm, vm_1_3_2, vm_1_4_1, vm_1_4_2, vm_boojum_integration, vm_fast, vm_latest, vm_m5, + vm_m6, vm_refunds_enhancement, vm_virtual_blocks, }, vm_instance::VmInstance, }; diff --git a/core/lib/multivm/src/tracers/call_tracer/era_vm/mod.rs b/core/lib/multivm/src/tracers/call_tracer/era_vm/mod.rs new file mode 100644 index 000000000000..31d06610a4ec --- /dev/null +++ b/core/lib/multivm/src/tracers/call_tracer/era_vm/mod.rs @@ -0,0 +1,188 @@ +use era_vm::{ + opcode::{RetOpcode, Variant}, + value::FatPointer, + Execution, Opcode, +}; +use zksync_state::ReadStorage; +use zksync_types::{ + vm_trace::{Call, CallType}, + zk_evm_types::FarCallOpcode, + CONTRACT_DEPLOYER_ADDRESS, U256, +}; + +use super::CallTracer; +use crate::{ + era_vm::tracers::traits::{Tracer, VmTracer}, + interface::VmRevertReason, +}; + +impl Tracer for CallTracer { + fn after_execution( + &mut self, + opcode: &Opcode, + execution: &mut era_vm::Execution, + _state: &mut era_vm::state::VMState, + ) { + match opcode.variant { + Variant::NearCall(_) => { + self.increase_near_call_count(); + } + Variant::FarCall(far_call) => { + // We use parent gas for properly calculating gas used in the trace. + let current_ergs = execution.gas_left().unwrap(); + let parent_gas = execution + .running_contexts + .last() + .map(|call| call.frame.gas_left.0.saturating_add(current_ergs)) + .unwrap_or(current_ergs) as u64; + + // we need to do this cast because `Call` uses another library + let far_call_variant = match far_call as u8 { + 0 => FarCallOpcode::Normal, + 1 => FarCallOpcode::Delegate, + 2 => FarCallOpcode::Mimic, + _ => FarCallOpcode::Normal, // Should never happen + }; + + let mut current_call = Call { + r#type: CallType::Call(far_call_variant), + gas: 0, + parent_gas: parent_gas as u64, + ..Default::default() + }; + + self.handle_far_call_op_code_era(execution, &mut current_call); + self.push_call_and_update_stats(current_call, 0); + } + Variant::Ret(ret_code) => { + self.handle_ret_op_code_era(execution, ret_code); + } + _ => {} + }; + } +} + +impl VmTracer for CallTracer { + fn after_bootloader_execution(&mut self, _state: &mut crate::era_vm::vm::Vm) { + self.store_result(); + } +} + +impl CallTracer { + fn handle_far_call_op_code_era(&mut self, execution: &Execution, current_call: &mut Call) { + // since this is a far_call, the current_context represents the current frame + let current = execution.current_context().unwrap(); + // All calls from the actual users are mimic calls, + // so we need to check that the previous call was to the deployer. + // Actually it's a call of the constructor. + // And at this stage caller is user and callee is deployed contract. + let call_type = if let CallType::Call(far_call) = current_call.r#type { + if matches!(far_call, FarCallOpcode::Mimic) { + let previous_caller = execution + .running_contexts + .first() + .map(|call| call.caller) + // Actually it's safe to just unwrap here, because we have at least one call in the stack + // But i want to be sure that we will not have any problems in the future + .unwrap_or(current.caller); + if previous_caller == CONTRACT_DEPLOYER_ADDRESS { + CallType::Create + } else { + CallType::Call(far_call) + } + } else { + CallType::Call(far_call) + } + } else { + return; + }; + let calldata = if current.heap_id == 0 || current.frame.gas_left.0 == 0 { + vec![] + } else { + let packed_abi = execution.get_register(1); + assert!(packed_abi.is_pointer); + let pointer = FatPointer::decode(packed_abi.value); + execution + .heaps + .get(pointer.page) + .unwrap() + .read_unaligned_from_pointer(&pointer) + .unwrap_or_default() + }; + + current_call.input = calldata; + current_call.r#type = call_type; + current_call.from = current.caller; + current_call.to = current.contract_address; + current_call.value = U256::from(current.context_u128); + current_call.gas = current.frame.gas_left.0 as u64; + } + + fn save_output_era( + &mut self, + execution: &Execution, + ret_opcode: RetOpcode, + current_call: &mut Call, + ) { + let fat_data_pointer = execution.get_register(1); + + // if `fat_data_pointer` is not a pointer then there is no output + let output = match fat_data_pointer.is_pointer { + true => { + let fat_data_pointer = FatPointer::decode(fat_data_pointer.value); + match (fat_data_pointer.len, fat_data_pointer.offset) { + (0, 0) => execution + .heaps + .get(fat_data_pointer.page) + .and_then(|ptr| ptr.read_unaligned_from_pointer(&fat_data_pointer).ok()), + _ => None, + } + } + _ => None, + }; + + match ret_opcode { + RetOpcode::Ok => { + current_call.output = output.unwrap_or_default(); + } + RetOpcode::Revert => { + if let Some(output) = output { + current_call.revert_reason = + Some(VmRevertReason::from(output.as_slice()).to_string()); + } else { + current_call.revert_reason = Some("Unknown revert reason".to_string()); + } + } + RetOpcode::Panic => { + current_call.error = Some("Panic".to_string()); + } + } + } + + fn handle_ret_op_code_era(&mut self, execution: &Execution, ret_opcode: RetOpcode) { + let Some(mut current_call) = self.stack.pop() else { + return; + }; + + if current_call.near_calls_after > 0 { + current_call.near_calls_after -= 1; + self.push_call_and_update_stats(current_call.farcall, current_call.near_calls_after); + return; + } + + current_call.farcall.gas_used = current_call + .farcall + .parent_gas + .saturating_sub(execution.gas_left().unwrap() as u64); + + self.save_output_era(execution, ret_opcode, &mut current_call.farcall); + + // If there is a parent call, push the current call to it + // Otherwise, push the current call to the stack, because it's the top level call + if let Some(parent_call) = self.stack.last_mut() { + parent_call.farcall.calls.push(current_call.farcall); + } else { + self.push_call_and_update_stats(current_call.farcall, current_call.near_calls_after); + } + } +} diff --git a/core/lib/multivm/src/tracers/call_tracer/mod.rs b/core/lib/multivm/src/tracers/call_tracer/mod.rs index 855768067b8a..1f176ae02e39 100644 --- a/core/lib/multivm/src/tracers/call_tracer/mod.rs +++ b/core/lib/multivm/src/tracers/call_tracer/mod.rs @@ -5,6 +5,7 @@ use zksync_types::vm_trace::Call; use crate::{glue::tracers::IntoOldVmTracer, tracers::call_tracer::metrics::CALL_METRICS}; +pub mod era_vm; mod metrics; pub mod vm_1_4_1; pub mod vm_1_4_2; diff --git a/core/lib/multivm/src/tracers/multivm_dispatcher.rs b/core/lib/multivm/src/tracers/multivm_dispatcher.rs index 5b0d36b5e793..e0126f18ed8c 100644 --- a/core/lib/multivm/src/tracers/multivm_dispatcher.rs +++ b/core/lib/multivm/src/tracers/multivm_dispatcher.rs @@ -1,6 +1,6 @@ -use zksync_state::WriteStorage; +use zksync_state::{ImmutableStorageView, StorageView, WriteStorage}; -use crate::{tracers::old_tracers, HistoryMode, MultiVmTracerPointer}; +use crate::{tracers::old_tracers, HistoryMode, MultiVMTracer, MultiVmTracerPointer}; /// Tracer dispatcher is a tracer that can dispatch calls to multiple tracers. pub struct TracerDispatcher { @@ -37,6 +37,14 @@ impl From> } } +impl From> + for crate::era_vm::tracers::dispatcher::TracerDispatcher +{ + fn from(value: TracerDispatcher) -> Self { + Self::new(value.tracers.into_iter().map(|x| x.into_era_vm()).collect()) + } +} + impl From> for crate::vm_boojum_integration::TracerDispatcher { diff --git a/core/lib/multivm/src/tracers/storage_invocation/era_vm/mod.rs b/core/lib/multivm/src/tracers/storage_invocation/era_vm/mod.rs new file mode 100644 index 000000000000..e15c53bc4061 --- /dev/null +++ b/core/lib/multivm/src/tracers/storage_invocation/era_vm/mod.rs @@ -0,0 +1,22 @@ +use era_vm::{ + opcode::{RetOpcode, Variant}, + value::FatPointer, + Execution, Opcode, +}; +use zksync_state::ReadStorage; +use zksync_types::{ + vm_trace::{Call, CallType}, + zk_evm_types::FarCallOpcode, + CONTRACT_DEPLOYER_ADDRESS, U256, +}; + +use super::StorageInvocations; +use crate::{ + era_vm::tracers::traits::{Tracer, VmTracer}, + interface::VmRevertReason, +}; + +//TODO: Implement the Tracer trait for StorageInvocations +impl Tracer for StorageInvocations {} + +impl VmTracer for StorageInvocations {} diff --git a/core/lib/multivm/src/tracers/storage_invocation/mod.rs b/core/lib/multivm/src/tracers/storage_invocation/mod.rs index 8c9677ba8d7c..49ca28e2f961 100644 --- a/core/lib/multivm/src/tracers/storage_invocation/mod.rs +++ b/core/lib/multivm/src/tracers/storage_invocation/mod.rs @@ -1,5 +1,6 @@ use crate::{glue::tracers::IntoOldVmTracer, tracers::old_tracers::OldTracers}; +pub mod era_vm; pub mod vm_1_4_1; pub mod vm_1_4_2; pub mod vm_boojum_integration; diff --git a/core/lib/multivm/src/tracers/validator/era_vm/mod.rs b/core/lib/multivm/src/tracers/validator/era_vm/mod.rs new file mode 100644 index 000000000000..806fb641ee48 --- /dev/null +++ b/core/lib/multivm/src/tracers/validator/era_vm/mod.rs @@ -0,0 +1,22 @@ +use era_vm::{ + opcode::{RetOpcode, Variant}, + value::FatPointer, + Execution, Opcode, +}; +use zksync_state::ReadStorage; +use zksync_types::{ + vm_trace::{Call, CallType}, + zk_evm_types::FarCallOpcode, + CONTRACT_DEPLOYER_ADDRESS, U256, +}; + +use super::ValidationTracer; +use crate::{ + era_vm::tracers::traits::{Tracer, VmTracer}, + interface::VmRevertReason, +}; + +//TODO: Implement the Tracer trait for ValidationTracer +impl Tracer for ValidationTracer {} + +impl VmTracer for ValidationTracer {} diff --git a/core/lib/multivm/src/tracers/validator/mod.rs b/core/lib/multivm/src/tracers/validator/mod.rs index 635915f95278..daf93c3fb8f9 100644 --- a/core/lib/multivm/src/tracers/validator/mod.rs +++ b/core/lib/multivm/src/tracers/validator/mod.rs @@ -18,6 +18,7 @@ use crate::{ tracers::validator::types::{NewTrustedValidationItems, ValidationTracerMode}, }; +mod era_vm; mod types; mod vm_1_4_1; mod vm_1_4_2; diff --git a/core/lib/multivm/src/versions/era_vm/bootloader_state/l2_block.rs b/core/lib/multivm/src/versions/era_vm/bootloader_state/l2_block.rs new file mode 100644 index 000000000000..009f2040d20b --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/bootloader_state/l2_block.rs @@ -0,0 +1,85 @@ +use std::cmp::Ordering; + +use zksync_types::{L2BlockNumber, H256}; +use zksync_utils::concat_and_hash; + +use super::{snapshot::L2BlockSnapshot, tx::BootloaderTx}; +use crate::{ + interface::{L2Block, L2BlockEnv}, + vm_latest::utils::l2_blocks::l2_block_hash, +}; + +const EMPTY_TXS_ROLLING_HASH: H256 = H256::zero(); + +#[derive(Debug, Clone)] +pub(crate) struct BootloaderL2Block { + pub(crate) number: u32, + pub(crate) timestamp: u64, + pub(crate) txs_rolling_hash: H256, // The rolling hash of all the transactions in the miniblock + pub(crate) prev_block_hash: H256, + // Number of the first L2 block tx in L1 batch + pub(crate) first_tx_index: usize, + pub(crate) max_virtual_blocks_to_create: u32, + pub(crate) txs: Vec, +} + +impl BootloaderL2Block { + pub(crate) fn new(l2_block: L2BlockEnv, first_tx_place: usize) -> Self { + Self { + number: l2_block.number, + timestamp: l2_block.timestamp, + txs_rolling_hash: EMPTY_TXS_ROLLING_HASH, + prev_block_hash: l2_block.prev_block_hash, + first_tx_index: first_tx_place, + max_virtual_blocks_to_create: l2_block.max_virtual_blocks_to_create, + txs: vec![], + } + } + + pub(super) fn push_tx(&mut self, tx: BootloaderTx) { + self.update_rolling_hash(tx.hash); + self.txs.push(tx) + } + + pub(crate) fn get_hash(&self) -> H256 { + l2_block_hash( + L2BlockNumber(self.number), + self.timestamp, + self.prev_block_hash, + self.txs_rolling_hash, + ) + } + + fn update_rolling_hash(&mut self, tx_hash: H256) { + self.txs_rolling_hash = concat_and_hash(self.txs_rolling_hash, tx_hash) + } + + pub(crate) fn interim_version(&self) -> BootloaderL2Block { + let mut interim = self.clone(); + interim.max_virtual_blocks_to_create = 0; + interim + } + + pub(crate) fn make_snapshot(&self) -> L2BlockSnapshot { + L2BlockSnapshot { + txs_rolling_hash: self.txs_rolling_hash, + txs_len: self.txs.len(), + } + } + + pub(crate) fn apply_snapshot(&mut self, snapshot: L2BlockSnapshot) { + self.txs_rolling_hash = snapshot.txs_rolling_hash; + match self.txs.len().cmp(&snapshot.txs_len) { + Ordering::Greater => self.txs.truncate(snapshot.txs_len), + Ordering::Less => panic!("Applying snapshot from future is not supported"), + Ordering::Equal => {} + } + } + pub(crate) fn l2_block(&self) -> L2Block { + L2Block { + number: self.number, + timestamp: self.timestamp, + hash: self.get_hash(), + } + } +} diff --git a/core/lib/multivm/src/versions/era_vm/bootloader_state/mod.rs b/core/lib/multivm/src/versions/era_vm/bootloader_state/mod.rs new file mode 100644 index 000000000000..73830de2759b --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/bootloader_state/mod.rs @@ -0,0 +1,8 @@ +mod l2_block; +mod snapshot; +mod state; +mod tx; + +pub(crate) mod utils; +pub(crate) use snapshot::BootloaderStateSnapshot; +pub use state::BootloaderState; diff --git a/core/lib/multivm/src/versions/era_vm/bootloader_state/snapshot.rs b/core/lib/multivm/src/versions/era_vm/bootloader_state/snapshot.rs new file mode 100644 index 000000000000..8f1cec3cb7f1 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/bootloader_state/snapshot.rs @@ -0,0 +1,25 @@ +use zksync_types::H256; + +#[derive(Debug, Clone)] +pub(crate) struct BootloaderStateSnapshot { + /// ID of the next transaction to be executed. + pub(crate) tx_to_execute: usize, + /// Stored L2 blocks in bootloader memory + pub(crate) l2_blocks_len: usize, + /// Snapshot of the last L2 block. Only this block could be changed during the rollback + pub(crate) last_l2_block: L2BlockSnapshot, + /// The number of 32-byte words spent on the already included compressed bytecodes. + pub(crate) compressed_bytecodes_encoding: usize, + /// Current offset of the free space in the bootloader memory. + pub(crate) free_tx_offset: usize, + /// Whether the pubdata information has been provided already + pub(crate) is_pubdata_information_provided: bool, +} + +#[derive(Debug, Clone)] +pub(crate) struct L2BlockSnapshot { + /// The rolling hash of all the transactions in the miniblock + pub(crate) txs_rolling_hash: H256, + /// The number of transactions in the last L2 block + pub(crate) txs_len: usize, +} diff --git a/core/lib/multivm/src/versions/era_vm/bootloader_state/state.rs b/core/lib/multivm/src/versions/era_vm/bootloader_state/state.rs new file mode 100644 index 000000000000..a97bccd661bd --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/bootloader_state/state.rs @@ -0,0 +1,292 @@ +use std::cmp::Ordering; + +use once_cell::sync::OnceCell; +use zksync_types::{L2ChainId, U256}; +use zksync_utils::bytecode::CompressedBytecodeInfo; + +use super::{ + l2_block::BootloaderL2Block, + tx::BootloaderTx, + utils::{apply_l2_block, apply_pubdata_to_memory, apply_tx_to_memory, PubdataInput}, + BootloaderStateSnapshot, +}; +use crate::{ + era_vm::transaction_data::TransactionData, + interface::{BootloaderMemory, L2BlockEnv, TxExecutionMode}, + vm_latest::{constants::TX_DESCRIPTION_OFFSET, utils::l2_blocks::assert_next_block}, +}; + +/// Intermediate bootloader-related VM state. +/// +/// Required to process transactions one by one (since we intercept the VM execution to execute +/// transactions and add new ones to the memory on the fly). +/// Keeps tracking everything related to the bootloader memory and can restore the whole memory. +/// +/// +/// Serves two purposes: +/// - Tracks where next tx should be pushed to in the bootloader memory. +/// - Tracks which transaction should be executed next. +#[derive(Debug, Clone)] +pub struct BootloaderState { + /// ID of the next transaction to be executed. + /// See the structure doc-comment for a better explanation of purpose. + tx_to_execute: usize, + /// Stored txs in bootloader memory + l2_blocks: Vec, + /// The number of 32-byte words spent on the already included compressed bytecodes. + compressed_bytecodes_encoding: usize, + /// Initial memory of bootloader + initial_memory: BootloaderMemory, + /// Mode of txs for execution, it can be changed once per vm lunch + execution_mode: TxExecutionMode, + /// Current offset of the free space in the bootloader memory. + free_tx_offset: usize, + /// Information about the the pubdata that will be needed to supply to the L1Messenger + pubdata_information: OnceCell, +} + +impl BootloaderState { + pub(crate) fn new( + execution_mode: TxExecutionMode, + initial_memory: BootloaderMemory, + first_l2_block: L2BlockEnv, + ) -> Self { + let l2_block = BootloaderL2Block::new(first_l2_block, 0); + Self { + tx_to_execute: 0, + compressed_bytecodes_encoding: 0, + l2_blocks: vec![l2_block], + initial_memory, + execution_mode, + free_tx_offset: 0, + pubdata_information: Default::default(), + } + } + + pub(crate) fn set_refund_for_current_tx(&mut self, refund: u64) { + let current_tx = self.current_tx(); + // We can't set the refund for the latest tx or using the latest l2_block for fining tx + // Because we can fill the whole batch first and then execute txs one by one + let tx = self.find_tx_mut(current_tx); + tx.refund = refund; + } + + pub(crate) fn set_pubdata_input(&mut self, info: PubdataInput) { + self.pubdata_information + .set(info) + .expect("Pubdata information is already set"); + } + + pub(crate) fn start_new_l2_block(&mut self, l2_block: L2BlockEnv) { + let last_block = self.last_l2_block(); + assert!( + !last_block.txs.is_empty(), + "Can not create new miniblocks on top of empty ones" + ); + assert_next_block(&last_block.l2_block(), &l2_block); + self.push_l2_block(l2_block); + } + + /// This method bypass sanity checks and should be used carefully. + pub(crate) fn push_l2_block(&mut self, l2_block: L2BlockEnv) { + self.l2_blocks + .push(BootloaderL2Block::new(l2_block, self.free_tx_index())) + } + + pub(crate) fn push_tx( + &mut self, + tx: TransactionData, + predefined_overhead: u32, + predefined_refund: u64, + compressed_bytecodes: Vec, + trusted_ergs_limit: U256, + chain_id: L2ChainId, + ) -> BootloaderMemory { + let tx_offset = self.free_tx_offset(); + let bootloader_tx = BootloaderTx::new( + tx, + predefined_refund, + predefined_overhead, + trusted_ergs_limit, + compressed_bytecodes, + tx_offset, + chain_id, + ); + + let mut memory = vec![]; + let compressed_bytecode_size = apply_tx_to_memory( + &mut memory, + &bootloader_tx, + self.last_l2_block(), + self.free_tx_index(), + self.free_tx_offset(), + self.compressed_bytecodes_encoding, + self.execution_mode, + self.last_l2_block().txs.is_empty(), + ); + 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 + } + + pub(crate) fn last_l2_block(&self) -> &BootloaderL2Block { + self.l2_blocks.last().unwrap() + } + pub(crate) fn get_pubdata_information(&self) -> &PubdataInput { + self.pubdata_information + .get() + .expect("Pubdata information is not set") + } + + fn last_mut_l2_block(&mut self) -> &mut BootloaderL2Block { + self.l2_blocks.last_mut().unwrap() + } + + /// Apply all bootloader transaction to the initial memory + pub(crate) fn bootloader_memory(&self) -> BootloaderMemory { + let mut initial_memory = self.initial_memory.clone(); + let mut offset = 0; + let mut compressed_bytecodes_offset = 0; + let mut tx_index = 0; + for l2_block in &self.l2_blocks { + for (num, tx) in l2_block.txs.iter().enumerate() { + let compressed_bytecodes_size = apply_tx_to_memory( + &mut initial_memory, + tx, + l2_block, + tx_index, + offset, + compressed_bytecodes_offset, + self.execution_mode, + num == 0, + ); + offset += tx.encoded_len(); + compressed_bytecodes_offset += compressed_bytecodes_size; + tx_index += 1; + } + if l2_block.txs.is_empty() { + apply_l2_block(&mut initial_memory, l2_block, tx_index) + } + } + + let pubdata_information = self + .pubdata_information + .clone() + .into_inner() + .expect("Empty pubdata information"); + + apply_pubdata_to_memory(&mut initial_memory, pubdata_information); + initial_memory + } + + fn free_tx_offset(&self) -> usize { + self.free_tx_offset + } + + pub(crate) fn free_tx_index(&self) -> usize { + let l2_block = self.last_l2_block(); + l2_block.first_tx_index + l2_block.txs.len() + } + + pub(crate) fn get_last_tx_compressed_bytecodes(&self) -> Vec { + if let Some(tx) = self.last_l2_block().txs.last() { + tx.compressed_bytecodes.clone() + } else { + vec![] + } + } + + /// Returns the id of current tx + pub(crate) fn current_tx(&self) -> usize { + self.tx_to_execute + .checked_sub(1) + .expect("There are no current tx to execute") + } + + /// Returns the ID of the next transaction to be executed and increments the local transaction counter. + pub(crate) fn move_tx_to_execute_pointer(&mut self) -> usize { + assert!( + self.tx_to_execute < self.free_tx_index(), + "Attempt to execute tx that was not pushed to memory. Tx ID: {}, txs in bootloader: {}", + self.tx_to_execute, + self.free_tx_index() + ); + + let old = self.tx_to_execute; + self.tx_to_execute += 1; + old + } + + /// Get offset of tx description + pub(crate) fn get_tx_description_offset(&self, tx_index: usize) -> usize { + TX_DESCRIPTION_OFFSET + self.find_tx(tx_index).offset + } + + pub(crate) fn insert_fictive_l2_block(&mut self) -> &BootloaderL2Block { + let block = self.last_l2_block(); + if !block.txs.is_empty() { + self.start_new_l2_block(L2BlockEnv { + timestamp: block.timestamp + 1, + number: block.number + 1, + prev_block_hash: block.get_hash(), + max_virtual_blocks_to_create: 1, + }); + } + self.last_l2_block() + } + + fn find_tx(&self, tx_index: usize) -> &BootloaderTx { + for block in self.l2_blocks.iter().rev() { + if tx_index >= block.first_tx_index { + return &block.txs[tx_index - block.first_tx_index]; + } + } + panic!("The tx with index {} must exist", tx_index) + } + + fn find_tx_mut(&mut self, tx_index: usize) -> &mut BootloaderTx { + for block in self.l2_blocks.iter_mut().rev() { + if tx_index >= block.first_tx_index { + return &mut block.txs[tx_index - block.first_tx_index]; + } + } + panic!("The tx with index {} must exist", tx_index) + } + + pub(crate) fn get_snapshot(&self) -> BootloaderStateSnapshot { + BootloaderStateSnapshot { + tx_to_execute: self.tx_to_execute, + l2_blocks_len: self.l2_blocks.len(), + last_l2_block: self.last_l2_block().make_snapshot(), + compressed_bytecodes_encoding: self.compressed_bytecodes_encoding, + free_tx_offset: self.free_tx_offset, + is_pubdata_information_provided: self.pubdata_information.get().is_some(), + } + } + + pub(crate) fn apply_snapshot(&mut self, snapshot: BootloaderStateSnapshot) { + self.tx_to_execute = snapshot.tx_to_execute; + self.compressed_bytecodes_encoding = snapshot.compressed_bytecodes_encoding; + self.free_tx_offset = snapshot.free_tx_offset; + match self.l2_blocks.len().cmp(&snapshot.l2_blocks_len) { + Ordering::Greater => self.l2_blocks.truncate(snapshot.l2_blocks_len), + Ordering::Less => panic!("Applying snapshot from future is not supported"), + Ordering::Equal => {} + } + self.last_mut_l2_block() + .apply_snapshot(snapshot.last_l2_block); + + if !snapshot.is_pubdata_information_provided { + self.pubdata_information = Default::default(); + } else { + // Under the correct usage of the snapshots of the bootloader state, + // this assertion should never fail, i.e. since the pubdata information + // can be set only once. However, we have this assertion just in case. + assert!( + self.pubdata_information.get().is_some(), + "Snapshot with no pubdata can not rollback to snapshot with one" + ); + } + } +} diff --git a/core/lib/multivm/src/versions/era_vm/bootloader_state/tx.rs b/core/lib/multivm/src/versions/era_vm/bootloader_state/tx.rs new file mode 100644 index 000000000000..211661384143 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/bootloader_state/tx.rs @@ -0,0 +1,49 @@ +use zksync_types::{L2ChainId, H256, U256}; +use zksync_utils::bytecode::CompressedBytecodeInfo; + +use crate::era_vm::transaction_data::TransactionData; + +/// Information about tx necessary for execution in bootloader. +#[derive(Debug, Clone)] +pub(crate) struct BootloaderTx { + pub(crate) hash: H256, + /// Encoded transaction + pub(crate) encoded: Vec, + /// Compressed bytecodes, which has been published during this transaction + pub(crate) compressed_bytecodes: Vec, + /// Refunds for this transaction + pub(crate) refund: u64, + /// Gas overhead + pub(crate) gas_overhead: u32, + /// Gas Limit for this transaction. It can be different from the gas limit inside the transaction + pub(crate) trusted_gas_limit: U256, + /// Offset of the tx in bootloader memory + pub(crate) offset: usize, +} + +impl BootloaderTx { + pub(super) fn new( + tx: TransactionData, + predefined_refund: u64, + predefined_overhead: u32, + trusted_gas_limit: U256, + compressed_bytecodes: Vec, + offset: usize, + chain_id: L2ChainId, + ) -> Self { + let hash = tx.tx_hash(chain_id); + Self { + hash, + encoded: tx.into_tokens(), + compressed_bytecodes, + refund: predefined_refund, + gas_overhead: predefined_overhead, + trusted_gas_limit, + offset, + } + } + + pub(super) fn encoded_len(&self) -> usize { + self.encoded.len() + } +} diff --git a/core/lib/multivm/src/versions/era_vm/bootloader_state/utils.rs b/core/lib/multivm/src/versions/era_vm/bootloader_state/utils.rs new file mode 100644 index 000000000000..cc9cb21fa64a --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/bootloader_state/utils.rs @@ -0,0 +1,298 @@ +use zksync_types::{ethabi, U256}; +use zksync_utils::{bytecode::CompressedBytecodeInfo, bytes_to_be_words, h256_to_u256}; + +use super::{l2_block::BootloaderL2Block, tx::BootloaderTx}; +use crate::{ + interface::{BootloaderMemory, TxExecutionMode}, + vm_latest::constants::{ + BOOTLOADER_TX_DESCRIPTION_OFFSET, BOOTLOADER_TX_DESCRIPTION_SIZE, + COMPRESSED_BYTECODES_OFFSET, OPERATOR_PROVIDED_L1_MESSENGER_PUBDATA_OFFSET, + OPERATOR_PROVIDED_L1_MESSENGER_PUBDATA_SLOTS, OPERATOR_REFUNDS_OFFSET, + TX_DESCRIPTION_OFFSET, TX_OPERATOR_L2_BLOCK_INFO_OFFSET, + TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO, TX_OVERHEAD_OFFSET, TX_TRUSTED_GAS_LIMIT_OFFSET, + }, +}; + +pub(super) fn get_memory_for_compressed_bytecodes( + compressed_bytecodes: &[CompressedBytecodeInfo], +) -> Vec { + let memory_addition: Vec<_> = compressed_bytecodes + .iter() + .flat_map(|x| x.encode_call()) + .collect(); + + bytes_to_be_words(memory_addition) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn apply_tx_to_memory( + memory: &mut BootloaderMemory, + bootloader_tx: &BootloaderTx, + bootloader_l2_block: &BootloaderL2Block, + tx_index: usize, + tx_offset: usize, + compressed_bytecodes_size: usize, + execution_mode: TxExecutionMode, + start_new_l2_block: bool, +) -> usize { + let bootloader_description_offset = + BOOTLOADER_TX_DESCRIPTION_OFFSET + BOOTLOADER_TX_DESCRIPTION_SIZE * tx_index; + let tx_description_offset = TX_DESCRIPTION_OFFSET + tx_offset; + + memory.push(( + bootloader_description_offset, + assemble_tx_meta(execution_mode, true), + )); + + memory.push(( + bootloader_description_offset + 1, + U256::from_big_endian(&(32 * tx_description_offset).to_be_bytes()), + )); + + let refund_offset = OPERATOR_REFUNDS_OFFSET + tx_index; + memory.push((refund_offset, bootloader_tx.refund.into())); + + let overhead_offset = TX_OVERHEAD_OFFSET + tx_index; + memory.push((overhead_offset, bootloader_tx.gas_overhead.into())); + + let trusted_gas_limit_offset = TX_TRUSTED_GAS_LIMIT_OFFSET + tx_index; + memory.push((trusted_gas_limit_offset, bootloader_tx.trusted_gas_limit)); + + memory.extend( + (tx_description_offset..tx_description_offset + bootloader_tx.encoded_len()) + .zip(bootloader_tx.encoded.clone()), + ); + + let bootloader_l2_block = if start_new_l2_block { + bootloader_l2_block.clone() + } else { + bootloader_l2_block.interim_version() + }; + apply_l2_block(memory, &bootloader_l2_block, tx_index); + + // Note, +1 is moving for pointer + let compressed_bytecodes_offset = COMPRESSED_BYTECODES_OFFSET + 1 + compressed_bytecodes_size; + + let encoded_compressed_bytecodes = + get_memory_for_compressed_bytecodes(&bootloader_tx.compressed_bytecodes); + let compressed_bytecodes_encoding = encoded_compressed_bytecodes.len(); + + memory.extend( + (compressed_bytecodes_offset + ..compressed_bytecodes_offset + encoded_compressed_bytecodes.len()) + .zip(encoded_compressed_bytecodes), + ); + compressed_bytecodes_encoding +} + +pub(crate) fn apply_l2_block( + memory: &mut BootloaderMemory, + bootloader_l2_block: &BootloaderL2Block, + txs_index: usize, +) { + // Since L2 block information start from the `TX_OPERATOR_L2_BLOCK_INFO_OFFSET` and each + // L2 block info takes `TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO` slots, the position where the L2 block info + // for this transaction needs to be written is: + + let block_position = + TX_OPERATOR_L2_BLOCK_INFO_OFFSET + txs_index * TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO; + + memory.extend(vec![ + (block_position, bootloader_l2_block.number.into()), + (block_position + 1, bootloader_l2_block.timestamp.into()), + ( + block_position + 2, + h256_to_u256(bootloader_l2_block.prev_block_hash), + ), + ( + block_position + 3, + bootloader_l2_block.max_virtual_blocks_to_create.into(), + ), + ]) +} + +pub(crate) fn apply_pubdata_to_memory( + memory: &mut BootloaderMemory, + pubdata_information: PubdataInput, +) { + // Skipping two slots as they will be filled by the bootloader itself: + // - One slot is for the selector of the call to the L1Messenger. + // - The other slot is for the 0x20 offset for the calldata. + let l1_messenger_pubdata_start_slot = OPERATOR_PROVIDED_L1_MESSENGER_PUBDATA_OFFSET + 2; + + // Need to skip first word as it represents array offset + // while bootloader expects only [len || data] + let pubdata = ethabi::encode(&[ethabi::Token::Bytes( + pubdata_information.build_pubdata(true), + )])[32..] + .to_vec(); + + assert!( + pubdata.len() / 32 <= OPERATOR_PROVIDED_L1_MESSENGER_PUBDATA_SLOTS - 2, + "The encoded pubdata is too big" + ); + + pubdata + .chunks(32) + .enumerate() + .for_each(|(slot_offset, value)| { + memory.push(( + l1_messenger_pubdata_start_slot + slot_offset, + U256::from(value), + )) + }); +} + +/// Forms a word that contains meta information for the transaction execution. +/// +/// # Current layout +/// +/// - 0 byte (MSB): server-side tx execution mode +/// In the server, we may want to execute different parts of the transaction in the different context +/// For example, when checking validity, we don't want to actually execute transaction and have side effects. +/// +/// Possible values: +/// - 0x00: validate & execute (normal mode) +/// - 0x02: execute but DO NOT validate +/// +/// - 31 byte (LSB): whether to execute transaction or not (at all). +pub(super) fn assemble_tx_meta(execution_mode: TxExecutionMode, execute_tx: bool) -> U256 { + let mut output = [0u8; 32]; + + // Set 0 byte (execution mode) + output[0] = match execution_mode { + TxExecutionMode::VerifyExecute => 0x00, + TxExecutionMode::EstimateFee { .. } => 0x00, + TxExecutionMode::EthCall { .. } => 0x02, + }; + + // Set 31 byte (marker for tx execution) + output[31] = u8::from(execute_tx); + + U256::from_big_endian(&output) +} + +use zksync_types::{ + event::L1MessengerL2ToL1Log, + writes::{compress_state_diffs, StateDiffRecord}, +}; + +/// Struct based on which the pubdata blob is formed +#[derive(Debug, Clone, Default)] +pub(crate) struct PubdataInput { + pub(crate) user_logs: Vec, + pub(crate) l2_to_l1_messages: Vec>, + pub(crate) published_bytecodes: Vec>, + pub(crate) state_diffs: Vec, +} + +impl PubdataInput { + pub(crate) fn build_pubdata(self, with_uncompressed_state_diffs: bool) -> Vec { + let mut l1_messenger_pubdata = vec![]; + + let PubdataInput { + user_logs, + l2_to_l1_messages, + published_bytecodes, + state_diffs, + } = self; + + // Encoding user L2->L1 logs. + // Format: `[(numberOfL2ToL1Logs as u32) || l2tol1logs[1] || ... || l2tol1logs[n]]` + l1_messenger_pubdata.extend((user_logs.len() as u32).to_be_bytes()); + for l2tol1log in user_logs { + l1_messenger_pubdata.extend(l2tol1log.packed_encoding()); + } + + // Encoding L2->L1 messages + // Format: `[(numberOfMessages as u32) || (messages[1].len() as u32) || messages[1] || ... || (messages[n].len() as u32) || messages[n]]` + l1_messenger_pubdata.extend((l2_to_l1_messages.len() as u32).to_be_bytes()); + for message in l2_to_l1_messages { + l1_messenger_pubdata.extend((message.len() as u32).to_be_bytes()); + l1_messenger_pubdata.extend(message); + } + + // Encoding bytecodes + // Format: `[(numberOfBytecodes as u32) || (bytecodes[1].len() as u32) || bytecodes[1] || ... || (bytecodes[n].len() as u32) || bytecodes[n]]` + l1_messenger_pubdata.extend((published_bytecodes.len() as u32).to_be_bytes()); + for bytecode in published_bytecodes { + l1_messenger_pubdata.extend((bytecode.len() as u32).to_be_bytes()); + l1_messenger_pubdata.extend(bytecode); + } + + // Encoding state diffs + // Format: `[size of compressed state diffs u32 || compressed state diffs || (# state diffs: intial + repeated) as u32 || sorted state diffs by ]` + let state_diffs_compressed = compress_state_diffs(state_diffs.clone()); + l1_messenger_pubdata.extend(state_diffs_compressed); + + if with_uncompressed_state_diffs { + l1_messenger_pubdata.extend((state_diffs.len() as u32).to_be_bytes()); + for state_diff in state_diffs { + l1_messenger_pubdata.extend(state_diff.encode_padded()); + } + } + + l1_messenger_pubdata + } +} + +#[cfg(test)] +mod tests { + use zksync_system_constants::{ACCOUNT_CODE_STORAGE_ADDRESS, BOOTLOADER_ADDRESS}; + use zksync_utils::u256_to_h256; + + use super::*; + + #[test] + fn test_basic_pubdata_building() { + // Just using some constant addresses for tests + let addr1 = BOOTLOADER_ADDRESS; + let addr2 = ACCOUNT_CODE_STORAGE_ADDRESS; + + let user_logs = vec![L1MessengerL2ToL1Log { + l2_shard_id: 0, + is_service: false, + tx_number_in_block: 0, + sender: addr1, + key: 1.into(), + value: 128.into(), + }]; + + let l2_to_l1_messages = vec![hex::decode("deadbeef").unwrap()]; + + let published_bytecodes = vec![hex::decode("aaaabbbb").unwrap()]; + + // For covering more cases, we have two state diffs: + // One with enumeration index present (and so it is a repeated write) and the one without it. + let state_diffs = vec![ + StateDiffRecord { + address: addr2, + key: 155.into(), + derived_key: u256_to_h256(125.into()).0, + enumeration_index: 12, + initial_value: 11.into(), + final_value: 12.into(), + }, + StateDiffRecord { + address: addr2, + key: 156.into(), + derived_key: u256_to_h256(126.into()).0, + enumeration_index: 0, + initial_value: 0.into(), + final_value: 14.into(), + }, + ]; + + let input = PubdataInput { + user_logs, + l2_to_l1_messages, + published_bytecodes, + state_diffs, + }; + + let pubdata = + ethabi::encode(&[ethabi::Token::Bytes(input.build_pubdata(true))])[32..].to_vec(); + + assert_eq!(hex::encode(pubdata), "00000000000000000000000000000000000000000000000000000000000002c700000001000000000000000000000000000000000000000000008001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000100000004deadbeef0000000100000004aaaabbbb0100002a040001000000000000000000000000000000000000000000000000000000000000007e090e0000000c0901000000020000000000000000000000000000000000008002000000000000000000000000000000000000000000000000000000000000009b000000000000000000000000000000000000000000000000000000000000007d000000000000000c000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008002000000000000000000000000000000000000000000000000000000000000009c000000000000000000000000000000000000000000000000000000000000007e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + } +} diff --git a/core/lib/multivm/src/versions/era_vm/bytecode.rs b/core/lib/multivm/src/versions/era_vm/bytecode.rs new file mode 100644 index 000000000000..de063145ca03 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/bytecode.rs @@ -0,0 +1,27 @@ +use itertools::Itertools; +use zksync_types::H256; +use zksync_utils::bytecode::{compress_bytecode, hash_bytecode, CompressedBytecodeInfo}; + +pub(crate) fn compress_bytecodes( + bytecodes: &[Vec], + mut is_bytecode_known: impl FnMut(H256) -> bool, +) -> Vec { + bytecodes + .iter() + .enumerate() + .sorted_by_key(|(_idx, dep)| *dep) + .dedup_by(|x, y| x.1 == y.1) + .filter(|(_idx, dep)| !is_bytecode_known(hash_bytecode(dep))) + .sorted_by_key(|(idx, _dep)| *idx) + .filter_map(|(_idx, dep)| { + let compressed_bytecode = compress_bytecode(dep); + + compressed_bytecode + .ok() + .map(|compressed| CompressedBytecodeInfo { + original: dep.clone(), + compressed, + }) + }) + .collect() +} diff --git a/core/lib/multivm/src/versions/era_vm/event.rs b/core/lib/multivm/src/versions/era_vm/event.rs new file mode 100644 index 000000000000..6b3c18e3b2b4 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/event.rs @@ -0,0 +1,112 @@ +use era_vm::state::Event; +use zksync_types::{L1BatchNumber, VmEvent, H256}; +use zksync_utils::h256_to_account_address; + +#[derive(Clone)] +pub struct EventAccumulator { + pub(crate) shard_id: u8, + pub(crate) tx_number_in_block: u16, + pub(crate) topics: Vec<[u8; 32]>, + pub(crate) data: Vec, +} + +impl EventAccumulator { + fn into_vm_event(self, block_number: L1BatchNumber) -> VmEvent { + VmEvent { + location: (block_number, self.tx_number_in_block as u32), + address: h256_to_account_address(&H256(self.topics[0])), + indexed_topics: self.topics[1..].into_iter().map(H256::from).collect(), + value: self.data, + } + } +} + +pub(crate) fn merge_events(events: &[Event], block_number: L1BatchNumber) -> Vec { + let mut result = vec![]; + let mut current: Option<(usize, u32, EventAccumulator)> = None; + + for message in events.into_iter() { + let Event { + shard_id, + is_first, + tx_number, + key, + value, + } = message.clone(); + + if !is_first { + if let Some((mut remaining_data_length, mut remaining_topics, mut event)) = + current.take() + { + if event.shard_id != shard_id || event.tx_number_in_block != tx_number { + continue; + } + let mut data_0 = [0u8; 32]; + let mut data_1 = [0u8; 32]; + key.to_big_endian(&mut data_0); + value.to_big_endian(&mut data_1); + for el in [data_0, data_1].iter() { + if remaining_topics != 0 { + event.topics.push(*el); + remaining_topics -= 1; + } else if remaining_data_length != 0 { + if remaining_data_length >= 32 { + event.data.extend_from_slice(el); + remaining_data_length -= 32; + } else { + event.data.extend_from_slice(&el[..remaining_data_length]); + remaining_data_length = 0; + } + } + } + + if remaining_data_length != 0 || remaining_topics != 0 { + current = Some((remaining_data_length, remaining_topics, event)) + } else { + result.push(event.into_vm_event(block_number)); + } + } + } else { + // start new one. First take the old one only if it's well formed + if let Some((remaining_data_length, remaining_topics, event)) = current.take() { + if remaining_data_length == 0 && remaining_topics == 0 { + result.push(event.into_vm_event(block_number)); + } + } + + // split key as our internal marker. Ignore higher bits + let mut num_topics = key.0[0] as u32; + let mut data_length = (key.0[0] >> 32) as usize; + let mut buffer = [0u8; 32]; + value.to_big_endian(&mut buffer); + + let (topics, data) = if num_topics == 0 && data_length == 0 { + (vec![], vec![]) + } else if num_topics == 0 { + data_length -= 32; + (vec![], buffer.to_vec()) + } else { + num_topics -= 1; + (vec![buffer], vec![]) + }; + + let new_event = EventAccumulator { + shard_id, + tx_number_in_block: tx_number, + topics, + data, + }; + + current = Some((data_length, num_topics, new_event)) + } + } + + // add the last one + if let Some((remaining_data_length, remaining_topics, event)) = current.take() { + if remaining_data_length == 0 && remaining_topics == 0 { + result.push(event.into_vm_event(block_number)); + } + } + + result +} diff --git a/core/lib/multivm/src/versions/era_vm/hook.rs b/core/lib/multivm/src/versions/era_vm/hook.rs new file mode 100644 index 000000000000..348ae11c5324 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/hook.rs @@ -0,0 +1,40 @@ +#[derive(Debug, Clone)] + +pub enum Hook { + AccountValidationEntered, + PaymasterValidationEntered, + AccountValidationExited, + ValidationStepEnded, + TxHasEnded, + DebugLog, + DebugReturnData, + NearCallCatch, + AskOperatorForRefund, + NotifyAboutRefund, + PostResult, + FinalBatchInfo, + PubdataRequested, +} + +impl Hook { + /// # Panics + /// Panics if the number does not correspond to any hook. + pub fn from_u32(hook: u32) -> Self { + match hook { + 0 => Hook::AccountValidationEntered, + 1 => Hook::PaymasterValidationEntered, + 2 => Hook::AccountValidationExited, + 3 => Hook::ValidationStepEnded, + 4 => Hook::TxHasEnded, + 5 => Hook::DebugLog, + 6 => Hook::DebugReturnData, + 7 => Hook::NearCallCatch, + 8 => Hook::AskOperatorForRefund, + 9 => Hook::NotifyAboutRefund, + 10 => Hook::PostResult, + 11 => Hook::FinalBatchInfo, + 12 => Hook::PubdataRequested, + _ => panic!("Unknown hook {}", hook), + } + } +} diff --git a/core/lib/multivm/src/versions/era_vm/initial_bootloader_memory.rs b/core/lib/multivm/src/versions/era_vm/initial_bootloader_memory.rs new file mode 100644 index 000000000000..b3bf15cb1be5 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/initial_bootloader_memory.rs @@ -0,0 +1,44 @@ +use zksync_types::U256; +use zksync_utils::{address_to_u256, h256_to_u256}; + +use crate::{interface::L1BatchEnv, vm_latest::utils::fee::get_batch_base_fee}; + +const OPERATOR_ADDRESS_SLOT: usize = 0; +const PREV_BLOCK_HASH_SLOT: usize = 1; +const NEW_BLOCK_TIMESTAMP_SLOT: usize = 2; +const NEW_BLOCK_NUMBER_SLOT: usize = 3; +const FAIR_PUBDATA_PRICE_SLOT: usize = 4; +const FAIR_L2_GAS_PRICE_SLOT: usize = 5; +const EXPECTED_BASE_FEE_SLOT: usize = 6; +const SHOULD_SET_NEW_BLOCK_SLOT: usize = 7; + +/// Returns the initial memory for the bootloader based on the current batch environment. +pub(crate) fn bootloader_initial_memory(l1_batch: &L1BatchEnv) -> Vec<(usize, U256)> { + let (prev_block_hash, should_set_new_block) = l1_batch + .previous_batch_hash + .map(|prev_block_hash| (h256_to_u256(prev_block_hash), U256::one())) + .unwrap_or_default(); + + vec![ + ( + OPERATOR_ADDRESS_SLOT, + address_to_u256(&l1_batch.fee_account), + ), + (PREV_BLOCK_HASH_SLOT, prev_block_hash), + (NEW_BLOCK_TIMESTAMP_SLOT, U256::from(l1_batch.timestamp)), + (NEW_BLOCK_NUMBER_SLOT, U256::from(l1_batch.number.0)), + ( + FAIR_PUBDATA_PRICE_SLOT, + U256::from(l1_batch.fee_input.fair_pubdata_price()), + ), + ( + FAIR_L2_GAS_PRICE_SLOT, + U256::from(l1_batch.fee_input.fair_l2_gas_price()), + ), + ( + EXPECTED_BASE_FEE_SLOT, + U256::from(get_batch_base_fee(l1_batch)), + ), + (SHOULD_SET_NEW_BLOCK_SLOT, should_set_new_block), + ] +} diff --git a/core/lib/multivm/src/versions/era_vm/logs.rs b/core/lib/multivm/src/versions/era_vm/logs.rs new file mode 100644 index 000000000000..3d8b70a9c78a --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/logs.rs @@ -0,0 +1,28 @@ +use zksync_types::l2_to_l1_log::{L2ToL1Log, SystemL2ToL1Log}; +use zksync_utils::u256_to_h256; + +pub trait IntoSystemLog { + fn into_system_log(&self) -> SystemL2ToL1Log; +} + +impl IntoSystemLog for era_vm::state::L2ToL1Log { + fn into_system_log(&self) -> SystemL2ToL1Log { + let era_vm::state::L2ToL1Log { + key, + value, + is_service, + address, + shard_id, + tx_number, + } = *self; + + SystemL2ToL1Log(L2ToL1Log { + shard_id, + is_service, + tx_number_in_block: tx_number, + sender: address, + key: u256_to_h256(key), + value: u256_to_h256(value), + }) + } +} diff --git a/core/lib/multivm/src/versions/era_vm/mod.rs b/core/lib/multivm/src/versions/era_vm/mod.rs new file mode 100644 index 000000000000..b595aaa86d79 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/mod.rs @@ -0,0 +1,12 @@ +pub mod bootloader_state; +mod bytecode; +mod event; +mod hook; +mod initial_bootloader_memory; +mod logs; +mod snapshot; +#[cfg(test)] +mod tests; +pub mod tracers; +mod transaction_data; +pub mod vm; diff --git a/core/lib/multivm/src/versions/era_vm/snapshot.rs b/core/lib/multivm/src/versions/era_vm/snapshot.rs new file mode 100644 index 000000000000..11ffe40d0030 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/snapshot.rs @@ -0,0 +1,9 @@ +use super::bootloader_state::BootloaderStateSnapshot; + +pub struct VmSnapshot { + // execution: era_vm::execution::Execution, + pub vm_snapshot: era_vm::vm::VmSnapshot, + pub(crate) bootloader_snapshot: BootloaderStateSnapshot, + pub suspended_at: u16, + pub(crate) gas_for_account_validation: u32, +} diff --git a/core/lib/multivm/src/versions/era_vm/test_contract/storage b/core/lib/multivm/src/versions/era_vm/test_contract/storage new file mode 100644 index 000000000000..e554933db1b8 Binary files /dev/null and b/core/lib/multivm/src/versions/era_vm/test_contract/storage differ diff --git a/core/lib/multivm/src/versions/era_vm/tests/block_tip.rs b/core/lib/multivm/src/versions/era_vm/tests/block_tip.rs new file mode 100644 index 000000000000..cd06ae73bf59 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/block_tip.rs @@ -0,0 +1,413 @@ +use ethabi::Token; +use itertools::Itertools; +use zksync_contracts::load_sys_contract; +use zksync_system_constants::{ + CONTRACT_FORCE_DEPLOYER_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L1_MESSENGER_ADDRESS, +}; +use zksync_types::{ + commitment::SerializeCommitment, fee_model::BatchFeeInput, get_code_key, + l2_to_l1_log::L2ToL1Log, writes::StateDiffRecord, Address, Execute, H256, U256, +}; +use zksync_utils::{bytecode::hash_bytecode, u256_to_h256}; + +use super::utils::{get_complex_upgrade_abi, read_complex_upgrade}; +use crate::{ + era_vm::{ + tests::tester::{default_l1_batch, get_empty_storage, VmTesterBuilder}, + tracers::{dispatcher::TracerDispatcher, pubdata_tracer::PubdataTracer}, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, + vm_latest::{ + constants::{ + BOOTLOADER_BATCH_TIP_CIRCUIT_STATISTICS_OVERHEAD, + BOOTLOADER_BATCH_TIP_METRICS_SIZE_OVERHEAD, BOOTLOADER_BATCH_TIP_OVERHEAD, + MAX_VM_PUBDATA_PER_BATCH, + }, + L1BatchEnv, + }, +}; + +#[derive(Debug, Clone, Default)] +struct L1MessengerTestData { + l2_to_l1_logs: usize, + messages: Vec>, + bytecodes: Vec>, + state_diffs: Vec, +} + +struct MimicCallInfo { + to: Address, + who_to_mimic: Address, + data: Vec, +} + +const CALLS_PER_TX: usize = 1_000; +fn populate_mimic_calls(data: L1MessengerTestData) -> Vec> { + let complex_upgrade = get_complex_upgrade_abi(); + let l1_messenger = load_sys_contract("L1Messenger"); + + let logs_mimic_calls = (0..data.l2_to_l1_logs).map(|_| MimicCallInfo { + to: L1_MESSENGER_ADDRESS, + who_to_mimic: KNOWN_CODES_STORAGE_ADDRESS, + data: l1_messenger + .function("sendL2ToL1Log") + .unwrap() + .encode_input(&[ + Token::Bool(false), + Token::FixedBytes(H256::random().0.to_vec()), + Token::FixedBytes(H256::random().0.to_vec()), + ]) + .unwrap(), + }); + let messages_mimic_calls = data.messages.iter().map(|message| MimicCallInfo { + to: L1_MESSENGER_ADDRESS, + who_to_mimic: KNOWN_CODES_STORAGE_ADDRESS, + data: l1_messenger + .function("sendToL1") + .unwrap() + .encode_input(&[Token::Bytes(message.clone())]) + .unwrap(), + }); + let bytecodes_mimic_calls = data.bytecodes.iter().map(|bytecode| MimicCallInfo { + to: L1_MESSENGER_ADDRESS, + who_to_mimic: KNOWN_CODES_STORAGE_ADDRESS, + data: l1_messenger + .function("requestBytecodeL1Publication") + .unwrap() + .encode_input(&[Token::FixedBytes(hash_bytecode(bytecode).0.to_vec())]) + .unwrap(), + }); + + let encoded_calls = logs_mimic_calls + .chain(messages_mimic_calls) + .chain(bytecodes_mimic_calls) + .map(|call| { + Token::Tuple(vec![ + Token::Address(call.to), + Token::Address(call.who_to_mimic), + Token::Bytes(call.data), + ]) + }) + .chunks(CALLS_PER_TX) + .into_iter() + .map(|chunk| { + complex_upgrade + .function("mimicCalls") + .unwrap() + .encode_input(&[Token::Array(chunk.collect_vec())]) + .unwrap() + }) + .collect_vec(); + + encoded_calls +} + +struct TestStatistics { + pub max_used_gas: u32, + pub circuit_statistics: u64, + pub execution_metrics_size: u64, +} + +struct StatisticsTagged { + pub statistics: TestStatistics, + pub tag: String, +} + +fn execute_test(test_data: L1MessengerTestData) -> TestStatistics { + let mut storage = get_empty_storage(); + let complex_upgrade_code = read_complex_upgrade(); + + // For this test we'll just put the bytecode onto the force deployer address + storage.set_value( + get_code_key(&CONTRACT_FORCE_DEPLOYER_ADDRESS), + hash_bytecode(&complex_upgrade_code), + ); + storage.store_factory_dep(hash_bytecode(&complex_upgrade_code), complex_upgrade_code); + + // We are measuring computational cost, so prices for pubdata don't matter, while they artificially dilute + // the gas limit + + let batch_env = L1BatchEnv { + fee_input: BatchFeeInput::pubdata_independent(100_000, 100_000, 100_000), + ..default_l1_batch(zksync_types::L1BatchNumber(1)) + }; + + let mut vm = VmTesterBuilder::new() + .with_storage(storage) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .with_l1_batch_env(batch_env) + .build(); + + for code in &test_data.bytecodes { + vm.storage + .borrow_mut() + .store_factory_dep(hash_bytecode(code), code.clone()); + } + + let txs_data = populate_mimic_calls(test_data.clone()); + let account = &mut vm.rich_accounts[0]; + + for (i, data) in txs_data.into_iter().enumerate() { + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: CONTRACT_FORCE_DEPLOYER_ADDRESS, + calldata: data, + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx); + + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "Transaction {i} wasn't successful for input: {:#?}", + test_data + ); + } + + // Now we count how much ergs were spent at the end of the batch + // It is assumed that the top level frame is the bootloader + + let ergs_before = vm.vm.inner.execution.gas_left().unwrap(); + + // We ensure that indeed the provided state diffs are used + let pubdata_tracer = PubdataTracer::new_with_forced_state_diffs( + VmExecutionMode::Batch, + test_data.state_diffs.clone(), + ); + + let result = vm.vm.inspect_inner( + TracerDispatcher::default(), + Some(pubdata_tracer), + VmExecutionMode::Batch, + ); + + assert!( + !result.result.is_failed(), + "Batch wasn't successful for input: {:?}", + test_data + ); + + let ergs_after = vm.vm.inner.execution.gas_left().unwrap(); + + assert_eq!( + (ergs_before - ergs_after) as u64, + result.statistics.gas_used + ); + + TestStatistics { + max_used_gas: ergs_before - ergs_after, + circuit_statistics: result.statistics.circuit_statistic.total() as u64, + execution_metrics_size: result.get_execution_metrics(None).size() as u64, + } +} + +fn generate_state_diffs( + repeated_writes: bool, + small_diff: bool, + number_of_state_diffs: usize, +) -> Vec { + (0..number_of_state_diffs) + .map(|i| { + let address = Address::from_low_u64_be(i as u64); + let key = U256::from(i); + let enumeration_index = if repeated_writes { i + 1 } else { 0 }; + + let (initial_value, final_value) = if small_diff { + // As small as it gets, one byte to denote zeroing out the value + (U256::from(1), U256::from(0)) + } else { + // As large as it gets + (U256::from(0), U256::from(2).pow(255.into())) + }; + + StateDiffRecord { + address, + key, + derived_key: u256_to_h256(i.into()).0, + enumeration_index: enumeration_index as u64, + initial_value, + final_value, + } + }) + .collect() +} + +// A valid zkEVM bytecode has odd number of 32 byte words +fn get_valid_bytecode_length(length: usize) -> usize { + // Firstly ensure that the length is divisible by 32 + let length_padded_to_32 = if length % 32 == 0 { + length + } else { + length + 32 - (length % 32) + }; + + // Then we ensure that the number returned by division by 32 is odd + if length_padded_to_32 % 64 == 0 { + length_padded_to_32 + 32 + } else { + length_padded_to_32 + } +} + +#[test] +fn test_dry_run_upper_bound() { + // Some of the pubdata is consumed by constant fields (such as length of messages, number of logs, etc.). + // While this leaves some room for error, at the end of the test we require that the `BOOTLOADER_BATCH_TIP_OVERHEAD` + // is sufficient with a very large margin, so it is okay to ignore 1% of possible pubdata. + const MAX_EFFECTIVE_PUBDATA_PER_BATCH: usize = + (MAX_VM_PUBDATA_PER_BATCH as f64 * 0.99) as usize; + + // We are re-using the `ComplexUpgrade` contract as it already has the `mimicCall` functionality. + // To get the upper bound, we'll try to do the following: + // 1. Max number of logs. + // 2. Lots of small L2->L1 messages / one large L2->L1 message. + // 3. Lots of small bytecodes / one large bytecode. + // 4. Lots of storage slot updates. + + let statistics = vec![ + // max logs + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + l2_to_l1_logs: MAX_EFFECTIVE_PUBDATA_PER_BATCH / L2ToL1Log::SERIALIZED_SIZE, + ..Default::default() + }), + tag: "max_logs".to_string(), + }, + // max messages + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + // Each L2->L1 message is accompanied by a Log + its length, which is a 4 byte number, + // so the max number of pubdata is bound by it + messages: vec![ + vec![0; 0]; + MAX_EFFECTIVE_PUBDATA_PER_BATCH / (L2ToL1Log::SERIALIZED_SIZE + 4) + ], + ..Default::default() + }), + tag: "max_messages".to_string(), + }, + // long message + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + // Each L2->L1 message is accompanied by a Log, so the max number of pubdata is bound by it + messages: vec![vec![0; MAX_EFFECTIVE_PUBDATA_PER_BATCH]; 1], + ..Default::default() + }), + tag: "long_message".to_string(), + }, + // // max bytecodes + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + // Each bytecode must be at least 32 bytes long. + // Each uncompressed bytecode is accompanied by its length, which is a 4 byte number + bytecodes: vec![vec![0; 32]; MAX_EFFECTIVE_PUBDATA_PER_BATCH / (32 + 4)], + ..Default::default() + }), + tag: "max_bytecodes".to_string(), + }, + // long bytecode + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + bytecodes: vec![ + vec![0; get_valid_bytecode_length(MAX_EFFECTIVE_PUBDATA_PER_BATCH)]; + 1 + ], + ..Default::default() + }), + tag: "long_bytecode".to_string(), + }, + // lots of small repeated writes + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + // In theory each state diff can require only 5 bytes to be published (enum index + 4 bytes for the key) + state_diffs: generate_state_diffs(true, true, MAX_EFFECTIVE_PUBDATA_PER_BATCH / 5), + ..Default::default() + }), + tag: "small_repeated_writes".to_string(), + }, + // lots of big repeated writes + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + // Each big repeated write will approximately require 4 bytes for key + 1 byte for encoding type + 32 bytes for value + state_diffs: generate_state_diffs( + true, + false, + MAX_EFFECTIVE_PUBDATA_PER_BATCH / 37, + ), + ..Default::default() + }), + tag: "big_repeated_writes".to_string(), + }, + // lots of small initial writes + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + // Each small initial write will take at least 32 bytes for derived key + 1 bytes encoding zeroing out + state_diffs: generate_state_diffs( + false, + true, + MAX_EFFECTIVE_PUBDATA_PER_BATCH / 33, + ), + ..Default::default() + }), + tag: "small_initial_writes".to_string(), + }, + // lots of large initial writes + StatisticsTagged { + statistics: execute_test(L1MessengerTestData { + // Each big write will take at least 32 bytes for derived key + 1 byte for encoding type + 32 bytes for value + state_diffs: generate_state_diffs( + false, + false, + MAX_EFFECTIVE_PUBDATA_PER_BATCH / 65, + ), + ..Default::default() + }), + tag: "big_initial_writes".to_string(), + }, + ]; + + // We use 2x overhead for the batch tip compared to the worst estimated scenario. + let max_used_gas = statistics + .iter() + .map(|s| (s.statistics.max_used_gas, s.tag.clone())) + .max() + .unwrap(); + assert!( + max_used_gas.0 * 3 / 2 <= BOOTLOADER_BATCH_TIP_OVERHEAD, + "BOOTLOADER_BATCH_TIP_OVERHEAD is too low for {} with result {}, BOOTLOADER_BATCH_TIP_OVERHEAD = {}", + max_used_gas.1, + max_used_gas.0, + BOOTLOADER_BATCH_TIP_OVERHEAD + ); + + let circuit_statistics = statistics + .iter() + .map(|s| (s.statistics.circuit_statistics, s.tag.clone())) + .max() + .unwrap(); + assert!( + circuit_statistics.0 * 3 / 2 <= BOOTLOADER_BATCH_TIP_CIRCUIT_STATISTICS_OVERHEAD as u64, + "BOOTLOADER_BATCH_TIP_CIRCUIT_STATISTICS_OVERHEAD is too low for {} with result {}, BOOTLOADER_BATCH_TIP_CIRCUIT_STATISTICS_OVERHEAD = {}", + circuit_statistics.1, + circuit_statistics.0, + BOOTLOADER_BATCH_TIP_CIRCUIT_STATISTICS_OVERHEAD + ); + + let execution_metrics_size = statistics + .iter() + .map(|s| (s.statistics.execution_metrics_size, s.tag.clone())) + .max() + .unwrap(); + assert!( + execution_metrics_size.0 * 3 / 2 <= BOOTLOADER_BATCH_TIP_METRICS_SIZE_OVERHEAD as u64, + "BOOTLOADER_BATCH_TIP_METRICS_SIZE_OVERHEAD is too low for {} with result {}, BOOTLOADER_BATCH_TIP_METRICS_SIZE_OVERHEAD = {}", + execution_metrics_size.1, + execution_metrics_size.0, + BOOTLOADER_BATCH_TIP_METRICS_SIZE_OVERHEAD + ); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/bootloader.rs b/core/lib/multivm/src/versions/era_vm/tests/bootloader.rs new file mode 100644 index 000000000000..99f025e7ba77 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/bootloader.rs @@ -0,0 +1,52 @@ +use zksync_types::U256; + +use crate::{ + interface::{ExecutionResult, Halt, TxExecutionMode, VmExecutionMode, VmInterface}, + versions::era_vm::tests::{ + tester::VmTesterBuilder, + utils::{get_bootloader, verify_required_memory, BASE_SYSTEM_CONTRACTS}, + }, +}; + +#[test] +fn test_dummy_bootloader() { + let mut base_system_contracts = BASE_SYSTEM_CONTRACTS.clone(); + base_system_contracts.bootloader = get_bootloader("dummy"); + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_base_system_smart_contracts(base_system_contracts) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .build(); + + let result = vm.vm.execute(VmExecutionMode::Batch); + assert!(!result.result.is_failed()); + + let correct_first_cell = U256::from_str_radix("123123123", 16).unwrap(); + + verify_required_memory( + &vm.vm.inner.execution, + vec![(correct_first_cell, vm2::FIRST_HEAP, 0)], + ); +} + +#[test] +fn test_bootloader_out_of_gas() { + let mut base_system_contracts = BASE_SYSTEM_CONTRACTS.clone(); + base_system_contracts.bootloader = get_bootloader("dummy"); + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_base_system_smart_contracts(base_system_contracts) + .with_bootloader_gas_limit(10) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .build(); + + let res = vm.vm.execute(VmExecutionMode::Batch); + assert!(matches!( + res.result, + ExecutionResult::Halt { + reason: Halt::BootloaderOutOfGas + } + )); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/bytecode_publishing.rs b/core/lib/multivm/src/versions/era_vm/tests/bytecode_publishing.rs new file mode 100644 index 000000000000..dee124c5815c --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/bytecode_publishing.rs @@ -0,0 +1,41 @@ +use zksync_types::event::extract_long_l2_to_l1_messages; +use zksync_utils::bytecode::compress_bytecode; + +use crate::{ + era_vm::tests::{ + tester::{DeployContractsTx, TxType, VmTesterBuilder}, + utils::read_test_contract, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, +}; + +#[test] +fn test_bytecode_publishing() { + // In this test, we aim to ensure that the contents of the compressed bytecodes + // are included as part of the L2->L1 long messages + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let counter = read_test_contract(); + let account = &mut vm.rich_accounts[0]; + + let compressed_bytecode = compress_bytecode(&counter).unwrap(); + + let DeployContractsTx { tx, .. } = account.get_deploy_tx(&counter, None, TxType::L2); + vm.vm.push_transaction(tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed(), "Transaction wasn't successful"); + let result = vm.vm.execute(VmExecutionMode::Batch); + println!("RESULT {:?}", result); + + let state = vm.vm.get_current_execution_state(); + let long_messages = extract_long_l2_to_l1_messages(&state.events); + + assert!( + long_messages.contains(&compressed_bytecode), + "Bytecode not published" + ); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/call_tracer.rs b/core/lib/multivm/src/versions/era_vm/tests/call_tracer.rs new file mode 100644 index 000000000000..9d752a3ba116 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/call_tracer.rs @@ -0,0 +1,97 @@ +use std::sync::Arc; + +use once_cell::sync::OnceCell; +use zksync_types::{Address, Execute}; + +use crate::{ + era_vm::{ + tests::{ + tester::VmTesterBuilder, + utils::{read_max_depth_contract, read_test_contract}, + }, + tracers::dispatcher::TracerDispatcher, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, + tracers::CallTracer, + vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT, +}; + +// This test is ultra slow, so it's ignored by default. +#[test] +fn test_max_depth() { + let contarct = read_max_depth_contract(); + let address = Address::random(); + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_random_rich_accounts(1) + .with_deployer() + .with_bootloader_gas_limit(BATCH_COMPUTATIONAL_GAS_LIMIT) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_custom_contracts(vec![(contarct, address, true)]) + .build(); + + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: address, + calldata: vec![], + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + + let result = Arc::new(OnceCell::new()); + let call_tracer = Box::new(CallTracer::new(result.clone())); + vm.vm.push_transaction(tx); + let res = vm.vm.inspect( + TracerDispatcher::new(vec![call_tracer]), + VmExecutionMode::OneTx, + ); + assert!(result.get().is_some()); + assert!(res.result.is_failed()); +} + +#[test] +fn test_basic_behavior() { + let contarct = read_test_contract(); + let address = Address::random(); + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_random_rich_accounts(1) + .with_deployer() + .with_bootloader_gas_limit(BATCH_COMPUTATIONAL_GAS_LIMIT) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_custom_contracts(vec![(contarct, address, true)]) + .build(); + + let increment_by_6_calldata = + "7cf5dab00000000000000000000000000000000000000000000000000000000000000006"; + + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: address, + calldata: hex::decode(increment_by_6_calldata).unwrap(), + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + + let result = Arc::new(OnceCell::new()); + let call_tracer = Box::new(CallTracer::new(result.clone())); + vm.vm.push_transaction(tx); + let res = vm.vm.inspect( + TracerDispatcher::new(vec![call_tracer]), + VmExecutionMode::OneTx, + ); + + let call_tracer_result = result.get().unwrap(); + + assert_eq!(call_tracer_result.len(), 1); + // Expect that there are a plenty of subcalls underneath. + let subcall = &call_tracer_result[0].calls; + assert!(subcall.len() > 10); + assert!(!res.result.is_failed()); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/circuits.rs b/core/lib/multivm/src/versions/era_vm/tests/circuits.rs new file mode 100644 index 000000000000..0e70f6b38104 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/circuits.rs @@ -0,0 +1,73 @@ +use zksync_types::{Address, Execute, U256}; + +use crate::{ + era_vm::tests::tester::VmTesterBuilder, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, + vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT, +}; + +// Checks that estimated number of circuits for simple transfer doesn't differ much +// from hardcoded expected value. +#[test] +fn test_circuits() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_random_rich_accounts(1) + .with_deployer() + .with_bootloader_gas_limit(BATCH_COMPUTATIONAL_GAS_LIMIT) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .build(); + + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: Address::random(), + calldata: Vec::new(), + value: U256::from(1u8), + factory_deps: vec![], + }, + None, + ); + vm.vm.push_transaction(tx); + let res = vm.vm.inspect(Default::default(), VmExecutionMode::OneTx); + + let s = res.statistics.circuit_statistic; + // Check `circuit_statistic`. + const EXPECTED: [f32; 13] = [ + 1.34935, 0.15026, 1.66666, 0.00315, 1.0594, 0.00058, 0.00348, 0.00076, 0.11945, 0.14285, + 0.0, 0.0, 0.0, + ]; + let actual = [ + (s.main_vm, "main_vm"), + (s.ram_permutation, "ram_permutation"), + (s.storage_application, "storage_application"), + (s.storage_sorter, "storage_sorter"), + (s.code_decommitter, "code_decommitter"), + (s.code_decommitter_sorter, "code_decommitter_sorter"), + (s.log_demuxer, "log_demuxer"), + (s.events_sorter, "events_sorter"), + (s.keccak256, "keccak256"), + (s.ecrecover, "ecrecover"), + (s.sha256, "sha256"), + (s.secp256k1_verify, "secp256k1_verify"), + (s.transient_storage_checker, "transient_storage_checker"), + ]; + for ((actual, name), expected) in actual.iter().zip(EXPECTED) { + if expected == 0.0 { + assert_eq!( + *actual, expected, + "Check failed for {}, expected {}, actual {}", + name, expected, actual + ); + } else { + let diff = (actual - expected) / expected; + assert!( + diff.abs() < 0.1, + "Check failed for {}, expected {}, actual {}", + name, + expected, + actual + ); + } + } +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/code_oracle.rs b/core/lib/multivm/src/versions/era_vm/tests/code_oracle.rs new file mode 100644 index 000000000000..a0e3e6bc0345 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/code_oracle.rs @@ -0,0 +1,252 @@ +use ethabi::Token; +use zksync_types::{ + get_known_code_key, web3::keccak256, Address, Execute, StorageLogWithPreviousValue, U256, +}; +use zksync_utils::{bytecode::hash_bytecode, h256_to_u256, u256_to_h256}; + +use crate::{ + era_vm::tests::{ + tester::{get_empty_storage, VmTesterBuilder}, + utils::{load_precompiles_contract, read_precompiles_contract, read_test_contract}, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, +}; + +fn generate_large_bytecode() -> Vec { + // This is the maximal possible size of a zkEVM bytecode + vec![2u8; ((1 << 16) - 1) * 32] +} + +#[test] +fn test_code_oracle() { + let precompiles_contract_address = Address::random(); + let precompile_contract_bytecode = read_precompiles_contract(); + + // Filling the zkevm bytecode + let normal_zkevm_bytecode = read_test_contract(); + let normal_zkevm_bytecode_hash = hash_bytecode(&normal_zkevm_bytecode); + let normal_zkevm_bytecode_keccak_hash = keccak256(&normal_zkevm_bytecode); + let mut storage = get_empty_storage(); + storage.set_value( + get_known_code_key(&normal_zkevm_bytecode_hash), + u256_to_h256(U256::one()), + ); + + // In this test, we aim to test whether a simple account interaction (without any fee logic) + // will work. The account will try to deploy a simple contract from integration tests. + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .with_custom_contracts(vec![( + precompile_contract_bytecode, + precompiles_contract_address, + false, + )]) + .with_storage(storage) + .build(); + + let precompile_contract = load_precompiles_contract(); + let call_code_oracle_function = precompile_contract.function("callCodeOracle").unwrap(); + + vm.vm.insert_bytecodes([normal_zkevm_bytecode.as_slice()]); + let account = &mut vm.rich_accounts[0]; + + // Firstly, let's ensure that the contract works. + let tx1 = account.get_l2_tx_for_execute( + Execute { + contract_address: precompiles_contract_address, + calldata: call_code_oracle_function + .encode_input(&[ + Token::FixedBytes(normal_zkevm_bytecode_hash.0.to_vec()), + Token::FixedBytes(normal_zkevm_bytecode_keccak_hash.to_vec()), + ]) + .unwrap(), + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx1); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "Transaction wasn't successful: {result:#?}" + ); + + // Now, we ask for the same bytecode. We use to partially check whether the memory page with + // the decommitted bytecode gets erased (it shouldn't). + let tx2 = account.get_l2_tx_for_execute( + Execute { + contract_address: precompiles_contract_address, + calldata: call_code_oracle_function + .encode_input(&[ + Token::FixedBytes(normal_zkevm_bytecode_hash.0.to_vec()), + Token::FixedBytes(normal_zkevm_bytecode_keccak_hash.to_vec()), + ]) + .unwrap(), + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + vm.vm.push_transaction(tx2); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "Transaction wasn't successful: {result:#?}" + ); +} + +fn find_code_oracle_cost_log( + precompiles_contract_address: Address, + logs: &[StorageLogWithPreviousValue], +) -> &StorageLogWithPreviousValue { + logs.iter() + .find(|log| { + *log.log.key.address() == precompiles_contract_address && log.log.key.key().is_zero() + }) + .expect("no code oracle cost log") +} + +#[test] +fn test_code_oracle_big_bytecode() { + let precompiles_contract_address = Address::random(); + let precompile_contract_bytecode = read_precompiles_contract(); + + let big_zkevm_bytecode = generate_large_bytecode(); + let big_zkevm_bytecode_hash = hash_bytecode(&big_zkevm_bytecode); + let big_zkevm_bytecode_keccak_hash = keccak256(&big_zkevm_bytecode); + + let mut storage = get_empty_storage(); + storage.set_value( + get_known_code_key(&big_zkevm_bytecode_hash), + u256_to_h256(U256::one()), + ); + + // In this test, we aim to test whether a simple account interaction (without any fee logic) + // will work. The account will try to deploy a simple contract from integration tests. + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .with_custom_contracts(vec![( + precompile_contract_bytecode, + precompiles_contract_address, + false, + )]) + .with_storage(storage) + .build(); + + let precompile_contract = load_precompiles_contract(); + let call_code_oracle_function = precompile_contract.function("callCodeOracle").unwrap(); + + vm.vm.insert_bytecodes([big_zkevm_bytecode.as_slice()]); + + let account = &mut vm.rich_accounts[0]; + + // Firstly, let's ensure that the contract works. + let tx1 = account.get_l2_tx_for_execute( + Execute { + contract_address: precompiles_contract_address, + calldata: call_code_oracle_function + .encode_input(&[ + Token::FixedBytes(big_zkevm_bytecode_hash.0.to_vec()), + Token::FixedBytes(big_zkevm_bytecode_keccak_hash.to_vec()), + ]) + .unwrap(), + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx1); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "Transaction wasn't successful: {result:#?}" + ); +} + +#[test] +fn refunds_in_code_oracle() { + let precompiles_contract_address = Address::random(); + let precompile_contract_bytecode = read_precompiles_contract(); + + let normal_zkevm_bytecode = read_test_contract(); + let normal_zkevm_bytecode_hash = hash_bytecode(&normal_zkevm_bytecode); + let normal_zkevm_bytecode_keccak_hash = keccak256(&normal_zkevm_bytecode); + let mut storage = get_empty_storage(); + storage.set_value( + get_known_code_key(&normal_zkevm_bytecode_hash), + u256_to_h256(U256::one()), + ); + + let precompile_contract = load_precompiles_contract(); + let call_code_oracle_function = precompile_contract.function("callCodeOracle").unwrap(); + + // Execute code oracle twice with identical VM state that only differs in that the queried bytecode + // is already decommitted the second time. The second call must consume less gas (`decommit` doesn't charge additional gas + // for already decommitted codes). + let mut oracle_costs = vec![]; + for decommit in [false, true] { + let mut vm = VmTesterBuilder::new() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .with_custom_contracts(vec![( + precompile_contract_bytecode.clone(), + precompiles_contract_address, + false, + )]) + .with_storage(storage.clone()) + .build(); + + vm.vm.insert_bytecodes([normal_zkevm_bytecode.as_slice()]); + + let account = &mut vm.rich_accounts[0]; + if decommit { + let (_, was_decommited) = vm + .vm + .inner + .state + .decommit(h256_to_u256(normal_zkevm_bytecode_hash), &mut vm.vm.world); + + assert!(!was_decommited); + } + + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: precompiles_contract_address, + calldata: call_code_oracle_function + .encode_input(&[ + Token::FixedBytes(normal_zkevm_bytecode_hash.0.to_vec()), + Token::FixedBytes(normal_zkevm_bytecode_keccak_hash.to_vec()), + ]) + .unwrap(), + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "Transaction wasn't successful: {result:#?}" + ); + let log = + find_code_oracle_cost_log(precompiles_contract_address, &result.logs.storage_logs); + oracle_costs.push(log.log.value); + } + + // The refund is equal to `gasCost` parameter passed to the `decommit` opcode, which is defined as `4 * contract_length_in_words` + // in `CodeOracle.yul`. + let code_oracle_refund = h256_to_u256(oracle_costs[0]) - h256_to_u256(oracle_costs[1]); + assert_eq!( + code_oracle_refund, + (4 * (normal_zkevm_bytecode.len() / 32)).into() + ); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/default_aa.rs b/core/lib/multivm/src/versions/era_vm/tests/default_aa.rs new file mode 100644 index 000000000000..bf5fa4c77a18 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/default_aa.rs @@ -0,0 +1,80 @@ +use zksync_system_constants::L2_BASE_TOKEN_ADDRESS; +use zksync_types::{ + get_code_key, get_known_code_key, get_nonce_key, + system_contracts::{DEPLOYMENT_NONCE_INCREMENT, TX_NONCE_INCREMENT}, + AccountTreeId, U256, +}; +use zksync_utils::u256_to_h256; + +use crate::{ + era_vm::tests::{ + tester::{DeployContractsTx, TxType, VmTesterBuilder}, + utils::{get_balance, read_test_contract, verify_required_storage}, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, + vm_latest::utils::fee::get_batch_base_fee, +}; + +#[test] +fn test_default_aa_interaction() { + // In this test, we aim to test whether a simple account interaction (without any fee logic) + // will work. The account will try to deploy a simple contract from integration tests. + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let counter = read_test_contract(); + let account = &mut vm.rich_accounts[0]; + let DeployContractsTx { + tx, + bytecode_hash, + address, + } = account.get_deploy_tx(&counter, None, TxType::L2); + let maximal_fee = tx.gas_limit() * get_batch_base_fee(&vm.vm.batch_env); + + vm.vm.push_transaction(tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed(), "Transaction wasn't successful"); + + vm.vm.execute(VmExecutionMode::Batch); + + vm.vm.get_current_execution_state(); + + // Both deployment and ordinary nonce should be incremented by one. + let account_nonce_key = get_nonce_key(&account.address); + let expected_nonce = TX_NONCE_INCREMENT + DEPLOYMENT_NONCE_INCREMENT; + + // The code hash of the deployed contract should be marked as republished. + let known_codes_key = get_known_code_key(&bytecode_hash); + + // The contract should be deployed successfully. + let account_code_key = get_code_key(&address); + let expected_slots = [ + (u256_to_h256(expected_nonce), account_nonce_key), + (u256_to_h256(U256::from(1u32)), known_codes_key), + (bytecode_hash, account_code_key), + ]; + + verify_required_storage( + &expected_slots, + &mut vm.vm.storage, + vm.vm.inner.state.storage_changes(), + ); + + let expected_fee = maximal_fee + - U256::from(result.refunds.gas_refunded) + * U256::from(get_batch_base_fee(&vm.vm.batch_env)); + let operator_balance = get_balance( + AccountTreeId::new(L2_BASE_TOKEN_ADDRESS), + &vm.fee_account, + &mut vm.vm.storage, + vm.vm.inner.state.storage_changes(), + ); + + assert_eq!( + operator_balance, expected_fee, + "Operator did not receive his fee" + ); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/gas_limit.rs b/core/lib/multivm/src/versions/era_vm/tests/gas_limit.rs new file mode 100644 index 000000000000..c6f4aa3d7469 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/gas_limit.rs @@ -0,0 +1,35 @@ +use zksync_test_account::Account; +use zksync_types::{fee::Fee, Execute}; + +use crate::{ + era_vm::tests::tester::VmTesterBuilder, + interface::{TxExecutionMode, VmInterface}, + vm_latest::constants::{TX_DESCRIPTION_OFFSET, TX_GAS_LIMIT_OFFSET}, +}; + +/// Checks that `TX_GAS_LIMIT_OFFSET` constant is correct. +#[test] +fn test_tx_gas_limit_offset() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let gas_limit = 9999.into(); + let tx = vm.rich_accounts[0].get_l2_tx_for_execute( + Execute::default(), + Some(Fee { + gas_limit, + ..Account::default_fee() + }), + ); + + vm.vm.push_transaction(tx); + + assert!(vm.vm.inner.execution.running_contexts.len() == 1); + let gas_limit_from_memory = vm + .vm + .read_heap_word(TX_DESCRIPTION_OFFSET + TX_GAS_LIMIT_OFFSET); + assert_eq!(gas_limit_from_memory, gas_limit); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/get_used_contracts.rs b/core/lib/multivm/src/versions/era_vm/tests/get_used_contracts.rs new file mode 100644 index 000000000000..8ec01ae8d0ae --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/get_used_contracts.rs @@ -0,0 +1,103 @@ +use std::collections::HashSet; + +use itertools::Itertools; +use zksync_state::ReadStorage; +use zksync_system_constants::CONTRACT_DEPLOYER_ADDRESS; +use zksync_test_account::Account; +use zksync_types::{Execute, U256}; +use zksync_utils::{bytecode::hash_bytecode, h256_to_u256}; + +use crate::{ + era_vm::{ + tests::{ + tester::{TxType, VmTesterBuilder}, + utils::{read_test_contract, BASE_SYSTEM_CONTRACTS}, + }, + vm::Vm, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, +}; + +#[test] +fn test_get_used_contracts() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .build(); + + assert!(known_bytecodes_without_aa_code(&vm.vm).is_empty()); + + // create and push and execute some not-empty factory deps transaction with success status + // to check that `get_decommitted_hashes()` updates + let contract_code = read_test_contract(); + let mut account = Account::random(); + let tx = account.get_deploy_tx(&contract_code, None, TxType::L1 { serial_id: 0 }); + vm.vm.push_transaction(tx.tx.clone()); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed()); + + assert!(vm + .vm + .inner + .state + .decommitted_hashes() + .contains(&h256_to_u256(tx.bytecode_hash))); + + // Note: `Default_AA` will be in the list of used contracts if L2 tx is used + assert_eq!( + *vm.vm.inner.state.decommitted_hashes(), + known_bytecodes_without_aa_code(&vm.vm) + ); + + // create push and execute some non-empty factory deps transaction that fails + // (`known_bytecodes` will be updated but we expect `get_decommitted_hashes()` to not be updated) + + let calldata = [1, 2, 3]; + let big_calldata: Vec = calldata + .iter() + .cycle() + .take(calldata.len() * 1024) + .cloned() + .collect(); + let account2 = Account::random(); + let tx2 = account2.get_l1_tx( + Execute { + contract_address: CONTRACT_DEPLOYER_ADDRESS, + calldata: big_calldata, + value: Default::default(), + factory_deps: vec![vec![1; 32]], + }, + 1, + ); + + vm.vm.push_transaction(tx2.clone()); + + let res2 = vm.vm.execute(VmExecutionMode::OneTx); + + assert!(res2.result.is_failed()); + + for factory_dep in tx2.execute.factory_deps { + let hash = hash_bytecode(&factory_dep); + let hash_to_u256 = h256_to_u256(hash); + assert!(known_bytecodes_without_aa_code(&vm.vm).contains(&hash_to_u256)); + assert!(!vm + .vm + .inner + .state + .decommitted_hashes() + .contains(&hash_to_u256)); + } +} + +fn known_bytecodes_without_aa_code(vm: &Vm) -> HashSet { + let mut known_bytecodes_without_aa_code = vm + .program_cache + .borrow() + .keys() + .cloned() + .collect::>(); + + known_bytecodes_without_aa_code.remove(&h256_to_u256(BASE_SYSTEM_CONTRACTS.default_aa.hash)); + + known_bytecodes_without_aa_code +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/is_write_initial.rs b/core/lib/multivm/src/versions/era_vm/tests/is_write_initial.rs new file mode 100644 index 000000000000..10ef737d4d49 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/is_write_initial.rs @@ -0,0 +1,45 @@ +use zksync_state::ReadStorage; +use zksync_types::get_nonce_key; + +use crate::{ + era_vm::tests::{ + tester::{Account, TxType, VmTesterBuilder}, + utils::read_test_contract, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, +}; + +#[test] +fn test_is_write_initial_behaviour() { + // In this test, we check result of `is_write_initial` at different stages. + // The main idea is to check that `is_write_initial` storage uses the correct cache for initial_writes and doesn't + // messed up it with the repeated writes during the one batch execution. + + let mut account = Account::random(); + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_rich_accounts(vec![account.clone()]) + .build(); + + let nonce_key = get_nonce_key(&account.address); + // Check that the next write to the nonce key will be initial. + assert!(vm + .storage + .as_ref() + .borrow_mut() + .is_write_initial(&nonce_key)); + + let contract_code = read_test_contract(); + let tx = account.get_deploy_tx(&contract_code, None, TxType::L2).tx; + + vm.vm.push_transaction(tx); + vm.vm.execute(VmExecutionMode::OneTx); + + // Check that `is_write_initial` still returns true for the nonce key. + assert!(vm + .storage + .as_ref() + .borrow_mut() + .is_write_initial(&nonce_key)); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/l1_tx_execution.rs b/core/lib/multivm/src/versions/era_vm/tests/l1_tx_execution.rs new file mode 100644 index 000000000000..03f60dc277a8 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/l1_tx_execution.rs @@ -0,0 +1,199 @@ +use ethabi::Token; +use zksync_contracts::l1_messenger_contract; +use zksync_system_constants::{BOOTLOADER_ADDRESS, L1_MESSENGER_ADDRESS}; +use zksync_types::{ + get_code_key, get_known_code_key, + l2_to_l1_log::{L2ToL1Log, UserL2ToL1Log}, + storage_writes_deduplicator::StorageWritesDeduplicator, + Execute, ExecuteTransactionCommon, U256, +}; +use zksync_utils::{h256_to_u256, u256_to_h256}; + +use crate::{ + era_vm::{ + tests::{ + tester::{TxType, VmTesterBuilder}, + utils::{read_test_contract, zk_storage_key_to_lambda, BASE_SYSTEM_CONTRACTS}, + }, + transaction_data::TransactionData, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, +}; + +#[test] +fn test_l1_tx_execution() { + // In this test, we try to execute a contract deployment from L1 + // Here instead of marking code hash via the bootloader means, we will be + // using L1->L2 communication, the same it would likely be done during the priority mode. + + // There are always at least 9 initial writes here, because we pay fees from l1: + // - `totalSupply` of ETH token + // - balance of the refund recipient + // - balance of the bootloader + // - `tx_rolling` hash + // - `gasPerPubdataByte` + // - `basePubdataSpent` + // - rolling hash of L2->L1 logs + // - transaction number in block counter + // - L2->L1 log counter in `L1Messenger` + + // TODO(PLA-537): right now we are using 5 slots instead of 9 due to 0 fee for transaction. + let basic_initial_writes = 5; + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_base_system_smart_contracts(BASE_SYSTEM_CONTRACTS.clone()) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let contract_code = read_test_contract(); + let account = &mut vm.rich_accounts[0]; + let deploy_tx = account.get_deploy_tx(&contract_code, None, TxType::L1 { serial_id: 1 }); + let tx_data: TransactionData = deploy_tx.tx.clone().into(); + + let required_l2_to_l1_logs: Vec<_> = vec![L2ToL1Log { + shard_id: 0, + is_service: true, + tx_number_in_block: 0, + sender: BOOTLOADER_ADDRESS, + key: tx_data.tx_hash(0.into()), + value: u256_to_h256(U256::from(1u32)), + }] + .into_iter() + .map(UserL2ToL1Log) + .collect(); + + vm.vm.push_transaction(deploy_tx.tx.clone()); + + let res = vm.vm.execute(VmExecutionMode::OneTx); + + // The code hash of the deployed contract should be marked as republished. + let known_codes_key = get_known_code_key(&deploy_tx.bytecode_hash); + + // The contract should be deployed successfully. + let account_code_key = get_code_key(&deploy_tx.address); + + assert!(!res.result.is_failed()); + + for (expected_value, storage_location) in [ + (U256::from(1u32), known_codes_key), + (h256_to_u256(deploy_tx.bytecode_hash), account_code_key), + ] { + let actual_value = vm + .vm + .inner + .state + .storage_changes() + .get(&zk_storage_key_to_lambda(&storage_location)); + assert_eq!(expected_value, *actual_value.unwrap_or(&U256::zero())); + } + + assert_eq!(res.logs.user_l2_to_l1_logs, required_l2_to_l1_logs); + + let tx = account.get_test_contract_transaction( + deploy_tx.address, + true, + None, + false, + TxType::L1 { serial_id: 0 }, + ); + vm.vm.push_transaction(tx); + let res = vm.vm.execute(VmExecutionMode::OneTx); + let storage_logs = res.logs.storage_logs; + let res = StorageWritesDeduplicator::apply_on_empty_state(&storage_logs); + + // Tx panicked + assert_eq!(res.initial_storage_writes, basic_initial_writes); + + let tx = account.get_test_contract_transaction( + deploy_tx.address, + false, + None, + false, + TxType::L1 { serial_id: 0 }, + ); + vm.vm.push_transaction(tx.clone()); + let res = vm.vm.execute(VmExecutionMode::OneTx); + let storage_logs: Vec = res.logs.storage_logs; + let res = StorageWritesDeduplicator::apply_on_empty_state(&storage_logs); + // We changed one slot inside contract. However, the rewrite of the `basePubdataSpent` didn't happen, since it was the same + // as the start of the previous tx. Thus we have `+1` slot for the changed counter and `-1` slot for base pubdata spent + assert_eq!(res.initial_storage_writes, basic_initial_writes); + + // No repeated writes + let repeated_writes = res.repeated_storage_writes; + assert_eq!(res.repeated_storage_writes, 0); + + vm.vm.push_transaction(tx); + let storage_logs = vm.vm.execute(VmExecutionMode::OneTx).logs.storage_logs; + let res = StorageWritesDeduplicator::apply_on_empty_state(&storage_logs); + // We do the same storage write, it will be deduplicated, so still 4 initial write and 0 repeated. + // But now the base pubdata spent has changed too. + assert_eq!(res.initial_storage_writes, basic_initial_writes + 1); + assert_eq!(res.repeated_storage_writes, repeated_writes); + + let tx = account.get_test_contract_transaction( + deploy_tx.address, + false, + Some(10.into()), + false, + TxType::L1 { serial_id: 1 }, + ); + vm.vm.push_transaction(tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + // Method is not payable tx should fail + assert!(result.result.is_failed(), "The transaction should fail"); + + let res = StorageWritesDeduplicator::apply_on_empty_state(&result.logs.storage_logs); + assert_eq!(res.initial_storage_writes, basic_initial_writes); + assert_eq!(res.repeated_storage_writes, 1); +} + +#[test] +fn test_l1_tx_execution_high_gas_limit() { + // In this test, we try to execute an L1->L2 transaction with a high gas limit. + // Usually priority transactions with dangerously gas limit should even pass the checks on the L1, + // however, they might pass during the transition period to the new fee model, so we check that we can safely process those. + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_base_system_smart_contracts(BASE_SYSTEM_CONTRACTS.clone()) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let account = &mut vm.rich_accounts[0]; + + let l1_messenger = l1_messenger_contract(); + + let contract_function = l1_messenger.function("sendToL1").unwrap(); + let params = [ + // Even a message of size 100k should not be able to be sent by a priority transaction + Token::Bytes(vec![0u8; 100_000]), + ]; + let calldata = contract_function.encode_input(¶ms).unwrap(); + + let mut tx = account.get_l1_tx( + Execute { + contract_address: L1_MESSENGER_ADDRESS, + value: 0.into(), + factory_deps: vec![], + calldata, + }, + 0, + ); + + if let ExecuteTransactionCommon::L1(data) = &mut tx.common_data { + // Using some large gas limit + data.gas_limit = 300_000_000.into(); + } else { + unreachable!() + }; + + vm.vm.push_transaction(tx); + + let res = vm.vm.execute(VmExecutionMode::OneTx); + + assert!(res.result.is_failed(), "The transaction should've failed"); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/l2_blocks.rs b/core/lib/multivm/src/versions/era_vm/tests/l2_blocks.rs new file mode 100644 index 000000000000..ed572e8ff312 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/l2_blocks.rs @@ -0,0 +1,424 @@ +//! +//! Tests for the bootloader +//! The description for each of the tests can be found in the corresponding `.yul` file. +//! + +use zksync_state::ReadStorage; +use zksync_system_constants::REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE; +use zksync_types::{ + block::{pack_block_info, L2BlockHasher}, + AccountTreeId, Execute, ExecuteTransactionCommon, L1BatchNumber, L1TxCommonData, L2BlockNumber, + ProtocolVersionId, StorageKey, Transaction, H160, H256, SYSTEM_CONTEXT_ADDRESS, + SYSTEM_CONTEXT_BLOCK_INFO_POSITION, SYSTEM_CONTEXT_CURRENT_L2_BLOCK_INFO_POSITION, + SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION, U256, +}; +use zksync_utils::{h256_to_u256, u256_to_h256}; + +use crate::{ + era_vm::{ + tests::tester::{default_l1_batch, VmTesterBuilder}, + vm::Vm, + }, + interface::{ExecutionResult, Halt, L2BlockEnv, TxExecutionMode, VmExecutionMode, VmInterface}, + vm_latest::{ + constants::{TX_OPERATOR_L2_BLOCK_INFO_OFFSET, TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO}, + utils::l2_blocks::get_l2_block_hash_key, + }, +}; + +fn get_l1_noop() -> Transaction { + Transaction { + common_data: ExecuteTransactionCommon::L1(L1TxCommonData { + sender: H160::random(), + gas_limit: U256::from(2000000u32), + gas_per_pubdata_limit: REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE.into(), + ..Default::default() + }), + execute: Execute { + contract_address: H160::zero(), + calldata: vec![], + value: U256::zero(), + factory_deps: vec![], + }, + received_timestamp_ms: 0, + raw_bytes: None, + } +} + +#[test] +fn test_l2_block_initialization_timestamp() { + // This test checks that the L2 block initialization works correctly. + // Here we check that that the first block must have timestamp that is greater or equal to the timestamp + // of the current batch. + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + // Override the timestamp of the current L2 block to be 0. + vm.vm.bootloader_state.push_l2_block(L2BlockEnv { + number: 1, + timestamp: 0, + prev_block_hash: L2BlockHasher::legacy_hash(L2BlockNumber(0)), + max_virtual_blocks_to_create: 1, + }); + let l1_tx = get_l1_noop(); + + vm.vm.push_transaction(l1_tx); + let res = vm.vm.execute(VmExecutionMode::OneTx); + + assert_eq!( + res.result, + ExecutionResult::Halt {reason: Halt::FailedToSetL2Block("The timestamp of the L2 block must be greater than or equal to the timestamp of the current batch".to_string())} + ); +} + +#[test] +fn test_l2_block_initialization_number_non_zero() { + // This test checks that the L2 block initialization works correctly. + // Here we check that the first L2 block number can not be zero. + + let l1_batch = default_l1_batch(L1BatchNumber(1)); + let first_l2_block = L2BlockEnv { + number: 0, + timestamp: l1_batch.timestamp, + prev_block_hash: L2BlockHasher::legacy_hash(L2BlockNumber(0)), + max_virtual_blocks_to_create: 1, + }; + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_l1_batch_env(l1_batch) + .with_random_rich_accounts(1) + .build(); + + let l1_tx = get_l1_noop(); + + vm.vm.push_transaction(l1_tx); + + set_manual_l2_block_info(&mut vm.vm, 0, first_l2_block); + + let res = vm.vm.execute(VmExecutionMode::OneTx); + + assert_eq!( + res.result, + ExecutionResult::Halt { + reason: Halt::FailedToSetL2Block( + "L2 block number is never expected to be zero".to_string() + ) + } + ); +} + +fn test_same_l2_block( + expected_error: Option, + override_timestamp: Option, + override_prev_block_hash: Option, +) { + let mut l1_batch = default_l1_batch(L1BatchNumber(1)); + l1_batch.timestamp = 1; + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_l1_batch_env(l1_batch) + .with_random_rich_accounts(1) + .build(); + + let l1_tx = get_l1_noop(); + vm.vm.push_transaction(l1_tx.clone()); + let res = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!res.result.is_failed()); + + let mut current_l2_block = vm.vm.batch_env.first_l2_block; + + if let Some(timestamp) = override_timestamp { + current_l2_block.timestamp = timestamp; + } + if let Some(prev_block_hash) = override_prev_block_hash { + current_l2_block.prev_block_hash = prev_block_hash; + } + + if (None, None) == (override_timestamp, override_prev_block_hash) { + current_l2_block.max_virtual_blocks_to_create = 0; + } + + vm.vm.push_transaction(l1_tx); + set_manual_l2_block_info(&mut vm.vm, 1, current_l2_block); + + let result = vm.vm.execute(VmExecutionMode::OneTx); + + if let Some(err) = expected_error { + assert_eq!(result.result, ExecutionResult::Halt { reason: err }); + } else { + assert_eq!(result.result, ExecutionResult::Success { output: vec![] }); + } +} + +#[test] +fn test_l2_block_same_l2_block() { + // This test aims to test the case when there are multiple transactions inside the same L2 block. + + // Case 1: Incorrect timestamp + test_same_l2_block( + Some(Halt::FailedToSetL2Block( + "The timestamp of the same L2 block must be same".to_string(), + )), + Some(0), + None, + ); + + // Case 2: Incorrect previous block hash + test_same_l2_block( + Some(Halt::FailedToSetL2Block( + "The previous hash of the same L2 block must be same".to_string(), + )), + None, + Some(H256::zero()), + ); + + // Case 3: Correct continuation of the same L2 block + test_same_l2_block(None, None, None); +} + +fn test_new_l2_block( + first_l2_block: L2BlockEnv, + overriden_second_block_number: Option, + overriden_second_block_timestamp: Option, + overriden_second_block_prev_block_hash: Option, + expected_error: Option, +) { + let mut l1_batch = default_l1_batch(L1BatchNumber(1)); + l1_batch.timestamp = 1; + l1_batch.first_l2_block = first_l2_block; + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_l1_batch_env(l1_batch) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let l1_tx = get_l1_noop(); + + // Firstly we execute the first transaction + vm.vm.push_transaction(l1_tx.clone()); + vm.vm.execute(VmExecutionMode::OneTx); + + let mut second_l2_block = vm.vm.batch_env.first_l2_block; + second_l2_block.number += 1; + second_l2_block.timestamp += 1; + second_l2_block.prev_block_hash = vm.vm.bootloader_state.last_l2_block().get_hash(); + + if let Some(block_number) = overriden_second_block_number { + second_l2_block.number = block_number; + } + if let Some(timestamp) = overriden_second_block_timestamp { + second_l2_block.timestamp = timestamp; + } + if let Some(prev_block_hash) = overriden_second_block_prev_block_hash { + second_l2_block.prev_block_hash = prev_block_hash; + } + + vm.vm.bootloader_state.push_l2_block(second_l2_block); + + vm.vm.push_transaction(l1_tx); + + let result = vm.vm.execute(VmExecutionMode::OneTx); + if let Some(err) = expected_error { + assert_eq!(result.result, ExecutionResult::Halt { reason: err }); + } else { + assert_eq!(result.result, ExecutionResult::Success { output: vec![] }); + } +} + +#[test] +fn test_l2_block_new_l2_block() { + // This test is aimed to cover potential issue + + let correct_first_block = L2BlockEnv { + number: 1, + timestamp: 1, + prev_block_hash: L2BlockHasher::legacy_hash(L2BlockNumber(0)), + max_virtual_blocks_to_create: 1, + }; + + // Case 1: Block number increasing by more than 1 + test_new_l2_block( + correct_first_block, + Some(3), + None, + None, + Some(Halt::FailedToSetL2Block( + "Invalid new L2 block number".to_string(), + )), + ); + + // Case 2: Timestamp not increasing + test_new_l2_block( + correct_first_block, + None, + Some(1), + None, + Some(Halt::FailedToSetL2Block("The timestamp of the new L2 block must be greater than the timestamp of the previous L2 block".to_string())), + ); + + // Case 3: Incorrect previous block hash + test_new_l2_block( + correct_first_block, + None, + None, + Some(H256::zero()), + Some(Halt::FailedToSetL2Block( + "The current L2 block hash is incorrect".to_string(), + )), + ); + + // Case 4: Correct new block + test_new_l2_block(correct_first_block, None, None, None, None); +} + +#[allow(clippy::too_many_arguments)] +fn test_first_in_batch( + miniblock_timestamp: u64, + miniblock_number: u32, + pending_txs_hash: H256, + batch_timestamp: u64, + new_batch_timestamp: u64, + batch_number: u32, + proposed_block: L2BlockEnv, + expected_error: Option, +) { + let mut l1_batch = default_l1_batch(L1BatchNumber(1)); + l1_batch.number += 1; + l1_batch.timestamp = new_batch_timestamp; + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_l1_batch_env(l1_batch) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + let l1_tx = get_l1_noop(); + + // Setting the values provided. + let mut storage_ptr = vm.vm.storage.borrow_mut(); + let miniblock_info_slot = StorageKey::new( + AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS), + SYSTEM_CONTEXT_CURRENT_L2_BLOCK_INFO_POSITION, + ); + let pending_txs_hash_slot = StorageKey::new( + AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS), + SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION, + ); + let batch_info_slot = StorageKey::new( + AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS), + SYSTEM_CONTEXT_BLOCK_INFO_POSITION, + ); + let prev_block_hash_position = get_l2_block_hash_key(miniblock_number - 1); + + storage_ptr.set_value( + miniblock_info_slot, + u256_to_h256(pack_block_info( + miniblock_number as u64, + miniblock_timestamp, + )), + ); + storage_ptr.set_value(pending_txs_hash_slot, pending_txs_hash); + storage_ptr.set_value( + batch_info_slot, + u256_to_h256(pack_block_info(batch_number as u64, batch_timestamp)), + ); + storage_ptr.set_value( + prev_block_hash_position, + L2BlockHasher::legacy_hash(L2BlockNumber(miniblock_number - 1)), + ); + drop(storage_ptr); + + // In order to skip checks from the Rust side of the VM, we firstly use some definitely correct L2 block info. + // And then override it with the user-provided value + + let last_l2_block = vm.vm.bootloader_state.last_l2_block(); + let new_l2_block = L2BlockEnv { + number: last_l2_block.number + 1, + timestamp: last_l2_block.timestamp + 1, + prev_block_hash: last_l2_block.get_hash(), + max_virtual_blocks_to_create: last_l2_block.max_virtual_blocks_to_create, + }; + + vm.vm.bootloader_state.push_l2_block(new_l2_block); + vm.vm.push_transaction(l1_tx); + set_manual_l2_block_info(&mut vm.vm, 0, proposed_block); + + let result = vm.vm.execute(VmExecutionMode::OneTx); + if let Some(err) = expected_error { + assert_eq!(result.result, ExecutionResult::Halt { reason: err }); + } else { + assert_eq!(result.result, ExecutionResult::Success { output: vec![] }); + } +} + +#[test] +fn test_l2_block_first_in_batch() { + let prev_block_hash = L2BlockHasher::legacy_hash(L2BlockNumber(0)); + let prev_block_hash = L2BlockHasher::new(L2BlockNumber(1), 1, prev_block_hash) + .finalize(ProtocolVersionId::latest()); + test_first_in_batch( + 1, + 1, + H256::zero(), + 1, + 2, + 1, + L2BlockEnv { + number: 2, + timestamp: 2, + prev_block_hash, + max_virtual_blocks_to_create: 1, + }, + None, + ); + + let prev_block_hash = L2BlockHasher::legacy_hash(L2BlockNumber(0)); + let prev_block_hash = L2BlockHasher::new(L2BlockNumber(1), 8, prev_block_hash) + .finalize(ProtocolVersionId::latest()); + test_first_in_batch( + 8, + 1, + H256::zero(), + 5, + 12, + 1, + L2BlockEnv { + number: 2, + timestamp: 9, + prev_block_hash, + max_virtual_blocks_to_create: 1, + }, + Some(Halt::FailedToSetL2Block("The timestamp of the L2 block must be greater than or equal to the timestamp of the current batch".to_string())), + ); +} + +fn set_manual_l2_block_info( + vm: &mut Vm, + tx_number: usize, + block_info: L2BlockEnv, +) { + let fictive_miniblock_position = + TX_OPERATOR_L2_BLOCK_INFO_OFFSET + TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO * tx_number; + + vm.write_to_bootloader_heap([ + (fictive_miniblock_position, block_info.number.into()), + (fictive_miniblock_position + 1, block_info.timestamp.into()), + ( + fictive_miniblock_position + 2, + h256_to_u256(block_info.prev_block_hash), + ), + ( + fictive_miniblock_position + 3, + block_info.max_virtual_blocks_to_create.into(), + ), + ]) +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/mod.rs b/core/lib/multivm/src/versions/era_vm/tests/mod.rs new file mode 100644 index 000000000000..801b6450ef85 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/mod.rs @@ -0,0 +1,25 @@ +mod block_tip; +mod bootloader; +mod bytecode_publishing; +mod call_tracer; +mod circuits; +mod code_oracle; +mod default_aa; +mod gas_limit; +mod get_used_contracts; +mod is_write_initial; +mod l1_tx_execution; +mod l2_blocks; +mod nonce_holder; +mod precompiles; +mod refunds; +mod require_eip712; +mod rollbacks; +mod sekp256r1; +mod simple_execution; +mod storage; +mod tester; +mod tracing_execution_error; +mod transfer; +mod upgrade; +mod utils; diff --git a/core/lib/multivm/src/versions/era_vm/tests/nonce_holder.rs b/core/lib/multivm/src/versions/era_vm/tests/nonce_holder.rs new file mode 100644 index 000000000000..482f3f4ffaea --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/nonce_holder.rs @@ -0,0 +1,179 @@ +use zksync_types::{Execute, ExecuteTransactionCommon, Nonce}; + +use crate::{ + era_vm::tests::{ + tester::{Account, VmTesterBuilder}, + utils::read_nonce_holder_tester, + }, + interface::{ + ExecutionResult, Halt, TxExecutionMode, TxRevertReason, VmExecutionMode, VmInterface, + VmRevertReason, + }, +}; + +pub enum NonceHolderTestMode { + SetValueUnderNonce, + IncreaseMinNonceBy5, + IncreaseMinNonceTooMuch, + LeaveNonceUnused, + IncreaseMinNonceBy1, + SwitchToArbitraryOrdering, +} + +impl From for u8 { + fn from(mode: NonceHolderTestMode) -> u8 { + match mode { + NonceHolderTestMode::SetValueUnderNonce => 0, + NonceHolderTestMode::IncreaseMinNonceBy5 => 1, + NonceHolderTestMode::IncreaseMinNonceTooMuch => 2, + NonceHolderTestMode::LeaveNonceUnused => 3, + NonceHolderTestMode::IncreaseMinNonceBy1 => 4, + NonceHolderTestMode::SwitchToArbitraryOrdering => 5, + } + } +} + +#[test] +fn test_nonce_holder() { + let mut account = Account::random(); + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_deployer() + .with_custom_contracts(vec![( + read_nonce_holder_tester().to_vec(), + account.address, + true, + )]) + .with_rich_accounts(vec![account.clone()]) + .build(); + + let mut run_nonce_test = |nonce: u32, + test_mode: NonceHolderTestMode, + error_message: Option, + comment: &'static str| { + // In this test we have to reset VM state after each test case. Because once bootloader failed during the validation of the transaction, + // it will fail again and again. At the same time we have to keep the same storage, because we want to keep the nonce holder contract state. + // The easiest way in terms of lifetimes is to reuse `vm_builder` to achieve it. + vm.reset_state(true); + let mut transaction = account.get_l2_tx_for_execute_with_nonce( + Execute { + contract_address: account.address, + calldata: vec![12], + value: Default::default(), + factory_deps: vec![], + }, + None, + Nonce(nonce), + ); + let ExecuteTransactionCommon::L2(tx_data) = &mut transaction.common_data else { + unreachable!(); + }; + tx_data.signature = vec![test_mode.into()]; + vm.vm.push_transaction(transaction); + let result = vm.vm.execute(VmExecutionMode::OneTx); + + if let Some(msg) = error_message { + let expected_error = + TxRevertReason::Halt(Halt::ValidationFailed(VmRevertReason::General { + msg, + data: vec![], + })); + let ExecutionResult::Halt { reason } = result.result else { + panic!("Expected revert, got {:?}", result.result); + }; + assert_eq!(reason.to_string(), expected_error.to_string(), "{comment}"); + } else { + assert!(!result.result.is_failed(), "{comment}: {result:?}"); + } + }; + // Test 1: trying to set value under non sequential nonce value. + run_nonce_test( + 1u32, + NonceHolderTestMode::SetValueUnderNonce, + Some("Previous nonce has not been used".to_string()), + "Allowed to set value under non sequential value", + ); + + // Test 2: increase min nonce by 1 with sequential nonce ordering: + run_nonce_test( + 0u32, + NonceHolderTestMode::IncreaseMinNonceBy1, + None, + "Failed to increment nonce by 1 for sequential account", + ); + + // Test 3: correctly set value under nonce with sequential nonce ordering: + run_nonce_test( + 1u32, + NonceHolderTestMode::SetValueUnderNonce, + None, + "Failed to set value under nonce sequential value", + ); + + // Test 5: migrate to the arbitrary nonce ordering: + run_nonce_test( + 2u32, + NonceHolderTestMode::SwitchToArbitraryOrdering, + None, + "Failed to switch to arbitrary ordering", + ); + + // Test 6: increase min nonce by 5 + run_nonce_test( + 6u32, + NonceHolderTestMode::IncreaseMinNonceBy5, + None, + "Failed to increase min nonce by 5", + ); + + // Test 7: since the nonces in range [6,10] are no longer allowed, the + // tx with nonce 10 should not be allowed + run_nonce_test( + 10u32, + NonceHolderTestMode::IncreaseMinNonceBy5, + Some("Reusing the same nonce twice".to_string()), + "Allowed to reuse nonce below the minimal one", + ); + + // Test 8: we should be able to use nonce 13 + run_nonce_test( + 13u32, + NonceHolderTestMode::SetValueUnderNonce, + None, + "Did not allow to use unused nonce 10", + ); + + // Test 9: we should not be able to reuse nonce 13 + run_nonce_test( + 13u32, + NonceHolderTestMode::IncreaseMinNonceBy5, + Some("Reusing the same nonce twice".to_string()), + "Allowed to reuse the same nonce twice", + ); + + // Test 10: we should be able to simply use nonce 14, while bumping the minimal nonce by 5 + run_nonce_test( + 14u32, + NonceHolderTestMode::IncreaseMinNonceBy5, + None, + "Did not allow to use a bumped nonce", + ); + + // Test 11: Do not allow bumping nonce by too much + run_nonce_test( + 16u32, + NonceHolderTestMode::IncreaseMinNonceTooMuch, + Some("The value for incrementing the nonce is too high".to_string()), + "Allowed for incrementing min nonce too much", + ); + + // Test 12: Do not allow not setting a nonce as used + run_nonce_test( + 16u32, + NonceHolderTestMode::LeaveNonceUnused, + Some("The nonce was not set as used".to_string()), + "Allowed to leave nonce as unused", + ); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/precompiles.rs b/core/lib/multivm/src/versions/era_vm/tests/precompiles.rs new file mode 100644 index 000000000000..b3008295482a --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/precompiles.rs @@ -0,0 +1,108 @@ +use zksync_types::{Address, Execute}; + +use crate::{ + era_vm::tests::{tester::VmTesterBuilder, utils::read_precompiles_contract}, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, + vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT, +}; + +#[test] +fn test_keccak() { + // Execute special transaction and check that at least 1000 keccak calls were made. + let contract = read_precompiles_contract(); + let address = Address::random(); + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_random_rich_accounts(1) + .with_deployer() + .with_bootloader_gas_limit(BATCH_COMPUTATIONAL_GAS_LIMIT) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_custom_contracts(vec![(contract, address, true)]) + .build(); + + // calldata for `doKeccak(1000)`. + let keccak1000_calldata = + "370f20ac00000000000000000000000000000000000000000000000000000000000003e8"; + + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: address, + calldata: hex::decode(keccak1000_calldata).unwrap(), + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + vm.vm.push_transaction(tx); + let _ = vm.vm.inspect(Default::default(), VmExecutionMode::OneTx); + + let keccak_count = vm.vm.inner.statistics.keccak256_cycles; + + assert!(keccak_count >= 1000); +} + +#[test] +fn test_sha256() { + // Execute special transaction and check that at least 1000 `sha256` calls were made. + let contract = read_precompiles_contract(); + let address = Address::random(); + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_random_rich_accounts(1) + .with_deployer() + .with_bootloader_gas_limit(BATCH_COMPUTATIONAL_GAS_LIMIT) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_custom_contracts(vec![(contract, address, true)]) + .build(); + + // calldata for `doSha256(1000)`. + let sha1000_calldata = + "5d0b4fb500000000000000000000000000000000000000000000000000000000000003e8"; + + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: address, + calldata: hex::decode(sha1000_calldata).unwrap(), + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + vm.vm.push_transaction(tx); + let _ = vm.vm.inspect(Default::default(), VmExecutionMode::OneTx); + + let sha_count = vm.vm.inner.statistics.sha256_cycles; + + assert!(sha_count >= 1000); +} + +#[test] +fn test_ecrecover() { + // Execute simple transfer and check that exactly 1 `ecrecover` call was made (it's done during tx validation). + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_random_rich_accounts(1) + .with_deployer() + .with_bootloader_gas_limit(BATCH_COMPUTATIONAL_GAS_LIMIT) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .build(); + + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: account.address, + calldata: Vec::new(), + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + vm.vm.push_transaction(tx); + let _ = vm.vm.inspect(Default::default(), VmExecutionMode::OneTx); + + let ecrecover_count = vm.vm.inner.statistics.ecrecover_cycles; + + assert_eq!(ecrecover_count, 1); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/refunds.rs b/core/lib/multivm/src/versions/era_vm/tests/refunds.rs new file mode 100644 index 000000000000..722949f77cb4 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/refunds.rs @@ -0,0 +1,209 @@ +use ethabi::Token; +use zksync_types::{Address, Execute, U256}; + +use crate::{ + era_vm::tests::{ + tester::{DeployContractsTx, TxType, VmTesterBuilder}, + utils::{read_expensive_contract, read_test_contract, sort_execution_state}, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, +}; + +#[test] +fn test_predetermined_refunded_gas() { + // In this test, we compare the execution of the bootloader with the predefined + // refunded gas and without them + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + let l1_batch = vm.vm.batch_env.clone(); + + let counter = read_test_contract(); + let account = &mut vm.rich_accounts[0]; + + let DeployContractsTx { + tx, + bytecode_hash: _, + address: _, + } = account.get_deploy_tx(&counter, None, TxType::L2); + vm.vm.push_transaction(tx.clone()); + let result = vm.vm.execute(VmExecutionMode::OneTx); + + assert!(!result.result.is_failed()); + + // If the refund provided by the operator or the final refund are the 0 + // there is no impact of the operator's refund at all and so this test does not + // make much sense. + assert!( + result.refunds.operator_suggested_refund > 0, + "The operator's refund is 0" + ); + assert!(result.refunds.gas_refunded > 0, "The final refund is 0"); + + let result_without_predefined_refunds = vm.vm.execute(VmExecutionMode::Batch); + let mut current_state_without_predefined_refunds = vm.vm.get_current_execution_state(); + sort_execution_state(&mut current_state_without_predefined_refunds); + assert!(!result_without_predefined_refunds.result.is_failed(),); + + // Here we want to provide the same refund from the operator and check that it's the correct one. + // We execute the whole block without refund tracer, because refund tracer will eventually override the provided refund. + // But the overall result should be the same + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_l1_batch_env(l1_batch.clone()) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_rich_accounts(vec![account.clone()]) + .build(); + + vm.vm + .push_transaction_inner(tx.clone(), result.refunds.gas_refunded, true); + + let result_with_predefined_refunds = vm.vm.execute(VmExecutionMode::Batch); + let mut current_state_with_predefined_refunds = vm.vm.get_current_execution_state(); + sort_execution_state(&mut current_state_with_predefined_refunds); + assert!(!result_with_predefined_refunds.result.is_failed()); + + assert_eq!( + current_state_with_predefined_refunds.events, + current_state_without_predefined_refunds.events + ); + + assert_eq!( + current_state_with_predefined_refunds.user_l2_to_l1_logs, + current_state_without_predefined_refunds.user_l2_to_l1_logs + ); + + assert_eq!( + current_state_with_predefined_refunds.system_logs, + current_state_without_predefined_refunds.system_logs + ); + + assert_eq!( + current_state_with_predefined_refunds.deduplicated_storage_logs, + current_state_without_predefined_refunds.deduplicated_storage_logs + ); + assert_eq!( + current_state_with_predefined_refunds.used_contract_hashes, + current_state_without_predefined_refunds.used_contract_hashes + ); + + // In this test we put the different refund from the operator. + // We still can't use the refund tracer, because it will override the refund. + // But we can check that the logs and events have changed. + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_l1_batch_env(l1_batch) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_rich_accounts(vec![account.clone()]) + .build(); + + let changed_operator_suggested_refund = result.refunds.gas_refunded + 1000; + vm.vm + .push_transaction_inner(tx, changed_operator_suggested_refund, true); + let result = vm.vm.execute(VmExecutionMode::Batch); + let mut current_state_with_changed_predefined_refunds = vm.vm.get_current_execution_state(); + sort_execution_state(&mut current_state_with_changed_predefined_refunds); + + assert!(!result.result.is_failed()); + + assert_eq!( + current_state_with_changed_predefined_refunds.events.len(), + current_state_without_predefined_refunds.events.len() + ); + + assert_ne!( + current_state_with_changed_predefined_refunds.events, + current_state_without_predefined_refunds.events + ); + + assert_eq!( + current_state_with_changed_predefined_refunds.user_l2_to_l1_logs, + current_state_without_predefined_refunds.user_l2_to_l1_logs + ); + + assert_ne!( + current_state_with_changed_predefined_refunds.system_logs, + current_state_without_predefined_refunds.system_logs + ); + + assert_eq!( + current_state_with_changed_predefined_refunds + .deduplicated_storage_logs + .len(), + current_state_without_predefined_refunds + .deduplicated_storage_logs + .len() + ); + + assert_ne!( + current_state_with_changed_predefined_refunds.deduplicated_storage_logs, + current_state_without_predefined_refunds.deduplicated_storage_logs + ); + assert_eq!( + current_state_with_changed_predefined_refunds.used_contract_hashes, + current_state_without_predefined_refunds.used_contract_hashes + ); +} + +#[test] +fn negative_pubdata_for_transaction() { + let expensive_contract_address = Address::random(); + let (expensive_contract_bytecode, expensive_contract) = read_expensive_contract(); + let expensive_function = expensive_contract.function("expensive").unwrap(); + let cleanup_function = expensive_contract.function("cleanUp").unwrap(); + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .with_custom_contracts(vec![( + expensive_contract_bytecode, + expensive_contract_address, + false, + )]) + .build(); + + let expensive_tx = vm.rich_accounts[0].get_l2_tx_for_execute( + Execute { + contract_address: expensive_contract_address, + calldata: expensive_function + .encode_input(&[Token::Uint(10.into())]) + .unwrap(), + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + vm.vm.push_transaction(expensive_tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "Transaction wasn't successful: {result:#?}" + ); + + // This transaction cleans all initial writes in the contract, thus having negative `pubdata` impact. + let clean_up_tx = vm.rich_accounts[0].get_l2_tx_for_execute( + Execute { + contract_address: expensive_contract_address, + calldata: cleanup_function.encode_input(&[]).unwrap(), + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + vm.vm.push_transaction(clean_up_tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "Transaction wasn't successful: {result:#?}" + ); + assert!(result.refunds.operator_suggested_refund > 0); + assert_eq!( + result.refunds.gas_refunded, + result.refunds.operator_suggested_refund + ); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/require_eip712.rs b/core/lib/multivm/src/versions/era_vm/tests/require_eip712.rs new file mode 100644 index 000000000000..47fec38e486d --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/require_eip712.rs @@ -0,0 +1,172 @@ +use ethabi::Token; +use zksync_eth_signer::{EthereumSigner, TransactionParameters}; +use zksync_state::ReadStorage; +use zksync_system_constants::L2_BASE_TOKEN_ADDRESS; +use zksync_types::{ + fee::Fee, l2::L2Tx, transaction_request::TransactionRequest, + utils::storage_key_for_standard_token_balance, AccountTreeId, Address, Eip712Domain, Execute, + L2ChainId, Nonce, Transaction, U256, +}; +use zksync_utils::h256_to_u256; + +use super::utils::zk_storage_key_to_lambda; +use crate::{ + era_vm::tests::{ + tester::{Account, VmTester, VmTesterBuilder}, + utils::read_many_owners_custom_account_contract, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, +}; + +impl VmTester { + pub(crate) fn get_eth_balance(&mut self, address: Address) -> U256 { + let key = storage_key_for_standard_token_balance( + AccountTreeId::new(L2_BASE_TOKEN_ADDRESS), + &address, + ); + self.vm + .inner + .state + .storage_changes() + .get(&zk_storage_key_to_lambda(&key)) + .copied() + .unwrap_or_else(|| h256_to_u256(self.vm.storage.read_value(&key))) + } +} + +/// This test deploys 'buggy' account abstraction code, and then tries accessing it both with legacy +/// and EIP712 transactions. +/// Currently we support both, but in the future, we should allow only EIP712 transactions to access the AA accounts. +#[tokio::test] +async fn test_require_eip712() { + // Use 3 accounts: + // - `private_address` - EOA account, where we have the key + // - `account_address` - AA account, where the contract is deployed + // - beneficiary - an EOA account, where we'll try to transfer the tokens. + let account_abstraction = Account::random(); + let mut private_account = Account::random(); + let beneficiary = Account::random(); + + let (bytecode, contract) = read_many_owners_custom_account_contract(); + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_custom_contracts(vec![(bytecode, account_abstraction.address, true)]) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_rich_accounts(vec![account_abstraction.clone(), private_account.clone()]) + .build(); + + assert_eq!(vm.get_eth_balance(beneficiary.address), U256::from(0)); + + let chain_id: u32 = 270; + + // First, let's set the owners of the AA account to the `private_address`. + // (so that messages signed by `private_address`, are authorized to act on behalf of the AA account). + let set_owners_function = contract.function("setOwners").unwrap(); + let encoded_input = set_owners_function + .encode_input(&[Token::Array(vec![Token::Address(private_account.address)])]) + .unwrap(); + + let tx = private_account.get_l2_tx_for_execute( + Execute { + contract_address: account_abstraction.address, + calldata: encoded_input, + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed()); + + let private_account_balance = vm.get_eth_balance(private_account.address); + + // And now let's do the transfer from the 'account abstraction' to 'beneficiary' (using 'legacy' transaction). + // Normally this would not work - unless the operator is malicious. + let aa_raw_tx = TransactionParameters { + nonce: U256::from(0), + to: Some(beneficiary.address), + gas: U256::from(100000000), + gas_price: Some(U256::from(10000000)), + value: U256::from(888000088), + data: vec![], + chain_id: 270, + transaction_type: None, + access_list: None, + max_fee_per_gas: U256::from(1000000000), + max_priority_fee_per_gas: U256::from(1000000000), + max_fee_per_blob_gas: None, + blob_versioned_hashes: None, + }; + + let aa_tx = private_account.sign_legacy_tx(aa_raw_tx).await; + let (tx_request, hash) = TransactionRequest::from_bytes(&aa_tx, L2ChainId::from(270)).unwrap(); + + let mut l2_tx: L2Tx = L2Tx::from_request(tx_request, 10000).unwrap(); + l2_tx.set_input(aa_tx, hash); + // Pretend that operator is malicious and sets the initiator to the AA account. + l2_tx.common_data.initiator_address = account_abstraction.address; + let transaction: Transaction = l2_tx.into(); + + vm.vm.push_transaction(transaction); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed()); + + assert_eq!( + vm.get_eth_balance(beneficiary.address), + U256::from(888000088) + ); + // Make sure that the tokens were transferred from the AA account. + assert_eq!( + private_account_balance, + vm.get_eth_balance(private_account.address) + ); + + // // Now send the 'classic' EIP712 transaction + let tx_712 = L2Tx::new( + beneficiary.address, + vec![], + Nonce(1), + Fee { + gas_limit: U256::from(1000000000), + max_fee_per_gas: U256::from(1000000000), + max_priority_fee_per_gas: U256::from(1000000000), + gas_per_pubdata_limit: U256::from(1000000000), + }, + account_abstraction.address, + U256::from(28374938), + vec![], + Default::default(), + ); + + let mut transaction_request: TransactionRequest = tx_712.into(); + transaction_request.chain_id = Some(chain_id.into()); + + let domain = Eip712Domain::new(L2ChainId::from(chain_id)); + let signature = private_account + .get_pk_signer() + .sign_typed_data(&domain, &transaction_request) + .await + .unwrap(); + let encoded_tx = transaction_request.get_signed_bytes(&signature).unwrap(); + + let (aa_txn_request, aa_hash) = + TransactionRequest::from_bytes(&encoded_tx, L2ChainId::from(chain_id)).unwrap(); + + let mut l2_tx = L2Tx::from_request(aa_txn_request, 100000).unwrap(); + l2_tx.set_input(encoded_tx, aa_hash); + + let transaction: Transaction = l2_tx.into(); + vm.vm.push_transaction(transaction); + vm.vm.execute(VmExecutionMode::OneTx); + + assert_eq!( + vm.get_eth_balance(beneficiary.address), + U256::from(916375026) + ); + assert_eq!( + private_account_balance, + vm.get_eth_balance(private_account.address) + ); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/rollbacks.rs b/core/lib/multivm/src/versions/era_vm/tests/rollbacks.rs new file mode 100644 index 000000000000..444563f0aa4e --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/rollbacks.rs @@ -0,0 +1,177 @@ +use ethabi::Token; +use zksync_contracts::{get_loadnext_contract, test_contracts::LoadnextContractExecutionParams}; +use zksync_types::{Execute, U256}; + +use crate::{ + era_vm::tests::{ + tester::{DeployContractsTx, TransactionTestInfo, TxModifier, TxType, VmTesterBuilder}, + utils::{read_test_contract, sort_execution_state}, + }, + interface::TxExecutionMode, +}; + +#[test] +fn test_vm_rollbacks() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let mut account = vm.rich_accounts[0].clone(); + let counter = read_test_contract(); + let tx_0 = account.get_deploy_tx(&counter, None, TxType::L2).tx; + let tx_1 = account.get_deploy_tx(&counter, None, TxType::L2).tx; + let tx_2 = account.get_deploy_tx(&counter, None, TxType::L2).tx; + + let mut result_without_rollbacks = vm.execute_and_verify_txs(&vec![ + TransactionTestInfo::new_processed(tx_0.clone(), false), + TransactionTestInfo::new_processed(tx_1.clone(), false), + TransactionTestInfo::new_processed(tx_2.clone(), false), + ]); + + // reset vm + vm.reset_with_empty_storage(); + + let mut result_with_rollbacks = vm.execute_and_verify_txs(&vec![ + TransactionTestInfo::new_rejected(tx_0.clone(), TxModifier::WrongSignatureLength.into()), + TransactionTestInfo::new_rejected(tx_0.clone(), TxModifier::WrongMagicValue.into()), + TransactionTestInfo::new_rejected(tx_0.clone(), TxModifier::WrongSignature.into()), + // The correct nonce is 0, this tx will fail + TransactionTestInfo::new_rejected(tx_2.clone(), TxModifier::WrongNonce.into()), + // This tx will succeed + TransactionTestInfo::new_processed(tx_0.clone(), false), + // The correct nonce is 1, this tx will fail + TransactionTestInfo::new_rejected(tx_0.clone(), TxModifier::NonceReused.into()), + // The correct nonce is 1, this tx will fail + TransactionTestInfo::new_rejected(tx_2.clone(), TxModifier::WrongNonce.into()), + // This tx will succeed + TransactionTestInfo::new_processed(tx_1, false), + // The correct nonce is 2, this tx will fail + TransactionTestInfo::new_rejected(tx_0.clone(), TxModifier::NonceReused.into()), + // This tx will succeed + TransactionTestInfo::new_processed(tx_2.clone(), false), + // This tx will fail + TransactionTestInfo::new_rejected(tx_2, TxModifier::NonceReused.into()), + TransactionTestInfo::new_rejected(tx_0, TxModifier::NonceReused.into()), + ]); + result_with_rollbacks + .deduplicated_storage_logs + .sort_by(|a, b| { + a.key + .account() + .address() + .cmp(&b.key.account().address()) + .then_with(|| a.key.key().cmp(b.key.key())) + }); + + sort_execution_state(&mut result_with_rollbacks); + sort_execution_state(&mut result_without_rollbacks); + assert_eq!(result_without_rollbacks, result_with_rollbacks); +} + +#[test] +fn test_vm_loadnext_rollbacks() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + let mut account = vm.rich_accounts[0].clone(); + + let loadnext_contract = get_loadnext_contract(); + let loadnext_constructor_data = &[Token::Uint(U256::from(100))]; + let DeployContractsTx { + tx: loadnext_deploy_tx, + address, + .. + } = account.get_deploy_tx_with_factory_deps( + &loadnext_contract.bytecode, + Some(loadnext_constructor_data), + loadnext_contract.factory_deps.clone(), + TxType::L2, + ); + + let loadnext_tx_1 = account.get_l2_tx_for_execute( + Execute { + contract_address: address, + calldata: LoadnextContractExecutionParams { + reads: 100, + writes: 100, + events: 100, + hashes: 500, + recursive_calls: 10, + deploys: 60, + } + .to_bytes(), + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + + let loadnext_tx_2 = account.get_l2_tx_for_execute( + Execute { + contract_address: address, + calldata: LoadnextContractExecutionParams { + reads: 100, + writes: 100, + events: 100, + hashes: 500, + recursive_calls: 10, + deploys: 60, + } + .to_bytes(), + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + + let mut result_without_rollbacks = vm.execute_and_verify_txs(&vec![ + TransactionTestInfo::new_processed(loadnext_deploy_tx.clone(), false), + TransactionTestInfo::new_processed(loadnext_tx_1.clone(), false), + TransactionTestInfo::new_processed(loadnext_tx_2.clone(), false), + ]); + + // reset vm + vm.reset_with_empty_storage(); + + let mut result_with_rollbacks = vm.execute_and_verify_txs(&vec![ + TransactionTestInfo::new_processed(loadnext_deploy_tx.clone(), false), + TransactionTestInfo::new_processed(loadnext_tx_1.clone(), true), + TransactionTestInfo::new_rejected( + loadnext_deploy_tx.clone(), + TxModifier::NonceReused.into(), + ), + TransactionTestInfo::new_processed(loadnext_tx_1, false), + TransactionTestInfo::new_processed(loadnext_tx_2.clone(), true), + TransactionTestInfo::new_processed(loadnext_tx_2.clone(), true), + TransactionTestInfo::new_rejected(loadnext_deploy_tx, TxModifier::NonceReused.into()), + TransactionTestInfo::new_processed(loadnext_tx_2, false), + ]); + + result_with_rollbacks + .deduplicated_storage_logs + .sort_by(|a, b| { + a.key + .account() + .address() + .cmp(&b.key.account().address()) + .then_with(|| a.key.key().cmp(b.key.key())) + }); + + result_without_rollbacks + .deduplicated_storage_logs + .sort_by(|a, b| { + a.key + .account() + .address() + .cmp(&b.key.account().address()) + .then_with(|| a.key.key().cmp(b.key.key())) + }); + + sort_execution_state(&mut result_with_rollbacks); + sort_execution_state(&mut result_without_rollbacks); + assert_eq!(result_without_rollbacks, result_with_rollbacks); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/sekp256r1.rs b/core/lib/multivm/src/versions/era_vm/tests/sekp256r1.rs new file mode 100644 index 000000000000..2aaed0f90d9c --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/sekp256r1.rs @@ -0,0 +1,75 @@ +use zk_evm_1_5_0::zkevm_opcode_defs::p256; +use zksync_system_constants::P256VERIFY_PRECOMPILE_ADDRESS; +use zksync_types::{web3::keccak256, Execute, H256, U256}; +use zksync_utils::h256_to_u256; + +use crate::{ + era_vm::tests::tester::VmTesterBuilder, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, + vm_latest::ExecutionResult, +}; + +#[test] +fn test_sekp256r1() { + // In this test, we aim to test whether a simple account interaction (without any fee logic) + // will work. The account will try to deploy a simple contract from integration tests. + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_execution_mode(TxExecutionMode::EthCall) + .with_random_rich_accounts(1) + .build(); + + let account = &mut vm.rich_accounts[0]; + + // The digest, secret key and public key were copied from the following test suit: `https://github.com/hyperledger/besu/blob/b6a6402be90339367d5bcabcd1cfd60df4832465/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/SECP256R1Test.java#L36` + let sk = p256::SecretKey::from_slice( + &hex::decode("519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464").unwrap(), + ) + .unwrap(); + let sk = p256::ecdsa::SigningKey::from(sk); + + let digest = keccak256(&hex::decode("5905238877c77421f73e43ee3da6f2d9e2ccad5fc942dcec0cbd25482935faaf416983fe165b1a045ee2bcd2e6dca3bdf46c4310a7461f9a37960ca672d3feb5473e253605fb1ddfd28065b53cb5858a8ad28175bf9bd386a5e471ea7a65c17cc934a9d791e91491eb3754d03799790fe2d308d16146d5c9b0d0debd97d79ce8").unwrap()); + let public_key_encoded = hex::decode("1ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83ce4014c68811f9a21a1fdb2c0e6113e06db7ca93b7404e78dc7ccd5ca89a4ca9").unwrap(); + + let (sig, _) = sk.sign_prehash_recoverable(&digest).unwrap(); + let (r, s) = sig.split_bytes(); + + let mut encoded_r = [0u8; 32]; + encoded_r.copy_from_slice(&r); + + let mut encoded_s = [0u8; 32]; + encoded_s.copy_from_slice(&s); + + let mut x = [0u8; 32]; + x.copy_from_slice(&public_key_encoded[0..32]); + + let mut y = [0u8; 32]; + y.copy_from_slice(&public_key_encoded[32..64]); + + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: P256VERIFY_PRECOMPILE_ADDRESS, + calldata: [digest, encoded_r, encoded_s, x, y].concat(), + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx); + + let execution_result = vm.vm.execute(VmExecutionMode::Batch); + + let ExecutionResult::Success { output } = execution_result.result else { + panic!("batch failed") + }; + + let output = H256::from_slice(&output); + + assert_eq!( + h256_to_u256(output), + U256::from(1u32), + "verification was not successful" + ); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/simple_execution.rs b/core/lib/multivm/src/versions/era_vm/tests/simple_execution.rs new file mode 100644 index 000000000000..b0e03ebe84c1 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/simple_execution.rs @@ -0,0 +1,78 @@ +use crate::{ + era_vm::tests::tester::{TxType, VmTesterBuilder}, + interface::{ExecutionResult, VmExecutionMode, VmInterface}, +}; + +#[test] +fn estimate_fee() { + let mut vm_tester = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_deployer() + .with_random_rich_accounts(1) + .build(); + + vm_tester.deploy_test_contract(); + let account = &mut vm_tester.rich_accounts[0]; + + let tx = account.get_test_contract_transaction( + vm_tester.test_contract.unwrap(), + false, + Default::default(), + false, + TxType::L2, + ); + + vm_tester.vm.push_transaction(tx); + + let result = vm_tester.vm.execute(VmExecutionMode::OneTx); + assert!(matches!(result.result, ExecutionResult::Success { .. })); +} + +#[test] +fn simple_execute() { + let mut vm_tester = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_deployer() + .with_random_rich_accounts(1) + .build(); + + vm_tester.deploy_test_contract(); + + let account = &mut vm_tester.rich_accounts[0]; + + let tx1 = account.get_test_contract_transaction( + vm_tester.test_contract.unwrap(), + false, + Default::default(), + false, + TxType::L1 { serial_id: 1 }, + ); + + let tx2 = account.get_test_contract_transaction( + vm_tester.test_contract.unwrap(), + true, + Default::default(), + false, + TxType::L1 { serial_id: 1 }, + ); + + let tx3 = account.get_test_contract_transaction( + vm_tester.test_contract.unwrap(), + false, + Default::default(), + false, + TxType::L1 { serial_id: 1 }, + ); + let vm = &mut vm_tester.vm; + vm.push_transaction(tx1); + vm.push_transaction(tx2); + vm.push_transaction(tx3); + let tx = vm.execute(VmExecutionMode::OneTx); + assert!(matches!(tx.result, ExecutionResult::Success { .. })); + let tx = vm.execute(VmExecutionMode::OneTx); + assert!(matches!(tx.result, ExecutionResult::Revert { .. })); + let tx = vm.execute(VmExecutionMode::OneTx); + assert!(matches!(tx.result, ExecutionResult::Success { .. })); + let block_tip = vm.execute(VmExecutionMode::Batch); + assert!(matches!(block_tip.result, ExecutionResult::Success { .. })); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/storage.rs b/core/lib/multivm/src/versions/era_vm/tests/storage.rs new file mode 100644 index 000000000000..7bc16054837d --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/storage.rs @@ -0,0 +1,130 @@ +use ethabi::Token; +use zksync_contracts::{load_contract, read_bytecode}; +use zksync_types::{Address, Execute, U256}; + +use crate::{ + era_vm::tests::tester::VmTesterBuilder, + interface::{TxExecutionMode, VmExecutionMode, VmInterface, VmInterfaceHistoryEnabled}, +}; + +fn test_storage(first_tx_calldata: Vec, second_tx_calldata: Vec) -> u32 { + let bytecode = read_bytecode( + "etc/contracts-test-data/artifacts-zk/contracts/storage/storage.sol/StorageTester.json", + ); + + let test_contract_address = Address::random(); + + // In this test, we aim to test whether a simple account interaction (without any fee logic) + // will work. The account will try to deploy a simple contract from integration tests. + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_deployer() + .with_random_rich_accounts(1) + .with_custom_contracts(vec![(bytecode, test_contract_address, false)]) + .build(); + + let account = &mut vm.rich_accounts[0]; + + let tx1 = account.get_l2_tx_for_execute( + Execute { + contract_address: test_contract_address, + calldata: first_tx_calldata, + value: 0.into(), + factory_deps: vec![], + }, + None, + ); + + let tx2 = account.get_l2_tx_for_execute( + Execute { + contract_address: test_contract_address, + calldata: second_tx_calldata, + value: 0.into(), + factory_deps: vec![], + }, + None, + ); + + vm.vm.make_snapshot(); + vm.vm.push_transaction(tx1); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed(), "First tx failed"); + vm.vm.pop_snapshot_no_rollback(); + + // We rollback once because transient storage and rollbacks are a tricky combination. + vm.vm.make_snapshot(); + vm.vm.push_transaction(tx2.clone()); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed(), "Second tx failed"); + vm.vm.rollback_to_the_latest_snapshot(); + + vm.vm.make_snapshot(); + vm.vm.push_transaction(tx2); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed(), "Second tx failed on second run"); + + result.statistics.pubdata_published +} + +fn test_storage_one_tx(second_tx_calldata: Vec) -> u32 { + test_storage(vec![], second_tx_calldata) +} + +#[test] +fn test_storage_behavior() { + let contract = load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/storage/storage.sol/StorageTester.json", + ); + + // In all of the tests below we provide the first tx to ensure that the tracers will not include + // the statistics from the start of the bootloader and will only include those for the transaction itself. + + let base_pubdata = test_storage_one_tx(vec![]); + let simple_test_pubdata = test_storage_one_tx( + contract + .function("simpleWrite") + .unwrap() + .encode_input(&[]) + .unwrap(), + ); + let resetting_write_pubdata = test_storage_one_tx( + contract + .function("resettingWrite") + .unwrap() + .encode_input(&[]) + .unwrap(), + ); + let resetting_write_via_revert_pubdata = test_storage_one_tx( + contract + .function("resettingWriteViaRevert") + .unwrap() + .encode_input(&[]) + .unwrap(), + ); + + assert_eq!(simple_test_pubdata - base_pubdata, 65); + assert_eq!(resetting_write_pubdata - base_pubdata, 34); + assert_eq!(resetting_write_via_revert_pubdata - base_pubdata, 34); +} + +#[test] +fn test_transient_storage_behavior() { + let contract = load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/storage/storage.sol/StorageTester.json", + ); + + let first_tstore_test = contract + .function("testTransientStore") + .unwrap() + .encode_input(&[]) + .unwrap(); + // Second transaction checks that, as expected, the transient storage is cleared after the first transaction. + let second_tstore_test = contract + .function("assertTValue") + .unwrap() + .encode_input(&[Token::Uint(U256::zero())]) + .unwrap(); + + test_storage(first_tstore_test, second_tstore_test); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/tester/mod.rs b/core/lib/multivm/src/versions/era_vm/tests/tester/mod.rs new file mode 100644 index 000000000000..781069ddf499 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/tester/mod.rs @@ -0,0 +1,6 @@ +pub(crate) use transaction_test_info::{ExpectedError, TransactionTestInfo, TxModifier}; +pub(crate) use vm_tester::{default_l1_batch, get_empty_storage, VmTester, VmTesterBuilder}; +pub(crate) use zksync_test_account::{Account, DeployContractsTx, TxType}; + +mod transaction_test_info; +mod vm_tester; diff --git a/core/lib/multivm/src/versions/era_vm/tests/tester/transaction_test_info.rs b/core/lib/multivm/src/versions/era_vm/tests/tester/transaction_test_info.rs new file mode 100644 index 000000000000..31f5e4b83b8d --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/tester/transaction_test_info.rs @@ -0,0 +1,262 @@ +use std::collections::{HashMap, HashSet}; + +use era_vm::{ + state::{Event, L2ToL1Log}, + store::StorageKey, +}; +use zksync_state::ReadStorage; +use zksync_types::{ExecuteTransactionCommon, Transaction, H160, U256}; + +use super::VmTester; +use crate::{ + era_vm::vm::Vm, + interface::{ + CurrentExecutionState, ExecutionResult, Halt, TxRevertReason, VmExecutionMode, + VmExecutionResultAndLogs, VmInterface, VmInterfaceHistoryEnabled, VmRevertReason, + }, +}; + +#[derive(Debug, Clone)] +pub(crate) enum TxModifier { + WrongSignatureLength, + WrongSignature, + WrongMagicValue, + WrongNonce, + NonceReused, +} + +#[derive(Debug, Clone)] +pub(crate) enum TxExpectedResult { + Rejected { error: ExpectedError }, + Processed { rollback: bool }, +} + +#[derive(Debug, Clone)] +pub(crate) struct TransactionTestInfo { + tx: Transaction, + result: TxExpectedResult, +} + +#[derive(Debug, Clone)] +pub(crate) struct ExpectedError { + pub(crate) revert_reason: TxRevertReason, + pub(crate) modifier: Option, +} + +impl From for ExpectedError { + fn from(value: TxModifier) -> Self { + let revert_reason = match value { + TxModifier::WrongSignatureLength => { + Halt::ValidationFailed(VmRevertReason::General { + msg: "Signature length is incorrect".to_string(), + data: vec![ + 8, 195, 121, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 83, 105, 103, 110, 97, 116, 117, 114, 101, 32, + 108, 101, 110, 103, 116, 104, 32, 105, 115, 32, 105, 110, 99, 111, 114, 114, 101, 99, + 116, 0, 0, 0, + ], + }) + } + TxModifier::WrongSignature => { + Halt::ValidationFailed(VmRevertReason::General { + msg: "Account validation returned invalid magic value. Most often this means that the signature is incorrect".to_string(), + data: vec![], + }) + } + TxModifier::WrongMagicValue => { + Halt::ValidationFailed(VmRevertReason::General { + msg: "v is neither 27 nor 28".to_string(), + data: vec![ + 8, 195, 121, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 118, 32, 105, 115, 32, 110, 101, 105, 116, 104, + 101, 114, 32, 50, 55, 32, 110, 111, 114, 32, 50, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + }) + + } + TxModifier::WrongNonce => { + Halt::ValidationFailed(VmRevertReason::General { + msg: "Incorrect nonce".to_string(), + data: vec![ + 8, 195, 121, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 73, 110, 99, 111, 114, 114, 101, 99, 116, 32, 110, + 111, 110, 99, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + }) + } + TxModifier::NonceReused => { + Halt::ValidationFailed(VmRevertReason::General { + msg: "Reusing the same nonce twice".to_string(), + data: vec![ + 8, 195, 121, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 82, 101, 117, 115, 105, 110, 103, 32, 116, 104, + 101, 32, 115, 97, 109, 101, 32, 110, 111, 110, 99, 101, 32, 116, 119, 105, 99, 101, 0, + 0, 0, 0, + ], + }) + } + }; + + ExpectedError { + revert_reason: TxRevertReason::Halt(revert_reason), + modifier: Some(value), + } + } +} + +impl TransactionTestInfo { + pub(crate) fn new_rejected( + mut transaction: Transaction, + expected_error: ExpectedError, + ) -> Self { + transaction.common_data = match transaction.common_data { + ExecuteTransactionCommon::L2(mut data) => { + if let Some(modifier) = &expected_error.modifier { + match modifier { + TxModifier::WrongSignatureLength => { + data.signature = data.signature[..data.signature.len() - 20].to_vec() + } + TxModifier::WrongSignature => data.signature = vec![27u8; 65], + TxModifier::WrongMagicValue => data.signature = vec![1u8; 65], + TxModifier::WrongNonce => { + // Do not need to modify signature for nonce error + } + TxModifier::NonceReused => { + // Do not need to modify signature for nonce error + } + } + } + ExecuteTransactionCommon::L2(data) + } + _ => panic!("L1 transactions are not supported"), + }; + + Self { + tx: transaction, + result: TxExpectedResult::Rejected { + error: expected_error, + }, + } + } + + pub(crate) fn new_processed(transaction: Transaction, should_be_rollbacked: bool) -> Self { + Self { + tx: transaction, + result: TxExpectedResult::Processed { + rollback: should_be_rollbacked, + }, + } + } + + fn verify_result(&self, result: &VmExecutionResultAndLogs) { + match &self.result { + TxExpectedResult::Rejected { error } => match &result.result { + ExecutionResult::Success { .. } => { + panic!("Transaction should be reverted {:?}", self.tx.nonce()) + } + ExecutionResult::Revert { output } => match &error.revert_reason { + TxRevertReason::TxReverted(expected) => { + assert_eq!(output, expected) + } + _ => { + panic!("Error types mismatch"); + } + }, + ExecutionResult::Halt { reason } => match &error.revert_reason { + TxRevertReason::Halt(expected) => { + assert_eq!(reason, expected) + } + _ => { + panic!("Error types mismatch"); + } + }, + }, + TxExpectedResult::Processed { .. } => { + assert!(!result.result.is_failed()); + } + } + } + + fn should_rollback(&self) -> bool { + match &self.result { + TxExpectedResult::Rejected { .. } => true, + TxExpectedResult::Processed { rollback } => *rollback, + } + } +} + +#[derive(Debug, PartialEq)] +struct VmStateDump { + storage_changes: HashMap, + transient_storage: HashMap, + l2_to_l1_logs: Vec, + events: Vec, + pubdata: i32, + pubdata_costs: Vec, + refunds: Vec, + decommitted_hashes: HashSet, +} +#[derive(Debug, PartialEq)] +struct VmDump { + execution: era_vm::execution::Execution, + state: VmStateDump, +} + +impl Vm { + fn dump_state(&self) -> VmDump { + VmDump { + execution: self.inner.execution.clone(), + state: VmStateDump { + storage_changes: self.inner.state.storage_changes().clone(), + transient_storage: self.inner.state.transient_storage().clone(), + l2_to_l1_logs: self.inner.state.l2_to_l1_logs().to_vec(), + events: self.inner.state.events().to_vec(), + pubdata: self.inner.state.pubdata().clone(), + pubdata_costs: self.inner.state.pubdata_costs().to_vec(), + refunds: self.inner.state.refunds().to_vec(), + decommitted_hashes: self.inner.state.decommitted_hashes().clone(), + }, + } + } +} + +impl VmTester { + pub(crate) fn execute_and_verify_txs( + &mut self, + txs: &[TransactionTestInfo], + ) -> CurrentExecutionState { + for tx_test_info in txs { + self.execute_tx_and_verify(tx_test_info.clone()); + } + self.vm.execute(VmExecutionMode::Batch); + let mut state = self.vm.get_current_execution_state(); + state.used_contract_hashes.sort(); + state + } + + pub(crate) fn execute_tx_and_verify( + &mut self, + tx_test_info: TransactionTestInfo, + ) -> VmExecutionResultAndLogs { + self.vm.make_snapshot(); + let inner_state_before = self.vm.dump_state(); + self.vm.push_transaction(tx_test_info.tx.clone()); + let result = self.vm.execute(VmExecutionMode::OneTx); + tx_test_info.verify_result(&result); + if tx_test_info.should_rollback() { + self.vm.rollback_to_the_latest_snapshot(); + let inner_state_after = self.vm.dump_state(); + assert_eq!( + inner_state_before, inner_state_after, + "Inner state before and after rollback should be equal" + ); + } else { + self.vm.pop_snapshot_no_rollback(); + } + result + } +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/tester/vm_tester.rs b/core/lib/multivm/src/versions/era_vm/tests/tester/vm_tester.rs new file mode 100644 index 000000000000..21387c5a36fe --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/tester/vm_tester.rs @@ -0,0 +1,312 @@ +use std::{cell::RefCell, rc::Rc}; + +use zksync_contracts::BaseSystemContracts; +use zksync_state::{InMemoryStorage, StoragePtr}; +use zksync_test_account::{Account, TxType}; +use zksync_types::{ + block::L2BlockHasher, + fee_model::BatchFeeInput, + get_code_key, get_is_account_key, + helpers::unix_timestamp_ms, + utils::{deployed_address_create, storage_key_for_eth_balance}, + AccountTreeId, Address, L1BatchNumber, L2BlockNumber, L2ChainId, Nonce, ProtocolVersionId, + StorageKey, U256, +}; +use zksync_utils::{bytecode::hash_bytecode, u256_to_h256}; + +use crate::{ + era_vm::{ + tests::utils::read_test_contract, + vm::{Vm, World}, + }, + interface::{ + L1BatchEnv, L2Block, L2BlockEnv, SystemEnv, TxExecutionMode, VmExecutionMode, VmFactory, + VmInterface, + }, + vm_latest::{constants::BATCH_COMPUTATIONAL_GAS_LIMIT, utils::l2_blocks::load_last_l2_block}, +}; + +pub(crate) struct VmTester { + pub(crate) vm: Vm, + pub(crate) storage: StoragePtr, + pub(crate) deployer: Option, + pub(crate) test_contract: Option
, + pub(crate) fee_account: Address, + pub(crate) rich_accounts: Vec, + pub(crate) custom_contracts: Vec, +} + +impl VmTester { + pub(crate) fn deploy_test_contract(&mut self) { + let contract = read_test_contract(); + let tx = self + .deployer + .as_mut() + .expect("You have to initialize builder with deployer") + .get_deploy_tx(&contract, None, TxType::L2) + .tx; + let nonce = tx.nonce().unwrap().0.into(); + self.vm.push_transaction(tx); + self.vm.execute(VmExecutionMode::OneTx); + let deployed_address = + deployed_address_create(self.deployer.as_ref().unwrap().address, nonce); + self.test_contract = Some(deployed_address); + } + pub(crate) fn reset_with_empty_storage(&mut self) { + self.storage = Rc::new(RefCell::new(get_empty_storage())); + // let world_storage = Rc::new(RefCell::new(World::new( + // self.storage.clone(), + // self.vm.program_cache.clone(), + // ))); + // self.vm.inner.state.storage = world_storage; + self.vm.inner.state.reset(); + self.reset_state(false); + } + + /// Reset the state of the VM to the initial state. + /// If `use_latest_l2_block` is true, then the VM will use the latest L2 block from storage, + /// otherwise it will use the first L2 block of l1 batch env + pub(crate) fn reset_state(&mut self, use_latest_l2_block: bool) { + for account in self.rich_accounts.iter_mut() { + account.nonce = Nonce(0); + make_account_rich(self.storage.clone(), account); + } + if let Some(deployer) = &self.deployer { + make_account_rich(self.storage.clone(), deployer); + } + + if !self.custom_contracts.is_empty() { + println!("Inserting custom contracts is not yet supported") + // insert_contracts(&mut self.storage, &self.custom_contracts); + } + + let storage = self.storage.clone(); + { + // Commit pending storage changes (old VM versions commit them on successful execution) + let mut storage = storage.borrow_mut(); + for (key, value) in self.vm.inner.state.storage_changes() { + let key = StorageKey::new(AccountTreeId::new(key.address), u256_to_h256(key.key)); + storage.set_value(key, u256_to_h256(*value)); + } + } + + let mut l1_batch = self.vm.batch_env.clone(); + if use_latest_l2_block { + let last_l2_block = load_last_l2_block(&storage).unwrap_or(L2Block { + number: 0, + timestamp: 0, + hash: L2BlockHasher::legacy_hash(L2BlockNumber(0)), + }); + l1_batch.first_l2_block = L2BlockEnv { + number: last_l2_block.number + 1, + timestamp: std::cmp::max(last_l2_block.timestamp + 1, l1_batch.timestamp), + prev_block_hash: last_l2_block.hash, + max_virtual_blocks_to_create: 1, + }; + } + + let vm = Vm::new(l1_batch, self.vm.system_env.clone(), storage); + + if self.test_contract.is_some() { + self.deploy_test_contract(); + } + self.vm = vm; + } +} + +pub(crate) type ContractsToDeploy = (Vec, Address, bool); + +pub(crate) struct VmTesterBuilder { + storage: Option, + l1_batch_env: Option, + system_env: SystemEnv, + deployer: Option, + rich_accounts: Vec, + custom_contracts: Vec, +} + +impl Clone for VmTesterBuilder { + fn clone(&self) -> Self { + Self { + storage: None, + l1_batch_env: self.l1_batch_env.clone(), + system_env: self.system_env.clone(), + deployer: self.deployer.clone(), + rich_accounts: self.rich_accounts.clone(), + custom_contracts: self.custom_contracts.clone(), + } + } +} + +#[allow(dead_code)] +impl VmTesterBuilder { + pub(crate) fn new() -> Self { + Self { + storage: None, + l1_batch_env: None, + system_env: SystemEnv { + zk_porter_available: false, + version: ProtocolVersionId::latest(), + base_system_smart_contracts: BaseSystemContracts::playground(), + bootloader_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, + execution_mode: TxExecutionMode::VerifyExecute, + default_validation_computational_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, + chain_id: L2ChainId::from(270), + }, + deployer: None, + rich_accounts: vec![], + custom_contracts: vec![], + } + } + + pub(crate) fn with_l1_batch_env(mut self, l1_batch_env: L1BatchEnv) -> Self { + self.l1_batch_env = Some(l1_batch_env); + self + } + + pub(crate) fn with_system_env(mut self, system_env: SystemEnv) -> Self { + self.system_env = system_env; + self + } + + pub(crate) fn with_storage(mut self, storage: InMemoryStorage) -> Self { + self.storage = Some(storage); + self + } + + pub(crate) fn with_base_system_smart_contracts( + mut self, + base_system_smart_contracts: BaseSystemContracts, + ) -> Self { + self.system_env.base_system_smart_contracts = base_system_smart_contracts; + self + } + + pub(crate) fn with_bootloader_gas_limit(mut self, gas_limit: u32) -> Self { + self.system_env.bootloader_gas_limit = gas_limit; + self + } + + pub(crate) fn with_execution_mode(mut self, execution_mode: TxExecutionMode) -> Self { + self.system_env.execution_mode = execution_mode; + self + } + + pub(crate) fn with_empty_in_memory_storage(mut self) -> Self { + self.storage = None; + self + } + + pub(crate) fn with_random_rich_accounts(mut self, number: u32) -> Self { + for _ in 0..number { + let account = Account::random(); + self.rich_accounts.push(account); + } + self + } + + pub(crate) fn with_rich_accounts(mut self, accounts: Vec) -> Self { + self.rich_accounts.extend(accounts); + self + } + + pub(crate) fn with_deployer(mut self) -> Self { + let deployer = Account::random(); + self.deployer = Some(deployer); + self + } + + pub(crate) fn with_custom_contracts(mut self, contracts: Vec) -> Self { + self.custom_contracts = contracts; + self + } + + pub(crate) fn build(self) -> VmTester { + let l1_batch_env = self + .l1_batch_env + .unwrap_or_else(|| default_l1_batch(L1BatchNumber(1))); + + let raw_storage = self.storage.unwrap_or_else(get_empty_storage); + let mut storage_ptr = Rc::new(RefCell::new(raw_storage)); + insert_contracts(&mut storage_ptr, &self.custom_contracts); + for account in self.rich_accounts.iter() { + make_account_rich(storage_ptr.clone(), account); + } + if let Some(deployer) = &self.deployer { + make_account_rich(storage_ptr.clone(), deployer); + } + + let fee_account = l1_batch_env.fee_account; + let vm = Vm::new(l1_batch_env, self.system_env, storage_ptr.clone()); + + VmTester { + vm, + storage: storage_ptr, + deployer: self.deployer, + test_contract: None, + fee_account, + rich_accounts: self.rich_accounts.clone(), + custom_contracts: self.custom_contracts.clone(), + } + } +} + +pub(crate) fn default_l1_batch(number: L1BatchNumber) -> L1BatchEnv { + let timestamp = unix_timestamp_ms(); + L1BatchEnv { + previous_batch_hash: None, + number, + timestamp, + fee_input: BatchFeeInput::l1_pegged( + 50_000_000_000, // 50 gwei + 250_000_000, // 0.25 gwei + ), + fee_account: Address::random(), + enforced_base_fee: None, + first_l2_block: L2BlockEnv { + number: 1, + timestamp, + prev_block_hash: L2BlockHasher::legacy_hash(L2BlockNumber(0)), + max_virtual_blocks_to_create: 100, + }, + } +} + +pub(crate) fn make_account_rich(storage: StoragePtr, account: &Account) { + let key = storage_key_for_eth_balance(&account.address); + storage + .as_ref() + .borrow_mut() + .set_value(key, u256_to_h256(U256::from(10u64.pow(19)))); +} + +pub(crate) fn get_empty_storage() -> InMemoryStorage { + InMemoryStorage::with_system_contracts(hash_bytecode) +} + +// Inserts the contracts into the test environment, bypassing the +// deployer system contract. Besides the reference to storage +// it accepts a `contracts` tuple of information about the contract +// and whether or not it is an account. +fn insert_contracts( + raw_storage: &mut StoragePtr, + contracts: &[ContractsToDeploy], +) { + for (contract, address, is_account) in contracts { + let deployer_code_key = get_code_key(address); + raw_storage + .borrow_mut() + .set_value(deployer_code_key, hash_bytecode(contract)); + + if *is_account { + let is_account_key = get_is_account_key(address); + raw_storage + .borrow_mut() + .set_value(is_account_key, u256_to_h256(1_u32.into())); + } + + raw_storage + .borrow_mut() + .store_factory_dep(hash_bytecode(contract), contract.clone()); + } +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/tracing_execution_error.rs b/core/lib/multivm/src/versions/era_vm/tests/tracing_execution_error.rs new file mode 100644 index 000000000000..e35f1af8fd02 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/tracing_execution_error.rs @@ -0,0 +1,51 @@ +use zksync_types::{Execute, H160}; + +use crate::{ + era_vm::tests::{ + tester::{ExpectedError, TransactionTestInfo, VmTesterBuilder}, + utils::{get_execute_error_calldata, read_error_contract, BASE_SYSTEM_CONTRACTS}, + }, + interface::{TxExecutionMode, TxRevertReason, VmRevertReason}, +}; + +#[test] +fn test_tracing_of_execution_errors() { + let contract_address = H160::random(); + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_base_system_smart_contracts(BASE_SYSTEM_CONTRACTS.clone()) + .with_custom_contracts(vec![(read_error_contract(), contract_address, false)]) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_deployer() + .with_random_rich_accounts(1) + .build(); + + let account = &mut vm.rich_accounts[0]; + + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address, + calldata: get_execute_error_calldata(), + value: Default::default(), + factory_deps: vec![], + }, + None, + ); + + vm.execute_tx_and_verify(TransactionTestInfo::new_rejected( + tx, + ExpectedError { + revert_reason: TxRevertReason::TxReverted(VmRevertReason::General { + msg: "short".to_string(), + data: vec![ + 8, 195, 121, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 115, 104, 111, 114, 116, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, + ], + }), + modifier: None, + }, + )); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/transfer.rs b/core/lib/multivm/src/versions/era_vm/tests/transfer.rs new file mode 100644 index 000000000000..75cf383d7c35 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/transfer.rs @@ -0,0 +1,218 @@ +use ethabi::Token; +use zksync_contracts::{load_contract, read_bytecode}; +use zksync_system_constants::L2_BASE_TOKEN_ADDRESS; +use zksync_types::{utils::storage_key_for_eth_balance, AccountTreeId, Address, Execute, U256}; +use zksync_utils::u256_to_h256; + +use crate::{ + era_vm::tests::{ + tester::{get_empty_storage, VmTesterBuilder}, + utils::get_balance, + }, + interface::{TxExecutionMode, VmExecutionMode, VmInterface}, +}; + +enum TestOptions { + Send(U256), + Transfer(U256), +} + +fn test_send_or_transfer(test_option: TestOptions) { + let test_bytecode = read_bytecode( + "etc/contracts-test-data/artifacts-zk/contracts/transfer/transfer.sol/TransferTest.json", + ); + let recipeint_bytecode = read_bytecode( + "etc/contracts-test-data/artifacts-zk/contracts/transfer/transfer.sol/Recipient.json", + ); + let test_abi = load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/transfer/transfer.sol/TransferTest.json", + ); + + let test_contract_address = Address::random(); + let recipient_address = Address::random(); + + let (value, calldata) = match test_option { + TestOptions::Send(value) => ( + value, + test_abi + .function("send") + .unwrap() + .encode_input(&[Token::Address(recipient_address), Token::Uint(value)]) + .unwrap(), + ), + TestOptions::Transfer(value) => ( + value, + test_abi + .function("transfer") + .unwrap() + .encode_input(&[Token::Address(recipient_address), Token::Uint(value)]) + .unwrap(), + ), + }; + + let mut storage = get_empty_storage(); + storage.set_value( + storage_key_for_eth_balance(&test_contract_address), + u256_to_h256(value), + ); + + let mut vm = VmTesterBuilder::new() + .with_storage(storage) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_deployer() + .with_random_rich_accounts(1) + .with_custom_contracts(vec![ + (test_bytecode, test_contract_address, false), + (recipeint_bytecode, recipient_address, false), + ]) + .build(); + + let account = &mut vm.rich_accounts[0]; + let tx = account.get_l2_tx_for_execute( + Execute { + contract_address: test_contract_address, + calldata, + value: U256::zero(), + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx); + let tx_result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !tx_result.result.is_failed(), + "Transaction wasn't successful" + ); + + let batch_result = vm.vm.execute(VmExecutionMode::Batch); + assert!(!batch_result.result.is_failed(), "Batch wasn't successful"); + + let new_recipient_balance = get_balance( + AccountTreeId::new(L2_BASE_TOKEN_ADDRESS), + &recipient_address, + &mut vm.vm.storage, + vm.vm.inner.state.storage_changes(), + ); + + assert_eq!(new_recipient_balance, value); +} + +#[test] +fn test_send_and_transfer() { + test_send_or_transfer(TestOptions::Send(U256::zero())); + test_send_or_transfer(TestOptions::Send(U256::from(10).pow(18.into()))); + test_send_or_transfer(TestOptions::Transfer(U256::zero())); + test_send_or_transfer(TestOptions::Transfer(U256::from(10).pow(18.into()))); +} + +fn test_reentrancy_protection_send_or_transfer(test_option: TestOptions) { + let test_bytecode = read_bytecode( + "etc/contracts-test-data/artifacts-zk/contracts/transfer/transfer.sol/TransferTest.json", + ); + let reentrant_recipeint_bytecode = read_bytecode( + "etc/contracts-test-data/artifacts-zk/contracts/transfer/transfer.sol/ReentrantRecipient.json", + ); + let test_abi = load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/transfer/transfer.sol/TransferTest.json", + ); + let reentrant_recipient_abi = load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/transfer/transfer.sol/ReentrantRecipient.json", + ); + + let test_contract_address = Address::random(); + let reentrant_recipeint_address = Address::random(); + + let (value, calldata) = match test_option { + TestOptions::Send(value) => ( + value, + test_abi + .function("send") + .unwrap() + .encode_input(&[ + Token::Address(reentrant_recipeint_address), + Token::Uint(value), + ]) + .unwrap(), + ), + TestOptions::Transfer(value) => ( + value, + test_abi + .function("transfer") + .unwrap() + .encode_input(&[ + Token::Address(reentrant_recipeint_address), + Token::Uint(value), + ]) + .unwrap(), + ), + }; + + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_deployer() + .with_random_rich_accounts(1) + .with_custom_contracts(vec![ + (test_bytecode, test_contract_address, false), + ( + reentrant_recipeint_bytecode, + reentrant_recipeint_address, + false, + ), + ]) + .build(); + + // First transaction, the job of which is to warm up the slots for balance of the recipient as well as its storage variable. + let account = &mut vm.rich_accounts[0]; + let tx1 = account.get_l2_tx_for_execute( + Execute { + contract_address: reentrant_recipeint_address, + calldata: reentrant_recipient_abi + .function("setX") + .unwrap() + .encode_input(&[]) + .unwrap(), + value: U256::from(1), + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx1); + let tx1_result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !tx1_result.result.is_failed(), + "Transaction 1 wasn't successful" + ); + + let tx2 = account.get_l2_tx_for_execute( + Execute { + contract_address: test_contract_address, + calldata, + value, + factory_deps: vec![], + }, + None, + ); + + vm.vm.push_transaction(tx2); + let tx2_result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + tx2_result.result.is_failed(), + "Transaction 2 should have failed, but it succeeded" + ); + + let batch_result = vm.vm.execute(VmExecutionMode::Batch); + assert!(!batch_result.result.is_failed(), "Batch wasn't successful"); +} + +#[test] +fn test_reentrancy_protection_send_and_transfer() { + test_reentrancy_protection_send_or_transfer(TestOptions::Send(U256::zero())); + test_reentrancy_protection_send_or_transfer(TestOptions::Send(U256::from(10).pow(18.into()))); + test_reentrancy_protection_send_or_transfer(TestOptions::Transfer(U256::zero())); + test_reentrancy_protection_send_or_transfer(TestOptions::Transfer( + U256::from(10).pow(18.into()), + )); +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/upgrade.rs b/core/lib/multivm/src/versions/era_vm/tests/upgrade.rs new file mode 100644 index 000000000000..e0e82bcde048 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/upgrade.rs @@ -0,0 +1,343 @@ +use zksync_contracts::{deployer_contract, load_sys_contract, read_bytecode}; +use zksync_test_account::TxType; +use zksync_types::{ + ethabi::{Contract, Token}, + get_code_key, get_known_code_key, + protocol_upgrade::ProtocolUpgradeTxCommonData, + Address, Execute, ExecuteTransactionCommon, Transaction, COMPLEX_UPGRADER_ADDRESS, + CONTRACT_DEPLOYER_ADDRESS, CONTRACT_FORCE_DEPLOYER_ADDRESS, H160, H256, + REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE, U256, +}; +use zksync_utils::{bytecode::hash_bytecode, u256_to_h256}; + +use crate::{ + era_vm::tests::{ + tester::VmTesterBuilder, + utils::{ + get_complex_upgrade_abi, read_complex_upgrade, read_test_contract, + verify_required_storage, + }, + }, + interface::{ + ExecutionResult, Halt, TxExecutionMode, VmExecutionMode, VmInterface, + VmInterfaceHistoryEnabled, + }, +}; + +/// In this test we ensure that the requirements for protocol upgrade transactions are enforced by the bootloader: +/// - This transaction must be the only one in block +/// - If present, this transaction must be the first one in block +#[test] +fn test_protocol_upgrade_is_first() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let bytecode_hash = hash_bytecode(&read_test_contract()); + vm.storage + .borrow_mut() + .set_value(get_known_code_key(&bytecode_hash), u256_to_h256(1.into())); + + // Here we just use some random transaction of protocol upgrade type: + let protocol_upgrade_transaction = get_forced_deploy_tx(&[ForceDeployment { + // The bytecode hash to put on an address + bytecode_hash, + // The address on which to deploy the bytecode hash to + address: H160::random(), + // Whether to run the constructor on the force deployment + call_constructor: false, + // The value with which to initialize a contract + value: U256::zero(), + // The constructor calldata + input: vec![], + }]); + + // Another random upgrade transaction + let another_protocol_upgrade_transaction = get_forced_deploy_tx(&[ForceDeployment { + // The bytecode hash to put on an address + bytecode_hash, + // The address on which to deploy the bytecode hash to + address: H160::random(), + // Whether to run the constructor on the force deployment + call_constructor: false, + // The value with which to initialize a contract + value: U256::zero(), + // The constructor calldata + input: vec![], + }]); + + let normal_l1_transaction = vm.rich_accounts[0] + .get_deploy_tx(&read_test_contract(), None, TxType::L1 { serial_id: 0 }) + .tx; + + let expected_error = + Halt::UnexpectedVMBehavior("Assertion error: Protocol upgrade tx not first".to_string()); + + vm.vm.make_snapshot(); + // Test 1: there must be only one system transaction in block + vm.vm.push_transaction(protocol_upgrade_transaction.clone()); + vm.vm.push_transaction(normal_l1_transaction.clone()); + vm.vm.push_transaction(another_protocol_upgrade_transaction); + + vm.vm.execute(VmExecutionMode::OneTx); + vm.vm.execute(VmExecutionMode::OneTx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert_eq!( + result.result, + ExecutionResult::Halt { + reason: expected_error.clone() + } + ); + + // Test 2: the protocol upgrade tx must be the first one in block + vm.vm.rollback_to_the_latest_snapshot(); + vm.vm.make_snapshot(); + vm.vm.push_transaction(normal_l1_transaction.clone()); + vm.vm.push_transaction(protocol_upgrade_transaction.clone()); + + vm.vm.execute(VmExecutionMode::OneTx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert_eq!( + result.result, + ExecutionResult::Halt { + reason: expected_error + } + ); + + vm.vm.rollback_to_the_latest_snapshot(); + vm.vm.make_snapshot(); + vm.vm.push_transaction(protocol_upgrade_transaction); + vm.vm.push_transaction(normal_l1_transaction); + + vm.vm.execute(VmExecutionMode::OneTx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed()); +} + +/// In this test we try to test how force deployments could be done via protocol upgrade transactions. +#[test] +fn test_force_deploy_upgrade() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let storage_view = vm.storage.clone(); + let bytecode_hash = hash_bytecode(&read_test_contract()); + + let known_code_key = get_known_code_key(&bytecode_hash); + // It is generally expected that all the keys will be set as known prior to the protocol upgrade. + storage_view + .borrow_mut() + .set_value(known_code_key, u256_to_h256(1.into())); + drop(storage_view); + + let address_to_deploy = H160::random(); + // Here we just use some random transaction of protocol upgrade type: + let transaction = get_forced_deploy_tx(&[ForceDeployment { + // The bytecode hash to put on an address + bytecode_hash, + // The address on which to deploy the bytecode hash to + address: address_to_deploy, + // Whether to run the constructor on the force deployment + call_constructor: false, + // The value with which to initialize a contract + value: U256::zero(), + // The constructor calldata + input: vec![], + }]); + + vm.vm.push_transaction(transaction); + + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "The force upgrade was not successful" + ); + + let expected_slots = [(bytecode_hash, get_code_key(&address_to_deploy))]; + + // Verify that the bytecode has been set correctly + verify_required_storage( + &expected_slots, + &mut *vm.storage.borrow_mut(), + vm.vm.inner.state.storage_changes(), + ); +} + +/// Here we show how the work with the complex upgrader could be done +#[test] +fn test_complex_upgrader() { + let mut vm = VmTesterBuilder::new() + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let bytecode_hash = hash_bytecode(&read_complex_upgrade()); + let msg_sender_test_hash = hash_bytecode(&read_msg_sender_test()); + + // Let's assume that the bytecode for the implementation of the complex upgrade + // is already deployed in some address in user space + let upgrade_impl = H160::random(); + let account_code_key = get_code_key(&upgrade_impl); + + { + let mut storage = vm.storage.borrow_mut(); + storage.set_value(get_known_code_key(&bytecode_hash), u256_to_h256(1.into())); + storage.set_value( + get_known_code_key(&msg_sender_test_hash), + u256_to_h256(1.into()), + ); + storage.set_value(account_code_key, bytecode_hash); + storage.store_factory_dep(bytecode_hash, read_complex_upgrade()); + storage.store_factory_dep(msg_sender_test_hash, read_msg_sender_test()); + } + + let address_to_deploy1 = H160::random(); + let address_to_deploy2 = H160::random(); + + let transaction = get_complex_upgrade_tx( + upgrade_impl, + address_to_deploy1, + address_to_deploy2, + bytecode_hash, + ); + + vm.vm.push_transaction(transaction); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!( + !result.result.is_failed(), + "The force upgrade was not successful" + ); + + let expected_slots = [ + (bytecode_hash, get_code_key(&address_to_deploy1)), + (bytecode_hash, get_code_key(&address_to_deploy2)), + ]; + + // Verify that the bytecode has been set correctly + verify_required_storage( + &expected_slots, + &mut *vm.storage.borrow_mut(), + vm.vm.inner.state.storage_changes(), + ); +} + +#[derive(Debug, Clone)] +struct ForceDeployment { + // The bytecode hash to put on an address + bytecode_hash: H256, + // The address on which to deploy the bytecode hash to + address: Address, + // Whether to run the constructor on the force deployment + call_constructor: bool, + // The value with which to initialize a contract + value: U256, + // The constructor calldata + input: Vec, +} + +fn get_forced_deploy_tx(deployment: &[ForceDeployment]) -> Transaction { + let deployer = deployer_contract(); + let contract_function = deployer.function("forceDeployOnAddresses").unwrap(); + + let encoded_deployments: Vec<_> = deployment + .iter() + .map(|deployment| { + Token::Tuple(vec![ + Token::FixedBytes(deployment.bytecode_hash.as_bytes().to_vec()), + Token::Address(deployment.address), + Token::Bool(deployment.call_constructor), + Token::Uint(deployment.value), + Token::Bytes(deployment.input.clone()), + ]) + }) + .collect(); + + let params = [Token::Array(encoded_deployments)]; + + let calldata = contract_function + .encode_input(¶ms) + .expect("failed to encode parameters"); + + let execute = Execute { + contract_address: CONTRACT_DEPLOYER_ADDRESS, + calldata, + factory_deps: vec![], + value: U256::zero(), + }; + + Transaction { + common_data: ExecuteTransactionCommon::ProtocolUpgrade(ProtocolUpgradeTxCommonData { + sender: CONTRACT_FORCE_DEPLOYER_ADDRESS, + gas_limit: U256::from(200_000_000u32), + gas_per_pubdata_limit: REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE.into(), + ..Default::default() + }), + execute, + received_timestamp_ms: 0, + raw_bytes: None, + } +} + +// Returns the transaction that performs a complex protocol upgrade. +// The first param is the address of the implementation of the complex upgrade +// in user-space, while the next 3 params are params of the implementation itself +// For the explanation for the parameters, please refer to: +// etc/contracts-test-data/complex-upgrade/complex-upgrade.sol +fn get_complex_upgrade_tx( + implementation_address: Address, + address1: Address, + address2: Address, + bytecode_hash: H256, +) -> Transaction { + let impl_contract = get_complex_upgrade_abi(); + let impl_function = impl_contract.function("someComplexUpgrade").unwrap(); + let impl_calldata = impl_function + .encode_input(&[ + Token::Address(address1), + Token::Address(address2), + Token::FixedBytes(bytecode_hash.as_bytes().to_vec()), + ]) + .unwrap(); + + let complex_upgrader = get_complex_upgrader_abi(); + let upgrade_function = complex_upgrader.function("upgrade").unwrap(); + let complex_upgrader_calldata = upgrade_function + .encode_input(&[ + Token::Address(implementation_address), + Token::Bytes(impl_calldata), + ]) + .unwrap(); + + let execute = Execute { + contract_address: COMPLEX_UPGRADER_ADDRESS, + calldata: complex_upgrader_calldata, + factory_deps: vec![], + value: U256::zero(), + }; + + Transaction { + common_data: ExecuteTransactionCommon::ProtocolUpgrade(ProtocolUpgradeTxCommonData { + sender: CONTRACT_FORCE_DEPLOYER_ADDRESS, + gas_limit: U256::from(200_000_000u32), + gas_per_pubdata_limit: REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE.into(), + ..Default::default() + }), + execute, + received_timestamp_ms: 0, + raw_bytes: None, + } +} + +fn read_msg_sender_test() -> Vec { + read_bytecode("etc/contracts-test-data/artifacts-zk/contracts/complex-upgrade/msg-sender.sol/MsgSenderTest.json") +} + +fn get_complex_upgrader_abi() -> Contract { + load_sys_contract("ComplexUpgrader") +} diff --git a/core/lib/multivm/src/versions/era_vm/tests/utils.rs b/core/lib/multivm/src/versions/era_vm/tests/utils.rs new file mode 100644 index 000000000000..e1d69d53734d --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tests/utils.rs @@ -0,0 +1,179 @@ +use std::collections::HashMap; + +use era_vm::{store::StorageKey, Execution}; +use ethabi::Contract; +use once_cell::sync::Lazy; +use vm2::HeapId; +use zksync_contracts::{ + load_contract, read_bytecode, read_zbin_bytecode, BaseSystemContracts, SystemContractCode, +}; +use zksync_state::ReadStorage; +use zksync_types::{ + self, utils::storage_key_for_standard_token_balance, AccountTreeId, Address, + StorageKey as ZKStorageKey, H256, U256, +}; +use zksync_utils::{bytecode::hash_bytecode, bytes_to_be_words, h256_to_u256, u256_to_h256}; + +use crate::vm_latest::CurrentExecutionState; + +pub(crate) static BASE_SYSTEM_CONTRACTS: Lazy = + Lazy::new(BaseSystemContracts::load_from_disk); + +pub fn lambda_storage_key_to_zk(key: StorageKey) -> ZKStorageKey { + ZKStorageKey::new(AccountTreeId::new(key.address), u256_to_h256(key.key)) +} + +pub fn zk_storage_key_to_lambda(key: &ZKStorageKey) -> StorageKey { + StorageKey { + address: key.address().clone(), + key: h256_to_u256(key.key().clone()), + } +} + +pub fn sort_execution_state(state: &mut CurrentExecutionState) { + // sort events + state.events.sort_by(|a, b| a.address.cmp(&b.address)); + + // sort user_l2_to_l1_logs + state.user_l2_to_l1_logs.sort_by(|a, b| { + a.0.key + .cmp(&b.0.key) + .then_with(|| a.0.sender.cmp(&b.0.sender)) + }); + + // sort system_logs + state.system_logs.sort_by(|a, b| { + a.0.key + .cmp(&b.0.key) + .then_with(|| a.0.sender.cmp(&b.0.sender)) + }); + + // sort deduplicated_storage_logs + state.deduplicated_storage_logs.sort_by(|a, b| { + a.key + .account() + .address() + .cmp(&b.key.account().address()) + .then_with(|| a.key.key().cmp(b.key.key())) + }); + + // sort used_contract_hashes + state.used_contract_hashes.sort(); +} + +pub(crate) fn verify_required_memory(vm: &Execution, required_values: Vec<(U256, HeapId, u32)>) { + for (required_value, memory_page, cell) in required_values { + let current_value = vm.heaps.get(memory_page.to_u32()).unwrap().read(cell * 32); + assert_eq!(current_value, required_value); + } +} + +pub(crate) fn verify_required_storage( + required_values: &[(H256, ZKStorageKey)], + main_storage: &mut impl ReadStorage, + storage_changes: &HashMap, +) { + for &(required_value, key) in required_values { + let current_value = storage_changes + .get(&zk_storage_key_to_lambda(&key)) + .copied() + .unwrap_or_else(|| h256_to_u256(main_storage.read_value(&key))); + + assert_eq!( + u256_to_h256(current_value), + required_value, + "Invalid value at key {key:?}" + ); + } +} +pub(crate) fn get_balance( + token_id: AccountTreeId, + account: &Address, + main_storage: &mut impl ReadStorage, + storage_changes: &HashMap, +) -> U256 { + let key = storage_key_for_standard_token_balance(token_id, account); + + storage_changes + .get(&zk_storage_key_to_lambda(&key)) + .copied() + .unwrap_or_else(|| h256_to_u256(main_storage.read_value(&key))) +} + +pub(crate) fn read_test_contract() -> Vec { + read_bytecode("etc/contracts-test-data/artifacts-zk/contracts/counter/counter.sol/Counter.json") +} + +pub(crate) fn get_bootloader(test: &str) -> SystemContractCode { + let bootloader_code = read_zbin_bytecode(format!( + "contracts/system-contracts/bootloader/tests/artifacts/{}.yul.zbin", + test + )); + + let bootloader_hash = hash_bytecode(&bootloader_code); + SystemContractCode { + code: bytes_to_be_words(bootloader_code), + hash: bootloader_hash, + } +} + +pub(crate) fn read_error_contract() -> Vec { + read_bytecode( + "etc/contracts-test-data/artifacts-zk/contracts/error/error.sol/SimpleRequire.json", + ) +} + +pub(crate) fn get_execute_error_calldata() -> Vec { + let test_contract = load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/error/error.sol/SimpleRequire.json", + ); + + let function = test_contract.function("require_short").unwrap(); + + function + .encode_input(&[]) + .expect("failed to encode parameters") +} + +pub(crate) fn read_many_owners_custom_account_contract() -> (Vec, Contract) { + let path = "etc/contracts-test-data/artifacts-zk/contracts/custom-account/many-owners-custom-account.sol/ManyOwnersCustomAccount.json"; + (read_bytecode(path), load_contract(path)) +} + +pub(crate) fn read_precompiles_contract() -> Vec { + read_bytecode( + "etc/contracts-test-data/artifacts-zk/contracts/precompiles/precompiles.sol/Precompiles.json", + ) +} + +pub(crate) fn load_precompiles_contract() -> Contract { + load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/precompiles/precompiles.sol/Precompiles.json", + ) +} + +pub(crate) fn read_nonce_holder_tester() -> Vec { + read_bytecode("etc/contracts-test-data/artifacts-zk/contracts/custom-account/nonce-holder-test.sol/NonceHolderTest.json") +} + +pub(crate) fn read_complex_upgrade() -> Vec { + read_bytecode("etc/contracts-test-data/artifacts-zk/contracts/complex-upgrade/complex-upgrade.sol/ComplexUpgrade.json") +} + +pub(crate) fn get_complex_upgrade_abi() -> Contract { + load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/complex-upgrade/complex-upgrade.sol/ComplexUpgrade.json" + ) +} + +pub(crate) fn read_expensive_contract() -> (Vec, Contract) { + const PATH: &str = + "etc/contracts-test-data/artifacts-zk/contracts/expensive/expensive.sol/Expensive.json"; + (read_bytecode(PATH), load_contract(PATH)) +} + +pub(crate) fn read_max_depth_contract() -> Vec { + read_zbin_bytecode( + "core/tests/ts-integration/contracts/zkasm/artifacts/deep_stak.zkasm/deep_stak.zkasm.zbin", + ) +} diff --git a/core/lib/multivm/src/versions/era_vm/tracers/circuits_tracer.rs b/core/lib/multivm/src/versions/era_vm/tracers/circuits_tracer.rs new file mode 100644 index 000000000000..32ed7dde1e88 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tracers/circuits_tracer.rs @@ -0,0 +1,228 @@ +use circuit_sequencer_api_1_5_0::{geometry_config::get_geometry_config, toolset::GeometryConfig}; +use era_vm::{ + opcode::{LogOpcode, Opcode, UMAOpcode, Variant}, + statistics::VmStatistics, +}; +use zksync_state::ReadStorage; +use zksync_types::circuit::CircuitStatistic; + +use super::traits::{Tracer, VmTracer}; + +const GEOMETRY_CONFIG: GeometryConfig = get_geometry_config(); + +// "Rich addressing" opcodes are opcodes that can write their return value/read the input onto the stack +// and so take 1-2 RAM permutations more than an average opcode. +// In the worst case, a rich addressing may take 3 ram permutations +// (1 for reading the opcode, 1 for writing input value, 1 for writing output value). +pub(crate) const RICH_ADDRESSING_OPCODE_RAM_CYCLES: u32 = 3; + +pub(crate) const AVERAGE_OPCODE_RAM_CYCLES: u32 = 1; + +pub(crate) const STORAGE_READ_RAM_CYCLES: u32 = 1; +pub(crate) const STORAGE_READ_LOG_DEMUXER_CYCLES: u32 = 1; +pub(crate) const STORAGE_READ_STORAGE_SORTER_CYCLES: u32 = 1; + +pub(crate) const TRANSIENT_STORAGE_READ_RAM_CYCLES: u32 = 1; +pub(crate) const TRANSIENT_STORAGE_READ_LOG_DEMUXER_CYCLES: u32 = 1; +pub(crate) const TRANSIENT_STORAGE_READ_TRANSIENT_STORAGE_CHECKER_CYCLES: u32 = 1; + +pub(crate) const EVENT_RAM_CYCLES: u32 = 1; +pub(crate) const EVENT_LOG_DEMUXER_CYCLES: u32 = 2; +pub(crate) const EVENT_EVENTS_SORTER_CYCLES: u32 = 2; + +pub(crate) const STORAGE_WRITE_RAM_CYCLES: u32 = 1; +pub(crate) const STORAGE_WRITE_LOG_DEMUXER_CYCLES: u32 = 2; +pub(crate) const STORAGE_WRITE_STORAGE_SORTER_CYCLES: u32 = 2; + +pub(crate) const TRANSIENT_STORAGE_WRITE_RAM_CYCLES: u32 = 1; +pub(crate) const TRANSIENT_STORAGE_WRITE_LOG_DEMUXER_CYCLES: u32 = 2; +pub(crate) const TRANSIENT_STORAGE_WRITE_TRANSIENT_STORAGE_CHECKER_CYCLES: u32 = 2; + +pub(crate) const FAR_CALL_RAM_CYCLES: u32 = 1; +pub(crate) const FAR_CALL_STORAGE_SORTER_CYCLES: u32 = 1; +pub(crate) const FAR_CALL_CODE_DECOMMITTER_SORTER_CYCLES: u32 = 1; +pub(crate) const FAR_CALL_LOG_DEMUXER_CYCLES: u32 = 1; + +// 5 RAM permutations, because: 1 to read opcode + 2 reads + 2 writes. +// 2 reads and 2 writes are needed because unaligned access is implemented with +// aligned queries. +pub(crate) const UMA_WRITE_RAM_CYCLES: u32 = 5; + +// 3 RAM permutations, because: 1 to read opcode + 2 reads. +// 2 reads are needed because unaligned access is implemented with aligned queries. +pub(crate) const UMA_READ_RAM_CYCLES: u32 = 3; + +pub(crate) const PRECOMPILE_RAM_CYCLES: u32 = 1; +pub(crate) const PRECOMPILE_LOG_DEMUXER_CYCLES: u32 = 1; + +pub(crate) const LOG_DECOMMIT_RAM_CYCLES: u32 = 1; +pub(crate) const LOG_DECOMMIT_DECOMMITTER_SORTER_CYCLES: u32 = 1; + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct CircuitsTracer { + rich_addressing_opcodes: u32, + average_opcodes: u32, + storage_reads: u32, + storage_writes: u32, + transient_storage_reads: u32, + transient_storage_writes: u32, + events: u32, + precompile_calls: u32, + decommits: u32, + far_calls: u32, + heap_writes: u32, + heap_reads: u32, +} + +impl CircuitsTracer { + pub fn new() -> Self { + Self::default() + } + + pub fn circuit_statistics(&self, vm_statistics: &VmStatistics) -> CircuitStatistic { + let VmStatistics { + code_decommitter_cycles, + ecrecover_cycles, + keccak256_cycles, + secp255r1_verify_cycles: secp256k1_verify_cycles, + sha256_cycles, + storage_application_cycles, + .. + } = *vm_statistics; + + CircuitStatistic { + main_vm: (self.rich_addressing_opcodes + + self.average_opcodes + + self.storage_reads + + self.storage_writes + + self.transient_storage_reads + + self.transient_storage_writes + + self.events + + self.precompile_calls + + self.decommits + + self.far_calls + + self.heap_writes + + self.heap_reads) as f32 + / GEOMETRY_CONFIG.cycles_per_vm_snapshot as f32, + ram_permutation: (self.rich_addressing_opcodes * RICH_ADDRESSING_OPCODE_RAM_CYCLES + + self.average_opcodes * AVERAGE_OPCODE_RAM_CYCLES + + self.storage_reads * STORAGE_READ_RAM_CYCLES + + self.storage_writes * STORAGE_WRITE_RAM_CYCLES + + self.transient_storage_reads * TRANSIENT_STORAGE_READ_RAM_CYCLES + + self.transient_storage_writes * TRANSIENT_STORAGE_WRITE_RAM_CYCLES + + self.events * EVENT_RAM_CYCLES + + self.precompile_calls * PRECOMPILE_RAM_CYCLES + + self.decommits * LOG_DECOMMIT_RAM_CYCLES + + self.far_calls * FAR_CALL_RAM_CYCLES + + self.heap_writes * UMA_WRITE_RAM_CYCLES + + self.heap_reads * UMA_READ_RAM_CYCLES) as f32 + / GEOMETRY_CONFIG.cycles_per_ram_permutation as f32, + storage_application: storage_application_cycles as f32 + / GEOMETRY_CONFIG.cycles_per_storage_application as f32, + storage_sorter: (self.storage_reads * STORAGE_READ_STORAGE_SORTER_CYCLES + + self.storage_writes * STORAGE_WRITE_STORAGE_SORTER_CYCLES + + self.transient_storage_reads + * TRANSIENT_STORAGE_READ_TRANSIENT_STORAGE_CHECKER_CYCLES + + self.transient_storage_writes + * TRANSIENT_STORAGE_WRITE_TRANSIENT_STORAGE_CHECKER_CYCLES + + self.far_calls * FAR_CALL_STORAGE_SORTER_CYCLES) + as f32 + / GEOMETRY_CONFIG.cycles_per_storage_sorter as f32, + code_decommitter: code_decommitter_cycles as f32 + / GEOMETRY_CONFIG.cycles_per_code_decommitter as f32, + code_decommitter_sorter: (self.decommits * LOG_DECOMMIT_DECOMMITTER_SORTER_CYCLES + + self.far_calls * FAR_CALL_CODE_DECOMMITTER_SORTER_CYCLES) + as f32 + / GEOMETRY_CONFIG.cycles_code_decommitter_sorter as f32, + log_demuxer: (self.storage_reads * STORAGE_READ_LOG_DEMUXER_CYCLES + + self.storage_writes * STORAGE_WRITE_LOG_DEMUXER_CYCLES + + self.transient_storage_reads * TRANSIENT_STORAGE_READ_LOG_DEMUXER_CYCLES + + self.transient_storage_writes * TRANSIENT_STORAGE_WRITE_LOG_DEMUXER_CYCLES + + self.events * EVENT_LOG_DEMUXER_CYCLES + + self.precompile_calls * PRECOMPILE_LOG_DEMUXER_CYCLES + + self.far_calls * FAR_CALL_LOG_DEMUXER_CYCLES) as f32 + / GEOMETRY_CONFIG.cycles_per_log_demuxer as f32, + events_sorter: (self.events * EVENT_EVENTS_SORTER_CYCLES) as f32 + / GEOMETRY_CONFIG.cycles_per_events_or_l1_messages_sorter as f32, + keccak256: keccak256_cycles as f32 + / GEOMETRY_CONFIG.cycles_per_keccak256_circuit as f32, + ecrecover: ecrecover_cycles as f32 + / GEOMETRY_CONFIG.cycles_per_ecrecover_circuit as f32, + sha256: sha256_cycles as f32 / GEOMETRY_CONFIG.cycles_per_sha256_circuit as f32, + secp256k1_verify: secp256k1_verify_cycles as f32 + / GEOMETRY_CONFIG.cycles_per_secp256r1_verify_circuit as f32, + transient_storage_checker: (self.transient_storage_reads + * TRANSIENT_STORAGE_READ_TRANSIENT_STORAGE_CHECKER_CYCLES + + self.transient_storage_writes + * TRANSIENT_STORAGE_WRITE_TRANSIENT_STORAGE_CHECKER_CYCLES) + as f32 + / GEOMETRY_CONFIG.cycles_per_transient_storage_sorter as f32, + } + } +} + +impl Tracer for CircuitsTracer { + fn after_execution( + &mut self, + opcode: &Opcode, + _execution: &mut era_vm::Execution, + _state: &mut era_vm::state::VMState, + ) { + match opcode.variant { + Variant::Nop(_) + | Variant::Add(_) + | Variant::Sub(_) + | Variant::Mul(_) + | Variant::Div(_) + | Variant::Jump(_) + | Variant::Shift(_) + | Variant::Binop(_) + | Variant::Ptr(_) => { + self.rich_addressing_opcodes += 1; + } + Variant::Context(_) | Variant::Ret(_) | Variant::NearCall(_) => { + self.average_opcodes += 1; + } + Variant::Log(LogOpcode::StorageRead) => { + self.storage_reads += 1; + } + Variant::Log(LogOpcode::TransientStorageRead) => { + self.transient_storage_reads += 1; + } + Variant::Log(LogOpcode::StorageWrite) => { + self.storage_writes += 1; + } + Variant::Log(LogOpcode::TransientStorageWrite) => { + self.transient_storage_writes += 1; + } + Variant::Log(LogOpcode::ToL1Message) | Variant::Log(LogOpcode::Event) => { + self.events += 1; + } + Variant::Log(LogOpcode::PrecompileCall) => { + self.precompile_calls += 1; + } + Variant::Log(LogOpcode::Decommit) => { + self.decommits += 1; + } + Variant::FarCall(_) => { + self.far_calls += 1; + } + Variant::UMA( + UMAOpcode::AuxHeapWrite | UMAOpcode::HeapWrite | UMAOpcode::StaticMemoryWrite, + ) => { + self.heap_writes += 1; + } + Variant::UMA( + UMAOpcode::AuxHeapRead + | UMAOpcode::HeapRead + | UMAOpcode::FatPointerRead + | UMAOpcode::StaticMemoryRead, + ) => { + self.heap_reads += 1; + } + Variant::Invalid(_) => {} + } + } +} + +impl VmTracer for CircuitsTracer {} diff --git a/core/lib/multivm/src/versions/era_vm/tracers/debug_tracer.rs b/core/lib/multivm/src/versions/era_vm/tracers/debug_tracer.rs new file mode 100644 index 000000000000..5585745e05a2 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tracers/debug_tracer.rs @@ -0,0 +1,58 @@ +use zksync_state::ReadStorage; +use zksync_types::U256; +use zksync_utils::u256_to_h256; + +use super::traits::{Tracer, VmTracer}; +use crate::era_vm::hook::Hook; + +pub struct DebugTracer {} + +impl Tracer for DebugTracer {} + +impl VmTracer for DebugTracer { + fn bootloader_hook_call( + &mut self, + _vm: &mut super::traits::Vm, + hook: crate::era_vm::hook::Hook, + hook_params: &[U256; 3], + ) { + match hook { + Hook::DebugLog => { + let msg = u256_to_h256(hook_params[0]).as_bytes().to_vec(); + let data = u256_to_h256(hook_params[1]).as_bytes().to_vec(); + + let msg = String::from_utf8(msg).expect("Invalid debug message"); + let data = U256::from_big_endian(&data); + + // For long data, it is better to use hex-encoding for greater readability + let data_str = if data > U256::from(u64::max_value()) { + let mut bytes = [0u8; 32]; + data.to_big_endian(&mut bytes); + format!("0x{}", hex::encode(bytes)) + } else { + data.to_string() + }; + + println!("======== BOOTLOADER DEBUG LOG ========"); + println!("MSG: {:?}", msg); + println!("DATA: {}", data_str); + } + Hook::AccountValidationEntered => { + // println!("ACCOUNT VALIDATION ENTERED"); + } + Hook::ValidationStepEnded => { + // println!("VALIDATION STEP ENDED"); + } + Hook::AccountValidationExited => { + // println!("ACCOUNT VALIDATION EXITED"); + } + Hook::DebugReturnData => { + // println!("DEBUG RETURN DATA"); + } + Hook::NearCallCatch => { + // println!("NOTIFY ABOUT NEAR CALL CATCH"); + } + _ => {} + }; + } +} diff --git a/core/lib/multivm/src/versions/era_vm/tracers/dispatcher.rs b/core/lib/multivm/src/versions/era_vm/tracers/dispatcher.rs new file mode 100644 index 000000000000..4f00ca535398 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tracers/dispatcher.rs @@ -0,0 +1,90 @@ +use era_vm::{state::VMState, tracers::tracer::Tracer, Execution, Opcode}; +use zksync_state::ReadStorage; + +use super::traits::VmTracer; +use crate::{era_vm::vm::Vm, interface::tracer::TracerExecutionStatus}; + +// dispatcher calls to other tracers +pub struct TracerDispatcher { + tracers: Vec>>, +} + +impl Default for TracerDispatcher { + fn default() -> Self { + Self { tracers: vec![] } + } +} + +impl TracerDispatcher { + pub fn new(tracers: Vec>>) -> Self { + Self { tracers } + } +} + +impl Tracer for TracerDispatcher { + fn before_decoding(&mut self, execution: &mut Execution, state: &mut VMState) { + for tracer in self.tracers.iter_mut() { + tracer.before_decoding(execution, state); + } + } + + fn after_decoding(&mut self, opcode: &Opcode, execution: &mut Execution, state: &mut VMState) { + for tracer in self.tracers.iter_mut() { + tracer.after_decoding(opcode, execution, state); + } + } + + fn before_execution( + &mut self, + opcode: &Opcode, + execution: &mut Execution, + state: &mut VMState, + ) { + for tracer in self.tracers.iter_mut() { + tracer.before_execution(opcode, execution, state); + } + } + + fn after_execution(&mut self, opcode: &Opcode, execution: &mut Execution, state: &mut VMState) { + for tracer in self.tracers.iter_mut() { + tracer.after_execution(opcode, execution, state); + } + } +} + +impl VmTracer for TracerDispatcher { + fn before_bootloader_execution(&mut self, state: &mut Vm) { + for tracer in self.tracers.iter_mut() { + tracer.before_bootloader_execution(state); + } + } + + fn after_bootloader_execution(&mut self, state: &mut Vm) { + for tracer in self.tracers.iter_mut() { + tracer.after_bootloader_execution(state); + } + } + + fn bootloader_hook_call( + &mut self, + state: &mut Vm, + hook: crate::era_vm::hook::Hook, + hook_params: &[zksync_types::U256; 3], + ) { + for tracer in self.tracers.iter_mut() { + tracer.bootloader_hook_call(state, hook.clone(), &hook_params); + } + } + + fn after_vm_run( + &mut self, + vm: &mut Vm, + output: era_vm::vm::ExecutionOutput, + ) -> crate::interface::tracer::TracerExecutionStatus { + let mut result = TracerExecutionStatus::Continue; + for tracer in self.tracers.iter_mut() { + result = result.stricter(&tracer.after_vm_run(vm, output.clone())); + } + result + } +} diff --git a/core/lib/multivm/src/versions/era_vm/tracers/manager.rs b/core/lib/multivm/src/versions/era_vm/tracers/manager.rs new file mode 100644 index 000000000000..c08cc5d4b18a --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tracers/manager.rs @@ -0,0 +1,200 @@ +use era_vm::{ + execution::Execution, opcode::Opcode, state::VMState, tracers::tracer::Tracer, + vm::ExecutionOutput, +}; +use zksync_state::ReadStorage; + +use super::{ + circuits_tracer::CircuitsTracer, debug_tracer::DebugTracer, dispatcher::TracerDispatcher, + pubdata_tracer::PubdataTracer, refunds_tracer::RefundsTracer, traits::VmTracer, +}; +use crate::{era_vm::vm::Vm, interface::tracer::TracerExecutionStatus, vm_1_4_1::VmExecutionMode}; + +// this tracer manager is the one that gets called when running the vm +// all the logic of hooks and results parsing is managed from here +// the most important tracers are: `result_tracer`, `refund_tracer`, `pubdata_tracer`, and `circuits_tracer` +pub struct VmTracerManager { + pub dispatcher: TracerDispatcher, + // This tracer is designed specifically for calculating refunds and saves the results to `VmResultAndLogs`. + // it is marked as optional, because tipically we want to track refunds when we are in OneTx mode. + pub refund_tracer: Option, + // The pubdata tracer is responsible for inserting the pubdata packing information into the bootloader + // memory at the end of the batch. + pub pubdata_tracer: PubdataTracer, + // This tracers keeps track of opcodes calls and collects circuits statistics + // used later by the prover + pub circuits_tracer: CircuitsTracer, + // Tracer used for debugging purposes + pub debug_tracer: Option, +} + +impl VmTracerManager { + pub fn new( + execution_mode: VmExecutionMode, + dispatcher: TracerDispatcher, + refund_tracer: Option, + pubdata_tracer: Option, + ) -> Self { + Self { + dispatcher, + refund_tracer, + circuits_tracer: CircuitsTracer::new(), + pubdata_tracer: pubdata_tracer.unwrap_or(PubdataTracer::new(execution_mode)), + debug_tracer: None, // or Some(DebugTracer) to enable debugger + } + } +} + +impl Tracer for VmTracerManager { + fn before_decoding(&mut self, execution: &mut Execution, state: &mut VMState) { + // Call the dispatcher to handle all the tracers added to it + self.dispatcher.before_decoding(execution, state); + + // Individual tracers + if let Some(refunds_tracer) = &mut self.refund_tracer { + refunds_tracer.before_decoding(execution, state); + } + if let Some(debug_tracer) = &mut self.debug_tracer { + debug_tracer.before_decoding(execution, state); + } + self.pubdata_tracer.before_decoding(execution, state); + self.circuits_tracer.before_decoding(execution, state); + } + + fn after_decoding(&mut self, opcode: &Opcode, execution: &mut Execution, state: &mut VMState) { + // Call the dispatcher to handle all the tracers added to it + self.dispatcher.after_decoding(opcode, execution, state); + + // Individual tracers + if let Some(refunds_tracer) = &mut self.refund_tracer { + refunds_tracer.after_decoding(opcode, execution, state); + } + if let Some(debug_tracer) = &mut self.debug_tracer { + debug_tracer.after_decoding(opcode, execution, state); + } + self.pubdata_tracer.after_decoding(opcode, execution, state); + self.circuits_tracer + .after_decoding(opcode, execution, state); + } + + fn before_execution( + &mut self, + opcode: &Opcode, + execution: &mut Execution, + state: &mut VMState, + ) { + // Call the dispatcher to handle all the tracers added to it + self.dispatcher.before_execution(opcode, execution, state); + + // Individual tracers + if let Some(refunds_tracer) = &mut self.refund_tracer { + refunds_tracer.before_execution(opcode, execution, state); + } + if let Some(debug_tracer) = &mut self.debug_tracer { + debug_tracer.before_execution(opcode, execution, state); + } + self.pubdata_tracer + .before_execution(opcode, execution, state); + self.circuits_tracer + .before_execution(opcode, execution, state); + } + + fn after_execution(&mut self, opcode: &Opcode, execution: &mut Execution, state: &mut VMState) { + // Call the dispatcher to handle all the tracers added to it + self.dispatcher.after_execution(opcode, execution, state); + + // Individual tracers + if let Some(refunds_tracer) = &mut self.refund_tracer { + refunds_tracer.after_execution(opcode, execution, state); + } + if let Some(debug_tracer) = &mut self.debug_tracer { + debug_tracer.after_execution(opcode, execution, state); + } + self.pubdata_tracer + .after_execution(opcode, execution, state); + self.circuits_tracer + .after_execution(opcode, execution, state); + } +} + +impl VmTracer for VmTracerManager { + fn before_bootloader_execution(&mut self, state: &mut Vm) { + // Call the dispatcher to handle all the tracers added to it + self.dispatcher.before_bootloader_execution(state); + + // Individual tracers + if let Some(refunds_tracer) = &mut self.refund_tracer { + refunds_tracer.before_bootloader_execution(state); + } + if let Some(debug_tracer) = &mut self.debug_tracer { + debug_tracer.before_bootloader_execution(state); + } + self.pubdata_tracer.before_bootloader_execution(state); + self.circuits_tracer.before_bootloader_execution(state); + } + + fn after_bootloader_execution(&mut self, state: &mut Vm) { + // Call the dispatcher to handle all the tracers added to it + self.dispatcher.after_bootloader_execution(state); + + // Individual tracers + if let Some(refunds_tracer) = &mut self.refund_tracer { + refunds_tracer.after_bootloader_execution(state); + } + if let Some(debug_tracer) = &mut self.debug_tracer { + debug_tracer.after_bootloader_execution(state); + } + self.pubdata_tracer.after_bootloader_execution(state); + self.circuits_tracer.after_bootloader_execution(state); + } + + fn bootloader_hook_call( + &mut self, + state: &mut Vm, + hook: crate::era_vm::hook::Hook, + hook_params: &[zksync_types::U256; 3], + ) { + // Call the dispatcher to handle all the tracers added to it + self.dispatcher + .bootloader_hook_call(state, hook.clone(), hook_params); + + // Individual tracers + if let Some(refunds_tracer) = &mut self.refund_tracer { + refunds_tracer.bootloader_hook_call(state, hook.clone(), hook_params); + } + if let Some(debug_tracer) = &mut self.debug_tracer { + debug_tracer.bootloader_hook_call(state, hook.clone(), hook_params); + } + self.pubdata_tracer + .bootloader_hook_call(state, hook.clone(), hook_params); + self.circuits_tracer + .bootloader_hook_call(state, hook.clone(), hook_params); + } + + // here we apply the stricter, to make sure that the stricter output is returned + // for example: if one tracer output is Continue and the other Finish, Finish is stricter + // so we would return Finish as the final output. + fn after_vm_run(&mut self, vm: &mut Vm, output: ExecutionOutput) -> TracerExecutionStatus { + // Call the dispatcher to handle all the tracers added to it + let mut result = self.dispatcher.after_vm_run(vm, output.clone()); + + // Individual tracers + if let Some(refunds_tracer) = &mut self.refund_tracer { + result = refunds_tracer + .after_vm_run(vm, output.clone()) + .stricter(&result); + } + if let Some(debug_tracer) = &mut self.debug_tracer { + result = debug_tracer + .after_vm_run(vm, output.clone()) + .stricter(&result); + } + result = self + .pubdata_tracer + .after_vm_run(vm, output.clone()) + .stricter(&result); + self.circuits_tracer + .after_vm_run(vm, output.clone()) + .stricter(&result) + } +} diff --git a/core/lib/multivm/src/versions/era_vm/tracers/mod.rs b/core/lib/multivm/src/versions/era_vm/tracers/mod.rs new file mode 100644 index 000000000000..ff4c310e3293 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tracers/mod.rs @@ -0,0 +1,7 @@ +pub mod circuits_tracer; +pub mod debug_tracer; +pub mod dispatcher; +pub mod manager; +pub mod pubdata_tracer; +pub mod refunds_tracer; +pub mod traits; diff --git a/core/lib/multivm/src/versions/era_vm/tracers/pubdata_tracer.rs b/core/lib/multivm/src/versions/era_vm/tracers/pubdata_tracer.rs new file mode 100644 index 000000000000..0041a10da526 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tracers/pubdata_tracer.rs @@ -0,0 +1,171 @@ +use itertools::Itertools; +use zksync_state::ReadStorage; +use zksync_types::{ + event::{ + extract_l2tol1logs_from_l1_messenger, extract_long_l2_to_l1_messages, + L1_MESSENGER_BYTECODE_PUBLICATION_EVENT_SIGNATURE, + }, + writes::StateDiffRecord, + AccountTreeId, StorageKey, L1_MESSENGER_ADDRESS, U256, +}; +use zksync_utils::u256_to_h256; + +use super::traits::{Tracer, Vm, VmTracer}; +use crate::{ + era_vm::{ + bootloader_state::utils::{apply_pubdata_to_memory, PubdataInput}, + event::merge_events, + hook::Hook, + }, + interface::tracer::{TracerExecutionStatus, TracerExecutionStopReason}, + vm_1_4_1::VmExecutionMode, +}; + +pub struct PubdataTracer { + execution_mode: VmExecutionMode, + pubdata_before_run: i32, + should_stop: bool, + pub pubdata_published: u32, + // this field is to enforce a custom storage diff when setting the pubdata to the bootloader + // this is meant to be used for testing purposes only. + enforced_storage_diff: Option>, +} + +impl PubdataTracer { + pub fn new(execution_mode: VmExecutionMode) -> Self { + Self { + execution_mode, + pubdata_before_run: 0, + pubdata_published: 0, + enforced_storage_diff: None, + should_stop: false, + } + } + + pub fn new_with_forced_state_diffs( + execution_mode: VmExecutionMode, + diff: Vec, + ) -> Self { + Self { + enforced_storage_diff: Some(diff), + ..Self::new(execution_mode) + } + } + + fn get_storage_diff(&mut self, vm: &mut Vm) -> Vec { + vm.inner + .state + .get_storage_changes(&mut vm.world) + .iter() + .filter_map(|(storage_key, initial_value, value)| { + let address = storage_key.address; + + if address == L1_MESSENGER_ADDRESS { + return None; + } + + let key = storage_key.key; + + let diff = StateDiffRecord { + key, + address, + derived_key: + zk_evm_1_5_0::aux_structures::LogQuery::derive_final_address_for_params( + &address, &key, + ), + enumeration_index: vm + .storage + .get_enumeration_index(&StorageKey::new( + AccountTreeId::new(address), + u256_to_h256(key), + )) + .unwrap_or_default(), + initial_value: initial_value.unwrap_or_default(), + final_value: value.clone(), + }; + + Some(diff) + }) + // the compressor expects the storage diff to be sorted + .sorted_by(|a, b| a.address.cmp(&b.address).then_with(|| a.key.cmp(&b.key))) + .collect() + } +} + +impl Tracer for PubdataTracer {} + +impl VmTracer for PubdataTracer { + fn before_bootloader_execution(&mut self, vm: &mut super::traits::Vm) { + self.pubdata_before_run = vm.inner.state.pubdata(); + } + + fn after_bootloader_execution(&mut self, vm: &mut super::traits::Vm) { + self.pubdata_published = (vm.inner.state.pubdata() - self.pubdata_before_run).max(0) as u32; + } + + fn after_vm_run( + &mut self, + _vm: &mut Vm, + _output: era_vm::vm::ExecutionOutput, + ) -> TracerExecutionStatus { + if self.should_stop { + return TracerExecutionStatus::Stop(TracerExecutionStopReason::Finish); + } + TracerExecutionStatus::Continue + } + + fn bootloader_hook_call( + &mut self, + vm: &mut Vm, + hook: Hook, + _hook_params: &[zksync_types::U256; 3], + ) { + if let Hook::PubdataRequested = hook { + if !matches!(self.execution_mode, VmExecutionMode::Batch) { + self.should_stop = true; + }; + + let state_diffs = if let Some(diff) = &self.enforced_storage_diff { + diff.clone() + } else { + self.get_storage_diff(vm) + }; + + let events = merge_events(vm.inner.state.events(), vm.batch_env.number); + + let published_bytecodes: Vec> = events + .iter() + .filter(|event| { + // Filter events from the l1 messenger contract that match the expected signature. + event.address == L1_MESSENGER_ADDRESS + && !event.indexed_topics.is_empty() + && event.indexed_topics[0] + == *L1_MESSENGER_BYTECODE_PUBLICATION_EVENT_SIGNATURE + }) + .map(|event| { + let hash = U256::from_big_endian(&event.value[..32]); + vm.storage + .load_factory_dep(u256_to_h256(hash)) + .expect("published unknown bytecode") + .clone() + }) + .collect(); + + let pubdata_input = PubdataInput { + user_logs: extract_l2tol1logs_from_l1_messenger(&events), + l2_to_l1_messages: extract_long_l2_to_l1_messages(&events), + published_bytecodes, + state_diffs, + }; + + // Save the pubdata for the future initial bootloader memory building + vm.bootloader_state.set_pubdata_input(pubdata_input.clone()); + + // Apply the pubdata to the current memory + let mut memory_to_apply = vec![]; + + apply_pubdata_to_memory(&mut memory_to_apply, pubdata_input); + vm.write_to_bootloader_heap(memory_to_apply); + } + } +} diff --git a/core/lib/multivm/src/versions/era_vm/tracers/refunds_tracer.rs b/core/lib/multivm/src/versions/era_vm/tracers/refunds_tracer.rs new file mode 100644 index 000000000000..c436b7342d4d --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tracers/refunds_tracer.rs @@ -0,0 +1,151 @@ +use zksync_state::ReadStorage; +use zksync_types::{H256, U256}; +use zksync_utils::ceil_div_u256; + +use super::traits::{Tracer, VmTracer}; +use crate::{ + era_vm::hook::Hook, + vm_latest::{ + constants::{OPERATOR_REFUNDS_OFFSET, TX_GAS_LIMIT_OFFSET}, + utils::fee::get_batch_base_fee, + L1BatchEnv, Refunds, + }, +}; + +#[derive(Default)] +pub struct RefundsTracer { + pub gas_refunded: u64, + pub operator_suggested_refund: u64, + pubdata_before: u32, +} + +impl RefundsTracer { + pub fn new() -> Self { + Self { + gas_refunded: 0, + operator_suggested_refund: 0, + pubdata_before: 0, + } + } +} + +impl Into for RefundsTracer { + fn into(self) -> Refunds { + Refunds { + gas_refunded: self.gas_refunded, + operator_suggested_refund: self.operator_suggested_refund, + } + } +} + +impl Tracer for RefundsTracer {} + +impl VmTracer for RefundsTracer { + fn before_bootloader_execution(&mut self, vm: &mut super::traits::Vm) { + self.pubdata_before = vm.inner.state.pubdata() as u32; + } + + fn bootloader_hook_call( + &mut self, + vm: &mut super::traits::Vm, + hook: crate::era_vm::hook::Hook, + hook_params: &[zksync_types::U256; 3], + ) { + match hook { + Hook::NotifyAboutRefund => self.gas_refunded = hook_params[0].low_u64(), + Hook::AskOperatorForRefund => { + let [bootloader_refund, gas_spent_on_pubdata, gas_per_pubdata_byte] = hook_params; + let current_tx_index = vm.bootloader_state.current_tx(); + let tx_description_offset = vm + .bootloader_state + .get_tx_description_offset(current_tx_index); + let tx_gas_limit = vm + .read_heap_word(tx_description_offset + TX_GAS_LIMIT_OFFSET) + .as_u64(); + + let pubdata_published = vm.inner.state.pubdata() as u32; + + self.operator_suggested_refund = compute_refund( + &vm.batch_env, + bootloader_refund.as_u64(), + gas_spent_on_pubdata.as_u64(), + tx_gas_limit, + gas_per_pubdata_byte.low_u32(), + pubdata_published.saturating_sub(self.pubdata_before), + vm.bootloader_state.last_l2_block().txs.last().unwrap().hash, + ); + + self.pubdata_before = pubdata_published; + let refund_value = self.operator_suggested_refund; + vm.write_to_bootloader_heap([( + OPERATOR_REFUNDS_OFFSET + current_tx_index, + refund_value.into(), + )]); + vm.bootloader_state.set_refund_for_current_tx(refund_value); + } + _ => {} + }; + } +} + +pub(crate) fn compute_refund( + l1_batch: &L1BatchEnv, + bootloader_refund: u64, + gas_spent_on_pubdata: u64, + tx_gas_limit: u64, + current_ergs_per_pubdata_byte: u32, + pubdata_published: u32, + tx_hash: H256, +) -> u64 { + let total_gas_spent = tx_gas_limit - bootloader_refund; + + let gas_spent_on_computation = total_gas_spent + .checked_sub(gas_spent_on_pubdata) + .unwrap_or_else(|| { + tracing::error!( + "Gas spent on pubdata is greater than total gas spent. On pubdata: {}, total: {}", + gas_spent_on_pubdata, + total_gas_spent + ); + 0 + }); + + // For now, bootloader charges only for base fee. + let effective_gas_price = get_batch_base_fee(l1_batch); + + let bootloader_eth_price_per_pubdata_byte = + U256::from(effective_gas_price) * U256::from(current_ergs_per_pubdata_byte); + + let fair_eth_price_per_pubdata_byte = U256::from(l1_batch.fee_input.fair_pubdata_price()); + + // For now, L1 originated transactions are allowed to pay less than fair fee per pubdata, + // so we should take it into account. + let eth_price_per_pubdata_byte_for_calculation = std::cmp::min( + bootloader_eth_price_per_pubdata_byte, + fair_eth_price_per_pubdata_byte, + ); + + let fair_fee_eth = U256::from(gas_spent_on_computation) + * U256::from(l1_batch.fee_input.fair_l2_gas_price()) + + U256::from(pubdata_published) * eth_price_per_pubdata_byte_for_calculation; + let pre_paid_eth = U256::from(tx_gas_limit) * U256::from(effective_gas_price); + let refund_eth = pre_paid_eth.checked_sub(fair_fee_eth).unwrap_or_else(|| { + tracing::error!( + "Fair fee is greater than pre paid. Fair fee: {} wei, pre paid: {} wei", + fair_fee_eth, + pre_paid_eth + ); + U256::zero() + }); + + tracing::trace!( + "Fee benchmark for transaction with hash {}", + hex::encode(tx_hash.as_bytes()) + ); + tracing::trace!("Gas Limit: {}", tx_gas_limit); + tracing::trace!("Gas spent on computation: {}", gas_spent_on_computation); + tracing::trace!("Gas spent on pubdata: {}", gas_spent_on_pubdata); + tracing::trace!("Pubdata published: {}", pubdata_published); + + ceil_div_u256(refund_eth, effective_gas_price.into()).as_u64() +} diff --git a/core/lib/multivm/src/versions/era_vm/tracers/traits.rs b/core/lib/multivm/src/versions/era_vm/tracers/traits.rs new file mode 100644 index 000000000000..cc6d8adb1a74 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/tracers/traits.rs @@ -0,0 +1,20 @@ +pub use era_vm::tracers::tracer::Tracer; +use era_vm::vm::ExecutionOutput; +use zksync_state::ReadStorage; +use zksync_types::U256; + +use crate::{era_vm::hook::Hook, interface::tracer::TracerExecutionStatus}; +pub use crate::{era_vm::vm::Vm, vm_latest::ExecutionResult}; + +pub trait VmTracer: Tracer { + fn before_bootloader_execution(&mut self, _state: &mut Vm) {} + + fn after_bootloader_execution(&mut self, _state: &mut Vm) {} + + fn bootloader_hook_call(&mut self, _state: &mut Vm, _hook: Hook, _hook_params: &[U256; 3]) {} + + // runs after every vm execution or transaction + fn after_vm_run(&mut self, _vm: &mut Vm, _output: ExecutionOutput) -> TracerExecutionStatus { + TracerExecutionStatus::Continue + } +} diff --git a/core/lib/multivm/src/versions/era_vm/transaction_data.rs b/core/lib/multivm/src/versions/era_vm/transaction_data.rs new file mode 100644 index 000000000000..502be0dc22cc --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/transaction_data.rs @@ -0,0 +1,338 @@ +use std::convert::TryInto; + +use zksync_types::{ + ethabi::{encode, Address, Token}, + fee::{encoding_len, Fee}, + l1::is_l1_tx_type, + l2::{L2Tx, TransactionType}, + transaction_request::{PaymasterParams, TransactionRequest}, + web3::Bytes, + Execute, ExecuteTransactionCommon, L2ChainId, L2TxCommonData, Nonce, Transaction, H256, U256, +}; +use zksync_utils::{address_to_h256, bytecode::hash_bytecode, bytes_to_be_words, h256_to_u256}; + +use crate::vm_latest::{ + constants::{MAX_GAS_PER_PUBDATA_BYTE, TX_MAX_COMPUTE_GAS_LIMIT}, + utils::overhead::derive_overhead, +}; + +/// This structure represents the data that is used by +/// the Bootloader to describe the transaction. +#[derive(Debug, Default, Clone)] +pub(crate) struct TransactionData { + pub(crate) tx_type: u8, + pub(crate) from: Address, + pub(crate) to: Address, + pub(crate) gas_limit: U256, + pub(crate) pubdata_price_limit: U256, + pub(crate) max_fee_per_gas: U256, + pub(crate) max_priority_fee_per_gas: U256, + pub(crate) paymaster: Address, + pub(crate) nonce: U256, + pub(crate) value: U256, + // The reserved fields that are unique for different types of transactions. + // E.g. nonce is currently used in all transaction, but it should not be mandatory + // in the long run. + pub(crate) reserved: [U256; 4], + pub(crate) data: Vec, + pub(crate) signature: Vec, + // The factory deps provided with the transaction. + // Note that *only hashes* of these bytecodes are signed by the user + // and they are used in the ABI encoding of the struct. + // TODO: include this into the tx signature as part of SMA-1010 + pub(crate) factory_deps: Vec>, + pub(crate) paymaster_input: Vec, + pub(crate) reserved_dynamic: Vec, + pub(crate) raw_bytes: Option>, +} + +impl From for TransactionData { + fn from(execute_tx: Transaction) -> Self { + match execute_tx.common_data { + ExecuteTransactionCommon::L2(common_data) => { + let nonce = U256::from_big_endian(&common_data.nonce.to_be_bytes()); + + let should_check_chain_id = if matches!( + common_data.transaction_type, + TransactionType::LegacyTransaction + ) && common_data.extract_chain_id().is_some() + { + U256([1, 0, 0, 0]) + } else { + U256::zero() + }; + + // Ethereum transactions do not sign gas per pubdata limit, and so for them we need to use + // some default value. We use the maximum possible value that is allowed by the bootloader + // (i.e. we can not use u64::MAX, because the bootloader requires gas per pubdata for such + // transactions to be higher than `MAX_GAS_PER_PUBDATA_BYTE`). + let gas_per_pubdata_limit = if common_data.transaction_type.is_ethereum_type() { + MAX_GAS_PER_PUBDATA_BYTE.into() + } else { + common_data.fee.gas_per_pubdata_limit + }; + + TransactionData { + tx_type: (common_data.transaction_type as u32) as u8, + from: common_data.initiator_address, + to: execute_tx.execute.contract_address, + gas_limit: common_data.fee.gas_limit, + pubdata_price_limit: gas_per_pubdata_limit, + max_fee_per_gas: common_data.fee.max_fee_per_gas, + max_priority_fee_per_gas: common_data.fee.max_priority_fee_per_gas, + paymaster: common_data.paymaster_params.paymaster, + nonce, + value: execute_tx.execute.value, + reserved: [ + should_check_chain_id, + U256::zero(), + U256::zero(), + U256::zero(), + ], + data: execute_tx.execute.calldata, + signature: common_data.signature, + factory_deps: execute_tx.execute.factory_deps, + paymaster_input: common_data.paymaster_params.paymaster_input, + reserved_dynamic: vec![], + raw_bytes: execute_tx.raw_bytes.map(|a| a.0), + } + } + ExecuteTransactionCommon::L1(common_data) => { + let refund_recipient = h256_to_u256(address_to_h256(&common_data.refund_recipient)); + TransactionData { + tx_type: common_data.tx_format() as u8, + from: common_data.sender, + to: execute_tx.execute.contract_address, + gas_limit: common_data.gas_limit, + pubdata_price_limit: common_data.gas_per_pubdata_limit, + // It doesn't matter what we put here, since + // the bootloader does not charge anything + max_fee_per_gas: common_data.max_fee_per_gas, + max_priority_fee_per_gas: U256::zero(), + paymaster: Address::default(), + nonce: U256::from(common_data.serial_id.0), // priority op ID + value: execute_tx.execute.value, + reserved: [ + common_data.to_mint, + refund_recipient, + U256::zero(), + U256::zero(), + ], + data: execute_tx.execute.calldata, + // The signature isn't checked for L1 transactions so we don't care + signature: vec![], + factory_deps: execute_tx.execute.factory_deps, + paymaster_input: vec![], + reserved_dynamic: vec![], + raw_bytes: None, + } + } + ExecuteTransactionCommon::ProtocolUpgrade(common_data) => { + let refund_recipient = h256_to_u256(address_to_h256(&common_data.refund_recipient)); + TransactionData { + tx_type: common_data.tx_format() as u8, + from: common_data.sender, + to: execute_tx.execute.contract_address, + gas_limit: common_data.gas_limit, + pubdata_price_limit: common_data.gas_per_pubdata_limit, + // It doesn't matter what we put here, since + // the bootloader does not charge anything + max_fee_per_gas: common_data.max_fee_per_gas, + max_priority_fee_per_gas: U256::zero(), + paymaster: Address::default(), + nonce: U256::from(common_data.upgrade_id as u16), + value: execute_tx.execute.value, + reserved: [ + common_data.to_mint, + refund_recipient, + U256::zero(), + U256::zero(), + ], + data: execute_tx.execute.calldata, + // The signature isn't checked for L1 transactions so we don't care + signature: vec![], + factory_deps: execute_tx.execute.factory_deps, + paymaster_input: vec![], + reserved_dynamic: vec![], + raw_bytes: None, + } + } + } + } +} + +impl TransactionData { + pub(crate) fn abi_encode_with_custom_factory_deps( + self, + factory_deps_hashes: Vec, + ) -> Vec { + encode(&[Token::Tuple(vec![ + Token::Uint(U256::from_big_endian(&self.tx_type.to_be_bytes())), + Token::Address(self.from), + Token::Address(self.to), + Token::Uint(self.gas_limit), + Token::Uint(self.pubdata_price_limit), + Token::Uint(self.max_fee_per_gas), + Token::Uint(self.max_priority_fee_per_gas), + Token::Address(self.paymaster), + Token::Uint(self.nonce), + Token::Uint(self.value), + Token::FixedArray(self.reserved.iter().copied().map(Token::Uint).collect()), + Token::Bytes(self.data), + Token::Bytes(self.signature), + Token::Array(factory_deps_hashes.into_iter().map(Token::Uint).collect()), + Token::Bytes(self.paymaster_input), + Token::Bytes(self.reserved_dynamic), + ])]) + } + + pub(crate) fn abi_encode(self) -> Vec { + let factory_deps_hashes = self + .factory_deps + .iter() + .map(|dep| h256_to_u256(hash_bytecode(dep))) + .collect(); + self.abi_encode_with_custom_factory_deps(factory_deps_hashes) + } + + pub(crate) fn into_tokens(self) -> Vec { + let bytes = self.abi_encode(); + assert!(bytes.len() % 32 == 0); + + bytes_to_be_words(bytes) + } + + pub(crate) fn overhead_gas(&self) -> u32 { + let encoded_len = encoding_len( + self.data.len() as u64, + self.signature.len() as u64, + self.factory_deps.len() as u64, + self.paymaster_input.len() as u64, + self.reserved_dynamic.len() as u64, + ); + + derive_overhead(encoded_len) + } + + pub(crate) fn trusted_ergs_limit(&self) -> U256 { + // No transaction is allowed to spend more than `TX_MAX_COMPUTE_GAS_LIMIT` gas on compute. + U256::from(TX_MAX_COMPUTE_GAS_LIMIT).min(self.gas_limit) + } + + pub(crate) fn tx_hash(&self, chain_id: L2ChainId) -> H256 { + if is_l1_tx_type(self.tx_type) { + return self.canonical_l1_tx_hash().unwrap(); + } + + let l2_tx: L2Tx = self.clone().try_into().unwrap(); + let mut transaction_request: TransactionRequest = l2_tx.into(); + 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") + } + + fn canonical_l1_tx_hash(&self) -> Result { + use zksync_types::web3::keccak256; + + if !is_l1_tx_type(self.tx_type) { + return Err(TxHashCalculationError::CannotCalculateL1HashForL2Tx); + } + + let encoded_bytes = self.clone().abi_encode(); + + Ok(H256(keccak256(&encoded_bytes))) + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum TxHashCalculationError { + CannotCalculateL1HashForL2Tx, + CannotCalculateL2HashForL1Tx, +} + +impl TryInto for TransactionData { + type Error = TxHashCalculationError; + + fn try_into(self) -> Result { + if is_l1_tx_type(self.tx_type) { + return Err(TxHashCalculationError::CannotCalculateL2HashForL1Tx); + } + + let common_data = L2TxCommonData { + transaction_type: (self.tx_type as u32).try_into().unwrap(), + nonce: Nonce(self.nonce.as_u32()), + fee: Fee { + max_fee_per_gas: self.max_fee_per_gas, + max_priority_fee_per_gas: self.max_priority_fee_per_gas, + gas_limit: self.gas_limit, + gas_per_pubdata_limit: self.pubdata_price_limit, + }, + signature: self.signature, + input: None, + initiator_address: self.from, + paymaster_params: PaymasterParams { + paymaster: self.paymaster, + paymaster_input: self.paymaster_input, + }, + }; + let execute = Execute { + contract_address: self.to, + value: self.value, + calldata: self.data, + factory_deps: self.factory_deps, + }; + + Ok(L2Tx { + execute, + common_data, + received_timestamp_ms: 0, + raw_bytes: self.raw_bytes.map(Bytes::from), + }) + } +} + +#[cfg(test)] +mod tests { + use zksync_types::fee::encoding_len; + + use super::*; + + #[test] + fn test_consistency_with_encoding_length() { + let transaction = TransactionData { + tx_type: 113, + from: Address::random(), + to: Address::random(), + gas_limit: U256::from(1u32), + pubdata_price_limit: U256::from(1u32), + max_fee_per_gas: U256::from(1u32), + max_priority_fee_per_gas: U256::from(1u32), + paymaster: Address::random(), + nonce: U256::zero(), + value: U256::zero(), + // The reserved fields that are unique for different types of transactions. + // E.g. nonce is currently used in all transaction, but it should not be mandatory + // in the long run. + reserved: [U256::zero(); 4], + data: vec![0u8; 65], + signature: vec![0u8; 75], + // The factory deps provided with the transaction. + // Note that *only hashes* of these bytecodes are signed by the user + // and they are used in the ABI encoding of the struct. + // TODO: include this into the tx signature as part of SMA-1010 + factory_deps: vec![vec![0u8; 32], vec![1u8; 32]], + paymaster_input: vec![0u8; 85], + reserved_dynamic: vec![0u8; 32], + raw_bytes: None, + }; + + let assumed_encoded_len = encoding_len(65, 75, 2, 85, 32); + + let true_encoding_len = transaction.into_tokens().len(); + + assert_eq!(assumed_encoded_len, true_encoding_len); + } +} diff --git a/core/lib/multivm/src/versions/era_vm/vm.rs b/core/lib/multivm/src/versions/era_vm/vm.rs new file mode 100644 index 000000000000..af4dee3053a1 --- /dev/null +++ b/core/lib/multivm/src/versions/era_vm/vm.rs @@ -0,0 +1,759 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use era_vm::{ + rollbacks::Rollbackable, store::StorageKey as EraStorageKey, value::FatPointer, + vm::ExecutionOutput, EraVM, Execution, +}; +use itertools::Itertools; +use zksync_state::{ReadStorage, StoragePtr}; +use zksync_types::{ + event::{ + extract_l2tol1logs_from_l1_messenger, extract_long_l2_to_l1_messages, + L1_MESSENGER_BYTECODE_PUBLICATION_EVENT_SIGNATURE, + }, + get_known_code_key, + l1::is_l1_tx_type, + l2_to_l1_log::UserL2ToL1Log, + utils::key_for_eth_balance, + writes::{ + compression::compress_with_best_strategy, BYTES_PER_DERIVED_KEY, + BYTES_PER_ENUMERATION_INDEX, + }, + AccountTreeId, StorageKey, StorageLog, StorageLogKind, StorageLogWithPreviousValue, + Transaction, BOOTLOADER_ADDRESS, H160, H256, KNOWN_CODES_STORAGE_ADDRESS, + L2_BASE_TOKEN_ADDRESS, U256, +}; +use zksync_utils::{ + bytecode::{hash_bytecode, CompressedBytecodeInfo}, + h256_to_u256, u256_to_h256, +}; + +use super::{ + bootloader_state::{utils::apply_l2_block, BootloaderState}, + event::merge_events, + hook::Hook, + initial_bootloader_memory::bootloader_initial_memory, + logs::IntoSystemLog, + snapshot::VmSnapshot, + tracers::{ + dispatcher::TracerDispatcher, manager::VmTracerManager, pubdata_tracer::PubdataTracer, + refunds_tracer::RefundsTracer, traits::VmTracer, + }, +}; +use crate::{ + era_vm::{bytecode::compress_bytecodes, transaction_data::TransactionData}, + interface::{ + tracer::{TracerExecutionStatus, TracerExecutionStopReason}, + BytecodeCompressionError, Halt, TxRevertReason, VmFactory, VmInterface, + VmInterfaceHistoryEnabled, VmRevertReason, + }, + vm_latest::{ + constants::{ + get_result_success_first_slot, get_vm_hook_position, get_vm_hook_start_position_latest, + VM_HOOK_PARAMS_COUNT, + }, + BootloaderMemory, CurrentExecutionState, ExecutionResult, FinishedL1Batch, L1BatchEnv, + L2BlockEnv, Refunds, SystemEnv, VmExecutionLogs, VmExecutionMode, VmExecutionResultAndLogs, + VmExecutionStatistics, + }, +}; + +pub struct Vm { + pub(crate) inner: EraVM, + pub suspended_at: u16, + pub gas_for_account_validation: u32, + pub world: World, + + pub bootloader_state: BootloaderState, + pub(crate) storage: StoragePtr, + + // TODO: Maybe not necessary, check + pub(crate) program_cache: Rc>>>, + + // these two are only needed for tests so far + pub(crate) batch_env: L1BatchEnv, + pub(crate) system_env: SystemEnv, + + pub snapshot: Option, +} + +/// Encapsulates creating VM instance based on the provided environment. +impl VmFactory for Vm { + /// Creates a new VM instance. + fn new(batch_env: L1BatchEnv, system_env: SystemEnv, storage: StoragePtr) -> Self { + let bootloader_code = system_env + .base_system_smart_contracts + .bootloader + .code + .clone(); + let vm_hook_position = + get_vm_hook_position(crate::vm_latest::MultiVMSubversion::IncreasedBootloaderMemory) + * 32; + let vm_execution = Execution::new( + bootloader_code.to_owned(), + Vec::new(), + BOOTLOADER_ADDRESS, + H160::zero(), + 0_u128, + system_env + .base_system_smart_contracts + .default_aa + .hash + .to_fixed_bytes(), + system_env + .base_system_smart_contracts + .default_aa //TODO: Add real evm interpreter + .hash + .to_fixed_bytes(), + vm_hook_position, + true, + system_env.bootloader_gas_limit, + ); + let pre_contract_storage = Rc::new(RefCell::new(HashMap::new())); + pre_contract_storage.borrow_mut().insert( + h256_to_u256(system_env.base_system_smart_contracts.default_aa.hash), + system_env + .base_system_smart_contracts + .default_aa + .code + .clone(), + ); + let world = World::new(storage.clone(), pre_contract_storage.clone()); + let mut vm = EraVM::new(vm_execution); + let bootloader_memory = bootloader_initial_memory(&batch_env); + + // The bootloader shouldn't pay for growing memory and it writes results + // to the end of its heap, so it makes sense to preallocate it in its entirety. + const BOOTLOADER_MAX_MEMORY_SIZE: u32 = u32::MAX; + vm.execution + .heaps + .get_mut(era_vm::execution::FIRST_HEAP) + .unwrap() + .expand_memory(BOOTLOADER_MAX_MEMORY_SIZE); + vm.execution + .heaps + .get_mut(era_vm::execution::FIRST_HEAP + 1) + .unwrap() + .expand_memory(BOOTLOADER_MAX_MEMORY_SIZE); + + let mut mv = Self { + inner: vm, + suspended_at: 0, + gas_for_account_validation: system_env.default_validation_computational_gas_limit, + bootloader_state: BootloaderState::new( + system_env.execution_mode.clone(), + bootloader_initial_memory(&batch_env), + batch_env.first_l2_block, + ), + program_cache: pre_contract_storage, + storage, + batch_env, + system_env, + snapshot: None, + world, + }; + + mv.write_to_bootloader_heap(bootloader_memory); + mv + } +} + +impl Vm { + pub fn run( + &mut self, + execution_mode: VmExecutionMode, + tracer: &mut impl VmTracer, + ) -> ExecutionResult { + tracer.before_bootloader_execution(self); + let mut last_tx_result: Option = None; + let result = loop { + let output = self + .inner + .run_program_with_custom_bytecode_and_tracer(tracer, &mut self.world); + let status = tracer.after_vm_run(self, output.clone()); + let (hook, hook_params) = match output { + ExecutionOutput::Ok(output) => break ExecutionResult::Success { output }, + ExecutionOutput::Revert(output) => match TxRevertReason::parse_error(&output) { + TxRevertReason::TxReverted(output) => break ExecutionResult::Revert { output }, + TxRevertReason::Halt(reason) => break ExecutionResult::Halt { reason }, + }, + ExecutionOutput::Panic => { + break ExecutionResult::Halt { + reason: if self.inner.execution.gas_left().unwrap() == 0 { + Halt::BootloaderOutOfGas + } else { + Halt::VMPanic + }, + } + } + ExecutionOutput::SuspendedOnHook { + hook, + pc_to_resume_from, + } => { + self.suspended_at = pc_to_resume_from; + self.inner.execution.current_frame_mut().unwrap().pc = self.suspended_at as u64; + (Hook::from_u32(hook), self.get_hook_params()) + } + }; + + tracer.bootloader_hook_call(self, hook.clone(), &self.get_hook_params()); + + match hook { + Hook::PostResult => { + let result = hook_params[0]; + let value = hook_params[1]; + let pointer = FatPointer::decode(value); + assert_eq!(pointer.offset, 0); + + let return_data = self + .inner + .execution + .heaps + .get(pointer.page) + .unwrap() + .read_unaligned_from_pointer(&pointer) + .unwrap(); + + last_tx_result = Some(if result.is_zero() { + ExecutionResult::Revert { + output: VmRevertReason::from(return_data.as_slice()), + } + } else { + ExecutionResult::Success { + output: return_data, + } + }); + } + Hook::TxHasEnded => { + if let VmExecutionMode::OneTx = execution_mode { + break last_tx_result + .expect("There should always be a result if we got this hook"); + } + } + Hook::FinalBatchInfo => { + // set fictive l2 block + let txs_index = self.bootloader_state.free_tx_index(); + let l2_block = self.bootloader_state.insert_fictive_l2_block(); + let mut memory = vec![]; + apply_l2_block(&mut memory, l2_block, txs_index); + self.write_to_bootloader_heap(memory); + } + _ => {} + } + + if let TracerExecutionStatus::Stop(reason) = status { + match reason { + TracerExecutionStopReason::Abort(halt) => { + break ExecutionResult::Halt { reason: halt } + } + TracerExecutionStopReason::Finish => { + if self.inner.execution.gas_left().unwrap() == 0 { + break ExecutionResult::Halt { + reason: Halt::BootloaderOutOfGas, + }; + } + if last_tx_result.is_some() { + break last_tx_result.unwrap(); + } + let has_failed = + self.tx_has_failed(self.bootloader_state.current_tx() as u32); + if has_failed { + break ExecutionResult::Revert { + output: crate::interface::VmRevertReason::General { + msg: "Transaction reverted with empty reason. Possibly out of gas".to_string(), + data: vec![], + }, + }; + } else { + break ExecutionResult::Success { output: vec![] }; + } + } + _ => {} + } + } + }; + tracer.after_bootloader_execution(self); + result + } + + fn tx_has_failed(&self, tx_id: u32) -> bool { + let mem_slot = get_result_success_first_slot( + crate::vm_latest::MultiVMSubversion::IncreasedBootloaderMemory, + ) + tx_id; + let mem_value = self.read_heap_word(mem_slot as usize); + mem_value == U256::zero() + } + + pub(crate) fn insert_bytecodes<'a>(&mut self, bytecodes: impl IntoIterator) { + for code in bytecodes { + let mut program_code = vec![]; + for raw_opcode_slice in code.chunks(32) { + let mut raw_opcode_bytes: [u8; 32] = [0; 32]; + raw_opcode_bytes.copy_from_slice(&raw_opcode_slice[..32]); + let raw_opcode_u256 = U256::from_big_endian(&raw_opcode_bytes); + program_code.push(raw_opcode_u256); + } + self.program_cache.borrow_mut().insert( + U256::from_big_endian(hash_bytecode(code).as_bytes()), + program_code, + ); + } + } + + pub fn get_hook_params(&self) -> [U256; 3] { + let vm_hooks_param_start = get_vm_hook_start_position_latest(); + (vm_hooks_param_start..vm_hooks_param_start + VM_HOOK_PARAMS_COUNT) + .map(|word| { + let res = self.read_heap_word(word as usize); + res + }) + .collect::>() + .try_into() + .unwrap() + } + + /// Typically used to read the bootloader heap. We know that we're in the bootloader + /// when a hook occurs, as they are only enabled when preprocessing bootloader code. + pub fn read_heap_word(&self, word: usize) -> U256 { + let heap = self + .inner + .execution + .heaps + .get(self.inner.execution.current_context().unwrap().heap_id) + .unwrap(); + heap.read((word * 32) as u32) + } + + pub fn write_to_bootloader_heap(&mut self, memory: impl IntoIterator) { + assert!(self.inner.execution.running_contexts.len() == 1); // No on-going far calls + if let Some(heap) = &mut self + .inner + .execution + .heaps + .get_mut(self.inner.execution.current_context().unwrap().heap_id) + { + for (slot, value) in memory { + let end = (slot + 1) * 32; + heap.expand_memory(end as u32); + heap.store((slot * 32) as u32, value); + } + } + } + + pub fn push_transaction_inner(&mut self, tx: 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 + .state + .storage_changes() + .get(&EraStorageKey::new( + KNOWN_CODES_STORAGE_ADDRESS, + h256_to_u256(hash), + )) + .map(|x| !x.is_zero()) + .unwrap_or_else(|| self.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); + } + + fn has_unpublished_bytecodes(&mut self) -> bool { + self.bootloader_state + .get_last_tx_compressed_bytecodes() + .iter() + .any(|info| { + let hash_bytecode = hash_bytecode(&info.original); + let code_key = get_known_code_key(&hash_bytecode); + self.storage.borrow_mut().read_value(&code_key) != H256::zero() + }) + } +} + +impl Vm { + pub fn inspect_inner( + &mut self, + tracer: TracerDispatcher, + custom_pubdata_tracer: Option, + execution_mode: VmExecutionMode, + ) -> VmExecutionResultAndLogs { + let mut track_refunds = false; + if let VmExecutionMode::OneTx = execution_mode { + // Move the pointer to the next transaction + self.bootloader_state.move_tx_to_execute_pointer(); + track_refunds = true; + } + + let refund_tracer = if track_refunds { + Some(RefundsTracer::new()) + } else { + None + }; + let mut tracer = + VmTracerManager::new(execution_mode, tracer, refund_tracer, custom_pubdata_tracer); + let snapshot = self.inner.state.snapshot(); + + let ergs_before = self.inner.execution.gas_left().unwrap(); + let monotonic_counter_before = self.inner.statistics.monotonic_counter; + + let result = self.run(execution_mode, &mut tracer); + let ergs_after = self.inner.execution.gas_left().unwrap(); + + let ignore_world_diff = matches!(execution_mode, VmExecutionMode::OneTx) + && matches!(result, ExecutionResult::Halt { .. }); + + let logs = if ignore_world_diff { + VmExecutionLogs::default() + } else { + let events = merge_events( + self.inner.state.get_events_after_snapshot(snapshot.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(); + let system_l2_to_l1_logs = self + .inner + .state + .get_l2_to_l1_logs_after_snapshot(snapshot.l2_to_l1_logs) + .iter() + .map(|log| log.into_system_log()) + .collect(); + let storage_logs: Vec = self + .inner + .state + .get_storage_changes_from_snapshot(snapshot.storage_changes, &mut self.world) + .iter() + .map(|(storage_key, previos_value, value, is_initial)| { + let key = StorageKey::new( + AccountTreeId::new(storage_key.address), + u256_to_h256(storage_key.key), + ); + + StorageLogWithPreviousValue { + log: StorageLog { + key, + value: u256_to_h256(*value), + kind: if *is_initial { + StorageLogKind::InitialWrite + } else { + StorageLogKind::RepeatedWrite + }, + }, + previous_value: u256_to_h256(previos_value.unwrap_or_default()), + } + }) + .sorted_by(|a, b| { + a.log + .key + .address() + .cmp(&b.log.key.address()) + .then_with(|| a.log.key.key().cmp(&b.log.key.key())) + }) + .collect(); + + VmExecutionLogs { + storage_logs, + events, + user_l2_to_l1_logs, + system_l2_to_l1_logs, + total_log_queries_count: 0, // This field is unused + } + }; + + VmExecutionResultAndLogs { + result, + logs, + statistics: VmExecutionStatistics { + contracts_used: self.inner.state.decommitted_hashes().len(), + cycles_used: self.inner.statistics.monotonic_counter - monotonic_counter_before, + gas_used: (ergs_before - ergs_after) as u64, + gas_remaining: ergs_after, + computational_gas_used: ergs_before - ergs_after, + total_log_queries: 0, + pubdata_published: tracer.pubdata_tracer.pubdata_published, + circuit_statistic: tracer + .circuits_tracer + .circuit_statistics(&self.inner.statistics), + }, + refunds: tracer.refund_tracer.unwrap_or_default().into(), + } + } +} + +impl VmInterface for Vm { + type TracerDispatcher = TracerDispatcher; + + fn push_transaction(&mut self, tx: Transaction) { + self.push_transaction_inner(tx, 0, true); + } + + fn inspect( + &mut self, + tracer: Self::TracerDispatcher, + execution_mode: VmExecutionMode, + ) -> VmExecutionResultAndLogs { + self.inspect_inner(tracer, None, execution_mode) + } + + fn get_bootloader_memory(&self) -> BootloaderMemory { + self.bootloader_state.bootloader_memory() + } + + fn get_last_tx_compressed_bytecodes(&self) -> Vec { + self.bootloader_state.get_last_tx_compressed_bytecodes() + } + + fn start_new_l2_block(&mut self, l2_block_env: L2BlockEnv) { + self.bootloader_state.start_new_l2_block(l2_block_env) + } + + fn get_current_execution_state(&self) -> CurrentExecutionState { + let state = &self.inner.state; + let events = merge_events(state.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: state + .storage_changes() + .iter() + .map(|(storage_key, value)| StorageLog { + key: StorageKey::new( + AccountTreeId::new(storage_key.address), + u256_to_h256(storage_key.key), + ), + value: u256_to_h256(*value), + kind: StorageLogKind::RepeatedWrite, + }) + .collect(), + used_contract_hashes: state.decommitted_hashes().iter().cloned().collect(), + system_logs: state + .l2_to_l1_logs() + .iter() + .map(|log| log.into_system_log()) + .collect(), + user_l2_to_l1_logs, + storage_refunds: state.refunds().to_vec(), + pubdata_costs: state.pubdata_costs().to_vec(), + } + } + + fn inspect_transaction_with_bytecode_compression( + &mut self, + tracer: Self::TracerDispatcher, + tx: zksync_types::Transaction, + with_compression: bool, + ) -> ( + Result<(), crate::interface::BytecodeCompressionError>, + VmExecutionResultAndLogs, + ) { + self.push_transaction_inner(tx, 0, with_compression); + let result = self.inspect(tracer, VmExecutionMode::OneTx); + + let compression_result = if self.has_unpublished_bytecodes() { + Err(BytecodeCompressionError::BytecodeCompressionFailed) + } else { + Ok(()) + }; + (compression_result, result) + } + + fn record_vm_memory_metrics(&self) -> crate::vm_1_4_1::VmMemoryMetrics { + todo!() + } + + fn gas_remaining(&self) -> u32 { + self.inner.execution.current_frame().unwrap().gas_left.0 + } + + fn finish_batch(&mut self) -> FinishedL1Batch { + let result = self.execute(VmExecutionMode::Batch); + let execution_state = self.get_current_execution_state(); + let bootloader_memory = self.get_bootloader_memory(); + FinishedL1Batch { + block_tip_execution_result: result, + final_execution_state: execution_state, + final_bootloader_memory: Some(bootloader_memory), + pubdata_input: Some( + self.bootloader_state + .get_pubdata_information() + .clone() + .build_pubdata(false), + ), + state_diffs: Some( + self.bootloader_state + .get_pubdata_information() + .state_diffs + .to_vec(), + ), + } + } +} + +impl VmInterfaceHistoryEnabled for Vm { + fn make_snapshot(&mut self) { + assert!( + self.snapshot.is_none(), + "cannot create a VM snapshot until a previous snapshot is rolled back to or popped" + ); + + self.snapshot = Some(VmSnapshot { + vm_snapshot: self.inner.snapshot(), + suspended_at: self.suspended_at, + gas_for_account_validation: self.gas_for_account_validation, + bootloader_snapshot: self.bootloader_state.get_snapshot(), + }); + } + + fn rollback_to_the_latest_snapshot(&mut self) { + let VmSnapshot { + vm_snapshot, + suspended_at, + gas_for_account_validation, + bootloader_snapshot, + } = self.snapshot.take().expect("no snapshots to rollback to"); + + self.inner.rollback(vm_snapshot); + self.bootloader_state.apply_snapshot(bootloader_snapshot); + self.suspended_at = suspended_at; + self.gas_for_account_validation = gas_for_account_validation; + } + + fn pop_snapshot_no_rollback(&mut self) { + self.snapshot = None; + } +} + +#[derive(Debug, Clone)] +pub struct World { + pub storage: StoragePtr, + pub contract_storage: Rc>>>, +} + +impl World { + pub fn new_empty(storage: StoragePtr) -> Self { + let contract_storage = Rc::new(RefCell::new(HashMap::new())); + Self { + contract_storage, + storage, + } + } + + pub fn new( + storage: StoragePtr, + contract_storage: Rc>>>, + ) -> Self { + Self { + storage, + contract_storage, + } + } +} + +impl era_vm::store::Storage for World { + fn decommit(&mut self, hash: U256) -> Option> { + Some( + self.contract_storage + .borrow_mut() + .entry(hash) + .or_insert_with(|| { + let contract = self + .storage + .borrow_mut() + .load_factory_dep(u256_to_h256(hash)) + .expect("Bytecode not found"); + let mut program_code = vec![]; + for raw_opcode_slice in contract.chunks(32) { + let mut raw_opcode_bytes: [u8; 32] = [0; 32]; + raw_opcode_bytes.copy_from_slice(&raw_opcode_slice[..32]); + + let raw_opcode_u256 = U256::from_big_endian(&raw_opcode_bytes); + program_code.push(raw_opcode_u256); + } + program_code + }) + .clone(), + ) + } + + fn storage_read( + &mut self, + storage_key: &era_vm::store::StorageKey, + ) -> std::option::Option { + let key = &StorageKey::new( + AccountTreeId::new(storage_key.address), + u256_to_h256(storage_key.key), + ); + + if self.storage.is_write_initial(&key) { + None + } else { + Some(self.storage.borrow_mut().read_value(key).0.into()) + } + } + + fn cost_of_writing_storage( + &mut self, + storage_key: &era_vm::store::StorageKey, + value: U256, + ) -> u32 { + let initial_value = self.storage_read(storage_key); + let is_initial = initial_value.is_none(); + let initial_value = initial_value.unwrap_or_default(); + + if initial_value == 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(initial_value, value).len() as u32; + + if is_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, storage_key: &era_vm::store::StorageKey) -> bool { + storage_key.address == zksync_system_constants::SYSTEM_CONTEXT_ADDRESS + || storage_key.address == L2_BASE_TOKEN_ADDRESS + && u256_to_h256(storage_key.key) == key_for_eth_balance(&BOOTLOADER_ADDRESS) + } +} diff --git a/core/lib/multivm/src/versions/mod.rs b/core/lib/multivm/src/versions/mod.rs index e9e34c1cda16..6708f5512eb7 100644 --- a/core/lib/multivm/src/versions/mod.rs +++ b/core/lib/multivm/src/versions/mod.rs @@ -1,3 +1,4 @@ +pub mod era_vm; pub mod shadow; pub mod vm_1_3_2; pub mod vm_1_4_1; diff --git a/core/lib/multivm/src/vm_instance.rs b/core/lib/multivm/src/vm_instance.rs index c8a7ce837991..8fb96be01263 100644 --- a/core/lib/multivm/src/vm_instance.rs +++ b/core/lib/multivm/src/vm_instance.rs @@ -1,3 +1,5 @@ +use std::{cell::RefCell, rc::Rc}; + use zksync_state::{ImmutableStorageView, ReadStorage, StoragePtr, StorageView}; use zksync_types::vm::{FastVmMode, VmVersion}; use zksync_utils::bytecode::CompressedBytecodeInfo; @@ -15,7 +17,8 @@ use crate::{ pub type ShadowedFastVm = ShadowVm, H>>; -#[derive(Debug)] +// TODO READ ADD THE DEBUG +// #[derive(Debug)] pub enum VmInstance { VmM5(crate::vm_m5::Vm, H>), VmM6(crate::vm_m6::Vm, H>), @@ -27,6 +30,7 @@ pub enum VmInstance { Vm1_4_2(crate::vm_1_4_2::Vm, H>), Vm1_5_0(crate::vm_latest::Vm, H>), VmFast(crate::vm_fast::Vm>), + LambdaVm(crate::era_vm::vm::Vm>), ShadowedVmFast(ShadowedFastVm), } @@ -43,6 +47,7 @@ macro_rules! dispatch_vm { VmInstance::Vm1_4_2(vm) => vm.$function($($params)*), VmInstance::Vm1_5_0(vm) => vm.$function($($params)*), VmInstance::VmFast(vm) => vm.$function($($params)*), + VmInstance::LambdaVm(vm) => vm.$function($($params)*), VmInstance::ShadowedVmFast(vm) => vm.$function($($params)*), } }; @@ -260,8 +265,14 @@ impl VmInstance { VmVersion::Vm1_5_0IncreasedBootloaderMemory => match mode { FastVmMode::Old => Self::new(l1_batch_env, system_env, storage_view), FastVmMode::New => { - let storage = ImmutableStorageView::new(storage_view); - Self::VmFast(crate::vm_fast::Vm::new(l1_batch_env, system_env, storage)) + //let storage = ImmutableStorageView::new(storage_view); + //Self::VmFast(crate::vm_fast::Vm::new(l1_batch_env, system_env, storage)) + + Self::LambdaVm(crate::era_vm::vm::Vm::new( + l1_batch_env, + system_env, + storage_view, + )) } FastVmMode::Shadow => { Self::ShadowedVmFast(ShadowVm::new(l1_batch_env, system_env, storage_view)) diff --git a/core/tests/ts-integration/package.json b/core/tests/ts-integration/package.json index 03bd84bb3f48..9fd4d32cbd43 100644 --- a/core/tests/ts-integration/package.json +++ b/core/tests/ts-integration/package.json @@ -4,7 +4,7 @@ "license": "MIT", "private": true, "scripts": { - "test": "zk f jest --forceExit --testTimeout 60000", + "test": "zk f jest --forceExit --testTimeout 600000", "long-running-test": "zk f jest", "fee-test": "RUN_FEE_TEST=1 zk f jest -- fees.test.ts", "api-test": "zk f jest -- api/web3.test.ts api/debug.test.ts", diff --git a/core/tests/ts-integration/tests/api/web3.test.ts b/core/tests/ts-integration/tests/api/web3.test.ts index 569321d548ce..9a22c89cd43a 100644 --- a/core/tests/ts-integration/tests/api/web3.test.ts +++ b/core/tests/ts-integration/tests/api/web3.test.ts @@ -1232,7 +1232,7 @@ describe('web3 API compatibility tests', () => { * @param iterationStep The number of times this function has been called. */ async function tryWait(iterationStep: number) { - const maxWaitTimeMs = 100_000; // 100 seconds + const maxWaitTimeMs = 1_000_000; // 1000 seconds const maxRetries = maxWaitTimeMs / alice.provider.pollingInterval; await zksync.utils.sleep(alice.provider.pollingInterval); if (iterationStep >= maxRetries) { diff --git a/core/tests/vm-benchmark/Cargo.toml b/core/tests/vm-benchmark/Cargo.toml index efbc08a957a6..27218d79aafe 100644 --- a/core/tests/vm-benchmark/Cargo.toml +++ b/core/tests/vm-benchmark/Cargo.toml @@ -6,8 +6,11 @@ license.workspace = true publish = false [dependencies] -zksync_vm_benchmark_harness.workspace = true +zksync_types.workspace = true zksync_vlog.workspace = true +zksync_vm_benchmark_harness.workspace = true + +rand.workspace = true vise.workspace = true tokio.workspace = true diff --git a/core/tests/vm-benchmark/benches/criterion.rs b/core/tests/vm-benchmark/benches/criterion.rs index 5becccfab801..5021ec62563c 100644 --- a/core/tests/vm-benchmark/benches/criterion.rs +++ b/core/tests/vm-benchmark/benches/criterion.rs @@ -1,20 +1,107 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use zksync_vm_benchmark_harness::{cut_to_allowed_bytecode_size, get_deploy_tx, BenchmarkingVm}; +use std::time::Duration; -fn benches_in_folder(c: &mut Criterion) { - for path in std::fs::read_dir("deployment_benchmarks").unwrap() { +use criterion::{ + black_box, criterion_group, criterion_main, measurement::WallTime, BatchSize, BenchmarkGroup, + Criterion, +}; +use zksync_types::Transaction; +use zksync_vm_benchmark_harness::{ + cut_to_allowed_bytecode_size, get_deploy_tx, get_heavy_load_test_tx, get_load_test_deploy_tx, + get_load_test_tx, get_realistic_load_test_tx, BenchmarkingVm, BenchmarkingVmFactory, Fast, + Lambda, Legacy, LoadTestParams, +}; + +const SAMPLE_SIZE: usize = 20; +const ZKSYNC_HOME: &str = std::env!("ZKSYNC_HOME"); + +fn benches_in_folder(c: &mut Criterion) { + let mut group = c.benchmark_group(VM::LABEL.as_str()); + group + .sample_size(SAMPLE_SIZE) + .measurement_time(Duration::from_secs(10)); + + let benches = format!( + "{}/core/tests/vm-benchmark/deployment_benchmarks", + ZKSYNC_HOME + ); + + for path in std::fs::read_dir(&benches).unwrap() { let path = path.unwrap().path(); let test_contract = std::fs::read(&path).expect("failed to read file"); let code = cut_to_allowed_bytecode_size(&test_contract).unwrap(); let tx = get_deploy_tx(code); - - c.bench_function(path.file_name().unwrap().to_str().unwrap(), |b| { - b.iter(|| BenchmarkingVm::new().run_transaction(black_box(&tx))) + let file_name = path.file_name().unwrap().to_str().unwrap(); + let full_suffix = if FULL { "/full" } else { "" }; + let bench_name = format!("{file_name}{full_suffix}"); + group.bench_function(bench_name, |bencher| { + if FULL { + // Include VM initialization / drop into the measured time + bencher.iter(|| BenchmarkingVm::::default().run_transaction(black_box(&tx))); + } else { + bencher.iter_batched( + BenchmarkingVm::::default, + |mut vm| { + let result = vm.run_transaction(black_box(&tx)); + (vm, result) + }, + BatchSize::LargeInput, // VM can consume significant amount of RAM, especially the new one + ); + } }); } } -criterion_group!(benches, benches_in_folder); +fn bench_load_test(c: &mut Criterion) { + let mut group = c.benchmark_group(VM::LABEL.as_str()); + group + .sample_size(SAMPLE_SIZE) + .measurement_time(Duration::from_secs(10)); + + // Nonce 0 is used for the deployment transaction + let tx = get_load_test_tx(1, 10_000_000, LoadTestParams::default()); + bench_load_test_transaction::(&mut group, "load_test", &tx); + + let tx = get_realistic_load_test_tx(1); + bench_load_test_transaction::(&mut group, "load_test_realistic", &tx); + + let tx = get_heavy_load_test_tx(1); + bench_load_test_transaction::(&mut group, "load_test_heavy", &tx); +} + +fn bench_load_test_transaction( + group: &mut BenchmarkGroup<'_, WallTime>, + name: &str, + tx: &Transaction, +) { + group.bench_function(name, |bencher| { + bencher.iter_batched( + || { + let mut vm = BenchmarkingVm::::default(); + vm.run_transaction(&get_load_test_deploy_tx()); + vm + }, + |mut vm| { + let result = vm.run_transaction(black_box(tx)); + assert!(!result.result.is_failed(), "{:?}", result.result); + (vm, result) + }, + BatchSize::LargeInput, + ); + }); +} + +criterion_group!( + benches, + benches_in_folder::, + 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/benches/fill_bootloader.rs b/core/tests/vm-benchmark/benches/fill_bootloader.rs index fac422c82375..a91149844312 100644 --- a/core/tests/vm-benchmark/benches/fill_bootloader.rs +++ b/core/tests/vm-benchmark/benches/fill_bootloader.rs @@ -1,23 +1,197 @@ -use std::time::Instant; +//! Benchmarks executing entire batches of transactions with varying size (from 1 to 5,000). +//! +//! - `fill_bootloader_full/*` benches emulate the entire transaction lifecycle including taking a snapshot +//! before a transaction and rolling back to it on halt. They also include VM initialization and drop. +//! In contrast, `fill_bootloader/*` benches only cover transaction execution. +//! - `deploy_simple_contract` benches deploy a simple contract in each transaction. All transactions succeed. +//! - `transfer` benches perform the base token transfer in each transaction. All transactions succeed. +//! - `transfer_with_invalid_nonce` benches are similar to `transfer`, but each transaction with a probability +//! `TX_FAILURE_PROBABILITY` has a previously used nonce and thus halts during validation. +//! - `load_test(|_realistic|_heavy)` execute the load test contract (a mixture of storage reads, writes, emitting events, +//! recursive calls, hashing and deploying new contracts). These 3 categories differ in how many operations of each kind +//! are performed in each transaction. Beware that the first executed transaction is load test contract deployment, +//! which skews results for small-size batches. -use criterion::black_box; +use std::{iter, time::Duration}; + +use criterion::{ + black_box, criterion_group, criterion_main, measurement::WallTime, BatchSize, BenchmarkGroup, + BenchmarkId, Criterion, Throughput, +}; +use rand::{rngs::StdRng, Rng, SeedableRng}; +use zksync_types::Transaction; use zksync_vm_benchmark_harness::{ - cut_to_allowed_bytecode_size, get_deploy_tx_with_gas_limit, BenchmarkingVm, + cut_to_allowed_bytecode_size, get_deploy_tx_with_gas_limit, get_heavy_load_test_tx, + get_load_test_deploy_tx, get_load_test_tx, get_realistic_load_test_tx, get_transfer_tx, + BenchmarkingVm, BenchmarkingVmFactory, Fast, Lambda, Legacy, LoadTestParams, }; -fn main() { - let test_contract = - std::fs::read("deployment_benchmarks/event_spam").expect("failed to read file"); +/// Gas limit for deployment transactions. +const DEPLOY_GAS_LIMIT: u32 = 30_000_000; +/// Tested numbers of transactions in a batch. +const TXS_IN_BATCH: &[usize] = &[1, 10, 50, 100, 200, 500, 1_000, 2_000, 5_000]; + +/// RNG seed used e.g. to randomize failing transactions. +const RNG_SEED: u64 = 123; +/// Probability for a transaction to fail in the `transfer_with_invalid_nonce` benchmarks. +const TX_FAILURE_PROBABILITY: f64 = 0.2; + +fn bench_vm( + vm: &mut BenchmarkingVm, + txs: &[Transaction], + expected_failures: &[bool], +) { + for (i, tx) in txs.iter().enumerate() { + let result = if FULL { + vm.run_transaction_full(black_box(tx)) + } else { + vm.run_transaction(black_box(tx)) + }; + let result = &result.result; + let expecting_failure = expected_failures.get(i).copied().unwrap_or(false); + assert_eq!( + result.is_failed(), + expecting_failure, + "{result:?} on tx #{i}" + ); + black_box(result); + } +} + +fn run_vm_expecting_failures( + group: &mut BenchmarkGroup<'_, WallTime>, + name: &str, + txs: &[Transaction], + expected_failures: &[bool], +) { + for txs_in_batch in TXS_IN_BATCH { + if *txs_in_batch > txs.len() { + break; + } + + group.throughput(Throughput::Elements(*txs_in_batch as u64)); + group.bench_with_input( + BenchmarkId::new(name, txs_in_batch), + txs_in_batch, + |bencher, &txs_in_batch| { + if FULL { + // Include VM initialization / drop into the measured time + bencher.iter(|| { + let mut vm = BenchmarkingVm::::default(); + bench_vm::<_, true>(&mut vm, &txs[..txs_in_batch], expected_failures); + }); + } else { + bencher.iter_batched( + BenchmarkingVm::::default, + |mut vm| { + bench_vm::<_, false>(&mut vm, &txs[..txs_in_batch], expected_failures); + vm + }, + BatchSize::LargeInput, // VM can consume significant amount of RAM, especially the new one + ); + } + }, + ); + } +} +fn run_vm( + group: &mut BenchmarkGroup<'_, WallTime>, + name: &str, + txs: &[Transaction], +) { + run_vm_expecting_failures::(group, name, txs, &[]); +} + +fn bench_fill_bootloader(c: &mut Criterion) { + let is_test_mode = !std::env::args().any(|arg| arg == "--bench"); + let txs_in_batch = if is_test_mode { + &TXS_IN_BATCH[..3] // Reduce the number of transactions in a batch so that tests don't take long + } else { + TXS_IN_BATCH + }; + + let mut group = c.benchmark_group(if FULL { + format!("fill_bootloader_full{}", VM::LABEL.as_suffix()) + } else { + format!("fill_bootloader{}", VM::LABEL.as_suffix()) + }); + group + .sample_size(10) + .measurement_time(Duration::from_secs(10)); + + // Deploying simple contract + let test_contract = + std::fs::read("deployment_benchmarks/deploy_simple_contract").expect("failed to read file"); let code = cut_to_allowed_bytecode_size(&test_contract).unwrap(); - let tx = get_deploy_tx_with_gas_limit(code, 1000); + let max_txs = *txs_in_batch.last().unwrap() as u32; + let txs: Vec<_> = (0..max_txs) + .map(|nonce| get_deploy_tx_with_gas_limit(code, DEPLOY_GAS_LIMIT, nonce)) + .collect(); + run_vm::(&mut group, "deploy_simple_contract", &txs); + drop(txs); + + // Load test with various parameters + let txs = + (1..=max_txs).map(|nonce| get_load_test_tx(nonce, 10_000_000, LoadTestParams::default())); + let txs: Vec<_> = iter::once(get_load_test_deploy_tx()).chain(txs).collect(); + run_vm::(&mut group, "load_test", &txs); + drop(txs); - let start = Instant::now(); + let txs = (1..=max_txs).map(get_realistic_load_test_tx); + let txs: Vec<_> = iter::once(get_load_test_deploy_tx()).chain(txs).collect(); + run_vm::(&mut group, "load_test_realistic", &txs); + drop(txs); - let mut vm = BenchmarkingVm::new(); - for _ in 0..1000 { - vm.run_transaction(black_box(&tx)); + let txs = (1..=max_txs).map(get_heavy_load_test_tx); + let txs: Vec<_> = iter::once(get_load_test_deploy_tx()).chain(txs).collect(); + run_vm::(&mut group, "load_test_heavy", &txs); + drop(txs); + + // Base token transfers + let txs: Vec<_> = (0..max_txs).map(get_transfer_tx).collect(); + run_vm::(&mut group, "transfer", &txs); + + // Halted transactions produced by the following benchmarks *must* be rolled back, + // otherwise the bootloader will process following transactions incorrectly. + if !FULL { + return; } - println!("{:?}", start.elapsed()); + let mut rng = StdRng::seed_from_u64(RNG_SEED); + + let mut txs_with_failures = Vec::with_capacity(txs.len()); + let mut expected_failures = Vec::with_capacity(txs.len()); + txs_with_failures.push(txs[0].clone()); + expected_failures.push(false); + let mut successful_txs = &txs[1..]; + for _ in 1..txs.len() { + let (tx, should_fail) = if rng.gen_bool(TX_FAILURE_PROBABILITY) { + // Since we add the transaction with nonce 0 unconditionally as the first tx to execute, + // all transactions generated here should halt during validation. + (get_transfer_tx(0), true) + } else { + let (tx, remaining_txs) = successful_txs.split_first().unwrap(); + successful_txs = remaining_txs; + (tx.clone(), false) + }; + txs_with_failures.push(tx); + expected_failures.push(should_fail); + } + run_vm_expecting_failures::( + &mut group, + "transfer_with_invalid_nonce", + &txs_with_failures, + &expected_failures, + ); } + +criterion_group!( + benches, + bench_fill_bootloader::, + bench_fill_bootloader::, + bench_fill_bootloader::, + bench_fill_bootloader::, + bench_fill_bootloader:: +); +criterion_main!(benches); diff --git a/core/tests/vm-benchmark/benches/iai.rs b/core/tests/vm-benchmark/benches/iai.rs index f0ba43f26853..7cca1cac8ea1 100644 --- a/core/tests/vm-benchmark/benches/iai.rs +++ b/core/tests/vm-benchmark/benches/iai.rs @@ -1,33 +1,44 @@ use iai::black_box; -use zksync_vm_benchmark_harness::{cut_to_allowed_bytecode_size, get_deploy_tx, BenchmarkingVm}; +use zksync_vm_benchmark_harness::{ + cut_to_allowed_bytecode_size, get_deploy_tx, BenchmarkingVm, BenchmarkingVmFactory, Fast, + Lambda, Legacy, +}; -fn run_bytecode(path: &str) { +fn run_bytecode(path: &str) { let test_contract = std::fs::read(path).expect("failed to read file"); let code = cut_to_allowed_bytecode_size(&test_contract).unwrap(); let tx = get_deploy_tx(code); - black_box(BenchmarkingVm::new().run_transaction(&tx)); + black_box(BenchmarkingVm::::default().run_transaction(&tx)); } macro_rules! make_functions_and_main { - ($($file:ident,)+) => { + ($($file:ident => $legacy_name:ident $lambda_name:ident,)+) => { $( - fn $file() { - run_bytecode(concat!("deployment_benchmarks/", stringify!($file))) - } + fn $file() { + run_bytecode::(concat!("deployment_benchmarks/", stringify!($file))); + } + + fn $legacy_name() { + run_bytecode::(concat!("deployment_benchmarks/", stringify!($file))); + } + + fn $lambda_name() { + run_bytecode::(concat!("deployment_benchmarks/", stringify!($file))); + } )+ - iai::main!($($file,)+); + iai::main!($($file, $legacy_name, $lambda_name,)+); }; } make_functions_and_main!( - access_memory, - call_far, - decode_shl_sub, - deploy_simple_contract, - finish_eventful_frames, - write_and_decode, - event_spam, - slot_hash_collision, + access_memory => access_memory_legacy access_memory_lambda, + call_far => call_far_legacy call_far_lambda, + decode_shl_sub => decode_shl_sub_legacy decode_shl_sub_lambda, + deploy_simple_contract => deploy_simple_contract_legacy deploy_simple_contract_lambda, + finish_eventful_frames => finish_eventful_frames_legacy finish_eventful_frames_lambda, + write_and_decode => write_and_decode_legacy write_and_decode_lambda, + event_spam => event_spam_legacy event_spam_lambda, + slot_hash_collision => slot_hash_collision_legacy slot_hash_collision_lambda, ); diff --git a/core/tests/vm-benchmark/harness/Cargo.toml b/core/tests/vm-benchmark/harness/Cargo.toml index acd5f37cbc7b..a24d3fa1294a 100644 --- a/core/tests/vm-benchmark/harness/Cargo.toml +++ b/core/tests/vm-benchmark/harness/Cargo.toml @@ -14,3 +14,6 @@ zksync_system_constants.workspace = true zksync_contracts.workspace = true zk_evm.workspace = true once_cell.workspace = true + +[dev-dependencies] +assert_matches.workspace = true diff --git a/core/tests/vm-benchmark/harness/src/instruction_counter.rs b/core/tests/vm-benchmark/harness/src/instruction_counter.rs index 017b13da44ca..c08ff74837a2 100644 --- a/core/tests/vm-benchmark/harness/src/instruction_counter.rs +++ b/core/tests/vm-benchmark/harness/src/instruction_counter.rs @@ -13,7 +13,7 @@ pub struct InstructionCounter { /// A tracer that counts the number of instructions executed by the VM. impl InstructionCounter { - #[allow(dead_code)] // FIXME + #[allow(dead_code)] // FIXME: re-enable instruction counting once new tracers are merged pub fn new(output: Rc>) -> Self { Self { count: 0, output } } diff --git a/core/tests/vm-benchmark/harness/src/lib.rs b/core/tests/vm-benchmark/harness/src/lib.rs index a30221cfa0be..988db5b7745f 100644 --- a/core/tests/vm-benchmark/harness/src/lib.rs +++ b/core/tests/vm-benchmark/harness/src/lib.rs @@ -1,16 +1,19 @@ use std::{cell::RefCell, rc::Rc}; use once_cell::sync::Lazy; -use zksync_contracts::{deployer_contract, BaseSystemContracts}; +pub use zksync_contracts::test_contracts::LoadnextContractExecutionParams as LoadTestParams; +use zksync_contracts::{deployer_contract, BaseSystemContracts, TestContract}; use zksync_multivm::{ + era_vm, interface::{ - L2BlockEnv, TxExecutionMode, VmExecutionMode, VmExecutionResultAndLogs, VmInterface, + ExecutionResult, L1BatchEnv, L2BlockEnv, SystemEnv, TxExecutionMode, VmExecutionMode, + VmExecutionResultAndLogs, VmFactory, VmInterface, VmInterfaceHistoryEnabled, }, utils::get_max_gas_per_pubdata_byte, - vm_fast::Vm, - vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT, + vm_fast, vm_latest, + vm_latest::{constants::BATCH_COMPUTATIONAL_GAS_LIMIT, HistoryEnabled}, }; -use zksync_state::InMemoryStorage; +use zksync_state::{InMemoryStorage, StorageView}; use zksync_types::{ block::L2BlockHasher, ethabi::{encode, Token}, @@ -18,7 +21,7 @@ use zksync_types::{ fee_model::BatchFeeInput, helpers::unix_timestamp_ms, l2::L2Tx, - utils::storage_key_for_eth_balance, + utils::{deployed_address_create, storage_key_for_eth_balance}, Address, K256PrivateKey, L1BatchNumber, L2BlockNumber, L2ChainId, Nonce, ProtocolVersionId, Transaction, CONTRACT_DEPLOYER_ADDRESS, H256, U256, }; @@ -40,18 +43,24 @@ pub fn cut_to_allowed_bytecode_size(bytes: &[u8]) -> Option<&[u8]> { Some(&bytes[..32 * words]) } +const LOAD_TEST_MAX_READS: usize = 100; + +static LOAD_TEST_CONTRACT_ADDRESS: Lazy
= + Lazy::new(|| deployed_address_create(PRIVATE_KEY.address(), 0.into())); + static STORAGE: Lazy = Lazy::new(|| { let mut storage = InMemoryStorage::with_system_contracts(hash_bytecode); - // Give `PRIVATE_KEY` some money + let balance = U256::from(10u32).pow(U256::from(32)); //10^32 wei let key = storage_key_for_eth_balance(&PRIVATE_KEY.address()); - storage.set_value(key, zksync_utils::u256_to_h256(U256([0, 0, 1, 0]))); - + storage.set_value(key, zksync_utils::u256_to_h256(balance)); storage }); static SYSTEM_CONTRACTS: Lazy = Lazy::new(BaseSystemContracts::load_from_disk); +static LOAD_TEST_CONTRACT: Lazy = Lazy::new(zksync_contracts::get_loadnext_contract); + static CREATE_FUNCTION_SIGNATURE: Lazy<[u8; 4]> = Lazy::new(|| { deployer_contract() .function("create") @@ -62,15 +71,114 @@ static CREATE_FUNCTION_SIGNATURE: Lazy<[u8; 4]> = Lazy::new(|| { static PRIVATE_KEY: Lazy = Lazy::new(|| K256PrivateKey::from_bytes(H256([42; 32])).expect("invalid key bytes")); -pub struct BenchmarkingVm(Vm<&'static InMemoryStorage>); +/// VM label used to name `criterion` benchmarks. +#[derive(Debug, Clone, Copy)] +pub enum VmLabel { + Fast, + Lambda, + Legacy, +} -impl BenchmarkingVm { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - let timestamp = unix_timestamp_ms(); +impl VmLabel { + /// Non-empty name for `criterion` benchmark naming. + pub const fn as_str(self) -> &'static str { + match self { + Self::Fast => "fast", + Self::Lambda => "lambda", + Self::Legacy => "legacy", + } + } + + /// Optional prefix for `criterion` benchmark naming (including a starting `/`). + pub const fn as_suffix(self) -> &'static str { + match self { + Self::Fast => "", + Self::Lambda => "/lambda", + Self::Legacy => "/legacy", + } + } +} + +/// Factory for VMs used in benchmarking. +pub trait BenchmarkingVmFactory { + /// VM label used to name `criterion` benchmarks. + const LABEL: VmLabel; + + /// Type of the VM instance created by this factory. + type Instance: VmInterfaceHistoryEnabled; + + /// Creates a VM instance. + fn create( + batch_env: L1BatchEnv, + system_env: SystemEnv, + storage: &'static InMemoryStorage, + ) -> Self::Instance; +} + +/// Factory for the LambdaClass VM. +#[derive(Debug)] +pub struct Lambda(()); + +impl BenchmarkingVmFactory for Lambda { + const LABEL: VmLabel = VmLabel::Lambda; + + type Instance = era_vm::vm::Vm; + + fn create( + batch_env: L1BatchEnv, + system_env: SystemEnv, + storage: &'static InMemoryStorage, + ) -> Self::Instance { + let storage = Rc::new(RefCell::new(storage.clone())); + era_vm::vm::Vm::new(batch_env, system_env, storage) + } +} + +/// Factory for the new / fast VM. +#[derive(Debug)] +pub struct Fast(()); + +impl BenchmarkingVmFactory for Fast { + const LABEL: VmLabel = VmLabel::Fast; + + type Instance = vm_fast::Vm<&'static InMemoryStorage>; + + fn create( + batch_env: L1BatchEnv, + system_env: SystemEnv, + storage: &'static InMemoryStorage, + ) -> Self::Instance { + vm_fast::Vm::new(batch_env, system_env, storage) + } +} + +/// Factory for the legacy VM (latest version). +#[derive(Debug)] +pub struct Legacy; + +impl BenchmarkingVmFactory for Legacy { + const LABEL: VmLabel = VmLabel::Legacy; + + type Instance = vm_latest::Vm, HistoryEnabled>; + + fn create( + batch_env: L1BatchEnv, + system_env: SystemEnv, + storage: &'static InMemoryStorage, + ) -> Self::Instance { + let storage = StorageView::new(storage).to_rc_ptr(); + vm_latest::Vm::new(batch_env, system_env, storage) + } +} - Self(Vm::new( - zksync_multivm::interface::L1BatchEnv { +#[derive(Debug)] +pub struct BenchmarkingVm(VM::Instance); + +impl Default for BenchmarkingVm { + fn default() -> Self { + let timestamp = unix_timestamp_ms(); + Self(VM::create( + L1BatchEnv { previous_batch_hash: None, number: L1BatchNumber(1), timestamp, @@ -87,7 +195,7 @@ impl BenchmarkingVm { max_virtual_blocks_to_create: 100, }, }, - zksync_multivm::interface::SystemEnv { + SystemEnv { zk_porter_available: false, version: ProtocolVersionId::latest(), base_system_smart_contracts: SYSTEM_CONTRACTS.clone(), @@ -96,33 +204,63 @@ impl BenchmarkingVm { default_validation_computational_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, chain_id: L2ChainId::from(270), }, - &*STORAGE, + &STORAGE, )) } +} +impl BenchmarkingVm { pub fn run_transaction(&mut self, tx: &Transaction) -> VmExecutionResultAndLogs { self.0.push_transaction(tx.clone()); self.0.execute(VmExecutionMode::OneTx) } + pub fn run_transaction_full(&mut self, tx: &Transaction) -> VmExecutionResultAndLogs { + self.0.make_snapshot(); + let (compression_result, tx_result) = self.0.inspect_transaction_with_bytecode_compression( + Default::default(), + tx.clone(), + true, + ); + compression_result.expect("compressing bytecodes failed"); + + if matches!(tx_result.result, ExecutionResult::Halt { .. }) { + self.0.rollback_to_the_latest_snapshot(); + } else { + self.0.pop_snapshot_no_rollback(); + } + tx_result + } + pub fn instruction_count(&mut self, tx: &Transaction) -> usize { self.0.push_transaction(tx.clone()); - let count = Rc::new(RefCell::new(0)); + self.0.inspect(Default::default(), VmExecutionMode::OneTx); // FIXME: re-enable instruction counting once new tracers are merged + count.take() + } +} - self.0.inspect((), VmExecutionMode::OneTx); +impl BenchmarkingVm { + pub fn new() -> Self { + Self::default() + } +} - count.take() +impl BenchmarkingVm { + pub fn legacy() -> Self { + Self::default() } } pub fn get_deploy_tx(code: &[u8]) -> Transaction { - get_deploy_tx_with_gas_limit(code, 30_000_000) + get_deploy_tx_with_gas_limit(code, 30_000_000, 0) } -pub fn get_deploy_tx_with_gas_limit(code: &[u8], gas_limit: u32) -> Transaction { +pub fn get_deploy_tx_with_gas_limit(code: &[u8], gas_limit: u32, nonce: u32) -> Transaction { + let mut salt = vec![0_u8; 32]; + salt[28..32].copy_from_slice(&nonce.to_be_bytes()); let params = [ - Token::FixedBytes(vec![0u8; 32]), + Token::FixedBytes(salt), Token::FixedBytes(hash_bytecode(code).0.to_vec()), Token::Bytes([].to_vec()), ]; @@ -135,15 +273,8 @@ pub fn get_deploy_tx_with_gas_limit(code: &[u8], gas_limit: u32) -> Transaction let mut signed = L2Tx::new_signed( CONTRACT_DEPLOYER_ADDRESS, calldata, - Nonce(0), - Fee { - gas_limit: U256::from(gas_limit), - max_fee_per_gas: U256::from(250_000_000), - max_priority_fee_per_gas: U256::from(0), - gas_per_pubdata_limit: U256::from(get_max_gas_per_pubdata_byte( - ProtocolVersionId::latest().into(), - )), - }, + Nonce(nonce), + tx_fee(gas_limit), U256::zero(), L2ChainId::from(270), &PRIVATE_KEY, @@ -153,13 +284,144 @@ pub fn get_deploy_tx_with_gas_limit(code: &[u8], gas_limit: u32) -> Transaction .expect("should create a signed execute transaction"); signed.set_input(H256::random().as_bytes().to_vec(), H256::random()); + signed.into() +} + +fn tx_fee(gas_limit: u32) -> Fee { + Fee { + gas_limit: U256::from(gas_limit), + max_fee_per_gas: U256::from(250_000_000), + max_priority_fee_per_gas: U256::from(0), + gas_per_pubdata_limit: U256::from(get_max_gas_per_pubdata_byte( + ProtocolVersionId::latest().into(), + )), + } +} +pub fn get_transfer_tx(nonce: u32) -> Transaction { + let mut signed = L2Tx::new_signed( + PRIVATE_KEY.address(), + vec![], // calldata + Nonce(nonce), + tx_fee(1_000_000), + 1_000_000_000.into(), // value + L2ChainId::from(270), + &PRIVATE_KEY, + vec![], // factory deps + Default::default(), // paymaster params + ) + .expect("should create a signed execute transaction"); + + signed.set_input(H256::random().as_bytes().to_vec(), H256::random()); signed.into() } +pub fn get_load_test_deploy_tx() -> Transaction { + let calldata = [Token::Uint(LOAD_TEST_MAX_READS.into())]; + let params = [ + Token::FixedBytes(vec![0_u8; 32]), + Token::FixedBytes(hash_bytecode(&LOAD_TEST_CONTRACT.bytecode).0.to_vec()), + Token::Bytes(encode(&calldata)), + ]; + let create_calldata = CREATE_FUNCTION_SIGNATURE + .iter() + .cloned() + .chain(encode(¶ms)) + .collect(); + + let mut factory_deps = LOAD_TEST_CONTRACT.factory_deps.clone(); + factory_deps.push(LOAD_TEST_CONTRACT.bytecode.clone()); + + let mut signed = L2Tx::new_signed( + CONTRACT_DEPLOYER_ADDRESS, + create_calldata, + Nonce(0), + tx_fee(100_000_000), + U256::zero(), + L2ChainId::from(270), + &PRIVATE_KEY, + factory_deps, + Default::default(), + ) + .expect("should create a signed execute transaction"); + + signed.set_input(H256::random().as_bytes().to_vec(), H256::random()); + signed.into() +} + +pub fn get_load_test_tx(nonce: u32, gas_limit: u32, params: LoadTestParams) -> Transaction { + assert!( + params.reads <= LOAD_TEST_MAX_READS, + "Too many reads: {params:?}, should be <={LOAD_TEST_MAX_READS}" + ); + + let execute_function = LOAD_TEST_CONTRACT + .contract + .function("execute") + .expect("no `execute` function in load test contract"); + let calldata = execute_function + .encode_input(&vec![ + Token::Uint(U256::from(params.reads)), + Token::Uint(U256::from(params.writes)), + Token::Uint(U256::from(params.hashes)), + Token::Uint(U256::from(params.events)), + Token::Uint(U256::from(params.recursive_calls)), + Token::Uint(U256::from(params.deploys)), + ]) + .expect("cannot encode `execute` inputs"); + + let mut signed = L2Tx::new_signed( + *LOAD_TEST_CONTRACT_ADDRESS, + calldata, + Nonce(nonce), + tx_fee(gas_limit), + U256::zero(), + L2ChainId::from(270), + &PRIVATE_KEY, + LOAD_TEST_CONTRACT.factory_deps.clone(), + Default::default(), + ) + .expect("should create a signed execute transaction"); + + signed.set_input(H256::random().as_bytes().to_vec(), H256::random()); + signed.into() +} + +pub fn get_realistic_load_test_tx(nonce: u32) -> Transaction { + get_load_test_tx( + nonce, + 10_000_000, + LoadTestParams { + reads: 30, + writes: 2, + events: 5, + hashes: 10, + recursive_calls: 0, + deploys: 0, + }, + ) +} + +pub fn get_heavy_load_test_tx(nonce: u32) -> Transaction { + get_load_test_tx( + nonce, + 10_000_000, + LoadTestParams { + reads: 100, + writes: 5, + events: 20, + hashes: 100, + recursive_calls: 20, + deploys: 5, + }, + ) +} + #[cfg(test)] mod tests { + use assert_matches::assert_matches; use zksync_contracts::read_bytecode; + use zksync_multivm::interface::ExecutionResult; use crate::*; @@ -171,9 +433,44 @@ mod tests { let mut vm = BenchmarkingVm::new(); let res = vm.run_transaction(&get_deploy_tx(&test_contract)); - assert!(matches!( - res.result, - zksync_multivm::interface::ExecutionResult::Success { .. } - )); + assert_matches!(res.result, ExecutionResult::Success { .. }); + } + + #[test] + fn can_transfer() { + let mut vm = BenchmarkingVm::new(); + let res = vm.run_transaction(&get_transfer_tx(0)); + assert_matches!(res.result, ExecutionResult::Success { .. }); + } + + #[test] + fn can_load_test() { + let mut vm = BenchmarkingVm::new(); + let res = vm.run_transaction(&get_load_test_deploy_tx()); + assert_matches!(res.result, ExecutionResult::Success { .. }); + + let params = LoadTestParams::default(); + let res = vm.run_transaction(&get_load_test_tx(1, 10_000_000, params)); + assert_matches!(res.result, ExecutionResult::Success { .. }); + } + + #[test] + fn can_load_test_with_realistic_txs() { + let mut vm = BenchmarkingVm::new(); + let res = vm.run_transaction(&get_load_test_deploy_tx()); + assert_matches!(res.result, ExecutionResult::Success { .. }); + + let res = vm.run_transaction(&get_realistic_load_test_tx(1)); + assert_matches!(res.result, ExecutionResult::Success { .. }); + } + + #[test] + fn can_load_test_with_heavy_txs() { + let mut vm = BenchmarkingVm::new(); + let res = vm.run_transaction(&get_load_test_deploy_tx()); + assert_matches!(res.result, ExecutionResult::Success { .. }); + + let res = vm.run_transaction(&get_heavy_load_test_tx(1)); + assert_matches!(res.result, ExecutionResult::Success { .. }); } } diff --git a/core/tests/vm-benchmark/src/instruction_counts.rs b/core/tests/vm-benchmark/src/instruction_counts.rs index c038c8f2bf6b..12d780bd9ffd 100644 --- a/core/tests/vm-benchmark/src/instruction_counts.rs +++ b/core/tests/vm-benchmark/src/instruction_counts.rs @@ -23,6 +23,6 @@ fn main() { let name = path.file_name().unwrap().to_str().unwrap(); - println!("{} {}", name, BenchmarkingVm::new().instruction_count(&tx)); + // println!("{} {}", name, BenchmarkingVm::new().instruction_count(&tx)); } } diff --git a/etc/env/base/vm_runner.toml b/etc/env/base/vm_runner.toml index 8e6171d79366..53016ed548c4 100644 --- a/etc/env/base/vm_runner.toml +++ b/etc/env/base/vm_runner.toml @@ -18,10 +18,10 @@ first_processed_batch = 0 [experimental_vm] # Mode in which to run the new fast VM in the state keeper. Don't set to "new" / "shadow" in production yet! -state_keeper_fast_vm_mode = "old" # default value +state_keeper_fast_vm_mode = "new" # default value [experimental_vm.playground] # Path to the directory that contains RocksDB with protective reads writer cache. db_path = "./db/main/vm_playground" # Mode in which to run the new fast VM -fast_vm_mode = "shadow" +fast_vm_mode = "new" diff --git a/prover/Cargo.lock b/prover/Cargo.lock index e6ef7fd95f83..27893ff4d005 100644 --- a/prover/Cargo.lock +++ b/prover/Cargo.lock @@ -1919,6 +1919,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "era_vm" +version = "0.1.0" +source = "git+https://github.com/lambdaclass/era_vm.git#d3f6a983870b3807b65e33e1269569f7d38b4634" +dependencies = [ + "hex", + "lazy_static", + "primitive-types", + "rocksdb", + "thiserror", + "zk_evm_abstractions 1.5.1", + "zkevm_opcode_defs 1.5.0", +] + [[package]] name = "errno" version = "0.3.9" @@ -6254,18 +6268,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2 1.0.85", "quote 1.0.36", @@ -7480,6 +7494,18 @@ dependencies = [ "zkevm_opcode_defs 0.150.0", ] +[[package]] +name = "zk_evm_abstractions" +version = "1.5.1" +source = "git+https://github.com/matter-labs/era-zk_evm_abstractions.git?branch=v1.5.1#eecb28df7d6dbcb728d4e9106933d288e4cb267e" +dependencies = [ + "anyhow", + "num_enum 0.6.1", + "serde", + "static_assertions", + "zkevm_opcode_defs 1.5.0", +] + [[package]] name = "zkevm-assembly" version = "0.132.0" @@ -7643,6 +7669,22 @@ dependencies = [ "sha3 0.10.8", ] +[[package]] +name = "zkevm_opcode_defs" +version = "1.5.0" +source = "git+https://github.com/matter-labs/era-zkevm_opcode_defs.git?branch=v1.5.1#9c470e3dbb093c4878b04b61e4d9459d94b41d45" +dependencies = [ + "bitflags 2.6.0", + "blake2 0.10.6", + "ethereum-types", + "k256 0.13.3", + "lazy_static", + "p256", + "serde", + "sha2 0.10.8", + "sha3 0.10.8", +] + [[package]] name = "zkevm_test_harness" version = "0.140.1-gpu-wrapper.1" @@ -8069,6 +8111,7 @@ dependencies = [ "circuit_sequencer_api 0.141.1", "circuit_sequencer_api 0.142.0", "circuit_sequencer_api 0.150.2-rc.3", + "era_vm", "hex", "itertools 0.10.5", "once_cell",