This commit is contained in:
poma 2020-09-28 05:28:34 +03:00
parent df89ab41d5
commit c4cf2863e3
No known key found for this signature in database
GPG Key ID: BA20CB01FE165657
26 changed files with 246783 additions and 916 deletions

View File

@ -26,7 +26,7 @@
"semi": ["error", "never"],
"object-curly-spacing": ["error", "always"],
"require-await": "error",
"comma-dangle": ["error", "never"],
"comma-dangle": ["error", "only-multiline"],
"space-before-function-paren": [
"error",
{

1085
abis/mining.abi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,17 @@ require('dotenv').config()
module.exports = {
netId: Number(process.env.NET_ID) || 42,
redisUrl: process.env.REDIS_URL,
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/',
oracleAddress: '0xA2b8E7ee7c8a18ea561A5CF7C9C365592026E374',
minerAddress: '0x4c4C5cCC263A4531b90042561523c4a1Ad571751',
minerMerkleTreeHeight: 10,
privateKey: process.env.PRIVATE_KEY,
mixers: {
instances: {
netId1: {
eth: {
mixerAddress: {
instanceAddress: {
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
@ -20,7 +22,7 @@ module.exports = {
decimals: 18
},
dai: {
mixerAddress: {
instanceAddress: {
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
'10000': '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
@ -31,7 +33,7 @@ module.exports = {
decimals: 18
},
cdai: {
mixerAddress: {
instanceAddress: {
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
'50000': '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
'500000': '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
@ -42,7 +44,7 @@ module.exports = {
decimals: 8
},
usdc: {
mixerAddress: {
instanceAddress: {
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
'10000': '0xD691F27f38B395864Ea86CfC7253969B409c362d',
@ -53,7 +55,7 @@ module.exports = {
decimals: 6
},
cusdc: {
mixerAddress: {
instanceAddress: {
'5000': '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
'50000': '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
'500000': '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
@ -64,7 +66,7 @@ module.exports = {
decimals: 8
},
usdt: {
mixerAddress: {
instanceAddress: {
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
'10000': '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
@ -77,7 +79,7 @@ module.exports = {
},
netId42: {
eth: {
mixerAddress: {
instanceAddress: {
'0.1': '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
'1': '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
'10': '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
@ -87,7 +89,7 @@ module.exports = {
decimals: 18
},
dai: {
mixerAddress: {
instanceAddress: {
'100': '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
'1000': '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
'10000': '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
@ -98,7 +100,7 @@ module.exports = {
decimals: 18
},
cdai: {
mixerAddress: {
instanceAddress: {
'5000': '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
'50000': '0x7182EA067e0f050997444FCb065985Fd677C16b6',
'500000': '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
@ -109,7 +111,7 @@ module.exports = {
decimals: 8
},
usdc: {
mixerAddress: {
instanceAddress: {
'100': '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
'1000': '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
'10000': '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
@ -120,7 +122,7 @@ module.exports = {
decimals: 6
},
cusdc: {
mixerAddress: {
instanceAddress: {
'5000': '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
'50000': '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
'500000': '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
@ -131,7 +133,7 @@ module.exports = {
decimals: 8
},
usdt: {
mixerAddress: {
instanceAddress: {
'100': '0x327853Da7916a6A0935563FB1919A48843036b42',
'1000': '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
'10000': '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
@ -144,7 +146,7 @@ module.exports = {
}
},
defaultGasPrice: 20,
port: process.env.APP_PORT,
port: process.env.APP_PORT || 8000,
relayerServiceFee: Number(process.env.RELAYER_FEE),
maxGasPrice: process.env.MAX_GAS_PRICE || 200,
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,

2
keys/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# avoid committing non-prduction blobs
./*

243053
keys/TreeUpdate.json Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

2709
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,9 @@
"name": "relay",
"version": "3.0.2",
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"main": "app.js",
"scripts": {
"start": "node app.js",
"server": "node src/server.js",
"treeUpdater": "node src/treeWatcher",
"eslint": "npx eslint --ignore-path .gitignore .",
"test": "echo \"Error: no test specified\" && exit 1"
},
@ -12,12 +12,16 @@
"license": "MIT",
"dependencies": {
"bull": "^3.12.1",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#5beb6aee94923052faeecea40135d45b6ce6172c",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"fixed-merkle-tree": "^0.4.0",
"gas-price-oracle": "^0.1.5",
"ioredis": "^4.14.1",
"node-fetch": "^2.6.0",
"web3": "^1.2.2",
"tornado-cash-anonymity-mining": "git+https://github.com/tornadocash/tornado-anonymity-mining.git#820bd83254f3264cebaf255869641ebc33288dc3",
"uuid": "^8.3.0",
"web3": "^1.3.0",
"web3-utils": "^1.2.2"
},
"devDependencies": {

View File

@ -1,9 +1,11 @@
const Queue = require('bull')
const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils')
const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils')
const mixerABI = require('../abis/mixerABI.json')
const { isValidProof, isValidArgs, isKnownContract, isEnoughFee } = require('./utils')
const config = require('../config')
const { redisClient, redisOpts } = require('./redis')
const { GasPriceOracle } = require('gas-price-oracle')
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
const { web3, fetcher, sender, gasPriceOracle } = require('./instances')
const withdrawQueue = new Queue('withdraw', redisOpts)

19
src.bak/treeUpdate.js Normal file
View File

@ -0,0 +1,19 @@
const fs = require('fs')
const { Controller } = require('tornado-cash-anonymity-mining')
const { web3 } = require('./instances')
const { farmingAddress, farmingMerkleTreeHeight } = require('../config')
const contract = web3.eth.contract(require('../abis/mining.abi.json'), farmingAddress)
const provingKeys = {
treeUpdateCircuit: require('.../keys/TreeUpdate.json'),
treeUpdateProvingKey: fs.readFileSync('../keys/TreeUpdate_proving_key.bin').buffer,
}
const controller = new Controller({
contract,
provingKeys,
merkleTreeHeight: farmingMerkleTreeHeight,
})
// await controller.init()
// await controller.treeUpdate(commitment)

178
src.bak/utils.js Normal file
View File

@ -0,0 +1,178 @@
const { isHexStrict, toBN, toWei, BN } = require('web3-utils')
const { netId, mixers, relayerServiceFee } = require('../config')
function isValidProof(proof) {
// validator expects `websnarkUtils.toSolidityInput(proof)` output
if (!proof) {
return { valid: false, reason: 'The proof is empty.' }
}
if (!isHexStrict(proof) || proof.length !== 2 + 2 * 8 * 32) {
return { valid: false, reason: 'Corrupted proof' }
}
return { valid: true }
}
function isValidArgs(args) {
if (!args) {
return { valid: false, reason: 'Args are empty' }
}
if (args.length !== 6) {
return { valid: false, reason: 'Length of args is lower than 6' }
}
for (let signal of args) {
if (!isHexStrict(signal)) {
return { valid: false, reason: `Corrupted signal ${signal}` }
}
}
if (
args[0].length !== 66 ||
args[1].length !== 66 ||
args[2].length !== 42 ||
args[3].length !== 42 ||
args[4].length !== 66 ||
args[5].length !== 66
) {
return { valid: false, reason: 'The length one of the signals is incorrect' }
}
return { valid: true }
}
function isKnownContract(contract) {
const mixers = getMixers()
for (let currency of Object.keys(mixers)) {
for (let amount of Object.keys(mixers[currency].mixerAddress)) {
if (mixers[currency].mixerAddress[amount] === contract) {
return { valid: true, currency, amount }
}
}
}
return { valid: false }
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function fromDecimals(value, decimals) {
value = value.toString()
let ether = value.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 ' + value + ' 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 ' + value + ' 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 ' + value + ' 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 isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee }) {
const { decimals } = mixers[`netId${netId}`][currency]
const decimalsPoint =
Math.floor(relayerServiceFee) === relayerServiceFee
? 0
: relayerServiceFee.toString().split('.')[1].length
const roundDecimal = 10 ** decimalsPoint
const feePercent = toBN(fromDecimals(amount, decimals))
.mul(toBN(relayerServiceFee * roundDecimal))
.div(toBN(roundDecimal * 100))
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(gas))
let desiredFee
switch (currency) {
case 'eth': {
desiredFee = expense.add(feePercent)
break
}
default: {
desiredFee = expense
.add(refund)
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
desiredFee = desiredFee.add(feePercent)
break
}
}
console.log(
'sent fee, desired fee, feePercent',
fee.toString(),
desiredFee.toString(),
feePercent.toString()
)
if (fee.lt(desiredFee)) {
return { isEnough: false, reason: 'Not enough fee' }
}
return { isEnough: true }
}
function getArgsForOracle() {
const tokens = mixers['netId1']
const tokenAddresses = []
const oneUintAmount = []
const currencyLookup = {}
Object.entries(tokens).map(([currency, data]) => {
if (currency !== 'eth') {
tokenAddresses.push(data.tokenAddress)
oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
currencyLookup[data.tokenAddress] = currency
}
})
return { tokenAddresses, oneUintAmount, currencyLookup }
}
function getMixers() {
return mixers[`netId${netId}`]
}
module.exports = {
isValidProof,
isValidArgs,
sleep,
isKnownContract,
isEnoughFee,
getMixers,
getArgsForOracle
}

21
src/controller.js Normal file
View File

@ -0,0 +1,21 @@
const { getWithdrawInputError } = require('./validate')
const { postJob } = require('./queue')
async function tornadoWithdraw(req, res) {
const inputError = getWithdrawInputError(req.body)
if (inputError) {
console.log('Invalid input:', inputError)
return res.status(400).json({ error: inputError })
}
const { proof, args, contract } = req.body
const id = await postJob({
type: 'withdraw',
data: { proof, args, contract },
})
return res.json({ id })
}
module.exports = {
tornadoWithdraw,
}

40
src/queue.js Normal file
View File

@ -0,0 +1,40 @@
const { v4: uuid } = require('uuid')
const Queue = require('bull')
const Redis = require('ioredis')
const { redisUrl } = require('../config')
const redis = new Redis(redisUrl)
const queue = new Queue('proofs', redisUrl)
async function postJob(type, data) {
const id = uuid()
const job = await queue.add(
'proofs',
{
id,
type,
data,
},
// { removeOnComplete: true },
)
await redis.set(`job:${id}`, job.id)
return id
}
async function getJob(uuid) {
const id = await redis.get(`job:${uuid}`)
return queue.getJobFromId(id)
}
async function getJobStatus(uuid) {
const job = await getJob(uuid)
// ...
}
module.exports = {
postJob,
getJob,
getJobStatus,
queue,
}

34
src/server.js Normal file
View File

@ -0,0 +1,34 @@
const express = require('express')
const status = require('status')
const controller = require('controller')
const { port } = require('../config')
const { version } = require('../package.json')
const app = express()
app.use(express.json())
// Log error to console but don't send it to the client to avoid leaking data
app.use((err, req, res, next) => {
if (err) {
console.error(err)
return res.sendStatus(500)
}
next()
})
// Add CORS headers
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
next()
})
app.get('/', status.index)
app.get('/v1/status', status.status)
app.post('/v1/jobs/:id', status.getJob)
app.post('/v1/tornadoWithdraw', controller.tornadoWithdraw)
app.post('/v1/miningReward', controller.miningReward)
app.post('/v1/miningWithdraw', controller.miningWithdraw)
console.log('Version:', version)
app.listen(port)

38
src/status.js Normal file
View File

@ -0,0 +1,38 @@
const queue = require('queue')
async function status(req, res) {
let nonce = await redisClient.get('nonce')
let latestBlock = null
try {
latestBlock = await web3.eth.getBlockNumber()
} catch (e) {
console.error('Problem with RPC', e)
}
const { ethPrices } = fetcher
res.json({
relayerAddress: web3.eth.defaultAccount,
mixers,
gasPrices: await gasPriceOracle.gasPrices(),
netId,
ethPrices,
relayerServiceFee,
nonce,
version,
latestBlock
})
}
function index(req, res) {
res.send('This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/v1/status>/status</a> for settings')
}
async function getJob(req, res) {
const status = await queue.getJobStatus(req.params.id)
return res.send(status)
}
module.exports = {
status,
index,
getJob,
}

95
src/treeWatcher.js Normal file
View File

@ -0,0 +1,95 @@
const MerkleTree = require('fixed-merkle-tree')
const { redisUrl, rpcUrl, minerMerkleTreeHeight, minerAddress } = require('../config')
const { poseidonHash2 } = require('./utils')
const { toBN } = require('web3-utils')
const Redis = require('ioredis')
const redis = new Redis(redisUrl)
const Web3 = require('web3')
const web3 = new Web3(rpcUrl)
const contract = new web3.eth.Contract(require('../abis/mining.abi.json'), minerAddress)
let tree, eventSubscription, blockSubscription
async function fetchEvents(from = 0, to = 'latest') {
const events = await contract.getPastEvents('NewAccount', {
fromBlock: from,
toBlock: to,
})
return events
.sort((a, b) => a.returnValues.index - b.returnValues.index)
.map((e) => toBN(e.returnValues.commitment))
}
async function processNewEvent(err, event) {
if (err) {
throw new Error(`Event handler error: ${err}`)
// console.error(err)
// return
}
console.log('New account event', event.returnValues)
const { commitment, index } = event.returnValues
if (tree.elements().length === Number(index)) {
tree.insert(toBN(commitment))
await updateRedis()
} else if (tree.elements().length === Number(index) + 1) {
console.log('Replacing element', index)
tree.update(index, toBN(commitment))
await updateRedis()
} else {
console.log(`Invalid element index ${index}, rebuilding tree`)
await rebuild()
}
}
async function processNewBlock(err) {
if (err) {
throw new Error(`Event handler error: ${err}`)
// console.error(err)
// return
}
await updateRedis()
}
async function updateRedis() {
const rootOnContract = await contract.methods.getLastAccountRoot().call()
if (!tree.root().eq(toBN(rootOnContract))) {
console.log(`Invalid tree root: ${tree.root()} != ${toBN(rootOnContract)}, rebuilding tree`)
await rebuild()
return
}
const rootInRedis = await redis.get('tree:root')
if (!rootInRedis || !tree.root().eq(toBN(rootInRedis))) {
const serializedTree = JSON.stringify(tree.serialize())
await redis.set('tree:elements', serializedTree)
await redis.set('tree:root', tree.root().toString())
await redis.publish('treeUpdate', tree.root().toString())
console.log('Updated tree in redis, new root:', tree.root().toString())
} else {
console.log('Tree in redis is up to date, skipping update')
}
}
async function rebuild() {
await eventSubscription.unsubscribe()
await blockSubscription.unsubscribe()
setTimeout(init, 3000)
}
async function init() {
console.log('Initializing')
const block = await web3.eth.getBlockNumber()
const events = await fetchEvents(0, block)
tree = new MerkleTree(minerMerkleTreeHeight, events, { hashFunction: poseidonHash2 })
console.log(`Rebuilt tree with ${events.length} elements, root: ${tree.root()}`)
eventSubscription = contract.events.NewAccount({ fromBlock: block + 1 }, processNewEvent)
blockSubscription = web3.eth.subscribe('newBlockHeaders', processNewBlock)
await updateRedis()
}
init()
process.on('unhandledRejection', error => {
console.error('Unhandled promise rejection', error)
process.exit(1)
})

View File

@ -1,178 +1,40 @@
const { isHexStrict, toBN, toWei, BN } = require('web3-utils')
const { netId, mixers, relayerServiceFee } = require('../config')
const { instances, netId } = require('../config')
const { poseidon } = require('circomlib')
const { toBN } = require('web3-utils')
function isValidProof(proof) {
// validator expects `websnarkUtils.toSolidityInput(proof)` output
if (!proof) {
return { valid: false, reason: 'The proof is empty.' }
}
if (!isHexStrict(proof) || proof.length !== 2 + 2 * 8 * 32) {
return { valid: false, reason: 'Corrupted proof' }
}
return { valid: true }
}
function isValidArgs(args) {
if (!args) {
return { valid: false, reason: 'Args are empty' }
}
if (args.length !== 6) {
return { valid: false, reason: 'Length of args is lower than 6' }
}
for (let signal of args) {
if (!isHexStrict(signal)) {
return { valid: false, reason: `Corrupted signal ${signal}` }
}
}
if (
args[0].length !== 66 ||
args[1].length !== 66 ||
args[2].length !== 42 ||
args[3].length !== 42 ||
args[4].length !== 66 ||
args[5].length !== 66
) {
return { valid: false, reason: 'The length one of the signals is incorrect' }
}
return { valid: true }
}
function isKnownContract(contract) {
const mixers = getMixers()
for (let currency of Object.keys(mixers)) {
for (let amount of Object.keys(mixers[currency].mixerAddress)) {
if (mixers[currency].mixerAddress[amount] === contract) {
return { valid: true, currency, amount }
function getInstance(address) {
const inst = instances[`netId${netId}`]
for (const currency of Object.keys(inst)) {
for (const amount of Object.keys(inst[currency].instanceAddress)) {
if (inst[currency].instanceAddress[amount] === address) {
return { currency, amount }
}
}
}
return { valid: false }
return null
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// async function setSafeInterval(func, interval) {
// try {
// await func()
// } catch (e) {
// console.error('Unhandled promise error:', e)
// } finally {
// setTimeout(() => setSafeInterval(func, interval), interval)
// }
// }
function fromDecimals(value, decimals) {
value = value.toString()
let ether = value.toString()
const base = new BN('10').pow(new BN(decimals))
const baseLength = base.toString(10).length - 1 || 1
const poseidonHash = (items) => toBN(poseidon(items).toString())
const poseidonHash2 = (a, b) => poseidonHash([a, b])
const negative = ether.substring(0, 1) === '-'
if (negative) {
ether = ether.substring(1)
}
if (ether === '.') {
throw new Error('[ethjs-unit] while converting number ' + value + ' 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 ' + value + ' 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 ' + value + ' 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 isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee }) {
const { decimals } = mixers[`netId${netId}`][currency]
const decimalsPoint =
Math.floor(relayerServiceFee) === relayerServiceFee
? 0
: relayerServiceFee.toString().split('.')[1].length
const roundDecimal = 10 ** decimalsPoint
const feePercent = toBN(fromDecimals(amount, decimals))
.mul(toBN(relayerServiceFee * roundDecimal))
.div(toBN(roundDecimal * 100))
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(gas))
let desiredFee
switch (currency) {
case 'eth': {
desiredFee = expense.add(feePercent)
break
}
default: {
desiredFee = expense
.add(refund)
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
desiredFee = desiredFee.add(feePercent)
break
}
}
console.log(
'sent fee, desired fee, feePercent',
fee.toString(),
desiredFee.toString(),
feePercent.toString()
)
if (fee.lt(desiredFee)) {
return { isEnough: false, reason: 'Not enough fee' }
}
return { isEnough: true }
}
function getArgsForOracle() {
const tokens = mixers['netId1']
const tokenAddresses = []
const oneUintAmount = []
const currencyLookup = {}
Object.entries(tokens).map(([currency, data]) => {
if (currency !== 'eth') {
tokenAddresses.push(data.tokenAddress)
oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
currencyLookup[data.tokenAddress] = currency
}
function setSafeInterval(func, interval) {
func().catch(console.error).finally(() => {
setTimeout(() => setSafeInterval(func, interval), interval)
})
return { tokenAddresses, oneUintAmount, currencyLookup }
}
function getMixers() {
return mixers[`netId${netId}`]
}
module.exports = {
isValidProof,
isValidArgs,
sleep,
isKnownContract,
isEnoughFee,
getMixers,
getArgsForOracle
getInstance,
setSafeInterval,
poseidonHash2,
}

83
src/validate.js Normal file
View File

@ -0,0 +1,83 @@
const { isHexStrict } = require('web3-utils')
const { getInstance } = require('./utils')
const { rewardAccount } = require('../config')
function getProofError(proof) {
if (!proof) {
return 'The proof is empty'
}
if (!isHexStrict(proof) || proof.length !== 2 + 2 * 8 * 32) {
return 'Corrupted proof'
}
return null
}
function getArgsError(args, expectedLengths) {
if (!args) {
return 'Args are empty'
}
if (!Array.isArray(args)) {
return 'Args should be an array'
}
if (args.length !== expectedLengths.length) {
return `Expected ${expectedLengths.length} args`
}
for (let i = 0; i < args.length; i++) {
if (!isHexStrict(args[i])) {
return `Corrupted signal ${i}: ${args[i]}`
}
if (args[i].length !== 2 + expectedLengths * 20) {
return `Signal ${i} has invalid length: ${args[i]}`
}
}
return null
}
function getContractError(contract) {
if (!contract) {
return 'The contract is empty'
}
if (!isHexStrict(contract) || contract.length !== 42) {
return 'Corrupted contract'
}
if (!getInstance(contract)) {
return `This relayer does not support the token: ${contract}`
}
return null
}
function getRewardAddressError(address) {
if (address.toLowerCase() !== rewardAccount.toLowerCase()) {
return 'This proof is for different relayer'
}
return null
}
function getWithdrawInputError(input) {
return getProofError(input.proof) || getArgsError(input.args, [32, 32, 20, 20, 32, 32]) || getContractError(input.contract) || getRewardAddressError(input.args[3])
}
function getClaimInputError(input) {
return getProofError(input.proof) || getArgsError(input.args, [32, 32, 20, 20, 32, 32])
}
function getRewardInputError(input) {
return getProofError(input.proof) || getArgsError(input.args, [32, 32, 20, 20, 32, 32])
}
module.exports = {
getWithdrawInputError,
getClaimInputError,
getRewardInputError,
}

102
src/worker.js Normal file
View File

@ -0,0 +1,102 @@
const { queue } = require('./queue')
const Web3 = require('web3')
const { rpcUrl, redisUrl, privateKey, netId, gasBumpInterval, gasBumpPercentage, maxGasPrice } = require('../config')
const { numberToHex, toWei, toHex, toBN, fromWei, toChecksumAddress, BN } = require('web3-utils')
const tornadoABI = require('../abis/tornadoABI.json')
const MerkleTree = require('fixed-merkle-tree')
const { setSafeInterval, poseidonHash2 } = require('./utils')
const Redis = require('ioredis')
const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl)
const { GasPriceOracle } = require('gas-price-oracle')
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
queue.process(process)
redisSubscribe.subscribe('treeUpdate', fetchTree)
let web3
let nonce
let currentTx
let tree
async function fetchTree() {
const elements = await redis.get('tree:elements')
const convert = (_, val) => typeof(val) === 'string' ? toBN(val) : val
tree = MerkleTree.deserialize(JSON.parse(elements, convert), poseidonHash2)
if (currentTx) {
// todo replace
}
}
async function watcher() {
if (currentTx && Date.now() - currentTx.date > gasBumpInterval) {
const newGasPrice = toBN(currentTx.gasPrice).mul(toBN(gasBumpPercentage)).div(toBN(100))
const maxGasPrice = toBN(toWei(maxGasPrice.toString(), 'Gwei'))
currentTx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
currentTx.date = Date.now()
console.log(`Resubmitting with gas price ${fromWei(currentTx.gasPrice.toString(), 'gwei')} gwei`)
//await this.sendTx(tx, null, 9999)
}
}
async function init() {
web3 = new Web3(rpcUrl, null, { transactionConfirmationBlocks: 1 })
const account = web3.eth.accounts.privateKeyToAccount('0x' + privateKey)
web3.eth.accounts.wallet.add('0x' + privateKey)
web3.eth.defaultAccount = account.address
nonce = await web3.eth.getTransactionCount(account.address, 'latest')
await fetchTree()
setSafeInterval(watcher, 1000)
}
async function checkTornadoFee(contract, fee, refund) {
}
async function process(job) {
if (job.type !== 'tornadoWithdraw') {
throw new Error('not implemented')
}
console.log(Date.now(), ' withdraw started', job.id)
const { proof, args, contract } = job.data
const fee = toBN(args[4])
const refund = toBN(args[5])
await checkTornadoFee(contract, fee, refund)
const instance = new web3.eth.Contract(tornadoABI, contract)
const data = instance.methods.withdraw(proof, ...args).encodeABI()
const gasPrices = await gasPriceOracle.gasPrices()
const tx = {
from: web3.eth.defaultAccount,
value: numberToHex(refund),
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
to: contract,
netId,
data,
nonce,
}
// nonce++ later
const gas = await web3.eth.estimateGas(tx)
tx.gas = gas
let signedTx = await this.web3.eth.accounts.signTransaction(tx, privateKey)
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
result.once('transactionHash', async (txHash) => {
console.log(`A new successfully sent tx ${txHash}`)
job.data.txHash = txHash
await job.update(job.data)
})
await result
}
async function main() {
await init()
}
// main()
fetchTree()