Skip to content

Commit 3a783d6

Browse files
authored
feat(Smt): Reject duplicate keys in inputs (#899)
This change makes it so all SMT operations that operate on batches of key-value pairs will reject any input where the same key occurs more than once. This ensures that all of the various SMT implementations are coherent on the results they provide as there are no longer order-dependent computations. This commit introduces no measurable performance change for any impacted code path. No performance regressions are included.
1 parent b779dbf commit 3a783d6

File tree

8 files changed

+243
-51
lines changed

8 files changed

+243
-51
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
- Harden MerkleStore deserialization and fuzz coverage ([#878](https://github.com/0xMiden/crypto/pull/878)).
2828
- [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).
2929
- [BREAKING] Changed the `LargeSmtForest::entries` iterator to be fallible by explicitly returning `Result<TreeEntry>` as the iterator item.
30+
- [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.
31+
- [BREAKING] `SimpleSmt::compute_mutations` now returns a result so it can fail gracefully if the input batch contains duplicate keys.
3032

3133
## 0.22.4 (2026-03-03)
3234

miden-crypto/src/merkle/smt/full/concurrent/mod.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,13 @@ impl Smt {
103103
where
104104
Self: Sized + Sync,
105105
{
106-
// Collect and sort key-value pairs by their corresponding leaf index
106+
// Collect and sort key-value pairs by their corresponding leaf index and then their key
107+
// value.
107108
let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect();
108-
sorted_kv_pairs
109-
.par_sort_unstable_by_key(|(key, _)| Self::key_to_leaf_index(key).position());
109+
sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key);
110+
111+
// After sorting, check for duplicate keys which are adjacent after the sort.
112+
Self::check_for_duplicate_keys(&sorted_kv_pairs)?;
110113

111114
// Convert sorted pairs into mutated leaves and capture any new pairs
112115
let (mut subtree_leaves, new_pairs) =
@@ -278,11 +281,7 @@ impl Smt {
278281
if pairs.len() > 1 {
279282
pairs.sort_by(|(key_1, _), (key_2, _)| leaf::cmp_keys(*key_1, *key_2));
280283
// Check for duplicates in a sorted list by comparing adjacent pairs
281-
if let Some(window) = pairs.windows(2).find(|window| window[0].0 == window[1].0) {
282-
// If we find a duplicate, return an error
283-
let col = Self::key_to_leaf_index(&window[0].0).index.position();
284-
return Err(MerkleError::DuplicateValuesForIndex(col));
285-
}
284+
Self::check_for_duplicate_keys(&pairs)?;
286285
Ok(Some(SmtLeaf::new_multiple(pairs).unwrap()))
287286
} else {
288287
let (key, value) = pairs.pop().unwrap();

miden-crypto/src/merkle/smt/full/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,14 @@ impl Smt {
336336
/// [`Smt::apply_mutations()`] can be called in order to commit these changes to the Merkle
337337
/// tree, or [`drop()`] to discard them.
338338
///
339+
/// # Errors
340+
///
341+
/// - [`MerkleError::DuplicateValuesForIndex`] if `kv_pairs` contains the same key more than
342+
/// once.
343+
/// - [`MerkleError::TooManyLeafEntries`] if mutations would exceed 1024 entries in a leaf.
344+
///
339345
/// # Example
346+
///
340347
/// ```
341348
/// # use miden_crypto::{Felt, Word};
342349
/// # use miden_crypto::merkle::{EmptySubtreeRoots, smt::{Smt, SMT_DEPTH}};

miden-crypto/src/merkle/smt/full/tests.rs

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -489,9 +489,7 @@ fn test_prospective_insertion() {
489489

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

506-
// Edge case: multiple values at the same key, where a later pair restores the original value.
507-
let mutations = smt.compute_mutations(vec![(key_3, EMPTY_WORD), (key_3, value_3)]).unwrap();
508-
assert_eq!(mutations.root(), root_3);
509-
let old_root = smt.root();
510-
let revert = apply_mutations(&mut smt, mutations);
511-
assert_eq!(smt.root(), root_3);
512-
assert_eq!(revert.old_root, smt.root(), "reverse mutations old root did not match");
513-
assert_eq!(revert.root(), old_root, "reverse mutations new root did not match");
514-
assert_eq!(
515-
revert.new_pairs,
516-
Map::from_iter([(key_3, value_3)]),
517-
"reverse mutations pairs did not match"
518-
);
519-
520504
// Test batch updates, and that the order doesn't matter.
521-
let pairs =
522-
vec![(key_3, value_2), (key_2, EMPTY_WORD), (key_1, EMPTY_WORD), (key_3, EMPTY_WORD)];
505+
let pairs = vec![(key_3, EMPTY_WORD), (key_2, EMPTY_WORD), (key_1, EMPTY_WORD)];
523506
let mutations = smt.compute_mutations(pairs).unwrap();
524507
assert_eq!(
525508
mutations.root(),
@@ -1064,6 +1047,116 @@ fn test_smt_leaf_try_from_elements_invalid_length() {
10641047
assert_matches!(result, Err(SmtLeafError::DecodingError(_)));
10651048
}
10661049

1050+
// DUPLICATE KEY DETECTION
1051+
// --------------------------------------------------------------------------------------------
1052+
1053+
/// Tests that `compute_mutations` rejects duplicate keys (same key, same value).
1054+
#[test]
1055+
fn test_compute_mutations_rejects_duplicate_keys() {
1056+
use crate::merkle::MerkleError;
1057+
1058+
let smt = Smt::default();
1059+
let key = Word::from([ONE, ONE, ONE, Felt::new(42)]);
1060+
let value = Word::new([ONE; WORD_SIZE]);
1061+
1062+
let result = smt.compute_mutations(vec![(key, value), (key, value)]);
1063+
1064+
let expected_pos = Smt::key_to_leaf_index(&key).position();
1065+
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
1066+
}
1067+
1068+
/// Tests that `compute_mutations` rejects duplicate keys even with different values.
1069+
#[test]
1070+
fn test_compute_mutations_rejects_duplicate_keys_different_values() {
1071+
use crate::merkle::MerkleError;
1072+
1073+
let smt = Smt::default();
1074+
let key = Word::from([ONE, ONE, ONE, Felt::new(42)]);
1075+
let value_1 = Word::new([ONE; WORD_SIZE]);
1076+
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
1077+
1078+
let result = smt.compute_mutations(vec![(key, value_1), (key, value_2)]);
1079+
1080+
let expected_pos = Smt::key_to_leaf_index(&key).position();
1081+
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
1082+
}
1083+
1084+
/// Tests that `compute_mutations` rejects duplicate keys even when interleaved with another key
1085+
/// that shares the same leaf index: `[(k1, v1), (k2, v2), (k1, v3)]`.
1086+
#[test]
1087+
fn test_compute_mutations_rejects_interleaved_duplicate_keys() {
1088+
use crate::merkle::MerkleError;
1089+
1090+
let smt = Smt::default();
1091+
1092+
// Two different keys that map to the same leaf (same most significant felt)
1093+
let key_1 = Word::from([ONE, ONE, ONE, Felt::new(42)]);
1094+
let key_2 = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]);
1095+
1096+
let value_1 = Word::new([ONE; WORD_SIZE]);
1097+
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
1098+
let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]);
1099+
1100+
// k1 appears at positions 0 and 2, interleaved with k2
1101+
let result = smt.compute_mutations(vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)]);
1102+
1103+
let expected_pos = Smt::key_to_leaf_index(&key_1).position();
1104+
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
1105+
}
1106+
1107+
/// Tests that different keys mapping to the same leaf index do NOT trigger the duplicate error.
1108+
#[test]
1109+
fn test_compute_mutations_no_false_positives() {
1110+
let smt = Smt::default();
1111+
1112+
// Two different keys that map to the same leaf (same most significant felt)
1113+
let key_1 = Word::from([ONE, ONE, ONE, Felt::new(42)]);
1114+
let key_2 = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]);
1115+
1116+
let value_1 = Word::new([ONE; WORD_SIZE]);
1117+
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
1118+
1119+
// These are different keys (despite sharing a leaf index), so this should succeed.
1120+
let result = smt.compute_mutations(vec![(key_1, value_1), (key_2, value_2)]);
1121+
1122+
assert!(result.is_ok(), "Different keys at the same leaf index should not be rejected");
1123+
}
1124+
1125+
/// Tests that `Smt::with_entries` rejects duplicate keys.
1126+
#[test]
1127+
fn test_with_entries_rejects_duplicate_keys() {
1128+
use crate::merkle::MerkleError;
1129+
1130+
let key = Word::from([ONE, ONE, ONE, Felt::new(42)]);
1131+
let value_1 = Word::new([ONE; WORD_SIZE]);
1132+
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
1133+
1134+
let result = Smt::with_entries(vec![(key, value_1), (key, value_2)]);
1135+
1136+
let expected_pos = Smt::key_to_leaf_index(&key).position();
1137+
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
1138+
}
1139+
1140+
/// Tests that `Smt::with_entries` rejects interleaved duplicate keys.
1141+
#[test]
1142+
fn test_with_entries_rejects_interleaved_duplicate_keys() {
1143+
use crate::merkle::MerkleError;
1144+
1145+
// Two different keys that map to the same leaf (same most significant felt)
1146+
let key_1 = Word::from([ONE, ONE, ONE, Felt::new(42)]);
1147+
let key_2 = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]);
1148+
1149+
let value_1 = Word::new([ONE; WORD_SIZE]);
1150+
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
1151+
let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]);
1152+
1153+
// k1 appears at positions 0 and 2, interleaved with k2
1154+
let result = Smt::with_entries(vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)]);
1155+
1156+
let expected_pos = Smt::key_to_leaf_index(&key_1).position();
1157+
assert_matches!(result, Err(MerkleError::DuplicateValuesForIndex(pos)) if pos == expected_pos);
1158+
}
1159+
10671160
// HELPERS
10681161
// --------------------------------------------------------------------------------------------
10691162

