validation

This commit is contained in:
Alexey 2020-09-28 17:54:54 +03:00
parent c4cf2863e3
commit 5a94966e98
9 changed files with 4378 additions and 5453 deletions

@ -59,3 +59,13 @@ tornado.cash UI from submitting your request over http connection
Disclaimer:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## New relayer architecture
1. TreeWatcher module keeps track of Account Tree changes and automatically caches the actual state in Redis
2. Server module is Express.js instance to accepts http requests
3. Controller contains handlers for the Server endpoints. It validates input data and put a Job to Queue
4. Queue module is used by Controller to put and get Job from queue (bull wrapper)
5. Status module contains handler to get a Job status. It's used by UI for pull updates
6. Validate contains validation logic for all endpoints
7. Worker is the main module that gets a Job from queue and processes it

@ -151,5 +151,5 @@ module.exports = {
maxGasPrice: process.env.MAX_GAS_PRICE || 200,
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000,
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20
rewardAccount: '0x0000000000000000000000000000000000000000',
}

5385
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -11,6 +11,7 @@
"author": "tornado.cash",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.5",
"bull": "^3.12.1",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#5beb6aee94923052faeecea40135d45b6ce6172c",
"dotenv": "^8.2.0",

@ -1,8 +1,8 @@
const { getWithdrawInputError } = require('./validate')
const { getTornadoWithdrawInputError } = require('./validate')
const { postJob } = require('./queue')
async function tornadoWithdraw(req, res) {
const inputError = getWithdrawInputError(req.body)
const inputError = getTornadoWithdrawInputError(req.body)
if (inputError) {
console.log('Invalid input:', inputError)
return res.status(400).json({ error: inputError })

@ -48,6 +48,7 @@ async function processNewBlock(err) {
// console.error(err)
// return
}
// what if updateRedis takes more than 15 sec?
await updateRedis()
}

@ -1,83 +1,191 @@
const { isHexStrict } = require('web3-utils')
const { isAddress } = require('web3-utils')
const { getInstance } = require('./utils')
const { rewardAccount } = require('../config')
function getProofError(proof) {
if (!proof) {
return 'The proof is empty'
}
const Ajv = require('ajv')
const ajv = new Ajv({ format: 'fast' })
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]}`
ajv.addKeyword('isAddress', {
validate: (schema, data) => {
try {
return isAddress(data)
} catch (e) {
return false
}
if (args[i].length !== 2 + expectedLengths * 20) {
return `Signal ${i} has invalid length: ${args[i]}`
},
errors: true
})
ajv.addKeyword('isKnownContract', {
validate: (schema, data) => {
try {
return getInstance(data) !== null
} catch (e) {
return false
}
}
},
errors: true
})
ajv.addKeyword('isForFee', {
validate: (schema, data) => {
try {
return rewardAccount === data
} catch (e) {
return false
}
},
errors: true
})
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 = JSON.parse(JSON.stringify(addressType))
instanceType.isKnownContract = true
const relayerType = JSON.parse(JSON.stringify(addressType))
relayerType.isForFee = true
const tornadoWithdrawSchema = {
type: 'object',
properties: {
proof: proofType,
contract: instanceType,
args: {
type: 'array',
maxItems: 6,
minItems: 6,
uniqueItems: true,
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,
fee: bytes32Type,
extDataHash: bytes32Type,
extData: {
type: 'object',
properties: {
recipient: addressType,
relayer: relayerType,
encryptedAccount: encryptedAccountType
},
additionalProperties: false,
required: ['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', 'fee', '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 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 getTornadoWithdrawInputError(data) {
return getInputError(validateTornadoWithdraw, data)
}
function getRewardAddressError(address) {
if (address.toLowerCase() !== rewardAccount.toLowerCase()) {
return 'This proof is for different relayer'
}
return null
function getMiningRewardInputError(data) {
return getInputError(validateMiningReward, data)
}
function getWithdrawInputError(input) {
return getProofError(input.proof) || getArgsError(input.args, [32, 32, 20, 20, 32, 32]) || getContractError(input.contract) || getRewardAddressError(input.args[3])
function getMiningWithdrawInputError(data) {
return getInputError(validateMiningWithdraw, data)
}
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,
getTornadoWithdrawInputError,
getMiningRewardInputError,
getMiningWithdrawInputError
}

72
testValidation.js Normal file

@ -0,0 +1,72 @@
const {
getTornadoWithdrawInputError,
getMiningRewardInputError,
getMiningWithdrawInputError
} = require('./src/validate')
const withdrawData = {
proof:
'0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6',
args: [
'0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3',
'0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d',
'0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5',
'0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5',
'0x000000000000000000000000000000000000000000000000058d15e176280000',
'0x0000000000000000000000000000000000000000000000000000000000000000'
],
contract: '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f'
}
const rewardData = {
proof:
'0x2e0f4c76b35ce3275bf57492cbe12ddc76fae4eabdbeaacdcc7cd5255d0abb2325bd80b2a867f9c1bab854de5d7c443a18eb9ad796943dd53c30c04e8f0a37ae164916c932776b3c28dd49808a5d5e1648d8bc9006b2386096b88757644ce8f102f7e2f1505bb66385a1d53a101922a17d8ab653694dedd7d150ec71d543202e0f0a67e5d59904d75af1c52bef4dfac0a302c2beb2ca3bb29b6bbbe1038368702e5ba8d6d829d74968a94e321cc91cccbc0654f5df6460a0a6ad73b06c42b7d1289ff36655fc7106b5538bd2c6617dd0c313919331e63bcb4de9c9b45dc2207b098a5729efbecf79a4cab39ade3c99e5772bfbe5ae75d932facbf9e0910a34ae',
args: {
rate: '0x000000000000000000000000000000000000000000000000000000000000000a',
fee: '0x0000000000000000000000000000000000000000000000000000000000000000',
instance: '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
rewardNullifier: '0x08fdc416b85c76d246925994ae0c0df539789fd1669c45b57104907c7ef8b0b5',
extDataHash: '0x006c5f12c20933beab10cfffab31ea0c9d736cf9aa868ee29eed3047d4ea4c2e',
depositRoot: '0x0405962838a47fb25ffd75d80d53b268654a06bc1bdde7e5ad94c675c2f2f0ff',
withdrawalRoot: '0x1cd83f5df5dbc826fecbf6be87f05db9c9dc617a3f1b1f3a421b1335c1ff7dbf',
extData: {
relayer: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5',
encryptedAccount:
'0x6a8494fca4c433ef323d03f0db3fede90c3d2c6f216d73345ffc77ceec79622f327a83c4254063a3027620c262835e335fa32c33600a70547a53b2aa311d3ff35cf943e8f9e8f321f60d4266f680e0606a5837d78deb4d74c8b4fa3e9b67414513c71b73e38995cd8d57fd08aa9e135b342cecaf4128d4cfbb26148022e7a87da8b2423440b62034be202a6a48b45baa9736def6455771b442baaf2358fc52aa6c1d14a9a452b064d280fafd69f2a3ba416c10c1d8276f1c3810c664b24e0f1eefc75d63'
},
account: {
inputRoot: '0x22e875e5e54d8569fb40d0c568984e87b4c97da6383d8d8a334a79e22b48fd54',
inputNullifierHash: '0x24be972a00e3938a58f44ea6f8ead271ecdd6ab2cab42d1910fb7190b5816188',
outputRoot: '0x04a3cd1e37487dcee5da51cbce4245742903262a5824aef77fb7aff84a3cb053',
outputPathIndices: '0x0000000000000000000000000000000000000000000000000000000000000000',
outputCommitment: '0x0ae58c1605312bd42fffdfc41d5e0f9a364ad458717c522bf9338068ab258601'
}
}
}
const miningWithdrawData = {
proof:
'0x087c02cdc5946b44f295e1adb8b65341708fe43854e44f05f205da6e46e2e4c4248b2dd5ee30236e7be2ea657265765b4e43dae263d67ff43190bb806faaafc10dd0a771f9d589b5061ddf0a713f27fc0b496d1b136dc4e98838b88f60efb072087c3018fa5c25b1f78b4bb968291b9afa3966d976e961d0a86719a8e07d771209dad29620f3bc2fc21c00510749a19e7ff369ade6b9fd1a7f05b74e70faee771fd839c710bd983927c9d3d5f39bb5e839a2ece19e899c4d50a91b29d5ac3f1a0e8faf7eeb2f6f672561bfba39bcb1d851f6c97d5c14b7fce6661cf315af3468119855a426fc4df511e848011bcdb704369deba20541a7651ab4d5813a60c056',
args: {
amount: '0x000000000000000000000000000000000000000000000000000000000000000f',
fee: '0x0000000000000000000000000000000000000000000000000000000000000000',
extDataHash: '0x00d95a201b89061613b5bc539bcf8fdee63a400ea80f1f5e813d6aacfee3ec67',
extData: {
recipient: '0xf17f52151ebef6c7334fad080c5704d77216b732',
relayer: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5',
encryptedAccount:
'0x4bd7f84edab796b390181d8b1dd850c418c8b3fe41d63b9677b7b99a2fadc505dcc70df336a42847dc00fa39175d16ddfec0d80dc166282e024b5371f561467651ed94e71524fa2e365a8330b053d5cff7c3bcc3564b335fb9e74fb805a3a6e760b811db60e5d6b4e154376196c3cb61457bac6d5ea804f63208a389555cde72f40ab1b94705e728f692e699fc441504b9df34390b3992a1a1eac160dcf0df0b5c5a9ec9cd6c0c8f5f8aa11627fdf2b3bedece5836e9ca38b09d70ff7ba06702971d245d'
},
account: {
inputRoot: '0x1a756aeee7f7d05f276b20c8ca83150e110e1a436c2d959e501ab306420ab536',
inputNullifierHash: '0x0dc8ea0330171a1f868ef5f3f9f92e919d7be754846f6145c5e7819e87738e65',
outputRoot: '0x0d9d85371bd8c941400ae54815491799e98d1f335a9d263e41f0b81f22b55aa8',
outputPathIndices: '0x0000000000000000000000000000000000000000000000000000000000000001',
outputCommitment: '0x1ebd38a8bc53f47386687386397c8b5cefd33d55341b62a2a576b39d9bcec57c'
}
}
}
console.log(getTornadoWithdrawInputError(withdrawData))
console.log(getMiningRewardInputError(rewardData))
console.log(getMiningWithdrawInputError(miningWithdrawData))

4118
yarn.lock Normal file

File diff suppressed because it is too large Load Diff