Merge pull request #55 from tornadocash/phase2

CLI and Phase2 updates
This commit is contained in:
Roman Storm 2020-06-09 12:39:27 -07:00 committed by GitHub
commit a0ef1a526d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 187 additions and 35 deletions

View File

@ -15,7 +15,7 @@ You can read more about it in [this medium article](https://medium.com/@tornado.
- Circuit Proof time = 10213ms (1071 + 347 * tree_depth)
- Serverless
![image](diagram.png)
![image](docs/diagram.png)
## Whitepaper
**[https://tornado.cash/Tornado.cash_whitepaper_v1.4.pdf](https://tornado.cash/Tornado.cash_whitepaper_v1.4.pdf)**
@ -95,7 +95,6 @@ Example:
> Transaction mined in block 17036120
> Done
## Deploy ETH Tornado Cash
1. `cp .env.example .env`
1. Tune all necessary params
@ -109,6 +108,15 @@ Example:
**Note**. If you want to reuse the same verifier for all the instances, then after you deployed one of the instances you should only run 4th or 5th migration for ETH or ERC20 contracts respectively (`--f 4 --to 4` or `--f 5`).
## How to resolve ENS name to DNS name for a relayer
1. Visit https://etherscan.io/enslookup and put relayer ENS name to the form.
2. Copy the namehash (1) and click on the `Resolver` link (2)
![enslookup](docs/enslookup.png)
3. Go to `Contract` tab. Click on `Read Contract` and scrolldown to the `5. text` method.
4. Put the values:
![resolver](docs/resolver.png)
5. Click `Query` and you will get the DNS name. Just add `https://` to it and use it as `relayer url`
## Credits
Special thanks to @barryWhiteHat and @kobigurk for valuable input,
@ -117,8 +125,18 @@ and to @jbaylina for awesome [Circom](https://github.com/iden3/circom) & [Websna
## Minimal demo example
1. `npm i`
1. `ganache-cli -d`
1. `npm run downloadKeys`
1. `npm build:contract`
1. `npm run download`
1. `npm run build:contract`
1. `cp .env.example .env`
1. `npm run migrate:dev`
1. `node minimal-demo.js`
## Emulate MPC trusted setup ceremony
```bash
cargo install zkutil
npx circom circuits/withdraw.circom -o build/circuits/withdraw.json
zkutil setup -c build/circuits/withdraw.json -p build/circuits/withdraw.params
zkutil export-keys -c build/circuits/withdraw.json -p build/circuits/withdraw.params -r build/circuits/withdraw_proving_key.json -v build/circuits/withdraw_verification_key.json
zkutil generate-verifier -p build/circuits/withdraw.params -v build/circuits/Verifier.sol
sed -i -e 's/pragma solidity \^0.6.0/pragma solidity 0.5.17/g' ./build/circuits/Verifier.sol
```

196
cli.js
View File

@ -24,7 +24,6 @@ let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, PRIVATE_KEY
/** Whether we are in a browser or node.js */
const inBrowser = (typeof window !== 'undefined')
let isLocalRPC = false
const networks = { '1': 'mainnet', '42': 'kovan' }
/** Generate random number of specified byte length */
const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
@ -57,7 +56,9 @@ function createDeposit({ nullifier, secret }) {
const deposit = { nullifier, secret }
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(deposit.preimage)
deposit.commitmentHex = toHex(deposit.commitment)
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
deposit.nullifierHex = toHex(deposit.nullifierHash)
return deposit
}
@ -76,7 +77,7 @@ async function deposit({ currency, amount }) {
await printETHBalance({ address: senderAccount, name: 'Sender account' })
const value = isLocalRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 })
console.log('Submitting deposit transaction')
await tornado.methods.deposit(toHex(deposit.commitment)).send({ value, from: senderAccount, gas:2e6 })
await tornado.methods.deposit(toHex(deposit.commitment)).send({ value, from: senderAccount, gas: 2e6 })
await printETHBalance({ address: tornado._address, name: 'Tornado' })
await printETHBalance({ address: senderAccount, name: 'Sender account' })
} else { // a token
@ -84,7 +85,7 @@ async function deposit({ currency, amount }) {
await printERC20Balance({ address: senderAccount, name: 'Sender account' })
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
const tokenAmount = isLocalRPC ? TOKEN_AMOUNT : fromDecimals({ amount, decimals })
if(isLocalRPC) {
if (isLocalRPC) {
console.log('Minting some test tokens to deposit')
await erc20.methods.mint(senderAccount, tokenAmount).send({ from: senderAccount, gas: 2e6 })
}
@ -93,11 +94,11 @@ async function deposit({ currency, amount }) {
console.log('Current allowance is', fromWei(allowance))
if (toBN(allowance).lt(toBN(tokenAmount))) {
console.log('Approving tokens for deposit')
await erc20.methods.approve(tornado._address, tokenAmount).send({ from: senderAccount, gas:1e6 })
await erc20.methods.approve(tornado._address, tokenAmount).send({ from: senderAccount, gas: 1e6 })
}
console.log('Submitting deposit transaction')
await tornado.methods.deposit(toHex(deposit.commitment)).send({ from: senderAccount, gas:2e6 })
await tornado.methods.deposit(toHex(deposit.commitment)).send({ from: senderAccount, gas: 2e6 })
await printERC20Balance({ address: tornado._address, name: 'Tornado' })
await printERC20Balance({ address: senderAccount, name: 'Sender account' })
}
@ -125,7 +126,8 @@ async function generateMerkleProof(deposit) {
const leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
// Validate that our data is correct
const isValidRoot = await tornado.methods.isKnownRoot(toHex(await tree.root())).call()
const root = await tree.root()
const isValidRoot = await tornado.methods.isKnownRoot(toHex(root)).call()
const isSpent = await tornado.methods.isSpent(toHex(deposit.nullifierHash)).call()
assert(isValidRoot === true, 'Merkle tree is corrupted')
assert(isSpent === false, 'The note is already spent')
@ -193,6 +195,9 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
}
refund = toWei(refund)
if (relayerURL) {
if (relayerURL.endsWith('.eth')) {
throw new Error('ENS name resolving is not supported. Please provide DNS name of the relayer. See instuctions in README.md')
}
const relayerStatus = await axios.get(relayerURL + '/status')
const { relayerAddress, netId, gasPrices, ethPrices, relayerServiceFee } = relayerStatus.data
assert(netId === await web3.eth.net.getId() || netId === '*', 'This relay is for different network')
@ -206,17 +211,17 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
const { proof, args } = await generateProof({ deposit, recipient, relayerAddress, fee, refund })
console.log('Sending withdraw transaction through relay')
try{
try {
const relay = await axios.post(relayerURL + '/relay', { contract: tornado._address, proof, args })
if (netId === 1 || netId === 42) {
console.log(`Transaction submitted through the relay. View transaction on etherscan https://${networks[netId]}.etherscan.io/tx/${relay.data.txHash}`)
console.log(`Transaction submitted through the relay. View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${relay.data.txHash}`)
} else {
console.log(`Transaction submitted through the relay. The transaction hash is ${relay.data.txHash}`)
}
const receipt = await waitForTxReceipt({ txHash: relay.data.txHash })
console.log('Transaction mined in block', receipt.blockNumber)
} catch(e) {
} catch (e) {
if (e.response) {
console.error(e.response.data.error)
} else {
@ -228,13 +233,13 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
console.log('Submitting withdraw transaction')
await tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, value: refund.toString(), gas: 1e6 })
.on('transactionHash', function(txHash){
.on('transactionHash', function (txHash) {
if (netId === 1 || netId === 42) {
console.log(`View transaction on etherscan https://${networks[netId]}.etherscan.io/tx/${txHash}`)
console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`)
} else {
console.log(`The transaction hash is ${txHash}`)
}
}).on('error', function(e){
}).on('error', function (e) {
console.error('on transactionHash error', e.message)
})
}
@ -294,8 +299,58 @@ function fromDecimals({ amount, decimals }) {
return new BN(wei.toString(10), 10)
}
function toDecimals(value, decimals, fixed) {
const zero = new BN(0)
const negative1 = new BN(-1)
decimals = decimals || 18
fixed = fixed || 7
value = new BN(value)
const negative = value.lt(zero)
const base = new BN('10').pow(new BN(decimals))
const baseLength = base.toString(10).length - 1 || 1
if (negative) {
value = value.mul(negative1)
}
let fraction = value.mod(base).toString(10)
while (fraction.length < baseLength) {
fraction = `0${fraction}`
}
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1]
const whole = value.div(base).toString(10)
value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`
if (negative) {
value = `-${value}`
}
if (fixed) {
value = value.slice(0, fixed)
}
return value
}
function getCurrentNetworkName() {
switch (netId) {
case 1:
return ''
case 42:
return 'kovan.'
}
}
function calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals }) {
const feePercent = toBN(fromDecimals({ amount, decimals })).mul(toBN(relayerServiceFee * 10)).div(toBN('1000'))
const decimalsPoint = Math.floor(relayerServiceFee) === Number(relayerServiceFee) ?
0 :
relayerServiceFee.toString().split('.')[1].length
const roundDecimal = 10 ** decimalsPoint
const total = toBN(fromDecimals({ amount, decimals }))
const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100))
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(5e5))
let desiredFee
switch (currency) {
@ -304,10 +359,9 @@ function calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerS
break
}
default: {
desiredFee =
expense.add(toBN(refund))
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
desiredFee = expense.add(toBN(refund))
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
desiredFee = desiredFee.add(feePercent)
break
}
@ -359,6 +413,60 @@ function parseNote(noteString) {
return { currency: match.groups.currency, amount: match.groups.amount, netId, deposit }
}
async function loadDepositData({ deposit }) {
try {
const eventWhenHappened = await tornado.getPastEvents('Deposit', {
filter: {
commitment: deposit.commitmentHex
},
fromBlock: 0,
toBlock: 'latest'
})
if (eventWhenHappened.length === 0) {
throw new Error('There is no related deposit, the note is invalid')
}
const { timestamp } = eventWhenHappened[0].returnValues
const txHash = eventWhenHappened[0].transactionHash
const isSpent = await tornado.methods.isSpent(deposit.nullifierHex).call()
const receipt = await web3.eth.getTransactionReceipt(txHash)
return { timestamp, txHash, isSpent, from: receipt.from, commitment: deposit.commitmentHex }
} catch (e) {
console.error('loadDepositData', e)
}
return {}
}
async function loadWithdrawalData({ amount, currency, deposit }) {
try {
const events = await await tornado.getPastEvents('Withdrawal', {
fromBlock: 0,
toBlock: 'latest'
})
const withdrawEvent = events.filter((event) => {
return event.returnValues.nullifierHash === deposit.nullifierHex
})[0]
const fee = withdrawEvent.returnValues.fee
const decimals = config.deployments[`netId${netId}`][currency].decimals
const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub(
toBN(fee)
)
const { timestamp } = await web3.eth.getBlock(withdrawEvent.blockHash)
return {
amount: toDecimals(withdrawalAmount, decimals, 9),
txHash: withdrawEvent.transactionHash,
to: withdrawEvent.returnValues.to,
timestamp,
nullifier: deposit.nullifierHex,
fee: toDecimals(fee, decimals, 9)
}
} catch (e) {
console.error('loadWithdrawalData', e)
}
}
/**
* Init web3, contracts, and snark
*/
@ -382,10 +490,18 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
contractJson = require('./build/contracts/ETHTornado.json')
circuit = require('./build/circuits/withdraw.json')
proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT || 20
ETH_AMOUNT = process.env.ETH_AMOUNT
TOKEN_AMOUNT = process.env.TOKEN_AMOUNT
PRIVATE_KEY = process.env.PRIVATE_KEY
if (PRIVATE_KEY) {
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
web3.eth.defaultAccount = account.address
senderAccount = account.address
} else {
console.log('Warning! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you deposit')
}
erc20ContractJson = require('./build/contracts/ERC20Mock.json')
erc20tornadoJson = require('./build/contracts/ERC20Tornado.json')
}
@ -403,22 +519,12 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
senderAccount = (await web3.eth.getAccounts())[0]
} else {
try {
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
// eslint-disable-next-line require-atomic-updates
web3.eth.defaultAccount = account.address
senderAccount = account.address
} catch(e) {
console.error('Please provide PRIVATE_KEY in .env file')
process.exit(1)
}
try{
tornadoAddress = config.deployments[`netId${netId}`][currency].instanceAddress[amount]
if (!tornadoAddress) {
throw new Error()
}
tokenAddress = config.deployments[`netId${netId}`][currency].tokenAddress
} catch(e) {
} catch (e) {
console.error('There is no such tornado instance, check the currency and amount you provide')
process.exit(1)
}
@ -472,6 +578,34 @@ async function main() {
await printERC20Balance({ address, name: '', tokenAddress })
}
})
program
.command('compliance <note>')
.description('Shows the deposit and withdrawal of the provided note. This might be necessary to show the origin of assets held in your withdrawal address.')
.action(async (noteString) => {
const { currency, amount, netId, deposit } = parseNote(noteString)
await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
const depositInfo = await loadDepositData({ deposit })
const depositDate = new Date(depositInfo.timestamp * 1000)
console.log('\n=============Deposit=================')
console.log('Deposit :', amount, currency)
console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString())
console.log('From :', `https://${getCurrentNetworkName()}etherscan.io/address/${depositInfo.from}`)
console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${depositInfo.txHash}`)
console.log('Commitment :', depositInfo.commitment)
if (deposit.isSpent) {
console.log('The note was not spent')
}
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit })
const withdrawalDate = new Date(withdrawInfo.timestamp * 1000)
console.log('\n=============Withdrawal==============')
console.log('Withdrawal :', withdrawInfo.amount, currency)
console.log('Relayer Fee :', withdrawInfo.fee, currency)
console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString())
console.log('To :', `https://${getCurrentNetworkName()}etherscan.io/address/${withdrawInfo.to}`)
console.log('Transaction :', `https://${getCurrentNetworkName()}etherscan.io/tx/${withdrawInfo.txHash}`)
console.log('Nullifier :', withdrawInfo.nullifier)
})
program
.command('test')
.description('Perform an automated test. It deposits and withdraws one ETH and one ERC20 note. Uses ganache.')
@ -489,13 +623,13 @@ async function main() {
amount = '100'
await init({ rpc: program.rpc, currency, amount })
noteString = await deposit({ currency, amount })
;(parsedNote = parseNote(noteString))
; (parsedNote = parseNote(noteString))
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, refund: '0.02', relayerURL: program.relayer })
})
try {
await program.parseAsync(process.argv)
process.exit(0)
} catch(e) {
} catch (e) {
console.log('Error:', e)
process.exit(1)
}

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/enslookup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
docs/resolver.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB