Skip to content

Commit

Permalink
feat(api): Integrate new VM into API server (no tracers) (#3033)
Browse files Browse the repository at this point in the history
## What ❔

Integrates the new VM into API server for 3 use cases: gas estimation,
calls, and tx execution (without the validation stage).

## Why ❔

These use cases do not require tracers and could benefit from faster VM
execution (particularly gas estimation, which runs the VM multiple
times).

## 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_supervisor fmt` and `zk_supervisor
lint`.

---------

Co-authored-by: Joonatan Saarhelo <[email protected]>
  • Loading branch information
slowli and joonazan authored Oct 25, 2024
1 parent c76da16 commit 8e75d4b
Show file tree
Hide file tree
Showing 34 changed files with 721 additions and 262 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/ci-core-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
- name: Loadtest configuration
run: |
echo EXPECTED_TX_COUNT=${{ matrix.vm_mode == 'NEW' && 21000 || 16000 }} >> .env
echo EXPECTED_TX_COUNT=${{ matrix.vm_mode == 'NEW' && 30000 || 16000 }} >> .env
echo ACCOUNTS_AMOUNT="100" >> .env
echo MAX_INFLIGHT_TXS="10" >> .env
echo SYNC_API_REQUESTS_LIMIT="15" >> .env
Expand Down Expand Up @@ -360,12 +360,16 @@ jobs:
- name: Run servers
run: |
# Override config for part of chains to test the default config as well
ci_run zkstack dev config-writer --path etc/env/file_based/overrides/tests/integration.yaml --chain era
ci_run zkstack dev config-writer --path etc/env/file_based/overrides/tests/integration.yaml --chain validium
ci_run zkstack server --ignore-prerequisites --chain era &> ${{ env.SERVER_LOGS_DIR }}/rollup.log &
ci_run zkstack server --ignore-prerequisites --chain validium &> ${{ env.SERVER_LOGS_DIR }}/validium.log &
ci_run zkstack server --ignore-prerequisites --chain custom_token &> ${{ env.SERVER_LOGS_DIR }}/custom_token.log &
ci_run zkstack server --ignore-prerequisites --chain consensus \
--components=api,tree,eth,state_keeper,housekeeper,commitment_generator,vm_runner_protective_reads,vm_runner_bwip,vm_playground,da_dispatcher,consensus \
&> ${{ env.SERVER_LOGS_DIR }}/consensus.log &
--components=api,tree,eth,state_keeper,housekeeper,commitment_generator,vm_runner_protective_reads,vm_runner_bwip,vm_playground,da_dispatcher,consensus \
&> ${{ env.SERVER_LOGS_DIR }}/consensus.log &
ci_run sleep 5
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions core/bin/zksync_server/src/node_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,12 @@ impl MainNodeBuilder {
latest_values_cache_size: rpc_config.latest_values_cache_size() as u64,
latest_values_max_block_lag: rpc_config.latest_values_max_block_lag(),
};
let vm_config = try_load_config!(self.configs.experimental_vm_config);

// On main node we always use master pool sink.
self.node.add_layer(MasterPoolSinkLayer);
self.node.add_layer(TxSenderLayer::new(

let layer = TxSenderLayer::new(
TxSenderConfig::new(
&sk_config,
&rpc_config,
Expand All @@ -323,7 +325,9 @@ impl MainNodeBuilder {
),
postgres_storage_caches_config,
rpc_config.vm_concurrency_limit(),
));
);
let layer = layer.with_vm_mode(vm_config.api_fast_vm_mode);
self.node.add_layer(layer);
Ok(self)
}

Expand Down
5 changes: 5 additions & 0 deletions core/lib/config/src/configs/experimental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,9 @@ pub struct ExperimentalVmConfig {
/// the new VM doesn't produce call traces and can diverge from the old VM!
#[serde(default)]
pub state_keeper_fast_vm_mode: FastVmMode,

/// Fast VM mode to use in the API server. Currently, some operations are not supported by the fast VM (e.g., `debug_traceCall`
/// or transaction validation), so the legacy VM will always be used for them.
#[serde(default)]
pub api_fast_vm_mode: FastVmMode,
}
1 change: 1 addition & 0 deletions core/lib/config/src/testonly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ impl Distribution<configs::ExperimentalVmConfig> for EncodeDist {
configs::ExperimentalVmConfig {
playground: self.sample(rng),
state_keeper_fast_vm_mode: gen_fast_vm_mode(rng),
api_fast_vm_mode: gen_fast_vm_mode(rng),
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions core/lib/env_config/src/vm_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ mod tests {
let mut lock = MUTEX.lock();
let config = r#"
EXPERIMENTAL_VM_STATE_KEEPER_FAST_VM_MODE=new
EXPERIMENTAL_VM_API_FAST_VM_MODE=shadow
EXPERIMENTAL_VM_PLAYGROUND_FAST_VM_MODE=shadow
EXPERIMENTAL_VM_PLAYGROUND_DB_PATH=/db/vm_playground
EXPERIMENTAL_VM_PLAYGROUND_FIRST_PROCESSED_BATCH=123
Expand All @@ -64,6 +65,7 @@ mod tests {

let config = ExperimentalVmConfig::from_env().unwrap();
assert_eq!(config.state_keeper_fast_vm_mode, FastVmMode::New);
assert_eq!(config.api_fast_vm_mode, FastVmMode::Shadow);
assert_eq!(config.playground.fast_vm_mode, FastVmMode::Shadow);
assert_eq!(config.playground.db_path.unwrap(), "/db/vm_playground");
assert_eq!(config.playground.first_processed_batch, L1BatchNumber(123));
Expand Down
49 changes: 46 additions & 3 deletions core/lib/multivm/src/versions/testonly/l1_tx_execution.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
use assert_matches::assert_matches;
use ethabi::Token;
use zksync_contracts::l1_messenger_contract;
use zksync_system_constants::{BOOTLOADER_ADDRESS, L1_MESSENGER_ADDRESS};
use zksync_test_account::TxType;
use zksync_types::{
get_code_key, get_known_code_key,
l2_to_l1_log::{L2ToL1Log, UserL2ToL1Log},
Execute, ExecuteTransactionCommon, U256,
Address, Execute, ExecuteTransactionCommon, U256,
};
use zksync_utils::{h256_to_u256, u256_to_h256};

use super::{read_test_contract, tester::VmTesterBuilder, TestedVm, BASE_SYSTEM_CONTRACTS};
use super::{
read_test_contract, tester::VmTesterBuilder, ContractToDeploy, TestedVm, BASE_SYSTEM_CONTRACTS,
};
use crate::{
interface::{InspectExecutionMode, TxExecutionMode, VmInterfaceExt},
interface::{
ExecutionResult, InspectExecutionMode, TxExecutionMode, VmInterfaceExt, VmRevertReason,
},
utils::StorageWritesDeduplicator,
};

Expand Down Expand Up @@ -180,3 +185,41 @@ pub(crate) fn test_l1_tx_execution_high_gas_limit<VM: TestedVm>() {

assert!(res.result.is_failed(), "The transaction should've failed");
}

pub(crate) fn test_l1_tx_execution_gas_estimation_with_low_gas<VM: TestedVm>() {
let counter_contract = read_test_contract();
let counter_address = Address::repeat_byte(0x11);
let mut vm = VmTesterBuilder::new()
.with_empty_in_memory_storage()
.with_base_system_smart_contracts(BASE_SYSTEM_CONTRACTS.clone())
.with_execution_mode(TxExecutionMode::EstimateFee)
.with_custom_contracts(vec![ContractToDeploy::new(
counter_contract,
counter_address,
)])
.with_rich_accounts(1)
.build::<VM>();

let account = &mut vm.rich_accounts[0];
let mut tx = account.get_test_contract_transaction(
counter_address,
false,
None,
false,
TxType::L1 { serial_id: 0 },
);
let ExecuteTransactionCommon::L1(data) = &mut tx.common_data else {
unreachable!();
};
// This gas limit is chosen so that transaction starts getting executed by the bootloader, but then runs out of gas
// before its execution result is posted.
data.gas_limit = 15_000.into();

vm.vm.push_transaction(tx);
let res = vm.vm.execute(InspectExecutionMode::OneTx);
assert_matches!(
&res.result,
ExecutionResult::Revert { output: VmRevertReason::General { msg, .. } }
if msg.contains("reverted with empty reason")
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
versions::testonly::l1_tx_execution::{
test_l1_tx_execution, test_l1_tx_execution_high_gas_limit,
test_l1_tx_execution, test_l1_tx_execution_gas_estimation_with_low_gas,
test_l1_tx_execution_high_gas_limit,
},
vm_fast::Vm,
};
Expand All @@ -14,3 +15,8 @@ fn l1_tx_execution() {
fn l1_tx_execution_high_gas_limit() {
test_l1_tx_execution_high_gas_limit::<Vm<_>>();
}

#[test]
fn l1_tx_execution_gas_estimation_with_low_gas() {
test_l1_tx_execution_gas_estimation_with_low_gas::<Vm<_>>();
}
27 changes: 24 additions & 3 deletions core/lib/multivm/src/versions/vm_fast/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ use crate::{
},
vm_latest::{
constants::{
get_vm_hook_params_start_position, get_vm_hook_position, OPERATOR_REFUNDS_OFFSET,
TX_GAS_LIMIT_OFFSET, VM_HOOK_PARAMS_COUNT,
get_result_success_first_slot, get_vm_hook_params_start_position, get_vm_hook_position,
OPERATOR_REFUNDS_OFFSET, TX_GAS_LIMIT_OFFSET, VM_HOOK_PARAMS_COUNT,
},
MultiVMSubversion,
},
Expand Down Expand Up @@ -213,7 +213,22 @@ impl<S: ReadStorage, Tr: Tracer + Default> Vm<S, Tr> {
}
Hook::TxHasEnded => {
if let VmExecutionMode::OneTx = execution_mode {
break (last_tx_result.take().unwrap(), false);
// The bootloader may invoke `TxHasEnded` hook without posting a tx result previously. One case when this can happen
// is estimating gas for L1 transactions, if a transaction runs out of gas during execution.
let tx_result = last_tx_result.take().unwrap_or_else(|| {
let tx_has_failed = self.get_tx_result().is_zero();
if tx_has_failed {
let output = VmRevertReason::General {
msg: "Transaction reverted with empty reason. Possibly out of gas"
.to_string(),
data: vec![],
};
ExecutionResult::Revert { output }
} else {
ExecutionResult::Success { output: vec![] }
}
});
break (tx_result, false);
}
}
Hook::AskOperatorForRefund => {
Expand Down Expand Up @@ -361,6 +376,12 @@ impl<S: ReadStorage, Tr: Tracer + Default> Vm<S, Tr> {
.unwrap()
}

fn get_tx_result(&self) -> U256 {
let tx_idx = self.bootloader_state.current_tx();
let slot = get_result_success_first_slot(VM_VERSION) as usize + tx_idx;
self.read_word_from_bootloader_heap(slot)
}

fn get_debug_log(&self) -> (String, String) {
let hook_params = self.get_hook_params();
let mut msg = u256_to_h256(hook_params[0]).as_bytes().to_vec();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
versions::testonly::l1_tx_execution::{
test_l1_tx_execution, test_l1_tx_execution_high_gas_limit,
test_l1_tx_execution, test_l1_tx_execution_gas_estimation_with_low_gas,
test_l1_tx_execution_high_gas_limit,
},
vm_latest::{HistoryEnabled, Vm},
};
Expand All @@ -14,3 +15,8 @@ fn l1_tx_execution() {
fn l1_tx_execution_high_gas_limit() {
test_l1_tx_execution_high_gas_limit::<Vm<_, HistoryEnabled>>();
}

#[test]
fn l1_tx_execution_gas_estimation_with_low_gas() {
test_l1_tx_execution_gas_estimation_with_low_gas::<Vm<_, HistoryEnabled>>();
}
2 changes: 1 addition & 1 deletion core/lib/multivm/src/vm_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ pub type ShadowedFastVm<S, Tr = ()> = ShadowVm<

/// Fast VM variants.
#[derive(Debug)]
pub enum FastVmInstance<S: ReadStorage, Tr> {
pub enum FastVmInstance<S: ReadStorage, Tr = ()> {
/// Fast VM running in isolation.
Fast(crate::vm_fast::Vm<ImmutableStorageView<S>, Tr>),
/// Fast VM shadowed by the latest legacy VM.
Expand Down
17 changes: 11 additions & 6 deletions core/lib/protobuf_config/src/experimental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ use zksync_protobuf::{repr::ProtoRepr, required};

use crate::{proto::experimental as proto, read_optional_repr};

fn parse_vm_mode(raw: Option<i32>) -> anyhow::Result<FastVmMode> {
Ok(raw
.map(proto::FastVmMode::try_from)
.transpose()
.context("fast_vm_mode")?
.map_or_else(FastVmMode::default, |mode| mode.parse()))
}

impl ProtoRepr for proto::Db {
type Type = configs::ExperimentalDBConfig;

Expand Down Expand Up @@ -105,12 +113,8 @@ impl ProtoRepr for proto::Vm {
fn read(&self) -> anyhow::Result<Self::Type> {
Ok(Self::Type {
playground: read_optional_repr(&self.playground).unwrap_or_default(),
state_keeper_fast_vm_mode: self
.state_keeper_fast_vm_mode
.map(proto::FastVmMode::try_from)
.transpose()
.context("fast_vm_mode")?
.map_or_else(FastVmMode::default, |mode| mode.parse()),
state_keeper_fast_vm_mode: parse_vm_mode(self.state_keeper_fast_vm_mode)?,
api_fast_vm_mode: parse_vm_mode(self.api_fast_vm_mode)?,
})
}

Expand All @@ -120,6 +124,7 @@ impl ProtoRepr for proto::Vm {
state_keeper_fast_vm_mode: Some(
proto::FastVmMode::new(this.state_keeper_fast_vm_mode).into(),
),
api_fast_vm_mode: Some(proto::FastVmMode::new(this.api_fast_vm_mode).into()),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ message VmPlayground {
message Vm {
optional VmPlayground playground = 1; // optional
optional FastVmMode state_keeper_fast_vm_mode = 2; // optional; if not set, fast VM is not used
optional FastVmMode api_fast_vm_mode = 3; // optional; if not set, fast VM is not used
}
1 change: 1 addition & 0 deletions core/lib/vm_executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ vise.workspace = true

[dev-dependencies]
assert_matches.workspace = true
test-casing.workspace = true
16 changes: 12 additions & 4 deletions core/lib/vm_executor/src/oneshot/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub(super) fn report_vm_memory_metrics(
tx_id: &str,
memory_metrics: &VmMemoryMetrics,
vm_execution_took: Duration,
storage_metrics: &StorageViewStats,
storage_stats: &StorageViewStats,
) {
MEMORY_METRICS.event_sink_size[&SizeType::Inner].observe(memory_metrics.event_sink_inner);
MEMORY_METRICS.event_sink_size[&SizeType::History].observe(memory_metrics.event_sink_history);
Expand All @@ -65,10 +65,18 @@ pub(super) fn report_vm_memory_metrics(

MEMORY_METRICS
.storage_view_cache_size
.observe(storage_metrics.cache_size);
.observe(storage_stats.cache_size);
MEMORY_METRICS
.full
.observe(memory_metrics.full_size() + storage_metrics.cache_size);
.observe(memory_metrics.full_size() + storage_stats.cache_size);

STORAGE_METRICS.observe(&format!("Tx {tx_id}"), vm_execution_took, storage_metrics);
report_vm_storage_metrics(tx_id, vm_execution_took, storage_stats);
}

pub(super) fn report_vm_storage_metrics(
tx_id: &str,
vm_execution_took: Duration,
storage_stats: &StorageViewStats,
) {
STORAGE_METRICS.observe(&format!("Tx {tx_id}"), vm_execution_took, storage_stats);
}
Loading

0 comments on commit 8e75d4b

Please sign in to comment.