feat(smt): impl hashing leaves that don't yet exist

This commit implements 'prospective leaf hashing' -- computing what the
hash of a sparse Merkle tree leaf *would* be for a key-value insertion
without actually performing that insertion.

For SimpleSmt, this is trivial, since the leaf hash and its payload are
the same.

For the full Smt, the new leaf payload (and thus, its hash) depend on
the existing payload in that leaf. Since almost all leaves are very
small, we can just clone the leaf and modify a copy.

Future work could optimize the uncommon SmtLeaf::Multiple case and
avoid a heap clone by combining the normal insertion logic and the
normal hash logic without an intermediate leaf value.
This commit is contained in:
Qyriad 2024-08-12 15:56:07 -06:00
parent f4a9d5b027
commit 731afe30ec
5 changed files with 125 additions and 3 deletions

View file

@ -350,7 +350,7 @@ impl Deserializable for SmtLeaf {
// ================================================================================================
/// Converts a key-value tuple to an iterator of `Felt`s
fn kv_to_elements((key, value): (RpoDigest, Word)) -> impl Iterator<Item = Felt> {
pub(crate) fn kv_to_elements((key, value): (RpoDigest, Word)) -> impl Iterator<Item = Felt> {
let key_elements = key.into_iter();
let value_elements = value.into_iter();
@ -359,7 +359,7 @@ fn kv_to_elements((key, value): (RpoDigest, Word)) -> impl Iterator<Item = Felt>
/// Compares two keys, compared element-by-element using their integer representations starting with
/// the most significant element.
fn cmp_keys(key_1: RpoDigest, key_2: RpoDigest) -> Ordering {
pub(crate) fn cmp_keys(key_1: RpoDigest, key_2: RpoDigest) -> Ordering {
for (v1, v2) in key_1.iter().zip(key_2.iter()).rev() {
let v1 = v1.as_int();
let v2 = v2.as_int();

View file

@ -263,6 +263,25 @@ impl SparseMerkleTree<SMT_DEPTH> for Smt {
leaf.hash()
}
fn hash_prospective_leaf(&self, key: &RpoDigest, value: &Word) -> RpoDigest {
// Future work could avoid cloning the leaf by mirroring some of the insertion logic and
// hashing without an intermediate leaf, but cloning is only expensive for multi-leaves,
// which should be really rare.
let leaf_index: LeafIndex<SMT_DEPTH> = Self::key_to_leaf_index(key);
match self.leaves.get(&leaf_index.value()) {
None => SmtLeaf::new_single(*key, *value).hash(),
Some(existing_leaf) => {
let mut new_leaf = existing_leaf.clone();
if *value != EMPTY_WORD {
new_leaf.insert(*key, *value);
} else {
new_leaf.remove(*key);
}
new_leaf.hash()
},
}
}
fn key_to_leaf_index(key: &RpoDigest) -> LeafIndex<SMT_DEPTH> {
let most_significant_felt = key[3];
LeafIndex::new_max_depth(most_significant_felt.as_int())

View file

@ -2,7 +2,7 @@ use alloc::vec::Vec;
use super::{Felt, LeafIndex, NodeIndex, Rpo256, RpoDigest, Smt, SmtLeaf, EMPTY_WORD, SMT_DEPTH};
use crate::{
merkle::{EmptySubtreeRoots, MerkleStore},
merkle::{smt::SparseMerkleTree, EmptySubtreeRoots, MerkleStore},
utils::{Deserializable, Serializable},
Word, ONE, WORD_SIZE,
};
@ -258,6 +258,96 @@ fn test_smt_removal() {
}
}
#[test]
fn test_prospective_hash() {
let mut smt = Smt::default();
let raw = 0b_01101001_01101100_00011111_11111111_10010110_10010011_11100000_00000000_u64;
let key_1: RpoDigest = RpoDigest::from([ONE, ONE, ONE, Felt::new(raw)]);
let key_2: RpoDigest =
RpoDigest::from([2_u32.into(), 2_u32.into(), 2_u32.into(), Felt::new(raw)]);
let key_3: RpoDigest =
RpoDigest::from([3_u32.into(), 3_u32.into(), 3_u32.into(), Felt::new(raw)]);
let value_1 = [ONE; WORD_SIZE];
let value_2 = [2_u32.into(); WORD_SIZE];
let value_3: [Felt; 4] = [3_u32.into(); WORD_SIZE];
// insert key-value 1
{
let prospective = smt.hash_prospective_leaf(&key_1, &value_1);
let old_value_1 = smt.insert(key_1, value_1);
assert_eq!(old_value_1, EMPTY_WORD);
assert_eq!(prospective, smt.get_leaf(&key_1).hash());
assert_eq!(smt.get_leaf(&key_1), SmtLeaf::Single((key_1, value_1)));
}
// insert key-value 2
{
let prospective = smt.hash_prospective_leaf(&key_2, &value_2);
let old_value_2 = smt.insert(key_2, value_2);
assert_eq!(old_value_2, EMPTY_WORD);
assert_eq!(prospective, smt.get_leaf(&key_2).hash());
assert_eq!(
smt.get_leaf(&key_2),
SmtLeaf::Multiple(vec![(key_1, value_1), (key_2, value_2)])
);
}
// insert key-value 3
{
let prospective = smt.hash_prospective_leaf(&key_3, &value_3);
let old_value_3 = smt.insert(key_3, value_3);
assert_eq!(old_value_3, EMPTY_WORD);
assert_eq!(prospective, smt.get_leaf(&key_3).hash());
assert_eq!(
smt.get_leaf(&key_3),
SmtLeaf::Multiple(vec![(key_1, value_1), (key_2, value_2), (key_3, value_3)])
);
}
// remove key 3
{
let old_hash = smt.get_leaf(&key_3).hash();
let old_value_3 = smt.insert(key_3, EMPTY_WORD);
assert_eq!(old_value_3, value_3);
assert_eq!(old_hash, smt.hash_prospective_leaf(&key_3, &old_value_3));
assert_eq!(
smt.get_leaf(&key_3),
SmtLeaf::Multiple(vec![(key_1, value_1), (key_2, value_2)])
);
}
// remove key 2
{
let old_hash = smt.get_leaf(&key_2).hash();
let old_value_2 = smt.insert(key_2, EMPTY_WORD);
assert_eq!(old_value_2, value_2);
assert_eq!(old_hash, smt.hash_prospective_leaf(&key_2, &old_value_2));
assert_eq!(smt.get_leaf(&key_2), SmtLeaf::Single((key_1, value_1)));
}
// remove key 1
{
let old_hash = smt.get_leaf(&key_1).hash();
let old_value_1 = smt.insert(key_1, EMPTY_WORD);
assert_eq!(old_value_1, value_1);
assert_eq!(old_hash, smt.hash_prospective_leaf(&key_1, &old_value_1));
assert_eq!(smt.get_leaf(&key_1), SmtLeaf::new_empty(key_1.into()));
}
}
/// Tests that 2 key-value pairs stored in the same leaf have the same path
#[test]
fn test_smt_path_to_keys_in_same_leaf_are_equal() {

View file

@ -167,6 +167,15 @@ pub(crate) trait SparseMerkleTree<const DEPTH: u8> {
/// Returns the hash of a leaf
fn hash_leaf(leaf: &Self::Leaf) -> RpoDigest;
/// Returns the hash of a leaf if the leaf WERE inserted into the tree,
/// without performing any insertion or other mutation.
///
/// Note: calling this function after actually performing an insert with the same arguments
/// will *not* return the same result, as inserting multiple times with the same key mutates
/// the leaf each time.
#[cfg_attr(not(test), allow(dead_code))]
fn hash_prospective_leaf(&self, key: &Self::Key, value: &Self::Value) -> RpoDigest;
/// Maps a key to a leaf index
fn key_to_leaf_index(key: &Self::Key) -> LeafIndex<DEPTH>;

View file

@ -302,6 +302,10 @@ impl<const DEPTH: u8> SparseMerkleTree<DEPTH> for SimpleSmt<DEPTH> {
leaf.into()
}
fn hash_prospective_leaf(&self, _key: &LeafIndex<DEPTH>, value: &Word) -> RpoDigest {
Self::hash_leaf(value)
}
fn key_to_leaf_index(key: &LeafIndex<DEPTH>) -> LeafIndex<DEPTH> {
*key
}