This commit is contained in:
Roman Storm 2020-05-21 12:29:33 -07:00
commit 2e4d59aed0
No known key found for this signature in database
GPG Key ID: 522F2A785F34E71F
15 changed files with 703783 additions and 0 deletions

15
.env.example Normal file
View File

@ -0,0 +1,15 @@
MERKLE_TREE_HEIGHT=20
# in wei
ETH_AMOUNT=100000000000000000
TOKEN_AMOUNT=100000000000000000
PRIVATE_KEY=
ERC20_TOKEN=
# DAI mirror in Kovan
#ERC20_TOKEN=0xd2b1a6b34f4a68425e7c28b4db5a37be3b7a4947
# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some DAI is 13146218
# USDT mirror in Kovan
#ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105
#TOKEN_AMOUNT=1000000
# the block when 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 has some USDT is 13147586

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
.env

28
README.md Normal file
View File

@ -0,0 +1,28 @@
### Kovan, Mainnet
1. Add `PRIVATE_KEY` to `.env` file
2. `./cli.js --help`
Example:
```bash
./cli.js deposit ETH 0.1 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448
```
> Your note: tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652
> Tornado ETH balance is 8.9
> Sender account ETH balance is 1004873.470619891361352542
> Submitting deposit transaction
> Tornado ETH balance is 9
> Sender account ETH balance is 1004873.361652048361352542
```bash
./cli.js withdraw tornado-eth-0.1-42-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://kovan.infura.io/v3/27a9649f826b4e31a83e07ae09a87448 --relayer https://kovan-frelay.duckdns.org
```
> Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754
> Getting current state from tornado contract
> Generating SNARK proof
> Proof time: 9117.051ms
> Sending withdraw transaction through relay
> Transaction submitted through the relay. View transaction on etherscan https://kovan.etherscan.io/tx/0xcb21ae8cad723818c6bc7273e83e00c8393fcdbe74802ce5d562acad691a2a7b
> Transaction mined in block 17036120
> Done

687488
build/circuits/withdraw.json Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

637
cli.js Executable file
View File

