ethers; tests

This commit is contained in:
Alexey 2021-06-09 13:56:33 +03:00
parent 01c4930dcd
commit cd18bef60b
7 changed files with 116 additions and 82 deletions

14
TODO
View File

@ -1,11 +1,9 @@
* shuffle outputs and inputs
* utxo data encryption for recipient as for mining * utxo data encryption for recipient as for mining
- combine privkey hash and ethereum public key = address - combine privkey hash and ethereum public key = address
* outputs merging * outputs merging (second snark with 32 inputs) (poma)
* switch web3 to etherjs * wasmsnark (poma)
* tests
* wasmsnark
* ERC20?
* design
* relayer * relayer
* race condition ?
* design
* race condition ? (sequencer or something)
* the current trusted setup is not secure

View File

@ -21,6 +21,7 @@
"@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1",
"@openzeppelin/contracts": "^3.4.0", "@openzeppelin/contracts": "^3.4.0",
"bignumber.js": "^9.0.0", "bignumber.js": "^9.0.0",
"chai": "^4.3.4",
"circom": "^0.5.45", "circom": "^0.5.45",
"circom_runtime": "^0.1.13", "circom_runtime": "^0.1.13",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#d20d53411d1bef61f38c99a8b36d5d0cc4836aa1", "circomlib": "git+https://github.com/tornadocash/circomlib.git#d20d53411d1bef61f38c99a8b36d5d0cc4836aa1",

View File

