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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
- Harden MerkleStore deserialization and fuzz coverage ([#878](https://github.com/0xMiden/crypto/pull/878)).
- [BREAKING] Upgraded Plonky3 from 0.4.2 to 0.5.0 and replaced `p3-miden-air`, `p3-miden-fri`, and `p3-miden-prover` with the unified `p3-miden-lifted-stark` crate. The `stark` module now re-exports the Lifted STARK proving system from [p3-miden](https://github.com/0xMiden/p3-miden).
- [BREAKING] Changed the `LargeSmtForest::entries` iterator to be fallible by explicitly returning `Result<TreeEntry>` as the iterator item.
- [BREAKING] Updated `SparseMerkleTree` and its implementations to reject batches of key-value pairs that contain more than one instance of any given key. This may cause previously successful operations to now fail if your input batch is not de-duplicated.
- [BREAKING] `SimpleSmt::compute_mutations` now returns a result so it can fail gracefully if the input batch contains duplicate keys.

## 0.22.4 (2026-03-03)

Expand Down
15 changes: 7 additions & 8 deletions miden-crypto/src/merkle/smt/full/concurrent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,13 @@ impl Smt {
where
Self: Sized + Sync,
{
// Collect and sort key-value pairs by their corresponding leaf index
// Collect and sort key-value pairs by their corresponding leaf index and then their key
// value.
let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect();
sorted_kv_pairs
.par_sort_unstable_by_key(|(key, _)| Self::key_to_leaf_index(key).position());
sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key);

// After sorting, check for duplicate keys which are adjacent after the sort.
Self::check_for_duplicate_keys(&sorted_kv_pairs)?;

// Convert sorted pairs into mutated leaves and capture any new pairs
let (mut subtree_leaves, new_pairs) =
Expand Down Expand Up @@ -278,11 +281,7 @@ impl Smt {
if pairs.len() > 1 {
pairs.sort_by(|(key_1, _), (key_2, _)| leaf::cmp_keys(*key_1, *key_2));
// Check for duplicates in a sorted list by comparing adjacent pairs
if let Some(window) = pairs.windows(2).find(|window| window[0].0 == window[1].0) {
// If we find a duplicate, return an error
let col = Self::key_to_leaf_index(&window[0].0).index.position();
return Err(MerkleError::DuplicateValuesForIndex(col));
}
Self::check_for_duplicate_keys(&pairs)?;
Ok(Some(SmtLeaf::new_multiple(pairs).unwrap()))
} else {
let (key, value) = pairs.pop().unwrap();
Expand Down
7 changes: 7 additions & 0 deletions miden-crypto/src/merkle/smt/full/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,14 @@ impl Smt {
/// [`Smt::apply_mutations()`] can be called in order to commit these changes to the Merkle
/// tree, or [`drop()`] to discard them.
///
/// # Errors
///
/// - [`MerkleError::DuplicateValuesForIndex`] if `kv_pairs` contains the same key more than
/// once.
/// - [`MerkleError::TooManyLeafEntries`] if mutations would exceed 1024 entries in a leaf.
///
/// # Example
///
/// ```
/// # use miden_crypto::{Felt, Word};
/// # use miden_crypto::merkle::{EmptySubtreeRoots, smt::{Smt, SMT_DEPTH}};
Expand Down
131 changes: 112 additions & 19 deletions miden-crypto/src/merkle/smt/full/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,7 @@ fn test_prospective_insertion() {

let mutations = smt.compute_mutations(vec![(key_2, value_2)]).unwrap();
assert_eq!(mutations.root(), root_2, "prospective root 2 did not match actual root 2");
let mutations = smt
.compute_mutations(vec![(key_3, EMPTY_WORD), (key_2, value_2), (key_3, value_3)])
.unwrap();
let mutations = smt.compute_mutations(vec![(key_2, value_2), (key_3, value_3)]).unwrap();
assert_eq!(mutations.root(), root_3, "mutations before and after apply did not match");
let old_root = smt.root();
let revert = apply_mutations(&mut smt, mutations);
Expand All @@ -503,23 +501,8 @@ fn test_prospective_insertion() {
"reverse mutations pairs did not match"
);

// Edge case: multiple values at the same key, where a later pair restores the original value.
let mutations = smt.compute_mutations(vec![(key_3, EMPTY_WORD), (key_3, value_3)]).unwrap();
assert_eq!(mutations.root(), root_3);
let old_root = smt.root();
let revert = apply_mutations(&mut smt, mutations);
assert_eq!(smt.root(), root_3);
assert_eq!(revert.old_root, smt.root(), "reverse mutations old root did not match");
assert_eq!(revert.root(), old_root, "reverse mutations new root did not match");
assert_eq!(
revert.new_pairs,
Map::from_iter([(key_3, value_3)]),
"reverse mutations pairs did not match"
);

// Test batch updates, and that the order doesn't matter.
let pairs =
vec![(key_3, value_2), (key_2, EMPTY_WORD), (key_1, EMPTY_WORD), (key_3, EMPTY_WORD)];
let pairs = vec![(key_3, EMPTY_WORD), (key_2, EMPTY_WORD), (key_1, EMPTY_WORD)];
let mutations = smt.compute_mutations(pairs).unwrap();
assert_eq!(
mutations.root(),
Expand Down Expand Up @@ -1064,6 +1047,116 @@ fn test_smt_leaf_try_from_elements_invalid_length() {
assert_matches!(result, Err(SmtLeafError::DecodingError(_)));
}

// DUPLICATE KEY DETECTION
// --------------------------------------------------------------------------------------------

/// Tests that `compute_mutations` rejects duplicate keys (same key, same value).
#[test]
fn test_compute_mutations_rejects_duplicate_keys() {
use crate::merkle::MerkleError;

let smt = Smt::default();
let key = Word::from([ONE, ONE, ONE, Felt::new(42)]);
let value = Word::new([ONE; WORD_SIZE]);

let result = smt.compute_mutations(vec![(key, value), (key, value)]);

let expected_pos = Smt::key_to_leaf_index(&key).position();
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
}

/// Tests that `compute_mutations` rejects duplicate keys even with different values.
#[test]
fn test_compute_mutations_rejects_duplicate_keys_different_values() {
use crate::merkle::MerkleError;

let smt = Smt::default();
let key = Word::from([ONE, ONE, ONE, Felt::new(42)]);
let value_1 = Word::new([ONE; WORD_SIZE]);
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);

let result = smt.compute_mutations(vec![(key, value_1), (key, value_2)]);

let expected_pos = Smt::key_to_leaf_index(&key).position();
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
}

/// Tests that `compute_mutations` rejects duplicate keys even when interleaved with another key
/// that shares the same leaf index: `[(k1, v1), (k2, v2), (k1, v3)]`.
#[test]
fn test_compute_mutations_rejects_interleaved_duplicate_keys() {
use crate::merkle::MerkleError;

let smt = Smt::default();

// Two different keys that map to the same leaf (same most significant felt)
let key_1 = Word::from([ONE, ONE, ONE, Felt::new(42)]);
let key_2 = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]);

let value_1 = Word::new([ONE; WORD_SIZE]);
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]);

// k1 appears at positions 0 and 2, interleaved with k2
let result = smt.compute_mutations(vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)]);

let expected_pos = Smt::key_to_leaf_index(&key_1).position();
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
}

/// Tests that different keys mapping to the same leaf index do NOT trigger the duplicate error.
#[test]
fn test_compute_mutations_no_false_positives() {
let smt = Smt::default();

// Two different keys that map to the same leaf (same most significant felt)
let key_1 = Word::from([ONE, ONE, ONE, Felt::new(42)]);
let key_2 = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]);

let value_1 = Word::new([ONE; WORD_SIZE]);
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);

