UNPKG

@unirep/core

Version:

Client library for protocol related functions which are used in UniRep protocol.

1,159 lines (1,158 loc) 41.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Synchronizer = exports.toDecString = void 0; const events_1 = require("events"); const ethers_1 = require("ethers"); const utils_1 = require("@unirep/utils"); const Unirep_json_1 = __importDefault(require("@unirep/contracts/abi/Unirep.json")); const schema_1 = require("./schema"); const nanoid_1 = require("nanoid"); // TODO: consolidate these into 'anondb' index const types_1 = require("anondb/types"); const web_1 = require("anondb/web"); const async_lock_1 = __importDefault(require("async-lock")); /** * Turn either a decimal or hex string to a decimal string. * @param content A `bigint`, `string` or `number` type data. * @returns A decimal string. */ function toDecString(content) { return BigInt(content).toString(); } exports.toDecString = toDecString; /** * The synchronizer is used to construct the Unirep state. After events are emitted from the Unirep contract, * the synchronizer will verify the events and then save the states. * @example * ```ts * import { Synchronizer } from '@unirep/core' * * const state = new Synchronizer({ * unirepAddress: '0xaabbccaabbccaabbccaabbccaabbccaabbccaaaa', * provider, // an ethers.js provider * }) * * // start the synchronizer deamon * await state.start() * await state.waitForSync() * * // stop the synchronizer deamon * state.stop() * ``` */ class Synchronizer extends events_1.EventEmitter { constructor(config) { super(); /** * @private * The array of attester IDs which are synchronized in the synchronizer. */ this._attesterId = []; /** * @private * The settings of attesters. There are `startTimestamp` and `epochLength` of each attester. */ this._attesterSettings = {}; /** * @protected * The default zero [state tree](https://developer.unirep.io/docs/protocol/trees#state-tree) leaf. */ this.defaultStateTreeLeaf = BigInt(0); /** * @protected * The default zero [epoch tree](https://developer.unirep.io/docs/protocol/trees#epoch-tree) leaf. */ this.defaultEpochTreeLeaf = BigInt(0); /** * @private * Indicates if the synchronizer is to sync with all Unirep attesters. */ this._syncAll = false; /** * @private * The id of the poll event. */ this.pollId = null; /** * How frequently the synchronizer will poll the blockchain for new events (specified in milliseconds). Default: `5000` */ this.pollRate = 5000; /** * How many blocks the synchronizer will query on each poll. Default: `100000` */ this.blockRate = 10000; /** * Allow passing the genesis block as the starting block for querying events */ this._genesisBlock = 0; /** * @private * Check if a setup is completed or not. */ this.setupComplete = false; /** * @private * Lock on poll event. */ this.lock = new async_lock_1.default(); /** * @private * Load events promises. */ this.promises = []; /** * @private * Unprocessed events. */ this._blocks = []; /** * @private * The latest completed block number. */ this._blockEnd = 0; const { db, unirepAddress, provider, attesterId, genesisBlock } = config; if (Array.isArray(attesterId)) { // multiple attesters this._attesterId = attesterId.map((a) => BigInt(a)); } else if (!!attesterId) { // single attester this._attesterId = [BigInt(attesterId)]; } else if (!attesterId) { this._syncAll = true; } this._db = db !== null && db !== void 0 ? db : new web_1.MemoryConnector((0, types_1.constructSchema)(schema_1.schema)); this._unirepContract = new ethers_1.ethers.Contract(unirepAddress, Unirep_json_1.default, provider); this._provider = provider; this._genesisBlock = genesisBlock !== null && genesisBlock !== void 0 ? genesisBlock : 0; this._settings = { stateTreeDepth: 0, epochTreeDepth: 0, historyTreeDepth: 0, numEpochKeyNoncePerEpoch: 0, epochLength: 0, fieldCount: 0, sumFieldCount: 0, replNonceBits: 0, replFieldBits: 0, }; this.setup().then(() => (this.setupComplete = true)); } buildEventHandlers() { const allEventNames = {}; this._eventHandlers = Object.keys(this.contracts).reduce((acc, address) => { // build _eventHandlers and decodeData functions const { contract, eventNames } = this.contracts[address]; const handlers = {}; for (const name of eventNames) { if (allEventNames[name]) { throw new Error(`duplicate event name registered "${name}"`); } allEventNames[name] = true; const topic = contract.filters[name]().topics[0]; const handlerName = `handle${name}`; if (typeof this[handlerName] !== 'function') { throw new Error(`No handler for event ${name} expected property "${handlerName}" to exist and be a function`); } // set this up here to avoid re-binding on every call const handler = this[`handle${name}`].bind(this); handlers[topic] = ({ event, ...args }) => { const decodedData = contract.interface.decodeEventLog(name, event.data, event.topics); // call the handler with the event and decodedData return handler({ decodedData, event, ...args }) .then((r) => { if (r) { this.emit(name, { decodedData, event }); } return r; }) .catch((err) => { console.log(`${name} handler error`); throw err; }); // uncomment this to debug // console.log(name, decodedData) }; } return { ...acc, ...handlers, }; }, {}); this._eventFilters = Object.keys(this.contracts).reduce((acc, address) => { const { contract, eventNames } = this.contracts[address]; const filter = { address, topics: [ // don't spread here, it should be a nested array eventNames.map((name) => contract.filters[name]().topics[0]), ], }; return { ...acc, [address]: filter, }; }, {}); } /** * Read the database object. */ get db() { return this._db; } /** * Read the provider which is connected in the synchronizer. */ get provider() { return this._provider; } /** * Read the UniRep smart contract object which is connected in the synchronizer. */ get unirepContract() { return this._unirepContract; } /** * Read the settings of the UniRep smart contract. */ get settings() { return this._settings; } /** * Read the genesis block of the provider environment. */ get genesisBlock() { return this._genesisBlock; } /** * The default attester ID that is set when constructed. * If there is a list of attester IDs, then the first one will be the default attester ID. * If no attester ID is given during construction, all attesters will be synchronized and the default `attesterId` will be `BigInt(0)`. * * :::caution * The default attester ID should be checked carefully while synchronizing more than one attester. * The default attester ID can be changed with [setAttesterId](#setattesterid). * ::: */ get attesterId() { if (this._attesterId.length === 0) return BigInt(0); return this._attesterId[0]; } get attestersOrClauses() { const orClauses = []; for (let id = 0; id < this._attesterId.length; id++) { orClauses.push({ attesterId: toDecString(this._attesterId[id]), }); } return orClauses; } /** * Change default [attesterId](#attesterid) to another attester ID. * It will fail if an `attesterId` is not synchronized during construction. * @param attesterId The default attester Id to be set. */ setAttesterId(attesterId) { const index = this._attesterId.indexOf(BigInt(attesterId)); if (index === -1) { throw new Error(`@unirep/core:Synchronizer: attester ID ${attesterId.toString()} is not synchronized`); } ; [this._attesterId[0], this._attesterId[index]] = [ this._attesterId[index], this._attesterId[0], ]; } /** * Check if attester events are synchronized in this synchronizer. * @param attesterId The queried attester ID. * @returns True if the attester events are synced, false otherwise. */ attesterExist(attesterId) { return this._attesterId.indexOf(BigInt(attesterId)) !== -1; } /** * Check if attester events are synchronized in this synchronizer. It will throw an error if the attester is not synchronized. * @param attesterId The queried attester ID. */ checkAttesterId(attesterId) { if (this._attesterId.length === 0) { throw new Error(`@unirep/core:Synchronizer: no attester ID is synchronized`); } if (!this.attesterExist(attesterId)) { throw new Error(`@unirep/core:Synchronizer: attester ID ${attesterId.toString()} is not synchronized`); } } /** * Run setup promises. * @returns Setup promises. */ async setup() { if (!this.setupPromise) { this.setupPromise = this._setup().catch((err) => { this.setupPromise = undefined; this.setupComplete = false; throw err; }); } return this.setupPromise; } /** * Query settings from smart contract and setup event handlers. */ async _setup() { if (this.setupComplete) return; const config = await this.unirepContract.config(); this.settings.stateTreeDepth = config.stateTreeDepth; this.settings.epochTreeDepth = config.epochTreeDepth; this.settings.historyTreeDepth = config.historyTreeDepth; this.settings.numEpochKeyNoncePerEpoch = config.numEpochKeyNoncePerEpoch; this.settings.fieldCount = config.fieldCount; this.settings.sumFieldCount = config.sumFieldCount; this.settings.replNonceBits = config.replNonceBits; this.settings.replFieldBits = config.replFieldBits; this.buildEventHandlers(); await this._findStartBlock(); this.setupComplete = true; } /** * Find the attester's genesis block in the Unirep smart contract. * Then store the `startTimestamp` and `epochLength` in database and in the memory. */ async _findStartBlock() { var _a; // look for the first attesterSignUp event // no events could be emitted before this const filter = this.unirepContract.filters.AttesterSignedUp(); if (!this._syncAll && this._attesterId.length) { (_a = filter.topics) === null || _a === void 0 ? void 0 : _a.push([ ...this._attesterId.map((n) => '0x' + n.toString(16).padStart(64, '0')), ]); } const events = await this.unirepContract.queryFilter(filter, this._genesisBlock); if (events.length === 0) { throw new Error(`@unirep/core:Synchronizer: failed to fetch genesis event`); } await this._db.transaction(async (db) => { for (let event of events) { const decodedData = this.unirepContract.interface.decodeEventLog('AttesterSignedUp', event.data, event.topics); const { timestamp, epochLength, attesterId } = decodedData; this._attesterSettings[toDecString(attesterId)] = { startTimestamp: Number(timestamp), epochLength: Number(epochLength), }; if (this._syncAll && !this.attesterExist(attesterId) && BigInt(attesterId) !== BigInt(0)) { this._attesterId.push(BigInt(attesterId)); } const syncStartBlock = event.blockNumber - 1; db.upsert('SynchronizerState', { where: { attesterId: toDecString(attesterId), }, create: { attesterId: toDecString(attesterId), latestCompleteBlock: syncStartBlock, }, update: {}, }); } }); } /** * Start the synchronizer daemon. * Start polling the blockchain for new events. If we're behind the HEAD block we'll poll many times quickly */ async start() { await this.setup(); (async () => { const pollId = (0, nanoid_1.nanoid)(); this.pollId = pollId; const minBackoff = 128; let backoff = minBackoff; for (;;) { // poll repeatedly until we're up to date try { await this.loadBlocks(this.blockRate); } catch (err) { console.error(`--- unable to load blocks`); console.error(err); console.error(`---`); } try { const { complete } = await this.poll(); if (complete) break; backoff = Math.max(backoff / 2, minBackoff); } catch (err) { backoff *= 2; console.error(`--- unirep poll failed`); console.error(err); console.error(`---`); } await new Promise((r) => setTimeout(r, backoff)); if (pollId != this.pollId) break; } for (;;) { await this.loadBlocks(this.blockRate); await new Promise((r) => setTimeout(r, this.pollRate)); if (pollId != this.pollId) break; await this.poll().catch((err) => { console.error(`--- unirep poll failed`); console.error(err); console.error(`---`); }); } })(); } /** * Stop synchronizing with Unirep contract. */ stop() { this.pollId = null; } /** * Manually poll for new events. * Returns a boolean indicating whether the synchronizer has synced to the head of the blockchain. */ async poll() { return this.lock.acquire('poll', () => this._poll()); } /** * @private * Execute polling events and processing events. */ async _poll() { if (!this.setupComplete) { console.warn('@unirep/core:Synchronizer: polled before setup, nooping'); return { complete: false }; } this.emit('pollStart'); const state = await this._db.findOne('SynchronizerState', { where: { OR: this.attestersOrClauses, }, orderBy: { latestCompleteBlock: 'asc', }, }); const latestBlock = await this.provider.getBlockNumber(); const newEvents = this._blocks; this._blocks = []; // filter out the events that have already been seen const unprocessedEvents = newEvents.filter((e) => { if (e.blockNumber === state.latestProcessedBlock) { if (e.transactionIndex === state.latestProcessedTransactionIndex) { return e.logIndex > state.latestProcessedEventIndex; } return (e.transactionIndex > state.latestProcessedTransactionIndex); } return e.blockNumber > state.latestProcessedBlock; }); await this.processEvents(unprocessedEvents); await this._db.update('SynchronizerState', { where: { OR: this.attestersOrClauses, }, update: { latestCompleteBlock: this._blockEnd, }, }); return { complete: latestBlock === this._blockEnd, }; } /** * Load more events from the smart contract. * @param n How many blocks will be loaded. */ async loadBlocks(n) { const state = await this._db.findOne('SynchronizerState', { where: { OR: this.attestersOrClauses, }, orderBy: { latestCompleteBlock: 'asc', }, }); const latestProcessed = state.latestCompleteBlock; const latestBlock = await this.provider.getBlockNumber(); const blockStart = latestProcessed + 1; const count = Math.ceil((latestBlock - blockStart + 1) / n); this._blockEnd = latestBlock; if (count <= 0) return; const promises = Array.from(Array(count).keys()).map(async (_, i) => { return this.loadNewEvents(blockStart + n * i, Math.min(blockStart + n * (i + 1) - 1, latestBlock)); }); await Promise.all(promises); this.promises.sort((a, b) => { return a.blockNumber - b.blockNumber; }); const tmp = []; for (const chunk of this.promises) { if (chunk === undefined || chunk.length === 0) { continue; } for (const block of chunk) { this._blocks.splice(0, 0, block); } } this.promises = tmp; } /** * Load new event from smart contract. * @param fromBlock From which block number. * @param toBlock To which block number. * @returns All events in the block range. */ async loadNewEvents(fromBlock, toBlock) { const promises = []; const minBackOff = 128; for (const address of Object.keys(this.contracts)) { const { contract } = this.contracts[address]; const filter = this._eventFilters[address]; let backoff = minBackOff; for (;;) { try { const request = contract.queryFilter(filter, fromBlock, toBlock); promises.push(request); request.then((r) => { this.promises.push(r); }); break; } catch (err) { console.error(`--- unable to load new events`); console.error(err); console.error(`---`); backoff *= 2; } await new Promise((r) => setTimeout(r, backoff)); } } return Promise.all(promises); } /** * Overwrite this to query events in different attester addresses. * @example * ```ts * export class AppSynchronizer extends Synchronizer { * ... * get contracts(){ * return { * ...super.contracts, * [this.appContract.address]: { * contract: this.appContract, * eventNames: [ * 'Event1', * 'Event2', * 'Event3', * ... * ] * } * } * } * ``` */ get contracts() { return { [this.unirepContract.address]: { contract: this.unirepContract, eventNames: [ 'UserSignedUp', 'UserStateTransitioned', 'Attestation', 'EpochEnded', 'StateTreeLeaf', 'EpochTreeLeaf', 'AttesterSignedUp', 'HistoryTreeLeaf', ], }, }; } /** * Process events with each event handler. * @param events The array of events will be proccessed. */ async processEvents(events) { if (events.length === 0) return; events.sort((a, b) => { if (a.blockNumber !== b.blockNumber) { return a.blockNumber - b.blockNumber; } if (a.transactionIndex !== b.transactionIndex) { return a.transactionIndex - b.transactionIndex; } return a.logIndex - b.logIndex; }); for (const event of events) { try { let success; await this._db.transaction(async (db) => { const handler = this._eventHandlers[event.topics[0]]; if (!handler) { throw new Error(`@unirep/core:Synchronizer: Unrecognized event topic "${event.topics[0]}"`); } success = await handler({ event, db, }); db.update('SynchronizerState', { where: { OR: this.attestersOrClauses, }, update: { latestProcessedBlock: +event.blockNumber, latestProcessedTransactionIndex: +event.transactionIndex, latestProcessedEventIndex: +event.logIndex, }, }); }); if (success) this.emit(event.topics[0], event); this.emit('processedEvent', event); } catch (err) { console.log(`@unirep/core:Synchronizer: Error processing event:`, err); console.log(event); throw err; } } } /** * Wait for the synchronizer to sync up to a certain block. * By default this will wait until the current latest known block (according to the provider). * @param blockNumber The block number to be synced to. */ async waitForSync(blockNumber) { const latestBlock = blockNumber !== null && blockNumber !== void 0 ? blockNumber : (await this.unirepContract.provider.getBlockNumber()); for (;;) { const state = await this._db.findOne('SynchronizerState', { where: { OR: this.attestersOrClauses, }, orderBy: { latestCompleteBlock: 'asc', }, }); if (state && state.latestCompleteBlock >= latestBlock) return; await new Promise((r) => setTimeout(r, 250)); } } /** * Get the latest processed epoch from the database. * * :::caution * This value may mismatch the onchain value depending on synchronization status. * ::: * @param attesterId The queried attester Id. * @returns `{number: number, sealed: boolean}` */ async readCurrentEpoch(attesterId = this.attesterId) { const currentEpoch = await this._db.findOne('Epoch', { where: { attesterId: attesterId.toString(), }, orderBy: { number: 'desc', }, }); return (currentEpoch || { number: 0, sealed: false, }); } /** * Calculate the current epoch determining the amount of time since the attester registration timestamp. * This operation is **synchronous** and does not involve any database operations. * @param attesterId The queried attester Id. * @returns The number of current calculated epoch. */ calcCurrentEpoch(attesterId = this.attesterId) { this.checkAttesterId(attesterId); const decAttesterId = toDecString(attesterId); const timestamp = Math.floor(+new Date() / 1000); const { startTimestamp, epochLength } = this._attesterSettings[decAttesterId]; return Math.max(0, Math.floor((timestamp - startTimestamp) / epochLength)); } /** * Calculate the amount of time remaining in the current epoch. This operation is **synchronous** and does not involve any database operations. * @param attesterId The queried attester Id. * @returns Current calculated time to the next epoch. */ calcEpochRemainingTime(attesterId = this.attesterId) { const timestamp = Math.floor(+new Date() / 1000); const currentEpoch = this.calcCurrentEpoch(attesterId); const decAttesterId = toDecString(attesterId); const { startTimestamp, epochLength } = this._attesterSettings[decAttesterId]; const epochEnd = startTimestamp + (currentEpoch + 1) * epochLength; return Math.max(0, epochEnd - timestamp); } /** * Load the current epoch number from the blockchain. * * :::tip * Use this function in test environments where the blockchain timestamp may not match the real timestamp (e.g. due to snapshot/revert patterns). * ::: * @param attesterId The queried attester Id. * @returns The number of current epoch on-chain. */ async loadCurrentEpoch(attesterId = this.attesterId) { const epoch = await this.unirepContract.attesterCurrentEpoch(attesterId); return Number(epoch); } /** * Get the epoch tree root for a certain epoch. * @param epoch The epoch to be queried. * @param attesterId The attester to be queried. * @returns The epoch tree root. */ async epochTreeRoot(epoch, attesterId = this.attesterId) { return this.unirepContract.attesterEpochRoot(attesterId, epoch); } /** * Build a merkle inclusion proof for the tree from a certain epoch. * @param epoch The epoch to be queried. * @param leafIndex The leaf index to be queried. * @param attesterId The attester to be queried. * @returns The merkle proof of the epoch tree index. */ async epochTreeProof(epoch, leafIndex, attesterId = this.attesterId) { const tree = await this.genEpochTree(epoch, attesterId); const proof = tree.createProof(leafIndex); return proof; } /** * Determine if a [nullifier](https://developer.unirep.io/docs/protocol/nullifiers) exists. All nullifiers are stored in a single mapping and expected to be globally unique. * @param nullifier A nullifier to be queried. * @returns True if the nullifier exists on-chain before, false otherwise. */ async nullifierExist(nullifier) { const epochEmitted = await this.unirepContract.usedNullifiers(nullifier); return epochEmitted; } /** * Build the latest state tree for a certain epoch. * @param epoch The epoch to be queried. * @param attesterId The attester to be queried. * @returns The state tree. */ async genStateTree(epoch, attesterId = this.attesterId) { this.checkAttesterId(attesterId); const _epoch = Number(epoch.toString()); const tree = new utils_1.IncrementalMerkleTree(this.settings.stateTreeDepth, this.defaultStateTreeLeaf); const leaves = await this._db.findMany('StateTreeLeaf', { where: { epoch: _epoch, attesterId: toDecString(attesterId), }, orderBy: { index: 'asc', }, }); for (const leaf of leaves) { tree.insert(leaf.hash); } return tree; } /** * Build the latest history tree for the current attester. * @param attesterId The attester to be queried. * @returns The history tree. */ async genHistoryTree(attesterId = this.attesterId) { const tree = new utils_1.IncrementalMerkleTree(this.settings.historyTreeDepth); const _attesterId = toDecString(attesterId); this.checkAttesterId(_attesterId); const leaves = await this._db.findMany('HistoryTreeLeaf', { where: { attesterId: _attesterId, }, orderBy: { index: 'asc', }, }); for (const { leaf } of leaves) { tree.insert(leaf); } return tree; } /** * Build the latest epoch tree for a certain epoch. * @param epoch The epoch to be queried. * @param attesterId The attester to be queried. * @returns The epoch tree. */ async genEpochTree(epoch, attesterId = this.attesterId) { this.checkAttesterId(attesterId); const _epoch = Number(epoch.toString()); const tree = new utils_1.IncrementalMerkleTree(this.settings.epochTreeDepth, this.defaultEpochTreeLeaf); const leaves = await this._db.findMany('EpochTreeLeaf', { where: { epoch: _epoch, attesterId: toDecString(attesterId), }, orderBy: { index: 'asc', }, }); for (const { hash } of leaves) { tree.insert(hash); } return tree; } /** * Determine if a state root exists in a certain epoch. * @param root The queried global state tree root * @param epoch The queried epoch of the global state tree * @param attesterId The attester to be queried. * @returns True if the global state tree root exists, false otherwise. */ async stateTreeRootExists(root, epoch, attesterId = this.attesterId) { return this.unirepContract.attesterStateTreeRootExists(attesterId, epoch, root); } /** * Check if the epoch tree root is stored in the database. * @note * :::caution * Epoch tree root of current epoch might change frequently and the tree root is not stored everytime. * Try use this function when querying the epoch tree in the **previous epoch**. * ::: * @param root The queried epoch tree root * @param epoch The queried epoch of the epoch tree * @param attesterId The attester to be queried. * @returns True if the epoch tree root is in the database, false otherwise. */ async epochTreeRootExists(root, epoch, attesterId = this.attesterId) { const _root = await this.unirepContract.attesterEpochRoot(attesterId, epoch); return _root.toString() === root.toString(); } /** * Get the number of state tree leaves in a certain epoch. * @param epoch The queried epoch. * @returns The number of the state tree leaves. */ async numStateTreeLeaves(epoch, attesterId = this.attesterId) { this.checkAttesterId(attesterId); return this._db.count('StateTreeLeaf', { epoch, attesterId: toDecString(attesterId), }); } // unirep event handlers /** * Handle state tree leaf event * @param args `EventHandlerArgs` type arguments. * @returns True if succeeds, false otherwise. */ async handleStateTreeLeaf({ event, db, decodedData }) { const epoch = Number(decodedData.epoch); const index = Number(decodedData.index); const attesterId = toDecString(decodedData.attesterId); const hash = toDecString(decodedData.leaf); const { blockNumber } = event; if (!this.attesterExist(attesterId)) return; const id = `${epoch}-${index}-${attesterId}`; db.upsert('StateTreeLeaf', { where: { id, }, update: { hash, blockNumber, }, create: { id, epoch, index, attesterId, hash, blockNumber, }, }); return true; } /** * Handle epoch tree leaf event * @param args `EventHandlerArgs` type arguments. * @returns True if succeeds, false otherwise. */ async handleEpochTreeLeaf({ event, db, decodedData }) { const epoch = Number(decodedData.epoch); const index = toDecString(decodedData.index); const attesterId = toDecString(decodedData.attesterId); const hash = toDecString(decodedData.leaf); const { blockNumber } = event; if (!this.attesterExist(attesterId)) return; const id = `${epoch}-${index}-${attesterId}`; db.upsert('EpochTreeLeaf', { where: { id, }, update: { hash, blockNumber, }, create: { id, epoch, index, attesterId, hash, blockNumber, }, }); return true; } /** * Handle user signup event * @param args `EventHandlerArgs` type arguments. * @returns True if succeeds, false otherwise. */ async handleUserSignedUp({ decodedData, event, db }) { const epoch = Number(decodedData.epoch); const commitment = toDecString(decodedData.identityCommitment); const attesterId = toDecString(decodedData.attesterId); const leafIndex = toDecString(decodedData.leafIndex); const { blockNumber } = event; if (!this.attesterExist(attesterId)) return; const existing = await this._db.findOne('UserSignUp', { where: { commitment, attesterId, }, }); if (existing) return true; db.create('UserSignUp', { commitment, epoch, attesterId, blockNumber, }); return true; } /** * Handle attestation event * @param args `EventHandlerArgs` type arguments. * @returns True if succeeds, false otherwise. */ async handleAttestation({ decodedData, event, db }) { const epoch = Number(decodedData.epoch); const epochKey = toDecString(decodedData.epochKey); const attesterId = toDecString(decodedData.attesterId); const fieldIndex = Number(decodedData.fieldIndex); const change = toDecString(decodedData.change); const { blockNumber } = event; if (!this.attesterExist(attesterId)) return; const index = `${event.blockNumber .toString() .padStart(15, '0')}${event.transactionIndex .toString() .padStart(8, '0')}${event.logIndex.toString().padStart(8, '0')}`; const currentEpoch = await this.readCurrentEpoch(attesterId); if (epoch !== currentEpoch.number && epoch !== utils_1.MAX_EPOCH) { throw new Error(`Synchronizer: Epoch (${epoch}) must be the same as the current synced epoch ${currentEpoch.number}`); } db.upsert('Attestation', { where: { index, }, update: {}, create: { epoch, epochKey, index, attesterId, fieldIndex, change, blockNumber, }, }); if (epoch === utils_1.MAX_EPOCH) return true; const findEpoch = await this._db.findOne('Epoch', { where: { attesterId, number: epoch, }, }); if (!findEpoch) { db.create('Epoch', { number: epoch, attesterId, sealed: false, }); } return true; } /** * Handle user state transition event * @param args `EventHandlerArgs` type arguments. * @returns True if succeeds, false otherwise. */ async handleUserStateTransitioned({ decodedData, event, db, }) { const transactionHash = event.transactionHash; const epoch = Number(decodedData.epoch); const attesterId = toDecString(decodedData.attesterId); const nullifier = toDecString(decodedData.nullifier); const { blockNumber } = event; if (!this.attesterExist(attesterId)) return; db.upsert('Nullifier', { where: { nullifier, }, update: {}, create: { epoch, attesterId, nullifier, transactionHash, blockNumber, }, }); return true; } /** * Handle epoch ended event * @param args `EventHandlerArgs` type arguments. * @returns True if succeeds, false otherwise. */ async handleEpochEnded({ decodedData, event, db }) { const number = Number(decodedData.epoch); const attesterId = toDecString(decodedData.attesterId); console.log(`Epoch ${number} ended`); if (!this.attesterExist(attesterId)) return; const existingDoc = await this._db.findOne('Epoch', { where: { number, attesterId, }, }); const sealed = true; if (existingDoc) { db.update('Epoch', { where: { number, attesterId, }, update: { sealed, }, }); } else { db.create('Epoch', { number, attesterId, sealed, }); } const newEpochExists = await this._db.findOne('Epoch', { where: { number: number + 1, attesterId, }, }); if (newEpochExists) return true; // create the next stub entry db.create('Epoch', { number: number + 1, attesterId, sealed: false, }); return true; } /** * Handle attester signup event * @param args `EventHandlerArgs` type arguments. * @returns True if succeeds, false otherwise. */ async handleAttesterSignedUp({ decodedData, event, db }) { const _id = toDecString(decodedData.attesterId); const epochLength = Number(decodedData.epochLength); const startTimestamp = Number(decodedData.timestamp); if (this._syncAll && !this.attesterExist(_id) && _id !== '0') { this._attesterSettings[_id] = { startTimestamp, epochLength, }; this._attesterId.push(BigInt(_id)); db.upsert('SynchronizerState', { where: { attesterId: _id, }, update: {}, create: { attesterId: _id, latestCompleteBlock: event.blockNumber - 1, }, }); } if (!this.attesterExist(_id)) return; db.upsert('Attester', { where: { _id, }, create: { _id, epochLength, startTimestamp, }, update: {}, }); return true; } /** * Handle history tree leaf event * @param args `EventHandlerArgs` type arguments. * @returns True if succeeds, false otherwise. */ async handleHistoryTreeLeaf({ decodedData, event, db }) { const attesterId = BigInt(decodedData.attesterId).toString(); const leaf = BigInt(decodedData.leaf).toString(); if (!this.attesterExist(attesterId)) return; const index = await this._db.count('HistoryTreeLeaf', { attesterId, }); const id = `${index}-${attesterId}`; db.upsert('HistoryTreeLeaf', { where: { id, }, update: {}, create: { id, index, attesterId, leaf, }, }); return true; } } exports.Synchronizer = Synchronizer;