From a8331eea081f4ca69771201cbb2e94795a8ae1b5 Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 25 Sep 2019 21:29:41 +0300 Subject: [PATCH] withdraw test --- cli.js | 4 +- contracts/ETHMixer.sol | 31 +++--- lib/ganacheHelper.js | 5 + package-lock.json | 40 ++++---- package.json | 2 +- test/GSNsupport.test.js | 207 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 255 insertions(+), 34 deletions(-) create mode 100644 test/GSNsupport.test.js diff --git a/cli.js b/cli.js index a6e4bbc..ad6e4fa 100755 --- a/cli.js +++ b/cli.js @@ -258,13 +258,13 @@ async function withdrawViaRelayer(note, receiver) { verbose: true, } // const provider = new GSNProvider('https://rinkeby.infura.io/v3/c7463beadf2144e68646ff049917b716', { signKey: account }) - const provider = new GSNDevProvider('http://localhost:8545', { signKey: account, ...HARDCODED_RELAYER_OPTS }) + const provider = new GSNDevProvider('http://localhost:8545', { signKey: account, HARDCODED_RELAYER_OPTS }) web3 = new Web3(provider) const netId = await web3.eth.net.getId() // eslint-disable-next-line require-atomic-updates mixer = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address) console.log('mixer address', contractJson.networks[netId].address) - const tx = await mixer.methods.withdrawViaRelayer(pi_a, pi_b, pi_c, publicSignals).send({ from: account.address, gas: '5000000' }) + const tx = await mixer.methods.withdrawViaRelayer(pi_a, pi_b, pi_c, publicSignals).send({ from: account.address, gas: '2000000' }) console.log('tx', tx) console.log('Done') } diff --git a/contracts/ETHMixer.sol b/contracts/ETHMixer.sol index fdb0aca..fdf67c6 100644 --- a/contracts/ETHMixer.sol +++ b/contracts/ETHMixer.sol @@ -36,11 +36,13 @@ contract ETHMixer is Mixer, GSNRecipient { function withdrawViaRelayer(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[3] memory input) public { uint256 root = input[0]; uint256 nullifierHash = input[1]; + address receiver = address(input[2]); 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(a, b, c, input), "Invalid withdraw proof"); nullifierHashes[nullifierHash] = true; + emit Withdraw(receiver, nullifierHash, tx.origin); // we will process withdraw in postRelayedCall func } @@ -49,24 +51,26 @@ contract ETHMixer is Mixer, GSNRecipient { function acceptRelayedCall( address relay, address from, - bytes calldata encodedFunction, + bytes memory encodedFunction, uint256 transactionFee, uint256 gasPrice, uint256 gasLimit, uint256 nonce, - bytes calldata approvalData, + bytes memory approvalData, uint256 maxPossibleCharge - ) external view returns (uint256, bytes memory) { + ) public view returns (uint256, bytes memory) { // think of a withdraw dry-run - if (_computeCharge(gasLimit, gasPrice, transactionFee) * 2 > mixDenomination) { - return (1, "Fee exceeds 50% of transfer value"); - } - if (!compareBytesWithSelector(encodedFunction, this.withdrawViaRelayer.selector)) { - return (2, "Only withdrawViaRelayer can be called"); + return (1, "Only withdrawViaRelayer can be called"); } - - return _approveRelayedCall(); + bytes memory recipient; + assembly { + let dataPointer := add(encodedFunction, 32) + let recipientPointer := mload(add(dataPointer, 324)) // 4 + (8 * 32) + (32) + (32) == selector + proof + root + nullifier + mstore(recipient, 32) // save array length + mstore(add(recipient, 32), recipientPointer) // save recipient address + } + return (0, recipient); } // this func is called by RelayerHub right before calling a target func @@ -78,7 +82,7 @@ contract ETHMixer is Mixer, GSNRecipient { IRelayHub relayHub = IRelayHub(getHubAddr()); address payable recipient; assembly { - recipient := sload(add(context, 324)) // 4 + (8 * 32) + (32) + (32) == selector + proof + root + nullifier + recipient := mload(add(context, 32)) } emit Debug(actualCharge, context, recipient); @@ -98,4 +102,9 @@ contract ETHMixer is Mixer, GSNRecipient { require(msg.sender == operator, "unauthorized"); IRelayHub(getHubAddr()).withdraw(amount, dest); } + + function upgradeRelayHub(address newRelayHub) external { + require(msg.sender == operator, "unauthorized"); + _upgradeRelayHub(newRelayHub); + } } diff --git a/lib/ganacheHelper.js b/lib/ganacheHelper.js index 56680f2..b981d54 100644 --- a/lib/ganacheHelper.js +++ b/lib/ganacheHelper.js @@ -17,6 +17,10 @@ const takeSnapshot = async () => { return await send('evm_snapshot') } +const traceTransaction = async (tx) => { + return await send('debug_traceTransaction', [tx, {}]) +} + const revertSnapshot = async (id) => { await send('evm_revert', [id]) } @@ -44,4 +48,5 @@ module.exports = { minerStop, minerStart, increaseTime, + traceTransaction } diff --git a/package-lock.json b/package-lock.json index f8e9a42..86b70a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -179,9 +179,9 @@ "dev": true }, "minipass": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.8.1.tgz", - "integrity": "sha512-QCG523ParRcE2+9A6wYh9UI3uy2FFLw4DQaVYQrY5HPfszc5M6VDD+j0QCwHm19LI2imes4RB+NBD8cOJccyCg==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.8.6.tgz", + "integrity": "sha512-lFG7d6g3+/UaFDCOtqPiKAC9zngWWsQZl1g5q6gaONqrjq61SX2xFqXMleQiFVyDpYwa018E9hmlAFY22PCb+A==", "dev": true, "requires": { "safe-buffer": "^5.1.2", @@ -322,14 +322,14 @@ } }, "tar": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.11.tgz", - "integrity": "sha512-iI4zh3ktLJKaDNZKZc+fUONiQrSn9HkCFzamtb7k8FFmVilHVob7QsLX/VySAW8lAviMzMbFw4QtFb4errwgYA==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "dev": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.6.4", + "minipass": "^2.8.6", "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", @@ -685,9 +685,9 @@ } }, "@openzeppelin/gsn-provider": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@openzeppelin/gsn-provider/-/gsn-provider-0.1.7.tgz", - "integrity": "sha512-eqpFNOTbXsJpsMWCsdb7qWTDhNH0r6jiez1NAb3CUeX5CzrSLvnDqmoyNCdnqSc+PQTIg6BEwdIWmcVbKOTSPw==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@openzeppelin/gsn-provider/-/gsn-provider-0.1.8.tgz", + "integrity": "sha512-/XRvCMs2MufOKORPeFRaYnp4a1l3szAJ39v5v0NiBHvLIdS8WotcYG9izVaUQllQFQrGCcVkCM+nptutWCr+gg==", "dev": true, "requires": { "abi-decoder": "^2.1.0", @@ -833,9 +833,9 @@ } }, "minipass": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.8.1.tgz", - "integrity": "sha512-QCG523ParRcE2+9A6wYh9UI3uy2FFLw4DQaVYQrY5HPfszc5M6VDD+j0QCwHm19LI2imes4RB+NBD8cOJccyCg==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.8.6.tgz", + "integrity": "sha512-lFG7d6g3+/UaFDCOtqPiKAC9zngWWsQZl1g5q6gaONqrjq61SX2xFqXMleQiFVyDpYwa018E9hmlAFY22PCb+A==", "dev": true, "requires": { "safe-buffer": "^5.1.2", @@ -965,14 +965,14 @@ } }, "tar": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.11.tgz", - "integrity": "sha512-iI4zh3ktLJKaDNZKZc+fUONiQrSn9HkCFzamtb7k8FFmVilHVob7QsLX/VySAW8lAviMzMbFw4QtFb4errwgYA==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "dev": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.6.4", + "minipass": "^2.8.6", "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", @@ -3063,9 +3063,9 @@ } }, "abi-decoder": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/abi-decoder/-/abi-decoder-2.2.0.tgz", - "integrity": "sha512-FVgkAvPRNa08E85Q+t52KlGto8XZeQITmCYdRIWHHth/t/pgdpAzZijy3LKUCBqmJjXnrosj4c6WGOB1q+KJ9w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/abi-decoder/-/abi-decoder-2.2.1.tgz", + "integrity": "sha512-kqVn3TIRPPMHZT+ciHFbkjZBxeTPvJMg2+8b1fhA2aBBTC6ny6orRUgFYuLZrJz/mc7opdhmy/MHzcI9kdPDYw==", "dev": true, "requires": { "web3-eth-abi": "^1.2.1", diff --git a/package.json b/package.json index 6b5a019..71879c9 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@openzeppelin/contracts-ethereum-package": "^2.2.3", "@openzeppelin/gsn-helpers": "^0.2.0", - "@openzeppelin/gsn-provider": "^0.1.7", + "@openzeppelin/gsn-provider": "^0.1.8", "@openzeppelin/network": "^0.2.9", "@openzeppelin/upgrades": "^2.5.3", "truffle-flattener": "^1.4.0" diff --git a/test/GSNsupport.test.js b/test/GSNsupport.test.js new file mode 100644 index 0000000..ad880db --- /dev/null +++ b/test/GSNsupport.test.js @@ -0,0 +1,207 @@ +/* global artifacts, web3, contract */ +require('chai') + .use(require('bn-chai')(web3.utils.BN)) + .use(require('chai-as-promised')) + .should() +const fs = require('fs') +const Web3 = require('web3') + +const { toBN, toHex, toChecksumAddress } = require('web3-utils') +const { takeSnapshot, revertSnapshot, traceTransaction } = require('../lib/ganacheHelper') +const { deployRelayHub, fundRecipient } = require('@openzeppelin/gsn-helpers') +const { GSNDevProvider } = require('@openzeppelin/gsn-provider') +const { ephemeral } = require('@openzeppelin/network') + +const Mixer = artifacts.require('./ETHMixer.sol') +const { ETH_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env + +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 +const crypto = require('crypto') +const circomlib = require('circomlib') +const MerkleTree = require('../lib/MerkleTree') + +const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes)) +const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0] + +function generateDeposit() { + let deposit = { + secret: rbigint(31), + nullifier: rbigint(31), + } + const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)]) + deposit.commitment = pedersenHash(preimage) + return deposit +} + +function getRandomReceiver() { + let receiver = rbigint(20) + while (toHex(receiver.toString()).length !== 42) { + receiver = rbigint(20) + } + return receiver +} + +contract('GSN support', accounts => { + let mixer + let gsnMixer + let relayHubAddress + const sender = accounts[0] + const operator = accounts[0] + const ownerAddress = accounts[8] + const relayerAddress = accounts[9] + const levels = MERKLE_TREE_HEIGHT || 16 + const zeroValue = EMPTY_ELEMENT || 1337 + const value = ETH_AMOUNT || '1000000000000000000' // 1 ether + let snapshotId + let prefix = 'test' + let tree + const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17) + const receiver = getRandomReceiver() + const relayer = accounts[1] + let groth16 + let circuit + let proving_key + + before(async () => { + tree = new MerkleTree( + levels, + zeroValue, + null, + prefix, + ) + mixer = await Mixer.deployed() + relayHubAddress = toChecksumAddress(await deployRelayHub(web3, { + from: sender + })) + await fundRecipient(web3, { recipient: mixer.address, relayHubAddress }) + await mixer.upgradeRelayHub(relayHubAddress) + 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', () => { + it('should initialize', async () => { + const hub = await mixer.getHubAddr() + hub.should.be.equal(relayHubAddress) + }) + }) + + describe('#withdrawViaRelayer', () => { + it.only('should work', async () => { + const gasPrice = toBN('20000000000') + const relayerTxFee = 10 // 20% + const deposit = generateDeposit() + const user = accounts[4] + await tree.insert(deposit.commitment) + + const balanceUserBefore = await web3.eth.getBalance(user) + + // Uncomment to measure gas usage + // let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' }) + // console.log('deposit gas:', gas) + const txDeposit = await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice }) + // console.log('txDeposit', txDeposit.receipt) + const txFee = toBN(txDeposit.receipt.gasUsed).mul(gasPrice) + // console.log('txFee', txFee.toString()) + const balanceUserAfter = await web3.eth.getBalance(user) + balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value).add(txFee))) + + const { root, path_elements, path_index } = await tree.path(0) + + // Circuit input + const input = stringifyBigInts({ + // public + root, + nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), + receiver, + + // private + nullifier: deposit.nullifier, + secret: deposit.secret, + pathElements: path_elements, + pathIndex: path_index, + }) + + + const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) + const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof) + + const balanceMixerBefore = await web3.eth.getBalance(mixer.address) + const balanceHubBefore = await web3.eth.getBalance(relayHubAddress) + const balanceRelayerBefore = await web3.eth.getBalance(relayerAddress) + const balanceRelayerOwnerBefore = await web3.eth.getBalance(ownerAddress) + const balanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString())) + let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) + isSpent.should.be.equal(false) + + const account = ephemeral() + const provider = new GSNDevProvider('http://localhost:8545', { + signKey: account, + ownerAddress, + relayerAddress, + verbose: true, + txFee: relayerTxFee + }) + // console.log('relayerAddress', relayerAddress) + const gsnWeb3 = new Web3(provider, null, { transactionConfirmationBlocks: 1 }) + gsnMixer = new gsnWeb3.eth.Contract(mixer.abi, mixer.address) + const tx = await gsnMixer.methods.withdrawViaRelayer(pi_a, pi_b, pi_c, publicSignals).send({ + from: account.address, + gas: 3e6, + gasPrice, + value: 0 + }) + // console.log('tx', tx) + const debug = await traceTransaction(tx.transactionHash) + console.log('debug', debug.result.structLogs) + const { events, gasUsed } = tx + // console.log('events', events, gasUsed) + const balanceMixerAfter = await web3.eth.getBalance(mixer.address) + const balanceHubAfter = await web3.eth.getBalance(relayHubAddress) + const balanceRelayerAfter = await web3.eth.getBalance(relayerAddress) + const balanceRelayerOwnerAfter = await web3.eth.getBalance(ownerAddress) + const balanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString())) + console.log('balanceMixerBefore, balanceMixerAfter', balanceMixerBefore.toString(), balanceMixerAfter.toString()) + console.log('balanceRecieverBefore, balanceRecieverAfter', balanceRecieverBefore.toString(), balanceRecieverAfter.toString()) + console.log('balanceHubBefore, balanceHubAfter', balanceHubBefore.toString(), balanceHubAfter.toString()) + console.log('balanceRelayerBefore, balanceRelayerAfter', balanceRelayerBefore.toString(), balanceRelayerAfter.toString(), toBN(balanceRelayerBefore).sub(toBN(balanceRelayerAfter)).toString()) + console.log('balanceRelayerOwnerBefore, balanceRelayerOwnerAfter', balanceRelayerOwnerBefore.toString(), balanceRelayerOwnerAfter.toString()) + // balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value))) + const networkFee = toBN(gasUsed).mul(gasPrice) + const chargedFee = networkFee.add(networkFee.div(toBN(relayerTxFee))) + console.log('networkFee, calc chargedFee', networkFee.toString(), chargedFee.toString()) + // const fee = toBN(value).sub(toBN(balanceRecieverAfter)) + // console.log('actual charged fee', fee.toString()) + balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).sub(networkFee)) + // balanceRelayerOwnerAfter.should.be.eq.BN(toBN(balanceRelayerOwnerBefore)) + // balanceRecieverAfter.should.be.gt.BN(toBN(balanceRecieverBefore)) + // balanceHubAfter.should.be.eq.BN(toBN(balanceHubBefore).add(fee)) + + // console.log('events.Withdraw.returnValues.nullifierHash', events.Withdraw.returnValues.nullifierHash.toString(), input.nullifierHash.toString()) + // events.Withdraw.returnValues.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString())) + events.Withdraw.returnValues.relayer.should.be.eq.BN(relayerAddress) + events.Withdraw.returnValues.to.should.be.eq.BN(toHex(receiver.toString())) + + isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000')) + isSpent.should.be.equal(true) + }) + }) + + afterEach(async () => { + await revertSnapshot(snapshotId.result) + // eslint-disable-next-line require-atomic-updates + snapshotId = await takeSnapshot() + tree = new MerkleTree( + levels, + zeroValue, + null, + prefix, + ) + }) +})