@ -0,0 +1,637 @@
#!/usr/bin/env NODE_OPTIONS=--no-warnings node
// Temporary demo client
// Works both in browser and node.js
require('dotenv').config()
const fs = require('fs')
const axios = require('axios')
const assert = require('assert')
const snarkjs = require('snarkjs')
const crypto = require('crypto')
const circomlib = require('circomlib')
const bigInt = snarkjs.bigInt
const merkleTree = require('./lib/MerkleTree')
const Web3 = require('web3')
const buildGroth16 = require('websnark/src/groth16')
const websnarkUtils = require('websnark/src/utils')
const { toWei, fromWei, toBN, BN } = require('web3-utils')
const config = require('./config')
const program = require('commander')
let web3, tornado, circuit, proving_key, groth16, erc20, senderAccount, netId
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))
/** Compute pedersen hash */
const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
/** BigNumber to hex string of specified length */
function toHex(number, length = 32) {
const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)
return '0x' + str.padStart(length * 2, '0')
}
/** Display ETH account balance */
async function printETHBalance({ address, name }) {
console.log(`${name} ETH balance is`, web3.utils.fromWei(await web3.eth.getBalance(address)))
}
/** Display ERC20 account balance */
async function printERC20Balance({ address, name, tokenAddress }) {
const erc20ContractJson = require('./build/contracts/ERC20Mock.json')
erc20 = tokenAddress ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : erc20
console.log(`${name} Token Balance is`, web3.utils.fromWei(await erc20.methods.balanceOf(address).call()))
}
/**
* Create deposit object from secret and nullifier
*/
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
}
/**
* Make a deposit
* @param currency Сurrency
* @param amount Deposit amount
*/
async function deposit({ currency, amount }) {
const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) })
const note = toHex(deposit.preimage, 62)
const noteString = `tornado-${currency}-${amount}-${netId}-${note}`
console.log(`Your note: ${noteString}`)
if (currency === 'eth') {
await printETHBalance({ address: tornado._address, name: 'Tornado' })
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 printETHBalance({ address: tornado._address, name: 'Tornado' })
await printETHBalance({ address: senderAccount, name: 'Sender account' })
} else { // a token
await printERC20Balance({ address: tornado._address, name: 'Tornado' })
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) {
console.log('Minting some test tokens to deposit')
await erc20.methods.mint(senderAccount, tokenAmount).send({ from: senderAccount, gas: 2e6 })
}
const allowance = await erc20.methods.allowance(senderAccount, tornado._address).call({ from: senderAccount })
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 })
}
console.log('Submitting deposit transaction')
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' })
}
return noteString
}
/**
* Generate merkle tree for a deposit.
* Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf
* in it and generates merkle proof
* @param deposit Deposit object
*/
async function generateMerkleProof(deposit) {
// Get all deposit events from smart contract and assemble merkle tree from them
console.log('Getting current state from tornado contract')
const events = await tornado.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' })
const leaves = events
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
.map(e => e.returnValues.commitment)
const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves)
// Find current commitment in the tree
const depositEvent = events.find(e => e.returnValues.commitment === toHex(deposit.commitment))
const leafIndex = depositEvent ? depositEvent.returnValues.leafIndex : -1
// Validate that our data is correct
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')
assert(leafIndex >= 0, 'The deposit is not found in the tree')
// Compute merkle proof of our commitment
return tree.path(leafIndex)
}
/**
* Generate SNARK proof for withdrawal
* @param deposit Deposit object
* @param recipient Funds recipient
* @param relayer Relayer address
* @param fee Relayer fee
* @param refund Receive ether for exchanged tokens
*/
async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0, refund = 0 }) {
// Compute merkle proof of our commitment
const { root, path_elements, path_index } = await generateMerkleProof(deposit)
// Prepare circuit input
const input = {
// Public snark inputs
root: root,
nullifierHash: deposit.nullifierHash,
recipient: bigInt(recipient),
relayer: bigInt(relayerAddress),
fee: bigInt(fee),
refund: bigInt(refund),
// Private snark inputs
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndices: path_index,
}
console.log('Generating SNARK proof')
console.time('Proof time')
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { proof } = websnarkUtils.toSolidityInput(proofData)
console.timeEnd('Proof time')
const args = [
toHex(input.root),
toHex(input.nullifierHash),
toHex(input.recipient, 20),
toHex(input.relayer, 20),
toHex(input.fee),
toHex(input.refund)
]
return { proof, args }
}
/**
* Do an ETH withdrawal
* @param noteString Note to withdraw
* @param recipient Recipient address
*/
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund = '0' }) {
if (currency === 'eth' && refund !== '0') {
throw new Error('The ETH purchase is supposted to be 0 for ETH withdrawals')
}
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')
console.log('Relay address: ', relayerAddress)
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
const fee = calculateFee({ gasPrices, currency, amount, refund, ethPrices, relayerServiceFee, decimals })
if (fee.gt(fromDecimals({ amount, decimals }))) {
throw new Error('Too high refund')
}
const { proof, args } = await generateProof({ deposit, recipient, relayerAddress, fee, refund })
console.log('Sending withdraw transaction through relay')
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://${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) {
if (e.response) {
console.error(e.response.data.error)
} else {
console.error(e.message)
}
}
} else { // using private key
const { proof, args } = await generateProof({ deposit, recipient, refund })
console.log('Submitting withdraw transaction')
await tornado.methods.withdraw(proof, ...args).send({ from: senderAccount, value: refund.toString(), gas: 1e6 })
.on('transactionHash', function (txHash) {
if (netId === 1 || netId === 42) {
console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`)
} else {
console.log(`The transaction hash is ${txHash}`)
}
}).on('error', function (e) {
console.error('on transactionHash error', e.message)
})
}
console.log('Done')
}
function fromDecimals({ amount, decimals }) {
amount = amount.toString()
let ether = amount.toString()
const base = new BN('10').pow(new BN(decimals))
const baseLength = base.toString(10).length - 1 || 1
const negative = ether.substring(0, 1) === '-'
if (negative) {
ether = ether.substring(1)
}
if (ether === '.') {
throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, invalid value')
}
// Split it into a whole and fractional part
const comps = ether.split('.')
if (comps.length > 2) {
throw new Error(
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points'
)
}
let whole = comps[0]
let fraction = comps[1]
if (!whole) {
whole = '0'
}
if (!fraction) {
fraction = '0'
}
if (fraction.length > baseLength) {
throw new Error(
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places'
)
}
while (fraction.length < baseLength) {
fraction += '0'
}
whole = new BN(whole)
fraction = new BN(fraction)
let wei = whole.mul(base).add(fraction)
if (negative) {
wei = wei.mul(negative)
}
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 total = toBN(fromDecimals({ amount, decimals }))
const fee = relayerServiceFee
const decimalsPoint = Math.floor(fee) === fee ? 0 : fee.toString().split('.')[1].length
const roundDecimal = 10 ** decimalsPoint
relayerServiceFee = total.mul(toBN(fee * roundDecimal)).div(toBN(roundDecimal * 100))
const ethFee = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN('500000'))
switch (currency) {
case 'eth': {
return ethFee.add(relayerServiceFee)
}
default: {
const tokenFee = ethFee
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
return tokenFee.add(relayerServiceFee)
}
}
}
/**
* Waits for transaction to be mined
* @param txHash Hash of transaction
* @param attempts
* @param delay
*/
function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
return new Promise((resolve, reject) => {
const checkForTx = async (txHash, retryAttempt = 0) => {
const result = await web3.eth.getTransactionReceipt(txHash)
if (!result || !result.blockNumber) {
if (retryAttempt <= attempts) {
setTimeout(() => checkForTx(txHash, retryAttempt + 1), delay)
} else {
reject(new Error('tx was not mined'))
}
} else {
resolve(result)
}
}
checkForTx(txHash)
})
}
/**
* Parses Tornado.cash note
* @param noteString the note
*/
function parseNote(noteString) {
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
const match = noteRegex.exec(noteString)
if (!match) {
throw new Error('The note has invalid format')
}
const buf = Buffer.from(match.groups.note, 'hex')
const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
const secret = bigInt.leBuff2int(buf.slice(31, 62))
const deposit = createDeposit({ nullifier, secret })
const netId = Number(match.groups.netId)
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
*/
async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
let contractJson, erc20ContractJson, erc20tornadoJson, tornadoAddress, tokenAddress
// TODO do we need this? should it work in browser really?
if (inBrowser) {
// Initialize using injected web3 (Metamask)
// To assemble web version run `npm run browserify`
web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
contractJson = await (await fetch('build/contracts/ETHTornado.json')).json()
circuit = await (await fetch('build/circuits/withdraw.json')).json()
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
MERKLE_TREE_HEIGHT = 20
ETH_AMOUNT = 1e18
TOKEN_AMOUNT = 1e19
senderAccount = (await web3.eth.getAccounts())[0]
} else {
// Initialize from local node
web3 = new Web3(rpc, null, { transactionConfirmationBlocks: 1 })
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 || 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')
}
// groth16 initialises a lot of Promises that will never be resolved, that's why we need to use process.exit to terminate the CLI
groth16 = await buildGroth16()
netId = await web3.eth.net.getId()
if (noteNetId && Number(noteNetId) !== netId) {
throw new Error('This note is for a different network. Specify the --rpc option explicitly')
}
isLocalRPC = netId > 42
if (isLocalRPC) {
tornadoAddress = currency === 'eth' ? contractJson.networks[netId].address : erc20tornadoJson.networks[netId].address
tokenAddress = currency !== 'eth' ? erc20ContractJson.networks[netId].address : null
senderAccount = (await web3.eth.getAccounts())[0]
} else {
try {
tornadoAddress = config.deployments[`netId${netId}`][currency].instanceAddress[amount]
if (!tornadoAddress) {
throw new Error()
}
tokenAddress = config.deployments[`netId${netId}`][currency].tokenAddress
} catch (e) {
console.error('There is no such tornado instance, check the currency and amount you provide')
process.exit(1)
}
}
tornado = new web3.eth.Contract(contractJson.abi, tornadoAddress)
erc20 = currency !== 'eth' ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : {}
}
async function main() {
if (inBrowser) {
const instance = { currency: 'eth', amount: '0.1' }
await init(instance)
window.deposit = async () => {
await deposit(instance)
}
window.withdraw = async () => {
const noteString = prompt('Enter the note to withdraw')
const recipient = (await web3.eth.getAccounts())[0]
const { currency, amount, netId, deposit } = parseNote(noteString)
await init({ noteNetId: netId, currency, amount })
await withdraw({ deposit, currency, amount, recipient })
}
} else {
program
.option('-r, --rpc <URL>', 'The RPC, CLI should interact with', 'http://localhost:8545')
.option('-R, --relayer <URL>', 'Withdraw via relayer')
program
.command('deposit <currency> <amount>')
.description('Submit a deposit of specified currency and amount from default eth account and return the resulting note. The currency is one of (ETH|DAI|cDAI|USDC|cUSDC|USDT). The amount depends on currency, see config.js file or visit https://tornado.cash.')
.action(async (currency, amount) => {
currency = currency.toLowerCase()
await init({ rpc: program.rpc, currency, amount })
await deposit({ currency, amount })
})
program
.command('withdraw <note> <recipient> [ETH_purchase]')
.description('Withdraw a note to a recipient account using relayer or specified private key. You can exchange some of your deposit`s tokens to ETH during the withdrawal by specifing ETH_purchase (e.g. 0.01) to pay for gas in future transactions. Also see the --relayer option.')
.action(async (noteString, recipient, refund) => {
const { currency, amount, netId, deposit } = parseNote(noteString)
await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
await withdraw({ deposit, currency, amount, recipient, refund, relayerURL: program.relayer })
})
program
.command('balance <address> [token_address]')
.description('Check ETH and ERC20 balance')
.action(async (address, tokenAddress) => {
await init({ rpc: program.rpc })
await printETHBalance({ address, name: '' })
if (tokenAddress) {
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.')
.action(async () => {
console.log('Start performing ETH deposit-withdraw test')
let currency = 'eth'
let amount = '0.1'
await init({ rpc: program.rpc, currency, amount })
let noteString = await deposit({ currency, amount })
let parsedNote = parseNote(noteString)
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, relayerURL: program.relayer })
console.log('\nStart performing DAI deposit-withdraw test')
currency = 'dai'
amount = '100'
await init({ rpc: program.rpc, currency, amount })
noteString = await deposit({ currency, amount })
; (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) {
console.log('Error:', e)
process.exit(1)
}
}
}
main()

140
config.js Normal file
View File

@ -0,0 +1,140 @@
require('dotenv').config()
module.exports = {
deployments: {
netId1: {
eth: {
instanceAddress: {
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
'100': '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291'
},
symbol: 'ETH',
decimals: 18
},
dai: {
instanceAddress: {
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
'10000': '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
'100000': undefined
},
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
symbol: 'DAI',
decimals: 18
},
cdai: {
instanceAddress: {
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
'50000': '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
'500000': '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
'5000000': undefined
},
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
symbol: 'cDAI',
decimals: 8
},
usdc: {
instanceAddress: {
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
'10000': '0xD691F27f38B395864Ea86CfC7253969B409c362d',
'100000': undefined
},
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
symbol: 'USDC',
decimals: 6
},
cusdc: {
instanceAddress: {
'5000': '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
'50000': '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
'500000': '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
'5000000': undefined
},
tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
symbol: 'cUSDC',
decimals: 8
},
usdt: {
instanceAddress: {
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
'10000': '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
'100000': '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E'
},
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
symbol: 'USDT',
decimals: 6
}
},
netId42: {
eth: {
instanceAddress: {
'0.1': '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
'1': '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
'10': '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
'100': '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D'
},
symbol: 'ETH',
decimals: 18
},
dai: {
instanceAddress: {
'100': '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
'1000': '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
'10000': '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
'100000': undefined
},
tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa',
symbol: 'DAI',
decimals: 18
},
cdai: {
instanceAddress: {
'5000': '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
'50000': '0x7182EA067e0f050997444FCb065985Fd677C16b6',
'500000': '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
'5000000': undefined
},
tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c',
symbol: 'cDAI',
decimals: 8
},
usdc: {
instanceAddress: {
'100': '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
'1000': '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
'10000': '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
'100000': undefined
},
tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF',
symbol: 'USDC',
decimals: 6
},
cusdc: {
instanceAddress: {
'5000': '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
'50000': '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
'500000': '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
'5000000': undefined
},
tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35',
symbol: 'cUSDC',
decimals: 8
},
usdt: {
instanceAddress: {
'100': '0x327853Da7916a6A0935563FB1919A48843036b42',
'1000': '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
'10000': '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
'100000': '0x14aEd24B67EaF3FF28503eB92aeb217C47514364'
},
tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1',
symbol: 'USDT',
decimals: 6
}
}
}
}

195
lib/MerkleTree.js Normal file
View File

@ -0,0 +1,195 @@
const jsStorage = require('./Storage')
const hasherImpl = require('./MiMC')
class MerkleTree {
constructor(n_levels, defaultElements, prefix, storage, hasher) {
this.prefix = prefix
this.storage = storage || new jsStorage()
this.hasher = hasher || new hasherImpl()
this.n_levels = n_levels
this.zero_values = []
this.totalElements = 0
let current_zero_value = '21663839004416932945382355908790599225266501822907911457504978515578255421292'
this.zero_values.push(current_zero_value)
for (let i = 0; i < n_levels; i++) {
current_zero_value = this.hasher.hash(i, current_zero_value, current_zero_value)
this.zero_values.push(
current_zero_value.toString(),
)
}
if (defaultElements) {
let level = 0
this.totalElements = defaultElements.length
defaultElements.forEach((element, i) => {
this.storage.put(MerkleTree.index_to_key(prefix, level, i), element)
})
level++
let numberOfElementsInLevel = Math.ceil(defaultElements.length / 2)
for (level; level <= this.n_levels; level++) {
for(let i = 0; i < numberOfElementsInLevel; i++) {
const leftKey = MerkleTree.index_to_key(prefix, level - 1, 2 * i)
const rightKey = MerkleTree.index_to_key(prefix, level - 1, 2 * i + 1)
const left = this.storage.get(leftKey)
const right = this.storage.get_or_element(rightKey, this.zero_values[level - 1])
const subRoot = this.hasher.hash(null, left, right)
this.storage.put(MerkleTree.index_to_key(prefix, level, i), subRoot)
}
numberOfElementsInLevel = Math.ceil(numberOfElementsInLevel / 2)
}
}
}
static index_to_key(prefix, level, index) {
const key = `${prefix}_tree_${level}_${index}`
return key
}
async root() {
let root = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
this.zero_values[this.n_levels],
)
return root
}
async path(index) {
class PathTraverser {
constructor(prefix, storage, zero_values) {
this.prefix = prefix
this.storage = storage
this.zero_values = zero_values
this.path_elements = []
this.path_index = []
}
async handle_index(level, element_index, sibling_index) {
const sibling = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, level, sibling_index),
this.zero_values[level],
)
this.path_elements.push(sibling)
this.path_index.push(element_index % 2)
}
}
index = Number(index)
let traverser = new PathTraverser(this.prefix, this.storage, this.zero_values)
const root = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
this.zero_values[this.n_levels],
)
const element = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, 0, index),
this.zero_values[0],
)
await this.traverse(index, traverser)
return {
root,
path_elements: traverser.path_elements,
path_index: traverser.path_index,
element
}
}
async update(index, element, insert = false) {
if (!insert && index >= this.totalElements) {
throw Error('Use insert method for new elements.')
} else if(insert && index < this.totalElements) {
throw Error('Use update method for existing elements.')
}
try {
class UpdateTraverser {
constructor(prefix, storage, hasher, element, zero_values) {
this.prefix = prefix
this.current_element = element
this.zero_values = zero_values
this.storage = storage
this.hasher = hasher
this.key_values_to_put = []
}
async handle_index(level, element_index, sibling_index) {
if (level == 0) {
this.original_element = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, level, element_index),
this.zero_values[level],
)
}
const sibling = await this.storage.get_or_element(
MerkleTree.index_to_key(this.prefix, level, sibling_index),
this.zero_values[level],
)
let left, right
if (element_index % 2 == 0) {
left = this.current_element
right = sibling
} else {
left = sibling
right = this.current_element
}
this.key_values_to_put.push({
key: MerkleTree.index_to_key(this.prefix, level, element_index),
value: this.current_element,
})
this.current_element = this.hasher.hash(level, left, right)
}
}
let traverser = new UpdateTraverser(
this.prefix,
this.storage,
this.hasher,
element,
this.zero_values
)
await this.traverse(index, traverser)
traverser.key_values_to_put.push({
key: MerkleTree.index_to_key(this.prefix, this.n_levels, 0),
value: traverser.current_element,
})
await this.storage.put_batch(traverser.key_values_to_put)
} catch(e) {
console.error(e)
}
}
async insert(element) {
const index = this.totalElements
await this.update(index, element, true)
this.totalElements++
}
async traverse(index, handler) {
let current_index = index
for (let i = 0; i < this.n_levels; i++) {
let sibling_index = current_index
if (current_index % 2 == 0) {
sibling_index += 1
} else {
sibling_index -= 1
}
await handler.handle_index(i, current_index, sibling_index)
current_index = Math.floor(current_index / 2)
}
}
getIndexByElement(element) {
for(let i = this.totalElements - 1; i >= 0; i--) {
const elementFromTree = this.storage.get(MerkleTree.index_to_key(this.prefix, 0, i))
if (elementFromTree === element) {
return i
}
}
return false
}
}
module.exports = MerkleTree

