diff --git a/.editorconfig b/.editorconfig index 53b061a..3c44241 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,8 +2,8 @@ root = true [*] indent_style = space -indent_size = 2 +indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = true \ No newline at end of file +insert_final_newline = true diff --git a/TODO b/TODO index e7206ff..3c6114f 100644 --- a/TODO +++ b/TODO @@ -1 +1,3 @@ -* update verifier +* update verifier version +* shuffle outputs +* diff --git a/circuits/merkleTree.circom b/circuits/merkleTree.circom index 193f381..c1831c0 100644 --- a/circuits/merkleTree.circom +++ b/circuits/merkleTree.circom @@ -29,10 +29,11 @@ template DualMux() { // pathIndices input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path template MerkleTree(levels) { signal input leaf; - signal input root; signal input pathElements[levels]; signal input pathIndices; + signal output root; + component selectors[levels]; component hashers[levels]; @@ -50,5 +51,5 @@ template MerkleTree(levels) { hashers[i].right <== selectors[i].out[1]; } - root === hashers[levels - 1].hash; + root <== hashers[levels - 1].hash; } diff --git a/circuits/transaction.circom b/circuits/transaction.circom index c3273fe..0626b81 100644 --- a/circuits/transaction.circom +++ b/circuits/transaction.circom @@ -38,6 +38,7 @@ template Transaction(levels, zeroLeaf) { // data for 2 transaction outputs signal private input outAmount[2]; signal private input outBlinding[2]; + signal private input outPubkey[2]; signal private input outPathIndices; signal private input outPathElements[levels - 1]; @@ -88,7 +89,7 @@ template Transaction(levels, zeroLeaf) { outUtxoHasher[tx] = TransactionHasher(); outUtxoHasher[tx].amount <== outAmount[tx]; outUtxoHasher[tx].blinding <== outBlinding[tx]; - outUtxoHasher[tx].publicKey <== keypair.publicKey; + outUtxoHasher[tx].publicKey <== outPubkey[tx]; outUtxoHasher[tx].commitment === outputCommitment[tx]; // Check that amount fits into 248 bits to prevent overflow @@ -120,4 +121,4 @@ template Transaction(levels, zeroLeaf) { } } -component main = Transaction(5, 3193090221241211970002919215846211184824251841300455796635909287157453409439); +component main = Transaction(5, 16923532097304556005972200564242292693309333953544141029519619077135960040221); diff --git a/circuits/utils.circom b/circuits/utils.circom index 67981d4..3c1a554 100644 --- a/circuits/utils.circom +++ b/circuits/utils.circom @@ -7,8 +7,11 @@ template Keypair() { signal input privateKey; signal output publicKey; - publicKey <== privateKey; - // todo + component hasher = MiMCSponge(1, 1); + hasher.ins[0] <== privateKey; + hasher.k <== 0; + + publicKey <== hasher.outs[0]; } template TransactionHasher() { diff --git a/contracts/TornadoPool.sol b/contracts/TornadoPool.sol index 7a32b26..fa73568 100644 --- a/contracts/TornadoPool.sol +++ b/contracts/TornadoPool.sol @@ -34,8 +34,9 @@ contract TornadoPool is ReentrancyGuard { @dev The constructor @param _verifier the address of SNARK verifier for this contract */ - constructor(IVerifier _verifier) public { + constructor(IVerifier _verifier, bytes32 _currentRoot) public { verifier = _verifier; + currentRoot = _currentRoot; } function transaction( @@ -83,8 +84,8 @@ contract TornadoPool is ReentrancyGuard { transfer(_relayer, _fee); } - emit NewCommitment(_outputCommitments[0], currentDepositIndex++); - emit NewCommitment(_outputCommitments[1], currentDepositIndex++); + emit NewCommitment(_outputCommitments[0], currentCommitmentIndex++); + emit NewCommitment(_outputCommitments[1], currentCommitmentIndex++); emit NewNullifier(_inputNullifiers[0]); emit NewNullifier(_inputNullifiers[1]); // emit Transaction(); diff --git a/lib/merkleTree.js b/lib/merkleTree.js new file mode 100644 index 0000000..7d22fd4 --- /dev/null +++ b/lib/merkleTree.js @@ -0,0 +1,201 @@ +const jsStorage = require('./storage') +const hasherImpl = require('./mimc') + +class MerkleTree { + + constructor(n_levels, defaultElements, prefix, storage, hasher) { + this.prefix = prefix + this.storage = storage || new jsStorage() + this.hasher = hasher || new hasherImpl() + this.n_levels = n_levels + this.zero_values = [] + this.totalElements = 0 + + let current_zero_value = '21663839004416932945382355908790599225266501822907911457504978515578255421292' + this.zero_values.push(current_zero_value) + for (let i = 0; i < n_levels; i++) { + current_zero_value = this.hasher.hash(i, current_zero_value, current_zero_value) + this.zero_values.push( + current_zero_value.toString(), + ) + } + if (defaultElements) { + let level = 0 + this.totalElements = defaultElements.length + defaultElements.forEach((element, i) => { + this.storage.put(MerkleTree.index_to_key(prefix, level, i), element) + }) + level++ + let numberOfElementsInLevel = Math.ceil(defaultElements.length / 2) + for (level; level <= this.n_levels; level++) { + for(let i = 0; i < numberOfElementsInLevel; i++) { + const leftKey = MerkleTree.index_to_key(prefix, level - 1, 2 * i) + const rightKey = MerkleTree.index_to_key(prefix, level - 1, 2 * i + 1) + + const left = this.storage.get(leftKey) + const right = this.storage.get_or_element(rightKey, this.zero_values[level - 1]) + + const subRoot = this.hasher.hash(null, left, right) + this.storage.put(MerkleTree.index_to_key(prefix, level, i), subRoot) + } + numberOfElementsInLevel = Math.ceil(numberOfElementsInLevel / 2) + } + } + } + + static index_to_key(prefix, level, index) { + const key = `${prefix}_tree_${level}_${index}` + return key + } + + async root() { + let root = await this.storage.get_or_element( + MerkleTree.index_to_key(this.prefix, this.n_levels, 0), + this.zero_values[this.n_levels], + ) + + return root + } + + async path(index) { + class PathTraverser { + constructor(prefix, storage, zero_values) { + this.prefix = prefix + this.storage = storage + this.zero_values = zero_values + this.path_elements = [] + this.path_index = [] + } + + async handle_index(level, element_index, sibling_index) { + const sibling = await this.storage.get_or_element( + MerkleTree.index_to_key(this.prefix, level, sibling_index), + this.zero_values[level], + ) + this.path_elements.push(sibling) + this.path_index.push(element_index % 2) + } + } + index = Number(index) + let traverser = new PathTraverser(this.prefix, this.storage, this.zero_values) + const root = await this.storage.get_or_element( + MerkleTree.index_to_key(this.prefix, this.n_levels, 0), + this.zero_values[this.n_levels], + ) + + const element = await this.storage.get_or_element( + MerkleTree.index_to_key(this.prefix, 0, index), + this.zero_values[0], + ) + + await this.traverse(index, traverser) + return { + root, + path_elements: traverser.path_elements, + path_index: traverser.path_index, + element + } + } + + async update(index, element, insert = false) { + if (!insert && index >= this.totalElements) { + throw Error('Use insert method for new elements.') + } else if(insert && index < this.totalElements) { + throw Error('Use update method for existing elements.') + } + try { + class UpdateTraverser { + constructor(prefix, storage, hasher, element, zero_values) { + this.prefix = prefix + this.current_element = element + this.zero_values = zero_values + this.storage = storage + this.hasher = hasher + this.key_values_to_put = [] + } + + async handle_index(level, element_index, sibling_index) { + if (level == 0) { + this.original_element = await this.storage.get_or_element( + MerkleTree.index_to_key(this.prefix, level, element_index), + this.zero_values[level], + ) + } + const sibling = await this.storage.get_or_element( + MerkleTree.index_to_key(this.prefix, level, sibling_index), + this.zero_values[level], + ) + let left, right + if (element_index % 2 == 0) { + left = this.current_element + right = sibling + } else { + left = sibling + right = this.current_element + } + + this.key_values_to_put.push({ + key: MerkleTree.index_to_key(this.prefix, level, element_index), + value: this.current_element, + }) + this.current_element = this.hasher.hash(level, left, right) + } + } + let traverser = new UpdateTraverser( + this.prefix, + this.storage, + this.hasher, + element, + this.zero_values + ) + + await this.traverse(index, traverser) + traverser.key_values_to_put.push({ + key: MerkleTree.index_to_key(this.prefix, this.n_levels, 0), + value: traverser.current_element, + }) + + await this.storage.put_batch(traverser.key_values_to_put) + } catch(e) { + console.error(e) + } + } + + async insert(element) { + const index = this.totalElements + await this.update(index, element, true) + this.totalElements++ + } + + // todo it can be mode optimal + async insertPair(first, second) { + await insert(first) + await insert(second) + } + + async traverse(index, handler) { + let current_index = index + for (let i = 0; i < this.n_levels; i++) { + let sibling_index = current_index + if (current_index % 2 == 0) { + sibling_index += 1 + } else { + sibling_index -= 1 + } + await handler.handle_index(i, current_index, sibling_index) + current_index = Math.floor(current_index / 2) + } + } + + getIndexByElement(element) { + for(let i = this.totalElements - 1; i >= 0; i--) { + const elementFromTree = this.storage.get(MerkleTree.index_to_key(this.prefix, 0, i)) + if (elementFromTree === element) { + return i + } + } + return false + } +} + +module.exports = MerkleTree diff --git a/lib/mimc.js b/lib/mimc.js new file mode 100644 index 0000000..413793b --- /dev/null +++ b/lib/mimc.js @@ -0,0 +1,17 @@ +const circomlib = require('circomlib') +const mimcsponge = circomlib.mimcsponge +const snarkjs = require('snarkjs') + +const bigInt = snarkjs.bigInt + +class MimcSpongeHasher { + hash(level, left, right) { + return mimcsponge.multiHash([bigInt(left), bigInt(right)]).toString() + } + + hashArray(items) { + return mimcsponge.multiHash(items.map(item => bigInt(item))).toString() + } +} + +module.exports = MimcSpongeHasher diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..52b97d6 --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,39 @@ + + +class JsStorage { + constructor() { + this.db = {} + } + + get(key) { + return this.db[key] + } + + get_or_element(key, defaultElement) { + const element = this.db[key] + if (element === undefined) { + return defaultElement + } else { + return element + } + } + + put(key, value) { + if (key === undefined || value === undefined) { + throw Error('key or value is undefined') + } + this.db[key] = value + } + + del(key) { + delete this.db[key] + } + + put_batch(key_values) { + key_values.forEach(element => { + this.db[element.key] = element.value + }) + } +} + +module.exports = JsStorage diff --git a/migrations/2_deploy_pool.js b/migrations/2_deploy_pool.js index 93ab091..0b447d1 100644 --- a/migrations/2_deploy_pool.js +++ b/migrations/2_deploy_pool.js @@ -1,11 +1,19 @@ /* global artifacts */ const Verifier = artifacts.require('Verifier') const TornadoPool = artifacts.require('TornadoPool') +const MERKLE_TREE_HEIGHT = 5 +const MerkleTree = require('../lib/merkleTree') +const { bigInt } = require('snarkjs') +const toHex = (number, length = 32) => '0x' + (number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)).padStart(length * 2, '0') + module.exports = function(deployer, network, accounts) { return deployer.then(async () => { + const tree = new MerkleTree(MERKLE_TREE_HEIGHT) + const root = await tree.root() const verifier = await Verifier.deployed() - const tornado = await deployer.deploy(TornadoPool, verifier.address) + + const tornado = await deployer.deploy(TornadoPool, verifier.address, toHex(root)) console.log('TornadoPool\'s address ', tornado.address) }) } diff --git a/src/index.js b/src/index.js index 609ecb7..bed29f6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,31 +1,39 @@ -const MERKLE_TREE_HEIGHT = 10; +const MerkleTree = require('../lib/merkleTree') +const fs = require('fs') +const { bigInt, stringifyBigInts } = require('snarkjs') +const crypto = require('crypto') +const Hasher = require('../lib/mimc') +const Web3 = require('web3') +const buildGroth16 = require('websnark/src/groth16') +const websnarkUtils = require('websnark/src/utils') -function rbigint(bytes = 31) { +let contract, web3, circuit, proving_key, groth16 +const hasher = new Hasher() -} +// console.log(hasher.hashArray(['21663839004416932945382355908790599225266501822907911457504978515578255421292', '21663839004416932945382355908790599225266501822907911457504978515578255421292'])) -function randomBoolArray(length) { +const MERKLE_TREE_HEIGHT = 5 +const RPC_URL = 'http://localhost:8545' -} +/** Generate random number of specified byte length */ +const rbigint = (nbytes = 31) => bigInt.leBuff2int(crypto.randomBytes(nbytes)) -function randomArray(length) { +/** BigNumber to hex string of specified length */ +const toHex = (number, length = 32) => '0x' + (number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)).padStart(length * 2, '0') -} - -function hash(dataArray) { - for(let item of dataArray) { - - } -} function merklePathIndicesToBigint(indexArray) { - + let result = 0 + for(let item of indexArray) { + result = (result << 1) + item + } + return result } function fromPrivkey(privkey) { return { privkey, - pubkey: privkey, + pubkey: hasher.hashArray([privkey]), } } @@ -34,7 +42,14 @@ function randomKeypair() { } function createZeroUtxo(keypair) { - return createUtxo(0, rbigint(), keypair.pubkey, keypair.privkey, randomBoolArray(MERKLE_TREE_HEIGHT), randomArray(MERKLE_TREE_HEIGHT)) + return createUtxo( + 0, + rbigint(), + keypair.pubkey, + keypair.privkey, + Array(MERKLE_TREE_HEIGHT).fill(0), + Array(MERKLE_TREE_HEIGHT).fill(0) + ) } function createOutput(amount, pubkey) { @@ -51,9 +66,9 @@ function createInput(amount, blinding, pubkey, privkey, merklePathIndices, merkl /// unsafe function without sanity checks function createUtxo(amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements) { let utxo = { amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements } - utxo.commitment = hash([amount, blinding, pubkey]) + utxo.commitment = hasher.hashArray([amount, blinding, pubkey]) if (privkey) { - utxo.nullifier = hash([commitment, merklePathIndicesToBigint(merklePathIndices), privkey]) + utxo.nullifier = hasher.hashArray([utxo.commitment, merklePathIndicesToBigint(merklePathIndices), privkey]) } return utxo } @@ -62,7 +77,7 @@ function createDeposit(amount, pubkey) { const keypair = randomKeypair() const tx = { inputs: [createZeroUtxo(keypair), createZeroUtxo(keypair)], - outputs: [createOutput(amount, pubkey), createZeroUtxo()], // todo shuffle + outputs: [createOutput(amount, pubkey), createZeroUtxo(keypair)], // todo shuffle } return tx; } @@ -73,51 +88,64 @@ async function buildMerkleTree() { const leaves = events .sort((a, b) => a.returnValues.index - b.returnValues.index) // todo sort by event date .map(e => e.returnValues.commitment) - return new merkleTree(MERKLE_TREE_HEIGHT, leaves) + return new MerkleTree(MERKLE_TREE_HEIGHT, leaves) } -function insertTransaction(tree, tx) { - let path = await tree.insertPair(tx.outputs[0].commitment, tx.outputs[1].commitment) - - tx.outputs[0].merklePath = [...path, 0] - tx.outputs[1].merklePath = [...path, 1] +async function insertOutput(tree, output) { + await tree.insert(output.commitment) + let { path_elements, path_index } = await tree.path(tree.totalElements - 1) + output.merklePathIndices = path_index + output.merklePathElements = path_elements } async function main() { - const amount = 1e6; + web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL, { timeout: 5 * 60 * 1000 }), null, { transactionConfirmationBlocks: 1 }) + circuit = require('../build/circuits/transaction.json') + proving_key = fs.readFileSync('../build/circuits/transaction_proving_key.bin').buffer + groth16 = await buildGroth16() + netId = await web3.eth.net.getId() + const contractData = require('../build/contracts/TornadoPool.json') + contract = new web3.eth.Contract(contractData.abi, contractData.networks[netId].address) + web3.eth.defaultAccount = (await web3.eth.getAccounts())[0] + const amount = 1e6; const tree = await buildMerkleTree() - const oldRoot = tree.root; + const oldRoot = await tree.root() const keypair = randomKeypair() const tx = createDeposit(amount, keypair.pubkey) - insertTransaction(tree, tx) + await insertOutput(tree, tx.outputs[0]) + await insertOutput(tree, tx.outputs[1]) + console.log('Note', tx.outputs[0]) let input = { root: oldRoot, - newRoot: tree.root, + newRoot: await tree.root(), inputNullifier: [tx.inputs[0].nullifier, tx.inputs[1].nullifier], outputCommitment: [tx.outputs[0].commitment, tx.outputs[1].commitment], extAmount: amount, 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: [merklePathIndicesToBigint(tx.inputs[0].merklePathIndices), merklePathIndicesToBigint(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], - outPathIndices: merklePathIndicesToBigint(tx.outputs[0].merklePathIndices.slice(0, -1)), - outPathElements: tx.outputs[0].merklePathElements.slice(0, -1) + outPubkey: [tx.outputs[0].pubkey, tx.outputs[1].pubkey], + outPathIndices: merklePathIndicesToBigint(tx.outputs[0].merklePathIndices.slice(1)), + outPathElements: tx.outputs[0].merklePathElements.slice(1) } + console.log('input', JSON.stringify(stringifyBigInts(input))) + console.log('Generating SNARK proof...') const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const { proof } = websnarkUtils.toSolidityInput(proofData) @@ -125,17 +153,17 @@ async function main() { const args = [ toHex(input.root), toHex(input.newRoot), - [toHex(input.inputs[0].nullifier), toHex(input.inputs[1].nullifier)], - [toHex(input.inputs[0].commitment), toHex(input.inputs[1].commitment)], - toHex(input.amount), + [toHex(tx.inputs[0].nullifier), toHex(tx.inputs[1].nullifier)], + [toHex(tx.outputs[0].commitment), toHex(tx.outputs[1].commitment)], + toHex(amount), toHex(input.fee), toHex(input.recipient, 20), toHex(input.relayer, 20), ] - console.log('Sending withdrawal transaction...') - const receipt = await contract.methods.transaction(proof, ...args).send({ valued: amount, from: web3.eth.defaultAccount, gas: 1e6 }) - console.log(`https://kovan.etherscan.io/tx/${receipt.transactionHash}`) + console.log('Sending deposit transaction...') + const receipt = await contract.methods.transaction(proof, ...args).send({ value: amount, from: web3.eth.defaultAccount, gas: 1e6 }) + console.log(`Receipt ${receipt.transactionHash}`) }