diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 08df69724..0c002c078 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -64,6 +64,9 @@ jobs: - name: Setup Circom run: wget https://github.com/iden3/circom/releases/latest/download/circom-linux-amd64 && sudo mv ./circom-linux-amd64 /usr/bin/circom && sudo chmod +x /usr/bin/circom + - name: Install Nargo + uses: noir-lang/noirup@v0.1.3 + - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT diff --git a/packages/circuits/README.md b/packages/circuits/README.md index 7ac1fb160..01dc94e1a 100644 --- a/packages/circuits/README.md +++ b/packages/circuits/README.md @@ -36,6 +36,12 @@ - Circom: - [PoseidonProof](./circom/poseidon-proof.circom): It proves the possession of a Poseidon pre-image without revealing the pre-image itself. - [BinaryMerkleRoot](./circom/binary-merkle-root.circom): It calculates the root of a binary Merkle tree using a provided proof-of-membership. +- Noir: + - [Sparse Merkle Tree PoseidonBN254](./noir/crates/smt_bn254/src/lib.nr): A reusable library of functions related to Sparse Merkle Trees based on the JS implementation of [@zk-kit/smt](../smt). The library uses the Poseidon hash to implement the following functions: + - verifying membership and non-membership proofs + - adding a new entry to a SMT + - updating an entry of an SMT + - deleting an existing entry from an SMT ## 🛠 Install @@ -52,3 +58,12 @@ or yarn: ```bash yarn add @zk-kit/circuits ``` + +### Using Nargo (for Noir circuits) + +In your Nargo.toml file, add the following dependency: + +```toml +[dependencies] +smt_bn254 = { tag = "v0.1.0", git = "https://github.com/privacy-scaling-explorations/zk-kit/packages/circuits/noir", directory="crates/smt_bn254" } +``` diff --git a/packages/circuits/noir/Nargo.toml b/packages/circuits/noir/Nargo.toml new file mode 100644 index 000000000..60e8a4b40 --- /dev/null +++ b/packages/circuits/noir/Nargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/smt_bn254"] \ No newline at end of file diff --git a/packages/circuits/noir/crates/smt_bn254/Nargo.toml b/packages/circuits/noir/crates/smt_bn254/Nargo.toml new file mode 100644 index 000000000..bace4ce60 --- /dev/null +++ b/packages/circuits/noir/crates/smt_bn254/Nargo.toml @@ -0,0 +1,5 @@ +[package] +name = "smt_bn254" +authors = ["fabianschu"] +type = "lib" +compiler_version = ">=0.19.3" diff --git a/packages/circuits/noir/crates/smt_bn254/src/lib.nr b/packages/circuits/noir/crates/smt_bn254/src/lib.nr new file mode 100644 index 000000000..ca7a714bd --- /dev/null +++ b/packages/circuits/noir/crates/smt_bn254/src/lib.nr @@ -0,0 +1,246 @@ +use dep::std::option::Option; + +mod utils; + +global TREE_DEPTH: u32 = 256; + +/** + * Verifies a membership or a non-membership proof, ie it calculates the tree root + * based on an entry or matching entry and all siblings and compares that calculated root + * with the root that is passed to this function. + * @param entry Contains key and value of an entry: [key, value] + * @param matching_entry Contains [key, value] of a matching entry only for non-membership proofs + * @param siblings Contains array of siblings of entry / matching_entry + * @param root The expected root of the tree + */ +pub fn verify(entry: [Field; 2], matching_entry: [Option; 2], siblings: [Field; TREE_DEPTH], root: Field) { + let mut calculcated_root: Field = 0; + let path = utils::key_to_path(entry[0]); + // if there is no matching_entry it is a membership proof + // if there is a matching_entry it is a non_membership proof + if matching_entry[0].is_none() | matching_entry[1].is_none() { + // membership proof: the root is calculated based on the entry, the siblings, + // and the path determined by the key of entry through consecutive hashing + calculcated_root = utils::calculcate_root(entry, siblings, path); + } else { + // non-membership proof: the root is calculated based on the matching_entry, the siblings + // and the path that is determined by the key of entry. This makes sure that matching_entry is in fact + // a matching entry for entry meaning that it shares the same first bits as path + calculcated_root = utils::calculcate_root([matching_entry[0].unwrap(), matching_entry[1].unwrap()], siblings, path); + } + assert(calculcated_root == root); +} + +/** + * Adds a NEW entry to an existing tree. Based on the siblings first validates the correctness of + * the old root. Then uses the new entry and the siblings to calculate the new tree root. + * NOTE: this function doesn't validate if the key for the new entry already exists in the tree, ie + * if the operation is actually an update. For this operation there is a separate function. + * @param entry Contains key and value of an entry: [key, value] + * @param old_root The root of the tree before the new entry is added + * @param siblings Contains array of siblings of entry / matching_entry + * @returns The new root after the addition + */ +pub fn add(new_entry: [Field; 2], old_root: Field, siblings: [Field; TREE_DEPTH]) -> Field { + // if the root node is zero the first leaf is added to the tree in which case + // the new root equals H(k,v,1) + // otherwise the correctness of the old root is validated based on the siblings after which + // the new root is calculated and returned + if (old_root == 0) { + utils::hash(new_entry[0], new_entry[1], true) + } else { + let (old, new) = utils::calculate_two_roots(new_entry, siblings); + assert(old == old_root); + new + } +} + +/** + * Deletes an existing entry from a tree. Based on the siblings first does a membership proof + * of that existing entry and then calculates the new root (without the entry). + * @param entry Contains key and value of the to-be-deleted entry: [key, value] + * @param old_root The root of the tree if the entry is still included + * @param sigbils Contains array of siblings of entry + * @returns The new root after the deletion + */ +pub fn delete(entry: [Field; 2], old_root: Field, siblings: [Field; TREE_DEPTH]) -> Field { + // proves membership of entry in the old root, then calculates and returns the new root + let (new, old) = utils::calculate_two_roots(entry, siblings); + assert(old == old_root); + new +} + +/** + * Updates the value of an existing entry in a tree. Based on the siblings first does a membership proof + * first verifies the membership of the old entry. Then recalculates the new root. + * @param new_value The new value to be added (instead of old_entry[1]) + * @param old_entry Contains key and value of the entry to be updated: [key, value] + * @param old_root The root of the tree before the update + * @param siblings Contains an array of siblings of old_entry + * @returns The new root after the update + */ +pub fn update(new_value: Field, old_entry: [Field; 2], old_root: Field, siblings: [Field; TREE_DEPTH]) -> Field { + let key = old_entry[0]; + let old_value = old_entry[1]; + // both the old entry and new entry share the same key that is used to calculate the path + let path = utils::key_to_path(key); + // old_parent is a container to temporarily store the nodes that ultimately lead to the OLD root + let mut old_parent: Field = utils::hash(key, old_value, true); + // new_parent is a container to temporarily store the nodes that ultimately lead to the NEW root + let mut new_parent: Field = utils::hash(key, new_value, true); + // starting from the botton of the tree, for each level it checks whether there is a sibling and if + // that is the case, it hashes the two containers with the sibling and updates the containers with the + // resulting hashes until the uppermost level is reached aka the root node + for i in 0..TREE_DEPTH { + let sibling = siblings[i]; + if sibling != 0 { + if path[i] == 0 { + new_parent = utils::hash(new_parent, sibling, false); + old_parent = utils::hash(old_parent, sibling, false); + } else { + new_parent = utils::hash(sibling, new_parent, false); + old_parent = utils::hash(sibling, old_parent, false); + } + } + } + assert(old_parent == old_root); + new_parent +} + +/* +Visual representations of the trees used in the tests for reference + +The big tree corresponds to the tree that is used for +testing in @zk-kit/smt: + +big_tree_root: 46574...31272 +├── 1: 78429...40557 +│ ├── 1 +│ ├── v: 17150...90784 +│ └── k: 20438...35547 +└── 0: + ├── 1: 74148...2867 + │ ├── 1: 89272...68433 || This leaf + │ │ ├── 1 || is missing + │ │ ├── v: 85103...45170 || for the + │ │ └── k: 84596...08785 || small_tree_root + │ └── 0: 18126...22196 + │ ├── 1 + │ ├── v: 13761...25802 + │ └── k: 13924...78098 + └── 0: 79011...20495 + ├── 1 + ├── v: 10223...67791 + └── k: 18746...38844 + +The small tree lacks one leaf as indicated in the previous +tree and looks as follows: + +small_tree_root: 35328...54128 +├── 1: 78429...40557 +│ ├── 1 +│ ├── v: 17150...90784 +│ └── k: 20438...35547 +└── 0: + ├── 1: 18126...22196 + │ ├── 1 + │ ├── v: 13761...25802 + │ └── k: 13924...78098 + └── 0: 79011...20495 + ├── 1 + ├── v: 10223...67791 + └── k: 18746...38844 +*/ + +#[test] +fn test_verify_membership_proof() { + let small_tree_root = 3532809757480436997969526334543526996242857122876262144596246439822675654128; + let key = 18746990989203767017840856832962652635369613415011636432610873672704085238844; + let value = 10223238458026721676606706894638558676629446348345239719814856822628482567791; + let entry = [key, value]; + let matching_entry = [Option::none(), Option::none()]; + let mut siblings: [Field; TREE_DEPTH] = [0; TREE_DEPTH]; + siblings[254] = 18126944477260144816572365299295230808286197301459941187567621915186392922196; + siblings[255] = 7842913321420301106140788486336995496832503825951977327575501561489697540557; + verify(entry, matching_entry, siblings, small_tree_root); +} + +#[test] +fn test_verify_non_membership_proof() { + let small_tree_root = 3532809757480436997969526334543526996242857122876262144596246439822675654128; + let key = 8459688297517826598613412977307486050019239051864711035321718508109192087854; + let value = 8510347201346963732943571140849185725417245763047403804445415726302354045170; + let entry = [key, value]; + let matching_entry = [ + Option::some(13924553918840562069536446401916499801909138643922241340476956069386532478098), + Option::some(13761779908325789083343687318102407319424329800042729673292939195255502025802) + ]; + let mut siblings: [Field; TREE_DEPTH] = [0; TREE_DEPTH]; + siblings[254] = 14443001516360873457302534246953033880503978184674311810335857314606403404583; + siblings[255] = 7842913321420301106140788486336995496832503825951977327575501561489697540557; + verify(entry, matching_entry, siblings, small_tree_root); +} + +#[test] +fn test_add_first_element() { + let key = 20438969296305830531522370305156029982566273432331621236661483041446048135547; + let value = 17150136040889237739751319962368206600863150289695545292530539263327413090784; + let entry = [key, value]; + let siblings: [Field; TREE_DEPTH] = [0; TREE_DEPTH]; + let zero_node = 0; + assert(add(entry, zero_node, siblings) == 7842913321420301106140788486336995496832503825951977327575501561489697540557); +} + +#[test] +fn test_add_element_to_one_element_tree() { + let key = 8459688297517826598613412977307486050019239051864711035321718508109192087854; + let value = 8510347201346963732943571140849185725417245763047403804445415726302354045170; + let entry = [key, value]; + let old_root = 7842913321420301106140788486336995496832503825951977327575501561489697540557; + let mut siblings: [Field; TREE_DEPTH] = [0; TREE_DEPTH]; + siblings[255] = 7842913321420301106140788486336995496832503825951977327575501561489697540557; + assert(add(entry, old_root, siblings) == 6309163561753770186763792861087421800063032915545949912480764922611421686766); +} + +#[test] +fn test_add_element_to_existing_tree() { + let key = 8459688297517826598613412977307486050019239051864711035321718508109192087854; + let value = 8510347201346963732943571140849185725417245763047403804445415726302354045170; + let entry = [key, value]; + let small_tree_root = 3532809757480436997969526334543526996242857122876262144596246439822675654128; + let mut siblings: [Field; TREE_DEPTH] = [0; TREE_DEPTH]; + siblings[253] = 18126944477260144816572365299295230808286197301459941187567621915186392922196; + siblings[254] = 14443001516360873457302534246953033880503978184674311810335857314606403404583; + siblings[255] = 7842913321420301106140788486336995496832503825951977327575501561489697540557; + let big_tree_root = 4657474665007910823901096287220097081233671466281873230928277896829046731272; + assert(add(entry, small_tree_root, siblings) == big_tree_root); +} + +#[test] +fn test_delete() { + let key = 8459688297517826598613412977307486050019239051864711035321718508109192087854; + let value = 8510347201346963732943571140849185725417245763047403804445415726302354045170; + let entry = [key, value]; + let big_tree_root = 4657474665007910823901096287220097081233671466281873230928277896829046731272; + let mut siblings: [Field; TREE_DEPTH] = [0; TREE_DEPTH]; + siblings[253] = 18126944477260144816572365299295230808286197301459941187567621915186392922196; + siblings[254] = 14443001516360873457302534246953033880503978184674311810335857314606403404583; + siblings[255] = 7842913321420301106140788486336995496832503825951977327575501561489697540557; + let small_tree_root = 3532809757480436997969526334543526996242857122876262144596246439822675654128; + assert(delete(entry, big_tree_root, siblings) == small_tree_root); +} + +#[test] +fn test_update() { + let key = 8459688297517826598613412977307486050019239051864711035321718508109192087854; + let old_value = 8510347201346963732943571140849185725417245763047403804445415726302354045169; + let new_value = 8510347201346963732943571140849185725417245763047403804445415726302354045170; + let old_entry = [key, old_value]; + let old_root = 4202917944688591919039016743999516589372052081571553696755434379850460220435; + let mut siblings: [Field; TREE_DEPTH] = [0; TREE_DEPTH]; + siblings[253] = 18126944477260144816572365299295230808286197301459941187567621915186392922196; + siblings[254] = 14443001516360873457302534246953033880503978184674311810335857314606403404583; + siblings[255] = 7842913321420301106140788486336995496832503825951977327575501561489697540557; + let big_tree_root = 4657474665007910823901096287220097081233671466281873230928277896829046731272; + assert(update(new_value, old_entry, old_root, siblings) == big_tree_root); +} \ No newline at end of file diff --git a/packages/circuits/noir/crates/smt_bn254/src/utils.nr b/packages/circuits/noir/crates/smt_bn254/src/utils.nr new file mode 100644 index 000000000..c9e5c48e8 --- /dev/null +++ b/packages/circuits/noir/crates/smt_bn254/src/utils.nr @@ -0,0 +1,118 @@ +use dep::std::hash::poseidon; +use crate::TREE_DEPTH; + +/* + * Transforms the key into into a big endian array of bits so that when determining the position + * of a tree entry starting from the root node, the first array element to look at is the last. + * @param key The key of a tree entry + * @returns The path that determines the position of a key in the tree + */ +pub fn key_to_path(key: Field) -> [u1] { + key.to_be_bits(TREE_DEPTH) +} + +/* + * Calculates the poseidon bn254 hash. If a leaf node is created, the number 1 is appended to + * the hashed values as follows: H(k,v,1). + * @param left The left element of the hashing pair + * @param right The right element of the hashing pair + * @param is_leaf Whether what is created is a leaf node or not + * @returns The poseidon hash + */ +pub fn hash(left: Field, right: Field, is_leaf: bool) -> Field { + if (is_leaf) { + poseidon::bn254::hash_3([left, right, 1]) + } else { + poseidon::bn254::hash_2([left, right]) + } +} + + +/* + * Calculates the root for a given tree entry based on the passed array of siblings and the passed path. + * @param entry The key and value of an entry [k, v] + * @param siblings Contains the siblings from bottom to top + * @param path The position of the entry in the tree as represented by bits from bottom to top + * @returns The calculated root node + */ +pub fn calculcate_root(entry: [Field; 2], siblings: [Field; TREE_DEPTH], path: [u1]) -> Field { + // serves as container for hashes and is initialized to be the leaf node + let mut node = hash(entry[0], entry[1], true); + // iterates over the list of siblings until the first sibling is found + // arbitrarily assigns the sibling to be the left and the node to be the + // right element of the hashing pair unless the path indicates the opposite + // order in which case the order is changed. The new hash is stored in the container + // until the root node is reached and returned. + for i in 0..TREE_DEPTH { + let sibling = siblings[i]; + if sibling != 0 { + let mut left = sibling; + let mut right = node; + if path[i] == 0 { + left = node; + right = sibling; + } + node = hash(left, right, false); + } + } + node +} + +/* + * Calculates two roots for a given leaf entry based on the passed array of siblings: one root + * for if the leaf entry was included in the tree and one for if the leaf entry was not included + * in the tree. This is useful for efficiently proving the membership of leaf entries for a + * tree while simultaneously modifiying the tree. + * @param entry The key and value of an entry [k, v] + * @param siblings Contains the siblings from bottom to top + * @returns Two root nodes: the first one doesn't include entry, the second does + */ +pub fn calculate_two_roots(entry: [Field; 2], siblings: [Field; TREE_DEPTH]) -> (Field, Field) { + let path = key_to_path(entry[0]); + // long_path_node is a container for hashes to derive the root node for the tree that + // includes the entry + let mut long_path_node = hash(entry[0], entry[1], true); + // long_path_node is a container for hashes to derive the root node for the tree that + // doesn't include the entry + let mut short_path_node: Field = 0; + // iterate over the levels of the tree from bottom to top + for i in 0..TREE_DEPTH { + let sibling = siblings[i]; + // After the first sibling is found, the processes are started to calculate the two root nodes. + // The calulcation of the root node that includes the entry is comparable to `calculate_root`. + // To calc the root node that doesn't include entry, the first sibling is put into the container + // and starting from each SUBSEQUENT iteration it is hashed with its sibling and the resulting hash + // again stored in the container until the root is reached + if sibling != 0 { + if siblings[i - 1] == 0 { + short_path_node = siblings[i]; + } + if path[i] == 0 { + long_path_node = hash(long_path_node, sibling, false); + if(short_path_node != sibling) { + short_path_node = hash(short_path_node, sibling, false); + } + } else { + long_path_node = hash(sibling, long_path_node, false); + if(short_path_node != sibling) { + short_path_node = hash(sibling, short_path_node, false); + } + } + } + } + (short_path_node, long_path_node) +} + +#[test] +fn test_hash_leaf_node() { + let key = 20438969296305830531522370305156029982566273432331621236661483041446048135547; + let value = 17150136040889237739751319962368206600863150289695545292530539263327413090784; + assert(hash(key, value, true) == 7842913321420301106140788486336995496832503825951977327575501561489697540557); +} + +#[test] +fn test_hash_node() { + let left = 7901139023013500965671892970738327280683439536483910503527659926438417204955; + let right = 7842913321420301106140788486336995496832503825951977327575501561489697540557; + assert(hash(left, right, false) == 4657474665007910823901096287220097081233671466281873230928277896829046731272); +} diff --git a/packages/circuits/package.json b/packages/circuits/package.json index 6e0f284c9..56dd92ee5 100644 --- a/packages/circuits/package.json +++ b/packages/circuits/package.json @@ -16,7 +16,7 @@ "scripts": { "circom:compile": "circomkit compile", "circom:setup": "circomkit setup", - "test": "mocha" + "test": "mocha && cd noir && nargo test" }, "dependencies": { "circomlib": "^2.0.5"