mirror of
https://github.com/tornadocash/tornado-relayer.git
synced 2024-10-01 08:25:37 -04:00
init
This commit is contained in:
commit
c349ed8bb6
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.vscode
|
||||
node_modules/
|
||||
env.json
|
||||
|
40
README.md
Normal file
40
README.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Relayer for Tornado mixer
|
||||
## Setup
|
||||
1. `npm i`
|
||||
2. `cp env.json.example env.json`
|
||||
3. Modify `env.json` as needed
|
||||
|
||||
## Run
|
||||
1. `node index.js`
|
||||
2. `curl -X POST -H 'content-type:application/json' --data '<PROOF>' http://127.0.0.1:8000/relay`
|
||||
Relayer should return a transaction hash.
|
||||
|
||||
## Proof example
|
||||
```json
|
||||
{
|
||||
"pi_a":[
|
||||
"0x0ed9b1afc791a551f5baa2f84786963b1463ca3f7c68eb0de3b267e6cb491f05",
|
||||
"0x1335f2af3c71e442fd82f63f8f1c605ca2612b8d0fa22b4cbd1239cca839aa3d"
|
||||
],
|
||||
"pi_b":[
|
||||
[
|
||||
"0x000189f7f1067a768d116cd86980eae6963dd9bc6c1f8204ceacf90a94f60d81",
|
||||
"0x1abb4b71da0efa67cbc76a97ac360826b17a88f07bd89151258bf076474a4804"
|
||||
],
|
||||
[
|
||||
"0x0526b509ba2cda2b21b09401d70d23ea0225be4fdaa9097af842ff6783d1e0f4",
|
||||
"0x15b11f9f5441adeea61534105902170a409b228e159fe7428abf6e863fc05273"
|
||||
]
|
||||
],
|
||||
"pi_c":[
|
||||
"0x2cd9a2305827f7da64aa1a3136c11ae1d3d7b3cb69832d8c04ab39d8b9393cda",
|
||||
"0x2090cd3f9d09d66ca4e1e9bed2c72d5fa174b47599cb47e572324b1a98a3cb7a"
|
||||
],
|
||||
"publicSignals":[
|
||||
"0x1e8a85160889dfb5c03a8e2a6cca18b4c476c0b486003e9ed666a33e04114658",
|
||||
"0x00bfb0befe19eac571ecaf7858e50d70273fbe2952cc8431f59399bb28665796",
|
||||
"0x00000000000000000000000003ebd0748aa4d1457cf479cce56309641e0a98f5",
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
]
|
||||
}
|
||||
```
|
11
env.json.example
Normal file
11
env.json.example
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"netId": 42,
|
||||
"rpcUrl": "https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51",
|
||||
"privateKey": "",
|
||||
"mixerAddress": "0x30AF2e92263C5387A8A689322BbfE60b6B0855C4",
|
||||
"defaultGasPrice": 1,
|
||||
"gasOracleUrls": [
|
||||
"https://www.etherchain.org/api/gasPriceOracle",
|
||||
"https://gasprice.poa.network/"
|
||||
]
|
||||
}
|
68
index.js
Normal file
68
index.js
Normal file
@ -0,0 +1,68 @@
|
||||
const { fetchGasPrice, isValidProof } = require('./utils')
|
||||
const { numberToHex, toWei, toHex } = require('web3-utils')
|
||||
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
|
||||
const { netId, rpcUrl, privateKey, mixerAddress, defaultGasPrice } = require('./env.json')
|
||||
|
||||
const Web3 = require('web3')
|
||||
const 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
|
||||
|
||||
const mixerABI = require('./mixerABI.json')
|
||||
const mixer = new web3.eth.Contract(mixerABI, mixerAddress)
|
||||
const gasPrices = { fast: defaultGasPrice }
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
// just for testing purposes
|
||||
res.send(`Tornado mixer relayer. Gas Price is ${JSON.stringify(gasPrices)}`)
|
||||
})
|
||||
|
||||
app.post('/relay', async (req, resp) => {
|
||||
const { valid , reason } = isValidProof(req.body)
|
||||
if (!valid) {
|
||||
console.log('Proof is invalid:', reason)
|
||||
return resp.status(400).send('Proof is invalid')
|
||||
}
|
||||
|
||||
let { pi_a, pi_b, pi_c, publicSignals } = req.body
|
||||
|
||||
// TODO
|
||||
// if (bigInt(proof.publicSignals[3]) < getMinimumFee()) {
|
||||
// resp.status(403).send('Fee is too low')
|
||||
// }
|
||||
|
||||
try {
|
||||
const nullifier = publicSignals[1]
|
||||
const isSpent = await mixer.methods.isSpent(nullifier).call()
|
||||
if (isSpent) {
|
||||
throw new Error('The note has been spent')
|
||||
}
|
||||
const gas = await mixer.methods.withdraw(pi_a, pi_b, pi_c, publicSignals).estimateGas()
|
||||
const result = mixer.methods.withdraw(pi_a, pi_b, pi_c, publicSignals).send({
|
||||
gas: numberToHex(gas + 50000),
|
||||
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
|
||||
// TODO: nonce
|
||||
})
|
||||
result.once('transactionHash', function(hash){
|
||||
resp.send({ transaction: hash })
|
||||
}).on('error', function(e){
|
||||
console.log(e)
|
||||
resp.status(400).send('Proof is malformed')
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
resp.status(400).send('Proof is malformed or spent')
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(8000)
|
||||
|
||||
if (netId === 1) {
|
||||
fetchGasPrice({ gasPrices })
|
||||
console.log('Gas price oracle started.')
|
||||
}
|
332
mixerABI.json
Normal file
332
mixerABI.json
Normal file
@ -0,0 +1,332 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "filled_subtrees",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "transferValue",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "roots",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "commitments",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "zeros",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "levels",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "left",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "right",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "hashLeftRight",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mimc_hash",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "pure",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "next_index",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "current_root",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "root",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "isKnownRoot",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getLastRoot",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "nullifiers",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_verifier",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_transferValue",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_merkleTreeHeight",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"name": "_emptyElement",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "commitment",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Deposit",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "nullifier",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "fee",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Withdraw",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "leaf",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "leaf_index",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"name": "LeafAdded",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "commitment",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "deposit",
|
||||
"outputs": [],
|
||||
"payable": true,
|
||||
"stateMutability": "payable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": "uint256[2]"
|
||||
},
|
||||
{
|
||||
"name": "b",
|
||||
"type": "uint256[2][2]"
|
||||
},
|
||||
{
|
||||
"name": "c",
|
||||
"type": "uint256[2]"
|
||||
},
|
||||
{
|
||||
"name": "input",
|
||||
"type": "uint256[4]"
|
||||
}
|
||||
],
|
||||
"name": "withdraw",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "nullifier",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "isSpent",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
4747
package-lock.json
generated
Normal file
4747
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "relay",
|
||||
"version": "1.0.0",
|
||||
"description": "Relayer for Tornado mixer. https://tornado.cash",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"web3": "^1.0.0-beta.55",
|
||||
"web3-utils": "^1.0.0"
|
||||
}
|
||||
}
|
81
utils.js
Normal file
81
utils.js
Normal file
@ -0,0 +1,81 @@
|
||||
const fetch = require('node-fetch')
|
||||
const { isHexStrict } = require('web3-utils')
|
||||
const { gasOracleUrls } = require('./env.json')
|
||||
|
||||
async function fetchGasPrice({ gasPrices, oracleIndex = 0 }) {
|
||||
oracleIndex = (oracleIndex + 1) % gasOracleUrls.length
|
||||
try {
|
||||
const response = await fetch(gasOracleUrls[oracleIndex])
|
||||
if (response.status === 200) {
|
||||
const json = await response.json()
|
||||
|
||||
if (json.slow) {
|
||||
gasPrices.low = Number(json.slow)
|
||||
}
|
||||
if (json.safeLow) {
|
||||
gasPrices.low = Number(json.safeLow)
|
||||
}
|
||||
if (json.standard) {
|
||||
gasPrices.standard = Number(json.standard)
|
||||
}
|
||||
if (json.fast) {
|
||||
gasPrices.fast = Number(json.fast)
|
||||
}
|
||||
} else {
|
||||
throw Error('Fetch gasPrice failed')
|
||||
}
|
||||
setTimeout(() => fetchGasPrice({ gasPrices, oracleIndex }), 15000)
|
||||
} catch (e) {
|
||||
setTimeout(() => fetchGasPrice({ gasPrices, oracleIndex }), 15000)
|
||||
}
|
||||
}
|
||||
|
||||
function isValidProof(proof) {
|
||||
// validator expects `websnarkUtils.toSolidityInput(proof)` output
|
||||
|
||||
if (!(proof.pi_a && proof.pi_b && proof.pi_c && proof.publicSignals)) {
|
||||
return { valid: false, reason: 'One of inputs is empty. There must be pi_a, pi_b, pi_c and publicSignals' }
|
||||
}
|
||||
|
||||
Object.keys(proof).forEach(key => {
|
||||
if (!Array.isArray(proof[key])) {
|
||||
return { valid: false, reason: `Corrupted ${key}` }
|
||||
}
|
||||
if (key === 'pi_b') {
|
||||
if (!Array.isArray(proof[key][0]) || !Array.isArray(proof[key][1])) {
|
||||
return { valid: false, reason: `Corrupted ${key}` }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (proof.pi_a.length !== 2) {
|
||||
return { valid: false, reason: 'Corrupted pi_a' }
|
||||
}
|
||||
|
||||
if (proof.pi_b.length !== 2 || proof.pi_b[0].length !== 2 || proof.pi_b[1].length !== 2) {
|
||||
return { valid: false, reason: 'Corrupted pi_b' }
|
||||
}
|
||||
|
||||
if (proof.pi_c.length !== 2) {
|
||||
return { valid: false, reason: 'Corrupted pi_c' }
|
||||
}
|
||||
|
||||
if (proof.publicSignals.length !== 4) {
|
||||
return { valid: false, reason: 'Corrupted publicSignals' }
|
||||
}
|
||||
|
||||
for (let [key, input] of Object.entries(proof)) {
|
||||
if (key === 'pi_b') {
|
||||
input = input[0].concat(input[1])
|
||||
}
|
||||
|
||||
for (let i = 0; i < input.length; i++ ) {
|
||||
if (!isHexStrict(input[i]) || input[i].length !== 66) {
|
||||
return { valid: false, reason: `Corrupted ${key}` }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
module.exports = { fetchGasPrice, isValidProof }
|
Loading…
Reference in New Issue
Block a user