mirror of
https://github.com/zkopru-network/zkopru.git
synced 2026-02-19 01:54:22 -05:00
refactor: withdrawal tree updates
This commit is contained in:
@@ -3,7 +3,7 @@ import Web3 from 'web3'
|
||||
import { Account, EncryptedKeystoreV3Json, AddAccount } from 'web3-core'
|
||||
import { Field, Point, EdDSA, signEdDSA, verifyEdDSA } from '@zkopru/babyjubjub'
|
||||
import { Keystore } from '@zkopru/prisma'
|
||||
import { Note, ZkTx } from '@zkopru/transaction'
|
||||
import { ZkTx, Utxo } from '@zkopru/transaction'
|
||||
import { hexify } from '@zkopru/utils'
|
||||
import assert from 'assert'
|
||||
|
||||
@@ -59,15 +59,15 @@ export class ZkAccount {
|
||||
}
|
||||
}
|
||||
|
||||
decrypt(zkTx: ZkTx): Note | undefined {
|
||||
decrypt(zkTx: ZkTx): Utxo | undefined {
|
||||
const { memo } = zkTx
|
||||
if (!memo) {
|
||||
return
|
||||
}
|
||||
let note: Note | undefined
|
||||
let note: Utxo | undefined
|
||||
for (const outflow of zkTx.outflow) {
|
||||
try {
|
||||
note = Note.decrypt({
|
||||
note = Utxo.decrypt({
|
||||
utxoHash: outflow.note,
|
||||
memo,
|
||||
privKey: this.snarkPK.toHex(32),
|
||||
@@ -77,7 +77,7 @@ export class ZkAccount {
|
||||
}
|
||||
if (note) break
|
||||
}
|
||||
return note
|
||||
return note ? Utxo.from(note) : undefined
|
||||
}
|
||||
|
||||
static fromEncryptedKeystoreV3Json(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Point, Field } from '@zkopru/babyjubjub'
|
||||
import { Sum, TxBuilder, RawTx, Note, Utxo } from '@zkopru/transaction'
|
||||
import { Sum, TxBuilder, RawTx, Utxo } from '@zkopru/transaction'
|
||||
import { parseStringToUnit, logger } from '@zkopru/utils'
|
||||
import { fromWei, toBN, toWei } from 'web3-utils'
|
||||
import App, { AppMenu, Context } from '..'
|
||||
@@ -12,7 +12,7 @@ export default class TransferEth extends App {
|
||||
const wallet = this.base
|
||||
const { account } = context
|
||||
if (!account) throw Error('Acocunt is not set')
|
||||
const spendables: Note[] = await wallet.getSpendableNotes(account)
|
||||
const spendables: Utxo[] = await wallet.getSpendables(account)
|
||||
const spendableAmount = Sum.from(spendables)
|
||||
let weiPerByte!: string
|
||||
try {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Field } from '@zkopru/babyjubjub'
|
||||
import express, { RequestHandler } from 'express'
|
||||
import { scheduleJob, Job } from 'node-schedule'
|
||||
import { EventEmitter } from 'events'
|
||||
import { ZkTx } from '@zkopru/transaction'
|
||||
import { Item } from '@zkopru/tree'
|
||||
import { ZkTx, OutflowType, Withdrawal } from '@zkopru/transaction'
|
||||
import { Leaf } from '@zkopru/tree'
|
||||
import { logger, root, bnToBytes32, bnToUint256 } from '@zkopru/utils'
|
||||
import {
|
||||
FullNode,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
serializeHeader,
|
||||
headerHash,
|
||||
Block,
|
||||
getMassMigrations,
|
||||
} from '@zkopru/core'
|
||||
import { Account } from 'web3-core'
|
||||
import { Subscription } from 'web3-core-subscriptions'
|
||||
@@ -26,6 +27,7 @@ import { MassDeposit as MassDepositSql } from '@zkopru/prisma'
|
||||
import { Server } from 'http'
|
||||
import chalk from 'chalk'
|
||||
import { Address, Bytes32, Uint256 } from 'soltypes'
|
||||
import BN from 'bn.js'
|
||||
import { TxMemPool, TxPoolInterface } from './tx_pool'
|
||||
|
||||
export interface CoordinatorConfig {
|
||||
@@ -433,16 +435,25 @@ export class Coordinator extends EventEmitter {
|
||||
.map(outflow => outflow.note),
|
||||
]
|
||||
}, deposits)
|
||||
.map(leafHash => ({ leafHash })) as Item<Field>[]
|
||||
.map(hash => ({ hash })) as Leaf<Field>[]
|
||||
|
||||
const withdrawals = txs.reduce((arr, tx) => {
|
||||
const withdrawals: Leaf<BN>[] = txs.reduce((arr, tx) => {
|
||||
return [
|
||||
...arr,
|
||||
...tx.outflow
|
||||
.filter(outflow => outflow.outflowType.eqn(1))
|
||||
.map(outflow => outflow.note),
|
||||
.filter(outflow => outflow.outflowType.eqn(OutflowType.WITHDRAWAL))
|
||||
.map(outflow => {
|
||||
if (!outflow.data) throw Error('No withdrawal public data')
|
||||
return {
|
||||
hash: Withdrawal.withdrawalHash(
|
||||
outflow.note,
|
||||
outflow.data,
|
||||
).toBN(),
|
||||
noteHash: outflow.note,
|
||||
}
|
||||
}),
|
||||
]
|
||||
}, [] as Field[])
|
||||
}, [] as Leaf<BN>[])
|
||||
|
||||
if (
|
||||
pendingDeposits.length ||
|
||||
@@ -464,9 +475,7 @@ export class Coordinator extends EventEmitter {
|
||||
throw Error('Layer 2 chain is not synced yet.')
|
||||
}
|
||||
// TODO acquire lock during gen block
|
||||
const massMigrations: MassMigration[] = []
|
||||
// TODO remove this line
|
||||
logger.info('current nullifier')
|
||||
const massMigrations: MassMigration[] = getMassMigrations(txs)
|
||||
const expectedGrove = await this.node.l2Chain.grove.dryPatch({
|
||||
utxos,
|
||||
withdrawals,
|
||||
@@ -494,7 +503,7 @@ export class Coordinator extends EventEmitter {
|
||||
utxoRoot: expectedGrove.utxoTreeRoot.toUint256(),
|
||||
utxoIndex: expectedGrove.utxoTreeIndex.toUint256(),
|
||||
nullifierRoot: bnToBytes32(expectedGrove.nullifierTreeRoot),
|
||||
withdrawalRoot: bnToBytes32(expectedGrove.withdrawalTreeRoot),
|
||||
withdrawalRoot: bnToUint256(expectedGrove.withdrawalTreeRoot),
|
||||
withdrawalIndex: bnToUint256(expectedGrove.withdrawalTreeIndex),
|
||||
txRoot: root(txs.map(tx => tx.hash())),
|
||||
depositRoot: root(massDeposits.map(massDepositHash)),
|
||||
|
||||
@@ -51,9 +51,9 @@ export interface Header {
|
||||
fee: Uint256
|
||||
utxoRoot: Uint256
|
||||
utxoIndex: Uint256
|
||||
nullifierRoot: Bytes32
|
||||
withdrawalRoot: Bytes32
|
||||
withdrawalRoot: Uint256
|
||||
withdrawalIndex: Uint256
|
||||
nullifierRoot: Bytes32
|
||||
txRoot: Bytes32
|
||||
depositRoot: Bytes32
|
||||
migrationRoot: Bytes32
|
||||
@@ -95,7 +95,7 @@ export function sqlToHeader(sql: HeaderSql): Header {
|
||||
utxoRoot: Uint256.from(sql.utxoRoot),
|
||||
utxoIndex: Uint256.from(sql.utxoIndex),
|
||||
nullifierRoot: Bytes32.from(sql.nullifierRoot),
|
||||
withdrawalRoot: Bytes32.from(sql.withdrawalRoot),
|
||||
withdrawalRoot: Uint256.from(sql.withdrawalRoot),
|
||||
withdrawalIndex: Uint256.from(sql.withdrawalIndex),
|
||||
txRoot: Bytes32.from(sql.txRoot),
|
||||
depositRoot: Bytes32.from(sql.depositRoot),
|
||||
@@ -225,7 +225,7 @@ function deserializeHeaderFrom(
|
||||
utxoRoot: queue.dequeueToUint256(),
|
||||
utxoIndex: queue.dequeueToUint256(),
|
||||
nullifierRoot: queue.dequeueToBytes32(),
|
||||
withdrawalRoot: queue.dequeueToBytes32(),
|
||||
withdrawalRoot: queue.dequeueToUint256(),
|
||||
withdrawalIndex: queue.dequeueToUint256(),
|
||||
txRoot: queue.dequeueToBytes32(),
|
||||
depositRoot: queue.dequeueToBytes32(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { genesisRoot, poseidonHasher, keccakHasher } from '@zkopru/tree'
|
||||
import { bnToBytes32 } from '@zkopru/utils'
|
||||
import { bnToBytes32, bnToUint256 } from '@zkopru/utils'
|
||||
import { Address, Bytes32 } from 'soltypes'
|
||||
import BN from 'bn.js'
|
||||
import { Config } from '@zkopru/prisma'
|
||||
@@ -18,7 +18,7 @@ export const genesis = ({
|
||||
const withdrawalHasher = keccakHasher(config.withdrawalTreeDepth)
|
||||
const nullifierHasher = keccakHasher(config.nullifierTreeDepth)
|
||||
const utxoRoot = genesisRoot(utxoHasher).toUint256()
|
||||
const withdrawalRoot = bnToBytes32(genesisRoot(withdrawalHasher))
|
||||
const withdrawalRoot = bnToUint256(genesisRoot(withdrawalHasher))
|
||||
const nullifierRoot = bnToBytes32(genesisRoot(nullifierHasher))
|
||||
const zeroBytes = bnToBytes32(new BN(0))
|
||||
return {
|
||||
|
||||
@@ -5,16 +5,22 @@ import {
|
||||
BlockStatus,
|
||||
Proposal,
|
||||
MassDeposit as MassDepositSql,
|
||||
Note as NoteSql,
|
||||
NoteType,
|
||||
} from '@zkopru/prisma'
|
||||
import { Grove, GrovePatch, Item } from '@zkopru/tree'
|
||||
import { Grove, GrovePatch, Leaf } from '@zkopru/tree'
|
||||
import BN from 'bn.js'
|
||||
import AsyncLock from 'async-lock'
|
||||
import { Bytes32, Address, Uint256 } from 'soltypes'
|
||||
import { logger } from '@zkopru/utils'
|
||||
import { Field } from '@zkopru/babyjubjub'
|
||||
import { Note, OutflowType, UtxoStatus, ZkTx } from '@zkopru/transaction'
|
||||
import {
|
||||
OutflowType,
|
||||
UtxoStatus,
|
||||
WithdrawalStatus,
|
||||
ZkTx,
|
||||
Utxo,
|
||||
Withdrawal,
|
||||
ZkOutflow,
|
||||
} from '@zkopru/transaction'
|
||||
import { ZkAccount } from '@zkopru/account'
|
||||
import {
|
||||
Block,
|
||||
@@ -28,8 +34,10 @@ import { BootstrapData } from './bootstrap'
|
||||
export interface Patch {
|
||||
result: VerifyResult
|
||||
block: Bytes32
|
||||
header: Header
|
||||
prevHeader: Header
|
||||
massDeposits?: Bytes32[]
|
||||
treePatch?: GrovePatch
|
||||
treePatch: GrovePatch
|
||||
nullifiers?: Uint256[]
|
||||
}
|
||||
|
||||
@@ -187,7 +195,7 @@ export class L2Chain {
|
||||
utxoRoot: Uint256.from(lastVerifiedHeader.utxoRoot),
|
||||
utxoIndex: Uint256.from(lastVerifiedHeader.utxoIndex),
|
||||
nullifierRoot: Bytes32.from(lastVerifiedHeader.nullifierRoot),
|
||||
withdrawalRoot: Bytes32.from(lastVerifiedHeader.withdrawalRoot),
|
||||
withdrawalRoot: Uint256.from(lastVerifiedHeader.withdrawalRoot),
|
||||
withdrawalIndex: Uint256.from(lastVerifiedHeader.withdrawalIndex),
|
||||
txRoot: Bytes32.from(lastVerifiedHeader.txRoot),
|
||||
depositRoot: Bytes32.from(lastVerifiedHeader.depositRoot),
|
||||
@@ -201,53 +209,98 @@ export class L2Chain {
|
||||
}
|
||||
}
|
||||
|
||||
async applyPatchAndMarkAsVerified(patch: Patch) {
|
||||
async applyPatch(patch: Patch) {
|
||||
logger.info('layer2.ts: applyPatch()')
|
||||
const { result, block, treePatch, massDeposits } = patch
|
||||
const { result, block, header, prevHeader, treePatch, massDeposits } = patch
|
||||
// Apply tree patch
|
||||
if (treePatch) {
|
||||
if (result === VerifyResult.INVALIDATED)
|
||||
throw Error('Invalid result cannot make a patch')
|
||||
await this.grove.applyPatch(treePatch)
|
||||
await this.nullifyNotes(block, treePatch.nullifiers)
|
||||
}
|
||||
// Record the verify result
|
||||
if (result === VerifyResult.INVALIDATED) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.proposal.update({
|
||||
where: { hash: block.toString() },
|
||||
data: { invalidated: true },
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
if (!patch) throw Error('patch does not exists')
|
||||
await this.db.write(prisma =>
|
||||
prisma.block.update({
|
||||
where: { hash: block.toString() },
|
||||
data: { verified: true },
|
||||
}),
|
||||
)
|
||||
throw Error('Invalid result cannot make a patch')
|
||||
}
|
||||
const { utxoTreeId, withdrawalTreeId } = await this.grove.applyGrovePatch(
|
||||
treePatch,
|
||||
)
|
||||
await this.nullifyUtxos(block, treePatch.nullifiers)
|
||||
// Record the verify result
|
||||
await this.db.write(prisma =>
|
||||
prisma.block.update({
|
||||
where: { hash: block.toString() },
|
||||
data: { verified: true },
|
||||
}),
|
||||
)
|
||||
// Update mass deposits inclusion status
|
||||
if (massDeposits) {
|
||||
await this.markMassDepositsAsIncludedIn(massDeposits, block)
|
||||
}
|
||||
await this.markUtxosAsUnspent(patch.treePatch?.utxos || [])
|
||||
await this.markWithdrawalsAsUnfinalized(patch.treePatch?.withdrawals || [])
|
||||
const utxoLeafStartIndex = header.utxoIndex
|
||||
.toBN()
|
||||
.gt(prevHeader.utxoIndex.toBN())
|
||||
? prevHeader.utxoIndex.toBN() // appending to an exising utxo tree
|
||||
: new BN(0) // started new utxo tree
|
||||
await this.updateUtxoLeafIndexes(
|
||||
utxoTreeId,
|
||||
utxoLeafStartIndex,
|
||||
patch.treePatch?.utxos || [],
|
||||
)
|
||||
const withdrawalLeafStartIndex = header.withdrawalIndex
|
||||
.toBN()
|
||||
.gt(prevHeader.withdrawalIndex.toBN())
|
||||
? prevHeader.withdrawalIndex.toBN() // appending to an exising utxo tree
|
||||
: new BN(0) // started new utxo tree
|
||||
await this.updateWithdrawalLeafIndexes(
|
||||
withdrawalTreeId,
|
||||
withdrawalLeafStartIndex,
|
||||
patch.treePatch?.withdrawals || [],
|
||||
)
|
||||
await this.updateWithdrawalProof(patch.treePatch?.withdrawals || [])
|
||||
}
|
||||
|
||||
async findMyNotes(txs: ZkTx[], utxos: Item<Field>[], accounts: ZkAccount[]) {
|
||||
private async markUtxosAsUnspent(utxos: Leaf<Field>[]) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.utxo.updateMany({
|
||||
where: {
|
||||
hash: { in: utxos.map(utxo => utxo.hash.toUint256().toString()) },
|
||||
},
|
||||
data: { status: UtxoStatus.UNSPENT },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private async markWithdrawalsAsUnfinalized(withdrawals: Leaf<BN>[]) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.withdrawal.updateMany({
|
||||
where: {
|
||||
hash: {
|
||||
in: withdrawals.map(Withdrawal => Withdrawal.hash.toString()),
|
||||
},
|
||||
},
|
||||
data: { status: WithdrawalStatus.UNFINALIZED },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async findMyUtxos(txs: ZkTx[], accounts: ZkAccount[]) {
|
||||
const txsWithMemo = txs.filter(tx => tx.memo)
|
||||
logger.info(`findMyNotes`)
|
||||
const myNotes: Note[] = []
|
||||
const myUtxos: Utxo[] = []
|
||||
for (const tx of txsWithMemo) {
|
||||
for (const account of accounts) {
|
||||
const note = account.decrypt(tx)
|
||||
logger.info(`decrypt result ${note}`)
|
||||
if (note) myNotes.push(note)
|
||||
if (note) myUtxos.push(note)
|
||||
}
|
||||
}
|
||||
for (const tx of txs) {
|
||||
for (const account of accounts) {
|
||||
const note = account.decrypt(tx)
|
||||
logger.info(`decrypt result ${note}`)
|
||||
if (note) myUtxos.push(note)
|
||||
}
|
||||
}
|
||||
// TODO needs batch transaction
|
||||
for (const note of myNotes) {
|
||||
const noteSql = {
|
||||
for (const note of myUtxos) {
|
||||
const utxoSql = {
|
||||
hash: note
|
||||
.hash()
|
||||
.toUint256()
|
||||
@@ -259,25 +312,60 @@ export class L2Chain {
|
||||
erc20Amount: note.erc20Amount.toUint256().toString(),
|
||||
nft: note.nft.toUint256().toString(),
|
||||
status: UtxoStatus.UNSPENT,
|
||||
noteType: NoteType.UTXO,
|
||||
usedFor: null,
|
||||
}
|
||||
await this.db.write(prisma =>
|
||||
prisma.note.upsert({
|
||||
where: { hash: noteSql.hash },
|
||||
create: noteSql,
|
||||
update: noteSql,
|
||||
prisma.utxo.upsert({
|
||||
where: { hash: utxoSql.hash },
|
||||
create: utxoSql,
|
||||
update: utxoSql,
|
||||
}),
|
||||
)
|
||||
}
|
||||
await this.db.write(prisma =>
|
||||
prisma.note.updateMany({
|
||||
where: {
|
||||
hash: { in: utxos.map(utxo => utxo.leafHash.toUint256().toString()) },
|
||||
},
|
||||
data: { status: UtxoStatus.UNSPENT },
|
||||
}),
|
||||
}
|
||||
|
||||
async findMyWithdrawals(txs: ZkTx[], accounts: ZkAccount[]) {
|
||||
const outflows = txs.reduce(
|
||||
(acc, tx) => [
|
||||
...acc,
|
||||
...tx.outflow.filter(outflow =>
|
||||
outflow.outflowType.eqn(OutflowType.WITHDRAWAL),
|
||||
),
|
||||
],
|
||||
[] as ZkOutflow[],
|
||||
)
|
||||
const myWithdrawalOutputs: ZkOutflow[] = outflows.filter(
|
||||
outflow =>
|
||||
outflow.data &&
|
||||
outflow.data?.to.toAddress().toString() in
|
||||
accounts.map(account => account.address),
|
||||
)
|
||||
// TODO needs batch transaction
|
||||
for (const output of myWithdrawalOutputs) {
|
||||
if (!output.data) throw Error('Withdrawal does not have public data')
|
||||
const withdrawalSql = {
|
||||
hash: output.note.toUint256().toString(),
|
||||
withdrawalHash: Withdrawal.withdrawalHash(
|
||||
output.note,
|
||||
output.data,
|
||||
).toString(),
|
||||
to: output.data.to.toAddress().toString(),
|
||||
eth: output.data.eth.toUint256().toString(),
|
||||
tokenAddr: output.data.tokenAddr.toAddress().toString(),
|
||||
erc20Amount: output.data.erc20Amount.toUint256().toString(),
|
||||
nft: output.data.nft.toUint256().toString(),
|
||||
fee: output.data.fee.toUint256().toString(),
|
||||
status: WithdrawalStatus.WITHDRAWABLE,
|
||||
usedFor: null,
|
||||
}
|
||||
await this.db.write(prisma =>
|
||||
prisma.withdrawal.upsert({
|
||||
where: { hash: withdrawalSql.hash },
|
||||
create: withdrawalSql,
|
||||
update: withdrawalSql,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async applyBootstrap(block: Block, bootstrapData: BootstrapData) {
|
||||
@@ -348,9 +436,9 @@ export class L2Chain {
|
||||
)
|
||||
}
|
||||
|
||||
private async nullifyNotes(blockHash: Bytes32, nullifiers: BN[]) {
|
||||
private async nullifyUtxos(blockHash: Bytes32, nullifiers: BN[]) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.note.updateMany({
|
||||
prisma.utxo.updateMany({
|
||||
where: { nullifier: { in: nullifiers.map(v => v.toString()) } },
|
||||
data: {
|
||||
status: UtxoStatus.SPENT,
|
||||
@@ -360,47 +448,144 @@ export class L2Chain {
|
||||
)
|
||||
}
|
||||
|
||||
private async updateUtxoLeafIndexes(
|
||||
utxoTreeId: string,
|
||||
startIndex: BN,
|
||||
utxos: Leaf<Field>[],
|
||||
) {
|
||||
for (let i = 0; i < utxos.length; i += 1) {
|
||||
const index = startIndex.addn(i)
|
||||
const leaf = utxos[i]
|
||||
const { hash, shouldTrack } = leaf
|
||||
logger.debug(leaf)
|
||||
logger.debug(`hash: ${hash}, shouldTrack: ${shouldTrack}`)
|
||||
if (shouldTrack) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.utxo.update({
|
||||
where: { hash: hash.toString() },
|
||||
data: {
|
||||
index: index.toString(),
|
||||
tree: { connect: { id: utxoTreeId } },
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateWithdrawalLeafIndexes(
|
||||
withdrawalTreeId: string,
|
||||
startIndex: BN,
|
||||
withdrawals: Leaf<BN>[],
|
||||
) {
|
||||
for (let i = 0; i < withdrawals.length; i += 1) {
|
||||
const index = startIndex.addn(i)
|
||||
const leaf = withdrawals[i]
|
||||
const { noteHash, shouldTrack } = leaf
|
||||
if (!noteHash) throw Error('Withdrawal leaf should contain note hash')
|
||||
if (shouldTrack) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.withdrawal.update({
|
||||
where: { hash: noteHash.toString() },
|
||||
data: {
|
||||
index: index.toString(),
|
||||
tree: { connect: { id: withdrawalTreeId } },
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateWithdrawalProof(withdrawals: Leaf<BN>[]) {
|
||||
const myWithdrawals = withdrawals.filter(w => w.shouldTrack)
|
||||
for (const withdrawal of myWithdrawals) {
|
||||
const merkleProof = await this.grove.withdrawalMerkleProof(
|
||||
withdrawal.hash,
|
||||
)
|
||||
const { noteHash } = withdrawal
|
||||
if (!noteHash) throw Error('Withdrawal does not have note hash')
|
||||
await this.db.write(prisma =>
|
||||
prisma.withdrawal.update({
|
||||
where: { hash: noteHash.toString() },
|
||||
data: {
|
||||
siblings: JSON.stringify(
|
||||
merkleProof.siblings.map(sib => sib.toString(10)),
|
||||
),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async getGrovePatch(block: Block): Promise<GrovePatch> {
|
||||
logger.info(`get grove patch for block ${block.hash.toString()}`)
|
||||
const header = block.hash.toString()
|
||||
const utxos: Item<Field>[] = []
|
||||
const withdrawals: BN[] = []
|
||||
const nullifiers: BN[] = []
|
||||
const utxos: Leaf<Field>[] = []
|
||||
const withdrawals: Leaf<BN>[] = []
|
||||
const nullifiers: Field[] = []
|
||||
|
||||
const deposits = await this.getDeposits(...block.body.massDeposits)
|
||||
utxos.push(
|
||||
...deposits.map(deposit => ({
|
||||
leafHash: Field.from(deposit.note),
|
||||
})),
|
||||
)
|
||||
|
||||
const utxoHashes: Field[] = []
|
||||
utxoHashes.push(...deposits.map(deposit => Field.from(deposit.note)))
|
||||
|
||||
const withdrawalHashes: { noteHash: Field; withdrawalHash: Uint256 }[] = []
|
||||
for (const tx of block.body.txs) {
|
||||
for (const outflow of tx.outflow) {
|
||||
logger.debug(`outflow type ${outflow.outflowType.toString()}`)
|
||||
if (outflow.outflowType.eqn(OutflowType.UTXO)) {
|
||||
utxoHashes.push(outflow.note)
|
||||
} else if (outflow.outflowType.eqn(OutflowType.WITHDRAWAL)) {
|
||||
withdrawals.push(outflow.note)
|
||||
if (!outflow.data) throw Error('Withdrawal should have public data')
|
||||
withdrawalHashes.push({
|
||||
noteHash: outflow.note,
|
||||
withdrawalHash: Withdrawal.withdrawalHash(
|
||||
outflow.note,
|
||||
outflow.data,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const myNoteList = await this.db.read(prisma =>
|
||||
prisma.note.findMany({
|
||||
logger.debug(`utxo list.. ${utxoHashes}`)
|
||||
const myUtxoList = await this.db.read(prisma =>
|
||||
prisma.utxo.findMany({
|
||||
where: {
|
||||
hash: { in: utxoHashes.map(output => output.toHex()) },
|
||||
hash: { in: utxoHashes.map(output => output.toString(10)) },
|
||||
treeId: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
const myNotes: { [key: string]: NoteSql } = {}
|
||||
for (const myNote of myNoteList) {
|
||||
myNotes[myNote.hash] = myNote
|
||||
const myWithdrawalList = await this.db.read(prisma =>
|
||||
prisma.withdrawal.findMany({
|
||||
where: {
|
||||
hash: { in: withdrawalHashes.map(h => h.noteHash.toString(10)) },
|
||||
treeId: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
const shouldTrack: { [key: string]: boolean } = {}
|
||||
for (const myNote of myUtxoList) {
|
||||
shouldTrack[myNote.hash] = true
|
||||
logger.debug(`found my note: ${myNote.hash}`)
|
||||
}
|
||||
for (const myNote of myWithdrawalList) {
|
||||
shouldTrack[myNote.hash] = true
|
||||
}
|
||||
for (const output of utxoHashes) {
|
||||
const myNote = myNotes[output.toHex()]
|
||||
logger.debug(`utxo...: ${output.toString(10)}`)
|
||||
const trackThisNote = shouldTrack[output.toString(10)]
|
||||
utxos.push({
|
||||
leafHash: output,
|
||||
note: myNote ? Note.fromSql(myNote) : undefined,
|
||||
hash: output,
|
||||
shouldTrack: !!trackThisNote,
|
||||
})
|
||||
}
|
||||
for (const hash of withdrawalHashes) {
|
||||
const keepTrack = shouldTrack[hash.noteHash.toString(10)]
|
||||
withdrawals.push({
|
||||
hash: hash.withdrawalHash.toBN(),
|
||||
noteHash: hash.noteHash,
|
||||
shouldTrack: !!keepTrack,
|
||||
})
|
||||
}
|
||||
for (const tx of block.body.txs) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Deposit as DepositSql } from '@zkopru/prisma'
|
||||
import { Bytes32, Uint256 } from 'soltypes'
|
||||
import { soliditySha3 } from 'web3-utils'
|
||||
import BN from 'bn.js'
|
||||
import { DryPatchResult } from '@zkopru/tree'
|
||||
import { DryPatchResult, GrovePatch } from '@zkopru/tree'
|
||||
import {
|
||||
Block,
|
||||
Header,
|
||||
@@ -45,10 +45,12 @@ export class Verifier {
|
||||
async verifyBlock({
|
||||
layer2,
|
||||
prevHeader,
|
||||
treePatch,
|
||||
block,
|
||||
}: {
|
||||
layer2: L2Chain
|
||||
prevHeader: Header
|
||||
treePatch: GrovePatch
|
||||
block: Block
|
||||
}): Promise<{ patch?: Patch; challenge?: Challenge }> {
|
||||
logger.info(`Verifying ${block.hash}`)
|
||||
@@ -78,7 +80,6 @@ export class Verifier {
|
||||
break
|
||||
}
|
||||
// verify and gen challenge codes here
|
||||
const treePatch = await layer2.getGrovePatch(block)
|
||||
const dryPatchResult = await layer2.grove.dryPatch(treePatch)
|
||||
if (this.option.header) {
|
||||
const code = Verifier.verifyHeader(block, dryPatchResult)
|
||||
@@ -127,6 +128,8 @@ export class Verifier {
|
||||
block: block.hash,
|
||||
massDeposits: block.body.massDeposits.map(massDepositHash),
|
||||
treePatch,
|
||||
header: block.header,
|
||||
prevHeader,
|
||||
nullifiers: block.body.txs.reduce((arr, tx) => {
|
||||
return [
|
||||
...arr,
|
||||
|
||||
@@ -155,22 +155,28 @@ export class ZkOPRUNode extends EventEmitter {
|
||||
|
||||
logger.info(`Processing block ${block.hash.toString()}`)
|
||||
try {
|
||||
// should find and save my notes before calling getGrovePatch
|
||||
await this.l2Chain.findMyUtxos(block.body.txs, this.accounts || [])
|
||||
await this.l2Chain.findMyWithdrawals(block.body.txs, this.accounts || [])
|
||||
const treePatch = await this.l2Chain.getGrovePatch(block)
|
||||
const { patch, challenge } = await this.verifier.verifyBlock({
|
||||
layer2: this.l2Chain,
|
||||
prevHeader,
|
||||
treePatch,
|
||||
block,
|
||||
})
|
||||
if (patch) {
|
||||
await this.l2Chain.applyPatchAndMarkAsVerified(patch)
|
||||
await this.l2Chain.findMyNotes(
|
||||
block.body.txs,
|
||||
patch.treePatch?.utxos || [],
|
||||
this.accounts || [],
|
||||
)
|
||||
await this.l2Chain.applyPatch(patch)
|
||||
this.processUnverifiedBlocks(true)
|
||||
} else if (challenge) {
|
||||
// implement challenge here & mark as invalidated
|
||||
this.onlyRunRecursiveCall = false
|
||||
await this.db.write(prisma =>
|
||||
prisma.proposal.update({
|
||||
where: { hash: block.hash.toString() },
|
||||
data: { invalidated: true },
|
||||
}),
|
||||
)
|
||||
logger.warn(challenge)
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -179,6 +185,7 @@ export class ZkOPRUNode extends EventEmitter {
|
||||
this.onlyRunRecursiveCall = false
|
||||
logger.error(err)
|
||||
}
|
||||
// TODO remove proposal data if it completes verification or if the block is finalized
|
||||
}
|
||||
|
||||
async latestBlock(): Promise<string | null> {
|
||||
|
||||
@@ -1,60 +1,48 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { Field, Point } from '@zkopru/babyjubjub'
|
||||
import { RawTx, Note, TokenUtils, Utxo } from '@zkopru/transaction'
|
||||
import { RawTx, TokenUtils, Utxo } from '@zkopru/transaction'
|
||||
|
||||
const alicePrivKey = "I am Alice's private key"
|
||||
const alicePubKey: Point = Point.fromPrivKey(alicePrivKey)
|
||||
const bobPrivKey = "I am Bob's private key"
|
||||
const bobPubKey: Point = Point.fromPrivKey(bobPrivKey)
|
||||
|
||||
const utxo1_in_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 3333,
|
||||
pubKey: alicePubKey,
|
||||
salt: 11,
|
||||
}),
|
||||
)
|
||||
const utxo1_out_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 2221,
|
||||
pubKey: bobPubKey,
|
||||
salt: 12,
|
||||
}),
|
||||
)
|
||||
const utxo1_out_2: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 1111,
|
||||
pubKey: alicePubKey,
|
||||
salt: 13,
|
||||
}),
|
||||
)
|
||||
const utxo2_1_in_1: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 22222333333,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 8888,
|
||||
pubKey: alicePubKey,
|
||||
salt: 14,
|
||||
}),
|
||||
)
|
||||
const utxo2_1_out_1: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 22222333332,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: alicePubKey,
|
||||
salt: 15,
|
||||
}),
|
||||
)
|
||||
const utxo2_1_out_2: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 3333,
|
||||
pubKey: bobPubKey,
|
||||
salt: 16,
|
||||
}),
|
||||
)
|
||||
const utxo1_in_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 3333,
|
||||
pubKey: alicePubKey,
|
||||
salt: 11,
|
||||
})
|
||||
const utxo1_out_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 2221,
|
||||
pubKey: bobPubKey,
|
||||
salt: 12,
|
||||
})
|
||||
const utxo1_out_2: Utxo = Utxo.newEtherNote({
|
||||
eth: 1111,
|
||||
pubKey: alicePubKey,
|
||||
salt: 13,
|
||||
})
|
||||
const utxo2_1_in_1: Utxo = Utxo.newERC20Note({
|
||||
eth: 22222333333,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 8888,
|
||||
pubKey: alicePubKey,
|
||||
salt: 14,
|
||||
})
|
||||
const utxo2_1_out_1: Utxo = Utxo.newERC20Note({
|
||||
eth: 22222333332,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: alicePubKey,
|
||||
salt: 15,
|
||||
})
|
||||
const utxo2_1_out_2: Utxo = Utxo.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 3333,
|
||||
pubKey: bobPubKey,
|
||||
salt: 16,
|
||||
})
|
||||
|
||||
const KITTY_1 =
|
||||
'0x0078917891789178917891789178917891789178917891789178917891789178'
|
||||
@@ -65,113 +53,86 @@ const KITTY_2 =
|
||||
const USER_A = '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1'
|
||||
const CONTRACT_B = '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0'
|
||||
|
||||
const utxo2_2_in_1: Utxo = Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 7777777777,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: KITTY_1,
|
||||
pubKey: bobPubKey,
|
||||
salt: 17,
|
||||
}),
|
||||
)
|
||||
const utxo2_2_out_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 7777777776,
|
||||
pubKey: bobPubKey,
|
||||
salt: 18,
|
||||
}),
|
||||
)
|
||||
const utxo2_2_out_2: Utxo = Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: KITTY_1,
|
||||
pubKey: alicePubKey,
|
||||
salt: 19,
|
||||
}),
|
||||
)
|
||||
|
||||
const utxo3_in_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 111111111111111,
|
||||
pubKey: alicePubKey,
|
||||
salt: 21,
|
||||
}),
|
||||
)
|
||||
const utxo3_in_2: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 222222222222222,
|
||||
pubKey: alicePubKey,
|
||||
salt: 22,
|
||||
}),
|
||||
)
|
||||
const utxo3_in_3: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 333333333333333,
|
||||
pubKey: alicePubKey,
|
||||
salt: 23,
|
||||
}),
|
||||
)
|
||||
const utxo3_out_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 666666666666664,
|
||||
pubKey: alicePubKey,
|
||||
salt: 24,
|
||||
}),
|
||||
)
|
||||
const utxo2_2_in_1: Utxo = Utxo.newNFTNote({
|
||||
eth: 7777777777,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: KITTY_1,
|
||||
pubKey: bobPubKey,
|
||||
salt: 17,
|
||||
})
|
||||
const utxo2_2_out_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 7777777776,
|
||||
pubKey: bobPubKey,
|
||||
salt: 18,
|
||||
})
|
||||
const utxo2_2_out_2: Utxo = Utxo.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: KITTY_1,
|
||||
pubKey: alicePubKey,
|
||||
salt: 19,
|
||||
})
|
||||
const utxo3_in_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 111111111111111,
|
||||
pubKey: alicePubKey,
|
||||
salt: 21,
|
||||
})
|
||||
const utxo3_in_2: Utxo = Utxo.newEtherNote({
|
||||
eth: 222222222222222,
|
||||
pubKey: alicePubKey,
|
||||
salt: 22,
|
||||
})
|
||||
const utxo3_in_3: Utxo = Utxo.newEtherNote({
|
||||
eth: 333333333333333,
|
||||
pubKey: alicePubKey,
|
||||
salt: 23,
|
||||
})
|
||||
const utxo3_out_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 666666666666664,
|
||||
pubKey: alicePubKey,
|
||||
salt: 24,
|
||||
})
|
||||
|
||||
utxo3_out_1.toWithdrawal({ to: Field.from(USER_A), fee: Field.from(1) })
|
||||
|
||||
const utxo4_in_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 8888888888888,
|
||||
pubKey: alicePubKey,
|
||||
salt: 25,
|
||||
}),
|
||||
)
|
||||
const utxo4_in_2: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: alicePubKey,
|
||||
salt: 26,
|
||||
}),
|
||||
)
|
||||
const utxo4_in_3: Utxo = Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: KITTY_2,
|
||||
pubKey: alicePubKey,
|
||||
salt: 27,
|
||||
}),
|
||||
)
|
||||
const utxo4_out_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 8888888888884,
|
||||
pubKey: alicePubKey,
|
||||
salt: 28,
|
||||
}), // fee for tx & fee for withdrawal for each utxos
|
||||
)
|
||||
const utxo4_out_2: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: alicePubKey,
|
||||
salt: 29,
|
||||
}),
|
||||
)
|
||||
const utxo4_out_3: Utxo = Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: KITTY_2,
|
||||
pubKey: alicePubKey,
|
||||
salt: 30,
|
||||
}),
|
||||
)
|
||||
const utxo4_in_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 8888888888888,
|
||||
pubKey: alicePubKey,
|
||||
salt: 25,
|
||||
})
|
||||
const utxo4_in_2: Utxo = Utxo.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: alicePubKey,
|
||||
salt: 26,
|
||||
})
|
||||
const utxo4_in_3: Utxo = Utxo.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: KITTY_2,
|
||||
pubKey: alicePubKey,
|
||||
salt: 27,
|
||||
})
|
||||
const utxo4_out_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 8888888888884,
|
||||
pubKey: alicePubKey,
|
||||
salt: 28,
|
||||
}) // fee for tx & fee for withdrawal for each utxos
|
||||
const utxo4_out_2: Utxo = Utxo.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: alicePubKey,
|
||||
salt: 29,
|
||||
})
|
||||
const utxo4_out_3: Utxo = Utxo.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: KITTY_2,
|
||||
pubKey: alicePubKey,
|
||||
salt: 30,
|
||||
})
|
||||
const migration_4_1 = utxo4_out_1.toMigration({
|
||||
to: Field.from(CONTRACT_B),
|
||||
fee: Field.from(1),
|
||||
|
||||
@@ -14,9 +14,9 @@ export const dummyHeader: Header = {
|
||||
fee: strToField('totalFee').toUint256(),
|
||||
utxoRoot: strToField('utxoRoot').toUint256(),
|
||||
utxoIndex: strToField('utxoIndex').toUint256(),
|
||||
nullifierRoot: strToField('nullifierRoot').toBytes32(),
|
||||
withdrawalRoot: strToField('withdrawalRoot').toBytes32(),
|
||||
withdrawalRoot: strToField('withdrawalRoot').toUint256(),
|
||||
withdrawalIndex: strToField('withdrawalIndex').toUint256(),
|
||||
nullifierRoot: strToField('nullifierRoot').toBytes32(),
|
||||
txRoot: strToField('txRoot').toBytes32(),
|
||||
depositRoot: strToField('depositRoot').toBytes32(),
|
||||
migrationRoot: strToField('migrationRoot').toBytes32(),
|
||||
|
||||
@@ -1,168 +1,130 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { Field } from '@zkopru/babyjubjub'
|
||||
import { Note, TokenUtils, Utxo, Withdrawal } from '@zkopru/transaction'
|
||||
import { TokenUtils, Utxo, Withdrawal } from '@zkopru/transaction'
|
||||
import { accounts, address, nfts } from './testset-keys'
|
||||
|
||||
const utxo1_in_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 3333,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 11,
|
||||
}),
|
||||
)
|
||||
const utxo1_out_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 2221,
|
||||
pubKey: accounts.bob.pubKey,
|
||||
salt: 12,
|
||||
}),
|
||||
)
|
||||
const utxo1_out_2: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 1111,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 13,
|
||||
}),
|
||||
)
|
||||
const utxo2_1_in_1: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 22222333333,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 8888,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 14,
|
||||
}),
|
||||
)
|
||||
const utxo2_1_out_1: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 22222333332,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 15,
|
||||
}),
|
||||
)
|
||||
const utxo2_1_out_2: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 3333,
|
||||
pubKey: accounts.bob.pubKey,
|
||||
salt: 16,
|
||||
}),
|
||||
)
|
||||
const utxo1_in_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 3333,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 11,
|
||||
})
|
||||
const utxo1_out_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 2221,
|
||||
pubKey: accounts.bob.pubKey,
|
||||
salt: 12,
|
||||
})
|
||||
const utxo1_out_2: Utxo = Utxo.newEtherNote({
|
||||
eth: 1111,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 13,
|
||||
})
|
||||
const utxo2_1_in_1: Utxo = Utxo.newERC20Note({
|
||||
eth: 22222333333,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 8888,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 14,
|
||||
})
|
||||
const utxo2_1_out_1: Utxo = Utxo.newERC20Note({
|
||||
eth: 22222333332,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 15,
|
||||
})
|
||||
const utxo2_1_out_2: Utxo = Utxo.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 3333,
|
||||
pubKey: accounts.bob.pubKey,
|
||||
salt: 16,
|
||||
})
|
||||
|
||||
const utxo2_2_in_1: Utxo = Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 7777777777,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: nfts.KITTY_1,
|
||||
pubKey: accounts.bob.pubKey,
|
||||
salt: 17,
|
||||
}),
|
||||
)
|
||||
const utxo2_2_out_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 7777777776,
|
||||
pubKey: accounts.bob.pubKey,
|
||||
salt: 18,
|
||||
}),
|
||||
)
|
||||
const utxo2_2_out_2: Utxo = Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: nfts.KITTY_1,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 19,
|
||||
}),
|
||||
)
|
||||
const utxo2_2_in_1: Utxo = Utxo.newNFTNote({
|
||||
eth: 7777777777,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: nfts.KITTY_1,
|
||||
pubKey: accounts.bob.pubKey,
|
||||
salt: 17,
|
||||
})
|
||||
const utxo2_2_out_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 7777777776,
|
||||
pubKey: accounts.bob.pubKey,
|
||||
salt: 18,
|
||||
})
|
||||
const utxo2_2_out_2: Utxo = Utxo.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: nfts.KITTY_1,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 19,
|
||||
})
|
||||
|
||||
const utxo3_in_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 111111111111111,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 21,
|
||||
}),
|
||||
)
|
||||
const utxo3_in_2: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 222222222222222,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 22,
|
||||
}),
|
||||
)
|
||||
const utxo3_in_3: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 333333333333333,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 23,
|
||||
}),
|
||||
)
|
||||
const withdrawal3_out_1: Withdrawal = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 666666666666664,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 24,
|
||||
}),
|
||||
).toWithdrawal({ to: Field.from(address.USER_A), fee: Field.from(1) })
|
||||
const utxo3_in_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 111111111111111,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 21,
|
||||
})
|
||||
const utxo3_in_2: Utxo = Utxo.newEtherNote({
|
||||
eth: 222222222222222,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 22,
|
||||
})
|
||||
const utxo3_in_3: Utxo = Utxo.newEtherNote({
|
||||
eth: 333333333333333,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 23,
|
||||
})
|
||||
const withdrawal3_out_1: Withdrawal = Utxo.newEtherNote({
|
||||
eth: 666666666666664,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 24,
|
||||
}).toWithdrawal({ to: Field.from(address.USER_A), fee: Field.from(1) })
|
||||
|
||||
const utxo4_in_1: Utxo = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 8888888888888,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 25,
|
||||
}),
|
||||
)
|
||||
const utxo4_in_2: Utxo = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 26,
|
||||
}),
|
||||
)
|
||||
const utxo4_in_3: Utxo = Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: nfts.KITTY_2,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 27,
|
||||
}),
|
||||
)
|
||||
const migration_4_1 = Utxo.from(
|
||||
Note.newEtherNote({
|
||||
eth: 8888888888884,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 28,
|
||||
}), // fee for tx & fee for withdrawal for each utxos
|
||||
).toMigration({
|
||||
const utxo4_in_1: Utxo = Utxo.newEtherNote({
|
||||
eth: 8888888888888,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 25,
|
||||
})
|
||||
const utxo4_in_2: Utxo = Utxo.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 26,
|
||||
})
|
||||
const utxo4_in_3: Utxo = Utxo.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: nfts.KITTY_2,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 27,
|
||||
})
|
||||
const migration_4_1 = Utxo.newEtherNote({
|
||||
eth: 8888888888884,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 28,
|
||||
}).toMigration({
|
||||
to: Field.from(address.CONTRACT_B),
|
||||
fee: Field.from(1), // fee for tx & fee for withdrawal for each utxos
|
||||
})
|
||||
const migration_4_2 = Utxo.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 29,
|
||||
}).toMigration({
|
||||
to: Field.from(address.CONTRACT_B),
|
||||
fee: Field.from(1),
|
||||
})
|
||||
const migration_4_2 = Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.DAI,
|
||||
erc20Amount: 5555,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 29,
|
||||
}),
|
||||
).toMigration({
|
||||
to: Field.from(address.CONTRACT_B),
|
||||
fee: Field.from(1),
|
||||
})
|
||||
const migration_4_3 = Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: nfts.KITTY_2,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 30,
|
||||
}),
|
||||
).toMigration({
|
||||
const migration_4_3 = Utxo.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: TokenUtils.CRYPTO_KITTIES,
|
||||
nft: nfts.KITTY_2,
|
||||
pubKey: accounts.alice.pubKey,
|
||||
salt: 30,
|
||||
}).toMigration({
|
||||
to: Field.from(address.CONTRACT_B),
|
||||
fee: Field.from(1),
|
||||
})
|
||||
|
||||
@@ -101,7 +101,7 @@ export async function loadGrove(db: DB): Promise<{ grove: Grove }> {
|
||||
const latestTree = grove.latestUTXOTree()
|
||||
const size = latestTree ? latestTree.latestLeafIndex() : Field.zero
|
||||
if (size.eqn(0)) {
|
||||
await grove.applyPatch({
|
||||
await grove.applyGrovePatch({
|
||||
utxos: [
|
||||
utxos.utxo1_in_1,
|
||||
utxos.utxo2_1_in_1,
|
||||
@@ -112,7 +112,7 @@ export async function loadGrove(db: DB): Promise<{ grove: Grove }> {
|
||||
utxos.utxo4_in_1,
|
||||
utxos.utxo4_in_2,
|
||||
utxos.utxo4_in_3,
|
||||
].map(utxo => ({ leafHash: utxo.hash(), note: utxo })),
|
||||
].map(utxo => ({ hash: utxo.hash(), note: utxo })),
|
||||
withdrawals: [],
|
||||
nullifiers: [],
|
||||
})
|
||||
|
||||
Binary file not shown.
@@ -116,22 +116,55 @@ model Deposit {
|
||||
queuedAt String
|
||||
}
|
||||
|
||||
model Note {
|
||||
model Utxo {
|
||||
hash String @id
|
||||
index String?
|
||||
eth String?
|
||||
pubKey String?
|
||||
salt String?
|
||||
tokenAddr String?
|
||||
erc20Amount String?
|
||||
nft String?
|
||||
type Int?
|
||||
withdrawOutTo String?
|
||||
status Int?
|
||||
treeId String?
|
||||
index String?
|
||||
nullifier String?
|
||||
usedFor String?
|
||||
tree LightTree? @relation(fields: [treeId], references: [id])
|
||||
}
|
||||
|
||||
model Withdrawal {
|
||||
hash String @id
|
||||
withdrawalHash String
|
||||
eth String
|
||||
pubKey String?
|
||||
salt String?
|
||||
tokenAddr String
|
||||
erc20Amount String
|
||||
nft String
|
||||
to String
|
||||
fee String
|
||||
status Int?
|
||||
treeId String?
|
||||
index String?
|
||||
usedFor String?
|
||||
prepayer String?
|
||||
siblings String? // stringified str[]
|
||||
tree LightTree? @relation(fields: [treeId], references: [id])
|
||||
}
|
||||
|
||||
model Migration {
|
||||
hash String @id
|
||||
eth String?
|
||||
pubKey String?
|
||||
salt String?
|
||||
tokenAddr String?
|
||||
erc20Amount String?
|
||||
nft String?
|
||||
to String?
|
||||
fee String?
|
||||
status Int?
|
||||
noteType Int
|
||||
treeId String?
|
||||
nullifier String?
|
||||
index String?
|
||||
usedFor String?
|
||||
tree LightTree? @relation(fields: [treeId], references: [id])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { Field, F } from '@zkopru/babyjubjub'
|
||||
import { hexify } from '@zkopru/utils'
|
||||
import { v4 } from 'uuid'
|
||||
import { TreeNode, PrismaClient, PrismaClientOptions } from '@prisma/client'
|
||||
import {
|
||||
TreeNode,
|
||||
Utxo,
|
||||
Withdrawal,
|
||||
Migration,
|
||||
PrismaClient,
|
||||
PrismaClientOptions,
|
||||
} from '@prisma/client'
|
||||
import BN from 'bn.js'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import AsyncLock from 'async-lock'
|
||||
|
||||
export type NoteSql = Utxo | Withdrawal | Migration
|
||||
|
||||
export enum TreeSpecies {
|
||||
UTXO = 0,
|
||||
WITHDRAWAL = 1,
|
||||
@@ -22,12 +31,6 @@ export enum BlockStatus {
|
||||
REVERTED = 6,
|
||||
}
|
||||
|
||||
export enum NoteType {
|
||||
UTXO = 0,
|
||||
WITHDRAWAL = 1,
|
||||
MIGRATION = 2,
|
||||
}
|
||||
|
||||
export const NULLIFIER_TREE_ID = 'nullifier-tree'
|
||||
|
||||
export {
|
||||
@@ -43,7 +46,6 @@ export {
|
||||
Deposit,
|
||||
MassDeposit,
|
||||
Proposal,
|
||||
Note,
|
||||
} from '@prisma/client'
|
||||
|
||||
export interface MockupDB {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { randomHex } from 'web3-utils'
|
||||
import * as circomlib from 'circomlib'
|
||||
import * as chacha20 from 'chacha20'
|
||||
import { Field, F, Point } from '@zkopru/babyjubjub'
|
||||
import { Note as NoteSql } from '@zkopru/prisma'
|
||||
import * as TokenUtils from './tokens'
|
||||
import { ZkOutflow } from './zk_tx'
|
||||
import { Field, Point } from '@zkopru/babyjubjub'
|
||||
import { NoteSql } from '@zkopru/prisma'
|
||||
|
||||
const poseidonHash = circomlib.poseidon.createHash(6, 8, 57)
|
||||
|
||||
@@ -14,6 +10,17 @@ export enum OutflowType {
|
||||
MIGRATION = 2,
|
||||
}
|
||||
|
||||
export enum NoteStatus {
|
||||
NON_INCLUDED = 0,
|
||||
UNSPENT = 1,
|
||||
SPENDING = 2,
|
||||
SPENT = 3,
|
||||
WAITING_FINALIZATION = 4,
|
||||
WITHDRAWABLE = 5,
|
||||
TRANSFERRED = 6,
|
||||
WITHDRAWN = 7,
|
||||
}
|
||||
|
||||
export class Note {
|
||||
outflowType: OutflowType
|
||||
|
||||
@@ -46,69 +53,18 @@ export class Note {
|
||||
this.outflowType = OutflowType.UTXO
|
||||
}
|
||||
|
||||
static newEtherNote({
|
||||
eth,
|
||||
pubKey,
|
||||
salt,
|
||||
}: {
|
||||
eth: F
|
||||
pubKey: Point
|
||||
salt?: F
|
||||
}): Note {
|
||||
return new Note(
|
||||
Field.from(eth),
|
||||
salt ? Field.from(salt) : Field.from(randomHex(16)),
|
||||
Field.from(0),
|
||||
Field.from(0),
|
||||
Field.from(0),
|
||||
pubKey,
|
||||
)
|
||||
}
|
||||
|
||||
static newERC20Note({
|
||||
eth,
|
||||
tokenAddr,
|
||||
erc20Amount,
|
||||
pubKey,
|
||||
salt,
|
||||
}: {
|
||||
eth: F
|
||||
tokenAddr: F
|
||||
erc20Amount: F
|
||||
pubKey: Point
|
||||
salt?: F
|
||||
}): Note {
|
||||
return new Note(
|
||||
Field.from(eth),
|
||||
salt ? Field.from(salt) : Field.from(randomHex(16)),
|
||||
Field.from(tokenAddr),
|
||||
Field.from(erc20Amount),
|
||||
Field.from(0),
|
||||
pubKey,
|
||||
)
|
||||
}
|
||||
|
||||
static newNFTNote({
|
||||
eth,
|
||||
tokenAddr,
|
||||
nft,
|
||||
pubKey,
|
||||
salt,
|
||||
}: {
|
||||
eth: F
|
||||
tokenAddr: F
|
||||
nft: F
|
||||
pubKey: Point
|
||||
salt?: F
|
||||
}): Note {
|
||||
return new Note(
|
||||
Field.from(eth),
|
||||
salt ? Field.from(salt) : Field.from(randomHex(16)),
|
||||
Field.from(tokenAddr),
|
||||
Field.from(0),
|
||||
Field.from(nft),
|
||||
pubKey,
|
||||
)
|
||||
toJSON(): string {
|
||||
return JSON.stringify({
|
||||
eth: this.eth,
|
||||
salt: this.salt,
|
||||
token: this.tokenAddr,
|
||||
amount: this.erc20Amount,
|
||||
nft: this.nft.toHex(),
|
||||
pubKey: {
|
||||
x: this.pubKey.x,
|
||||
y: this.pubKey.y,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
hash(): Field {
|
||||
@@ -131,57 +87,6 @@ export class Note {
|
||||
return resultHash
|
||||
}
|
||||
|
||||
nullifier(): Field {
|
||||
const hash = poseidonHash([
|
||||
this.hash().toIden3BigInt(),
|
||||
this.salt.toIden3BigInt(),
|
||||
]).toString()
|
||||
const val = Field.from(hash)
|
||||
return val
|
||||
}
|
||||
|
||||
encrypt(): Buffer {
|
||||
const ephemeralSecretKey: Field = Field.from(randomHex(16))
|
||||
const sharedKey: Buffer = this.pubKey.mul(ephemeralSecretKey).encode()
|
||||
const tokenId = TokenUtils.getTokenId(this.tokenAddr)
|
||||
const value = this.eth || this.erc20Amount || this.nft
|
||||
const secret = [
|
||||
this.salt.toBuffer('be', 16),
|
||||
Field.from(tokenId).toBuffer('be', 1),
|
||||
value.toBuffer('be', 32),
|
||||
]
|
||||
const ciphertext = chacha20.encrypt(sharedKey, 0, Buffer.concat(secret))
|
||||
const encryptedMemo = Buffer.concat([
|
||||
Point.generate(ephemeralSecretKey).encode(),
|
||||
ciphertext,
|
||||
])
|
||||
// 32bytes ephemeral pub key + 16 bytes salt + 1 byte token id + 32 bytes toIden3BigInt()ue = 81 bytes
|
||||
return encryptedMemo
|
||||
}
|
||||
|
||||
toZkOutflow(): ZkOutflow {
|
||||
const outflowType: Field = Field.from(this.outflowType)
|
||||
const outflow = {
|
||||
note: this.hash(),
|
||||
outflowType,
|
||||
}
|
||||
return outflow
|
||||
}
|
||||
|
||||
toJSON(): string {
|
||||
return JSON.stringify({
|
||||
eth: this.eth,
|
||||
salt: this.salt,
|
||||
token: this.tokenAddr,
|
||||
amount: this.erc20Amount,
|
||||
nft: this.nft.toHex(),
|
||||
pubKey: {
|
||||
x: this.pubKey.x,
|
||||
y: this.pubKey.y,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
static fromJSON(data: string): Note {
|
||||
const obj = JSON.parse(data)
|
||||
return new Note(
|
||||
@@ -207,62 +112,4 @@ export class Note {
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
static decrypt({
|
||||
utxoHash,
|
||||
memo,
|
||||
privKey,
|
||||
}: {
|
||||
utxoHash: Field
|
||||
memo: Buffer
|
||||
privKey: string
|
||||
}): Note | undefined {
|
||||
const multiplier = Point.getMultiplier(privKey)
|
||||
const ephemeralPubKey = Point.decode(memo.subarray(0, 32))
|
||||
const sharedKey = ephemeralPubKey.mul(multiplier).encode()
|
||||
const data = memo.subarray(32, 81)
|
||||
const decrypted = chacha20.decrypt(sharedKey, 0, data)
|
||||
const salt = Field.fromBuffer(decrypted.subarray(0, 16))
|
||||
const tokenAddress = TokenUtils.getTokenAddress(
|
||||
decrypted.subarray(16, 17)[0],
|
||||
)
|
||||
if (tokenAddress === null) {
|
||||
return
|
||||
}
|
||||
const value = Field.fromBuffer(decrypted.subarray(17, 49))
|
||||
|
||||
const myPubKey: Point = Point.fromPrivKey(privKey)
|
||||
if (tokenAddress.isZero()) {
|
||||
const etherNote = Note.newEtherNote({
|
||||
eth: value,
|
||||
pubKey: myPubKey,
|
||||
salt,
|
||||
})
|
||||
if (utxoHash.eq(etherNote.hash())) {
|
||||
return etherNote
|
||||
}
|
||||
} else {
|
||||
const erc20Note = Note.newERC20Note({
|
||||
eth: Field.from(0),
|
||||
tokenAddr: tokenAddress,
|
||||
erc20Amount: value,
|
||||
pubKey: myPubKey,
|
||||
salt,
|
||||
})
|
||||
if (utxoHash.eq(erc20Note.hash())) {
|
||||
return erc20Note
|
||||
}
|
||||
const nftNote = Note.newNFTNote({
|
||||
eth: Field.from(0),
|
||||
tokenAddr: tokenAddress,
|
||||
nft: value,
|
||||
pubKey: myPubKey,
|
||||
salt,
|
||||
})
|
||||
if (utxoHash.eq(nftNote.hash())) {
|
||||
return nftNote
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
5
packages/transaction/src/outflow.ts
Normal file
5
packages/transaction/src/outflow.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Withdrawal } from './withdrawal'
|
||||
import { Utxo } from './utxo'
|
||||
import { Migration } from './migration'
|
||||
|
||||
export type Outflow = Utxo | Withdrawal | Migration
|
||||
@@ -4,13 +4,14 @@ import { fromWei } from 'web3-utils'
|
||||
import assert from 'assert'
|
||||
import { Utxo } from './utxo'
|
||||
import { Sum } from './note-sum'
|
||||
import { Note, OutflowType } from './note'
|
||||
import { Outflow } from './outflow'
|
||||
import { Withdrawal } from './withdrawal'
|
||||
import { Migration } from './migration'
|
||||
import { OutflowType } from './note'
|
||||
|
||||
export interface RawTx {
|
||||
inflow: Utxo[]
|
||||
outflow: Note[]
|
||||
outflow: Outflow[]
|
||||
swap?: Field
|
||||
fee: Field
|
||||
}
|
||||
@@ -18,7 +19,7 @@ export interface RawTx {
|
||||
export class TxBuilder {
|
||||
spendables: Utxo[]
|
||||
|
||||
sendings: Note[]
|
||||
sendings: Outflow[]
|
||||
|
||||
feePerByte: Field
|
||||
|
||||
@@ -77,7 +78,7 @@ export class TxBuilder {
|
||||
throw Error(
|
||||
'You should have only one value of withdrawalTo or migrationTo',
|
||||
)
|
||||
const note = Note.newEtherNote({ eth, pubKey: to })
|
||||
const note = Utxo.newEtherNote({ eth, pubKey: to })
|
||||
this.send(note)
|
||||
return this
|
||||
}
|
||||
@@ -103,7 +104,7 @@ export class TxBuilder {
|
||||
fee: F
|
||||
}
|
||||
}): TxBuilder {
|
||||
const note = Note.newERC20Note({
|
||||
const note = Utxo.newERC20Note({
|
||||
eth: eth || 0,
|
||||
tokenAddr,
|
||||
erc20Amount,
|
||||
@@ -134,7 +135,7 @@ export class TxBuilder {
|
||||
fee: F
|
||||
}
|
||||
}): TxBuilder {
|
||||
const note = Note.newNFTNote({
|
||||
const note = Utxo.newNFTNote({
|
||||
eth: eth || 0,
|
||||
tokenAddr,
|
||||
nft,
|
||||
@@ -232,14 +233,12 @@ export class TxBuilder {
|
||||
const change = spendingAmount().erc20[addr].sub(sendingAmount.erc20[addr])
|
||||
if (!change.isZero()) {
|
||||
changes.push(
|
||||
Utxo.from(
|
||||
Note.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: Field.from(addr),
|
||||
erc20Amount: change,
|
||||
pubKey: this.changeTo,
|
||||
}),
|
||||
),
|
||||
Utxo.newERC20Note({
|
||||
eth: 0,
|
||||
tokenAddr: Field.from(addr),
|
||||
erc20Amount: change,
|
||||
pubKey: this.changeTo,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -259,14 +258,12 @@ export class TxBuilder {
|
||||
Object.keys(extraNFTs).forEach(addr => {
|
||||
extraNFTs[addr].forEach(nft => {
|
||||
changes.push(
|
||||
Utxo.from(
|
||||
Note.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: Field.from(addr),
|
||||
nft,
|
||||
pubKey: this.changeTo,
|
||||
}),
|
||||
),
|
||||
Utxo.newNFTNote({
|
||||
eth: 0,
|
||||
tokenAddr: Field.from(addr),
|
||||
nft,
|
||||
pubKey: this.changeTo,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -314,9 +311,7 @@ export class TxBuilder {
|
||||
const changeETH = spendingAmount().eth.sub(getRequiredETH())
|
||||
const finalFee = getTxFee()
|
||||
if (!changeETH.isZero()) {
|
||||
changes.push(
|
||||
Utxo.from(Note.newEtherNote({ eth: changeETH, pubKey: this.changeTo })),
|
||||
)
|
||||
changes.push(Utxo.newEtherNote({ eth: changeETH, pubKey: this.changeTo }))
|
||||
}
|
||||
|
||||
const inflow = [...spendings]
|
||||
@@ -348,7 +343,7 @@ export class TxBuilder {
|
||||
}
|
||||
|
||||
private send(
|
||||
note: Note,
|
||||
note: Outflow,
|
||||
withdrawal?: {
|
||||
to: F
|
||||
fee: F
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { randomHex } from 'web3-utils'
|
||||
import * as circomlib from 'circomlib'
|
||||
import * as chacha20 from 'chacha20'
|
||||
import { Field, F, Point } from '@zkopru/babyjubjub'
|
||||
import { Note, OutflowType } from './note'
|
||||
import { Note, OutflowType, NoteStatus } from './note'
|
||||
import { Withdrawal } from './withdrawal'
|
||||
import { Migration } from './migration'
|
||||
import * as TokenUtils from './tokens'
|
||||
import { ZkOutflow } from './zk_tx'
|
||||
|
||||
const poseidonHash = circomlib.poseidon.createHash(6, 8, 57)
|
||||
|
||||
export enum UtxoStatus {
|
||||
NON_INCLUDED = 0,
|
||||
UNSPENT = 1,
|
||||
SPENDING = 2,
|
||||
SPENT = 3,
|
||||
NON_INCLUDED = NoteStatus.NON_INCLUDED,
|
||||
UNSPENT = NoteStatus.UNSPENT,
|
||||
SPENDING = NoteStatus.SPENDING,
|
||||
SPENT = NoteStatus.SPENT,
|
||||
}
|
||||
|
||||
export class Utxo extends Note {
|
||||
@@ -68,4 +75,167 @@ export class Utxo extends Note {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
encrypt(): Buffer {
|
||||
const ephemeralSecretKey: Field = Field.from(randomHex(16))
|
||||
const sharedKey: Buffer = this.pubKey.mul(ephemeralSecretKey).encode()
|
||||
const tokenId = TokenUtils.getTokenId(this.tokenAddr)
|
||||
const value = this.eth || this.erc20Amount || this.nft
|
||||
const secret = [
|
||||
this.salt.toBuffer('be', 16),
|
||||
Field.from(tokenId).toBuffer('be', 1),
|
||||
value.toBuffer('be', 32),
|
||||
]
|
||||
const ciphertext = chacha20.encrypt(sharedKey, 0, Buffer.concat(secret))
|
||||
const encryptedMemo = Buffer.concat([
|
||||
Point.generate(ephemeralSecretKey).encode(),
|
||||
ciphertext,
|
||||
])
|
||||
// 32bytes ephemeral pub key + 16 bytes salt + 1 byte token id + 32 bytes toIden3BigInt()ue = 81 bytes
|
||||
return encryptedMemo
|
||||
}
|
||||
|
||||
nullifier(): Field {
|
||||
const hash = poseidonHash([
|
||||
this.hash().toIden3BigInt(),
|
||||
this.salt.toIden3BigInt(),
|
||||
]).toString()
|
||||
const val = Field.from(hash)
|
||||
return val
|
||||
}
|
||||
|
||||
toZkOutflow(): ZkOutflow {
|
||||
const outflowType: Field = Field.from(OutflowType.UTXO)
|
||||
const outflow = {
|
||||
note: this.hash(),
|
||||
outflowType,
|
||||
}
|
||||
return outflow
|
||||
}
|
||||
|
||||
static decrypt({
|
||||
utxoHash,
|
||||
memo,
|
||||
privKey,
|
||||
}: {
|
||||
utxoHash: Field
|
||||
memo: Buffer
|
||||
privKey: string
|
||||
}): Utxo | undefined {
|
||||
const multiplier = Point.getMultiplier(privKey)
|
||||
const ephemeralPubKey = Point.decode(memo.subarray(0, 32))
|
||||
const sharedKey = ephemeralPubKey.mul(multiplier).encode()
|
||||
const data = memo.subarray(32, 81)
|
||||
const decrypted = chacha20.decrypt(sharedKey, 0, data)
|
||||
const salt = Field.fromBuffer(decrypted.subarray(0, 16))
|
||||
const tokenAddress = TokenUtils.getTokenAddress(
|
||||
decrypted.subarray(16, 17)[0],
|
||||
)
|
||||
if (tokenAddress === null) {
|
||||
return
|
||||
}
|
||||
const value = Field.fromBuffer(decrypted.subarray(17, 49))
|
||||
|
||||
const myPubKey: Point = Point.fromPrivKey(privKey)
|
||||
if (tokenAddress.isZero()) {
|
||||
const etherNote = Utxo.newEtherNote({
|
||||
eth: value,
|
||||
pubKey: myPubKey,
|
||||
salt,
|
||||
})
|
||||
if (utxoHash.eq(etherNote.hash())) {
|
||||
return etherNote
|
||||
}
|
||||
} else {
|
||||
const erc20Note = Utxo.newERC20Note({
|
||||
eth: Field.from(0),
|
||||
tokenAddr: tokenAddress,
|
||||
erc20Amount: value,
|
||||
pubKey: myPubKey,
|
||||
salt,
|
||||
})
|
||||
if (utxoHash.eq(erc20Note.hash())) {
|
||||
return erc20Note
|
||||
}
|
||||
const nftNote = Utxo.newNFTNote({
|
||||
eth: Field.from(0),
|
||||
tokenAddr: tokenAddress,
|
||||
nft: value,
|
||||
pubKey: myPubKey,
|
||||
salt,
|
||||
})
|
||||
if (utxoHash.eq(nftNote.hash())) {
|
||||
return nftNote
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
static newEtherNote({
|
||||
eth,
|
||||
pubKey,
|
||||
salt,
|
||||
}: {
|
||||
eth: F
|
||||
pubKey: Point
|
||||
salt?: F
|
||||
}): Utxo {
|
||||
const note = new Note(
|
||||
Field.from(eth),
|
||||
salt ? Field.from(salt) : Field.from(randomHex(16)),
|
||||
Field.from(0),
|
||||
Field.from(0),
|
||||
Field.from(0),
|
||||
pubKey,
|
||||
)
|
||||
return Utxo.from(note)
|
||||
}
|
||||
|
||||
static newERC20Note({
|
||||
eth,
|
||||
tokenAddr,
|
||||
erc20Amount,
|
||||
pubKey,
|
||||
salt,
|
||||
}: {
|
||||
eth: F
|
||||
tokenAddr: F
|
||||
erc20Amount: F
|
||||
pubKey: Point
|
||||
salt?: F
|
||||
}): Utxo {
|
||||
const note = new Note(
|
||||
Field.from(eth),
|
||||
salt ? Field.from(salt) : Field.from(randomHex(16)),
|
||||
Field.from(tokenAddr),
|
||||
Field.from(erc20Amount),
|
||||
Field.from(0),
|
||||
pubKey,
|
||||
)
|
||||
return Utxo.from(note)
|
||||
}
|
||||
|
||||
static newNFTNote({
|
||||
eth,
|
||||
tokenAddr,
|
||||
nft,
|
||||
pubKey,
|
||||
salt,
|
||||
}: {
|
||||
eth: F
|
||||
tokenAddr: F
|
||||
nft: F
|
||||
pubKey: Point
|
||||
salt?: F
|
||||
}): Utxo {
|
||||
const note = new Note(
|
||||
Field.from(eth),
|
||||
salt ? Field.from(salt) : Field.from(randomHex(16)),
|
||||
Field.from(tokenAddr),
|
||||
Field.from(0),
|
||||
Field.from(nft),
|
||||
pubKey,
|
||||
)
|
||||
return Utxo.from(note)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Field, Point, F } from '@zkopru/babyjubjub'
|
||||
import { ZkOutflow } from './zk_tx'
|
||||
import { Note, OutflowType } from './note'
|
||||
import { Uint256, Bytes32 } from 'soltypes'
|
||||
import { soliditySha3 } from 'web3-utils'
|
||||
import { ZkOutflow, PublicData } from './zk_tx'
|
||||
import { Note, OutflowType, NoteStatus } from './note'
|
||||
|
||||
export enum WithdrawalStatus {
|
||||
NON_INCLUDED = 0,
|
||||
INCLUDED = 1,
|
||||
NON_INCLUDED = NoteStatus.NON_INCLUDED,
|
||||
UNFINALIZED = NoteStatus.WAITING_FINALIZATION,
|
||||
WITHDRAWABLE = NoteStatus.WITHDRAWABLE,
|
||||
TRANSFERRED = NoteStatus.TRANSFERRED,
|
||||
WITHDRAWN = NoteStatus.WITHDRAWN,
|
||||
}
|
||||
|
||||
export class Withdrawal extends Note {
|
||||
@@ -15,8 +20,6 @@ export class Withdrawal extends Note {
|
||||
fee: Field
|
||||
}
|
||||
|
||||
static outflowType: Field = Field.from(1)
|
||||
|
||||
constructor(
|
||||
eth: Field,
|
||||
salt: Field,
|
||||
@@ -38,7 +41,7 @@ export class Withdrawal extends Note {
|
||||
toZkOutflow(): ZkOutflow {
|
||||
const outflow = {
|
||||
note: this.hash(),
|
||||
outflowType: Withdrawal.outflowType,
|
||||
outflowType: Field.from(OutflowType.WITHDRAWAL),
|
||||
data: {
|
||||
to: this.publicData.to,
|
||||
eth: this.eth,
|
||||
@@ -51,6 +54,33 @@ export class Withdrawal extends Note {
|
||||
return outflow
|
||||
}
|
||||
|
||||
withdrawalHash(): Uint256 {
|
||||
return Withdrawal.withdrawalHash(this.hash(), {
|
||||
to: this.publicData.to,
|
||||
eth: this.eth,
|
||||
tokenAddr: this.tokenAddr,
|
||||
erc20Amount: this.erc20Amount,
|
||||
nft: this.nft,
|
||||
fee: this.publicData.fee,
|
||||
})
|
||||
}
|
||||
|
||||
static withdrawalHash(note: Field, publicData: PublicData): Uint256 {
|
||||
const concatenated = Buffer.concat([
|
||||
note.toBuffer(),
|
||||
publicData.to.toAddress().toBuffer(),
|
||||
publicData.eth.toBytes32().toBuffer(),
|
||||
publicData.tokenAddr.toAddress().toBuffer(),
|
||||
publicData.erc20Amount.toBytes32().toBuffer(),
|
||||
publicData.nft.toBytes32().toBuffer(),
|
||||
publicData.fee.toBytes32().toBuffer(),
|
||||
])
|
||||
const result = soliditySha3(`0x${concatenated.toString('hex')}`)
|
||||
// uint256 note = uint256(keccak256(abi.encodePacked(owner, eth, token, amount, nft, fee)));
|
||||
if (result === null) throw Error('hash result is null')
|
||||
return Bytes32.from(result).toUint()
|
||||
}
|
||||
|
||||
static from(note: Note, to: F, fee: F): Withdrawal {
|
||||
return new Withdrawal(
|
||||
note.eth,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { toBN } from 'web3-utils'
|
||||
import { DB, TreeSpecies, LightTree, TreeNode } from '@zkopru/prisma'
|
||||
import { Hasher, genesisRoot } from './hasher'
|
||||
import { MerkleProof, verifyProof, startingLeafProof } from './merkle-proof'
|
||||
import { Item } from './light-rollup-tree'
|
||||
import { Leaf } from './light-rollup-tree'
|
||||
import { UtxoTree } from './utxo-tree'
|
||||
import { WithdrawalTree } from './withdrawal-tree'
|
||||
import { NullifierTree } from './nullifier-tree'
|
||||
@@ -29,9 +29,9 @@ export interface GroveConfig {
|
||||
|
||||
export interface GrovePatch {
|
||||
header?: string
|
||||
utxos: Item<Field>[]
|
||||
withdrawals: BN[]
|
||||
nullifiers: BN[]
|
||||
utxos: Leaf<Field>[]
|
||||
withdrawals: Leaf<BN>[]
|
||||
nullifiers: Field[]
|
||||
}
|
||||
|
||||
export interface GroveSnapshot {
|
||||
@@ -170,15 +170,26 @@ export class Grove {
|
||||
return this.withdrawalTrees[this.withdrawalTrees.length - 1]
|
||||
}
|
||||
|
||||
async applyPatch(patch: GrovePatch) {
|
||||
async applyGrovePatch(
|
||||
patch: GrovePatch,
|
||||
): Promise<{
|
||||
utxoTreeId: string
|
||||
withdrawalTreeId: string
|
||||
}> {
|
||||
let utxoTreeId!: string
|
||||
let withdrawalTreeId!: string
|
||||
await this.lock.acquire('grove', async () => {
|
||||
await this.appendUTXOs(patch.utxos)
|
||||
await this.appendWithdrawals(patch.withdrawals)
|
||||
utxoTreeId = await this.appendUTXOs(patch.utxos)
|
||||
withdrawalTreeId = await this.appendWithdrawals(patch.withdrawals)
|
||||
await this.markAsNullified(patch.nullifiers)
|
||||
if (this.config.fullSync) {
|
||||
await this.recordBootstrap(patch.header)
|
||||
}
|
||||
})
|
||||
return {
|
||||
utxoTreeId,
|
||||
withdrawalTreeId,
|
||||
}
|
||||
}
|
||||
|
||||
async dryPatch(patch: GrovePatch): Promise<GroveSnapshot> {
|
||||
@@ -187,10 +198,10 @@ export class Grove {
|
||||
this.lock
|
||||
.acquire('grove', async () => {
|
||||
const utxoResult = await this.latestUTXOTree().dryAppend(
|
||||
...patch.utxos,
|
||||
...patch.utxos.map(leaf => ({ ...leaf, shouldTrack: false })),
|
||||
)
|
||||
const withdrawalResult = await this.latestWithdrawalTree().dryAppend(
|
||||
...patch.withdrawals.map(leafHash => ({ leafHash })),
|
||||
...patch.withdrawals.map(leaf => ({ ...leaf, shouldTrack: false })),
|
||||
)
|
||||
const nullifierRoot = await this.nullifierTree?.dryRunNullify(
|
||||
...patch.nullifiers,
|
||||
@@ -253,19 +264,25 @@ export class Grove {
|
||||
}
|
||||
}
|
||||
|
||||
private async appendUTXOs(utxos: Item<Field>[]): Promise<void> {
|
||||
/**
|
||||
*
|
||||
* @param utxos utxos to append
|
||||
* @returns treeId of appended to
|
||||
*/
|
||||
private async appendUTXOs(utxos: Leaf<Field>[]): Promise<string> {
|
||||
const totalItemLen =
|
||||
this.config.utxoSubTreeSize *
|
||||
Math.ceil(utxos.length / this.config.utxoSubTreeSize)
|
||||
|
||||
const fixedSizeUtxos: Item<Field>[] = Array(totalItemLen).fill({
|
||||
leafHash: Field.zero,
|
||||
const fixedSizeUtxos: Leaf<Field>[] = Array(totalItemLen).fill({
|
||||
hash: Field.zero,
|
||||
})
|
||||
utxos.forEach((item: Item<Field>, index: number) => {
|
||||
utxos.forEach((item: Leaf<Field>, index: number) => {
|
||||
fixedSizeUtxos[index] = item
|
||||
})
|
||||
const latestTree = this.latestUTXOTree()
|
||||
if (!latestTree) throw Error('Grove is not initialized')
|
||||
let treeId: string
|
||||
if (
|
||||
latestTree
|
||||
.latestLeafIndex()
|
||||
@@ -273,30 +290,32 @@ export class Grove {
|
||||
.lt(latestTree.maxSize())
|
||||
) {
|
||||
await latestTree.append(...fixedSizeUtxos)
|
||||
treeId = latestTree.metadata.id
|
||||
} else {
|
||||
const { tree } = await this.bootstrapUtxoTree(
|
||||
latestTree.metadata.index + 1,
|
||||
)
|
||||
this.utxoTrees.push(tree)
|
||||
await tree.append(...fixedSizeUtxos)
|
||||
treeId = tree.metadata.id
|
||||
}
|
||||
return treeId
|
||||
}
|
||||
|
||||
private async appendWithdrawals(withdrawals: BN[]): Promise<void> {
|
||||
private async appendWithdrawals(withdrawals: Leaf<BN>[]): Promise<string> {
|
||||
const totalItemLen =
|
||||
this.config.withdrawalSubTreeSize *
|
||||
Math.ceil(withdrawals.length / this.config.withdrawalSubTreeSize)
|
||||
|
||||
const fixedSizeWithdrawals: Item<BN>[] = Array(totalItemLen).fill({
|
||||
leafHash: new BN(0),
|
||||
const fixedSizeWithdrawals: Leaf<BN>[] = Array(totalItemLen).fill({
|
||||
hash: new BN(0),
|
||||
})
|
||||
withdrawals.forEach((withdrawal: BN, index: number) => {
|
||||
fixedSizeWithdrawals[index] = {
|
||||
leafHash: withdrawal,
|
||||
}
|
||||
withdrawals.forEach((withdrawal: Leaf<BN>, index: number) => {
|
||||
fixedSizeWithdrawals[index] = withdrawal
|
||||
})
|
||||
const latestTree = this.latestWithdrawalTree()
|
||||
if (!latestTree) throw Error('Grove is not initialized')
|
||||
let treeId: string
|
||||
if (
|
||||
latestTree
|
||||
.latestLeafIndex()
|
||||
@@ -304,13 +323,16 @@ export class Grove {
|
||||
.lt(latestTree.maxSize())
|
||||
) {
|
||||
await latestTree.append(...fixedSizeWithdrawals)
|
||||
treeId = latestTree.metadata.id
|
||||
} else {
|
||||
const { tree } = await this.bootstrapWithdrawalTree(
|
||||
latestTree.metadata.index + 1,
|
||||
)
|
||||
this.withdrawalTrees.push(tree)
|
||||
await tree.append(...fixedSizeWithdrawals)
|
||||
treeId = tree.metadata.id
|
||||
}
|
||||
return treeId
|
||||
}
|
||||
|
||||
private async markAsNullified(nullifiers: BN[]): Promise<void> {
|
||||
@@ -323,7 +345,7 @@ export class Grove {
|
||||
|
||||
async utxoMerkleProof(hash: Field): Promise<MerkleProof<Field>> {
|
||||
const utxo = await this.db.read(prisma =>
|
||||
prisma.note.findOne({
|
||||
prisma.utxo.findOne({
|
||||
where: {
|
||||
hash: hash.toString(10),
|
||||
},
|
||||
@@ -365,9 +387,9 @@ export class Grove {
|
||||
|
||||
async withdrawalMerkleProof(hash: BN): Promise<MerkleProof<BN>> {
|
||||
const withdrawal = await this.db.read(prisma =>
|
||||
prisma.note.findOne({
|
||||
prisma.withdrawal.findOne({
|
||||
where: {
|
||||
hash: hexify(hash),
|
||||
hash: hash.toString(10),
|
||||
},
|
||||
include: { tree: true },
|
||||
}),
|
||||
@@ -397,10 +419,11 @@ export class Grove {
|
||||
const proof = {
|
||||
root,
|
||||
index: toBN(withdrawal.index),
|
||||
leaf: toBN(withdrawal.hash),
|
||||
leaf: toBN(withdrawal.withdrawalHash),
|
||||
siblings,
|
||||
}
|
||||
verifyProof(this.config.withdrawalHasher, proof)
|
||||
const isValid = verifyProof(this.config.withdrawalHasher, proof)
|
||||
if (!isValid) throw Error('Failed to generate withdrawal merkle proof')
|
||||
return proof
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export {
|
||||
Item,
|
||||
Leaf,
|
||||
TreeMetadata,
|
||||
TreeData,
|
||||
TreeConfig,
|
||||
@@ -16,4 +16,9 @@ export { NullifierTree } from './nullifier-tree'
|
||||
|
||||
export { Hasher, keccakHasher, poseidonHasher, genesisRoot } from './hasher'
|
||||
|
||||
export { Grove, GroveConfig, GrovePatch, GroveSnapshot as DryPatchResult } from './grove'
|
||||
export {
|
||||
Grove,
|
||||
GroveConfig,
|
||||
GrovePatch,
|
||||
GroveSnapshot as DryPatchResult,
|
||||
} from './grove'
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { Field } from '@zkopru/babyjubjub'
|
||||
import AsyncLock from 'async-lock'
|
||||
import { Note, UtxoStatus } from '@zkopru/transaction'
|
||||
import BN from 'bn.js'
|
||||
import { toBN } from 'web3-utils'
|
||||
import { hexify } from '@zkopru/utils'
|
||||
import { DB, TreeSpecies, Note as NoteSql } from '@zkopru/prisma'
|
||||
import { DB, TreeSpecies } from '@zkopru/prisma'
|
||||
import { Hasher } from './hasher'
|
||||
import { MerkleProof, startingLeafProof } from './merkle-proof'
|
||||
|
||||
export interface Item<T extends Field | BN> {
|
||||
leafHash: T
|
||||
note?: Note
|
||||
export interface Leaf<T extends Field | BN> {
|
||||
hash: T
|
||||
noteHash?: Field
|
||||
shouldTrack?: boolean
|
||||
}
|
||||
|
||||
export interface TreeMetadata<T extends Field | BN> {
|
||||
@@ -145,7 +145,7 @@ export abstract class LightRollUpTree<T extends Field | BN> {
|
||||
}
|
||||
|
||||
async append(
|
||||
...items: Item<T>[]
|
||||
...items: Leaf<T>[]
|
||||
): Promise<{
|
||||
root: T
|
||||
index: T
|
||||
@@ -163,7 +163,7 @@ export abstract class LightRollUpTree<T extends Field | BN> {
|
||||
}
|
||||
|
||||
async dryAppend(
|
||||
...items: Item<T>[]
|
||||
...items: Leaf<T>[]
|
||||
): Promise<{
|
||||
root: T
|
||||
index: T
|
||||
@@ -183,7 +183,7 @@ export abstract class LightRollUpTree<T extends Field | BN> {
|
||||
// if note exists, save the data and mark as an item to keep tracking
|
||||
// udpate the latest siblings and save the intermediate value if it needs to be tracked
|
||||
const leafIndex = new BN(1).shln(this.depth).or(index)
|
||||
let node = item.leafHash
|
||||
let node = item.hash
|
||||
let hasRightSibling!: boolean
|
||||
for (let level = 0; level < this.depth; level += 1) {
|
||||
const pathIndex = leafIndex.shrn(level)
|
||||
@@ -339,7 +339,7 @@ export abstract class LightRollUpTree<T extends Field | BN> {
|
||||
}
|
||||
|
||||
private async _append(
|
||||
...items: Item<T>[]
|
||||
...leaves: Leaf<T>[]
|
||||
): Promise<{
|
||||
root: T
|
||||
index: T
|
||||
@@ -351,47 +351,23 @@ export abstract class LightRollUpTree<T extends Field | BN> {
|
||||
[nodeIndex: string]: string
|
||||
} = {}
|
||||
|
||||
const itemsToSave: NoteSql[] = []
|
||||
let root: T = this.root()
|
||||
|
||||
const trackingLeaves: T[] = await this.indexesOfTrackingLeaves()
|
||||
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
const item = items[i]
|
||||
const index = (item.leafHash instanceof Field
|
||||
for (let i = 0; i < leaves.length; i += 1) {
|
||||
const leaf = leaves[i]
|
||||
const index = (leaf.hash instanceof Field
|
||||
? Field.from(i).add(start)
|
||||
: new BN(i).add(start)) as T
|
||||
// if note exists, save the data and mark as an item to keep tracking
|
||||
if (!item.leafHash.isZero() && (this.config.fullSync || item.note)) {
|
||||
let noteSql
|
||||
if (item.note) {
|
||||
noteSql = {
|
||||
hash: item.leafHash.toString(10),
|
||||
index: index.toString(10),
|
||||
eth: item.note.eth.toString(10),
|
||||
pubKey: item.note.pubKey.toHex(),
|
||||
salt: item.note.salt.toString(10),
|
||||
tokenAddr: item.note.tokenAddr.toHex(20),
|
||||
erc20Amount: item.note?.erc20Amount.toString(10),
|
||||
nft: item.note?.nft.toString(10),
|
||||
status: UtxoStatus.UNSPENT,
|
||||
}
|
||||
} else {
|
||||
noteSql = {
|
||||
hash: item.leafHash.toString(10),
|
||||
index: index.toString(10),
|
||||
}
|
||||
}
|
||||
itemsToSave.push(noteSql)
|
||||
}
|
||||
|
||||
if (items[i].note) {
|
||||
// TODO batch transaction
|
||||
if (leaf.shouldTrack) {
|
||||
trackingLeaves.push(index)
|
||||
}
|
||||
|
||||
// udpate the latest siblings and save the intermediate value if it needs to be tracked
|
||||
const leafNodeIndex = new BN(1).shln(this.depth).or(index)
|
||||
let node = item.leafHash
|
||||
let node = leaf.hash
|
||||
let hasRightSibling!: boolean
|
||||
for (let level = 0; level < this.depth; level += 1) {
|
||||
const pathIndex = leafNodeIndex.shrn(level)
|
||||
@@ -437,7 +413,7 @@ export abstract class LightRollUpTree<T extends Field | BN> {
|
||||
// update root
|
||||
root = node
|
||||
}
|
||||
const end: T = start.addn(items.length) as T
|
||||
const end: T = start.addn(leaves.length) as T
|
||||
// update the latest siblings
|
||||
this.data = {
|
||||
root,
|
||||
@@ -480,24 +456,6 @@ export abstract class LightRollUpTree<T extends Field | BN> {
|
||||
},
|
||||
}),
|
||||
)
|
||||
// insert notes
|
||||
// TODO prisma batch transaction
|
||||
for (const noteSql of itemsToSave) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.note.upsert({
|
||||
where: { hash: noteSql.hash },
|
||||
update: {
|
||||
...noteSql,
|
||||
tree: { connect: { id: this.metadata.id } },
|
||||
},
|
||||
create: {
|
||||
...noteSql,
|
||||
noteType: this.species,
|
||||
tree: { connect: { id: this.metadata.id } },
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
// update cached nodes
|
||||
// TODO prisma batch transaction
|
||||
for (const nodeIndex of Object.keys(cached)) {
|
||||
|
||||
@@ -31,7 +31,7 @@ export class UtxoTree extends LightRollUpTree<Field> {
|
||||
: []
|
||||
|
||||
const trackingLeaves = await this.db.read(prisma =>
|
||||
prisma.note.findMany({
|
||||
prisma.utxo.findMany({
|
||||
where: {
|
||||
treeId: this.metadata.id,
|
||||
pubKey: { in: keys },
|
||||
|
||||
@@ -30,10 +30,10 @@ export class WithdrawalTree extends LightRollUpTree<BN> {
|
||||
const keys: string[] = this.addressesToObserve || []
|
||||
|
||||
const trackingLeaves = await this.db.read(prisma =>
|
||||
prisma.note.findMany({
|
||||
prisma.withdrawal.findMany({
|
||||
where: {
|
||||
treeId: this.metadata.id,
|
||||
pubKey: { in: keys },
|
||||
OR: [{ to: { in: keys } }, { prepayer: { in: keys } }],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import BN from 'bn.js'
|
||||
import { toBN } from 'web3-utils'
|
||||
import { DB, MockupDB } from '~prisma'
|
||||
import { Field } from '~babyjubjub'
|
||||
import { Grove, poseidonHasher, keccakHasher, Item } from '~tree'
|
||||
import { Grove, poseidonHasher, keccakHasher, Leaf } from '~tree'
|
||||
import { utxos } from '~dataset/testset-utxos'
|
||||
import { address, accounts } from '~dataset/testset-keys'
|
||||
|
||||
@@ -56,19 +56,26 @@ describe('grove full sync grove()', () => {
|
||||
withdrawalIndex: fullSyncGrvoe.latestWithdrawalTree().latestLeafIndex(),
|
||||
nullifierRoot: await fullSyncGrvoe.nullifierTree?.root(),
|
||||
}
|
||||
const utxosToAppend: Item<Field>[] = [
|
||||
utxos.utxo1_in_1.toWithdrawal({ to: address.USER_A, fee: 1 }),
|
||||
const utxosToAppend: Leaf<Field>[] = [
|
||||
utxos.utxo1_out_1,
|
||||
utxos.utxo2_1_in_1,
|
||||
].map(note => ({
|
||||
leafHash: note.hash(),
|
||||
hash: note.hash(),
|
||||
note,
|
||||
shouldTrack: true,
|
||||
}))
|
||||
const withdrawalsToAppend: Leaf<BN>[] = [
|
||||
utxos.utxo1_in_1.toWithdrawal({ to: address.USER_A, fee: 1 }),
|
||||
].map(note => ({
|
||||
hash: note.withdrawalHash().toBN(),
|
||||
noteHash: note.hash(),
|
||||
shouldTrack: true,
|
||||
}))
|
||||
const patch = {
|
||||
header: 'sampleheader',
|
||||
utxos: utxosToAppend,
|
||||
withdrawals: [toBN(1), toBN(2)],
|
||||
nullifiers: [toBN(12), toBN(23)],
|
||||
withdrawals: withdrawalsToAppend,
|
||||
nullifiers: [Field.from(12), Field.from(23)],
|
||||
}
|
||||
await fullSyncGrvoe.dryPatch(patch)
|
||||
const postResult = {
|
||||
@@ -93,21 +100,27 @@ describe('grove full sync grove()', () => {
|
||||
})
|
||||
describe('applyPatch()', () => {
|
||||
it('should update the grove and have same result with the dry patch result', async () => {
|
||||
const utxosToAppend: Item<Field>[] = [
|
||||
utxos.utxo1_in_1.toWithdrawal({ to: address.USER_A, fee: 1 }),
|
||||
const utxosToAppend: Leaf<Field>[] = [
|
||||
utxos.utxo1_out_1,
|
||||
utxos.utxo2_1_in_1,
|
||||
].map(note => ({
|
||||
leafHash: note.hash(),
|
||||
hash: note.hash(),
|
||||
note,
|
||||
}))
|
||||
const withdrawalsToAppend: Leaf<BN>[] = [
|
||||
utxos.utxo1_in_1.toWithdrawal({ to: address.USER_A, fee: 1 }),
|
||||
].map(note => ({
|
||||
hash: note.withdrawalHash().toBN(),
|
||||
noteHash: note.hash(),
|
||||
shouldTrack: true,
|
||||
}))
|
||||
const patch = {
|
||||
utxos: utxosToAppend,
|
||||
withdrawals: [toBN(1), toBN(2)],
|
||||
nullifiers: [toBN(12), toBN(23)],
|
||||
withdrawals: withdrawalsToAppend,
|
||||
nullifiers: [Field.from(12), Field.from(23)],
|
||||
}
|
||||
const expected = await fullSyncGrvoe.dryPatch(patch)
|
||||
await fullSyncGrvoe.applyPatch(patch)
|
||||
await fullSyncGrvoe.applyGrovePatch(patch)
|
||||
const result = {
|
||||
utxoRoot: fullSyncGrvoe.latestUTXOTree().root(),
|
||||
utxoIndex: fullSyncGrvoe.latestUTXOTree().latestLeafIndex(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
UtxoTree,
|
||||
TreeConfig,
|
||||
poseidonHasher,
|
||||
Item,
|
||||
Leaf,
|
||||
genesisRoot,
|
||||
verifyProof,
|
||||
} from '~tree'
|
||||
@@ -64,9 +64,9 @@ describe('utxo tree unit test', () => {
|
||||
}
|
||||
beforeAll(async () => {
|
||||
prevRoot = utxoTree.root()
|
||||
const items: Item<Field>[] = [
|
||||
{ leafHash: Field.from(1) },
|
||||
{ leafHash: Field.from(2) },
|
||||
const items: Leaf<Field>[] = [
|
||||
{ hash: Field.from(1) },
|
||||
{ hash: Field.from(2) },
|
||||
]
|
||||
result = await utxoTree.dryAppend(...items)
|
||||
})
|
||||
@@ -93,9 +93,9 @@ describe('utxo tree unit test', () => {
|
||||
}
|
||||
beforeAll(async () => {
|
||||
prevRoot = utxoTree.root()
|
||||
const items: Item<Field>[] = [
|
||||
{ leafHash: Field.from(1) },
|
||||
{ leafHash: Field.from(2) },
|
||||
const items: Leaf<Field>[] = [
|
||||
{ hash: Field.from(1) },
|
||||
{ hash: Field.from(2) },
|
||||
]
|
||||
dryResult = await utxoTree.dryAppend(...items)
|
||||
result = await utxoTree.append(...items)
|
||||
@@ -111,13 +111,13 @@ describe('utxo tree unit test', () => {
|
||||
it.todo('should have same result with its solidity version')
|
||||
})
|
||||
describe('tracking', () => {
|
||||
const items: Item<Field>[] = [
|
||||
const items: Leaf<Field>[] = [
|
||||
utxos.utxo1_out_1,
|
||||
utxos.utxo1_out_2,
|
||||
utxos.utxo2_1_out_1,
|
||||
utxos.utxo2_1_out_2,
|
||||
].map(note => ({
|
||||
leafHash: note.hash(),
|
||||
hash: note.hash(),
|
||||
note,
|
||||
}))
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
WithdrawalTree,
|
||||
TreeConfig,
|
||||
keccakHasher,
|
||||
Item,
|
||||
Leaf,
|
||||
genesisRoot,
|
||||
verifyProof,
|
||||
} from '~tree'
|
||||
@@ -66,7 +66,7 @@ describe('withdrawal tree unit test', () => {
|
||||
}
|
||||
beforeAll(async () => {
|
||||
prevRoot = withdrawalTree.root()
|
||||
const items: Item<BN>[] = [{ leafHash: toBN(1) }, { leafHash: toBN(2) }]
|
||||
const items: Leaf<BN>[] = [{ hash: toBN(1) }, { hash: toBN(2) }]
|
||||
result = await withdrawalTree.dryAppend(...items)
|
||||
})
|
||||
it('should not update its root', () => {
|
||||
@@ -92,7 +92,7 @@ describe('withdrawal tree unit test', () => {
|
||||
}
|
||||
it('should update its root and its value should equal to the dry run', async () => {
|
||||
prevRoot = withdrawalTree.root()
|
||||
const items: Item<BN>[] = [{ leafHash: toBN(1) }, { leafHash: toBN(2) }]
|
||||
const items: Leaf<BN>[] = [{ hash: toBN(1) }, { hash: toBN(2) }]
|
||||
dryResult = await withdrawalTree.dryAppend(...items)
|
||||
result = await withdrawalTree.append(...items)
|
||||
expect(result.root.eq(prevRoot)).toBe(false)
|
||||
@@ -106,26 +106,27 @@ describe('withdrawal tree unit test', () => {
|
||||
})
|
||||
describe('tracking', () => {
|
||||
const addresses = [address.USER_A]
|
||||
const items: Item<BN>[] = [
|
||||
const items: Leaf<BN>[] = [
|
||||
utxos.utxo1_in_1.toWithdrawal({ to: address.USER_A, fee: 1 }),
|
||||
utxos.utxo1_out_1,
|
||||
utxos.utxo2_1_in_1,
|
||||
utxos.utxo1_out_1.toWithdrawal({ to: address.USER_A, fee: 1 }),
|
||||
utxos.utxo2_1_in_1.toWithdrawal({ to: address.USER_A, fee: 1 }),
|
||||
].map(note => ({
|
||||
leafHash: toBN(note.hash().toHex()),
|
||||
note,
|
||||
hash: note.withdrawalHash().toBN(),
|
||||
noteHash: note.hash(),
|
||||
shouldTrack: true,
|
||||
}))
|
||||
it("should track Alice's utxos while not tracking Bob's", async () => {
|
||||
withdrawalTree.updateAddresses(addresses)
|
||||
await withdrawalTree.append(...items)
|
||||
const proof = await withdrawalTree.merkleProof({
|
||||
hash: items[0].leafHash,
|
||||
hash: items[0].hash,
|
||||
})
|
||||
expect(verifyProof(keccakHasher(depth), proof)).toBe(true)
|
||||
})
|
||||
it('should generate merkle proof using index together', async () => {
|
||||
const index = withdrawalTree.latestLeafIndex().subn(3)
|
||||
const proof = await withdrawalTree.merkleProof({
|
||||
hash: items[0].leafHash,
|
||||
hash: items[0].hash,
|
||||
index,
|
||||
})
|
||||
expect(verifyProof(keccakHasher(depth), proof)).toBe(true)
|
||||
@@ -134,7 +135,7 @@ describe('withdrawal tree unit test', () => {
|
||||
const index = withdrawalTree.latestLeafIndex().subn(1)
|
||||
await expect(
|
||||
withdrawalTree.merkleProof({
|
||||
hash: items[0].leafHash,
|
||||
hash: items[0].hash,
|
||||
index,
|
||||
}),
|
||||
).rejects.toThrow('')
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { Note, UtxoStatus, Sum, RawTx } from '@zkopru/transaction'
|
||||
import {
|
||||
UtxoStatus,
|
||||
Sum,
|
||||
RawTx,
|
||||
Withdrawal,
|
||||
Utxo,
|
||||
Note,
|
||||
} from '@zkopru/transaction'
|
||||
import { Field, F, Point } from '@zkopru/babyjubjub'
|
||||
import { Layer1 } from '@zkopru/contracts'
|
||||
import { HDWallet, ZkAccount } from '@zkopru/account'
|
||||
import { ZkOPRUNode } from '@zkopru/core'
|
||||
import { DB, NoteType } from '@zkopru/prisma'
|
||||
import { DB } from '@zkopru/prisma'
|
||||
import { logger } from '@zkopru/utils'
|
||||
import { Bytes32 } from 'soltypes'
|
||||
import fetch, { Response } from 'node-fetch'
|
||||
@@ -91,25 +98,25 @@ export class ZkWallet {
|
||||
}
|
||||
|
||||
async getSpendableAmount(account: ZkAccount): Promise<Sum> {
|
||||
const notes: Note[] = await this.getSpendableNotes(account)
|
||||
const notes: Utxo[] = await this.getSpendables(account)
|
||||
const assets = Sum.from(notes)
|
||||
return assets
|
||||
}
|
||||
|
||||
async getLockedAmount(account: ZkAccount): Promise<Sum> {
|
||||
const notes: Note[] = await this.getNotesFor(account, UtxoStatus.SPENDING)
|
||||
const notes: Utxo[] = await this.getUtxos(account, UtxoStatus.SPENDING)
|
||||
const assets = Sum.from(notes)
|
||||
return assets
|
||||
}
|
||||
|
||||
async getSpendableNotes(account: ZkAccount): Promise<Note[]> {
|
||||
const utxos = this.getNotesFor(account, UtxoStatus.UNSPENT)
|
||||
async getSpendables(account: ZkAccount): Promise<Utxo[]> {
|
||||
const utxos = this.getUtxos(account, UtxoStatus.UNSPENT)
|
||||
return utxos
|
||||
}
|
||||
|
||||
async getNotesFor(account: ZkAccount, status: UtxoStatus): Promise<Note[]> {
|
||||
async getUtxos(account: ZkAccount, status: UtxoStatus): Promise<Utxo[]> {
|
||||
const noteSqls = await this.db.read(prisma =>
|
||||
prisma.note.findMany({
|
||||
prisma.utxo.findMany({
|
||||
where: {
|
||||
pubKey: { in: [account.pubKey.toHex()] },
|
||||
status,
|
||||
@@ -117,7 +124,7 @@ export class ZkWallet {
|
||||
},
|
||||
}),
|
||||
)
|
||||
const notes: Note[] = []
|
||||
const notes: Utxo[] = []
|
||||
noteSqls.forEach(obj => {
|
||||
if (!obj.eth) throw Error('should have Ether data')
|
||||
if (!obj.pubKey) throw Error('should have pubkey data')
|
||||
@@ -125,15 +132,15 @@ export class ZkWallet {
|
||||
throw Error('should have same pubkey')
|
||||
if (!obj.salt) throw Error('should have salt data')
|
||||
|
||||
let note!: Note
|
||||
let note!: Utxo
|
||||
if (!obj.tokenAddr) {
|
||||
note = Note.newEtherNote({
|
||||
note = Utxo.newEtherNote({
|
||||
eth: obj.eth,
|
||||
pubKey: account.pubKey,
|
||||
salt: obj.salt,
|
||||
})
|
||||
} else if (obj.erc20Amount) {
|
||||
note = Note.newERC20Note({
|
||||
note = Utxo.newERC20Note({
|
||||
eth: obj.eth,
|
||||
pubKey: account.pubKey,
|
||||
salt: obj.salt,
|
||||
@@ -141,7 +148,7 @@ export class ZkWallet {
|
||||
erc20Amount: obj.erc20Amount,
|
||||
})
|
||||
} else if (obj.nft) {
|
||||
note = Note.newNFTNote({
|
||||
note = Utxo.newNFTNote({
|
||||
eth: obj.eth,
|
||||
pubKey: account.pubKey,
|
||||
salt: obj.salt,
|
||||
@@ -243,7 +250,7 @@ export class ZkWallet {
|
||||
logger.error('Not enough Ether')
|
||||
return false
|
||||
}
|
||||
const note = Note.newEtherNote({
|
||||
const note = Utxo.newEtherNote({
|
||||
eth,
|
||||
pubKey: to || this.account.pubKey,
|
||||
})
|
||||
@@ -282,7 +289,7 @@ export class ZkWallet {
|
||||
logger.error('Not enough ERC20 balance')
|
||||
return false
|
||||
}
|
||||
const note = Note.newERC20Note({
|
||||
const note = Utxo.newERC20Note({
|
||||
eth,
|
||||
pubKey: to || this.account.pubKey,
|
||||
tokenAddr: addr,
|
||||
@@ -315,7 +322,7 @@ export class ZkWallet {
|
||||
logger.error('Not enough Ether')
|
||||
return false
|
||||
}
|
||||
const note = Note.newNFTNote({
|
||||
const note = Utxo.newNFTNote({
|
||||
eth,
|
||||
pubKey: to || this.account.pubKey,
|
||||
tokenAddr: addr,
|
||||
@@ -363,32 +370,11 @@ export class ZkWallet {
|
||||
})
|
||||
// add newly created notes
|
||||
for (const note of tx.outflow) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.note.create({
|
||||
data: {
|
||||
hash: note
|
||||
.hash()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
eth: note.eth.toUint256().toString(),
|
||||
pubKey: Bytes32.from(note.pubKey.toHex()).toString(),
|
||||
salt: note.salt.toUint256().toString(),
|
||||
tokenAddr: note.tokenAddr.toAddress().toString(),
|
||||
erc20Amount: note.erc20Amount.toUint256().toString(),
|
||||
nft: note.nft.toUint256().toString(),
|
||||
status: UtxoStatus.NON_INCLUDED,
|
||||
noteType: NoteType.UTXO,
|
||||
nullifier: note
|
||||
.nullifier()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
},
|
||||
}),
|
||||
)
|
||||
await this.saveNote(note)
|
||||
}
|
||||
// mark used notes as spending
|
||||
await this.db.write(prisma =>
|
||||
prisma.note.updateMany({
|
||||
prisma.utxo.updateMany({
|
||||
where: {
|
||||
hash: {
|
||||
in: tx.inflow.map(utxo =>
|
||||
@@ -409,7 +395,7 @@ export class ZkWallet {
|
||||
}
|
||||
}
|
||||
|
||||
private async deposit(note: Note, fee: Field): Promise<boolean> {
|
||||
private async deposit(note: Utxo, fee: Field): Promise<boolean> {
|
||||
if (!this.account) {
|
||||
logger.error('Account is not set')
|
||||
return false
|
||||
@@ -430,30 +416,56 @@ export class ZkWallet {
|
||||
from: this.account.address,
|
||||
value: note.eth.add(fee).toString(),
|
||||
})
|
||||
await this.db.write(prisma =>
|
||||
prisma.note.create({
|
||||
data: {
|
||||
hash: note
|
||||
.hash()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
eth: note.eth.toUint256().toString(),
|
||||
pubKey: Bytes32.from(note.pubKey.toHex()).toString(),
|
||||
salt: note.salt.toUint256().toString(),
|
||||
tokenAddr: note.tokenAddr.toAddress().toString(),
|
||||
erc20Amount: note.erc20Amount.toUint256().toString(),
|
||||
nft: note.nft.toUint256().toString(),
|
||||
status: UtxoStatus.NON_INCLUDED,
|
||||
noteType: NoteType.UTXO,
|
||||
nullifier: note
|
||||
.nullifier()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
},
|
||||
}),
|
||||
)
|
||||
await this.saveNote(note)
|
||||
// TODO check what web3 methods returns when it failes
|
||||
if (receipt) return true
|
||||
return false
|
||||
}
|
||||
|
||||
private async saveNote(note: Note) {
|
||||
if (note instanceof Utxo) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.utxo.create({
|
||||
data: {
|
||||
hash: note
|
||||
.hash()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
eth: note.eth.toUint256().toString(),
|
||||
pubKey: Bytes32.from(note.pubKey.toHex()).toString(),
|
||||
salt: note.salt.toUint256().toString(),
|
||||
tokenAddr: note.tokenAddr.toAddress().toString(),
|
||||
erc20Amount: note.erc20Amount.toUint256().toString(),
|
||||
nft: note.nft.toUint256().toString(),
|
||||
status: UtxoStatus.NON_INCLUDED,
|
||||
nullifier: note
|
||||
.nullifier()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else if (note instanceof Withdrawal) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.withdrawal.create({
|
||||
data: {
|
||||
hash: note
|
||||
.hash()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
withdrawalHash: note.withdrawalHash().toString(),
|
||||
eth: note.eth.toUint256().toString(),
|
||||
pubKey: Bytes32.from(note.pubKey.toHex()).toString(),
|
||||
salt: note.salt.toUint256().toString(),
|
||||
tokenAddr: note.tokenAddr.toAddress().toString(),
|
||||
to: note.publicData.to.toAddress().toString(),
|
||||
fee: note.publicData.fee.toAddress().toString(),
|
||||
erc20Amount: note.erc20Amount.toUint256().toString(),
|
||||
nft: note.nft.toUint256().toString(),
|
||||
status: UtxoStatus.NON_INCLUDED,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
OutflowType,
|
||||
Withdrawal,
|
||||
Migration,
|
||||
Utxo,
|
||||
} from '@zkopru/transaction'
|
||||
import { MerkleProof, Grove } from '@zkopru/tree'
|
||||
import path from 'path'
|
||||
@@ -245,7 +246,7 @@ export class ZkWizard {
|
||||
const noteToEncrypt = tx.outflow.find(outflow =>
|
||||
outflow.pubKey.eq(encryptTo),
|
||||
)
|
||||
if (noteToEncrypt) memo = noteToEncrypt.encrypt()
|
||||
if (noteToEncrypt instanceof Utxo) memo = noteToEncrypt.encrypt()
|
||||
}
|
||||
const zkTx: ZkTx = new ZkTx({
|
||||
inflow: tx.inflow.map((utxo, index) => {
|
||||
|
||||
Reference in New Issue
Block a user