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 all 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
39 changes: 31 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Implement recursion
- RFC: https://github.com/o1-labs/snarkyjs/issues/89
- Enable smart contract methods to take previous proofs as argument
- Add new primitive `ZkProgram` which represents a collection of circuits that produce instances of the same proof.
Like smart contracts, ZkPrograms can produce execution proofs and merge in previous proofs, but they are more general and suitable for roll-up-type systems.
- Supported numbers of merged proofs are 0, 1 and 2
- PRs: https://github.com/o1-labs/snarkyjs/pull/245 https://github.com/o1-labs/snarkyjs/pull/250 https://github.com/o1-labs/snarkyjs/pull/261
- `SmartContract.digest()` to quickly compute a hash of the contract's circuit. This will be used by the zkApp CLI to figure out whether `compile` should be re-run or a cached verification key can be used.
- **Recursive proofs**. RFC: https://github.com/o1-labs/snarkyjs/issues/89, PRs: https://github.com/o1-labs/snarkyjs/pull/245 https://github.com/o1-labs/snarkyjs/pull/250 https://github.com/o1-labs/snarkyjs/pull/261
- Enable smart contract methods to take previous proofs as arguments, and verify them in the circuit
- Add `ZkProgram`, a new primitive which represents a collection of circuits that produce instances of the same proof. So, it's a more general version of `SmartContract`, without any of the Mina-related API.
`ZkProgram` is suitable for rollup-type systems and offchain usage of Pickles + Kimchi.
- **zkApp composability** -- calling other zkApps from inside zkApps. RFC: https://github.com/o1-labs/snarkyjs/issues/303, PRs: https://github.com/o1-labs/snarkyjs/pull/285, https://github.com/o1-labs/snarkyjs/pull/296, https://github.com/o1-labs/snarkyjs/pull/294, https://github.com/o1-labs/snarkyjs/pull/297
- **Events** support via `SmartContract.events`, `this.emitEvent`. RFC: https://github.com/o1-labs/snarkyjs/issues/248, PR: https://github.com/o1-labs/snarkyjs/pull/272
- `fetchEvents` partially implemented for local testing: https://github.com/o1-labs/snarkyjs/pull/323
- **Payments**: `this.send({ to, amount })` as an easier API for sending Mina from smart contracts https://github.com/o1-labs/snarkyjs/pull/325
- `Party.send()` to transfer Mina between any accounts, for example, from users to smart contracts
- `SmartContract.digest()` to quickly compute a hash of the contract's circuit. This is [used by the zkApp CLI](https://github.com/o1-labs/zkapp-cli/pull/233) to figure out whether `compile` should be re-run or a cached verification key can be used. https://github.com/o1-labs/snarkyjs/pull/268
- `Circuit.constraintSystem()` for creating a circuit from a function, counting the number of constraints and computing a digest of the circuit https://github.com/o1-labs/snarkyjs/pull/279
- `this.account.isNew` to assert that an account did not (or did) exist before the transaction https://github.com/MinaProtocol/mina/pull/11524
- `LocalBlockchain.setTimestamp` and other setters for network state, to test network preconditions locally https://github.com/o1-labs/snarkyjs/pull/329
- **Experimental APIs** are now collected under the `Experimental` import, or on `this.experimental` in a smart contract.
- Custom tokens (_experimental_), via `this.experimental.token`. RFC: https://github.com/o1-labs/snarkyjs/issues/233, PR: https://github.com/o1-labs/snarkyjs/pull/273,
- Actions / sequence events support (_experimental_), via `Experimental.Reducer`. RFC: https://github.com/o1-labs/snarkyjs/issues/265, PR: https://github.com/o1-labs/snarkyjs/pull/274
- Merkle tree implementation (_experimental_) via `Experimental.MerkleTree` https://github.com/o1-labs/snarkyjs/pull/343

### Changed

