From 74913e67b2988bdeb4fbe566e1d889410a797a13 Mon Sep 17 00:00:00 2001 From: poma Date: Mon, 4 Nov 2019 22:42:41 +0300 Subject: [PATCH] typed withdraw inputs --- cli.js | 31 +++++++-- contracts/Mixer.sol | 25 +++---- package.json | 4 +- test/ERC20Mixer.test.js | 57 +++++++++++++--- test/ETHMixer.test.js | 140 +++++++++++++++++++++++++++++++--------- 5 files changed, 195 insertions(+), 62 deletions(-) diff --git a/cli.js b/cli.js index 61aae9a..793072f 100755 --- a/cli.js +++ b/cli.js @@ -113,11 +113,19 @@ async function withdrawErc20(note, receiver, relayer) { console.log('Generating SNARK proof') console.time('Proof time') const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) + const { proof } = websnarkUtils.toSolidityInput(proofData) console.timeEnd('Proof time') console.log('Submitting withdraw transaction') - await erc20mixer.methods.withdraw(proof, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 }) + const args = [ + toHex(input.root), + toHex(input.nullifierHash), + toHex(input.receiver, 20), + toHex(input.relayer, 20), + toHex(input.fee), + toHex(input.refund) + ] + await erc20mixer.methods.withdraw(proof, ...args).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 }) console.log('Done') } @@ -138,6 +146,13 @@ async function getBalanceErc20(receiver, relayer) { console.log('Relayer token Balance is ', web3.utils.fromWei(tokenBalanceRelayer.toString())) } +function toHex(number, length = 32) { + let str = bigInt(number).toString(16) + while (str.length < length * 2) str = '0' + str + str = '0x' + str + return str +} + async function withdraw(note, receiver) { // Decode hex string and restore the deposit object let buf = Buffer.from(note.slice(2), 'hex') @@ -188,11 +203,19 @@ async function withdraw(note, receiver) { console.log('Generating SNARK proof') console.time('Proof time') const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) + const { proof } = websnarkUtils.toSolidityInput(proofData) console.timeEnd('Proof time') console.log('Submitting withdraw transaction') - await mixer.methods.withdraw(proof, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 }) + const args = [ + toHex(input.root), + toHex(input.nullifierHash), + toHex(input.receiver, 20), + toHex(input.relayer, 20), + toHex(input.fee), + toHex(input.refund) + ] + await mixer.methods.withdraw(proof, ...args).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 }) console.log('Done') } diff --git a/contracts/Mixer.sol b/contracts/Mixer.sol index 4fa301f..c6e3f2a 100644 --- a/contracts/Mixer.sol +++ b/contracts/Mixer.sol @@ -14,7 +14,7 @@ pragma solidity ^0.5.8; import "./MerkleTreeWithHistory.sol"; contract IVerifier { - function verifyProof(uint256[8] memory _proof, uint256[6] memory _input) public returns(bool); + function verifyProof(bytes memory _proof, uint256[6] memory _input) public returns(bool); } contract Mixer is MerkleTreeWithHistory { @@ -64,6 +64,7 @@ contract Mixer is MerkleTreeWithHistory { function deposit(uint256 _commitment) public payable { require(!isDepositsDisabled, "deposits are disabled"); require(!commitments[_commitment], "The commitment has been submitted"); + uint32 insertedIndex = _insert(_commitment); commitments[_commitment] = true; _processDeposit(); @@ -82,21 +83,15 @@ contract Mixer is MerkleTreeWithHistory { - the receiver of funds - optional fee that goes to the transaction sender (usually a relay) */ - function withdraw(uint256[8] memory _proof, uint256[6] memory _input) public payable { - uint256 root = _input[0]; - uint256 nullifierHash = _input[1]; - address payable receiver = address(_input[2]); - address payable relayer = address(_input[3]); - uint256 fee = _input[4]; - uint256 refund = _input[5]; - require(fee <= denomination, "Fee exceeds transfer value"); - require(!nullifierHashes[nullifierHash], "The note has been already spent"); + function withdraw(bytes memory _proof, uint256 _root, uint256 _nullifierHash, address payable _receiver, address payable _relayer, uint256 _fee, uint256 _refund) public payable { + require(_fee <= denomination, "Fee exceeds transfer value"); + require(!nullifierHashes[_nullifierHash], "The note has been already spent"); + require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one + require(verifier.verifyProof(_proof, [_root, _nullifierHash, uint256(_receiver), uint256(_relayer), _fee, _refund]), "Invalid withdraw proof"); - require(isKnownRoot(root), "Cannot find your merkle root"); // Make sure to use a recent one - require(verifier.verifyProof(_proof, _input), "Invalid withdraw proof"); - nullifierHashes[nullifierHash] = true; - _processWithdraw(receiver, relayer, fee, refund); - emit Withdrawal(receiver, nullifierHash, relayer, fee); + nullifierHashes[_nullifierHash] = true; + _processWithdraw(_receiver, _relayer, _fee, _refund); + emit Withdrawal(_receiver, _nullifierHash, _relayer, _fee); } /** @dev this function is defined in a child contract */ diff --git a/package.json b/package.json index 609988b..959edb5 100644 --- a/package.json +++ b/package.json @@ -36,14 +36,14 @@ "dotenv": "^8.0.0", "eslint": "^6.2.2", "ganache-cli": "^6.4.5", - "snarkjs": "git+https://github.com/peppersec/snarkjs.git#0e2f8ab28092ee6d922dc4d3ac7afc8ef5a25154", + "snarkjs": "git+https://github.com/peppersec/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5", "truffle": "^5.0.27", "truffle-artifactor": "^4.0.23", "truffle-contract": "^4.0.24", "truffle-hdwallet-provider": "^1.0.14", "web3": "^1.0.0-beta.55", "web3-utils": "^1.0.0-beta.55", - "websnark": "git+https://github.com/peppersec/websnark.git#966eafc47df639195c98374d3c366c32acd6f231" + "websnark": "git+https://github.com/peppersec/websnark.git#c254b5962287b788081be1047fa0041c2885b39f" }, "devDependencies": { "truffle-flattener": "^1.4.0" diff --git a/test/ERC20Mixer.test.js b/test/ERC20Mixer.test.js index 5d8af64..8681682 100644 --- a/test/ERC20Mixer.test.js +++ b/test/ERC20Mixer.test.js @@ -43,6 +43,13 @@ function getRandomReceiver() { return receiver } +function toFixedHex(number, length = 32) { + let str = bigInt(number).toString(16) + while (str.length < length * 2) str = '0' + str + str = '0x' + str + return str +} + contract('ERC20Mixer', accounts => { let mixer let token @@ -147,7 +154,7 @@ contract('ERC20Mixer', accounts => { const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) + const { proof } = websnarkUtils.toSolidityInput(proofData) const balanceMixerBefore = await token.balanceOf(mixer.address) const balanceRelayerBefore = await token.balanceOf(relayer) @@ -161,7 +168,15 @@ contract('ERC20Mixer', accounts => { // Uncomment to measure gas usage // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // console.log('withdraw gas:', gas) - const { logs } = await mixer.withdraw(proof, publicSignals, { value: refund, from: relayer, gasPrice: '0' }) + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + const { logs } = await mixer.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' }) const balanceMixerAfter = await token.balanceOf(mixer.address) const balanceRelayerAfter = await token.balanceOf(relayer) @@ -215,13 +230,21 @@ contract('ERC20Mixer', accounts => { const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) + const { proof } = websnarkUtils.toSolidityInput(proofData) - let { reason } = await mixer.withdraw(proof, publicSignals, { value: 1, from: relayer, gasPrice: '0' }).should.be.rejected + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + let { reason } = await mixer.withdraw(proof, ...args, { value: 1, from: relayer, gasPrice: '0' }).should.be.rejected reason.should.be.equal('Incorrect refund amount received by the contract') - ;({ reason } = await mixer.withdraw(proof, publicSignals, { value: toBN(refund).mul(toBN(2)), from: relayer, gasPrice: '0' }).should.be.rejected) + ;({ reason } = await mixer.withdraw(proof, ...args, { value: toBN(refund).mul(toBN(2)), from: relayer, gasPrice: '0' }).should.be.rejected) reason.should.be.equal('Incorrect refund amount received by the contract') }) @@ -274,7 +297,7 @@ contract('ERC20Mixer', accounts => { const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) + const { proof } = websnarkUtils.toSolidityInput(proofData) const balanceMixerBefore = await usdtToken.balanceOf(mixer.address) const balanceRelayerBefore = await usdtToken.balanceOf(relayer) @@ -287,7 +310,15 @@ contract('ERC20Mixer', accounts => { // Uncomment to measure gas usage // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // console.log('withdraw gas:', gas) - const { logs } = await mixer.withdraw(proof, publicSignals, { value: refund, from: relayer, gasPrice: '0' }) + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + const { logs } = await mixer.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' }) const balanceMixerAfter = await usdtToken.balanceOf(mixer.address) const balanceRelayerAfter = await usdtToken.balanceOf(relayer) @@ -355,7 +386,7 @@ contract('ERC20Mixer', accounts => { const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) + const { proof } = websnarkUtils.toSolidityInput(proofData) const balanceMixerBefore = await token.balanceOf(mixer.address) const balanceRelayerBefore = await token.balanceOf(relayer) @@ -368,7 +399,15 @@ contract('ERC20Mixer', accounts => { // Uncomment to measure gas usage // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // console.log('withdraw gas:', gas) - const { logs } = await mixer.withdraw(proof, publicSignals, { value: refund, from: relayer, gasPrice: '0' }) + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + const { logs } = await mixer.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' }) console.log('withdraw done') const balanceMixerAfter = await token.balanceOf(mixer.address) diff --git a/test/ETHMixer.test.js b/test/ETHMixer.test.js index 8582f35..bd897e5 100644 --- a/test/ETHMixer.test.js +++ b/test/ETHMixer.test.js @@ -57,6 +57,13 @@ function snarkVerify(proof) { return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals) } +function toFixedHex(number, length = 32) { + let str = bigInt(number).toString(16) + while (str.length < length * 2) str = '0' + str + str = '0x' + str + return str +} + contract('ETHMixer', accounts => { let mixer const sender = accounts[0] @@ -215,7 +222,7 @@ contract('ETHMixer', accounts => { const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) + const { proof } = websnarkUtils.toSolidityInput(proofData) const balanceMixerBefore = await web3.eth.getBalance(mixer.address) const balanceRelayerBefore = await web3.eth.getBalance(relayer) @@ -227,7 +234,15 @@ contract('ETHMixer', accounts => { // Uncomment to measure gas usage // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // console.log('withdraw gas:', gas) - const { logs } = await mixer.withdraw(proof, publicSignals, { from: relayer, gasPrice: '0' }) + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + const { logs } = await mixer.withdraw(proof, ...args, { from: relayer, gasPrice: '0' }) const balanceMixerAfter = await web3.eth.getBalance(mixer.address) const balanceRelayerAfter = await web3.eth.getBalance(relayer) @@ -268,9 +283,17 @@ contract('ETHMixer', accounts => { pathIndices: path_index, }) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) - await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.fulfilled - const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected + const { proof } = websnarkUtils.toSolidityInput(proofData) + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + await mixer.withdraw(proof, ...args, { from: relayer }).should.be.fulfilled + const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected error.reason.should.be.equal('The note has been already spent') }) @@ -294,9 +317,16 @@ contract('ETHMixer', accounts => { pathIndices: path_index, }) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) - publicSignals[1] ='0x' + toBN(publicSignals[1]).add(toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617')).toString('hex') - const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected + const { proof } = websnarkUtils.toSolidityInput(proofData) + const args = [ + toFixedHex(input.root), + toFixedHex(toBN(input.nullifierHash).add(toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617'))), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected error.reason.should.be.equal('verifier-gte-snark-scalar-field') }) @@ -321,8 +351,16 @@ contract('ETHMixer', accounts => { }) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) - const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected + const { proof } = websnarkUtils.toSolidityInput(proofData) + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected error.reason.should.be.equal('Fee exceeds transfer value') }) @@ -346,12 +384,18 @@ contract('ETHMixer', accounts => { pathIndices: path_index, }) - const dummyRoot = randomHex(32) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) - publicSignals[0] = dummyRoot + const { proof } = websnarkUtils.toSolidityInput(proofData) - const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected + const args = [ + toFixedHex(randomHex(32)), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected error.reason.should.be.equal('Cannot find your merkle root') }) @@ -375,36 +419,60 @@ contract('ETHMixer', accounts => { pathIndices: path_index, }) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - let { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) - const originalPublicSignals = publicSignals.slice() + let { proof } = websnarkUtils.toSolidityInput(proofData) + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + let incorrectArgs const originalProof = proof.slice() // receiver - publicSignals[2] = '0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337' - - let error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected + incorrectArgs = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex('0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337', 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + let error = await mixer.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected error.reason.should.be.equal('Invalid withdraw proof') // fee - publicSignals = originalPublicSignals.slice() - publicSignals[3] = '0x000000000000000000000000000000000000000000000000015345785d8a0000' - - error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected + incorrectArgs = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex('0x000000000000000000000000000000000000000000000000015345785d8a0000'), + toFixedHex(input.refund) + ] + error = await mixer.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected error.reason.should.be.equal('Invalid withdraw proof') // nullifier - publicSignals = originalPublicSignals.slice() - publicSignals[1] = '0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad' - - error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected + incorrectArgs = [ + toFixedHex(input.root), + toFixedHex('0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + error = await mixer.withdraw(proof, ...incorrectArgs, { from: relayer }).should.be.rejected error.reason.should.be.equal('Invalid withdraw proof') // proof itself - proof[0] = '0x261d81d8203437f29b38a88c4263476d858e6d9645cf21740461684412b31337' - await mixer.withdraw(proof, originalPublicSignals, { from: relayer }).should.be.rejected + proof = '0xbeef' + proof.substr(6) + await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected // should work with original values - await mixer.withdraw(originalProof, originalPublicSignals, { from: relayer }).should.be.fulfilled + await mixer.withdraw(originalProof, ...args, { from: relayer }).should.be.fulfilled }) it('should reject with non zero refund', async () => { @@ -428,9 +496,17 @@ contract('ETHMixer', accounts => { }) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) - const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData) + const { proof } = websnarkUtils.toSolidityInput(proofData) - const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected + const args = [ + toFixedHex(input.root), + toFixedHex(input.nullifierHash), + toFixedHex(input.receiver, 20), + toFixedHex(input.relayer, 20), + toFixedHex(input.fee), + toFixedHex(input.refund) + ] + const error = await mixer.withdraw(proof, ...args, { from: relayer }).should.be.rejected error.reason.should.be.equal('Refund value is supposed to be zero for ETH mixer') }) })