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

Merkle Path with Predecessor/Successor + Universal Query Gadget #388

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

Conversation

nicholas-mainardi
Copy link
Contributor

First PR for batching query circuits. It provides:

  • The gadget for Merkle-path verification that also locates predecessor and successor nodes in the BST
  • A refactoring of universal query circuit logic in 2 components, universal_query_hash_gadget and universal_query_value_gadget, which will then be employed distinctly in the new batching circuits

Copy link
Collaborator

@nikkolasg nikkolasg left a comment

Choose a reason for hiding this comment

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

Logic looks fine (the merkle tree verification with predecessor/successor extraction) - Mostly left comments about API and naming which is a bit confusing to me. Thanks !

}
}

/// Build an instance of `Self` without range-check the `UInt256Target`s
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you write in which conditions this is safe to use ? (i'm reading the PR top to bottom so maybe you explain it below)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's up to the caller to determine whether it's safe to use this method or not, depending on how the values are employed. For instance, if the node info is employed to compute the hash of the node, it should be safe to allocate without range checks since the hash will be different in case the prover provides out-of-range limbs.

Comment on lines 77 to 85
/// Input wires related to the data of the end node whose membership is proven with `MerklePathWithNeighborsGadget`
pub struct EndNodeInputs {
node_min: UInt256Target,
node_max: UInt256Target,
left_child_exists: BoolTarget,
left_child_info: NodeInfoTarget,
right_child_exists: BoolTarget,
right_child_info: NodeInfoTarget,
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Naming: This comment is saying it's being used to verify the merkle path with neirghbors yet this struct contains the children, not the neighbors (predecessor / successor). It's confusing, since right after, naming seems more consistent:

/// Target containing data about a neighbor of a node (neighbor can be
/// either the predecessor or the successor of a node)
pub struct NeighborInfoTarget

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you specify what is the data and why is it required / why is there ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comments added in commit 600a6e9.

Comment on lines +155 to +161
pub struct MerklePathWithNeighborsTargetInputs<const MAX_DEPTH: usize>
where
[(); MAX_DEPTH - 1]:,
{
pub(crate) path_inputs: MerklePathTargetInputs<MAX_DEPTH>,
pub(crate) end_node_inputs: EndNodeInputs,
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Naming: Here the name claims with Neighbors, and yet there is no neighbors information (predecessor/successor) inside. Could you clarify what is the input and why it's needed there ? And why do you put neighbors if it doesn't contain neighbors information ? or maybe im just missing it, where is it ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Like right after

pub struct MerklePathWithNeighborsTarget<const MAX_DEPTH: usize>

and it uses

    pub(crate) predecessor_info: NeighborInfoTarget,

which makes sense there then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because MerklePathWithNeighborsTargetInputs is the data structure that contains only the input wires (i.e., the ones that need to be assigned). The information about the neighbors is an output of the gadget, that's why they appear only in the latter data structure. Might be clearer if I rename the data structure to MerklePathWithNeighborsInputTargets maybe?

Copy link
Collaborator

Choose a reason for hiding this comment

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

lol ok maybe just MPathInputs or stg ? It's ok to not put the full name in relation to what it does, personally i find MerklePathWithNeighborsTargetInputs more confusing.

Copy link
Contributor Author

@nicholas-mainardi nicholas-mainardi Nov 13, 2024

Choose a reason for hiding this comment

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

I added the WithNeighbors sufffix to distinguish this from the simpler gadget MerklePathTargetInputs which only checks membership in the merkle path, without computing data about the predecessor/successor nodes

Comment on lines 353 to 361
pub struct MerklePathWithNeighborsGadget<const MAX_DEPTH: usize>
where
[(); MAX_DEPTH - 1]:,
{
path_gadget: MerklePathGadget<MAX_DEPTH>,
end_node_min: U256,
end_node_max: U256,
end_node_children: [Option<NodeInfo>; 2],
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Naming: same here, neighbor mentionned but no neighbors structs used, only NodeInfo... Could you clarify what are the fields and what's the purpose of the struct, wrt to the "neighbor" especially ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above, there are no neighbors since this a gadget is expected to contain only the input values. I will add comments to clarify the purpose of the additional fields. They re basically needed because in case the predecessor/successor is not in the path, then we can get its value from the min and max of the children of the node.

Copy link
Contributor Author

@nicholas-mainardi nicholas-mainardi Oct 25, 2024

Choose a reason for hiding this comment

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

Comments added in commit 600a6e9.

Comment on lines 483 to 489
fn build_common<const MAX_DEPTH: usize>(
b: &mut CircuitBuilder<F, D>,
end_node: HashOutTarget,
index_id: Target,
with_neighbors: bool,
end_node_info: Option<EndNodeInputs>,
) -> MerklePathWires<MAX_DEPTH>
Copy link
Collaborator

Choose a reason for hiding this comment

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

The API is a bit convoluted imo with_neighbors must always be correct wrt end_node_info AND when looking at the actual function logic, most lines are only regarding the successor/predecessor function.
Also, maybe I'm missing something, but it looks like all the logic only need the node_hash computed from the "normal / without neighbor" logic, is that correct ?

Wdyt about we maybe extract out the only small portion about verifying the hashes, return all hashes and do the successor/predecessor check in its own function ?
That would separate the concern cleanly imo and also able to avoid the convoluted arguments, since now we would not mix at all the two logic ("verifying mpath" + "extracting successor").

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially thought also about what you suggested but I was unsure whether the build_path method would have been well-defined or not. But probably it is since you suggested this structure too :) Done in commit 600a6e9.

Comment on lines 555 to 562
let rest = node_min[i]
.to_targets()
.into_iter()
.chain(node_max[i].to_targets())
.chain(once(index_id))
.chain(node_value[i].to_targets())
.chain(embedded_tree_hash[i].to_targets())
.collect_vec();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we use the node input target struct which already defines the sequence of elements to hash ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure I fully understand what you mean: you are suggesting to add a method to NodeInfoTarget that will compute rest basically? If so, I am not sure how to name this method, computing rest inside of the whole hash is an optimization but it's not a "well-defined" operation. Wdyt?

Base automatically changed from feat/unproven-limit-offset-queries to feat/tabular-queries October 29, 2024 21:06
@nicholas-mainardi nicholas-mainardi marked this pull request as ready for review October 29, 2024 21:29
Base automatically changed from feat/tabular-queries to main November 13, 2024 16:08
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.

3 participants