Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- [BREAKING] Changed the serialization format of `PartialSmt` to be more compact on the wire. If you depended on the serialization format directly (or if it is stored), this will be breaking. This ser/de is now covered by fuzz tests.
- [BREAKING] Changed `SmtLeaf::hash` to perform domain-separated hashing, reducing the risk of a collision with the hash of an inner node. Miden VM **must** be updated to comply with this.
- Fixed `SimpleSmt::set_subtree()` to clear stale leaves and inner nodes in the replaced subtree region ([#981](https://github.com/0xMiden/crypto/pull/981)).
- [BREAKING] Extracted `SmtStorageReader` and `SparseMerkleTreeReader`, allowing `LargeSmt<S>` to work with read-only storage backends ([#958](https://github.com/0xMiden/crypto/pull/958)).
- [BREAKING] Extracted `SmtStorageReader` and `SparseMerkleTreeReader`, allowing `LargeSmt<S>` to work with read-only storage backends ([#967](https://github.com/0xMiden/crypto/pull/967)).

## 0.24.0 (2026-04-19)

Expand Down
11 changes: 7 additions & 4 deletions miden-crypto/src/boxed_storage.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
//! Adapter used only by the `miden-crypto` executable benchmark.
//!
//! The executable chooses memory or RocksDB storage at runtime. This wrapper erases the concrete
//! storage type while preserving the associated reader type needed by `SmtStorage`.

use miden_crypto::{
Map, Word,
merkle::smt::{InnerNode, SmtLeaf, SmtStorage, SmtStorageReader, StorageError, StorageUpdates},
};

/// Type alias for boxed storage with boxed reader
pub type BoxedSmtStorage = Box<dyn SmtStorage<Reader = Box<dyn SmtStorageReader>>>;
pub(crate) type BoxedSmtStorage = Box<dyn SmtStorage<Reader = Box<dyn SmtStorageReader>>>;

/// Generic wrapper that boxes the Reader type for any SmtStorage implementation
#[derive(Debug)]
pub struct BoxedStorage<T>(pub T);
pub(crate) struct BoxedStorage<T>(pub(crate) T);

impl<T: SmtStorageReader> SmtStorageReader for BoxedStorage<T> {
fn leaf_count(&self) -> Result<usize, StorageError> {
Expand Down
1 change: 1 addition & 0 deletions miden-crypto/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use miden_crypto::{
};
use rand::{Rng, prelude::IteratorRandom, rng};

#[cfg(feature = "executable")]
mod boxed_storage;
use boxed_storage::{BoxedSmtStorage as Storage, BoxedStorage};

Expand Down
4 changes: 2 additions & 2 deletions miden-crypto/src/merkle/smt/large/batch_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ impl<S: SmtStorage> LargeSmt<S> {
}

let new_root = leaves[0][0].hash;
self.in_memory_nodes[ROOT_MEMORY_INDEX] = new_root;
self.in_memory_nodes_mut()[ROOT_MEMORY_INDEX] = new_root;

// Build leaf updates for storage (convert Empty to None for deletion)
let mut leaf_update_map = leaf_map;
Expand Down Expand Up @@ -578,7 +578,7 @@ impl<S: SmtStorage> LargeSmt<S> {
} = prepared;

// Update the root in memory
self.in_memory_nodes[ROOT_MEMORY_INDEX] = new_root;
self.in_memory_nodes_mut()[ROOT_MEMORY_INDEX] = new_root;

// Process node mutations - group by subtree and apply in batch
// Since mutations are sorted by subtree root, we can process them in groups
Expand Down
9 changes: 5 additions & 4 deletions miden-crypto/src/merkle/smt/large/construction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ impl<S: SmtStorageReader> LargeSmt<S> {
if is_empty {
return Ok(Self {
storage,
in_memory_nodes,
in_memory_nodes: in_memory_nodes.into(),
leaf_count: 0,
entry_count: 0,
});
Expand Down Expand Up @@ -192,7 +192,7 @@ impl<S: SmtStorageReader> LargeSmt<S> {

Ok(Self {
storage,
in_memory_nodes,
in_memory_nodes: in_memory_nodes.into(),
leaf_count,
entry_count,
})
Expand Down Expand Up @@ -340,9 +340,10 @@ impl<S: SmtStorage> LargeSmt<S> {
for (index, node) in nodes {
if index.depth() < IN_MEMORY_DEPTH {
let memory_index = to_memory_index(&index);
let in_memory_nodes = self.in_memory_nodes_mut();
// Store in flat layout: left at 2*i, right at 2*i+1
self.in_memory_nodes[memory_index * 2] = node.left;
self.in_memory_nodes[memory_index * 2 + 1] = node.right;
in_memory_nodes[memory_index * 2] = node.left;
in_memory_nodes[memory_index * 2 + 1] = node.right;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion miden-crypto/src/merkle/smt/large/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
enum InnerNodeIteratorState<'a> {
InMemory {
current_index: usize,
large_smt_in_memory_nodes: &'a Vec<Word>,
large_smt_in_memory_nodes: &'a [Word],
},
Subtree {
subtree_iter: Box<dyn Iterator<Item = Subtree> + 'a>,
Expand Down
28 changes: 16 additions & 12 deletions miden-crypto/src/merkle/smt/large/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@
//! To optimize memory and I/O: group updates by key locality so that keys sharing
//! high-order bits are processed together.

use alloc::vec::Vec;
use alloc::{sync::Arc, vec::Vec};

use super::{
EmptySubtreeRoots, InnerNode, InnerNodeInfo, LeafIndex, MerkleError, NodeIndex, NodeMutation,
Expand All @@ -261,11 +261,11 @@ pub use subtree::{Subtree, SubtreeError};

mod storage;
pub use storage::{
CloneableSmtStorageReader, MemoryStorage, SmtStorage, SmtStorageReader, SmtStorageSnapshot,
StorageError, StorageUpdateParts, StorageUpdates, SubtreeUpdate,
MemoryStorage, MemoryStorageSnapshot, SmtStorage, SmtStorageReader, StorageError,
StorageUpdateParts, StorageUpdates, SubtreeUpdate,
};
#[cfg(feature = "rocksdb")]
pub use storage::{RocksDbConfig, RocksDbStorage};
pub use storage::{RocksDbConfig, RocksDbSnapshotStorage, RocksDbStorage};

mod iter;
pub use iter::LargeSmtInnerNodeIterator;
Expand Down Expand Up @@ -334,15 +334,15 @@ type MutatedLeaves = (MutatedSubtreeLeaves, Map<u64, SmtLeaf>, Map<Word, Word>,
/// - Depths 0-23: Stored in memory as a flat array for fast access
/// - Depths 24-64: Stored in external storage organized as subtrees for efficient batch operations
///
/// `LargeSmt` implements [`Clone`] only when `S` is a cloneable reader storage type. This prevents
/// accidental duplication of writable storage backends while keeping read-only snapshots cloneable.
/// `LargeSmt` implements [`Clone`] when its storage is cloneable. The in-memory top is shared and
/// detaches on mutation.
#[derive(Debug)]
pub struct LargeSmt<S: SmtStorageReader> {
storage: S,
/// Flat vector representation of in-memory nodes.
/// Shared flat array representation of in-memory nodes.
/// Index 0 is unused; index 1 is root.
/// For node at index i: left child at 2*i, right child at 2*i+1.
in_memory_nodes: Vec<Word>,
in_memory_nodes: Arc<[Word]>,
/// Cached count of non-empty leaves. Initialized from storage on load,
/// updated after each mutation.
leaf_count: usize,
Expand All @@ -351,7 +351,7 @@ pub struct LargeSmt<S: SmtStorageReader> {
entry_count: usize,
}

impl<S: CloneableSmtStorageReader> Clone for LargeSmt<S> {
impl<S: SmtStorageReader + Clone> Clone for LargeSmt<S> {
fn clone(&self) -> Self {
Self {
storage: self.storage.clone(),
Expand Down Expand Up @@ -478,6 +478,10 @@ impl<S: SmtStorageReader> LargeSmt<S> {
<Self as SparseMerkleTreeReader<SMT_DEPTH>>::get_inner_node(self, index)
}

pub(crate) fn in_memory_nodes_mut(&mut self) -> &mut [Word] {
Arc::make_mut(&mut self.in_memory_nodes)
}

/// Helper to get an in-memory node if not empty.
///
/// # Panics
Expand All @@ -503,7 +507,7 @@ impl<S: SmtStorageReader> LargeSmt<S> {
// --------------------------------------------------------------------------------------------

#[cfg(test)]
pub(crate) fn in_memory_nodes(&self) -> &Vec<Word> {
pub(crate) fn in_memory_nodes(&self) -> &[Word] {
&self.in_memory_nodes
}
}
Expand All @@ -512,8 +516,8 @@ impl<S: SmtStorage> LargeSmt<S> {
/// Returns a read-only `LargeSmt` backed by a reader view of this tree's storage.
///
/// The new tree shares the same root, leaf count, and entry count as `self`, and its storage
/// is produced by [`SmtStorage::reader`]. The returned tree's storage type is
/// `S::Reader: SmtStorageReader`, so it cannot be used for mutations.
/// is a point-in-time snapshot produced by [`SmtStorage::reader`]. The returned tree's storage
/// type is `S::Reader: SmtStorageReader`, so it cannot be used for mutations.
pub fn reader(&self) -> Result<LargeSmt<S::Reader>, LargeSmtError> {
Ok(LargeSmt {
storage: self.storage.reader()?,
Expand Down
20 changes: 11 additions & 9 deletions miden-crypto/src/merkle/smt/large/smt_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,19 +157,20 @@ impl<S: SmtStorageReader> SparseMerkleTreeReader<SMT_DEPTH> for LargeSmt<S> {

impl<S: SmtStorage> SparseMerkleTree<SMT_DEPTH> for LargeSmt<S> {
fn set_root(&mut self, root: Word) {
self.in_memory_nodes[ROOT_MEMORY_INDEX] = root;
self.in_memory_nodes_mut()[ROOT_MEMORY_INDEX] = root;
}

fn insert_inner_node(&mut self, index: NodeIndex, inner_node: InnerNode) -> Option<InnerNode> {
if index.depth() < IN_MEMORY_DEPTH {
let i = to_memory_index(&index);
let nodes = self.in_memory_nodes_mut();
// Get the old node before replacing
let old_left = self.in_memory_nodes[i * 2];
let old_right = self.in_memory_nodes[i * 2 + 1];
let old_left = nodes[i * 2];
let old_right = nodes[i * 2 + 1];

// Store new node in flat layout
self.in_memory_nodes[i * 2] = inner_node.left;
self.in_memory_nodes[i * 2 + 1] = inner_node.right;
nodes[i * 2] = inner_node.left;
nodes[i * 2 + 1] = inner_node.right;

// Check if the old node was empty
if is_empty_parent(old_left, old_right, index.depth() + 1) {
Expand All @@ -186,15 +187,16 @@ impl<S: SmtStorage> SparseMerkleTree<SMT_DEPTH> for LargeSmt<S> {
fn remove_inner_node(&mut self, index: NodeIndex) -> Option<InnerNode> {
if index.depth() < IN_MEMORY_DEPTH {
let memory_index = to_memory_index(&index);
let nodes = self.in_memory_nodes_mut();
// Get the old node before replacing with empty hashes
let old_left = self.in_memory_nodes[memory_index * 2];
let old_right = self.in_memory_nodes[memory_index * 2 + 1];
let old_left = nodes[memory_index * 2];
let old_right = nodes[memory_index * 2 + 1];

// Replace with empty hashes
let child_depth = index.depth() + 1;
let empty_hash = *EmptySubtreeRoots::entry(SMT_DEPTH, child_depth);
self.in_memory_nodes[memory_index * 2] = empty_hash;
self.in_memory_nodes[memory_index * 2 + 1] = empty_hash;
nodes[memory_index * 2] = empty_hash;
nodes[memory_index * 2 + 1] = empty_hash;

// Return the old node if it wasn't already empty
if is_empty_parent(old_left, old_right, child_depth) {
Expand Down
17 changes: 6 additions & 11 deletions miden-crypto/src/merkle/smt/large/storage/memory.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use alloc::{boxed::Box, vec::Vec};

use super::{
CloneableSmtStorageReader, SmtStorage, SmtStorageReader, StorageError, StorageUpdateParts,
StorageUpdates, SubtreeUpdate,
SmtStorage, SmtStorageReader, StorageError, StorageUpdateParts, StorageUpdates, SubtreeUpdate,
};
use crate::{
EMPTY_WORD, Map, MapEntry, Word,
Expand Down Expand Up @@ -37,7 +36,7 @@ pub struct MemoryStorage {
/// storage backends that need to hand out a detached point-in-time copy without also exposing
/// mutation methods through [`SmtStorage`].
#[derive(Debug, Clone)]
pub struct SmtStorageSnapshot(MemoryStorage);
pub struct MemoryStorageSnapshot(MemoryStorage);

impl MemoryStorage {
/// Creates a new, empty in-memory storage for a Sparse Merkle Tree.
Expand All @@ -48,8 +47,8 @@ impl MemoryStorage {
}

/// Converts this storage into a read-only snapshot.
pub fn into_snapshot(self) -> SmtStorageSnapshot {
SmtStorageSnapshot(self)
pub fn into_snapshot(self) -> MemoryStorageSnapshot {
MemoryStorageSnapshot(self)
}
}

Expand All @@ -59,7 +58,7 @@ impl Default for MemoryStorage {
}
}

impl SmtStorageReader for SmtStorageSnapshot {
impl SmtStorageReader for MemoryStorageSnapshot {
fn leaf_count(&self) -> Result<usize, StorageError> {
self.0.leaf_count()
}
Expand Down Expand Up @@ -113,8 +112,6 @@ impl SmtStorageReader for SmtStorageSnapshot {
}
}

impl CloneableSmtStorageReader for SmtStorageSnapshot {}

impl SmtStorageReader for MemoryStorage {
/// Gets the total number of non-empty leaves currently stored.
fn leaf_count(&self) -> Result<usize, StorageError> {
Expand Down Expand Up @@ -212,10 +209,8 @@ impl SmtStorageReader for MemoryStorage {
}
}

impl CloneableSmtStorageReader for MemoryStorage {}

impl SmtStorage for MemoryStorage {
type Reader = SmtStorageSnapshot;
type Reader = MemoryStorageSnapshot;

/// Returns a read-only snapshot of this in-memory storage by cloning it.
fn reader(&self) -> Result<Self::Reader, StorageError> {
Expand Down
20 changes: 9 additions & 11 deletions miden-crypto/src/merkle/smt/large/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ pub use error::StorageError;
#[cfg(feature = "rocksdb")]
mod rocksdb;
#[cfg(feature = "rocksdb")]
pub use rocksdb::{RocksDbConfig, RocksDbStorage};
pub use rocksdb::{RocksDbConfig, RocksDbSnapshotStorage, RocksDbStorage};

mod memory;
pub use memory::{MemoryStorage, SmtStorageSnapshot};
pub use memory::{MemoryStorage, MemoryStorageSnapshot};

mod updates;
pub use updates::{StorageUpdateParts, StorageUpdates, SubtreeUpdate};
Expand All @@ -38,7 +38,9 @@ pub use updates::{StorageUpdateParts, StorageUpdates, SubtreeUpdate};
/// All methods are expected to handle potential storage errors by returning a
/// `Result<_, StorageError>`.
///
/// Implementations may return either a point-in-time snapshot or a live view.
/// Implementations used as [`SmtStorage::Reader`] must be point-in-time snapshots. This is required
/// because `LargeSmt::reader()` copies the in-memory portion of the tree and pairs it with the
/// returned storage reader.
pub trait SmtStorageReader: 'static + fmt::Debug + Send + Sync {
/// Retrieves the total number of leaf nodes currently stored.
///
Expand Down Expand Up @@ -137,9 +139,6 @@ pub trait SmtStorageReader: 'static + fmt::Debug + Send + Sync {
fn get_depth24(&self) -> Result<Vec<(u64, Word)>, StorageError>;
}

/// Marker trait for reader storage types that can be cloned without duplicating mutable storage.
pub trait CloneableSmtStorageReader: SmtStorageReader + Clone {}

impl<T: SmtStorageReader + ?Sized> SmtStorageReader for Box<T> {
#[inline]
fn leaf_count(&self) -> Result<usize, StorageError> {
Expand Down Expand Up @@ -220,15 +219,14 @@ pub trait SmtStorage: SmtStorageReader {
/// The read-only view type returned by [`Self::reader`].
type Reader: SmtStorageReader;

/// Returns a read-only view of this storage that observes its current state.
/// Returns a read-only snapshot of this storage at its current committed state.
///
/// The returned value is used to construct a read-only `LargeSmt` (via
/// [`super::LargeSmt::reader`]) from a writable one. Implementations are responsible for
/// ensuring that the returned reader is consistent with `self` at the time of the call.
/// ensuring that the returned reader remains consistent with `self` at the time of the call.
///
/// Implementations may return either a point-in-time snapshot or a live view. Either way, the
/// view must be of consistent / committed state (not partial). Holding the reader must not
/// block writes in any way.
/// Implementations must return a point-in-time snapshot. Later writes through `self` must not
/// affect the returned reader. Holding the reader must not block writes in any way.
fn reader(&self) -> Result<Self::Reader, StorageError>;

/// Inserts a key-value pair into the SMT leaf at the specified logical `index`.
Expand Down
Loading
Loading