- BREAKING CHANGE: Make on-chain state consistent with other preconditions - throw an error when state is not explicitly constrained https://github.com/o1-labs/snarkyjs/pull/267
- `CircuitValue` improvements https://github.com/o1-labs/snarkyjs/pull/269, https://github.com/o1-labs/snarkyjs/pull/306, https://github.com/o1-labs/snarkyjs/pull/341
- Added a base constructor, so overriding the constructor on classes that extend `CircuitValue` is now _optional_. When overriding, the base constructor can be called without arguments, as previously: `super()`. When not overriding, the expected arguments are all the `@prop`s on the class, in the order they were defined in: `new MyCircuitValue(prop1, prop2)`.
- `CircuitValue.fromObject({ prop1, prop2 })` is a new, better-typed alternative for using the base constructor.
- Fixed: the overridden constructor is now free to have any argument structure -- previously, arguments had to be the props in their declared order. I.e., the behaviour that's now used by the base constructor used to be forced on all constructors, which is no longer the case.
- `Mina.transaction` improvements
- Support zkApp proofs when there are other parties in the same transaction block https://github.com/o1-labs/snarkyjs/pull/280
- Support multiple independent zkApp proofs in one transaction block https://github.com/o1-labs/snarkyjs/pull/296
- Add previously unimplemented preconditions, like `this.network.timestamp` https://github.com/o1-labs/snarkyjs/pull/324 https://github.com/MinaProtocol/mina/pull/11577
- Improve error messages thrown from Wasm, by making Rust's `panic` log to the JS console https://github.com/MinaProtocol/mina/pull/11644
- Not user-facing, but essential: Smart contracts fully constrain the account updates they create, inside the circuit https://github.com/o1-labs/snarkyjs/pull/278

### Fixed

- Fix comparisons on `UInt32` and `UInt64` (`UInt32.lt`, `UInt32.gt`, etc) https://github.com/o1-labs/snarkyjs/issues/174, https://github.com/o1-labs/snarkyjs/issues/101. PR: https://github.com/o1-labs/snarkyjs/pull/307

## [0.4.3](https://github.com/o1-labs/snarkyjs/compare/e66f08d...2375f08)

Expand Down
50 changes: 43 additions & 7 deletions src/examples/sudoku/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,65 @@
import { deploy, submitSolution, getZkappState } from './sudoku-zkapp.js';
import { Sudoku, SudokuZkapp } from './sudoku-zkapp.js';
import { cloneSudoku, generateSudoku, solveSudoku } from './sudoku-lib.js';
import { shutdown } from 'snarkyjs';
import {
Bool,
isReady,
Mina,
Party,
Permissions,
PrivateKey,
shutdown,
} from 'snarkyjs';

// setup
await isReady;
const Local = Mina.LocalBlockchain();
Mina.setActiveInstance(Local);
const account1 = Local.testAccounts[0].privateKey;
const zkappKey = PrivateKey.random();
let zkappAddress = zkappKey.toPublicKey();
let zkapp = new SudokuZkapp(zkappAddress);

// generate sudoku, compile & deploy
let sudoku = generateSudoku(0.5);

console.log('Deploying Sudoku...');
await deploy(sudoku);
console.log('Is the sudoku solved?', (await getZkappState()).isSolved);
await SudokuZkapp.compile(zkappAddress);
let tx = await Mina.transaction(account1, () => {
Party.fundNewAccount(account1);
let zkapp = new SudokuZkapp(zkappAddress);
let sudokuInstance = new Sudoku(sudoku);
zkapp.deploy({ zkappKey });
zkapp.sudokuHash.set(sudokuInstance.hash());
zkapp.isSolved.set(Bool(false));
});
await tx.send().wait();
console.log('Is the sudoku solved?', zkapp.isSolved.get().toBoolean());

let solution = solveSudoku(sudoku);
if (solution === undefined) throw Error('cannot happen');

// submit a wrong solution
// submit a wrong solution (this runs quickly, because proof creation fails)
let noSolution = cloneSudoku(solution);
noSolution[0][0] = (noSolution[0][0] % 9) + 1;

console.log('Submitting wrong solution...');
try {
await submitSolution(sudoku, noSolution);
} catch {}
console.log('Is the sudoku solved?', (await getZkappState()).isSolved);
console.log('Is the sudoku solved?', zkapp.isSolved.get().toBoolean());

// submit the actual solution
console.log('Submitting solution...');
await submitSolution(sudoku, solution);
console.log('Is the sudoku solved?', (await getZkappState()).isSolved);
console.log('Is the sudoku solved?', zkapp.isSolved.get().toBoolean());

shutdown();

async function submitSolution(sudoku: number[][], solution: number[][]) {
let tx = await Mina.transaction(account1, () => {
let zkapp = new SudokuZkapp(zkappAddress);
zkapp.submitSolution(new Sudoku(sudoku), new Sudoku(solution));
});
await tx.prove();
await tx.send().wait();
}
59 changes: 5 additions & 54 deletions src/examples/sudoku/sudoku-zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@ import {
Field,
SmartContract,
method,
PrivateKey,
Mina,
Bool,
state,
State,
Poseidon,
Party,
Permissions,
isReady,
} from 'snarkyjs';

export { deploy, submitSolution, getZkappState };
export { SudokuZkapp, Sudoku };

