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

Add Experimental Merkle Tree Implementation #343

Merged
merged 52 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
67997d5
feat: add merkle_tree file
ymekuria Aug 8, 2022
2204d63
feature: merkletree class boilerplate
ymekuria Aug 8, 2022
d204778
feat: declare node and zeroes
ymekuria Aug 8, 2022
dd036a2
feat: merkletree constructor
ymekuria Aug 9, 2022
c23d0fd
add getnode method
ymekuria Aug 9, 2022
831dedd
feat: getroot method
ymekuria Aug 9, 2022
de9b382
feat: add setnode merkle tree method
ymekuria Aug 9, 2022
06cb4dc
feat: add set leaf method
ymekuria Aug 9, 2022
6315b1a
feat: getWitness method
ymekuria Aug 9, 2022
78140a7
feat: validate method
ymekuria Aug 9, 2022
7903276
feat: add fill method
ymekuria Aug 10, 2022
fb2f8cc
chore: create merkle_tree test file
ymekuria Aug 10, 2022
74a037d
feat: add beforeall and afterall hooks
ymekuria Aug 10, 2022
b971174
test: root of empty tree size 1
ymekuria Aug 10, 2022
2fa6a65
test: root is correct
ymekuria Aug 10, 2022
447b06f
test: builds correct tree
ymekuria Aug 10, 2022
a65108d
test: tree height of 128
ymekuria Aug 10, 2022
d5f065f
test: tree height of 256
ymekuria Aug 10, 2022
2fa1777
feat: add merkletree to experimental namespace
ymekuria Aug 10, 2022
04ca2fc
fix: update tests with experimental namespace
ymekuria Aug 11, 2022
00a0320
refactor: add comments
ymekuria Aug 11, 2022
83e6693
make CircuitValue support `super()` w/o arguments
mitschabaude Aug 11, 2022
bfcea03
refactor sudoku example to confirm CircuitValue changes are non-breaking
mitschabaude Aug 11, 2022
7a44142
feat: create merkle witness class
ymekuria Aug 11, 2022
605c5f7
feat: addd constructor
ymekuria Aug 11, 2022
1a0357f
feat: calculateroot merklewitness method
ymekuria Aug 11, 2022
aa0eaa4
feat: calculateindex method
ymekuria Aug 11, 2022
4012fc1
feat: add merklewitness to experimental namespace
ymekuria Aug 11, 2022
db313f2
update changelog
mitschabaude Aug 11, 2022
19322e3
move exports to top
mitschabaude Aug 11, 2022
b1d882d
Merge branch 'main' into feature/merkle-tree
mitschabaude Aug 11, 2022
8b0c939
temp: add merkle tree example
Trivo25 Aug 11, 2022
3b58850
Merge branch 'feature/merkle-tree' of https://github.com/o1-labs/snar…
Trivo25 Aug 11, 2022
dfe1ee0
rm debug
Trivo25 Aug 11, 2022
c7ab3f0
tweak namespace to work with classes
mitschabaude Aug 11, 2022
ee54e61
code golf
mitschabaude Aug 11, 2022
b8fb1c1
add sceptical comments
mitschabaude Aug 11, 2022
f143a7d
Merge branch 'release/0.5' into feature/merkle-tree
mitschabaude Aug 11, 2022
7919125
fix MerkleWitness constructor
mitschabaude Aug 11, 2022
6006431
add leafCount getter
Trivo25 Aug 11, 2022
fbf272a
add range check
Trivo25 Aug 11, 2022
b7f598d
Merge branch 'feature/merkle-tree' of https://github.com/o1-labs/snar…
Trivo25 Aug 11, 2022
b67f59d
fix CircuitValue.toJSON
mitschabaude Aug 11, 2022
f5562c5
Merge branch 'feature/merkle-tree' of https://github.com/o1-labs/snar…
mitschabaude Aug 11, 2022
076449a
tweak leafCount
mitschabaude Aug 11, 2022
f83b7dd
finish example
Trivo25 Aug 11, 2022
83d0f66
dynamic merkle witness
mitschabaude Aug 11, 2022
48ed1f4
Merge branch 'feature/merkle-tree' of https://github.com/o1-labs/snar…
mitschabaude Aug 11, 2022
6f02581
fix
mitschabaude Aug 11, 2022
973d096
add test for MerkleWitness
mitschabaude Aug 11, 2022
4649c5b
make example work
mitschabaude Aug 11, 2022
eaaa83e
make example create proof, clean up
mitschabaude Aug 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions src/examples/zkapps/merkle_tree/merkle_zkapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
Description:

This example describes how developers can use Merkle Trees as a basic off-chain storage tool.

zkApps on Mina can only store a small amount of data on-chain, but many use cases require your application to at least reference big amounts of data.
Merkle Trees give developers the power of storing large amounts of data off-chain, but proving its integrity to the on-chain smart contract!


! Unfamiliar with Merkle Trees? No problem! Check out https://blog.ethereum.org/2015/11/15/merkling-in-ethereum/
*/