// These are different keys (despite sharing a leaf index), so this should succeed.
let result = smt.compute_mutations(vec![(key_1, value_1), (key_2, value_2)]);

assert!(result.is_ok(), "Different keys at the same leaf index should not be rejected");
}

/// Tests that `Smt::with_entries` rejects duplicate keys.
#[test]
fn test_with_entries_rejects_duplicate_keys() {
use crate::merkle::MerkleError;

let key = Word::from([ONE, ONE, ONE, Felt::new(42)]);
let value_1 = Word::new([ONE; WORD_SIZE]);
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);

let result = Smt::with_entries(vec![(key, value_1), (key, value_2)]);

let expected_pos = Smt::key_to_leaf_index(&key).position();
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
}

/// Tests that `Smt::with_entries` rejects interleaved duplicate keys.
#[test]
fn test_with_entries_rejects_interleaved_duplicate_keys() {
use crate::merkle::MerkleError;

// Two different keys that map to the same leaf (same most significant felt)
let key_1 = Word::from([ONE, ONE, ONE, Felt::new(42)]);
let key_2 = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]);

let value_1 = Word::new([ONE; WORD_SIZE]);
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]);

// k1 appears at positions 0 and 2, interleaved with k2
let result = Smt::with_entries(vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)]);

let expected_pos = Smt::key_to_leaf_index(&key_1).position();
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
}

// HELPERS
// --------------------------------------------------------------------------------------------

