From 15c7a0abd1d505fff8c1f6f9f2435a7639f57b75 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 15 Apr 2026 11:44:42 +0200 Subject: [PATCH 1/9] Scaffolding for the test --- nomt/tests/compute_root.rs | 76 +++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/nomt/tests/compute_root.rs b/nomt/tests/compute_root.rs index db24e51a30..a85b22a636 100644 --- a/nomt/tests/compute_root.rs +++ b/nomt/tests/compute_root.rs @@ -1,7 +1,10 @@ mod common; use common::Test; -use nomt::{hasher::Blake3Hasher, trie::NodeKind}; +use nomt::{hasher::Blake3Hasher, trie::NodeKind, KeyReadWrite}; +use nomt_core::hasher::ValueHasher; +use nomt_core::proof::{verify_multi_proof, verify_multi_proof_update, MultiProof}; +use nomt_core::trie::{KeyPath, Node, ValueHash}; #[test] fn root_on_empty_db() { @@ -45,3 +48,74 @@ fn root_on_internal() { NodeKind::Internal ); } + +#[test] +fn simple_roots_match() { + let accesses = vec![ + ([0; 32], KeyReadWrite::Write(Some(vec![1, 2, 3]))), + ([1; 32], KeyReadWrite::Write(Some(vec![1, 2, 3]))), + ]; + + test_root_match_with_inputs(&accesses); +} + +fn test_root_match_with_inputs(accesses: &[(KeyPath, KeyReadWrite)]) { + let mut t = Test::new("compute_root_internal"); + let prev_root = t.root(); + for (key_path, operation) in accesses { + match operation { + KeyReadWrite::Read(y) => { + let x = t.read(*key_path); + assert_eq!(&x, y); + } + KeyReadWrite::Write(v) => { + t.write(*key_path, v.clone()); + } + KeyReadWrite::ReadThenWrite(r, v) => { + let x = t.read(*key_path); + assert_eq!(&x, r); + t.write(*key_path, v.clone()); + } + } + } + + let (prover_root, witness) = t.commit(); + let nomt::Witness { + path_proofs, + operations: nomt::WitnessedOperations { .. }, + } = witness; + let mut inner = path_proofs.into_iter().map(|p| p.inner).collect::>(); + inner.sort_by(|a, b| a.terminal.path().cmp(b.terminal.path())); + let multi_proof = MultiProof::from_path_proofs(inner); + + let verifier_root = run_verifier(multi_proof, accesses, prev_root.into_inner()).unwrap(); + + assert_eq!( + prover_root.into_inner(), + verifier_root, + "Verifier and Prover have mismatched next state roots" + ); +} + +fn run_verifier( + multi_proof: MultiProof, + accesses: &[(KeyPath, KeyReadWrite)], + prev_root: Node, +) -> anyhow::Result { + let verified = verify_multi_proof::(&multi_proof, prev_root) + .expect("multiproof must verify against the prior root"); + + let mut updates: Vec<(KeyPath, Option)> = accesses + .iter() + .filter_map(|(k, op)| match op { + KeyReadWrite::Write(v) | KeyReadWrite::ReadThenWrite(_, v) => { + Some((*k, v.as_deref().map(Blake3Hasher::hash_value))) + } + KeyReadWrite::Read(_) => None, + }) + .collect(); + updates.sort_by(|a, b| a.0.cmp(&b.0)); + + verify_multi_proof_update::(&verified, updates) + .map_err(|e| anyhow::anyhow!("Failed to verify_multi_proof_update: {e:?}")) +} From 80d63cf87eca4999b749dc705a8598135523efe3 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 15 Apr 2026 12:03:56 +0200 Subject: [PATCH 2/9] Add prev data support --- nomt/tests/compute_root.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/nomt/tests/compute_root.rs b/nomt/tests/compute_root.rs index a85b22a636..f5b9862b35 100644 --- a/nomt/tests/compute_root.rs +++ b/nomt/tests/compute_root.rs @@ -1,10 +1,11 @@ mod common; use common::Test; -use nomt::{hasher::Blake3Hasher, trie::NodeKind, KeyReadWrite}; +use nomt::{hasher::Blake3Hasher, trie::NodeKind, KeyReadWrite, Value}; use nomt_core::hasher::ValueHasher; use nomt_core::proof::{verify_multi_proof, verify_multi_proof_update, MultiProof}; use nomt_core::trie::{KeyPath, Node, ValueHash}; +use std::path::Path; #[test] fn root_on_empty_db() { @@ -56,11 +57,21 @@ fn simple_roots_match() { ([1; 32], KeyReadWrite::Write(Some(vec![1, 2, 3]))), ]; - test_root_match_with_inputs(&accesses); + test_root_match_with_inputs("simple_roots_match", Vec::new(), &accesses); } -fn test_root_match_with_inputs(accesses: &[(KeyPath, KeyReadWrite)]) { - let mut t = Test::new("compute_root_internal"); +fn test_root_match_with_inputs( + name: impl AsRef, + prev_data: Vec<(KeyPath, Option)>, + accesses: &[(KeyPath, KeyReadWrite)], +) { + let mut t = Test::new(name); + { + for (key_path, write) in prev_data { + t.write(key_path, write.clone()); + } + t.commit(); + } let prev_root = t.root(); for (key_path, operation) in accesses { match operation { From 5aac7b20c30e197444b1b3a8d0d8a6710e9eea40 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 15 Apr 2026 12:23:26 +0200 Subject: [PATCH 3/9] Extending tests with keys and scenarios --- nomt/tests/common/mod.rs | 31 ++++++ nomt/tests/compute_root.rs | 209 ++++++++++++++++++++++++++++++++++++- 2 files changed, 239 insertions(+), 1 deletion(-) diff --git a/nomt/tests/common/mod.rs b/nomt/tests/common/mod.rs index 603209fd4a..cc8457dba5 100644 --- a/nomt/tests/common/mod.rs +++ b/nomt/tests/common/mod.rs @@ -24,6 +24,37 @@ pub fn account_path(id: u64) -> KeyPath { path } +/// Build a 32-byte key whose first `bit_depth` bits are 0 and whose bit at +/// MSB-position `bit_depth` is 1 iff `right`. Remaining bits are 0. +/// Two keys built with the same `bit_depth` and opposite `right` flags +/// differ in exactly one bit, at MSB-position `bit_depth`. +#[allow(dead_code)] +pub fn key_diverging_at(bit_depth: usize, right: bool) -> KeyPath { + assert!(bit_depth < 256); + let mut key = KeyPath::default(); + if right { + let byte = bit_depth / 8; + let bit_in_byte = 7 - (bit_depth % 8); + key[byte] = 1 << bit_in_byte; + } + key +} + +/// Build a key from an explicit MSB-first bit prefix; remaining bits are 0. +#[allow(dead_code)] +pub fn key_with_prefix(bits: &[bool]) -> KeyPath { + assert!(bits.len() <= 256); + let mut key = KeyPath::default(); + for (i, &b) in bits.iter().enumerate() { + if b { + let byte = i / 8; + let bit_in_byte = 7 - (i % 8); + key[byte] |= 1 << bit_in_byte; + } + } + key +} + #[allow(dead_code)] pub fn expected_root(accounts: u64) -> Node { let mut ops = (0..accounts) diff --git a/nomt/tests/compute_root.rs b/nomt/tests/compute_root.rs index f5b9862b35..be04d38f52 100644 --- a/nomt/tests/compute_root.rs +++ b/nomt/tests/compute_root.rs @@ -1,6 +1,6 @@ mod common; -use common::Test; +use common::{account_path, key_diverging_at, Test}; use nomt::{hasher::Blake3Hasher, trie::NodeKind, KeyReadWrite, Value}; use nomt_core::hasher::ValueHasher; use nomt_core::proof::{verify_multi_proof, verify_multi_proof_update, MultiProof}; @@ -130,3 +130,210 @@ fn run_verifier( verify_multi_proof_update::(&verified, updates) .map_err(|e| anyhow::anyhow!("Failed to verify_multi_proof_update: {e:?}")) } + +fn run_depth_sweep_case(name: &str, depth: usize) { + let k0 = key_diverging_at(depth, false); + let k1 = key_diverging_at(depth, true); + + // Sanity: the pair differs in exactly one bit, at MSB-position `depth`. + let xor: Vec = k0.iter().zip(k1.iter()).map(|(a, b)| a ^ b).collect(); + let set_bits: u32 = xor.iter().map(|b| b.count_ones()).sum(); + assert_eq!(set_bits, 1, "keys should differ in exactly one bit"); + let diverging_byte = depth / 8; + let diverging_mask = 1u8 << (7 - (depth % 8)); + assert_eq!( + xor[diverging_byte], diverging_mask, + "divergence should be at MSB-position {depth}" + ); + + let (k_lo, k_hi) = if k0 < k1 { (k0, k1) } else { (k1, k0) }; + let accesses = vec![ + (k_lo, KeyReadWrite::Write(Some(vec![0xAA]))), + (k_hi, KeyReadWrite::Write(Some(vec![0xBB]))), + ]; + test_root_match_with_inputs(name, Vec::new(), &accesses); +} + +macro_rules! depth_sweep_test { + ($name:ident, $depth:expr) => { + #[test] + fn $name() { + run_depth_sweep_case(stringify!($name), $depth); + } + }; +} + +depth_sweep_test!(depth_sweep_d0, 0); +depth_sweep_test!(depth_sweep_d1, 1); +depth_sweep_test!(depth_sweep_d7, 7); +depth_sweep_test!(depth_sweep_d8, 8); +depth_sweep_test!(depth_sweep_d15, 15); +depth_sweep_test!(depth_sweep_d16, 16); +depth_sweep_test!(depth_sweep_d127, 127); +depth_sweep_test!(depth_sweep_d255, 255); + +#[test] +fn overwrite_and_empty_value() { + let k_a = key_diverging_at(16, false); + let k_b = key_diverging_at(16, true); + let prev = vec![ + (k_a, Some(vec![1, 2, 3])), + (k_b, Some(vec![9, 9, 9])), + ]; + let accesses = vec![ + (k_a, KeyReadWrite::Write(Some(vec![1, 2, 3]))), // idempotent same-value overwrite + (k_b, KeyReadWrite::Write(Some(vec![]))), // empty value, distinct from None + ]; + test_root_match_with_inputs("overwrite_and_empty_value", prev, &accesses); +} + +#[test] +fn delete_collapses_internal_to_leaf() { + let k_a = key_diverging_at(16, false); + let k_b = key_diverging_at(16, true); + let prev = vec![ + (k_a, Some(vec![0xAA])), + (k_b, Some(vec![0xBB])), + ]; + let accesses = vec![(k_b, KeyReadWrite::Write(None))]; + test_root_match_with_inputs("delete_collapses_internal_to_leaf", prev, &accesses); +} + +#[test] +fn delete_last_key_to_terminator() { + let k = key_diverging_at(32, true); + let prev = vec![(k, Some(vec![0xAA]))]; + let accesses = vec![(k, KeyReadWrite::Write(None))]; + test_root_match_with_inputs("delete_last_key_to_terminator", prev, &accesses); +} + +#[test] +fn delete_nonexistent_key() { + let k_a = key_diverging_at(8, false); + let k_b = key_diverging_at(8, true); // not in prev_data + let prev = vec![(k_a, Some(vec![0xAA]))]; + let accesses = vec![(k_b, KeyReadWrite::Write(None))]; + test_root_match_with_inputs("delete_nonexistent_key", prev, &accesses); +} + +#[test] +fn read_then_write_mixed() { + let k0 = key_diverging_at(0, true); + let k1 = key_diverging_at(8, true); + let k2 = key_diverging_at(64, true); + let k_missing = key_diverging_at(128, true); + let v0 = vec![0xA0]; + let v1 = vec![0xA1]; + let v2 = vec![0xA2]; + let prev = vec![ + (k0, Some(v0.clone())), + (k1, Some(v1.clone())), + (k2, Some(v2.clone())), + ]; + let accesses = vec![ + (k0, KeyReadWrite::Read(Some(v0))), + (k1, KeyReadWrite::ReadThenWrite(Some(v1), Some(vec![0xB1]))), + (k_missing, KeyReadWrite::Read(None)), + ]; + test_root_match_with_inputs("read_then_write_mixed", prev, &accesses); +} + +#[test] +fn cluster_asymmetric_tree() { + let mk = |first_byte: u8| { + let mut k = KeyPath::default(); + k[0] = first_byte; + k + }; + let accesses = vec![ + (mk(0x10), KeyReadWrite::Write(Some(vec![1]))), + (mk(0x20), KeyReadWrite::Write(Some(vec![2]))), + (mk(0x40), KeyReadWrite::Write(Some(vec![3]))), + (mk(0xA0), KeyReadWrite::Write(Some(vec![4]))), + ]; + test_root_match_with_inputs("cluster_asymmetric_tree", Vec::new(), &accesses); +} + +#[test] +fn same_key_writes_in_batch() { + let k = key_diverging_at(24, false); + let k_other = key_diverging_at(24, true); + let prev = vec![(k_other, Some(vec![0xCC]))]; + // Two writes to the same key in one batch; harness HashMap + verifier BTreeMap both + // keep the last write. + let accesses = vec![ + (k, KeyReadWrite::Write(Some(vec![1]))), + (k, KeyReadWrite::Write(Some(vec![2]))), + ]; + test_root_match_with_inputs("same_key_writes_in_batch", prev, &accesses); +} + +#[test] +fn many_keys_round_trip() { + let prev: Vec<_> = (0..32) + .map(|i| (account_path(i), Some(vec![i as u8, 0x01]))) + .collect(); + let mut accesses: Vec<(KeyPath, KeyReadWrite)> = Vec::new(); + // Insert 16 new keys. + for i in 32..48 { + accesses.push(( + account_path(i), + KeyReadWrite::Write(Some(vec![i as u8, 0x02])), + )); + } + // Overwrite 8 existing keys. + for i in 0..8 { + accesses.push(( + account_path(i), + KeyReadWrite::Write(Some(vec![i as u8, 0x03])), + )); + } + // Delete 8 existing keys. + for i in 8..16 { + accesses.push((account_path(i), KeyReadWrite::Write(None))); + } + test_root_match_with_inputs("many_keys_round_trip", prev, &accesses); +} + +#[test] +fn many_keys_inserts_only() { + let prev: Vec<_> = (0..32) + .map(|i| (account_path(i), Some(vec![i as u8, 0x01]))) + .collect(); + let accesses: Vec<(KeyPath, KeyReadWrite)> = (32..48) + .map(|i| { + ( + account_path(i), + KeyReadWrite::Write(Some(vec![i as u8, 0x02])), + ) + }) + .collect(); + test_root_match_with_inputs("many_keys_inserts_only", prev, &accesses); +} + +#[test] +fn many_keys_overwrites_only() { + let prev: Vec<_> = (0..32) + .map(|i| (account_path(i), Some(vec![i as u8, 0x01]))) + .collect(); + let accesses: Vec<(KeyPath, KeyReadWrite)> = (0..8) + .map(|i| { + ( + account_path(i), + KeyReadWrite::Write(Some(vec![i as u8, 0x03])), + ) + }) + .collect(); + test_root_match_with_inputs("many_keys_overwrites_only", prev, &accesses); +} + +#[test] +fn many_keys_deletes_only() { + let prev: Vec<_> = (0..32) + .map(|i| (account_path(i), Some(vec![i as u8, 0x01]))) + .collect(); + let accesses: Vec<(KeyPath, KeyReadWrite)> = (8..16) + .map(|i| (account_path(i), KeyReadWrite::Write(None))) + .collect(); + test_root_match_with_inputs("many_keys_deletes_only", prev, &accesses); +} \ No newline at end of file From c0808bcef140de69d70b6bb8b1ee7d7a8811c06c Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 15 Apr 2026 12:30:01 +0200 Subject: [PATCH 4/9] More tests --- nomt/tests/compute_root.rs | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/nomt/tests/compute_root.rs b/nomt/tests/compute_root.rs index be04d38f52..04a95d4d24 100644 --- a/nomt/tests/compute_root.rs +++ b/nomt/tests/compute_root.rs @@ -254,6 +254,68 @@ fn cluster_asymmetric_tree() { test_root_match_with_inputs("cluster_asymmetric_tree", Vec::new(), &accesses); } +#[test] +fn insert_empty_value_new_key() { + let k_other = key_diverging_at(8, false); + let k_new = key_diverging_at(8, true); + let prev = vec![(k_other, Some(vec![0xCC]))]; + let accesses = vec![(k_new, KeyReadWrite::Write(Some(vec![])))]; + test_root_match_with_inputs("insert_empty_value_new_key", prev, &accesses); +} + +#[test] +fn read_then_delete() { + let k_a = key_diverging_at(16, false); + let k_b = key_diverging_at(16, true); + let v_b = vec![0xBB]; + let prev = vec![(k_a, Some(vec![0xAA])), (k_b, Some(v_b.clone()))]; + let accesses = vec![(k_b, KeyReadWrite::ReadThenWrite(Some(v_b), None))]; + test_root_match_with_inputs("read_then_delete", prev, &accesses); +} + +#[test] +fn delete_all_keys_to_terminator() { + let k0 = key_diverging_at(0, true); + let k1 = key_diverging_at(8, true); + let k2 = key_diverging_at(64, true); + let prev = vec![ + (k0, Some(vec![0xA0])), + (k1, Some(vec![0xA1])), + (k2, Some(vec![0xA2])), + ]; + let accesses = vec![ + (k0, KeyReadWrite::Write(None)), + (k1, KeyReadWrite::Write(None)), + (k2, KeyReadWrite::Write(None)), + ]; + test_root_match_with_inputs("delete_all_keys_to_terminator", prev, &accesses); +} + +#[test] +fn insert_splits_existing_leaf() { + // prev: two leaves diverging at bit 0 (left: [0;32], right: [0x80;..]). + let k_left = key_diverging_at(0, false); + let k_right = key_diverging_at(0, true); + // new key shares bits 0..6 with k_left and diverges at bit 7, forcing the + // existing left leaf to be demoted under a new internal at bit 7. + let k_new = key_diverging_at(7, true); + let prev = vec![(k_left, Some(vec![0x11])), (k_right, Some(vec![0x22]))]; + let accesses = vec![(k_new, KeyReadWrite::Write(Some(vec![0xEE])))]; + test_root_match_with_inputs("insert_splits_existing_leaf", prev, &accesses); +} + +#[test] +fn read_then_write_missing_key() { + let k_other = key_diverging_at(8, false); + let k_missing = key_diverging_at(8, true); // not in prev_data + let prev = vec![(k_other, Some(vec![0xCC]))]; + let accesses = vec![( + k_missing, + KeyReadWrite::ReadThenWrite(None, Some(vec![0xEE])), + )]; + test_root_match_with_inputs("read_then_write_missing_key", prev, &accesses); +} + #[test] fn same_key_writes_in_batch() { let k = key_diverging_at(24, false); From 3e251dfd91a888f1fd1de8003bc6bd308593cab2 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 15 Apr 2026 12:32:48 +0200 Subject: [PATCH 5/9] Remove same key test --- nomt/tests/compute_root.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/nomt/tests/compute_root.rs b/nomt/tests/compute_root.rs index 04a95d4d24..193df80c69 100644 --- a/nomt/tests/compute_root.rs +++ b/nomt/tests/compute_root.rs @@ -316,20 +316,6 @@ fn read_then_write_missing_key() { test_root_match_with_inputs("read_then_write_missing_key", prev, &accesses); } -#[test] -fn same_key_writes_in_batch() { - let k = key_diverging_at(24, false); - let k_other = key_diverging_at(24, true); - let prev = vec![(k_other, Some(vec![0xCC]))]; - // Two writes to the same key in one batch; harness HashMap + verifier BTreeMap both - // keep the last write. - let accesses = vec![ - (k, KeyReadWrite::Write(Some(vec![1]))), - (k, KeyReadWrite::Write(Some(vec![2]))), - ]; - test_root_match_with_inputs("same_key_writes_in_batch", prev, &accesses); -} - #[test] fn many_keys_round_trip() { let prev: Vec<_> = (0..32) From 77e52bb24bcc6739e66fe6513a215ed65003b691 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 15 Apr 2026 12:47:54 +0200 Subject: [PATCH 6/9] Fix format --- nomt/tests/compute_root.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/nomt/tests/compute_root.rs b/nomt/tests/compute_root.rs index 193df80c69..d6ef3c3133 100644 --- a/nomt/tests/compute_root.rs +++ b/nomt/tests/compute_root.rs @@ -176,10 +176,7 @@ depth_sweep_test!(depth_sweep_d255, 255); fn overwrite_and_empty_value() { let k_a = key_diverging_at(16, false); let k_b = key_diverging_at(16, true); - let prev = vec![ - (k_a, Some(vec![1, 2, 3])), - (k_b, Some(vec![9, 9, 9])), - ]; + let prev = vec![(k_a, Some(vec![1, 2, 3])), (k_b, Some(vec![9, 9, 9]))]; let accesses = vec![ (k_a, KeyReadWrite::Write(Some(vec![1, 2, 3]))), // idempotent same-value overwrite (k_b, KeyReadWrite::Write(Some(vec![]))), // empty value, distinct from None @@ -191,10 +188,7 @@ fn overwrite_and_empty_value() { fn delete_collapses_internal_to_leaf() { let k_a = key_diverging_at(16, false); let k_b = key_diverging_at(16, true); - let prev = vec![ - (k_a, Some(vec![0xAA])), - (k_b, Some(vec![0xBB])), - ]; + let prev = vec![(k_a, Some(vec![0xAA])), (k_b, Some(vec![0xBB]))]; let accesses = vec![(k_b, KeyReadWrite::Write(None))]; test_root_match_with_inputs("delete_collapses_internal_to_leaf", prev, &accesses); } @@ -384,4 +378,4 @@ fn many_keys_deletes_only() { .map(|i| (account_path(i), KeyReadWrite::Write(None))) .collect(); test_root_match_with_inputs("many_keys_deletes_only", prev, &accesses); -} \ No newline at end of file +} From e7683e25fb08f336ab9dac0490db75f22e42ea4e Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 15 Apr 2026 14:27:35 +0200 Subject: [PATCH 7/9] More clean up --- nomt/tests/common/mod.rs | 15 ----------- nomt/tests/compute_root.rs | 51 +++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/nomt/tests/common/mod.rs b/nomt/tests/common/mod.rs index cc8457dba5..aa28eaf28f 100644 --- a/nomt/tests/common/mod.rs +++ b/nomt/tests/common/mod.rs @@ -40,21 +40,6 @@ pub fn key_diverging_at(bit_depth: usize, right: bool) -> KeyPath { key } -/// Build a key from an explicit MSB-first bit prefix; remaining bits are 0. -#[allow(dead_code)] -pub fn key_with_prefix(bits: &[bool]) -> KeyPath { - assert!(bits.len() <= 256); - let mut key = KeyPath::default(); - for (i, &b) in bits.iter().enumerate() { - if b { - let byte = i / 8; - let bit_in_byte = 7 - (i % 8); - key[byte] |= 1 << bit_in_byte; - } - } - key -} - #[allow(dead_code)] pub fn expected_root(accounts: u64) -> Node { let mut ops = (0..accounts) diff --git a/nomt/tests/compute_root.rs b/nomt/tests/compute_root.rs index d6ef3c3133..ac943bcb49 100644 --- a/nomt/tests/compute_root.rs +++ b/nomt/tests/compute_root.rs @@ -68,7 +68,7 @@ fn test_root_match_with_inputs( let mut t = Test::new(name); { for (key_path, write) in prev_data { - t.write(key_path, write.clone()); + t.write(key_path, write); } t.commit(); } @@ -99,7 +99,7 @@ fn test_root_match_with_inputs( inner.sort_by(|a, b| a.terminal.path().cmp(b.terminal.path())); let multi_proof = MultiProof::from_path_proofs(inner); - let verifier_root = run_verifier(multi_proof, accesses, prev_root.into_inner()).unwrap(); + let verifier_root = run_verifier(multi_proof, accesses, prev_root.into_inner()); assert_eq!( prover_root.into_inner(), @@ -112,7 +112,7 @@ fn run_verifier( multi_proof: MultiProof, accesses: &[(KeyPath, KeyReadWrite)], prev_root: Node, -) -> anyhow::Result { +) -> Node { let verified = verify_multi_proof::(&multi_proof, prev_root) .expect("multiproof must verify against the prior root"); @@ -128,24 +128,12 @@ fn run_verifier( updates.sort_by(|a, b| a.0.cmp(&b.0)); verify_multi_proof_update::(&verified, updates) - .map_err(|e| anyhow::anyhow!("Failed to verify_multi_proof_update: {e:?}")) + .expect("verify_multi_proof_update must succeed") } fn run_depth_sweep_case(name: &str, depth: usize) { let k0 = key_diverging_at(depth, false); let k1 = key_diverging_at(depth, true); - - // Sanity: the pair differs in exactly one bit, at MSB-position `depth`. - let xor: Vec = k0.iter().zip(k1.iter()).map(|(a, b)| a ^ b).collect(); - let set_bits: u32 = xor.iter().map(|b| b.count_ones()).sum(); - assert_eq!(set_bits, 1, "keys should differ in exactly one bit"); - let diverging_byte = depth / 8; - let diverging_mask = 1u8 << (7 - (depth % 8)); - assert_eq!( - xor[diverging_byte], diverging_mask, - "divergence should be at MSB-position {depth}" - ); - let (k_lo, k_hi) = if k0 < k1 { (k0, k1) } else { (k1, k0) }; let accesses = vec![ (k_lo, KeyReadWrite::Write(Some(vec![0xAA]))), @@ -154,6 +142,23 @@ fn run_depth_sweep_case(name: &str, depth: usize) { test_root_match_with_inputs(name, Vec::new(), &accesses); } +#[test] +fn key_diverging_at_differs_by_one_bit() { + for depth in [0usize, 1, 7, 8, 15, 16, 127, 255] { + let k0 = key_diverging_at(depth, false); + let k1 = key_diverging_at(depth, true); + let xor: Vec = k0.iter().zip(k1.iter()).map(|(a, b)| a ^ b).collect(); + let set_bits: u32 = xor.iter().map(|b| b.count_ones()).sum(); + assert_eq!(set_bits, 1, "depth {depth}: keys should differ in one bit"); + let diverging_byte = depth / 8; + let diverging_mask = 1u8 << (7 - (depth % 8)); + assert_eq!( + xor[diverging_byte], diverging_mask, + "depth {depth}: divergence should be at MSB-position" + ); + } +} + macro_rules! depth_sweep_test { ($name:ident, $depth:expr) => { #[test] @@ -178,8 +183,8 @@ fn overwrite_and_empty_value() { let k_b = key_diverging_at(16, true); let prev = vec![(k_a, Some(vec![1, 2, 3])), (k_b, Some(vec![9, 9, 9]))]; let accesses = vec![ - (k_a, KeyReadWrite::Write(Some(vec![1, 2, 3]))), // idempotent same-value overwrite - (k_b, KeyReadWrite::Write(Some(vec![]))), // empty value, distinct from None + (k_a, KeyReadWrite::Write(Some(vec![1, 2, 3]))), + (k_b, KeyReadWrite::Write(Some(vec![]))), ]; test_root_match_with_inputs("overwrite_and_empty_value", prev, &accesses); } @@ -204,7 +209,7 @@ fn delete_last_key_to_terminator() { #[test] fn delete_nonexistent_key() { let k_a = key_diverging_at(8, false); - let k_b = key_diverging_at(8, true); // not in prev_data + let k_b = key_diverging_at(8, true); let prev = vec![(k_a, Some(vec![0xAA]))]; let accesses = vec![(k_b, KeyReadWrite::Write(None))]; test_root_match_with_inputs("delete_nonexistent_key", prev, &accesses); @@ -287,11 +292,8 @@ fn delete_all_keys_to_terminator() { #[test] fn insert_splits_existing_leaf() { - // prev: two leaves diverging at bit 0 (left: [0;32], right: [0x80;..]). let k_left = key_diverging_at(0, false); let k_right = key_diverging_at(0, true); - // new key shares bits 0..6 with k_left and diverges at bit 7, forcing the - // existing left leaf to be demoted under a new internal at bit 7. let k_new = key_diverging_at(7, true); let prev = vec![(k_left, Some(vec![0x11])), (k_right, Some(vec![0x22]))]; let accesses = vec![(k_new, KeyReadWrite::Write(Some(vec![0xEE])))]; @@ -301,7 +303,7 @@ fn insert_splits_existing_leaf() { #[test] fn read_then_write_missing_key() { let k_other = key_diverging_at(8, false); - let k_missing = key_diverging_at(8, true); // not in prev_data + let k_missing = key_diverging_at(8, true); let prev = vec![(k_other, Some(vec![0xCC]))]; let accesses = vec![( k_missing, @@ -316,21 +318,18 @@ fn many_keys_round_trip() { .map(|i| (account_path(i), Some(vec![i as u8, 0x01]))) .collect(); let mut accesses: Vec<(KeyPath, KeyReadWrite)> = Vec::new(); - // Insert 16 new keys. for i in 32..48 { accesses.push(( account_path(i), KeyReadWrite::Write(Some(vec![i as u8, 0x02])), )); } - // Overwrite 8 existing keys. for i in 0..8 { accesses.push(( account_path(i), KeyReadWrite::Write(Some(vec![i as u8, 0x03])), )); } - // Delete 8 existing keys. for i in 8..16 { accesses.push((account_path(i), KeyReadWrite::Write(None))); } From 535a75dcbc572f0b7fcfdfabc5c4895291eef816 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 12 May 2026 11:19:42 +0200 Subject: [PATCH 8/9] Attempted fix --- core/src/proof/multi_proof.rs | 107 ++++++++++++++++++++++++++++++---- core/src/proof/path_proof.rs | 2 +- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/core/src/proof/multi_proof.rs b/core/src/proof/multi_proof.rs index a52571b520..a007f62683 100644 --- a/core/src/proof/multi_proof.rs +++ b/core/src/proof/multi_proof.rs @@ -631,7 +631,6 @@ impl CommonSiblings { next_bisection.start_depth + 1, next_bisection.common_siblings.end, &proof.siblings, - false, ); self.bisection_stack.push(next_bisection.clone()); } @@ -641,7 +640,6 @@ impl CommonSiblings { next_terminal.depth - terminal_n + 1, next_terminal.unique_siblings.end, &proof.siblings, - true, ); self.terminal_index += 1; } @@ -660,15 +658,9 @@ impl CommonSiblings { } } - fn extend(&mut self, start_depth: usize, end: usize, siblings: &[Node], reverse: bool) { - if reverse { - for (i, sibling) in siblings[self.taken_siblings..end].iter().rev().enumerate() { - self.stack.push((start_depth + i, *sibling)) - } - } else { - for (i, sibling) in siblings[self.taken_siblings..end].iter().enumerate() { - self.stack.push((start_depth + i, *sibling)) - } + fn extend(&mut self, start_depth: usize, end: usize, siblings: &[Node]) { + for (i, sibling) in siblings[self.taken_siblings..end].iter().enumerate() { + self.stack.push((start_depth + i, *sibling)) } self.taken_siblings = end; @@ -1507,6 +1499,99 @@ mod tests { ); } + #[test] + fn verify_update_terminal_with_multi_unique_siblings() { + // Regression test: exercises `CommonSiblings::extend` for a terminal + // whose unique-sibling tail has length >= 2 with at least one + // non-TERMINATOR entry. Earlier `verify_update` unit tests all had + // terminal_n <= 1, masking the sibling-depth swap that affected + // `verify_multi_proof_update`. + // + // root + // / \ + // i1 l_other + // / \ + // i2 T + // / \ + // i3 T + // / \ + // i4 T + // / \ + // i5 T + // / \ + // i6 T + // / \ + // i7 T + // / \ + // l_alone l_neighbor + + let mut k_alone = [0u8; 32]; + k_alone[0] = 0b00000000; + let mut k_neighbor = [0u8; 32]; + k_neighbor[0] = 0b00000001; + let mut k_other = [0u8; 32]; + k_other[0] = 0b10000000; + + let make_leaf = |key_path, value_byte| { + let leaf_data = LeafData { + key_path, + value_hash: [value_byte; 32], + }; + let hash = Blake3Hasher::hash_leaf(&leaf_data); + (leaf_data, hash) + }; + let internal_hash = + |left, right| Blake3Hasher::hash_internal(&InternalData { left, right }); + + let (l_alone, h_alone) = make_leaf(k_alone, 0xAA); + let (l_neighbor, h_neighbor) = make_leaf(k_neighbor, 0xBB); + let (l_other, h_other) = make_leaf(k_other, 0xCC); + + let i7 = internal_hash(h_alone, h_neighbor); + let i6 = internal_hash(i7, TERMINATOR); + let i5 = internal_hash(i6, TERMINATOR); + let i4 = internal_hash(i5, TERMINATOR); + let i3 = internal_hash(i4, TERMINATOR); + let i2 = internal_hash(i3, TERMINATOR); + let i1 = internal_hash(i2, TERMINATOR); + let root = internal_hash(i1, h_other); + + // Witness for k_alone: 8 siblings at depths 1..=8 (ascending by depth). + let path_proof_alone = PathProof { + terminal: PathProofTerminal::Leaf(l_alone.clone()), + siblings: vec![ + h_other, TERMINATOR, TERMINATOR, TERMINATOR, TERMINATOR, TERMINATOR, TERMINATOR, + h_neighbor, + ], + }; + let path_proof_other = PathProof { + terminal: PathProofTerminal::Leaf(l_other.clone()), + siblings: vec![i1], + }; + + let multi_proof = + MultiProof::from_path_proofs(vec![path_proof_alone, path_proof_other]); + + let verified = verify::(&multi_proof, root).unwrap(); + + // Update k_alone with a new value. + let new_value: ValueHash = [0xDD; 32]; + let ops = vec![(k_alone, Some(new_value))]; + + // Expected post-update root: rebuild the trie with k_alone's new value. + let new_state = vec![ + (k_alone, new_value), + (k_neighbor, l_neighbor.value_hash), + (k_other, l_other.value_hash), + ]; + let expected_root = build_trie::(0, new_state, |_| {}); + + assert_eq!( + verify_update::(&verified, ops).unwrap(), + expected_root, + ); + } + #[test] fn multi_proof_verify_4_leaves_with_long_bisections() { // R diff --git a/core/src/proof/path_proof.rs b/core/src/proof/path_proof.rs index 4cca7537ab..e893c366bb 100644 --- a/core/src/proof/path_proof.rs +++ b/core/src/proof/path_proof.rs @@ -59,7 +59,7 @@ pub struct PathProof { /// The terminal node encountered when looking up a key. This is always either a terminator or /// leaf. pub terminal: PathProofTerminal, - /// Sibling nodes encountered during lookup, in descending order by depth. + /// Sibling nodes encountered during lookup, in ascending order by depth. pub siblings: Vec, } From 4c676b12679a8f2101de8024f109d42188c5dc5d Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 12 May 2026 11:28:19 +0200 Subject: [PATCH 9/9] fmt fix --- core/src/proof/multi_proof.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/proof/multi_proof.rs b/core/src/proof/multi_proof.rs index a007f62683..25291899c8 100644 --- a/core/src/proof/multi_proof.rs +++ b/core/src/proof/multi_proof.rs @@ -1569,8 +1569,7 @@ mod tests { siblings: vec![i1], }; - let multi_proof = - MultiProof::from_path_proofs(vec![path_proof_alone, path_proof_other]); + let multi_proof = MultiProof::from_path_proofs(vec![path_proof_alone, path_proof_other]); let verified = verify::(&multi_proof, root).unwrap();