From df25eeadefb26194e8de963d98ca3e5f250a0cf2 Mon Sep 17 00:00:00 2001 From: Leonid Tyurin Date: Mon, 9 Jan 2023 20:21:28 +0400 Subject: [PATCH] Add internal account validation (#135) * Add internal account validation * Fix response value * Add supportId header for validation request --- CONFIGURATION.md | 4 +- zp-relayer/config.ts | 2 + .../test/worker-tests/poolWorker.test.ts | 2 +- zp-relayer/utils/helpers.ts | 2 +- zp-relayer/validateTx.ts | 73 +++++++++++++------ zp-relayer/workers/poolTxWorker.ts | 13 ++-- zp-relayer/workers/sentTxWorker.ts | 10 ++- 7 files changed, 73 insertions(+), 33 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index f734f33..181564f 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -35,4 +35,6 @@ | PERMIT_DEADLINE_THRESHOLD_INITIAL | Minimum time threshold in seconds for permit signature deadline to be valid (before initial transaction submission) | integer | | PERMIT_DEADLINE_THRESHOLD_RESEND | Minimum time threshold in seconds for permit signature deadline to be valid (for re-send attempts) | integer | | RELAYER_REQUIRE_TRACE_ID | If set to `true`, then requests to relayer (except `/info`, `/version`, `/params/hash/tree`, `/params/hash/tx`) without `zkbob-support-id` header will be rejected. | boolean | -| RELAYER_REQUIRE_HTTPS | If set to `true`, then RPC URL(s) must be in HTTPS format. HTTP RPC URL(s) should be used in test environment only. | boolean | \ No newline at end of file +| RELAYER_REQUIRE_HTTPS | If set to `true`, then RPC URL(s) must be in HTTPS format. HTTP RPC URL(s) should be used in test environment only. | boolean | +| RELAYER_SCREENER_URL | Screener service URL | URL | +| RELAYER_SCREENER_TOKEN | Authorization token for screener service | string | diff --git a/zp-relayer/config.ts b/zp-relayer/config.ts index e201681..6d6b26b 100644 --- a/zp-relayer/config.ts +++ b/zp-relayer/config.ts @@ -47,6 +47,8 @@ const config = { .map(s => parseInt(s, 10)), requireTraceId: process.env.RELAYER_REQUIRE_TRACE_ID === 'true', requireHTTPS: process.env.RELAYER_REQUIRE_HTTPS === 'true', + screenerUrl: process.env.RELAYER_SCREENER_URL || null, + screenerToken: process.env.RELAYER_SCREENER_TOKEN || null, } export default config diff --git a/zp-relayer/test/worker-tests/poolWorker.test.ts b/zp-relayer/test/worker-tests/poolWorker.test.ts index 952f494..f8f32bc 100644 --- a/zp-relayer/test/worker-tests/poolWorker.test.ts +++ b/zp-relayer/test/worker-tests/poolWorker.test.ts @@ -99,7 +99,7 @@ describe('poolWorker', () => { gasPriceService.stop() }) - async function expectJobFinished(job: Job) { + async function expectJobFinished(job: Job) { const [[initialHash, sentId]] = await job.waitUntilFinished(poolQueueEvents) expect(initialHash.length).eq(66) diff --git a/zp-relayer/utils/helpers.ts b/zp-relayer/utils/helpers.ts index d0cc8f4..6700273 100644 --- a/zp-relayer/utils/helpers.ts +++ b/zp-relayer/utils/helpers.ts @@ -169,7 +169,7 @@ export function waitForFunds( address: string, cb: (balance: BN) => void, minimumBalance: BN, - timeout: number, + timeout: number ) { return promiseRetry( async retry => { diff --git a/zp-relayer/validateTx.ts b/zp-relayer/validateTx.ts index 7911d72..acdc44a 100644 --- a/zp-relayer/validateTx.ts +++ b/zp-relayer/validateTx.ts @@ -10,7 +10,7 @@ import TokenAbi from './abi/token-abi.json' import { web3 } from './services/web3' import { numToHex, unpackSignature } from './utils/helpers' import { recoverSaltedPermit } from './utils/EIP712SaltedPermit' -import { ZERO_ADDRESS } from './utils/constants' +import { ZERO_ADDRESS, TRACE_ID } from './utils/constants' import { TxPayload } from './queue/poolTxQueue' import { getTxProofField, parseDelta } from './utils/proofInputs' import type { PoolState } from './state/PoolState' @@ -207,7 +207,39 @@ async function checkRoot(proofIndex: BN, proofRoot: string, state: PoolState) { return null } -export async function validateTx({ txType, rawMemo, txProof, depositSignature }: TxPayload, pool: Pool) { +async function checkScreener(address: string, traceId?: string) { + if (config.screenerUrl === null || config.screenerToken === null) { + return null + } + + const ACC_VALIDATION_FAILED = 'Internal account validation failed' + + const headers: Record = { + 'Content-type': 'application/json', + 'Authorization': `Bearer ${config.screenerToken}`, + } + + if (traceId) headers[TRACE_ID] = traceId + + try { + const rawResponse = await fetch(config.screenerUrl, { + method: 'POST', + headers, + body: JSON.stringify({ address }), + }) + const response = await rawResponse.json() + if (response.result === true) { + return new TxValidationError(ACC_VALIDATION_FAILED) + } + } catch (e) { + logger.error('Request to screener failed', { error: (e as Error).message }) + return new TxValidationError(ACC_VALIDATION_FAILED) + } + + return null +} + +export async function validateTx({ txType, rawMemo, txProof, depositSignature }: TxPayload, pool: Pool, traceId?: string) { const buf = Buffer.from(rawMemo, 'hex') const txData = getTxData(buf, txType) @@ -223,44 +255,41 @@ export async function validateTx({ txType, rawMemo, txProof, depositSignature }: fee.toString(10) ) - // prettier-ignore - await checkAssertion(() => checkRoot( - delta.transferIndex, - root, - pool.optimisticState, - )) + await checkAssertion(() => checkRoot(delta.transferIndex, root, pool.optimisticState)) await checkAssertion(() => checkNullifier(nullifier, pool.state.nullifiers)) await checkAssertion(() => checkNullifier(nullifier, pool.optimisticState.nullifiers)) await checkAssertion(() => checkTransferIndex(toBN(pool.optimisticState.getNextIndex()), delta.transferIndex)) - await checkAssertion(() => checkFee(fee)) - - if (txType === TxType.WITHDRAWAL) { - const { nativeAmount, receiver } = txData as WithdrawTxData - const receiverAddress = web3.utils.bytesToHex(Array.from(receiver)) - logger.info('Withdraw address: %s', receiverAddress) - await checkAssertion(() => checkNonZeroWithdrawAddress(receiverAddress)) - await checkAssertion(() => checkNativeAmount(toBN(nativeAmount))) - } - await checkAssertion(() => checkProof(txProof, (p, i) => pool.verifyProof(p, i))) const tokenAmountWithFee = delta.tokenAmount.add(fee) await checkAssertion(() => checkTxSpecificFields(txType, tokenAmountWithFee, delta.energyAmount)) - const requiredTokenAmount = tokenAmountWithFee.mul(pool.denominator) let userAddress = ZERO_ADDRESS - if (txType === TxType.DEPOSIT || txType === TxType.PERMITTABLE_DEPOSIT) { + + if (txType === TxType.WITHDRAWAL) { + const { nativeAmount, receiver } = txData as WithdrawTxData + userAddress = web3.utils.bytesToHex(Array.from(receiver)) + logger.info('Withdraw address: %s', userAddress) + await checkAssertion(() => checkNonZeroWithdrawAddress(userAddress)) + await checkAssertion(() => checkNativeAmount(toBN(nativeAmount))) + } else if (txType === TxType.DEPOSIT || txType === TxType.PERMITTABLE_DEPOSIT) { + const requiredTokenAmount = tokenAmountWithFee.mul(pool.denominator) userAddress = await getRecoveredAddress(txType, nullifier, txData, requiredTokenAmount, depositSignature) logger.info('Deposit address: %s', userAddress) await checkAssertion(() => checkDepositEnoughBalance(userAddress, requiredTokenAmount)) } + + const limits = await pool.getLimitsFor(userAddress) + await checkAssertion(() => checkLimits(limits, delta.tokenAmount)) + if (txType === TxType.PERMITTABLE_DEPOSIT) { const { deadline } = txData as PermittableDepositTxData logger.info('Deadline: %s', deadline) await checkAssertion(() => checkDeadline(toBN(deadline), config.permitDeadlineThresholdInitial)) } - const limits = await pool.getLimitsFor(userAddress) - await checkAssertion(() => checkLimits(limits, delta.tokenAmount)) + if (txType === TxType.DEPOSIT || txType === TxType.PERMITTABLE_DEPOSIT || txType === TxType.WITHDRAWAL) { + await checkAssertion(() => checkScreener(userAddress, traceId)) + } } diff --git a/zp-relayer/workers/poolTxWorker.ts b/zp-relayer/workers/poolTxWorker.ts index 531a9b9..b7ec807 100644 --- a/zp-relayer/workers/poolTxWorker.ts +++ b/zp-relayer/workers/poolTxWorker.ts @@ -21,7 +21,7 @@ import { TxValidationError } from '@/validateTx' export async function createPoolTxWorker( gasPrice: GasPrice, - validateTx: (tx: TxPayload, pool: Pool) => Promise, + validateTx: (tx: TxPayload, pool: Pool, traceId?: string) => Promise, mutex: Mutex, redis: Redis ) { @@ -53,7 +53,7 @@ export async function createPoolTxWorker( for (const tx of txs) { const { gas, amount, rawMemo, txType, txProof } = tx - await validateTx(tx, pool) + await validateTx(tx, pool, traceId) const { data, commitIndex, rootAfter } = await processTx(tx) @@ -141,10 +141,11 @@ export async function createPoolTxWorker( const poolTxWorker = new Worker( TX_QUEUE_NAME, - job => withErrorLog( - withMutex(mutex, () => poolTxWorkerProcessor(job)), - [TxValidationError] - ), + job => + withErrorLog( + withMutex(mutex, () => poolTxWorkerProcessor(job)), + [TxValidationError] + ), WORKER_OPTIONS ) diff --git a/zp-relayer/workers/sentTxWorker.ts b/zp-relayer/workers/sentTxWorker.ts index f2f6f76..28cc985 100644 --- a/zp-relayer/workers/sentTxWorker.ts +++ b/zp-relayer/workers/sentTxWorker.ts @@ -7,8 +7,14 @@ import config from '@/config' import { pool } from '@/pool' import { web3, web3Redundant } from '@/services/web3' import { logger } from '@/services/appLogger' -import { GasPrice, EstimationType, chooseGasPriceOptions, addExtraGasPrice, getMaxRequiredGasPrice } from '@/services/gas-price' -import { buildPrefixedMemo, waitForFunds, withErrorLog, withLoop, withMutex } from '@/utils/helpers' +import { + GasPrice, + EstimationType, + chooseGasPriceOptions, + addExtraGasPrice, + getMaxRequiredGasPrice, +} from '@/services/gas-price' +import { buildPrefixedMemo, withErrorLog, withLoop, withMutex } from '@/utils/helpers' import { OUTPLUSONE, SENT_TX_QUEUE_NAME } from '@/utils/constants' import { isGasPriceError, isInsufficientBalanceError, isSameTransactionError } from '@/utils/web3Errors' import { SendAttempt, SentTxPayload, sentTxQueue, SentTxResult, SentTxState } from '@/queue/sentTxQueue'