This commit is contained in:
Alexey 2019-07-18 15:43:26 +03:00
commit c349ed8bb6
8 changed files with 5300 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.vscode
node_modules/
env.json

40
README.md Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View 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
View 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 }