diff --git a/README.md b/README.md index d1c973c..361284f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # miksi [![Test](https://github.com/miksi-labs/miksi-core/workflows/Test/badge.svg)](https://github.com/miksi-labs/miksi-core/actions?query=workflow%3ATest) -*From Esperanto, **miksi** (miks·i): to mingle, to blend, to mix, to shuffle* +Ethereum zk mixer where all the computation & constructions are done offchain and then proved inside a zkSNARK to the smart-contract (both to *deposit* and *withdraw*). + +## Overview -Ethereum mixer where all the computation & constructions are done offchain and then proved inside a zkSNARK to the smart-contract (both to *deposit* and *withdraw*). +*From Esperanto, **miksi** (miks·i): to mingle, to blend, to mix, to shuffle* The client builds a MerkleTree, carries out the required computation, and then generates a zk-proof proving that the offchain computation has been done correctly (no leaf deletion, and only one correctly formatted leaf addition). This approach requires only `~325.000 gas` to *deposit* (compared to `~1M gas` for an onchain computation approach) , and `~308.000 gas` to *withdraw*. @@ -13,20 +15,25 @@ These gas savings come from the fact that we don't need to carry out the MerkleT **Warning:** This repository is in a very early stage. The current version works, but is not finished. There are some improvements in the works. -The WebApp to use miksi-core can be found at https://github.com/arnaucube/miksi-app +The WebApp to use miksi-core can be found at https://github.com/arnaucube/miksi-app, and a live-demo with the smart contract deployed at Goerli Ethereum testnet can be used here: https://arnaucube.github.io/miksi-app/ -## Circuits tests +## Run +- Circuits tests ``` npm run test-circuits ``` -## Smart Contracts tests +- Smart Contracts tests ``` npm run test-sc ``` -### Compile circom circuit & generate Groth16 verifier contract +- javascript lib tests +``` +npm run test +``` +- Compile circom circuit & generate Groth16 verifier contract ``` ./compile-circuits.sh ``` @@ -41,23 +48,17 @@ npm run test-sc From the depositer's perspective, the interface facilitates the following flow: 1. Generate a random `secret` & `nullifier` - 2. Compute the `commitment`, which is the Poseidon hash: `commitment = H(coinCode, amount, secret, nullifier)`, where: - `coinCode`: code that specifies which currency is being used (`0`==ETH) - `amount`: the amount to be deposited - `secret`: random, private - `nullifier`: random - 3. Fetch all the commitments from the smart-contract - 4. Build the MerkleTree with the fetched commitments - 5. Add the newly computed `commitment` to the MerkleTree - 6. Generate a zkSNARK proof, which proves: - you know the `secret` & `nullifier` for the `commitment` contained in the leaf you've just added to the MerkleTree - the transition from `RootOld` (the current one in the smart-contract) to `RootNew` has been done following the rules (no leaf deletion, and only one correctly formatted leaf addition, etc.) - 7. Send ETH to the smart-contract `deposit` call, together with the zkProof data Once these steps have been carried out, the smart-contract verifies the zkProof of the deposit, and if everything checks out ok, stores the commitment and the new root. @@ -70,11 +71,8 @@ The deposit circuit can be found [here](https://github.com/miksi-labs/miksi-core From the withdrawer's perspective, the interface facilitates the following flow: 1. Fetch all the commitments from the smart-contract - 2. Build the MerkleTree with the fetched commitments - 3. Generate the siblings (merkle proof) for the `commitment` whose `secret` & `nullifier` you know - 4. Generate a zkSNARK proof, which proves: - you know a `secret` for a `nullifier` you reveal, whose `commitment` is in a MerkleTree with `root` matching the one stored in the smart-contract diff --git a/src/miksi.ts b/src/miksi.ts index d83b6dc..53b84e7 100644 --- a/src/miksi.ts +++ b/src/miksi.ts @@ -35,7 +35,7 @@ exports.calcDepositWitness = async (wasm, nLevels, key, secret, commitments) => // rebuild the tree let tree = await smt.newMemEmptyTrie(); await tree.insert(0, 0); - for (let i=0; i const wBuff = Buffer.allocUnsafe(witness.length*32); - for (let i=0; i { @@ -64,74 +55,63 @@ contract("miksi", (accounts) => { tree = await smt.newMemEmptyTrie(); await tree.insert(currKey, 0); - await computeTree(0); - - expect(rootOld[0].toString()).to.be.equal('7191590165524151132621032034309259185021876706372059338263145339926209741311'); - // expect(rootNew[0].toString()).to.be.equal('9328869343897770565751281504295758914771207504252217956739346620422361279598'); + expect(tree.root.toString()).to.be.equal('7191590165524151132621032034309259185021876706372059338263145339926209741311'); }); it("Make first deposit", async () => { - await makeDeposit(0, addr1); + nullifier[0] = await makeDeposit(secret[0], addr1); balance_wei = await web3.eth.getBalance(addr1); // console.log("Balance at " + addr1, web3.utils.fromWei(balance_wei, 'ether')); // expect(balance_wei).to.be.equal('98993526980000000000'); }); it("Make second deposit", async () => { - // await computeTree(1); - await makeDeposit(1, addr3); + nullifier[1] = await makeDeposit(secret[1], addr3); }); it("Make 3rd deposit", async () => { - // await computeTree(2); - await makeDeposit(2, addr3); + nullifier[2] = await makeDeposit(secret[2], addr3); }); - - it("Get the commitments data", async () => { - // getCommitments data + it("Get the commitments data & rebuild the tree", async () => { + // get the commitments data let res = await insMiksi.getCommitments(); expect(res[1].toString()).to.be.equal(tree.root.toString()); - commitmentsArray[0] = res[0]; + let commitmentsArray = res[0]; currKey = res[2]; - }); - it("Rebuild the tree from sc commitments", async () => { + // rebuild the tree let treeTmp = await smt.newMemEmptyTrie(); await treeTmp.insert(0, 0); - for (let i=0; i { - await genZKProof(0, addr2, "1"); - await genZKProof(1, addr4, "2"); - await genZKProof(2, addr4, "3"); + proofs[0] = await genWithdrawZKProof(secret[0], nullifier[0], addr2, "1"); + proofs[1] = await genWithdrawZKProof(secret[1], nullifier[1], addr4, "2"); + proofs[2] = await genWithdrawZKProof(secret[2], nullifier[2], addr4, "3"); }); - it("Try to use the zkProof with another address and get revert", async () => { // console.log("Try to reuse the zkproof and expect revert"); await truffleAssert.fails( - withdrawSC(0, addr1), + withdrawSC(nullifier[0], addr1, proofs[0]), truffleAssert.ErrorType.REVERT, "zkProof withdraw could not be verified" ); }); - it("Withdraw 1 ETH with the zkProof of the 1st deposit to addr2", async () => { // withdraw // console.log("Withdraw of " + ethAmount + " ETH to " + addr2); - let resW = await withdrawSC(0, addr2); + let resW = await withdrawSC(nullifier[0], addr2, proofs[0]); // console.log("resW", resW); balance_wei = await web3.eth.getBalance(addr2); // console.log("Balance at " + addr2, web3.utils.fromWei(balance_wei, 'ether')); expect(balance_wei).to.be.equal('101000000000000000000'); }); - it("Try to reuse the zkProof and get revert", async () => { // console.log("Try to reuse the zkproof and expect revert"); await truffleAssert.fails( - withdrawSC(0, addr2), + withdrawSC(nullifier[0], addr2, proofs[0]), truffleAssert.ErrorType.REVERT, "nullifier already used" ); @@ -139,71 +119,44 @@ contract("miksi", (accounts) => { expect(balance_wei).to.be.equal('101000000000000000000'); }); it("Withdraw 1 ETH with the zkProof of the 2nd deposit to addr4", async () => { - let resW = await withdrawSC(1, addr4); + let resW = await withdrawSC(nullifier[1], addr4, proofs[1]); balance_wei = await web3.eth.getBalance(addr4); expect(balance_wei).to.be.equal('101000000000000000000'); }); it("Withdraw 1 ETH with the zkProof of the 3rd deposit to addr4", async () => { - let resW = await withdrawSC(2, addr4); + let resW = await withdrawSC(nullifier[2], addr4, proofs[2]); balance_wei = await web3.eth.getBalance(addr4); expect(balance_wei).to.be.equal('102000000000000000000'); }); }); +async function makeDeposit(secret, addr) { + currKey += 1; -async function computeTree(u) { const poseidon = circomlib.poseidon.createHash(6, 8, 57); - nullifier[u] = poseidon([currKey+1, secret[u]]).toString(); - commitment[u] = poseidon([coinCode, amount, secret[u], nullifier[u]]).toString(); - - // deposit - // add commitment into SMT + let currNullifier = poseidon([currKey, secret]).toString(); + let currCommitment = poseidon([coinCode, amount, secret, currNullifier]).toString(); - // console.log("currKey", currKey); - rootOld[u] = tree.root; - const resC = await tree.find(currKey+1); - assert(!resC.found); - oldKey[u] = "0"; - oldValue[u] = "0"; - if (!resC.found) { - oldKey[u] = resC.notFoundKey.toString(); - oldValue[u] = resC.notFoundValue.toString(); - } - // console.log(oldValue[u]); - // console.log("FIND", resC); - siblingsOld[u] = resC.siblings; - while (siblingsOld[u].length < nLevels) { - siblingsOld[u].push("0"); - }; - - await tree.insert(currKey+1, commitment[u]); - rootNew[u] = tree.root; - currKey += 1; - // console.log("currKey", currKey); -} - -async function makeDeposit(u, addr) { - let resInsert = await tree.insert(currKey+1, commitment[u]); - rootNew[u] = tree.root; - currKey += 1; + let resI = await tree.insert(currKey, currCommitment); + while (resI.siblings.length < nLevels) resI.siblings.push(Fr.e(0)); // calculate witness const wasm = await fs.promises.readFile("./test/build/deposit.wasm"); const input = unstringifyBigInts({ "coinCode": coinCode, "amount": amount, - "secret": secret[u], - "oldKey": resInsert.isOld0 ? 0 : resInsert.oldKey, - "oldValue": resInsert.isOld0 ? 0 : resInsert.oldValue, - "isOld0": resInsert.isOld0 ? 1 : 0, - "siblings": resInsert.siblings, - "rootOld": resInsert.oldRoot, - "rootNew": resInsert.newRoot, - "commitment": commitment[u], + "secret": secret, + "oldKey": resI.isOld0 ? 0 : resI.oldKey, + "oldValue": resI.isOld0 ? 0 : resI.oldValue, + "isOld0": resI.isOld0 ? 1 : 0, + "siblings": resI.siblings, + "rootOld": resI.oldRoot, + "rootNew": resI.newRoot, + "commitment": currCommitment, "key": currKey }); const options = {}; - // console.log("Calculate witness"); + // console.log("Calculate witness", input); const wc = await WitnessCalculatorBuilder(wasm, options); const w = await wc.calculateWitness(input); const witness = unstringifyBigInts(stringifyBigInts(w)); @@ -213,43 +166,42 @@ async function makeDeposit(u, addr) { // console.log("Generate zkSNARK proof"); const res = groth.genProof(provingKey, witness); - proof[u] = res.proof; - publicSignals[u] = res.publicSignals; + let proof = res.proof; const verificationKey = unstringifyBigInts(JSON.parse(fs.readFileSync("./test/build/deposit-verification_key.json", "utf8"))); - let pubI = unstringifyBigInts([coinCode, amount, res.oldRoot.toString(), res.newRoot.toString(), commitment[u], currKey]); - let validCheck = groth.isValid(verificationKey, proof[u], pubI); + let pubI = unstringifyBigInts([coinCode, amount, resI.oldRoot.toString(), resI.newRoot.toString(), currCommitment, currKey]); + let validCheck = groth.isValid(verificationKey, proof, pubI); + // console.log("VALIDCHECK", validCheck, pubI, proof); assert(validCheck); await insMiksi.deposit( - commitment[u], + currCommitment, tree.root.toString(), - [proof[u].pi_a[0].toString(), proof[u].pi_a[1].toString()], + [proof.pi_a[0].toString(), proof.pi_a[1].toString()], [ - [proof[u].pi_b[0][1].toString(), proof[u].pi_b[0][0].toString()], - [proof[u].pi_b[1][1].toString(), proof[u].pi_b[1][0].toString()] + [proof.pi_b[0][1].toString(), proof.pi_b[0][0].toString()], + [proof.pi_b[1][1].toString(), proof.pi_b[1][0].toString()] ], - [proof[u].pi_c[0].toString(), proof[u].pi_c[1].toString()], + [proof.pi_c[0].toString(), proof.pi_c[1].toString()], {from: addr, value: amount} ); - + return currNullifier; } -async function genZKProof(u, addr, k) { +async function genWithdrawZKProof(secret, nullifier, addr, k) { const resC = await tree.find(k); assert(resC.found); let siblings = resC.siblings; while (siblings.length < nLevels) { siblings.push("0"); }; - // console.log("siblings", siblings); // calculate witness const wasm = await fs.promises.readFile("./test/build/withdraw.wasm"); const input = unstringifyBigInts({ "coinCode": coinCode, "amount": amount, - "secret": secret[u], - "nullifier": nullifier[u], + "secret": secret, + "nullifier": nullifier, "siblings": siblings, "root": tree.root, "address": addr, @@ -266,20 +218,20 @@ async function genZKProof(u, addr, k) { // console.log("Generate zkSNARK proof"); const res = groth.genProof(provingKey, witness); - proof[u] = res.proof; - publicSignals[u] = res.publicSignals; + return res.proof; } -async function withdrawSC(u, addr) { +async function withdrawSC(nullifier, addr, proof) { + // console.log("withdrawSC", proof); return insMiksi.withdraw( addr, - nullifier[u], - [proof[u].pi_a[0].toString(), proof[u].pi_a[1].toString()], + nullifier, + [proof.pi_a[0].toString(), proof.pi_a[1].toString()], [ - [proof[u].pi_b[0][1].toString(), proof[u].pi_b[0][0].toString()], - [proof[u].pi_b[1][1].toString(), proof[u].pi_b[1][0].toString()] + [proof.pi_b[0][1].toString(), proof.pi_b[0][0].toString()], + [proof.pi_b[1][1].toString(), proof.pi_b[1][0].toString()] ], - [proof[u].pi_c[0].toString(), proof[u].pi_c[1].toString()] + [proof.pi_c[0].toString(), proof.pi_c[1].toString()] ); }