Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add internal account validation #135

Merged
merged 3 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| 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 |
2 changes: 2 additions & 0 deletions zp-relayer/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion zp-relayer/test/worker-tests/poolWorker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('poolWorker', () => {
gasPriceService.stop()
})

async function expectJobFinished(job: Job<TxPayload[], PoolTxResult[]>) {
async function expectJobFinished(job: Job<BatchTx, PoolTxResult[]>) {
const [[initialHash, sentId]] = await job.waitUntilFinished(poolQueueEvents)
expect(initialHash.length).eq(66)

Expand Down
2 changes: 1 addition & 1 deletion zp-relayer/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export function waitForFunds(
address: string,
cb: (balance: BN) => void,
minimumBalance: BN,
timeout: number,
timeout: number
) {
return promiseRetry(
async retry => {
Expand Down
73 changes: 51 additions & 22 deletions zp-relayer/validateTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, string> = {
'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)

Expand All @@ -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))
}
}
13 changes: 7 additions & 6 deletions zp-relayer/workers/poolTxWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { TxValidationError } from '@/validateTx'

export async function createPoolTxWorker<T extends EstimationType>(
gasPrice: GasPrice<T>,
validateTx: (tx: TxPayload, pool: Pool) => Promise<void>,
validateTx: (tx: TxPayload, pool: Pool, traceId?: string) => Promise<void>,
mutex: Mutex,
redis: Redis
) {
Expand Down Expand Up @@ -53,7 +53,7 @@ export async function createPoolTxWorker<T extends EstimationType>(
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)

Expand Down Expand Up @@ -141,10 +141,11 @@ export async function createPoolTxWorker<T extends EstimationType>(

const poolTxWorker = new Worker<BatchTx, PoolTxResult[]>(
TX_QUEUE_NAME,
job => withErrorLog(
withMutex(mutex, () => poolTxWorkerProcessor(job)),
[TxValidationError]
),
job =>
withErrorLog(
withMutex(mutex, () => poolTxWorkerProcessor(job)),
[TxValidationError]
),
WORKER_OPTIONS
)

Expand Down
10 changes: 8 additions & 2 deletions zp-relayer/workers/sentTxWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down