From 22314118849dd0fcde995f8ad225d8e97eb39c96 Mon Sep 17 00:00:00 2001 From: Evgen Date: Tue, 6 Dec 2022 01:27:25 +0300 Subject: [PATCH 01/11] Getting root at index, siblings, few helpers routines --- package.json | 2 +- src/client.ts | 27 ++++++++++++++++++++------- src/index.ts | 1 + src/networks/evm.ts | 9 +++++++-- src/networks/network.ts | 2 +- src/networks/polkadot.ts | 2 +- src/state.ts | 10 +++++++++- src/utils.ts | 25 ++++++++++++++++++++++++- src/worker.ts | 26 +++++++++++++++++++++++++- yarn.lock | 4 +--- 10 files changed, 90 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index ed900ad3..bac2ec40 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "comlink": "^4.3.1", "hdwallet-babyjub": "^0.0.2", "idb": "^7.0.0", - "libzkbob-rs-wasm-web": "0.8.0", + "libzkbob-rs-wasm-web": "file:../libzkbob-rs/libzkbob-rs-wasm/web", "regenerator-runtime": "^0.13.9", "web3": "1.8.0", "@ethereumjs/util": "^8.0.2", diff --git a/src/client.ts b/src/client.ts index b25c7aac..0e62494b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9,7 +9,7 @@ import { EphemeralAddress } from './ephemeral'; import { Output, Proof, DecryptedMemo, ITransferData, IWithdrawData, - ParseTxsResult, StateUpdate, IndexedTx + ParseTxsResult, StateUpdate, IndexedTx, TreeNode } from 'libzkbob-rs-wasm-web'; import { @@ -1231,11 +1231,18 @@ export class ZkBobClient { } // Get the local Merkle tree root & index - public async getLocalState(tokenAddress: string): Promise { - const root = await this.zpStates[tokenAddress].getRoot(); - const index = await this.zpStates[tokenAddress].getNextIndex(); + // Retuned the latest root when the index is undefined + public async getLocalState(tokenAddress: string, index?: bigint): Promise { + if (index === undefined) { + const index = await this.zpStates[tokenAddress].getNextIndex(); + const root = await this.zpStates[tokenAddress].getRoot(); - return {root, index}; + return {root, index}; + } else { + const root = await this.zpStates[tokenAddress].getRootAt(index); + + return {root, index}; + } } // Get relayer regular root & index @@ -1255,13 +1262,19 @@ export class ZkBobClient { } // Get pool info (direct web3 request) - public async getPoolState(tokenAddress: string): Promise { + public async getPoolState(tokenAddress: string, index?: bigint): Promise { const token = this.tokens[tokenAddress]; - const res = await this.config.network.poolState(token.poolAddress); + const res = await this.config.network.poolState(token.poolAddress, index); return {index: res.index, root: res.root}; } + public async getLeftSiblings(tokenAddress: string, index: bigint): Promise { + const siblings = await this.zpStates[tokenAddress].getLeftSiblings(index); + + return siblings; + } + // Getting array of accounts and notes for the current account public async rawState(tokenAddress: string): Promise { return await this.zpStates[tokenAddress].rawState(); diff --git a/src/index.ts b/src/index.ts index 24af7f6d..fce492e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { wrap } from 'comlink'; import { SnarkConfigParams } from './config'; import { FileCache } from './file-cache'; export { ZkBobClient, TransferConfig, FeeAmount, PoolLimits, TreeState } from './client'; +export { TreeNode } from 'libzkbob-rs-wasm-web'; export { TxType } from './tx'; export { HistoryRecord, HistoryTransactionType, HistoryRecordState } from './history' export { EphemeralAddress, EphemeralPool } from './ephemeral' diff --git a/src/networks/evm.ts b/src/networks/evm.ts index dfe0fc2d..4fa6eef0 100644 --- a/src/networks/evm.ts +++ b/src/networks/evm.ts @@ -200,9 +200,14 @@ export class EvmNetwork implements NetworkBackend { return await this.contract.methods.getLimitsFor(addr).call(); } - public async poolState(contractAddress: string): Promise<{index: bigint, root: bigint}> { + public async poolState(contractAddress: string, index?: bigint): Promise<{index: bigint, root: bigint}> { this.contract.options.address = contractAddress; - const idx = await this.contract.methods.pool_index().call(); + let idx; + if (index === undefined) { + idx = await this.contract.methods.pool_index().call(); + } else { + idx = index?.toString(); + } const root = await this.contract.methods.roots(idx).call(); diff --git a/src/networks/network.ts b/src/networks/network.ts index b823e590..0c5f2706 100644 --- a/src/networks/network.ts +++ b/src/networks/network.ts @@ -4,7 +4,7 @@ export interface NetworkBackend { getTokenNonce(tokenAddress: string, address: string): Promise; getDenominator(contractAddress: string): Promise; poolLimits(contractAddress: string, address: string | undefined): Promise; - poolState(contractAddress: string): Promise<{index: bigint, root: bigint}>; + poolState(contractAddress: string, index?: bigint): Promise<{index: bigint, root: bigint}>; getTxRevertReason(txHash: string): Promise isSignatureCompact(): boolean; defaultNetworkName(): string; diff --git a/src/networks/polkadot.ts b/src/networks/polkadot.ts index b53ca947..ae2a4ba0 100644 --- a/src/networks/polkadot.ts +++ b/src/networks/polkadot.ts @@ -21,7 +21,7 @@ export class PolkadotNetwork implements NetworkBackend { return undefined; // FIXME } - async poolState(contractAddress: string): Promise<{index: bigint, root: bigint}> { + async poolState(contractAddress: string, index?: bigint): Promise<{index: bigint, root: bigint}> { return {index: BigInt(0), root: BigInt(0)}; } diff --git a/src/state.ts b/src/state.ts index 948be680..2e16b997 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,4 +1,4 @@ -import { IDepositData, IDepositPermittableData, ITransferData, IWithdrawData, StateUpdate } from 'libzkbob-rs-wasm-web'; +import { IDepositData, IDepositPermittableData, ITransferData, IWithdrawData, StateUpdate, TreeNode } from 'libzkbob-rs-wasm-web'; import { HistoryStorage } from './history' import { bufToHex } from './utils'; import { hash } from 'tweetnacl'; @@ -69,6 +69,14 @@ export class ZkBobState { return BigInt(await this.worker.getRoot(this.tokenAddress)); } + public async getRootAt(index: bigint): Promise { + return BigInt(await this.worker.getRootAt(this.tokenAddress, index)); + } + + public async getLeftSiblings(index: bigint): Promise { + return await this.worker.getLeftSiblings(this.tokenAddress, index); + } + public async getNextIndex(): Promise { return await this.worker.nextTreeIndex(this.tokenAddress); } diff --git a/src/utils.ts b/src/utils.ts index 50ce836d..c80831b7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,8 @@ import { numberToHex, padLeft } from 'web3-utils'; import { NetworkType } from './network-type'; import { InternalError } from './errors'; +import { TreeNode } from 'libzkbob-rs-wasm-web'; + const util = require('ethereumjs-util'); // It's a healthy-man function @@ -134,7 +136,6 @@ export function isEqualBuffers(buf1: Uint8Array, buf2: Uint8Array): boolean { return true; } - export class HexStringWriter { buf: string; @@ -325,4 +326,26 @@ export function addressFromSignature(signature: string, signedData: string): str const addrBuf = util.pubToAddress(pub); return addHexPrefix(bufToHex(addrBuf)); +} + +export function nodeToHex(node: TreeNode): string { + const writer = new HexStringWriter(); + writer.writeNumber(node.height, 1); + writer.writeNumber(node.index, 6); + writer.writeBigInt(BigInt(node.value), 32); + + return writer.toString(); +} + +export function hexToNode(data: string): TreeNode | null { + const reader = new HexStringReader(data); + const height = reader.readNumber(1); + const index = reader.readNumber(6); + const value = reader.readBigInt(32); + + if (height && index && value) { + return { height, index, value: value.toString()}; + } + + return null; } \ No newline at end of file diff --git a/src/worker.ts b/src/worker.ts index 65170569..300723ef 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,5 +1,9 @@ import { expose } from 'comlink'; -import { Proof, Params, TxParser, IndexedTx, ParseTxsResult, default as init, initThreadPool, UserState, UserAccount, StateUpdate, validateAddress, assembleAddress, SnarkProof, ITransferData, IDepositData, IWithdrawData, IDepositPermittableData } from 'libzkbob-rs-wasm-web'; +import { Proof, SnarkProof, Params, TxParser, IndexedTx, ParseTxsResult, + default as init, initThreadPool, + UserState, UserAccount, StateUpdate, + validateAddress, assembleAddress, + ITransferData, IDepositData, IWithdrawData, IDepositPermittableData, TreeNode } from 'libzkbob-rs-wasm-web'; import { FileCache } from './file-cache'; let txParams: Params; @@ -232,6 +236,26 @@ const obj = { }); }, + async getRootAt(address: string, index: bigint): Promise { + return new Promise(async (resolve, reject) => { + try { + resolve(zpAccounts[address].getRootAt(index)); + } catch (e) { + reject(e) + } + }); + }, + + async getLeftSiblings(address: string, index: bigint): Promise { + return new Promise(async (resolve, reject) => { + try { + resolve(zpAccounts[address].getLeftSiblings(index)); + } catch (e) { + reject(e) + } + }); + }, + async updateState(address: string, stateUpdate: StateUpdate): Promise { return new Promise(async resolve => { resolve(zpAccounts[address].updateState(stateUpdate)); diff --git a/yarn.lock b/yarn.lock index dcc72e80..23b1f06f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2855,10 +2855,8 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libzkbob-rs-wasm-web@0.8.0: +"libzkbob-rs-wasm-web@file:../libzkbob-rs/libzkbob-rs-wasm/web": version "0.8.0" - resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web/-/libzkbob-rs-wasm-web-0.8.0.tgz#b133aa0f1a381567fde082662ce4c4e46ea7aa47" - integrity sha512-EIPeDyl2wmpSMx299LnG0UCqg5wy8sr7TqfCdgPDxA8kH5Gu92MF12KOOOx7sfLpoYZ965y7MbkkuUfz5OzVAg== loader-runner@^4.2.0: version "4.3.0" From 0d26fc2fb2a5fc7d4e2c9e02ab16bd007aac66b9 Mon Sep 17 00:00:00 2001 From: Evgen Date: Tue, 6 Dec 2022 03:14:05 +0300 Subject: [PATCH 02/11] Initialize account with birthday and sync state starting from it --- src/client.ts | 68 +++++++++++++++++++++++++++++++++++++++++++++------ src/state.ts | 4 +-- src/utils.ts | 2 +- src/worker.ts | 4 +-- 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/client.ts b/src/client.ts index 0e62494b..e146189f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,5 @@ import { Tokens } from './config'; -import { ethAddrToBuf, toCompactSignature, truncateHexPrefix, toTwosComplementHex, addressFromSignature } from './utils'; +import { ethAddrToBuf, toCompactSignature, truncateHexPrefix, toTwosComplementHex, addressFromSignature, hexToNode } from './utils'; import { ZkBobState } from './state'; import { TxType } from './tx'; import { NetworkBackend } from './networks/network'; @@ -23,6 +23,7 @@ const DEFAULT_TX_FEE = BigInt(100000000); const BATCH_SIZE = 1000; const PERMIT_DEADLINE_INTERVAL = 1200; // permit deadline is current time + 20 min const PERMIT_DEADLINE_THRESHOLD = 300; // minimum time to deadline before tx proof calculation and sending (5 min) +const PARTIAL_TREE_USAGE_THRESHOLD = 500; // minimum tx count in Merkle tree to partial tree update using export interface RelayerInfo { root: string; @@ -141,15 +142,20 @@ export interface LimitsFetch { } export interface ClientConfig { - /** Spending key. */ + // Spending key sk: Uint8Array; - /** A map of supported tokens (token address => token params). */ + // A map of supported tokens (token address => token params) tokens: Tokens; - /** A worker instance acquired through init() function of this package. */ + // A worker instance acquired through init() function of this package worker: any; - /** The name of the network is only used for storage. */ + // The name of the network is only used for storage networkName: string | undefined; + // An endpoint to interact with the blockchain network: NetworkBackend; + // Account birthday: + // no transactions associated with the account should exist lower that index + // set -1 to use the latest index (create _NEW_ account) + birthindex: number | undefined; } export class ZkBobClient { @@ -1311,13 +1317,30 @@ export class ZkBobClient { const token = this.tokens[tokenAddress]; const state = this.zpStates[tokenAddress]; - const startIndex = Number(await zpState.getNextIndex()); + let startIndex = Number(await zpState.getNextIndex()); const stateInfo = await this.info(token.relayerUrl); const nextIndex = Number(stateInfo.deltaIndex); const optimisticIndex = Number(stateInfo.optimisticDeltaIndex); if (optimisticIndex > startIndex) { + // Use partial tree loading if possible + let birthindex = this.config.birthindex ?? 0; + if (birthindex < 0 || birthindex >= Number(stateInfo.deltaIndex)) { + // we should grab almost one transaction from the current state + birthindex = Number(stateInfo.deltaIndex) - OUTPLUSONE; + } + let siblings: TreeNode[] | undefined; + if (startIndex == 0 && birthindex >= PARTIAL_TREE_USAGE_THRESHOLD) { + try { + siblings = await this.siblings(token.relayerUrl, birthindex); + console.log(`Got ${siblings.length} sibling(s) for index ${birthindex}`); + startIndex = birthindex; + } catch (err) { + console.warn(`Cannot retrieve siblings: ${err}`); + } + } + const startTime = Date.now(); console.log(`⬇ Fetching transactions between ${startIndex} and ${optimisticIndex}...`); @@ -1426,7 +1449,7 @@ export class ZkBobClient { for (const idx of idxs) { const oneStateUpdate = totalRes.state.get(idx); if (oneStateUpdate !== undefined) { - await state.updateState(oneStateUpdate); + await state.updateState(oneStateUpdate, siblings); } else { throw Error(`Cannot find state batch at index ${idx}`); } @@ -1642,6 +1665,37 @@ export class ZkBobClient { }; } + private async siblings(relayerUrl: string, index: number): Promise { + const url = new URL(`/siblings`, relayerUrl); + url.searchParams.set('index', index.toString()); + const headers = {'content-type': 'application/json;charset=UTF-8'}; + + const siblings = await this.fetchJson(url.toString(), {headers}); + // TODO: here is a test case only, remove after testing + /*let siblings: string[] = []; + if (index == 278016) { + siblings = [ + "0900000000021e0f3a711be80e44496151924743c5587860a3fbde0f283659c9d0d21659c544b5", + "0a00000000010e11de590842d36b791ffa3c0d15cfdc89d44dfe77c9254102cffe892718788c3b", + "0b0000000000861cb6c5ce6d5849ff46f84dfb01bcda53923f9a25cb00798112dcb7b323b6301a", + "0c000000000042010b42a01303918e0323f44294ad8fbbfdca86a967ee2c2b5775a866ed1cca2b", + "0d00000000002004be1969bae104b72efcc0ac9e887fa1d8c6a581e7cbfa769663c5f11ae39f29", + "12000000000000297f215cef4bd2b5991071b43389a9de1d3b947538612a62daab13aa29c13d3f" + ]; + }*/ + if (!Array.isArray(siblings)) { + throw new RelayerError(200, `Response should be an array`); + } + + return siblings.map((aNode) => { + let node = hexToNode(aNode) + if (!node) { + throw new RelayerError(200, `Cannot convert \'${aNode}\' to a TreeNode`); + } + return node; + }); + } + // Universal response parser private async fetchJson(url: string, headers: RequestInit): Promise { let response: Response; diff --git a/src/state.ts b/src/state.ts index 2e16b997..175aa689 100644 --- a/src/state.ts +++ b/src/state.ts @@ -119,7 +119,7 @@ export class ZkBobState { return await this.worker.createTransfer(this.tokenAddress, transfer); } - public async updateState(stateUpdate: StateUpdate): Promise { - return await this.worker.updateState(this.tokenAddress, stateUpdate); + public async updateState(stateUpdate: StateUpdate, siblings?: TreeNode[]): Promise { + return await this.worker.updateState(this.tokenAddress, stateUpdate, siblings); } } diff --git a/src/utils.ts b/src/utils.ts index c80831b7..c4369e86 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -343,7 +343,7 @@ export function hexToNode(data: string): TreeNode | null { const index = reader.readNumber(6); const value = reader.readBigInt(32); - if (height && index && value) { + if (height != null && index != null && value != null) { return { height, index, value: value.toString()}; } diff --git a/src/worker.ts b/src/worker.ts index 300723ef..6d2432e1 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -256,9 +256,9 @@ const obj = { }); }, - async updateState(address: string, stateUpdate: StateUpdate): Promise { + async updateState(address: string, stateUpdate: StateUpdate, siblings?: TreeNode[]): Promise { return new Promise(async resolve => { - resolve(zpAccounts[address].updateState(stateUpdate)); + resolve(zpAccounts[address].updateState(stateUpdate, siblings)); }); }, From c77c6da2c55c99882e32a9d3fe341624cd6f013e Mon Sep 17 00:00:00 2001 From: Evgen Date: Tue, 6 Dec 2022 21:24:25 +0300 Subject: [PATCH 03/11] Verify local state stub, getting fist index --- package.json | 18 +++++++++--------- src/client.ts | 31 ++++++++++++++++++++++++++++++- src/state.ts | 4 ++++ src/worker.ts | 6 ++++++ 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index bac2ec40..95f8571a 100644 --- a/package.json +++ b/package.json @@ -17,26 +17,26 @@ "clean": "rm -rf lib/" }, "dependencies": { + "@ethereumjs/util": "^8.0.2", + "@metamask/eth-sig-util": "5.0.0", + "@scure/bip32": "1.1.1", + "@scure/bip39": "1.1.0", "comlink": "^4.3.1", + "fast-sha256": "^1.3.0", "hdwallet-babyjub": "^0.0.2", "idb": "^7.0.0", "libzkbob-rs-wasm-web": "file:../libzkbob-rs/libzkbob-rs-wasm/web", "regenerator-runtime": "^0.13.9", "web3": "1.8.0", - "@ethereumjs/util": "^8.0.2", - "web3-utils": "1.8.0", - "fast-sha256": "^1.3.0", - "@scure/bip32": "1.1.1", - "@scure/bip39": "1.1.0", - "@metamask/eth-sig-util": "5.0.0" + "web3-utils": "1.8.0" }, "devDependencies": { + "@types/ethereum-protocol": "^1.0.1", + "@types/web3": "1.0.20", "ts-loader": "^9.2.6", "typescript": "^4.1.2", "webpack": "^5.64.2", - "webpack-cli": "^4.9.1", - "@types/ethereum-protocol": "^1.0.1", - "@types/web3": "1.0.20" + "webpack-cli": "^4.9.1" }, "resolutions": { "@types/responselike": "1.0.0" diff --git a/src/client.ts b/src/client.ts index e146189f..f19ae0dc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1275,12 +1275,20 @@ export class ZkBobClient { return {index: res.index, root: res.root}; } + // Just for testing purposes. This method do not need for client public async getLeftSiblings(tokenAddress: string, index: bigint): Promise { const siblings = await this.zpStates[tokenAddress].getLeftSiblings(index); return siblings; } + // Just informal method needed for the debug purposes + public async getTreeStartIndex(tokenAddress: string): Promise { + const index = await this.zpStates[tokenAddress].getFirstIndex(); + + return index; + } + // Getting array of accounts and notes for the current account public async rawState(tokenAddress: string): Promise { return await this.zpStates[tokenAddress].rawState(); @@ -1327,7 +1335,7 @@ export class ZkBobClient { // Use partial tree loading if possible let birthindex = this.config.birthindex ?? 0; if (birthindex < 0 || birthindex >= Number(stateInfo.deltaIndex)) { - // we should grab almost one transaction from the current state + // we should grab almost one transaction from the regular state birthindex = Number(stateInfo.deltaIndex) - OUTPLUSONE; } let siblings: TreeNode[] | undefined; @@ -1465,6 +1473,8 @@ export class ZkBobClient { console.log(`Sync finished in ${msElapsed / 1000} sec | ${totalRes.txCount} tx, avg speed ${avgSpeed.toFixed(1)} ms/tx`); + await this.verifyState(tokenAddress); + return readyToTransact; } else { zpState.history.setLastMinedTxIndex(nextIndex - OUTPLUSONE); @@ -1561,6 +1571,25 @@ export class ZkBobClient { } } + private async verifyState(tokenAddress: string): Promise { + const zpState = this.zpStates[tokenAddress]; + const token = this.tokens[tokenAddress]; + const state = this.zpStates[tokenAddress]; + + const checkIndex = Number(await zpState.getNextIndex()); + const localRoot = await zpState.getRoot(); + const poolRoot = (await this.config.network.poolState(token.poolAddress, BigInt(checkIndex))).root; + + if (localRoot != poolRoot) { + console.log(`🚑[StateVerify] Merkle tree root at index ${checkIndex} mistmatch! Trying to restore...`); + + + return false; + } + + return true; + } + public async verifyShieldedAddress(address: string): Promise { return await this.worker.verifyShieldedAddress(address); } diff --git a/src/state.ts b/src/state.ts index 175aa689..1fd387b6 100644 --- a/src/state.ts +++ b/src/state.ts @@ -81,6 +81,10 @@ export class ZkBobState { return await this.worker.nextTreeIndex(this.tokenAddress); } + public async getFirstIndex(): Promise { + return await this.worker.firstTreeIndex(this.tokenAddress); + } + public async rawState(): Promise { return await this.worker.rawState(this.tokenAddress); } diff --git a/src/worker.ts b/src/worker.ts index 6d2432e1..a4f31568 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -230,6 +230,12 @@ const obj = { }); }, + async firstTreeIndex(address: string): Promise { + return new Promise(async resolve => { + resolve(zpAccounts[address].firstTreeIndex()); + }); + }, + async getRoot(address: string): Promise { return new Promise(async resolve => { resolve(zpAccounts[address].getRoot()); From b2e3ab13ba12b78f15032fb9bf9d1d8033c57c47 Mon Sep 17 00:00:00 2001 From: Evgen Date: Thu, 8 Dec 2022 23:03:32 +0300 Subject: [PATCH 04/11] Rollback and wipe user's state support --- src/client.ts | 6 ++++-- src/history.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ src/state.ts | 12 ++++++++++-- src/worker.ts | 20 ++++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/client.ts b/src/client.ts index f19ae0dc..5b935c1d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1294,8 +1294,10 @@ export class ZkBobClient { return await this.zpStates[tokenAddress].rawState(); } - - // TODO: implement correct state cleaning + public async rollbackState(tokenAddress: string, index: bigint): Promise { + return await this.zpStates[tokenAddress].rollback(index); + } + public async cleanState(tokenAddress: string): Promise { await this.zpStates[tokenAddress].clean(); } diff --git a/src/history.ts b/src/history.ts index 3f5a3e87..c79627bd 100644 --- a/src/history.ts +++ b/src/history.ts @@ -471,6 +471,47 @@ export class HistoryStorage { await this.db.delete(DECRYPTED_PENDING_MEMO_TABLE, IDBKeyRange.lowerBound(index, true)); } + public async rollbackHistory(rollbackIndex: number): Promise { + if (this.syncHistoryPromise) { + // wait while sync is finished (if started) + await this.syncHistoryPromise; + } + + // rollback local objects + this.currentHistory.forEach((_value: HistoryRecord, key: number) => { + if (key >= rollbackIndex) { + this.currentHistory.delete(key); + } + }); + let new_sync_index = -1; + this.unparsedMemo.forEach((_value: DecryptedMemo, key: number) => { + if (key >= rollbackIndex) { + this.unparsedMemo.delete(key); + } else if (key > new_sync_index) { + new_sync_index = key; + } + }); + this.unparsedPendingMemo.forEach((_value: DecryptedMemo, key: number) => { + if (key >= rollbackIndex) { + this.unparsedPendingMemo.delete(key); + } + }); + + + // Remove records after the specified idex from the database + await this.db.delete(TX_TABLE, IDBKeyRange.lowerBound(rollbackIndex)); + await this.db.delete(DECRYPTED_MEMO_TABLE, IDBKeyRange.lowerBound(rollbackIndex)); + await this.db.delete(DECRYPTED_PENDING_MEMO_TABLE, IDBKeyRange.lowerBound(rollbackIndex)); + + // update sync_index + this.syncIndex = new_sync_index + if (this.syncIndex < 0) { + this.db.delete(HISTORY_STATE_TABLE, 'sync_index'); + } else { + this.db.put(HISTORY_STATE_TABLE, this.syncIndex, 'sync_index'); + } + } + public async cleanHistory(): Promise { if (this.syncHistoryPromise) { // wait while sync is finished (if started) @@ -489,6 +530,7 @@ export class HistoryStorage { this.unparsedMemo.clear(); this.unparsedPendingMemo.clear(); this.currentHistory.clear(); + this.failedHistory = []; } // ------- Private rouutines -------- diff --git a/src/state.ts b/src/state.ts index 1fd387b6..7d9dd0b5 100644 --- a/src/state.ts +++ b/src/state.ts @@ -89,9 +89,17 @@ export class ZkBobState { return await this.worker.rawState(this.tokenAddress); } - // TODO: implement thiss method + // Wipe whole user's state + public async rollback(rollbackIndex: bigint): Promise { + const realRollbackIndex = await this.worker.rollbackState(this.tokenAddress, rollbackIndex); + await this.history.rollbackHistory(Number(realRollbackIndex)); + + return realRollbackIndex; + } + + // Wipe whole user's state public async clean(): Promise { - //await this.account.cleanState(); + await this.worker.wipeState(this.tokenAddress); await this.history.cleanHistory(); } diff --git a/src/worker.ts b/src/worker.ts index a4f31568..7da18466 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -262,6 +262,26 @@ const obj = { }); }, + async rollbackState(address: string, index: bigint): Promise { + return new Promise(async (resolve, reject) => { + try { + resolve(zpAccounts[address].rollbackState(index)); + } catch (e) { + reject(e) + } + }); + }, + + async wipeState(address: string): Promise { + return new Promise(async (resolve, reject) => { + try { + resolve(zpAccounts[address].wipeState()); + } catch (e) { + reject(e) + } + }); + }, + async updateState(address: string, stateUpdate: StateUpdate, siblings?: TreeNode[]): Promise { return new Promise(async resolve => { resolve(zpAccounts[address].updateState(stateUpdate, siblings)); From 3481babefcdb0b20089778ffd96754e720f5262a Mon Sep 17 00:00:00 2001 From: EvgenKor Date: Fri, 9 Dec 2022 02:48:12 +0300 Subject: [PATCH 05/11] Self-healing rotine --- src/client.ts | 32 +++++++++++++++++++++++++++----- src/state.ts | 8 ++++++++ src/worker.ts | 12 ++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/client.ts b/src/client.ts index 5b935c1d..4887ecd0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -24,6 +24,8 @@ const BATCH_SIZE = 1000; const PERMIT_DEADLINE_INTERVAL = 1200; // permit deadline is current time + 20 min const PERMIT_DEADLINE_THRESHOLD = 300; // minimum time to deadline before tx proof calculation and sending (5 min) const PARTIAL_TREE_USAGE_THRESHOLD = 500; // minimum tx count in Merkle tree to partial tree update using +const CORRUPT_STATE_ROLLBACK_ATTEMPTS = 2; // number of state restore attempts (via rollback) +const CORRUPT_STATE_WIPE_ATTEMPTS = 100; // number of state restore attempts (via wipe) export interface RelayerInfo { root: string; @@ -170,6 +172,10 @@ export class ZkBobClient { private monitoredJobs = new Map(); private jobsMonitors = new Map>(); + // State self-healing + private rollbackAttempts = 0; + private wipeAttempts = 0; + public static async create(config: ClientConfig): Promise { const client = new ZkBobClient(); client.zpStates = {}; @@ -1573,20 +1579,36 @@ export class ZkBobClient { } } + // returns false when recovery is impossible private async verifyState(tokenAddress: string): Promise { const zpState = this.zpStates[tokenAddress]; const token = this.tokens[tokenAddress]; const state = this.zpStates[tokenAddress]; - const checkIndex = Number(await zpState.getNextIndex()); + const checkIndex = await zpState.getNextIndex(); const localRoot = await zpState.getRoot(); - const poolRoot = (await this.config.network.poolState(token.poolAddress, BigInt(checkIndex))).root; + const poolRoot = (await this.config.network.poolState(token.poolAddress, checkIndex)).root; if (localRoot != poolRoot) { - console.log(`🚑[StateVerify] Merkle tree root at index ${checkIndex} mistmatch! Trying to restore...`); - + console.log(`🚑[StateVerify] Merkle tree root at index ${checkIndex} mistmatch!`); + if (this.rollbackAttempts < CORRUPT_STATE_ROLLBACK_ATTEMPTS) { + this.rollbackAttempts++; + const rollbackIndex = await zpState.lastVerifiedIndex(); + let realRollbackIndex = await zpState.rollback(rollbackIndex); + console.log(`🚑[StateVerify] Rollback tree to ${realRollbackIndex} index`); + } else if (this.wipeAttempts < CORRUPT_STATE_WIPE_ATTEMPTS) { + this.wipeAttempts++; + await zpState.clean(); + console.log(`🚑[StateVerify] Wipe tree`); + } else { + return false; + } - return false; + await this.updateStateOptimisticWorker(tokenAddress); + } else { + await zpState.setLastVerifiedIndex(checkIndex); + this.rollbackAttempts = 0; + this.wipeAttempts = 0; } return true; diff --git a/src/state.ts b/src/state.ts index 7d9dd0b5..412475fc 100644 --- a/src/state.ts +++ b/src/state.ts @@ -134,4 +134,12 @@ export class ZkBobState { public async updateState(stateUpdate: StateUpdate, siblings?: TreeNode[]): Promise { return await this.worker.updateState(this.tokenAddress, stateUpdate, siblings); } + + public async lastVerifiedIndex(): Promise { + return await this.worker.getTreeLastStableIndex(this.tokenAddress); + } + + public async setLastVerifiedIndex(index: bigint): Promise { + return await this.worker.setTreeLastStableIndex(this.tokenAddress, index); + } } diff --git a/src/worker.ts b/src/worker.ts index 7da18466..a60f5361 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -282,6 +282,18 @@ const obj = { }); }, + async getTreeLastStableIndex(address: string): Promise { + return new Promise(async (resolve) => { + resolve(zpAccounts[address].treeGetStableIndex()); + }); + }, + + async setTreeLastStableIndex(address: string, index: bigint): Promise { + return new Promise(async (resolve) => { + resolve(zpAccounts[address].treeSetStableIndex(index)); + }); + }, + async updateState(address: string, stateUpdate: StateUpdate, siblings?: TreeNode[]): Promise { return new Promise(async resolve => { resolve(zpAccounts[address].updateState(stateUpdate, siblings)); From 5e4067d5f24e75fef81858df4b99eb377e69ae77 Mon Sep 17 00:00:00 2001 From: Evgen Date: Fri, 9 Dec 2022 12:44:58 +0300 Subject: [PATCH 06/11] Setting self-healing attempts limit --- src/client.ts | 75 +++++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/src/client.ts b/src/client.ts index 4887ecd0..a97e5cf7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -25,7 +25,7 @@ const PERMIT_DEADLINE_INTERVAL = 1200; // permit deadline is current time + 20 const PERMIT_DEADLINE_THRESHOLD = 300; // minimum time to deadline before tx proof calculation and sending (5 min) const PARTIAL_TREE_USAGE_THRESHOLD = 500; // minimum tx count in Merkle tree to partial tree update using const CORRUPT_STATE_ROLLBACK_ATTEMPTS = 2; // number of state restore attempts (via rollback) -const CORRUPT_STATE_WIPE_ATTEMPTS = 100; // number of state restore attempts (via wipe) +const CORRUPT_STATE_WIPE_ATTEMPTS = 5; // number of state restore attempts (via wipe) export interface RelayerInfo { root: string; @@ -1339,6 +1339,8 @@ export class ZkBobClient { const nextIndex = Number(stateInfo.deltaIndex); const optimisticIndex = Number(stateInfo.optimisticDeltaIndex); + let readyToTransact = true; + if (optimisticIndex > startIndex) { // Use partial tree loading if possible let birthindex = this.config.birthindex ?? 0; @@ -1358,14 +1360,9 @@ export class ZkBobClient { } const startTime = Date.now(); - + console.log(`⬇ Fetching transactions between ${startIndex} and ${optimisticIndex}...`); - - const batches: Promise[] = []; - - let readyToTransact = true; - for (let i = startIndex; i <= optimisticIndex; i = i + BATCH_SIZE * OUTPLUSONE) { const oneBatch = this.fetchTransactionsOptimistic(token.relayerUrl, BigInt(i), BATCH_SIZE).then( async txs => { console.log(`Getting ${txs.length} transactions from index ${i}`); @@ -1399,6 +1396,11 @@ export class ZkBobClient { commitment: commitment, } + // TESTING CASE: artifitial relayer glitch at a fixed index + //if (memo_idx == 284800 && this.wipeAttempts < 3) { + // indexedTx.commitment = "25833f16e95ed85bb5570a670cca39f63a76e7a41f29e0504ee1efcea4121ce7"; + //} + // 3. Get txHash const txHash = tx.substr(1, 64); @@ -1480,18 +1482,43 @@ export class ZkBobClient { const avgSpeed = msElapsed / totalRes.txCount console.log(`Sync finished in ${msElapsed / 1000} sec | ${totalRes.txCount} tx, avg speed ${avgSpeed.toFixed(1)} ms/tx`); - - await this.verifyState(tokenAddress); - - return readyToTransact; } else { zpState.history.setLastMinedTxIndex(nextIndex - OUTPLUSONE); zpState.history.setLastPendingTxIndex(-1); console.log(`Local state is up to date @${startIndex}`); + } - return true; + // Self-healing code + const checkIndex = await zpState.getNextIndex(); + const stableIndex = await zpState.lastVerifiedIndex(); + if (checkIndex != stableIndex) { + const isStateCorrect = await this.verifyState(tokenAddress); + if (!isStateCorrect) { + console.log(`🚑[StateVerify] Merkle tree root at index ${checkIndex} mistmatch!`); + if (stableIndex > 0 && stableIndex < checkIndex && + this.rollbackAttempts < CORRUPT_STATE_ROLLBACK_ATTEMPTS + ) { + let realRollbackIndex = await zpState.rollback(stableIndex); + console.log(`🚑[StateVerify] The user state was rollbacked to index ${realRollbackIndex} [attempt ${this.rollbackAttempts + 1}]`); + this.rollbackAttempts++; + } else if (this.wipeAttempts < CORRUPT_STATE_WIPE_ATTEMPTS) { + await zpState.clean(); + console.log(`🚑[StateVerify] Full user state was wiped [attempt ${this.wipeAttempts + 1}]...`); + this.wipeAttempts++; + } else { + throw new InternalError(`Unable to synchronize pool state`); + } + + // resync the state + return await this.updateStateOptimisticWorker(tokenAddress); + } else { + this.rollbackAttempts = 0; + this.wipeAttempts = 0; + } } + + return readyToTransact; } // Just fetch and process the new state without local state updating @@ -1589,29 +1616,13 @@ export class ZkBobClient { const localRoot = await zpState.getRoot(); const poolRoot = (await this.config.network.poolState(token.poolAddress, checkIndex)).root; - if (localRoot != poolRoot) { - console.log(`🚑[StateVerify] Merkle tree root at index ${checkIndex} mistmatch!`); - if (this.rollbackAttempts < CORRUPT_STATE_ROLLBACK_ATTEMPTS) { - this.rollbackAttempts++; - const rollbackIndex = await zpState.lastVerifiedIndex(); - let realRollbackIndex = await zpState.rollback(rollbackIndex); - console.log(`🚑[StateVerify] Rollback tree to ${realRollbackIndex} index`); - } else if (this.wipeAttempts < CORRUPT_STATE_WIPE_ATTEMPTS) { - this.wipeAttempts++; - await zpState.clean(); - console.log(`🚑[StateVerify] Wipe tree`); - } else { - return false; - } - - await this.updateStateOptimisticWorker(tokenAddress); - } else { + if (localRoot == poolRoot) { await zpState.setLastVerifiedIndex(checkIndex); - this.rollbackAttempts = 0; - this.wipeAttempts = 0; + + return true; } - return true; + return false; } public async verifyShieldedAddress(address: string): Promise { From b7e5bb2ec71c30b43e5207ba3576b3adfd4e4c4b Mon Sep 17 00:00:00 2001 From: Evgen Date: Fri, 9 Dec 2022 13:26:57 +0300 Subject: [PATCH 07/11] Reset account birthindex during self-healing --- src/client.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client.ts b/src/client.ts index a97e5cf7..5914b0f3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1505,6 +1505,13 @@ export class ZkBobClient { } else if (this.wipeAttempts < CORRUPT_STATE_WIPE_ATTEMPTS) { await zpState.clean(); console.log(`🚑[StateVerify] Full user state was wiped [attempt ${this.wipeAttempts + 1}]...`); + + if(this.rollbackAttempts > 0) { + // If the first wipe has no effect + // reset account birthday if presented + this.config.birthindex = undefined; + } + this.wipeAttempts++; } else { throw new InternalError(`Unable to synchronize pool state`); From 94300769c8c14b43475c58271e9b9f48b0243065 Mon Sep 17 00:00:00 2001 From: EvgenKor Date: Mon, 19 Dec 2022 23:53:38 +0300 Subject: [PATCH 08/11] Checking tree root after the cold storage sync --- src/client.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 061882ed..1d6a49a3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1769,14 +1769,27 @@ export class ZkBobClient { zpState.history.saveDecryptedMemo(aMemo, false); }); + syncResult.txCount = result.txCnt; syncResult.decryptedLeafs = result.decryptedLeafsCnt; syncResult.firstIndex = actualRangeStart; syncResult.nextIndex = actualRangeEnd; syncResult.totalTime = Date.now() - startTime; - - console.log(`🧊[ColdSync] ${syncResult.txCount} txs have been loaded in ${syncResult.totalTime / 1000} secs (${syncResult.totalTime / syncResult.txCount} ms/tx)`); - console.log(`🧊[ColdSync] Merkle root after tree update: ${await zpState.getRoot()} @ ${await zpState.getNextIndex()}`); + + const isStateCorrect = await this.verifyState(tokenAddress); + if (!isStateCorrect) { + console.warn(`🧊[ColdSync] Merkle tree root at index ${await zpState.getNextIndex()} mistmatch! Wiping the state...`); + await zpState.clean(); // rollback to 0 + this.skipColdStorage = true; // prevent cold storage usage + + syncResult.txCount = 0; + syncResult.decryptedLeafs = 0; + syncResult.firstIndex = 0; + syncResult.nextIndex = 0; + } else { + console.log(`🧊[ColdSync] ${syncResult.txCount} txs have been loaded in ${syncResult.totalTime / 1000} secs (${syncResult.totalTime / syncResult.txCount} ms/tx)`); + console.log(`🧊[ColdSync] Merkle root after tree update: ${await zpState.getRoot()} @ ${await zpState.getNextIndex()}`); + } } catch (err) { console.warn(`🧊[ColdSync] cannot sync with cold storage: ${err}`); From 081f665c87859b6fae9a7148ce1f6b3dc4c4f531 Mon Sep 17 00:00:00 2001 From: Evgen Date: Thu, 22 Dec 2022 13:35:37 +0300 Subject: [PATCH 09/11] Preparing to publish: switching to the published libs, removing test code, update state error workaround --- package.json | 4 ++-- src/client.ts | 30 ++++++++---------------------- src/utils.ts | 12 ++++++++---- src/worker.ts | 9 +++++++-- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index cc14738b..7fac73e7 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "fast-sha256": "^1.3.0", "hdwallet-babyjub": "^0.0.2", "idb": "^7.0.0", - "libzkbob-rs-wasm-web": "file:../libzkbob-rs/libzkbob-rs-wasm/web", - "libzkbob-rs-wasm-web-mt": "file:../libzkbob-rs/libzkbob-rs-wasm/web-mt", + "libzkbob-rs-wasm-web": "0.9.0", + "libzkbob-rs-wasm-web-mt": "0.9.0", "regenerator-runtime": "^0.13.9", "web3": "1.8.0", "@ethereumjs/util": "^8.0.2", diff --git a/src/client.ts b/src/client.ts index 1d6a49a3..0d37db1f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1442,9 +1442,6 @@ export class ZkBobClient { // 2. Get transaction commitment const commitment = tx.substr(65, 64) - - // TEST-CASE: sync tree partially - //if (memo_idx >= 85248) continue; const indexedTx: IndexedTx = { index: memo_idx, @@ -1452,11 +1449,6 @@ export class ZkBobClient { commitment: commitment, } - // TESTING CASE: artifitial relayer glitch at a fixed index - //if (memo_idx == 284800 && this.wipeAttempts < 3) { - // indexedTx.commitment = "25833f16e95ed85bb5570a670cca39f63a76e7a41f29e0504ee1efcea4121ce7"; - //} - // 3. Get txHash const txHash = tx.substr(1, 64); @@ -1524,11 +1516,17 @@ export class ZkBobClient { for (const idx of idxs) { const oneStateUpdate = totalRes.state.get(idx); if (oneStateUpdate !== undefined) { - await zpState.updateState(oneStateUpdate, siblings); + try { + await zpState.updateState(oneStateUpdate, siblings); + } catch (err) { + const siblingsDescr = siblings !== undefined ? ` (+ ${siblings.length} siblings)` : ''; + console.warn(`🔥[HotSync] cannot update state from index ${idx}${siblingsDescr}`); + throw new InternalError(`Unable to synchronize pool state`); + } curStat.decryptedLeafs += oneStateUpdate.newLeafs.length; } else { - throw Error(`Cannot find state batch at index ${idx}`); + throw new InternalError(`Cannot find state batch at index ${idx}`); } } @@ -1909,18 +1907,6 @@ export class ZkBobClient { const headers = {'content-type': 'application/json;charset=UTF-8'}; const siblings = await this.fetchJson(url.toString(), {headers}); - // TODO: here is a test case only, remove after testing - /*let siblings: string[] = []; - if (index == 278016) { - siblings = [ - "0900000000021e0f3a711be80e44496151924743c5587860a3fbde0f283659c9d0d21659c544b5", - "0a00000000010e11de590842d36b791ffa3c0d15cfdc89d44dfe77c9254102cffe892718788c3b", - "0b0000000000861cb6c5ce6d5849ff46f84dfb01bcda53923f9a25cb00798112dcb7b323b6301a", - "0c000000000042010b42a01303918e0323f44294ad8fbbfdca86a967ee2c2b5775a866ed1cca2b", - "0d00000000002004be1969bae104b72efcc0ac9e887fa1d8c6a581e7cbfa769663c5f11ae39f29", - "12000000000000297f215cef4bd2b5991071b43389a9de1d3b947538612a62daab13aa29c13d3f" - ]; - }*/ if (!Array.isArray(siblings)) { throw new RelayerError(200, `Response should be an array`); } diff --git a/src/utils.ts b/src/utils.ts index dcc1c6b6..1637d6d1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -151,8 +151,12 @@ export class HexStringWriter { this.buf += hex; } - writeBigInt(num: bigint, numBytes: number) { - this.buf += toTwosComplementHex(num, numBytes); + writeBigInt(num: bigint, numBytes: number, le: boolean = false) { + let hex = toTwosComplementHex(num, numBytes); + if (le) { + hex = hex.match(/../g)!.reverse().join(''); + } + this.buf += hex; } writeBigIntArray(nums: bigint[], numBytes: number) { @@ -332,7 +336,7 @@ export function nodeToHex(node: TreeNode): string { const writer = new HexStringWriter(); writer.writeNumber(node.height, 1); writer.writeNumber(node.index, 6); - writer.writeBigInt(BigInt(node.value), 32); + writer.writeBigInt(BigInt(node.value), 32, true); return writer.toString(); } @@ -341,7 +345,7 @@ export function hexToNode(data: string): TreeNode | null { const reader = new HexStringReader(data); const height = reader.readNumber(1); const index = reader.readNumber(6); - const value = reader.readBigInt(32); + const value = reader.readBigInt(32, true); if (height != null && index != null && value != null) { return { height, index, value: value.toString()}; diff --git a/src/worker.ts b/src/worker.ts index b5d8fd57..c8a075b8 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -308,8 +308,13 @@ const obj = { }, async updateState(address: string, stateUpdate: StateUpdate, siblings?: TreeNode[]): Promise { - return new Promise(async resolve => { - resolve(zpAccounts[address].updateState(stateUpdate, siblings)); + return new Promise(async (resolve, reject) => { + try { + let result = zpAccounts[address].updateState(stateUpdate, siblings); + resolve(result) + } catch (e) { + reject(e) + } }); }, From 704e26eb4ec05e2df882a5337b12e3103e413dd5 Mon Sep 17 00:00:00 2001 From: Evgen Date: Thu, 22 Dec 2022 13:46:09 +0300 Subject: [PATCH 10/11] Updating yarn.lock --- yarn.lock | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index f7ba73a7..46b4ad6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2732,11 +2732,15 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -"libzkbob-rs-wasm-web-mt@file:../libzkbob-rs/libzkbob-rs-wasm/web-mt": - version "0.8.0" +libzkbob-rs-wasm-web-mt@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web-mt/-/libzkbob-rs-wasm-web-mt-0.9.0.tgz#0f3cf5353392c384e8cf827f2abc0daf31acadb3" + integrity sha512-3z6gw9n+iW7QGS63uqTM7bUtdNBcCA50JsptrfIOW11+kjxGSEH1fY/VB84C3Cod5SST7Ye8mF2YwmbzsaNCFw== -"libzkbob-rs-wasm-web@file:../libzkbob-rs/libzkbob-rs-wasm/web": - version "0.8.0" +libzkbob-rs-wasm-web@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web/-/libzkbob-rs-wasm-web-0.9.0.tgz#8bf6198382bbdd3750ee7386b0ffc32ea2be09c3" + integrity sha512-H9a6QYuR7ZXhwSOstJWAobJh128xV5EI66wQSw/oMFjGvMgD8JuClVw/eDLV+WDncFuT7p2OPnAPwBdeehVx7A== loader-runner@^4.2.0: version "4.3.0" From 44bb291f5d58147bfa2142bdf9b23de25d9d1a5d Mon Sep 17 00:00:00 2001 From: Evgen Date: Thu, 22 Dec 2022 13:50:56 +0300 Subject: [PATCH 11/11] Increasing library version: 1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7fac73e7..e2771acf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zkbob-client-js", - "version": "1.1.0", + "version": "1.2.0", "description": "zkBob integration library", "repository": "git@github.com:zkBob/libzkbob-client-js.git", "author": "Dmitry Vdovin ",