diff --git a/package-lock.json b/package-lock.json index a480047..c4e6ebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8537,8 +8537,8 @@ } }, "websnark": { - "version": "git+https://github.com/poma/websnark.git#59078413786500a78fdc3cc2c74f834fc759049d", - "from": "git+https://github.com/poma/websnark.git#59078413786500a78fdc3cc2c74f834fc759049d", + "version": "git+https://github.com/poma/websnark.git#271c74a76f4d9277c049283c45929fb3a5bb46fc", + "from": "git+https://github.com/poma/websnark.git#271c74a76f4d9277c049283c45929fb3a5bb46fc", "requires": { "big-integer": "^1.6.42" } diff --git a/package.json b/package.json index 43ab101..f78f961 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,6 @@ "truffle-hdwallet-provider": "^1.0.14", "web3": "^1.0.0-beta.55", "web3-utils": "^1.0.0-beta.55", - "websnark": "git+https://github.com/poma/websnark.git#59078413786500a78fdc3cc2c74f834fc759049d" + "websnark": "git+https://github.com/poma/websnark.git#271c74a76f4d9277c049283c45929fb3a5bb46fc" } } diff --git a/scripts/test_snark.js b/scripts/test_snark.js deleted file mode 100644 index bdd6ac8..0000000 --- a/scripts/test_snark.js +++ /dev/null @@ -1,61 +0,0 @@ -const assert = require('assert'); -const snarkjs = require("snarkjs"); -const bigInt = snarkjs.bigInt; -const utils = require("./utils"); -const merkleTree = require('../lib/MerkleTree'); - -function generateDeposit() { - let deposit = { - secret: utils.rbigint(31), - nullifier: utils.rbigint(31), - }; - const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(32), deposit.secret.leInt2Buff(32)]); - deposit.commitment = utils.pedersenHash(preimage); - return deposit; -} - -(async () => { - // === Create 3 deposits === - const dep1 = generateDeposit(); - const dep2 = generateDeposit(); - const dep3 = generateDeposit(); - - const tree = new merkleTree(16); - - await tree.insert(dep1.commitment); - await tree.insert(dep2.commitment); - await tree.insert(dep3.commitment); - - // === Withdrawing deposit 2 === - const {root, path_elements, path_index} = await tree.path(1); - - // Circuit input - const input = { - // public - root: root, - nullifier: dep2.nullifier, - receiver: utils.rbigint(20), - fee: bigInt(1e17), - - // private - secret: dep2.secret, - pathElements: path_elements, - pathIndex: path_index, - }; - - console.log("Input:\n", input); - console.time("Time"); - const proof = await utils.snarkProof(input); - console.log("Proof:\n", proof); - console.timeEnd("Time"); - - const verify = await utils.snarkVerify(proof); - assert(verify); - - // try to cheat with recipient - proof.publicSignals[2] = '0x000000000000000000000000000000000000000000000000000000000000beef'; - const verifyScam = await utils.snarkVerify(proof); - assert(!verifyScam); - - console.log("Done."); -})(); diff --git a/test/Mixer.test.js b/test/Mixer.test.js index c3afb5f..f8d5e54 100644 --- a/test/Mixer.test.js +++ b/test/Mixer.test.js @@ -2,6 +2,7 @@ const should = require('chai') .use(require('bn-chai')(web3.utils.BN)) .use(require('chai-as-promised')) .should() +const fs = require('fs') const { toWei, toBN, fromWei, toHex, randomHex } = require('web3-utils') const { takeSnapshot, revertSnapshot, increaseTime } = require('../scripts/ganacheHelper'); @@ -10,6 +11,8 @@ const Mixer = artifacts.require('./Mixer.sol') const { AMOUNT } = process.env const utils = require('../scripts/utils') +const websnarkUtils = require('websnark/src/utils') +const buildGroth16 = require('websnark/src/groth16'); const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts const snarkjs = require('snarkjs'); const bigInt = snarkjs.bigInt; @@ -33,6 +36,14 @@ function BNArrayToStringArray(array) { return arrayToPrint } +function getRandomReceiver() { + let receiver = utils.rbigint(20) + while (toHex(receiver.toString()).length !== 42) { + receiver = utils.rbigint(20) + } + return receiver +} + contract('Mixer', async accounts => { let mixer const sender = accounts[0] @@ -43,8 +54,11 @@ contract('Mixer', async accounts => { let prefix = 'test' let tree const fee = bigInt(1e17) - const receiver = utils.rbigint(20) + const receiver = getRandomReceiver() const relayer = accounts[1] + let groth16 + let circuit + let proving_key before(async () => { tree = new MerkleTree( @@ -55,6 +69,9 @@ contract('Mixer', async accounts => { ) mixer = await Mixer.deployed() snapshotId = await takeSnapshot() + groth16 = await buildGroth16() + circuit = require("../build/circuits/withdraw.json") + proving_key = fs.readFileSync("build/circuits/withdraw_proving_key.bin").buffer }) describe('#constructor', async () => { @@ -85,6 +102,47 @@ contract('Mixer', async accounts => { }) }) + describe('snark proof verification on js side', async () => { + it('should detect tampering', async () => { + const deposit = generateDeposit() + await tree.insert(deposit.commitment) + const { root, path_elements, path_index } = await tree.path(0); + + const input = stringifyBigInts({ + root, + nullifier: deposit.nullifier, + receiver, + fee, + secret: deposit.secret, + pathElements: path_elements, + pathIndex: path_index, + }) + + let proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) + const originalProof = JSON.parse(JSON.stringify(proof)) + let result = await utils.snarkVerify(proof) + result.should.be.equal(true) + + // nullifier + proof.publicSignals[1] = '133792158246920651341275668520530514036799294649489851421007411546007850802' + result = await utils.snarkVerify(proof) + result.should.be.equal(false) + proof = originalProof + + // try to cheat with recipient + proof.publicSignals[2] = '133738360804642228759657445999390850076318544422' + result = await utils.snarkVerify(proof) + result.should.be.equal(false) + proof = originalProof + + // fee + proof.publicSignals[3] = '1337100000000000000000' + result = await utils.snarkVerify(proof) + result.should.be.equal(false) + proof = originalProof + }) + }) + describe('#withdraw', async () => { it('should work', async () => { const deposit = generateDeposit() @@ -204,6 +262,55 @@ contract('Mixer', async accounts => { const error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected error.reason.should.be.equal('Cannot find your merkle root') }) + + it('should reject with tampered public inputs', async () => { + const deposit = generateDeposit() + await tree.insert(deposit.commitment) + await mixer.deposit(toBN(deposit.commitment.toString()), { value: AMOUNT, from: sender }) + + let {root, path_elements, path_index} = await tree.path(0) + + const userInput = stringifyBigInts({ + root, + nullifier: deposit.nullifier, + receiver, + fee, + secret: deposit.secret, + pathElements: path_elements, + pathIndex: path_index, + }) + + let { pi_a, pi_b, pi_c, publicSignals } = await utils.snarkProof(userInput) + const originalPublicSignals = publicSignals.slice() + const originalPi_a = pi_a.slice() + + // receiver + publicSignals[2] = '0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337' + + let error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected + error.reason.should.be.equal('Invalid withdraw proof'); + + // fee + publicSignals = originalPublicSignals.slice() + publicSignals[3] = '0x000000000000000000000000000000000000000000000000015345785d8a0000' + + error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected + error.reason.should.be.equal('Invalid withdraw proof'); + + // nullifier + publicSignals = originalPublicSignals.slice() + publicSignals[1] = '0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad' + + error = await mixer.withdraw(pi_a, pi_b, pi_c, publicSignals, { from: relayer }).should.be.rejected + error.reason.should.be.equal('Invalid withdraw proof'); + + // proof itself + pi_a[0] = '0x261d81d8203437f29b38a88c4263476d858e6d9645cf21740461684412b31337' + await mixer.withdraw(pi_a, pi_b, pi_c, originalPublicSignals, { from: relayer }).should.be.rejected + + // should work with original values + await mixer.withdraw(originalPi_a, pi_b, pi_c, originalPublicSignals, { from: relayer }).should.be.fulfilled + }) }) afterEach(async () => {