mirror of
https://github.com/tornadocash/tornado-relayer
synced 2024-02-02 15:04:06 +01:00
wip
This commit is contained in:
parent
df89ab41d5
commit
c4cf2863e3
@ -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
1085
abis/mining.abi.json
Normal file
File diff suppressed because it is too large
Load Diff
32
config.js
32
config.js
@ -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
2
keys/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# avoid committing non-prduction blobs
|
||||
./*
|
243053
keys/TreeUpdate.json
Normal file
243053
keys/TreeUpdate.json
Normal file
File diff suppressed because one or more lines are too long
BIN
keys/TreeUpdate_proving_key.bin
Normal file
BIN
keys/TreeUpdate_proving_key.bin
Normal file
Binary file not shown.
2709
package-lock.json
generated
2709
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -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": {
|
||||
|
@ -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
19
src.bak/treeUpdate.js
Normal 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
178
src.bak/utils.js
Normal 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
21
src/controller.js
Normal 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
40
src/queue.js
Normal 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
34
src/server.js
Normal 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
38
src/status.js
Normal 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
95
src/treeWatcher.js
Normal 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)
|
||||
})
|
192
src/utils.js
192
src/utils.js
@ -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
83
src/validate.js
Normal 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
102
src/worker.js
Normal 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()
|
Loading…
Reference in New Issue
Block a user