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:
parent
f4a9d5b027
commit
731afe30ec
5 changed files with 125 additions and 3 deletions
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue