refactor: withdrawal tree updates

This commit is contained in:
Wanseob Lim
2020-06-21 22:57:55 +09:00
parent af483382d4
commit 3ba67e41cd
30 changed files with 1046 additions and 824 deletions

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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)),

View File

@@ -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(),

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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> {

View File

@@ -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),

View File

@@ -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(),

View File

@@ -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),
})

View File

@@ -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.

View File

@@ -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])
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -0,0 +1,5 @@
import { Withdrawal } from './withdrawal'
import { Utxo } from './utxo'
import { Migration } from './migration'
export type Outflow = Utxo | Withdrawal | Migration

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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'

View File

@@ -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)) {

View File

@@ -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 },

View File

@@ -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 } }],
},
}),
)

View File

@@ -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(),

View File

@@ -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 () => {

View File

@@ -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('')

View File

@@ -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,
},
}),
)
}
}
}

View File

@@ -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) => {