diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 6ddb126..839a57a 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -1,9 +1,10 @@ # Summary - [Introduction](./README.md) -- [Preliminary](./preliminary/README.md) - - [Group Theory](./preliminary/group-theory.md) - - [Commitments](./preliminary/commitments.md) +- [Concepts](./concepts/README.md) + - [Group Theory](./concepts/group-theory.md) + - [Commitments](./concepts/commitments.md) + - [Merkle Trees](./concepts/merkle-trees.md) - [Arithmetic](./arithmetic/README.md) - [Multiplication](./arithmetic/multiplication.md) - [Addition](./arithmetic/addition.md) diff --git a/book/src/preliminary/README.md b/book/src/concepts/README.md similarity index 51% rename from book/src/preliminary/README.md rename to book/src/concepts/README.md index 8e4897f..893bd56 100644 --- a/book/src/preliminary/README.md +++ b/book/src/concepts/README.md @@ -1,6 +1,7 @@ -# Preliminaries +# Concepts -Although not mandatory, you may want to check out some preliminary theory to better understand some concepts within Circom. +Although not mandatory, you may want to check out some preliminary theory & concepts to better understand the world of circuits within Circom. - [Group Theory](./group-theory.md) - [Commitments](./commitments.md) +- [Merkle Trees](./merkle-trees.md) diff --git a/book/src/preliminary/commitments.md b/book/src/concepts/commitments.md similarity index 100% rename from book/src/preliminary/commitments.md rename to book/src/concepts/commitments.md diff --git a/book/src/preliminary/group-theory.md b/book/src/concepts/group-theory.md similarity index 100% rename from book/src/preliminary/group-theory.md rename to book/src/concepts/group-theory.md diff --git a/book/src/concepts/merkle-trees.md b/book/src/concepts/merkle-trees.md new file mode 100644 index 0000000..2a85c28 --- /dev/null +++ b/book/src/concepts/merkle-trees.md @@ -0,0 +1,108 @@ +# Merkle Trees + +If you have been in the world of crypto for a while, it is highly likely that you have heard the term [Merkle Tree](https://brilliant.org/wiki/merkle-tree/), also known as Merkle Hash Tree. + +## Definition + +A Merkle Tree is a hash-based data structure, and can serve as a **cryptographic commitment scheme**. + +You can commit to a set of values using a Merkle Tree, such as: + +- Evaluations of a function +- Coefficients of a polynomial +- Files in your database + +Here is an example, where we commit to a vector $\vec{v} = [m, y, v, e, c, t, o, r]$ using a Merkle Tree: + +```mermaid +graph BT + subgraph Merkle Tree + h1["h1 (Merkle Root)"] + h2 --> h1; h3 --> h1 + h4 --> h2; h5 --> h2 + h6 --> h3; h7 --> h3 + h8 --> h4; h9 --> h4 + h10 --> h5; h11 --> h5 + h12 --> h6; h13 --> h6 + h14 --> h7; h15 --> h7 + end + + m -- hash --> h8 + y -- hash --> h9 + v -- hash --> h10 + e -- hash --> h11 + c -- hash --> h12 + t -- hash --> h13 + o -- hash --> h14 + r -- hash --> h15 +``` + +In a Merkle Tree, every node is made up of the hash of its children. In this example binary tree, that would be: + +- $h_1 = H(h_2, h_3)$ +- $h_2 = H(h_4, h_5)$ +- $h_3 = H(h_6, h_7)$ +- and so on. + +The leaf nodes are the hashes of elements of the committed set of data. The final hash $h_1$ at the root of the tree is called the **Merkle Root**. + +> Merkle Trees are often implemented as binary trees, but the concept works for $n$-ary trees as well, where each node has $n$ children. + +## Merkle Proof + +At some point, we may be asked to show that indeed some element of the comitted data exists at some position. For our example above, a verifier could ask "is there really a $t$ at position 6?". + +A naive method here would be give the entire comitted set of data, and let them prove the Merkle Root; however, we can do much better than that! With Merkle Trees, we can answer such queries in a much more efficient way, and without revealing any other data than the one we are asked to reveal. + +The trick is to provide the hashes needed to compute all the way from the requested element up to the Merkle Root. In total, we only need to provide one hash per level (in the case of a binary-tree Merkle Tree) and the verifier can compute the root! + +For instance, to show that we have $t$ at position 6, we need to provide the hashes that are used to compute the parent nodes. + +- $h_{13} = H(t)$ is computed by verifier. +- $h_6 = H(h_{12}, h_{13})$ requires $h_{12}$ to be provided. +- $h_3 = H(h_6, h_7)$ requires $h_7$ to be provided. +- $h_1 = H(h_2, h_3)$ requires $h_2$ to be provided. +- $h_1$ is our commitment, which the verifier knows already. + +The proof is visualized below for the same tree, where the values known & computed by the verifier are colored green and the values provided by the prover are colored blue: + +```mermaid +graph BT + classDef g fill:#afa + classDef b fill:#aaf + + subgraph Merkle Tree + h1["h1 (Merkle Root)"]:::g + h2:::b --> h1 + h3:::g --> h1 + h4[fa:fa-eye-slash] --> h2 + h5[fa:fa-eye-slash] --> h2 + h6:::g --> h3 + h7:::b --> h3 + h8[fa:fa-eye-slash] --> h4 + h9[fa:fa-eye-slash] --> h4 + h10[fa:fa-eye-slash] --> h5 + h11[fa:fa-eye-slash] --> h5 + h12:::b --> h6 + h13:::g --> h6 + h14[fa:fa-eye-slash] --> h7 + h15[fa:fa-eye-slash] --> h7 + end + + t:::g -- hash --> h13 +``` + +You see, we only needed to provide 3 hashes here, although our data had 8 elements! In fact, if you have $n$ elements you only need to provide $\log_2{n}$ elements to the verifier, this is so much more efficient than the naive method of sending all the data to the verifier. + +> A Merkle Root can serve as a cryptographic **commitment** to a set of data. +> +> - It is **hiding** because you can't find the preimage of an hash efficiently. +> - It is **binding** because assuming otherwise would require you to find a hash-collision efficiently, which is known to be intractable. +> +> To **reveal** that some value is part of the comitted set of data at a specific point, you only need to reveal the path from that node to the root, along with the value itself, as described [above](#merkle-proof). + +## Further Reading + +- The original paper is ["A Digital Signature Based on a Conventional Encryption Function"](https://people.eecs.berkeley.edu/~raluca/cs261-f15/readings/me) by Ralph Merkle. +- ["Providing Authentication and Integrity in Outsourced Databases using Merkle Hash Tree's"](https://people.eecs.berkeley.edu/~raluca/cs261-f15/readings/merkleodb.pdf) by Mykletun, Narasimha, Tsudik is another paper that is slightly easier to read. +- RareSkills Merkle Tree second-preimage attack [blog post](https://www.rareskills.io/post/merkle-tree-second-preimage-attack). diff --git a/book/src/merkle-trees/README.md b/book/src/merkle-trees/README.md index b28e6f1..2b20b91 100644 --- a/book/src/merkle-trees/README.md +++ b/book/src/merkle-trees/README.md @@ -1,116 +1,9 @@ # Merkle Trees -If you have been in the world of crypto for a while, it is highly likely that you have heard the term [Merkle Tree](https://brilliant.org/wiki/merkle-tree/), also known as Merkle Hash Tree. - -They play a huge role in many applications, especially in Blockchain; and there are several types of it, such as: +There are several types of Merkle Trees: - [Binary Merkle Tree](./bmt.md) - [Complete Binary Merkle Tree](./cbmt.md) - [Sparse Merkle Tree](./smt.md) - [Incremential Merkle Tree](./imt.md) - [Merkle Mountain Range](./mmr.md) - -## Merkle Tree Basics - -A Merkle Tree is a hash-based data structure, and can serve as a **cryptographic commitment scheme**. - -You can commit to a set of values using a Merkle Tree, such as: - -- Evaluations of a function -- Coefficients of a polynomial -- Files in your database - -Here is an example, where we commit to a vector $\vec{v} = [m, y, v, e, c, t, o, r]$ using a Merkle Tree: - -```mermaid -graph BT - subgraph Merkle Tree - h1["h1 (Merkle Root)"] - h2 --> h1; h3 --> h1 - h4 --> h2; h5 --> h2 - h6 --> h3; h7 --> h3 - h8 --> h4; h9 --> h4 - h10 --> h5; h11 --> h5 - h12 --> h6; h13 --> h6 - h14 --> h7; h15 --> h7 - end - - m -- hash --> h8 - y -- hash --> h9 - v -- hash --> h10 - e -- hash --> h11 - c -- hash --> h12 - t -- hash --> h13 - o -- hash --> h14 - r -- hash --> h15 -``` - -In a Merkle Tree, every node is made up of the hash of its children. In this example binary tree, that would be: - -- $h_1 = H(h_2, h_3)$ -- $h_2 = H(h_4, h_5)$ -- $h_3 = H(h_6, h_7)$ -- and so on. - -The leaf nodes are the hashes of elements of the committed set of data. The final hash $h_1$ at the root of the tree is called the **Merkle Root**. - -> Merkle Trees are often implemented as binary trees, but the concept works for $n$-ary trees as well, where each node has $n$ children. - -## Merkle Proof - -At some point, we may be asked to show that indeed some element of the comitted data exists at some position. For our example above, a verifier could ask "is there really a $t$ at position 6?". - -A naive method here would be give the entire comitted set of data, and let them prove the Merkle Root; however, we can do much better than that! With Merkle Trees, we can answer such queries in a much more efficient way, and without revealing any other data than the one we are asked to reveal. - -The trick is to provide the hashes needed to compute all the way from the requested element up to the Merkle Root. In total, we only need to provide one hash per level (in the case of a binary-tree Merkle Tree) and the verifier can compute the root! - -For instance, to show that we have $t$ at position 6, we need to provide the hashes that are used to compute the parent nodes. - -- $h_{13} = H(t)$ is computed by verifier. -- $h_6 = H(h_{12}, h_{13})$ requires $h_{12}$ to be provided. -- $h_3 = H(h_6, h_7)$ requires $h_7$ to be provided. -- $h_1 = H(h_2, h_3)$ requires $h_2$ to be provided. -- $h_1$ is our commitment, which the verifier knows already. - -The proof is visualized below for the same tree, where the values known & computed by the verifier are colored green and the values provided by the prover are colored blue: - -```mermaid -graph BT - classDef g fill:#afa - classDef b fill:#aaf - - subgraph Merkle Tree - h1["h1 (Merkle Root)"]:::g - h2:::b --> h1 - h3:::g --> h1 - h4[fa:fa-eye-slash] --> h2 - h5[fa:fa-eye-slash] --> h2 - h6:::g --> h3 - h7:::b --> h3 - h8[fa:fa-eye-slash] --> h4 - h9[fa:fa-eye-slash] --> h4 - h10[fa:fa-eye-slash] --> h5 - h11[fa:fa-eye-slash] --> h5 - h12:::b --> h6 - h13:::g --> h6 - h14[fa:fa-eye-slash] --> h7 - h15[fa:fa-eye-slash] --> h7 - end - - t:::g -- hash --> h13 -``` - -You see, we only needed to provide 3 hashes here, although our data had 8 elements! In fact, if you have $n$ elements you only need to provide $\log_2{n}$ elements to the verifier, this is so much more efficient than the naive method of sending all the data to the verifier. - -> A Merkle Root can serve as a cryptographic **commitment** to a set of data. -> -> - It is **hiding** because you can't find the preimage of an hash efficiently. -> - It is **binding** because assuming otherwise would require you to find a hash-collision efficiently, which is known to be intractable. -> -> To **reveal** that some value is part of the comitted set of data at a specific point, you only need to reveal the path from that node to the root, along with the value itself, as described [above](#merkle-proof). - -## Further Reading - -- The original paper is ["A Digital Signature Based on a Conventional Encryption Function"](https://people.eecs.berkeley.edu/~raluca/cs261-f15/readings/me) by Ralph Merkle. -- ["Providing Authentication and Integrity in Outsourced Databases using Merkle Hash Tree's"](https://people.eecs.berkeley.edu/~raluca/cs261-f15/readings/merkleodb.pdf) by Mykletun, Narasimha, Tsudik is another paper that is slightly easier to read. -- RareSkills Merkle Tree second-preimage attack [blog post](https://www.rareskills.io/post/merkle-tree-second-preimage-attack). diff --git a/book/src/merkle-trees/dmt.md b/book/src/merkle-trees/dmt.md new file mode 100644 index 0000000..5d7f9e4 --- /dev/null +++ b/book/src/merkle-trees/dmt.md @@ -0,0 +1 @@ +# Dense Merkle Tree 🚧 diff --git a/circuits/merkle-trees/bmt.circom b/circuits/merkle-trees/bmt.circom index 98e33c7..226c715 100644 --- a/circuits/merkle-trees/bmt.circom +++ b/circuits/merkle-trees/bmt.circom @@ -1,6 +1,7 @@ pragma circom 2.1.0; include "circomlib/circuits/poseidon.circom"; +include "../control-flow/index.circom"; // Binary Merkle Tree // @@ -24,7 +25,7 @@ template BinaryMerkleTree(n) { // compute hashes of leaves for (var i = 0; i < NUM_LEAVES; i++) { - nodes[(NUM_NODES - 1) - i] <== Poseidon(1)([leafs[i]]); + nodes[(NUM_NODES - 1) - i] <== Poseidon(1)([leafs[(NUM_LEAVES - 1) - i]]); } // build the tree from the leaves to the root in reverse @@ -34,3 +35,40 @@ template BinaryMerkleTree(n) { root <== nodes[0]; } + +// Binary Merkle Tree +// +// Parameters: +// - n: depth of the tree such that number of leaves is 2^n +// +// Inputs: +// - leafs: the leaves of the tree +// +// Outputs: +// - root: the root of the tree +// +template BinaryMerkleProof(n) { + assert(n > 0); + signal input in; + signal input siblings[n]; + signal input indices[n]; + signal output root; + + // the path from the leaf to the root + signal path[n]; + + // the last element in the path is the leaf hash + path[n-1] <== Poseidon(1)([in]); + + // compute the "audit path" w.r.t indices + signal children[n-1][2]; + for (var i = n-1; i != 0; i--) { + // index 0: sibling is on the right + // index 1: sibling is on the left + children[i-1] <== Switch()(indices[i], [path[i], siblings[i]]); + path[i-1] <== Poseidon(2)(children[i-1]); + } + + // the last element in the path is the root hash + root <== path[0]; +} diff --git a/circuits/test/merkle-trees/bmt-1-proof.circom b/circuits/test/merkle-trees/bmt-1-proof.circom new file mode 100644 index 0000000..9e3aa15 --- /dev/null +++ b/circuits/test/merkle-trees/bmt-1-proof.circom @@ -0,0 +1,6 @@ +// auto-generated by circomkit +pragma circom 2.0.0; + +include "../../merkle-trees/bmt.circom"; + +component main = BinaryMerkleTree(1); diff --git a/circuits/test/merkle-trees/bmt-2-proof.circom b/circuits/test/merkle-trees/bmt-2-proof.circom new file mode 100644 index 0000000..1e9c433 --- /dev/null +++ b/circuits/test/merkle-trees/bmt-2-proof.circom @@ -0,0 +1,6 @@ +// auto-generated by circomkit +pragma circom 2.0.0; + +include "../../merkle-trees/bmt.circom"; + +component main = BinaryMerkleProof(2); diff --git a/tests/merkle-trees/bmt.test.ts b/tests/merkle-trees/bmt.test.ts index 865bb2e..3615905 100644 --- a/tests/merkle-trees/bmt.test.ts +++ b/tests/merkle-trees/bmt.test.ts @@ -1,6 +1,6 @@ import { poseidon1, poseidon2 } from "poseidon-lite"; import { circomkit } from "../common"; -import { describe, beforeAll, it } from "bun:test"; +import { describe, it } from "bun:test"; function binaryMerkleTree(leafs: bigint[]) { if (leafs.length === 0) { @@ -18,7 +18,7 @@ function binaryMerkleTree(leafs: bigint[]) { // compute hashes of leaves for (let i = 0; i < NUM_LEAVES; i++) { - nodes[NUM_NODES - 1 - i] = poseidon1([leafs[i]]); + nodes[NUM_NODES - 1 - i] = poseidon1([leafs[NUM_LEAVES - 1 - i]]); } // build the tree from the leaves to the root in reverse @@ -31,29 +31,58 @@ function binaryMerkleTree(leafs: bigint[]) { } describe("binary merkle tree", () => { - it("n = 1", async () => { - const circuit = await circomkit.WitnessTester<["leafs"], ["root"]>("bmt-1", { - file: "merkle-trees/bmt", - template: "BinaryMerkleTree", - dir: "test/merkle-trees", - params: [1], - }); + const [LEFT, RIGHT] = [1, 0] as const; + describe("n = 1", () => { const leafs = [123n, 345n]; - const { root } = binaryMerkleTree(leafs); - await circuit.expectPass({ leafs }, { root }); - }); + const tree = binaryMerkleTree(leafs); + + it("should build the tree", async () => { + const circuit = await circomkit.WitnessTester<["leafs"], ["root"]>("bmt-1-proof", { + file: "merkle-trees/bmt", + template: "BinaryMerkleTree", + dir: "test/merkle-trees", + params: [1], + }); - it("n = 2", async () => { - const circuit = await circomkit.WitnessTester<["leafs"], ["root"]>("bmt-2", { - file: "merkle-trees/bmt", - template: "BinaryMerkleTree", - dir: "test/merkle-trees", - params: [2], + await circuit.expectPass({ leafs }, { root: tree.root }); }); + }); + describe("n = 2", () => { const leafs = [123n, 345n, 678n, 981n]; - const { root } = binaryMerkleTree(leafs); - await circuit.expectPass({ leafs }, { root }); + const tree = binaryMerkleTree(leafs); + + it("should build the tree", async () => { + const circuit = await circomkit.WitnessTester<["leafs"], ["root"]>("bmt-2", { + file: "merkle-trees/bmt", + template: "BinaryMerkleTree", + dir: "test/merkle-trees", + params: [2], + }); + + await circuit.expectPass({ leafs }, { root: tree.root }); + }); + + it("should generate proof", async () => { + const circuit = await circomkit.WitnessTester<["in", "siblings", "indices"], ["root"]>("bmt-2-proof", { + file: "merkle-trees/bmt", + template: "BinaryMerkleProof", + dir: "test/merkle-trees", + params: [2], + }); + + /** + * (0) + * <1> (2) right, node[1] + * 3 4 (5) <6> left, node[6] + * 2 (index) + */ + const [siblings, indices] = [ + [tree.nodes[1], tree.nodes[6]], + [RIGHT, LEFT], + ]; + await circuit.expectPass({ in: leafs[2], siblings, indices }, { root: tree.root }); + }); }); });