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

Adds sparse merkle tree in noname stdlib #249

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

0xnullifier
Copy link
Contributor

This pr adds the sparse merkle tree to stdlib and closes #223

The testing was done against the output of circomlibjs you can find the script here.

The approach taken is almost identical to the circom's implementation mentioned in the issue.

@0xnullifier
Copy link
Contributor Author

Hey @katat can you take a look?

@katat
Copy link
Collaborator

katat commented Jan 3, 2025

Thanks! Will take a look soon.

@0xnullifier
Copy link
Contributor Author

hey it seems the checks failed due to enforce formating
So I will go ahead and fix them

Copy link
Collaborator

@katat katat left a comment

Choose a reason for hiding this comment

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

Nice work!
I just had a round of review, and will need another rounds. I left some comments.

I think some of the arithmetic logic can be simplified using ifelse block.
More documentation on the arithmetic logic in the compute_roots and next_state, if they can't be simplified ifelse, would be useful.

src/tests/stdlib/smt/smt_verify.no Outdated Show resolved Hide resolved
src/stdlib/native/smt/lib.no Outdated Show resolved Hide resolved
/// Field: mimc7 hash for key = 1 and values = [key,value]
fn compute_leaf_hash(key: Field, value: Field) -> Field {
let hash_values = [key, value];
return mimc::mimc7_hash(hash_values, 1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it correct to provide 1 as the secret key?

Copy link
Contributor Author

@0xnullifier 0xnullifier Jan 6, 2025

Choose a reason for hiding this comment

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

Yeah this was in the iden3 specification I think this is mainly for domain seperation of the internal and leaf hash functions other than that I cannot find a reason why

src/stdlib/native/smt/lib.no Outdated Show resolved Hide resolved
src/stdlib/native/smt/lib.no Outdated Show resolved Hide resolved
/// `is_update`: whether the operation is an update operation
/// # Returns
/// `[Bool;6]`: the next state
fn next_state(
Copy link
Collaborator

Choose a reason for hiding this comment

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

it'd better to have more documentation on the logic for each transition.

Copy link
Contributor Author

@0xnullifier 0xnullifier Jan 6, 2025

Choose a reason for hiding this comment

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

Got it. It was written at the top but I will repeat it below

src/stdlib/native/smt/lib.no Show resolved Hide resolved
src/stdlib/native/smt/lib.no Outdated Show resolved Hide resolved
let new_key_bits = bits::to_bits(LEN , new_key);

let mut level_inserted = old_level_inserted(enabled,siblings);
let mut states = [[false;6]; LEN];
Copy link
Collaborator

Choose a reason for hiding this comment

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

would it be easier if it is a field value to represent the different states intead of using an array of booleans?

Copy link
Contributor Author

@0xnullifier 0xnullifier Jan 6, 2025

Choose a reason for hiding this comment

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

yeah but that makes the states computation a bit more difficult as in this case I can directly do boolean operations as my state transitions are dictated by booleans.
Also the assertion that there is only one state at a time

I tried the single state variable earlier but it got too difficult but when we do #248 it will be way easier maybe we can refactor then?

One more point is that circomlib also uses 6 variables for some reason maybe it has less constraints this way?

src/stdlib/native/smt/lib.no Outdated Show resolved Hide resolved
Copy link
Collaborator

@katat katat left a comment

Choose a reason for hiding this comment

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

Sorry for the delay. Finally got a chance to go through the code logic :D
Lgtm, @mimoo might want to have another round.

I am wondering if we can encapsulate the code a bit more using structs. The idea is like:

struct SMT {
    root: Field,
    ...
}

// init smt with a root
fn SMT.new(root, ...) -> SMT {
...
}

// differentiate the operations via different methods
// once updated, it should update the SMT.root internally
fn SMT.insert(...)
fn SMT.update(...)
fn SMT.delete(...)

// for verifying the membership
fn SMT.verify_membership(...)
fn SMT.verify_non_membership(...)

For the non membership verification logic, I am wondering what are the assumptions.

There seems no comparisons in terms of branch navigations among the following two keys in order to indicate the key doesn't exist because of the proof of exists of the not_found_key.

Is this something agreed externally in practice, or actually something missing in the circuit? In other word, how can it prove the key doesn't exist by proving its relationship with not_found_key, which has been proven exist already.




// State Constants for computing new roots
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// State Constants for computing new roots
// States for computing new roots

done[LEN - 2] = level_inserted[LEN - 1];

for idx in 1..(LEN-1) {
let is_sibling_zero = siblings[(LEN - 2) - idx] == 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

it'd better to have a var to represent the reversed index:

let reversed_idx = (LEN - 1) - idx;

let mut done = [false; LEN];


level_inserted[(LEN - 1)] = !(siblings[LEN- 2] == 0);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
level_inserted[(LEN - 1)] = !(siblings[LEN- 2] == 0);
level_inserted[LEN - 1] = !(siblings[LEN - 2] == 0);

/// This function get the level where the key will be inserted. level 0 is the root , 1 is next and so on..
/// To find this level all the child level must have a sibling of 0 and
/// the parent level has a sibling != 0. The root is assumed to have a parent level with a sibling != 0
fn level_inserted(siblings: [Field;LEN]) -> [Bool;LEN] {
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't sibling need to be [Field; LEN - 1]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup there is no need for the full array but then I would have to slice the array which would be expensive so I think it is better to do this correct me if I'm wrong



level_inserted[(LEN - 1)] = !(siblings[LEN- 2] == 0);
done[LEN - 2] = level_inserted[LEN - 1];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be better to use a mutable bool variable for done instead of array.
Then in the for loop

for idx in 1..(LEN - 1) {
    ...
    done = done || level_inserted[(LEN - 1) - idx];
    ...
}

let not_found_leaf = compute_leaf_hash(not_found_key,not_found_val);
let leaf = compute_leaf_hash(key,value);

let nfk_bits = bits::to_bits(LEN , not_found_key);
Copy link
Collaborator

Choose a reason for hiding this comment

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

is nfk_bits going to be used somewhere?

Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe add a comment here to note this is just for range checking.

@0xnullifier
Copy link
Contributor Author

0xnullifier commented Jan 21, 2025

Hey @katat the run fails as the test do not pass due to If-Else Monomarphisation but it seems that there is a bug in the way we are type checking in the mast for ExprKind::IfElse.

if then_mono.typ != else_mono.typ {

As my code fails due to unmatching types if Field {const : true} and Field { const: false} if I change it to something like this :

            let is_match = match (&then_mono.typ, &else_mono.typ) {
                (Some(then_ty), Some(else_ty)) => then_ty.match_expected(else_ty, false),
                // handle all the case
            };

Then it works

@0xnullifier
Copy link
Contributor Author

0xnullifier commented Jan 21, 2025

I am wondering if we can encapsulate the code a bit more using structs. The idea is like:

This looks better I will refactor the code to something like this

Is this something agreed externally in practice, or actually something missing in the circuit? In other word, how can it prove the key doesn't exist by proving its relationship with not_found_key, which has been proven exist already

When proving non membership proofs we want to give a merkle proof (root + siblings) to the position where the key should have been which can be a zero or a occupied leaf the not_found_key represents this position maybe I can change the name to something else if it is confusing like auxiliary_key . This is the case for the iden3 implementations of sparse merkle tree. For example in their golang implementation they also give a auxiliary NodeAux or zero. some other smt implementations like celestia also do the same of adding the Auxiliary node data in non membership proofs

There seems no comparisons in terms of branch navigations among the following two keys in order to indicate the key doesn't exist because of the proof of exists of the not_found_key.

The not found key is used to compute the the not_found_leaf which is used to compute the root to match the current root

@katat
Copy link
Collaborator

katat commented Jan 21, 2025

The not found key is used to compute the the not_found_leaf which is used to compute the root to match the current root

but how does this relate to proving not_found_leaf occupies where the membership key is supposed to be in the path?

@katat
Copy link
Collaborator

katat commented Jan 21, 2025

As my code fails due to unmatching types if Field {const : true} and Field { const: false} if I change it to something like this :

Oh yeah, nice spot!
Do you want to create a separate PR to fix this?

@0xnullifier
Copy link
Contributor Author

0xnullifier commented Jan 21, 2025

Oh yeah, nice spot!
Do you want to create a separate PR to fix this?

Do I just raise the pr directly or open a issue and then raise a pr?

but how does this relate to proving not_found_leaf occupies where the membership key is supposed to be in the path?

I think i don't get the question. The compute_root function place this root in place of where key is supposed to be like. So here is what I am saying
image
To compute the green node we need the not_found_key right ? If we did not have that we would not have any way of computing the root and checking if the given root is equal to the computed root. lmk if there is anything wrong with the way I am thinking

@katat
Copy link
Collaborator

katat commented Jan 22, 2025

Do I just raise the pr directly or open a issue and then raise a pr?

It is fine to just create a pr and point it to the comment above for reference.

I think i don't get the question. The compute_root function place this root in place of where key is supposed to be like. So here is what I am saying

Nice diagram, thanks!
I think I got it now. The compute_root always takes the key bits to determine the branches. So if the not_found_leaf is proven exist via the key_bits, it implies the key doesn't exist.

let new_root = compute_root(
    states[(LEN - 1) - idx],
    computed_root,
    siblings[(LEN - 1) - idx],
    key_bits[(LEN - 1) - idx], // the branching bit is always based on the key
    not_found_leaf,
    leaf
);

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.

[stdlib] add sparse merkle tree in
2 participants