before dangerous ops

Signed-off-by: T-Hax <>
This commit is contained in:
T-Hax 2023-05-06 23:59:38 +00:00
parent 4065d89909
commit 704648ed2c
41 changed files with 31021 additions and 460 deletions

View file

@ -1,13 +0,0 @@
# RPC URLs
ETH_MAINNET_TEST_RPC=
# debug (debug events are logged to console)
DEBUG=
# use tor (torify tests)
TORIFY=
# tor port (regular = 9050, browser = 9150)
TOR_PORT=
# relayer DOMAIN (the example.xyz in https://example.xyz) for testing
TEST_RELAYER_DOMAIN=

View file

@ -0,0 +1 @@
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.

View file

@ -49,13 +49,14 @@
"ganache": "^7.7.7",
"mocha": "^10.2.0",
"prettier": "^2.3.0",
"rimraf": "^4.4.0",
"rimraf": "^5.0.0",
"source-map-support": "^0.5.19",
"ts-essentials": "^9.3.1",
"ts-node": "^10.9.1",
"tsc-alias": "^1.2.11",
"tsconfig-paths": "^4.1.2",
"typechain": "^8.1.1",
"typedoc": "^0.24.6",
"typescript": "^5.0.4"
},
"scripts": {
@ -67,7 +68,7 @@
"postversion": "git push --follow-tags && npm publish",
"lint": "eslint --ext ts,js --fix src",
"build-live": "tsc -w && tsc-alias -w",
"clean": "rm -rf --interactive=never cache/*",
"clean": "rm -rf --interactive=never docs/*",
"format": "prettier src/**/*.ts -w"
},
"files": [

View file

@ -10,7 +10,7 @@ import { ZKDepositData, InputFor } from '@tornado/sdk-crypto'
// External imports
import { TransactionRequest } from '@ethersproject/abstract-provider'
import { BigNumber, providers } from 'ethers'
import { BigNumber, EventFilter, providers } from 'ethers'
import { parseUnits } from 'ethers/lib/utils'
import { bigInt } from 'snarkjs'
@ -30,10 +30,6 @@ export namespace Options {
export interface Deposit {
depositsPerInstance?: Array<number>
doNotPopulate?: boolean
backup?: {
invoices?: boolean
notes?: boolean
}
}
export type Invoice = Deposit
@ -128,58 +124,52 @@ export class WithdrawalCache extends Cache.Syncable<Docs.Withdrawal> {
type Provider = providers.Provider
type BackupDepositDoc = {
network: string
denomination: string
token: string
invoice?: string
note?: string
}
type RelayerProperties = MarkOptional<
Pick<RelayerDataProperties, 'address' | 'serviceFeePercent' | 'prices'>,
'serviceFeePercent' | 'prices'
>
export class Core extends Synchronizer {
chain: Chain
private _mutex: AsyncUtils.SimpleMutex
caches: Map<string, Cache.Base<Docs.Base>>
instances: Map<string, TornadoInstance>
chain?: Chain
constructor(provider: providers.Provider) {
constructor() {
super()
this.chain = new Chain(provider)
this.caches = new Map<string, Cache.Syncable<Docs.Base>>()
this.instances = new Map<string, TornadoInstance>()
this._mutex = new AsyncUtils.SimpleMutex()
}
connect(provider: Provider): void {
this.chain.provider = provider
private _checkProvider(parentCallName: string): void {
try {
this.chain?.id
} catch (err) {
throw ErrorUtils.getError('Core.' + parentCallName + ': you must first connect a provider!')
}
}
async getInstances(
keys: Array<{ token: string; denomination: number | string }>
): Promise<Array<TornadoInstance>> {
const chainId = await this.chain.getChainId()
return Promise.all(
keys.map((key) =>
Contracts.getInstance(String(chainId), key.token, String(key.denomination), this.chain.provider)
)
async connect(provider: Provider): Promise<void> {
if (!this.chain) this.chain = new Chain(provider)
else this.chain.provider = provider
await this.chain.fetchChainData()
}
getInstances(keys: Array<{ token: string; denomination: number | string }>): Array<TornadoInstance> {
this._checkProvider('getInstances')
return keys.map((key) =>
Contracts.getInstance(String(this.chain!.id), key.token, String(key.denomination), this.chain!.provider)
)
}
async getInstance(token: string, denomination: number | string): Promise<TornadoInstance> {
const chainId = String(await this.chain.getChainId())
token = token.toLowerCase()
denomination = String(denomination)
if (this.instances.has(chainId + token + denomination))
return this.instances.get(chainId + token + denomination)!
else return Contracts.getInstance(chainId, token, denomination, this.chain.provider)
getInstance(token: string, denomination: number | string): TornadoInstance {
this._checkProvider('getInstance')
return this.loadInstance(this.chain!.id, token, denomination)
}
async getProxy(): Promise<TornadoProxy> {
const chainId = await this.chain.getChainId()
return Contracts.getProxy(String(chainId), this.chain.provider)
getProxy(): TornadoProxy {
this._checkProvider('getProxy')
return Contracts.getProxy(String(this.chain!.id), this.chain!.provider)
}
async buildDepositProof(
@ -215,6 +205,8 @@ export class Core extends Synchronizer {
zkDepositsData: Array<ZKDepositData>,
options?: Options.Core.BuildDepositProof
): Promise<Array<Array<string>>> {
this._checkProvider('buildDepositProofs')
// Extract commitments and nullifier hashes
const hexCommitments: string[] = []
const hexNullifierHashes: string[] = []
@ -237,8 +229,8 @@ export class Core extends Synchronizer {
})
// Determine cache name
const lookupKeys = await Onchain.getInstanceLookupKeys(instance.address)
const name = 'Deposits' + (lookupKeys.network + lookupKeys.token + lookupKeys.denomination).toUpperCase()
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(instance.address)
const name = 'Deposits' + (network + token + denomination).toUpperCase()
// Find all leaves & indices by reading from cache
const [leaves, leafIndices] = await this._findLeavesAndIndices(name, hexCommitments)
@ -305,20 +297,23 @@ export class Core extends Synchronizer {
// Rest of note invariant arguments
const inputsForProofs: InputFor.ZKProof[] = []
const gasPrice = options?.gasPrice ?? (await this.chain.getGasPrice())
const gasPrice = options?.gasPrice ?? (await this.chain!.getGasPrice())
const gasPriceCushion = options?.gasPrice ?? gasPrice.mul(10).div(100)
// In reality, if a manual withdraw is made, we don't differentiate it from a relayer withdraw
// Since it is only serviceFee 0 AND without a token price, the function will not buy more tokens
const serviceFeePercent = relayerProperties.serviceFeePercent ?? 0
const tokenPrice = relayerProperties.prices?.get(lookupKeys.token)
const tokenPrice = relayerProperties.prices?.get(token)
const decimals = BigNumber.from(10).pow(
options?.tokenDecimals ?? (await Onchain.getTokenDecimals(lookupKeys.network, lookupKeys.token))
options?.tokenDecimals ?? (await Onchain.getTokenDecimals(network, token))
)
const toWithdraw = BigNumber.from(+lookupKeys.denomination * 10 ** lookupKeys.denomination.length)
const toWithdraw = BigNumber.from(+denomination * 10 ** denomination.length)
.mul(decimals)
.div(10 ** lookupKeys.denomination.length)
const native = lookupKeys.token == (await this.chain.getChainSymbol())
.div(10 ** denomination.length)
const native = token == this.chain!.symbol
if (!tokenPrice && !native)
throw ErrorUtils.getError(
@ -403,6 +398,9 @@ export class Core extends Synchronizer {
const leaves: Array<string> = []
const cache = this.loadCache<Cache.Base<Docs.Deposit>>(instanceName)
// Shallow copy so we can find indexes again for commitments
const commitmentsCopy = [...commitments]
const docs = await cache.db.allDocs()
// If no docs in cache throw and stop
@ -415,13 +413,25 @@ export class Core extends Synchronizer {
// Otherwise start looking for commitment leaf indices and also pick up all other leafs on the way
for (const row of docs.rows) {
const [, leafIndex, loadedCommitment] = parseIndexableString(row.id)
const index = commitments.findIndex((commitment) => commitment === loadedCommitment)
let index = -1
// If some commitment is found then add the leaf index and remove that commitment
const [, leafIndex, loadedCommitment] = parseIndexableString(row.id)
// Search only if there is some left
if (commitments.length !== 0)
index = commitments.findIndex((commitment) => commitment === loadedCommitment)
// If some commitment is found then add the leaf index
if (index !== -1) {
indices[index] = leafIndex
// Add it there where we intended for it to be originally
indices[commitmentsCopy.findIndex((commitment) => commitment === loadedCommitment)] = leafIndex
commitments.splice(index, 1)
this.emit(
'debug',
`\nMatched commitment ${loadedCommitment} @ leaf index ${leafIndex}, leftover commitments:\n\n${commitments.join(
'\n'
)}\n`
)
}
// In any case push every leaf
@ -443,8 +453,7 @@ export class Core extends Synchronizer {
if (indexes)
for (let i = 0, len = rows.length; i < len; i++) {
const [index, , ,] = parseIndexableString(rows[i].id)[0]
if (0 < indexes.findIndex(index)) docs.push(rows[i].doc)
docs.push(rows[indexes[i]].doc)
}
else docs = rows.map((row) => row.doc)
@ -469,82 +478,141 @@ export class Core extends Synchronizer {
return this.parseNotes([note])[0]
}
async createInvoice(
instance: TornadoInstance,
options?: Omit<Options.Core.Invoice, 'depositsPerInstance'>
): Promise<Transactions.Invoice> {
let opts: Options.Core.Invoice = options ?? {}
opts.depositsPerInstance = [1]
return (await this.createInvoices([instance], options))[0]
clearListener(
instance: TornadoInstance | string,
event: Function | number = 0,
listenerIndex: number = 0
): void {
const _instance = this._resolveInstance(instance)
const filter = this._resolveInstanceEvent(_instance, event)
this.clearListenerByIndex(
_instance,
this._instanceEventToFilter(filter, _instance.filters.Deposit),
listenerIndex
)
}
async createInvoices(
instances: Array<TornadoInstance>,
options?: Options.Core.Invoice
): Promise<Array<Transactions.Invoice>> {
if (!options) options = {}
if (!options.backup) options.backup = {}
options.backup.invoices = options.backup.invoices ?? true
options.backup.notes = options.backup.notes ?? true
options.doNotPopulate = options.doNotPopulate ?? true
return this.buildDepositTransactions(instances, options)
clearListeners(instance: TornadoInstance | string): void {
this._resolveInstance(instance).removeAllListeners()
}
async buildDepositTransaction(
instance: TornadoInstance,
options?: Options.Core.Deposit
): Promise<Transactions.Deposit> {
listenForDeposits(instance: TornadoInstance | string): void {
this.listenForInstanceEvents(instance, 0)
}
listenForWithdrawals(instance: TornadoInstance | string): void {
this.listenForInstanceEvents(instance, 1)
}
listenForInstanceEvents(instance: TornadoInstance | string, event: Function | number = 0): void {
let _instance: TornadoInstance
let key: string
if (typeof instance !== 'string') {
const { network, token, denomination } = Onchain.getInstanceLookupKeysSync(instance.address)
_instance = this.loadInstance(network, token, denomination)
key = network + token + denomination
} else {
key = instance.toLowerCase()
_instance = this._resolveInstance(key)
}
const filter = this._resolveInstanceEvent(_instance!, event)
const isDeposit = filter == _instance.filters.Deposit
const cache = isDeposit
? this.loadDepositCache('Deposits' + key.toUpperCase())
: this.loadWithdrawalCache('Withdrawals' + key.toUpperCase())
this.listenForEvents(
isDeposit ? 'deposit' : 'withdrawal',
_instance!,
this._instanceEventToFilter(filter, _instance.filters.Deposit),
cache
)
}
private _instanceEventToFilter(event: Function, depositEvent: Function): EventFilter {
return event == depositEvent ? event(null, null, null) : event(null, null, null, null)
}
private _resolveInstanceEvent(instance: TornadoInstance, event: Function | number = 0): Function {
let filter: Function
if (typeof event === 'number') {
filter = event === 0 ? instance.filters.Deposit : instance.filters.Withdrawal
} else filter = event
return filter
}
private _resolveInstance(instance: TornadoInstance | string): TornadoInstance {
let _instance: TornadoInstance
if (typeof instance === 'string') {
instance = instance.toLowerCase()
const regexp = /([0-9]+)([a-z]+)([0-9.]+)/
const matches = instance.match(regexp)?.slice(1)
if (!matches || matches.length === 0)
throw ErrorUtils.getError('Core._resolveInstance: instance string key invalid.')
_instance = this.loadInstance(matches[0], matches[1], matches[2])
} else _instance = instance
return _instance
}
/**
* This is the main function to build a single Tornado Cash Classic deposit. An address need not be supplied because the returned note proves a deposit.
* @param instance The TornadoInstance for which to build transactions.
* @param options Whether or not to populate the transactions (only in the sense of encoding transaction data), and whether to backup notes and invoices. Defaults: `depositsPerInstance = [1], doNotPopulate = false, backup { notes = true, invoices = false }` Deposits per instance are hardcoded to 1, since we're doing a single transaction.
* @returns A promise which resolves to the created transaction.
*/
buildDepositTransaction(instance: TornadoInstance, options?: Options.Core.Deposit): Transactions.Deposit {
let opts: Options.Core.Deposit = options ?? {}
opts.depositsPerInstance = [1]
return (await this.buildDepositTransactions([instance], opts))[0]
return this.buildDepositTransactions([instance], opts)[0]
}
async buildDepositTransactions(
/**
* This is the main function which is used to build Tornado Cash Classic deposit transactions. An address need not be supplied because the returned note proves a deposit.
* @param instances The TornadoInstance instances for which to build transactions.
* @param options The number of deposits per instance, whether or not to populate the transactions (only in the sense of encoding transaction data), and whether to backup notes and invoices. Defaults: `depositsPerInstance = [1]*instance_num, doNotPopulate = false, backup { notes = true, invoices = false }`
* @returns A promise which resolves to the created transactions.
* @todo TODO: Maybe this should be sync and deposit backups should be async somewhere else
*/
buildDepositTransactions(
instances: Array<TornadoInstance>,
options?: Options.Core.Deposit
): Promise<Array<Transactions.Deposit>> {
): Array<Transactions.Deposit> {
this._checkProvider('buildDepositTransactions')
const depositsPerInstance = options?.depositsPerInstance ?? new Array<number>(instances.length).fill(1)
const doNotPopulate = options?.doNotPopulate ?? false
const backupNotes = options?.backup?.notes ?? true
const backupInvoices = options?.backup?.invoices ?? false
if (depositsPerInstance.length != instances.length)
throw ErrorUtils.getError(
'Core.buildDepositTx: number of deposit amount elements must equal the number of instances!'
)
const notesToBackup: Array<BackupDepositDoc> = []
const invoicesToBackup: Array<BackupDepositDoc> = []
const txs: Array<Transactions.Deposit> = []
const chainId = await this.chain.getChainId()
const chainId = this.chain!.id
const proxy: TornadoProxy = await Contracts.getProxy(String(chainId), this.chain.provider)
const proxy: TornadoProxy = Contracts.getProxy(String(chainId), this.chain!.provider)
const txs: Array<Transactions.Deposit> = []
for (let i = 0, nInstances = instances.length; i < nInstances; i++) {
const lookupKeys = await Onchain.getInstanceLookupKeys(instances[i].address)
const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination
const { network, token, denomination } = Onchain.getInstanceLookupKeysSync(instances[i].address)
const pathstring = network + token + denomination
for (let d = 0, nDeposits = depositsPerInstance[i]; d < nDeposits; d++) {
const deposit = Primitives.createDeposit()
const note = Primitives.createNote(deposit.preimage)
if (backupNotes)
notesToBackup.push({
network: lookupKeys.network,
denomination: lookupKeys.denomination,
token: lookupKeys.token,
note: note
})
if (backupInvoices)
invoicesToBackup.push({
network: lookupKeys.network,
denomination: lookupKeys.denomination,
token: lookupKeys.token,
invoice: deposit.hexCommitment
})
if (!doNotPopulate) {
txs.push({
request: {
@ -554,7 +622,7 @@ export class Core extends Synchronizer {
deposit.hexCommitment,
[]
]),
value: lookupKeys.token == 'eth' ? parseUnits(lookupKeys.denomination) : BigNumber.from(0)
value: token == 'eth' ? parseUnits(denomination) : BigNumber.from(0)
},
note: pathstring + '_' + note,
invoice: pathstring + '_' + deposit.hexCommitment
@ -568,41 +636,67 @@ export class Core extends Synchronizer {
}
}
if (backupNotes)
await this._backupDepositData(this.loadCache<Cache.Base<Docs.Note>>('DepositNotes'), notesToBackup)
if (backupInvoices)
await this._backupDepositData(
this.loadCache<Cache.Base<Docs.Invoice>>('DepositInvoices'),
invoicesToBackup
)
return txs
}
async backupNote(instance: TornadoInstance, transaction: Transactions.Deposit): Promise<void> {
await this.backupNotes(instance, [transaction])
}
async backupInvoice(instance: TornadoInstance, transaction: Transactions.Deposit): Promise<void> {
await this.backupInvoices(instance, [transaction])
}
async backupNotes(instance: TornadoInstance, transactions: Array<Transactions.Deposit>): Promise<void> {
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(instance.address)
await this._backupDepositData(
network,
token,
denomination,
transactions,
this.loadCache<Cache.Base<Docs.Note>>('DepositNotes')
)
}
async backupInvoices(instance: TornadoInstance, transactions: Array<Transactions.Deposit>): Promise<void> {
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(instance.address)
await this._backupDepositData(
network,
token,
denomination,
transactions,
this.loadCache<Cache.Base<Docs.Invoice>>('DepositInvoices')
)
}
private async _backupDepositData<T extends Docs.Note | Docs.Invoice>(
cache: Cache.Base<T>,
backupData: Array<BackupDepositDoc>
network: string,
token: string,
denomination: string,
transactions: Array<Transactions.Deposit>,
cache: Cache.Base<T>
): Promise<void> {
const notes = cache.name.length === 12 ? true : false
const name = notes ? 'notes' : 'invoices'
// We need a mutex here
const release = await this._mutex.acquire(name)
let id = +(await cache.db.info()).update_seq
await cache.db
.bulkDocs(
backupData.map((entry) => {
if (entry.note)
return new Docs.Note(++id, entry.network, entry.token, entry.denomination, entry.note)
else if (entry.invoice)
return new Docs.Invoice(++id, entry.network, entry.token, entry.denomination, entry.invoice)
transactions.map((transaction) => {
if (notes) return new Docs.Note(++id, network, token, denomination, transaction.note!)
else return new Docs.Invoice(++id, network, token, denomination, transaction.invoice!)
}) as Array<T>
)
.catch((err) => {
throw ErrorUtils.ensureError(err)
})
// TODO: Decide whether to close caches by default or not
//await cache.close().catch((err) => {
// throw ErrorUtils.ensureError(err)
//})
// Release
release()
}
loadDepositCache(name: string, options?: Options.Sync): DepositCache {
@ -644,50 +738,57 @@ export class Core extends Synchronizer {
return this.caches.get(name) as C
}
loadInstance(chainId: number | string, token: string, denomination: number | string): TornadoInstance {
token = token.toLowerCase()
return Contracts.getInstance('' + chainId, token, '' + denomination, this.chain!.provider)
}
async syncDeposits(instance: TornadoInstance, options?: Options.Sync): Promise<void> {
const lookupKeys = await Onchain.getInstanceLookupKeys(instance.address)
const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination
this._checkProvider('syncDeposits')
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(instance.address)
const pathstring = network + token + denomination
options = options ?? {}
options.startBlock = await Onchain.getInstanceDeployBlockNum(
lookupKeys.network,
lookupKeys.token,
lookupKeys.denomination
)
options.startBlock = await Onchain.getInstanceDeployBlockNum(network, token, denomination)
const populatedOptions = await this._populateSyncOptions(options)
const cache = this.loadDepositCache('Deposits' + pathstring.toUpperCase(), populatedOptions)
await this.sync(instance.filters.Deposit(null, null, null), instance, cache, populatedOptions)
await this.sync('deposit', instance.filters.Deposit(null, null, null), instance, cache, populatedOptions)
if (!this.instances.has(pathstring)) this.instances.set(pathstring, instance)
if (!this.caches.has(cache.name)) this.caches.set(cache.name, cache)
}
async syncWithdrawals(instance: TornadoInstance, options?: Options.Sync): Promise<void> {
const lookupKeys = await Onchain.getInstanceLookupKeys(instance.address)
const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination
this._checkProvider('syncWithdrawals')
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(instance.address)
const pathstring = network + token + denomination
options = options ?? {}
options.startBlock = await Onchain.getInstanceDeployBlockNum(
lookupKeys.network,
lookupKeys.token,
lookupKeys.denomination
)
options.startBlock = await Onchain.getInstanceDeployBlockNum(network, token, denomination)
const populatedOptions = await this._populateSyncOptions(options)
const cache = this.loadWithdrawalCache('Withdrawals' + pathstring.toUpperCase(), populatedOptions)
await this.sync(instance.filters.Withdrawal(null, null, null), instance, cache, populatedOptions)
await this.sync(
'withdrawal',
instance.filters.Withdrawal(null, null, null),
instance,
cache,
populatedOptions
)
if (!this.instances.has(pathstring)) this.instances.set(pathstring, instance)
if (!this.caches.has(cache.name)) this.caches.set(cache.name, cache)
}
protected async _populateSyncOptions(options: Options.Sync): Promise<DeepRequired<Options.Sync>> {
if (!options.startBlock) throw ErrorUtils.getError('Core._populateSyncOptions: startBlock not set.')
options.targetBlock = options.targetBlock ?? (await this.chain.latestBlockNum())
options.targetBlock = options.targetBlock ?? (await this.chain!.latestBlockNum())
options.blockDivisor = options.blockDivisor ?? 40