-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CMT: Subtree Append #171
base: main
Are you sure you want to change the base?
CMT: Subtree Append #171
Changes from 5 commits
a1d709a
b8c583e
b0986e6
1411873
1b5c014
5312ba0
094ab22
62ab83d
c362e18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
use crate::{ | ||
error::CMTError, | ||
state::{ChangeLog, Node, Path, EMPTY}, | ||
utils::{empty_node, empty_node_cached, fill_in_proof, recompute, hash_to_parent}, | ||
utils::{empty_node, empty_node_cached, fill_in_proof, hash_to_parent, recompute}, | ||
}; | ||
use bytemuck::{Pod, Zeroable}; | ||
pub(crate) use log_compute; | ||
|
@@ -144,7 +144,7 @@ impl<const MAX_DEPTH: usize, const MAX_BUFFER_SIZE: usize> MerkleRoll<MAX_DEPTH, | |
} | ||
} | ||
|
||
/// Basic operation that always succeeds | ||
/// Append leaf to tree | ||
pub fn append(&mut self, mut node: Node) -> Result<Node, CMTError> { | ||
check_bounds(MAX_DEPTH, MAX_BUFFER_SIZE); | ||
if node == EMPTY { | ||
|
@@ -196,6 +196,91 @@ impl<const MAX_DEPTH: usize, const MAX_BUFFER_SIZE: usize> MerkleRoll<MAX_DEPTH, | |
Ok(node) | ||
} | ||
|
||
/// Append subtree to current tree | ||
pub fn append_subtree( | ||
&mut self, | ||
subtree_root: Node, | ||
subtree_rightmost_leaf: Node, | ||
subtree_rightmost_index: u32, | ||
subtree_rightmost_proof: Vec<Node>, | ||
) -> Result<Node, CMTError> { | ||
check_bounds(MAX_DEPTH, MAX_BUFFER_SIZE); | ||
// TODO: consider adding check that the subtree is not empty (but will omit for now) | ||
// note: it will be more time consuming to check that the subtree_root is not a root to an empty tree, because it requires applying O(depth) hashes of trivial nodes | ||
if self.rightmost_proof.index >= 1 << MAX_DEPTH { | ||
return Err(CMTError::TreeFull); | ||
} | ||
|
||
if self.rightmost_proof.index == 0 { | ||
// TODO(sorend): Consider if we want to be able to initialize a tree with a subtree append (I think it would be cool, but is a little complex). write an equivallent function for subtree_append: initialize_tree_from_subtree_append | ||
// For now | ||
return Err(CMTError::CannotInitializeWithSubtreeAppend); | ||
} | ||
|
||
// Confirm that subtree_rightmost_proof is valid | ||
if recompute( | ||
subtree_rightmost_leaf, | ||
&subtree_rightmost_proof[..], | ||
subtree_rightmost_index - 1, | ||
) != subtree_root | ||
{ | ||
return Err(CMTError::InvalidProof); | ||
} | ||
|
||
let leaf = subtree_rightmost_leaf.clone(); | ||
let intersection = self.rightmost_proof.index.trailing_zeros() as usize; | ||
|
||
// @dev: At any given time (other than initialization), there is only one valid size of subtree that can be appended | ||
if subtree_rightmost_proof.len() != intersection { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jarry-xiao here we enforce that we only append a tree of the correct size given the nodes inserted into the tree so far There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah this is quite clever! Makes sense, because the intersection nodes maps to the root of the subtree where the newly appended node is the left most child |
||
return Err(CMTError::SubtreeInvalidSize); | ||
} | ||
|
||
let mut change_list = [EMPTY; MAX_DEPTH]; | ||
let mut intersection_node = self.rightmost_proof.leaf; | ||
|
||
// This will be mutated into the new root after the append by gradually hashing this node with the RMP to the subtree, then the critical node, and then the rest of the RMP to this tree. | ||
let mut node = subtree_rightmost_leaf; | ||
|
||
for i in 0..MAX_DEPTH { | ||
change_list[i] = node; | ||
if i < intersection { | ||
// Compute proof to the appended node from empty nodes | ||
hash_to_parent( | ||
&mut intersection_node, | ||
&self.rightmost_proof.proof[i], | ||
((self.rightmost_proof.index - 1) >> i) & 1 == 0, | ||
); | ||
hash_to_parent( | ||
&mut node, | ||
&subtree_rightmost_proof[i], | ||
((subtree_rightmost_index - 1) >> i) & 1 == 0, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic seems like the right idea! |
||
); | ||
self.rightmost_proof.proof[i] = subtree_rightmost_proof[i]; | ||
} else if i == intersection { | ||
// Compute where the new node intersects the main tree | ||
hash_to_parent(&mut node, &intersection_node, false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To check my understanding, at this point |
||
self.rightmost_proof.proof[intersection] = intersection_node; | ||
} else { | ||
// Update the change list path up to the root | ||
hash_to_parent( | ||
&mut node, | ||
&self.rightmost_proof.proof[i], | ||
((self.rightmost_proof.index - 1) >> i) & 1 == 0, | ||
); | ||
} | ||
} | ||
|
||
self.update_internal_counters(); | ||
self.change_logs[self.active_index as usize] = ChangeLog::<MAX_DEPTH>::new( | ||
node, | ||
change_list, | ||
self.rightmost_proof.index + subtree_rightmost_index - 1, | ||
); | ||
self.rightmost_proof.index = self.rightmost_proof.index + subtree_rightmost_index; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can potentially mess up the tree There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed due to the intersection check |
||
self.rightmost_proof.leaf = leaf; | ||
Ok(node) | ||
} | ||
|
||
/// Convenience function for `set_leaf` | ||
/// On write conflict: | ||
/// Will append | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think 0 should always be a safe index to start from because the subtree topology doesn't interfere with the main tree topology (i.e. each node can be mapped cleanly to another node)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree, in general appending to an empty tree should be ok. I think the reason why I skipped it here for now was because there are multiple sizes of trees that are valid to append to an empty tree, and I was unsure if it would gel well with the logic for appending to an initialized tree.
But it might be as simple as making a different size check on the depth of tree being appended if the tree is empty. will review the logic here