mirror of
https://github.com/tornadocash/tornado-relayer.git
synced 2024-10-01 08:25:37 -04:00
clean up
This commit is contained in:
parent
8bc5b7be9e
commit
ae61e3ec96
@ -6,8 +6,7 @@
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:security/recommended"
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
|
@ -48,7 +48,6 @@
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-prettier": "^6.12.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-security": "^1.5.0",
|
||||
"mocha": "^8.1.3",
|
||||
"nodemon": "^2.0.15",
|
||||
"ts-node": "^10.7.0",
|
||||
|
@ -1,57 +0,0 @@
|
||||
import { postJob } from './queue';
|
||||
import { jobType } from './types';
|
||||
|
||||
const {
|
||||
getTornadoWithdrawInputError,
|
||||
getMiningRewardInputError,
|
||||
getMiningWithdrawInputError,
|
||||
} = require('./validator');
|
||||
|
||||
|
||||
async function tornadoWithdraw(req, res) {
|
||||
const inputError = getTornadoWithdrawInputError(req.body);
|
||||
if (inputError) {
|
||||
console.log('Invalid input:', inputError);
|
||||
return res.status(400).json({ error: inputError });
|
||||
}
|
||||
|
||||
const id = await postJob({
|
||||
type: jobType.TORNADO_WITHDRAW,
|
||||
request: req.body,
|
||||
});
|
||||
return res.json({ id });
|
||||
}
|
||||
|
||||
async function miningReward(req, res) {
|
||||
const inputError = getMiningRewardInputError(req.body);
|
||||
if (inputError) {
|
||||
console.log('Invalid input:', inputError);
|
||||
return res.status(400).json({ error: inputError });
|
||||
}
|
||||
|
||||
const id = await postJob({
|
||||
type: jobType.MINING_REWARD,
|
||||
request: req.body,
|
||||
});
|
||||
return res.json({ id });
|
||||
}
|
||||
|
||||
async function miningWithdraw(req, res) {
|
||||
const inputError = getMiningWithdrawInputError(req.body);
|
||||
if (inputError) {
|
||||
console.log('Invalid input:', inputError);
|
||||
return res.status(400).json({ error: inputError });
|
||||
}
|
||||
|
||||
const id = await postJob({
|
||||
type: jobType.MINING_WITHDRAW,
|
||||
request: req.body,
|
||||
});
|
||||
return res.json({ id });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
tornadoWithdraw,
|
||||
miningReward,
|
||||
miningWithdraw,
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
const Web3 = require('web3');
|
||||
const Redis = require('ioredis');
|
||||
const { toBN, fromWei } = require('web3-utils');
|
||||
|
||||
const { setSafeInterval } = require('./utils');
|
||||
const { redisUrl, httpRpcUrl, privateKey, minimumBalance } = require('./config');
|
||||
|
||||
const web3 = new Web3(httpRpcUrl);
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const { address } = web3.eth.accounts.privateKeyToAccount(privateKey);
|
||||
const balance = await web3.eth.getBalance(address);
|
||||
|
||||
if (toBN(balance).lt(toBN(minimumBalance))) {
|
||||
throw new Error(`Not enough balance, less than ${fromWei(minimumBalance)} ETH`);
|
||||
}
|
||||
|
||||
await redis.hset('health', { status: true, error: '' });
|
||||
} catch (e) {
|
||||
console.error('healthWatcher', e.message);
|
||||
await redis.hset('health', { status: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
setSafeInterval(main, 30 * 1000);
|
@ -1,46 +0,0 @@
|
||||
const Redis = require('ioredis');
|
||||
const { redisUrl, offchainOracleAddress, oracleRpcUrl } = require('./config');
|
||||
const { getArgsForOracle, setSafeInterval } = require('./utils');
|
||||
const { toChecksumAddress } = require('web3-utils');
|
||||
const redis = new Redis(redisUrl);
|
||||
const Web3 = require('web3');
|
||||
const web3 = new Web3(
|
||||
new Web3.providers.HttpProvider(oracleRpcUrl, {
|
||||
timeout: 200000, // ms
|
||||
}),
|
||||
);
|
||||
|
||||
const offchainOracleABI = require('../abis/OffchainOracle.abi.json');
|
||||
|
||||
const offchainOracle = new web3.eth.Contract(offchainOracleABI, offchainOracleAddress);
|
||||
const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle();
|
||||
|
||||
const { toBN } = require('web3-utils');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const ethPrices = {};
|
||||
for (let i = 0; i < tokenAddresses.length; i++) {
|
||||
try {
|
||||
const isWrap =
|
||||
toChecksumAddress(tokenAddresses[i]) ===
|
||||
toChecksumAddress('0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643');
|
||||
const price = await offchainOracle.methods.getRateToEth(tokenAddresses[i], isWrap).call();
|
||||
const numerator = toBN(oneUintAmount[i]);
|
||||
const denominator = toBN(10).pow(toBN(18)); // eth decimals
|
||||
const priceFormatted = toBN(price).mul(numerator).div(denominator);
|
||||
|
||||
ethPrices[currencyLookup[tokenAddresses[i]]] = priceFormatted.toString();
|
||||
} catch (e) {
|
||||
console.error('cant get price of ', tokenAddresses[i]);
|
||||
}
|
||||
}
|
||||
|
||||
await redis.hmset('prices', ethPrices);
|
||||
console.log('Wrote following prices to redis', ethPrices);
|
||||
} catch (e) {
|
||||
console.error('priceWatcher error', e);
|
||||
}
|
||||
}
|
||||
|
||||
setSafeInterval(main, 30 * 1000);
|
57
src/queue.js
57
src/queue.js
@ -1,57 +0,0 @@
|
||||
const { v4: uuid } = require('uuid');
|
||||
const Queue = require('bull');
|
||||
const Redis = require('ioredis');
|
||||
const { redisUrl } = require('./config');
|
||||
const { status } = require('./types');
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
const queue = new Queue('proofs', redisUrl, {
|
||||
lockDuration: 300000, // Key expiration time for job locks.
|
||||
lockRenewTime: 30000, // Interval on which to acquire the job lock
|
||||
stalledInterval: 30000, // How often check for stalled jobs (use 0 for never checking).
|
||||
maxStalledCount: 3, // Max amount of times a stalled job will be re-processed.
|
||||
guardInterval: 5000, // Poll interval for delayed jobs and added jobs.
|
||||
retryProcessDelay: 5000, // delay before processing next job in case of internal error.
|
||||
drainDelay: 5, // A timeout for when the queue is in drained state (empty waiting for jobs).
|
||||
});
|
||||
|
||||
async function postJob({ type, request }) {
|
||||
const id = uuid();
|
||||
|
||||
const job = await queue.add(
|
||||
{
|
||||
id,
|
||||
type,
|
||||
status: status.QUEUED,
|
||||
...request, // proof, args, ?contract
|
||||
},
|
||||
{
|
||||
//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);
|
||||
if (!job) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...job.data,
|
||||
failedReason: job.failedReason,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
postJob,
|
||||
getJob,
|
||||
getJobStatus,
|
||||
queue,
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
const { httpRpcUrl, aggregatorAddress } = require('./config');
|
||||
const Web3 = require('web3');
|
||||
const web3 = new Web3(httpRpcUrl);
|
||||
const aggregator = new web3.eth.Contract(require('../abis/Aggregator.abi.json'), aggregatorAddress);
|
||||
const ens = require('eth-ens-namehash');
|
||||
|
||||
class ENSResolver {
|
||||
constructor() {
|
||||
this.addresses = {};
|
||||
}
|
||||
|
||||
async resolve(domains) {
|
||||
if (!Array.isArray(domains)) {
|
||||
domains = [domains];
|
||||
}
|
||||
|
||||
const unresolved = domains.filter(d => !this.addresses[d]);
|
||||
if (unresolved.length) {
|
||||
const resolved = await aggregator.methods.bulkResolve(unresolved.map(ens.hash)).call();
|
||||
for (let i = 0; i < resolved.length; i++) {
|
||||
this.addresses[domains[i]] = resolved[i];
|
||||
}
|
||||
}
|
||||
|
||||
const addresses = domains.map(domain => this.addresses[domain]);
|
||||
return addresses.length === 1 ? addresses[0] : addresses;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ENSResolver;
|
@ -1,41 +0,0 @@
|
||||
const queue = require('./queue')
|
||||
const { netId, tornadoServiceFee, miningServiceFee, instances, redisUrl, rewardAccount } = require('./config')
|
||||
const { version } = require('../package.json')
|
||||
const Redis = require('ioredis')
|
||||
const redis = new Redis(redisUrl)
|
||||
|
||||
async function status(req, res) {
|
||||
const ethPrices = await redis.hgetall('prices')
|
||||
const health = await redis.hgetall('health')
|
||||
|
||||
const { waiting: currentQueue } = await queue.queue.getJobCounts()
|
||||
|
||||
res.json({
|
||||
rewardAccount,
|
||||
instances: instances[`netId${netId}`],
|
||||
netId,
|
||||
ethPrices,
|
||||
tornadoServiceFee,
|
||||
miningServiceFee,
|
||||
version,
|
||||
health,
|
||||
currentQueue,
|
||||
})
|
||||
}
|
||||
|
||||
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 status ? res.json(status) : res.status(400).json({ error: "The job doesn't exist" })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
status,
|
||||
index,
|
||||
getJob,
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
const MerkleTree = require('fixed-merkle-tree');
|
||||
const { redisUrl, wsRpcUrl, minerMerkleTreeHeight, torn, netId } = require('./config');
|
||||
const { poseidonHash2 } = require('./utils');
|
||||
const { toBN } = require('web3-utils');
|
||||
const Redis = require('ioredis');
|
||||
const redis = new Redis(redisUrl);
|
||||
const ENSResolver = require('./resolver');
|
||||
const resolver = new ENSResolver();
|
||||
const Web3 = require('web3');
|
||||
const web3 = new Web3(
|
||||
new Web3.providers.WebsocketProvider(wsRpcUrl, {
|
||||
clientConfig: {
|
||||
maxReceivedFrameSize: 100000000,
|
||||
maxReceivedMessageSize: 100000000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const MinerABI = require('../abis/mining.abi.json');
|
||||
let contract;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let tree, eventSubscription, blockSubscription;
|
||||
|
||||
async function fetchEvents(fromBlock, toBlock) {
|
||||
if (fromBlock <= toBlock) {
|
||||
try {
|
||||
return await contract.getPastEvents('NewAccount', {
|
||||
fromBlock,
|
||||
toBlock,
|
||||
});
|
||||
} catch (error) {
|
||||
const midBlock = (fromBlock + toBlock) >> 1;
|
||||
|
||||
if (midBlock - fromBlock < 2) {
|
||||
throw new Error(`error fetching events: ${error.message}`);
|
||||
}
|
||||
|
||||
const arr1 = await fetchEvents(fromBlock, midBlock);
|
||||
const arr2 = await fetchEvents(midBlock + 1, toBlock);
|
||||
return [...arr1, ...arr2];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function processNewEvent(err, event) {
|
||||
if (err) {
|
||||
throw new Error(`Event handler error: ${err}`);
|
||||
// console.error(err)
|
||||
// return
|
||||
}
|
||||
|
||||
console.log(
|
||||
`New account event
|
||||
Index: ${event.returnValues.index}
|
||||
Commitment: ${event.returnValues.commitment}
|
||||
Nullifier: ${event.returnValues.nullifier}
|
||||
EncAcc: ${event.returnValues.encryptedAccount}`,
|
||||
);
|
||||
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`);
|
||||
rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
async function processNewBlock(err) {
|
||||
if (err) {
|
||||
throw new Error(`Event handler error: ${err}`);
|
||||
// console.error(err)
|
||||
// return
|
||||
}
|
||||
// what if updateRedis takes more than 15 sec?
|
||||
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`);
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
function rebuild() {
|
||||
process.exit(1);
|
||||
// await eventSubscription.unsubscribe()
|
||||
// await blockSubscription.unsubscribe()
|
||||
// setTimeout(init, 3000)
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
console.log('Initializing');
|
||||
const miner = await resolver.resolve(torn.miningV2.address);
|
||||
contract = new web3.eth.Contract(MinerABI, miner);
|
||||
|
||||
const cachedEvents = require(`../cache/accounts_farmer_${netId}.json`);
|
||||
const cachedCommitments = cachedEvents.map(e => toBN(e.commitment));
|
||||
|
||||
const toBlock = await web3.eth.getBlockNumber();
|
||||
const [{ blockNumber: fromBlock }] = cachedEvents.slice(-1);
|
||||
|
||||
const newEvents = await fetchEvents(fromBlock + 1, toBlock);
|
||||
const newCommitments = newEvents
|
||||
.sort((a, b) => a.returnValues.index - b.returnValues.index)
|
||||
.map(e => toBN(e.returnValues.commitment))
|
||||
.filter((item, index, arr) => !index || item !== arr[index - 1]);
|
||||
|
||||
const commitments = cachedCommitments.concat(newCommitments);
|
||||
|
||||
tree = new MerkleTree(minerMerkleTreeHeight, commitments, { hashFunction: poseidonHash2 });
|
||||
await updateRedis();
|
||||
console.log(`Rebuilt tree with ${commitments.length} elements, root: ${tree.root()}`);
|
||||
|
||||
eventSubscription = contract.events.NewAccount({ fromBlock: toBlock + 1 }, processNewEvent);
|
||||
blockSubscription = web3.eth.subscribe('newBlockHeaders', processNewBlock);
|
||||
} catch (e) {
|
||||
console.error('error on init treeWatcher', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
process.on('unhandledRejection', error => {
|
||||
console.error('Unhandled promise rejection', error);
|
||||
process.exit(1);
|
||||
});
|
139
src/utils.js
139
src/utils.js
@ -1,139 +0,0 @@
|
||||
const { instances, netId } = require('./config');
|
||||
const { poseidon } = require('circomlib');
|
||||
const { toBN, toChecksumAddress, BN } = require('web3-utils');
|
||||
|
||||
const TOKENS = {
|
||||
torn: {
|
||||
tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
|
||||
symbol: 'TORN',
|
||||
decimals: 18,
|
||||
},
|
||||
};
|
||||
|
||||
const addressMap = new Map();
|
||||
const instance = instances[`netId${netId}`];
|
||||
|
||||
for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(instance)) {
|
||||
Object.entries(instanceAddress).forEach(([amount, address]) =>
|
||||
addressMap.set(`${netId}_${address}`, {
|
||||
currency,
|
||||
amount,
|
||||
symbol,
|
||||
decimals,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const sleep = ms => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
export function getInstance(address) {
|
||||
const key = `${netId}_${toChecksumAddress(address)}`;
|
||||
if (addressMap.has(key)) {
|
||||
return addressMap.get(key);
|
||||
} else {
|
||||
throw new Error('Unknown contact address');
|
||||
}
|
||||
}
|
||||
|
||||
const poseidonHash = items => toBN(poseidon(items).toString());
|
||||
const poseidonHash2 = (a, b) => poseidonHash([a, b]);
|
||||
|
||||
function setSafeInterval(func, interval) {
|
||||
func()
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
setTimeout(() => setSafeInterval(func, interval), interval);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise that resolves when the source emits specified event
|
||||
*/
|
||||
function when(source, event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
source
|
||||
.once(event, payload => {
|
||||
resolve(payload);
|
||||
})
|
||||
.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getArgsForOracle() {
|
||||
const tokens = {
|
||||
...instances.netId1,
|
||||
...TOKENS,
|
||||
};
|
||||
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 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);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getInstance,
|
||||
setSafeInterval,
|
||||
poseidonHash2,
|
||||
sleep,
|
||||
when,
|
||||
getArgsForOracle,
|
||||
fromDecimals,
|
||||
};
|
166
src/validator.js
166
src/validator.js
@ -1,166 +0,0 @@
|
||||
const { isAddress, toChecksumAddress } = require('web3-utils');
|
||||
const { getInstance } = require('./utils');
|
||||
const { rewardAccount } = require('./config');
|
||||
|
||||
const Ajv = require('ajv');
|
||||
|
||||
const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', isAddress: true };
|
||||
const proofType = { type: 'string', pattern: '^0x[a-fA-F0-9]{512}$' };
|
||||
const encryptedAccountType = { type: 'string', pattern: '^0x[a-fA-F0-9]{392}$' };
|
||||
const bytes32Type = { type: 'string', pattern: '^0x[a-fA-F0-9]{64}$' };
|
||||
const instanceType = { ...addressType, isKnownContract: true };
|
||||
const relayerType = { ...addressType, isFeeRecipient: true };
|
||||
|
||||
const tornadoWithdrawSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
proof: proofType,
|
||||
contract: instanceType,
|
||||
args: {
|
||||
type: 'array',
|
||||
maxItems: 6,
|
||||
minItems: 6,
|
||||
items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['proof', 'contract', 'args'],
|
||||
};
|
||||
|
||||
const miningRewardSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
proof: proofType,
|
||||
args: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
rate: bytes32Type,
|
||||
fee: bytes32Type,
|
||||
instance: instanceType,
|
||||
rewardNullifier: bytes32Type,
|
||||
extDataHash: bytes32Type,
|
||||
depositRoot: bytes32Type,
|
||||
withdrawalRoot: bytes32Type,
|
||||
extData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
relayer: relayerType,
|
||||
encryptedAccount: encryptedAccountType,
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['relayer', 'encryptedAccount'],
|
||||
},
|
||||
account: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
inputRoot: bytes32Type,
|
||||
inputNullifierHash: bytes32Type,
|
||||
outputRoot: bytes32Type,
|
||||
outputPathIndices: bytes32Type,
|
||||
outputCommitment: bytes32Type,
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
'inputRoot',
|
||||
'inputNullifierHash',
|
||||
'outputRoot',
|
||||
'outputPathIndices',
|
||||
'outputCommitment',
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
'rate',
|
||||
'fee',
|
||||
'instance',
|
||||
'rewardNullifier',
|
||||
'extDataHash',
|
||||
'depositRoot',
|
||||
'withdrawalRoot',
|
||||
'extData',
|
||||
'account',
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['proof', 'args'],
|
||||
};
|
||||
|
||||
const miningWithdrawSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
proof: proofType,
|
||||
args: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: bytes32Type,
|
||||
extDataHash: bytes32Type,
|
||||
extData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fee: bytes32Type,
|
||||
recipient: addressType,
|
||||
relayer: relayerType,
|
||||
encryptedAccount: encryptedAccountType,
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['fee', 'relayer', 'encryptedAccount', 'recipient'],
|
||||
},
|
||||
account: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
inputRoot: bytes32Type,
|
||||
inputNullifierHash: bytes32Type,
|
||||
outputRoot: bytes32Type,
|
||||
outputPathIndices: bytes32Type,
|
||||
outputCommitment: bytes32Type,
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
'inputRoot',
|
||||
'inputNullifierHash',
|
||||
'outputRoot',
|
||||
'outputPathIndices',
|
||||
'outputCommitment',
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['amount', 'extDataHash', 'extData', 'account'],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['proof', 'args'],
|
||||
};
|
||||
|
||||
const validateTornadoWithdraw = ajv.compile(tornadoWithdrawSchema);
|
||||
const validateMiningReward = ajv.compile(miningRewardSchema);
|
||||
const validateMiningWithdraw = ajv.compile(miningWithdrawSchema);
|
||||
|
||||
function getInputError(validator, data) {
|
||||
validator(data);
|
||||
if (validator.errors) {
|
||||
const error = validator.errors[0];
|
||||
return `${error.dataPath} ${error.message}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getTornadoWithdrawInputError(data) {
|
||||
return getInputError(validateTornadoWithdraw, data);
|
||||
}
|
||||
|
||||
function getMiningRewardInputError(data) {
|
||||
return getInputError(validateMiningReward, data);
|
||||
}
|
||||
|
||||
function getMiningWithdrawInputError(data) {
|
||||
return getInputError(validateMiningWithdraw, data);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTornadoWithdrawInputError,
|
||||
getMiningRewardInputError,
|
||||
getMiningWithdrawInputError,
|
||||
};
|
350
src/worker.js
350
src/worker.js
@ -1,350 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const Web3 = require('web3');
|
||||
const { toBN, toWei, fromWei, toChecksumAddress } = require('web3-utils');
|
||||
const MerkleTree = require('fixed-merkle-tree');
|
||||
const Redis = require('ioredis');
|
||||
const { GasPriceOracle } = require('gas-price-oracle');
|
||||
const { Utils, Controller } = require('tornado-anonymity-mining');
|
||||
|
||||
// const swapABI = require('../abis/swap.abi.json');
|
||||
const miningABI = require('../abis/mining.abi.json');
|
||||
const tornadoABI = require('../abis/tornadoABI.json');
|
||||
const tornadoProxyABI = require('../abis/tornadoProxyABI.json');
|
||||
const aggregatorAbi = require('../abis/Aggregator.abi.json');
|
||||
const { queue } = require('./queue');
|
||||
const { poseidonHash2, getInstance, fromDecimals, sleep } = require('./utils');
|
||||
const { jobType, status } = require('./constants');
|
||||
const {
|
||||
torn,
|
||||
netId,
|
||||
redisUrl,
|
||||
gasLimits,
|
||||
instances,
|
||||
privateKey,
|
||||
httpRpcUrl,
|
||||
oracleRpcUrl,
|
||||
baseFeeReserve,
|
||||
miningServiceFee,
|
||||
tornadoServiceFee,
|
||||
tornadoGoerliProxy,
|
||||
governanceAddress,
|
||||
aggregatorAddress,
|
||||
} = require('./config');
|
||||
const ENSResolver = require('./resolver');
|
||||
const resolver = new ENSResolver();
|
||||
const { TxManager } = require('tx-manager');
|
||||
|
||||
let web3;
|
||||
let currentTx;
|
||||
let currentJob;
|
||||
let tree;
|
||||
let txManager;
|
||||
let controller;
|
||||
let swap;
|
||||
let minerContract;
|
||||
const redis = new Redis(redisUrl);
|
||||
const redisSubscribe = new Redis(redisUrl);
|
||||
const gasPriceOracle = new GasPriceOracle({ defaultRpc: oracleRpcUrl });
|
||||
|
||||
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 && currentJob && ['MINING_REWARD', 'MINING_WITHDRAW'].includes(currentJob.data.type)) {
|
||||
const { proof, args } = currentJob.data;
|
||||
if (toBN(args.account.inputRoot).eq(toBN(tree.root()))) {
|
||||
console.log('Account root is up to date. Skipping Root Update operation...');
|
||||
return;
|
||||
} else {
|
||||
console.log('Account root is outdated. Starting Root Update operation...');
|
||||
}
|
||||
|
||||
const update = await controller.treeUpdate(args.account.outputCommitment, tree);
|
||||
|
||||
const minerAddress = await resolver.resolve(torn.miningV2.address);
|
||||
const instance = new web3.eth.Contract(miningABI, minerAddress);
|
||||
const data =
|
||||
currentJob.data.type === 'MINING_REWARD'
|
||||
? instance.methods.reward(proof, args, update.proof, update.args).encodeABI()
|
||||
: instance.methods.withdraw(proof, args, update.proof, update.args).encodeABI();
|
||||
await currentTx.replace({
|
||||
to: minerAddress,
|
||||
data,
|
||||
});
|
||||
console.log('replaced pending tx');
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
web3 = new Web3(httpRpcUrl);
|
||||
const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env;
|
||||
txManager = new TxManager({
|
||||
privateKey,
|
||||
rpcUrl: httpRpcUrl,
|
||||
config: {
|
||||
CONFIRMATIONS,
|
||||
MAX_GAS_PRICE,
|
||||
THROW_ON_REVERT: false,
|
||||
BASE_FEE_RESERVE_PERCENTAGE: baseFeeReserve,
|
||||
},
|
||||
});
|
||||
const provingKeys = {
|
||||
treeUpdateCircuit: require('../keys/TreeUpdate.json'),
|
||||
treeUpdateProvingKey: fs.readFileSync('./keys/TreeUpdate_proving_key.bin').buffer,
|
||||
};
|
||||
// controller = new Controller({ provingKeys });
|
||||
// await controller.init();
|
||||
queue.process(processJob);
|
||||
console.log('Worker started');
|
||||
} catch (e) {
|
||||
console.error('error on start worker', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function checkFee({ data }) {
|
||||
if (data.type === jobType.TORNADO_WITHDRAW) {
|
||||
return checkTornadoFee(data);
|
||||
}
|
||||
// return checkMiningFee(data);
|
||||
}
|
||||
|
||||
async function getGasPrice() {
|
||||
const block = await web3.eth.getBlock('latest');
|
||||
|
||||
if (block && block.baseFeePerGas) {
|
||||
const baseFeePerGas = toBN(block.baseFeePerGas);
|
||||
return baseFeePerGas;
|
||||
}
|
||||
|
||||
const { fast } = await gasPriceOracle.gasPrices();
|
||||
const gasPrice = toBN(toWei(fast.toString(), 'gwei'));
|
||||
return gasPrice;
|
||||
}
|
||||
|
||||
async function checkTornadoFee({ args, contract }) {
|
||||
const { currency, amount } = getInstance(contract);
|
||||
const { decimals } = instances[`netId${netId}`][currency];
|
||||
const [fee, refund] = [args[4], args[5]].map(toBN);
|
||||
const gasPrice = await getGasPrice();
|
||||
|
||||
const ethPrice = await redis.hget('prices', currency);
|
||||
const expense = gasPrice.mul(toBN(gasLimits[jobType.TORNADO_WITHDRAW]));
|
||||
|
||||
const feePercent = toBN(fromDecimals(amount, decimals))
|
||||
.mul(toBN(parseInt(tornadoServiceFee * 1e10)))
|
||||
.div(toBN(1e10 * 100));
|
||||
|
||||
let desiredFee;
|
||||
switch (currency) {
|
||||
case 'eth': {
|
||||
desiredFee = expense.add(feePercent);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
desiredFee = expense
|
||||
.add(refund)
|
||||
.mul(toBN(10 ** decimals))
|
||||
.div(toBN(ethPrice));
|
||||
desiredFee = desiredFee.add(feePercent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
'sent fee, desired fee, feePercent',
|
||||
fromWei(fee.toString()),
|
||||
fromWei(desiredFee.toString()),
|
||||
fromWei(feePercent.toString()),
|
||||
);
|
||||
if (fee.lt(desiredFee)) {
|
||||
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.');
|
||||
}
|
||||
}
|
||||
|
||||
// async function checkMiningFee({ args }) {
|
||||
// const gasPrice = await getGasPrice();
|
||||
// const ethPrice = await redis.hget('prices', 'torn');
|
||||
// const isMiningReward = currentJob.data.type === jobType.MINING_REWARD;
|
||||
// const providedFee = isMiningReward ? toBN(args.fee) : toBN(args.extData.fee);
|
||||
//
|
||||
// const expense = gasPrice.mul(toBN(gasLimits[currentJob.data.type]));
|
||||
// const expenseInTorn = expense.mul(toBN(1e18)).div(toBN(ethPrice));
|
||||
// // todo make aggregator for ethPrices and rewardSwap data
|
||||
// const balance = await swap.methods.tornVirtualBalance().call();
|
||||
// const poolWeight = await swap.methods.poolWeight().call();
|
||||
// const expenseInPoints = Utils.reverseTornadoFormula({ balance, tokens: expenseInTorn, poolWeight });
|
||||
// /* eslint-disable */
|
||||
// const serviceFeePercent = isMiningReward
|
||||
// ? toBN(0)
|
||||
// : toBN(args.amount)
|
||||
// .sub(providedFee) // args.amount includes fee
|
||||
// .mul(toBN(parseInt(miningServiceFee * 1e10)))
|
||||
// .div(toBN(1e10 * 100))
|
||||
// /* eslint-enable */
|
||||
// const desiredFee = expenseInPoints.add(serviceFeePercent); // in points
|
||||
// console.log(
|
||||
// 'user provided fee, desired fee, serviceFeePercent',
|
||||
// providedFee.toString(),
|
||||
// desiredFee.toString(),
|
||||
// serviceFeePercent.toString(),
|
||||
// );
|
||||
// if (toBN(providedFee).lt(desiredFee)) {
|
||||
// throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.');
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
async function getProxyContract() {
|
||||
let proxyAddress;
|
||||
if (netId === 5) {
|
||||
proxyAddress = tornadoGoerliProxy;
|
||||
} else {
|
||||
proxyAddress = await resolver.resolve(torn.tornadoRouter.address)
|
||||
}
|
||||
const contract = new web3.eth.Contract(tornadoProxyABI, proxyAddress);
|
||||
|
||||
return {
|
||||
contract,
|
||||
isOldProxy: checkOldProxy(proxyAddress),
|
||||
};
|
||||
}
|
||||
|
||||
function checkOldProxy(address) {
|
||||
const OLD_PROXY = '0x905b63Fff465B9fFBF41DeA908CEb12478ec7601';
|
||||
return toChecksumAddress(address) === toChecksumAddress(OLD_PROXY);
|
||||
}
|
||||
|
||||
async function getTxObject({ data }) {
|
||||
if (data.type === jobType.TORNADO_WITHDRAW) {
|
||||
let { contract, isOldProxy } = await getProxyContract();
|
||||
|
||||
let calldata = contract.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI();
|
||||
|
||||
if (isOldProxy && getInstance(data.contract).currency !== 'eth') {
|
||||
contract = new web3.eth.Contract(tornadoABI, data.contract);
|
||||
calldata = contract.methods.withdraw(data.proof, ...data.args).encodeABI();
|
||||
}
|
||||
|
||||
return {
|
||||
value: data.args[5],
|
||||
to: contract._address,
|
||||
data: calldata,
|
||||
gasLimit: gasLimits['WITHDRAW_WITH_EXTRA'],
|
||||
};
|
||||
} else {
|
||||
const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw';
|
||||
const calldata = minerContract.methods[method](data.proof, data.args).encodeABI();
|
||||
return {
|
||||
to: minerContract._address,
|
||||
data: calldata,
|
||||
gasLimit: gasLimits[data.type],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function isOutdatedTreeRevert(receipt, currentTx) {
|
||||
try {
|
||||
await web3.eth.call(currentTx.tx, receipt.blockNumber);
|
||||
console.log('Simulated call successful');
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.log('Decoded revert reason:', e.message);
|
||||
return (
|
||||
e.message.indexOf('Outdated account merkle root') !== -1 ||
|
||||
e.message.indexOf('Outdated tree update merkle root') !== -1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function processJob(job) {
|
||||
try {
|
||||
if (!jobType[job.data.type]) {
|
||||
throw new Error(`Unknown job type: ${job.data.type}`);
|
||||
}
|
||||
currentJob = job;
|
||||
await updateStatus(status.ACCEPTED);
|
||||
console.log(`Start processing a new ${job.data.type} job #${job.id}`);
|
||||
await submitTx(job);
|
||||
} catch (e) {
|
||||
console.error('processJob', e.message);
|
||||
await updateStatus(status.FAILED);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitTx(job, retry = 0) {
|
||||
await checkFee(job);
|
||||
currentTx = await txManager.createTx(await getTxObject(job));
|
||||
|
||||
if (job.data.type !== jobType.TORNADO_WITHDRAW) {
|
||||
await fetchTree();
|
||||
}
|
||||
|
||||
try {
|
||||
const receipt = await currentTx
|
||||
.send()
|
||||
.on('transactionHash', txHash => {
|
||||
updateTxHash(txHash);
|
||||
updateStatus(status.SENT);
|
||||
})
|
||||
.on('mined', receipt => {
|
||||
console.log('Mined in block', receipt.blockNumber);
|
||||
updateStatus(status.MINED);
|
||||
})
|
||||
.on('confirmations', updateConfirmations);
|
||||
|
||||
if (receipt.status === 1) {
|
||||
await updateStatus(status.CONFIRMED);
|
||||
} else {
|
||||
if (job.data.type !== jobType.TORNADO_WITHDRAW && (await isOutdatedTreeRevert(receipt, currentTx))) {
|
||||
if (retry < 3) {
|
||||
await updateStatus(status.RESUBMITTED);
|
||||
await submitTx(job, retry + 1);
|
||||
} else {
|
||||
throw new Error('Tree update retry limit exceeded');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Submitted transaction failed');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// todo this could result in duplicated error logs
|
||||
// todo handle a case where account tree is still not up to date (wait and retry)?
|
||||
if (
|
||||
job.data.type !== jobType.TORNADO_WITHDRAW &&
|
||||
(e.message.indexOf('Outdated account merkle root') !== -1 ||
|
||||
e.message.indexOf('Outdated tree update merkle root') !== -1)
|
||||
) {
|
||||
if (retry < 5) {
|
||||
await sleep(3000);
|
||||
console.log('Tree is still not up to date, resubmitting');
|
||||
await submitTx(job, retry + 1);
|
||||
} else {
|
||||
throw new Error('Tree update retry limit exceeded');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Revert by smart contract ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTxHash(txHash) {
|
||||
console.log(`A new successfully sent tx ${txHash}`);
|
||||
currentJob.data.txHash = txHash;
|
||||
await currentJob.update(currentJob.data);
|
||||
}
|
||||
|
||||
async function updateConfirmations(confirmations) {
|
||||
console.log(`Confirmations count ${confirmations}`);
|
||||
currentJob.data.confirmations = confirmations;
|
||||
await currentJob.update(currentJob.data);
|
||||
}
|
||||
|
||||
async function updateStatus(status) {
|
||||
console.log(`Job status updated ${status}`);
|
||||
currentJob.data.status = status;
|
||||
await currentJob.update(currentJob.data);
|
||||
}
|
||||
|
||||
start();
|
Loading…
Reference in New Issue
Block a user