@ -1,29 +1,27 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
const MerkleTree = require('fixed-merkle-tree') const MerkleTree = require('fixed-merkle-tree')
const Web3 = require('web3')
const { ethers } = require('hardhat') const { ethers } = require('hardhat')
const { BigNumber } = ethers
const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE } = require('./utils') const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE } = require('./utils')
const Utxo = require('./utxo') const Utxo = require('./utxo')
let contract, web3
const { prove } = require('./prover') const { prove } = require('./prover')
const MERKLE_TREE_HEIGHT = 5 const MERKLE_TREE_HEIGHT = 5
const RPC_URL = 'http://localhost:8545'
async function buildMerkleTree() { async function buildMerkleTree({ tornadoPool }) {
console.log('Getting contract state...') console.log('Getting contract state...')
const events = await contract.getPastEvents('NewCommitment', { fromBlock: 0, toBlock: 'latest' }) const filter = tornadoPool.filters.NewCommitment()
const events = await tornadoPool.queryFilter(filter, 0)
const leaves = events const leaves = events
.sort((a, b) => a.returnValues.index - b.returnValues.index) // todo sort by event date .sort((a, b) => a.args.index - b.args.index) // todo sort by event date
.map((e) => toFixedHex(e.returnValues.commitment)) .map((e) => toFixedHex(e.args.commitment))
// console.log('leaves', leaves) // console.log('leaves', leaves)
return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 }) return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 })
} }
async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer }) { async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer }) {
// todo shuffle inputs and outputs // todo shuffle inputs and outputs
if (inputs.length !== 2 || outputs.length !== 2 ) { if (inputs.length !== 2 || outputs.length !== 2) {
throw new Error('Unsupported number of inputs/outputs') throw new Error('Unsupported number of inputs/outputs')
} }
@ -63,23 +61,23 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
let input = { let input = {
root: oldRoot, root: oldRoot,
newRoot: tree.root(), newRoot: tree.root(),
inputNullifier: inputs.map(x => x.getNullifier()), inputNullifier: inputs.map((x) => x.getNullifier()),
outputCommitment: outputs.map(x => x.getCommitment()), outputCommitment: outputs.map((x) => x.getCommitment()),
extAmount, extAmount,
fee, fee,
extDataHash, extDataHash,
// data for 2 transaction inputs // data for 2 transaction inputs
inAmount: inputs.map(x => x.amount), inAmount: inputs.map((x) => x.amount),
inPrivateKey: inputs.map(x => x.privkey), inPrivateKey: inputs.map((x) => x.privkey),
inBlinding: inputs.map(x => x.blinding), inBlinding: inputs.map((x) => x.blinding),
inPathIndices: inputMerklePathIndices, inPathIndices: inputMerklePathIndices,
inPathElements: inputMerklePathElements, inPathElements: inputMerklePathElements,
// data for 2 transaction outputs // data for 2 transaction outputs
outAmount: outputs.map(x => x.amount), outAmount: outputs.map((x) => x.amount),
outBlinding: outputs.map(x => x.blinding), outBlinding: outputs.map((x) => x.blinding),
outPubkey: outputs.map(x => x.pubkey), outPubkey: outputs.map((x) => x.pubkey),
outPathIndices: outputIndex >> 1, outPathIndices: outputIndex >> 1,
outPathElements: outputPath, outPathElements: outputPath,
} }
@ -92,8 +90,8 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
const args = [ const args = [
toFixedHex(input.root), toFixedHex(input.root),
toFixedHex(input.newRoot), toFixedHex(input.newRoot),
inputs.map(x => toFixedHex(x.getNullifier())), inputs.map((x) => toFixedHex(x.getNullifier())),
outputs.map(x => toFixedHex(x.getCommitment())), outputs.map((x) => toFixedHex(x.getCommitment())),
toFixedHex(extAmount), toFixedHex(extAmount),
toFixedHex(fee), toFixedHex(fee),
extData, extData,
@ -107,7 +105,7 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
} }
} }
async function deposit() { async function deposit({ tornadoPool }) {
const amount = 1e6 const amount = 1e6
const inputs = [new Utxo(), new Utxo()] const inputs = [new Utxo(), new Utxo()]
const outputs = [new Utxo({ amount }), new Utxo()] const outputs = [new Utxo({ amount }), new Utxo()]
@ -115,7 +113,7 @@ async function deposit() {
const { proof, args } = await getProof({ const { proof, args } = await getProof({
inputs, inputs,
outputs, outputs,
tree: await buildMerkleTree(), tree: await buildMerkleTree({ tornadoPool }),
extAmount: amount, extAmount: amount,
fee: 0, fee: 0,
recipient: 0, recipient: 0,
@ -123,24 +121,25 @@ async function deposit() {
}) })
console.log('Sending deposit transaction...') console.log('Sending deposit transaction...')
const receipt = await contract.methods const receipt = await tornadoPool.transaction(proof, ...args, {
.transaction(proof, ...args) value: amount,
.send({ value: amount, from: web3.eth.defaultAccount, gas: 1e6 }) gasLimit: 1e6,
console.log(`Receipt ${receipt.transactionHash}`) })
console.log(`Receipt ${receipt.hash}`)
return outputs[0] return outputs[0]
} }
async function transact(utxo) { async function transact({ tornadoPool, utxo }) {
const inputs = [utxo, new Utxo()] const inputs = [utxo, new Utxo()]
const outputs = [ const outputs = [
new Utxo({ amount: utxo.amount / 4 }), new Utxo({ amount: utxo.amount / 4 }),
new Utxo({ amount: utxo.amount * 3 / 4, privkey: utxo.privkey}), new Utxo({ amount: (utxo.amount * 3) / 4, privkey: utxo.privkey }),
] ]
const { proof, args } = await getProof({ const { proof, args } = await getProof({
inputs, inputs,
outputs, outputs,
tree: await buildMerkleTree(), tree: await buildMerkleTree({ tornadoPool }),
extAmount: 0, extAmount: 0,
fee: 0, fee: 0,
recipient: 0, recipient: 0,
@ -148,49 +147,28 @@ async function transact(utxo) {
}) })
console.log('Sending transfer transaction...') console.log('Sending transfer transaction...')
const receipt = await contract.methods const receipt = await tornadoPool.transaction(proof, ...args, { gasLimit: 1e6 })
.transaction(proof, ...args) console.log(`Receipt ${receipt.hash}`)
.send({ from: web3.eth.defaultAccount, gas: 1e6 })
console.log(`Receipt ${receipt.transactionHash}`)
return outputs[0] return outputs[0]
} }
async function withdraw(utxo) { async function withdraw({ tornadoPool, utxo, recipient }) {
const inputs = [utxo, new Utxo()] const inputs = [utxo, new Utxo()]
const outputs = [new Utxo(), new Utxo()] const outputs = [new Utxo(), new Utxo()]
const { proof, args } = await getProof({ const { proof, args } = await getProof({
inputs, inputs,
outputs, outputs,
tree: await buildMerkleTree(), tree: await buildMerkleTree({ tornadoPool }),
extAmount: FIELD_SIZE.sub(utxo.amount), extAmount: FIELD_SIZE.sub(utxo.amount),
fee: 0, fee: 0,
recipient: '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48', recipient,
relayer: 0, relayer: 0,
}) })
console.log('Sending withdraw transaction...') console.log('Sending withdraw transaction...')
const receipt = await contract.methods const receipt = await tornadoPool.transaction(proof, ...args, { gasLimit: 1e6 })
.transaction(proof, ...args) console.log(`Receipt ${receipt.hash}`)
.send({ from: web3.eth.defaultAccount, gas: 1e6 })
console.log(`Receipt ${receipt.transactionHash}`)
let bal = await web3.eth.getBalance('0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48')
console.log('balance', bal)
} }
async function main() { module.exports = { deposit, withdraw, transact }
web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL, { timeout: 5 * 60 * 1000 }), null, {
transactionConfirmationBlocks: 1,
})
netId = await web3.eth.net.getId()
const contractData = require('../artifacts/contracts/TornadoPool.sol/TornadoPool.json')
contract = new web3.eth.Contract(contractData.abi, '0x0E801D84Fa97b50751Dbf25036d067dCf18858bF')
web3.eth.defaultAccount = (await web3.eth.getAccounts())[0]
const utxo1 = await deposit()
const utxo2 = await transact(utxo1)
await withdraw(utxo2)
}
main()

View File

@ -1,7 +1,7 @@
const crypto = require('crypto') const crypto = require('crypto')
const ethers = require('ethers') const { ethers } = require('hardhat')
const BigNumber = ethers.BigNumber const BigNumber = ethers.BigNumber
const {poseidon} = require('circomlib') const { poseidon } = require('circomlib')
const poseidonHash = (items) => BigNumber.from(poseidon(items).toString()) const poseidonHash = (items) => BigNumber.from(poseidon(items).toString())
const poseidonHash2 = (a, b) => poseidonHash([a, b]) const poseidonHash2 = (a, b) => poseidonHash([a, b])
@ -12,17 +12,19 @@ const FIELD_SIZE = BigNumber.from(
/** Generate random number of specified byte length */ /** Generate random number of specified byte length */
const randomBN = (nbytes = 31) => BigNumber.from(crypto.randomBytes(nbytes)) 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 abi = new ethers.utils.AbiCoder()
const encodedData = abi.encode( const encodedData = abi.encode(
['tuple(address recipient,address relayer,bytes encryptedOutput1,bytes encryptedOutput2)'], ['tuple(address recipient,address relayer,bytes encryptedOutput1,bytes encryptedOutput2)'],
[{ [
{
recipient: toFixedHex(recipient, 20), recipient: toFixedHex(recipient, 20),
relayer: toFixedHex(relayer, 20), relayer: toFixedHex(relayer, 20),
encryptedOutput1: encryptedOutput1, encryptedOutput1: encryptedOutput1,
encryptedOutput2: encryptedOutput2, encryptedOutput2: encryptedOutput2,
}], },
],
) )
const hash = ethers.utils.keccak256(encodedData) const hash = ethers.utils.keccak256(encodedData)
return BigNumber.from(hash).mod(FIELD_SIZE) return BigNumber.from(hash).mod(FIELD_SIZE)
@ -32,8 +34,8 @@ function getExtDataHash({recipient, relayer, encryptedOutput1, encryptedOutput2}
const toFixedHex = (number, length = 32) => const toFixedHex = (number, length = 32) =>
'0x' + '0x' +
(number instanceof Buffer (number instanceof Buffer
? number.toString('hex') ? number.toString('hex')
: BigNumber.from(number).toHexString().slice(2) : BigNumber.from(number).toHexString().slice(2)
).padStart(length * 2, '0') ).padStart(length * 2, '0')
const toBuffer = (value, length) => const toBuffer = (value, length) =>
@ -45,6 +47,14 @@ const toBuffer = (value, length) =>
'hex', 'hex',
) )
async function takeSnapshot() {
return await ethers.provider.send('evm_snapshot', [])
}
async function revertSnapshot(id) {
await ethers.provider.send('evm_revert', [id])
}
module.exports = { module.exports = {
FIELD_SIZE, FIELD_SIZE,
randomBN, randomBN,
@ -53,4 +63,6 @@ module.exports = {
poseidonHash, poseidonHash,
poseidonHash2, poseidonHash2,
getExtDataHash, getExtDataHash,
takeSnapshot,
revertSnapshot,
} }

View File

@ -10,19 +10,19 @@ function fromPrivkey(privkey) {
} }
class Utxo { class Utxo {
constructor({amount, pubkey, privkey, blinding, index} = {}) { constructor({ amount, pubkey, privkey, blinding, index } = {}) {
if (!pubkey) { if (!pubkey) {
if (privkey) { if (privkey) {
pubkey = fromPrivkey(privkey).pubkey pubkey = fromPrivkey(privkey).pubkey
} else { } else {
({pubkey, privkey} = fromPrivkey(randomBN())) ;({ pubkey, privkey } = fromPrivkey(randomBN()))
} }
} }
this.amount = BigNumber.from(amount || 0); this.amount = BigNumber.from(amount || 0)
this.blinding = blinding || randomBN(); this.blinding = blinding || randomBN()
this.pubkey = pubkey; this.pubkey = pubkey
this.privkey = privkey; this.privkey = privkey
this.index = index; this.index = index
} }
getCommitment() { getCommitment() {

45
test/full.test.js Normal file
View File

@ -0,0 +1,45 @@
/* global ethers */
const { expect, should } = require('chai')
should()
const { poseidonHash2, toFixedHex, takeSnapshot, revertSnapshot } = require('../src/utils')
const MERKLE_TREE_HEIGHT = 5
const MerkleTree = require('fixed-merkle-tree')
const { deposit, transact, withdraw } = require('../src/index')
describe('TornadoPool', () => {
let snapshotId, tornadoPool
/* prettier-ignore */
before(async function () {
const Verifier = await ethers.getContractFactory('Verifier')
const verifier = await Verifier.deploy()
await verifier.deployed()
const tree = new MerkleTree(MERKLE_TREE_HEIGHT, [], { hashFunction: poseidonHash2 })
const root = await tree.root()
const Pool = await ethers.getContractFactory('TornadoPool')
tornadoPool = await Pool.deploy(verifier.address, toFixedHex(root))
snapshotId = await takeSnapshot()
})
it('should deposit, transact and withdraw', async function () {
const utxo1 = await deposit({ tornadoPool })
const utxo2 = await transact({ tornadoPool, utxo: utxo1 })
const recipient = '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48'
await withdraw({ tornadoPool, utxo: utxo2, recipient })
let bal = await ethers.provider.getBalance(recipient)
expect(bal).to.be.gt(0)
})
afterEach(async () => {
await revertSnapshot(snapshotId)
snapshotId = await takeSnapshot()
})
})

View File

@ -1773,9 +1773,9 @@ blake2b-wasm@^1.1.0:
dependencies: dependencies:
nanoassert "^1.0.0" nanoassert "^1.0.0"
"blake2b-wasm@git+https://github.com/jbaylina/blake2b-wasm.git": "blake2b-wasm@https://github.com/jbaylina/blake2b-wasm.git":
version "2.1.0" version "2.1.0"
resolved "git+https://github.com/jbaylina/blake2b-wasm.git#0d5f024b212429c7f50a7f533aa3a2406b5b42b3" resolved "https://github.com/jbaylina/blake2b-wasm.git#0d5f024b212429c7f50a7f533aa3a2406b5b42b3"
dependencies: dependencies:
nanoassert "^1.0.0" nanoassert "^1.0.0"
@ -2078,7 +2078,7 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
chai@^4.2.0: chai@^4.2.0, chai@^4.3.4:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"
integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA== integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==