Skip to content

Commit efc0007

Browse files
authored
feat(zkos): Implement ZK OS tree manager (#3730)
## What ❔ Implements a tree manager component responsible for updating the ZK OS Merkle tree based on Postgres data and providing REST API for it (to be used for witness input generation). ## Why ❔ Necessary to keep the ZK OS Merkle tree synced. ## Is this a breaking change? - [ ] Yes - [x] No ## Operational changes No operational changes; the new component is not tied to the node binaries. ## 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 `zkstack dev fmt` and `zkstack dev lint`.
1 parent b82e2e4 commit efc0007

File tree

32 files changed

+2931
-350
lines changed

32 files changed

+2931
-350
lines changed

core/Cargo.lock

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

core/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ members = [
4040
"node/external_proof_integration_api",
4141
"node/logs_bloom_backfill",
4242
"node/da_clients",
43+
"node/zk_os_tree_manager",
4344
# Libraries
4445
"lib/db_connection",
4546
"lib/zksync_core_leftovers",
@@ -303,6 +304,7 @@ zksync_web3_decl = { version = "27.0.0-non-semver-compat", path = "lib/web3_decl
303304
zksync_crypto_primitives = { version = "27.0.0-non-semver-compat", path = "lib/crypto_primitives" }
304305
zksync_external_price_api = { version = "27.0.0-non-semver-compat", path = "lib/external_price_api" }
305306
zksync_task_management = { version = "27.0.0-non-semver-compat", path = "lib/task_management" }
307+
zk_os_merkle_tree = { version = "27.0.0-non-semver-compat", path = "lib/zk_os_merkle_tree" }
306308

307309
# Framework and components
308310
zksync_node_framework = { version = "27.0.0-non-semver-compat", path = "node/node_framework" }

core/lib/types/src/block.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,26 @@ impl DeployedContract {
2727
}
2828
}
2929

30-
/// Holder for l1 batches data, used in eth sender metrics
30+
/// Holder for l1 batches data.
31+
#[derive(Debug, Clone, Copy, PartialEq)]
3132
pub struct L1BatchStatistics {
3233
pub number: L1BatchNumber,
3334
pub timestamp: u64,
3435
pub l2_tx_count: u32,
3536
pub l1_tx_count: u32,
3637
}
3738

39+
impl From<L1BatchHeader> for L1BatchStatistics {
40+
fn from(header: L1BatchHeader) -> Self {
41+
Self {
42+
number: header.number,
43+
timestamp: header.timestamp,
44+
l1_tx_count: header.l1_tx_count.into(),
45+
l2_tx_count: header.l2_tx_count.into(),
46+
}
47+
}
48+
}
49+
3850
/// Holder for the block metadata that is not available from transactions themselves.
3951
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4052
pub struct L1BatchHeader {

core/lib/zk_os_merkle_tree/src/hasher/proofs.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ use zksync_basic_types::H256;
99
use crate::{types::Leaf, BatchOutput, HashTree, TreeEntry};
1010

1111
/// Operation on a Merkle tree entry used in [`BatchTreeProof`].
12-
#[derive(Debug, Clone, Copy)]
13-
#[cfg_attr(test, derive(PartialEq))]
12+
#[derive(Debug, Clone, Copy, PartialEq)]
1413
pub enum TreeOperation {
1514
/// Operation hitting an existing entry (i.e., an update or read).
1615
Hit { index: u64 },
@@ -29,6 +28,13 @@ pub struct IntermediateHash {
2928
pub location: (u8, u64),
3029
}
3130

31+
#[cfg(not(test))]
32+
impl From<H256> for IntermediateHash {
33+
fn from(value: H256) -> Self {
34+
Self { value }
35+
}
36+
}
37+
3238
/// Partial view of the Merkle tree returned from [`BatchTreeProof::verify()`].
3339
#[derive(Debug)]
3440
pub struct MerkleTreeView {

core/lib/zk_os_merkle_tree/src/lib.rs

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,22 @@ pub use zksync_crypto_primitives::hasher::blake2::Blake2Hasher;
88

99
pub use self::{
1010
errors::DeserializeError,
11-
hasher::HashTree,
11+
hasher::{BatchTreeProof, HashTree, TreeOperation},
12+
reader::MerkleTreeReader,
1213
storage::{Database, MerkleTreeColumnFamily, PatchSet, Patched, RocksDBWrapper},
1314
types::{BatchOutput, TreeEntry},
1415
};
1516
use crate::{
16-
hasher::BatchTreeProof,
1717
metrics::{BatchProofStage, LoadStage, MerkleTreeInfo, METRICS},
18-
storage::{TreeUpdate, WorkingPatchSet},
19-
types::MAX_TREE_DEPTH,
18+
storage::{AsEntry, TreeUpdate, WorkingPatchSet},
19+
types::{Leaf, MAX_TREE_DEPTH},
2020
};
2121

2222
mod consistency;
2323
mod errors;
2424
mod hasher;
2525
mod metrics;
26+
mod reader;
2627
mod storage;
2728
#[cfg(test)]
2829
mod tests;
@@ -33,7 +34,7 @@ mod types;
3334
/// these types will remain stable.
3435
#[doc(hidden)]
3536
pub mod unstable {
36-
pub use crate::types::{KeyLookup, Manifest, Node, NodeKey, Root};
37+
pub use crate::types::{KeyLookup, Leaf, Manifest, Node, NodeKey, RawNode, Root};
3738
}
3839

3940
/// Marker trait for tree parameters.
@@ -101,6 +102,23 @@ impl<DB: Database> MerkleTree<DB> {
101102
}
102103
}
103104

105+
impl<DB: Database, P: TreeParams> MerkleTree<DB, P>
106+
where
107+
P::Hasher: Default,
108+
{
109+
/// Returns the hash of the empty tree.
110+
pub fn empty_tree_hash() -> H256 {
111+
let hasher = P::Hasher::default();
112+
let min_guard_hash = hasher.hash_leaf(&Leaf::MIN_GUARD);
113+
let max_guard_hash = hasher.hash_leaf(&Leaf::MAX_GUARD);
114+
let mut hash = hasher.hash_branch(&min_guard_hash, &max_guard_hash);
115+
for depth in 1..P::TREE_DEPTH {
116+
hash = hasher.hash_branch(&hash, &hasher.empty_subtree_hash(depth));
117+
}
118+
hash
119+
}
120+
}
121+
104122
impl<DB: Database, P: TreeParams> MerkleTree<DB, P> {
105123
/// Loads a tree with the specified hasher.
106124
///
@@ -125,6 +143,11 @@ impl<DB: Database, P: TreeParams> MerkleTree<DB, P> {
125143
Ok(Self { db, hasher })
126144
}
127145

146+
/// Returns a reference to the database.
147+
pub fn db(&self) -> &DB {
148+
&self.db
149+
}
150+
128151
/// Returns the root hash of a tree at the specified `version`, or `None` if the version
129152
/// was not written yet.
130153
pub fn root_hash(&self, version: u64) -> anyhow::Result<Option<H256>> {
@@ -134,6 +157,15 @@ impl<DB: Database, P: TreeParams> MerkleTree<DB, P> {
134157
Ok(Some(root.hash::<P>(&self.hasher)))
135158
}
136159

160+
/// Returns the root hash and leaf count at the specified version.
161+
pub fn root_info(&self, version: u64) -> anyhow::Result<Option<(H256, u64)>> {
162+
let Some(root) = self.db.try_root(version)? else {
163+
return Ok(None);
164+
};
165+
let root_hash = root.hash::<P>(&self.hasher);
166+
Ok(Some((root_hash, root.leaf_count)))
167+
}
168+
137169
/// Returns the latest version of the tree present in the database, or `None` if
138170
/// no versions are present yet.
139171
pub fn latest_version(&self) -> anyhow::Result<Option<u64>> {
@@ -160,7 +192,7 @@ impl<DB: Database, P: TreeParams> MerkleTree<DB, P> {
160192
/// - Returns an error if the version doesn't exist.
161193
/// - Proxies database errors.
162194
pub fn prove(&self, version: u64, keys: &[H256]) -> anyhow::Result<BatchTreeProof> {
163-
let (patch, mut update) = self.create_patch(version, &[], keys)?;
195+
let (patch, mut update) = self.create_patch::<TreeEntry>(version, &[], keys)?;
164196
Ok(patch.create_batch_proof(&self.hasher, vec![], update.take_read_operations()))
165197
}
166198

@@ -180,10 +212,22 @@ impl<DB: Database, P: TreeParams> MerkleTree<DB, P> {
180212
Ok(output)
181213
}
182214

215+
/// Same as [`Self::extend()`], but with a cross-check for the leaf indices of `entries`.
216+
///
217+
/// `reference_indices` must have the same length as `entries`. Note that because indices are *only* used for cross-checking,
218+
/// entries (more specifically, new insertions) must still be correctly ordered!
219+
pub fn extend_with_reference(
220+
&mut self,
221+
entries: &[(u64, TreeEntry)],
222+
) -> anyhow::Result<BatchOutput> {
223+
let (output, _) = self.extend_inner(entries, None)?;
224+
Ok(output)
225+
}
226+
183227
#[tracing::instrument(level = "debug", name = "extend", skip_all, fields(latest_version))]
184228
fn extend_inner(
185229
&mut self,
186-
entries: &[TreeEntry],
230+
entries: &[impl AsEntry],
187231
read_keys: Option<&[H256]>,
188232
) -> anyhow::Result<(BatchOutput, Option<BatchTreeProof>)> {
189233
let latest_version = self
@@ -291,3 +335,10 @@ impl<DB: Database, P: TreeParams> MerkleTree<DB, P> {
291335
Ok(())
292336
}
293337
}
338+
339+
impl<DB: Database, P: TreeParams> MerkleTree<Patched<DB>, P> {
340+
/// Flushes changes to the underlying storage.
341+
pub fn flush(&mut self) -> anyhow::Result<()> {
342+
self.db.flush()
343+
}
344+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! Merkle tree reader.
2+
3+
use std::fmt;
4+
5+
use zksync_basic_types::H256;
6+
7+
use crate::{
8+
hasher::BatchTreeProof,
9+
types::{NodeKey, RawNode},
10+
Database, DefaultTreeParams, MerkleTree, RocksDBWrapper, TreeParams,
11+
};
12+
13+
pub struct MerkleTreeReader<DB, P: TreeParams = DefaultTreeParams>(MerkleTree<DB, P>);
14+
15+
impl<DB: fmt::Debug, P: TreeParams> fmt::Debug for MerkleTreeReader<DB, P>
16+
where
17+
P::Hasher: fmt::Debug,
18+
{
19+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20+
fmt::Debug::fmt(&self.0, formatter)
21+
}
22+
}
23+
24+
impl<DB: Database + Clone, P: TreeParams> Clone for MerkleTreeReader<DB, P>
25+
where
26+
P::Hasher: Clone,
27+
{
28+
fn clone(&self) -> Self {
29+
Self(MerkleTree {
30+
db: self.0.db.clone(),
31+
hasher: self.0.hasher.clone(),
32+
})
33+
}
34+
}
35+
36+
impl<DB: Database> MerkleTreeReader<DB> {
37+
/// Creates a tree reader based on the provided database.
38+
///
39+
/// # Errors
40+
///
41+
/// Errors if sanity checks fail.
42+
pub fn new(db: DB) -> anyhow::Result<Self> {
43+
MerkleTree::new(db).map(Self)
44+
}
45+
}
46+
47+
impl<DB: Database, P: TreeParams> MerkleTreeReader<DB, P> {
48+
/// Returns a reference to the database.
49+
pub fn db(&self) -> &DB {
50+
&self.0.db
51+
}
52+
53+
/// Converts this reader to the underlying DB.
54+
pub fn into_db(self) -> DB {
55+
self.0.db
56+
}
57+
58+
/// Returns the latest version of the tree present in the database, or `None` if
59+
/// no versions are present yet.
60+
pub fn latest_version(&self) -> anyhow::Result<Option<u64>> {
61+
self.0.latest_version()
62+
}
63+
64+
/// Returns the root hash and leaf count at the specified version.
65+
pub fn root_info(&self, version: u64) -> anyhow::Result<Option<(H256, u64)>> {
66+
self.0.root_info(version)
67+
}
68+
69+
/// Creates a batch proof for `keys` at the specified tree version.
70+
///
71+
/// # Errors
72+
///
73+
/// - Returns an error if the version doesn't exist.
74+
/// - Proxies database errors.
75+
pub fn prove(&self, version: u64, keys: &[H256]) -> anyhow::Result<BatchTreeProof> {
76+
self.0.prove(version, keys)
77+
}
78+
}
79+
80+
impl<P: TreeParams> MerkleTreeReader<RocksDBWrapper, P> {
81+
/// Returns raw nodes for the specified `keys`.
82+
pub fn raw_nodes(&self, node_keys: &[NodeKey]) -> anyhow::Result<Vec<Option<RawNode>>> {
83+
let raw_nodes = self.0.db.raw_nodes(node_keys).into_iter();
84+
let raw_nodes = node_keys.iter().zip(raw_nodes).map(|(key, slice)| {
85+
let slice = slice?;
86+
Some(if key.nibble_count == 0 {
87+
RawNode::deserialize_root(&slice)
88+
} else {
89+
RawNode::deserialize(&slice)
90+
})
91+
});
92+
Ok(raw_nodes.collect())
93+
}
94+
}

core/lib/zk_os_merkle_tree/src/storage/mod.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub(crate) use self::patch::{TreeUpdate, WorkingPatchSet};
1010
pub use self::rocksdb::{MerkleTreeColumnFamily, RocksDBWrapper};
1111
use crate::{
1212
errors::{DeserializeContext, DeserializeError, DeserializeErrorKind},
13-
types::{InternalNode, KeyLookup, Leaf, Manifest, Node, NodeKey, Root},
13+
types::{InternalNode, KeyLookup, Leaf, Manifest, Node, NodeKey, Root, TreeEntry},
1414
};
1515

1616
mod patch;
@@ -19,6 +19,38 @@ mod serialization;
1919
#[cfg(test)]
2020
mod tests;
2121

22+
/// [`TreeEntry`] generalization allowing to check leaf index assignment when loading data from the tree.
23+
pub(crate) trait AsEntry {
24+
fn as_entry(&self) -> &TreeEntry;
25+
26+
fn check_index(&self, index: u64) -> anyhow::Result<()>;
27+
}
28+
29+
impl AsEntry for TreeEntry {
30+
fn as_entry(&self) -> &TreeEntry {
31+
self
32+
}
33+
34+
fn check_index(&self, _index: u64) -> anyhow::Result<()> {
35+
Ok(())
36+
}
37+
}
38+
39+
impl AsEntry for (u64, TreeEntry) {
40+
fn as_entry(&self) -> &TreeEntry {
41+
&self.1
42+
}
43+
44+
fn check_index(&self, index: u64) -> anyhow::Result<()> {
45+
let (ref_index, entry) = self;
46+
anyhow::ensure!(
47+
index == *ref_index,
48+
"Unexpected index for {entry:?}: reference is {ref_index}, but tree implies {index}",
49+
);
50+
Ok(())
51+
}
52+
}
53+
2254
/// Generic database functionality. Its main implementation is [`RocksDB`].
2355
pub trait Database: Send + Sync {
2456
fn indices(&self, version: u64, keys: &[H256]) -> Result<Vec<KeyLookup>, DeserializeError>;
@@ -303,6 +335,11 @@ impl<DB: Database> Patched<DB> {
303335
self.patch = None;
304336
}
305337

338+
/// Returns a reference to the underlying database. It is unsound to modify the database using this reference.
339+
pub fn inner(&self) -> &DB {
340+
&self.inner
341+
}
342+
306343
/// Returns the wrapped database.
307344
///
308345
/// # Panics

0 commit comments

Comments
 (0)