Skip to content
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

Refactor MT & Generic Implementation #120

Merged
merged 31 commits into from
Nov 7, 2022
Merged

Conversation

mrain
Copy link
Contributor

@mrain mrain commented Sep 15, 2022

Description

Part of: #88

  • Generic merkle tree traits
  • Generic merkle tree implementation [WIP]
    • Merkle tree
    • Updatable / Appendable Merkle tree
    • Forgetable Merkle tree
    • Unit tests
  • Merkle tree gadgets

Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.

  • Targeted PR against correct branch (main)
  • Linked to GitHub issue with discussion and accepted design OR have an explanation in the PR that describes this work.
  • Wrote unit tests
  • Updated relevant documentation in the code
  • Added a relevant changelog entry to the Pending section in CHANGELOG.md
  • Re-reviewed Files changed in the GitHub PR explorer

@mrain mrain linked an issue Sep 15, 2022 that may be closed by this pull request
@alxiong alxiong self-requested a review September 15, 2022 16:52
@mrain mrain requested a review from tri-joe September 19, 2022 15:26
Copy link
Member

@jbearer jbearer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just commenting on the interface, haven't looked at the implementation yet.

One thing I think is missing, that we use extensively in wallet/EsQS/validator interactions, is the ability to extract a commitment and frontier from a MerkleTree, and then later rebuild a MerkleTree from the commitment/frontier. The actual commitment/frontier combo can be opaque (e.g. an associated type), the important thing is that there is some commitment operation which gives you something concise which you can then use to reinstantiate a MerkleTree.

primitives/src/merkle_tree/merkle_tree_traits.rs Outdated Show resolved Hide resolved
primitives/src/merkle_tree/merkle_tree_impl.rs Outdated Show resolved Hide resolved
primitives/src/merkle_tree/merkle_tree_traits.rs Outdated Show resolved Hide resolved
primitives/src/merkle_tree/merkle_tree_traits.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@alxiong alxiong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work to kickstart more discussion and refactoring work. 👍

One thing I struggle with, is the unclear abstraction of our trait MT, should we incline towards a Set accumulator or a Vector accumulator?

  • API like append() or push() is more on the "vector accumulator" side, whereas (unordered) insert is leaning towards "set accumulator"
  • Our Interval Merkle Tree is insertion order dependent, thus should more behave like a Vector accumulator. (quite honestly, so is our current merkle_tree impl as an append-only MT)

Comment on lines 8 to 11
mod merkle_tree_impl;
mod merkle_tree_traits;

pub use merkle_tree_traits::*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo,

  • bring trait definitions up, directly in this merkle_tree/mod.rs (as we did for pcs/mod.rs)
  • rename trait MerkleTreeScheme or MerkleTreeTrait ?
  • the actual implementation can be in files such as: standard_mt.rs append_only_mt.rs or maybe forgettable_mt.rs with struct name struct MerkleTree

Comment on lines 65 to 66
/// Merkle tree element type
type ElementType: ElementType<F>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about type Element: Default + ... + AsRef<[F]>; and remove ElementType?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again,

  • inline this and also IndexType
  • add a trait ToFields or AsFieldSlice or sth to capture the extra interface requirement
  • rename to type Element, similarly type Index

Comment on lines 58 to 61
pub trait Hasher<F: Field> {
/// Digest a list of values
fn digest(data: &[F]) -> F;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some thoughts (debatable):

  • if we expect a stateless hash function (i.e. no &self or &mut self as input), then we can completely remove this trait, and just accept type Hash: FnOnce(input: &GenericArray<F, Self::LeafArity>) -> F
  • naming it trait Hasher will potentially collide with std/libcore's Hasher, also, Hasher should be a trait that our RescueHash should implement.
  • a better name could derive from trait Digest and borrow some API design there.

my current thinking is in favor of option 1.

primitives/src/merkle_tree/merkle_tree_traits.rs Outdated Show resolved Hide resolved
primitives/src/merkle_tree/merkle_tree_traits.rs Outdated Show resolved Hide resolved
Comment on lines 73 to 76
/// Exsistential proof
type Proof;
/// Batch proof
type BatchProof;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • should we call it just Proof, orMerkleProof, MembershipProof, or else?
  • why the need for BatchProof here? in another word, should batch proof a struct-specific types & API, should it be a universal one?
  • Proofs should at least type Proof: Clone + Serialize + ..
  • I hesitate on the word "existential". where do we put non-membership proof then?

Comment on lines 15 to 18
pub enum LookupResult<F, P> {
/// The value at the given index, and a proof of validity
Ok(F, P),
/// The index is valid but we do not have the leaf in memory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without trait bound, <F, P> is quite unintelligible.