Expand Down
26 changes: 15 additions & 11 deletions miden-crypto/src/merkle/smt/large/batch_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ impl<S: SmtStorage> LargeSmt<S> {
&self,
sorted_kv_pairs: &[(Word, Word)],
) -> Result<LoadedLeaves, LargeSmtError> {
// Collect the unique leaf indices
// Collect the unique leaf indices. If the input is truly sorted, then we can dedup
// directly.
let mut leaf_indices: Vec<u64> = sorted_kv_pairs
.iter()
.map(|(key, _)| Self::key_to_leaf_index(key).position())
.collect();
leaf_indices.dedup();
leaf_indices.par_sort_unstable();

// Get leaves from storage
let leaves_from_storage = self.storage.get_leaves(&leaf_indices)?;
Expand Down Expand Up @@ -335,9 +335,12 @@ impl<S: SmtStorage> LargeSmt<S> {
where
Self: Sized + Sync,
{
// Sort key-value pairs by leaf index
// Sort key-value pairs by their corresponding leaf index and then the key value itself.
let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect();
sorted_kv_pairs.par_sort_by_key(|(key, _)| Self::key_to_leaf_index(key).position());
sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key);

// After sorting, check for duplicate keys which are adjacent after the sort.
Self::check_for_duplicate_keys(&sorted_kv_pairs)?;

// Load leaves from storage
let (_leaf_indices, leaf_map) = self.load_leaves_for_pairs(&sorted_kv_pairs)?;
Expand Down Expand Up @@ -501,15 +504,14 @@ impl<S: SmtStorage> LargeSmt<S> {

// Collect and sort key-value pairs by their corresponding leaf index
let mut sorted_kv_pairs: Vec<_> = new_pairs.iter().map(|(k, v)| (*k, *v)).collect();
sorted_kv_pairs
.par_sort_by_key(|(key, _)| LargeSmt::<S>::key_to_leaf_index(key).position());
sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key);

// Collect the unique leaf indices
// Collect the unique leaf indices, relying on the global sort order given by the above
// sort.
let mut leaf_indices: Vec<u64> = sorted_kv_pairs
.iter()
.map(|(key, _)| LargeSmt::<S>::key_to_leaf_index(key).position())
.collect();
leaf_indices.par_sort_unstable();
leaf_indices.dedup();

// Get leaves from storage
Expand Down Expand Up @@ -693,10 +695,12 @@ impl<S: SmtStorage> LargeSmt<S> {
where
Self: Sized + Sync,
{
// Collect and sort key-value pairs by their corresponding leaf index
// Sort key-value pairs by their corresponding leaf index and then the key value itself.
let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect();
sorted_kv_pairs
.par_sort_unstable_by_key(|(key, _)| LargeSmt::<S>::key_to_leaf_index(key).position());
sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key);

// After sorting, check for duplicate keys which are adjacent after the sort.
Self::check_for_duplicate_keys(&sorted_kv_pairs)?;

// Load leaves from storage using helper
let (_leaf_indices, leaf_map) = self.load_leaves_for_pairs(&sorted_kv_pairs)?;
Expand Down
52 changes: 52 additions & 0 deletions miden-crypto/src/merkle/smt/large/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,58 @@ fn test_duplicate_key_insertion() {
assert!(result.is_err(), "Expected an error when inserting duplicate keys");
}

#[test]
fn test_compute_mutations_rejects_duplicate_keys() {
let storage = MemoryStorage::new();
let smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap();

let key = Word::from([ONE, ONE, ONE, ONE]);
let value = Word::new([ONE; WORD_SIZE]);

let entries = vec![(key, value), (key, value)];
let result = smt.compute_mutations(entries);
assert!(
result.is_err(),
"Expected an error when computing mutations with duplicate keys"
);
}

#[test]
fn test_compute_mutations_rejects_interleaved_duplicate_keys() {
let storage = MemoryStorage::new();
let smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap();

// Two different keys that map to the same leaf (same most significant felt)
let key_1 = Word::from([ONE, ONE, ONE, Felt::new(42)]);
let key_2 = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]);

let value_1 = Word::new([ONE; WORD_SIZE]);
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]);

// k1 appears at positions 0 and 2, interleaved with k2
let entries = vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)];
let result = smt.compute_mutations(entries);
assert!(
result.is_err(),
"Expected an error when computing mutations with interleaved duplicate keys"
);
}

#[test]
fn test_insert_batch_rejects_duplicate_keys() {
let storage = MemoryStorage::new();
let mut smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap();

let key = Word::from([ONE, ONE, ONE, ONE]);
let value1 = Word::new([ONE; WORD_SIZE]);
let value2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);

let entries = vec![(key, value1), (key, value2)];
let result = smt.insert_batch(entries);
assert!(result.is_err(), "Expected an error when inserting batch with duplicate keys");
}

#[test]
fn test_delete_entry() {
let storage = MemoryStorage::new();
Expand Down
Loading
Loading