From bfe2e550fb07e00a353250dea73a2b71f03d86fa Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Mon, 23 Sep 2024 12:58:07 +0300 Subject: [PATCH] =?UTF-8?q?feat(vm):=20Extract=20oneshot=20VM=20executor?= =?UTF-8?q?=20=E2=80=93=20environment=20types=20(#2885)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ Continues oneshot VM executor extraction by moving generic environment types to the `zksync_vm_executor` crate. ## Why ❔ Makes it possible to use the VM executor outside the API server. E.g., it's now used in `zksync_node_consensus`. ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [x] Tests for the changes have been added / updated. - [x] Documentation comments have been added / updated. - [x] Code has been formatted via `zk fmt` and `zk lint`. --- Cargo.lock | 2 +- core/bin/external_node/src/node_builder.rs | 4 +- core/bin/zksync_server/src/node_builder.rs | 3 +- .../vm_executor/src/oneshot/block.rs} | 420 +++++++++--------- core/lib/vm_executor/src/oneshot/contracts.rs | 91 ++++ core/lib/vm_executor/src/oneshot/env.rs | 138 ++++++ core/lib/vm_executor/src/oneshot/mod.rs | 15 +- .../src/execution_sandbox/execute.rs | 213 +++++++-- .../api_server/src/execution_sandbox/mod.rs | 126 ++---- .../api_server/src/execution_sandbox/tests.rs | 129 +++--- .../src/execution_sandbox/validate.rs | 30 +- core/node/api_server/src/tx_sender/mod.rs | 318 ++++--------- core/node/api_server/src/tx_sender/tests.rs | 10 +- .../api_server/src/web3/namespaces/debug.rs | 66 +-- core/node/api_server/src/web3/testonly.rs | 46 +- core/node/api_server/src/web3/tests/vm.rs | 109 ++++- core/node/consensus/Cargo.toml | 3 +- core/node/consensus/src/storage/connection.rs | 33 +- core/node/consensus/src/vm.rs | 83 ++-- core/node/fee_model/src/lib.rs | 28 +- .../layers/web3_api/tx_sender.rs | 20 +- 21 files changed, 1079 insertions(+), 808 deletions(-) rename core/{node/api_server/src/execution_sandbox/apply.rs => lib/vm_executor/src/oneshot/block.rs} (56%) create mode 100644 core/lib/vm_executor/src/oneshot/contracts.rs create mode 100644 core/lib/vm_executor/src/oneshot/env.rs diff --git a/Cargo.lock b/Cargo.lock index 8164d412af55..60ed1b3f15fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10225,7 +10225,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "hex", "rand 0.8.5", "secrecy", "semver", @@ -10259,6 +10258,7 @@ dependencies = [ "zksync_test_account", "zksync_types", "zksync_utils", + "zksync_vm_executor", "zksync_vm_interface", "zksync_web3_decl", ] diff --git a/core/bin/external_node/src/node_builder.rs b/core/bin/external_node/src/node_builder.rs index 98e286c253a2..d0055896d42e 100644 --- a/core/bin/external_node/src/node_builder.rs +++ b/core/bin/external_node/src/node_builder.rs @@ -12,7 +12,7 @@ use zksync_config::{ PostgresConfig, }; use zksync_metadata_calculator::{MetadataCalculatorConfig, MetadataCalculatorRecoveryConfig}; -use zksync_node_api_server::{tx_sender::ApiContracts, web3::Namespace}; +use zksync_node_api_server::web3::Namespace; use zksync_node_framework::{ implementations::layers::{ batch_status_updater::BatchStatusUpdaterLayer, @@ -380,12 +380,10 @@ impl ExternalNodeBuilder { latest_values_cache_size: self.config.optional.latest_values_cache_size() as u64, }; let max_vm_concurrency = self.config.optional.vm_concurrency_limit; - let api_contracts = ApiContracts::load_from_disk_blocking(); // TODO (BFT-138): Allow to dynamically reload API contracts; let tx_sender_layer = TxSenderLayer::new( (&self.config).into(), postgres_storage_config, max_vm_concurrency, - api_contracts, ) .with_whitelisted_tokens_for_aa_cache(true); diff --git a/core/bin/zksync_server/src/node_builder.rs b/core/bin/zksync_server/src/node_builder.rs index 069a7a799ab5..14db83b9f25a 100644 --- a/core/bin/zksync_server/src/node_builder.rs +++ b/core/bin/zksync_server/src/node_builder.rs @@ -12,7 +12,7 @@ use zksync_config::{ use zksync_core_leftovers::Component; use zksync_metadata_calculator::MetadataCalculatorConfig; use zksync_node_api_server::{ - tx_sender::{ApiContracts, TxSenderConfig}, + tx_sender::TxSenderConfig, web3::{state::InternalApiConfig, Namespace}, }; use zksync_node_framework::{ @@ -322,7 +322,6 @@ impl MainNodeBuilder { ), postgres_storage_caches_config, rpc_config.vm_concurrency_limit(), - ApiContracts::load_from_disk_blocking(), // TODO (BFT-138): Allow to dynamically reload API contracts )); Ok(self) } diff --git a/core/node/api_server/src/execution_sandbox/apply.rs b/core/lib/vm_executor/src/oneshot/block.rs similarity index 56% rename from core/node/api_server/src/execution_sandbox/apply.rs rename to core/lib/vm_executor/src/oneshot/block.rs index 0fbf8abc3dd4..8ba77305ad7d 100644 --- a/core/node/api_server/src/execution_sandbox/apply.rs +++ b/core/lib/vm_executor/src/oneshot/block.rs @@ -1,89 +1,236 @@ -//! This module provides primitives focusing on the VM instantiation and execution for different use cases. -//! It is rather generic and low-level, so it's not supposed to be a part of public API. -//! -//! Instead, we expect people to write wrappers in the `execution_sandbox` module with a more high-level API -//! that would, in its turn, be used by the actual API method handlers. -//! -//! This module is intended to be blocking. - -use std::time::{Duration, Instant}; - -use anyhow::Context as _; -use tokio::runtime::Handle; +use anyhow::Context; use zksync_dal::{Connection, Core, CoreDal, DalError}; use zksync_multivm::{ - interface::{L1BatchEnv, L2BlockEnv, OneshotEnv, StoredL2BlockEnv, SystemEnv}, - utils::get_eth_call_gas_limit, + interface::{L1BatchEnv, L2BlockEnv, OneshotEnv, StoredL2BlockEnv, SystemEnv, TxExecutionMode}, vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT, }; -use zksync_state::PostgresStorage; -use zksync_system_constants::{ - SYSTEM_CONTEXT_ADDRESS, SYSTEM_CONTEXT_CURRENT_L2_BLOCK_INFO_POSITION, - SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION, ZKPORTER_IS_AVAILABLE, -}; use zksync_types::{ api, block::{unpack_block_info, L2BlockHasher}, fee_model::BatchFeeInput, - AccountTreeId, L1BatchNumber, L2BlockNumber, ProtocolVersionId, StorageKey, H256, U256, + AccountTreeId, L1BatchNumber, L2BlockNumber, ProtocolVersionId, StorageKey, H256, + SYSTEM_CONTEXT_ADDRESS, SYSTEM_CONTEXT_CURRENT_L2_BLOCK_INFO_POSITION, + SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION, ZKPORTER_IS_AVAILABLE, }; use zksync_utils::{h256_to_u256, time::seconds_since_epoch}; -use super::{ - vm_metrics::{SandboxStage, SANDBOX_METRICS}, - BlockArgs, TxSetupArgs, -}; +use super::env::OneshotEnvParameters; -pub(super) async fn prepare_env_and_storage( - mut connection: Connection<'static, Core>, - setup_args: TxSetupArgs, - block_args: &BlockArgs, -) -> anyhow::Result<(OneshotEnv, PostgresStorage<'static>)> { - let initialization_stage = SANDBOX_METRICS.sandbox[&SandboxStage::Initialization].start(); +/// Block information necessary to execute a transaction / call. Unlike [`ResolvedBlockInfo`], this information is *partially* resolved, +/// which is beneficial for some data workflows. +#[derive(Debug, Clone, Copy)] +pub struct BlockInfo { + resolved_block_number: L2BlockNumber, + l1_batch_timestamp_s: Option, +} - let resolve_started_at = Instant::now(); - let resolved_block_info = block_args - .resolve_block_info(&mut connection) - .await - .with_context(|| format!("cannot resolve block numbers for {block_args:?}"))?; - let resolve_time = resolve_started_at.elapsed(); - // We don't want to emit too many logs. - if resolve_time > Duration::from_millis(10) { - tracing::debug!("Resolved block numbers (took {resolve_time:?})"); +impl BlockInfo { + /// Fetches information for a pending block. + pub async fn pending(connection: &mut Connection<'_, Core>) -> anyhow::Result { + let resolved_block_number = connection + .blocks_web3_dal() + .resolve_block_id(api::BlockId::Number(api::BlockNumber::Pending)) + .await + .map_err(DalError::generalize)? + .context("pending block should always be present in Postgres")?; + Ok(Self { + resolved_block_number, + l1_batch_timestamp_s: None, + }) } - if block_args.resolves_to_latest_sealed_l2_block() { - setup_args - .caches - .schedule_values_update(resolved_block_info.state_l2_block_number); + /// Fetches information for an existing block. Will error if the block is not present in Postgres. + pub async fn for_existing_block( + connection: &mut Connection<'_, Core>, + number: L2BlockNumber, + ) -> anyhow::Result { + let l1_batch = connection + .storage_web3_dal() + .resolve_l1_batch_number_of_l2_block(number) + .await + .with_context(|| format!("failed resolving L1 batch number of L2 block #{number}"))?; + let l1_batch_timestamp = connection + .blocks_web3_dal() + .get_expected_l1_batch_timestamp(&l1_batch) + .await + .map_err(DalError::generalize)? + .context("missing timestamp for non-pending block")?; + Ok(Self { + resolved_block_number: number, + l1_batch_timestamp_s: Some(l1_batch_timestamp), + }) } - let (next_block, current_block) = load_l2_block_info( - &mut connection, - block_args.is_pending_l2_block(), - &resolved_block_info, - ) - .await?; + /// Returns L2 block number. + pub fn block_number(&self) -> L2BlockNumber { + self.resolved_block_number + } - let storage = PostgresStorage::new_async( - Handle::current(), - connection, - resolved_block_info.state_l2_block_number, - false, - ) - .await - .context("cannot create `PostgresStorage`")? - .with_caches(setup_args.caches.clone()); + /// Returns L1 batch timestamp in seconds, or `None` for a pending block. + pub fn l1_batch_timestamp(&self) -> Option { + self.l1_batch_timestamp_s + } - let (system, l1_batch) = prepare_env(setup_args, &resolved_block_info, next_block); + fn is_pending_l2_block(&self) -> bool { + self.l1_batch_timestamp_s.is_none() + } - let env = OneshotEnv { - system, - l1_batch, - current_block, - }; - initialization_stage.observe(); - Ok((env, storage)) + /// Loads historical fee input for this block. If the block is not present in the DB, returns an error. + pub async fn historical_fee_input( + &self, + connection: &mut Connection<'_, Core>, + ) -> anyhow::Result { + let header = connection + .blocks_dal() + .get_l2_block_header(self.resolved_block_number) + .await? + .context("resolved L2 block is not in storage")?; + Ok(header.batch_fee_input) + } + + /// Resolves this information into [`ResolvedBlockInfo`]. + pub async fn resolve( + &self, + connection: &mut Connection<'_, Core>, + ) -> anyhow::Result { + let (state_l2_block_number, vm_l1_batch_number, l1_batch_timestamp); + + let l2_block_header = if let Some(l1_batch_timestamp_s) = self.l1_batch_timestamp_s { + vm_l1_batch_number = connection + .storage_web3_dal() + .resolve_l1_batch_number_of_l2_block(self.resolved_block_number) + .await + .context("failed resolving L1 batch for L2 block")? + .expected_l1_batch(); + l1_batch_timestamp = l1_batch_timestamp_s; + state_l2_block_number = self.resolved_block_number; + + connection + .blocks_dal() + .get_l2_block_header(self.resolved_block_number) + .await? + .context("resolved L2 block disappeared from storage")? + } else { + vm_l1_batch_number = connection + .blocks_dal() + .get_sealed_l1_batch_number() + .await? + .context("no L1 batches in storage")?; + let sealed_l2_block_header = connection + .blocks_dal() + .get_last_sealed_l2_block_header() + .await? + .context("no L2 blocks in storage")?; + + state_l2_block_number = sealed_l2_block_header.number; + // Timestamp of the next L1 batch must be greater than the timestamp of the last L2 block. + l1_batch_timestamp = seconds_since_epoch().max(sealed_l2_block_header.timestamp + 1); + sealed_l2_block_header + }; + + // Blocks without version specified are considered to be of `Version9`. + // TODO: remove `unwrap_or` when protocol version ID will be assigned for each block. + let protocol_version = l2_block_header + .protocol_version + .unwrap_or(ProtocolVersionId::last_potentially_undefined()); + + Ok(ResolvedBlockInfo { + state_l2_block_number, + state_l2_block_hash: l2_block_header.hash, + vm_l1_batch_number, + l1_batch_timestamp, + protocol_version, + is_pending: self.is_pending_l2_block(), + }) + } +} + +/// Resolved [`BlockInfo`] containing additional data from VM state. +#[derive(Debug)] +pub struct ResolvedBlockInfo { + state_l2_block_number: L2BlockNumber, + state_l2_block_hash: H256, + vm_l1_batch_number: L1BatchNumber, + l1_batch_timestamp: u64, + protocol_version: ProtocolVersionId, + is_pending: bool, +} + +impl ResolvedBlockInfo { + /// L2 block number (as stored in Postgres). This number may differ from `block.number` provided to the VM. + pub fn state_l2_block_number(&self) -> L2BlockNumber { + self.state_l2_block_number + } +} + +impl OneshotEnvParameters { + pub(super) async fn to_env_inner( + &self, + connection: &mut Connection<'_, Core>, + execution_mode: TxExecutionMode, + resolved_block_info: &ResolvedBlockInfo, + fee_input: BatchFeeInput, + enforced_base_fee: Option, + ) -> anyhow::Result { + let (next_block, current_block) = load_l2_block_info( + connection, + resolved_block_info.is_pending, + resolved_block_info, + ) + .await?; + + let (system, l1_batch) = self.prepare_env( + execution_mode, + resolved_block_info, + next_block, + fee_input, + enforced_base_fee, + ); + Ok(OneshotEnv { + system, + l1_batch, + current_block, + }) + } + + fn prepare_env( + &self, + execution_mode: TxExecutionMode, + resolved_block_info: &ResolvedBlockInfo, + next_block: L2BlockEnv, + fee_input: BatchFeeInput, + enforced_base_fee: Option, + ) -> (SystemEnv, L1BatchEnv) { + let &Self { + operator_account, + validation_computational_gas_limit, + chain_id, + .. + } = self; + + let system_env = SystemEnv { + zk_porter_available: ZKPORTER_IS_AVAILABLE, + version: resolved_block_info.protocol_version, + base_system_smart_contracts: self + .base_system_contracts + .get_by_protocol_version(resolved_block_info.protocol_version) + .clone(), + bootloader_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, + execution_mode, + default_validation_computational_gas_limit: validation_computational_gas_limit, + chain_id, + }; + let l1_batch_env = L1BatchEnv { + previous_batch_hash: None, + number: resolved_block_info.vm_l1_batch_number, + timestamp: resolved_block_info.l1_batch_timestamp, + fee_input, + fee_account: *operator_account.address(), + enforced_base_fee, + first_l2_block: next_block, + }; + (system_env, l1_batch_env) + } } async fn load_l2_block_info( @@ -155,48 +302,6 @@ async fn load_l2_block_info( Ok((next_block, current_block)) } -fn prepare_env( - setup_args: TxSetupArgs, - resolved_block_info: &ResolvedBlockInfo, - next_block: L2BlockEnv, -) -> (SystemEnv, L1BatchEnv) { - let TxSetupArgs { - execution_mode, - operator_account, - fee_input, - base_system_contracts, - validation_computational_gas_limit, - chain_id, - enforced_base_fee, - .. - } = setup_args; - - // In case we are executing in a past block, we'll use the historical fee data. - let fee_input = resolved_block_info - .historical_fee_input - .unwrap_or(fee_input); - let system_env = SystemEnv { - zk_porter_available: ZKPORTER_IS_AVAILABLE, - version: resolved_block_info.protocol_version, - base_system_smart_contracts: base_system_contracts - .get_by_protocol_version(resolved_block_info.protocol_version), - bootloader_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, - execution_mode, - default_validation_computational_gas_limit: validation_computational_gas_limit, - chain_id, - }; - let l1_batch_env = L1BatchEnv { - previous_batch_hash: None, - number: resolved_block_info.vm_l1_batch_number, - timestamp: resolved_block_info.l1_batch_timestamp, - fee_input, - fee_account: *operator_account.address(), - enforced_base_fee, - first_l2_block: next_block, - }; - (system_env, l1_batch_env) -} - async fn read_stored_l2_block( connection: &mut Connection<'_, Core>, l2_block_number: L2BlockNumber, @@ -226,102 +331,3 @@ async fn read_stored_l2_block( txs_rolling_hash, }) } - -#[derive(Debug)] -pub(crate) struct ResolvedBlockInfo { - state_l2_block_number: L2BlockNumber, - state_l2_block_hash: H256, - vm_l1_batch_number: L1BatchNumber, - l1_batch_timestamp: u64, - pub(crate) protocol_version: ProtocolVersionId, - historical_fee_input: Option, -} - -impl BlockArgs { - fn is_pending_l2_block(&self) -> bool { - matches!( - self.block_id, - api::BlockId::Number(api::BlockNumber::Pending) - ) - } - - pub(crate) async fn default_eth_call_gas( - &self, - connection: &mut Connection<'_, Core>, - ) -> anyhow::Result { - let protocol_version = self - .resolve_block_info(connection) - .await - .context("failed to resolve block info")? - .protocol_version; - Ok(get_eth_call_gas_limit(protocol_version.into()).into()) - } - - async fn resolve_block_info( - &self, - connection: &mut Connection<'_, Core>, - ) -> anyhow::Result { - let (state_l2_block_number, vm_l1_batch_number, l1_batch_timestamp); - - let l2_block_header = if self.is_pending_l2_block() { - vm_l1_batch_number = connection - .blocks_dal() - .get_sealed_l1_batch_number() - .await? - .context("no L1 batches in storage")?; - let sealed_l2_block_header = connection - .blocks_dal() - .get_last_sealed_l2_block_header() - .await? - .context("no L2 blocks in storage")?; - - state_l2_block_number = sealed_l2_block_header.number; - // Timestamp of the next L1 batch must be greater than the timestamp of the last L2 block. - l1_batch_timestamp = seconds_since_epoch().max(sealed_l2_block_header.timestamp + 1); - sealed_l2_block_header - } else { - vm_l1_batch_number = connection - .storage_web3_dal() - .resolve_l1_batch_number_of_l2_block(self.resolved_block_number) - .await - .context("failed resolving L1 batch for L2 block")? - .expected_l1_batch(); - l1_batch_timestamp = self - .l1_batch_timestamp_s - .context("L1 batch timestamp is `None` for non-pending block args")?; - state_l2_block_number = self.resolved_block_number; - - connection - .blocks_dal() - .get_l2_block_header(self.resolved_block_number) - .await? - .context("resolved L2 block disappeared from storage")? - }; - - let historical_fee_input = if !self.resolves_to_latest_sealed_l2_block() { - let l2_block_header = connection - .blocks_dal() - .get_l2_block_header(self.resolved_block_number) - .await? - .context("resolved L2 block is not in storage")?; - Some(l2_block_header.batch_fee_input) - } else { - None - }; - - // Blocks without version specified are considered to be of `Version9`. - // TODO: remove `unwrap_or` when protocol version ID will be assigned for each block. - let protocol_version = l2_block_header - .protocol_version - .unwrap_or(ProtocolVersionId::last_potentially_undefined()); - - Ok(ResolvedBlockInfo { - state_l2_block_number, - state_l2_block_hash: l2_block_header.hash, - vm_l1_batch_number, - l1_batch_timestamp, - protocol_version, - historical_fee_input, - }) - } -} diff --git a/core/lib/vm_executor/src/oneshot/contracts.rs b/core/lib/vm_executor/src/oneshot/contracts.rs new file mode 100644 index 000000000000..3b3a65fe30ba --- /dev/null +++ b/core/lib/vm_executor/src/oneshot/contracts.rs @@ -0,0 +1,91 @@ +use zksync_contracts::BaseSystemContracts; +use zksync_types::ProtocolVersionId; + +/// System contracts (bootloader and default account abstraction) for all supported VM versions. +#[derive(Debug, Clone)] +pub(super) struct MultiVMBaseSystemContracts { + /// Contracts to be used for pre-virtual-blocks protocol versions. + pre_virtual_blocks: BaseSystemContracts, + /// Contracts to be used for post-virtual-blocks protocol versions. + post_virtual_blocks: BaseSystemContracts, + /// Contracts to be used for protocol versions after virtual block upgrade fix. + post_virtual_blocks_finish_upgrade_fix: BaseSystemContracts, + /// Contracts to be used for post-boojum protocol versions. + post_boojum: BaseSystemContracts, + /// Contracts to be used after the allow-list removal upgrade + post_allowlist_removal: BaseSystemContracts, + /// Contracts to be used after the 1.4.1 upgrade + post_1_4_1: BaseSystemContracts, + /// Contracts to be used after the 1.4.2 upgrade + post_1_4_2: BaseSystemContracts, + /// Contracts to be used during the `v23` upgrade. This upgrade was done on an internal staging environment only. + vm_1_5_0_small_memory: BaseSystemContracts, + /// Contracts to be used after the 1.5.0 upgrade + vm_1_5_0_increased_memory: BaseSystemContracts, +} + +impl MultiVMBaseSystemContracts { + /// Gets contracts for a certain version. + pub fn get_by_protocol_version(&self, version: ProtocolVersionId) -> &BaseSystemContracts { + match version { + ProtocolVersionId::Version0 + | ProtocolVersionId::Version1 + | ProtocolVersionId::Version2 + | ProtocolVersionId::Version3 + | ProtocolVersionId::Version4 + | ProtocolVersionId::Version5 + | ProtocolVersionId::Version6 + | ProtocolVersionId::Version7 + | ProtocolVersionId::Version8 + | ProtocolVersionId::Version9 + | ProtocolVersionId::Version10 + | ProtocolVersionId::Version11 + | ProtocolVersionId::Version12 => &self.pre_virtual_blocks, + ProtocolVersionId::Version13 => &self.post_virtual_blocks, + ProtocolVersionId::Version14 + | ProtocolVersionId::Version15 + | ProtocolVersionId::Version16 + | ProtocolVersionId::Version17 => &self.post_virtual_blocks_finish_upgrade_fix, + ProtocolVersionId::Version18 => &self.post_boojum, + ProtocolVersionId::Version19 => &self.post_allowlist_removal, + ProtocolVersionId::Version20 => &self.post_1_4_1, + ProtocolVersionId::Version21 | ProtocolVersionId::Version22 => &self.post_1_4_2, + ProtocolVersionId::Version23 => &self.vm_1_5_0_small_memory, + ProtocolVersionId::Version24 | ProtocolVersionId::Version25 => { + &self.vm_1_5_0_increased_memory + } + } + } + + pub(super) fn load_estimate_gas_blocking() -> Self { + Self { + pre_virtual_blocks: BaseSystemContracts::estimate_gas_pre_virtual_blocks(), + post_virtual_blocks: BaseSystemContracts::estimate_gas_post_virtual_blocks(), + post_virtual_blocks_finish_upgrade_fix: + BaseSystemContracts::estimate_gas_post_virtual_blocks_finish_upgrade_fix(), + post_boojum: BaseSystemContracts::estimate_gas_post_boojum(), + post_allowlist_removal: BaseSystemContracts::estimate_gas_post_allowlist_removal(), + post_1_4_1: BaseSystemContracts::estimate_gas_post_1_4_1(), + post_1_4_2: BaseSystemContracts::estimate_gas_post_1_4_2(), + vm_1_5_0_small_memory: BaseSystemContracts::estimate_gas_1_5_0_small_memory(), + vm_1_5_0_increased_memory: + BaseSystemContracts::estimate_gas_post_1_5_0_increased_memory(), + } + } + + pub(super) fn load_eth_call_blocking() -> Self { + Self { + pre_virtual_blocks: BaseSystemContracts::playground_pre_virtual_blocks(), + post_virtual_blocks: BaseSystemContracts::playground_post_virtual_blocks(), + post_virtual_blocks_finish_upgrade_fix: + BaseSystemContracts::playground_post_virtual_blocks_finish_upgrade_fix(), + post_boojum: BaseSystemContracts::playground_post_boojum(), + post_allowlist_removal: BaseSystemContracts::playground_post_allowlist_removal(), + post_1_4_1: BaseSystemContracts::playground_post_1_4_1(), + post_1_4_2: BaseSystemContracts::playground_post_1_4_2(), + vm_1_5_0_small_memory: BaseSystemContracts::playground_1_5_0_small_memory(), + vm_1_5_0_increased_memory: BaseSystemContracts::playground_post_1_5_0_increased_memory( + ), + } + } +} diff --git a/core/lib/vm_executor/src/oneshot/env.rs b/core/lib/vm_executor/src/oneshot/env.rs new file mode 100644 index 000000000000..51154d561ec6 --- /dev/null +++ b/core/lib/vm_executor/src/oneshot/env.rs @@ -0,0 +1,138 @@ +use std::marker::PhantomData; + +use anyhow::Context; +use zksync_dal::{Connection, Core}; +use zksync_multivm::interface::{OneshotEnv, TxExecutionMode}; +use zksync_types::{fee_model::BatchFeeInput, l2::L2Tx, AccountTreeId, L2ChainId}; + +use crate::oneshot::{contracts::MultiVMBaseSystemContracts, ResolvedBlockInfo}; + +/// Marker for [`OneshotEnvParameters`] used for gas estimation. +#[derive(Debug)] +pub struct EstimateGas(()); + +/// Marker for [`OneshotEnvParameters`] used for calls and/or transaction execution. +#[derive(Debug)] +pub struct CallOrExecute(()); + +/// Oneshot environment parameters that are expected to be constant or rarely change during the program lifetime. +/// These parameters can be used to create [a full environment](OneshotEnv) for transaction / call execution. +/// +/// Notably, these parameters include base system contracts (bootloader and default account abstraction) for all supported +/// VM versions. +#[derive(Debug)] +pub struct OneshotEnvParameters { + pub(super) chain_id: L2ChainId, + pub(super) base_system_contracts: MultiVMBaseSystemContracts, + pub(super) operator_account: AccountTreeId, + pub(super) validation_computational_gas_limit: u32, + _ty: PhantomData, +} + +impl OneshotEnvParameters { + /// Returns gas limit for account validation of transactions. + pub fn validation_computational_gas_limit(&self) -> u32 { + self.validation_computational_gas_limit + } +} + +impl OneshotEnvParameters { + /// Creates env parameters for gas estimation. + /// + /// System contracts (mainly, bootloader) for these params are tuned to provide accurate + /// execution metrics. + pub async fn for_gas_estimation( + chain_id: L2ChainId, + operator_account: AccountTreeId, + ) -> anyhow::Result { + Ok(Self { + chain_id, + base_system_contracts: tokio::task::spawn_blocking( + MultiVMBaseSystemContracts::load_estimate_gas_blocking, + ) + .await + .context("failed loading system contracts for gas estimation")?, + operator_account, + validation_computational_gas_limit: u32::MAX, + _ty: PhantomData, + }) + } + + /// Prepares environment for gas estimation. + pub async fn to_env( + &self, + connection: &mut Connection<'_, Core>, + resolved_block_info: &ResolvedBlockInfo, + fee_input: BatchFeeInput, + base_fee: u64, + ) -> anyhow::Result { + self.to_env_inner( + connection, + TxExecutionMode::EstimateFee, + resolved_block_info, + fee_input, + Some(base_fee), + ) + .await + } +} + +impl OneshotEnvParameters { + /// Creates env parameters for transaction / call execution. + /// + /// System contracts (mainly, bootloader) for these params tuned to provide better UX + /// experience (e.g. revert messages). + pub async fn for_execution( + chain_id: L2ChainId, + operator_account: AccountTreeId, + validation_computational_gas_limit: u32, + ) -> anyhow::Result { + Ok(Self { + chain_id, + base_system_contracts: tokio::task::spawn_blocking( + MultiVMBaseSystemContracts::load_eth_call_blocking, + ) + .await + .context("failed loading system contracts for calls")?, + operator_account, + validation_computational_gas_limit, + _ty: PhantomData, + }) + } + + /// Prepares environment for a call. + pub async fn to_call_env( + &self, + connection: &mut Connection<'_, Core>, + resolved_block_info: &ResolvedBlockInfo, + fee_input: BatchFeeInput, + enforced_base_fee: Option, + ) -> anyhow::Result { + self.to_env_inner( + connection, + TxExecutionMode::EthCall, + resolved_block_info, + fee_input, + enforced_base_fee, + ) + .await + } + + /// Prepares environment for executing a provided transaction. + pub async fn to_execute_env( + &self, + connection: &mut Connection<'_, Core>, + resolved_block_info: &ResolvedBlockInfo, + fee_input: BatchFeeInput, + tx: &L2Tx, + ) -> anyhow::Result { + self.to_env_inner( + connection, + TxExecutionMode::VerifyExecute, + resolved_block_info, + fee_input, + Some(tx.common_data.fee.max_fee_per_gas.as_u64()), + ) + .await + } +} diff --git a/core/lib/vm_executor/src/oneshot/mod.rs b/core/lib/vm_executor/src/oneshot/mod.rs index 1838381d2a01..11a7252dc7f6 100644 --- a/core/lib/vm_executor/src/oneshot/mod.rs +++ b/core/lib/vm_executor/src/oneshot/mod.rs @@ -1,4 +1,10 @@ //! Oneshot VM executor. +//! +//! # Overview +//! +//! The root type of this module is [`MainOneshotExecutor`], a "default" [`OneshotExecutor`] implementation. +//! In addition to it, the module provides [`OneshotEnvParameters`] and [`BlockInfo`] / [`ResolvedBlockInfo`], +//! which can be used to prepare environment for `MainOneshotExecutor` (i.e., a [`OneshotEnv`] instance). use std::{ sync::Arc, @@ -32,8 +38,15 @@ use zksync_types::{ }; use zksync_utils::{h256_to_u256, u256_to_h256}; -pub use self::mock::MockOneshotExecutor; +pub use self::{ + block::{BlockInfo, ResolvedBlockInfo}, + env::{CallOrExecute, EstimateGas, OneshotEnvParameters}, + mock::MockOneshotExecutor, +}; +mod block; +mod contracts; +mod env; mod metrics; mod mock; diff --git a/core/node/api_server/src/execution_sandbox/execute.rs b/core/node/api_server/src/execution_sandbox/execute.rs index d22d7de47d0f..d974f2e9aa1d 100644 --- a/core/node/api_server/src/execution_sandbox/execute.rs +++ b/core/node/api_server/src/execution_sandbox/execute.rs @@ -1,6 +1,10 @@ //! Implementation of "executing" methods, e.g. `eth_call`. +use std::time::{Duration, Instant}; + +use anyhow::Context as _; use async_trait::async_trait; +use tokio::runtime::Handle; use zksync_dal::{Connection, Core}; use zksync_multivm::interface::{ executor::{OneshotExecutor, TransactionValidator}, @@ -9,17 +13,71 @@ use zksync_multivm::interface::{ Call, OneshotEnv, OneshotTracingParams, OneshotTransactionExecutionResult, TransactionExecutionMetrics, TxExecutionArgs, VmExecutionResultAndLogs, }; -use zksync_types::{api::state_override::StateOverride, l2::L2Tx}; +use zksync_state::{PostgresStorage, PostgresStorageCaches}; +use zksync_types::{ + api::state_override::StateOverride, fee_model::BatchFeeInput, l2::L2Tx, Transaction, +}; use zksync_vm_executor::oneshot::{MainOneshotExecutor, MockOneshotExecutor}; use super::{ - apply, storage::StorageWithOverrides, vm_metrics, BlockArgs, TxSetupArgs, VmPermit, - SANDBOX_METRICS, + storage::StorageWithOverrides, + vm_metrics::{self, SandboxStage}, + BlockArgs, VmPermit, SANDBOX_METRICS, }; -use crate::execution_sandbox::vm_metrics::SandboxStage; +use crate::tx_sender::SandboxExecutorOptions; + +/// Action that can be executed by [`SandboxExecutor`]. +#[derive(Debug)] +pub(crate) enum SandboxAction { + /// Execute a transaction. + Execution { tx: L2Tx, fee_input: BatchFeeInput }, + /// Execute a call, possibly with tracing. + Call { + call: L2Tx, + fee_input: BatchFeeInput, + enforced_base_fee: Option, + tracing_params: OneshotTracingParams, + }, + /// Estimate gas for a transaction. + GasEstimation { + tx: Transaction, + fee_input: BatchFeeInput, + base_fee: u64, + }, +} + +impl SandboxAction { + fn factory_deps_count(&self) -> usize { + match self { + Self::Execution { tx, .. } | Self::Call { call: tx, .. } => { + tx.execute.factory_deps.len() + } + Self::GasEstimation { tx, .. } => tx.execute.factory_deps.len(), + } + } + fn into_parts(self) -> (TxExecutionArgs, OneshotTracingParams) { + match self { + Self::Execution { tx, .. } => ( + TxExecutionArgs::for_validation(tx), + OneshotTracingParams::default(), + ), + Self::GasEstimation { tx, .. } => ( + TxExecutionArgs::for_gas_estimate(tx), + OneshotTracingParams::default(), + ), + Self::Call { + call, + tracing_params, + .. + } => (TxExecutionArgs::for_eth_call(call), tracing_params), + } + } +} + +/// Output of [`SandboxExecutor::execute_in_sandbox()`]. #[derive(Debug, Clone)] -pub struct TransactionExecutionOutput { +pub(crate) struct SandboxExecutionOutput { /// Output of the VM. pub vm: VmExecutionResultAndLogs, /// Traced calls if requested. @@ -30,42 +88,64 @@ pub struct TransactionExecutionOutput { pub are_published_bytecodes_ok: bool, } -/// Executor of transactions. #[derive(Debug)] -pub enum TransactionExecutor { +enum SandboxExecutorEngine { Real(MainOneshotExecutor), - #[doc(hidden)] // Intended for tests only Mock(MockOneshotExecutor), } -impl TransactionExecutor { - pub fn real(missed_storage_invocation_limit: usize) -> Self { +/// Executor of transactions / calls used in the API server. +#[derive(Debug)] +pub(crate) struct SandboxExecutor { + engine: SandboxExecutorEngine, + pub(super) options: SandboxExecutorOptions, + storage_caches: Option, +} + +impl SandboxExecutor { + pub fn real( + options: SandboxExecutorOptions, + caches: PostgresStorageCaches, + missed_storage_invocation_limit: usize, + ) -> Self { let mut executor = MainOneshotExecutor::new(missed_storage_invocation_limit); executor .set_execution_latency_histogram(&SANDBOX_METRICS.sandbox[&SandboxStage::Execution]); - Self::Real(executor) + Self { + engine: SandboxExecutorEngine::Real(executor), + options, + storage_caches: Some(caches), + } + } + + pub(crate) async fn mock(executor: MockOneshotExecutor) -> Self { + Self { + engine: SandboxExecutorEngine::Mock(executor), + options: SandboxExecutorOptions::mock().await, + storage_caches: None, + } } /// This method assumes that (block with number `resolved_block_number` is present in DB) /// or (`block_id` is `pending` and block with number `resolved_block_number - 1` is present in DB) #[allow(clippy::too_many_arguments)] #[tracing::instrument(level = "debug", skip_all)] - pub async fn execute_tx_in_sandbox( + pub async fn execute_in_sandbox( &self, vm_permit: VmPermit, - setup_args: TxSetupArgs, - execution_args: TxExecutionArgs, connection: Connection<'static, Core>, - block_args: BlockArgs, + action: SandboxAction, + block_args: &BlockArgs, state_override: Option, - tracing_params: OneshotTracingParams, - ) -> anyhow::Result { - let total_factory_deps = execution_args.transaction.execute.factory_deps.len() as u16; - let (env, storage) = - apply::prepare_env_and_storage(connection, setup_args, &block_args).await?; + ) -> anyhow::Result { + let total_factory_deps = action.factory_deps_count() as u16; + let (env, storage) = self + .prepare_env_and_storage(connection, block_args, &action) + .await?; + let state_override = state_override.unwrap_or_default(); let storage = StorageWithOverrides::new(storage, &state_override); - + let (execution_args, tracing_params) = action.into_parts(); let result = self .inspect_transaction_with_bytecode_compression( storage, @@ -78,23 +158,88 @@ impl TransactionExecutor { let metrics = vm_metrics::collect_tx_execution_metrics(total_factory_deps, &result.tx_result); - Ok(TransactionExecutionOutput { + Ok(SandboxExecutionOutput { vm: *result.tx_result, call_traces: result.call_traces, metrics, are_published_bytecodes_ok: result.compression_result.is_ok(), }) } -} -impl From for TransactionExecutor { - fn from(executor: MockOneshotExecutor) -> Self { - Self::Mock(executor) + pub(super) async fn prepare_env_and_storage( + &self, + mut connection: Connection<'static, Core>, + block_args: &BlockArgs, + action: &SandboxAction, + ) -> anyhow::Result<(OneshotEnv, PostgresStorage<'static>)> { + let initialization_stage = SANDBOX_METRICS.sandbox[&SandboxStage::Initialization].start(); + let resolve_started_at = Instant::now(); + let resolve_time = resolve_started_at.elapsed(); + let resolved_block_info = block_args.inner.resolve(&mut connection).await?; + // We don't want to emit too many logs. + if resolve_time > Duration::from_millis(10) { + tracing::debug!("Resolved block numbers (took {resolve_time:?})"); + } + + let env = match action { + SandboxAction::Execution { fee_input, tx } => { + self.options + .eth_call + .to_execute_env(&mut connection, &resolved_block_info, *fee_input, tx) + .await? + } + &SandboxAction::Call { + fee_input, + enforced_base_fee, + .. + } => { + self.options + .eth_call + .to_call_env( + &mut connection, + &resolved_block_info, + fee_input, + enforced_base_fee, + ) + .await? + } + &SandboxAction::GasEstimation { + fee_input, + base_fee, + .. + } => { + self.options + .estimate_gas + .to_env(&mut connection, &resolved_block_info, fee_input, base_fee) + .await? + } + }; + + if block_args.resolves_to_latest_sealed_l2_block() { + if let Some(caches) = &self.storage_caches { + caches.schedule_values_update(resolved_block_info.state_l2_block_number()); + } + } + + let mut storage = PostgresStorage::new_async( + Handle::current(), + connection, + resolved_block_info.state_l2_block_number(), + false, + ) + .await + .context("cannot create `PostgresStorage`")?; + + if let Some(caches) = &self.storage_caches { + storage = storage.with_caches(caches.clone()); + } + initialization_stage.observe(); + Ok((env, storage)) } } #[async_trait] -impl OneshotExecutor for TransactionExecutor +impl OneshotExecutor for SandboxExecutor where S: ReadStorage + Send + 'static, { @@ -105,8 +250,8 @@ where args: TxExecutionArgs, tracing_params: OneshotTracingParams, ) -> anyhow::Result { - match self { - Self::Real(executor) => { + match &self.engine { + SandboxExecutorEngine::Real(executor) => { executor .inspect_transaction_with_bytecode_compression( storage, @@ -116,7 +261,7 @@ where ) .await } - Self::Mock(executor) => { + SandboxExecutorEngine::Mock(executor) => { executor .inspect_transaction_with_bytecode_compression( storage, @@ -131,7 +276,7 @@ where } #[async_trait] -impl TransactionValidator for TransactionExecutor +impl TransactionValidator for SandboxExecutor where S: ReadStorage + Send + 'static, { @@ -142,13 +287,13 @@ where tx: L2Tx, validation_params: ValidationParams, ) -> anyhow::Result> { - match self { - Self::Real(executor) => { + match &self.engine { + SandboxExecutorEngine::Real(executor) => { executor .validate_transaction(storage, env, tx, validation_params) .await } - Self::Mock(executor) => { + SandboxExecutorEngine::Mock(executor) => { executor .validate_transaction(storage, env, tx, validation_params) .await diff --git a/core/node/api_server/src/execution_sandbox/mod.rs b/core/node/api_server/src/execution_sandbox/mod.rs index 79c6123642cc..36f10b8e9b08 100644 --- a/core/node/api_server/src/execution_sandbox/mod.rs +++ b/core/node/api_server/src/execution_sandbox/mod.rs @@ -6,23 +6,21 @@ use std::{ use anyhow::Context as _; use rand::{thread_rng, Rng}; use zksync_dal::{pruning_dal::PruningInfo, Connection, Core, CoreDal, DalError}; -use zksync_multivm::interface::TxExecutionMode; -use zksync_state::PostgresStorageCaches; +use zksync_multivm::utils::get_eth_call_gas_limit; use zksync_types::{ - api, fee_model::BatchFeeInput, AccountTreeId, Address, L1BatchNumber, L2BlockNumber, L2ChainId, + api, fee_model::BatchFeeInput, L1BatchNumber, L2BlockNumber, ProtocolVersionId, U256, }; +use zksync_vm_executor::oneshot::BlockInfo; -pub use self::execute::TransactionExecutor; // FIXME (PLA-1018): remove use self::vm_metrics::SandboxStage; pub(super) use self::{ error::SandboxExecutionError, + execute::{SandboxAction, SandboxExecutor}, validate::ValidationError, vm_metrics::{SubmitTxStage, SANDBOX_METRICS}, }; -use super::tx_sender::MultiVMBaseSystemContracts; // Note: keep the modules private, and instead re-export functions that make public interface. -mod apply; mod error; mod execute; mod storage; @@ -136,53 +134,6 @@ impl VmConcurrencyLimiter { } } -async fn get_pending_state( - connection: &mut Connection<'_, Core>, -) -> anyhow::Result<(api::BlockId, L2BlockNumber)> { - let block_id = api::BlockId::Number(api::BlockNumber::Pending); - let resolved_block_number = connection - .blocks_web3_dal() - .resolve_block_id(block_id) - .await - .map_err(DalError::generalize)? - .context("pending block should always be present in Postgres")?; - Ok((block_id, resolved_block_number)) -} - -/// Arguments for VM execution necessary to set up storage and environment. -#[derive(Debug, Clone)] -pub struct TxSetupArgs { - pub execution_mode: TxExecutionMode, - pub operator_account: AccountTreeId, - pub fee_input: BatchFeeInput, - pub base_system_contracts: MultiVMBaseSystemContracts, - pub caches: PostgresStorageCaches, - pub validation_computational_gas_limit: u32, - pub chain_id: L2ChainId, - pub whitelisted_tokens_for_aa: Vec
, - pub enforced_base_fee: Option, -} - -impl TxSetupArgs { - #[cfg(test)] - pub fn mock( - execution_mode: TxExecutionMode, - base_system_contracts: MultiVMBaseSystemContracts, - ) -> Self { - Self { - execution_mode, - operator_account: AccountTreeId::default(), - fee_input: BatchFeeInput::l1_pegged(55, 555), - base_system_contracts, - caches: PostgresStorageCaches::new(1, 1), - validation_computational_gas_limit: u32::MAX, - chain_id: L2ChainId::default(), - whitelisted_tokens_for_aa: vec![], - enforced_base_fee: None, - } - } -} - #[derive(Debug, Clone, Copy)] struct BlockStartInfoInner { info: PruningInfo, @@ -335,19 +286,17 @@ pub enum BlockArgsError { /// Information about a block provided to VM. #[derive(Debug, Clone, Copy)] -pub struct BlockArgs { +pub(crate) struct BlockArgs { + inner: BlockInfo, block_id: api::BlockId, - resolved_block_number: L2BlockNumber, - l1_batch_timestamp_s: Option, } impl BlockArgs { - pub(crate) async fn pending(connection: &mut Connection<'_, Core>) -> anyhow::Result { - let (block_id, resolved_block_number) = get_pending_state(connection).await?; + pub async fn pending(connection: &mut Connection<'_, Core>) -> anyhow::Result { + let inner = BlockInfo::pending(connection).await?; Ok(Self { - block_id, - resolved_block_number, - l1_batch_timestamp_s: None, + inner, + block_id: api::BlockId::Number(api::BlockNumber::Pending), }) } @@ -365,7 +314,7 @@ impl BlockArgs { .await?; if block_id == api::BlockId::Number(api::BlockNumber::Pending) { - return Ok(BlockArgs::pending(connection).await?); + return Ok(Self::pending(connection).await?); } let resolved_block_number = connection @@ -373,32 +322,25 @@ impl BlockArgs { .resolve_block_id(block_id) .await .map_err(DalError::generalize)?; - let Some(resolved_block_number) = resolved_block_number else { + let Some(block_number) = resolved_block_number else { return Err(BlockArgsError::Missing); }; - let l1_batch = connection - .storage_web3_dal() - .resolve_l1_batch_number_of_l2_block(resolved_block_number) - .await - .with_context(|| { - format!("failed resolving L1 batch number of L2 block #{resolved_block_number}") - })?; - let l1_batch_timestamp = connection - .blocks_web3_dal() - .get_expected_l1_batch_timestamp(&l1_batch) - .await - .map_err(DalError::generalize)? - .context("missing timestamp for non-pending block")?; Ok(Self { + inner: BlockInfo::for_existing_block(connection, block_number).await?, block_id, - resolved_block_number, - l1_batch_timestamp_s: Some(l1_batch_timestamp), }) } pub fn resolved_block_number(&self) -> L2BlockNumber { - self.resolved_block_number + self.inner.block_number() + } + + fn is_pending(&self) -> bool { + matches!( + self.block_id, + api::BlockId::Number(api::BlockNumber::Pending) + ) } pub fn resolves_to_latest_sealed_l2_block(&self) -> bool { @@ -409,4 +351,30 @@ impl BlockArgs { ) ) } + + pub async fn historical_fee_input( + &self, + connection: &mut Connection<'_, Core>, + ) -> anyhow::Result { + self.inner.historical_fee_input(connection).await + } + + pub async fn default_eth_call_gas( + &self, + connection: &mut Connection<'_, Core>, + ) -> anyhow::Result { + let protocol_version = if self.is_pending() { + connection.blocks_dal().pending_protocol_version().await? + } else { + let block_number = self.inner.block_number(); + connection + .blocks_dal() + .get_l2_block_header(block_number) + .await? + .with_context(|| format!("missing header for resolved block #{block_number}"))? + .protocol_version + .unwrap_or_else(ProtocolVersionId::last_potentially_undefined) + }; + Ok(get_eth_call_gas_limit(protocol_version.into()).into()) + } } diff --git a/core/node/api_server/src/execution_sandbox/tests.rs b/core/node/api_server/src/execution_sandbox/tests.rs index 79c5a7330384..f98ddc2e7965 100644 --- a/core/node/api_server/src/execution_sandbox/tests.rs +++ b/core/node/api_server/src/execution_sandbox/tests.rs @@ -5,27 +5,21 @@ use std::collections::HashMap; use assert_matches::assert_matches; use test_casing::test_casing; use zksync_dal::ConnectionPool; -use zksync_multivm::{ - interface::{ - executor::{OneshotExecutor, TransactionValidator}, - tracer::ValidationError, - Halt, OneshotTracingParams, TxExecutionArgs, - }, - utils::derive_base_fee_and_gas_per_pubdata, -}; +use zksync_multivm::{interface::ExecutionResult, utils::derive_base_fee_and_gas_per_pubdata}; use zksync_node_genesis::{insert_genesis_batch, GenesisParams}; use zksync_node_test_utils::{create_l2_block, prepare_recovery_snapshot}; +use zksync_state::PostgresStorageCaches; use zksync_types::{ api::state_override::{OverrideAccount, StateOverride}, fee::Fee, + fee_model::BatchFeeInput, l2::L2Tx, transaction_request::PaymasterParams, - K256PrivateKey, Nonce, ProtocolVersionId, Transaction, U256, + Address, K256PrivateKey, L2ChainId, Nonce, ProtocolVersionId, Transaction, U256, }; -use zksync_vm_executor::oneshot::MainOneshotExecutor; -use super::{storage::StorageWithOverrides, *}; -use crate::tx_sender::ApiContracts; +use super::*; +use crate::{execution_sandbox::execute::SandboxExecutor, tx_sender::SandboxExecutorOptions}; #[tokio::test] async fn creating_block_args() { @@ -46,8 +40,9 @@ async fn creating_block_args() { pending_block_args.block_id, api::BlockId::Number(api::BlockNumber::Pending) ); - assert_eq!(pending_block_args.resolved_block_number, L2BlockNumber(2)); - assert_eq!(pending_block_args.l1_batch_timestamp_s, None); + assert_eq!(pending_block_args.resolved_block_number(), L2BlockNumber(2)); + assert_eq!(pending_block_args.inner.l1_batch_timestamp(), None); + assert!(pending_block_args.is_pending()); let start_info = BlockStartInfo::new(&mut storage, Duration::MAX) .await @@ -66,9 +61,9 @@ async fn creating_block_args() { .await .unwrap(); assert_eq!(latest_block_args.block_id, latest_block); - assert_eq!(latest_block_args.resolved_block_number, L2BlockNumber(1)); + assert_eq!(latest_block_args.resolved_block_number(), L2BlockNumber(1)); assert_eq!( - latest_block_args.l1_batch_timestamp_s, + latest_block_args.inner.l1_batch_timestamp(), Some(l2_block.timestamp) ); @@ -77,8 +72,11 @@ async fn creating_block_args() { .await .unwrap(); assert_eq!(earliest_block_args.block_id, earliest_block); - assert_eq!(earliest_block_args.resolved_block_number, L2BlockNumber(0)); - assert_eq!(earliest_block_args.l1_batch_timestamp_s, Some(0)); + assert_eq!( + earliest_block_args.resolved_block_number(), + L2BlockNumber(0) + ); + assert_eq!(earliest_block_args.inner.l1_batch_timestamp(), Some(0)); let missing_block = api::BlockId::Number(100.into()); let err = BlockArgs::new(&mut storage, missing_block, &start_info) @@ -100,10 +98,10 @@ async fn creating_block_args_after_snapshot_recovery() { api::BlockId::Number(api::BlockNumber::Pending) ); assert_eq!( - pending_block_args.resolved_block_number, + pending_block_args.resolved_block_number(), snapshot_recovery.l2_block_number + 1 ); - assert_eq!(pending_block_args.l1_batch_timestamp_s, None); + assert!(pending_block_args.is_pending()); let start_info = BlockStartInfo::new(&mut storage, Duration::MAX) .await @@ -159,9 +157,9 @@ async fn creating_block_args_after_snapshot_recovery() { .await .unwrap(); assert_eq!(latest_block_args.block_id, latest_block); - assert_eq!(latest_block_args.resolved_block_number, l2_block.number); + assert_eq!(latest_block_args.resolved_block_number(), l2_block.number); assert_eq!( - latest_block_args.l1_batch_timestamp_s, + latest_block_args.inner.l1_batch_timestamp(), Some(l2_block.timestamp) ); @@ -203,28 +201,31 @@ async fn estimating_gas() { } async fn test_instantiating_vm(connection: Connection<'static, Core>, block_args: BlockArgs) { - let estimate_gas_contracts = ApiContracts::load_from_disk().await.unwrap().estimate_gas; - let mut setup_args = TxSetupArgs::mock(TxExecutionMode::EstimateFee, estimate_gas_contracts); - let (base_fee, gas_per_pubdata) = derive_base_fee_and_gas_per_pubdata( - setup_args.fee_input, - ProtocolVersionId::latest().into(), + let executor = SandboxExecutor::real( + SandboxExecutorOptions::mock().await, + PostgresStorageCaches::new(1, 1), + usize::MAX, ); - setup_args.enforced_base_fee = Some(base_fee); - let transaction = Transaction::from(create_transfer(base_fee, gas_per_pubdata)); - let execution_args = TxExecutionArgs::for_gas_estimate(transaction.clone()); - let (env, storage) = apply::prepare_env_and_storage(connection, setup_args, &block_args) - .await - .unwrap(); - let storage = StorageWithOverrides::new(storage, &StateOverride::default()); + let fee_input = BatchFeeInput::l1_pegged(55, 555); + let (base_fee, gas_per_pubdata) = + derive_base_fee_and_gas_per_pubdata(fee_input, ProtocolVersionId::latest().into()); + let tx = Transaction::from(create_transfer(base_fee, gas_per_pubdata)); - let tracing_params = OneshotTracingParams::default(); - let output = MainOneshotExecutor::new(usize::MAX) - .inspect_transaction_with_bytecode_compression(storage, env, execution_args, tracing_params) + let (limiter, _) = VmConcurrencyLimiter::new(1); + let vm_permit = limiter.acquire().await.unwrap(); + let action = SandboxAction::GasEstimation { + fee_input, + base_fee, + tx, + }; + let output = executor + .execute_in_sandbox(vm_permit, connection, action, &block_args, None) .await .unwrap(); - output.compression_result.unwrap(); - let tx_result = *output.tx_result; + + assert!(output.are_published_bytecodes_ok); + let tx_result = output.vm; assert!(!tx_result.result.is_failed(), "{tx_result:#?}"); } @@ -260,47 +261,47 @@ async fn validating_transaction(set_balance: bool) { let block_args = BlockArgs::pending(&mut connection).await.unwrap(); - let call_contracts = ApiContracts::load_from_disk().await.unwrap().eth_call; - let mut setup_args = TxSetupArgs::mock(TxExecutionMode::VerifyExecute, call_contracts); - let (base_fee, gas_per_pubdata) = derive_base_fee_and_gas_per_pubdata( - setup_args.fee_input, - ProtocolVersionId::latest().into(), + let executor = SandboxExecutor::real( + SandboxExecutorOptions::mock().await, + PostgresStorageCaches::new(1, 1), + usize::MAX, ); - setup_args.enforced_base_fee = Some(base_fee); - let transaction = create_transfer(base_fee, gas_per_pubdata); - let validation_params = - validate::get_validation_params(&mut connection, &transaction, u32::MAX, &[]) - .await - .unwrap(); - let (env, storage) = apply::prepare_env_and_storage(connection, setup_args, &block_args) - .await - .unwrap(); + let fee_input = BatchFeeInput::l1_pegged(55, 555); + let (base_fee, gas_per_pubdata) = + derive_base_fee_and_gas_per_pubdata(fee_input, ProtocolVersionId::latest().into()); + let tx = create_transfer(base_fee, gas_per_pubdata); + + let (limiter, _) = VmConcurrencyLimiter::new(1); + let vm_permit = limiter.acquire().await.unwrap(); let state_override = if set_balance { let account_override = OverrideAccount { balance: Some(U256::from(1) << 128), ..OverrideAccount::default() }; - StateOverride::new(HashMap::from([( - transaction.initiator_account(), - account_override, - )])) + StateOverride::new(HashMap::from([(tx.initiator_account(), account_override)])) } else { StateOverride::default() }; - let storage = StorageWithOverrides::new(storage, &state_override); - let validation_result = MainOneshotExecutor::new(usize::MAX) - .validate_transaction(storage, env, transaction, validation_params) + let result = executor + .execute_in_sandbox( + vm_permit, + connection, + SandboxAction::Execution { tx, fee_input }, + &block_args, + Some(state_override), + ) .await .unwrap(); + + let result = result.vm.result; if set_balance { - validation_result.expect("validation failed"); + assert_matches!(result, ExecutionResult::Success { .. }); } else { assert_matches!( - validation_result.unwrap_err(), - ValidationError::FailedTx(Halt::ValidationFailed(reason)) - if reason.to_string().contains("Not enough balance") + result, + ExecutionResult::Halt { reason } if reason.to_string().contains("Not enough balance") ); } } diff --git a/core/node/api_server/src/execution_sandbox/validate.rs b/core/node/api_server/src/execution_sandbox/validate.rs index e9087e608eeb..9a3c88f8bf0c 100644 --- a/core/node/api_server/src/execution_sandbox/validate.rs +++ b/core/node/api_server/src/execution_sandbox/validate.rs @@ -8,16 +8,15 @@ use zksync_multivm::interface::{ tracer::{ValidationError as RawValidationError, ValidationParams}, }; use zksync_types::{ - api::state_override::StateOverride, l2::L2Tx, Address, TRUSTED_ADDRESS_SLOTS, - TRUSTED_TOKEN_SLOTS, + api::state_override::StateOverride, fee_model::BatchFeeInput, l2::L2Tx, Address, + TRUSTED_ADDRESS_SLOTS, TRUSTED_TOKEN_SLOTS, }; use super::{ - apply, - execute::TransactionExecutor, + execute::{SandboxAction, SandboxExecutor}, storage::StorageWithOverrides, vm_metrics::{SandboxStage, EXECUTION_METRICS, SANDBOX_METRICS}, - BlockArgs, TxSetupArgs, VmPermit, + BlockArgs, VmPermit, }; /// Validation error used by the sandbox. Besides validation errors returned by VM, it also includes an internal error @@ -30,29 +29,34 @@ pub(crate) enum ValidationError { Internal(#[from] anyhow::Error), } -impl TransactionExecutor { +impl SandboxExecutor { #[tracing::instrument(level = "debug", skip_all)] pub(crate) async fn validate_tx_in_sandbox( &self, - mut connection: Connection<'static, Core>, vm_permit: VmPermit, + mut connection: Connection<'static, Core>, tx: L2Tx, - setup_args: TxSetupArgs, block_args: BlockArgs, - computational_gas_limit: u32, + fee_input: BatchFeeInput, + whitelisted_tokens_for_aa: &[Address], ) -> Result<(), ValidationError> { let total_latency = SANDBOX_METRICS.sandbox[&SandboxStage::ValidateInSandbox].start(); let validation_params = get_validation_params( &mut connection, &tx, - computational_gas_limit, - &setup_args.whitelisted_tokens_for_aa, + self.options.eth_call.validation_computational_gas_limit(), + whitelisted_tokens_for_aa, ) .await .context("failed getting validation params")?; - let (env, storage) = - apply::prepare_env_and_storage(connection, setup_args, &block_args).await?; + let action = SandboxAction::Execution { fee_input, tx }; + let (env, storage) = self + .prepare_env_and_storage(connection, &block_args, &action) + .await?; + let SandboxAction::Execution { tx, .. } = action else { + unreachable!(); // by construction + }; let storage = StorageWithOverrides::new(storage, &StateOverride::default()); let stage_latency = SANDBOX_METRICS.sandbox[&SandboxStage::Validation].start(); diff --git a/core/node/api_server/src/tx_sender/mod.rs b/core/node/api_server/src/tx_sender/mod.rs index 44eaae2e3eee..e2285c167110 100644 --- a/core/node/api_server/src/tx_sender/mod.rs +++ b/core/node/api_server/src/tx_sender/mod.rs @@ -5,20 +5,15 @@ use std::{sync::Arc, time::Instant}; use anyhow::Context as _; use tokio::sync::RwLock; use zksync_config::configs::{api::Web3JsonRpcConfig, chain::StateKeeperConfig}; -use zksync_contracts::BaseSystemContracts; use zksync_dal::{ transactions_dal::L2TxSubmissionResult, Connection, ConnectionPool, Core, CoreDal, }; use zksync_multivm::{ - interface::{ - OneshotTracingParams, TransactionExecutionMetrics, TxExecutionArgs, TxExecutionMode, - VmExecutionResultAndLogs, - }, + interface::{OneshotTracingParams, TransactionExecutionMetrics, VmExecutionResultAndLogs}, utils::{ adjust_pubdata_price_for_tx, derive_base_fee_and_gas_per_pubdata, derive_overhead, get_max_batch_gas_limit, }, - vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT, }; use zksync_node_fee_model::{ApiFeeInputProvider, BatchFeeModelInputProvider}; use zksync_state::PostgresStorageCaches; @@ -39,12 +34,13 @@ use zksync_types::{ ProtocolVersionId, Transaction, H160, H256, MAX_L2_TX_GAS_LIMIT, MAX_NEW_FACTORY_DEPS, U256, }; use zksync_utils::h256_to_u256; +use zksync_vm_executor::oneshot::{CallOrExecute, EstimateGas, OneshotEnvParameters}; pub(super) use self::result::SubmitTxError; use self::{master_pool_sink::MasterPoolSink, tx_sink::TxSink}; use crate::{ execution_sandbox::{ - BlockArgs, SubmitTxStage, TransactionExecutor, TxSetupArgs, VmConcurrencyBarrier, + BlockArgs, SandboxAction, SandboxExecutor, SubmitTxStage, VmConcurrencyBarrier, VmConcurrencyLimiter, VmPermit, SANDBOX_METRICS, }, tx_sender::result::ApiCallResult, @@ -80,133 +76,55 @@ pub async fn build_tx_sender( let batch_fee_input_provider = ApiFeeInputProvider::new(batch_fee_model_input_provider, replica_pool); - + let executor_options = SandboxExecutorOptions::new( + tx_sender_config.chain_id, + AccountTreeId::new(tx_sender_config.fee_account_addr), + tx_sender_config.validation_computational_gas_limit, + ) + .await?; let tx_sender = tx_sender_builder.build( Arc::new(batch_fee_input_provider), Arc::new(vm_concurrency_limiter), - ApiContracts::load_from_disk().await?, + executor_options, storage_caches, ); Ok((tx_sender, vm_barrier)) } -#[derive(Debug, Clone)] -pub struct MultiVMBaseSystemContracts { - /// Contracts to be used for pre-virtual-blocks protocol versions. - pub(crate) pre_virtual_blocks: BaseSystemContracts, - /// Contracts to be used for post-virtual-blocks protocol versions. - pub(crate) post_virtual_blocks: BaseSystemContracts, - /// Contracts to be used for protocol versions after virtual block upgrade fix. - pub(crate) post_virtual_blocks_finish_upgrade_fix: BaseSystemContracts, - /// Contracts to be used for post-boojum protocol versions. - pub(crate) post_boojum: BaseSystemContracts, - /// Contracts to be used after the allow-list removal upgrade - pub(crate) post_allowlist_removal: BaseSystemContracts, - /// Contracts to be used after the 1.4.1 upgrade - pub(crate) post_1_4_1: BaseSystemContracts, - /// Contracts to be used after the 1.4.2 upgrade - pub(crate) post_1_4_2: BaseSystemContracts, - /// Contracts to be used during the `v23` upgrade. This upgrade was done on an internal staging environment only. - pub(crate) vm_1_5_0_small_memory: BaseSystemContracts, - /// Contracts to be used after the 1.5.0 upgrade - pub(crate) vm_1_5_0_increased_memory: BaseSystemContracts, -} - -impl MultiVMBaseSystemContracts { - pub fn get_by_protocol_version(self, version: ProtocolVersionId) -> BaseSystemContracts { - match version { - ProtocolVersionId::Version0 - | ProtocolVersionId::Version1 - | ProtocolVersionId::Version2 - | ProtocolVersionId::Version3 - | ProtocolVersionId::Version4 - | ProtocolVersionId::Version5 - | ProtocolVersionId::Version6 - | ProtocolVersionId::Version7 - | ProtocolVersionId::Version8 - | ProtocolVersionId::Version9 - | ProtocolVersionId::Version10 - | ProtocolVersionId::Version11 - | ProtocolVersionId::Version12 => self.pre_virtual_blocks, - ProtocolVersionId::Version13 => self.post_virtual_blocks, - ProtocolVersionId::Version14 - | ProtocolVersionId::Version15 - | ProtocolVersionId::Version16 - | ProtocolVersionId::Version17 => self.post_virtual_blocks_finish_upgrade_fix, - ProtocolVersionId::Version18 => self.post_boojum, - ProtocolVersionId::Version19 => self.post_allowlist_removal, - ProtocolVersionId::Version20 => self.post_1_4_1, - ProtocolVersionId::Version21 | ProtocolVersionId::Version22 => self.post_1_4_2, - ProtocolVersionId::Version23 => self.vm_1_5_0_small_memory, - ProtocolVersionId::Version24 | ProtocolVersionId::Version25 => { - self.vm_1_5_0_increased_memory - } - } - } - - pub fn load_estimate_gas_blocking() -> Self { - Self { - pre_virtual_blocks: BaseSystemContracts::estimate_gas_pre_virtual_blocks(), - post_virtual_blocks: BaseSystemContracts::estimate_gas_post_virtual_blocks(), - post_virtual_blocks_finish_upgrade_fix: - BaseSystemContracts::estimate_gas_post_virtual_blocks_finish_upgrade_fix(), - post_boojum: BaseSystemContracts::estimate_gas_post_boojum(), - post_allowlist_removal: BaseSystemContracts::estimate_gas_post_allowlist_removal(), - post_1_4_1: BaseSystemContracts::estimate_gas_post_1_4_1(), - post_1_4_2: BaseSystemContracts::estimate_gas_post_1_4_2(), - vm_1_5_0_small_memory: BaseSystemContracts::estimate_gas_1_5_0_small_memory(), - vm_1_5_0_increased_memory: - BaseSystemContracts::estimate_gas_post_1_5_0_increased_memory(), - } - } - - pub fn load_eth_call_blocking() -> Self { - Self { - pre_virtual_blocks: BaseSystemContracts::playground_pre_virtual_blocks(), - post_virtual_blocks: BaseSystemContracts::playground_post_virtual_blocks(), - post_virtual_blocks_finish_upgrade_fix: - BaseSystemContracts::playground_post_virtual_blocks_finish_upgrade_fix(), - post_boojum: BaseSystemContracts::playground_post_boojum(), - post_allowlist_removal: BaseSystemContracts::playground_post_allowlist_removal(), - post_1_4_1: BaseSystemContracts::playground_post_1_4_1(), - post_1_4_2: BaseSystemContracts::playground_post_1_4_2(), - vm_1_5_0_small_memory: BaseSystemContracts::playground_1_5_0_small_memory(), - vm_1_5_0_increased_memory: BaseSystemContracts::playground_post_1_5_0_increased_memory( - ), - } - } -} - -/// Smart contracts to be used in the API sandbox requests, e.g. for estimating gas and -/// performing `eth_call` requests. -#[derive(Debug, Clone)] -pub struct ApiContracts { - /// Contracts to be used when estimating gas. - /// These contracts (mainly, bootloader) normally should be tuned to provide accurate - /// execution metrics. - pub(crate) estimate_gas: MultiVMBaseSystemContracts, - /// Contracts to be used when performing `eth_call` requests. - /// These contracts (mainly, bootloader) normally should be tuned to provide better UX - /// experience (e.g. revert messages). - pub(crate) eth_call: MultiVMBaseSystemContracts, +/// Oneshot executor options used by the API server sandbox. +#[derive(Debug)] +pub struct SandboxExecutorOptions { + /// Env parameters to be used when estimating gas. + pub(crate) estimate_gas: OneshotEnvParameters, + /// Env parameters to be used when performing `eth_call` requests. + pub(crate) eth_call: OneshotEnvParameters, } -impl ApiContracts { +impl SandboxExecutorOptions { /// Loads the contracts from the local file system. /// This method is *currently* preferred to be used in all contexts, /// given that there is no way to fetch "playground" contracts from the main node. - pub async fn load_from_disk() -> anyhow::Result { - tokio::task::spawn_blocking(Self::load_from_disk_blocking) - .await - .context("loading `ApiContracts` panicked") + pub async fn new( + chain_id: L2ChainId, + operator_account: AccountTreeId, + validation_computational_gas_limit: u32, + ) -> anyhow::Result { + Ok(Self { + estimate_gas: OneshotEnvParameters::for_gas_estimation(chain_id, operator_account) + .await?, + eth_call: OneshotEnvParameters::for_execution( + chain_id, + operator_account, + validation_computational_gas_limit, + ) + .await?, + }) } - /// Blocking version of [`Self::load_from_disk()`]. - pub fn load_from_disk_blocking() -> Self { - Self { - estimate_gas: MultiVMBaseSystemContracts::load_estimate_gas_blocking(), - eth_call: MultiVMBaseSystemContracts::load_eth_call_blocking(), - } + pub(crate) async fn mock() -> Self { + Self::new(L2ChainId::default(), AccountTreeId::default(), u32::MAX) + .await + .unwrap() } } @@ -254,7 +172,7 @@ impl TxSenderBuilder { self, batch_fee_input_provider: Arc, vm_concurrency_limiter: Arc, - api_contracts: ApiContracts, + executor_options: SandboxExecutorOptions, storage_caches: PostgresStorageCaches, ) -> TxSender { // Use noop sealer if no sealer was explicitly provided. @@ -267,18 +185,21 @@ impl TxSenderBuilder { .config .vm_execution_cache_misses_limit .unwrap_or(usize::MAX); + let executor = SandboxExecutor::real( + executor_options, + storage_caches, + missed_storage_invocation_limit, + ); TxSender(Arc::new(TxSenderInner { sender_config: self.config, tx_sink: self.tx_sink, replica_connection_pool: self.replica_connection_pool, batch_fee_input_provider, - api_contracts, vm_concurrency_limiter, - storage_caches, whitelisted_tokens_for_aa_cache, sealer, - executor: TransactionExecutor::real(missed_storage_invocation_limit), + executor, })) } } @@ -327,16 +248,13 @@ pub struct TxSenderInner { pub replica_connection_pool: ConnectionPool, // Used to keep track of gas prices for the fee ticker. pub batch_fee_input_provider: Arc, - pub(super) api_contracts: ApiContracts, /// Used to limit the amount of VMs that can be executed simultaneously. pub(super) vm_concurrency_limiter: Arc, - // Caches used in VM execution. - storage_caches: PostgresStorageCaches, // Cache for white-listed tokens. pub(super) whitelisted_tokens_for_aa_cache: Arc>>, /// Batch sealer used to check whether transaction can be executed by the sequencer. pub(super) sealer: Arc, - pub(super) executor: TransactionExecutor, + pub(super) executor: SandboxExecutor, } #[derive(Clone)] @@ -353,10 +271,6 @@ impl TxSender { Arc::clone(&self.0.vm_concurrency_limiter) } - pub(crate) fn storage_caches(&self) -> PostgresStorageCaches { - self.0.storage_caches.clone() - } - pub(crate) async fn read_whitelisted_tokens_for_aa_cache(&self) -> Vec
{ self.0.whitelisted_tokens_for_aa_cache.read().await.clone() } @@ -383,8 +297,20 @@ impl TxSender { stage_latency.observe(); let stage_latency = SANDBOX_METRICS.start_tx_submit_stage(tx_hash, SubmitTxStage::DryRun); - let setup_args = self.call_args(&tx, None).await?; + // **Important.** For the main node, this method acquires a DB connection inside `get_batch_fee_input()`. + // Thus, it must not be called it if you're holding a DB connection already. + let fee_input = self + .0 + .batch_fee_input_provider + .get_batch_fee_input() + .await + .context("cannot get batch fee input")?; + let vm_permit = self.0.vm_concurrency_limiter.acquire().await; + let action = SandboxAction::Execution { + fee_input, + tx: tx.clone(), + }; let vm_permit = vm_permit.ok_or(SubmitTxError::ServerShuttingDown)?; let mut connection = self.acquire_replica_connection().await?; let block_args = BlockArgs::pending(&mut connection).await?; @@ -392,15 +318,7 @@ impl TxSender { let execution_output = self .0 .executor - .execute_tx_in_sandbox( - vm_permit.clone(), - setup_args.clone(), - TxExecutionArgs::for_validation(tx.clone()), - connection, - block_args, - None, - OneshotTracingParams::default(), - ) + .execute_in_sandbox(vm_permit.clone(), connection, action, &block_args, None) .await?; tracing::info!( "Submit tx {tx_hash:?} with execution metrics {:?}", @@ -411,17 +329,16 @@ impl TxSender { let stage_latency = SANDBOX_METRICS.start_tx_submit_stage(tx_hash, SubmitTxStage::VerifyExecute); let connection = self.acquire_replica_connection().await?; - let computational_gas_limit = self.0.sender_config.validation_computational_gas_limit; let validation_result = self .0 .executor .validate_tx_in_sandbox( - connection, vm_permit, + connection, tx.clone(), - setup_args, block_args, - computational_gas_limit, + fee_input, + &self.read_whitelisted_tokens_for_aa_cache().await, ) .await; stage_latency.observe(); @@ -473,43 +390,6 @@ impl TxSender { } } - /// **Important.** For the main node, this method acquires a DB connection inside `get_batch_fee_input()`. - /// Thus, you shouldn't call it if you're holding a DB connection already. - async fn call_args( - &self, - tx: &L2Tx, - call_overrides: Option<&CallOverrides>, - ) -> anyhow::Result { - let fee_input = self - .0 - .batch_fee_input_provider - .get_batch_fee_input() - .await - .context("cannot get batch fee input")?; - Ok(TxSetupArgs { - execution_mode: if call_overrides.is_some() { - TxExecutionMode::EthCall - } else { - TxExecutionMode::VerifyExecute - }, - operator_account: AccountTreeId::new(self.0.sender_config.fee_account_addr), - fee_input, - base_system_contracts: self.0.api_contracts.eth_call.clone(), - caches: self.storage_caches(), - validation_computational_gas_limit: self - .0 - .sender_config - .validation_computational_gas_limit, - chain_id: self.0.sender_config.chain_id, - whitelisted_tokens_for_aa: self.read_whitelisted_tokens_for_aa_cache().await, - enforced_base_fee: if let Some(overrides) = call_overrides { - overrides.enforced_base_fee - } else { - Some(tx.common_data.fee.max_fee_per_gas.as_u64()) - }, - }) - } - async fn validate_tx( &self, tx: &L2Tx, @@ -686,7 +566,7 @@ impl TxSender { mut tx: Transaction, tx_gas_limit: u64, gas_price_per_pubdata: u32, - fee_model_params: BatchFeeInput, + fee_input: BatchFeeInput, block_args: BlockArgs, base_fee: u64, vm_version: VmVersion, @@ -715,49 +595,26 @@ impl TxSender { } ExecuteTransactionCommon::ProtocolUpgrade(common_data) => { common_data.gas_limit = forced_gas_limit.into(); - let required_funds = common_data.gas_limit * common_data.max_fee_per_gas + tx.execute.value; - common_data.to_mint = required_funds; } } - let setup_args = self.args_for_gas_estimate(fee_model_params, base_fee).await; - let execution_args = TxExecutionArgs::for_gas_estimate(tx); let connection = self.acquire_replica_connection().await?; + let action = SandboxAction::GasEstimation { + fee_input, + base_fee, + tx, + }; let execution_output = self .0 .executor - .execute_tx_in_sandbox( - vm_permit, - setup_args, - execution_args, - connection, - block_args, - state_override, - OneshotTracingParams::default(), - ) + .execute_in_sandbox(vm_permit, connection, action, &block_args, state_override) .await?; Ok((execution_output.vm, execution_output.metrics)) } - async fn args_for_gas_estimate(&self, fee_input: BatchFeeInput, base_fee: u64) -> TxSetupArgs { - let config = &self.0.sender_config; - TxSetupArgs { - execution_mode: TxExecutionMode::EstimateFee, - operator_account: AccountTreeId::new(config.fee_account_addr), - fee_input, - // We want to bypass the computation gas limit check for gas estimation - validation_computational_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, - base_system_contracts: self.0.api_contracts.estimate_gas.clone(), - caches: self.storage_caches(), - chain_id: config.chain_id, - whitelisted_tokens_for_aa: self.read_whitelisted_tokens_for_aa_cache().await, - enforced_base_fee: Some(base_fee), - } - } - #[tracing::instrument(level = "debug", skip_all, fields( initiator = ?tx.initiator_account(), nonce = ?tx.nonce(), @@ -1014,30 +871,41 @@ impl TxSender { .await } - pub async fn eth_call( + pub(crate) async fn eth_call( &self, block_args: BlockArgs, call_overrides: CallOverrides, - tx: L2Tx, + call: L2Tx, state_override: Option, ) -> Result, SubmitTxError> { let vm_permit = self.0.vm_concurrency_limiter.acquire().await; let vm_permit = vm_permit.ok_or(SubmitTxError::ServerShuttingDown)?; - let setup_args = self.call_args(&tx, Some(&call_overrides)).await?; - let connection = self.acquire_replica_connection().await?; + let mut connection; + let fee_input = if block_args.resolves_to_latest_sealed_l2_block() { + let fee_input = self + .0 + .batch_fee_input_provider + .get_batch_fee_input() + .await?; + // It is important to acquire a connection after calling the provider; see the comment above. + connection = self.acquire_replica_connection().await?; + fee_input + } else { + connection = self.acquire_replica_connection().await?; + block_args.historical_fee_input(&mut connection).await? + }; + + let action = SandboxAction::Call { + call, + fee_input, + enforced_base_fee: call_overrides.enforced_base_fee, + tracing_params: OneshotTracingParams::default(), + }; let result = self .0 .executor - .execute_tx_in_sandbox( - vm_permit, - setup_args, - TxExecutionArgs::for_eth_call(tx), - connection, - block_args, - state_override, - OneshotTracingParams::default(), - ) + .execute_in_sandbox(vm_permit, connection, action, &block_args, state_override) .await?; result.vm.into_api_call_result() } diff --git a/core/node/api_server/src/tx_sender/tests.rs b/core/node/api_server/src/tx_sender/tests.rs index 0ac3eb0b4f38..ece35fbdbdac 100644 --- a/core/node/api_server/src/tx_sender/tests.rs +++ b/core/node/api_server/src/tx_sender/tests.rs @@ -32,7 +32,8 @@ async fn getting_nonce_for_account() { .await .unwrap(); - let tx_executor = MockOneshotExecutor::default().into(); + let tx_executor = MockOneshotExecutor::default(); + let tx_executor = SandboxExecutor::mock(tx_executor).await; let (tx_sender, _) = create_test_tx_sender(pool.clone(), l2_chain_id, tx_executor).await; let nonce = tx_sender.get_expected_nonce(test_address).await.unwrap(); @@ -82,7 +83,8 @@ async fn getting_nonce_for_account_after_snapshot_recovery() { .await; let l2_chain_id = L2ChainId::default(); - let tx_executor = MockOneshotExecutor::default().into(); + let tx_executor = MockOneshotExecutor::default(); + let tx_executor = SandboxExecutor::mock(tx_executor).await; let (tx_sender, _) = create_test_tx_sender(pool.clone(), l2_chain_id, tx_executor).await; storage @@ -142,7 +144,7 @@ async fn submitting_tx_requires_one_connection() { assert_eq!(received_tx.hash(), tx_hash); ExecutionResult::Success { output: vec![] } }); - let tx_executor = tx_executor.into(); + let tx_executor = SandboxExecutor::mock(tx_executor).await; let (tx_sender, _) = create_test_tx_sender(pool.clone(), l2_chain_id, tx_executor).await; let submission_result = tx_sender.submit_tx(tx).await.unwrap(); @@ -184,7 +186,7 @@ async fn eth_call_requires_single_connection() { output: b"success!".to_vec(), } }); - let tx_executor = tx_executor.into(); + let tx_executor = SandboxExecutor::mock(tx_executor).await; let (tx_sender, _) = create_test_tx_sender( pool.clone(), genesis_params.config().l2_chain_id, diff --git a/core/node/api_server/src/web3/namespaces/debug.rs b/core/node/api_server/src/web3/namespaces/debug.rs index ad00f6a878b9..2c6c70f6faa1 100644 --- a/core/node/api_server/src/web3/namespaces/debug.rs +++ b/core/node/api_server/src/web3/namespaces/debug.rs @@ -1,11 +1,6 @@ use anyhow::Context as _; use zksync_dal::{CoreDal, DalError}; -use zksync_multivm::{ - interface::{ - Call, CallType, ExecutionResult, OneshotTracingParams, TxExecutionArgs, TxExecutionMode, - }, - vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT, -}; +use zksync_multivm::interface::{Call, CallType, ExecutionResult, OneshotTracingParams}; use zksync_system_constants::MAX_ENCODED_TX_SIZE; use zksync_types::{ api::{BlockId, BlockNumber, DebugCall, DebugCallType, ResultDebugCall, TracerConfig}, @@ -13,13 +8,12 @@ use zksync_types::{ fee_model::BatchFeeInput, l2::L2Tx, transaction_request::CallRequest, - web3, AccountTreeId, H256, U256, + web3, H256, U256, }; use zksync_web3_decl::error::Web3Error; use crate::{ - execution_sandbox::TxSetupArgs, - tx_sender::{ApiContracts, TxSenderConfig}, + execution_sandbox::SandboxAction, web3::{backend_jsonrpsee::MethodTracer, state::RpcState}, }; @@ -27,13 +21,12 @@ use crate::{ pub(crate) struct DebugNamespace { batch_fee_input: BatchFeeInput, state: RpcState, - api_contracts: ApiContracts, } impl DebugNamespace { pub async fn new(state: RpcState) -> anyhow::Result { - let api_contracts = ApiContracts::load_from_disk().await?; let fee_input_provider = &state.tx_sender.0.batch_fee_input_provider; + // FIXME (PLA-1033): use the fee input provider instead of a constant value let batch_fee_input = fee_input_provider .get_batch_fee_input_scaled( state.api_config.estimate_gas_scale_factor, @@ -46,7 +39,6 @@ impl DebugNamespace { // For now, the same scaling is used for both the L1 gas price and the pubdata price batch_fee_input, state, - api_contracts, }) } @@ -79,10 +71,6 @@ impl DebugNamespace { } } - fn sender_config(&self) -> &TxSenderConfig { - &self.state.tx_sender.0.sender_config - } - pub(crate) fn current_method(&self) -> &MethodTracer { &self.state.current_method } @@ -174,12 +162,16 @@ impl DebugNamespace { if request.gas.is_none() { request.gas = Some(block_args.default_eth_call_gas(&mut connection).await?); } + let fee_input = if block_args.resolves_to_latest_sealed_l2_block() { + self.batch_fee_input + } else { + block_args.historical_fee_input(&mut connection).await? + }; drop(connection); let call_overrides = request.get_call_overrides()?; - let tx = L2Tx::from_request(request.into(), MAX_ENCODED_TX_SIZE)?; + let call = L2Tx::from_request(request.into(), MAX_ENCODED_TX_SIZE)?; - let setup_args = self.call_args(call_overrides.enforced_base_fee).await; let vm_permit = self .state .tx_sender @@ -196,14 +188,17 @@ impl DebugNamespace { let connection = self.state.acquire_connection().await?; let executor = &self.state.tx_sender.0.executor; let result = executor - .execute_tx_in_sandbox( + .execute_in_sandbox( vm_permit, - setup_args, - TxExecutionArgs::for_eth_call(tx.clone()), connection, - block_args, + SandboxAction::Call { + call: call.clone(), + fee_input, + enforced_base_fee: call_overrides.enforced_base_fee, + tracing_params, + }, + &block_args, None, - tracing_params, ) .await?; @@ -219,33 +214,14 @@ impl DebugNamespace { }; let call = Call::new_high_level( - tx.common_data.fee.gas_limit.as_u64(), + call.common_data.fee.gas_limit.as_u64(), result.vm.statistics.gas_used, - tx.execute.value, - tx.execute.calldata, + call.execute.value, + call.execute.calldata, output, revert_reason, result.call_traces, ); Ok(Self::map_call(call, false)) } - - async fn call_args(&self, enforced_base_fee: Option) -> TxSetupArgs { - let sender_config = self.sender_config(); - TxSetupArgs { - execution_mode: TxExecutionMode::EthCall, - operator_account: AccountTreeId::default(), - fee_input: self.batch_fee_input, - base_system_contracts: self.api_contracts.eth_call.clone(), - caches: self.state.tx_sender.storage_caches().clone(), - validation_computational_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, - chain_id: sender_config.chain_id, - whitelisted_tokens_for_aa: self - .state - .tx_sender - .read_whitelisted_tokens_for_aa_cache() - .await, - enforced_base_fee, - } - } } diff --git a/core/node/api_server/src/web3/testonly.rs b/core/node/api_server/src/web3/testonly.rs index 18ee3a641d0a..03aa4e68e6a5 100644 --- a/core/node/api_server/src/web3/testonly.rs +++ b/core/node/api_server/src/web3/testonly.rs @@ -2,56 +2,26 @@ use std::{pin::Pin, time::Instant}; -use async_trait::async_trait; use tokio::sync::watch; use zksync_config::configs::{api::Web3JsonRpcConfig, chain::StateKeeperConfig, wallets::Wallets}; use zksync_dal::ConnectionPool; use zksync_health_check::CheckHealth; -use zksync_node_fee_model::{BatchFeeModelInputProvider, MockBatchFeeParamsProvider}; +use zksync_node_fee_model::MockBatchFeeParamsProvider; use zksync_state::PostgresStorageCaches; use zksync_state_keeper::seal_criteria::NoopSealer; -use zksync_types::{ - fee_model::{BatchFeeInput, FeeParams}, - L2ChainId, -}; +use zksync_types::L2ChainId; use zksync_vm_executor::oneshot::MockOneshotExecutor; use super::{metrics::ApiTransportLabel, *}; -use crate::{execution_sandbox::TransactionExecutor, tx_sender::TxSenderConfig}; +use crate::{execution_sandbox::SandboxExecutor, tx_sender::TxSenderConfig}; const TEST_TIMEOUT: Duration = Duration::from_secs(90); const POLL_INTERVAL: Duration = Duration::from_millis(50); -/// Same as [`MockBatchFeeParamsProvider`], but also artificially acquires a Postgres connection on each call -/// (same as the real node implementation). -#[derive(Debug)] -struct MockApiBatchFeeParamsProvider { - inner: MockBatchFeeParamsProvider, - pool: ConnectionPool, -} - -#[async_trait] -impl BatchFeeModelInputProvider for MockApiBatchFeeParamsProvider { - async fn get_batch_fee_input_scaled( - &self, - l1_gas_price_scale_factor: f64, - l1_pubdata_price_scale_factor: f64, - ) -> anyhow::Result { - let _connection = self.pool.connection().await?; - self.inner - .get_batch_fee_input_scaled(l1_gas_price_scale_factor, l1_pubdata_price_scale_factor) - .await - } - - fn get_fee_model_params(&self) -> FeeParams { - self.inner.get_fee_model_params() - } -} - pub(crate) async fn create_test_tx_sender( pool: ConnectionPool, l2_chain_id: L2ChainId, - tx_executor: TransactionExecutor, + tx_executor: SandboxExecutor, ) -> (TxSender, VmConcurrencyBarrier) { let web3_config = Web3JsonRpcConfig::for_tests(); let state_keeper_config = StateKeeperConfig::for_tests(); @@ -64,10 +34,7 @@ pub(crate) async fn create_test_tx_sender( ); let storage_caches = PostgresStorageCaches::new(1, 1); - let batch_fee_model_input_provider = Arc::new(MockApiBatchFeeParamsProvider { - inner: MockBatchFeeParamsProvider::default(), - pool: pool.clone(), - }); + let batch_fee_model_input_provider = Arc::::default(); let (mut tx_sender, vm_barrier) = crate::tx_sender::build_tx_sender( &tx_sender_config, &web3_config, @@ -177,8 +144,9 @@ async fn spawn_server( method_tracer: Arc, stop_receiver: watch::Receiver, ) -> (ApiServerHandles, mpsc::UnboundedReceiver) { + let tx_executor = SandboxExecutor::mock(tx_executor).await; let (tx_sender, vm_barrier) = - create_test_tx_sender(pool.clone(), api_config.l2_chain_id, tx_executor.into()).await; + create_test_tx_sender(pool.clone(), api_config.l2_chain_id, tx_executor).await; let (pub_sub_events_sender, pub_sub_events_receiver) = mpsc::unbounded_channel(); let mut namespaces = Namespace::DEFAULT.to_vec(); diff --git a/core/node/api_server/src/web3/tests/vm.rs b/core/node/api_server/src/web3/tests/vm.rs index d8d1a2c7768e..9bdcf1159303 100644 --- a/core/node/api_server/src/web3/tests/vm.rs +++ b/core/node/api_server/src/web3/tests/vm.rs @@ -1,14 +1,22 @@ //! Tests for the VM-instantiating methods (e.g., `eth_call`). -use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Mutex, +}; use api::state_override::{OverrideAccount, StateOverride}; use zksync_multivm::interface::{ ExecutionResult, VmExecutionLogs, VmExecutionResultAndLogs, VmRevertReason, }; +use zksync_node_fee_model::BatchFeeModelInputProvider; use zksync_types::{ - api::ApiStorageLog, get_intrinsic_constants, transaction_request::CallRequest, K256PrivateKey, - L2ChainId, PackedEthSignature, StorageLogKind, StorageLogWithPreviousValue, U256, + api::ApiStorageLog, + fee_model::{BatchFeeInput, FeeParams}, + get_intrinsic_constants, + transaction_request::CallRequest, + K256PrivateKey, L2ChainId, PackedEthSignature, StorageLogKind, StorageLogWithPreviousValue, + U256, }; use zksync_utils::u256_to_h256; use zksync_vm_executor::oneshot::MockOneshotExecutor; @@ -16,8 +24,47 @@ use zksync_web3_decl::namespaces::DebugNamespaceClient; use super::*; -#[derive(Debug)] -struct CallTest; +#[derive(Debug, Clone)] +struct ExpectedFeeInput(Arc>); + +impl Default for ExpectedFeeInput { + fn default() -> Self { + let this = Self(Arc::default()); + this.expect_default(1.0); // works for transaction execution and calls + this + } +} + +impl ExpectedFeeInput { + fn expect_for_block(&self, number: api::BlockNumber, scale: f64) { + *self.0.lock().unwrap() = match number { + api::BlockNumber::Number(number) => create_l2_block(number.as_u32()).batch_fee_input, + _ => ::default_batch_fee_input_scaled( + FeeParams::sensible_v1_default(), + scale, + scale, + ), + }; + } + + fn expect_default(&self, scale: f64) { + self.expect_for_block(api::BlockNumber::Pending, scale); + } + + fn assert_eq(&self, actual: BatchFeeInput) { + let expected = *self.0.lock().unwrap(); + // We do relaxed comparisons to deal with the fact that the fee input provider may convert inputs to pubdata independent form. + assert_eq!( + actual.into_pubdata_independent(), + expected.into_pubdata_independent() + ); + } +} + +#[derive(Debug, Default)] +struct CallTest { + fee_input: ExpectedFeeInput, +} impl CallTest { fn call_request(data: &[u8]) -> CallRequest { @@ -31,9 +78,14 @@ impl CallTest { } } - fn create_executor(latest_block: L2BlockNumber) -> MockOneshotExecutor { + fn create_executor( + latest_block: L2BlockNumber, + expected_fee_input: ExpectedFeeInput, + ) -> MockOneshotExecutor { let mut tx_executor = MockOneshotExecutor::default(); tx_executor.set_call_responses(move |tx, env| { + expected_fee_input.assert_eq(env.l1_batch.fee_input); + let expected_block_number = match tx.execute.calldata() { b"pending" => latest_block + 1, b"latest" => latest_block, @@ -52,7 +104,7 @@ impl CallTest { #[async_trait] impl HttpTest for CallTest { fn transaction_executor(&self) -> MockOneshotExecutor { - Self::create_executor(L2BlockNumber(1)) + Self::create_executor(L2BlockNumber(1), self.fee_input.clone()) } async fn test( @@ -73,9 +125,10 @@ impl HttpTest for CallTest { let valid_block_numbers_and_calldata = [ (api::BlockNumber::Pending, b"pending" as &[_]), (api::BlockNumber::Latest, b"latest"), - (0.into(), b"latest"), + (1.into(), b"latest"), ]; for (number, calldata) in valid_block_numbers_and_calldata { + self.fee_input.expect_for_block(number, 1.0); let number = api::BlockIdVariant::BlockNumber(number); let call_result = client .call(Self::call_request(calldata), Some(number), None) @@ -101,11 +154,13 @@ impl HttpTest for CallTest { #[tokio::test] async fn call_method_basics() { - test_http_server(CallTest).await; + test_http_server(CallTest::default()).await; } -#[derive(Debug)] -struct CallTestAfterSnapshotRecovery; +#[derive(Debug, Default)] +struct CallTestAfterSnapshotRecovery { + fee_input: ExpectedFeeInput, +} #[async_trait] impl HttpTest for CallTestAfterSnapshotRecovery { @@ -115,7 +170,7 @@ impl HttpTest for CallTestAfterSnapshotRecovery { fn transaction_executor(&self) -> MockOneshotExecutor { let first_local_l2_block = StorageInitialization::SNAPSHOT_RECOVERY_BLOCK + 1; - CallTest::create_executor(first_local_l2_block) + CallTest::create_executor(first_local_l2_block, self.fee_input.clone()) } async fn test( @@ -150,6 +205,7 @@ impl HttpTest for CallTestAfterSnapshotRecovery { let first_l2_block_numbers = [api::BlockNumber::Latest, first_local_l2_block.0.into()]; for number in first_l2_block_numbers { + self.fee_input.expect_for_block(number, 1.0); let number = api::BlockIdVariant::BlockNumber(number); let call_result = client .call(CallTest::call_request(b"latest"), Some(number), None) @@ -162,7 +218,7 @@ impl HttpTest for CallTestAfterSnapshotRecovery { #[tokio::test] async fn call_method_after_snapshot_recovery() { - test_http_server(CallTestAfterSnapshotRecovery).await; + test_http_server(CallTestAfterSnapshotRecovery::default()).await; } #[derive(Debug)] @@ -390,10 +446,14 @@ async fn send_raw_transaction_with_detailed_output() { test_http_server(SendTransactionWithDetailedOutputTest).await; } -#[derive(Debug)] -struct TraceCallTest; +#[derive(Debug, Default)] +struct TraceCallTest { + fee_input: ExpectedFeeInput, +} impl TraceCallTest { + const FEE_SCALE: f64 = 1.2; // set in the tx sender config + fn assert_debug_call(call_request: &CallRequest, call_result: &api::DebugCall) { assert_eq!(call_result.from, Address::zero()); assert_eq!(call_result.gas, call_request.gas.unwrap()); @@ -413,7 +473,7 @@ impl TraceCallTest { #[async_trait] impl HttpTest for TraceCallTest { fn transaction_executor(&self) -> MockOneshotExecutor { - CallTest::create_executor(L2BlockNumber(1)) + CallTest::create_executor(L2BlockNumber(1), self.fee_input.clone()) } async fn test( @@ -426,6 +486,7 @@ impl HttpTest for TraceCallTest { store_l2_block(&mut connection, L2BlockNumber(1), &[]).await?; drop(connection); + self.fee_input.expect_default(Self::FEE_SCALE); let call_request = CallTest::call_request(b"pending"); let call_result = client.trace_call(call_request.clone(), None, None).await?; Self::assert_debug_call(&call_request, &call_result); @@ -438,6 +499,7 @@ impl HttpTest for TraceCallTest { let latest_block_numbers = [api::BlockNumber::Latest, 1.into()]; let call_request = CallTest::call_request(b"latest"); for number in latest_block_numbers { + self.fee_input.expect_for_block(number, Self::FEE_SCALE); let call_result = client .trace_call( call_request.clone(), @@ -469,11 +531,13 @@ impl HttpTest for TraceCallTest { #[tokio::test] async fn trace_call_basics() { - test_http_server(TraceCallTest).await; + test_http_server(TraceCallTest::default()).await; } -#[derive(Debug)] -struct TraceCallTestAfterSnapshotRecovery; +#[derive(Debug, Default)] +struct TraceCallTestAfterSnapshotRecovery { + fee_input: ExpectedFeeInput, +} #[async_trait] impl HttpTest for TraceCallTestAfterSnapshotRecovery { @@ -483,7 +547,7 @@ impl HttpTest for TraceCallTestAfterSnapshotRecovery { fn transaction_executor(&self) -> MockOneshotExecutor { let number = StorageInitialization::SNAPSHOT_RECOVERY_BLOCK + 1; - CallTest::create_executor(number) + CallTest::create_executor(number, self.fee_input.clone()) } async fn test( @@ -491,6 +555,7 @@ impl HttpTest for TraceCallTestAfterSnapshotRecovery { client: &DynClient, _pool: &ConnectionPool, ) -> anyhow::Result<()> { + self.fee_input.expect_default(TraceCallTest::FEE_SCALE); let call_request = CallTest::call_request(b"pending"); let call_result = client.trace_call(call_request.clone(), None, None).await?; TraceCallTest::assert_debug_call(&call_request, &call_result); @@ -514,6 +579,8 @@ impl HttpTest for TraceCallTestAfterSnapshotRecovery { let call_request = CallTest::call_request(b"latest"); let first_l2_block_numbers = [api::BlockNumber::Latest, first_local_l2_block.0.into()]; for number in first_l2_block_numbers { + self.fee_input + .expect_for_block(number, TraceCallTest::FEE_SCALE); let number = api::BlockId::Number(number); let call_result = client .trace_call(call_request.clone(), Some(number), None) @@ -526,7 +593,7 @@ impl HttpTest for TraceCallTestAfterSnapshotRecovery { #[tokio::test] async fn trace_call_after_snapshot_recovery() { - test_http_server(TraceCallTestAfterSnapshotRecovery).await; + test_http_server(TraceCallTestAfterSnapshotRecovery::default()).await; } #[derive(Debug)] diff --git a/core/node/consensus/Cargo.toml b/core/node/consensus/Cargo.toml index 707bd957d810..fdcc9089e339 100644 --- a/core/node/consensus/Cargo.toml +++ b/core/node/consensus/Cargo.toml @@ -32,8 +32,8 @@ zksync_system_constants.workspace = true zksync_types.workspace = true zksync_utils.workspace = true zksync_web3_decl.workspace = true -zksync_node_api_server.workspace = true zksync_state.workspace = true +zksync_vm_executor.workspace = true zksync_vm_interface.workspace = true anyhow.workspace = true async-trait.workspace = true @@ -41,7 +41,6 @@ secrecy.workspace = true tempfile.workspace = true thiserror.workspace = true tracing.workspace = true -hex.workspace = true tokio.workspace = true semver.workspace = true diff --git a/core/node/consensus/src/storage/connection.rs b/core/node/consensus/src/storage/connection.rs index 2c297eed7275..0f9d7c8527f3 100644 --- a/core/node/consensus/src/storage/connection.rs +++ b/core/node/consensus/src/storage/connection.rs @@ -5,10 +5,12 @@ use zksync_consensus_roles::{attester, attester::BatchNumber, validator}; use zksync_consensus_storage::{self as storage, BatchStoreState}; use zksync_dal::{consensus_dal, consensus_dal::Payload, Core, CoreDal, DalError}; use zksync_l1_contract_interface::i_executor::structures::StoredBatchInfo; -use zksync_node_api_server::execution_sandbox::{BlockArgs, BlockStartInfo}; use zksync_node_sync::{fetcher::IoCursorExt as _, ActionQueueSender, SyncState}; use zksync_state_keeper::io::common::IoCursor; -use zksync_types::{api, commitment::L1BatchWithMetadata, L1BatchNumber}; +use zksync_types::{ + commitment::L1BatchWithMetadata, fee_model::BatchFeeInput, L1BatchNumber, L2BlockNumber, +}; +use zksync_vm_executor::oneshot::{BlockInfo, ResolvedBlockInfo}; use super::{InsertCertificateError, PayloadQueue}; use crate::config; @@ -470,27 +472,30 @@ impl<'a> Connection<'a> { } /// Constructs `BlockArgs` for the last block of the batch. - pub async fn vm_block_args( + pub async fn vm_block_info( &mut self, ctx: &ctx::Ctx, batch: attester::BatchNumber, - ) -> ctx::Result { + ) -> ctx::Result<(ResolvedBlockInfo, BatchFeeInput)> { let (_, block) = self .get_l2_block_range_of_l1_batch(ctx, batch) .await .wrap("get_l2_block_range_of_l1_batch()")? .context("batch not sealed")?; - let block = api::BlockId::Number(api::BlockNumber::Number(block.0.into())); - let start_info = ctx - .wait(BlockStartInfo::new( - &mut self.0, - /*max_cache_age=*/ std::time::Duration::from_secs(10), - )) + // `unwrap()` is safe: the block range is returned as `L2BlockNumber`s + let block = L2BlockNumber(u32::try_from(block.0).unwrap()); + let block_info = ctx + .wait(BlockInfo::for_existing_block(&mut self.0, block)) .await? - .context("BlockStartInfo::new()")?; - Ok(ctx - .wait(BlockArgs::new(&mut self.0, block, &start_info)) + .context("BlockInfo")?; + let resolved_block_info = ctx + .wait(block_info.resolve(&mut self.0)) + .await? + .context("resolve()")?; + let fee_input = ctx + .wait(block_info.historical_fee_input(&mut self.0)) .await? - .context("BlockArgs::new")?) + .context("historical_fee_input()")?; + Ok((resolved_block_info, fee_input)) } } diff --git a/core/node/consensus/src/vm.rs b/core/node/consensus/src/vm.rs index c93cafc09f9c..149e6b3ccb03 100644 --- a/core/node/consensus/src/vm.rs +++ b/core/node/consensus/src/vm.rs @@ -1,17 +1,13 @@ use anyhow::Context as _; -use zksync_concurrency::{ctx, error::Wrap as _, scope}; +use tokio::runtime::Handle; +use zksync_concurrency::{ctx, error::Wrap as _}; use zksync_consensus_roles::attester; -use zksync_node_api_server::{ - execution_sandbox::{TransactionExecutor, TxSetupArgs, VmConcurrencyLimiter}, - tx_sender::MultiVMBaseSystemContracts, -}; -use zksync_state::PostgresStorageCaches; +use zksync_state::PostgresStorage; use zksync_system_constants::DEFAULT_L2_TX_GAS_PER_PUBDATA_BYTE; -use zksync_types::{ - ethabi, fee::Fee, fee_model::BatchFeeInput, l2::L2Tx, AccountTreeId, L2ChainId, Nonce, U256, -}; +use zksync_types::{ethabi, fee::Fee, l2::L2Tx, AccountTreeId, L2ChainId, Nonce, U256}; +use zksync_vm_executor::oneshot::{CallOrExecute, MainOneshotExecutor, OneshotEnvParameters}; use zksync_vm_interface::{ - ExecutionResult, OneshotTracingParams, TxExecutionArgs, TxExecutionMode, + executor::OneshotExecutor, ExecutionResult, OneshotTracingParams, TxExecutionArgs, }; use crate::{abi, storage::ConnectionPool}; @@ -20,8 +16,8 @@ use crate::{abi, storage::ConnectionPool}; #[derive(Debug)] pub(crate) struct VM { pool: ConnectionPool, - setup_args: TxSetupArgs, - limiter: VmConcurrencyLimiter, + options: OneshotEnvParameters, + executor: MainOneshotExecutor, } impl VM { @@ -29,25 +25,18 @@ impl VM { pub async fn new(pool: ConnectionPool) -> Self { Self { pool, - setup_args: TxSetupArgs { - execution_mode: TxExecutionMode::EthCall, - operator_account: AccountTreeId::default(), - fee_input: BatchFeeInput::sensible_l1_pegged_default(), - base_system_contracts: scope::wait_blocking( - MultiVMBaseSystemContracts::load_eth_call_blocking, - ) - .await, - caches: PostgresStorageCaches::new(1, 1), - validation_computational_gas_limit: u32::MAX, - chain_id: L2ChainId::default(), - whitelisted_tokens_for_aa: vec![], - enforced_base_fee: None, - }, - limiter: VmConcurrencyLimiter::new(1).0, + // L2 chain ID and fee account don't seem to matter for calls, hence the use of default values. + options: OneshotEnvParameters::for_execution( + L2ChainId::default(), + AccountTreeId::default(), + u32::MAX, + ) + .await + .expect("OneshotExecutorOptions"), + executor: MainOneshotExecutor::new(usize::MAX), } } - // FIXME (PLA-1018): switch to oneshot executor pub async fn call( &self, ctx: &ctx::Ctx, @@ -70,25 +59,39 @@ impl VM { vec![], Default::default(), ); - let permit = ctx.wait(self.limiter.acquire()).await?.unwrap(); + let mut conn = self.pool.connection(ctx).await.wrap("connection()")?; - let args = conn - .vm_block_args(ctx, batch) + let (block_info, fee_input) = conn + .vm_block_info(ctx, batch) .await - .wrap("vm_block_args()")?; - let output = ctx - .wait(TransactionExecutor::real(usize::MAX).execute_tx_in_sandbox( - permit, - self.setup_args.clone(), - TxExecutionArgs::for_eth_call(tx.clone()), + .wrap("vm_block_info()")?; + let env = ctx + .wait( + self.options + .to_call_env(&mut conn.0, &block_info, fee_input, None), + ) + .await? + .context("to_env()")?; + let storage = ctx + .wait(PostgresStorage::new_async( + Handle::current(), conn.0, - args, - None, + block_info.state_l2_block_number(), + false, + )) + .await? + .context("PostgresStorage")?; + + let output = ctx + .wait(self.executor.inspect_transaction_with_bytecode_compression( + storage, + env, + TxExecutionArgs::for_eth_call(tx), OneshotTracingParams::default(), )) .await? .context("execute_tx_in_sandbox()")?; - match output.vm.result { + match output.tx_result.result { ExecutionResult::Success { output } => { Ok(call.decode_outputs(&output).context("decode_output()")?) } diff --git a/core/node/fee_model/src/lib.rs b/core/node/fee_model/src/lib.rs index 217ed71e38cb..fe4f6a27ce29 100644 --- a/core/node/fee_model/src/lib.rs +++ b/core/node/fee_model/src/lib.rs @@ -34,8 +34,27 @@ pub trait BatchFeeModelInputProvider: fmt::Debug + 'static + Send + Sync { l1_pubdata_price_scale_factor: f64, ) -> anyhow::Result { let params = self.get_fee_model_params(); + Ok( + ::default_batch_fee_input_scaled( + params, + l1_gas_price_scale_factor, + l1_pubdata_price_scale_factor, + ), + ) + } + + /// Returns the fee model parameters using the denomination of the base token used (WEI for ETH). + fn get_fee_model_params(&self) -> FeeParams; +} - Ok(match params { +impl dyn BatchFeeModelInputProvider { + /// Provides the default implementation of `get_batch_fee_input_scaled()` given [`FeeParams`]. + pub fn default_batch_fee_input_scaled( + params: FeeParams, + l1_gas_price_scale_factor: f64, + l1_pubdata_price_scale_factor: f64, + ) -> BatchFeeInput { + match params { FeeParams::V1(params) => BatchFeeInput::L1Pegged(compute_batch_fee_model_input_v1( params, l1_gas_price_scale_factor, @@ -47,14 +66,9 @@ pub trait BatchFeeModelInputProvider: fmt::Debug + 'static + Send + Sync { l1_pubdata_price_scale_factor, )), ), - }) + } } - /// Returns the fee model parameters using the denomination of the base token used (WEI for ETH). - fn get_fee_model_params(&self) -> FeeParams; -} - -impl dyn BatchFeeModelInputProvider { /// Returns the batch fee input as-is, i.e. without any scaling for the L1 gas and pubdata prices. pub async fn get_batch_fee_input(&self) -> anyhow::Result { self.get_batch_fee_input_scaled(1.0, 1.0).await diff --git a/core/node/node_framework/src/implementations/layers/web3_api/tx_sender.rs b/core/node/node_framework/src/implementations/layers/web3_api/tx_sender.rs index 3574b8e8c24c..a09938055fae 100644 --- a/core/node/node_framework/src/implementations/layers/web3_api/tx_sender.rs +++ b/core/node/node_framework/src/implementations/layers/web3_api/tx_sender.rs @@ -3,10 +3,10 @@ use std::{sync::Arc, time::Duration}; use tokio::sync::RwLock; use zksync_node_api_server::{ execution_sandbox::{VmConcurrencyBarrier, VmConcurrencyLimiter}, - tx_sender::{ApiContracts, TxSenderBuilder, TxSenderConfig}, + tx_sender::{SandboxExecutorOptions, TxSenderBuilder, TxSenderConfig}, }; use zksync_state::{PostgresStorageCaches, PostgresStorageCachesTask}; -use zksync_types::Address; +use zksync_types::{AccountTreeId, Address}; use zksync_web3_decl::{ client::{DynClient, L2}, jsonrpsee, @@ -58,7 +58,6 @@ pub struct TxSenderLayer { tx_sender_config: TxSenderConfig, postgres_storage_caches_config: PostgresStorageCachesConfig, max_vm_concurrency: usize, - api_contracts: ApiContracts, whitelisted_tokens_for_aa_cache: bool, } @@ -89,13 +88,11 @@ impl TxSenderLayer { tx_sender_config: TxSenderConfig, postgres_storage_caches_config: PostgresStorageCachesConfig, max_vm_concurrency: usize, - api_contracts: ApiContracts, ) -> Self { Self { tx_sender_config, postgres_storage_caches_config, max_vm_concurrency, - api_contracts, whitelisted_tokens_for_aa_cache: false, } } @@ -148,8 +145,17 @@ impl WiringLayer for TxSenderLayer { let (vm_concurrency_limiter, vm_concurrency_barrier) = VmConcurrencyLimiter::new(self.max_vm_concurrency); + // TODO (BFT-138): Allow to dynamically reload API contracts + let config = self.tx_sender_config; + let executor_options = SandboxExecutorOptions::new( + config.chain_id, + AccountTreeId::new(config.fee_account_addr), + config.validation_computational_gas_limit, + ) + .await?; + // Build `TxSender`. - let mut tx_sender = TxSenderBuilder::new(self.tx_sender_config, replica_pool, tx_sink); + let mut tx_sender = TxSenderBuilder::new(config, replica_pool, tx_sink); if let Some(sealer) = sealer { tx_sender = tx_sender.with_sealer(sealer); } @@ -176,7 +182,7 @@ impl WiringLayer for TxSenderLayer { let tx_sender = tx_sender.build( fee_input, Arc::new(vm_concurrency_limiter), - self.api_contracts, + executor_options, storage_caches, );