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
24 changes: 21 additions & 3 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1160,9 +1160,27 @@ impl<const N: usize> Node<N> for ProllyNode<N> {
// implement get hash function of the ProllyNode
impl<const N: usize> ProllyNode<N> {
pub fn get_hash(&self) -> ValueDigest<N> {
let mut keys_and_values = self.keys.concat();
keys_and_values.extend(&self.values.concat());
ValueDigest::new(&keys_and_values)
// PREFIX-FREE content hash. The previous `keys.concat() ++ values.concat()`
// had no length delimiters, so distinct (k,v) sets could collide (e.g. values
// "ab","c" and "a","bc" both concat to "abc") -> same root -> broken content
// addressing / false merge convergence. Length-frame every element (u32 BE,
// never usize, for native==wasm), separate the key region from the value
// region by count, and bind is_leaf/level so a leaf cannot collide with an
// internal node of identical bytes.
let mut buf: Vec<u8> = Vec::new();
buf.push(self.is_leaf as u8);
buf.push(self.level);
buf.extend_from_slice(&(self.keys.len() as u32).to_be_bytes());
for k in &self.keys {
buf.extend_from_slice(&(k.len() as u32).to_be_bytes());
buf.extend_from_slice(k);
}
buf.extend_from_slice(&(self.values.len() as u32).to_be_bytes());
for v in &self.values {
buf.extend_from_slice(&(v.len() as u32).to_be_bytes());
buf.extend_from_slice(v);
}
ValueDigest::new(&buf)
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2808,6 +2808,34 @@ mod tests {
}
}

#[cfg(test)]
mod prefix_free_collision_probe {
use super::*;
use crate::storage::InMemoryNodeStorage;

/// Red-team attack #1: distinct (k,v) sets must NOT share a root hash. get_hash =
/// SHA256(keys.concat() ++ values.concat()) has no length delimiters, so values
/// "ab"+"c" and "a"+"bc" both concat to "abc" -> same hash input -> same root.
#[test]
fn distinct_contents_must_not_share_root() {
let cfg = TreeConfig::<32>::default();
let mut a = ProllyTree::new(InMemoryNodeStorage::<32>::default(), cfg.clone());
a.insert(b"k1".to_vec(), b"ab".to_vec());
a.insert(b"k2".to_vec(), b"c".to_vec());
let mut b = ProllyTree::new(InMemoryNodeStorage::<32>::default(), cfg.clone());
b.insert(b"k1".to_vec(), b"a".to_vec());
b.insert(b"k2".to_vec(), b"bc".to_vec());
let ra = a.get_root_hash().unwrap();
let rb = b.get_root_hash().unwrap();
// also a key-vs-value boundary collision: key "k1k2" value ... vs keys "k1","k2"
assert_ne!(
ra.as_bytes(),
rb.as_bytes(),
"PREFIX-FREE COLLISION CONFIRMED: distinct (k,v) sets share a root hash"
);
}
}

/// O(diff) structural-diff differential tests: the structural `diff_nodes_recursive`
/// must emit a BYTE-IDENTICAL `Vec<DiffResult>` to the proven full-leaf flatten oracle
/// (`diff_nodes_flatten`) on every shape — and load strictly fewer nodes when a large
Expand Down
Loading