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
- combine privkey hash and ethereum public key = address
* outputs merging
* switch web3 to etherjs
* tests
* wasmsnark
* ERC20?
* design
* outputs merging (second snark with 32 inputs) (poma)
* wasmsnark (poma)
* 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",
"@openzeppelin/contracts": "^3.4.0",
"bignumber.js": "^9.0.0",
"chai": "^4.3.4",
"circom": "^0.5.45",
"circom_runtime": "^0.1.13",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#d20d53411d1bef61f38c99a8b36d5d0cc4836aa1",

View File

@ -1,29 +1,27 @@
/* eslint-disable no-console */
const MerkleTree = require('fixed-merkle-tree')
const Web3 = require('web3')
const { ethers } = require('hardhat')
const { BigNumber } = ethers
const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE } = require('./utils')
const Utxo = require('./utxo')
let contract, web3
const { prove } = require('./prover')
const MERKLE_TREE_HEIGHT = 5
const RPC_URL = 'http://localhost:8545'
async function buildMerkleTree() {
async function buildMerkleTree({ tornadoPool }) {
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
.sort((a, b) => a.returnValues.index - b.returnValues.index) // todo sort by event date
.map((e) => toFixedHex(e.returnValues.commitment))
.sort((a, b) => a.args.index - b.args.index) // todo sort by event date
.map((e) => toFixedHex(e.args.commitment))
// console.log('leaves', leaves)
return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 })
}
async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer }) {
// 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')
}
@ -63,23 +61,23 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
let input = {
root: oldRoot,
newRoot: tree.root(),
inputNullifier: inputs.map(x => x.getNullifier()),
outputCommitment: outputs.map(x => x.getCommitment()),
inputNullifier: inputs.map((x) => x.getNullifier()),
outputCommitment: outputs.map((x) => x.getCommitment()),
extAmount,
fee,
extDataHash,
// data for 2 transaction inputs
inAmount: inputs.map(x => x.amount),
inPrivateKey: inputs.map(x => x.privkey),
inBlinding: inputs.map(x => x.blinding),
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: outputs.map(x => x.amount),
outBlinding: outputs.map(x => x.blinding),
outPubkey: outputs.map(x => x.pubkey),
outAmount: outputs.map((x) => x.amount),
outBlinding: outputs.map((x) => x.blinding),
outPubkey: outputs.map((x) => x.pubkey),
outPathIndices: outputIndex >> 1,
outPathElements: outputPath,
}
@ -92,8 +90,8 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
const args = [
toFixedHex(input.root),
toFixedHex(input.newRoot),
inputs.map(x => toFixedHex(x.getNullifier())),
outputs.map(x => toFixedHex(x.getCommitment())),
inputs.map((x) => toFixedHex(x.getNullifier())),
outputs.map((x) => toFixedHex(x.getCommitment())),
toFixedHex(extAmount),
toFixedHex(fee),
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 inputs = [new Utxo(), new Utxo()]
const outputs = [new Utxo({ amount }), new Utxo()]
@ -115,7 +113,7 @@ async function deposit() {
const { proof, args } = await getProof({
inputs,
outputs,
tree: await buildMerkleTree(),
tree: await buildMerkleTree({ tornadoPool }),
extAmount: amount,
fee: 0,
recipient: 0,
@ -123,24 +121,25 @@ async function deposit() {
})
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}`)
const receipt = await tornadoPool.transaction(proof, ...args, {
value: amount,
gasLimit: 1e6,
})
console.log(`Receipt ${receipt.hash}`)
return outputs[0]
}
async function transact(utxo) {
async function transact({ tornadoPool, utxo }) {
const inputs = [utxo, new Utxo()]
const outputs = [
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({
inputs,
outputs,
tree: await buildMerkleTree(),
tree: await buildMerkleTree({ tornadoPool }),
extAmount: 0,
fee: 0,
recipient: 0,
@ -148,49 +147,28 @@ async function transact(utxo) {
})
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}`)
const receipt = await tornadoPool.transaction(proof, ...args, { gasLimit: 1e6 })
console.log(`Receipt ${receipt.hash}`)
return outputs[0]
}
async function withdraw(utxo) {
async function withdraw({ tornadoPool, utxo, recipient }) {
const inputs = [utxo, new Utxo()]
const outputs = [new Utxo(), new Utxo()]
const { proof, args } = await getProof({
inputs,
outputs,
tree: await buildMerkleTree(),
tree: await buildMerkleTree({ tornadoPool }),
extAmount: FIELD_SIZE.sub(utxo.amount),
fee: 0,
recipient: '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48',
recipient,
relayer: 0,
})
console.log('Sending withdraw transaction...')
const receipt = await contract.methods
.transaction(proof, ...args)
.send({ from: web3.eth.defaultAccount, gas: 1e6 })
console.log(`Receipt ${receipt.transactionHash}`)
let bal = await web3.eth.getBalance('0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48')
console.log('balance', bal)
const receipt = await tornadoPool.transaction(proof, ...args, { gasLimit: 1e6 })
console.log(`Receipt ${receipt.hash}`)
}
async function main() {
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()
module.exports = { deposit, withdraw, transact }

View File

@ -1,7 +1,7 @@
const crypto = require('crypto')
const ethers = require('ethers')
const { ethers } = require('hardhat')
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,17 +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(
['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)
@ -32,8 +34,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) =>
@ -45,6 +47,14 @@ const toBuffer = (value, length) =>
'hex',
)
async function takeSnapshot() {
return await ethers.provider.send('evm_snapshot', [])
}
async function revertSnapshot(id) {
await ethers.provider.send('evm_revert', [id])
}
module.exports = {
FIELD_SIZE,
randomBN,
@ -53,4 +63,6 @@ module.exports = {
poseidonHash,
poseidonHash2,
getExtDataHash,
takeSnapshot,
revertSnapshot,
}

View File

@ -10,19 +10,19 @@ function fromPrivkey(privkey) {
}
class Utxo {
constructor({amount, pubkey, privkey, blinding, index} = {}) {
constructor({ amount, pubkey, privkey, blinding, index } = {}) {
if (!pubkey) {
if (privkey) {
pubkey = fromPrivkey(privkey).pubkey
} else {
({pubkey, privkey} = fromPrivkey(randomBN()))
;({ pubkey, privkey } = fromPrivkey(randomBN()))
}
}
this.amount = BigNumber.from(amount || 0);
this.blinding = blinding || randomBN();
this.pubkey = pubkey;
this.privkey = privkey;
this.index = index;
this.amount = BigNumber.from(amount || 0)
this.blinding = blinding || randomBN()
this.pubkey = pubkey
this.privkey = privkey
this.index = index
}
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:
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"
resolved "git+https://github.com/jbaylina/blake2b-wasm.git#0d5f024b212429c7f50a7f533aa3a2406b5b42b3"
resolved "https://github.com/jbaylina/blake2b-wasm.git#0d5f024b212429c7f50a7f533aa3a2406b5b42b3"
dependencies:
nanoassert "^1.0.0"
@ -2078,7 +2078,7 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
chai@^4.2.0:
chai@^4.2.0, chai@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"
integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==