import {
SmartContract,
isReady,
shutdown,
Poseidon,
Field,
Experimental,
Permissions,
DeployArgs,
State,
state,
Circuit,
CircuitValue,
PublicKey,
UInt64,
prop,
Mina,
method,
UInt32,
} from 'snarkyjs';

await isReady;

class Account extends CircuitValue {
@prop publicKey: PublicKey;
@prop points: UInt32;

constructor(publicKey: PublicKey, points: UInt32) {
super(publicKey, points);
this.publicKey = publicKey;
this.points = points;
}

hash(): Field {
return Poseidon.hash(this.toFields());
}
}
// we need the initiate tree root in order to tell the contract about our off-chain storage
let initialCommitment: Field = Field.zero;
/*
We want to write a smart contract that serves as a leaderboard,
but only has the commitment of the off-chain storage stored in an on-chain variable.
The accounts of all participants will be stored off-chain!
If a participant can guess the preimage of a hash, they will be granted one point :)
*/

class Leaderboard extends SmartContract {
// a commitment is a cryptographic primitive that allows us to commit to data, with the ability to "reveal" it later
@state(Field) commitment = State<Field>();

deploy(args: DeployArgs) {
super.deploy(args);
this.setPermissions({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
});
this.commitment.set(initialCommitment);
}

@method
guessPreimage(
guess: Field,
account: Account,
path: Experimental.MerkleWitness
) {
// this is our hash! its the hash of the preimage "22", but keep it a secret!
let target =
Field(
17057234437185175411792943285768571642343179330449434169483610110583519635705
);
// if our guess preimage hashes to our target, we won a point!
Poseidon.hash([guess]).assertEquals(target);

// we fetch the on-chain commitment
let commitment = this.commitment.get();
this.commitment.assertEquals(commitment);

// we check that the account is within the committed Merkle Tree
//path.calculateRoot(account.hash()).assertEquals(commitment);

// we update the account and grant one point!
account.points = account.points.add(1);

// we calculate the new Merkle Root, based on the account changes
//let newCommitment = path.calculateRoot(account.hash());

//this.commitment.set(newCommitment);
}
}

// we initialize a new Merkle Tree with height 8
const Tree = new Experimental.MerkleTree(8);

let Local = Mina.LocalBlockchain();
Mina.setActiveInstance(Local);

// this map serves as our off-chain in-memory storage
let Accounts: Map<string, Account> = new Map<string, Account>();

let bob = new Account(Local.testAccounts[0].publicKey, UInt32.from(0));
let alice = new Account(Local.testAccounts[1].publicKey, UInt32.from(0));
let charlie = new Account(Local.testAccounts[2].publicKey, UInt32.from(0));
let olivia = new Account(Local.testAccounts[3].publicKey, UInt32.from(0));

Accounts.set('Bob', bob);
Accounts.set('Alice', alice);
Accounts.set('Charlie', charlie);
Accounts.set('Olivia', olivia);

// we now need "wrap" the Merkle tree around our off-chain storage

Tree.setLeaf(0n, bob.hash());
Tree.setLeaf(1n, alice.hash());
Tree.setLeaf(2n, charlie.hash());
Tree.setLeaf(3n, olivia.hash());

// now that we got our accounts set up, we need the commitment to deploy our contract!
initialCommitment = Tree.getRoot();

console.log(Poseidon.hash([Field(22)]).toString());
23 changes: 18 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,34 @@ import {
} from './lib/circuit_value';
import { jsLayout, asFieldsAndAux } from './snarky/types';
import { packToFields } from './lib/hash';
import { MerkleTree, MerkleWitness as MerkleWitness_ } from './lib/merkle_tree';
export { Experimental };

/**
* This module exposes APIs that are unstable, in the sense that the API surface is expected to change.
* (Not unstable in the sense that they are less functional or tested than other parts.)
*/
const Experimental = {
const Experimental_ = {
Reducer,
createChildParty,
memoizeWitness,
// TODO: for testing, maybe remove later
jsLayout,
asFieldsAndAux,
packToFields,
MerkleTree,
};

/**
* This module exposes APIs that are unstable, in the sense that the API surface is expected to change.
* (Not unstable in the sense that they are less functional or tested than other parts.)
*/
namespace Experimental {
export let Reducer = Experimental_.Reducer;
export let createChildParty = Experimental_.createChildParty;
export let memoizeWitness = Experimental_.memoizeWitness;
export let jsLayout = Experimental_.jsLayout;
export let asFieldsAndAux = Experimental_.asFieldsAndAux;
export let packToFields = Experimental_.packToFields;
export let MerkleTree = Experimental_.MerkleTree;

export type AsFieldsAndAux<T, TJson> = AsFieldsAndAux_<T, TJson>;
export let MerkleWitness = MerkleWitness_;
export type MerkleWitness = MerkleWitness_;
}
61 changes: 61 additions & 0 deletions src/lib/merkle_tree.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
isReady,
shutdown,
Poseidon,
Field,
Experimental,
} from '../../dist/server';

