diff --git a/Cargo.lock b/Cargo.lock index d1c6c7e7d..75fd39949 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2629,7 +2629,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miden-agglayer" version = "0.14.0" -source = "git+https://github.com/0xMiden/miden-base?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" +source = "git+https://github.com/0xMiden/miden-base.git?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" dependencies = [ "fs-err", "miden-assembly", @@ -2700,7 +2700,7 @@ dependencies = [ [[package]] name = "miden-block-prover" version = "0.14.0" -source = "git+https://github.com/0xMiden/miden-base?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" +source = "git+https://github.com/0xMiden/miden-base.git?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" dependencies = [ "miden-protocol", "thiserror 2.0.18", @@ -2769,7 +2769,6 @@ dependencies = [ "rand_core 0.9.5", "rand_hc", "rayon", - "rocksdb", "sha2", "sha3", "subtle", @@ -2817,6 +2816,17 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "miden-large-smt-backend-rocksdb" +version = "0.14.0" +dependencies = [ + "miden-crypto", + "miden-protocol", + "rayon", + "rocksdb", + "winter-utils", +] + [[package]] name = "miden-mast-package" version = "0.20.6" @@ -3102,6 +3112,7 @@ dependencies = [ "miden-agglayer", "miden-block-prover", "miden-crypto", + "miden-large-smt-backend-rocksdb", "miden-node-db", "miden-node-proto", "miden-node-proto-build", @@ -3234,7 +3245,7 @@ dependencies = [ [[package]] name = "miden-protocol" version = "0.14.0" -source = "git+https://github.com/0xMiden/miden-base?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" +source = "git+https://github.com/0xMiden/miden-base.git?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" dependencies = [ "bech32", "fs-err", @@ -3264,7 +3275,7 @@ dependencies = [ [[package]] name = "miden-protocol-macros" version = "0.14.0" -source = "git+https://github.com/0xMiden/miden-base?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" +source = "git+https://github.com/0xMiden/miden-base.git?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" dependencies = [ "proc-macro2", "quote", @@ -3347,7 +3358,7 @@ dependencies = [ [[package]] name = "miden-standards" version = "0.14.0" -source = "git+https://github.com/0xMiden/miden-base?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" +source = "git+https://github.com/0xMiden/miden-base.git?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" dependencies = [ "fs-err", "miden-assembly", @@ -3364,7 +3375,7 @@ dependencies = [ [[package]] name = "miden-testing" version = "0.14.0" -source = "git+https://github.com/0xMiden/miden-base?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" +source = "git+https://github.com/0xMiden/miden-base.git?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" dependencies = [ "anyhow", "itertools 0.14.0", @@ -3387,7 +3398,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.14.0" -source = "git+https://github.com/0xMiden/miden-base?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" +source = "git+https://github.com/0xMiden/miden-base.git?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" dependencies = [ "miden-processor", "miden-protocol", @@ -3400,7 +3411,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" version = "0.14.0" -source = "git+https://github.com/0xMiden/miden-base?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" +source = "git+https://github.com/0xMiden/miden-base.git?branch=next#3154a371939125e5cc3faf39a7c42447db67584f" dependencies = [ "miden-protocol", "miden-tx", diff --git a/Cargo.toml b/Cargo.toml index da5015478..a6cd8d68f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/block-producer", "crates/db", "crates/grpc-error-macro", + "crates/large-smt-backend-rocksdb", "crates/ntx-builder", "crates/proto", "crates/remote-prover-client", @@ -45,33 +46,36 @@ debug = true [workspace.dependencies] # Workspace crates. -miden-node-block-producer = { path = "crates/block-producer", version = "0.14" } -miden-node-db = { path = "crates/db", version = "0.14" } -miden-node-grpc-error-macro = { path = "crates/grpc-error-macro", version = "0.14" } -miden-node-ntx-builder = { path = "crates/ntx-builder", version = "0.14" } -miden-node-proto = { path = "crates/proto", version = "0.14" } -miden-node-proto-build = { path = "proto", version = "0.14" } -miden-node-rpc = { path = "crates/rpc", version = "0.14" } -miden-node-store = { path = "crates/store", version = "0.14" } -miden-node-test-macro = { path = "crates/test-macro" } -miden-node-utils = { path = "crates/utils", version = "0.14" } -miden-node-validator = { path = "crates/validator", version = "0.14" } -miden-remote-prover-client = { path = "crates/remote-prover-client", version = "0.14" } +miden-large-smt-backend-rocksdb = { path = "crates/large-smt-backend-rocksdb", version = "0.14" } +miden-node-block-producer = { path = "crates/block-producer", version = "0.14" } +miden-node-db = { path = "crates/db", version = "0.14" } +miden-node-grpc-error-macro = { path = "crates/grpc-error-macro", version = "0.14" } +miden-node-ntx-builder = { path = "crates/ntx-builder", version = "0.14" } +miden-node-proto = { path = "crates/proto", version = "0.14" } +miden-node-proto-build = { path = "proto", version = "0.14" } +miden-node-rpc = { path = "crates/rpc", version = "0.14" } +miden-node-store = { path = "crates/store", version = "0.14" } +miden-node-test-macro = { path = "crates/test-macro" } +miden-node-utils = { path = "crates/utils", version = "0.14" } +miden-node-validator = { path = "crates/validator", version = "0.14" } +miden-remote-prover-client = { path = "crates/remote-prover-client", version = "0.14" } + # Temporary workaround until # is part of `rocksdb-rust` release miden-node-rocksdb-cxx-linkage-fix = { path = "crates/rocksdb-cxx-linkage-fix", version = "0.14" } # miden-base aka protocol dependencies. These should be updated in sync. -miden-block-prover = { branch = "next", git = "https://github.com/0xMiden/miden-base" } -miden-protocol = { branch = "next", default-features = false, git = "https://github.com/0xMiden/miden-base" } -miden-standards = { branch = "next", git = "https://github.com/0xMiden/miden-base" } -miden-testing = { branch = "next", git = "https://github.com/0xMiden/miden-base" } -miden-tx = { branch = "next", default-features = false, git = "https://github.com/0xMiden/miden-base" } -miden-tx-batch-prover = { branch = "next", git = "https://github.com/0xMiden/miden-base" } +miden-block-prover = { branch = "next", git = "https://github.com/0xMiden/miden-base.git" } +miden-protocol = { branch = "next", default-features = false, git = "https://github.com/0xMiden/miden-base.git" } +miden-standards = { branch = "next", git = "https://github.com/0xMiden/miden-base.git" } +miden-testing = { branch = "next", git = "https://github.com/0xMiden/miden-base.git" } +miden-tx = { branch = "next", default-features = false, git = "https://github.com/0xMiden/miden-base.git" } +miden-tx-batch-prover = { branch = "next", git = "https://github.com/0xMiden/miden-base.git" } # Other miden dependencies. These should align with those expected by miden-base. -miden-air = { features = ["std", "testing"], version = "0.20" } -miden-crypto = { default-features = false, version = "0.19" } +miden-air = { features = ["std", "testing"], version = "0.20" } + +miden-crypto = { version = "0.19.7" } # External dependencies anyhow = { version = "1.0" } diff --git a/bin/network-monitor/src/faucet.rs b/bin/network-monitor/src/faucet.rs index caeafe055..1e50a173d 100644 --- a/bin/network-monitor/src/faucet.rs +++ b/bin/network-monitor/src/faucet.rs @@ -189,14 +189,15 @@ async fn perform_faucet_test( debug!("Generated account ID: {} (length: {})", account_id, account_id.len()); // Step 1: Request PoW challenge - let pow_url = faucet_url.join("/pow")?; - let response = client - .get(pow_url) - .query(&[("account_id", &account_id), ("amount", &MINT_AMOUNT.to_string())]) - .send() - .await?; + let mut pow_url = faucet_url.join("/pow")?; + pow_url + .query_pairs_mut() + .append_pair("account_id", &account_id) + .append_pair("amount", &MINT_AMOUNT.to_string()); - let response_text = response.text().await?; + let response = client.get(pow_url).send().await?; + + let response_text: String = response.text().await?; debug!("Faucet PoW response: {}", response_text); let challenge_response: PowChallengeResponse = serde_json::from_str(&response_text) @@ -215,21 +216,18 @@ async fn perform_faucet_test( debug!("Solved PoW challenge with nonce: {}", nonce); // Step 3: Request tokens with the solution - let tokens_url = faucet_url.join("/get_tokens")?; - - let response = client - .get(tokens_url) - .query(&[ - ("account_id", account_id.as_str()), - ("is_private_note", "false"), - ("asset_amount", &MINT_AMOUNT.to_string()), - ("challenge", &challenge_response.challenge), - ("nonce", &nonce.to_string()), - ]) - .send() - .await?; - - let response_text = response.text().await?; + let mut tokens_url = faucet_url.join("/get_tokens")?; + tokens_url + .query_pairs_mut() + .append_pair("account_id", account_id.as_str()) + .append_pair("is_private_note", "false") + .append_pair("asset_amount", &MINT_AMOUNT.to_string()) + .append_pair("challenge", &challenge_response.challenge) + .append_pair("nonce", &nonce.to_string()); + + let response = client.get(tokens_url).send().await?; + + let response_text: String = response.text().await?; let tokens_response: GetTokensResponse = serde_json::from_str(&response_text) .with_context(|| format!("Failed to parse tokens response: {response_text}"))?; diff --git a/crates/large-smt-backend-rocksdb/Cargo.toml b/crates/large-smt-backend-rocksdb/Cargo.toml new file mode 100644 index 000000000..c7f009f92 --- /dev/null +++ b/crates/large-smt-backend-rocksdb/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors.workspace = true +description = "Large-scale Sparse Merkle Tree backed by pluggable storage - RocksDB backend" +edition.workspace = true +homepage.workspace = true +keywords = ["merkle", "miden", "node", "smt"] +license.workspace = true +name = "miden-large-smt-backend-rocksdb" +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +miden-crypto = { features = ["concurrent", "std"], workspace = true } +miden-protocol = { features = ["std"], workspace = true } +rayon = { version = "1.10" } +rocksdb = { default-features = false, features = ["bindgen-runtime", "lz4"], version = "0.24" } +winter-utils = { version = "0.13" } diff --git a/crates/large-smt-backend-rocksdb/README.md b/crates/large-smt-backend-rocksdb/README.md new file mode 100644 index 000000000..4b612c325 --- /dev/null +++ b/crates/large-smt-backend-rocksdb/README.md @@ -0,0 +1,45 @@ +# miden-large-smt-backend-rocksdb + +Large-scale Sparse Merkle Tree backed by pluggable storage - RocksDB backend implementation. + +This crate provides `LargeSmt`, a hybrid SMT implementation that stores the top of the tree +(depths 0–23) in memory and persists the lower depths (24–64) in storage as fixed-size subtrees. +This hybrid layout scales beyond RAM while keeping common operations fast. + +## Migration Status + +This crate is the future home for `LargeSmt` and its storage backends. Currently it re-exports +types from `miden-protocol` (which re-exports from `miden-crypto`). + +The migration will be completed in phases: +1. ✅ Create this crate as a re-export layer (current state) +2. Copy the full implementation from miden-crypto to this crate +3. Update miden-crypto to remove the rocksdb feature +4. Update dependents to use this crate directly + +## Features + +- **concurrent**: Enables parallel processing with rayon (enabled by default) +- **rocksdb**: (Future) Enables RocksDB storage backend + +## Usage + +```rust +use miden_large_smt::{LargeSmt, MemoryStorage}; + +// Create an empty tree with in-memory storage +let storage = MemoryStorage::new(); +let smt = LargeSmt::new(storage).unwrap(); +``` + +## Re-exported Types + +This crate re-exports the following types from `miden-protocol`: + +- `LargeSmt` - The large-scale SMT implementation +- `LargeSmtError` - Error type for LargeSmt operations +- `MemoryStorage` - In-memory storage backend +- `SmtStorage` - Storage backend trait +- `Subtree` - Serializable subtree representation +- `StorageUpdates` / `StorageUpdateParts` - Batch update types +- Various SMT types: `Smt`, `SmtLeaf`, `SmtProof`, `LeafIndex`, etc. diff --git a/crates/large-smt-backend-rocksdb/src/helpers.rs b/crates/large-smt-backend-rocksdb/src/helpers.rs new file mode 100644 index 000000000..23f3c8d88 --- /dev/null +++ b/crates/large-smt-backend-rocksdb/src/helpers.rs @@ -0,0 +1,83 @@ +use miden_crypto::merkle::smt::{MAX_LEAF_ENTRIES, SmtLeaf, SmtLeafError}; +use miden_crypto::word::LexicographicWord; +use rocksdb::Error as RocksDbError; + +use crate::{StorageError, Word}; + +pub(crate) fn map_rocksdb_err(err: RocksDbError) -> StorageError { + StorageError::Backend(Box::new(err)) +} + +pub(crate) fn insert_into_leaf( + leaf: &mut SmtLeaf, + key: Word, + value: Word, +) -> Result, StorageError> { + match leaf { + SmtLeaf::Empty(_) => { + *leaf = SmtLeaf::new_single(key, value); + Ok(None) + }, + SmtLeaf::Single(kv_pair) => { + if kv_pair.0 == key { + let old_value = kv_pair.1; + kv_pair.1 = value; + Ok(Some(old_value)) + } else { + let mut pairs = vec![*kv_pair, (key, value)]; + pairs.sort_by(|(key_1, _), (key_2, _)| { + LexicographicWord::from(*key_1).cmp(&LexicographicWord::from(*key_2)) + }); + *leaf = SmtLeaf::Multiple(pairs); + Ok(None) + } + }, + SmtLeaf::Multiple(kv_pairs) => match kv_pairs.binary_search_by(|kv_pair| { + LexicographicWord::from(kv_pair.0).cmp(&LexicographicWord::from(key)) + }) { + Ok(pos) => { + let old_value = kv_pairs[pos].1; + kv_pairs[pos].1 = value; + Ok(Some(old_value)) + }, + Err(pos) => { + if kv_pairs.len() >= MAX_LEAF_ENTRIES { + return Err(StorageError::Leaf(SmtLeafError::TooManyLeafEntries { + actual: kv_pairs.len() + 1, + })); + } + kv_pairs.insert(pos, (key, value)); + Ok(None) + }, + }, + } +} + +pub(crate) fn remove_from_leaf(leaf: &mut SmtLeaf, key: Word) -> (Option, bool) { + match leaf { + SmtLeaf::Empty(_) => (None, false), + SmtLeaf::Single((key_at_leaf, value_at_leaf)) => { + if *key_at_leaf == key { + let old_value = *value_at_leaf; + *leaf = SmtLeaf::new_empty(key.into()); + (Some(old_value), true) + } else { + (None, false) + } + }, + SmtLeaf::Multiple(kv_pairs) => match kv_pairs.binary_search_by(|kv_pair| { + LexicographicWord::from(kv_pair.0).cmp(&LexicographicWord::from(key)) + }) { + Ok(pos) => { + let old_value = kv_pairs[pos].1; + kv_pairs.remove(pos); + debug_assert!(!kv_pairs.is_empty()); + if kv_pairs.len() == 1 { + *leaf = SmtLeaf::Single(kv_pairs[0]); + } + (Some(old_value), false) + }, + Err(_) => (None, false), + }, + } +} diff --git a/crates/large-smt-backend-rocksdb/src/lib.rs b/crates/large-smt-backend-rocksdb/src/lib.rs new file mode 100644 index 000000000..563439c9f --- /dev/null +++ b/crates/large-smt-backend-rocksdb/src/lib.rs @@ -0,0 +1,59 @@ +//! Large-scale Sparse Merkle Tree backed by pluggable storage. +//! +//! `LargeSmt` stores the top of the tree (depths 0–23) in memory and persists the lower +//! depths (24–64) in storage as fixed-size subtrees. This hybrid layout scales beyond RAM +//! while keeping common operations fast. +//! +//! # Usage +//! +//! ```ignore +//! use miden_large_smt::{LargeSmt, MemoryStorage}; +//! +//! // Create an empty tree with in-memory storage +//! let storage = MemoryStorage::new(); +//! let smt = LargeSmt::new(storage).unwrap(); +//! ``` +//! +//! ```ignore +//! use miden_large_smt_backend_rocksdb::{LargeSmt, RocksDbConfig, RocksDbStorage}; +//! +//! let storage = RocksDbStorage::open(RocksDbConfig::new("/path/to/db")).unwrap(); +//! let smt = LargeSmt::new(storage).unwrap(); +//! ``` + +extern crate alloc; + +mod helpers; +#[expect(clippy::doc_markdown, clippy::inline_always)] +mod rocksdb; +// Re-export from miden-protocol. +pub use miden_protocol::crypto::merkle::smt::{ + InnerNode, + LargeSmt, + LargeSmtError, + LeafIndex, + MemoryStorage, + SMT_DEPTH, + Smt, + SmtLeaf, + SmtLeafError, + SmtProof, + SmtStorage, + StorageError, + StorageUpdateParts, + StorageUpdates, + Subtree, + SubtreeError, + SubtreeUpdate, +}; +// Also re-export commonly used types for convenience +pub use miden_protocol::{ + EMPTY_WORD, + Felt, + Word, + crypto::{ + hash::rpo::Rpo256, + merkle::{EmptySubtreeRoots, InnerNodeInfo, MerkleError, NodeIndex, SparseMerklePath}, + }, +}; +pub use rocksdb::{RocksDbConfig, RocksDbStorage}; diff --git a/crates/large-smt-backend-rocksdb/src/rocksdb.rs b/crates/large-smt-backend-rocksdb/src/rocksdb.rs new file mode 100644 index 000000000..92f187c4d --- /dev/null +++ b/crates/large-smt-backend-rocksdb/src/rocksdb.rs @@ -0,0 +1,1329 @@ +use alloc::boxed::Box; +use alloc::vec::Vec; +use std::path::PathBuf; +use std::sync::Arc; + +use miden_crypto::Map; +use miden_crypto::merkle::NodeIndex; +use miden_crypto::merkle::smt::{InnerNode, SmtLeaf, Subtree}; +use rocksdb::{ + BlockBasedOptions, + Cache, + ColumnFamilyDescriptor, + DB, + DBCompactionStyle, + DBCompressionType, + DBIteratorWithThreadMode, + FlushOptions, + IteratorMode, + Options, + ReadOptions, + WriteBatch, +}; +use winter_utils::{Deserializable, Serializable}; + +use super::{SmtStorage, StorageError, StorageUpdateParts, StorageUpdates, SubtreeUpdate}; +use crate::helpers::{insert_into_leaf, map_rocksdb_err, remove_from_leaf}; +use crate::{EMPTY_WORD, Word}; + +const IN_MEMORY_DEPTH: u8 = 24; + +/// The name of the `RocksDB` column family used for storing SMT leaves. +const LEAVES_CF: &str = "leaves"; +/// The names of the `RocksDB` column families used for storing SMT subtrees (deep nodes). +const SUBTREE_24_CF: &str = "st24"; +const SUBTREE_32_CF: &str = "st32"; +const SUBTREE_40_CF: &str = "st40"; +const SUBTREE_48_CF: &str = "st48"; +const SUBTREE_56_CF: &str = "st56"; +const SUBTREE_DEPTHS: [u8; 5] = [56, 48, 40, 32, 24]; + +/// The name of the `RocksDB` column family used for storing metadata (e.g., root, counts). +const METADATA_CF: &str = "metadata"; +/// The name of the `RocksDB` column family used for storing level 24 hashes for fast tree +/// rebuilding. +const DEPTH_24_CF: &str = "depth24"; + +/// The key used in the `METADATA_CF` column family to store the SMT's root hash. +const ROOT_KEY: &[u8] = b"smt_root"; +/// The key used in the `METADATA_CF` column family to store the total count of non-empty leaves. +const LEAF_COUNT_KEY: &[u8] = b"leaf_count"; +/// The key used in the `METADATA_CF` column family to store the total count of key-value entries. +const ENTRY_COUNT_KEY: &[u8] = b"entry_count"; + +/// A `RocksDB`-backed persistent storage implementation for a Sparse Merkle Tree (SMT). +/// +/// Implements the `SmtStorage` trait, providing durable storage for SMT components +/// including leaves, subtrees (for deeper parts of the tree), and metadata like the SMT root +/// and counts. It leverages `RocksDB` column families to organize data: +/// - `LEAVES_CF` ("leaves"): Stores `SmtLeaf` data, keyed by their logical u64 index. +/// - `SUBTREE_24_CF` ("st24"): Stores serialized `Subtree` data at depth 24, keyed by their root +/// `NodeIndex`. +/// - `SUBTREE_32_CF` ("st32"): Stores serialized `Subtree` data at depth 32, keyed by their root +/// `NodeIndex`. +/// - `SUBTREE_40_CF` ("st40"): Stores serialized `Subtree` data at depth 40, keyed by their root +/// `NodeIndex`. +/// - `SUBTREE_48_CF` ("st48"): Stores serialized `Subtree` data at depth 48, keyed by their root +/// `NodeIndex`. +/// - `SUBTREE_56_CF` ("st56"): Stores serialized `Subtree` data at depth 56, keyed by their root +/// `NodeIndex`. +/// - `METADATA_CF` ("metadata"): Stores overall SMT metadata such as the current root hash, total +/// leaf count, and total entry count. +#[derive(Debug, Clone)] +pub struct RocksDbStorage { + db: Arc, +} + +impl RocksDbStorage { + /// Opens or creates a RocksDB database at the specified `path` and configures it for SMT + /// storage. + /// + /// This method sets up the necessary column families (`leaves`, `subtrees`, `metadata`) + /// and applies various RocksDB options for performance, such as caching, bloom filters, + /// and compaction strategies tailored for SMT workloads. + /// + /// # Errors + /// Returns `StorageError::Backend` if the database cannot be opened or configured, + /// for example, due to path issues, permissions, or RocksDB internal errors. + pub fn open(config: RocksDbConfig) -> Result { + // Base DB options + let mut db_opts = Options::default(); + // Create DB if it doesn't exist + db_opts.create_if_missing(true); + // Auto-create missing column families + db_opts.create_missing_column_families(true); + #[expect(clippy::cast_possible_wrap)] + // Tune compaction threads to match CPU cores + db_opts.increase_parallelism(rayon::current_num_threads() as i32); + // Limit the number of open file handles + db_opts.set_max_open_files(config.max_open_files); + #[expect(clippy::cast_possible_wrap)] + // Parallelize flush/compaction up to CPU count + db_opts.set_max_background_jobs(rayon::current_num_threads() as i32); + // Maximum WAL size + db_opts.set_max_total_wal_size(512 * 1024 * 1024); + + // Shared block cache across all column families + let cache = Cache::new_lru_cache(config.cache_size); + + // Common table options for bloom filtering and cache + let mut table_opts = BlockBasedOptions::default(); + // Use shared LRU cache for block data + table_opts.set_block_cache(&cache); + table_opts.set_bloom_filter(10.0, false); + // Enable whole-key bloom filtering (better with point lookups) + table_opts.set_whole_key_filtering(true); + // Pin L0 filter and index blocks in cache (improves performance) + table_opts.set_pin_l0_filter_and_index_blocks_in_cache(true); + + // Column family for leaves + let mut leaves_opts = Options::default(); + leaves_opts.set_block_based_table_factory(&table_opts); + // 128 MB memtable + leaves_opts.set_write_buffer_size(128 << 20); + // Allow up to 3 memtables + leaves_opts.set_max_write_buffer_number(3); + leaves_opts.set_min_write_buffer_number_to_merge(1); + // Do not retain flushed memtables in memory + leaves_opts.set_max_write_buffer_size_to_maintain(0); + // Use level-based compaction + leaves_opts.set_compaction_style(DBCompactionStyle::Level); + // 512 MB target file size + leaves_opts.set_target_file_size_base(512 << 20); + leaves_opts.set_target_file_size_multiplier(2); + // LZ4 compression + leaves_opts.set_compression_type(DBCompressionType::Lz4); + // Set level-based compaction parameters + leaves_opts.set_level_zero_file_num_compaction_trigger(8); + + // Helper to build subtree CF options with correct prefix length + #[expect(clippy::items_after_statements)] + fn subtree_cf(cache: &Cache, bloom_filter_bits: f64) -> Options { + let mut tbl = BlockBasedOptions::default(); + // Use shared LRU cache for block data + tbl.set_block_cache(cache); + // Set bloom filter for subtree lookups + tbl.set_bloom_filter(bloom_filter_bits, false); + // Enable whole-key bloom filtering + tbl.set_whole_key_filtering(true); + // Pin L0 filter and index blocks in cache + tbl.set_pin_l0_filter_and_index_blocks_in_cache(true); + + let mut opts = Options::default(); + opts.set_block_based_table_factory(&tbl); + // 128 MB memtable + opts.set_write_buffer_size(128 << 20); + opts.set_max_write_buffer_number(3); + opts.set_min_write_buffer_number_to_merge(1); + // Do not retain flushed memtables in memory + opts.set_max_write_buffer_size_to_maintain(0); + // Use level-based compaction + opts.set_compaction_style(DBCompactionStyle::Level); + // 512 MB target file size + opts.set_target_file_size_base(512 << 20); + opts.set_target_file_size_multiplier(2); + // LZ4 compression + opts.set_compression_type(DBCompressionType::Lz4); + // Set level-based compaction parameters + opts.set_level_zero_file_num_compaction_trigger(8); + opts + } + + let mut depth24_opts = Options::default(); + depth24_opts.set_compression_type(DBCompressionType::Lz4); + depth24_opts.set_block_based_table_factory(&table_opts); + + // Metadata CF with no compression + let mut metadata_opts = Options::default(); + metadata_opts.set_compression_type(DBCompressionType::None); + + // Define column families with tailored options + let cfs = vec![ + ColumnFamilyDescriptor::new(LEAVES_CF, leaves_opts), + ColumnFamilyDescriptor::new(SUBTREE_24_CF, subtree_cf(&cache, 8.0)), + ColumnFamilyDescriptor::new(SUBTREE_32_CF, subtree_cf(&cache, 10.0)), + ColumnFamilyDescriptor::new(SUBTREE_40_CF, subtree_cf(&cache, 10.0)), + ColumnFamilyDescriptor::new(SUBTREE_48_CF, subtree_cf(&cache, 12.0)), + ColumnFamilyDescriptor::new(SUBTREE_56_CF, subtree_cf(&cache, 12.0)), + ColumnFamilyDescriptor::new(METADATA_CF, metadata_opts), + ColumnFamilyDescriptor::new(DEPTH_24_CF, depth24_opts), + ]; + + // Open the database with our tuned CFs + let db = DB::open_cf_descriptors(&db_opts, config.path, cfs).map_err(map_rocksdb_err)?; + + Ok(Self { db: Arc::new(db) }) + } + + /// Syncs the RocksDB database to disk. + /// + /// This ensures that all data is persisted to disk. + /// + /// # Errors + /// - Returns `StorageError::Backend` if the flush operation fails. + fn sync(&self) -> Result<(), StorageError> { + let mut fopts = FlushOptions::default(); + fopts.set_wait(true); + + for name in [ + LEAVES_CF, + SUBTREE_24_CF, + SUBTREE_32_CF, + SUBTREE_40_CF, + SUBTREE_48_CF, + SUBTREE_56_CF, + METADATA_CF, + DEPTH_24_CF, + ] { + let cf = self.cf_handle(name)?; + self.db.flush_cf_opt(cf, &fopts).map_err(map_rocksdb_err)?; + } + + self.db.flush_wal(true).map_err(map_rocksdb_err)?; + Ok(()) + } + + /// Converts an index (u64) into a fixed-size byte array for use as a `RocksDB` key. + #[inline(always)] + fn index_db_key(index: u64) -> [u8; 8] { + index.to_be_bytes() + } + + /// Converts a `NodeIndex` (for a subtree root) into a `KeyBytes` for use as a `RocksDB` key. + /// The `KeyBytes` is a wrapper around a 8-byte value with a variable-length prefix. + #[inline(always)] + fn subtree_db_key(index: NodeIndex) -> KeyBytes { + let keep = match index.depth() { + 24 => 3, + 32 => 4, + 40 => 5, + 48 => 6, + 56 => 7, + d => panic!("unsupported depth {d}"), + }; + KeyBytes::new(index.value(), keep) + } + + /// Retrieves a handle to a `RocksDB` column family by its name. + /// + /// # Errors + /// Returns `StorageError::Backend` if the column family with the given `name` does not + /// exist. + fn cf_handle(&self, name: &str) -> Result<&rocksdb::ColumnFamily, StorageError> { + self.db + .cf_handle(name) + .ok_or_else(|| StorageError::Unsupported(format!("unknown column family `{name}`"))) + } + + /* helper: CF handle from NodeIndex ------------------------------------- */ + #[inline(always)] + fn subtree_cf(&self, index: NodeIndex) -> &rocksdb::ColumnFamily { + let name = cf_for_depth(index.depth()); + self.cf_handle(name).expect("CF handle missing") + } +} + +impl SmtStorage for RocksDbStorage { + /// Retrieves the SMT root hash from the `METADATA_CF` column family. + /// + /// # Errors + /// - `StorageError::Backend`: If the metadata column family is missing or a RocksDB error + /// occurs. + /// - `StorageError::DeserializationError`: If the retrieved root hash bytes cannot be + /// deserialized. + fn get_root(&self) -> Result, StorageError> { + let cf = self.cf_handle(METADATA_CF)?; + match self.db.get_cf(cf, ROOT_KEY).map_err(map_rocksdb_err)? { + Some(bytes) => { + let digest = Word::read_from_bytes(&bytes)?; + Ok(Some(digest)) + }, + None => Ok(None), + } + } + + /// Stores the SMT root hash in the `METADATA_CF` column family. + /// + /// # Errors + /// - `StorageError::Backend`: If the metadata column family is missing or a RocksDB error + /// occurs. + fn set_root(&self, root: Word) -> Result<(), StorageError> { + let cf = self.cf_handle(METADATA_CF)?; + self.db.put_cf(cf, ROOT_KEY, root.to_bytes()).map_err(map_rocksdb_err)?; + Ok(()) + } + + /// Retrieves the total count of non-empty leaves from the `METADATA_CF` column family. + /// Returns 0 if the count is not found. + /// + /// # Errors + /// - `StorageError::Backend`: If the metadata column family is missing or a RocksDB error + /// occurs. + /// - `StorageError::BadValueLen`: If the retrieved count bytes are invalid. + fn leaf_count(&self) -> Result { + let cf = self.cf_handle(METADATA_CF)?; + self.db + .get_cf(cf, LEAF_COUNT_KEY) + .map_err(map_rocksdb_err)? + .map_or(Ok(0), |bytes| { + let arr: [u8; 8] = + bytes.as_slice().try_into().map_err(|_| StorageError::BadValueLen { + what: "leaf count", + expected: 8, + found: bytes.len(), + })?; + Ok(usize::from_be_bytes(arr)) + }) + } + + /// Retrieves the total count of key-value entries from the `METADATA_CF` column family. + /// Returns 0 if the count is not found. + /// + /// # Errors + /// - `StorageError::Backend`: If the metadata column family is missing or a RocksDB error + /// occurs. + /// - `StorageError::BadValueLen`: If the retrieved count bytes are invalid. + fn entry_count(&self) -> Result { + let cf = self.cf_handle(METADATA_CF)?; + self.db + .get_cf(cf, ENTRY_COUNT_KEY) + .map_err(map_rocksdb_err)? + .map_or(Ok(0), |bytes| { + let arr: [u8; 8] = + bytes.as_slice().try_into().map_err(|_| StorageError::BadValueLen { + what: "entry count", + expected: 8, + found: bytes.len(), + })?; + Ok(usize::from_be_bytes(arr)) + }) + } + + /// Inserts a key-value pair into the SMT leaf at the specified logical `index`. + /// + /// This operation involves: + /// 1. Retrieving the current leaf (if any) at `index`. + /// 2. Inserting the new key-value pair into the leaf. + /// 3. Updating the leaf and entry counts in the metadata column family. + /// 4. Writing all changes (leaf data, counts) to RocksDB in a single batch. + /// + /// Note: This only updates the leaf. Callers are responsible for recomputing and + /// persisting the corresponding inner nodes. + /// + /// # Errors + /// - `StorageError::Backend`: If column families are missing or a RocksDB error occurs. + /// - `StorageError::DeserializationError`: If existing leaf data is corrupt. + #[expect(clippy::single_match_else)] + fn insert_value( + &self, + index: u64, + key: Word, + value: Word, + ) -> Result, StorageError> { + debug_assert_ne!(value, EMPTY_WORD); + + let mut batch = WriteBatch::default(); + + // Fetch initial counts. + let mut current_leaf_count = self.leaf_count()?; + let mut current_entry_count = self.entry_count()?; + + let leaves_cf = self.cf_handle(LEAVES_CF)?; + let db_key = Self::index_db_key(index); + + let maybe_leaf = self.get_leaf(index)?; + + let value_to_return: Option = match maybe_leaf { + Some(mut existing_leaf) => { + let old_value = insert_into_leaf(&mut existing_leaf, key, value)?; + // Determine if the overall SMT entry_count needs to change. + // entry_count increases if: + // 1. The key was not present in this leaf before (`old_value` is `None`). + // 2. The key was present but held `EMPTY_WORD` (`old_value` is + // `Some(EMPTY_WORD)`). + if old_value.is_none_or(|old_v| old_v == EMPTY_WORD) { + current_entry_count += 1; + } + // current_leaf_count does not change because the leaf itself already existed. + batch.put_cf(leaves_cf, db_key, existing_leaf.to_bytes()); + old_value + }, + None => { + // Leaf at `index` does not exist, so create a new one. + let new_leaf = SmtLeaf::Single((key, value)); + // A new leaf is created. + current_leaf_count += 1; + // This new leaf contains one new SMT entry. + current_entry_count += 1; + batch.put_cf(leaves_cf, db_key, new_leaf.to_bytes()); + // No previous value, as the leaf (and thus the key in it) was new. + None + }, + }; + + // Add updated metadata counts to the batch. + let metadata_cf = self.cf_handle(METADATA_CF)?; + batch.put_cf(metadata_cf, LEAF_COUNT_KEY, current_leaf_count.to_be_bytes()); + batch.put_cf(metadata_cf, ENTRY_COUNT_KEY, current_entry_count.to_be_bytes()); + + // Atomically write all changes (leaf data and metadata counts). + self.db.write(batch).map_err(map_rocksdb_err)?; + + Ok(value_to_return) + } + + /// Removes a key-value pair from the SMT leaf at the specified logical `index`. + /// + /// This operation involves: + /// 1. Retrieving the leaf at `index`. + /// 2. Removing the `key` from the leaf. If the leaf becomes empty, it's deleted from RocksDB. + /// 3. Updating the leaf and entry counts in the metadata column family. + /// 4. Writing all changes (leaf data/deletion, counts) to RocksDB in a single batch. + /// + /// Returns `Ok(None)` if the leaf at `index` does not exist or the `key` is not found. + /// + /// Note: This only updates the leaf. Callers are responsible for recomputing and + /// persisting the corresponding inner nodes. + /// + /// # Errors + /// - `StorageError::Backend`: If column families are missing or a RocksDB error occurs. + /// - `StorageError::DeserializationError`: If existing leaf data is corrupt. + fn remove_value(&self, index: u64, key: Word) -> Result, StorageError> { + let Some(mut leaf) = self.get_leaf(index)? else { + return Ok(None); + }; + + let mut batch = WriteBatch::default(); + let cf = self.cf_handle(LEAVES_CF)?; + let metadata_cf = self.cf_handle(METADATA_CF)?; + let db_key = Self::index_db_key(index); + let mut entry_count = self.entry_count()?; + let mut leaf_count = self.leaf_count()?; + + let (current_value, is_empty) = remove_from_leaf(&mut leaf, key); + if let Some(current_value) = current_value + && current_value != EMPTY_WORD + { + entry_count -= 1; + } + if is_empty { + leaf_count -= 1; + batch.delete_cf(cf, db_key); + } else { + batch.put_cf(cf, db_key, leaf.to_bytes()); + } + batch.put_cf(metadata_cf, LEAF_COUNT_KEY, leaf_count.to_be_bytes()); + batch.put_cf(metadata_cf, ENTRY_COUNT_KEY, entry_count.to_be_bytes()); + self.db.write(batch).map_err(map_rocksdb_err)?; + Ok(current_value) + } + + /// Retrieves a single SMT leaf node by its logical `index` from the `LEAVES_CF` column family. + /// + /// # Errors + /// - `StorageError::Backend`: If the leaves column family is missing or a RocksDB error occurs. + /// - `StorageError::DeserializationError`: If the retrieved leaf data is corrupt. + fn get_leaf(&self, index: u64) -> Result, StorageError> { + let cf = self.cf_handle(LEAVES_CF)?; + let key = Self::index_db_key(index); + match self.db.get_cf(cf, key).map_err(map_rocksdb_err)? { + Some(bytes) => { + let leaf = SmtLeaf::read_from_bytes(&bytes)?; + Ok(Some(leaf)) + }, + None => Ok(None), + } + } + + /// Sets or updates multiple SMT leaf nodes in the `LEAVES_CF` column family. + /// + /// This method performs a batch write to RocksDB. It also updates the global + /// leaf and entry counts in the `METADATA_CF` based on the provided `leaves` map, + /// overwriting any previous counts. + /// + /// Note: This method assumes the provided `leaves` map represents the entirety + /// of leaves to be stored or that counts are being explicitly reset. + /// Note: This only updates the leaves. Callers are responsible for recomputing and + /// persisting the corresponding inner nodes. + /// + /// # Errors + /// - `StorageError::Backend`: If column families are missing or a RocksDB error occurs. + fn set_leaves(&self, leaves: Map) -> Result<(), StorageError> { + let cf = self.cf_handle(LEAVES_CF)?; + let leaf_count: usize = leaves.len(); + let entry_count: usize = leaves.values().map(|leaf| leaf.entries().len()).sum(); + let mut batch = WriteBatch::default(); + for (idx, leaf) in leaves { + let key = Self::index_db_key(idx); + let value = leaf.to_bytes(); + batch.put_cf(cf, key, &value); + } + let metadata_cf = self.cf_handle(METADATA_CF)?; + batch.put_cf(metadata_cf, LEAF_COUNT_KEY, leaf_count.to_be_bytes()); + batch.put_cf(metadata_cf, ENTRY_COUNT_KEY, entry_count.to_be_bytes()); + self.db.write(batch).map_err(map_rocksdb_err)?; + Ok(()) + } + + /// Removes a single SMT leaf node by its logical `index` from the `LEAVES_CF` column family. + /// + /// Important: This method currently *does not* update the global leaf and entry counts + /// in the metadata. Callers are responsible for managing these counts separately + /// if using this method directly, or preferably use `apply` or `remove_value` which handle + /// counts. + /// + /// Note: This only removes the leaf. Callers are responsible for recomputing and + /// persisting the corresponding inner nodes. + /// + /// # Errors + /// - `StorageError::Backend`: If the leaves column family is missing or a RocksDB error occurs. + /// - `StorageError::DeserializationError`: If the retrieved (to be returned) leaf data is + /// corrupt. + fn remove_leaf(&self, index: u64) -> Result, StorageError> { + let key = Self::index_db_key(index); + let cf = self.cf_handle(LEAVES_CF)?; + let old_bytes = self.db.get_cf(cf, key).map_err(map_rocksdb_err)?; + self.db.delete_cf(cf, key).map_err(map_rocksdb_err)?; + Ok(old_bytes + .map(|bytes| SmtLeaf::read_from_bytes(&bytes).expect("failed to deserialize leaf"))) + } + + /// Retrieves multiple SMT leaf nodes by their logical `indices` using RocksDB's `multi_get_cf`. + /// + /// # Errors + /// - `StorageError::Backend`: If the leaves column family is missing or a RocksDB error occurs. + /// - `StorageError::DeserializationError`: If any retrieved leaf data is corrupt. + fn get_leaves(&self, indices: &[u64]) -> Result>, StorageError> { + let cf = self.cf_handle(LEAVES_CF)?; + let db_keys: Vec<[u8; 8]> = indices.iter().map(|&idx| Self::index_db_key(idx)).collect(); + let results = self.db.multi_get_cf(db_keys.iter().map(|k| (cf, k.as_ref()))); + + results + .into_iter() + .map(|result| match result { + Ok(Some(bytes)) => Ok(Some(SmtLeaf::read_from_bytes(&bytes)?)), + Ok(None) => Ok(None), + Err(e) => Err(map_rocksdb_err(e)), + }) + .collect() + } + + /// Returns true if the storage has any leaves. + /// + /// # Errors + /// Returns `StorageError` if the storage read operation fails. + fn has_leaves(&self) -> Result { + Ok(self.leaf_count()? > 0) + } + + /// Batch-retrieves multiple subtrees from RocksDB by their node indices. + /// + /// This method groups requests by subtree depth into column family buckets, + /// then performs parallel `multi_get` operations to efficiently retrieve + /// all subtrees. Results are deserialized and placed in the same order as + /// the input indices. + /// + /// Note: Retrieval is performed in parallel. If multiple errors occur (e.g., + /// deserialization or backend errors), only the first one encountered is returned. + /// Other errors will be discarded. + /// + /// # Parameters + /// - `indices`: A slice of subtree root indices to retrieve. + /// + /// # Returns + /// - A `Vec>` where each index corresponds to the original input. + /// - `Ok(...)` if all fetches succeed. + /// - `Err(StorageError)` if any RocksDB access or deserialization fails. + fn get_subtree(&self, index: NodeIndex) -> Result, StorageError> { + let cf = self.subtree_cf(index); + let key = Self::subtree_db_key(index); + match self.db.get_cf(cf, key).map_err(map_rocksdb_err)? { + Some(bytes) => { + let subtree = Subtree::from_vec(index, &bytes)?; + Ok(Some(subtree)) + }, + None => Ok(None), + } + } + + /// Batch-retrieves multiple subtrees from RocksDB by their node indices. + /// + /// This method groups requests by subtree depth into column family buckets, + /// then performs parallel `multi_get` operations to efficiently retrieve + /// all subtrees. Results are deserialized and placed in the same order as + /// the input indices. + /// + /// # Parameters + /// - `indices`: A slice of subtree root indices to retrieve. + /// + /// # Returns + /// - A `Vec>` where each index corresponds to the original input. + /// - `Ok(...)` if all fetches succeed. + /// - `Err(StorageError)` if any RocksDB access or deserialization fails. + fn get_subtrees(&self, indices: &[NodeIndex]) -> Result>, StorageError> { + use rayon::prelude::*; + + let mut depth_buckets: [Vec<(usize, NodeIndex)>; 5] = Default::default(); + + for (original_index, &node_index) in indices.iter().enumerate() { + let depth = node_index.depth(); + let bucket_index = match depth { + 56 => 0, + 48 => 1, + 40 => 2, + 32 => 3, + 24 => 4, + _ => { + return Err(StorageError::Unsupported(format!( + "unsupported subtree depth {depth}" + ))); + }, + }; + depth_buckets[bucket_index].push((original_index, node_index)); + } + let mut results = vec![None; indices.len()]; + + // Process depth buckets in parallel + let bucket_results: Result, StorageError> = depth_buckets + .into_par_iter() + .enumerate() + .filter(|(_, bucket)| !bucket.is_empty()) + .map( + |(bucket_index, bucket)| -> Result)>, StorageError> { + let depth = SUBTREE_DEPTHS[bucket_index]; + let cf = self.cf_handle(cf_for_depth(depth))?; + let keys: Vec<_> = + bucket.iter().map(|(_, idx)| Self::subtree_db_key(*idx)).collect(); + + let db_results = self.db.multi_get_cf(keys.iter().map(|k| (cf, k.as_ref()))); + + // Process results for this bucket + bucket + .into_iter() + .zip(db_results) + .map(|((original_index, node_index), db_result)| { + let subtree = match db_result { + Ok(Some(bytes)) => Some(Subtree::from_vec(node_index, &bytes)?), + Ok(None) => None, + Err(e) => return Err(map_rocksdb_err(e)), + }; + Ok((original_index, subtree)) + }) + .collect() + }, + ) + .collect(); + + // Flatten results and place them in correct positions + for bucket_result in bucket_results? { + for (original_index, subtree) in bucket_result { + results[original_index] = subtree; + } + } + + Ok(results) + } + + /// Stores a single subtree in RocksDB and optionally updates the depth-24 root cache. + /// + /// The subtree is serialized and written to its corresponding column family. + /// If it's a depth-24 subtree, the root node’s hash is also stored in the + /// dedicated `DEPTH_24_CF` cache to support top-level reconstruction. + /// + /// # Parameters + /// - `subtree`: A reference to the subtree to be stored. + /// + /// # Errors + /// - Returns `StorageError` if column family lookup, serialization, or the write operation + /// fails. + fn set_subtree(&self, subtree: &Subtree) -> Result<(), StorageError> { + let subtrees_cf = self.subtree_cf(subtree.root_index()); + let mut batch = WriteBatch::default(); + + let key = Self::subtree_db_key(subtree.root_index()); + let value = subtree.to_vec(); + batch.put_cf(subtrees_cf, key, value); + + // Also update level 24 hash cache if this is a level 24 subtree + if subtree.root_index().depth() == IN_MEMORY_DEPTH { + let root_hash = subtree + .get_inner_node(subtree.root_index()) + .ok_or_else(|| StorageError::Unsupported("Subtree root node not found".into()))? + .hash(); + + let depth24_cf = self.cf_handle(DEPTH_24_CF)?; + let hash_key = Self::index_db_key(subtree.root_index().value()); + batch.put_cf(depth24_cf, hash_key, root_hash.to_bytes()); + } + + self.db.write(batch).map_err(map_rocksdb_err)?; + Ok(()) + } + + /// Bulk-writes subtrees to storage (bypassing WAL). + /// + /// This method writes a vector of serialized `Subtree` objects directly to their + /// corresponding RocksDB column families based on their root index. + /// + /// ⚠️ **Warning:** This function should only be used during **initial SMT construction**. + /// It disables the WAL, meaning writes are **not crash-safe** and can result in data loss + /// if the process terminates unexpectedly. + /// + /// # Parameters + /// - `subtrees`: A vector of `Subtree` objects to be serialized and persisted. + /// + /// # Errors + /// - Returns `StorageError::Backend` if any column family lookup or RocksDB write fails. + fn set_subtrees(&self, subtrees: Vec) -> Result<(), StorageError> { + let depth24_cf = self.cf_handle(DEPTH_24_CF)?; + let mut batch = WriteBatch::default(); + + for subtree in subtrees { + let subtrees_cf = self.subtree_cf(subtree.root_index()); + let key = Self::subtree_db_key(subtree.root_index()); + let value = subtree.to_vec(); + batch.put_cf(subtrees_cf, key, value); + + if subtree.root_index().depth() == IN_MEMORY_DEPTH + && let Some(root_node) = subtree.get_inner_node(subtree.root_index()) + { + let hash_key = Self::index_db_key(subtree.root_index().value()); + batch.put_cf(depth24_cf, hash_key, root_node.hash().to_bytes()); + } + } + + self.db.write(batch).map_err(map_rocksdb_err)?; + Ok(()) + } + + /// Removes a single SMT Subtree from storage, identified by its root `NodeIndex`. + /// + /// # Errors + /// - `StorageError::Backend`: If the subtrees column family is missing or a RocksDB error + /// occurs. + fn remove_subtree(&self, index: NodeIndex) -> Result<(), StorageError> { + let subtrees_cf = self.subtree_cf(index); + let mut batch = WriteBatch::default(); + + let key = Self::subtree_db_key(index); + batch.delete_cf(subtrees_cf, key); + + // Also remove level 24 hash cache if this is a level 24 subtree + if index.depth() == IN_MEMORY_DEPTH { + let depth24_cf = self.cf_handle(DEPTH_24_CF)?; + let hash_key = Self::index_db_key(index.value()); + batch.delete_cf(depth24_cf, hash_key); + } + + self.db.write(batch).map_err(map_rocksdb_err)?; + Ok(()) + } + + /// Retrieves a single inner node (non-leaf node) from within a Subtree. + /// + /// This method is intended for accessing nodes at depths greater than or equal to + /// `IN_MEMORY_DEPTH`. It first finds the appropriate Subtree containing the `index`, then + /// delegates to `Subtree::get_inner_node()`. + /// + /// # Errors + /// - `StorageError::Backend`: If `index.depth() < IN_MEMORY_DEPTH`, or if RocksDB errors occur. + /// - `StorageError::Value`: If the containing Subtree data is corrupt. + fn get_inner_node(&self, index: NodeIndex) -> Result, StorageError> { + if index.depth() < IN_MEMORY_DEPTH { + return Err(StorageError::Unsupported( + "Cannot get inner node from upper part of the tree".into(), + )); + } + let subtree_root_index = Subtree::find_subtree_root(index); + Ok(self + .get_subtree(subtree_root_index)? + .and_then(|subtree| subtree.get_inner_node(index))) + } + + /// Sets or updates a single inner node (non-leaf node) within a Subtree. + /// + /// This method is intended for `index.depth() >= IN_MEMORY_DEPTH`. + /// If the target Subtree does not exist, it is created. The `node` is then + /// inserted into the Subtree, and the modified Subtree is written back to storage. + /// + /// # Errors + /// - `StorageError::Backend`: If `index.depth() < IN_MEMORY_DEPTH`, or if RocksDB errors occur. + /// - `StorageError::Value`: If existing Subtree data is corrupt. + fn set_inner_node( + &self, + index: NodeIndex, + node: InnerNode, + ) -> Result, StorageError> { + if index.depth() < IN_MEMORY_DEPTH { + return Err(StorageError::Unsupported( + "Cannot set inner node in upper part of the tree".into(), + )); + } + + let subtree_root_index = Subtree::find_subtree_root(index); + let mut subtree = self + .get_subtree(subtree_root_index)? + .unwrap_or_else(|| Subtree::new(subtree_root_index)); + let old_node = subtree.insert_inner_node(index, node); + self.set_subtree(&subtree)?; + Ok(old_node) + } + + /// Removes a single inner node (non-leaf node) from within a Subtree. + /// + /// This method is intended for `index.depth() >= IN_MEMORY_DEPTH`. + /// If the Subtree becomes empty after removing the node, the Subtree itself + /// is removed from storage. + /// + /// # Errors + /// - `StorageError::Backend`: If `index.depth() < IN_MEMORY_DEPTH`, or if RocksDB errors occur. + /// - `StorageError::Value`: If existing Subtree data is corrupt. + fn remove_inner_node(&self, index: NodeIndex) -> Result, StorageError> { + if index.depth() < IN_MEMORY_DEPTH { + return Err(StorageError::Unsupported( + "Cannot remove inner node from upper part of the tree".into(), + )); + } + + let subtree_root_index = Subtree::find_subtree_root(index); + self.get_subtree(subtree_root_index) + .and_then(|maybe_subtree| match maybe_subtree { + Some(mut subtree) => { + let old_node = subtree.remove_inner_node(index); + let db_operation_result = if subtree.is_empty() { + self.remove_subtree(subtree_root_index) + } else { + self.set_subtree(&subtree) + }; + db_operation_result.map(|_| old_node) + }, + None => Ok(None), + }) + } + + /// Applies a batch of `StorageUpdates` atomically to the RocksDB backend. + /// + /// This is the primary method for persisting changes to the SMT. It constructs a single + /// RocksDB `WriteBatch` containing all specified changes: + /// - Leaf updates/deletions in `LEAVES_CF`. + /// - Subtree updates/deletions in `SUBTREE_24_CF`, `SUBTREE_32_CF`, `SUBTREE_40_CF`, + /// `SUBTREE_48_CF`, `SUBTREE_56_CF`. + /// - Updates to leaf and entry counts in `METADATA_CF` based on `leaf_count_delta` and + /// `entry_count_delta`. + /// - Sets the new SMT root in `METADATA_CF`. + /// + /// All operations in the batch are applied atomically by RocksDB. + /// + /// # Errors + /// - `StorageError::Backend`: If any column family is missing or a RocksDB write error occurs. + fn apply(&self, updates: StorageUpdates) -> Result<(), StorageError> { + use rayon::prelude::*; + + let mut batch = WriteBatch::default(); + + let leaves_cf = self.cf_handle(LEAVES_CF)?; + let metadata_cf = self.cf_handle(METADATA_CF)?; + let depth24_cf = self.cf_handle(DEPTH_24_CF)?; + + let StorageUpdateParts { + leaf_updates, + subtree_updates, + new_root, + leaf_count_delta, + entry_count_delta, + } = updates.into_parts(); + + // Process leaf updates + for (index, maybe_leaf) in leaf_updates { + let key = Self::index_db_key(index); + match maybe_leaf { + Some(leaf) => batch.put_cf(leaves_cf, key, leaf.to_bytes()), + None => batch.delete_cf(leaves_cf, key), + } + } + + // Helper for depth 24 operations + let is_depth_24 = |index: NodeIndex| index.depth() == IN_MEMORY_DEPTH; + + // Parallel preparation of subtree operations + let subtree_ops: Result, StorageError> = subtree_updates + .into_par_iter() + .map(|update| -> Result<_, StorageError> { + let (index, maybe_bytes, depth24_op) = match update { + SubtreeUpdate::Store { index, subtree } => { + let bytes = subtree.to_vec(); + let depth24_op = is_depth_24(index) + .then(|| subtree.get_inner_node(index)) + .flatten() + .map(|root_node| { + let hash_key = Self::index_db_key(index.value()); + (hash_key, Some(root_node.hash().to_bytes())) + }); + (index, Some(bytes), depth24_op) + }, + SubtreeUpdate::Delete { index } => { + let depth24_op = is_depth_24(index).then(|| { + let hash_key = Self::index_db_key(index.value()); + (hash_key, None) + }); + (index, None, depth24_op) + }, + }; + + let key = Self::subtree_db_key(index); + let subtrees_cf = self.subtree_cf(index); + + Ok((subtrees_cf, key, maybe_bytes, depth24_op)) + }) + .collect(); + + // Sequential batch building + for (subtrees_cf, key, maybe_bytes, depth24_op) in subtree_ops? { + match maybe_bytes { + Some(bytes) => batch.put_cf(subtrees_cf, key, bytes), + None => batch.delete_cf(subtrees_cf, key), + } + + if let Some((hash_key, maybe_hash_bytes)) = depth24_op { + match maybe_hash_bytes { + Some(hash_bytes) => batch.put_cf(depth24_cf, hash_key, hash_bytes), + None => batch.delete_cf(depth24_cf, hash_key), + } + } + } + + if leaf_count_delta != 0 || entry_count_delta != 0 { + let current_leaf_count = self.leaf_count()?; + let current_entry_count = self.entry_count()?; + + let new_leaf_count = current_leaf_count.saturating_add_signed(leaf_count_delta); + let new_entry_count = current_entry_count.saturating_add_signed(entry_count_delta); + + batch.put_cf(metadata_cf, LEAF_COUNT_KEY, new_leaf_count.to_be_bytes()); + batch.put_cf(metadata_cf, ENTRY_COUNT_KEY, new_entry_count.to_be_bytes()); + } + + batch.put_cf(metadata_cf, ROOT_KEY, new_root.to_bytes()); + + let mut write_opts = rocksdb::WriteOptions::default(); + // Disable immediate WAL sync to disk for better performance + write_opts.set_sync(false); + self.db.write_opt(batch, &write_opts).map_err(map_rocksdb_err)?; + + Ok(()) + } + + /// Returns an iterator over all (logical u64 index, `SmtLeaf`) pairs in the `LEAVES_CF`. + /// + /// The iterator uses a RocksDB snapshot for consistency and iterates in lexicographical + /// order of the keys (leaf indices). Errors during iteration (e.g., deserialization issues) + /// cause the iterator to skip the problematic item and attempt to continue. + /// + /// # Errors + /// - `StorageError::Backend`: If the leaves column family is missing or a RocksDB error occurs + /// during iterator creation. + fn iter_leaves(&self) -> Result + '_>, StorageError> { + let cf = self.cf_handle(LEAVES_CF)?; + let mut read_opts = ReadOptions::default(); + read_opts.set_total_order_seek(true); + let db_iter = self.db.iterator_cf_opt(cf, read_opts, IteratorMode::Start); + + Ok(Box::new(RocksDbDirectLeafIterator { iter: db_iter })) + } + + /// Returns an iterator over all `Subtree` instances across all subtree column families. + /// + /// The iterator uses a RocksDB snapshot and iterates in lexicographical order of keys + /// (subtree root NodeIndex) across all depth column families (24, 32, 40, 48, 56). + /// Errors during iteration (e.g., deserialization issues) cause the iterator to skip + /// the problematic item and attempt to continue. + /// + /// # Errors + /// - `StorageError::Backend`: If any subtree column family is missing or a RocksDB error occurs + /// during iterator creation. + fn iter_subtrees(&self) -> Result + '_>, StorageError> { + // All subtree column family names in order + const SUBTREE_CFS: [&str; 5] = + [SUBTREE_24_CF, SUBTREE_32_CF, SUBTREE_40_CF, SUBTREE_48_CF, SUBTREE_56_CF]; + + let mut cf_handles = Vec::new(); + for cf_name in SUBTREE_CFS { + cf_handles.push(self.cf_handle(cf_name)?); + } + + Ok(Box::new(RocksDbSubtreeIterator::new(&self.db, cf_handles))) + } + + /// Retrieves all depth 24 hashes for fast tree rebuilding. + /// + /// # Errors + /// - `StorageError::Backend`: If the depth24 column family is missing or a RocksDB error + /// occurs. + /// - `StorageError::Value`: If any hash bytes are corrupt. + fn get_depth24(&self) -> Result, StorageError> { + let cf = self.cf_handle(DEPTH_24_CF)?; + let iter = self.db.iterator_cf(cf, IteratorMode::Start); + let mut hashes = Vec::new(); + + for item in iter { + let (key_bytes, value_bytes) = item.map_err(map_rocksdb_err)?; + + let index = index_from_key_bytes(&key_bytes)?; + let hash = Word::read_from_bytes(&value_bytes)?; + + hashes.push((index, hash)); + } + + Ok(hashes) + } +} + +/// Syncs the RocksDB database to disk before dropping the storage. +/// +/// This ensures that all data is persisted to disk before the storage is dropped. +/// +/// # Panics +/// - If the RocksDB sync operation fails. +impl Drop for RocksDbStorage { + fn drop(&mut self) { + if let Err(e) = self.sync() { + panic!("failed to flush RocksDB on drop: {e}"); + } + } +} + +// ITERATORS +// -------------------------------------------------------------------------------------------- + +/// An iterator over leaves directly from RocksDB. +/// +/// Wraps a `DBIteratorWithThreadMode` and handles deserialization of keys to `u64` (leaf index) +/// and values to `SmtLeaf`. Skips items that fail to deserialize or if a RocksDB error occurs +/// for an item, attempting to continue iteration. +struct RocksDbDirectLeafIterator<'a> { + iter: DBIteratorWithThreadMode<'a, DB>, +} + +impl Iterator for RocksDbDirectLeafIterator<'_> { + type Item = (u64, SmtLeaf); + + fn next(&mut self) -> Option { + self.iter.find_map(|result| { + let (key_bytes, value_bytes) = result.ok()?; + let leaf_idx = index_from_key_bytes(&key_bytes).ok()?; + let leaf = SmtLeaf::read_from_bytes(&value_bytes).ok()?; + Some((leaf_idx, leaf)) + }) + } +} + +/// An iterator over subtrees from multiple RocksDB column families. +/// +/// Iterates through all subtree column families (24, 32, 40, 48, 56) sequentially. +/// When one column family is exhausted, it moves to the next one. +struct RocksDbSubtreeIterator<'a> { + db: &'a DB, + cf_handles: Vec<&'a rocksdb::ColumnFamily>, + current_cf_index: usize, + current_iter: Option>, +} + +impl<'a> RocksDbSubtreeIterator<'a> { + fn new(db: &'a DB, cf_handles: Vec<&'a rocksdb::ColumnFamily>) -> Self { + let mut iterator = Self { + db, + cf_handles, + current_cf_index: 0, + current_iter: None, + }; + iterator.advance_to_next_cf(); + iterator + } + + fn advance_to_next_cf(&mut self) { + if self.current_cf_index < self.cf_handles.len() { + let cf = self.cf_handles[self.current_cf_index]; + let mut read_opts = ReadOptions::default(); + read_opts.set_total_order_seek(true); + self.current_iter = Some(self.db.iterator_cf_opt(cf, read_opts, IteratorMode::Start)); + } else { + self.current_iter = None; + } + } + + fn try_next_from_iter( + iter: &mut DBIteratorWithThreadMode, + cf_index: usize, + ) -> Option { + iter.find_map(|result| { + let (key_bytes, value_bytes) = result.ok()?; + let depth = 24 + (cf_index * 8) as u8; + + let node_idx = subtree_root_from_key_bytes(&key_bytes, depth).ok()?; + let value_vec = value_bytes.into_vec(); + Subtree::from_vec(node_idx, &value_vec).ok() + }) + } +} + +impl Iterator for RocksDbSubtreeIterator<'_> { + type Item = Subtree; + + fn next(&mut self) -> Option { + loop { + let iter = self.current_iter.as_mut()?; + + // Try to get the next valid subtree from current iterator + if let Some(subtree) = Self::try_next_from_iter(iter, self.current_cf_index) { + return Some(subtree); + } + + // Current CF exhausted, advance to next + self.current_cf_index += 1; + self.advance_to_next_cf(); + + // If no more CFs, we're done + self.current_iter.as_ref()?; + } + } +} + +// ROCKSDB CONFIGURATION +// -------------------------------------------------------------------------------------------- + +/// Configuration for RocksDB storage used by the Sparse Merkle Tree implementation. +/// +/// This struct contains the essential configuration parameters needed to initialize +/// and optimize RocksDB for SMT storage operations. It provides sensible defaults +/// while allowing customization for specific performance requirements. +#[derive(Debug, Clone)] +pub struct RocksDbConfig { + /// The filesystem path where the RocksDB database will be stored. + /// + /// This should be a directory path that the application has read/write permissions for. + /// The database will create multiple files in this directory to store data, logs, and + /// metadata. + pub(crate) path: PathBuf, + + /// The size of the RocksDB block cache in bytes. + /// + /// This cache stores frequently accessed data blocks in memory to improve read performance. + /// Larger cache sizes generally improve read performance but consume more memory. + /// Default: 1GB (1 << 30 bytes) + pub(crate) cache_size: usize, + + /// The maximum number of files that RocksDB can have open simultaneously. + /// + /// This setting affects both memory usage and the number of file descriptors used by the + /// process. Higher values may improve performance for databases with many SST files but + /// increase resource usage. Default: 512 files + pub(crate) max_open_files: i32, +} + +impl RocksDbConfig { + /// Creates a new RocksDbConfig with the given database path and default settings. + /// + /// # Arguments + /// * `path` - The filesystem path where the RocksDB database will be stored. This can be any + /// type that converts into a `PathBuf`. + /// + /// # Default Settings + /// * `cache_size`: 1GB (1,073,741,824 bytes) + /// * `max_open_files`: 512 + /// + /// # Examples + /// ``` + /// use miden_large_smt_backend_rocksdb::RocksDbConfig; + /// + /// let config = RocksDbConfig::new("/path/to/database"); + /// ``` + pub fn new>(path: P) -> Self { + Self { + path: path.into(), + cache_size: 1 << 30, + max_open_files: 512, + } + } + + /// Sets the block cache size for RocksDB. + /// + /// The block cache stores frequently accessed data blocks in memory to improve read + /// performance. Larger cache sizes generally improve read performance but consume more + /// memory. + /// + /// # Arguments + /// * `size` - The cache size in bytes. + /// + /// # Examples + /// ``` + /// use miden_large_smt_backend_rocksdb::RocksDbConfig; + /// + /// let config = RocksDbConfig::new("/path/to/database") + /// .with_cache_size(2 * 1024 * 1024 * 1024); // 2GB cache + /// ``` + #[must_use] + pub fn with_cache_size(mut self, size: usize) -> Self { + self.cache_size = size; + self + } + + /// Sets the maximum number of files that RocksDB can have open simultaneously. + /// + /// This setting affects both memory usage and the number of file descriptors used by the + /// process. Higher values may improve performance for databases with many SST files but + /// increase resource usage. + /// + /// # Arguments + /// * `count` - The maximum number of open files. Must be positive. + /// + /// # Examples + /// ``` + /// use miden_large_smt_backend_rocksdb::RocksDbConfig; + /// + /// let config = RocksDbConfig::new("/path/to/database") + /// .with_max_open_files(1024); // Allow up to 1024 open files + /// ``` + #[must_use] + pub fn with_max_open_files(mut self, count: i32) -> Self { + self.max_open_files = count; + self + } +} + +// SUBTREE DB KEY +// -------------------------------------------------------------------------------------------- + +/// Compact key wrapper for variable-length subtree prefixes. +/// +/// * `bytes` always holds the big-endian 8-byte value. +/// * `len` is how many leading bytes are significant (3-7). +#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] +pub(crate) struct KeyBytes { + bytes: [u8; 8], + len: u8, +} + +impl KeyBytes { + #[inline(always)] + pub fn new(value: u64, keep: usize) -> Self { + debug_assert!((3..=7).contains(&keep)); + let bytes = value.to_be_bytes(); + debug_assert!(bytes[..8 - keep].iter().all(|&b| b == 0)); + Self { bytes, len: keep as u8 } + } + + #[inline(always)] + pub fn as_slice(&self) -> &[u8] { + &self.bytes[8 - self.len as usize..] + } +} + +impl AsRef<[u8]> for KeyBytes { + #[inline(always)] + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +// HELPERS +// -------------------------------------------------------------------------------------------- + +/// Deserializes an index (u64) from a RocksDB key byte slice. +/// Expects `key_bytes` to be exactly 8 bytes long. +/// +/// # Errors +/// - `StorageError::BadKeyLen`: If `key_bytes` is not 8 bytes long or conversion fails. +fn index_from_key_bytes(key_bytes: &[u8]) -> Result { + if key_bytes.len() != 8 { + return Err(StorageError::BadKeyLen { expected: 8, found: key_bytes.len() }); + } + let mut arr = [0u8; 8]; + arr.copy_from_slice(key_bytes); + Ok(u64::from_be_bytes(arr)) +} + +/// Reconstructs a `NodeIndex` from the variable-length subtree key stored in `RocksDB`. +/// +/// * `key_bytes` is the big-endian tail of the 64-bit value: +/// - depth 56 → 7 bytes +/// - depth 48 → 6 bytes +/// - depth 40 → 5 bytes +/// - depth 32 → 4 bytes +/// - depth 24 → 3 bytes +/// +/// # Errors +/// * `StorageError::Unsupported` - `depth` is not one of 24/32/40/48/56. +/// * `StorageError::DeserializationError` - `key_bytes.len()` does not match the length required by +/// `depth`. +#[inline(always)] +fn subtree_root_from_key_bytes(key_bytes: &[u8], depth: u8) -> Result { + let expected = match depth { + 24 => 3, + 32 => 4, + 40 => 5, + 48 => 6, + 56 => 7, + d => return Err(StorageError::Unsupported(format!("unsupported subtree depth {d}"))), + }; + + if key_bytes.len() != expected { + return Err(StorageError::BadSubtreeKeyLen { depth, expected, found: key_bytes.len() }); + } + let mut buf = [0u8; 8]; + buf[8 - expected..].copy_from_slice(key_bytes); + let value = u64::from_be_bytes(buf); + Ok(NodeIndex::new_unchecked(depth, value)) +} + +/// Helper that maps an SMT depth to its column family. +#[inline(always)] +fn cf_for_depth(depth: u8) -> &'static str { + match depth { + 24 => SUBTREE_24_CF, + 32 => SUBTREE_32_CF, + 40 => SUBTREE_40_CF, + 48 => SUBTREE_48_CF, + 56 => SUBTREE_56_CF, + _ => panic!("unsupported subtree depth: {depth}"), + } +} diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 17dcf9619..a5531d46f 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -15,24 +15,25 @@ version.workspace = true workspace = true [dependencies] -anyhow = { workspace = true } -deadpool = { default-features = false, features = ["managed", "rt_tokio_1"], version = "0.12" } -deadpool-diesel = { features = ["sqlite"], version = "0.6" } -diesel = { features = ["numeric", "sqlite"], version = "2.3" } -diesel_migrations = { features = ["sqlite"], version = "2.3" } -fs-err = { workspace = true } -futures = { workspace = true } -hex = { version = "0.4" } -indexmap = { workspace = true } -libsqlite3-sys = { workspace = true } -miden-block-prover = { workspace = true } -miden-crypto = { features = ["concurrent", "hashmaps"], workspace = true } -miden-node-db = { workspace = true } -miden-node-proto = { workspace = true } -miden-node-proto-build = { features = ["internal"], workspace = true } -miden-node-utils = { workspace = true } -miden-remote-prover-client = { features = ["block-prover"], workspace = true } -miden-standards = { workspace = true } +anyhow = { workspace = true } +deadpool = { default-features = false, features = ["managed", "rt_tokio_1"], version = "0.12" } +deadpool-diesel = { features = ["sqlite"], version = "0.6" } +diesel = { features = ["numeric", "sqlite"], version = "2.3" } +diesel_migrations = { features = ["sqlite"], version = "2.3" } +fs-err = { workspace = true } +futures = { workspace = true } +hex = { version = "0.4" } +indexmap = { workspace = true } +libsqlite3-sys = { workspace = true } +miden-block-prover = { workspace = true } +miden-crypto = { features = ["concurrent", "hashmaps"], workspace = true } +miden-large-smt-backend-rocksdb = { optional = true, workspace = true } +miden-node-db = { workspace = true } +miden-node-proto = { workspace = true } +miden-node-proto-build = { features = ["internal"], workspace = true } +miden-node-utils = { workspace = true } +miden-remote-prover-client = { features = ["block-prover"], workspace = true } +miden-standards = { workspace = true } # TODO remove `testing` from `miden-protocol`, required for `BlockProof::new_dummy` miden-protocol = { features = ["std", "testing"], workspace = true } pretty_assertions = { workspace = true } @@ -72,7 +73,7 @@ termtree = "1.0" [features] default = ["rocksdb"] -rocksdb = ["miden-crypto/rocksdb"] +rocksdb = ["dep:miden-large-smt-backend-rocksdb"] [[bench]] harness = false diff --git a/crates/store/benches/account_tree.rs b/crates/store/benches/account_tree.rs index 8c3f1009e..e69da7714 100644 --- a/crates/store/benches/account_tree.rs +++ b/crates/store/benches/account_tree.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; -use miden_crypto::merkle::smt::{RocksDbConfig, RocksDbStorage}; +use miden_large_smt_backend_rocksdb::{RocksDbConfig, RocksDbStorage}; use miden_node_store::AccountTreeWithHistory; use miden_protocol::Word; use miden_protocol::account::AccountId; diff --git a/crates/store/src/accounts/mod.rs b/crates/store/src/accounts/mod.rs index 2508c9d2d..f9815190b 100644 --- a/crates/store/src/accounts/mod.rs +++ b/crates/store/src/accounts/mod.rs @@ -2,6 +2,8 @@ use std::collections::{BTreeMap, HashMap}; +#[cfg(feature = "rocksdb")] +use miden_large_smt_backend_rocksdb::RocksDbStorage; use miden_protocol::account::{AccountId, AccountIdPrefix}; use miden_protocol::block::BlockNumber; use miden_protocol::block::account_tree::{AccountMutationSet, AccountTree, AccountWitness}; @@ -32,7 +34,7 @@ pub type InMemoryAccountTree = AccountTree>; #[cfg(feature = "rocksdb")] /// Convenience for a persistent account tree. -pub type PersistentAccountTree = AccountTree>; +pub type PersistentAccountTree = AccountTree>; // HISTORICAL ERROR TYPES // ================================================================================================ diff --git a/crates/store/src/state/loader.rs b/crates/store/src/state/loader.rs index 77cd9f4f4..af678899e 100644 --- a/crates/store/src/state/loader.rs +++ b/crates/store/src/state/loader.rs @@ -13,6 +13,8 @@ use std::num::NonZeroUsize; use std::path::Path; use miden_crypto::merkle::mmr::Mmr; +#[cfg(feature = "rocksdb")] +use miden_large_smt_backend_rocksdb::{RocksDbConfig, RocksDbStorage}; use miden_protocol::block::account_tree::{AccountTree, account_id_to_smt_key}; use miden_protocol::block::nullifier_tree::NullifierTree; use miden_protocol::block::{BlockNumber, Blockchain}; @@ -23,11 +25,6 @@ use miden_protocol::{Felt, FieldElement, Word}; #[cfg(feature = "rocksdb")] use tracing::info; use tracing::instrument; -#[cfg(feature = "rocksdb")] -use { - miden_crypto::merkle::smt::RocksDbStorage, - miden_protocol::crypto::merkle::smt::RocksDbConfig, -}; use crate::COMPONENT; use crate::db::Db;