Skip to content

Commit d01840d

Browse files
authored
feat(vm-runner): Implement batch data prefetching (#2724)
## What ❔ - Implements prefetching of storage slots / bytecodes accessed by a VM in a batch. Enables it for the VM playground. Optionally shadows prefetched snapshot storage. - Makes RocksDB cache optional for VM playground. ## Why ❔ - Prefetching will allow to load storage slots / bytecodes for a batch in O(1) DB queries, which is very efficient for local debugging etc. It may be on par or faster than using RocksDB cache. (There's a caveat: prefetching doesn't work w/o protective reads.) - Disabling RocksDB cache is useful for local testing, since the cache won't catch up during a single batch run anyway. ## 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`.
1 parent 755fc4a commit d01840d

File tree

23 files changed

+734
-206
lines changed

23 files changed

+734
-206
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/lib/config/src/configs/experimental.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ pub struct ExperimentalVmPlaygroundConfig {
6565
#[serde(default)]
6666
pub fast_vm_mode: FastVmMode,
6767
/// Path to the RocksDB cache directory.
68-
#[serde(default = "ExperimentalVmPlaygroundConfig::default_db_path")]
69-
pub db_path: String,
68+
pub db_path: Option<String>,
7069
/// First L1 batch to consider processed. Will not be used if the processing cursor is persisted, unless the `reset` flag is set.
7170
#[serde(default)]
7271
pub first_processed_batch: L1BatchNumber,
@@ -83,7 +82,7 @@ impl Default for ExperimentalVmPlaygroundConfig {
8382
fn default() -> Self {
8483
Self {
8584
fast_vm_mode: FastVmMode::default(),
86-
db_path: Self::default_db_path(),
85+
db_path: None,
8786
first_processed_batch: L1BatchNumber(0),
8887
window_size: Self::default_window_size(),
8988
reset: false,
@@ -92,10 +91,6 @@ impl Default for ExperimentalVmPlaygroundConfig {
9291
}
9392

9493
impl ExperimentalVmPlaygroundConfig {
95-
pub fn default_db_path() -> String {
96-
"./db/vm_playground".to_owned()
97-
}
98-
9994
pub fn default_window_size() -> NonZeroU32 {
10095
NonZeroU32::new(1).unwrap()
10196
}

core/lib/env_config/src/vm_runner.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ mod tests {
6565
let config = ExperimentalVmConfig::from_env().unwrap();
6666
assert_eq!(config.state_keeper_fast_vm_mode, FastVmMode::New);
6767
assert_eq!(config.playground.fast_vm_mode, FastVmMode::Shadow);
68-
assert_eq!(config.playground.db_path, "/db/vm_playground");
68+
assert_eq!(config.playground.db_path.unwrap(), "/db/vm_playground");
6969
assert_eq!(config.playground.first_processed_batch, L1BatchNumber(123));
7070
assert!(config.playground.reset);
7171

@@ -83,6 +83,6 @@ mod tests {
8383

8484
lock.remove_env(&["EXPERIMENTAL_VM_PLAYGROUND_DB_PATH"]);
8585
let config = ExperimentalVmConfig::from_env().unwrap();
86-
assert!(!config.playground.db_path.is_empty());
86+
assert!(config.playground.db_path.is_none());
8787
}
8888
}

core/lib/protobuf_config/src/experimental.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,7 @@ impl ProtoRepr for proto::VmPlayground {
8080
.transpose()
8181
.context("fast_vm_mode")?
8282
.map_or_else(FastVmMode::default, |mode| mode.parse()),
83-
db_path: self
84-
.db_path
85-
.clone()
86-
.unwrap_or_else(Self::Type::default_db_path),
83+
db_path: self.db_path.clone(),
8784
first_processed_batch: L1BatchNumber(self.first_processed_batch.unwrap_or(0)),
8885
window_size: NonZeroU32::new(self.window_size.unwrap_or(1))
8986
.context("window_size cannot be 0")?,
@@ -94,7 +91,7 @@ impl ProtoRepr for proto::VmPlayground {
9491
fn build(this: &Self::Type) -> Self {
9592
Self {
9693
fast_vm_mode: Some(proto::FastVmMode::new(this.fast_vm_mode).into()),
97-
db_path: Some(this.db_path.clone()),
94+
db_path: this.db_path.clone(),
9895
first_processed_batch: Some(this.first_processed_batch.0),
9996
window_size: Some(this.window_size.get()),
10097
reset: Some(this.reset),

core/lib/protobuf_config/src/proto/config/experimental.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ enum FastVmMode {
2828
// Experimental VM configuration
2929
message VmPlayground {
3030
optional FastVmMode fast_vm_mode = 1; // optional; if not set, fast VM is not used
31-
optional string db_path = 2; // optional; defaults to `./db/vm_playground`
31+
optional string db_path = 2; // optional; if not set, playground will not use RocksDB cache
3232
optional uint32 first_processed_batch = 3; // optional; defaults to 0
3333
optional bool reset = 4; // optional; defaults to false
3434
optional uint32 window_size = 5; // optional; non-zero; defaults to 1

core/lib/state/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub use self::{
2020
},
2121
shadow_storage::ShadowStorage,
2222
storage_factory::{
23-
BatchDiff, OwnedStorage, PgOrRocksdbStorage, ReadStorageFactory, RocksdbWithMemory,
23+
BatchDiff, CommonStorage, OwnedStorage, ReadStorageFactory, RocksdbWithMemory,
2424
},
2525
};
2626

core/lib/state/src/rocksdb/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ impl RocksdbStorage {
347347
let to_l1_batch_number = if let Some(to_l1_batch_number) = to_l1_batch_number {
348348
if to_l1_batch_number > latest_l1_batch_number {
349349
let err = anyhow::anyhow!(
350-
"Requested to update RocksDB to L1 batch number ({current_l1_batch_number}) that \
350+
"Requested to update RocksDB to L1 batch number ({to_l1_batch_number}) that \
351351
is greater than the last sealed L1 batch number in Postgres ({latest_l1_batch_number})"
352352
);
353353
return Err(err.into());

core/lib/state/src/shadow_storage.rs

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use std::fmt;
2+
13
use vise::{Counter, Metrics};
24
use zksync_types::{L1BatchNumber, StorageKey, StorageValue, H256};
35
use zksync_vm_interface::storage::ReadStorage;
46

5-
#[allow(clippy::struct_field_names)]
67
#[derive(Debug, Metrics)]
78
#[metrics(prefix = "shadow_storage")]
9+
#[allow(clippy::struct_field_names)] // false positive
810
struct ShadowStorageMetrics {
911
/// Number of mismatches when reading a value from a shadow storage.
1012
read_value_mismatch: Counter,
@@ -19,60 +21,78 @@ struct ShadowStorageMetrics {
1921
#[vise::register]
2022
static METRICS: vise::Global<ShadowStorageMetrics> = vise::Global::new();
2123

22-
/// [`ReadStorage`] implementation backed by 2 different backends:
23-
/// source_storage -- backend that will return values for function calls and be the source of truth
24-
/// to_check_storage -- secondary storage, which will verify it's own return values against source_storage
25-
/// Note that if to_check_storage value is different than source value, execution continues and metrics/ logs are emitted.
24+
/// [`ReadStorage`] implementation backed by 2 different backends which are compared for each performed operation.
25+
///
26+
/// - `Ref` is the backend that will return values for function calls and be the source of truth
27+
/// - `Check` is the secondary storage, which will have its return values verified against `Ref`
28+
///
29+
/// If `Check` value is different from a value from `Ref`, storage behavior depends on the [panic on divergence](Self::set_panic_on_divergence()) flag.
30+
/// If this flag is set (which it is by default), the storage panics; otherwise, execution continues and metrics / logs are emitted.
2631
#[derive(Debug)]
27-
pub struct ShadowStorage<'a> {
28-
source_storage: Box<dyn ReadStorage + 'a>,
29-
to_check_storage: Box<dyn ReadStorage + 'a>,
30-
metrics: &'a ShadowStorageMetrics,
32+
pub struct ShadowStorage<Ref, Check> {
33+
source_storage: Ref,
34+
to_check_storage: Check,
35+
metrics: &'static ShadowStorageMetrics,
3136
l1_batch_number: L1BatchNumber,
37+
panic_on_divergence: bool,
3238
}
3339

34-
impl<'a> ShadowStorage<'a> {
40+
impl<Ref: ReadStorage, Check: ReadStorage> ShadowStorage<Ref, Check> {
3541
/// Creates a new storage using the 2 underlying [`ReadStorage`]s, first as source, the second to be checked
3642
/// against the source.
3743
pub fn new(
38-
source_storage: Box<dyn ReadStorage + 'a>,
39-
to_check_storage: Box<dyn ReadStorage + 'a>,
44+
source_storage: Ref,
45+
to_check_storage: Check,
4046
l1_batch_number: L1BatchNumber,
4147
) -> Self {
4248
Self {
4349
source_storage,
4450
to_check_storage,
4551
metrics: &METRICS,
4652
l1_batch_number,
53+
panic_on_divergence: true,
54+
}
55+
}
56+
57+
/// Sets behavior if a storage divergence is detected.
58+
pub fn set_panic_on_divergence(&mut self, panic_on_divergence: bool) {
59+
self.panic_on_divergence = panic_on_divergence;
60+
}
61+
62+
fn error_or_panic(&self, args: fmt::Arguments<'_>) {
63+
if self.panic_on_divergence {
64+
panic!("{args}");
65+
} else {
66+
tracing::error!(l1_batch_number = self.l1_batch_number.0, "{args}");
4767
}
4868
}
4969
}
5070

51-
impl ReadStorage for ShadowStorage<'_> {
71+
impl<Ref: ReadStorage, Check: ReadStorage> ReadStorage for ShadowStorage<Ref, Check> {
5272
fn read_value(&mut self, key: &StorageKey) -> StorageValue {
53-
let source_value = self.source_storage.as_mut().read_value(key);
54-
let expected_value = self.to_check_storage.as_mut().read_value(key);
73+
let source_value = self.source_storage.read_value(key);
74+
let expected_value = self.to_check_storage.read_value(key);
5575
if source_value != expected_value {
5676
self.metrics.read_value_mismatch.inc();
57-
tracing::error!(
77+
self.error_or_panic(format_args!(
5878
"read_value({key:?}) -- l1_batch_number={:?} -- expected source={source_value:?} \
5979
to be equal to to_check={expected_value:?}",
6080
self.l1_batch_number
61-
);
81+
));
6282
}
6383
source_value
6484
}
6585

6686
fn is_write_initial(&mut self, key: &StorageKey) -> bool {
67-
let source_value = self.source_storage.as_mut().is_write_initial(key);
68-
let expected_value = self.to_check_storage.as_mut().is_write_initial(key);
87+
let source_value = self.source_storage.is_write_initial(key);
88+
let expected_value = self.to_check_storage.is_write_initial(key);
6989
if source_value != expected_value {
7090
self.metrics.is_write_initial_mismatch.inc();
71-
tracing::error!(
91+
self.error_or_panic(format_args!(
7292
"is_write_initial({key:?}) -- l1_batch_number={:?} -- expected source={source_value:?} \
7393
to be equal to to_check={expected_value:?}",
7494
self.l1_batch_number
75-
);
95+
));
7696
}
7797
source_value
7898
}
@@ -82,25 +102,25 @@ impl ReadStorage for ShadowStorage<'_> {
82102
let expected_value = self.to_check_storage.load_factory_dep(hash);
83103
if source_value != expected_value {
84104
self.metrics.load_factory_dep_mismatch.inc();
85-
tracing::error!(
105+
self.error_or_panic(format_args!(
86106
"load_factory_dep({hash:?}) -- l1_batch_number={:?} -- expected source={source_value:?} \
87107
to be equal to to_check={expected_value:?}",
88-
self.l1_batch_number
89-
);
108+
self.l1_batch_number
109+
));
90110
}
91111
source_value
92112
}
93113

94114
fn get_enumeration_index(&mut self, key: &StorageKey) -> Option<u64> {
95-
let source_value = self.source_storage.as_mut().get_enumeration_index(key);
96-
let expected_value = self.to_check_storage.as_mut().get_enumeration_index(key);
115+
let source_value = self.source_storage.get_enumeration_index(key);
116+
let expected_value = self.to_check_storage.get_enumeration_index(key);
97117
if source_value != expected_value {
98-
tracing::error!(
118+
self.metrics.get_enumeration_index_mismatch.inc();
119+
self.error_or_panic(format_args!(
99120
"get_enumeration_index({key:?}) -- l1_batch_number={:?} -- \
100121
expected source={source_value:?} to be equal to to_check={expected_value:?}",
101122
self.l1_batch_number
102-
);
103-
self.metrics.get_enumeration_index_mismatch.inc();
123+
));
104124
}
105125
source_value
106126
}

0 commit comments

Comments
 (0)