class Sudoku extends CircuitValue {
@matrixProp(Field, 9, 9) value: Field[][];

static from(value: number[][]) {
return new Sudoku(value.map((row) => row.map(Field)));
constructor(sudoku: number[][]) {
super();
this.value = sudoku.map((row) => row.map(Field));
}

hash() {
Expand Down Expand Up @@ -91,54 +87,9 @@ class SudokuZkapp extends SmartContract {
}
}

// setup
await isReady;
// helper

const Local = Mina.LocalBlockchain();
Mina.setActiveInstance(Local);
const account1 = Local.testAccounts[0].privateKey;

const zkappKey = PrivateKey.random();
let zkappAddress = zkappKey.toPublicKey();

async function deploy(sudoku: number[][]) {
let tx = await Mina.transaction(account1, () => {
Party.fundNewAccount(account1);
let zkapp = new SudokuZkapp(zkappAddress);
let sudokuInstance = Sudoku.from(sudoku);
zkapp.deploy({ zkappKey });
zkapp.setPermissions({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
});
zkapp.sudokuHash.set(sudokuInstance.hash());
zkapp.isSolved.set(Bool(false));
});
await tx.send().wait();
}

async function submitSolution(sudoku: number[][], solution: number[][]) {
let tx = await Mina.transaction(account1, () => {
let zkapp = new SudokuZkapp(zkappAddress);
zkapp.submitSolution(Sudoku.from(sudoku), Sudoku.from(solution));
zkapp.sign(zkappKey);
});
await tx.send().wait();
}

function getZkappState() {
let zkapp = new SudokuZkapp(zkappAddress);
let sudokuHash = fieldToHex(zkapp.sudokuHash.get());
let isSolved = zkapp.isSolved.get().toBoolean();
return { sudokuHash, isSolved };
}

// helpers
function divmod(k: number, n: number) {
let q = Math.floor(k / n);
return [q, k - q * n];
}

function fieldToHex(field: Field) {
return field.toBigInt().toString(16);
}
182 changes: 182 additions & 0 deletions src/examples/zkapps/merkle_tree/merkle_zkapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
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,
PrivateKey,
Party,
} from 'snarkyjs';

await isReady;

const doProofs = true;

class MerkleWitness extends Experimental.MerkleWitness(8) {}

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());
}

addPoints(n: number): Account {
return new Account(this.publicKey, this.points.add(n));
}
}
// 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.balance.addInPlace(UInt64.fromNumber(initialBalance));
this.commitment.set(initialCommitment);
}

@method
guessPreimage(guess: Field, account: Account, path: 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!
let newAccount = account.addPoints(1);

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

this.commitment.set(newCommitment);
}
}

type Names = 'Bob' | 'Alice' | 'Charlie' | 'Olivia';

let Local = Mina.LocalBlockchain();
Mina.setActiveInstance(Local);
let initialBalance = 10_000_000_000;

let feePayer = Local.testAccounts[0].privateKey;

// the zkapp account
let zkappKey = PrivateKey.random();
let zkappAddress = zkappKey.toPublicKey();

// this map serves as our off-chain in-memory storage
let Accounts: Map<string, Account> = new Map<Names, 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
// we initialize a new Merkle Tree with height 8
const Tree = new Experimental.MerkleTree(8);

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();

let leaderboardZkApp = new Leaderboard(zkappAddress);
console.log('Deploying leaderboard..');
if (doProofs) {
await Leaderboard.compile(zkappAddress);
}
let tx = await Mina.transaction(feePayer, () => {
Party.fundNewAccount(feePayer, { initialBalance });
leaderboardZkApp.deploy({ zkappKey });
});
tx.send();

console.log('Initial points: ' + Accounts.get('Bob')?.points);

console.log('Making guess..');
await makeGuess('Bob', 0n, 22);

console.log('Final points: ' + Accounts.get('Bob')?.points);

async function makeGuess(name: Names, index: bigint, guess: number) {
let account = Accounts.get(name)!;
let w = Tree.getWitness(index);
let witness = new MerkleWitness(w);

let tx = await Mina.transaction(feePayer, () => {
leaderboardZkApp.guessPreimage(Field(guess), account, witness);
if (!doProofs) leaderboardZkApp.sign(zkappKey);
});
if (doProofs) {
await tx.prove();
}
tx.send();

// if the transaction was successful, we can update our off-chain storage as well
account.points = account.points.add(1);
Tree.setLeaf(index, account.hash());
leaderboardZkApp.commitment.get().assertEquals(Tree.getRoot());
}
Loading