13
lib/MiMC.js Normal file
View File

@ -0,0 +1,13 @@
const circomlib = require('circomlib')
const mimcsponge = circomlib.mimcsponge
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
class MimcSpongeHasher {
hash(level, left, right) {
return mimcsponge.multiHash([bigInt(left), bigInt(right)]).toString()
}
}
module.exports = MimcSpongeHasher

39
lib/Storage.js Normal file
View File

@ -0,0 +1,39 @@
class JsStorage {
constructor() {
this.db = {}
}
get(key) {
return this.db[key]
}
get_or_element(key, defaultElement) {
const element = this.db[key]
if (element === undefined) {
return defaultElement
} else {
return element
}
}
put(key, value) {
if (key === undefined || value === undefined) {
throw Error('key or value is undefined')
}
this.db[key] = value
}
del(key) {
delete this.db[key]
}
put_batch(key_values) {
key_values.forEach(element => {
this.db[element.key] = element.value
})
}
}
module.exports = JsStorage

3874
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "cli-tornado",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.2",
"circom": "0.0.35",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#c372f14d324d57339c88451834bf2824e73bbdbc",
"commander": "^5.1.0",
"dotenv": "^8.2.0",
"snarkjs": "git+https://github.com/tornadocash/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
"web3": "^1.2.8",
"websnark": "git+https://github.com/tornadocash/websnark.git#4c0af6a8b65aabea3c09f377f63c44e7a58afa6d"
}
}