diff --git a/Cargo.lock b/Cargo.lock index 45d34bfcd8f..e5c438cb51e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3827,6 +3827,7 @@ dependencies = [ "vm_1_3_2", "vm_m5", "vm_m6", + "vm_virtual_blocks", "zksync_contracts", "zksync_state", "zksync_types", @@ -7032,6 +7033,29 @@ dependencies = [ "zksync_utils", ] +[[package]] +name = "vm_virtual_blocks" +version = "0.1.0" +dependencies = [ + "anyhow", + "ethabi", + "hex", + "itertools", + "once_cell", + "thiserror", + "tokio", + "tracing", + "vise", + "zk_evm 1.3.3", + "zksync_config", + "zksync_contracts", + "zksync_eth_signer", + "zksync_state", + "zksync_test_account", + "zksync_types", + "zksync_utils", +] + [[package]] name = "walkdir" version = "2.3.3" diff --git a/core/lib/multivm/Cargo.toml b/core/lib/multivm/Cargo.toml index 4abef1cc50b..b2dd8396f15 100644 --- a/core/lib/multivm/Cargo.toml +++ b/core/lib/multivm/Cargo.toml @@ -13,6 +13,7 @@ categories = ["cryptography"] vm_m5 = { path = "../../multivm_deps/vm_m5" } vm_m6 = { path = "../../multivm_deps/vm_m6" } vm_1_3_2 = { path = "../../multivm_deps/vm_1_3_2" } +vm_virtual_blocks= { path = "../../multivm_deps/vm_virtual_blocks" } vm_latest = { path = "../vm", package = "vm" } zksync_types = { path = "../types" } diff --git a/core/lib/multivm/src/glue/block_properties.rs b/core/lib/multivm/src/glue/block_properties.rs index e37dbfd895b..6dea6dddb1c 100644 --- a/core/lib/multivm/src/glue/block_properties.rs +++ b/core/lib/multivm/src/glue/block_properties.rs @@ -33,9 +33,9 @@ impl BlockProperties { }; Self::Vm1_3_2(inner) } - VmVersion::VmVirtualBlocks => { + VmVersion::VmVirtualBlocks | VmVersion::VmVirtualBlocksRefundsEnhancement => { unreachable!( - "Vm with virtual blocks has another initialization logic, \ + "From VmVirtualBlocks we have another initialization logic, \ so it's not required to have BlockProperties for it" ) } diff --git a/core/lib/multivm/src/glue/history_mode.rs b/core/lib/multivm/src/glue/history_mode.rs index 2055e963ffe..b40eec4f089 100644 --- a/core/lib/multivm/src/glue/history_mode.rs +++ b/core/lib/multivm/src/glue/history_mode.rs @@ -5,10 +5,12 @@ pub trait HistoryMode: + GlueInto + GlueInto + GlueInto + + GlueInto { type VmM6Mode: vm_m6::HistoryMode; type Vm1_3_2Mode: vm_1_3_2::HistoryMode; - type VmVirtualBlocksMode: vm_latest::HistoryMode; + type VmVirtualBlocksMode: vm_virtual_blocks::HistoryMode; + type VmVirtualBlocksRefundsEnhancement: vm_latest::HistoryMode; } impl GlueFrom for vm_m6::HistoryEnabled { @@ -23,6 +25,12 @@ impl GlueFrom for vm_1_3_2::HistoryEnabled { } } +impl GlueFrom for vm_virtual_blocks::HistoryEnabled { + fn glue_from(_: vm_latest::HistoryEnabled) -> Self { + Self + } +} + impl GlueFrom for vm_m6::HistoryDisabled { fn glue_from(_: vm_latest::HistoryDisabled) -> Self { Self @@ -35,14 +43,22 @@ impl GlueFrom for vm_1_3_2::HistoryDisabled { } } +impl GlueFrom for vm_virtual_blocks::HistoryDisabled { + fn glue_from(_: vm_latest::HistoryDisabled) -> Self { + Self + } +} + impl HistoryMode for vm_latest::HistoryEnabled { type VmM6Mode = vm_m6::HistoryEnabled; type Vm1_3_2Mode = vm_1_3_2::HistoryEnabled; - type VmVirtualBlocksMode = vm_latest::HistoryEnabled; + type VmVirtualBlocksMode = vm_virtual_blocks::HistoryEnabled; + type VmVirtualBlocksRefundsEnhancement = vm_latest::HistoryEnabled; } impl HistoryMode for vm_latest::HistoryDisabled { type VmM6Mode = vm_m6::HistoryDisabled; type Vm1_3_2Mode = vm_1_3_2::HistoryDisabled; - type VmVirtualBlocksMode = vm_latest::HistoryDisabled; + type VmVirtualBlocksMode = vm_virtual_blocks::HistoryDisabled; + type VmVirtualBlocksRefundsEnhancement = vm_latest::HistoryDisabled; } diff --git a/core/lib/multivm/src/glue/init_vm.rs b/core/lib/multivm/src/glue/init_vm.rs index a2f25139403..bd75c0fbec8 100644 --- a/core/lib/multivm/src/glue/init_vm.rs +++ b/core/lib/multivm/src/glue/init_vm.rs @@ -69,9 +69,9 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { } } VmInstanceData::VmVirtualBlocks(data) => { - let vm = vm_latest::Vm::new( + let vm = vm_virtual_blocks::Vm::new( l1_batch_env.glue_into(), - system_env.clone(), + system_env.clone().glue_into(), data.storage_view.clone(), H::VmVirtualBlocksMode::default(), ); @@ -82,6 +82,20 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { last_tx_compressed_bytecodes: vec![], } } + VmInstanceData::VmVirtualBlocksRefundsEnhancement(data) => { + let vm = vm_latest::Vm::new( + l1_batch_env.glue_into(), + system_env.clone(), + data.storage_view.clone(), + H::VmVirtualBlocksRefundsEnhancement::default(), + ); + let vm = VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(Box::new(vm)); + Self { + vm, + system_env, + last_tx_compressed_bytecodes: vec![], + } + } } } } diff --git a/core/lib/multivm/src/glue/oracle_tools.rs b/core/lib/multivm/src/glue/oracle_tools.rs index b13c15af168..1cd46d17692 100644 --- a/core/lib/multivm/src/glue/oracle_tools.rs +++ b/core/lib/multivm/src/glue/oracle_tools.rs @@ -27,11 +27,10 @@ where let oracle_tools = vm_m6::OracleTools::new(state, history.glue_into()); OracleTools::M6(oracle_tools) } - VmVersion::Vm1_3_2 => { - panic!("oracle tools for vm1.3.2 do not exist") - } - VmVersion::VmVirtualBlocks => { - panic!("oracle tools for VmVirtualBlocks do not exist") + VmVersion::VmVirtualBlocks + | VmVersion::VmVirtualBlocksRefundsEnhancement + | VmVersion::Vm1_3_2 => { + panic!("oracle tools for after VM1.3.2 do not exist") } } } diff --git a/core/lib/multivm/src/glue/tracer.rs b/core/lib/multivm/src/glue/tracer.rs index d80a1585084..78d991b29a4 100644 --- a/core/lib/multivm/src/glue/tracer.rs +++ b/core/lib/multivm/src/glue/tracer.rs @@ -15,6 +15,10 @@ //! into a form compatible with the latest VM version. //! It defines a method `latest` for obtaining a boxed tracer. //! +//! - `IntoVmVirtualBlocksTracer`:This trait is responsible for converting a tracer +//! into a form compatible with the vm_virtual_blocks version. +//! It defines a method `vm_virtual_blocks` for obtaining a boxed tracer. +//! //! For `MultivmTracer` to be implemented, Tracer must implement all N currently //! existing sub-traits. //! @@ -30,11 +34,15 @@ //! - Create a new trait performing conversion to the specified VM tracer, e.g. `IntoTracer`. //! - Provide implementations of this trait for all the structures that currently implement `MultivmTracer`. //! - Add this trait as a trait bound to the `MultivmTracer`. +//! - Add this trait as a trait bound for `T` in `MultivmTracer` implementation. //! - Integrate the newly added method to the MultiVM itself (e.g. add required tracer conversions where applicable). +mod implementations; + +use crate::HistoryMode; use zksync_state::WriteStorage; -pub trait MultivmTracer: - IntoLatestTracer +pub trait MultivmTracer: + IntoLatestTracer + IntoVmVirtualBlocksTracer { fn into_boxed(self) -> Box> where @@ -44,17 +52,21 @@ pub trait MultivmTracer: } } -pub trait IntoLatestTracer { - fn latest(&self) -> Box>; +pub trait IntoLatestTracer { + fn latest(&self) -> Box>; +} + +pub trait IntoVmVirtualBlocksTracer { + fn vm_virtual_blocks(&self) -> Box>; } -impl IntoLatestTracer for T +impl IntoLatestTracer for T where S: WriteStorage, - H: vm_latest::HistoryMode, - T: vm_latest::VmTracer + Clone + 'static, + H: HistoryMode, + T: vm_latest::VmTracer + Clone + 'static, { - fn latest(&self) -> Box> { + fn latest(&self) -> Box> { Box::new(self.clone()) } } @@ -62,7 +74,11 @@ where impl MultivmTracer for T where S: WriteStorage, - H: vm_latest::HistoryMode, - T: vm_latest::VmTracer + Clone + 'static, + H: HistoryMode, + T: vm_latest::VmTracer + + IntoLatestTracer + + IntoVmVirtualBlocksTracer + + Clone + + 'static, { } diff --git a/core/lib/multivm/src/glue/tracer/implementations.rs b/core/lib/multivm/src/glue/tracer/implementations.rs new file mode 100644 index 00000000000..d8f7056728b --- /dev/null +++ b/core/lib/multivm/src/glue/tracer/implementations.rs @@ -0,0 +1,48 @@ +use crate::glue::tracer::IntoVmVirtualBlocksTracer; +use vm_latest::{CallTracer, StorageInvocations, ValidationTracer}; +use zksync_state::WriteStorage; + +impl IntoVmVirtualBlocksTracer for StorageInvocations +where + H: crate::HistoryMode, + S: WriteStorage, +{ + fn vm_virtual_blocks(&self) -> Box> { + Box::new(vm_virtual_blocks::StorageInvocations::new(self.limit)) + } +} + +impl IntoVmVirtualBlocksTracer for CallTracer +where + H: crate::HistoryMode + 'static, + S: WriteStorage, +{ + fn vm_virtual_blocks(&self) -> Box> { + Box::new(vm_virtual_blocks::CallTracer::new( + self.result.clone(), + H::VmVirtualBlocksMode::default(), + )) + } +} + +impl IntoVmVirtualBlocksTracer + for ValidationTracer +where + H: crate::HistoryMode + 'static, + S: WriteStorage, +{ + fn vm_virtual_blocks(&self) -> Box> { + let params = self.params(); + Box::new(vm_virtual_blocks::ValidationTracer::new( + vm_virtual_blocks::ValidationTracerParams { + user_address: params.user_address, + paymaster_address: params.paymaster_address, + trusted_slots: params.trusted_slots, + trusted_addresses: params.trusted_addresses, + trusted_address_slots: params.trusted_address_slots, + computational_gas_limit: params.computational_gas_limit, + }, + self.result.clone(), + )) + } +} diff --git a/core/lib/multivm/src/glue/types/vm/bytecompression_result.rs b/core/lib/multivm/src/glue/types/vm/bytecompression_result.rs new file mode 100644 index 00000000000..53e65a36cdf --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/bytecompression_result.rs @@ -0,0 +1,33 @@ +use crate::glue::{GlueFrom, GlueInto}; +use vm_latest::{BytecodeCompressionError, VmExecutionResultAndLogs}; + +impl GlueFrom for BytecodeCompressionError { + fn glue_from(value: vm_virtual_blocks::BytecodeCompressionError) -> Self { + match value { + vm_virtual_blocks::BytecodeCompressionError::BytecodeCompressionFailed => { + Self::BytecodeCompressionFailed + } + } + } +} + +impl + GlueFrom< + Result< + vm_virtual_blocks::VmExecutionResultAndLogs, + vm_virtual_blocks::BytecodeCompressionError, + >, + > for Result +{ + fn glue_from( + value: Result< + vm_virtual_blocks::VmExecutionResultAndLogs, + vm_virtual_blocks::BytecodeCompressionError, + >, + ) -> Self { + match value { + Ok(result) => Ok(result.glue_into()), + Err(err) => Err(err.glue_into()), + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/current_execution_state.rs b/core/lib/multivm/src/glue/types/vm/current_execution_state.rs new file mode 100644 index 00000000000..41e77344da2 --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/current_execution_state.rs @@ -0,0 +1,15 @@ +use crate::glue::GlueFrom; +use vm_latest::CurrentExecutionState; + +impl GlueFrom for CurrentExecutionState { + fn glue_from(value: vm_virtual_blocks::CurrentExecutionState) -> Self { + Self { + events: value.events, + storage_log_queries: value.storage_log_queries, + used_contract_hashes: value.used_contract_hashes, + l2_to_l1_logs: value.l2_to_l1_logs, + total_log_queries: value.total_log_queries, + cycles_used: value.cycles_used, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/execution_result.rs b/core/lib/multivm/src/glue/types/vm/execution_result.rs new file mode 100644 index 00000000000..7dd4b361ffc --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/execution_result.rs @@ -0,0 +1,18 @@ +use crate::glue::{GlueFrom, GlueInto}; +use vm_latest::ExecutionResult; + +impl GlueFrom for ExecutionResult { + fn glue_from(value: vm_virtual_blocks::ExecutionResult) -> Self { + match value { + vm_virtual_blocks::ExecutionResult::Success { output } => { + ExecutionResult::Success { output } + } + vm_virtual_blocks::ExecutionResult::Revert { output } => ExecutionResult::Revert { + output: output.glue_into(), + }, + vm_virtual_blocks::ExecutionResult::Halt { reason } => ExecutionResult::Halt { + reason: reason.glue_into(), + }, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/halt.rs b/core/lib/multivm/src/glue/types/vm/halt.rs new file mode 100644 index 00000000000..d08f143f80e --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/halt.rs @@ -0,0 +1,44 @@ +use crate::glue::{GlueFrom, GlueInto}; +use vm_latest::Halt; + +impl GlueFrom for Halt { + fn glue_from(value: vm_virtual_blocks::Halt) -> Self { + match value { + vm_virtual_blocks::Halt::ValidationFailed(reason) => { + Self::ValidationFailed(reason.glue_into()) + } + vm_virtual_blocks::Halt::PaymasterValidationFailed(reason) => { + Self::PaymasterValidationFailed(reason.glue_into()) + } + vm_virtual_blocks::Halt::PrePaymasterPreparationFailed(reason) => { + Self::PrePaymasterPreparationFailed(reason.glue_into()) + } + vm_virtual_blocks::Halt::PayForTxFailed(reason) => { + Self::PayForTxFailed(reason.glue_into()) + } + vm_virtual_blocks::Halt::FailedToMarkFactoryDependencies(reason) => { + Self::FailedToMarkFactoryDependencies(reason.glue_into()) + } + vm_virtual_blocks::Halt::FailedToChargeFee(reason) => { + Self::FailedToChargeFee(reason.glue_into()) + } + vm_virtual_blocks::Halt::FromIsNotAnAccount => Self::FromIsNotAnAccount, + vm_virtual_blocks::Halt::InnerTxError => Self::InnerTxError, + vm_virtual_blocks::Halt::Unknown(reason) => Self::Unknown(reason.glue_into()), + vm_virtual_blocks::Halt::UnexpectedVMBehavior(reason) => { + Self::UnexpectedVMBehavior(reason) + } + vm_virtual_blocks::Halt::BootloaderOutOfGas => Self::BootloaderOutOfGas, + vm_virtual_blocks::Halt::TooBigGasLimit => Self::TooBigGasLimit, + vm_virtual_blocks::Halt::NotEnoughGasProvided => Self::NotEnoughGasProvided, + vm_virtual_blocks::Halt::MissingInvocationLimitReached => { + Self::MissingInvocationLimitReached + } + vm_virtual_blocks::Halt::FailedToSetL2Block(reason) => Self::FailedToSetL2Block(reason), + vm_virtual_blocks::Halt::FailedToAppendTransactionToL2Block(reason) => { + Self::FailedToAppendTransactionToL2Block(reason) + } + vm_virtual_blocks::Halt::VMPanic => Self::VMPanic, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/l1_batch.rs b/core/lib/multivm/src/glue/types/vm/l1_batch.rs new file mode 100644 index 00000000000..7d1cd758498 --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/l1_batch.rs @@ -0,0 +1,16 @@ +use crate::glue::{GlueFrom, GlueInto}; + +impl GlueFrom for vm_virtual_blocks::L1BatchEnv { + fn glue_from(value: vm_latest::L1BatchEnv) -> Self { + Self { + previous_batch_hash: value.previous_batch_hash, + number: value.number, + timestamp: value.timestamp, + l1_gas_price: value.l1_gas_price, + fair_l2_gas_price: value.fair_l2_gas_price, + fee_account: value.fee_account, + enforced_base_fee: value.enforced_base_fee, + first_l2_block: value.first_l2_block.glue_into(), + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/l2_block.rs b/core/lib/multivm/src/glue/types/vm/l2_block.rs new file mode 100644 index 00000000000..a12e5ec816b --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/l2_block.rs @@ -0,0 +1,12 @@ +use crate::glue::GlueFrom; + +impl GlueFrom for vm_virtual_blocks::L2BlockEnv { + fn glue_from(value: vm_latest::L2BlockEnv) -> Self { + Self { + number: value.number, + timestamp: value.timestamp, + prev_block_hash: value.prev_block_hash, + max_virtual_blocks_to_create: value.max_virtual_blocks_to_create, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/mod.rs b/core/lib/multivm/src/glue/types/vm/mod.rs index a9e8d84ad70..0a416a221b5 100644 --- a/core/lib/multivm/src/glue/types/vm/mod.rs +++ b/core/lib/multivm/src/glue/types/vm/mod.rs @@ -1,8 +1,20 @@ mod block_context_mode; +mod bytecompression_result; +mod current_execution_state; +mod execution_result; +mod halt; +mod l1_batch; +mod l2_block; +mod refunds; +mod system_env; mod tx_execution_mode; mod tx_revert_reason; mod vm_block_result; +mod vm_execution_mode; mod vm_execution_result; +mod vm_execution_result_and_logs; +mod vm_execution_statistics; +mod vm_memory_metrics; mod vm_partial_execution_result; mod vm_revert_reason; mod vm_tx_execution_result; diff --git a/core/lib/multivm/src/glue/types/vm/refunds.rs b/core/lib/multivm/src/glue/types/vm/refunds.rs new file mode 100644 index 00000000000..3127efbf661 --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/refunds.rs @@ -0,0 +1,11 @@ +use crate::glue::GlueFrom; +use vm_latest::Refunds; + +impl GlueFrom for Refunds { + fn glue_from(value: vm_virtual_blocks::Refunds) -> Self { + Self { + gas_refunded: value.gas_refunded, + operator_suggested_refund: value.operator_suggested_refund, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/system_env.rs b/core/lib/multivm/src/glue/types/vm/system_env.rs new file mode 100644 index 00000000000..0f3012287bf --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/system_env.rs @@ -0,0 +1,16 @@ +use crate::glue::{GlueFrom, GlueInto}; + +impl GlueFrom for vm_virtual_blocks::SystemEnv { + fn glue_from(value: vm_latest::SystemEnv) -> Self { + Self { + zk_porter_available: value.zk_porter_available, + version: value.version, + base_system_smart_contracts: value.base_system_smart_contracts, + gas_limit: value.gas_limit, + execution_mode: value.execution_mode.glue_into(), + default_validation_computational_gas_limit: value + .default_validation_computational_gas_limit, + chain_id: value.chain_id, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/tx_execution_mode.rs b/core/lib/multivm/src/glue/types/vm/tx_execution_mode.rs index e9b901ff990..2f68ed5edc7 100644 --- a/core/lib/multivm/src/glue/types/vm/tx_execution_mode.rs +++ b/core/lib/multivm/src/glue/types/vm/tx_execution_mode.rs @@ -45,3 +45,13 @@ impl GlueFrom for vm_1_3_2::vm_with_bootloader::TxEx } } } + +impl GlueFrom for vm_virtual_blocks::TxExecutionMode { + fn glue_from(value: vm_latest::TxExecutionMode) -> Self { + match value { + vm_latest::TxExecutionMode::VerifyExecute => Self::VerifyExecute, + vm_latest::TxExecutionMode::EstimateFee => Self::EstimateFee, + vm_latest::TxExecutionMode::EthCall => Self::EthCall, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/vm_execution_mode.rs b/core/lib/multivm/src/glue/types/vm/vm_execution_mode.rs new file mode 100644 index 00000000000..d02766c9fce --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/vm_execution_mode.rs @@ -0,0 +1,12 @@ +use crate::glue::GlueFrom; +use vm_latest::VmExecutionMode; + +impl GlueFrom for vm_virtual_blocks::VmExecutionMode { + fn glue_from(value: VmExecutionMode) -> Self { + match value { + VmExecutionMode::OneTx => vm_virtual_blocks::VmExecutionMode::OneTx, + VmExecutionMode::Batch => vm_virtual_blocks::VmExecutionMode::Batch, + VmExecutionMode::Bootloader => vm_virtual_blocks::VmExecutionMode::Bootloader, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/vm_execution_result_and_logs.rs b/core/lib/multivm/src/glue/types/vm/vm_execution_result_and_logs.rs new file mode 100644 index 00000000000..e6e7c411fd6 --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/vm_execution_result_and_logs.rs @@ -0,0 +1,13 @@ +use crate::glue::{GlueFrom, GlueInto}; +use vm_latest::VmExecutionResultAndLogs; + +impl GlueFrom for VmExecutionResultAndLogs { + fn glue_from(value: vm_virtual_blocks::VmExecutionResultAndLogs) -> Self { + Self { + result: value.result.glue_into(), + logs: value.logs, + statistics: value.statistics.glue_into(), + refunds: value.refunds.glue_into(), + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/vm_execution_statistics.rs b/core/lib/multivm/src/glue/types/vm/vm_execution_statistics.rs new file mode 100644 index 00000000000..3b4951ca9f6 --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/vm_execution_statistics.rs @@ -0,0 +1,14 @@ +use crate::glue::GlueFrom; +use vm_latest::VmExecutionStatistics; + +impl GlueFrom for VmExecutionStatistics { + fn glue_from(value: vm_virtual_blocks::VmExecutionStatistics) -> Self { + Self { + contracts_used: value.contracts_used, + cycles_used: value.cycles_used, + gas_used: value.gas_used, + computational_gas_used: value.computational_gas_used, + total_log_queries: value.total_log_queries, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/vm_memory_metrics.rs b/core/lib/multivm/src/glue/types/vm/vm_memory_metrics.rs new file mode 100644 index 00000000000..d81a67de321 --- /dev/null +++ b/core/lib/multivm/src/glue/types/vm/vm_memory_metrics.rs @@ -0,0 +1,17 @@ +use crate::glue::GlueFrom; +use vm_latest::VmMemoryMetrics; + +impl GlueFrom for VmMemoryMetrics { + fn glue_from(value: vm_virtual_blocks::VmMemoryMetrics) -> Self { + Self { + event_sink_inner: value.event_sink_inner, + event_sink_history: value.event_sink_history, + memory_inner: value.memory_inner, + memory_history: value.memory_history, + decommittment_processor_inner: value.decommittment_processor_inner, + decommittment_processor_history: value.decommittment_processor_history, + storage_inner: value.storage_inner, + storage_history: value.storage_history, + } + } +} diff --git a/core/lib/multivm/src/glue/types/vm/vm_revert_reason.rs b/core/lib/multivm/src/glue/types/vm/vm_revert_reason.rs index ec38027d114..691651e2baf 100644 --- a/core/lib/multivm/src/glue/types/vm/vm_revert_reason.rs +++ b/core/lib/multivm/src/glue/types/vm/vm_revert_reason.rs @@ -53,3 +53,20 @@ impl GlueFrom for vm_latest::VmRevertReason { } } } + +impl GlueFrom for vm_latest::VmRevertReason { + fn glue_from(value: vm_virtual_blocks::VmRevertReason) -> Self { + match value { + vm_virtual_blocks::VmRevertReason::General { msg, data } => Self::General { msg, data }, + vm_virtual_blocks::VmRevertReason::InnerTxError => Self::InnerTxError, + vm_virtual_blocks::VmRevertReason::VmError => Self::VmError, + vm_virtual_blocks::VmRevertReason::Unknown { + function_selector, + data, + } => Self::Unknown { + function_selector, + data, + }, + } + } +} diff --git a/core/lib/multivm/src/lib.rs b/core/lib/multivm/src/lib.rs index a45eacecfa3..fa9497c2153 100644 --- a/core/lib/multivm/src/lib.rs +++ b/core/lib/multivm/src/lib.rs @@ -1,5 +1,8 @@ pub use crate::{ - glue::{block_properties::BlockProperties, oracle_tools::OracleTools, tracer::MultivmTracer}, + glue::{ + block_properties::BlockProperties, history_mode::HistoryMode, oracle_tools::OracleTools, + tracer::MultivmTracer, + }, vm_instance::{VmInstance, VmInstanceData}, }; pub use zksync_types::vm_version::VmVersion; diff --git a/core/lib/multivm/src/vm_instance.rs b/core/lib/multivm/src/vm_instance.rs index dcc4d393c53..30b410b52b5 100644 --- a/core/lib/multivm/src/vm_instance.rs +++ b/core/lib/multivm/src/vm_instance.rs @@ -24,7 +24,10 @@ pub(crate) enum VmInstanceVersion<'a, S: ReadStorage, H: HistoryMode> { VmM5(Box>>), VmM6(Box, H::VmM6Mode>>), Vm1_3_2(Box, H::Vm1_3_2Mode>>), - VmVirtualBlocks(Box, H::VmVirtualBlocksMode>>), + VmVirtualBlocks(Box, H::VmVirtualBlocksMode>>), + VmVirtualBlocksRefundsEnhancement( + Box, H::VmVirtualBlocksRefundsEnhancement>>, + ), } impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { @@ -57,6 +60,9 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { VmInstanceVersion::VmVirtualBlocks(vm) => { vm.push_transaction(tx.clone()); } + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { + vm.push_transaction(tx.clone()); + } } } @@ -79,6 +85,16 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { ) .glue_into(), VmInstanceVersion::VmVirtualBlocks(vm) => { + let result = vm.execute(VmExecutionMode::Batch.glue_into()); + let execution_state = vm.get_current_execution_state(); + let bootloader_memory = vm.get_bootloader_memory(); + FinishedL1Batch { + block_tip_execution_result: result.glue_into(), + final_execution_state: execution_state.glue_into(), + final_bootloader_memory: Some(bootloader_memory), + } + } + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { let result = vm.execute(VmExecutionMode::Batch); let execution_state = vm.get_current_execution_state(); let bootloader_memory = vm.get_bootloader_memory(); @@ -98,7 +114,12 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { VmInstanceVersion::VmM5(vm) => vm.execute_block_tip().glue_into(), VmInstanceVersion::VmM6(vm) => vm.execute_block_tip().glue_into(), VmInstanceVersion::Vm1_3_2(vm) => vm.execute_block_tip().glue_into(), - VmInstanceVersion::VmVirtualBlocks(vm) => vm.execute(VmExecutionMode::Bootloader), + VmInstanceVersion::VmVirtualBlocks(vm) => vm + .execute(VmExecutionMode::Bootloader.glue_into()) + .glue_into(), + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { + vm.execute(VmExecutionMode::Bootloader) + } } } @@ -147,7 +168,12 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { .glue_into(), } } - VmInstanceVersion::VmVirtualBlocks(vm) => vm.execute(VmExecutionMode::OneTx), + VmInstanceVersion::VmVirtualBlocks(vm) => { + vm.execute(VmExecutionMode::OneTx.glue_into()).glue_into() + } + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { + vm.execute(VmExecutionMode::OneTx) + } } } @@ -155,6 +181,9 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { pub fn get_last_tx_compressed_bytecodes(&self) -> Vec { match &self.vm { VmInstanceVersion::VmVirtualBlocks(vm) => vm.get_last_tx_compressed_bytecodes(), + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { + vm.get_last_tx_compressed_bytecodes() + } _ => self.last_tx_compressed_bytecodes.clone(), } } @@ -162,10 +191,19 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { /// Execute next transaction with custom tracers pub fn inspect_next_transaction( &mut self, - tracers: Vec, H::VmVirtualBlocksMode>>>, + tracers: Vec, H>>>, ) -> vm_latest::VmExecutionResultAndLogs { match &mut self.vm { - VmInstanceVersion::VmVirtualBlocks(vm) => vm.inspect( + VmInstanceVersion::VmVirtualBlocks(vm) => vm + .inspect( + tracers + .into_iter() + .map(|tracer| tracer.vm_virtual_blocks()) + .collect(), + VmExecutionMode::OneTx.glue_into(), + ) + .glue_into(), + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => vm.inspect( tracers.into_iter().map(|tracer| tracer.latest()).collect(), VmExecutionMode::OneTx, ), @@ -316,7 +354,10 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { Ok(result) } } - VmInstanceVersion::VmVirtualBlocks(vm) => { + VmInstanceVersion::VmVirtualBlocks(vm) => vm + .execute_transaction_with_bytecode_compression(tx, with_compression) + .glue_into(), + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { vm.execute_transaction_with_bytecode_compression(tx, with_compression) } } @@ -325,25 +366,43 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { /// Inspect transaction with optional bytecode compression. pub fn inspect_transaction_with_bytecode_compression( &mut self, - tracers: Vec, H::VmVirtualBlocksMode>>>, + tracers: Vec, H>>>, tx: zksync_types::Transaction, with_compression: bool, ) -> Result { - if let VmInstanceVersion::VmVirtualBlocks(vm) = &mut self.vm { - vm.inspect_transaction_with_bytecode_compression( - tracers.into_iter().map(|tracer| tracer.latest()).collect(), - tx, - with_compression, - ) - } else { - self.last_tx_compressed_bytecodes = vec![]; - self.execute_transaction_with_bytecode_compression(tx, with_compression) + match &mut self.vm { + VmInstanceVersion::VmVirtualBlocks(vm) => vm + .inspect_transaction_with_bytecode_compression( + tracers + .into_iter() + .map(|tracer| tracer.vm_virtual_blocks()) + .collect(), + tx, + with_compression, + ) + .glue_into(), + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => vm + .inspect_transaction_with_bytecode_compression( + tracers.into_iter().map(|tracer| tracer.latest()).collect(), + tx, + with_compression, + ), + _ => { + self.last_tx_compressed_bytecodes = vec![]; + self.execute_transaction_with_bytecode_compression(tx, with_compression) + } } } pub fn start_new_l2_block(&mut self, l2_block_env: L2BlockEnv) { - if let VmInstanceVersion::VmVirtualBlocks(vm) = &mut self.vm { - vm.start_new_l2_block(l2_block_env); + match &mut self.vm { + VmInstanceVersion::VmVirtualBlocks(vm) => { + vm.start_new_l2_block(l2_block_env.glue_into()); + } + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { + vm.start_new_l2_block(l2_block_env); + } + _ => {} } } @@ -376,7 +435,12 @@ impl<'a, S: ReadStorage, H: HistoryMode> VmInstance<'a, S, H> { storage_inner: vm.state.storage.get_size(), storage_history: vm.state.storage.get_history_size(), }), - VmInstanceVersion::VmVirtualBlocks(vm) => Some(vm.record_vm_memory_metrics()), + VmInstanceVersion::VmVirtualBlocks(vm) => { + Some(vm.record_vm_memory_metrics().glue_into()) + } + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { + Some(vm.record_vm_memory_metrics()) + } } } } @@ -408,6 +472,7 @@ pub enum VmInstanceData { M6(M6NecessaryData), Vm1_3_2(Vm1_3_2NecessaryData), VmVirtualBlocks(VmVirtualBlocksNecessaryData), + VmVirtualBlocksRefundsEnhancement(VmVirtualBlocksNecessaryData), } impl VmInstanceData { @@ -435,6 +500,13 @@ impl VmInstanceData { } fn latest(storage_view: StoragePtr>, history_mode: H) -> Self { + Self::VmVirtualBlocksRefundsEnhancement(VmVirtualBlocksNecessaryData { + storage_view, + history_mode, + }) + } + + fn vm_virtual_blocks(storage_view: StoragePtr>, history_mode: H) -> Self { Self::VmVirtualBlocks(VmVirtualBlocksNecessaryData { storage_view, history_mode, @@ -515,7 +587,10 @@ impl VmInstanceData { ) } VmVersion::Vm1_3_2 => VmInstanceData::vm1_3_2(storage_view, history), - VmVersion::VmVirtualBlocks => VmInstanceData::latest(storage_view, history), + VmVersion::VmVirtualBlocks => VmInstanceData::vm_virtual_blocks(storage_view, history), + VmVersion::VmVirtualBlocksRefundsEnhancement => { + VmInstanceData::latest(storage_view, history) + } } } } @@ -527,6 +602,7 @@ impl VmInstance<'_, S, vm_latest::HistoryEnabled> { VmInstanceVersion::VmM6(vm) => vm.save_current_vm_as_snapshot(), VmInstanceVersion::Vm1_3_2(vm) => vm.save_current_vm_as_snapshot(), VmInstanceVersion::VmVirtualBlocks(vm) => vm.make_snapshot(), + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => vm.make_snapshot(), } } @@ -538,6 +614,9 @@ impl VmInstance<'_, S, vm_latest::HistoryEnabled> { VmInstanceVersion::VmVirtualBlocks(vm) => { vm.rollback_to_the_latest_snapshot(); } + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { + vm.rollback_to_the_latest_snapshot(); + } } } @@ -550,6 +629,9 @@ impl VmInstance<'_, S, vm_latest::HistoryEnabled> { VmInstanceVersion::VmM6(vm) => vm.pop_snapshot_no_rollback(), VmInstanceVersion::Vm1_3_2(vm) => vm.pop_snapshot_no_rollback(), VmInstanceVersion::VmVirtualBlocks(vm) => vm.pop_snapshot_no_rollback(), + VmInstanceVersion::VmVirtualBlocksRefundsEnhancement(vm) => { + vm.pop_snapshot_no_rollback() + } } } } diff --git a/core/lib/test_account/src/lib.rs b/core/lib/test_account/src/lib.rs index 509402b7b6b..abc57b937ea 100644 --- a/core/lib/test_account/src/lib.rs +++ b/core/lib/test_account/src/lib.rs @@ -2,6 +2,7 @@ use ethabi::Token; use zksync_config::constants::{ CONTRACT_DEPLOYER_ADDRESS, MAX_GAS_PER_PUBDATA_BYTE, REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE, }; +use zksync_contracts::test_contracts::LoadnextContractExecutionParams; use zksync_contracts::{deployer_contract, load_contract}; use zksync_types::fee::Fee; use zksync_types::l2::L2Tx; @@ -215,6 +216,26 @@ impl Account { } } + pub fn get_loadnext_transaction( + &mut self, + address: Address, + params: LoadnextContractExecutionParams, + tx_type: TxType, + ) -> Transaction { + let calldata = params.to_bytes(); + let execute = Execute { + contract_address: address, + calldata, + value: U256::zero(), + factory_deps: None, + }; + + match tx_type { + TxType::L2 => self.get_l2_tx_for_execute(execute, None), + TxType::L1 { serial_id } => self.get_l1_tx(execute, serial_id), + } + } + pub fn address(&self) -> Address { self.address } diff --git a/core/lib/types/Cargo.toml b/core/lib/types/Cargo.toml index 27220908aa2..08c47dd99f0 100644 --- a/core/lib/types/Cargo.toml +++ b/core/lib/types/Cargo.toml @@ -31,6 +31,7 @@ serde_with = { version = "1", features = ["base64"] } strum = { version = "0.24", features = ["derive"] } thiserror = "1.0" num_enum = "0.6" +hex = "0.4" # Crypto stuff # TODO (PLA-440): remove parity-crypto @@ -42,7 +43,6 @@ blake2 = "0.10" ethereum_types_old = { package = "ethereum-types", version = "0.12.0" } [dev-dependencies] -hex = "0.4" secp256k1 = { version = "0.27", features = ["recovery"] } tokio = { version = "1", features = ["rt", "macros"] } serde_with = { version = "1", features = ["hex"] } diff --git a/core/lib/types/src/protocol_version.rs b/core/lib/types/src/protocol_version.rs index a36fd003cfa..afc4868785a 100644 --- a/core/lib/types/src/protocol_version.rs +++ b/core/lib/types/src/protocol_version.rs @@ -36,15 +36,16 @@ pub enum ProtocolVersionId { Version14, Version15, Version16, + Version17, } impl ProtocolVersionId { pub fn latest() -> Self { - Self::Version15 + Self::Version16 } pub fn next() -> Self { - Self::Version16 + Self::Version17 } /// Returns VM version to be used by API for this protocol version. @@ -67,7 +68,8 @@ impl ProtocolVersionId { ProtocolVersionId::Version13 => VmVersion::VmVirtualBlocks, ProtocolVersionId::Version14 => VmVersion::VmVirtualBlocks, ProtocolVersionId::Version15 => VmVersion::VmVirtualBlocks, - ProtocolVersionId::Version16 => VmVersion::VmVirtualBlocks, + ProtocolVersionId::Version16 => VmVersion::VmVirtualBlocksRefundsEnhancement, + ProtocolVersionId::Version17 => VmVersion::VmVirtualBlocksRefundsEnhancement, } } } @@ -552,7 +554,8 @@ impl From for VmVersion { ProtocolVersionId::Version13 => VmVersion::VmVirtualBlocks, ProtocolVersionId::Version14 => VmVersion::VmVirtualBlocks, ProtocolVersionId::Version15 => VmVersion::VmVirtualBlocks, - ProtocolVersionId::Version16 => VmVersion::VmVirtualBlocks, + ProtocolVersionId::Version16 => VmVersion::VmVirtualBlocksRefundsEnhancement, + ProtocolVersionId::Version17 => VmVersion::VmVirtualBlocksRefundsEnhancement, } } } diff --git a/core/lib/types/src/vm_trace.rs b/core/lib/types/src/vm_trace.rs index 34ac1d77d63..c1fafa088da 100644 --- a/core/lib/types/src/vm_trace.rs +++ b/core/lib/types/src/vm_trace.rs @@ -2,8 +2,10 @@ use crate::{Address, U256}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::{HashMap, HashSet}; use std::fmt; +use std::fmt::Display; use zk_evm::zkevm_opcode_defs::FarCallOpcode; use zksync_config::constants::BOOTLOADER_ADDRESS; +use zksync_utils::u256_to_h256; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum VmTrace { @@ -194,3 +196,37 @@ impl fmt::Debug for Call { .finish() } } + +#[derive(Debug, Clone)] +pub enum ViolatedValidationRule { + TouchedUnallowedStorageSlots(Address, U256), + CalledContractWithNoCode(Address), + TouchedUnallowedContext, + TookTooManyComputationalGas(u32), +} + +impl Display for ViolatedValidationRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ViolatedValidationRule::TouchedUnallowedStorageSlots(contract, key) => write!( + f, + "Touched unallowed storage slots: address {}, key: {}", + hex::encode(contract), + hex::encode(u256_to_h256(*key)) + ), + ViolatedValidationRule::CalledContractWithNoCode(contract) => { + write!(f, "Called contract with no code: {}", hex::encode(contract)) + } + ViolatedValidationRule::TouchedUnallowedContext => { + write!(f, "Touched unallowed context") + } + ViolatedValidationRule::TookTooManyComputationalGas(gas_limit) => { + write!( + f, + "Took too many computational gas, allowed limit: {}", + gas_limit + ) + } + } + } +} diff --git a/core/lib/types/src/vm_version.rs b/core/lib/types/src/vm_version.rs index 410c85e899f..7f043bb5552 100644 --- a/core/lib/types/src/vm_version.rs +++ b/core/lib/types/src/vm_version.rs @@ -6,11 +6,12 @@ pub enum VmVersion { M6BugWithCompressionFixed, Vm1_3_2, VmVirtualBlocks, + VmVirtualBlocksRefundsEnhancement, } impl VmVersion { /// Returns the latest supported VM version. pub const fn latest() -> VmVersion { - Self::VmVirtualBlocks + Self::VmVirtualBlocksRefundsEnhancement } } diff --git a/core/lib/vm/src/lib.rs b/core/lib/vm/src/lib.rs index 38e6982ce81..f0a9d7c0330 100644 --- a/core/lib/vm/src/lib.rs +++ b/core/lib/vm/src/lib.rs @@ -6,9 +6,10 @@ pub use old_vm::{ history_recorder::{HistoryDisabled, HistoryEnabled, HistoryMode}, memory::SimpleMemory, - oracles::storage::StorageOracle, }; +pub use oracles::storage::StorageOracle; + pub use errors::{ BytecodeCompressionError, Halt, TxRevertReason, VmRevertReason, VmRevertReasonParsingError, }; @@ -20,7 +21,6 @@ pub use tracers::{ TracerExecutionStopReason, VmTracer, }, utils::VmExecutionStopReason, - validation::ViolatedValidationRule, StorageInvocations, ValidationError, ValidationTracer, ValidationTracerParams, }; @@ -42,6 +42,7 @@ mod bootloader_state; mod errors; mod implementation; mod old_vm; +mod oracles; mod tracers; mod types; mod vm; diff --git a/core/lib/vm/src/old_vm/history_recorder.rs b/core/lib/vm/src/old_vm/history_recorder.rs index 1a5f7db5866..31431a3cc7a 100644 --- a/core/lib/vm/src/old_vm/history_recorder.rs +++ b/core/lib/vm/src/old_vm/history_recorder.rs @@ -330,6 +330,10 @@ impl HistoryRecorder Option { + self.apply_historic_record(HashMapHistoryEvent { key, value: None }, timestamp) + } } /// A stack of stacks. The inner stacks are called frames. diff --git a/core/lib/vm/src/old_vm/oracles/mod.rs b/core/lib/vm/src/old_vm/oracles/mod.rs index daa2e21672d..725272e7060 100644 --- a/core/lib/vm/src/old_vm/oracles/mod.rs +++ b/core/lib/vm/src/old_vm/oracles/mod.rs @@ -2,7 +2,6 @@ use zk_evm::aux_structures::Timestamp; pub(crate) mod decommitter; pub(crate) mod precompile; -pub(crate) mod storage; pub(crate) trait OracleWithHistory { fn rollback_to_timestamp(&mut self, timestamp: Timestamp); diff --git a/core/lib/vm/src/oracles/mod.rs b/core/lib/vm/src/oracles/mod.rs new file mode 100644 index 00000000000..b21c842572f --- /dev/null +++ b/core/lib/vm/src/oracles/mod.rs @@ -0,0 +1 @@ +pub(crate) mod storage; diff --git a/core/lib/vm/src/oracles/storage.rs b/core/lib/vm/src/oracles/storage.rs new file mode 100644 index 00000000000..42d4f802247 --- /dev/null +++ b/core/lib/vm/src/oracles/storage.rs @@ -0,0 +1,414 @@ +use std::collections::HashMap; + +use crate::old_vm::history_recorder::{ + AppDataFrameManagerWithHistory, HashMapHistoryEvent, HistoryEnabled, HistoryMode, + HistoryRecorder, StorageWrapper, WithHistory, +}; +use crate::old_vm::oracles::OracleWithHistory; + +use zk_evm::abstractions::RefundedAmounts; +use zk_evm::zkevm_opcode_defs::system_params::INITIAL_STORAGE_WRITE_PUBDATA_BYTES; +use zk_evm::{ + abstractions::{RefundType, Storage as VmStorageOracle}, + aux_structures::{LogQuery, Timestamp}, +}; + +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::utils::storage_key_for_eth_balance; +use zksync_types::{ + AccountTreeId, Address, StorageKey, StorageLogQuery, StorageLogQueryType, BOOTLOADER_ADDRESS, + U256, +}; +use zksync_utils::u256_to_h256; + +// While the storage does not support different shards, it was decided to write the +// code of the StorageOracle with the shard parameters in mind. +pub(crate) fn triplet_to_storage_key(_shard_id: u8, address: Address, key: U256) -> StorageKey { + StorageKey::new(AccountTreeId::new(address), u256_to_h256(key)) +} + +pub(crate) fn storage_key_of_log(query: &LogQuery) -> StorageKey { + triplet_to_storage_key(query.shard_id, query.address, query.key) +} + +#[derive(Debug)] +pub struct StorageOracle { + // Access to the persistent storage. Please note that it + // is used only for read access. All the actual writes happen + // after the execution ended. + pub(crate) storage: HistoryRecorder, H>, + + pub(crate) frames_stack: AppDataFrameManagerWithHistory, H>, + + // The changes that have been paid for in previous transactions. + // It is a mapping from storage key to the number of *bytes* that was paid by the user + // to cover this slot. + pub(crate) pre_paid_changes: HistoryRecorder, H>, + + // The changes that have been paid for in the current transaction + pub(crate) paid_changes: HistoryRecorder, H>, + + // The map that contains all the first values read from storage for each slot. + // While formally it does not have to be rollbackable, we still do it to avoid memory bloat + // for unused slots. + pub(crate) initial_values: HistoryRecorder, H>, +} + +impl OracleWithHistory for StorageOracle { + fn rollback_to_timestamp(&mut self, timestamp: Timestamp) { + self.storage.rollback_to_timestamp(timestamp); + self.frames_stack.rollback_to_timestamp(timestamp); + self.pre_paid_changes.rollback_to_timestamp(timestamp); + self.paid_changes.rollback_to_timestamp(timestamp); + self.initial_values.rollback_to_timestamp(timestamp); + } +} + +impl StorageOracle { + pub fn new(storage: StoragePtr) -> Self { + Self { + storage: HistoryRecorder::from_inner(StorageWrapper::new(storage)), + frames_stack: Default::default(), + pre_paid_changes: Default::default(), + paid_changes: Default::default(), + initial_values: Default::default(), + } + } + + pub fn delete_history(&mut self) { + self.storage.delete_history(); + self.frames_stack.delete_history(); + self.pre_paid_changes.delete_history(); + self.paid_changes.delete_history(); + self.initial_values.delete_history(); + } + + fn is_storage_key_free(&self, key: &StorageKey) -> bool { + key.address() == &zksync_config::constants::SYSTEM_CONTEXT_ADDRESS + || *key == storage_key_for_eth_balance(&BOOTLOADER_ADDRESS) + } + + pub fn read_value(&mut self, mut query: LogQuery) -> LogQuery { + let key = triplet_to_storage_key(query.shard_id, query.address, query.key); + let current_value = self.storage.read_from_storage(&key); + + query.read_value = current_value; + + self.frames_stack.push_forward( + Box::new(StorageLogQuery { + log_query: query, + log_type: StorageLogQueryType::Read, + }), + query.timestamp, + ); + + query + } + + pub fn write_value(&mut self, mut query: LogQuery) -> LogQuery { + let key = triplet_to_storage_key(query.shard_id, query.address, query.key); + let current_value = + self.storage + .write_to_storage(key, query.written_value, query.timestamp); + + let is_initial_write = self.storage.get_ptr().borrow_mut().is_write_initial(&key); + let log_query_type = if is_initial_write { + StorageLogQueryType::InitialWrite + } else { + StorageLogQueryType::RepeatedWrite + }; + + query.read_value = current_value; + + if !self.initial_values.inner().contains_key(&key) { + self.initial_values + .insert(key, current_value, query.timestamp); + } + + let mut storage_log_query = StorageLogQuery { + log_query: query, + log_type: log_query_type, + }; + self.frames_stack + .push_forward(Box::new(storage_log_query), query.timestamp); + storage_log_query.log_query.rollback = true; + self.frames_stack + .push_rollback(Box::new(storage_log_query), query.timestamp); + storage_log_query.log_query.rollback = false; + + query + } + + // Returns the amount of funds that has been already paid for writes into the storage slot + fn prepaid_for_write(&self, storage_key: &StorageKey) -> u32 { + self.paid_changes + .inner() + .get(storage_key) + .copied() + .unwrap_or_else(|| { + self.pre_paid_changes + .inner() + .get(storage_key) + .copied() + .unwrap_or(0) + }) + } + + // Remembers the changes that have been paid for in the current transaction. + // It also returns how much pubdata did the user pay for and how much was actually published. + pub(crate) fn save_paid_changes(&mut self, timestamp: Timestamp) -> u32 { + let mut published = 0; + + let modified_keys = self + .paid_changes + .inner() + .iter() + .map(|(k, v)| (*k, *v)) + .collect::>(); + + for (key, _) in modified_keys { + // It is expected that for each slot for which we have paid changes, there is some + // first slot value already read. + let first_slot_value = self.initial_values.inner().get(&key).copied().unwrap(); + + // This is the value has been written to the storage slot at the end. + let current_slot_value = self.storage.read_from_storage(&key); + + let required_pubdata = + self.base_price_for_write(&key, first_slot_value, current_slot_value); + + // We assume that "prepaid_for_slot" represents both the number of pubdata published and the number of bytes paid by the previous transactions + // as they should be identical. + let prepaid_for_slot = self + .pre_paid_changes + .inner() + .get(&key) + .copied() + .unwrap_or_default(); + + published += required_pubdata.saturating_sub(prepaid_for_slot); + + // We remove the slot from the paid changes and move to the pre-paid changes as + // the transaction ends. + self.paid_changes.remove(key, timestamp); + self.pre_paid_changes + .insert(key, prepaid_for_slot.max(required_pubdata), timestamp); + } + + published + } + + fn base_price_for_write_query(&self, query: &LogQuery) -> u32 { + let storage_key = storage_key_of_log(query); + + self.base_price_for_write(&storage_key, query.read_value, query.written_value) + } + + pub(crate) fn base_price_for_write( + &self, + storage_key: &StorageKey, + prev_value: U256, + new_value: U256, + ) -> u32 { + if self.is_storage_key_free(storage_key) || prev_value == new_value { + return 0; + } + + let is_initial_write = self + .storage + .get_ptr() + .borrow_mut() + .is_write_initial(storage_key); + + get_pubdata_price_bytes(is_initial_write) + } + + // Returns the price of the update in terms of pubdata bytes. + // TODO (SMA-1701): update VM to accept gas instead of pubdata. + fn value_update_price(&self, query: &LogQuery) -> u32 { + let storage_key = storage_key_of_log(query); + + let base_cost = self.base_price_for_write_query(query); + + let already_paid = self.prepaid_for_write(&storage_key); + + if base_cost <= already_paid { + // Some other transaction has already paid for this slot, no need to pay anything + 0u32 + } else { + base_cost - already_paid + } + } + + /// Returns storage log queries from current frame where `log.log_query.timestamp >= from_timestamp`. + pub(crate) fn storage_log_queries_after_timestamp( + &self, + from_timestamp: Timestamp, + ) -> &[Box] { + let logs = self.frames_stack.forward().current_frame(); + + // Select all of the last elements where l.log_query.timestamp >= from_timestamp. + // Note, that using binary search here is dangerous, because the logs are not sorted by timestamp. + logs.rsplit(|l| l.log_query.timestamp < from_timestamp) + .next() + .unwrap_or(&[]) + } + + pub(crate) fn get_final_log_queries(&self) -> Vec { + assert_eq!( + self.frames_stack.len(), + 1, + "VM finished execution in unexpected state" + ); + + self.frames_stack + .forward() + .current_frame() + .iter() + .map(|x| **x) + .collect() + } + + pub(crate) fn get_size(&self) -> usize { + let frames_stack_size = self.frames_stack.get_size(); + let paid_changes_size = + self.paid_changes.inner().len() * std::mem::size_of::<(StorageKey, u32)>(); + + frames_stack_size + paid_changes_size + } + + pub(crate) fn get_history_size(&self) -> usize { + let storage_size = self.storage.borrow_history(|h| h.len(), 0) + * std::mem::size_of::< as WithHistory>::HistoryRecord>(); + let frames_stack_size = self.frames_stack.get_history_size(); + let paid_changes_size = self.paid_changes.borrow_history(|h| h.len(), 0) + * std::mem::size_of::< as WithHistory>::HistoryRecord>(); + storage_size + frames_stack_size + paid_changes_size + } +} + +impl VmStorageOracle for StorageOracle { + // Perform a storage read/write access by taking an partially filled query + // and returning filled query and cold/warm marker for pricing purposes + fn execute_partial_query( + &mut self, + _monotonic_cycle_counter: u32, + query: LogQuery, + ) -> LogQuery { + // tracing::trace!( + // "execute partial query cyc {:?} addr {:?} key {:?}, rw {:?}, wr {:?}, tx {:?}", + // _monotonic_cycle_counter, + // query.address, + // query.key, + // query.rw_flag, + // query.written_value, + // query.tx_number_in_block + // ); + assert!(!query.rollback); + if query.rw_flag { + // The number of bytes that have been compensated by the user to perform this write + let storage_key = storage_key_of_log(&query); + + // It is considered that the user has paid for the whole base price for the writes + let to_pay_by_user = self.base_price_for_write_query(&query); + let prepaid = self.prepaid_for_write(&storage_key); + + if to_pay_by_user > prepaid { + self.paid_changes.apply_historic_record( + HashMapHistoryEvent { + key: storage_key, + value: Some(to_pay_by_user), + }, + query.timestamp, + ); + } + self.write_value(query) + } else { + self.read_value(query) + } + } + + // We can return the size of the refund before each storage query. + // Note, that while the `RefundType` allows to provide refunds both in + // `ergs` and `pubdata`, only refunds in pubdata will be compensated for the users + fn estimate_refunds_for_write( + &mut self, // to avoid any hacks inside, like prefetch + _monotonic_cycle_counter: u32, + partial_query: &LogQuery, + ) -> RefundType { + let price_to_pay = self.value_update_price(partial_query); + + RefundType::RepeatedWrite(RefundedAmounts { + ergs: 0, + // `INITIAL_STORAGE_WRITE_PUBDATA_BYTES` is the default amount of pubdata bytes the user pays for. + pubdata_bytes: (INITIAL_STORAGE_WRITE_PUBDATA_BYTES as u32) - price_to_pay, + }) + } + + // Indicate a start of execution frame for rollback purposes + fn start_frame(&mut self, timestamp: Timestamp) { + self.frames_stack.push_frame(timestamp); + } + + // Indicate that execution frame went out from the scope, so we can + // log the history and either rollback immediately or keep records to rollback later + fn finish_frame(&mut self, timestamp: Timestamp, panicked: bool) { + // If we panic then we append forward and rollbacks to the forward of parent, + // otherwise we place rollbacks of child before rollbacks of the parent + if panicked { + // perform actual rollback + for query in self.frames_stack.rollback().current_frame().iter().rev() { + let read_value = match query.log_type { + StorageLogQueryType::Read => { + // Having Read logs in rollback is not possible + tracing::warn!("Read log in rollback queue {:?}", query); + continue; + } + StorageLogQueryType::InitialWrite | StorageLogQueryType::RepeatedWrite => { + query.log_query.read_value + } + }; + + let LogQuery { written_value, .. } = query.log_query; + let key = triplet_to_storage_key( + query.log_query.shard_id, + query.log_query.address, + query.log_query.key, + ); + let current_value = self.storage.write_to_storage( + key, + // NOTE, that since it is a rollback query, + // the `read_value` is being set + read_value, timestamp, + ); + + // Additional validation that the current value was correct + // Unwrap is safe because the return value from write_inner is the previous value in this leaf. + // It is impossible to set leaf value to `None` + assert_eq!(current_value, written_value); + } + + self.frames_stack + .move_rollback_to_forward(|_| true, timestamp); + } + self.frames_stack.merge_frame(timestamp); + } +} + +/// Returns the number of bytes needed to publish a slot. +// Since we need to publish the state diffs onchain, for each of the updated storage slot +// we basically need to publish the following pair: (). +// While new_value is always 32 bytes long, 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 this 8-byte instead of the 32 bytes of the entire key. +// So the total size of the publish pubdata is 40 bytes. We call this kind of write the repeated one +fn get_pubdata_price_bytes(is_initial: bool) -> u32 { + // TODO (SMA-1702): take into account the content of the log query, i.e. values that contain mostly zeroes + // should cost less. + if is_initial { + zk_evm::zkevm_opcode_defs::system_params::INITIAL_STORAGE_WRITE_PUBDATA_BYTES as u32 + } else { + zk_evm::zkevm_opcode_defs::system_params::REPEATED_STORAGE_WRITE_PUBDATA_BYTES as u32 + } +} diff --git a/core/lib/vm/src/tests/l1_tx_execution.rs b/core/lib/vm/src/tests/l1_tx_execution.rs index a231d8aba0b..cd1c8f2460c 100644 --- a/core/lib/vm/src/tests/l1_tx_execution.rs +++ b/core/lib/vm/src/tests/l1_tx_execution.rs @@ -1,7 +1,7 @@ use zksync_config::constants::BOOTLOADER_ADDRESS; use zksync_types::l2_to_l1_log::L2ToL1Log; use zksync_types::storage_writes_deduplicator::StorageWritesDeduplicator; -use zksync_types::{get_code_key, get_known_code_key, L2ChainId, U256}; +use zksync_types::{get_code_key, get_known_code_key, U256}; use zksync_utils::u256_to_h256; use crate::tests::tester::{TxType, VmTesterBuilder}; @@ -41,7 +41,7 @@ fn test_l1_tx_execution() { is_service: true, tx_number_in_block: 0, sender: BOOTLOADER_ADDRESS, - key: tx_data.tx_hash(L2ChainId::from(0)), + key: tx_data.tx_hash(0.into()), value: u256_to_h256(U256::from(1u32)), }]; diff --git a/core/lib/vm/src/tests/rollbacks.rs b/core/lib/vm/src/tests/rollbacks.rs index 1fa6a2afe39..9d6c48b8690 100644 --- a/core/lib/vm/src/tests/rollbacks.rs +++ b/core/lib/vm/src/tests/rollbacks.rs @@ -3,14 +3,19 @@ use ethabi::Token; use zksync_contracts::get_loadnext_contract; use zksync_contracts::test_contracts::LoadnextContractExecutionParams; -use zksync_types::{Execute, U256}; +use zksync_state::WriteStorage; +use zksync_types::{get_nonce_key, Execute, U256}; use crate::tests::tester::{ DeployContractsTx, TransactionTestInfo, TxModifier, TxType, VmTesterBuilder, }; use crate::tests::utils::read_test_contract; use crate::types::inputs::system_env::TxExecutionMode; -use crate::HistoryEnabled; +use crate::{ + BootloaderState, DynTracer, ExecutionEndTracer, ExecutionProcessing, HistoryEnabled, + HistoryMode, TracerExecutionStatus, TracerExecutionStopReason, VmExecutionMode, VmTracer, + ZkSyncVmState, +}; #[test] fn test_vm_rollbacks() { @@ -144,3 +149,123 @@ fn test_vm_loadnext_rollbacks() { assert_eq!(result_without_rollbacks, result_with_rollbacks); } + +// Testing tracer that does not allow the recursion to go deeper than a certain limit +struct MaxRecursionTracer { + max_recursion_depth: usize, + should_stop_execution: bool, +} + +/// Tracer responsible for calculating the number of storage invocations and +/// stopping the VM execution if the limit is reached. +impl DynTracer for MaxRecursionTracer {} + +impl ExecutionEndTracer for MaxRecursionTracer { + fn should_stop_execution(&self) -> TracerExecutionStatus { + if self.should_stop_execution { + TracerExecutionStatus::Stop(TracerExecutionStopReason::Finish) + } else { + TracerExecutionStatus::Continue + } + } +} + +impl ExecutionProcessing for MaxRecursionTracer { + fn after_cycle( + &mut self, + state: &mut ZkSyncVmState, + _bootloader_state: &mut BootloaderState, + ) { + let current_depth = state.local_state.callstack.depth(); + + if current_depth > self.max_recursion_depth { + self.should_stop_execution = true; + } + } +} + +impl VmTracer for MaxRecursionTracer {} + +#[test] +fn test_layered_rollback() { + // This test checks that the layered rollbacks work correctly, i.e. + // the rollback by the operator will always revert all the changes + + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let account = &mut vm.rich_accounts[0]; + let loadnext_contract = get_loadnext_contract().bytecode; + + let DeployContractsTx { + tx: deploy_tx, + address, + .. + } = account.get_deploy_tx( + &loadnext_contract, + Some(&[Token::Uint(0.into())]), + TxType::L2, + ); + vm.vm.push_transaction(deploy_tx); + let deployment_res = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!deployment_res.result.is_failed(), "transaction failed"); + + let loadnext_transaction = account.get_loadnext_transaction( + address, + LoadnextContractExecutionParams { + writes: 1, + recursive_calls: 20, + ..LoadnextContractExecutionParams::empty() + }, + TxType::L2, + ); + + let nonce_val = vm + .vm + .state + .storage + .storage + .read_from_storage(&get_nonce_key(&account.address)); + + vm.vm.make_snapshot(); + + vm.vm.push_transaction(loadnext_transaction.clone()); + vm.vm.inspect( + vec![Box::new(MaxRecursionTracer { + max_recursion_depth: 15, + should_stop_execution: false, + })], + VmExecutionMode::OneTx, + ); + + let nonce_val2 = vm + .vm + .state + .storage + .storage + .read_from_storage(&get_nonce_key(&account.address)); + + // The tracer stopped after the validation has passed, so nonce has already been increased + assert_eq!(nonce_val + U256::one(), nonce_val2, "nonce did not change"); + + vm.vm.rollback_to_the_latest_snapshot(); + + let nonce_val_after_rollback = vm + .vm + .state + .storage + .storage + .read_from_storage(&get_nonce_key(&account.address)); + + assert_eq!( + nonce_val, nonce_val_after_rollback, + "nonce changed after rollback" + ); + + vm.vm.push_transaction(loadnext_transaction); + let result = vm.vm.inspect(vec![], VmExecutionMode::OneTx); + assert!(!result.result.is_failed(), "transaction must not fail"); +} diff --git a/core/lib/vm/src/tests/tester/inner_state.rs b/core/lib/vm/src/tests/tester/inner_state.rs index 08220724b4d..24363743b9e 100644 --- a/core/lib/vm/src/tests/tester/inner_state.rs +++ b/core/lib/vm/src/tests/tester/inner_state.rs @@ -47,6 +47,10 @@ pub(crate) struct StorageOracleInnerState { pub(crate) modified_storage_keys: ModifiedKeysMap, pub(crate) frames_stack: AppDataFrameManagerWithHistory, H>, + + pub(crate) pre_paid_changes: HistoryRecorder, H>, + pub(crate) paid_changes: HistoryRecorder, H>, + pub(crate) initial_values: HistoryRecorder, H>, } #[derive(Clone, PartialEq, Debug)] @@ -101,6 +105,9 @@ impl Vm { .clone(), ), frames_stack: self.state.storage.frames_stack.clone(), + pre_paid_changes: self.state.storage.pre_paid_changes.clone(), + paid_changes: self.state.storage.paid_changes.clone(), + initial_values: self.state.storage.initial_values.clone(), }; let local_state = self.state.local_state.clone(); diff --git a/core/lib/vm/src/tracers/call.rs b/core/lib/vm/src/tracers/call.rs index 12750247604..aa2041f0bea 100644 --- a/core/lib/vm/src/tracers/call.rs +++ b/core/lib/vm/src/tracers/call.rs @@ -22,7 +22,7 @@ use crate::types::outputs::VmExecutionResultAndLogs; #[derive(Debug, Clone)] pub struct CallTracer { stack: Vec, - result: Arc>>, + pub result: Arc>>, _phantom: PhantomData H>, } diff --git a/core/lib/vm/src/tracers/refunds.rs b/core/lib/vm/src/tracers/refunds.rs index 47e3f83e20f..d7f15d7c9b9 100644 --- a/core/lib/vm/src/tracers/refunds.rs +++ b/core/lib/vm/src/tracers/refunds.rs @@ -1,7 +1,5 @@ use vise::{Buckets, EncodeLabelSet, EncodeLabelValue, Family, Histogram, Metrics}; -use std::collections::HashMap; - use zk_evm::{ aux_structures::Timestamp, tracing::{BeforeExecutionData, VmLocalStateData}, @@ -12,18 +10,18 @@ use zksync_state::{StoragePtr, WriteStorage}; use zksync_types::{ event::{extract_long_l2_to_l1_messages, extract_published_bytecodes}, l2_to_l1_log::L2ToL1Log, - zkevm_test_harness::witness::sort_storage_access::sort_storage_access_queries, - L1BatchNumber, StorageKey, U256, + L1BatchNumber, U256, }; use zksync_utils::bytecode::bytecode_len_in_bytes; use zksync_utils::{ceil_div_u256, u256_to_h256}; -use crate::bootloader_state::BootloaderState; use crate::constants::{BOOTLOADER_HEAP_PAGE, OPERATOR_REFUNDS_OFFSET, TX_GAS_LIMIT_OFFSET}; use crate::old_vm::{ events::merge_events, history_recorder::HistoryMode, memory::SimpleMemory, - oracles::storage::storage_key_of_log, utils::eth_price_per_pubdata_byte, + utils::eth_price_per_pubdata_byte, }; + +use crate::bootloader_state::BootloaderState; use crate::tracers::utils::gas_spent_on_bytecodes_and_long_messages_this_opcode; use crate::tracers::{ traits::{DynTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}, @@ -224,8 +222,16 @@ impl ExecutionProcessing for RefundsTrace .value .as_u32(); - let pubdata_published = - pubdata_published(state, self.timestamp_initial, self.l1_batch.number); + let used_published_storage_slots = state + .storage + .save_paid_changes(Timestamp(state.local_state.timestamp)); + + let pubdata_published = pubdata_published( + state, + used_published_storage_slots, + self.timestamp_initial, + self.l1_batch.number, + ); let current_ergs_per_pubdata_byte = state.local_state.current_ergs_per_pubdata_byte; let tx_body_refund = self.tx_body_refund( @@ -285,11 +291,10 @@ impl ExecutionProcessing for RefundsTrace /// Returns the given transactions' gas limit - by reading it directly from the VM memory. pub(crate) fn pubdata_published( state: &ZkSyncVmState, + storage_writes_pubdata_published: u32, from_timestamp: Timestamp, batch_number: L1BatchNumber, ) -> u32 { - let storage_writes_pubdata_published = pubdata_published_for_writes(state, from_timestamp); - let (raw_events, l1_messages) = state .event_sink .get_events_and_l2_l1_logs_after_timestamp(from_timestamp); @@ -328,62 +333,6 @@ pub(crate) fn pubdata_published( + published_bytecode_bytes } -fn pubdata_published_for_writes( - state: &ZkSyncVmState, - from_timestamp: Timestamp, -) -> u32 { - // This `HashMap` contains how much was already paid for every slot that was paid during the last tx execution. - // For the slots that weren't paid during the last tx execution we can just use - // `self.state.storage.paid_changes.inner().get(&key)` to get how much it was paid before. - let pre_paid_before_tx_map: HashMap = state - .storage - .paid_changes - .history() - .iter() - .rev() - .take_while(|history_elem| history_elem.0 >= from_timestamp) - .map(|history_elem| (history_elem.1.key, history_elem.1.value.unwrap_or(0))) - .collect(); - let pre_paid_before_tx = |key: &StorageKey| -> u32 { - if let Some(pre_paid) = pre_paid_before_tx_map.get(key) { - *pre_paid - } else { - state - .storage - .paid_changes - .inner() - .get(key) - .copied() - .unwrap_or(0) - } - }; - - let storage_logs = state - .storage - .storage_log_queries_after_timestamp(from_timestamp); - let (_, deduplicated_logs) = - sort_storage_access_queries(storage_logs.iter().map(|log| &log.log_query)); - - deduplicated_logs - .into_iter() - .filter_map(|log| { - if log.rw_flag { - let key = storage_key_of_log(&log); - let pre_paid = pre_paid_before_tx(&key); - let to_pay_by_user = state.storage.base_price_for_write(&log); - - if to_pay_by_user > pre_paid { - Some(to_pay_by_user - pre_paid) - } else { - None - } - } else { - None - } - }) - .sum() -} - impl VmTracer for RefundsTracer { fn save_results(&mut self, result: &mut VmExecutionResultAndLogs) { result.refunds = Refunds { diff --git a/core/lib/vm/src/tracers/storage_invocations.rs b/core/lib/vm/src/tracers/storage_invocations.rs index bd6f419eddf..bd7bbeb25c4 100644 --- a/core/lib/vm/src/tracers/storage_invocations.rs +++ b/core/lib/vm/src/tracers/storage_invocations.rs @@ -10,7 +10,7 @@ use zksync_state::WriteStorage; #[derive(Debug, Default, Clone)] pub struct StorageInvocations { - limit: usize, + pub limit: usize, current: usize, } diff --git a/core/lib/vm/src/tracers/validation/error.rs b/core/lib/vm/src/tracers/validation/error.rs index 49afb22e10d..8fb104cb67a 100644 --- a/core/lib/vm/src/tracers/validation/error.rs +++ b/core/lib/vm/src/tracers/validation/error.rs @@ -1,15 +1,6 @@ use crate::Halt; use std::fmt::Display; -use zksync_types::{Address, U256}; -use zksync_utils::u256_to_h256; - -#[derive(Debug, Clone)] -pub enum ViolatedValidationRule { - TouchedUnallowedStorageSlots(Address, U256), - CalledContractWithNoCode(Address), - TouchedUnallowedContext, - TookTooManyComputationalGas(u32), -} +use zksync_types::vm_trace::ViolatedValidationRule; #[derive(Debug, Clone)] pub enum ValidationError { @@ -17,32 +8,6 @@ pub enum ValidationError { ViolatedRule(ViolatedValidationRule), } -impl Display for ViolatedValidationRule { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ViolatedValidationRule::TouchedUnallowedStorageSlots(contract, key) => write!( - f, - "Touched unallowed storage slots: address {}, key: {}", - hex::encode(contract), - hex::encode(u256_to_h256(*key)) - ), - ViolatedValidationRule::CalledContractWithNoCode(contract) => { - write!(f, "Called contract with no code: {}", hex::encode(contract)) - } - ViolatedValidationRule::TouchedUnallowedContext => { - write!(f, "Touched unallowed context") - } - ViolatedValidationRule::TookTooManyComputationalGas(gas_limit) => { - write!( - f, - "Took too many computational gas, allowed limit: {}", - gas_limit - ) - } - } - } -} - impl Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/core/lib/vm/src/tracers/validation/mod.rs b/core/lib/vm/src/tracers/validation/mod.rs index d85d031665a..dd06fe8e5db 100644 --- a/core/lib/vm/src/tracers/validation/mod.rs +++ b/core/lib/vm/src/tracers/validation/mod.rs @@ -19,7 +19,8 @@ use zksync_config::constants::{ use zksync_state::{StoragePtr, WriteStorage}; use zksync_types::{ - get_code_key, web3::signing::keccak256, AccountTreeId, Address, StorageKey, H256, U256, + get_code_key, vm_trace::ViolatedValidationRule, web3::signing::keccak256, AccountTreeId, + Address, StorageKey, H256, U256, }; use zksync_utils::{ be_bytes_to_safe_address, h256_to_account_address, u256_to_account_address, u256_to_h256, @@ -35,7 +36,7 @@ use crate::tracers::utils::{ computational_gas_price, get_calldata_page_via_abi, print_debug_if_needed, VmHook, }; -pub use error::{ValidationError, ViolatedValidationRule}; +pub use error::ValidationError; pub use params::ValidationTracerParams; use types::NewTrustedValidationItems; @@ -59,7 +60,7 @@ pub struct ValidationTracer { trusted_address_slots: HashSet<(Address, U256)>, computational_gas_used: u32, computational_gas_limit: u32, - result: Arc>, + pub result: Arc>, _marker: PhantomData H>, } @@ -192,6 +193,17 @@ impl ValidationTracer { } } + pub fn params(&self) -> ValidationTracerParams { + ValidationTracerParams { + user_address: self.user_address, + paymaster_address: self.paymaster_address, + trusted_slots: self.trusted_slots.clone(), + trusted_addresses: self.trusted_addresses.clone(), + trusted_address_slots: self.trusted_address_slots.clone(), + computational_gas_limit: self.computational_gas_limit, + } + } + fn check_user_restrictions( &mut self, state: VmLocalStateData<'_>, diff --git a/core/lib/vm/src/types/internals/vm_state.rs b/core/lib/vm/src/types/internals/vm_state.rs index 60969241295..fa478251501 100644 --- a/core/lib/vm/src/types/internals/vm_state.rs +++ b/core/lib/vm/src/types/internals/vm_state.rs @@ -24,8 +24,8 @@ use crate::constants::BOOTLOADER_HEAP_PAGE; use crate::old_vm::{ event_sink::InMemoryEventSink, history_recorder::HistoryMode, memory::SimpleMemory, oracles::decommitter::DecommitterOracle, oracles::precompile::PrecompilesProcessorWithHistory, - oracles::storage::StorageOracle, }; +use crate::oracles::storage::StorageOracle; use crate::types::inputs::{L1BatchEnv, SystemEnv}; use crate::utils::l2_blocks::{assert_next_block, load_last_l2_block}; use crate::L2Block; diff --git a/core/lib/zksync_core/src/api_server/execution_sandbox/tracers.rs b/core/lib/zksync_core/src/api_server/execution_sandbox/tracers.rs index ad6a65f1373..468bb4649e0 100644 --- a/core/lib/zksync_core/src/api_server/execution_sandbox/tracers.rs +++ b/core/lib/zksync_core/src/api_server/execution_sandbox/tracers.rs @@ -12,7 +12,10 @@ pub(crate) enum ApiTracer { } impl ApiTracer { - pub fn into_boxed( + pub fn into_boxed< + S: WriteStorage, + H: HistoryMode + multivm::HistoryMode + 'static, + >( self, ) -> Box> { match self { diff --git a/core/lib/zksync_core/src/api_server/tx_sender/mod.rs b/core/lib/zksync_core/src/api_server/tx_sender/mod.rs index b51a094a835..320345ac189 100644 --- a/core/lib/zksync_core/src/api_server/tx_sender/mod.rs +++ b/core/lib/zksync_core/src/api_server/tx_sender/mod.rs @@ -90,7 +90,8 @@ impl MultiVMBaseSystemContracts { ProtocolVersionId::Version13 => self.post_virtual_blocks, ProtocolVersionId::Version14 | ProtocolVersionId::Version15 - | ProtocolVersionId::Version16 => self.post_virtual_blocks_finish_upgrade_fix, + | ProtocolVersionId::Version16 + | ProtocolVersionId::Version17 => self.post_virtual_blocks_finish_upgrade_fix, } } } diff --git a/core/multivm_deps/vm_virtual_blocks/Cargo.toml b/core/multivm_deps/vm_virtual_blocks/Cargo.toml new file mode 100644 index 00000000000..02237d6bf83 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "vm_virtual_blocks" +version = "0.1.0" +edition = "2018" +authors = ["The Matter Labs Team "] +homepage = "https://zksync.io/" +repository = "https://github.com/matter-labs/zksync-era" +license = "MIT OR Apache-2.0" +keywords = ["blockchain", "zksync"] +categories = ["cryptography"] + +[dependencies] +vise = { git = "https://github.com/matter-labs/vise.git", version = "0.1.0", rev = "9d097ab747b037b6e62504df1db5b975425b6bdd" } +zk_evm = { git = "https://github.com/matter-labs/era-zk_evm.git", branch = "v1.3.3" } +zksync_config = { path = "../../lib/config" } +zksync_types = { path = "../../lib/types" } +zksync_utils = { path = "../../lib/utils" } +zksync_state = { path = "../../lib/state" } +zksync_contracts = { path = "../../lib/contracts" } + +anyhow = "1.0" +hex = "0.4" +itertools = "0.10" +once_cell = "1.7" +thiserror = "1.0" +tracing = "0.1" + +[dev-dependencies] +tokio = { version = "1", features = ["time"] } +zksync_test_account = { path = "../../lib/test_account" } +ethabi = "18.0.0" +zksync_eth_signer = { path = "../../lib/eth_signer" } + diff --git a/core/multivm_deps/vm_virtual_blocks/README.md b/core/multivm_deps/vm_virtual_blocks/README.md new file mode 100644 index 00000000000..d515df0dfc6 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/README.md @@ -0,0 +1,44 @@ +# VM Crate + +This crate contains code that interacts with the VM (Virtual Machine). The VM itself is in a separate repository +[era-zk_evm][zk_evm_repo_ext]. + +## VM Dependencies + +The VM relies on several subcomponents or traits, such as Memory and Storage. These traits are defined in the `zk_evm` +repository, while their implementations can be found in this crate, such as the storage implementation in +`oracles/storage.rs` and the Memory implementation in `memory.rs`. + +Many of these implementations also support easy rollbacks and history, which is useful when creating a block with +multiple transactions and needing to return the VM to a previous state if a transaction doesn't fit. + +## Running the VM + +To interact with the VM, you must initialize it with `L1BatchEnv`, which represents the initial parameters of the batch, +`SystemEnv`, that represents the system parameters, and a reference to the Storage. To execute a transaction, you have +to push the transaction into the bootloader memory and call the `execute_next_transaction` method. + +### Tracers + +The VM implementation allows for the addition of `Tracers`, which are activated before and after each instruction. This +provides a more in-depth look into the VM, collecting detailed debugging information and logs. More details can be found +in the `tracer/` directory. + +This VM also supports custom tracers. You can call the `inspect_next_transaction` method with a custom tracer and +receive the result of the execution. + +### Bootloader + +In the context of zkEVM, we usually think about transactions. However, from the VM's perspective, it runs a single +program called the bootloader, which internally processes multiple transactions. + +### Rollbacks + +The `VMInstance` in `vm.rs` allows for easy rollbacks. You can save the current state at any moment by calling +`make_snapshot()` and return to that state using `rollback_to_the_latest_snapshot()`. + +This rollback affects all subcomponents, such as memory, storage, and events, and is mainly used if a transaction +doesn't fit in a block. + +[zk_evm_repo]: https://github.com/matter-labs/zk_evm 'internal zk EVM repo' +[zk_evm_repo_ext]: https://github.com/matter-labs/era-zk_evm 'external zk EVM repo' diff --git a/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/l2_block.rs b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/l2_block.rs new file mode 100644 index 00000000000..8b08978a9ad --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/l2_block.rs @@ -0,0 +1,83 @@ +use std::cmp::Ordering; +use zksync_types::{MiniblockNumber, H256}; +use zksync_utils::concat_and_hash; + +use crate::bootloader_state::snapshot::L2BlockSnapshot; +use crate::bootloader_state::tx::BootloaderTx; +use crate::utils::l2_blocks::l2_block_hash; +use crate::{L2Block, L2BlockEnv}; + +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(super) 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( + MiniblockNumber(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/multivm_deps/vm_virtual_blocks/src/bootloader_state/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/mod.rs new file mode 100644 index 00000000000..73830de2759 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/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/multivm_deps/vm_virtual_blocks/src/bootloader_state/snapshot.rs b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/snapshot.rs new file mode 100644 index 00000000000..e417a3b9ee6 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/snapshot.rs @@ -0,0 +1,23 @@ +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, +} + +#[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/multivm_deps/vm_virtual_blocks/src/bootloader_state/state.rs b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/state.rs new file mode 100644 index 00000000000..ca6f54e233e --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/state.rs @@ -0,0 +1,254 @@ +use crate::bootloader_state::l2_block::BootloaderL2Block; +use crate::bootloader_state::snapshot::BootloaderStateSnapshot; +use crate::bootloader_state::utils::{apply_l2_block, apply_tx_to_memory}; +use std::cmp::Ordering; +use zksync_types::{L2ChainId, U256}; +use zksync_utils::bytecode::CompressedBytecodeInfo; + +use crate::constants::TX_DESCRIPTION_OFFSET; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::types::internals::TransactionData; +use crate::types::outputs::BootloaderMemory; +use crate::utils::l2_blocks::assert_next_block; +use crate::L2BlockEnv; + +use super::tx::BootloaderTx; +/// 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, +} + +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, + } + } + + pub(crate) fn set_refund_for_current_tx(&mut self, refund: u32) { + 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 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: u32, + 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() + } + + 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) + } + } + 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 this index must exist") + } + + 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 this index must exist") + } + + 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, + } + } + + 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); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/tx.rs b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/tx.rs new file mode 100644 index 00000000000..ecf40eca824 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/tx.rs @@ -0,0 +1,48 @@ +use crate::types::internals::TransactionData; +use zksync_types::{L2ChainId, H256, U256}; +use zksync_utils::bytecode::CompressedBytecodeInfo; + +/// Information about tx necessary for execution in bootloader. +#[derive(Debug, Clone)] +pub(super) struct BootloaderTx { + pub(super) hash: H256, + /// Encoded transaction + pub(super) encoded: Vec, + /// Compressed bytecodes, which has been published during this transaction + pub(super) compressed_bytecodes: Vec, + /// Refunds for this transaction + pub(super) refund: u32, + /// Gas overhead + pub(super) gas_overhead: u32, + /// Gas Limit for this transaction. It can be different from the gaslimit inside the transaction + pub(super) trusted_gas_limit: U256, + /// Offset of the tx in bootloader memory + pub(super) offset: usize, +} + +impl BootloaderTx { + pub(super) fn new( + tx: TransactionData, + predefined_refund: u32, + 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/multivm_deps/vm_virtual_blocks/src/bootloader_state/utils.rs b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/utils.rs new file mode 100644 index 00000000000..31ec2ede599 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/bootloader_state/utils.rs @@ -0,0 +1,140 @@ +use zksync_types::U256; +use zksync_utils::bytecode::CompressedBytecodeInfo; +use zksync_utils::{bytes_to_be_words, h256_to_u256}; + +use crate::bootloader_state::l2_block::BootloaderL2Block; +use crate::constants::{ + BOOTLOADER_TX_DESCRIPTION_OFFSET, BOOTLOADER_TX_DESCRIPTION_SIZE, COMPRESSED_BYTECODES_OFFSET, + 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, +}; +use crate::{BootloaderMemory, TxExecutionMode}; + +use super::tx::BootloaderTx; + +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 poitner + 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 infos 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(), + ), + ]) +} + +/// 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) +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/constants.rs b/core/multivm_deps/vm_virtual_blocks/src/constants.rs new file mode 100644 index 00000000000..a51688b851e --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/constants.rs @@ -0,0 +1,111 @@ +use zk_evm::aux_structures::MemoryPage; + +use zksync_config::constants::{ + L1_GAS_PER_PUBDATA_BYTE, MAX_L2_TX_GAS_LIMIT, MAX_NEW_FACTORY_DEPS, MAX_TXS_IN_BLOCK, + USED_BOOTLOADER_MEMORY_WORDS, +}; + +pub use zk_evm::zkevm_opcode_defs::system_params::{ + ERGS_PER_CIRCUIT, INITIAL_STORAGE_WRITE_PUBDATA_BYTES, MAX_PUBDATA_PER_BLOCK, +}; + +use crate::old_vm::utils::heap_page_from_base; + +/// Max cycles for a single transaction. +pub const MAX_CYCLES_FOR_TX: u32 = u32::MAX; + +/// The first 32 slots are reserved for debugging purposes +pub(crate) const DEBUG_SLOTS_OFFSET: usize = 8; +pub(crate) const DEBUG_FIRST_SLOTS: usize = 32; +/// The next 33 slots are reserved for dealing with the paymaster context (1 slot for storing length + 32 slots for storing the actual context). +pub(crate) const PAYMASTER_CONTEXT_SLOTS: usize = 32 + 1; +/// The next PAYMASTER_CONTEXT_SLOTS + 7 slots free slots are needed before each tx, so that the +/// postOp operation could be encoded correctly. +pub(crate) const MAX_POSTOP_SLOTS: usize = PAYMASTER_CONTEXT_SLOTS + 7; + +/// Slots used to store the current L2 transaction's hash and the hash recommended +/// to be used for signing the transaction's content. +const CURRENT_L2_TX_HASHES_SLOTS: usize = 2; + +/// Slots used to store the calldata for the KnownCodesStorage to mark new factory +/// dependencies as known ones. Besides the slots for the new factory dependencies themselves +/// another 4 slots are needed for: selector, marker of whether the user should pay for the pubdata, +/// the offset for the encoding of the array as well as the length of the array. +const NEW_FACTORY_DEPS_RESERVED_SLOTS: usize = MAX_NEW_FACTORY_DEPS + 4; + +/// The operator can provide for each transaction the proposed minimal refund +pub(crate) const OPERATOR_REFUNDS_SLOTS: usize = MAX_TXS_IN_BLOCK; + +pub(crate) const OPERATOR_REFUNDS_OFFSET: usize = DEBUG_SLOTS_OFFSET + + DEBUG_FIRST_SLOTS + + PAYMASTER_CONTEXT_SLOTS + + CURRENT_L2_TX_HASHES_SLOTS + + NEW_FACTORY_DEPS_RESERVED_SLOTS; + +pub(crate) const TX_OVERHEAD_OFFSET: usize = OPERATOR_REFUNDS_OFFSET + OPERATOR_REFUNDS_SLOTS; +pub(crate) const TX_OVERHEAD_SLOTS: usize = MAX_TXS_IN_BLOCK; + +pub(crate) const TX_TRUSTED_GAS_LIMIT_OFFSET: usize = TX_OVERHEAD_OFFSET + TX_OVERHEAD_SLOTS; +pub(crate) const TX_TRUSTED_GAS_LIMIT_SLOTS: usize = MAX_TXS_IN_BLOCK; + +pub(crate) const COMPRESSED_BYTECODES_SLOTS: usize = 32768; + +pub(crate) const BOOTLOADER_TX_DESCRIPTION_OFFSET: usize = + COMPRESSED_BYTECODES_OFFSET + COMPRESSED_BYTECODES_SLOTS; + +/// The size of the bootloader memory dedicated to the encodings of transactions +pub const BOOTLOADER_TX_ENCODING_SPACE: u32 = + (USED_BOOTLOADER_MEMORY_WORDS - TX_DESCRIPTION_OFFSET - MAX_TXS_IN_BLOCK) as u32; + +// Size of the bootloader tx description in words +pub(crate) const BOOTLOADER_TX_DESCRIPTION_SIZE: usize = 2; + +/// The actual descriptions of transactions should start after the minor descriptions and a MAX_POSTOP_SLOTS +/// free slots to allow postOp encoding. +pub(crate) const TX_DESCRIPTION_OFFSET: usize = BOOTLOADER_TX_DESCRIPTION_OFFSET + + BOOTLOADER_TX_DESCRIPTION_SIZE * MAX_TXS_IN_BLOCK + + MAX_POSTOP_SLOTS; + +pub(crate) const TX_GAS_LIMIT_OFFSET: usize = 4; + +const INITIAL_BASE_PAGE: u32 = 8; +pub const BOOTLOADER_HEAP_PAGE: u32 = heap_page_from_base(MemoryPage(INITIAL_BASE_PAGE)).0; +pub(crate) const BLOCK_OVERHEAD_GAS: u32 = 1200000; +pub(crate) const BLOCK_OVERHEAD_L1_GAS: u32 = 1000000; +pub const BLOCK_OVERHEAD_PUBDATA: u32 = BLOCK_OVERHEAD_L1_GAS / L1_GAS_PER_PUBDATA_BYTE; + +/// VM Hooks are used for communication between bootloader and tracers. +/// The 'type'/'opcode' is put into VM_HOOK_POSITION slot, +/// and VM_HOOKS_PARAMS_COUNT parameters (each 32 bytes) are put in the slots before. +/// So the layout looks like this: +/// [param 0][param 1][vmhook opcode] +pub const VM_HOOK_POSITION: u32 = RESULT_SUCCESS_FIRST_SLOT - 1; +pub const VM_HOOK_PARAMS_COUNT: u32 = 2; +pub const VM_HOOK_PARAMS_START_POSITION: u32 = VM_HOOK_POSITION - VM_HOOK_PARAMS_COUNT; + +pub(crate) const MAX_MEM_SIZE_BYTES: u32 = 16777216; // 2^24 + +/// Arbitrary space in memory closer to the end of the page +pub const RESULT_SUCCESS_FIRST_SLOT: u32 = + (MAX_MEM_SIZE_BYTES - (MAX_TXS_IN_BLOCK as u32) * 32) / 32; + +/// How many gas bootloader is allowed to spend within one block. +/// Note that this value doesn't correspond to the gas limit of any particular transaction +/// (except for the fact that, of course, gas limit for each transaction should be <= `BLOCK_GAS_LIMIT`). +pub const BLOCK_GAS_LIMIT: u32 = zk_evm::zkevm_opcode_defs::system_params::VM_INITIAL_FRAME_ERGS; + +/// How many gas is allowed to spend on a single transaction in eth_call method +pub const ETH_CALL_GAS_LIMIT: u32 = MAX_L2_TX_GAS_LIMIT as u32; + +/// ID of the transaction from L1 +pub const L1_TX_TYPE: u8 = 255; + +pub(crate) const TX_OPERATOR_L2_BLOCK_INFO_OFFSET: usize = + TX_TRUSTED_GAS_LIMIT_OFFSET + TX_TRUSTED_GAS_LIMIT_SLOTS; + +pub(crate) const TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO: usize = 4; +pub(crate) const TX_OPERATOR_L2_BLOCK_INFO_SLOTS: usize = + (MAX_TXS_IN_BLOCK + 1) * TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO; + +pub(crate) const COMPRESSED_BYTECODES_OFFSET: usize = + TX_OPERATOR_L2_BLOCK_INFO_OFFSET + TX_OPERATOR_L2_BLOCK_INFO_SLOTS; diff --git a/core/multivm_deps/vm_virtual_blocks/src/errors/bootloader_error.rs b/core/multivm_deps/vm_virtual_blocks/src/errors/bootloader_error.rs new file mode 100644 index 00000000000..07ed0899b22 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/errors/bootloader_error.rs @@ -0,0 +1,67 @@ +/// Error codes returned by the bootloader. +#[derive(Debug)] +pub(crate) enum BootloaderErrorCode { + EthCall, + AccountTxValidationFailed, + FailedToChargeFee, + FromIsNotAnAccount, + FailedToCheckAccount, + UnacceptableGasPrice, + PayForTxFailed, + PrePaymasterPreparationFailed, + PaymasterValidationFailed, + FailedToSendFeesToTheOperator, + FailedToSetPrevBlockHash, + UnacceptablePubdataPrice, + TxValidationError, + MaxPriorityFeeGreaterThanMaxFee, + BaseFeeGreaterThanMaxFeePerGas, + PaymasterReturnedInvalidContext, + PaymasterContextIsTooLong, + AssertionError, + FailedToMarkFactoryDeps, + TxValidationOutOfGas, + NotEnoughGasProvided, + AccountReturnedInvalidMagic, + PaymasterReturnedInvalidMagic, + MintEtherFailed, + FailedToAppendTransactionToL2Block, + FailedToSetL2Block, + FailedToPublishBlockDataToL1, + Unknown, +} + +impl From for BootloaderErrorCode { + fn from(code: u8) -> BootloaderErrorCode { + match code { + 0 => BootloaderErrorCode::EthCall, + 1 => BootloaderErrorCode::AccountTxValidationFailed, + 2 => BootloaderErrorCode::FailedToChargeFee, + 3 => BootloaderErrorCode::FromIsNotAnAccount, + 4 => BootloaderErrorCode::FailedToCheckAccount, + 5 => BootloaderErrorCode::UnacceptableGasPrice, + 6 => BootloaderErrorCode::FailedToSetPrevBlockHash, + 7 => BootloaderErrorCode::PayForTxFailed, + 8 => BootloaderErrorCode::PrePaymasterPreparationFailed, + 9 => BootloaderErrorCode::PaymasterValidationFailed, + 10 => BootloaderErrorCode::FailedToSendFeesToTheOperator, + 11 => BootloaderErrorCode::UnacceptablePubdataPrice, + 12 => BootloaderErrorCode::TxValidationError, + 13 => BootloaderErrorCode::MaxPriorityFeeGreaterThanMaxFee, + 14 => BootloaderErrorCode::BaseFeeGreaterThanMaxFeePerGas, + 15 => BootloaderErrorCode::PaymasterReturnedInvalidContext, + 16 => BootloaderErrorCode::PaymasterContextIsTooLong, + 17 => BootloaderErrorCode::AssertionError, + 18 => BootloaderErrorCode::FailedToMarkFactoryDeps, + 19 => BootloaderErrorCode::TxValidationOutOfGas, + 20 => BootloaderErrorCode::NotEnoughGasProvided, + 21 => BootloaderErrorCode::AccountReturnedInvalidMagic, + 22 => BootloaderErrorCode::PaymasterReturnedInvalidMagic, + 23 => BootloaderErrorCode::MintEtherFailed, + 24 => BootloaderErrorCode::FailedToAppendTransactionToL2Block, + 25 => BootloaderErrorCode::FailedToSetL2Block, + 26 => BootloaderErrorCode::FailedToPublishBlockDataToL1, + _ => BootloaderErrorCode::Unknown, + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/errors/bytecode_compression.rs b/core/multivm_deps/vm_virtual_blocks/src/errors/bytecode_compression.rs new file mode 100644 index 00000000000..c6cd094ae94 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/errors/bytecode_compression.rs @@ -0,0 +1,8 @@ +use thiserror::Error; + +/// Errors related to bytecode compression. +#[derive(Debug, Error)] +pub enum BytecodeCompressionError { + #[error("Bytecode compression failed")] + BytecodeCompressionFailed, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/errors/halt.rs b/core/multivm_deps/vm_virtual_blocks/src/errors/halt.rs new file mode 100644 index 00000000000..10c8a8d702b --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/errors/halt.rs @@ -0,0 +1,107 @@ +use crate::errors::VmRevertReason; +use std::fmt::{Display, Formatter}; + +/// Structure for non-contract errors from the Virtual Machine (EVM). + +/// Differentiates VM-specific issues from contract-related errors. +#[derive(Debug, Clone, PartialEq)] +pub enum Halt { + // Can only be returned in VerifyAndExecute + ValidationFailed(VmRevertReason), + PaymasterValidationFailed(VmRevertReason), + PrePaymasterPreparationFailed(VmRevertReason), + PayForTxFailed(VmRevertReason), + FailedToMarkFactoryDependencies(VmRevertReason), + FailedToChargeFee(VmRevertReason), + // Emitted when trying to call a transaction from an account that has not + // been deployed as an account (i.e. the `from` is just a contract). + // Can only be returned in VerifyAndExecute + FromIsNotAnAccount, + // Currently cannot be returned. Should be removed when refactoring errors. + InnerTxError, + Unknown(VmRevertReason), + // Temporarily used instead of panics to provide better experience for developers: + // their transaction would simply be rejected and they'll be able to provide + // information about the cause to us. + UnexpectedVMBehavior(String), + // Bootloader is out of gas. + BootloaderOutOfGas, + // Transaction has a too big gas limit and will not be executed by the server. + TooBigGasLimit, + // The bootloader did not have enough gas to start the transaction in the first place + NotEnoughGasProvided, + // The tx consumes too much missing invocations to memory + MissingInvocationLimitReached, + // Failed to set information about the L2 block + FailedToSetL2Block(String), + // Failed to publish information about the batch and the L2 block onto L1 + FailedToAppendTransactionToL2Block(String), + VMPanic, +} + +impl Display for Halt { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Halt::ValidationFailed(reason) => { + write!(f, "Account validation error: {}", reason) + } + Halt::FailedToChargeFee(reason) => { + write!(f, "Failed to charge fee: {}", reason) + } + // Emitted when trying to call a transaction from an account that has no + // been deployed as an account (i.e. the `from` is just a contract). + Halt::FromIsNotAnAccount => write!(f, "Sender is not an account"), + Halt::InnerTxError => write!(f, "Bootloader-based tx failed"), + Halt::PaymasterValidationFailed(reason) => { + write!(f, "Paymaster validation error: {}", reason) + } + Halt::PrePaymasterPreparationFailed(reason) => { + write!(f, "Pre-paymaster preparation error: {}", reason) + } + Halt::Unknown(reason) => write!(f, "Unknown reason: {}", reason), + Halt::UnexpectedVMBehavior(problem) => { + write!(f, + "virtual machine entered unexpected state. Please contact developers and provide transaction details \ + that caused this error. Error description: {problem}" + ) + } + Halt::BootloaderOutOfGas => write!(f, "Bootloader out of gas"), + Halt::NotEnoughGasProvided => write!( + f, + "Bootloader did not have enough gas to start the transaction" + ), + Halt::FailedToMarkFactoryDependencies(reason) => { + write!(f, "Failed to mark factory dependencies: {}", reason) + } + Halt::PayForTxFailed(reason) => { + write!(f, "Failed to pay for the transaction: {}", reason) + } + Halt::TooBigGasLimit => { + write!( + f, + "Transaction has a too big ergs limit and will not be executed by the server" + ) + } + Halt::MissingInvocationLimitReached => { + write!(f, "Tx produced too much cold storage accesses") + } + Halt::VMPanic => { + write!(f, "VM panicked") + } + Halt::FailedToSetL2Block(reason) => { + write!( + f, + "Failed to set information about the L2 block: {}", + reason + ) + } + Halt::FailedToAppendTransactionToL2Block(reason) => { + write!( + f, + "Failed to append the transaction to the current L2 block: {}", + reason + ) + } + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/errors/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/errors/mod.rs new file mode 100644 index 00000000000..43aecf79601 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/errors/mod.rs @@ -0,0 +1,11 @@ +pub(crate) use bootloader_error::BootloaderErrorCode; +pub use bytecode_compression::BytecodeCompressionError; +pub use halt::Halt; +pub use tx_revert_reason::TxRevertReason; +pub use vm_revert_reason::{VmRevertReason, VmRevertReasonParsingError}; + +mod bootloader_error; +mod bytecode_compression; +mod halt; +mod tx_revert_reason; +mod vm_revert_reason; diff --git a/core/multivm_deps/vm_virtual_blocks/src/errors/tx_revert_reason.rs b/core/multivm_deps/vm_virtual_blocks/src/errors/tx_revert_reason.rs new file mode 100644 index 00000000000..8e65b15a097 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/errors/tx_revert_reason.rs @@ -0,0 +1,138 @@ +use crate::errors::halt::Halt; + +use std::fmt::Display; + +use super::{BootloaderErrorCode, VmRevertReason}; + +#[derive(Debug, Clone, PartialEq)] +pub enum TxRevertReason { + // Returned when the execution of an L2 transaction has failed + // Or EthCall has failed + TxReverted(VmRevertReason), + // Returned when some validation has failed or some internal errors + Halt(Halt), +} + +impl TxRevertReason { + pub fn parse_error(bytes: &[u8]) -> Self { + // The first 32 bytes should correspond with error code. + // If the error is smaller than that, we will use a standardized bootloader error. + if bytes.is_empty() { + return Self::Halt(Halt::UnexpectedVMBehavior( + "Bootloader returned an empty error".to_string(), + )); + } + + let (error_code, error_msg) = bytes.split_at(1); + let revert_reason = VmRevertReason::from(error_msg); + + // `error_code` is a big-endian number, so we can safely take the first byte of it. + match BootloaderErrorCode::from(error_code[0]) { + BootloaderErrorCode::EthCall => Self::TxReverted(revert_reason), + BootloaderErrorCode::AccountTxValidationFailed => Self::Halt(Halt::ValidationFailed(revert_reason)), + BootloaderErrorCode::FailedToChargeFee => Self::Halt(Halt::FailedToChargeFee(revert_reason)), + BootloaderErrorCode::FromIsNotAnAccount => Self::Halt(Halt::FromIsNotAnAccount), + BootloaderErrorCode::FailedToCheckAccount => Self::Halt(Halt::ValidationFailed(VmRevertReason::General { + msg: "Failed to check if `from` is an account. Most likely not enough gas provided".to_string(), + data: vec![], + })), + BootloaderErrorCode::UnacceptableGasPrice => Self::Halt(Halt::UnexpectedVMBehavior( + "The operator included transaction with an unacceptable gas price".to_owned(), + )), + BootloaderErrorCode::PrePaymasterPreparationFailed => { + Self::Halt(Halt::PrePaymasterPreparationFailed(revert_reason)) + } + BootloaderErrorCode::PaymasterValidationFailed => { + Self::Halt(Halt::PaymasterValidationFailed(revert_reason)) + } + BootloaderErrorCode::FailedToSendFeesToTheOperator => { + Self::Halt(Halt::UnexpectedVMBehavior("FailedToSendFeesToTheOperator".to_owned())) + } + BootloaderErrorCode::FailedToSetPrevBlockHash => { + panic!( + "The bootloader failed to set previous block hash. Reason: {}", + revert_reason + ) + } + BootloaderErrorCode::UnacceptablePubdataPrice => { + Self::Halt(Halt::UnexpectedVMBehavior("UnacceptablePubdataPrice".to_owned())) + } + // This is different from AccountTxValidationFailed error in a way that it means that + // the error was not produced by the account itself, but for some other unknown reason (most likely not enough gas) + BootloaderErrorCode::TxValidationError => Self::Halt(Halt::ValidationFailed(revert_reason)), + // Note, that `InnerTxError` is derived only after the actual tx execution, so + // it is not parsed here. Unknown error means that bootloader failed by a reason + // that was not specified by the protocol: + BootloaderErrorCode::MaxPriorityFeeGreaterThanMaxFee => { + Self::Halt(Halt::UnexpectedVMBehavior("Max priority fee greater than max fee".to_owned())) + } + BootloaderErrorCode::PaymasterReturnedInvalidContext => { + Self::Halt(Halt::PaymasterValidationFailed(VmRevertReason::General { + msg: String::from("Paymaster returned invalid context"), + data: vec![], + })) + } + BootloaderErrorCode::PaymasterContextIsTooLong => { + Self::Halt(Halt::PaymasterValidationFailed(VmRevertReason::General { + msg: String::from("Paymaster returned context that is too long"), + data: vec![], + })) + } + BootloaderErrorCode::AssertionError => { + Self::Halt(Halt::UnexpectedVMBehavior(format!("Assertion error: {}", revert_reason))) + } + BootloaderErrorCode::BaseFeeGreaterThanMaxFeePerGas => Self::Halt(Halt::UnexpectedVMBehavior( + "Block.basefee is greater than max fee per gas".to_owned(), + )), + BootloaderErrorCode::PayForTxFailed => { + Self::Halt(Halt::PayForTxFailed(revert_reason)) + }, + BootloaderErrorCode::FailedToMarkFactoryDeps => { + let (msg, data) = if let VmRevertReason::General { msg , data} = revert_reason { + (msg, data) + } else { + (String::from("Most likely not enough gas provided"), vec![]) + }; + Self::Halt(Halt::FailedToMarkFactoryDependencies(VmRevertReason::General { + msg, data + })) + }, + BootloaderErrorCode::TxValidationOutOfGas => { + Self::Halt(Halt::ValidationFailed(VmRevertReason::General { msg: String::from("Not enough gas for transaction validation"), data: vec![] })) + }, + BootloaderErrorCode::NotEnoughGasProvided => { + Self::Halt(Halt::NotEnoughGasProvided) + }, + BootloaderErrorCode::AccountReturnedInvalidMagic => { + Self::Halt(Halt::ValidationFailed(VmRevertReason::General { msg: String::from("Account validation returned invalid magic value. Most often this means that the signature is incorrect"), data: vec![] })) + }, + BootloaderErrorCode::PaymasterReturnedInvalidMagic => { + Self::Halt(Halt::ValidationFailed(VmRevertReason::General { msg: String::from("Paymaster validation returned invalid magic value. Please refer to the documentation of the paymaster for more details"), data: vec![] })) + } + BootloaderErrorCode::Unknown => Self::Halt(Halt::UnexpectedVMBehavior(format!( + "Unsupported error code: {}. Revert reason: {}", + error_code[0], revert_reason + ))), + BootloaderErrorCode::MintEtherFailed => Self::Halt(Halt::UnexpectedVMBehavior(format!("Failed to mint ether: {}", revert_reason))), + BootloaderErrorCode::FailedToAppendTransactionToL2Block => { + Self::Halt(Halt::FailedToAppendTransactionToL2Block(format!("Failed to append transaction to L2 block: {}", revert_reason))) + } + BootloaderErrorCode::FailedToSetL2Block => { + Self::Halt(Halt::FailedToSetL2Block(format!("{}", revert_reason))) + + } + BootloaderErrorCode::FailedToPublishBlockDataToL1 => { + Self::Halt(Halt::UnexpectedVMBehavior(format!("Failed to publish block data to L1: {}", revert_reason))) + } + } + } +} + +impl Display for TxRevertReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + TxRevertReason::TxReverted(reason) => write!(f, "{}", reason), + TxRevertReason::Halt(reason) => write!(f, "{}", reason), + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/errors/vm_revert_reason.rs b/core/multivm_deps/vm_virtual_blocks/src/errors/vm_revert_reason.rs new file mode 100644 index 00000000000..531d8b5507f --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/errors/vm_revert_reason.rs @@ -0,0 +1,252 @@ +use std::fmt::{Debug, Display}; + +use zksync_types::U256; + +#[derive(Debug, thiserror::Error)] +pub enum VmRevertReasonParsingError { + #[error("Incorrect data offset. Data: {0:?}")] + IncorrectDataOffset(Vec), + #[error("Input is too short. Data: {0:?}")] + InputIsTooShort(Vec), + #[error("Incorrect string length. Data: {0:?}")] + IncorrectStringLength(Vec), +} + +/// Rich Revert Reasons https://github.com/0xProject/ZEIPs/issues/32 +#[derive(Debug, Clone, PartialEq)] +pub enum VmRevertReason { + General { + msg: String, + data: Vec, + }, + InnerTxError, + VmError, + Unknown { + function_selector: Vec, + data: Vec, + }, +} + +impl VmRevertReason { + const GENERAL_ERROR_SELECTOR: &'static [u8] = &[0x08, 0xc3, 0x79, 0xa0]; + fn parse_general_error(raw_bytes: &[u8]) -> Result { + let bytes = &raw_bytes[4..]; + if bytes.len() < 32 { + return Err(VmRevertReasonParsingError::InputIsTooShort(bytes.to_vec())); + } + let data_offset = U256::from_big_endian(&bytes[0..32]).as_usize(); + + // Data offset couldn't be less than 32 because data offset size is 32 bytes + // and data offset bytes are part of the offset. Also data offset couldn't be greater than + // data length + if data_offset > bytes.len() || data_offset < 32 { + return Err(VmRevertReasonParsingError::IncorrectDataOffset( + bytes.to_vec(), + )); + }; + + let data = &bytes[data_offset..]; + + if data.len() < 32 { + return Err(VmRevertReasonParsingError::InputIsTooShort(bytes.to_vec())); + }; + + let string_length = U256::from_big_endian(&data[0..32]).as_usize(); + + if string_length + 32 > data.len() { + return Err(VmRevertReasonParsingError::IncorrectStringLength( + bytes.to_vec(), + )); + }; + + let raw_data = &data[32..32 + string_length]; + Ok(Self::General { + msg: String::from_utf8_lossy(raw_data).to_string(), + data: raw_bytes.to_vec(), + }) + } + + pub fn to_user_friendly_string(&self) -> String { + match self { + // In case of `Unknown` reason we suppress it to prevent verbose Error function_selector = 0x{} + // message shown to user. + VmRevertReason::Unknown { .. } => "".to_owned(), + _ => self.to_string(), + } + } + + pub fn encoded_data(&self) -> Vec { + match self { + VmRevertReason::Unknown { data, .. } => data.clone(), + VmRevertReason::General { data, .. } => data.clone(), + _ => vec![], + } + } + + fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < 4 { + // Note, that when the method reverts with no data + // the selector is empty as well. + // For now, we only accept errors with either no data or + // the data with complete selectors. + if !bytes.is_empty() { + return Err(VmRevertReasonParsingError::IncorrectStringLength( + bytes.to_owned(), + )); + } + + let result = VmRevertReason::Unknown { + function_selector: vec![], + data: bytes.to_vec(), + }; + + return Ok(result); + } + + let function_selector = &bytes[0..4]; + match function_selector { + VmRevertReason::GENERAL_ERROR_SELECTOR => Self::parse_general_error(bytes), + _ => { + let result = VmRevertReason::Unknown { + function_selector: function_selector.to_vec(), + data: bytes.to_vec(), + }; + tracing::warn!("Unsupported error type: {}", result); + Ok(result) + } + } + } +} + +impl From<&[u8]> for VmRevertReason { + fn from(error_msg: &[u8]) -> Self { + match Self::try_from_bytes(error_msg) { + Ok(reason) => reason, + Err(_) => { + let function_selector = if error_msg.len() >= 4 { + error_msg[0..4].to_vec() + } else { + error_msg.to_vec() + }; + + let data = if error_msg.len() > 4 { + error_msg[4..].to_vec() + } else { + vec![] + }; + + VmRevertReason::Unknown { + function_selector, + data, + } + } + } + } +} + +impl Display for VmRevertReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use VmRevertReason::{General, InnerTxError, Unknown, VmError}; + + match self { + General { msg, .. } => write!(f, "{}", msg), + VmError => write!(f, "VM Error",), + InnerTxError => write!(f, "Bootloader-based tx failed"), + Unknown { + function_selector, + data, + } => write!( + f, + "Error function_selector = 0x{}, data = 0x{}", + hex::encode(function_selector), + hex::encode(data) + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::VmRevertReason; + + #[test] + fn revert_reason_parsing() { + let msg = 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, 38, 69, 82, 67, 50, 48, 58, 32, 116, 114, 97, 110, + 115, 102, 101, 114, 32, 97, 109, 111, 117, 110, 116, 32, 101, 120, 99, 101, 101, 100, + 115, 32, 98, 97, 108, 97, 110, 99, 101, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let reason = VmRevertReason::try_from_bytes(msg.as_slice()).expect("Shouldn't be error"); + assert_eq!( + reason, + VmRevertReason::General { + msg: "ERC20: transfer amount exceeds balance".to_string(), + data: msg + } + ); + } + + #[test] + fn revert_reason_with_wrong_function_selector() { + let msg = vec![ + 8, 195, 121, 161, 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, 38, 69, 82, 67, 50, 48, 58, 32, 116, 114, 97, 110, + 115, 102, 101, 114, 32, 97, 109, 111, 117, 110, 116, 32, 101, 120, 99, 101, 101, 100, + 115, 32, 98, 97, 108, 97, 110, 99, 101, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let reason = VmRevertReason::try_from_bytes(msg.as_slice()).expect("Shouldn't be error"); + assert!(matches!(reason, VmRevertReason::Unknown { .. })); + } + + #[test] + fn revert_reason_with_wrong_data_offset() { + let msg = 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, 31, 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, 38, 69, 82, 67, 50, 48, 58, 32, 116, 114, 97, 110, + 115, 102, 101, 114, 32, 97, 109, 111, 117, 110, 116, 32, 101, 120, 99, 101, 101, 100, + 115, 32, 98, 97, 108, 97, 110, 99, 101, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let reason = VmRevertReason::try_from_bytes(msg.as_slice()); + assert!(reason.is_err()); + } + + #[test] + fn revert_reason_with_big_data_offset() { + let msg = 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, 132, 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, 38, 69, 82, 67, 50, 48, 58, 32, 116, 114, 97, 110, + 115, 102, 101, 114, 32, 97, 109, 111, 117, 110, 116, 32, 101, 120, 99, 101, 101, 100, + 115, 32, 98, 97, 108, 97, 110, 99, 101, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let reason = VmRevertReason::try_from_bytes(msg.as_slice()); + assert!(reason.is_err()); + } + + #[test] + fn revert_reason_with_wrong_string_length() { + let msg = 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, 158, 69, 82, 67, 50, 48, 58, 32, 116, 114, 97, 110, + 115, 102, 101, 114, 32, 97, 109, 111, 117, 110, 116, 32, 101, 120, 99, 101, 101, 100, + 115, 32, 98, 97, 108, 97, 110, 99, 101, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let reason = VmRevertReason::try_from_bytes(msg.as_slice()); + assert!(reason.is_err()); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/implementation/bytecode.rs b/core/multivm_deps/vm_virtual_blocks/src/implementation/bytecode.rs new file mode 100644 index 00000000000..053d980bad7 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/implementation/bytecode.rs @@ -0,0 +1,57 @@ +use itertools::Itertools; + +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::U256; +use zksync_utils::bytecode::{compress_bytecode, hash_bytecode, CompressedBytecodeInfo}; +use zksync_utils::bytes_to_be_words; + +use crate::{HistoryMode, Vm}; + +impl Vm { + /// Checks the last transaction has successfully published compressed bytecodes and returns `true` if there is at least one is still unknown. + pub(crate) fn has_unpublished_bytecodes(&mut self) -> bool { + self.get_last_tx_compressed_bytecodes().iter().any(|info| { + !self + .state + .storage + .storage + .get_ptr() + .borrow_mut() + .is_bytecode_known(&hash_bytecode(&info.original)) + }) + } +} + +/// Converts bytecode to tokens and hashes it. +pub(crate) fn bytecode_to_factory_dep(bytecode: Vec) -> (U256, Vec) { + let bytecode_hash = hash_bytecode(&bytecode); + let bytecode_hash = U256::from_big_endian(bytecode_hash.as_bytes()); + + let bytecode_words = bytes_to_be_words(bytecode); + + (bytecode_hash, bytecode_words) +} + +pub(crate) fn compress_bytecodes( + bytecodes: &[Vec], + storage: StoragePtr, +) -> Vec { + bytecodes + .iter() + .enumerate() + .sorted_by_key(|(_idx, dep)| *dep) + .dedup_by(|x, y| x.1 == y.1) + .filter(|(_idx, dep)| !storage.borrow_mut().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/multivm_deps/vm_virtual_blocks/src/implementation/execution.rs b/core/multivm_deps/vm_virtual_blocks/src/implementation/execution.rs new file mode 100644 index 00000000000..9944a37f7e8 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/implementation/execution.rs @@ -0,0 +1,123 @@ +use zk_evm::aux_structures::Timestamp; +use zksync_state::WriteStorage; + +use crate::old_vm::{ + history_recorder::HistoryMode, + utils::{vm_may_have_ended_inner, VmExecutionResult}, +}; +use crate::tracers::{ + traits::{BoxedTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}, + DefaultExecutionTracer, RefundsTracer, +}; +use crate::types::{inputs::VmExecutionMode, outputs::VmExecutionResultAndLogs}; +use crate::vm::Vm; +use crate::VmExecutionStopReason; + +impl Vm { + pub(crate) fn inspect_inner( + &mut self, + mut tracers: Vec>>, + execution_mode: VmExecutionMode, + ) -> VmExecutionResultAndLogs { + if let VmExecutionMode::OneTx = execution_mode { + // For correct results we have to include refunds tracer to the desired tracers + tracers.push(RefundsTracer::new(self.batch_env.clone()).into_boxed()); + // Move the pointer to the next transaction + self.bootloader_state.move_tx_to_execute_pointer(); + } + let (_, result) = self.inspect_and_collect_results(tracers, execution_mode); + result + } + + /// Execute VM with given traces until the stop reason is reached. + /// Collect the result from the default tracers. + fn inspect_and_collect_results( + &mut self, + tracers: Vec>>, + execution_mode: VmExecutionMode, + ) -> (VmExecutionStopReason, VmExecutionResultAndLogs) { + let mut tx_tracer: DefaultExecutionTracer = DefaultExecutionTracer::new( + self.system_env.default_validation_computational_gas_limit, + execution_mode, + tracers, + self.storage.clone(), + ); + + let timestamp_initial = Timestamp(self.state.local_state.timestamp); + let cycles_initial = self.state.local_state.monotonic_cycle_counter; + let gas_remaining_before = self.gas_remaining(); + let spent_pubdata_counter_before = self.state.local_state.spent_pubdata_counter; + + let stop_reason = self.execute_with_default_tracer(&mut tx_tracer); + + let gas_remaining_after = self.gas_remaining(); + + let logs = self.collect_execution_logs_after_timestamp(timestamp_initial); + + let statistics = self.get_statistics( + timestamp_initial, + cycles_initial, + &tx_tracer, + gas_remaining_before, + gas_remaining_after, + spent_pubdata_counter_before, + logs.total_log_queries_count, + ); + + let result = tx_tracer.result_tracer.into_result(); + + let mut result = VmExecutionResultAndLogs { + result, + logs, + statistics, + refunds: Default::default(), + }; + + for tracer in tx_tracer.custom_tracers.iter_mut() { + tracer.save_results(&mut result); + } + (stop_reason, result) + } + + /// Execute vm with given tracers until the stop reason is reached. + fn execute_with_default_tracer( + &mut self, + tracer: &mut DefaultExecutionTracer, + ) -> VmExecutionStopReason { + tracer.initialize_tracer(&mut self.state); + let result = loop { + // Sanity check: we should never reach the maximum value, because then we won't be able to process the next cycle. + assert_ne!( + self.state.local_state.monotonic_cycle_counter, + u32::MAX, + "VM reached maximum possible amount of cycles. Vm state: {:?}", + self.state + ); + + tracer.before_cycle(&mut self.state); + self.state + .cycle(tracer) + .expect("Failed execution VM cycle."); + + tracer.after_cycle(&mut self.state, &mut self.bootloader_state); + if self.has_ended() { + break VmExecutionStopReason::VmFinished; + } + + if tracer.should_stop_execution() { + break VmExecutionStopReason::TracerRequestedStop; + } + }; + tracer.after_vm_execution(&mut self.state, &self.bootloader_state, result); + result + } + + fn has_ended(&self) -> bool { + match vm_may_have_ended_inner(&self.state) { + None | Some(VmExecutionResult::MostLikelyDidNotFinish(_, _)) => false, + Some( + VmExecutionResult::Ok(_) | VmExecutionResult::Revert(_) | VmExecutionResult::Panic, + ) => true, + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/implementation/gas.rs b/core/multivm_deps/vm_virtual_blocks/src/implementation/gas.rs new file mode 100644 index 00000000000..a7938125540 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/implementation/gas.rs @@ -0,0 +1,42 @@ +use zksync_state::WriteStorage; + +use crate::old_vm::history_recorder::HistoryMode; +use crate::tracers::DefaultExecutionTracer; +use crate::vm::Vm; + +impl Vm { + /// Returns the amount of gas remaining to the VM. + /// Note that this *does not* correspond to the gas limit of a transaction. + /// To calculate the amount of gas spent by transaction, you should call this method before and after + /// the execution, and subtract these values. + /// + /// Note: this method should only be called when either transaction is fully completed or VM completed + /// its execution. Remaining gas value is read from the current stack frame, so if you'll attempt to + /// read it during the transaction execution, you may receive invalid value. + pub(crate) fn gas_remaining(&self) -> u32 { + self.state.local_state.callstack.current.ergs_remaining + } + + pub(crate) fn calculate_computational_gas_used( + &self, + tracer: &DefaultExecutionTracer, + gas_remaining_before: u32, + spent_pubdata_counter_before: u32, + ) -> u32 { + let total_gas_used = gas_remaining_before + .checked_sub(self.gas_remaining()) + .expect("underflow"); + let gas_used_on_pubdata = + tracer.gas_spent_on_pubdata(&self.state.local_state) - spent_pubdata_counter_before; + total_gas_used + .checked_sub(gas_used_on_pubdata) + .unwrap_or_else(|| { + tracing::error!( + "Gas used on pubdata is greater than total gas used. On pubdata: {}, total: {}", + gas_used_on_pubdata, + total_gas_used + ); + 0 + }) + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/implementation/logs.rs b/core/multivm_deps/vm_virtual_blocks/src/implementation/logs.rs new file mode 100644 index 00000000000..6bc095740ef --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/implementation/logs.rs @@ -0,0 +1,64 @@ +use zk_evm::aux_structures::Timestamp; +use zksync_state::WriteStorage; + +use zksync_types::l2_to_l1_log::L2ToL1Log; +use zksync_types::tx::tx_execution_info::VmExecutionLogs; +use zksync_types::VmEvent; + +use crate::old_vm::events::merge_events; +use crate::old_vm::history_recorder::HistoryMode; +use crate::old_vm::utils::precompile_calls_count_after_timestamp; +use crate::vm::Vm; + +impl Vm { + pub(crate) fn collect_execution_logs_after_timestamp( + &self, + from_timestamp: Timestamp, + ) -> VmExecutionLogs { + let storage_logs: Vec<_> = self + .state + .storage + .storage_log_queries_after_timestamp(from_timestamp) + .iter() + .map(|log| **log) + .collect(); + let storage_logs_count = storage_logs.len(); + + let (events, l2_to_l1_logs) = + self.collect_events_and_l1_logs_after_timestamp(from_timestamp); + + let log_queries = self + .state + .event_sink + .log_queries_after_timestamp(from_timestamp); + + let precompile_calls_count = precompile_calls_count_after_timestamp( + self.state.precompiles_processor.timestamp_history.inner(), + from_timestamp, + ); + + let total_log_queries_count = + storage_logs_count + log_queries.len() + precompile_calls_count; + VmExecutionLogs { + storage_logs, + events, + l2_to_l1_logs, + total_log_queries_count, + } + } + + pub(crate) fn collect_events_and_l1_logs_after_timestamp( + &self, + from_timestamp: Timestamp, + ) -> (Vec, Vec) { + let (raw_events, l1_messages) = self + .state + .event_sink + .get_events_and_l2_l1_logs_after_timestamp(from_timestamp); + let events = merge_events(raw_events) + .into_iter() + .map(|e| e.into_vm_event(self.batch_env.number)) + .collect(); + (events, l1_messages.into_iter().map(Into::into).collect()) + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/implementation/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/implementation/mod.rs new file mode 100644 index 00000000000..161732cf034 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/implementation/mod.rs @@ -0,0 +1,7 @@ +mod bytecode; +mod execution; +mod gas; +mod logs; +mod snapshots; +mod statistics; +mod tx; diff --git a/core/multivm_deps/vm_virtual_blocks/src/implementation/snapshots.rs b/core/multivm_deps/vm_virtual_blocks/src/implementation/snapshots.rs new file mode 100644 index 00000000000..e3ddb14a59e --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/implementation/snapshots.rs @@ -0,0 +1,92 @@ +use vise::{Buckets, EncodeLabelSet, EncodeLabelValue, Family, Histogram, Metrics}; + +use std::time::Duration; + +use zk_evm::aux_structures::Timestamp; +use zksync_state::WriteStorage; + +use crate::{ + old_vm::{history_recorder::HistoryEnabled, oracles::OracleWithHistory}, + types::internals::VmSnapshot, + vm::Vm, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EncodeLabelSet, EncodeLabelValue)] +#[metrics(label = "stage", rename_all = "snake_case")] +enum RollbackStage { + DecommitmentProcessorRollback, + EventSinkRollback, + StorageRollback, + MemoryRollback, + PrecompilesProcessorRollback, + ApplyBootloaderSnapshot, +} + +#[derive(Debug, Metrics)] +#[metrics(prefix = "server_vm_virtual_blocks")] +struct VmMetrics { + #[metrics(buckets = Buckets::LATENCIES)] + rollback_time: Family>, +} + +#[vise::register] +static METRICS: vise::Global = vise::Global::new(); + +/// Implementation of VM related to rollbacks inside virtual machine +impl Vm { + pub(crate) fn make_snapshot_inner(&mut self) { + self.snapshots.push(VmSnapshot { + // Vm local state contains O(1) various parameters (registers/etc). + // The only "expensive" copying here is copying of the callstack. + // It will take O(callstack_depth) to copy it. + // So it is generally recommended to get snapshots of the bootloader frame, + // where the depth is 1. + local_state: self.state.local_state.clone(), + bootloader_state: self.bootloader_state.get_snapshot(), + }); + } + + pub(crate) fn rollback_to_snapshot(&mut self, snapshot: VmSnapshot) { + let VmSnapshot { + local_state, + bootloader_state, + } = snapshot; + + let stage_latency = + METRICS.rollback_time[&RollbackStage::DecommitmentProcessorRollback].start(); + let timestamp = Timestamp(local_state.timestamp); + tracing::trace!("Rolling back decomitter"); + self.state + .decommittment_processor + .rollback_to_timestamp(timestamp); + stage_latency.observe(); + + let stage_latency = METRICS.rollback_time[&RollbackStage::EventSinkRollback].start(); + tracing::trace!("Rolling back event_sink"); + self.state.event_sink.rollback_to_timestamp(timestamp); + stage_latency.observe(); + + let stage_latency = METRICS.rollback_time[&RollbackStage::StorageRollback].start(); + tracing::trace!("Rolling back storage"); + self.state.storage.rollback_to_timestamp(timestamp); + stage_latency.observe(); + + let stage_latency = METRICS.rollback_time[&RollbackStage::MemoryRollback].start(); + tracing::trace!("Rolling back memory"); + self.state.memory.rollback_to_timestamp(timestamp); + stage_latency.observe(); + + let stage_latency = + METRICS.rollback_time[&RollbackStage::PrecompilesProcessorRollback].start(); + tracing::trace!("Rolling back precompiles_processor"); + self.state + .precompiles_processor + .rollback_to_timestamp(timestamp); + stage_latency.observe(); + + self.state.local_state = local_state; + let stage_latency = METRICS.rollback_time[&RollbackStage::ApplyBootloaderSnapshot].start(); + self.bootloader_state.apply_snapshot(bootloader_state); + stage_latency.observe(); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/implementation/statistics.rs b/core/multivm_deps/vm_virtual_blocks/src/implementation/statistics.rs new file mode 100644 index 00000000000..54b77d57494 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/implementation/statistics.rs @@ -0,0 +1,87 @@ +use zk_evm::aux_structures::Timestamp; +use zksync_state::WriteStorage; + +use zksync_types::U256; + +use crate::old_vm::history_recorder::HistoryMode; +use crate::tracers::DefaultExecutionTracer; +use crate::types::outputs::VmExecutionStatistics; +use crate::vm::Vm; + +use crate::VmMemoryMetrics; + +/// Module responsible for observing the VM behavior, i.e. calculating the statistics of the VM runs +/// or reporting the VM memory usage. + +impl Vm { + /// Get statistics about TX execution. + #[allow(clippy::too_many_arguments)] + pub(crate) fn get_statistics( + &self, + timestamp_initial: Timestamp, + cycles_initial: u32, + tracer: &DefaultExecutionTracer, + gas_remaining_before: u32, + gas_remaining_after: u32, + spent_pubdata_counter_before: u32, + total_log_queries_count: usize, + ) -> VmExecutionStatistics { + let computational_gas_used = self.calculate_computational_gas_used( + tracer, + gas_remaining_before, + spent_pubdata_counter_before, + ); + VmExecutionStatistics { + contracts_used: self + .state + .decommittment_processor + .get_decommitted_bytecodes_after_timestamp(timestamp_initial), + cycles_used: self.state.local_state.monotonic_cycle_counter - cycles_initial, + gas_used: gas_remaining_before - gas_remaining_after, + computational_gas_used, + total_log_queries: total_log_queries_count, + } + } + + /// Returns the hashes the bytecodes that have been decommitted by the decomittment processor. + pub(crate) fn get_used_contracts(&self) -> Vec { + self.state + .decommittment_processor + .decommitted_code_hashes + .inner() + .keys() + .cloned() + .collect() + } + + /// Returns the info about all oracles' sizes. + pub fn record_vm_memory_metrics(&self) -> VmMemoryMetrics { + VmMemoryMetrics { + event_sink_inner: self.state.event_sink.get_size(), + event_sink_history: self.state.event_sink.get_history_size(), + memory_inner: self.state.memory.get_size(), + memory_history: self.state.memory.get_history_size(), + decommittment_processor_inner: self.state.decommittment_processor.get_size(), + decommittment_processor_history: self.state.decommittment_processor.get_history_size(), + storage_inner: self.state.storage.get_size(), + storage_history: self.state.storage.get_history_size(), + } + } +} + +impl VmMemoryMetrics { + pub fn full_size(&self) -> usize { + [ + self.event_sink_inner, + self.event_sink_history, + self.memory_inner, + self.memory_history, + self.decommittment_processor_inner, + self.decommittment_processor_history, + self.storage_inner, + self.storage_history, + ] + .iter() + .sum::() + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/implementation/tx.rs b/core/multivm_deps/vm_virtual_blocks/src/implementation/tx.rs new file mode 100644 index 00000000000..8341782d8ab --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/implementation/tx.rs @@ -0,0 +1,65 @@ +use crate::constants::BOOTLOADER_HEAP_PAGE; +use crate::implementation::bytecode::{bytecode_to_factory_dep, compress_bytecodes}; +use zk_evm::aux_structures::Timestamp; +use zksync_state::WriteStorage; +use zksync_types::l1::is_l1_tx_type; +use zksync_types::Transaction; + +use crate::old_vm::history_recorder::HistoryMode; +use crate::types::internals::TransactionData; +use crate::vm::Vm; + +impl Vm { + pub(crate) fn push_raw_transaction( + &mut self, + tx: TransactionData, + predefined_overhead: u32, + predefined_refund: u32, + with_compression: bool, + ) { + let timestamp = Timestamp(self.state.local_state.timestamp); + let codes_for_decommiter = tx + .factory_deps + .iter() + .map(|dep| bytecode_to_factory_dep(dep.clone())) + .collect(); + + 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, self.state.storage.storage.get_ptr()) + }; + + self.state + .decommittment_processor + .populate(codes_for_decommiter, timestamp); + + let trusted_ergs_limit = + tx.trusted_ergs_limit(self.batch_env.block_gas_price_per_pubdata()); + + let memory = self.bootloader_state.push_tx( + tx, + predefined_overhead, + predefined_refund, + compressed_bytecodes, + trusted_ergs_limit, + self.system_env.chain_id, + ); + + self.state + .memory + .populate_page(BOOTLOADER_HEAP_PAGE as usize, memory, timestamp); + } + + pub(crate) fn push_transaction_with_compression( + &mut self, + tx: Transaction, + with_compression: bool, + ) { + let tx: TransactionData = tx.into(); + let block_gas_per_pubdata_byte = self.batch_env.block_gas_price_per_pubdata(); + let overhead = tx.overhead_gas(block_gas_per_pubdata_byte as u32); + self.push_raw_transaction(tx, overhead, 0, with_compression); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/lib.rs b/core/multivm_deps/vm_virtual_blocks/src/lib.rs new file mode 100644 index 00000000000..6c356dbdff9 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/lib.rs @@ -0,0 +1,49 @@ +#![deny(unreachable_pub)] +#![deny(unused_crate_dependencies)] +#![warn(unused_extern_crates)] +#![warn(unused_imports)] + +pub use old_vm::{ + history_recorder::{HistoryDisabled, HistoryEnabled, HistoryMode}, + memory::SimpleMemory, + oracles::storage::StorageOracle, +}; + +pub use errors::{ + BytecodeCompressionError, Halt, TxRevertReason, VmRevertReason, VmRevertReasonParsingError, +}; + +pub use tracers::{ + call::CallTracer, + traits::{BoxedTracer, DynTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}, + utils::VmExecutionStopReason, + StorageInvocations, ValidationError, ValidationTracer, ValidationTracerParams, +}; + +pub use types::{ + inputs::{L1BatchEnv, L2BlockEnv, SystemEnv, TxExecutionMode, VmExecutionMode}, + internals::ZkSyncVmState, + outputs::{ + BootloaderMemory, CurrentExecutionState, ExecutionResult, FinishedL1Batch, L2Block, + Refunds, VmExecutionResultAndLogs, VmExecutionStatistics, VmMemoryMetrics, + }, +}; +pub use utils::transaction_encoding::TransactionVmExt; + +pub use bootloader_state::BootloaderState; + +pub use crate::vm::Vm; + +mod bootloader_state; +mod errors; +mod implementation; +mod old_vm; +mod tracers; +mod types; +mod vm; + +pub mod constants; +pub mod utils; + +#[cfg(test)] +mod tests; diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/event_sink.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/event_sink.rs new file mode 100644 index 00000000000..03156e83b9f --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/event_sink.rs @@ -0,0 +1,171 @@ +use crate::old_vm::{ + history_recorder::{AppDataFrameManagerWithHistory, HistoryEnabled, HistoryMode}, + oracles::OracleWithHistory, +}; +use std::collections::HashMap; +use zk_evm::{ + abstractions::EventSink, + aux_structures::{LogQuery, Timestamp}, + reference_impls::event_sink::EventMessage, + zkevm_opcode_defs::system_params::{ + BOOTLOADER_FORMAL_ADDRESS, EVENT_AUX_BYTE, L1_MESSAGE_AUX_BYTE, + }, +}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct InMemoryEventSink { + frames_stack: AppDataFrameManagerWithHistory, H>, +} + +impl OracleWithHistory for InMemoryEventSink { + fn rollback_to_timestamp(&mut self, timestamp: Timestamp) { + self.frames_stack.rollback_to_timestamp(timestamp); + } +} + +// as usual, if we rollback the current frame then we apply changes to storage immediately, +// otherwise we carry rollbacks to the parent's frames + +impl InMemoryEventSink { + pub fn flatten(&self) -> (Vec, Vec, Vec) { + assert_eq!( + self.frames_stack.len(), + 1, + "there must exist an initial keeper frame" + ); + // we forget rollbacks as we have finished the execution and can just apply them + let history = self.frames_stack.forward().current_frame(); + + let (events, l1_messages) = Self::events_and_l1_messages_from_history(history); + (history.iter().map(|x| **x).collect(), events, l1_messages) + } + + pub fn get_log_queries(&self) -> usize { + self.frames_stack.forward().current_frame().len() + } + + /// Returns the log queries in the current frame where `log_query.timestamp >= from_timestamp`. + pub fn log_queries_after_timestamp(&self, from_timestamp: Timestamp) -> &[Box] { + let events = self.frames_stack.forward().current_frame(); + + // Select all of the last elements where e.timestamp >= from_timestamp. + // Note, that using binary search here is dangerous, because the logs are not sorted by timestamp. + events + .rsplit(|e| e.timestamp < from_timestamp) + .next() + .unwrap_or(&[]) + } + + pub fn get_events_and_l2_l1_logs_after_timestamp( + &self, + from_timestamp: Timestamp, + ) -> (Vec, Vec) { + Self::events_and_l1_messages_from_history(self.log_queries_after_timestamp(from_timestamp)) + } + + fn events_and_l1_messages_from_history( + history: &[Box], + ) -> (Vec, Vec) { + let mut tmp = HashMap::::with_capacity(history.len()); + + // note that we only use "forward" part and discard the rollbacks at the end, + // since if rollbacks of parents were not appended anywhere we just still keep them + for el in history { + // we are time ordered here in terms of rollbacks + if tmp.get(&el.timestamp.0).is_some() { + assert!(el.rollback); + tmp.remove(&el.timestamp.0); + } else { + assert!(!el.rollback); + tmp.insert(el.timestamp.0, **el); + } + } + + // naturally sorted by timestamp + let mut keys: Vec<_> = tmp.keys().cloned().collect(); + keys.sort_unstable(); + + let mut events = vec![]; + let mut l1_messages = vec![]; + + for k in keys.into_iter() { + let el = tmp.remove(&k).unwrap(); + let LogQuery { + shard_id, + is_service, + tx_number_in_block, + address, + key, + written_value, + aux_byte, + .. + } = el; + + let event = EventMessage { + shard_id, + is_first: is_service, + tx_number_in_block, + address, + key, + value: written_value, + }; + + if aux_byte == EVENT_AUX_BYTE { + events.push(event); + } else { + l1_messages.push(event); + } + } + + (events, l1_messages) + } + + pub(crate) fn get_size(&self) -> usize { + self.frames_stack.get_size() + } + + pub fn get_history_size(&self) -> usize { + self.frames_stack.get_history_size() + } + + pub fn delete_history(&mut self) { + self.frames_stack.delete_history(); + } +} + +impl EventSink for InMemoryEventSink { + // when we enter a new frame we should remember all our current applications and rollbacks + // when we exit the current frame then if we did panic we should concatenate all current + // forward and rollback cases + + fn add_partial_query(&mut self, _monotonic_cycle_counter: u32, mut query: LogQuery) { + assert!(query.rw_flag); + assert!(query.aux_byte == EVENT_AUX_BYTE || query.aux_byte == L1_MESSAGE_AUX_BYTE); + assert!(!query.rollback); + + // just append to rollbacks and a full history + + self.frames_stack + .push_forward(Box::new(query), query.timestamp); + // we do not need it explicitly here, but let's be consistent with circuit counterpart + query.rollback = true; + self.frames_stack + .push_rollback(Box::new(query), query.timestamp); + } + + fn start_frame(&mut self, timestamp: Timestamp) { + self.frames_stack.push_frame(timestamp) + } + + fn finish_frame(&mut self, panicked: bool, timestamp: Timestamp) { + // if we panic then we append forward and rollbacks to the forward of parent, + // otherwise we place rollbacks of child before rollbacks of the parent + if panicked { + self.frames_stack.move_rollback_to_forward( + |q| q.address != *BOOTLOADER_FORMAL_ADDRESS || q.aux_byte != EVENT_AUX_BYTE, + timestamp, + ); + } + self.frames_stack.merge_frame(timestamp); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/events.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/events.rs new file mode 100644 index 00000000000..384a0eb86d6 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/events.rs @@ -0,0 +1,146 @@ +use zk_evm::{ethereum_types::Address, reference_impls::event_sink::EventMessage}; +use zksync_types::{L1BatchNumber, VmEvent, EVENT_WRITER_ADDRESS, H256}; +use zksync_utils::{be_chunks_to_h256_words, h256_to_account_address}; + +#[derive(Clone)] +pub(crate) struct SolidityLikeEvent { + pub(crate) shard_id: u8, + pub(crate) tx_number_in_block: u16, + pub(crate) address: Address, + pub(crate) topics: Vec<[u8; 32]>, + pub(crate) data: Vec, +} + +impl SolidityLikeEvent { + pub(crate) fn into_vm_event(self, block_number: L1BatchNumber) -> VmEvent { + VmEvent { + location: (block_number, self.tx_number_in_block as u32), + address: self.address, + indexed_topics: be_chunks_to_h256_words(self.topics), + value: self.data, + } + } +} + +fn merge_events_inner(events: Vec) -> Vec { + let mut result = vec![]; + let mut current: Option<(usize, u32, SolidityLikeEvent)> = None; + + for message in events.into_iter() { + if !message.is_first { + let EventMessage { + shard_id, + is_first: _, + tx_number_in_block, + address, + key, + value, + } = message; + + if let Some((mut remaining_data_length, mut remaining_topics, mut event)) = + current.take() + { + if event.address != address + || event.shard_id != shard_id + || event.tx_number_in_block != tx_number_in_block + { + 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); + } + } + } 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); + } + } + + let EventMessage { + shard_id, + is_first: _, + tx_number_in_block, + address, + key, + value, + } = message; + // 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 = SolidityLikeEvent { + shard_id, + tx_number_in_block, + address, + 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); + } + } + + result +} + +pub(crate) fn merge_events(events: Vec) -> Vec { + let raw_events = merge_events_inner(events); + + raw_events + .into_iter() + .filter(|e| e.address == EVENT_WRITER_ADDRESS) + .map(|event| { + // The events writer events where the first topic is the actual address of the event and the rest of the topics are real topics + let address = h256_to_account_address(&H256(event.topics[0])); + let topics = event.topics.into_iter().skip(1).collect(); + + SolidityLikeEvent { + topics, + address, + ..event + } + }) + .collect() +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/history_recorder.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/history_recorder.rs new file mode 100644 index 00000000000..1a5f7db5866 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/history_recorder.rs @@ -0,0 +1,805 @@ +use std::{collections::HashMap, fmt::Debug, hash::Hash}; + +use zk_evm::{ + aux_structures::Timestamp, + vm_state::PrimitiveValue, + zkevm_opcode_defs::{self}, +}; + +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::{StorageKey, U256}; +use zksync_utils::{h256_to_u256, u256_to_h256}; + +pub(crate) type MemoryWithHistory = HistoryRecorder; +pub(crate) type IntFrameManagerWithHistory = HistoryRecorder, H>; + +// Within the same cycle, timestamps in range timestamp..timestamp+TIME_DELTA_PER_CYCLE-1 +// can be used. This can sometimes violate monotonicity of the timestamp within the +// same cycle, so it should be normalized. +#[inline] +fn normalize_timestamp(timestamp: Timestamp) -> Timestamp { + let timestamp = timestamp.0; + + // Making sure it is divisible by TIME_DELTA_PER_CYCLE + Timestamp(timestamp - timestamp % zkevm_opcode_defs::TIME_DELTA_PER_CYCLE) +} + +/// Accepts history item as its parameter and applies it. +pub trait WithHistory { + type HistoryRecord; + type ReturnValue; + + // Applies an action and returns the action that would + // rollback its effect as well as some returned value + fn apply_historic_record( + &mut self, + item: Self::HistoryRecord, + ) -> (Self::HistoryRecord, Self::ReturnValue); +} + +type EventList = Vec<(Timestamp, ::HistoryRecord)>; + +/// Controls if rolling back is possible or not. +/// Either [HistoryEnabled] or [HistoryDisabled]. +pub trait HistoryMode: private::Sealed + Debug + Clone + Default { + type History: Default; + + fn clone_history(history: &Self::History) -> Self::History + where + T::HistoryRecord: Clone; + fn mutate_history)>( + recorder: &mut HistoryRecorder, + f: F, + ); + fn borrow_history) -> R, R>( + recorder: &HistoryRecorder, + f: F, + default: R, + ) -> R; +} + +mod private { + pub trait Sealed {} + impl Sealed for super::HistoryEnabled {} + impl Sealed for super::HistoryDisabled {} +} + +// derives require that all type parameters implement the trait, which is why +// HistoryEnabled/Disabled derive so many traits even though they mostly don't +// exist at runtime. + +/// A data structure with this parameter can be rolled back. +/// See also: [HistoryDisabled] +#[derive(Debug, Clone, Default, PartialEq)] +pub struct HistoryEnabled; + +/// A data structure with this parameter cannot be rolled back. +/// It won't even have rollback methods. +/// See also: [HistoryEnabled] +#[derive(Debug, Clone, Default)] +pub struct HistoryDisabled; + +impl HistoryMode for HistoryEnabled { + type History = EventList; + + fn clone_history(history: &Self::History) -> Self::History + where + T::HistoryRecord: Clone, + { + history.clone() + } + fn mutate_history)>( + recorder: &mut HistoryRecorder, + f: F, + ) { + f(&mut recorder.inner, &mut recorder.history) + } + fn borrow_history) -> R, R>( + recorder: &HistoryRecorder, + f: F, + _: R, + ) -> R { + f(&recorder.history) + } +} + +impl HistoryMode for HistoryDisabled { + type History = (); + + fn clone_history(_: &Self::History) -> Self::History {} + fn mutate_history)>( + _: &mut HistoryRecorder, + _: F, + ) { + } + fn borrow_history) -> R, R>( + _: &HistoryRecorder, + _: F, + default: R, + ) -> R { + default + } +} + +/// A struct responsible for tracking history for +/// a component that is passed as a generic parameter to it (`inner`). +#[derive(Default)] +pub struct HistoryRecorder { + inner: T, + history: H::History, +} + +impl PartialEq for HistoryRecorder +where + T::HistoryRecord: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + && self.borrow_history(|h1| other.borrow_history(|h2| h1 == h2, true), true) + } +} + +impl Debug for HistoryRecorder +where + T::HistoryRecord: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug_struct = f.debug_struct("HistoryRecorder"); + debug_struct.field("inner", &self.inner); + self.borrow_history( + |h| { + debug_struct.field("history", h); + }, + (), + ); + debug_struct.finish() + } +} + +impl Clone for HistoryRecorder +where + T::HistoryRecord: Clone, + H: HistoryMode, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + history: H::clone_history(&self.history), + } + } +} + +impl HistoryRecorder { + pub fn from_inner(inner: T) -> Self { + Self { + inner, + history: Default::default(), + } + } + + pub fn inner(&self) -> &T { + &self.inner + } + + /// If history exists, modify it using `f`. + pub fn mutate_history)>(&mut self, f: F) { + H::mutate_history(self, f); + } + + /// If history exists, feed it into `f`. Otherwise return `default`. + pub fn borrow_history) -> R, R>(&self, f: F, default: R) -> R { + H::borrow_history(self, f, default) + } + + pub fn apply_historic_record( + &mut self, + item: T::HistoryRecord, + timestamp: Timestamp, + ) -> T::ReturnValue { + let (reversed_item, return_value) = self.inner.apply_historic_record(item); + + self.mutate_history(|_, history| { + let last_recorded_timestamp = history.last().map(|(t, _)| *t).unwrap_or(Timestamp(0)); + let timestamp = normalize_timestamp(timestamp); + assert!( + last_recorded_timestamp <= timestamp, + "Timestamps are not monotonic" + ); + history.push((timestamp, reversed_item)); + }); + + return_value + } + + /// Deletes all the history for its component, making + /// its current state irreversible + pub fn delete_history(&mut self) { + self.mutate_history(|_, h| h.clear()) + } +} + +impl HistoryRecorder { + pub fn history(&self) -> &Vec<(Timestamp, T::HistoryRecord)> { + &self.history + } + + pub(crate) fn rollback_to_timestamp(&mut self, timestamp: Timestamp) { + loop { + let should_undo = self + .history + .last() + .map(|(item_timestamp, _)| *item_timestamp >= timestamp) + .unwrap_or(false); + if !should_undo { + break; + } + + let (_, item_to_apply) = self.history.pop().unwrap(); + self.inner.apply_historic_record(item_to_apply); + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum VectorHistoryEvent { + Push(X), + Pop, +} + +impl WithHistory for Vec { + type HistoryRecord = VectorHistoryEvent; + type ReturnValue = Option; + fn apply_historic_record( + &mut self, + item: VectorHistoryEvent, + ) -> (Self::HistoryRecord, Self::ReturnValue) { + match item { + VectorHistoryEvent::Pop => { + // Note, that here we assume that the users + // will check themselves whether this vector is empty + // prior to popping from it. + let poped_item = self.pop().unwrap(); + + (VectorHistoryEvent::Push(poped_item), Some(poped_item)) + } + VectorHistoryEvent::Push(x) => { + self.push(x); + + (VectorHistoryEvent::Pop, None) + } + } + } +} + +impl HistoryRecorder, H> { + pub fn push(&mut self, elem: T, timestamp: Timestamp) { + self.apply_historic_record(VectorHistoryEvent::Push(elem), timestamp); + } + + pub fn pop(&mut self, timestamp: Timestamp) -> T { + self.apply_historic_record(VectorHistoryEvent::Pop, timestamp) + .unwrap() + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct HashMapHistoryEvent { + pub key: K, + pub value: Option, +} + +impl WithHistory for HashMap { + type HistoryRecord = HashMapHistoryEvent; + type ReturnValue = Option; + fn apply_historic_record( + &mut self, + item: Self::HistoryRecord, + ) -> (Self::HistoryRecord, Self::ReturnValue) { + let HashMapHistoryEvent { key, value } = item; + + let prev_value = match value { + Some(x) => self.insert(key, x), + None => self.remove(&key), + }; + + ( + HashMapHistoryEvent { + key, + value: prev_value.clone(), + }, + prev_value, + ) + } +} + +impl HistoryRecorder, H> { + pub fn insert(&mut self, key: K, value: V, timestamp: Timestamp) -> Option { + self.apply_historic_record( + HashMapHistoryEvent { + key, + value: Some(value), + }, + timestamp, + ) + } +} + +/// A stack of stacks. The inner stacks are called frames. +/// +/// Does not support popping from the outer stack. Instead, the outer stack can +/// push its topmost frame's contents onto the previous frame. +#[derive(Debug, Clone, PartialEq)] +pub struct FramedStack { + data: Vec, + frame_start_indices: Vec, +} + +impl Default for FramedStack { + fn default() -> Self { + // We typically require at least the first frame to be there + // since the last user-provided frame might be reverted + Self { + data: vec![], + frame_start_indices: vec![0], + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum FramedStackEvent { + Push(T), + Pop, + PushFrame(usize), + MergeFrame, +} + +impl WithHistory for FramedStack { + type HistoryRecord = FramedStackEvent; + type ReturnValue = (); + + fn apply_historic_record( + &mut self, + item: Self::HistoryRecord, + ) -> (Self::HistoryRecord, Self::ReturnValue) { + use FramedStackEvent::*; + match item { + Push(x) => { + self.data.push(x); + (Pop, ()) + } + Pop => { + let x = self.data.pop().unwrap(); + (Push(x), ()) + } + PushFrame(i) => { + self.frame_start_indices.push(i); + (MergeFrame, ()) + } + MergeFrame => { + let pos = self.frame_start_indices.pop().unwrap(); + (PushFrame(pos), ()) + } + } + } +} + +impl FramedStack { + fn push_frame(&self) -> FramedStackEvent { + FramedStackEvent::PushFrame(self.data.len()) + } + + pub fn current_frame(&self) -> &[T] { + &self.data[*self.frame_start_indices.last().unwrap()..self.data.len()] + } + + fn len(&self) -> usize { + self.frame_start_indices.len() + } + + /// Returns the amount of memory taken up by the stored items + pub fn get_size(&self) -> usize { + self.data.len() * std::mem::size_of::() + } +} + +impl HistoryRecorder, H> { + pub fn push_to_frame(&mut self, x: T, timestamp: Timestamp) { + self.apply_historic_record(FramedStackEvent::Push(x), timestamp); + } + pub fn clear_frame(&mut self, timestamp: Timestamp) { + let start = *self.inner.frame_start_indices.last().unwrap(); + while self.inner.data.len() > start { + self.apply_historic_record(FramedStackEvent::Pop, timestamp); + } + } + pub fn extend_frame(&mut self, items: impl IntoIterator, timestamp: Timestamp) { + for x in items { + self.push_to_frame(x, timestamp); + } + } + pub fn push_frame(&mut self, timestamp: Timestamp) { + self.apply_historic_record(self.inner.push_frame(), timestamp); + } + pub fn merge_frame(&mut self, timestamp: Timestamp) { + self.apply_historic_record(FramedStackEvent::MergeFrame, timestamp); + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct AppDataFrameManagerWithHistory { + forward: HistoryRecorder, H>, + rollback: HistoryRecorder, H>, +} + +impl Default for AppDataFrameManagerWithHistory { + fn default() -> Self { + Self { + forward: Default::default(), + rollback: Default::default(), + } + } +} + +impl AppDataFrameManagerWithHistory { + pub(crate) fn delete_history(&mut self) { + self.forward.delete_history(); + self.rollback.delete_history(); + } + + pub(crate) fn push_forward(&mut self, item: T, timestamp: Timestamp) { + self.forward.push_to_frame(item, timestamp); + } + pub(crate) fn push_rollback(&mut self, item: T, timestamp: Timestamp) { + self.rollback.push_to_frame(item, timestamp); + } + pub(crate) fn push_frame(&mut self, timestamp: Timestamp) { + self.forward.push_frame(timestamp); + self.rollback.push_frame(timestamp); + } + pub(crate) fn merge_frame(&mut self, timestamp: Timestamp) { + self.forward.merge_frame(timestamp); + self.rollback.merge_frame(timestamp); + } + + pub(crate) fn len(&self) -> usize { + self.forward.inner.len() + } + pub(crate) fn forward(&self) -> &FramedStack { + &self.forward.inner + } + pub(crate) fn rollback(&self) -> &FramedStack { + &self.rollback.inner + } + + /// Returns the amount of memory taken up by the stored items + pub(crate) fn get_size(&self) -> usize { + self.forward().get_size() + self.rollback().get_size() + } + + pub(crate) fn get_history_size(&self) -> usize { + (self.forward.borrow_history(|h| h.len(), 0) + self.rollback.borrow_history(|h| h.len(), 0)) + * std::mem::size_of::< as WithHistory>::HistoryRecord>() + } +} + +impl AppDataFrameManagerWithHistory { + pub(crate) fn move_rollback_to_forward bool>( + &mut self, + filter: F, + timestamp: Timestamp, + ) { + for x in self.rollback.inner.current_frame().iter().rev() { + if filter(x) { + self.forward.push_to_frame(x.clone(), timestamp); + } + } + self.rollback.clear_frame(timestamp); + } +} + +impl AppDataFrameManagerWithHistory { + pub(crate) fn rollback_to_timestamp(&mut self, timestamp: Timestamp) { + self.forward.rollback_to_timestamp(timestamp); + self.rollback.rollback_to_timestamp(timestamp); + } +} + +const PRIMITIVE_VALUE_EMPTY: PrimitiveValue = PrimitiveValue::empty(); +const PAGE_SUBDIVISION_LEN: usize = 64; + +#[derive(Debug, Default, Clone)] +struct MemoryPage { + root: Vec>>, +} + +impl MemoryPage { + fn get(&self, slot: usize) -> &PrimitiveValue { + self.root + .get(slot / PAGE_SUBDIVISION_LEN) + .and_then(|inner| inner.as_ref()) + .map(|leaf| &leaf[slot % PAGE_SUBDIVISION_LEN]) + .unwrap_or(&PRIMITIVE_VALUE_EMPTY) + } + fn set(&mut self, slot: usize, value: PrimitiveValue) -> PrimitiveValue { + let root_index = slot / PAGE_SUBDIVISION_LEN; + let leaf_index = slot % PAGE_SUBDIVISION_LEN; + + if self.root.len() <= root_index { + self.root.resize_with(root_index + 1, || None); + } + let node = &mut self.root[root_index]; + + if let Some(leaf) = node { + let old = leaf[leaf_index]; + leaf[leaf_index] = value; + old + } else { + let mut leaf = [PrimitiveValue::empty(); PAGE_SUBDIVISION_LEN]; + leaf[leaf_index] = value; + self.root[root_index] = Some(Box::new(leaf)); + PrimitiveValue::empty() + } + } + + fn get_size(&self) -> usize { + self.root.iter().filter_map(|x| x.as_ref()).count() + * PAGE_SUBDIVISION_LEN + * std::mem::size_of::() + } +} + +impl PartialEq for MemoryPage { + fn eq(&self, other: &Self) -> bool { + for slot in 0..self.root.len().max(other.root.len()) * PAGE_SUBDIVISION_LEN { + if self.get(slot) != other.get(slot) { + return false; + } + } + true + } +} + +#[derive(Debug, Default, Clone)] +pub struct MemoryWrapper { + memory: Vec, +} + +impl PartialEq for MemoryWrapper { + fn eq(&self, other: &Self) -> bool { + let empty_page = MemoryPage::default(); + let empty_pages = std::iter::repeat(&empty_page); + self.memory + .iter() + .chain(empty_pages.clone()) + .zip(other.memory.iter().chain(empty_pages)) + .take(self.memory.len().max(other.memory.len())) + .all(|(a, b)| a == b) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MemoryHistoryRecord { + pub page: usize, + pub slot: usize, + pub set_value: PrimitiveValue, +} + +impl MemoryWrapper { + pub fn ensure_page_exists(&mut self, page: usize) { + if self.memory.len() <= page { + // We don't need to record such events in history + // because all these vectors will be empty + self.memory.resize_with(page + 1, MemoryPage::default); + } + } + + pub fn dump_page_content_as_u256_words( + &self, + page_number: u32, + range: std::ops::Range, + ) -> Vec { + if let Some(page) = self.memory.get(page_number as usize) { + let mut result = vec![]; + for i in range { + result.push(*page.get(i as usize)); + } + result + } else { + vec![PrimitiveValue::empty(); range.len()] + } + } + + pub fn read_slot(&self, page: usize, slot: usize) -> &PrimitiveValue { + self.memory + .get(page) + .map(|page| page.get(slot)) + .unwrap_or(&PRIMITIVE_VALUE_EMPTY) + } + + pub fn get_size(&self) -> usize { + self.memory.iter().map(|page| page.get_size()).sum() + } +} + +impl WithHistory for MemoryWrapper { + type HistoryRecord = MemoryHistoryRecord; + type ReturnValue = PrimitiveValue; + + fn apply_historic_record( + &mut self, + item: MemoryHistoryRecord, + ) -> (Self::HistoryRecord, Self::ReturnValue) { + let MemoryHistoryRecord { + page, + slot, + set_value, + } = item; + + self.ensure_page_exists(page); + let page_handle = self.memory.get_mut(page).unwrap(); + let prev_value = page_handle.set(slot, set_value); + + let undo = MemoryHistoryRecord { + page, + slot, + set_value: prev_value, + }; + + (undo, prev_value) + } +} + +impl HistoryRecorder { + pub fn write_to_memory( + &mut self, + page: usize, + slot: usize, + value: PrimitiveValue, + timestamp: Timestamp, + ) -> PrimitiveValue { + self.apply_historic_record( + MemoryHistoryRecord { + page, + slot, + set_value: value, + }, + timestamp, + ) + } + + pub fn clear_page(&mut self, page: usize, timestamp: Timestamp) { + self.mutate_history(|inner, history| { + if let Some(page_handle) = inner.memory.get(page) { + for (i, x) in page_handle.root.iter().enumerate() { + if let Some(slots) = x { + for (j, value) in slots.iter().enumerate() { + if *value != PrimitiveValue::empty() { + history.push(( + timestamp, + MemoryHistoryRecord { + page, + slot: PAGE_SUBDIVISION_LEN * i + j, + set_value: *value, + }, + )) + } + } + } + } + inner.memory[page] = MemoryPage::default(); + } + }); + } +} + +#[derive(Debug)] +pub struct StorageWrapper { + storage_ptr: StoragePtr, +} + +impl StorageWrapper { + pub fn new(storage_ptr: StoragePtr) -> Self { + Self { storage_ptr } + } + + pub fn get_ptr(&self) -> StoragePtr { + self.storage_ptr.clone() + } + + pub fn read_from_storage(&self, key: &StorageKey) -> U256 { + h256_to_u256(self.storage_ptr.borrow_mut().read_value(key)) + } +} + +#[derive(Debug, Clone)] +pub struct StorageHistoryRecord { + pub key: StorageKey, + pub value: U256, +} + +impl WithHistory for StorageWrapper { + type HistoryRecord = StorageHistoryRecord; + type ReturnValue = U256; + + fn apply_historic_record( + &mut self, + item: Self::HistoryRecord, + ) -> (Self::HistoryRecord, Self::ReturnValue) { + let prev_value = h256_to_u256( + self.storage_ptr + .borrow_mut() + .set_value(item.key, u256_to_h256(item.value)), + ); + + let reverse_item = StorageHistoryRecord { + key: item.key, + value: prev_value, + }; + + (reverse_item, prev_value) + } +} + +impl HistoryRecorder, H> { + pub fn read_from_storage(&self, key: &StorageKey) -> U256 { + self.inner.read_from_storage(key) + } + + pub fn write_to_storage(&mut self, key: StorageKey, value: U256, timestamp: Timestamp) -> U256 { + self.apply_historic_record(StorageHistoryRecord { key, value }, timestamp) + } + + /// Returns a pointer to the storage. + /// Note, that any changes done to the storage via this pointer + /// will NOT be recorded as its history. + pub fn get_ptr(&self) -> StoragePtr { + self.inner.get_ptr() + } +} + +#[cfg(test)] +mod tests { + use crate::old_vm::history_recorder::{HistoryRecorder, MemoryWrapper}; + use crate::HistoryDisabled; + use zk_evm::{aux_structures::Timestamp, vm_state::PrimitiveValue}; + use zksync_types::U256; + + #[test] + fn memory_equality() { + let mut a: HistoryRecorder = Default::default(); + let mut b = a.clone(); + let nonzero = U256::from_dec_str("123").unwrap(); + let different_value = U256::from_dec_str("1234").unwrap(); + + let write = |memory: &mut HistoryRecorder, value| { + memory.write_to_memory( + 17, + 34, + PrimitiveValue { + value, + is_pointer: false, + }, + Timestamp::empty(), + ); + }; + + assert_eq!(a, b); + + write(&mut b, nonzero); + assert_ne!(a, b); + + write(&mut a, different_value); + assert_ne!(a, b); + + write(&mut a, nonzero); + assert_eq!(a, b); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/memory.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/memory.rs new file mode 100644 index 00000000000..8569c135d1e --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/memory.rs @@ -0,0 +1,323 @@ +use zk_evm::abstractions::{Memory, MemoryType}; +use zk_evm::aux_structures::{MemoryPage, MemoryQuery, Timestamp}; +use zk_evm::vm_state::PrimitiveValue; +use zk_evm::zkevm_opcode_defs::FatPointer; +use zksync_types::U256; + +use crate::old_vm::history_recorder::{ + FramedStack, HistoryEnabled, HistoryMode, IntFrameManagerWithHistory, MemoryWithHistory, + MemoryWrapper, WithHistory, +}; +use crate::old_vm::oracles::OracleWithHistory; +use crate::old_vm::utils::{aux_heap_page_from_base, heap_page_from_base, stack_page_from_base}; + +#[derive(Debug, Clone, PartialEq)] +pub struct SimpleMemory { + memory: MemoryWithHistory, + observable_pages: IntFrameManagerWithHistory, +} + +impl Default for SimpleMemory { + fn default() -> Self { + let mut memory: MemoryWithHistory = Default::default(); + memory.mutate_history(|_, h| h.reserve(607)); + Self { + memory, + observable_pages: Default::default(), + } + } +} + +impl OracleWithHistory for SimpleMemory { + fn rollback_to_timestamp(&mut self, timestamp: Timestamp) { + self.memory.rollback_to_timestamp(timestamp); + self.observable_pages.rollback_to_timestamp(timestamp); + } +} + +impl SimpleMemory { + pub fn populate(&mut self, elements: Vec<(u32, Vec)>, timestamp: Timestamp) { + for (page, values) in elements.into_iter() { + for (i, value) in values.into_iter().enumerate() { + let value = PrimitiveValue { + value, + is_pointer: false, + }; + self.memory + .write_to_memory(page as usize, i, value, timestamp); + } + } + } + + pub fn populate_page( + &mut self, + page: usize, + elements: Vec<(usize, U256)>, + timestamp: Timestamp, + ) { + elements.into_iter().for_each(|(offset, value)| { + let value = PrimitiveValue { + value, + is_pointer: false, + }; + + self.memory.write_to_memory(page, offset, value, timestamp); + }); + } + + pub fn dump_page_content_as_u256_words( + &self, + page: u32, + range: std::ops::Range, + ) -> Vec { + self.memory + .inner() + .dump_page_content_as_u256_words(page, range) + .into_iter() + .map(|v| v.value) + .collect() + } + + pub fn read_slot(&self, page: usize, slot: usize) -> &PrimitiveValue { + self.memory.inner().read_slot(page, slot) + } + + // This method should be used with relatively small lengths, since + // we don't heavily optimize here for cases with long lengths + pub fn read_unaligned_bytes(&self, page: usize, start: usize, length: usize) -> Vec { + if length == 0 { + return vec![]; + } + + let end = start + length - 1; + + let mut current_word = start / 32; + let mut result = vec![]; + while current_word * 32 <= end { + let word_value = self.read_slot(page, current_word).value; + let word_value = { + let mut bytes: Vec = vec![0u8; 32]; + word_value.to_big_endian(&mut bytes); + bytes + }; + + result.extend(extract_needed_bytes_from_word( + word_value, + current_word, + start, + end, + )); + + current_word += 1; + } + + assert_eq!(result.len(), length); + + result + } + + pub(crate) fn get_size(&self) -> usize { + // Hashmap memory overhead is neglected. + let memory_size = self.memory.inner().get_size(); + let observable_pages_size = self.observable_pages.inner().get_size(); + + memory_size + observable_pages_size + } + + pub fn get_history_size(&self) -> usize { + let memory_size = self.memory.borrow_history(|h| h.len(), 0) + * std::mem::size_of::<::HistoryRecord>(); + let observable_pages_size = self.observable_pages.borrow_history(|h| h.len(), 0) + * std::mem::size_of::< as WithHistory>::HistoryRecord>(); + + memory_size + observable_pages_size + } + + pub fn delete_history(&mut self) { + self.memory.delete_history(); + self.observable_pages.delete_history(); + } +} + +impl Memory for SimpleMemory { + fn execute_partial_query( + &mut self, + _monotonic_cycle_counter: u32, + mut query: MemoryQuery, + ) -> MemoryQuery { + match query.location.memory_type { + MemoryType::Stack => {} + MemoryType::Heap | MemoryType::AuxHeap => { + // The following assertion works fine even when doing a read + // from heap through pointer, since `value_is_pointer` can only be set to + // `true` during memory writes. + assert!( + !query.value_is_pointer, + "Pointers can only be stored on stack" + ); + } + MemoryType::FatPointer => { + assert!(!query.rw_flag); + assert!( + !query.value_is_pointer, + "Pointers can only be stored on stack" + ); + } + MemoryType::Code => { + unreachable!("code should be through specialized query"); + } + } + + let page = query.location.page.0 as usize; + let slot = query.location.index.0 as usize; + + if query.rw_flag { + self.memory.write_to_memory( + page, + slot, + PrimitiveValue { + value: query.value, + is_pointer: query.value_is_pointer, + }, + query.timestamp, + ); + } else { + let current_value = self.read_slot(page, slot); + query.value = current_value.value; + query.value_is_pointer = current_value.is_pointer; + } + + query + } + + fn specialized_code_query( + &mut self, + _monotonic_cycle_counter: u32, + mut query: MemoryQuery, + ) -> MemoryQuery { + assert_eq!(query.location.memory_type, MemoryType::Code); + assert!( + !query.value_is_pointer, + "Pointers are not used for decommmits" + ); + + let page = query.location.page.0 as usize; + let slot = query.location.index.0 as usize; + + if query.rw_flag { + self.memory.write_to_memory( + page, + slot, + PrimitiveValue { + value: query.value, + is_pointer: query.value_is_pointer, + }, + query.timestamp, + ); + } else { + let current_value = self.read_slot(page, slot); + query.value = current_value.value; + query.value_is_pointer = current_value.is_pointer; + } + + query + } + + fn read_code_query( + &self, + _monotonic_cycle_counter: u32, + mut query: MemoryQuery, + ) -> MemoryQuery { + assert_eq!(query.location.memory_type, MemoryType::Code); + assert!( + !query.value_is_pointer, + "Pointers are not used for decommmits" + ); + assert!(!query.rw_flag, "Only read queries can be processed"); + + let page = query.location.page.0 as usize; + let slot = query.location.index.0 as usize; + + let current_value = self.read_slot(page, slot); + query.value = current_value.value; + query.value_is_pointer = current_value.is_pointer; + + query + } + + fn start_global_frame( + &mut self, + _current_base_page: MemoryPage, + new_base_page: MemoryPage, + calldata_fat_pointer: FatPointer, + timestamp: Timestamp, + ) { + // Besides the calldata page, we also formally include the current stack + // page, heap page and aux heap page. + // The code page will be always left observable, so we don't include it here. + self.observable_pages.push_frame(timestamp); + self.observable_pages.extend_frame( + vec![ + calldata_fat_pointer.memory_page, + stack_page_from_base(new_base_page).0, + heap_page_from_base(new_base_page).0, + aux_heap_page_from_base(new_base_page).0, + ], + timestamp, + ); + } + + fn finish_global_frame( + &mut self, + base_page: MemoryPage, + returndata_fat_pointer: FatPointer, + timestamp: Timestamp, + ) { + // Safe to unwrap here, since `finish_global_frame` is never called with empty stack + let current_observable_pages = self.observable_pages.inner().current_frame(); + let returndata_page = returndata_fat_pointer.memory_page; + + for &page in current_observable_pages { + // If the page's number is greater than or equal to the base_page, + // it means that it was created by the internal calls of this contract. + // We need to add this check as the calldata pointer is also part of the + // observable pages. + if page >= base_page.0 && page != returndata_page { + self.memory.clear_page(page as usize, timestamp); + } + } + + self.observable_pages.clear_frame(timestamp); + self.observable_pages.merge_frame(timestamp); + + self.observable_pages + .push_to_frame(returndata_page, timestamp); + } +} + +// It is expected that there is some intersection between [word_number*32..word_number*32+31] and [start, end] +fn extract_needed_bytes_from_word( + word_value: Vec, + word_number: usize, + start: usize, + end: usize, +) -> Vec { + let word_start = word_number * 32; + let word_end = word_start + 31; // Note, that at word_start + 32 a new word already starts + + let intersection_left = std::cmp::max(word_start, start); + let intersection_right = std::cmp::min(word_end, end); + + if intersection_right < intersection_left { + vec![] + } else { + let start_bytes = intersection_left - word_start; + let to_take = intersection_right - intersection_left + 1; + + word_value + .into_iter() + .skip(start_bytes) + .take(to_take) + .collect() + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/mod.rs new file mode 100644 index 00000000000..afade198461 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/mod.rs @@ -0,0 +1,8 @@ +/// This module contains the parts from old VM implementation, which were not changed during the vm implementation. +/// It should be refactored and removed in the future. +pub(crate) mod event_sink; +pub(crate) mod events; +pub(crate) mod history_recorder; +pub(crate) mod memory; +pub(crate) mod oracles; +pub(crate) mod utils; diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/decommitter.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/decommitter.rs new file mode 100644 index 00000000000..e91380a6d38 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/decommitter.rs @@ -0,0 +1,238 @@ +use std::collections::HashMap; +use std::fmt::Debug; + +use crate::old_vm::history_recorder::{HistoryEnabled, HistoryMode, HistoryRecorder, WithHistory}; + +use zk_evm::abstractions::MemoryType; +use zk_evm::aux_structures::Timestamp; +use zk_evm::{ + abstractions::{DecommittmentProcessor, Memory}, + aux_structures::{DecommittmentQuery, MemoryIndex, MemoryLocation, MemoryPage, MemoryQuery}, +}; + +use zksync_state::{ReadStorage, StoragePtr}; +use zksync_types::U256; +use zksync_utils::bytecode::bytecode_len_in_words; +use zksync_utils::{bytes_to_be_words, u256_to_h256}; + +use super::OracleWithHistory; + +/// The main job of the DecommiterOracle is to implement the DecommittmentProcessor trait - that is +/// used by the VM to 'load' bytecodes into memory. +#[derive(Debug)] +pub struct DecommitterOracle { + /// Pointer that enables to read contract bytecodes from the database. + storage: StoragePtr, + /// The cache of bytecodes that the bootloader "knows", but that are not necessarily in the database. + /// And it is also used as a database cache. + pub known_bytecodes: HistoryRecorder>, H>, + /// Stores pages of memory where certain code hashes have already been decommitted. + /// It is expected that they all are present in the DB. + // `decommitted_code_hashes` history is necessary + pub decommitted_code_hashes: HistoryRecorder, HistoryEnabled>, + /// Stores history of decommitment requests. + decommitment_requests: HistoryRecorder, H>, +} + +impl DecommitterOracle { + pub fn new(storage: StoragePtr) -> Self { + Self { + storage, + known_bytecodes: HistoryRecorder::default(), + decommitted_code_hashes: HistoryRecorder::default(), + decommitment_requests: HistoryRecorder::default(), + } + } + + /// Gets the bytecode for a given hash (either from storage, or from 'known_bytecodes' that were populated by `populate` method). + /// Panics if bytecode doesn't exist. + pub fn get_bytecode(&mut self, hash: U256, timestamp: Timestamp) -> Vec { + let entry = self.known_bytecodes.inner().get(&hash); + + match entry { + Some(x) => x.clone(), + None => { + // It is ok to panic here, since the decommitter is never called directly by + // the users and always called by the VM. VM will never let decommit the + // code hash which we didn't previously claim to know the preimage of. + let value = self + .storage + .borrow_mut() + .load_factory_dep(u256_to_h256(hash)) + .expect("Trying to decode unexisting hash"); + + let value = bytes_to_be_words(value); + self.known_bytecodes.insert(hash, value.clone(), timestamp); + value + } + } + } + + /// Adds additional bytecodes. They will take precendent over the bytecodes from storage. + pub fn populate(&mut self, bytecodes: Vec<(U256, Vec)>, timestamp: Timestamp) { + for (hash, bytecode) in bytecodes { + self.known_bytecodes.insert(hash, bytecode, timestamp); + } + } + + pub fn get_used_bytecode_hashes(&self) -> Vec { + self.decommitted_code_hashes + .inner() + .iter() + .map(|item| *item.0) + .collect() + } + + pub fn get_decommitted_bytecodes_after_timestamp(&self, timestamp: Timestamp) -> usize { + // Note, that here we rely on the fact that for each used bytecode + // there is one and only one corresponding event in the history of it. + self.decommitted_code_hashes + .history() + .iter() + .rev() + .take_while(|(t, _)| *t >= timestamp) + .count() + } + + pub fn get_decommitted_code_hashes_with_history( + &self, + ) -> &HistoryRecorder, HistoryEnabled> { + &self.decommitted_code_hashes + } + + /// Returns the storage handle. Used only in tests. + pub fn get_storage(&self) -> StoragePtr { + self.storage.clone() + } + + /// Measures the amount of memory used by this Oracle (used for metrics only). + pub(crate) fn get_size(&self) -> usize { + // Hashmap memory overhead is neglected. + let known_bytecodes_size = self + .known_bytecodes + .inner() + .iter() + .map(|(_, value)| value.len() * std::mem::size_of::()) + .sum::(); + let decommitted_code_hashes_size = + self.decommitted_code_hashes.inner().len() * std::mem::size_of::<(U256, u32)>(); + + known_bytecodes_size + decommitted_code_hashes_size + } + + pub(crate) fn get_history_size(&self) -> usize { + let known_bytecodes_stack_size = self.known_bytecodes.borrow_history(|h| h.len(), 0) + * std::mem::size_of::<> as WithHistory>::HistoryRecord>(); + let known_bytecodes_heap_size = self.known_bytecodes.borrow_history( + |h| { + h.iter() + .map(|(_, event)| { + if let Some(bytecode) = event.value.as_ref() { + bytecode.len() * std::mem::size_of::() + } else { + 0 + } + }) + .sum::() + }, + 0, + ); + let decommitted_code_hashes_size = + self.decommitted_code_hashes.borrow_history(|h| h.len(), 0) + * std::mem::size_of::< as WithHistory>::HistoryRecord>(); + + known_bytecodes_stack_size + known_bytecodes_heap_size + decommitted_code_hashes_size + } + + pub fn delete_history(&mut self) { + self.decommitted_code_hashes.delete_history(); + self.known_bytecodes.delete_history(); + self.decommitment_requests.delete_history(); + } +} + +impl OracleWithHistory for DecommitterOracle { + fn rollback_to_timestamp(&mut self, timestamp: Timestamp) { + self.decommitted_code_hashes + .rollback_to_timestamp(timestamp); + self.known_bytecodes.rollback_to_timestamp(timestamp); + self.decommitment_requests.rollback_to_timestamp(timestamp); + } +} + +impl DecommittmentProcessor + for DecommitterOracle +{ + /// Loads a given bytecode hash into memory (see trait description for more details). + fn decommit_into_memory( + &mut self, + monotonic_cycle_counter: u32, + mut partial_query: DecommittmentQuery, + memory: &mut M, + ) -> Result< + ( + zk_evm::aux_structures::DecommittmentQuery, + Option>, + ), + anyhow::Error, + > { + self.decommitment_requests.push((), partial_query.timestamp); + // First - check if we didn't fetch this bytecode in the past. + // If we did - we can just return the page that we used before (as the memory is read only). + if let Some(memory_page) = self + .decommitted_code_hashes + .inner() + .get(&partial_query.hash) + .copied() + { + partial_query.is_fresh = false; + partial_query.memory_page = MemoryPage(memory_page); + partial_query.decommitted_length = + bytecode_len_in_words(&u256_to_h256(partial_query.hash)); + + Ok((partial_query, None)) + } else { + // We are fetching a fresh bytecode that we didn't read before. + let values = self.get_bytecode(partial_query.hash, partial_query.timestamp); + let page_to_use = partial_query.memory_page; + let timestamp = partial_query.timestamp; + partial_query.decommitted_length = values.len() as u16; + partial_query.is_fresh = true; + + // Create a template query, that we'll use for writing into memory. + // value & index are set to 0 - as they will be updated in the inner loop below. + let mut tmp_q = MemoryQuery { + timestamp, + location: MemoryLocation { + memory_type: MemoryType::Code, + page: page_to_use, + index: MemoryIndex(0), + }, + value: U256::zero(), + value_is_pointer: false, + rw_flag: true, + }; + self.decommitted_code_hashes + .insert(partial_query.hash, page_to_use.0, timestamp); + + // Copy the bytecode (that is stored in 'values' Vec) into the memory page. + if B { + for (i, value) in values.iter().enumerate() { + tmp_q.location.index = MemoryIndex(i as u32); + tmp_q.value = *value; + memory.specialized_code_query(monotonic_cycle_counter, tmp_q); + } + // If we're in the witness mode - we also have to return the values. + Ok((partial_query, Some(values))) + } else { + for (i, value) in values.into_iter().enumerate() { + tmp_q.location.index = MemoryIndex(i as u32); + tmp_q.value = value; + memory.specialized_code_query(monotonic_cycle_counter, tmp_q); + } + + Ok((partial_query, None)) + } + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/mod.rs new file mode 100644 index 00000000000..daa2e21672d --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/mod.rs @@ -0,0 +1,9 @@ +use zk_evm::aux_structures::Timestamp; + +pub(crate) mod decommitter; +pub(crate) mod precompile; +pub(crate) mod storage; + +pub(crate) trait OracleWithHistory { + fn rollback_to_timestamp(&mut self, timestamp: Timestamp); +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/precompile.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/precompile.rs new file mode 100644 index 00000000000..72b751c75d4 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/precompile.rs @@ -0,0 +1,75 @@ +use zk_evm::{ + abstractions::Memory, + abstractions::PrecompileCyclesWitness, + abstractions::PrecompilesProcessor, + aux_structures::{LogQuery, MemoryQuery, Timestamp}, + precompiles::DefaultPrecompilesProcessor, +}; + +use crate::old_vm::history_recorder::{HistoryEnabled, HistoryMode, HistoryRecorder}; + +use super::OracleWithHistory; + +/// Wrap of DefaultPrecompilesProcessor that store queue +/// of timestamp when precompiles are called to be executed. +/// Number of precompiles per block is strictly limited, +/// saving timestamps allows us to check the exact number +/// of log queries, that were used during the tx execution. +#[derive(Debug, Clone)] +pub struct PrecompilesProcessorWithHistory { + pub timestamp_history: HistoryRecorder, H>, + pub default_precompiles_processor: DefaultPrecompilesProcessor, +} + +impl Default for PrecompilesProcessorWithHistory { + fn default() -> Self { + Self { + timestamp_history: Default::default(), + default_precompiles_processor: DefaultPrecompilesProcessor, + } + } +} + +impl OracleWithHistory for PrecompilesProcessorWithHistory { + fn rollback_to_timestamp(&mut self, timestamp: Timestamp) { + self.timestamp_history.rollback_to_timestamp(timestamp); + } +} + +impl PrecompilesProcessorWithHistory { + pub fn get_timestamp_history(&self) -> &Vec { + self.timestamp_history.inner() + } + + pub fn delete_history(&mut self) { + self.timestamp_history.delete_history(); + } +} + +impl PrecompilesProcessor for PrecompilesProcessorWithHistory { + fn start_frame(&mut self) { + self.default_precompiles_processor.start_frame(); + } + fn execute_precompile( + &mut self, + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, + ) -> Option<(Vec, Vec, PrecompileCyclesWitness)> { + // In the next line we same `query.timestamp` as both + // an operation in the history of precompiles processor and + // the time when this operation occured. + // While slightly weird, it is done for consistency with other oracles + // where operations and timestamp have different types. + self.timestamp_history + .push(query.timestamp, query.timestamp); + self.default_precompiles_processor.execute_precompile( + monotonic_cycle_counter, + query, + memory, + ) + } + fn finish_frame(&mut self, _panicked: bool) { + self.default_precompiles_processor.finish_frame(_panicked); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/storage.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/storage.rs new file mode 100644 index 00000000000..482cc69bbbd --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/oracles/storage.rs @@ -0,0 +1,338 @@ +use std::collections::HashMap; + +use crate::old_vm::history_recorder::{ + AppDataFrameManagerWithHistory, HashMapHistoryEvent, HistoryEnabled, HistoryMode, + HistoryRecorder, StorageWrapper, WithHistory, +}; + +use zk_evm::abstractions::RefundedAmounts; +use zk_evm::zkevm_opcode_defs::system_params::INITIAL_STORAGE_WRITE_PUBDATA_BYTES; +use zk_evm::{ + abstractions::{RefundType, Storage as VmStorageOracle}, + aux_structures::{LogQuery, Timestamp}, +}; + +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::utils::storage_key_for_eth_balance; +use zksync_types::{ + AccountTreeId, Address, StorageKey, StorageLogQuery, StorageLogQueryType, BOOTLOADER_ADDRESS, + U256, +}; +use zksync_utils::u256_to_h256; + +use super::OracleWithHistory; + +// While the storage does not support different shards, it was decided to write the +// code of the StorageOracle with the shard parameters in mind. +pub(crate) fn triplet_to_storage_key(_shard_id: u8, address: Address, key: U256) -> StorageKey { + StorageKey::new(AccountTreeId::new(address), u256_to_h256(key)) +} + +pub(crate) fn storage_key_of_log(query: &LogQuery) -> StorageKey { + triplet_to_storage_key(query.shard_id, query.address, query.key) +} + +#[derive(Debug)] +pub struct StorageOracle { + // Access to the persistent storage. Please note that it + // is used only for read access. All the actual writes happen + // after the execution ended. + pub(crate) storage: HistoryRecorder, H>, + + pub(crate) frames_stack: AppDataFrameManagerWithHistory, H>, + + // The changes that have been paid for in previous transactions. + // It is a mapping from storage key to the number of *bytes* that was paid by the user + // to cover this slot. + // `paid_changes` history is necessary + pub(crate) paid_changes: HistoryRecorder, HistoryEnabled>, +} + +impl OracleWithHistory for StorageOracle { + fn rollback_to_timestamp(&mut self, timestamp: Timestamp) { + self.frames_stack.rollback_to_timestamp(timestamp); + self.storage.rollback_to_timestamp(timestamp); + self.paid_changes.rollback_to_timestamp(timestamp); + } +} + +impl StorageOracle { + pub fn new(storage: StoragePtr) -> Self { + Self { + storage: HistoryRecorder::from_inner(StorageWrapper::new(storage)), + frames_stack: Default::default(), + paid_changes: Default::default(), + } + } + + pub fn delete_history(&mut self) { + self.frames_stack.delete_history(); + self.storage.delete_history(); + self.paid_changes.delete_history(); + } + + fn is_storage_key_free(&self, key: &StorageKey) -> bool { + key.address() == &zksync_config::constants::SYSTEM_CONTEXT_ADDRESS + || *key == storage_key_for_eth_balance(&BOOTLOADER_ADDRESS) + } + + pub fn read_value(&mut self, mut query: LogQuery) -> LogQuery { + let key = triplet_to_storage_key(query.shard_id, query.address, query.key); + let current_value = self.storage.read_from_storage(&key); + + query.read_value = current_value; + + self.frames_stack.push_forward( + Box::new(StorageLogQuery { + log_query: query, + log_type: StorageLogQueryType::Read, + }), + query.timestamp, + ); + + query + } + + pub fn write_value(&mut self, mut query: LogQuery) -> LogQuery { + let key = triplet_to_storage_key(query.shard_id, query.address, query.key); + let current_value = + self.storage + .write_to_storage(key, query.written_value, query.timestamp); + + let is_initial_write = self.storage.get_ptr().borrow_mut().is_write_initial(&key); + let log_query_type = if is_initial_write { + StorageLogQueryType::InitialWrite + } else { + StorageLogQueryType::RepeatedWrite + }; + + query.read_value = current_value; + + let mut storage_log_query = StorageLogQuery { + log_query: query, + log_type: log_query_type, + }; + self.frames_stack + .push_forward(Box::new(storage_log_query), query.timestamp); + storage_log_query.log_query.rollback = true; + self.frames_stack + .push_rollback(Box::new(storage_log_query), query.timestamp); + storage_log_query.log_query.rollback = false; + + query + } + + // Returns the amount of funds that has been already paid for writes into the storage slot + fn prepaid_for_write(&self, storage_key: &StorageKey) -> u32 { + self.paid_changes + .inner() + .get(storage_key) + .copied() + .unwrap_or_default() + } + + pub(crate) fn base_price_for_write(&self, query: &LogQuery) -> u32 { + let storage_key = storage_key_of_log(query); + + if self.is_storage_key_free(&storage_key) { + return 0; + } + + let is_initial_write = self + .storage + .get_ptr() + .borrow_mut() + .is_write_initial(&storage_key); + + get_pubdata_price_bytes(query, is_initial_write) + } + + // Returns the price of the update in terms of pubdata bytes. + // TODO (SMA-1701): update VM to accept gas instead of pubdata. + fn value_update_price(&self, query: &LogQuery) -> u32 { + let storage_key = storage_key_of_log(query); + + let base_cost = self.base_price_for_write(query); + + let already_paid = self.prepaid_for_write(&storage_key); + + if base_cost <= already_paid { + // Some other transaction has already paid for this slot, no need to pay anything + 0u32 + } else { + base_cost - already_paid + } + } + + /// Returns storage log queries from current frame where `log.log_query.timestamp >= from_timestamp`. + pub(crate) fn storage_log_queries_after_timestamp( + &self, + from_timestamp: Timestamp, + ) -> &[Box] { + let logs = self.frames_stack.forward().current_frame(); + + // Select all of the last elements where l.log_query.timestamp >= from_timestamp. + // Note, that using binary search here is dangerous, because the logs are not sorted by timestamp. + logs.rsplit(|l| l.log_query.timestamp < from_timestamp) + .next() + .unwrap_or(&[]) + } + + pub(crate) fn get_final_log_queries(&self) -> Vec { + assert_eq!( + self.frames_stack.len(), + 1, + "VM finished execution in unexpected state" + ); + + self.frames_stack + .forward() + .current_frame() + .iter() + .map(|x| **x) + .collect() + } + + pub(crate) fn get_size(&self) -> usize { + let frames_stack_size = self.frames_stack.get_size(); + let paid_changes_size = + self.paid_changes.inner().len() * std::mem::size_of::<(StorageKey, u32)>(); + + frames_stack_size + paid_changes_size + } + + pub(crate) fn get_history_size(&self) -> usize { + let storage_size = self.storage.borrow_history(|h| h.len(), 0) + * std::mem::size_of::< as WithHistory>::HistoryRecord>(); + let frames_stack_size = self.frames_stack.get_history_size(); + let paid_changes_size = self.paid_changes.borrow_history(|h| h.len(), 0) + * std::mem::size_of::< as WithHistory>::HistoryRecord>(); + storage_size + frames_stack_size + paid_changes_size + } +} + +impl VmStorageOracle for StorageOracle { + // Perform a storage read/write access by taking an partially filled query + // and returning filled query and cold/warm marker for pricing purposes + fn execute_partial_query( + &mut self, + _monotonic_cycle_counter: u32, + query: LogQuery, + ) -> LogQuery { + // tracing::trace!( + // "execute partial query cyc {:?} addr {:?} key {:?}, rw {:?}, wr {:?}, tx {:?}", + // _monotonic_cycle_counter, + // query.address, + // query.key, + // query.rw_flag, + // query.written_value, + // query.tx_number_in_block + // ); + assert!(!query.rollback); + if query.rw_flag { + // The number of bytes that have been compensated by the user to perform this write + let storage_key = storage_key_of_log(&query); + + // It is considered that the user has paid for the whole base price for the writes + let to_pay_by_user = self.base_price_for_write(&query); + let prepaid = self.prepaid_for_write(&storage_key); + + if to_pay_by_user > prepaid { + self.paid_changes.apply_historic_record( + HashMapHistoryEvent { + key: storage_key, + value: Some(to_pay_by_user), + }, + query.timestamp, + ); + } + self.write_value(query) + } else { + self.read_value(query) + } + } + + // We can return the size of the refund before each storage query. + // Note, that while the `RefundType` allows to provide refunds both in + // `ergs` and `pubdata`, only refunds in pubdata will be compensated for the users + fn estimate_refunds_for_write( + &mut self, // to avoid any hacks inside, like prefetch + _monotonic_cycle_counter: u32, + partial_query: &LogQuery, + ) -> RefundType { + let price_to_pay = self.value_update_price(partial_query); + + RefundType::RepeatedWrite(RefundedAmounts { + ergs: 0, + // `INITIAL_STORAGE_WRITE_PUBDATA_BYTES` is the default amount of pubdata bytes the user pays for. + pubdata_bytes: (INITIAL_STORAGE_WRITE_PUBDATA_BYTES as u32) - price_to_pay, + }) + } + + // Indicate a start of execution frame for rollback purposes + fn start_frame(&mut self, timestamp: Timestamp) { + self.frames_stack.push_frame(timestamp); + } + + // Indicate that execution frame went out from the scope, so we can + // log the history and either rollback immediately or keep records to rollback later + fn finish_frame(&mut self, timestamp: Timestamp, panicked: bool) { + // If we panic then we append forward and rollbacks to the forward of parent, + // otherwise we place rollbacks of child before rollbacks of the parent + if panicked { + // perform actual rollback + for query in self.frames_stack.rollback().current_frame().iter().rev() { + let read_value = match query.log_type { + StorageLogQueryType::Read => { + // Having Read logs in rollback is not possible + tracing::warn!("Read log in rollback queue {:?}", query); + continue; + } + StorageLogQueryType::InitialWrite | StorageLogQueryType::RepeatedWrite => { + query.log_query.read_value + } + }; + + let LogQuery { written_value, .. } = query.log_query; + let key = triplet_to_storage_key( + query.log_query.shard_id, + query.log_query.address, + query.log_query.key, + ); + let current_value = self.storage.write_to_storage( + key, + // NOTE, that since it is a rollback query, + // the `read_value` is being set + read_value, timestamp, + ); + + // Additional validation that the current value was correct + // Unwrap is safe because the return value from write_inner is the previous value in this leaf. + // It is impossible to set leaf value to `None` + assert_eq!(current_value, written_value); + } + + self.frames_stack + .move_rollback_to_forward(|_| true, timestamp); + } + self.frames_stack.merge_frame(timestamp); + } +} + +/// Returns the number of bytes needed to publish a slot. +// Since we need to publish the state diffs onchain, for each of the updated storage slot +// we basically need to publish the following pair: (). +// While new_value is always 32 bytes long, 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 this 8-byte instead of the 32 bytes of the entire key. +// So the total size of the publish pubdata is 40 bytes. We call this kind of write the repeated one +fn get_pubdata_price_bytes(_query: &LogQuery, is_initial: bool) -> u32 { + // TODO (SMA-1702): take into account the content of the log query, i.e. values that contain mostly zeroes + // should cost less. + if is_initial { + zk_evm::zkevm_opcode_defs::system_params::INITIAL_STORAGE_WRITE_PUBDATA_BYTES as u32 + } else { + zk_evm::zkevm_opcode_defs::system_params::REPEATED_STORAGE_WRITE_PUBDATA_BYTES as u32 + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/old_vm/utils.rs b/core/multivm_deps/vm_virtual_blocks/src/old_vm/utils.rs new file mode 100644 index 00000000000..5df4c6aa801 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/old_vm/utils.rs @@ -0,0 +1,222 @@ +use crate::old_vm::memory::SimpleMemory; + +use crate::types::internals::ZkSyncVmState; +use crate::HistoryMode; + +use zk_evm::zkevm_opcode_defs::decoding::{AllowedPcOrImm, EncodingModeProduction, VmEncodingMode}; +use zk_evm::zkevm_opcode_defs::RET_IMPLICIT_RETURNDATA_PARAMS_REGISTER; +use zk_evm::{ + aux_structures::{MemoryPage, Timestamp}, + vm_state::PrimitiveValue, + zkevm_opcode_defs::FatPointer, +}; +use zksync_config::constants::L1_GAS_PER_PUBDATA_BYTE; +use zksync_state::WriteStorage; + +use zksync_types::{Address, U256}; + +#[derive(Debug, Clone)] +pub(crate) enum VmExecutionResult { + Ok(Vec), + Revert(Vec), + Panic, + MostLikelyDidNotFinish(Address, u16), +} + +pub(crate) const fn stack_page_from_base(base: MemoryPage) -> MemoryPage { + MemoryPage(base.0 + 1) +} + +pub(crate) const fn heap_page_from_base(base: MemoryPage) -> MemoryPage { + MemoryPage(base.0 + 2) +} + +pub(crate) const fn aux_heap_page_from_base(base: MemoryPage) -> MemoryPage { + MemoryPage(base.0 + 3) +} + +pub(crate) trait FixedLengthIterator<'a, I: 'a, const N: usize>: Iterator +where + Self: 'a, +{ + fn next(&mut self) -> Option<::Item> { + ::next(self) + } +} + +pub(crate) trait IntoFixedLengthByteIterator { + type IntoIter: FixedLengthIterator<'static, u8, N>; + fn into_le_iter(self) -> Self::IntoIter; + fn into_be_iter(self) -> Self::IntoIter; +} + +pub(crate) struct FixedBufferValueIterator { + iter: std::array::IntoIter, +} + +impl Iterator for FixedBufferValueIterator { + type Item = T; + fn next(&mut self) -> Option { + self.iter.next() + } +} + +impl FixedLengthIterator<'static, T, N> + for FixedBufferValueIterator +{ +} + +impl IntoFixedLengthByteIterator<32> for U256 { + type IntoIter = FixedBufferValueIterator; + fn into_le_iter(self) -> Self::IntoIter { + let mut buffer = [0u8; 32]; + self.to_little_endian(&mut buffer); + + FixedBufferValueIterator { + iter: IntoIterator::into_iter(buffer), + } + } + + fn into_be_iter(self) -> Self::IntoIter { + let mut buffer = [0u8; 32]; + self.to_big_endian(&mut buffer); + + FixedBufferValueIterator { + iter: IntoIterator::into_iter(buffer), + } + } +} + +/// Receives sorted slice of timestamps. +/// Returns count of timestamps that are greater than or equal to `from_timestamp`. +/// Works in O(log(sorted_timestamps.len())). +pub(crate) fn precompile_calls_count_after_timestamp( + sorted_timestamps: &[Timestamp], + from_timestamp: Timestamp, +) -> usize { + sorted_timestamps.len() - sorted_timestamps.partition_point(|t| *t < from_timestamp) +} + +pub(crate) fn eth_price_per_pubdata_byte(l1_gas_price: u64) -> u64 { + // This value will typically be a lot less than u64 + // unless the gas price on L1 goes beyond tens of millions of gwei + l1_gas_price * (L1_GAS_PER_PUBDATA_BYTE as u64) +} + +pub(crate) fn vm_may_have_ended_inner( + vm: &ZkSyncVmState, +) -> Option { + let execution_has_ended = vm.execution_has_ended(); + + let r1 = vm.local_state.registers[RET_IMPLICIT_RETURNDATA_PARAMS_REGISTER as usize]; + let current_address = vm.local_state.callstack.get_current_stack().this_address; + + let outer_eh_location = >::PcOrImm::MAX.as_u64(); + match ( + execution_has_ended, + vm.local_state.callstack.get_current_stack().pc.as_u64(), + ) { + (true, 0) => { + let returndata = dump_memory_page_using_primitive_value(&vm.memory, r1); + + Some(VmExecutionResult::Ok(returndata)) + } + (false, _) => None, + (true, l) if l == outer_eh_location => { + // check r1,r2,r3 + if vm.local_state.flags.overflow_or_less_than_flag { + Some(VmExecutionResult::Panic) + } else { + let returndata = dump_memory_page_using_primitive_value(&vm.memory, r1); + Some(VmExecutionResult::Revert(returndata)) + } + } + (_, a) => Some(VmExecutionResult::MostLikelyDidNotFinish( + current_address, + a as u16, + )), + } +} + +pub(crate) fn dump_memory_page_using_primitive_value( + memory: &SimpleMemory, + ptr: PrimitiveValue, +) -> Vec { + if !ptr.is_pointer { + return vec![]; + } + let fat_ptr = FatPointer::from_u256(ptr.value); + dump_memory_page_using_fat_pointer(memory, fat_ptr) +} + +pub(crate) fn dump_memory_page_using_fat_pointer( + memory: &SimpleMemory, + fat_ptr: FatPointer, +) -> Vec { + dump_memory_page_by_offset_and_length( + memory, + fat_ptr.memory_page, + (fat_ptr.start + fat_ptr.offset) as usize, + (fat_ptr.length - fat_ptr.offset) as usize, + ) +} + +pub(crate) fn dump_memory_page_by_offset_and_length( + memory: &SimpleMemory, + page: u32, + offset: usize, + length: usize, +) -> Vec { + assert!(offset < (1u32 << 24) as usize); + assert!(length < (1u32 << 24) as usize); + let mut dump = Vec::with_capacity(length); + if length == 0 { + return dump; + } + + let first_word = offset / 32; + let end_byte = offset + length; + let mut last_word = end_byte / 32; + if end_byte % 32 != 0 { + last_word += 1; + } + + let unalignment = offset % 32; + + let page_part = + memory.dump_page_content_as_u256_words(page, (first_word as u32)..(last_word as u32)); + + let mut is_first = true; + let mut remaining = length; + for word in page_part.into_iter() { + let it = word.into_be_iter(); + if is_first { + is_first = false; + let it = it.skip(unalignment); + for next in it { + if remaining > 0 { + dump.push(next); + remaining -= 1; + } + } + } else { + for next in it { + if remaining > 0 { + dump.push(next); + remaining -= 1; + } + } + } + } + + assert_eq!( + dump.len(), + length, + "tried to dump with offset {}, length {}, got a bytestring of length {}", + offset, + length, + dump.len() + ); + + dump +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/bootloader.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/bootloader.rs new file mode 100644 index 00000000000..0479672a6ef --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/bootloader.rs @@ -0,0 +1,53 @@ +use zksync_types::U256; + +use crate::constants::BOOTLOADER_HEAP_PAGE; +use crate::errors::Halt; +use crate::tests::tester::VmTesterBuilder; +use crate::tests::utils::{get_bootloader, verify_required_memory, BASE_SYSTEM_CONTRACTS}; +use crate::types::inputs::system_env::TxExecutionMode; + +use crate::types::outputs::ExecutionResult; +use crate::{HistoryEnabled, VmExecutionMode}; + +#[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(HistoryEnabled) + .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.state, + vec![(correct_first_cell, BOOTLOADER_HEAP_PAGE, 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(HistoryEnabled) + .with_empty_in_memory_storage() + .with_base_system_smart_contracts(base_system_contracts) + .with_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/multivm_deps/vm_virtual_blocks/src/tests/bytecode_publishing.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/bytecode_publishing.rs new file mode 100644 index 00000000000..60e45e25257 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/bytecode_publishing.rs @@ -0,0 +1,37 @@ +use zksync_types::event::extract_long_l2_to_l1_messages; +use zksync_utils::bytecode::compress_bytecode; + +use crate::tests::tester::{DeployContractsTx, TxType, VmTesterBuilder}; +use crate::tests::utils::read_test_contract; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::{HistoryEnabled, VmExecutionMode}; + +#[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(HistoryEnabled) + .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"); + + vm.vm.execute(VmExecutionMode::Batch); + + 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/multivm_deps/vm_virtual_blocks/src/tests/call_tracer.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/call_tracer.rs new file mode 100644 index 00000000000..d55ba826030 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/call_tracer.rs @@ -0,0 +1,86 @@ +use crate::constants::BLOCK_GAS_LIMIT; +use crate::tests::tester::VmTesterBuilder; +use crate::tests::utils::{read_max_depth_contract, read_test_contract}; +use crate::{CallTracer, HistoryEnabled, TxExecutionMode, VmExecutionMode}; +use once_cell::sync::OnceCell; +use std::sync::Arc; +use zksync_types::{Address, Execute}; + +// This test is ultra slow, so it's ignored by default. +#[test] +#[ignore] +fn test_max_depth() { + let contarct = read_max_depth_contract(); + let address = Address::random(); + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .with_empty_in_memory_storage() + .with_random_rich_accounts(1) + .with_deployer() + .with_gas_limit(BLOCK_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: None, + }, + None, + ); + + let result = Arc::new(OnceCell::new()); + let call_tracer = CallTracer::new(result.clone(), HistoryEnabled); + vm.vm.push_transaction(tx); + let res = vm + .vm + .inspect(vec![Box::new(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(HistoryEnabled) + .with_empty_in_memory_storage() + .with_random_rich_accounts(1) + .with_deployer() + .with_gas_limit(BLOCK_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: None, + }, + None, + ); + + let result = Arc::new(OnceCell::new()); + let call_tracer = CallTracer::new(result.clone(), HistoryEnabled); + vm.vm.push_transaction(tx); + let res = vm + .vm + .inspect(vec![Box::new(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/multivm_deps/vm_virtual_blocks/src/tests/default_aa.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/default_aa.rs new file mode 100644 index 00000000000..b4c5f6b5832 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/default_aa.rs @@ -0,0 +1,68 @@ +use zksync_config::constants::L2_ETH_TOKEN_ADDRESS; +use zksync_types::system_contracts::{DEPLOYMENT_NONCE_INCREMENT, TX_NONCE_INCREMENT}; + +use zksync_types::{get_code_key, get_known_code_key, get_nonce_key, AccountTreeId, U256}; +use zksync_utils::u256_to_h256; + +use crate::tests::tester::{DeployContractsTx, TxType, VmTesterBuilder}; +use crate::tests::utils::{get_balance, read_test_contract, verify_required_storage}; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::{HistoryEnabled, VmExecutionMode}; + +#[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(HistoryEnabled) + .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() * vm.vm.batch_env.base_fee(); + + 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 = vec![ + (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(&vm.vm.state, expected_slots); + + let expected_fee = maximal_fee + - U256::from(result.refunds.gas_refunded) * U256::from(vm.vm.batch_env.base_fee()); + let operator_balance = get_balance( + AccountTreeId::new(L2_ETH_TOKEN_ADDRESS), + &vm.fee_account, + vm.vm.state.storage.storage.get_ptr(), + ); + + assert_eq!( + operator_balance, expected_fee, + "Operator did not receive his fee" + ); +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/gas_limit.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/gas_limit.rs new file mode 100644 index 00000000000..c439b6d89b2 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/gas_limit.rs @@ -0,0 +1,45 @@ +use zksync_types::fee::Fee; +use zksync_types::Execute; + +use crate::constants::{BOOTLOADER_HEAP_PAGE, TX_DESCRIPTION_OFFSET, TX_GAS_LIMIT_OFFSET}; +use crate::tests::tester::VmTesterBuilder; + +use crate::types::inputs::system_env::TxExecutionMode; +use crate::HistoryDisabled; + +/// Checks that `TX_GAS_LIMIT_OFFSET` constant is correct. +#[test] +fn test_tx_gas_limit_offset() { + let mut vm = VmTesterBuilder::new(HistoryDisabled) + .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 { + contract_address: Default::default(), + calldata: vec![], + value: Default::default(), + factory_deps: None, + }, + Some(Fee { + gas_limit, + ..Default::default() + }), + ); + + vm.vm.push_transaction(tx); + + let gas_limit_from_memory = vm + .vm + .state + .memory + .read_slot( + BOOTLOADER_HEAP_PAGE as usize, + TX_DESCRIPTION_OFFSET + TX_GAS_LIMIT_OFFSET, + ) + .value; + assert_eq!(gas_limit_from_memory, gas_limit); +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/get_used_contracts.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/get_used_contracts.rs new file mode 100644 index 00000000000..90a8816eb55 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/get_used_contracts.rs @@ -0,0 +1,104 @@ +use std::collections::{HashMap, HashSet}; + +use itertools::Itertools; + +use zksync_config::constants::CONTRACT_DEPLOYER_ADDRESS; +use zksync_state::WriteStorage; +use zksync_test_account::Account; +use zksync_types::{Execute, U256}; +use zksync_utils::bytecode::hash_bytecode; +use zksync_utils::h256_to_u256; + +use crate::tests::tester::{TxType, VmTesterBuilder}; +use crate::tests::utils::{read_test_contract, BASE_SYSTEM_CONTRACTS}; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::{HistoryDisabled, HistoryMode, Vm, VmExecutionMode}; + +#[test] +fn test_get_used_contracts() { + let mut vm = VmTesterBuilder::new(HistoryDisabled) + .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_used_contracts() 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 + .get_used_contracts() + .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 + .get_used_contracts() + .into_iter() + .collect::>(), + known_bytecodes_without_aa_code(&vm.vm) + .keys() + .cloned() + .collect::>() + ); + + // create push and execute some non-empty factory deps transaction that fails + // (known_bytecodes will be updated but we expect get_used_contracts() 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: Some(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.unwrap() { + let hash = hash_bytecode(&factory_dep); + let hash_to_u256 = h256_to_u256(hash); + assert!(known_bytecodes_without_aa_code(&vm.vm) + .keys() + .contains(&hash_to_u256)); + assert!(!vm.vm.get_used_contracts().contains(&hash_to_u256)); + } +} + +fn known_bytecodes_without_aa_code( + vm: &Vm, +) -> HashMap> { + let mut known_bytecodes_without_aa_code = vm + .state + .decommittment_processor + .known_bytecodes + .inner() + .clone(); + + known_bytecodes_without_aa_code + .remove(&h256_to_u256(BASE_SYSTEM_CONTRACTS.default_aa.hash)) + .unwrap(); + + known_bytecodes_without_aa_code +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/invalid_bytecode.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/invalid_bytecode.rs new file mode 100644 index 00000000000..6353d445e71 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/invalid_bytecode.rs @@ -0,0 +1,120 @@ +use zksync_types::H256; +use zksync_utils::h256_to_u256; + +use crate::tests::tester::VmTesterBuilder; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::{HistoryEnabled, TxRevertReason}; + +// TODO this test requires a lot of hacks for bypassing the bytecode checks in the VM. +// Port it later, it's not significant. for now + +#[test] +fn test_invalid_bytecode() { + let mut vm_builder = VmTesterBuilder::new(HistoryEnabled) + .with_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1); + let mut storage = vm_builder.take_storage(); + let mut vm = vm_builder.build(&mut storage); + + let block_gas_per_pubdata = vm_test_env + .block_context + .context + .block_gas_price_per_pubdata(); + + let mut test_vm_with_custom_bytecode_hash = + |bytecode_hash: H256, expected_revert_reason: Option| { + let mut oracle_tools = + OracleTools::new(vm_test_env.storage_ptr.as_mut(), HistoryEnabled); + + let (encoded_tx, predefined_overhead) = get_l1_tx_with_custom_bytecode_hash( + h256_to_u256(bytecode_hash), + block_gas_per_pubdata as u32, + ); + + run_vm_with_custom_factory_deps( + &mut oracle_tools, + vm_test_env.block_context.context, + &vm_test_env.block_properties, + encoded_tx, + predefined_overhead, + expected_revert_reason, + ); + }; + + let failed_to_mark_factory_deps = |msg: &str, data: Vec| { + TxRevertReason::FailedToMarkFactoryDependencies(VmRevertReason::General { + msg: msg.to_string(), + data, + }) + }; + + // Here we provide the correctly-formatted bytecode hash of + // odd length, so it should work. + test_vm_with_custom_bytecode_hash( + H256([ + 1, 0, 0, 1, 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, + ]), + None, + ); + + // Here we provide correctly formatted bytecode of even length, so + // it should fail. + test_vm_with_custom_bytecode_hash( + H256([ + 1, 0, 2, 2, 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, + ]), + Some(failed_to_mark_factory_deps( + "Code length in words must be odd", + 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, 32, 67, 111, 100, 101, 32, 108, 101, 110, + 103, 116, 104, 32, 105, 110, 32, 119, 111, 114, 100, 115, 32, 109, 117, 115, 116, + 32, 98, 101, 32, 111, 100, 100, + ], + )), + ); + + // Here we provide incorrectly formatted bytecode of odd length, so + // it should fail. + test_vm_with_custom_bytecode_hash( + H256([ + 1, 1, 0, 1, 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, + ]), + Some(failed_to_mark_factory_deps( + "Incorrectly formatted bytecodeHash", + 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, 34, 73, 110, 99, 111, 114, 114, 101, 99, + 116, 108, 121, 32, 102, 111, 114, 109, 97, 116, 116, 101, 100, 32, 98, 121, 116, + 101, 99, 111, 100, 101, 72, 97, 115, 104, 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, + ], + )), + ); + + // Here we provide incorrectly formatted bytecode of odd length, so + // it should fail. + test_vm_with_custom_bytecode_hash( + H256([ + 2, 0, 0, 1, 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, + ]), + Some(failed_to_mark_factory_deps( + "Incorrectly formatted bytecodeHash", + 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, 34, 73, 110, 99, 111, 114, 114, 101, 99, + 116, 108, 121, 32, 102, 111, 114, 109, 97, 116, 116, 101, 100, 32, 98, 121, 116, + 101, 99, 111, 100, 101, 72, 97, 115, 104, 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, + ], + )), + ); +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/is_write_initial.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/is_write_initial.rs new file mode 100644 index 00000000000..7ccdf072744 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/is_write_initial.rs @@ -0,0 +1,42 @@ +use zksync_state::ReadStorage; +use zksync_types::get_nonce_key; + +use crate::tests::tester::{Account, TxType, VmTesterBuilder}; +use crate::tests::utils::read_test_contract; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::{HistoryDisabled, VmExecutionMode}; + +#[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(HistoryDisabled) + .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/multivm_deps/vm_virtual_blocks/src/tests/l1_tx_execution.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/l1_tx_execution.rs new file mode 100644 index 00000000000..cd1c8f2460c --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/l1_tx_execution.rs @@ -0,0 +1,123 @@ +use zksync_config::constants::BOOTLOADER_ADDRESS; +use zksync_types::l2_to_l1_log::L2ToL1Log; +use zksync_types::storage_writes_deduplicator::StorageWritesDeduplicator; +use zksync_types::{get_code_key, get_known_code_key, U256}; +use zksync_utils::u256_to_h256; + +use crate::tests::tester::{TxType, VmTesterBuilder}; +use crate::tests::utils::{read_test_contract, verify_required_storage, BASE_SYSTEM_CONTRACTS}; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::types::internals::TransactionData; +use crate::{HistoryEnabled, VmExecutionMode}; + +#[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 3 initial writes here, because we pay fees from l1: + // - totalSupply of ETH token + // - balance of the refund recipient + // - balance of the bootloader + // - tx_rollout hash + + let basic_initial_writes = 1; + + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .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![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)), + }]; + + 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); + + let expected_slots = vec![ + (u256_to_h256(U256::from(1u32)), known_codes_key), + (deploy_tx.bytecode_hash, account_code_key), + ]; + assert!(!res.result.is_failed()); + + verify_required_storage(&vm.vm.state, expected_slots); + + assert_eq!(res.logs.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, 0); + + 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 = res.logs.storage_logs; + let res = StorageWritesDeduplicator::apply_on_empty_state(&storage_logs); + // We changed one slot inside contract + assert_eq!(res.initial_storage_writes - basic_initial_writes, 1); + + // 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 + 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); + // There are only basic initial writes + assert_eq!(res.initial_storage_writes - basic_initial_writes, 2); +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/l2_blocks.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/l2_blocks.rs new file mode 100644 index 00000000000..9deac837f90 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/l2_blocks.rs @@ -0,0 +1,500 @@ +//! +//! Tests for the bootloader +//! The description for each of the tests can be found in the corresponding `.yul` file. +//! + +use crate::constants::{ + BOOTLOADER_HEAP_PAGE, TX_OPERATOR_L2_BLOCK_INFO_OFFSET, TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO, +}; +use crate::tests::tester::default_l1_batch; +use crate::tests::tester::VmTesterBuilder; +use crate::utils::l2_blocks::get_l2_block_hash_key; +use crate::{ + ExecutionResult, Halt, HistoryEnabled, HistoryMode, L2BlockEnv, TxExecutionMode, Vm, + VmExecutionMode, +}; +use zk_evm::aux_structures::Timestamp; +use zksync_config::constants::{ + CURRENT_VIRTUAL_BLOCK_INFO_POSITION, REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE, +}; +use zksync_state::{ReadStorage, WriteStorage}; +use zksync_types::block::{pack_block_info, unpack_block_info}; +use zksync_types::{ + block::{legacy_miniblock_hash, miniblock_hash}, + get_code_key, AccountTreeId, Execute, ExecuteTransactionCommon, L1BatchNumber, L1TxCommonData, + MiniblockNumber, 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}; + +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: None, + }, + 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(HistoryEnabled) + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + // Override the timestamp of the current miniblock to be 0. + vm.vm.bootloader_state.push_l2_block(L2BlockEnv { + number: 1, + timestamp: 0, + prev_block_hash: legacy_miniblock_hash(MiniblockNumber(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 miniblock 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: legacy_miniblock_hash(MiniblockNumber(0)), + max_virtual_blocks_to_create: 1, + }; + + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .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); + + let timestamp = Timestamp(vm.vm.state.local_state.timestamp); + set_manual_l2_block_info(&mut vm.vm, 0, first_l2_block, timestamp); + + 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(HistoryEnabled) + .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); + let timestamp = Timestamp(vm.vm.state.local_state.timestamp); + set_manual_l2_block_info(&mut vm.vm, 1, current_l2_block, timestamp); + + 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(HistoryEnabled) + .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: legacy_miniblock_hash(MiniblockNumber(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(HistoryEnabled) + .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 storage_ptr = vm.vm.state.storage.storage.get_ptr(); + 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.borrow_mut().set_value( + miniblock_info_slot, + u256_to_h256(pack_block_info( + miniblock_number as u64, + miniblock_timestamp, + )), + ); + storage_ptr + .borrow_mut() + .set_value(pending_txs_hash_slot, pending_txs_hash); + storage_ptr.borrow_mut().set_value( + batch_info_slot, + u256_to_h256(pack_block_info(batch_number as u64, batch_timestamp)), + ); + storage_ptr.borrow_mut().set_value( + prev_block_hash_position, + legacy_miniblock_hash(MiniblockNumber(miniblock_number - 1)), + ); + + // 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); + let timestamp = Timestamp(vm.vm.state.local_state.timestamp); + set_manual_l2_block_info(&mut vm.vm, 0, proposed_block, timestamp); + + 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() { + test_first_in_batch( + 1, + 1, + H256::zero(), + 1, + 2, + 1, + L2BlockEnv { + number: 2, + timestamp: 2, + prev_block_hash: miniblock_hash( + MiniblockNumber(1), + 1, + legacy_miniblock_hash(MiniblockNumber(0)), + H256::zero(), + ), + max_virtual_blocks_to_create: 1, + }, + None, + ); + + test_first_in_batch( + 8, + 1, + H256::zero(), + 5, + 12, + 1, + L2BlockEnv { + number: 2, + timestamp: 9, + prev_block_hash: miniblock_hash(MiniblockNumber(1), 8, legacy_miniblock_hash(MiniblockNumber(0)), H256::zero()), + 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())), + ); +} + +#[test] +fn test_l2_block_upgrade() { + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + vm.vm + .state + .storage + .storage + .get_ptr() + .borrow_mut() + .set_value(get_code_key(&SYSTEM_CONTEXT_ADDRESS), H256::default()); + + let l1_tx = get_l1_noop(); + // Firstly we execute the first transaction + vm.vm.push_transaction(l1_tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed(), "No revert reason expected"); + let result = vm.vm.execute(VmExecutionMode::Batch); + assert!(!result.result.is_failed(), "No revert reason expected"); +} + +#[test] +fn test_l2_block_upgrade_ending() { + let mut l1_batch = default_l1_batch(L1BatchNumber(1)); + l1_batch.timestamp = 1; + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_l1_batch_env(l1_batch.clone()) + .with_random_rich_accounts(1) + .build(); + + let l1_tx = get_l1_noop(); + + let storage = vm.storage.clone(); + + storage + .borrow_mut() + .set_value(get_code_key(&SYSTEM_CONTEXT_ADDRESS), H256::default()); + + vm.vm.push_transaction(l1_tx.clone()); + let result = vm.vm.execute(VmExecutionMode::OneTx); + + assert!(!result.result.is_failed(), "No revert reason expected"); + + let virtual_block_info = storage.borrow_mut().read_value(&StorageKey::new( + AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS), + CURRENT_VIRTUAL_BLOCK_INFO_POSITION, + )); + + let (virtual_block_number, virtual_block_timestamp) = + unpack_block_info(h256_to_u256(virtual_block_info)); + + assert_eq!(virtual_block_number as u32, l1_batch.first_l2_block.number); + assert_eq!(virtual_block_timestamp, l1_batch.first_l2_block.timestamp); + vm.vm.push_transaction(l1_tx); + let result = vm.vm.execute(VmExecutionMode::OneTx); + assert!(!result.result.is_failed(), "No revert reason expected"); + let result = vm.vm.execute(VmExecutionMode::Batch); + assert!(!result.result.is_failed(), "No revert reason expected"); +} + +fn set_manual_l2_block_info( + vm: &mut Vm, + tx_number: usize, + block_info: L2BlockEnv, + timestamp: Timestamp, +) { + let fictive_miniblock_position = + TX_OPERATOR_L2_BLOCK_INFO_OFFSET + TX_OPERATOR_SLOTS_PER_L2_BLOCK_INFO * tx_number; + + vm.state.memory.populate_page( + BOOTLOADER_HEAP_PAGE as usize, + vec![ + (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(), + ), + ], + timestamp, + ) +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/mod.rs new file mode 100644 index 00000000000..ffb38dd3725 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/mod.rs @@ -0,0 +1,20 @@ +mod bootloader; +mod default_aa; +// TODO - fix this test +// mod invalid_bytecode; +mod bytecode_publishing; +mod call_tracer; +mod gas_limit; +mod get_used_contracts; +mod is_write_initial; +mod l1_tx_execution; +mod l2_blocks; +mod nonce_holder; +mod refunds; +mod require_eip712; +mod rollbacks; +mod simple_execution; +mod tester; +mod tracing_execution_error; +mod upgrade; +mod utils; diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/nonce_holder.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/nonce_holder.rs new file mode 100644 index 00000000000..35af6ad15f4 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/nonce_holder.rs @@ -0,0 +1,180 @@ +use zksync_types::{Execute, Nonce}; + +use crate::errors::VmRevertReason; +use crate::tests::tester::{Account, VmTesterBuilder}; +use crate::tests::utils::read_nonce_holder_tester; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::types::internals::TransactionData; +use crate::{ExecutionResult, Halt, HistoryEnabled, TxRevertReason, VmExecutionMode}; + +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(HistoryEnabled) + .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_data: TransactionData = account + .get_l2_tx_for_execute_with_nonce( + Execute { + contract_address: account.address, + calldata: vec![12], + value: Default::default(), + factory_deps: None, + }, + None, + Nonce(nonce), + ) + .into(); + + transaction_data.signature = vec![test_mode.into()]; + vm.vm.push_raw_transaction(transaction_data, 0, 0, true); + 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); + } + }; + // 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/multivm_deps/vm_virtual_blocks/src/tests/refunds.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/refunds.rs new file mode 100644 index 00000000000..4314f57489e --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/refunds.rs @@ -0,0 +1,152 @@ +use crate::tests::tester::{DeployContractsTx, TxType, VmTesterBuilder}; +use crate::tests::utils::read_test_contract; +use crate::types::inputs::system_env::TxExecutionMode; + +use crate::types::internals::TransactionData; +use crate::{HistoryEnabled, VmExecutionMode}; + +#[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(HistoryEnabled) + .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(); + 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(HistoryEnabled) + .with_empty_in_memory_storage() + .with_l1_batch_env(l1_batch.clone()) + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_rich_accounts(vec![account.clone()]) + .build(); + + let tx: TransactionData = tx.into(); + let block_gas_per_pubdata_byte = vm.vm.batch_env.block_gas_price_per_pubdata(); + // Overhead + let overhead = tx.overhead_gas(block_gas_per_pubdata_byte as u32); + vm.vm + .push_raw_transaction(tx.clone(), overhead, 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(); + + assert!(!result_with_predefined_refunds.result.is_failed()); + + // We need to sort these lists as those are flattened from HashMaps + current_state_with_predefined_refunds + .used_contract_hashes + .sort(); + current_state_without_predefined_refunds + .used_contract_hashes + .sort(); + + assert_eq!( + current_state_with_predefined_refunds.events, + current_state_without_predefined_refunds.events + ); + + assert_eq!( + current_state_with_predefined_refunds.l2_to_l1_logs, + current_state_without_predefined_refunds.l2_to_l1_logs + ); + + assert_eq!( + current_state_with_predefined_refunds.storage_log_queries, + current_state_without_predefined_refunds.storage_log_queries + ); + 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(HistoryEnabled) + .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_raw_transaction(tx, overhead, 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(); + + assert!(!result.result.is_failed()); + current_state_with_changed_predefined_refunds + .used_contract_hashes + .sort(); + current_state_without_predefined_refunds + .used_contract_hashes + .sort(); + + 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.l2_to_l1_logs, + current_state_without_predefined_refunds.l2_to_l1_logs + ); + + assert_eq!( + current_state_with_changed_predefined_refunds + .storage_log_queries + .len(), + current_state_without_predefined_refunds + .storage_log_queries + .len() + ); + + assert_ne!( + current_state_with_changed_predefined_refunds.storage_log_queries, + current_state_without_predefined_refunds.storage_log_queries + ); + assert_eq!( + current_state_with_changed_predefined_refunds.used_contract_hashes, + current_state_without_predefined_refunds.used_contract_hashes + ); +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/require_eip712.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/require_eip712.rs new file mode 100644 index 00000000000..6538318c26f --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/require_eip712.rs @@ -0,0 +1,161 @@ +use std::convert::TryInto; + +use ethabi::Token; + +use zksync_config::constants::L2_ETH_TOKEN_ADDRESS; +use zksync_eth_signer::raw_ethereum_tx::TransactionParameters; +use zksync_eth_signer::EthereumSigner; +use zksync_types::fee::Fee; +use zksync_types::l2::L2Tx; +use zksync_types::transaction_request::TransactionRequest; +use zksync_types::utils::storage_key_for_standard_token_balance; +use zksync_types::{AccountTreeId, Address, Eip712Domain, Execute, Nonce, Transaction, U256}; + +use crate::tests::tester::{Account, VmTester, VmTesterBuilder}; +use crate::tests::utils::read_many_owners_custom_account_contract; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::{HistoryDisabled, VmExecutionMode}; + +impl VmTester { + pub(crate) fn get_eth_balance(&mut self, address: Address) -> U256 { + let key = storage_key_for_standard_token_balance( + AccountTreeId::new(L2_ETH_TOKEN_ADDRESS), + &address, + ); + self.vm.state.storage.storage.read_from_storage(&key) + } +} + +// TODO refactor this test it use too much internal details of the VM +#[tokio::test] +/// 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. +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(HistoryDisabled) + .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: None, + }, + 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), + }; + + let aa_tx = private_account.sign_legacy_tx(aa_raw_tx).await; + let (tx_request, hash) = TransactionRequest::from_bytes(&aa_tx, 270.into()).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.try_into().unwrap(); + + 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 transfered 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), + None, + Default::default(), + ); + + let transaction_request: TransactionRequest = tx_712.into(); + + let domain = Eip712Domain::new(chain_id.into()); + let signature = private_account + .get_pk_signer() + .sign_typed_data(&domain, &transaction_request) + .await + .unwrap(); + let encoded_tx = transaction_request.get_signed_bytes(&signature, chain_id.into()); + + let (aa_txn_request, aa_hash) = + TransactionRequest::from_bytes(&encoded_tx, chain_id.into()).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.try_into().unwrap(); + 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/multivm_deps/vm_virtual_blocks/src/tests/rollbacks.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/rollbacks.rs new file mode 100644 index 00000000000..1fa6a2afe39 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/rollbacks.rs @@ -0,0 +1,146 @@ +use ethabi::Token; + +use zksync_contracts::get_loadnext_contract; +use zksync_contracts::test_contracts::LoadnextContractExecutionParams; + +use zksync_types::{Execute, U256}; + +use crate::tests::tester::{ + DeployContractsTx, TransactionTestInfo, TxModifier, TxType, VmTesterBuilder, +}; +use crate::tests::utils::read_test_contract; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::HistoryEnabled; + +#[test] +fn test_vm_rollbacks() { + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .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 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 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()), + ]); + + assert_eq!(result_without_rollbacks, result_with_rollbacks); +} + +#[test] +fn test_vm_loadnext_rollbacks() { + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .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: None, + }, + 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: None, + }, + None, + ); + + let 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 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), + ]); + + assert_eq!(result_without_rollbacks, result_with_rollbacks); +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/simple_execution.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/simple_execution.rs new file mode 100644 index 00000000000..40e51739b07 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/simple_execution.rs @@ -0,0 +1,77 @@ +use crate::tests::tester::{TxType, VmTesterBuilder}; +use crate::types::outputs::ExecutionResult; +use crate::{HistoryDisabled, VmExecutionMode}; + +#[test] +fn estimate_fee() { + let mut vm_tester = VmTesterBuilder::new(HistoryDisabled) + .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(HistoryDisabled) + .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/multivm_deps/vm_virtual_blocks/src/tests/tester/inner_state.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/tester/inner_state.rs new file mode 100644 index 00000000000..08220724b4d --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/tester/inner_state.rs @@ -0,0 +1,116 @@ +use std::collections::HashMap; + +use zk_evm::aux_structures::Timestamp; +use zk_evm::vm_state::VmLocalState; +use zksync_state::WriteStorage; + +use zksync_types::{StorageKey, StorageLogQuery, StorageValue, U256}; + +use crate::old_vm::event_sink::InMemoryEventSink; +use crate::old_vm::history_recorder::{AppDataFrameManagerWithHistory, HistoryRecorder}; +use crate::{HistoryEnabled, HistoryMode, SimpleMemory, Vm}; + +#[derive(Clone, Debug)] +pub(crate) struct ModifiedKeysMap(HashMap); + +// We consider hashmaps to be equal even if there is a key +// that is not present in one but has zero value in another. +impl PartialEq for ModifiedKeysMap { + fn eq(&self, other: &Self) -> bool { + for (key, value) in self.0.iter() { + if *value != other.0.get(key).cloned().unwrap_or_default() { + return false; + } + } + for (key, value) in other.0.iter() { + if *value != self.0.get(key).cloned().unwrap_or_default() { + return false; + } + } + true + } +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct DecommitterTestInnerState { + /// There is no way to "trully" compare the storage pointer, + /// so we just compare the modified keys. This is reasonable enough. + pub(crate) modified_storage_keys: ModifiedKeysMap, + pub(crate) known_bytecodes: HistoryRecorder>, H>, + pub(crate) decommitted_code_hashes: HistoryRecorder, HistoryEnabled>, +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct StorageOracleInnerState { + /// There is no way to "trully" compare the storage pointer, + /// so we just compare the modified keys. This is reasonable enough. + pub(crate) modified_storage_keys: ModifiedKeysMap, + + pub(crate) frames_stack: AppDataFrameManagerWithHistory, H>, +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct PrecompileProcessorTestInnerState { + pub(crate) timestamp_history: HistoryRecorder, H>, +} + +/// A struct that encapsulates the state of the VM's oracles +/// The state is to be used in tests. +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct VmInstanceInnerState { + event_sink: InMemoryEventSink, + precompile_processor_state: PrecompileProcessorTestInnerState, + memory: SimpleMemory, + decommitter_state: DecommitterTestInnerState, + storage_oracle_state: StorageOracleInnerState, + local_state: VmLocalState, +} + +impl Vm { + // Dump inner state of the VM. + pub(crate) fn dump_inner_state(&self) -> VmInstanceInnerState { + let event_sink = self.state.event_sink.clone(); + let precompile_processor_state = PrecompileProcessorTestInnerState { + timestamp_history: self.state.precompiles_processor.timestamp_history.clone(), + }; + let memory = self.state.memory.clone(); + let decommitter_state = DecommitterTestInnerState { + modified_storage_keys: ModifiedKeysMap( + self.state + .decommittment_processor + .get_storage() + .borrow() + .modified_storage_keys() + .clone(), + ), + known_bytecodes: self.state.decommittment_processor.known_bytecodes.clone(), + decommitted_code_hashes: self + .state + .decommittment_processor + .get_decommitted_code_hashes_with_history() + .clone(), + }; + let storage_oracle_state = StorageOracleInnerState { + modified_storage_keys: ModifiedKeysMap( + self.state + .storage + .storage + .get_ptr() + .borrow() + .modified_storage_keys() + .clone(), + ), + frames_stack: self.state.storage.frames_stack.clone(), + }; + let local_state = self.state.local_state.clone(); + + VmInstanceInnerState { + event_sink, + precompile_processor_state, + memory, + decommitter_state, + storage_oracle_state, + local_state, + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/tester/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/tester/mod.rs new file mode 100644 index 00000000000..dfe8905a7e0 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/tester/mod.rs @@ -0,0 +1,7 @@ +pub(crate) use transaction_test_info::{ExpectedError, TransactionTestInfo, TxModifier}; +pub(crate) use vm_tester::{default_l1_batch, InMemoryStorageView, VmTester, VmTesterBuilder}; +pub(crate) use zksync_test_account::{Account, DeployContractsTx, TxType}; + +mod inner_state; +mod transaction_test_info; +mod vm_tester; diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/tester/transaction_test_info.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/tester/transaction_test_info.rs new file mode 100644 index 00000000000..65ceb3c5cf3 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/tester/transaction_test_info.rs @@ -0,0 +1,216 @@ +use zksync_types::{ExecuteTransactionCommon, Transaction}; + +use crate::errors::VmRevertReason; +use crate::tests::tester::vm_tester::VmTester; +use crate::{ + CurrentExecutionState, ExecutionResult, Halt, HistoryEnabled, TxRevertReason, VmExecutionMode, + VmExecutionResultAndLogs, +}; + +#[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, + } + } +} + +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 { + let inner_state_before = self.vm.dump_inner_state(); + self.vm.make_snapshot(); + 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_inner_state(); + assert_eq!( + inner_state_before, inner_state_after, + "Inner state before and after rollback should be equal" + ); + } + result + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/tester/vm_tester.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/tester/vm_tester.rs new file mode 100644 index 00000000000..3e69d2f01a5 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/tester/vm_tester.rs @@ -0,0 +1,298 @@ +use zksync_contracts::BaseSystemContracts; +use zksync_state::{InMemoryStorage, StoragePtr, StorageView, WriteStorage}; + +use zksync_types::block::legacy_miniblock_hash; +use zksync_types::helpers::unix_timestamp_ms; +use zksync_types::utils::{deployed_address_create, storage_key_for_eth_balance}; +use zksync_types::{ + get_code_key, get_is_account_key, Address, L1BatchNumber, MiniblockNumber, Nonce, + ProtocolVersionId, U256, +}; +use zksync_utils::bytecode::hash_bytecode; +use zksync_utils::u256_to_h256; + +use crate::constants::BLOCK_GAS_LIMIT; + +use crate::tests::tester::Account; +use crate::tests::tester::TxType; +use crate::tests::utils::read_test_contract; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::utils::l2_blocks::load_last_l2_block; +use crate::{HistoryMode, L1BatchEnv, L2Block, L2BlockEnv, SystemEnv, Vm, VmExecutionMode}; + +pub(crate) type InMemoryStorageView = StorageView; + +pub(crate) struct VmTester { + pub(crate) vm: Vm, + pub(crate) storage: StoragePtr, + pub(crate) fee_account: Address, + pub(crate) deployer: Option, + pub(crate) test_contract: Option
, + pub(crate) rich_accounts: Vec, + pub(crate) custom_contracts: Vec, + history_mode: H, +} + +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 = StorageView::new(get_empty_storage()).to_rc_ptr(); + 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 mut l1_batch = self.vm.batch_env.clone(); + if use_latest_l2_block { + let last_l2_block = load_last_l2_block(self.storage.clone()).unwrap_or(L2Block { + number: 0, + timestamp: 0, + hash: legacy_miniblock_hash(MiniblockNumber(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(), + self.storage.clone(), + self.history_mode.clone(), + ); + + if self.test_contract.is_some() { + self.deploy_test_contract(); + } + + self.vm = vm; + } +} + +pub(crate) type ContractsToDeploy = (Vec, Address, bool); + +pub(crate) struct VmTesterBuilder { + history_mode: H, + 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 { + history_mode: self.history_mode.clone(), + 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(history_mode: H) -> Self { + Self { + history_mode, + storage: None, + l1_batch_env: None, + system_env: SystemEnv { + zk_porter_available: false, + version: ProtocolVersionId::latest(), + base_system_smart_contracts: BaseSystemContracts::playground(), + gas_limit: BLOCK_GAS_LIMIT, + execution_mode: TxExecutionMode::VerifyExecute, + default_validation_computational_gas_limit: BLOCK_GAS_LIMIT, + chain_id: 270.into(), + }, + 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_gas_limit(mut self, gas_limit: u32) -> Self { + self.system_env.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 = Some(get_empty_storage()); + 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 mut raw_storage = self.storage.unwrap_or_else(get_empty_storage); + insert_contracts(&mut raw_storage, &self.custom_contracts); + let storage_ptr = StorageView::new(raw_storage).to_rc_ptr(); + 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(), + self.history_mode.clone(), + ); + + VmTester { + vm, + storage: storage_ptr, + fee_account, + deployer: self.deployer, + test_contract: None, + rich_accounts: self.rich_accounts.clone(), + custom_contracts: self.custom_contracts.clone(), + history_mode: self.history_mode, + } + } +} + +pub(crate) fn default_l1_batch(number: L1BatchNumber) -> L1BatchEnv { + let timestamp = unix_timestamp_ms(); + L1BatchEnv { + previous_batch_hash: None, + number, + timestamp, + l1_gas_price: 50_000_000_000, // 50 gwei + fair_l2_gas_price: 250_000_000, // 0.25 gwei + fee_account: Address::random(), + enforced_base_fee: None, + first_l2_block: L2BlockEnv { + number: 1, + timestamp, + prev_block_hash: legacy_miniblock_hash(MiniblockNumber(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 InMemoryStorage, contracts: &[ContractsToDeploy]) { + for (contract, address, is_account) in contracts { + let deployer_code_key = get_code_key(address); + raw_storage.set_value(deployer_code_key, hash_bytecode(contract)); + + if *is_account { + let is_account_key = get_is_account_key(address); + raw_storage.set_value(is_account_key, u256_to_h256(1_u32.into())); + } + + raw_storage.store_factory_dep(hash_bytecode(contract), contract.clone()); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/tracing_execution_error.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/tracing_execution_error.rs new file mode 100644 index 00000000000..dbe9f74a85b --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/tracing_execution_error.rs @@ -0,0 +1,49 @@ +use zksync_types::{Execute, H160}; + +use crate::errors::VmRevertReason; +use crate::tests::tester::{ExpectedError, TransactionTestInfo, VmTesterBuilder}; +use crate::tests::utils::{get_execute_error_calldata, read_error_contract, BASE_SYSTEM_CONTRACTS}; +use crate::types::inputs::system_env::TxExecutionMode; +use crate::{HistoryEnabled, TxRevertReason}; + +#[test] +fn test_tracing_of_execution_errors() { + let contract_address = H160::random(); + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .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: Some(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/multivm_deps/vm_virtual_blocks/src/tests/upgrade.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/upgrade.rs new file mode 100644 index 00000000000..05646326ffd --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/upgrade.rs @@ -0,0 +1,341 @@ +use zk_evm::aux_structures::Timestamp; + +use zksync_types::{ + ethabi::Contract, + Execute, COMPLEX_UPGRADER_ADDRESS, CONTRACT_DEPLOYER_ADDRESS, CONTRACT_FORCE_DEPLOYER_ADDRESS, + REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE, + {ethabi::Token, Address, ExecuteTransactionCommon, Transaction, H256, U256}, + {get_code_key, get_known_code_key, H160}, +}; + +use zksync_utils::{bytecode::hash_bytecode, bytes_to_be_words, h256_to_u256, u256_to_h256}; + +use zksync_contracts::{deployer_contract, load_contract, load_sys_contract, read_bytecode}; +use zksync_state::WriteStorage; +use zksync_test_account::TxType; + +use crate::tests::tester::VmTesterBuilder; +use crate::tests::utils::verify_required_storage; +use crate::{ExecutionResult, Halt, HistoryEnabled, TxExecutionMode, VmExecutionMode}; +use zksync_types::protocol_version::ProtocolUpgradeTxCommonData; + +use super::utils::read_test_contract; + +/// 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(HistoryEnabled) + .with_empty_in_memory_storage() + .with_execution_mode(TxExecutionMode::VerifyExecute) + .with_random_rich_accounts(1) + .build(); + + let bytecode_hash = hash_bytecode(&read_test_contract()); + + // 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 bytecodehash 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(protocol_upgrade_transaction.clone()); + + 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(HistoryEnabled) + .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 bytecodehash 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 = vec![(bytecode_hash, get_code_key(&address_to_deploy))]; + + // Verify that the bytecode has been set correctly + verify_required_storage(&vm.vm.state, expected_slots); +} + +/// Here we show how the work with the complex upgrader could be done +#[test] +fn test_complex_upgrader() { + let mut vm = VmTesterBuilder::new(HistoryEnabled) + .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_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 userspace + let upgrade_impl = H160::random(); + let account_code_key = get_code_key(&upgrade_impl); + + storage_view + .borrow_mut() + .set_value(get_known_code_key(&bytecode_hash), u256_to_h256(1.into())); + storage_view.borrow_mut().set_value( + get_known_code_key(&msg_sender_test_hash), + u256_to_h256(1.into()), + ); + storage_view + .borrow_mut() + .set_value(account_code_key, bytecode_hash); + drop(storage_view); + + vm.vm.state.decommittment_processor.populate( + vec![ + ( + h256_to_u256(bytecode_hash), + bytes_to_be_words(read_complex_upgrade()), + ), + ( + h256_to_u256(msg_sender_test_hash), + bytes_to_be_words(read_msg_sender_test()), + ), + ], + Timestamp(0), + ); + + 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 = vec![ + (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(&vm.vm.state, expected_slots); +} + +#[derive(Debug, Clone)] +struct ForceDeployment { + // The bytecode hash to put on an address + bytecode_hash: H256, + // The address on which to deploy the bytecodehash 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: None, + 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 implenentaiton itself +// For the explanatation 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: None, + 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_complex_upgrade() -> Vec { + read_bytecode("etc/contracts-test-data/artifacts-zk/contracts/complex-upgrade/complex-upgrade.sol/ComplexUpgrade.json") +} + +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_upgrade_abi() -> Contract { + load_contract( + "etc/contracts-test-data/artifacts-zk/contracts/complex-upgrade/complex-upgrade.sol/ComplexUpgrade.json" + ) +} + +fn get_complex_upgrader_abi() -> Contract { + load_sys_contract("ComplexUpgrader") +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tests/utils.rs b/core/multivm_deps/vm_virtual_blocks/src/tests/utils.rs new file mode 100644 index 00000000000..f709ebdd8ed --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tests/utils.rs @@ -0,0 +1,106 @@ +use ethabi::Contract; +use once_cell::sync::Lazy; + +use crate::tests::tester::InMemoryStorageView; +use zksync_contracts::{ + load_contract, read_bytecode, read_zbin_bytecode, BaseSystemContracts, SystemContractCode, +}; +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::utils::storage_key_for_standard_token_balance; +use zksync_types::{AccountTreeId, Address, StorageKey, H256, U256}; +use zksync_utils::bytecode::hash_bytecode; +use zksync_utils::{bytes_to_be_words, h256_to_u256, u256_to_h256}; + +use crate::types::internals::ZkSyncVmState; +use crate::HistoryMode; + +pub(crate) static BASE_SYSTEM_CONTRACTS: Lazy = + Lazy::new(BaseSystemContracts::load_from_disk); + +// Probably make it a part of vm tester +pub(crate) fn verify_required_storage( + state: &ZkSyncVmState, + required_values: Vec<(H256, StorageKey)>, +) { + for (required_value, key) in required_values { + let current_value = state.storage.storage.read_from_storage(&key); + + assert_eq!( + u256_to_h256(current_value), + required_value, + "Invalid value at key {key:?}" + ); + } +} + +pub(crate) fn verify_required_memory( + state: &ZkSyncVmState, + required_values: Vec<(U256, u32, u32)>, +) { + for (required_value, memory_page, cell) in required_values { + let current_value = state + .memory + .read_slot(memory_page as usize, cell as usize) + .value; + assert_eq!(current_value, required_value); + } +} + +pub(crate) fn get_balance( + token_id: AccountTreeId, + account: &Address, + main_storage: StoragePtr, +) -> U256 { + let key = storage_key_for_standard_token_balance(token_id, account); + h256_to_u256(main_storage.borrow_mut().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!( + "etc/system-contracts/bootloader/tests/artifacts/{}.yul/{}.yul.zbin", + test, test + )); + + let bootloader_hash = hash_bytecode(&bootloader_code); + SystemContractCode { + code: bytes_to_be_words(bootloader_code), + hash: bootloader_hash, + } +} + +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_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_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/multivm_deps/vm_virtual_blocks/src/tracers/call.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/call.rs new file mode 100644 index 00000000000..12750247604 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/call.rs @@ -0,0 +1,241 @@ +use once_cell::sync::OnceCell; +use std::marker::PhantomData; +use std::sync::Arc; + +use zk_evm::tracing::{AfterExecutionData, VmLocalStateData}; +use zk_evm::zkevm_opcode_defs::{ + FarCallABI, FarCallOpcode, FatPointer, Opcode, RetOpcode, + CALL_IMPLICIT_CALLDATA_FAT_PTR_REGISTER, RET_IMPLICIT_RETURNDATA_PARAMS_REGISTER, +}; + +use zksync_config::constants::CONTRACT_DEPLOYER_ADDRESS; +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::vm_trace::{Call, CallType}; +use zksync_types::U256; + +use crate::errors::VmRevertReason; +use crate::old_vm::history_recorder::HistoryMode; +use crate::old_vm::memory::SimpleMemory; +use crate::tracers::traits::{DynTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}; +use crate::types::outputs::VmExecutionResultAndLogs; + +#[derive(Debug, Clone)] +pub struct CallTracer { + stack: Vec, + result: Arc>>, + _phantom: PhantomData H>, +} + +#[derive(Debug, Clone)] +struct FarcallAndNearCallCount { + farcall: Call, + near_calls_after: usize, +} + +impl CallTracer { + pub fn new(resulted_stack: Arc>>, _history: H) -> Self { + Self { + stack: vec![], + result: resulted_stack, + _phantom: PhantomData, + } + } +} + +impl DynTracer for CallTracer { + fn after_execution( + &mut self, + state: VmLocalStateData<'_>, + data: AfterExecutionData, + memory: &SimpleMemory, + _storage: StoragePtr, + ) { + match data.opcode.variant.opcode { + Opcode::NearCall(_) => { + if let Some(last) = self.stack.last_mut() { + last.near_calls_after += 1; + } + } + Opcode::FarCall(far_call) => { + // We use parent gas for properly calculating gas used in the trace. + let current_ergs = state.vm_local_state.callstack.current.ergs_remaining; + let parent_gas = state + .vm_local_state + .callstack + .inner + .last() + .map(|call| call.ergs_remaining + current_ergs) + .unwrap_or(current_ergs); + + let mut current_call = Call { + r#type: CallType::Call(far_call), + gas: 0, + parent_gas, + ..Default::default() + }; + + self.handle_far_call_op_code(state, data, memory, &mut current_call); + self.stack.push(FarcallAndNearCallCount { + farcall: current_call, + near_calls_after: 0, + }); + } + Opcode::Ret(ret_code) => { + self.handle_ret_op_code(state, data, memory, ret_code); + } + _ => {} + }; + } +} + +impl ExecutionEndTracer for CallTracer {} + +impl ExecutionProcessing for CallTracer {} + +impl VmTracer for CallTracer { + fn save_results(&mut self, _result: &mut VmExecutionResultAndLogs) { + self.result + .set( + std::mem::take(&mut self.stack) + .into_iter() + .map(|x| x.farcall) + .collect(), + ) + .expect("Result is already set"); + } +} + +impl CallTracer { + fn handle_far_call_op_code( + &mut self, + state: VmLocalStateData<'_>, + _data: AfterExecutionData, + memory: &SimpleMemory, + current_call: &mut Call, + ) { + let current = state.vm_local_state.callstack.current; + // 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 = state + .vm_local_state + .callstack + .inner + .last() + .map(|call| call.this_address) + // 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.this_address); + if previous_caller == CONTRACT_DEPLOYER_ADDRESS { + CallType::Create + } else { + CallType::Call(far_call) + } + } else { + CallType::Call(far_call) + } + } else { + unreachable!() + }; + let calldata = if current.code_page.0 == 0 || current.ergs_remaining == 0 { + vec![] + } else { + let packed_abi = + state.vm_local_state.registers[CALL_IMPLICIT_CALLDATA_FAT_PTR_REGISTER as usize]; + assert!(packed_abi.is_pointer); + let far_call_abi = FarCallABI::from_u256(packed_abi.value); + memory.read_unaligned_bytes( + far_call_abi.memory_quasi_fat_pointer.memory_page as usize, + far_call_abi.memory_quasi_fat_pointer.start as usize, + far_call_abi.memory_quasi_fat_pointer.length as usize, + ) + }; + + current_call.input = calldata; + current_call.r#type = call_type; + current_call.from = current.msg_sender; + current_call.to = current.this_address; + current_call.value = U256::from(current.context_u128_value); + current_call.gas = current.ergs_remaining; + } + + fn save_output( + &mut self, + state: VmLocalStateData<'_>, + memory: &SimpleMemory, + ret_opcode: RetOpcode, + current_call: &mut Call, + ) { + let fat_data_pointer = + state.vm_local_state.registers[RET_IMPLICIT_RETURNDATA_PARAMS_REGISTER as usize]; + + // if fat_data_pointer is not a pointer then there is no output + let output = if fat_data_pointer.is_pointer { + let fat_data_pointer = FatPointer::from_u256(fat_data_pointer.value); + if !fat_data_pointer.is_trivial() { + Some(memory.read_unaligned_bytes( + fat_data_pointer.memory_page as usize, + fat_data_pointer.start as usize, + fat_data_pointer.length as usize, + )) + } else { + None + } + } else { + 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( + &mut self, + state: VmLocalStateData<'_>, + _data: AfterExecutionData, + memory: &SimpleMemory, + 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.stack.push(current_call); + return; + } + + current_call.farcall.gas_used = current_call + .farcall + .parent_gas + .saturating_sub(state.vm_local_state.callstack.current.ergs_remaining); + + self.save_output(state, memory, 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.stack.push(current_call); + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/default_tracers.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/default_tracers.rs new file mode 100644 index 00000000000..7cc1e19869c --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/default_tracers.rs @@ -0,0 +1,259 @@ +use std::fmt::{Debug, Formatter}; + +use zk_evm::witness_trace::DummyTracer; +use zk_evm::zkevm_opcode_defs::{Opcode, RetOpcode}; +use zk_evm::{ + tracing::{ + AfterDecodingData, AfterExecutionData, BeforeExecutionData, Tracer, VmLocalStateData, + }, + vm_state::VmLocalState, +}; +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::Timestamp; + +use crate::bootloader_state::utils::apply_l2_block; +use crate::bootloader_state::BootloaderState; +use crate::constants::BOOTLOADER_HEAP_PAGE; +use crate::old_vm::history_recorder::HistoryMode; +use crate::old_vm::memory::SimpleMemory; +use crate::tracers::traits::{DynTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}; +use crate::tracers::utils::{ + computational_gas_price, gas_spent_on_bytecodes_and_long_messages_this_opcode, + print_debug_if_needed, VmHook, +}; +use crate::tracers::ResultTracer; +use crate::types::internals::ZkSyncVmState; +use crate::{VmExecutionMode, VmExecutionStopReason}; + +/// Default tracer for the VM. It manages the other tracers execution and stop the vm when needed. +pub(crate) struct DefaultExecutionTracer { + tx_has_been_processed: bool, + execution_mode: VmExecutionMode, + + pub(crate) gas_spent_on_bytecodes_and_long_messages: u32, + // Amount of gas used during account validation. + pub(crate) computational_gas_used: u32, + // Maximum number of gas that we're allowed to use during account validation. + tx_validation_gas_limit: u32, + in_account_validation: bool, + final_batch_info_requested: bool, + pub(crate) result_tracer: ResultTracer, + pub(crate) custom_tracers: Vec>>, + ret_from_the_bootloader: Option, + storage: StoragePtr, +} + +impl Debug for DefaultExecutionTracer { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DefaultExecutionTracer").finish() + } +} + +impl Tracer for DefaultExecutionTracer { + const CALL_BEFORE_DECODING: bool = true; + const CALL_AFTER_DECODING: bool = true; + const CALL_BEFORE_EXECUTION: bool = true; + const CALL_AFTER_EXECUTION: bool = true; + type SupportedMemory = SimpleMemory; + + fn before_decoding(&mut self, state: VmLocalStateData<'_>, memory: &Self::SupportedMemory) { + >::before_decoding(&mut self.result_tracer, state, memory); + for tracer in self.custom_tracers.iter_mut() { + tracer.before_decoding(state, memory) + } + } + + fn after_decoding( + &mut self, + state: VmLocalStateData<'_>, + data: AfterDecodingData, + memory: &Self::SupportedMemory, + ) { + >::after_decoding( + &mut self.result_tracer, + state, + data, + memory, + ); + for tracer in self.custom_tracers.iter_mut() { + tracer.after_decoding(state, data, memory) + } + } + + fn before_execution( + &mut self, + state: VmLocalStateData<'_>, + data: BeforeExecutionData, + memory: &Self::SupportedMemory, + ) { + if self.in_account_validation { + self.computational_gas_used = self + .computational_gas_used + .saturating_add(computational_gas_price(state, &data)); + } + + let hook = VmHook::from_opcode_memory(&state, &data); + print_debug_if_needed(&hook, &state, memory); + + match hook { + VmHook::TxHasEnded => self.tx_has_been_processed = true, + VmHook::NoValidationEntered => self.in_account_validation = false, + VmHook::AccountValidationEntered => self.in_account_validation = true, + VmHook::FinalBatchInfo => self.final_batch_info_requested = true, + _ => {} + } + + self.gas_spent_on_bytecodes_and_long_messages += + gas_spent_on_bytecodes_and_long_messages_this_opcode(&state, &data); + self.result_tracer + .before_execution(state, data, memory, self.storage.clone()); + for tracer in self.custom_tracers.iter_mut() { + tracer.before_execution(state, data, memory, self.storage.clone()); + } + } + + fn after_execution( + &mut self, + state: VmLocalStateData<'_>, + data: AfterExecutionData, + memory: &Self::SupportedMemory, + ) { + if let VmExecutionMode::Bootloader = self.execution_mode { + let (next_opcode, _, _) = zk_evm::vm_state::read_and_decode( + state.vm_local_state, + memory, + &mut DummyTracer, + self, + ); + if current_frame_is_bootloader(state.vm_local_state) { + if let Opcode::Ret(ret) = next_opcode.inner.variant.opcode { + self.ret_from_the_bootloader = Some(ret); + } + } + } + + self.result_tracer + .after_execution(state, data, memory, self.storage.clone()); + for tracer in self.custom_tracers.iter_mut() { + tracer.after_execution(state, data, memory, self.storage.clone()); + } + } +} + +impl ExecutionEndTracer for DefaultExecutionTracer { + fn should_stop_execution(&self) -> bool { + let mut should_stop = match self.execution_mode { + VmExecutionMode::OneTx => self.tx_has_been_processed(), + VmExecutionMode::Batch => false, + VmExecutionMode::Bootloader => self.ret_from_the_bootloader == Some(RetOpcode::Ok), + }; + should_stop = should_stop || self.validation_run_out_of_gas(); + for tracer in self.custom_tracers.iter() { + should_stop = should_stop || tracer.should_stop_execution(); + } + should_stop + } +} + +impl DefaultExecutionTracer { + pub(crate) fn new( + computational_gas_limit: u32, + execution_mode: VmExecutionMode, + custom_tracers: Vec>>, + storage: StoragePtr, + ) -> Self { + Self { + tx_has_been_processed: false, + execution_mode, + gas_spent_on_bytecodes_and_long_messages: 0, + computational_gas_used: 0, + tx_validation_gas_limit: computational_gas_limit, + in_account_validation: false, + final_batch_info_requested: false, + result_tracer: ResultTracer::new(execution_mode), + custom_tracers, + ret_from_the_bootloader: None, + storage, + } + } + + pub(crate) fn tx_has_been_processed(&self) -> bool { + self.tx_has_been_processed + } + + pub(crate) fn validation_run_out_of_gas(&self) -> bool { + self.computational_gas_used > self.tx_validation_gas_limit + } + + pub(crate) fn gas_spent_on_pubdata(&self, vm_local_state: &VmLocalState) -> u32 { + self.gas_spent_on_bytecodes_and_long_messages + vm_local_state.spent_pubdata_counter + } + + fn set_fictive_l2_block( + &mut self, + state: &mut ZkSyncVmState, + bootloader_state: &mut BootloaderState, + ) { + let current_timestamp = Timestamp(state.local_state.timestamp); + let txs_index = bootloader_state.free_tx_index(); + let l2_block = bootloader_state.insert_fictive_l2_block(); + let mut memory = vec![]; + apply_l2_block(&mut memory, l2_block, txs_index); + state + .memory + .populate_page(BOOTLOADER_HEAP_PAGE as usize, memory, current_timestamp); + self.final_batch_info_requested = false; + } +} + +impl DynTracer for DefaultExecutionTracer {} + +impl ExecutionProcessing for DefaultExecutionTracer { + fn initialize_tracer(&mut self, state: &mut ZkSyncVmState) { + self.result_tracer.initialize_tracer(state); + for processor in self.custom_tracers.iter_mut() { + processor.initialize_tracer(state); + } + } + + fn before_cycle(&mut self, state: &mut ZkSyncVmState) { + self.result_tracer.before_cycle(state); + for processor in self.custom_tracers.iter_mut() { + processor.before_cycle(state); + } + } + + fn after_cycle( + &mut self, + state: &mut ZkSyncVmState, + bootloader_state: &mut BootloaderState, + ) { + self.result_tracer.after_cycle(state, bootloader_state); + for processor in self.custom_tracers.iter_mut() { + processor.after_cycle(state, bootloader_state); + } + if self.final_batch_info_requested { + self.set_fictive_l2_block(state, bootloader_state) + } + } + + fn after_vm_execution( + &mut self, + state: &mut ZkSyncVmState, + bootloader_state: &BootloaderState, + stop_reason: VmExecutionStopReason, + ) { + self.result_tracer + .after_vm_execution(state, bootloader_state, stop_reason); + for processor in self.custom_tracers.iter_mut() { + processor.after_vm_execution(state, bootloader_state, stop_reason); + } + } +} + +fn current_frame_is_bootloader(local_state: &VmLocalState) -> bool { + // The current frame is bootloader if the callstack depth is 1. + // Some of the near calls inside the bootloader can be out of gas, which is totally normal behavior + // and it shouldn't result in `is_bootloader_out_of_gas` becoming true. + local_state.callstack.inner.len() == 1 +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/mod.rs new file mode 100644 index 00000000000..11fefedc85a --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/mod.rs @@ -0,0 +1,15 @@ +pub(crate) use default_tracers::DefaultExecutionTracer; +pub(crate) use refunds::RefundsTracer; +pub(crate) use result_tracer::ResultTracer; +pub use storage_invocations::StorageInvocations; +pub use validation::{ValidationError, ValidationTracer, ValidationTracerParams}; + +pub(crate) mod default_tracers; +pub(crate) mod refunds; +pub(crate) mod result_tracer; + +pub(crate) mod call; +pub(crate) mod storage_invocations; +pub(crate) mod traits; +pub(crate) mod utils; +pub(crate) mod validation; diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/refunds.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/refunds.rs new file mode 100644 index 00000000000..14dc16f7c8e --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/refunds.rs @@ -0,0 +1,394 @@ +use vise::{Buckets, EncodeLabelSet, EncodeLabelValue, Family, Histogram, Metrics}; + +use std::collections::HashMap; + +use zk_evm::{ + aux_structures::Timestamp, + tracing::{BeforeExecutionData, VmLocalStateData}, + vm_state::VmLocalState, +}; +use zksync_config::constants::{PUBLISH_BYTECODE_OVERHEAD, SYSTEM_CONTEXT_ADDRESS}; +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::{ + event::{extract_long_l2_to_l1_messages, extract_published_bytecodes}, + l2_to_l1_log::L2ToL1Log, + zkevm_test_harness::witness::sort_storage_access::sort_storage_access_queries, + L1BatchNumber, StorageKey, U256, +}; +use zksync_utils::bytecode::bytecode_len_in_bytes; +use zksync_utils::{ceil_div_u256, u256_to_h256}; + +use crate::bootloader_state::BootloaderState; +use crate::constants::{BOOTLOADER_HEAP_PAGE, OPERATOR_REFUNDS_OFFSET, TX_GAS_LIMIT_OFFSET}; +use crate::old_vm::{ + events::merge_events, history_recorder::HistoryMode, memory::SimpleMemory, + oracles::storage::storage_key_of_log, utils::eth_price_per_pubdata_byte, +}; +use crate::tracers::utils::gas_spent_on_bytecodes_and_long_messages_this_opcode; +use crate::tracers::{ + traits::{DynTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}, + utils::{get_vm_hook_params, VmHook}, +}; +use crate::types::{ + inputs::L1BatchEnv, + internals::ZkSyncVmState, + outputs::{Refunds, VmExecutionResultAndLogs}, +}; + +/// Tracer responsible for collecting information about refunds. +#[derive(Debug, Clone)] +pub(crate) struct RefundsTracer { + // Some(x) means that the bootloader has asked the operator + // to provide the refund the user, where `x` is the refund proposed + // by the bootloader itself. + pending_operator_refund: Option, + refund_gas: u32, + operator_refund: Option, + timestamp_initial: Timestamp, + timestamp_before_cycle: Timestamp, + gas_remaining_before: u32, + spent_pubdata_counter_before: u32, + gas_spent_on_bytecodes_and_long_messages: u32, + l1_batch: L1BatchEnv, +} + +impl RefundsTracer { + pub(crate) fn new(l1_batch: L1BatchEnv) -> Self { + Self { + pending_operator_refund: None, + refund_gas: 0, + operator_refund: None, + timestamp_initial: Timestamp(0), + timestamp_before_cycle: Timestamp(0), + gas_remaining_before: 0, + spent_pubdata_counter_before: 0, + gas_spent_on_bytecodes_and_long_messages: 0, + l1_batch, + } + } +} + +impl RefundsTracer { + fn requested_refund(&self) -> Option { + self.pending_operator_refund + } + + fn set_refund_as_done(&mut self) { + self.pending_operator_refund = None; + } + + fn block_overhead_refund(&mut self) -> u32 { + 0 + } + + pub(crate) fn tx_body_refund( + &self, + bootloader_refund: u32, + gas_spent_on_pubdata: u32, + tx_gas_limit: u32, + current_ergs_per_pubdata_byte: u32, + pubdata_published: u32, + ) -> u32 { + 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 = self.l1_batch.base_fee(); + + 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(eth_price_per_pubdata_byte(self.l1_batch.l1_gas_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(self.l1_batch.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() + }); + + ceil_div_u256(refund_eth, effective_gas_price.into()).as_u32() + } + + pub(crate) fn gas_spent_on_pubdata(&self, vm_local_state: &VmLocalState) -> u32 { + self.gas_spent_on_bytecodes_and_long_messages + vm_local_state.spent_pubdata_counter + } +} + +impl DynTracer for RefundsTracer { + fn before_execution( + &mut self, + state: VmLocalStateData<'_>, + data: BeforeExecutionData, + memory: &SimpleMemory, + _storage: StoragePtr, + ) { + let hook = VmHook::from_opcode_memory(&state, &data); + match hook { + VmHook::NotifyAboutRefund => self.refund_gas = get_vm_hook_params(memory)[0].as_u32(), + VmHook::AskOperatorForRefund => { + self.pending_operator_refund = Some(get_vm_hook_params(memory)[0].as_u32()) + } + _ => {} + } + + self.gas_spent_on_bytecodes_and_long_messages += + gas_spent_on_bytecodes_and_long_messages_this_opcode(&state, &data); + } +} + +impl ExecutionEndTracer for RefundsTracer {} + +impl ExecutionProcessing for RefundsTracer { + fn initialize_tracer(&mut self, state: &mut ZkSyncVmState) { + self.timestamp_initial = Timestamp(state.local_state.timestamp); + self.gas_remaining_before = state.local_state.callstack.current.ergs_remaining; + self.spent_pubdata_counter_before = state.local_state.spent_pubdata_counter; + } + + fn before_cycle(&mut self, state: &mut ZkSyncVmState) { + self.timestamp_before_cycle = Timestamp(state.local_state.timestamp); + } + + fn after_cycle( + &mut self, + state: &mut ZkSyncVmState, + bootloader_state: &mut BootloaderState, + ) { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EncodeLabelValue, EncodeLabelSet)] + #[metrics(label = "type", rename_all = "snake_case")] + enum RefundType { + Bootloader, + Operator, + } + + const PERCENT_BUCKETS: Buckets = Buckets::values(&[ + 5.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 120.0, + ]); + + #[derive(Debug, Metrics)] + #[metrics(prefix = "vm_virtual_blocks")] + struct RefundMetrics { + #[metrics(buckets = PERCENT_BUCKETS)] + refund: Family>, + #[metrics(buckets = PERCENT_BUCKETS)] + refund_diff: Histogram, + } + + #[vise::register] + static METRICS: vise::Global = vise::Global::new(); + + // This means that the bootloader has informed the system (usually via VMHooks) - that some gas + // should be refunded back (see askOperatorForRefund in bootloader.yul for details). + if let Some(bootloader_refund) = self.requested_refund() { + assert!( + self.operator_refund.is_none(), + "Operator was asked for refund two times" + ); + let gas_spent_on_pubdata = + self.gas_spent_on_pubdata(&state.local_state) - self.spent_pubdata_counter_before; + + let current_tx_index = bootloader_state.current_tx(); + let tx_description_offset = + bootloader_state.get_tx_description_offset(current_tx_index); + let tx_gas_limit = state + .memory + .read_slot( + BOOTLOADER_HEAP_PAGE as usize, + tx_description_offset + TX_GAS_LIMIT_OFFSET, + ) + .value + .as_u32(); + + let pubdata_published = + pubdata_published(state, self.timestamp_initial, self.l1_batch.number); + + let current_ergs_per_pubdata_byte = state.local_state.current_ergs_per_pubdata_byte; + let tx_body_refund = self.tx_body_refund( + bootloader_refund, + gas_spent_on_pubdata, + tx_gas_limit, + current_ergs_per_pubdata_byte, + pubdata_published, + ); + + if tx_body_refund < bootloader_refund { + tracing::error!( + "Suggested tx body refund is less than bootloader refund. Tx body refund: {tx_body_refund}, \ + bootloader refund: {bootloader_refund}" + ); + } + + let refund_to_propose = tx_body_refund + self.block_overhead_refund(); + + let refund_slot = OPERATOR_REFUNDS_OFFSET + current_tx_index; + + // Writing the refund into memory + state.memory.populate_page( + BOOTLOADER_HEAP_PAGE as usize, + vec![(refund_slot, refund_to_propose.into())], + self.timestamp_before_cycle, + ); + + bootloader_state.set_refund_for_current_tx(refund_to_propose); + self.operator_refund = Some(refund_to_propose); + self.set_refund_as_done(); + + if tx_gas_limit < bootloader_refund { + tracing::error!( + "Tx gas limit is less than bootloader refund. Tx gas limit: {tx_gas_limit}, \ + bootloader refund: {bootloader_refund}" + ); + } + if tx_gas_limit < refund_to_propose { + tracing::error!( + "Tx gas limit is less than operator refund. Tx gas limit: {tx_gas_limit}, \ + operator refund: {refund_to_propose}" + ); + } + + METRICS.refund[&RefundType::Bootloader] + .observe(bootloader_refund as f64 / tx_gas_limit as f64 * 100.0); + METRICS.refund[&RefundType::Operator] + .observe(refund_to_propose as f64 / tx_gas_limit as f64 * 100.0); + let refund_diff = + (refund_to_propose as f64 - bootloader_refund as f64) / tx_gas_limit as f64 * 100.0; + METRICS.refund_diff.observe(refund_diff); + } + } +} + +/// Returns the given transactions' gas limit - by reading it directly from the VM memory. +pub(crate) fn pubdata_published( + state: &ZkSyncVmState, + from_timestamp: Timestamp, + batch_number: L1BatchNumber, +) -> u32 { + let storage_writes_pubdata_published = pubdata_published_for_writes(state, from_timestamp); + + let (raw_events, l1_messages) = state + .event_sink + .get_events_and_l2_l1_logs_after_timestamp(from_timestamp); + let events: Vec<_> = merge_events(raw_events) + .into_iter() + .map(|e| e.into_vm_event(batch_number)) + .collect(); + // For the first transaction in L1 batch there may be (it depends on the execution mode) an L2->L1 log + // that is sent by `SystemContext` in `setNewBlock`. It's a part of the L1 batch pubdata overhead and not the transaction itself. + let l2_l1_logs_bytes = (l1_messages + .into_iter() + .map(|log| L2ToL1Log { + shard_id: log.shard_id, + is_service: log.is_first, + tx_number_in_block: log.tx_number_in_block, + sender: log.address, + key: u256_to_h256(log.key), + value: u256_to_h256(log.value), + }) + .filter(|log| log.sender != SYSTEM_CONTEXT_ADDRESS) + .count() as u32) + * zk_evm::zkevm_opcode_defs::system_params::L1_MESSAGE_PUBDATA_BYTES; + let l2_l1_long_messages_bytes: u32 = extract_long_l2_to_l1_messages(&events) + .iter() + .map(|event| event.len() as u32) + .sum(); + + let published_bytecode_bytes: u32 = extract_published_bytecodes(&events) + .iter() + .map(|bytecodehash| bytecode_len_in_bytes(*bytecodehash) as u32 + PUBLISH_BYTECODE_OVERHEAD) + .sum(); + + storage_writes_pubdata_published + + l2_l1_logs_bytes + + l2_l1_long_messages_bytes + + published_bytecode_bytes +} + +fn pubdata_published_for_writes( + state: &ZkSyncVmState, + from_timestamp: Timestamp, +) -> u32 { + // This `HashMap` contains how much was already paid for every slot that was paid during the last tx execution. + // For the slots that weren't paid during the last tx execution we can just use + // `self.state.storage.paid_changes.inner().get(&key)` to get how much it was paid before. + let pre_paid_before_tx_map: HashMap = state + .storage + .paid_changes + .history() + .iter() + .rev() + .take_while(|history_elem| history_elem.0 >= from_timestamp) + .map(|history_elem| (history_elem.1.key, history_elem.1.value.unwrap_or(0))) + .collect(); + let pre_paid_before_tx = |key: &StorageKey| -> u32 { + if let Some(pre_paid) = pre_paid_before_tx_map.get(key) { + *pre_paid + } else { + state + .storage + .paid_changes + .inner() + .get(key) + .copied() + .unwrap_or(0) + } + }; + + let storage_logs = state + .storage + .storage_log_queries_after_timestamp(from_timestamp); + let (_, deduplicated_logs) = + sort_storage_access_queries(storage_logs.iter().map(|log| &log.log_query)); + + deduplicated_logs + .into_iter() + .filter_map(|log| { + if log.rw_flag { + let key = storage_key_of_log(&log); + let pre_paid = pre_paid_before_tx(&key); + let to_pay_by_user = state.storage.base_price_for_write(&log); + + if to_pay_by_user > pre_paid { + Some(to_pay_by_user - pre_paid) + } else { + None + } + } else { + None + } + }) + .sum() +} + +impl VmTracer for RefundsTracer { + fn save_results(&mut self, result: &mut VmExecutionResultAndLogs) { + result.refunds = Refunds { + gas_refunded: self.refund_gas, + operator_suggested_refund: self.operator_refund.unwrap_or_default(), + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/result_tracer.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/result_tracer.rs new file mode 100644 index 00000000000..b8e08949356 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/result_tracer.rs @@ -0,0 +1,246 @@ +use zk_evm::{ + tracing::{AfterDecodingData, BeforeExecutionData, VmLocalStateData}, + vm_state::{ErrorFlags, VmLocalState}, + zkevm_opcode_defs::FatPointer, +}; +use zksync_state::{StoragePtr, WriteStorage}; + +use zksync_types::U256; + +use crate::bootloader_state::BootloaderState; +use crate::errors::VmRevertReason; +use crate::old_vm::{ + history_recorder::HistoryMode, + memory::SimpleMemory, + utils::{vm_may_have_ended_inner, VmExecutionResult}, +}; +use crate::tracers::{ + traits::{DynTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}, + utils::{get_vm_hook_params, read_pointer, VmHook}, +}; +use crate::types::{ + internals::ZkSyncVmState, + outputs::{ExecutionResult, VmExecutionResultAndLogs}, +}; + +use crate::constants::{BOOTLOADER_HEAP_PAGE, RESULT_SUCCESS_FIRST_SLOT}; +use crate::{Halt, TxRevertReason}; +use crate::{VmExecutionMode, VmExecutionStopReason}; + +#[derive(Debug, Clone)] +enum Result { + Error { error_reason: VmRevertReason }, + Success { return_data: Vec }, + Halt { reason: Halt }, +} + +/// Tracer responsible for handling the VM execution result. +#[derive(Debug, Clone)] +pub(crate) struct ResultTracer { + result: Option, + bootloader_out_of_gas: bool, + execution_mode: VmExecutionMode, +} + +impl ResultTracer { + pub(crate) fn new(execution_mode: VmExecutionMode) -> Self { + Self { + result: None, + bootloader_out_of_gas: false, + execution_mode, + } + } +} + +fn current_frame_is_bootloader(local_state: &VmLocalState) -> bool { + // The current frame is bootloader if the callstack depth is 1. + // Some of the near calls inside the bootloader can be out of gas, which is totally normal behavior + // and it shouldn't result in `is_bootloader_out_of_gas` becoming true. + local_state.callstack.inner.len() == 1 +} + +impl DynTracer for ResultTracer { + fn after_decoding( + &mut self, + state: VmLocalStateData<'_>, + data: AfterDecodingData, + _memory: &SimpleMemory, + ) { + // We should check not only for the `NOT_ENOUGH_ERGS` flag but if the current frame is bootloader too. + if current_frame_is_bootloader(state.vm_local_state) + && data + .error_flags_accumulated + .contains(ErrorFlags::NOT_ENOUGH_ERGS) + { + self.bootloader_out_of_gas = true; + } + } + + fn before_execution( + &mut self, + state: VmLocalStateData<'_>, + data: BeforeExecutionData, + memory: &SimpleMemory, + _storage: StoragePtr, + ) { + let hook = VmHook::from_opcode_memory(&state, &data); + if let VmHook::ExecutionResult = hook { + let vm_hook_params = get_vm_hook_params(memory); + let success = vm_hook_params[0]; + let returndata_ptr = FatPointer::from_u256(vm_hook_params[1]); + let returndata = read_pointer(memory, returndata_ptr); + if success == U256::zero() { + self.result = Some(Result::Error { + // Tx has reverted, without bootloader error, we can simply parse the revert reason + error_reason: (VmRevertReason::from(returndata.as_slice())), + }); + } else { + self.result = Some(Result::Success { + return_data: returndata, + }); + } + } + } +} + +impl ExecutionEndTracer for ResultTracer {} + +impl ExecutionProcessing for ResultTracer { + fn after_vm_execution( + &mut self, + state: &mut ZkSyncVmState, + bootloader_state: &BootloaderState, + stop_reason: VmExecutionStopReason, + ) { + match stop_reason { + // Vm has finished execution, we need to check the result of it + VmExecutionStopReason::VmFinished => { + self.vm_finished_execution(state); + } + // One of the tracers above has requested to stop the execution. + // If it was the correct stop we already have the result, + // otherwise it can be out of gas error + VmExecutionStopReason::TracerRequestedStop => { + match self.execution_mode { + VmExecutionMode::OneTx => self.vm_stopped_execution(state, bootloader_state), + VmExecutionMode::Batch => self.vm_finished_execution(state), + VmExecutionMode::Bootloader => self.vm_finished_execution(state), + }; + } + } + } +} + +impl ResultTracer { + fn vm_finished_execution( + &mut self, + state: &ZkSyncVmState, + ) { + let Some(result) = vm_may_have_ended_inner(state) else { + // The VM has finished execution, but the result is not yet available. + self.result = Some(Result::Success { + return_data: vec![], + }); + return; + }; + + // Check it's not inside tx + match result { + VmExecutionResult::Ok(output) => { + self.result = Some(Result::Success { + return_data: output, + }); + } + VmExecutionResult::Revert(output) => { + // Unlike VmHook::ExecutionResult, vm has completely finished and returned not only the revert reason, + // but with bytecode, which represents the type of error from the bootloader side + let revert_reason = TxRevertReason::parse_error(&output); + + match revert_reason { + TxRevertReason::TxReverted(reason) => { + self.result = Some(Result::Error { + error_reason: reason, + }); + } + TxRevertReason::Halt(halt) => { + self.result = Some(Result::Halt { reason: halt }); + } + }; + } + VmExecutionResult::Panic => { + if self.bootloader_out_of_gas { + self.result = Some(Result::Halt { + reason: Halt::BootloaderOutOfGas, + }); + } else { + self.result = Some(Result::Halt { + reason: Halt::VMPanic, + }); + } + } + VmExecutionResult::MostLikelyDidNotFinish(_, _) => { + unreachable!() + } + } + } + + fn vm_stopped_execution( + &mut self, + state: &ZkSyncVmState, + bootloader_state: &BootloaderState, + ) { + if self.bootloader_out_of_gas { + self.result = Some(Result::Halt { + reason: Halt::BootloaderOutOfGas, + }); + } else { + if self.result.is_some() { + return; + } + + let has_failed = tx_has_failed(state, bootloader_state.current_tx() as u32); + if has_failed { + self.result = Some(Result::Error { + error_reason: VmRevertReason::General { + msg: "Transaction reverted with empty reason. Possibly out of gas" + .to_string(), + data: vec![], + }, + }); + } else { + self.result = Some(self.result.clone().unwrap_or(Result::Success { + return_data: vec![], + })); + } + } + } + + pub(crate) fn into_result(self) -> ExecutionResult { + match self.result.unwrap() { + Result::Error { error_reason } => ExecutionResult::Revert { + output: error_reason, + }, + Result::Success { return_data } => ExecutionResult::Success { + output: return_data, + }, + Result::Halt { reason } => ExecutionResult::Halt { reason }, + } + } +} + +impl VmTracer for ResultTracer { + fn save_results(&mut self, _result: &mut VmExecutionResultAndLogs) {} +} + +pub(crate) fn tx_has_failed( + state: &ZkSyncVmState, + tx_id: u32, +) -> bool { + let mem_slot = RESULT_SUCCESS_FIRST_SLOT + tx_id; + let mem_value = state + .memory + .read_slot(BOOTLOADER_HEAP_PAGE as usize, mem_slot as usize) + .value; + + mem_value == U256::zero() +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/storage_invocations.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/storage_invocations.rs new file mode 100644 index 00000000000..ef4b59c60a8 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/storage_invocations.rs @@ -0,0 +1,44 @@ +use crate::bootloader_state::BootloaderState; +use crate::old_vm::history_recorder::HistoryMode; +use crate::tracers::traits::{DynTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}; +use crate::types::internals::ZkSyncVmState; +use zksync_state::WriteStorage; + +#[derive(Debug, Default, Clone)] +pub struct StorageInvocations { + limit: usize, + current: usize, +} + +impl StorageInvocations { + pub fn new(limit: usize) -> Self { + Self { limit, current: 0 } + } +} + +/// Tracer responsible for calculating the number of storage invocations and +/// stopping the VM execution if the limit is reached. +impl DynTracer for StorageInvocations {} + +impl ExecutionEndTracer for StorageInvocations { + fn should_stop_execution(&self) -> bool { + self.current >= self.limit + } +} + +impl ExecutionProcessing for StorageInvocations { + fn after_cycle( + &mut self, + state: &mut ZkSyncVmState, + _bootloader_state: &mut BootloaderState, + ) { + self.current = state + .storage + .storage + .get_ptr() + .borrow() + .missed_storage_invocations(); + } +} + +impl VmTracer for StorageInvocations {} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/traits.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/traits.rs new file mode 100644 index 00000000000..6e76a041fab --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/traits.rs @@ -0,0 +1,85 @@ +use zk_evm::tracing::{ + AfterDecodingData, AfterExecutionData, BeforeExecutionData, VmLocalStateData, +}; +use zksync_state::{StoragePtr, WriteStorage}; + +use crate::bootloader_state::BootloaderState; +use crate::old_vm::history_recorder::HistoryMode; +use crate::old_vm::memory::SimpleMemory; +use crate::types::internals::ZkSyncVmState; +use crate::types::outputs::VmExecutionResultAndLogs; +use crate::VmExecutionStopReason; + +/// Run tracer for collecting data during the vm execution cycles +pub trait ExecutionProcessing: + DynTracer + ExecutionEndTracer +{ + fn initialize_tracer(&mut self, _state: &mut ZkSyncVmState) {} + fn before_cycle(&mut self, _state: &mut ZkSyncVmState) {} + fn after_cycle( + &mut self, + _state: &mut ZkSyncVmState, + _bootloader_state: &mut BootloaderState, + ) { + } + fn after_vm_execution( + &mut self, + _state: &mut ZkSyncVmState, + _bootloader_state: &BootloaderState, + _stop_reason: VmExecutionStopReason, + ) { + } +} + +/// Stop the vm execution if the tracer conditions are met +pub trait ExecutionEndTracer { + // Returns whether the vm execution should stop. + fn should_stop_execution(&self) -> bool { + false + } +} + +/// Version of zk_evm::Tracer suitable for dynamic dispatch. +pub trait DynTracer { + fn before_decoding(&mut self, _state: VmLocalStateData<'_>, _memory: &SimpleMemory) {} + fn after_decoding( + &mut self, + _state: VmLocalStateData<'_>, + _data: AfterDecodingData, + _memory: &SimpleMemory, + ) { + } + fn before_execution( + &mut self, + _state: VmLocalStateData<'_>, + _data: BeforeExecutionData, + _memory: &SimpleMemory, + _storage: StoragePtr, + ) { + } + fn after_execution( + &mut self, + _state: VmLocalStateData<'_>, + _data: AfterExecutionData, + _memory: &SimpleMemory, + _storage: StoragePtr, + ) { + } +} + +/// Save the results of the vm execution. +pub trait VmTracer: + DynTracer + ExecutionEndTracer + ExecutionProcessing + Send +{ + fn save_results(&mut self, _result: &mut VmExecutionResultAndLogs) {} +} + +pub trait BoxedTracer { + fn into_boxed(self) -> Box>; +} + +impl + 'static> BoxedTracer for T { + fn into_boxed(self) -> Box> { + Box::new(self) + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/utils.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/utils.rs new file mode 100644 index 00000000000..f86b496b078 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/utils.rs @@ -0,0 +1,224 @@ +use zk_evm::aux_structures::MemoryPage; +use zk_evm::zkevm_opcode_defs::{FarCallABI, FarCallForwardPageType}; +use zk_evm::{ + tracing::{BeforeExecutionData, VmLocalStateData}, + zkevm_opcode_defs::{FatPointer, LogOpcode, Opcode, UMAOpcode}, +}; + +use zksync_config::constants::{ + ECRECOVER_PRECOMPILE_ADDRESS, KECCAK256_PRECOMPILE_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, + L1_MESSENGER_ADDRESS, SHA256_PRECOMPILE_ADDRESS, +}; +use zksync_types::U256; +use zksync_utils::u256_to_h256; + +use crate::constants::{ + BOOTLOADER_HEAP_PAGE, VM_HOOK_PARAMS_COUNT, VM_HOOK_PARAMS_START_POSITION, VM_HOOK_POSITION, +}; +use crate::old_vm::history_recorder::HistoryMode; +use crate::old_vm::memory::SimpleMemory; +use crate::old_vm::utils::{aux_heap_page_from_base, heap_page_from_base}; + +#[derive(Clone, Debug, Copy)] +pub(crate) enum VmHook { + AccountValidationEntered, + PaymasterValidationEntered, + NoValidationEntered, + ValidationStepEndeded, + TxHasEnded, + DebugLog, + DebugReturnData, + NoHook, + NearCallCatch, + AskOperatorForRefund, + NotifyAboutRefund, + ExecutionResult, + FinalBatchInfo, +} + +impl VmHook { + pub(crate) fn from_opcode_memory( + state: &VmLocalStateData<'_>, + data: &BeforeExecutionData, + ) -> Self { + let opcode_variant = data.opcode.variant; + let heap_page = + heap_page_from_base(state.vm_local_state.callstack.current.base_memory_page).0; + + let src0_value = data.src0_value.value; + + let fat_ptr = FatPointer::from_u256(src0_value); + + let value = data.src1_value.value; + + // Only UMA opcodes in the bootloader serve for vm hooks + if !matches!(opcode_variant.opcode, Opcode::UMA(UMAOpcode::HeapWrite)) + || heap_page != BOOTLOADER_HEAP_PAGE + || fat_ptr.offset != VM_HOOK_POSITION * 32 + { + return Self::NoHook; + } + + match value.as_u32() { + 0 => Self::AccountValidationEntered, + 1 => Self::PaymasterValidationEntered, + 2 => Self::NoValidationEntered, + 3 => Self::ValidationStepEndeded, + 4 => Self::TxHasEnded, + 5 => Self::DebugLog, + 6 => Self::DebugReturnData, + 7 => Self::NearCallCatch, + 8 => Self::AskOperatorForRefund, + 9 => Self::NotifyAboutRefund, + 10 => Self::ExecutionResult, + 11 => Self::FinalBatchInfo, + _ => panic!("Unkown hook"), + } + } +} + +pub(crate) fn get_debug_log( + state: &VmLocalStateData<'_>, + memory: &SimpleMemory, +) -> String { + let vm_hook_params: Vec<_> = get_vm_hook_params(memory) + .into_iter() + .map(u256_to_h256) + .collect(); + let msg = vm_hook_params[0].as_bytes().to_vec(); + let data = vm_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 readibility + 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() + }; + + let tx_id = state.vm_local_state.tx_number_in_block; + + format!("Bootloader transaction {}: {} {}", tx_id, msg, data_str) +} + +/// Reads the memory slice represented by the fat pointer. +/// Note, that the fat pointer must point to the accesible memory (i.e. not cleared up yet). +pub(crate) fn read_pointer( + memory: &SimpleMemory, + pointer: FatPointer, +) -> Vec { + let FatPointer { + offset, + length, + start, + memory_page, + } = pointer; + + // The actual bounds of the returndata ptr is [start+offset..start+length] + let mem_region_start = start + offset; + let mem_region_length = length - offset; + + memory.read_unaligned_bytes( + memory_page as usize, + mem_region_start as usize, + mem_region_length as usize, + ) +} + +/// Outputs the returndata for the latest call. +/// This is usually used to output the revert reason. +pub(crate) fn get_debug_returndata(memory: &SimpleMemory) -> String { + let vm_hook_params: Vec<_> = get_vm_hook_params(memory); + let returndata_ptr = FatPointer::from_u256(vm_hook_params[0]); + let returndata = read_pointer(memory, returndata_ptr); + + format!("0x{}", hex::encode(returndata)) +} + +/// Accepts a vm hook and, if it requires to output some debug log, outputs it. +pub(crate) fn print_debug_if_needed( + hook: &VmHook, + state: &VmLocalStateData<'_>, + memory: &SimpleMemory, +) { + let log = match hook { + VmHook::DebugLog => get_debug_log(state, memory), + VmHook::DebugReturnData => get_debug_returndata(memory), + _ => return, + }; + + tracing::trace!("{}", log); +} + +pub(crate) fn computational_gas_price( + state: VmLocalStateData<'_>, + data: &BeforeExecutionData, +) -> u32 { + // We calculate computational gas used as a raw price for opcode plus cost for precompiles. + // This calculation is incomplete as it misses decommitment and memory growth costs. + // To calculate decommitment cost we need an access to decommitter oracle which is missing in tracer now. + // Memory growth calculation is complex and it will require different logic for different opcodes (`FarCall`, `Ret`, `UMA`). + let base_price = data.opcode.inner.variant.ergs_price(); + let precompile_price = match data.opcode.variant.opcode { + Opcode::Log(LogOpcode::PrecompileCall) => { + let address = state.vm_local_state.callstack.current.this_address; + + if address == KECCAK256_PRECOMPILE_ADDRESS + || address == SHA256_PRECOMPILE_ADDRESS + || address == ECRECOVER_PRECOMPILE_ADDRESS + { + data.src1_value.value.low_u32() + } else { + 0 + } + } + _ => 0, + }; + base_price + precompile_price +} + +pub(crate) fn gas_spent_on_bytecodes_and_long_messages_this_opcode( + state: &VmLocalStateData<'_>, + data: &BeforeExecutionData, +) -> u32 { + if data.opcode.variant.opcode == Opcode::Log(LogOpcode::PrecompileCall) { + let current_stack = state.vm_local_state.callstack.get_current_stack(); + // Trace for precompile calls from `KNOWN_CODES_STORAGE_ADDRESS` and `L1_MESSENGER_ADDRESS` that burn some gas. + // Note, that if there is less gas left than requested to burn it will be burnt anyway. + if current_stack.this_address == KNOWN_CODES_STORAGE_ADDRESS + || current_stack.this_address == L1_MESSENGER_ADDRESS + { + std::cmp::min(data.src1_value.value.as_u32(), current_stack.ergs_remaining) + } else { + 0 + } + } else { + 0 + } +} + +pub(crate) fn get_calldata_page_via_abi(far_call_abi: &FarCallABI, base_page: MemoryPage) -> u32 { + match far_call_abi.forwarding_mode { + FarCallForwardPageType::ForwardFatPointer => { + far_call_abi.memory_quasi_fat_pointer.memory_page + } + FarCallForwardPageType::UseAuxHeap => aux_heap_page_from_base(base_page).0, + FarCallForwardPageType::UseHeap => heap_page_from_base(base_page).0, + } +} +pub(crate) fn get_vm_hook_params(memory: &SimpleMemory) -> Vec { + memory.dump_page_content_as_u256_words( + BOOTLOADER_HEAP_PAGE, + VM_HOOK_PARAMS_START_POSITION..VM_HOOK_PARAMS_START_POSITION + VM_HOOK_PARAMS_COUNT, + ) +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum VmExecutionStopReason { + VmFinished, + TracerRequestedStop, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/error.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/error.rs new file mode 100644 index 00000000000..8fb104cb67a --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/error.rs @@ -0,0 +1,22 @@ +use crate::Halt; +use std::fmt::Display; +use zksync_types::vm_trace::ViolatedValidationRule; + +#[derive(Debug, Clone)] +pub enum ValidationError { + FailedTx(Halt), + ViolatedRule(ViolatedValidationRule), +} + +impl Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::FailedTx(revert_reason) => { + write!(f, "Validation revert: {}", revert_reason) + } + Self::ViolatedRule(rule) => { + write!(f, "Violated validation rules: {}", rule) + } + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/mod.rs new file mode 100644 index 00000000000..ca66aac9e73 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/mod.rs @@ -0,0 +1,388 @@ +mod error; +mod params; +mod types; + +use std::sync::Arc; +use std::{collections::HashSet, marker::PhantomData}; + +use once_cell::sync::OnceCell; +use zk_evm::{ + tracing::{BeforeExecutionData, VmLocalStateData}, + zkevm_opcode_defs::{ContextOpcode, FarCallABI, LogOpcode, Opcode}, +}; + +use zksync_config::constants::{ + ACCOUNT_CODE_STORAGE_ADDRESS, BOOTLOADER_ADDRESS, CONTRACT_DEPLOYER_ADDRESS, + KECCAK256_PRECOMPILE_ADDRESS, L2_ETH_TOKEN_ADDRESS, MSG_VALUE_SIMULATOR_ADDRESS, + SYSTEM_CONTEXT_ADDRESS, +}; +use zksync_state::{StoragePtr, WriteStorage}; + +use zksync_types::{ + get_code_key, web3::signing::keccak256, AccountTreeId, Address, StorageKey, H256, U256, +}; +use zksync_utils::{ + be_bytes_to_safe_address, h256_to_account_address, u256_to_account_address, u256_to_h256, +}; + +use crate::old_vm::history_recorder::HistoryMode; +use crate::old_vm::memory::SimpleMemory; +use crate::tracers::traits::{DynTracer, ExecutionEndTracer, ExecutionProcessing, VmTracer}; +use crate::tracers::utils::{ + computational_gas_price, get_calldata_page_via_abi, print_debug_if_needed, VmHook, +}; + +pub use error::ValidationError; +pub use params::ValidationTracerParams; + +use types::NewTrustedValidationItems; +use types::ValidationTracerMode; +use zksync_types::vm_trace::ViolatedValidationRule; + +use crate::VmExecutionResultAndLogs; + +/// Tracer that is used to ensure that the validation adheres to all the rules +/// to prevent DDoS attacks on the server. +#[derive(Debug, Clone)] +pub struct ValidationTracer { + validation_mode: ValidationTracerMode, + auxilary_allowed_slots: HashSet, + + user_address: Address, + #[allow(dead_code)] + paymaster_address: Address, + should_stop_execution: bool, + trusted_slots: HashSet<(Address, U256)>, + trusted_addresses: HashSet
, + trusted_address_slots: HashSet<(Address, U256)>, + computational_gas_used: u32, + computational_gas_limit: u32, + result: Arc>, + _marker: PhantomData H>, +} + +type ValidationRoundResult = Result; + +impl ValidationTracer { + pub fn new( + params: ValidationTracerParams, + result: Arc>, + ) -> Self { + Self { + validation_mode: ValidationTracerMode::NoValidation, + auxilary_allowed_slots: Default::default(), + + should_stop_execution: false, + user_address: params.user_address, + paymaster_address: params.paymaster_address, + trusted_slots: params.trusted_slots, + trusted_addresses: params.trusted_addresses, + trusted_address_slots: params.trusted_address_slots, + computational_gas_used: 0, + computational_gas_limit: params.computational_gas_limit, + result, + _marker: Default::default(), + } + } + + fn process_validation_round_result(&mut self, result: ValidationRoundResult) { + match result { + Ok(NewTrustedValidationItems { + new_allowed_slots, + new_trusted_addresses, + }) => { + self.auxilary_allowed_slots.extend(new_allowed_slots); + self.trusted_addresses.extend(new_trusted_addresses); + } + Err(err) => { + if self.result.get().is_some() { + tracing::trace!("Validation error is already set, skipping"); + return; + } + self.result.set(err).expect("Result should be empty"); + } + } + } + + // Checks whether such storage access is acceptable. + fn is_allowed_storage_read( + &self, + storage: StoragePtr, + address: Address, + key: U256, + msg_sender: Address, + ) -> bool { + // If there are no restrictions, all storage reads are valid. + // We also don't support the paymaster validation for now. + if matches!( + self.validation_mode, + ValidationTracerMode::NoValidation | ValidationTracerMode::PaymasterTxValidation + ) { + return true; + } + + // The pair of MSG_VALUE_SIMULATOR_ADDRESS & L2_ETH_TOKEN_ADDRESS simulates the behavior of transfering ETH + // that is safe for the DDoS protection rules. + if valid_eth_token_call(address, msg_sender) { + return true; + } + + if self.trusted_slots.contains(&(address, key)) + || self.trusted_addresses.contains(&address) + || self.trusted_address_slots.contains(&(address, key)) + { + return true; + } + + if touches_allowed_context(address, key) { + return true; + } + + // The user is allowed to touch its own slots or slots semantically related to him. + let valid_users_slot = address == self.user_address + || u256_to_account_address(&key) == self.user_address + || self.auxilary_allowed_slots.contains(&u256_to_h256(key)); + if valid_users_slot { + return true; + } + + if is_constant_code_hash(address, key, storage) { + return true; + } + + false + } + + // Used to remember user-related fields (its balance/allowance/etc). + // Note that it assumes that the length of the calldata is 64 bytes. + fn slot_to_add_from_keccak_call( + &self, + calldata: &[u8], + validated_address: Address, + ) -> Option { + assert_eq!(calldata.len(), 64); + + let (potential_address_bytes, potential_position_bytes) = calldata.split_at(32); + let potential_address = be_bytes_to_safe_address(potential_address_bytes); + + // If the validation_address is equal to the potential_address, + // then it is a request that could be used for mapping of kind mapping(address => ...). + // + // If the potential_position_bytes were already allowed before, then this keccak might be used + // for ERC-20 allowance or any other of mapping(address => mapping(...)) + if potential_address == Some(validated_address) + || self + .auxilary_allowed_slots + .contains(&H256::from_slice(potential_position_bytes)) + { + // This is request that could be used for mapping of kind mapping(address => ...) + + // We could theoretically wait for the slot number to be returned by the + // keccak256 precompile itself, but this would complicate the code even further + // so let's calculate it here. + let slot = keccak256(calldata); + + // Adding this slot to the allowed ones + Some(H256(slot)) + } else { + None + } + } + + fn check_user_restrictions( + &mut self, + state: VmLocalStateData<'_>, + data: BeforeExecutionData, + memory: &SimpleMemory, + storage: StoragePtr, + ) -> ValidationRoundResult { + if self.computational_gas_used > self.computational_gas_limit { + return Err(ViolatedValidationRule::TookTooManyComputationalGas( + self.computational_gas_limit, + )); + } + + let opcode_variant = data.opcode.variant; + match opcode_variant.opcode { + Opcode::FarCall(_) => { + let packed_abi = data.src0_value.value; + let call_destination_value = data.src1_value.value; + + let called_address = u256_to_account_address(&call_destination_value); + let far_call_abi = FarCallABI::from_u256(packed_abi); + + if called_address == KECCAK256_PRECOMPILE_ADDRESS + && far_call_abi.memory_quasi_fat_pointer.length == 64 + { + let calldata_page = get_calldata_page_via_abi( + &far_call_abi, + state.vm_local_state.callstack.current.base_memory_page, + ); + let calldata = memory.read_unaligned_bytes( + calldata_page as usize, + far_call_abi.memory_quasi_fat_pointer.start as usize, + 64, + ); + + let slot_to_add = + self.slot_to_add_from_keccak_call(&calldata, self.user_address); + + if let Some(slot) = slot_to_add { + return Ok(NewTrustedValidationItems { + new_allowed_slots: vec![slot], + ..Default::default() + }); + } + } else if called_address != self.user_address { + let code_key = get_code_key(&called_address); + let code = storage.borrow_mut().read_value(&code_key); + + if code == H256::zero() { + // The users are not allowed to call contracts with no code + return Err(ViolatedValidationRule::CalledContractWithNoCode( + called_address, + )); + } + } + } + Opcode::Context(context) => { + match context { + ContextOpcode::Meta => { + return Err(ViolatedValidationRule::TouchedUnallowedContext); + } + ContextOpcode::ErgsLeft => { + // TODO (SMA-1168): implement the correct restrictions for the gas left opcode. + } + _ => {} + } + } + Opcode::Log(LogOpcode::StorageRead) => { + let key = data.src0_value.value; + let this_address = state.vm_local_state.callstack.current.this_address; + let msg_sender = state.vm_local_state.callstack.current.msg_sender; + + if !self.is_allowed_storage_read(storage.clone(), this_address, key, msg_sender) { + return Err(ViolatedValidationRule::TouchedUnallowedStorageSlots( + this_address, + key, + )); + } + + if self.trusted_address_slots.contains(&(this_address, key)) { + let storage_key = + StorageKey::new(AccountTreeId::new(this_address), u256_to_h256(key)); + + let value = storage.borrow_mut().read_value(&storage_key); + + return Ok(NewTrustedValidationItems { + new_trusted_addresses: vec![h256_to_account_address(&value)], + ..Default::default() + }); + } + } + _ => {} + } + + Ok(Default::default()) + } +} + +impl DynTracer for ValidationTracer { + fn before_execution( + &mut self, + state: VmLocalStateData<'_>, + data: BeforeExecutionData, + memory: &SimpleMemory, + storage: StoragePtr, + ) { + // For now, we support only validations for users. + if let ValidationTracerMode::UserTxValidation = self.validation_mode { + self.computational_gas_used = self + .computational_gas_used + .saturating_add(computational_gas_price(state, &data)); + + let validation_round_result = + self.check_user_restrictions(state, data, memory, storage); + self.process_validation_round_result(validation_round_result); + } + + let hook = VmHook::from_opcode_memory(&state, &data); + print_debug_if_needed(&hook, &state, memory); + + let current_mode = self.validation_mode; + match (current_mode, hook) { + (ValidationTracerMode::NoValidation, VmHook::AccountValidationEntered) => { + // Account validation can be entered when there is no prior validation (i.e. "nested" validations are not allowed) + self.validation_mode = ValidationTracerMode::UserTxValidation; + } + (ValidationTracerMode::NoValidation, VmHook::PaymasterValidationEntered) => { + // Paymaster validation can be entered when there is no prior validation (i.e. "nested" validations are not allowed) + self.validation_mode = ValidationTracerMode::PaymasterTxValidation; + } + (_, VmHook::AccountValidationEntered | VmHook::PaymasterValidationEntered) => { + panic!( + "Unallowed transition inside the validation tracer. Mode: {:#?}, hook: {:#?}", + self.validation_mode, hook + ); + } + (_, VmHook::NoValidationEntered) => { + // Validation can be always turned off + self.validation_mode = ValidationTracerMode::NoValidation; + } + (_, VmHook::ValidationStepEndeded) => { + // The validation step has ended. + self.should_stop_execution = true; + } + (_, _) => { + // The hook is not relevant to the validation tracer. Ignore. + } + } + } +} + +impl ExecutionEndTracer for ValidationTracer { + fn should_stop_execution(&self) -> bool { + self.should_stop_execution || self.result.get().is_some() + } +} + +impl ExecutionProcessing for ValidationTracer {} + +impl VmTracer for ValidationTracer { + fn save_results(&mut self, _result: &mut VmExecutionResultAndLogs) {} +} + +fn touches_allowed_context(address: Address, key: U256) -> bool { + // Context is not touched at all + if address != SYSTEM_CONTEXT_ADDRESS { + return false; + } + + // Only chain_id is allowed to be touched. + key == U256::from(0u32) +} + +fn is_constant_code_hash( + address: Address, + key: U256, + storage: StoragePtr, +) -> bool { + if address != ACCOUNT_CODE_STORAGE_ADDRESS { + // Not a code hash + return false; + } + + let value = storage.borrow_mut().read_value(&StorageKey::new( + AccountTreeId::new(address), + u256_to_h256(key), + )); + + value != H256::zero() +} + +fn valid_eth_token_call(address: Address, msg_sender: Address) -> bool { + let is_valid_caller = msg_sender == MSG_VALUE_SIMULATOR_ADDRESS + || msg_sender == CONTRACT_DEPLOYER_ADDRESS + || msg_sender == BOOTLOADER_ADDRESS; + address == L2_ETH_TOKEN_ADDRESS && is_valid_caller +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/params.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/params.rs new file mode 100644 index 00000000000..1a4ced478b6 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/params.rs @@ -0,0 +1,18 @@ +use std::collections::HashSet; +use zksync_types::{Address, U256}; + +#[derive(Debug, Clone)] +pub struct ValidationTracerParams { + pub user_address: Address, + pub paymaster_address: Address, + /// Slots that are trusted (i.e. the user can access them). + pub trusted_slots: HashSet<(Address, U256)>, + /// Trusted addresses (the user can access any slots on these addresses). + pub trusted_addresses: HashSet
, + /// Slots, that are trusted and the value of them is the new trusted address. + /// They are needed to work correctly with beacon proxy, where the address of the implementation is + /// stored in the beacon. + pub trusted_address_slots: HashSet<(Address, U256)>, + /// Number of computational gas that validation step is allowed to use. + pub computational_gas_limit: u32, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/types.rs b/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/types.rs new file mode 100644 index 00000000000..b9d44227992 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/tracers/validation/types.rs @@ -0,0 +1,18 @@ +use zksync_types::{Address, H256}; + +#[derive(Debug, Clone, Eq, PartialEq, Copy)] +#[allow(clippy::enum_variant_names)] +pub(super) enum ValidationTracerMode { + /// Should be activated when the transaction is being validated by user. + UserTxValidation, + /// Should be activated when the transaction is being validated by the paymaster. + PaymasterTxValidation, + /// Is a state when there are no restrictions on the execution. + NoValidation, +} + +#[derive(Debug, Clone, Default)] +pub(super) struct NewTrustedValidationItems { + pub(super) new_allowed_slots: Vec, + pub(super) new_trusted_addresses: Vec
, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/inputs/execution_mode.rs b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/execution_mode.rs new file mode 100644 index 00000000000..41492af6edc --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/execution_mode.rs @@ -0,0 +1,15 @@ +/// Execution mode determines when the virtual machine execution should stop. +/// We are also using a different set of tracers, depending on the selected mode - for example for OneTx, +/// we use Refund Tracer, and for Bootloader we use 'DefaultTracer` in a special mode to track the Bootloader return code +/// Flow of execution: +/// VmStarted -> Enter the bootloader -> Tx1 -> Tx2 -> ... -> TxN -> +/// -> Terminate bootloader execution -> Exit bootloader -> VmStopped +#[derive(Debug, Copy, Clone)] +pub enum VmExecutionMode { + /// Stop after executing the next transaction. + OneTx, + /// Stop after executing the entire batch. + Batch, + /// Stop after executing the entire bootloader. But before you exit the bootloader. + Bootloader, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/inputs/l1_batch_env.rs b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/l1_batch_env.rs new file mode 100644 index 00000000000..ff843325769 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/l1_batch_env.rs @@ -0,0 +1,76 @@ +use std::collections::HashMap; + +use crate::L2BlockEnv; +use zk_evm::address_to_u256; +use zksync_types::{Address, L1BatchNumber, H256, U256}; +use zksync_utils::h256_to_u256; + +use crate::utils::fee::derive_base_fee_and_gas_per_pubdata; + +/// Unique params for each block +#[derive(Debug, Clone)] +pub struct L1BatchEnv { + // If previous batch hash is None, then this is the first batch + pub previous_batch_hash: Option, + pub number: L1BatchNumber, + pub timestamp: u64, + pub l1_gas_price: u64, + pub fair_l2_gas_price: u64, + pub fee_account: Address, + pub enforced_base_fee: Option, + pub first_l2_block: L2BlockEnv, +} + +impl L1BatchEnv { + pub fn base_fee(&self) -> u64 { + if let Some(base_fee) = self.enforced_base_fee { + return base_fee; + } + let (base_fee, _) = + derive_base_fee_and_gas_per_pubdata(self.l1_gas_price, self.fair_l2_gas_price); + base_fee + } +} + +impl L1BatchEnv { + 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 L1_GAS_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(&self) -> Vec<(usize, U256)> { + let mut base_params: HashMap = vec![ + ( + Self::OPERATOR_ADDRESS_SLOT, + address_to_u256(&self.fee_account), + ), + (Self::PREV_BLOCK_HASH_SLOT, Default::default()), + (Self::NEW_BLOCK_TIMESTAMP_SLOT, U256::from(self.timestamp)), + (Self::NEW_BLOCK_NUMBER_SLOT, U256::from(self.number.0)), + (Self::L1_GAS_PRICE_SLOT, U256::from(self.l1_gas_price)), + ( + Self::FAIR_L2_GAS_PRICE_SLOT, + U256::from(self.fair_l2_gas_price), + ), + (Self::EXPECTED_BASE_FEE_SLOT, U256::from(self.base_fee())), + (Self::SHOULD_SET_NEW_BLOCK_SLOT, U256::from(0u32)), + ] + .into_iter() + .collect(); + + if let Some(prev_block_hash) = self.previous_batch_hash { + base_params.insert(Self::PREV_BLOCK_HASH_SLOT, h256_to_u256(prev_block_hash)); + base_params.insert(Self::SHOULD_SET_NEW_BLOCK_SLOT, U256::from(1u32)); + } + base_params.into_iter().collect() + } + + pub(crate) fn block_gas_price_per_pubdata(&self) -> u64 { + derive_base_fee_and_gas_per_pubdata(self.l1_gas_price, self.fair_l2_gas_price).1 + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/inputs/l2_block.rs b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/l2_block.rs new file mode 100644 index 00000000000..42d0709f5dd --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/l2_block.rs @@ -0,0 +1,9 @@ +use zksync_types::H256; + +#[derive(Debug, Clone, Copy)] +pub struct L2BlockEnv { + pub number: u32, + pub timestamp: u64, + pub prev_block_hash: H256, + pub max_virtual_blocks_to_create: u32, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/inputs/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/mod.rs new file mode 100644 index 00000000000..f88d40def4b --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/mod.rs @@ -0,0 +1,9 @@ +pub use execution_mode::VmExecutionMode; +pub use l1_batch_env::L1BatchEnv; +pub use l2_block::L2BlockEnv; +pub use system_env::{SystemEnv, TxExecutionMode}; + +pub(crate) mod execution_mode; +pub(crate) mod l1_batch_env; +pub(crate) mod l2_block; +pub(crate) mod system_env; diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/inputs/system_env.rs b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/system_env.rs new file mode 100644 index 00000000000..3f861bddce0 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/inputs/system_env.rs @@ -0,0 +1,52 @@ +use std::fmt::Debug; + +use zksync_contracts::BaseSystemContracts; +use zksync_types::{L2ChainId, ProtocolVersionId}; + +/// Params related to the execution process, not batch it self +#[derive(Clone)] +pub struct SystemEnv { + // Always false for VM + pub zk_porter_available: bool, + pub version: ProtocolVersionId, + pub base_system_smart_contracts: BaseSystemContracts, + pub gas_limit: u32, + pub execution_mode: TxExecutionMode, + pub default_validation_computational_gas_limit: u32, + pub chain_id: L2ChainId, +} + +impl Debug for SystemEnv { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SystemEnv") + .field("zk_porter_available", &self.zk_porter_available) + .field("version", &self.version) + .field( + "base_system_smart_contracts", + &self.base_system_smart_contracts.hashes(), + ) + .field("gas_limit", &self.gas_limit) + .field( + "default_validation_computational_gas_limit", + &self.default_validation_computational_gas_limit, + ) + .field("execution_mode", &self.execution_mode) + .field("chain_id", &self.chain_id) + .finish() + } +} + +/// Enum denoting the *in-server* execution mode for the bootloader transactions. +/// +/// If `EthCall` mode is chosen, the bootloader will use `mimicCall` opcode +/// to simulate the call instead of using the standard `execute` method of account. +/// This is needed to be able to behave equivalently to Ethereum without much overhead for custom account builders. +/// With `VerifyExecute` mode, transaction will be executed normally. +/// With `EstimateFee`, the bootloader will be used that has the same behavior +/// as the full `VerifyExecute` block, but errors in the account validation will be ignored. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TxExecutionMode { + VerifyExecute, + EstimateFee, + EthCall, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/internals/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/types/internals/mod.rs new file mode 100644 index 00000000000..601b7b8bd01 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/internals/mod.rs @@ -0,0 +1,7 @@ +pub(crate) use snapshot::VmSnapshot; +pub(crate) use transaction_data::TransactionData; +pub(crate) use vm_state::new_vm_state; +pub use vm_state::ZkSyncVmState; +mod snapshot; +mod transaction_data; +mod vm_state; diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/internals/snapshot.rs b/core/multivm_deps/vm_virtual_blocks/src/types/internals/snapshot.rs new file mode 100644 index 00000000000..3b336d5e354 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/internals/snapshot.rs @@ -0,0 +1,11 @@ +use zk_evm::vm_state::VmLocalState; + +use crate::bootloader_state::BootloaderStateSnapshot; + +/// A snapshot of the VM that holds enough information to +/// rollback the VM to some historical state. +#[derive(Debug, Clone)] +pub(crate) struct VmSnapshot { + pub(crate) local_state: VmLocalState, + pub(crate) bootloader_state: BootloaderStateSnapshot, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/internals/transaction_data.rs b/core/multivm_deps/vm_virtual_blocks/src/types/internals/transaction_data.rs new file mode 100644 index 00000000000..7d8598842d8 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/internals/transaction_data.rs @@ -0,0 +1,344 @@ +use std::convert::TryInto; +use zksync_types::ethabi::{encode, Address, Token}; +use zksync_types::fee::{encoding_len, Fee}; +use zksync_types::l1::is_l1_tx_type; +use zksync_types::l2::L2Tx; +use zksync_types::transaction_request::{PaymasterParams, TransactionRequest}; +use zksync_types::{ + l2::TransactionType, Bytes, Execute, ExecuteTransactionCommon, L2ChainId, L2TxCommonData, + Nonce, Transaction, H256, U256, +}; +use zksync_utils::address_to_h256; +use zksync_utils::{bytecode::hash_bytecode, bytes_to_be_words, h256_to_u256}; + +use crate::utils::overhead::{get_amortized_overhead, OverheadCoeficients}; + +/// 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() + }; + + 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: common_data.fee.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.unwrap_or_default(), + 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.unwrap_or_default(), + 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.unwrap_or_default(), + 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 effective_gas_price_per_pubdata(&self, block_gas_price_per_pubdata: u32) -> u32 { + // It is enforced by the protocol that the L1 transactions always pay the exact amount of gas per pubdata + // as was supplied in the transaction. + if is_l1_tx_type(self.tx_type) { + self.pubdata_price_limit.as_u32() + } else { + block_gas_price_per_pubdata + } + } + + pub(crate) fn overhead_gas(&self, block_gas_price_per_pubdata: u32) -> u32 { + let total_gas_limit = self.gas_limit.as_u32(); + let gas_price_per_pubdata = + self.effective_gas_price_per_pubdata(block_gas_price_per_pubdata); + + 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, + ); + + let coeficients = OverheadCoeficients::from_tx_type(self.tx_type); + get_amortized_overhead( + total_gas_limit, + gas_price_per_pubdata, + encoded_len, + coeficients, + ) + } + + pub(crate) fn trusted_ergs_limit(&self, _block_gas_price_per_pubdata: u64) -> U256 { + // TODO (EVM-66): correctly calculate the trusted gas limit for a transaction + 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 transaction_request: TransactionRequest = l2_tx.into(); + + // It is assumed that the TransactionData always has all the necessary components to recover the hash. + transaction_request + .get_tx_hash(chain_id) + .expect("Could not recover L2 transaction hash") + } + + fn canonical_l1_tx_hash(&self) -> Result { + use zksync_types::web3::signing::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 factory_deps = (!self.factory_deps.is_empty()).then_some(self.factory_deps); + let execute = Execute { + contract_address: self.to, + value: self.value, + calldata: self.data, + factory_deps, + }; + + Ok(L2Tx { + execute, + common_data, + received_timestamp_ms: 0, + raw_bytes: self.raw_bytes.map(Bytes::from), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use zksync_types::fee::encoding_len; + + #[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/multivm_deps/vm_virtual_blocks/src/types/internals/vm_state.rs b/core/multivm_deps/vm_virtual_blocks/src/types/internals/vm_state.rs new file mode 100644 index 00000000000..60969241295 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/internals/vm_state.rs @@ -0,0 +1,175 @@ +use zk_evm::{ + aux_structures::MemoryPage, + aux_structures::Timestamp, + block_properties::BlockProperties, + vm_state::{CallStackEntry, PrimitiveValue, VmState}, + witness_trace::DummyTracer, + zkevm_opcode_defs::{ + system_params::{BOOTLOADER_MAX_MEMORY, INITIAL_FRAME_FORMAL_EH_LOCATION}, + FatPointer, BOOTLOADER_CALLDATA_PAGE, + }, +}; + +use zk_evm::zkevm_opcode_defs::{ + BOOTLOADER_BASE_PAGE, BOOTLOADER_CODE_PAGE, STARTING_BASE_PAGE, STARTING_TIMESTAMP, +}; +use zksync_config::constants::BOOTLOADER_ADDRESS; +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::block::legacy_miniblock_hash; +use zksync_types::{zkevm_test_harness::INITIAL_MONOTONIC_CYCLE_COUNTER, Address, MiniblockNumber}; +use zksync_utils::h256_to_u256; + +use crate::bootloader_state::BootloaderState; +use crate::constants::BOOTLOADER_HEAP_PAGE; +use crate::old_vm::{ + event_sink::InMemoryEventSink, history_recorder::HistoryMode, memory::SimpleMemory, + oracles::decommitter::DecommitterOracle, oracles::precompile::PrecompilesProcessorWithHistory, + oracles::storage::StorageOracle, +}; +use crate::types::inputs::{L1BatchEnv, SystemEnv}; +use crate::utils::l2_blocks::{assert_next_block, load_last_l2_block}; +use crate::L2Block; + +pub type ZkSyncVmState = VmState< + StorageOracle, + SimpleMemory, + InMemoryEventSink, + PrecompilesProcessorWithHistory, + DecommitterOracle, + DummyTracer, +>; + +fn formal_calldata_abi() -> PrimitiveValue { + let fat_pointer = FatPointer { + offset: 0, + memory_page: BOOTLOADER_CALLDATA_PAGE, + start: 0, + length: 0, + }; + + PrimitiveValue { + value: fat_pointer.to_u256(), + is_pointer: true, + } +} + +/// Initialize the vm state and all necessary oracles +pub(crate) fn new_vm_state( + storage: StoragePtr, + system_env: &SystemEnv, + l1_batch_env: &L1BatchEnv, +) -> (ZkSyncVmState, BootloaderState) { + let last_l2_block = if let Some(last_l2_block) = load_last_l2_block(storage.clone()) { + last_l2_block + } else { + // This is the scenario of either the first L2 block ever or + // the first block after the upgrade for support of L2 blocks. + L2Block { + number: l1_batch_env.first_l2_block.number.saturating_sub(1), + timestamp: 0, + hash: legacy_miniblock_hash(MiniblockNumber(l1_batch_env.first_l2_block.number) - 1), + } + }; + + assert_next_block(&last_l2_block, &l1_batch_env.first_l2_block); + let first_l2_block = l1_batch_env.first_l2_block; + let storage_oracle: StorageOracle = StorageOracle::new(storage.clone()); + let mut memory = SimpleMemory::default(); + let event_sink = InMemoryEventSink::default(); + let precompiles_processor = PrecompilesProcessorWithHistory::::default(); + let mut decommittment_processor: DecommitterOracle = + DecommitterOracle::new(storage); + + decommittment_processor.populate( + vec![( + h256_to_u256(system_env.base_system_smart_contracts.default_aa.hash), + system_env + .base_system_smart_contracts + .default_aa + .code + .clone(), + )], + Timestamp(0), + ); + + memory.populate( + vec![( + BOOTLOADER_CODE_PAGE, + system_env + .base_system_smart_contracts + .bootloader + .code + .clone(), + )], + Timestamp(0), + ); + + let bootloader_initial_memory = l1_batch_env.bootloader_initial_memory(); + memory.populate_page( + BOOTLOADER_HEAP_PAGE as usize, + bootloader_initial_memory.clone(), + Timestamp(0), + ); + + let mut vm = VmState::empty_state( + storage_oracle, + memory, + event_sink, + precompiles_processor, + decommittment_processor, + DummyTracer, + BlockProperties { + default_aa_code_hash: h256_to_u256( + system_env.base_system_smart_contracts.default_aa.hash, + ), + zkporter_is_available: system_env.zk_porter_available, + }, + ); + + vm.local_state.callstack.current.ergs_remaining = system_env.gas_limit; + + let initial_context = CallStackEntry { + this_address: BOOTLOADER_ADDRESS, + msg_sender: Address::zero(), + code_address: BOOTLOADER_ADDRESS, + base_memory_page: MemoryPage(BOOTLOADER_BASE_PAGE), + code_page: MemoryPage(BOOTLOADER_CODE_PAGE), + sp: 0, + pc: 0, + // Note, that since the results are written at the end of the memory + // it is needed to have the entire heap available from the beginning + heap_bound: BOOTLOADER_MAX_MEMORY, + aux_heap_bound: BOOTLOADER_MAX_MEMORY, + exception_handler_location: INITIAL_FRAME_FORMAL_EH_LOCATION, + ergs_remaining: system_env.gas_limit, + this_shard_id: 0, + caller_shard_id: 0, + code_shard_id: 0, + is_static: false, + is_local_frame: false, + context_u128_value: 0, + }; + + // We consider the contract that is being run as a bootloader + vm.push_bootloader_context(INITIAL_MONOTONIC_CYCLE_COUNTER - 1, initial_context); + vm.local_state.timestamp = STARTING_TIMESTAMP; + vm.local_state.memory_page_counter = STARTING_BASE_PAGE; + vm.local_state.monotonic_cycle_counter = INITIAL_MONOTONIC_CYCLE_COUNTER; + vm.local_state.current_ergs_per_pubdata_byte = 0; + vm.local_state.registers[0] = formal_calldata_abi(); + + // Deleting all the historical records brought by the initial + // initialization of the VM to make them permanent. + vm.decommittment_processor.delete_history(); + vm.event_sink.delete_history(); + vm.storage.delete_history(); + vm.memory.delete_history(); + vm.precompiles_processor.delete_history(); + let bootloader_state = BootloaderState::new( + system_env.execution_mode, + bootloader_initial_memory, + first_l2_block, + ); + + (vm, bootloader_state) +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/types/mod.rs new file mode 100644 index 00000000000..cd31e7dc5c5 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod inputs; +pub(crate) mod internals; +pub(crate) mod outputs; diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/outputs/execution_result.rs b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/execution_result.rs new file mode 100644 index 00000000000..bb46cb41ec8 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/execution_result.rs @@ -0,0 +1,83 @@ +use crate::{Halt, VmExecutionStatistics, VmRevertReason}; +use zksync_config::constants::PUBLISH_BYTECODE_OVERHEAD; +use zksync_types::event::{extract_long_l2_to_l1_messages, extract_published_bytecodes}; +use zksync_types::tx::tx_execution_info::VmExecutionLogs; +use zksync_types::tx::ExecutionMetrics; +use zksync_types::Transaction; +use zksync_utils::bytecode::bytecode_len_in_bytes; + +/// Refunds produced for the user. +#[derive(Debug, Clone, Default)] +pub struct Refunds { + pub gas_refunded: u32, + pub operator_suggested_refund: u32, +} + +/// Result and logs of the VM execution. +#[derive(Debug, Clone)] +pub struct VmExecutionResultAndLogs { + pub result: ExecutionResult, + pub logs: VmExecutionLogs, + pub statistics: VmExecutionStatistics, + pub refunds: Refunds, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ExecutionResult { + /// Returned successfully + Success { output: Vec }, + /// Reverted by contract + Revert { output: VmRevertReason }, + /// Reverted for various reasons + Halt { reason: Halt }, +} + +impl ExecutionResult { + /// Returns `true` if the execution was failed. + pub fn is_failed(&self) -> bool { + matches!(self, Self::Revert { .. } | Self::Halt { .. }) + } +} + +impl VmExecutionResultAndLogs { + pub fn get_execution_metrics(&self, tx: Option<&Transaction>) -> ExecutionMetrics { + let contracts_deployed = tx + .map(|tx| { + tx.execute + .factory_deps + .as_ref() + .map_or(0, |deps| deps.len() as u16) + }) + .unwrap_or(0); + + // We published the data as ABI-encoded `bytes`, so the total length is: + // - message length in bytes, rounded up to a multiple of 32 + // - 32 bytes of encoded offset + // - 32 bytes of encoded length + let l2_l1_long_messages = extract_long_l2_to_l1_messages(&self.logs.events) + .iter() + .map(|event| (event.len() + 31) / 32 * 32 + 64) + .sum(); + + let published_bytecode_bytes = extract_published_bytecodes(&self.logs.events) + .iter() + .map(|bytecodehash| { + bytecode_len_in_bytes(*bytecodehash) + PUBLISH_BYTECODE_OVERHEAD as usize + }) + .sum(); + + ExecutionMetrics { + gas_used: self.statistics.gas_used as usize, + published_bytecode_bytes, + l2_l1_long_messages, + l2_l1_logs: self.logs.l2_to_l1_logs.len(), + contracts_used: self.statistics.contracts_used, + contracts_deployed, + vm_events: self.logs.events.len(), + storage_logs: self.logs.storage_logs.len(), + total_log_queries: self.statistics.total_log_queries, + cycles_used: self.statistics.cycles_used, + computational_gas_used: self.statistics.computational_gas_used, + } + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/outputs/execution_state.rs b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/execution_state.rs new file mode 100644 index 00000000000..3ae36a17967 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/execution_state.rs @@ -0,0 +1,22 @@ +use zksync_types::l2_to_l1_log::L2ToL1Log; +use zksync_types::{StorageLogQuery, VmEvent, U256}; + +/// State of the VM since the start of the batch execution. +#[derive(Debug, Clone, PartialEq)] +pub struct CurrentExecutionState { + /// Events produced by the VM. + pub events: Vec, + /// Storage logs produced by the VM. + pub storage_log_queries: Vec, + /// Hashes of the contracts used by the VM. + pub used_contract_hashes: Vec, + /// L2 to L1 logs produced by the VM. + pub l2_to_l1_logs: Vec, + /// Number of log queries produced by the VM. Including l2_to_l1 logs, storage logs and events. + pub total_log_queries: usize, + /// Number of cycles used by the VM. + pub cycles_used: u32, +} + +/// Bootloader Memory of the VM. +pub type BootloaderMemory = Vec<(usize, U256)>; diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/outputs/finished_l1batch.rs b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/finished_l1batch.rs new file mode 100644 index 00000000000..064d4c2d658 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/finished_l1batch.rs @@ -0,0 +1,12 @@ +use crate::{BootloaderMemory, CurrentExecutionState, VmExecutionResultAndLogs}; + +/// State of the VM after the batch execution. +#[derive(Debug, Clone)] +pub struct FinishedL1Batch { + /// Result of the execution of the block tip part of the batch. + pub block_tip_execution_result: VmExecutionResultAndLogs, + /// State of the VM after the execution of the last transaction. + pub final_execution_state: CurrentExecutionState, + /// Memory of the bootloader with all executed transactions. Could be optional for old versions of the VM. + pub final_bootloader_memory: Option, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/outputs/l2_block.rs b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/l2_block.rs new file mode 100644 index 00000000000..ccbcba15f65 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/l2_block.rs @@ -0,0 +1,7 @@ +use zksync_types::H256; + +pub struct L2Block { + pub number: u32, + pub timestamp: u64, + pub hash: H256, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/outputs/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/mod.rs new file mode 100644 index 00000000000..8aa029cb53f --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/mod.rs @@ -0,0 +1,11 @@ +mod execution_result; +mod execution_state; +mod finished_l1batch; +mod l2_block; +mod statistic; + +pub use execution_result::{ExecutionResult, Refunds, VmExecutionResultAndLogs}; +pub use execution_state::{BootloaderMemory, CurrentExecutionState}; +pub use finished_l1batch::FinishedL1Batch; +pub use l2_block::L2Block; +pub use statistic::{VmExecutionStatistics, VmMemoryMetrics}; diff --git a/core/multivm_deps/vm_virtual_blocks/src/types/outputs/statistic.rs b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/statistic.rs new file mode 100644 index 00000000000..8f03678315f --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/types/outputs/statistic.rs @@ -0,0 +1,26 @@ +/// Statistics of the tx execution. +#[derive(Debug, Default, Clone)] +pub struct VmExecutionStatistics { + /// Number of contracts used by the VM during the tx execution. + pub contracts_used: usize, + /// Cycles used by the VM during the tx execution. + pub cycles_used: u32, + /// Gas used by the VM during the tx execution. + pub gas_used: u32, + /// Computational gas used by the VM during the tx execution. + pub computational_gas_used: u32, + /// Number of log queries produced by the VM during the tx execution. + pub total_log_queries: usize, +} + +/// Oracle metrics of the VM. +pub struct VmMemoryMetrics { + pub event_sink_inner: usize, + pub event_sink_history: usize, + pub memory_inner: usize, + pub memory_history: usize, + pub decommittment_processor_inner: usize, + pub decommittment_processor_history: usize, + pub storage_inner: usize, + pub storage_history: usize, +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/utils/fee.rs b/core/multivm_deps/vm_virtual_blocks/src/utils/fee.rs new file mode 100644 index 00000000000..e2e0bdbe599 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/utils/fee.rs @@ -0,0 +1,29 @@ +//! Utility functions for vm +use zksync_config::constants::MAX_GAS_PER_PUBDATA_BYTE; +use zksync_utils::ceil_div; + +use crate::old_vm::utils::eth_price_per_pubdata_byte; + +/// Calcluates the amount of gas required to publish one byte of pubdata +pub fn base_fee_to_gas_per_pubdata(l1_gas_price: u64, base_fee: u64) -> u64 { + let eth_price_per_pubdata_byte = eth_price_per_pubdata_byte(l1_gas_price); + + ceil_div(eth_price_per_pubdata_byte, base_fee) +} + +/// Calculates the base fee and gas per pubdata for the given L1 gas price. +pub fn derive_base_fee_and_gas_per_pubdata(l1_gas_price: u64, fair_gas_price: u64) -> (u64, u64) { + let eth_price_per_pubdata_byte = eth_price_per_pubdata_byte(l1_gas_price); + + // The baseFee is set in such a way that it is always possible for a transaction to + // publish enough public data while compensating us for it. + let base_fee = std::cmp::max( + fair_gas_price, + ceil_div(eth_price_per_pubdata_byte, MAX_GAS_PER_PUBDATA_BYTE), + ); + + ( + base_fee, + base_fee_to_gas_per_pubdata(l1_gas_price, base_fee), + ) +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/utils/l2_blocks.rs b/core/multivm_deps/vm_virtual_blocks/src/utils/l2_blocks.rs new file mode 100644 index 00000000000..ca33bce0dfd --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/utils/l2_blocks.rs @@ -0,0 +1,93 @@ +use crate::{L2Block, L2BlockEnv}; +use zksync_config::constants::{ + SYSTEM_CONTEXT_ADDRESS, SYSTEM_CONTEXT_CURRENT_L2_BLOCK_HASHES_POSITION, + SYSTEM_CONTEXT_CURRENT_L2_BLOCK_INFO_POSITION, SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION, + SYSTEM_CONTEXT_STORED_L2_BLOCK_HASHES, +}; +use zksync_state::{ReadStorage, StoragePtr}; +use zksync_types::block::unpack_block_info; +use zksync_types::web3::signing::keccak256; +use zksync_types::{AccountTreeId, MiniblockNumber, StorageKey, H256, U256}; +use zksync_utils::{h256_to_u256, u256_to_h256}; + +pub(crate) fn get_l2_block_hash_key(block_number: u32) -> StorageKey { + let position = h256_to_u256(SYSTEM_CONTEXT_CURRENT_L2_BLOCK_HASHES_POSITION) + + U256::from(block_number % SYSTEM_CONTEXT_STORED_L2_BLOCK_HASHES); + StorageKey::new( + AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS), + u256_to_h256(position), + ) +} + +pub(crate) fn assert_next_block(prev_block: &L2Block, next_block: &L2BlockEnv) { + if prev_block.number == 0 { + // Special case for the first block it can have the same timestamp as the previous block. + assert!(prev_block.timestamp <= next_block.timestamp); + } else { + assert_eq!(prev_block.number + 1, next_block.number); + assert!(prev_block.timestamp < next_block.timestamp); + } + assert_eq!(prev_block.hash, next_block.prev_block_hash); +} + +/// Returns the hash of the l2_block. +/// `txs_rolling_hash` of the l2_block is calculated the following way: +/// If the l2_block has 0 transactions, then `txs_rolling_hash` is equal to `H256::zero()`. +/// If the l2_block has i transactions, then `txs_rolling_hash` is equal to `H(H_{i-1}, H(tx_i))`, where +/// `H_{i-1}` is the `txs_rolling_hash` of the first i-1 transactions. +pub(crate) fn l2_block_hash( + l2_block_number: MiniblockNumber, + l2_block_timestamp: u64, + prev_l2_block_hash: H256, + txs_rolling_hash: H256, +) -> H256 { + let mut digest: [u8; 128] = [0u8; 128]; + U256::from(l2_block_number.0).to_big_endian(&mut digest[0..32]); + U256::from(l2_block_timestamp).to_big_endian(&mut digest[32..64]); + digest[64..96].copy_from_slice(prev_l2_block_hash.as_bytes()); + digest[96..128].copy_from_slice(txs_rolling_hash.as_bytes()); + + H256(keccak256(&digest)) +} + +/// Get last saved block from storage +pub fn load_last_l2_block(storage: StoragePtr) -> Option { + // Get block number and timestamp + let current_l2_block_info_key = StorageKey::new( + AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS), + SYSTEM_CONTEXT_CURRENT_L2_BLOCK_INFO_POSITION, + ); + let mut storage_ptr = storage.borrow_mut(); + let current_l2_block_info = storage_ptr.read_value(¤t_l2_block_info_key); + let (block_number, block_timestamp) = unpack_block_info(h256_to_u256(current_l2_block_info)); + let block_number = block_number as u32; + if block_number == 0 { + // The block does not exist yet + return None; + } + + // Get prev block hash + let position = get_l2_block_hash_key(block_number - 1); + let prev_block_hash = storage_ptr.read_value(&position); + + // Get current tx rolling hash + let position = StorageKey::new( + AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS), + SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION, + ); + let current_tx_rolling_hash = storage_ptr.read_value(&position); + + // Calculate current hash + let current_block_hash = l2_block_hash( + MiniblockNumber(block_number), + block_timestamp, + prev_block_hash, + current_tx_rolling_hash, + ); + + Some(L2Block { + number: block_number, + timestamp: block_timestamp, + hash: current_block_hash, + }) +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/utils/mod.rs b/core/multivm_deps/vm_virtual_blocks/src/utils/mod.rs new file mode 100644 index 00000000000..15ffa92b549 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/utils/mod.rs @@ -0,0 +1,5 @@ +/// Utility functions for the VM. +pub mod fee; +pub mod l2_blocks; +pub mod overhead; +pub mod transaction_encoding; diff --git a/core/multivm_deps/vm_virtual_blocks/src/utils/overhead.rs b/core/multivm_deps/vm_virtual_blocks/src/utils/overhead.rs new file mode 100644 index 00000000000..6fdce9c724e --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/utils/overhead.rs @@ -0,0 +1,347 @@ +use crate::constants::{BLOCK_OVERHEAD_GAS, BLOCK_OVERHEAD_PUBDATA, BOOTLOADER_TX_ENCODING_SPACE}; +use zk_evm::zkevm_opcode_defs::system_params::MAX_TX_ERGS_LIMIT; +use zksync_config::constants::{MAX_L2_TX_GAS_LIMIT, MAX_TXS_IN_BLOCK}; +use zksync_types::l1::is_l1_tx_type; +use zksync_types::U256; +use zksync_utils::ceil_div_u256; + +/// Derives the overhead for processing transactions in a block. +pub fn derive_overhead( + gas_limit: u32, + gas_price_per_pubdata: u32, + encoded_len: usize, + coeficients: OverheadCoeficients, +) -> u32 { + // Even if the gas limit is greater than the MAX_TX_ERGS_LIMIT, we assume that everything beyond MAX_TX_ERGS_LIMIT + // will be spent entirely on publishing bytecodes and so we derive the overhead solely based on the capped value + let gas_limit = std::cmp::min(MAX_TX_ERGS_LIMIT, gas_limit); + + // Using large U256 type to avoid overflow + let max_block_overhead = U256::from(block_overhead_gas(gas_price_per_pubdata)); + let gas_limit = U256::from(gas_limit); + let encoded_len = U256::from(encoded_len); + + // The MAX_TX_ERGS_LIMIT is formed in a way that may fullfills a single-instance circuits + // if used in full. That is, within MAX_TX_ERGS_LIMIT it is possible to fully saturate all the single-instance + // circuits. + let overhead_for_single_instance_circuits = + ceil_div_u256(gas_limit * max_block_overhead, MAX_TX_ERGS_LIMIT.into()); + + // The overhead for occupying the bootloader memory + let overhead_for_length = ceil_div_u256( + encoded_len * max_block_overhead, + BOOTLOADER_TX_ENCODING_SPACE.into(), + ); + + // The overhead for occupying a single tx slot + let tx_slot_overhead = ceil_div_u256(max_block_overhead, MAX_TXS_IN_BLOCK.into()); + + // We use "ceil" here for formal reasons to allow easier approach for calculating the overhead in O(1) + // let max_pubdata_in_tx = ceil_div_u256(gas_limit, gas_price_per_pubdata); + + // The maximal potential overhead from pubdata + // TODO (EVM-67): possibly use overhead for pubdata + // let pubdata_overhead = ceil_div_u256( + // max_pubdata_in_tx * max_block_overhead, + // MAX_PUBDATA_PER_BLOCK.into(), + // ); + + vec![ + (coeficients.ergs_limit_overhead_coeficient + * overhead_for_single_instance_circuits.as_u32() as f64) + .floor() as u32, + (coeficients.bootloader_memory_overhead_coeficient * overhead_for_length.as_u32() as f64) + .floor() as u32, + (coeficients.slot_overhead_coeficient * tx_slot_overhead.as_u32() as f64) as u32, + ] + .into_iter() + .max() + .unwrap() +} + +/// Contains the coeficients with which the overhead for transactions will be calculated. +/// All of the coeficients should be <= 1. There are here to provide a certain "discount" for normal transactions +/// at the risk of malicious transactions that may close the block prematurely. +/// IMPORTANT: to perform correct computations, `MAX_TX_ERGS_LIMIT / coeficients.ergs_limit_overhead_coeficient` MUST +/// result in an integer number +#[derive(Debug, Clone, Copy)] +pub struct OverheadCoeficients { + slot_overhead_coeficient: f64, + bootloader_memory_overhead_coeficient: f64, + ergs_limit_overhead_coeficient: f64, +} + +impl OverheadCoeficients { + // This method ensures that the parameters keep the required invariants + fn new_checked( + slot_overhead_coeficient: f64, + bootloader_memory_overhead_coeficient: f64, + ergs_limit_overhead_coeficient: f64, + ) -> Self { + assert!( + (MAX_TX_ERGS_LIMIT as f64 / ergs_limit_overhead_coeficient).round() + == MAX_TX_ERGS_LIMIT as f64 / ergs_limit_overhead_coeficient, + "MAX_TX_ERGS_LIMIT / ergs_limit_overhead_coeficient must be an integer" + ); + + Self { + slot_overhead_coeficient, + bootloader_memory_overhead_coeficient, + ergs_limit_overhead_coeficient, + } + } + + // L1->L2 do not receive any discounts + fn new_l1() -> Self { + OverheadCoeficients::new_checked(1.0, 1.0, 1.0) + } + + fn new_l2() -> Self { + OverheadCoeficients::new_checked( + 1.0, 1.0, + // For L2 transactions we allow a certain default discount with regard to the number of ergs. + // Multiinstance circuits can in theory be spawned infinite times, while projected future limitations + // on gas per pubdata allow for roughly 800kk gas per L1 batch, so the rough trust "discount" on the proof's part + // to be paid by the users is 0.1. + 0.1, + ) + } + + /// Return the coeficients for the given transaction type + pub fn from_tx_type(tx_type: u8) -> Self { + if is_l1_tx_type(tx_type) { + Self::new_l1() + } else { + Self::new_l2() + } + } +} + +/// This method returns the overhead for processing the block +pub(crate) fn get_amortized_overhead( + total_gas_limit: u32, + gas_per_pubdata_byte_limit: u32, + encoded_len: usize, + coeficients: OverheadCoeficients, +) -> u32 { + // Using large U256 type to prevent overflows. + let overhead_for_block_gas = U256::from(block_overhead_gas(gas_per_pubdata_byte_limit)); + let total_gas_limit = U256::from(total_gas_limit); + let encoded_len = U256::from(encoded_len); + + // Derivation of overhead consists of 4 parts: + // 1. The overhead for taking up a transaction's slot. (O1): O1 = 1 / MAX_TXS_IN_BLOCK + // 2. The overhead for taking up the bootloader's memory (O2): O2 = encoded_len / BOOTLOADER_TX_ENCODING_SPACE + // 3. The overhead for possible usage of pubdata. (O3): O3 = max_pubdata_in_tx / MAX_PUBDATA_PER_BLOCK + // 4. The overhead for possible usage of all the single-instance circuits. (O4): O4 = gas_limit / MAX_TX_ERGS_LIMIT + // + // The maximum of these is taken to derive the part of the block's overhead to be paid by the users: + // + // max_overhead = max(O1, O2, O3, O4) + // overhead_gas = ceil(max_overhead * overhead_for_block_gas). Thus, overhead_gas is a function of + // tx_gas_limit, gas_per_pubdata_byte_limit and encoded_len. + // + // While it is possible to derive the overhead with binary search in O(log n), it is too expensive to be done + // on L1, so here is a reference implementation of finding the overhead for transaction in O(1): + // + // Given total_gas_limit = tx_gas_limit + overhead_gas, we need to find overhead_gas and tx_gas_limit, such that: + // 1. overhead_gas is maximal possible (the operator is paid fairly) + // 2. overhead_gas(tx_gas_limit, gas_per_pubdata_byte_limit, encoded_len) >= overhead_gas (the user does not overpay) + // The third part boils to the following 4 inequalities (at least one of these must hold): + // ceil(O1 * overhead_for_block_gas) >= overhead_gas + // ceil(O2 * overhead_for_block_gas) >= overhead_gas + // ceil(O3 * overhead_for_block_gas) >= overhead_gas + // ceil(O4 * overhead_for_block_gas) >= overhead_gas + // + // Now, we need to solve each of these separately: + + // 1. The overhead for occupying a single tx slot is a constant: + let tx_slot_overhead = { + let tx_slot_overhead = + ceil_div_u256(overhead_for_block_gas, MAX_TXS_IN_BLOCK.into()).as_u32(); + (coeficients.slot_overhead_coeficient * tx_slot_overhead as f64).floor() as u32 + }; + + // 2. The overhead for occupying the bootloader memory can be derived from encoded_len + let overhead_for_length = { + let overhead_for_length = ceil_div_u256( + encoded_len * overhead_for_block_gas, + BOOTLOADER_TX_ENCODING_SPACE.into(), + ) + .as_u32(); + + (coeficients.bootloader_memory_overhead_coeficient * overhead_for_length as f64).floor() + as u32 + }; + + // TODO (EVM-67): possibly include the overhead for pubdata. The formula below has not been properly maintained, + // since the pubdat is not published. If decided to use the pubdata overhead, it needs to be updated. + // 3. ceil(O3 * overhead_for_block_gas) >= overhead_gas + // O3 = max_pubdata_in_tx / MAX_PUBDATA_PER_BLOCK = ceil(gas_limit / gas_per_pubdata_byte_limit) / MAX_PUBDATA_PER_BLOCK + // >= (gas_limit / (gas_per_pubdata_byte_limit * MAX_PUBDATA_PER_BLOCK). Throwing off the `ceil`, while may provide marginally lower + // overhead to the operator, provides substantially easier formula to work with. + // + // For better clarity, let's denote gas_limit = GL, MAX_PUBDATA_PER_BLOCK = MP, gas_per_pubdata_byte_limit = EP, overhead_for_block_gas = OB, total_gas_limit = TL, overhead_gas = OE + // ceil(OB * (TL - OE) / (EP * MP)) >= OE + // + // OB * (TL - OE) / (MP * EP) > OE - 1 + // OB * (TL - OE) > (OE - 1) * EP * MP + // OB * TL + EP * MP > OE * EP * MP + OE * OB + // (OB * TL + EP * MP) / (EP * MP + OB) > OE + // OE = floor((OB * TL + EP * MP) / (EP * MP + OB)) with possible -1 if the division is without remainder + // let overhead_for_pubdata = { + // let numerator: U256 = overhead_for_block_gas * total_gas_limit + // + gas_per_pubdata_byte_limit * U256::from(MAX_PUBDATA_PER_BLOCK); + // let denominator = + // gas_per_pubdata_byte_limit * U256::from(MAX_PUBDATA_PER_BLOCK) + overhead_for_block_gas; + + // // Corner case: if `total_gas_limit` = `gas_per_pubdata_byte_limit` = 0 + // // then the numerator will be 0 and subtracting 1 will cause a panic, so we just return a zero. + // if numerator.is_zero() { + // 0.into() + // } else { + // (numerator - 1) / denominator + // } + // }; + + // 4. K * ceil(O4 * overhead_for_block_gas) >= overhead_gas, where K is the discount + // O4 = gas_limit / MAX_TX_ERGS_LIMIT. Using the notation from the previous equation: + // ceil(OB * GL / MAX_TX_ERGS_LIMIT) >= (OE / K) + // ceil(OB * (TL - OE) / MAX_TX_ERGS_LIMIT) >= (OE/K) + // OB * (TL - OE) / MAX_TX_ERGS_LIMIT > (OE/K) - 1 + // OB * (TL - OE) > (OE/K) * MAX_TX_ERGS_LIMIT - MAX_TX_ERGS_LIMIT + // OB * TL + MAX_TX_ERGS_LIMIT > OE * ( MAX_TX_ERGS_LIMIT/K + OB) + // OE = floor(OB * TL + MAX_TX_ERGS_LIMIT / (MAX_TX_ERGS_LIMIT/K + OB)), with possible -1 if the division is without remainder + let overhead_for_gas = { + let numerator = overhead_for_block_gas * total_gas_limit + U256::from(MAX_TX_ERGS_LIMIT); + let denominator: U256 = U256::from( + (MAX_TX_ERGS_LIMIT as f64 / coeficients.ergs_limit_overhead_coeficient) as u64, + ) + overhead_for_block_gas; + + let overhead_for_gas = (numerator - 1) / denominator; + + overhead_for_gas.as_u32() + }; + + let overhead = vec![tx_slot_overhead, overhead_for_length, overhead_for_gas] + .into_iter() + .max() + // For the sake of consistency making sure that total_gas_limit >= max_overhead + .map(|max_overhead| std::cmp::min(max_overhead, total_gas_limit.as_u32())) + .unwrap(); + + let limit_after_deducting_overhead = total_gas_limit - overhead; + + // During double checking of the overhead, the bootloader will assume that the + // body of the transaction does not have any more than MAX_L2_TX_GAS_LIMIT ergs available to it. + if limit_after_deducting_overhead.as_u64() > MAX_L2_TX_GAS_LIMIT { + // We derive the same overhead that would exist for the MAX_L2_TX_GAS_LIMIT ergs + derive_overhead( + MAX_L2_TX_GAS_LIMIT as u32, + gas_per_pubdata_byte_limit, + encoded_len.as_usize(), + coeficients, + ) + } else { + overhead + } +} + +pub(crate) fn block_overhead_gas(gas_per_pubdata_byte: u32) -> u32 { + BLOCK_OVERHEAD_GAS + BLOCK_OVERHEAD_PUBDATA * gas_per_pubdata_byte +} + +#[cfg(test)] +mod tests { + + use super::*; + + // This method returns the maximum block overhead that can be charged from the user based on the binary search approach + pub(crate) fn get_maximal_allowed_overhead_bin_search( + total_gas_limit: u32, + gas_per_pubdata_byte_limit: u32, + encoded_len: usize, + coeficients: OverheadCoeficients, + ) -> u32 { + let mut left_bound = if MAX_TX_ERGS_LIMIT < total_gas_limit { + total_gas_limit - MAX_TX_ERGS_LIMIT + } else { + 0u32 + }; + // Safe cast: the gas_limit for a transaction can not be larger than 2^32 + let mut right_bound = total_gas_limit; + + // The closure returns whether a certain overhead would be accepted by the bootloader. + // It is accepted if the derived overhead (i.e. the actual overhead that the user has to pay) + // is >= than the overhead proposed by the operator. + let is_overhead_accepted = |suggested_overhead: u32| { + let derived_overhead = derive_overhead( + total_gas_limit - suggested_overhead, + gas_per_pubdata_byte_limit, + encoded_len, + coeficients, + ); + + derived_overhead >= suggested_overhead + }; + + // In order to find the maximal allowed overhead we are doing binary search + while left_bound + 1 < right_bound { + let mid = (left_bound + right_bound) / 2; + + if is_overhead_accepted(mid) { + left_bound = mid; + } else { + right_bound = mid; + } + } + + if is_overhead_accepted(right_bound) { + right_bound + } else { + left_bound + } + } + + #[test] + fn test_correctness_for_efficient_overhead() { + let test_params = |total_gas_limit: u32, + gas_per_pubdata: u32, + encoded_len: usize, + coeficients: OverheadCoeficients| { + let result_by_efficient_search = + get_amortized_overhead(total_gas_limit, gas_per_pubdata, encoded_len, coeficients); + + let result_by_binary_search = get_maximal_allowed_overhead_bin_search( + total_gas_limit, + gas_per_pubdata, + encoded_len, + coeficients, + ); + + assert_eq!(result_by_efficient_search, result_by_binary_search); + }; + + // Some arbitrary test + test_params(60_000_000, 800, 2900, OverheadCoeficients::new_l2()); + + // Very small parameters + test_params(0, 1, 12, OverheadCoeficients::new_l2()); + + // Relatively big parameters + let max_tx_overhead = derive_overhead( + MAX_TX_ERGS_LIMIT, + 5000, + 10000, + OverheadCoeficients::new_l2(), + ); + test_params( + MAX_TX_ERGS_LIMIT + max_tx_overhead, + 5000, + 10000, + OverheadCoeficients::new_l2(), + ); + + test_params(115432560, 800, 2900, OverheadCoeficients::new_l1()); + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/utils/transaction_encoding.rs b/core/multivm_deps/vm_virtual_blocks/src/utils/transaction_encoding.rs new file mode 100644 index 00000000000..e911a2805d8 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/utils/transaction_encoding.rs @@ -0,0 +1,15 @@ +use crate::types::internals::TransactionData; +use zksync_types::Transaction; + +/// Extension for transactions, specific for VM. Required for bypassing the orphan rule +pub trait TransactionVmExt { + /// Get the size of the transaction in tokens. + fn bootloader_encoding_size(&self) -> usize; +} + +impl TransactionVmExt for Transaction { + fn bootloader_encoding_size(&self) -> usize { + let transaction_data: TransactionData = self.clone().into(); + transaction_data.into_tokens().len() + } +} diff --git a/core/multivm_deps/vm_virtual_blocks/src/vm.rs b/core/multivm_deps/vm_virtual_blocks/src/vm.rs new file mode 100644 index 00000000000..ee196683db3 --- /dev/null +++ b/core/multivm_deps/vm_virtual_blocks/src/vm.rs @@ -0,0 +1,158 @@ +use zksync_state::{StoragePtr, WriteStorage}; +use zksync_types::Transaction; +use zksync_utils::bytecode::CompressedBytecodeInfo; + +use crate::old_vm::events::merge_events; +use crate::old_vm::history_recorder::{HistoryEnabled, HistoryMode}; + +use crate::bootloader_state::BootloaderState; +use crate::errors::BytecodeCompressionError; +use crate::tracers::traits::VmTracer; +use crate::types::{ + inputs::{L1BatchEnv, SystemEnv, VmExecutionMode}, + internals::{new_vm_state, VmSnapshot, ZkSyncVmState}, + outputs::{BootloaderMemory, CurrentExecutionState, VmExecutionResultAndLogs}, +}; +use crate::L2BlockEnv; + +/// Main entry point for Virtual Machine integration. +/// The instance should process only one l1 batch +#[derive(Debug)] +pub struct Vm { + pub(crate) bootloader_state: BootloaderState, + // Current state and oracles of virtual machine + pub(crate) state: ZkSyncVmState, + pub(crate) storage: StoragePtr, + pub(crate) system_env: SystemEnv, + pub(crate) batch_env: L1BatchEnv, + // Snapshots for the current run + pub(crate) snapshots: Vec, + _phantom: std::marker::PhantomData, +} + +/// Public interface for VM +impl Vm { + pub fn new(batch_env: L1BatchEnv, system_env: SystemEnv, storage: StoragePtr, _: H) -> Self { + let (state, bootloader_state) = new_vm_state(storage.clone(), &system_env, &batch_env); + Self { + bootloader_state, + state, + storage, + system_env, + batch_env, + snapshots: vec![], + _phantom: Default::default(), + } + } + + /// Push tx into memory for the future execution + pub fn push_transaction(&mut self, tx: Transaction) { + self.push_transaction_with_compression(tx, true) + } + + /// Execute VM with default tracers. The execution mode determines whether the VM will stop and + /// how the vm will be processed. + pub fn execute(&mut self, execution_mode: VmExecutionMode) -> VmExecutionResultAndLogs { + self.inspect(vec![], execution_mode) + } + + /// Execute VM with custom tracers. + pub fn inspect( + &mut self, + tracers: Vec>>, + execution_mode: VmExecutionMode, + ) -> VmExecutionResultAndLogs { + self.inspect_inner(tracers, execution_mode) + } + + /// Get current state of bootloader memory. + pub fn get_bootloader_memory(&self) -> BootloaderMemory { + self.bootloader_state.bootloader_memory() + } + + /// Get compressed bytecodes of the last executed transaction + pub fn get_last_tx_compressed_bytecodes(&self) -> Vec { + self.bootloader_state.get_last_tx_compressed_bytecodes() + } + + pub fn start_new_l2_block(&mut self, l2_block_env: L2BlockEnv) { + self.bootloader_state.start_new_l2_block(l2_block_env); + } + + /// Get current state of virtual machine. + /// This method should be used only after the batch execution. + /// Otherwise it can panic. + pub fn get_current_execution_state(&self) -> CurrentExecutionState { + let (_full_history, raw_events, l1_messages) = self.state.event_sink.flatten(); + let events = merge_events(raw_events) + .into_iter() + .map(|e| e.into_vm_event(self.batch_env.number)) + .collect(); + let l2_to_l1_logs = l1_messages.into_iter().map(|log| log.into()).collect(); + let total_log_queries = self.state.event_sink.get_log_queries() + + self + .state + .precompiles_processor + .get_timestamp_history() + .len() + + self.state.storage.get_final_log_queries().len(); + + CurrentExecutionState { + events, + storage_log_queries: self.state.storage.get_final_log_queries(), + used_contract_hashes: self.get_used_contracts(), + l2_to_l1_logs, + total_log_queries, + cycles_used: self.state.local_state.monotonic_cycle_counter, + } + } + + /// Execute transaction with optional bytecode compression. + pub fn execute_transaction_with_bytecode_compression( + &mut self, + tx: Transaction, + with_compression: bool, + ) -> Result { + self.inspect_transaction_with_bytecode_compression(vec![], tx, with_compression) + } + + /// Inspect transaction with optional bytecode compression. + pub fn inspect_transaction_with_bytecode_compression( + &mut self, + tracers: Vec>>, + tx: Transaction, + with_compression: bool, + ) -> Result { + self.push_transaction_with_compression(tx, with_compression); + let result = self.inspect(tracers, VmExecutionMode::OneTx); + if self.has_unpublished_bytecodes() { + Err(BytecodeCompressionError::BytecodeCompressionFailed) + } else { + Ok(result) + } + } +} + +/// Methods of vm, which required some history manipullations +impl Vm { + /// Create snapshot of current vm state and push it into the memory + pub fn make_snapshot(&mut self) { + self.make_snapshot_inner() + } + + /// Rollback vm state to the latest snapshot and destroy the snapshot + pub fn rollback_to_the_latest_snapshot(&mut self) { + let snapshot = self + .snapshots + .pop() + .expect("Snapshot should be created before rolling it back"); + self.rollback_to_snapshot(snapshot); + } + + /// Pop the latest snapshot from the memory and destroy it + pub fn pop_snapshot_no_rollback(&mut self) { + self.snapshots + .pop() + .expect("Snapshot should be created before rolling it back"); + } +}