  • either explicitly add trait bounds to them
  • or generic over LookupResult<T: MerkleTreeScheme<F>, F: Field> and Ok(F, T::Proof), since this enum is meaningless without association with a specific MT scheme.

primitives/src/merkle_tree/merkle_tree_traits.rs Outdated Show resolved Hide resolved
Comment on lines 133 to 138
pub trait UpdatableMerkleTree<F: Field>: MerkleTree<F> {
/// Update the leaf value at a given position
/// * `pos` - zero-based index of the leaf in the tree
/// * `elem` - newly updated element
fn update(&mut self, pos: u64, elem: &Self::ElementType) -> Result<(), PrimitivesError>;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo, fn update should not be in the trait,

classical categorization of accumulators,
normally you only support insert, if you further support delete, then you are a dynamic accumulator
(I guess "update" really means insert or delete for an accumulator)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more about a sparse(key/value) Merkle tree. Where you have to provide a key/index to insert an element.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should we call it RandomAccessMerkleTree?
With 2 interfaces provided.

  • insert(pos, elem) will error if given position is occupied.
  • overwrite(pos, elem) will ignore whether it's occupied.

@mrain
Copy link
Contributor Author

mrain commented Sep 20, 2022

Good work to kickstart more discussion and refactoring work. 👍

One thing I struggle with, is the unclear abstraction of our trait MT, should we incline towards a Set accumulator or a Vector accumulator?

  • API like append() or push() is more on the "vector accumulator" side, whereas (unordered) insert is leaning towards "set accumulator"

IMO it's more like a vector/map accumulator. To access an element you need to specify a position to it.

  • Our Interval Merkle Tree is insertion order dependent, thus should more behave like a Vector accumulator. (quite honestly, so is our current merkle_tree impl as an append-only MT)

UptablableMerkleTree is just implemented. See latest commitment.

@mrain mrain marked this pull request as ready for review October 3, 2022 17:55
Comment on lines 182 to 186
fn verify(
&self,
pos: Self::IndexType,
proof: impl Borrow<Self::MembershipProof>,
) -> Result<bool, PrimitivesError>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have pos as input param? Intuitively just a proof is enough and practically, the pos info is embedded in proof.

@alxiong
Copy link
Contributor

alxiong commented Oct 6, 2022

As we discuss before, we want to make our trait MerkleTree and our implementation generic beyond just <F: Field>, I have some ideas, we should discuss and pair-program on this change. @mrain

@alxiong alxiong changed the base branch from main to mt-refactor November 7, 2022 16:16
@alxiong alxiong merged commit 98059c3 into mt-refactor Nov 7, 2022
@alxiong alxiong deleted the chengyu/mt-refactor branch November 7, 2022 16:19
tessico added a commit that referenced this pull request Nov 22, 2022
* enable CI run on mt-refactor branch

* Refactor MT & Generic Implementation (#120)

* Initial commit

* structured + impl

* Base merkle tree impl

* merkle_tree_impl

* generic indextype

* Refactored code for further SMT impl.

* Batch insertion implementation.

* Major refactoring & forget implementation.

* Remember implemented.

* cargo fmt

* Big refactor. Leaf now digest index to prevent forge attack.

* unit test for Forgettable merkle tree.

* cleanup

* Clear Arity & replace F::zero() with default()

* trait bound refactor

* New example.

* cargo fmt

* More example.

* ElementType->Element; rename trait, impl and module; remove missing license check

* IndexType refactor

* Refactor DigestAlgorithm

* Pull out Element,Index,NodeValue as traits; add trait bound to unconstrainted generic struct/fn

* Refactor capacity interface

* Renaming & better crate import.

* var renaming

* MerkleProof API refactor

Co-authored-by: Alex Xiong <alex.xiong.tech@gmail.com>

* Universal Merkle tree implementation (#132)

* A new macro for general merkle tree implementation
* Better trait bound for data types
* New Universal Merkle tree implementation, including non-membership proof
* Better comments for examples.

* Address comments

* add back the module

* randomize the lookup test

* fixup rebase

* WIP

* adapt after rebase

* Fix the order of hashing elements, zero goes first as per spec

* test for all fields

* add back the failure case test for merkle root

* fix warnings, clean up unused code

* further cleanup

* consistency in u32 vs u64 type definition in tests

* Address review comments: better naming

* fix formatting

* Add prelude;
prelude now includes 2 canonical instantiation of rescue merkle tree.

* Adopted from sub PR.

* Add an explicit comment about concrete MT type used

* rename variables

* add back the benches

* Merge artefacts cleanup

Co-authored-by: Alex Xiong <alex.xiong.tech@gmail.com>
Co-authored-by: MRain <linmrain@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[API] Refactor Merkle tree and its gadget
4 participants