miden-crypto/src/merkle/smt/large/batch_ops.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,13 @@ impl<S: SmtStorage> LargeSmt<S> {
104104
&self,
105105
sorted_kv_pairs: &[(Word, Word)],
106106
) -> Result<LoadedLeaves, LargeSmtError> {
107-
// Collect the unique leaf indices
107+
// Collect the unique leaf indices. If the input is truly sorted, then we can dedup
108+
// directly.
108109
let mut leaf_indices: Vec<u64> = sorted_kv_pairs
109110
.iter()
110111
.map(|(key, _)| Self::key_to_leaf_index(key).position())
111112
.collect();
112113
leaf_indices.dedup();
113-
leaf_indices.par_sort_unstable();
114114

115115
// Get leaves from storage
116116
let leaves_from_storage = self.storage.get_leaves(&leaf_indices)?;
@@ -335,9 +335,12 @@ impl<S: SmtStorage> LargeSmt<S> {
335335
where
336336
Self: Sized + Sync,
337337
{
338-
// Sort key-value pairs by leaf index
338+
// Sort key-value pairs by their corresponding leaf index and then the key value itself.
339339
let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect();
340-
sorted_kv_pairs.par_sort_by_key(|(key, _)| Self::key_to_leaf_index(key).position());
340+
sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key);
341+
342+
// After sorting, check for duplicate keys which are adjacent after the sort.
343+
Self::check_for_duplicate_keys(&sorted_kv_pairs)?;
341344

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

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

507-
// Collect the unique leaf indices
509+
// Collect the unique leaf indices, relying on the global sort order given by the above
510+
// sort.
508511
let mut leaf_indices: Vec<u64> = sorted_kv_pairs
509512
.iter()
510513
.map(|(key, _)| LargeSmt::<S>::key_to_leaf_index(key).position())
511514
.collect();
512-
leaf_indices.par_sort_unstable();
513515
leaf_indices.dedup();
514516

515517
// Get leaves from storage
@@ -693,10 +695,12 @@ impl<S: SmtStorage> LargeSmt<S> {
693695
where
694696
Self: Sized + Sync,
695697
{
696-
// Collect and sort key-value pairs by their corresponding leaf index
698+
// Sort key-value pairs by their corresponding leaf index and then the key value itself.
697699
let mut sorted_kv_pairs: Vec<_> = kv_pairs.into_iter().collect();
698-
sorted_kv_pairs
699-
.par_sort_unstable_by_key(|(key, _)| LargeSmt::<S>::key_to_leaf_index(key).position());
700+
sorted_kv_pairs.par_sort_unstable_by_key(|(key, _)| *key);
701+
702+
// After sorting, check for duplicate keys which are adjacent after the sort.
703+
Self::check_for_duplicate_keys(&sorted_kv_pairs)?;
700704

701705
// Load leaves from storage using helper
702706
let (_leaf_indices, leaf_map) = self.load_leaves_for_pairs(&sorted_kv_pairs)?;

miden-crypto/src/merkle/smt/large/tests.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,58 @@ fn test_duplicate_key_insertion() {
256256
assert!(result.is_err(), "Expected an error when inserting duplicate keys");
257257
}
258258

259+
#[test]
260+
fn test_compute_mutations_rejects_duplicate_keys() {
261+
let storage = MemoryStorage::new();
262+
let smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap();
263+
264+
let key = Word::from([ONE, ONE, ONE, ONE]);
265+
let value = Word::new([ONE; WORD_SIZE]);
266+
267+
let entries = vec![(key, value), (key, value)];
268+
let result = smt.compute_mutations(entries);
269+
assert!(
270+
result.is_err(),
271+
"Expected an error when computing mutations with duplicate keys"
272+
);
273+
}
274+
275+
#[test]
276+
fn test_compute_mutations_rejects_interleaved_duplicate_keys() {
277+
let storage = MemoryStorage::new();
278+
let smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap();
279+
280+
// Two different keys that map to the same leaf (same most significant felt)
281+
let key_1 = Word::from([ONE, ONE, ONE, Felt::new(42)]);
282+
let key_2 = Word::from([Felt::new(2), Felt::new(2), Felt::new(2), Felt::new(42)]);
283+
284+
let value_1 = Word::new([ONE; WORD_SIZE]);
285+
let value_2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
286+
let value_3 = Word::new([Felt::from_u32(3_u32); WORD_SIZE]);
287+
288+
// k1 appears at positions 0 and 2, interleaved with k2
289+
let entries = vec![(key_1, value_1), (key_2, value_2), (key_1, value_3)];
290+
let result = smt.compute_mutations(entries);
291+
assert!(
292+
result.is_err(),
293+
"Expected an error when computing mutations with interleaved duplicate keys"
294+
);
295+
}
296+
297+
#[test]
298+
fn test_insert_batch_rejects_duplicate_keys() {
299+
let storage = MemoryStorage::new();
300+
let mut smt = LargeSmt::<_>::with_entries(storage, vec![]).unwrap();
301+
302+
let key = Word::from([ONE, ONE, ONE, ONE]);
303+
let value1 = Word::new([ONE; WORD_SIZE]);
304+
let value2 = Word::new([Felt::from_u32(2_u32); WORD_SIZE]);
305+
306+
let entries = vec![(key, value1), (key, value2)];
307+
let result = smt.insert_batch(entries);
308+
assert!(result.is_err(), "Expected an error when inserting batch with duplicate keys");
309+
}
310+
259311
#[test]
260312
fn test_delete_entry() {
261313
let storage = MemoryStorage::new();

0 commit comments

Comments
 (0)