From bd2252afa35cf37b5dc24e5588e2322b0930f877 Mon Sep 17 00:00:00 2001 From: poma Date: Tue, 8 Jun 2021 21:50:34 +0300 Subject: [PATCH] utxo class, separate private key for each input --- circuits/transaction.circom | 14 +- contracts/TornadoPool.sol | 8 +- src/index.js | 330 ++++++++++-------------------------- src/prover.js | 4 +- src/utils.js | 29 ++-- src/utxo.js | 46 +++++ 6 files changed, 164 insertions(+), 267 deletions(-) create mode 100644 src/utxo.js diff --git a/circuits/transaction.circom b/circuits/transaction.circom index af0924c..1d458c8 100644 --- a/circuits/transaction.circom +++ b/circuits/transaction.circom @@ -26,11 +26,10 @@ template Transaction(levels, zeroLeaf) { signal input fee; signal input extDataHash; - signal private input privateKey; - // data for 2 transaction inputs signal private input inAmount[2]; signal private input inBlinding[2]; + signal private input inPrivateKey[2]; signal private input inPathIndices[2]; signal private input inPathElements[2][levels]; @@ -42,6 +41,7 @@ template Transaction(levels, zeroLeaf) { signal private input outPathElements[levels - 1]; component inUtxoHasher[2]; + component inKeypair[2]; component outUtxoHasher[2]; component nullifierHasher[2]; component checkRoot[2] @@ -49,20 +49,20 @@ template Transaction(levels, zeroLeaf) { component inAmountCheck[2]; component outAmountCheck[2]; - component keypair = Keypair(); - keypair.privateKey <== privateKey; - // verify correctness of transaction inputs for (var tx = 0; tx < 2; tx++) { + inKeypair[tx] = Keypair(); + inKeypair[tx].privateKey <== inPrivateKey[tx]; + inUtxoHasher[tx] = TransactionHasher(); inUtxoHasher[tx].amount <== inAmount[tx]; inUtxoHasher[tx].blinding <== inBlinding[tx]; - inUtxoHasher[tx].publicKey <== keypair.publicKey; + inUtxoHasher[tx].publicKey <== inKeypair[tx].publicKey; nullifierHasher[tx] = NullifierHasher(); nullifierHasher[tx].commitment <== inUtxoHasher[tx].commitment; nullifierHasher[tx].merklePath <== inPathIndices[tx]; - nullifierHasher[tx].privateKey <== keypair.privateKey; + nullifierHasher[tx].privateKey <== inPrivateKey[tx]; nullifierHasher[tx].nullifier === inputNullifier[tx]; tree[tx] = MerkleTree(levels); diff --git a/contracts/TornadoPool.sol b/contracts/TornadoPool.sol index 286e403..1f7ebda 100644 --- a/contracts/TornadoPool.sol +++ b/contracts/TornadoPool.sol @@ -29,8 +29,8 @@ contract TornadoPool is ReentrancyGuard { IVerifier public verifier; struct ExtData { - address payable _recipient; - address payable _relayer; + address payable recipient; + address payable relayer; bytes encryptedOutput1; bytes encryptedOutput2; } @@ -87,11 +87,11 @@ contract TornadoPool is ReentrancyGuard { require(msg.value == uint256(extAmount), "Incorrect amount of ETH sent on deposit"); } else { require(msg.value == 0, "Sent ETH amount should be 0 for withdrawal"); - _extData._recipient.transfer(uint256(-extAmount)); + _extData.recipient.transfer(uint256(-extAmount)); } if (_fee > 0) { - _extData._relayer.transfer(_fee); + _extData.relayer.transfer(_fee); } // todo enforce currentCommitmentIndex value in snark diff --git a/src/index.js b/src/index.js index 7ed2b8e..72f6f77 100644 --- a/src/index.js +++ b/src/index.js @@ -3,127 +3,88 @@ const MerkleTree = require('fixed-merkle-tree') const Web3 = require('web3') const { ethers } = require('hardhat') const { BigNumber } = ethers -const { randomBN, bitsToNumber, toFixedHex, toBuffer, poseidonHash, poseidonHash2 } = require('./utils') +const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE } = require('./utils') +const Utxo = require('./utxo') let contract, web3 const { prove } = require('./prover') -const FIELD_SIZE = '21888242871839275222246405745257275088548364400416034343698204186575808495617' const MERKLE_TREE_HEIGHT = 5 const RPC_URL = 'http://localhost:8545' -function fromPrivkey(privkey) { - return { - privkey, - pubkey: poseidonHash([privkey]), - } -} - -function randomKeypair() { - return fromPrivkey(randomBN()) -} - -function createZeroUtxo(keypair) { - return createUtxo( - 0, - randomBN(), - keypair.pubkey, - keypair.privkey, - Array(MERKLE_TREE_HEIGHT).fill(0), - Array(MERKLE_TREE_HEIGHT).fill(0), - ) -} - -function createOutput(amount, pubkey) { - if (!pubkey) { - throw new Error('no pubkey') - } - return createUtxo(amount, randomBN(), pubkey) -} - -function createInput({ amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements }) { - return createUtxo(amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements) -} - -/// unsafe function without sanity checks -function createUtxo(amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements) { - let utxo = { amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements } - utxo.commitment = poseidonHash([amount, blinding, pubkey]) - if (privkey) { - utxo.nullifier = poseidonHash([utxo.commitment, bitsToNumber(merklePathIndices), privkey]) - } - return utxo -} - -function createDeposit(amount, keypair) { - const fakeKeypair = randomKeypair() - const output = createOutput(amount, keypair.pubkey) - output.privkey = keypair.privkey - const tx = { - inputs: [createZeroUtxo(fakeKeypair), createZeroUtxo(fakeKeypair)], - outputs: [output, createZeroUtxo(fakeKeypair)], // todo shuffle - } - return tx -} - async function buildMerkleTree() { console.log('Getting contract state...') const events = await contract.getPastEvents('NewCommitment', { fromBlock: 0, toBlock: 'latest' }) const leaves = events .sort((a, b) => a.returnValues.index - b.returnValues.index) // todo sort by event date .map((e) => toFixedHex(e.returnValues.commitment)) - console.log('leaves', leaves) + // console.log('leaves', leaves) return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 }) } -async function insertOutput(tree, output) { - await tree.insert(output.commitment) - let { pathElements, pathIndices } = await tree.path(tree.elements().length - 1) - output.merklePathIndices = pathIndices - output.merklePathElements = pathElements -} - -async function getProof({ input1, input2, output1, output2, tree, extAmount, fee, recipient, relayer }) { - const oldRoot = tree.root() - - // if deposit require(extAmount > 0) - - if (input1.amount !== 0) { - // transact - const index1 = await tree.indexOf(toFixedHex(input1.commitment)) - const path1 = await tree.path(index1) +async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer }) { + // todo shuffle inputs and outputs + if (inputs.length !== 2 || outputs.length !== 2 ) { + throw new Error('Unsupported number of inputs/outputs') } - await insertOutput(tree, output1) - await insertOutput(tree, output2) + let inputMerklePathIndices = [] + let inputMerklePathElements = [] - extData = recipient + relayer // TODO + for (const input of inputs) { + if (input.amount > 0) { + const index = tree.indexOf(toFixedHex(input.getCommitment())) + if (index < 0) { + throw new Error(`Input commitment ${input.getCommitment()} was not found`) + } + inputMerklePathIndices.push(index) + inputMerklePathElements.push(tree.path(index).pathElements) + } else { + inputMerklePathIndices.push(0) + inputMerklePathElements.push(new Array(tree.levels).fill(0)) + } + } + + const oldRoot = tree.root() + for (const output of outputs) { + output.index = tree.elements().length + tree.insert(output.getCommitment()) + } + const outputIndex = tree.elements().length - 1 + const outputPath = tree.path(outputIndex).pathElements.slice(1) + + const extData = { + recipient: toFixedHex(recipient, 20), + relayer: toFixedHex(relayer, 20), + encryptedOutput1: '0xff', + encryptedOutput2: '0xff', + } + + const extDataHash = getExtDataHash(extData) let input = { root: oldRoot, newRoot: tree.root(), - inputNullifier: [input1.nullifier, input2.nullifier], - outputCommitment: [outputs1.commitment, outputs2.commitment], + inputNullifier: inputs.map(x => x.getNullifier()), + outputCommitment: outputs.map(x => x.getCommitment()), extAmount, fee, - extData, - - // private inputs - privateKey: inputs1.privkey, // TODO make sure you use the right one when you shuffle inputs + extDataHash, // data for 2 transaction inputs - inAmount: [inputs1.amount, inputs2.amount], - inBlinding: [inputs1.blinding, inputs2.blinding], - inPathIndices: [bitsToNumber(inputs1.merklePathIndices), bitsToNumber(inputs2.merklePathIndices)], - inPathElements: [inputs1.merklePathElements, inputs2.merklePathElements], + inAmount: inputs.map(x => x.amount), + inPrivateKey: inputs.map(x => x.privkey), + inBlinding: inputs.map(x => x.blinding), + inPathIndices: inputMerklePathIndices, + inPathElements: inputMerklePathElements, // data for 2 transaction outputs - outAmount: [outputs1.amount, outputs2.amount], - outBlinding: [outputs1.blinding, outputs2.blinding], - outPubkey: [outputs1.pubkey, outputs2.pubkey], - outPathIndices: bitsToNumber[outputs1.merklePathIndices.slice(1)], - outPathElements: [outputs1.merklePathElements.slice(1)], + outAmount: outputs.map(x => x.amount), + outBlinding: outputs.map(x => x.blinding), + outPubkey: outputs.map(x => x.pubkey), + outPathIndices: outputIndex >> 1, + outPathElements: outputPath, } - console.log('SNARK input', input) + //console.log('SNARK input', input) console.log('Generating SNARK proof...') const proof = await prove(input, './artifacts/circuits/transaction') @@ -131,12 +92,14 @@ async function getProof({ input1, input2, output1, output2, tree, extAmount, fee const args = [ toFixedHex(input.root), toFixedHex(input.newRoot), - [toFixedHex(input1.nullifier), toFixedHex(input2.nullifier)], - [toFixedHex(output1.commitment), toFixedHex(output2.commitment)], - toFixedHex(0), - toFixedHex(input.fee), - toFixedHex(extData), // extData hash actually + inputs.map(x => toFixedHex(x.getNullifier())), + outputs.map(x => toFixedHex(x.getCommitment())), + toFixedHex(extAmount), + toFixedHex(fee), + extData, + toFixedHex(extDataHash), ] + // console.log('Solidity args', args) return { proof, @@ -146,16 +109,13 @@ async function getProof({ input1, input2, output1, output2, tree, extAmount, fee async function deposit() { const amount = 1e6 - const tree = await buildMerkleTree() - const keypair = randomKeypair() - const tx = createDeposit(amount, keypair) + const inputs = [new Utxo(), new Utxo()] + const outputs = [new Utxo({ amount }), new Utxo()] const { proof, args } = await getProof({ - input1: tx.inputs[0], - input2: tx.inputs[1], - output1: tx.outputs[1], - output2: tx.outputs[1], - tree, + inputs, + outputs, + tree: await buildMerkleTree(), extAmount: amount, fee: 0, recipient: 0, @@ -167,155 +127,47 @@ async function deposit() { .transaction(proof, ...args) .send({ value: amount, from: web3.eth.defaultAccount, gas: 1e6 }) console.log(`Receipt ${receipt.transactionHash}`) - return tx.outputs[0] + return outputs[0] } -async function transact(txOutput) { - console.log('txOutput', txOutput) - const tree = await buildMerkleTree() - console.log('tree', tree) - const oldRoot = await tree.root() - const keypair = randomKeypair() +async function transact(utxo) { + const inputs = [utxo, new Utxo()] + const outputs = [ + new Utxo({ amount: utxo.amount / 4 }), + new Utxo({ amount: utxo.amount * 3 / 4, privkey: utxo.privkey}), + ] - const index = await tree.indexOf(toFixedHex(txOutput.commitment)) - console.log('index', index) - const { pathElements, pathIndices } = await tree.path(index) - console.log('pathIndices', pathIndices) - txOutput.merklePathElements = pathElements - const input1 = createInput(txOutput) - const tx = { - inputs: [input1, createZeroUtxo(fromPrivkey(txOutput.privkey))], - outputs: [ - createOutput(txOutput.amount / 4, keypair.pubkey), - createOutput((txOutput.amount * 3) / 4, txOutput.pubkey), - ], // todo shuffle - } - tx.outputs[0].privkey = keypair.privkey - tx.outputs[1].privkey = txOutput.privkey - await insertOutput(tree, tx.outputs[0]) - await insertOutput(tree, tx.outputs[1]) - console.log('Note', tx.outputs[0]) - - let input = { - root: oldRoot, - newRoot: await tree.root(), - inputNullifier: [tx.inputs[0].nullifier, tx.inputs[1].nullifier], - outputCommitment: [tx.outputs[0].commitment, tx.outputs[1].commitment], + const { proof, args } = await getProof({ + inputs, + outputs, + tree: await buildMerkleTree(), extAmount: 0, fee: 0, recipient: 0, relayer: 0, - - // private inputs - privateKey: tx.inputs[0].privkey, - - // data for 2 transaction inputs - inAmount: [tx.inputs[0].amount, tx.inputs[1].amount], - inBlinding: [tx.inputs[0].blinding, tx.inputs[1].blinding], - inPathIndices: [ - bitsToNumber(tx.inputs[0].merklePathIndices), - bitsToNumber(tx.inputs[1].merklePathIndices), - ], - inPathElements: [tx.inputs[0].merklePathElements, tx.inputs[1].merklePathElements], - - // data for 2 transaction outputs - outAmount: [tx.outputs[0].amount, tx.outputs[1].amount], - outBlinding: [tx.outputs[0].blinding, tx.outputs[1].blinding], - outPubkey: [tx.outputs[0].pubkey, tx.outputs[1].pubkey], - outPathIndices: bitsToNumber(tx.outputs[0].merklePathIndices.slice(1)), - outPathElements: tx.outputs[0].merklePathElements.slice(1), - } - - console.log('TRANSFER input', input) - - console.log('Generating SNARK proof...') - const proof = await prove(input, './artifacts/circuits/transaction') - - const args = [ - toFixedHex(input.root), - toFixedHex(input.newRoot), - [toFixedHex(tx.inputs[0].nullifier), toFixedHex(tx.inputs[1].nullifier)], - [toFixedHex(tx.outputs[0].commitment), toFixedHex(tx.outputs[1].commitment)], - toFixedHex(0), - toFixedHex(input.fee), - toFixedHex(input.recipient, 20), - toFixedHex(input.relayer, 20), - ] + }) console.log('Sending transfer transaction...') const receipt = await contract.methods .transaction(proof, ...args) .send({ from: web3.eth.defaultAccount, gas: 1e6 }) console.log(`Receipt ${receipt.transactionHash}`) - return tx.outputs[0] + return outputs[0] } -async function withdraw(txOutput) { - console.log('txOutput', txOutput) - const tree = await buildMerkleTree() - const oldRoot = await tree.root() +async function withdraw(utxo) { + const inputs = [utxo, new Utxo()] + const outputs = [new Utxo(), new Utxo()] - const index = await tree.indexOf(toFixedHex(txOutput.commitment)) - console.log('index', index) - const { pathElements, pathIndices } = await tree.path(index) - console.log('pathIndices', pathIndices) - txOutput.merklePathElements = pathElements - const input1 = createInput(txOutput) - const fakeKeypair = randomKeypair() - const tx = { - inputs: [input1, createZeroUtxo(fromPrivkey(txOutput.privkey))], - outputs: [createZeroUtxo(fakeKeypair), createZeroUtxo(fakeKeypair)], // todo shuffle - } - await insertOutput(tree, tx.outputs[0]) - await insertOutput(tree, tx.outputs[1]) - - let input = { - root: oldRoot, - newRoot: await tree.root(), - inputNullifier: [tx.inputs[0].nullifier, tx.inputs[1].nullifier], - outputCommitment: [tx.outputs[0].commitment, tx.outputs[1].commitment], - extAmount: BigNumber.from(FIELD_SIZE).sub(BigNumber.from(txOutput.amount)), + const { proof, args } = await getProof({ + inputs, + outputs, + tree: await buildMerkleTree(), + extAmount: FIELD_SIZE.sub(utxo.amount), fee: 0, recipient: '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48', relayer: 0, - - // private inputs - privateKey: tx.inputs[0].privkey, - - // data for 2 transaction inputs - inAmount: [tx.inputs[0].amount, tx.inputs[1].amount], - inBlinding: [tx.inputs[0].blinding, tx.inputs[1].blinding], - inPathIndices: [ - bitsToNumber(tx.inputs[0].merklePathIndices), - bitsToNumber(tx.inputs[1].merklePathIndices), - ], - inPathElements: [tx.inputs[0].merklePathElements, tx.inputs[1].merklePathElements], - - // data for 2 transaction outputs - outAmount: [tx.outputs[0].amount, tx.outputs[1].amount], - outBlinding: [tx.outputs[0].blinding, tx.outputs[1].blinding], - outPubkey: [tx.outputs[0].pubkey, tx.outputs[1].pubkey], - outPathIndices: bitsToNumber(tx.outputs[0].merklePathIndices.slice(1)), - outPathElements: tx.outputs[0].merklePathElements.slice(1), - } - - console.log('WITHDRAW input', input) - - console.log('Generating SNARK proof...') - const proof = await prove(input, './artifacts/circuits/transaction') - - const args = [ - toFixedHex(input.root), - toFixedHex(input.newRoot), - [toFixedHex(tx.inputs[0].nullifier), toFixedHex(tx.inputs[1].nullifier)], - [toFixedHex(tx.outputs[0].commitment), toFixedHex(tx.outputs[1].commitment)], - toFixedHex(input.extAmount), - toFixedHex(input.fee), - toFixedHex(input.recipient, 20), - toFixedHex(input.relayer, 20), - ] - - console.log('args', args) + }) console.log('Sending withdraw transaction...') const receipt = await contract.methods @@ -333,12 +185,12 @@ async function main() { }) netId = await web3.eth.net.getId() const contractData = require('../artifacts/contracts/TornadoPool.sol/TornadoPool.json') - contract = new web3.eth.Contract(contractData.abi, '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9') + contract = new web3.eth.Contract(contractData.abi, '0x0E801D84Fa97b50751Dbf25036d067dCf18858bF') web3.eth.defaultAccount = (await web3.eth.getAccounts())[0] - const txOutput = await deposit() - const txOutput1 = await transact(txOutput) - await withdraw(txOutput1) + const utxo1 = await deposit() + const utxo2 = await transact(utxo1) + await withdraw(utxo2) } main() diff --git a/src/prover.js b/src/prover.js index a939217..e7876d7 100644 --- a/src/prover.js +++ b/src/prover.js @@ -8,7 +8,7 @@ const exec = util.promisify(require('child_process').exec) function prove(input, keyBasePath) { input = utils.stringifyBigInts(input) - console.log('input', input) + // console.log('input', input) return tmp.dir().then(async (dir) => { dir = dir.path let out @@ -28,6 +28,8 @@ function prove(input, keyBasePath) { out = await exec( `zkutil prove -c ${keyBasePath}.r1cs -p ${keyBasePath}.params -w ${dir}/witness.json -r ${dir}/proof.json -o ${dir}/public.json`, ) + // todo catch inconsistent input during witness generation + await exec(`zkutil verify -p ${keyBasePath}.params -r ${dir}/proof.json -i ${dir}/public.json`) } catch (e) { console.log(out, e) throw e diff --git a/src/utils.js b/src/utils.js index 379c99d..9539d6e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,7 +1,7 @@ const crypto = require('crypto') const ethers = require('ethers') const BigNumber = ethers.BigNumber -const { poseidon } = require('circomlib') +const {poseidon} = require('circomlib') const poseidonHash = (items) => BigNumber.from(poseidon(items).toString()) const poseidonHash2 = (a, b) => poseidonHash([a, b]) @@ -12,13 +12,19 @@ const FIELD_SIZE = BigNumber.from( /** Generate random number of specified byte length */ const randomBN = (nbytes = 31) => BigNumber.from(crypto.randomBytes(nbytes)) -function getExtDataHash({ recipient, relayer, encryptedOutput1, encryptedOutput2 }) { +function getExtDataHash({recipient, relayer, encryptedOutput1, encryptedOutput2}) { + const abi = new ethers.utils.AbiCoder() + const encodedData = abi.encode( - ['address', 'address', 'bytes', 'bytes'], - [toFixedHex(recipient, 20), toFixedHex(relayer, 20), encryptedOutput1, encryptedOutput2], + ['tuple(address recipient,address relayer,bytes encryptedOutput1,bytes encryptedOutput2)'], + [{ + recipient: toFixedHex(recipient, 20), + relayer: toFixedHex(relayer, 20), + encryptedOutput1: encryptedOutput1, + encryptedOutput2: encryptedOutput2, + }], ) const hash = ethers.utils.keccak256(encodedData) - return BigNumber.from(hash).mod(FIELD_SIZE) } @@ -26,8 +32,8 @@ function getExtDataHash({ recipient, relayer, encryptedOutput1, encryptedOutput2 const toFixedHex = (number, length = 32) => '0x' + (number instanceof Buffer - ? number.toString('hex') - : BigNumber.from(number).toHexString().slice(2) + ? number.toString('hex') + : BigNumber.from(number).toHexString().slice(2) ).padStart(length * 2, '0') const toBuffer = (value, length) => @@ -39,18 +45,9 @@ const toBuffer = (value, length) => 'hex', ) -function bitsToNumber(bits) { - let result = 0 - for (const item of bits.slice().reverse()) { - result = (result << 1) + item - } - return result -} - module.exports = { FIELD_SIZE, randomBN, - bitsToNumber, toFixedHex, toBuffer, poseidonHash, diff --git a/src/utxo.js b/src/utxo.js new file mode 100644 index 0000000..a647253 --- /dev/null +++ b/src/utxo.js @@ -0,0 +1,46 @@ +const { ethers } = require('hardhat') +const { BigNumber } = ethers +const { randomBN, poseidonHash } = require('./utils') + +function fromPrivkey(privkey) { + return { + privkey, + pubkey: poseidonHash([privkey]), + } +} + +class Utxo { + constructor({amount, pubkey, privkey, blinding, index} = {}) { + if (!pubkey) { + if (privkey) { + pubkey = fromPrivkey(privkey).pubkey + } else { + ({pubkey, privkey} = fromPrivkey(randomBN())) + } + } + this.amount = BigNumber.from(amount || 0); + this.blinding = blinding || randomBN(); + this.pubkey = pubkey; + this.privkey = privkey; + this.index = index; + } + + getCommitment() { + if (!this._commitment) { + this._commitment = poseidonHash([this.amount, this.blinding, this.pubkey]) + } + return this._commitment + } + + getNullifier() { + if (!this._nullifier) { + if (this.amount > 0 && (!this.index || !this.privkey)) { + throw new Error('Can not compute nullifier without utxo index or private key') + } + this._nullifier = poseidonHash([this.getCommitment(), this.index || 0, this.privkey || 0]) + } + return this._nullifier + } +} + +module.exports = Utxo