describe('Merkle Tree', () => {
beforeAll(async () => {
await isReady;
});
afterAll(async () => {
setTimeout(shutdown, 0);
});

it('root of empty tree of size 1', () => {
const tree = new Experimental.MerkleTree(1);
expect(tree.getRoot().toString()).toEqual(Field(0).toString());
});

it('root is correct', () => {
const tree = new Experimental.MerkleTree(2);
tree.setLeaf(0n, Field(1));
tree.setLeaf(1n, Field(2));
expect(tree.getRoot().toString()).toEqual(
Poseidon.hash([Field(1), Field(2)]).toString()
);
});

it('builds correct tree', () => {
const tree = new Experimental.MerkleTree(4);
tree.setLeaf(0n, Field(1));
tree.setLeaf(1n, Field(2));
tree.setLeaf(2n, Field(3));
expect(tree.validate(0n)).toBe(true);
expect(tree.validate(1n)).toBe(true);
expect(tree.validate(2n)).toBe(true);
expect(tree.validate(3n)).toBe(true);
});

it('tree of height 128', () => {
const tree = new Experimental.MerkleTree(128);

const index = 2n ** 64n;
expect(tree.validate(index)).toBe(true);

tree.setLeaf(index, Field(1));
expect(tree.validate(index)).toBe(true);
});

it('tree of height 256', () => {
const tree = new Experimental.MerkleTree(256);

const index = 2n ** 128n;
expect(tree.validate(index)).toBe(true);

tree.setLeaf(index, Field(1));
expect(tree.validate(index)).toBe(true);
});
});
115 changes: 115 additions & 0 deletions src/lib/merkle_tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Circuit, CircuitValue, arrayProp } from './circuit_value';
import { Poseidon } from './hash';
import { Bool, Field } from './core';

// external API
export { Witness, MerkleTree, MerkleWitness };

type Witness = { isLeft: boolean; sibling: Field }[];

/**
* Levels are indexed from leafs (level 0) to root (level N - 1).
*/
class MerkleTree {
private nodes: Record<number, Record<string, Field>> = {};
private zeroes: Field[];

constructor(public readonly height: number) {
this.zeroes = [Field(0)];
for (let i = 1; i < height; i++) {
this.zeroes.push(Poseidon.hash([this.zeroes[i - 1], this.zeroes[i - 1]]));
}
}

getNode(level: number, index: bigint): Field {
return this.nodes[level]?.[index.toString()] ?? this.zeroes[level];
}

getRoot(): Field {
return this.getNode(this.height - 1, 0n);
}

private setNode(level: number, index: bigint, value: Field) {
(this.nodes[level] ??= {})[index.toString()] = value;
}

setLeaf(index: bigint, leaf: Field) {
this.setNode(0, index, leaf);
let currIndex = index;
for (let level = 1; level < this.height; level++) {
currIndex = currIndex / 2n;

const left = this.getNode(level - 1, currIndex * 2n);
const right = this.getNode(level - 1, currIndex * 2n + 1n);

this.setNode(level, currIndex, Poseidon.hash([left, right]));
}
}

getWitness(index: bigint): Witness {
const witness = [];
for (let level = 0; level < this.height - 1; level++) {
const isLeft = index % 2n === 0n;
witness.push({
isLeft,
sibling: this.getNode(level, isLeft ? index + 1n : index - 1n),
});
index = index / 2n;
}
return witness;
}

validate(index: bigint): boolean {
const path = this.getWitness(index);
let hash = this.getNode(0, index);
for (const node of path) {
hash = Poseidon.hash(
node.isLeft ? [hash, node.sibling] : [node.sibling, hash]
);
}

return hash.toString() === this.getRoot().toString();
}

fill(leaves: Field[]) {
leaves.forEach((value, index) => {
this.setLeaf(BigInt(index), value);
});
}
}

class MerkleWitness extends CircuitValue {
@arrayProp(Field, 255) path: Field[];
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
@arrayProp(Bool, 255) isLeft: Bool[];

constructor(witness: Witness) {
super(witness);

this.path = witness.map((item) => item.sibling);
this.isLeft = witness.map((item) => Bool(item.isLeft));
}

calculateRoot(leaf: Field): Field {
let hash = leaf;

for (let i = 1; i < 256; ++i) {
const left = Circuit.if(this.isLeft[i - 1], hash, this.path[i - 1]);
const right = Circuit.if(this.isLeft[i - 1], this.path[i - 1], hash);
hash = Poseidon.hash([left, right]);
}

return hash;
}

calculateIndex(): Field {
let powerOfTwo = Field(1);
let index = Field(0);

for (let i = 1; i < 256; ++i) {
index = Circuit.if(this.isLeft[i - 1], index, index.add(powerOfTwo));
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
powerOfTwo = powerOfTwo.mul(2);
}

return index;
}
}