Skip to content

Commit

Permalink
added some merkle trees, mux docs
Browse files Browse the repository at this point in the history
  • Loading branch information
erhant committed Dec 14, 2024
1 parent d4f10ed commit d8b00fa
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 26 deletions.
4 changes: 4 additions & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
- [MiMC 🚧](./hashing/mimc.md)
- [Merkle Trees](./merkle-trees/README.md)
- [Sparse Merkle Tree 🚧](./merkle-trees/smt.md)
- [Dense Merkle Tree 🚧](./merkle-trees/dmt.md)
- [Incremential Merkle Tree 🚧](./merkle-trees/imt.md)
- [Complete Binary Merkle Tree 🚧](./merkle-trees/cbmt.md)
- [Merkle Mountain Range 🚧](./merkle-trees/mmr.md)
- [Advanced](./advanced/README.md)

# Examples
Expand Down
40 changes: 25 additions & 15 deletions book/src/control-flow/multiplexing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Multiplexing is a technique to select one of many signals based on a control sig

```cs
template Mux1() {
signal input in[2];
signal input sel;
signal input in[2];
signal input sel;
signal output out;

out <== (in[1] - in[0]) * sel + in[0];
Expand All @@ -23,13 +23,6 @@ template Mux1() {

The main idea here is that `in` has number of values equal to the number of bits of `sel`, and `out = in[sel]`.

To compute this expression, on can construct the truth table and then its corresponding boolean expression. The truth table for `Mux1` is:

| sel | out |
| --- | ------- |
| 0 | `in[0]` |
| 1 | `in[1]` |

## `Mux2`

```cs
Expand All @@ -50,12 +43,29 @@ template Mux2() {
}
```

One can actually construct a `Mux2` using three `Mux1` circuits.

- `inA <== Mux1(in[0], in[1], sel[0])`
- `inB <== Mux1(in[2], in[3], sel[0])`
- `out <== Mux1(inA, inB, sel[1])`
`Mux2` is a 2-bit multiplexer, which means it can select one of four inputs based on two control signals. The arithmetic above may look weird, but its actually simple to derive. Just think of the `sel` cases in order (note that the bits are in little-endian):

- `sel = [0, 0]`: You need `in[0]` here, and since `sel` multiplications will be 0, you need `in[0]` added by default.
- `sel = [1, 0]`: You need `in[1]`, but `in[0]` was added already as a constant so you need `in[1] - in[0]` in total.
- `sel = [0, 1]`: Similar to the previous case, you need `in[2]`, but `in[0]` was added already as a constant so you need `in[2] - in[0]` in total.
- `sel = [1, 1]`: You need `in[3]`, but now we have all things from the previous cases added together, which is a total: `in[0] + (in[1] - in[0]) + (in[2] - in[0])`. We need to add `in[3]` and get rid of evertyhing else here, which gives us `in[3] - in[2] - in[1] + in[0]`.

> You can actually construct a `Mux2` using three `Mux1` circuits.
>
> ```cs
> template Mux2() {
> signal input in[4];
> signal input sel[2];
> signal output out;
>
> signal inA <== Mux1()([in[0], in[1]], sel[0]);
> signal inB <== Mux1()([in[2], in[3]], sel[0]);
> out <== Mux1()([inA, inB], sel[1]);
> }
> ```
>
> In fact, this has 3 non-linear constraints while the previous implementation has 4 non-linear constraints!
## Larger Multiplexers
There are larger multiplexing circuits, e.g. [`Mux3`](https://github.com/iden3/circomlib/blob/circomlib2/circuits/mux3.circom), [`Mux4`](https://github.com/iden3/circomlib/blob/circomlib2/circuits/mux4.circom), etc. but we will not cover them here.
There are larger multiplexing circuits, e.g. [`Mux3`](https://github.com/iden3/circomlib/blob/circomlib2/circuits/mux3.circom), [`Mux4`](https://github.com/iden3/circomlib/blob/circomlib2/circuits/mux4.circom), etc. but we will not cover them here. You can construct them with a similar approach as we did for `Mux2`, i.e. by using smaller multiplexers and combining them.
31 changes: 21 additions & 10 deletions book/src/merkle-trees/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# 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. A Merkle Tree is a hash-based data structure, and can serve as a cryptographic commitment scheme.
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.

You can commit to a set of values using a merkle tree, such as:
They play a huge role in many applications, especially in Blockchain; and there are several types of it, such as:

- [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
Expand Down Expand Up @@ -90,16 +102,15 @@ graph BT

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.

## As a Commitment Scheme

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).
> 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).
7 changes: 7 additions & 0 deletions book/src/merkle-trees/bmt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Binary Merkle Tree

A Binary Merkle Tree is a **dense** Merkle Tree where each leaf node is **full** (non-zero). The [example](./README.md) of the vector with elements `[m, y, v, e, c, t, o, r]` is an example of a Dense Merkle Tree for a tree of depth 3.

## See Also

- [PSE zk-kit: Binary Merkle Root](https://github.com/privacy-scaling-explorations/zk-kit.circom/tree/main/packages/binary-merkle-root)
7 changes: 7 additions & 0 deletions book/src/merkle-trees/cbmt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Complete Binary Merkle Tree

A **Complete Binary Merkle Tree** is a Merkle Tree that does not necessarily have $2^n-1$ nodes, but instead it is optimized to use a few nodes as possible, doing so with only the exception of the last node possibly being empty.

## See Also

- [Nervos Network](https://www.nervos.org/)'s CBMT implementation: <https://github.com/nervosnetwork/merkle-tree>
10 changes: 10 additions & 0 deletions book/src/merkle-trees/imt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Incremential Merkle Tree

An Incremential Merkle Tree is a tree that one can start completely empty (all-zeros) with a fixed number of nodes, and then allow addition & removal of nodes to the tree.

## See Also

- [PSE zk-kit: Incremental Merkle Tree](https://github.com/privacy-scaling-explorations/zk-kit/tree/main/packages/imt)
- [PSE zk-kit: Lean Incremental Merkle Tree](https://github.com/privacy-scaling-explorations/zk-kit/tree/main/packages/lean-imt)
- [HerodotusDev's Accumulators IMT](https://github.com/HerodotusDev/accumulators/blob/main/packages/incremental-merkle-tree/README.md)
- [RareSkills Tornado Cash Line-by-Line](https://www.rareskills.io/post/how-does-tornado-cash-work)
7 changes: 7 additions & 0 deletions book/src/merkle-trees/mmr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Merkle Mountain Range

TODO: !!!

## See Also

- [HerodotusDev's Accumulators MMR](https://github.com/HerodotusDev/accumulators/tree/main/packages/merkle-mountain-range)
13 changes: 12 additions & 1 deletion book/src/merkle-trees/smt.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Sparse Merkle Tree

In our description of the Merkle Tree, we had a binary-tree with 8 leaf nodes, and we had filled all of them with elements of a vector $\vec{v} = [m, y, v, e, c, t, o, r]$. What if we did not have data to fill the entire leaves?
What if we don't even know the size of the tree in advance? This is where **Sparse Merkle Trees** come in. It is like an [**Incremential Merkle Tree**](./imt.md) in the sense that we can update the tree by adding / removing nodes, but in the case of a Sparse Merkle Tree the size of the tree is not known ahead of time!

A Sparse Merkle Tree allow us to create Merkle Trees where the leaf nodes are not entirely filled, and one can create either an inclusion proof or exclusion proof for an element in the tree without revealing the entire tree.

- **Inclusion proof** shows that an element is in the tree, as we discussed while describing Merkle Trees.
- **Exclusion proof** shows that an element is NOT in the tree. This is possible with a Sparse Merkle Tree because nodes

## See Also

- [Tochicool's SMT](https://github.com/tochicool/sparse-merkle-trees#readme)
- [Efficient Sparse Merkle Trees](https://eprint.iacr.org/2016/683.pdf) by Rasmus Dahlberg, Tobias Pulls, and Roel Peeters.
- Iden3 (authors of Circom) has a nice publication on Merkle Trees: [Sparse Merkle Trees](https://docs.iden3.io/publications/pdfs/Merkle-Tree.pdf) by Jordi Baylina and Marta Belles.
Binary file modified bun.lockb
Binary file not shown.
36 changes: 36 additions & 0 deletions circuits/merkle-trees/bmt.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
pragma circom 2.1.0;

include "circomlib/circuits/poseidon.circom";

// 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 BinaryMerkleTree(n) {
assert(n > 0);
var NUM_LEAVES = 1 << n;
var NUM_NODES = (1 << (n+1)) - 1;
signal input leafs[NUM_LEAVES];
signal output root;

signal nodes[NUM_NODES];

// compute hashes of leaves
for (var i = 0; i < NUM_LEAVES; i++) {
nodes[(NUM_NODES - 1) - i] <== Poseidon(1)([leafs[i]]);
}

// build the tree from the leaves to the root in reverse
for (var i = NUM_NODES - NUM_LEAVES - 1; i >= 0; i--) {
nodes[i] <== Poseidon(2)([nodes[2*i + 1], nodes[2*i + 2]]);
}

root <== nodes[0];
}
6 changes: 6 additions & 0 deletions circuits/test/merkle-trees/bmt-1.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// auto-generated by circomkit
pragma circom 2.0.0;

include "../../merkle-trees/bmt.circom";

component main = BinaryMerkleTree(1);
6 changes: 6 additions & 0 deletions circuits/test/merkle-trees/bmt-2.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// auto-generated by circomkit
pragma circom 2.0.0;

include "../../merkle-trees/bmt.circom";

component main = BinaryMerkleTree(2);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"circomkit": "^0.3.1",
"circomlib": "^2.0.5",
"poseidon-lite": "^0.3.0",
"snarkjs": "^0.7.5"
},
"devDependencies": {
Expand Down
59 changes: 59 additions & 0 deletions tests/merkle-trees/bmt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { poseidon1, poseidon2 } from "poseidon-lite";
import { circomkit } from "../common";
import { describe, beforeAll, it } from "bun:test";

function binaryMerkleTree(leafs: bigint[]) {
if (leafs.length === 0) {
throw new Error("The number of leafs must be greater than 0");
}

const n = Math.ceil(Math.log2(leafs.length));
if (1 << n !== leafs.length) {
throw new Error("The number of leafs must be a power of 2 " + `${1 << n} != ${leafs.length}`);
}

const NUM_LEAVES = 1 << n;
const NUM_NODES = (1 << (n + 1)) - 1;
const nodes: bigint[] = new Array(NUM_NODES).fill(0n);

// compute hashes of leaves
for (let i = 0; i < NUM_LEAVES; i++) {
nodes[NUM_NODES - 1 - i] = poseidon1([leafs[i]]);
}

// build the tree from the leaves to the root in reverse
for (let i = NUM_NODES - NUM_LEAVES - 1; i >= 0; i--) {
nodes[i] = poseidon2([nodes[2 * i + 1], nodes[2 * i + 2]]);
}

const root = nodes[0];
return { leafs, nodes, root };
}

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 leafs = [123n, 345n];
const { root } = binaryMerkleTree(leafs);
await circuit.expectPass({ leafs }, { root });
});

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],
});

const leafs = [123n, 345n, 678n, 981n];
const { root } = binaryMerkleTree(leafs);
await circuit.expectPass({ leafs }, { root });
});
});

0 comments on commit d8b00fa

Please sign in to comment.