diff --git a/src/boilerplate/common/api.mjs b/src/boilerplate/common/api.mjs index a7647706f..f5258f6df 100644 --- a/src/boilerplate/common/api.mjs +++ b/src/boilerplate/common/api.mjs @@ -4,7 +4,7 @@ import { ServiceManager } from './api_services.mjs'; import { Router } from './api_routes.mjs'; import Web3 from './common/web3.mjs'; ENCRYPTEDLISTENER_IMPORT -import BackupEncryptedDataEventListener from './common/backup-encrypted-data-listener.mjs'; +import BackupEncryptedDataEventListener from './backup-encrypted-data-listener.mjs'; function gracefulshutdown() { console.log('Shutting down'); diff --git a/src/boilerplate/common/backup-encrypted-data-listener.mjs b/src/boilerplate/common/backup-encrypted-data-listener.mjs index bb91a75f6..3f9a53a2a 100644 --- a/src/boilerplate/common/backup-encrypted-data-listener.mjs +++ b/src/boilerplate/common/backup-encrypted-data-listener.mjs @@ -6,14 +6,18 @@ import { getContractAddress, getContractInstance, registerKey, -} from './contract.mjs'; -import { storeCommitment } from './commitment-storage.mjs'; +} from './common/contract.mjs'; +import { storeCommitment } from './common/commitment-storage.mjs'; +import { + processNullifierEventData, + reconcileNullifiedCommitments, +} from './common/nullifier-reconciliation.mjs'; import { decompressStarlightKey, decrypt, poseidonHash, -} from './number-theory.mjs'; -import { getLeafIndex } from './timber.mjs'; +} from './common/number-theory.mjs'; +import { getLeafIndex } from './common/timber.mjs'; const keyDb = process.env.KEY_DB_PATH || '/app/orchestration/common/db/key.json'; @@ -45,6 +49,7 @@ export default class BackupEncryptedDataEventListener { this.ethAddress = generalise(config.web3.options.defaultAccount); this.contractMetadata = {}; this.eventSubscription = null; // Store as class property to prevent garbage collection + this.nullifierEventSubscription = null; this.heartbeatInterval = null; this.lastEventReceived = Date.now(); this.lastProcessedBlock = 0; // Track last block we processed @@ -115,6 +120,8 @@ export default class BackupEncryptedDataEventListener { `[BACKUP] Starting backup event listener from block ${startBlock}`, ); + NULLIFIER_RECONCILIATION_CODE_FIRST + // Store as class property to prevent garbage collection this.eventSubscription = this.instance.events[eventName]({ fromBlock: startBlock, @@ -245,6 +252,47 @@ export default class BackupEncryptedDataEventListener { } } + async startNullifierEventListener(fromBlock) { + const eventName = 'Nullifiers'; + const eventJsonInterface = this.instance._jsonInterface.find( + o => o.name === eventName && o.type === 'event', + ); + if (!eventJsonInterface) { + throw new Error( + '[BACKUP] Contract ABI does not include the Nullifiers event required for nullifier reconciliation.', + ); + } + + this.nullifierEventSubscription = this.instance.events[eventName]({ + fromBlock, + topics: [eventJsonInterface.signature], + }); + + this.nullifierEventSubscription.on('connected', subscriptionId => { + console.log( + `[BACKUP] Nullifier listener connected, ID: ${subscriptionId}`, + ); + }); + + this.nullifierEventSubscription.on('data', async eventData => { + try { + await processNullifierEventData(eventData); + } catch (error) { + console.error('[BACKUP] Error processing nullifier event:', error); + } + }); + + this.nullifierEventSubscription.on('error', async error => { + console.error('[BACKUP] Nullifier subscription error:', error); + await this.reconnect(); + }); + + this.nullifierEventSubscription.on('close', async () => { + console.log('[BACKUP] Nullifier subscription closed, reconnecting...'); + await this.reconnect(); + }); + } + async processBackupEventData(eventData) { activeBackupProcesses += 1; try { @@ -367,33 +415,6 @@ export default class BackupEncryptedDataEventListener { ); continue; // eslint-disable-line no-continue } - const nullifier = poseidonHash([ - BigInt(stateVarId.hex(32)), - BigInt(kp.secretKey.hex(32)), - BigInt(salt.hex(32)), - ]); - let isNullified = false; - // Check if nullifiers method exists on the contract - if (this.instance.methods.nullifiers) { - // eslint-disable-next-line no-await-in-loop - const nullification = await this.instance.methods - .nullifiers(nullifier.integer) - .call(); - if (nullification === 0n) { - isNullified = false; - } else if (nullification === BigInt(nullifier.integer)) { - isNullified = true; - } else { - throw new Error( - `The nullifier value: ${nullifier.integer} does not match the on-chain nullifier: ${nullification}`, - ); - } - } else { - console.log( - 'Contract does not have nullifiers method, assuming not nullified', - ); - isNullified = false; - } try { // eslint-disable-next-line no-await-in-loop await storeCommitment({ @@ -410,7 +431,7 @@ export default class BackupEncryptedDataEventListener { publicKey: kp.publicKey, }, secretKey: kp.secretKey, - isNullified, + isNullified: false, }); console.log('Added commitment', newCommitment.hex(32)); } catch (e) { @@ -423,6 +444,7 @@ export default class BackupEncryptedDataEventListener { } } } + NULLIFIER_RECONCILIATION_CODE_SECOND } finally { activeBackupProcesses = Math.max(0, activeBackupProcesses - 1); } @@ -451,6 +473,19 @@ export default class BackupEncryptedDataEventListener { this.eventSubscription = null; } + if (this.nullifierEventSubscription) { + try { + console.log('[BACKUP] Unsubscribing from nullifier subscription...'); + await this.nullifierEventSubscription.unsubscribe(); + } catch (e) { + console.log( + '[BACKUP] Error unsubscribing nullifier subscription:', + e.message, + ); + } + this.nullifierEventSubscription = null; + } + // Reset last event timestamp this.lastEventReceived = Date.now(); diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index fa6d4291e..8607ead05 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -22,6 +22,114 @@ const keyDb = process.env.KEY_DB_PATH || '/app/orchestration/common/db/key.json'; const PRINTABLE_ASCII_REGEX = /^[\x20-\x7E]*$/; +const NULLIFIER_EVENTS_COLLECTION = `${COMMITMENTS_COLLECTION}_nullifiers`; + +async function getCommitmentsDb() { + const connection = await mongo.connection(MONGO_URL); + return connection.db(COMMITMENTS_DB); +} + +async function getCommitmentsCollection() { + const db = await getCommitmentsDb(); + return db.collection(COMMITMENTS_COLLECTION); +} + +async function getNullifierEventsCollection() { + const db = await getCommitmentsDb(); + return db.collection(NULLIFIER_EVENTS_COLLECTION); +} + +function normaliseNullifier(nullifier) { + const normalised = generalise(nullifier); + return { + hex: normalised.hex(32), + integer: normalised.bigInt.toString(), + }; +} + +function normaliseNullifiers(nullifiers) { + const uniqueNullifiers = new Map(); + for (const nullifier of nullifiers) { + if (nullifier === null || nullifier === undefined || nullifier === '') + continue; // eslint-disable-line no-continue + const normalised = normaliseNullifier(nullifier); + if (BigInt(normalised.integer) === 0n) continue; // eslint-disable-line no-continue + uniqueNullifiers.set(normalised.hex, normalised); + } + return Array.from(uniqueNullifiers.values()); +} + +async function hasSeenNullifier(nullifier) { + if (!nullifier) return false; + const nullifierEventsCollection = await getNullifierEventsCollection(); + const normalised = normaliseNullifier(nullifier); + const knownNullifier = await nullifierEventsCollection.findOne({ + _id: normalised.hex, + }); + return !!knownNullifier; +} + +export async function recordNullifiers(nullifiers, metadata = {}) { + const normalisedNullifiers = normaliseNullifiers(nullifiers); + if (normalisedNullifiers.length === 0) { + return { + nullifierCount: 0, + modifiedCount: 0, + }; + } + + const nullifierEventsCollection = await getNullifierEventsCollection(); + const commitmentsCollection = await getCommitmentsCollection(); + const now = new Date(); + const blockNumber = + metadata.blockNumber === undefined || metadata.blockNumber === null + ? null + : Number(metadata.blockNumber); + const transactionHash = metadata.transactionHash ?? null; + + await nullifierEventsCollection.bulkWrite( + normalisedNullifiers.map(nullifier => ({ + updateOne: { + filter: { _id: nullifier.hex }, + update: { + $set: { + nullifier: nullifier.hex, + integer: nullifier.integer, + blockNumber, + transactionHash, + updatedAt: now, + }, + $setOnInsert: { + createdAt: now, + }, + }, + upsert: true, + }, + })), + { ordered: false }, + ); + + const nullifierValues = normalisedNullifiers.flatMap(nullifier => [ + nullifier.hex, + nullifier.integer, + ]); + const updateResult = await commitmentsCollection.updateMany( + { + isNullified: false, + nullifier: { $in: nullifierValues }, + }, + { + $set: { + isNullified: true, + }, + }, + ); + + return { + nullifierCount: normalisedNullifiers.length, + modifiedCount: updateResult.modifiedCount || 0, + }; +} const formatPreimageValue = (rawValue, typeName = null) => { const generalisedValue = generalise(rawValue); @@ -102,9 +210,15 @@ export function formatCommitment(commitment) { } export async function persistCommitment (data) { - const connection = await mongo.connection(MONGO_URL) - const db = connection.db(COMMITMENTS_DB) - return db.collection(COMMITMENTS_COLLECTION).insertOne(data) + const connection = await mongo.connection(MONGO_URL); + const db = connection.db(COMMITMENTS_DB); + const commitmentData = { ...data }; + if (commitmentData?.nullifier && !commitmentData.isNullified) { + commitmentData.isNullified = await hasSeenNullifier( + commitmentData.nullifier, + ); + } + return db.collection(COMMITMENTS_COLLECTION).insertOne(commitmentData); } // function to format a commitment for a mongo db and store it export async function storeCommitment (commitment) { @@ -114,10 +228,8 @@ export async function storeCommitment (commitment) { // function to retrieve commitment with a specified stateVarId export async function getCommitmentsById(id) { - const connection = await mongo.connection(MONGO_URL); - const db = connection.db(COMMITMENTS_DB); - const commitments = await db - .collection(COMMITMENTS_COLLECTION) + const commitmentsCollection = await getCommitmentsCollection(); + const commitments = await commitmentsCollection .find({ 'preimage.stateVarId': generalise(id).hex(32) }) .toArray(); return commitments; @@ -125,9 +237,8 @@ export async function getCommitmentsById(id) { // function to retrieve commitment with a specified stateVarId export async function getCurrentWholeCommitment(id) { - const connection = await mongo.connection(MONGO_URL); - const db = connection.db(COMMITMENTS_DB); - const commitment = await db.collection(COMMITMENTS_COLLECTION).findOne({ + const commitmentsCollection = await getCommitmentsCollection(); + const commitment = await commitmentsCollection.findOne({ 'preimage.stateVarId': generalise(id).hex(32), isNullified: false, }); @@ -136,12 +247,10 @@ export async function getCurrentWholeCommitment(id) { // function to retrieve commitment with a specified stateName export async function getCommitmentsByState(name, mappingKey = null) { - const connection = await mongo.connection(MONGO_URL); - const db = connection.db(COMMITMENTS_DB); + const commitmentsCollection = await getCommitmentsCollection(); const query = { name: name }; if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; - const commitments = await db - .collection(COMMITMENTS_COLLECTION) + const commitments = await commitmentsCollection .find(query) .toArray(); return commitments; @@ -161,10 +270,8 @@ export async function deleteCommitmentsByState(name, mappingKey = null) { // function to retrieve all known nullified commitments export async function getNullifiedCommitments() { - const connection = await mongo.connection(MONGO_URL); - const db = connection.db(COMMITMENTS_DB); - const commitments = await db - .collection(COMMITMENTS_COLLECTION) + const commitmentsCollection = await getCommitmentsCollection(); + const commitments = await commitmentsCollection .find({ isNullified: true }) .toArray(); return commitments; @@ -174,10 +281,8 @@ export async function getNullifiedCommitments() { * @returns {Promise} The sum of the values ​​of all non-nullified commitments */ export async function getBalance() { - const connection = await mongo.connection(MONGO_URL); - const db = connection.db(COMMITMENTS_DB); - const commitments = await db - .collection(COMMITMENTS_COLLECTION) + const commitmentsCollection = await getCommitmentsCollection(); + const commitments = await commitmentsCollection .find({ isNullified: false }) // no nullified .toArray(); @@ -189,12 +294,10 @@ export async function getBalance() { } export async function getBalanceByState(name, mappingKey = null) { - const connection = await mongo.connection(MONGO_URL); - const db = connection.db(COMMITMENTS_DB); + const commitmentsCollection = await getCommitmentsCollection(); const query = { name: name }; if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; - const commitments = await db - .collection(COMMITMENTS_COLLECTION) + const commitments = await commitmentsCollection .find(query) .toArray(); let sumOfValues = 0; @@ -210,10 +313,8 @@ export async function getBalanceByState(name, mappingKey = null) { * @returns all the commitments existent in this database. */ export async function getAllCommitments() { - const connection = await mongo.connection(MONGO_URL); - const db = connection.db(COMMITMENTS_DB); - const allCommitments = await db - .collection(COMMITMENTS_COLLECTION) + const commitmentsCollection = await getCommitmentsCollection(); + const allCommitments = await commitmentsCollection .find() .toArray(); return allCommitments; diff --git a/src/boilerplate/common/encrypted-data-listener.mjs b/src/boilerplate/common/encrypted-data-listener.mjs index 8ec456497..ec01036c6 100644 --- a/src/boilerplate/common/encrypted-data-listener.mjs +++ b/src/boilerplate/common/encrypted-data-listener.mjs @@ -4,6 +4,7 @@ import config from 'config'; import { generalise } from 'general-number'; import { getContractAddress, getContractInstance, registerKey } from './common/contract.mjs'; import { storeCommitment, formatCommitment, persistCommitment } from './common/commitment-storage.mjs'; +import { processNullifierEventData } from './common/nullifier-reconciliation.mjs'; import { decrypt, poseidonHash, } from './common/number-theory.mjs'; const keyDb = @@ -74,6 +75,7 @@ export default class EncryptedDataEventListener { const nullifierEvents = await instance.getPastEvents('Nullifiers', { fromBlock: this.contractMetadata.blockNumber || 1 }) + await Promise.all(nullifierEvents.map(processNullifierEventData)) const nullifiers = nullifierEvents .flatMap(e => e.returnValues.nullifiers) return Promise.all( diff --git a/src/boilerplate/common/nullifier-reconciliation.mjs b/src/boilerplate/common/nullifier-reconciliation.mjs new file mode 100644 index 000000000..0f7d04767 --- /dev/null +++ b/src/boilerplate/common/nullifier-reconciliation.mjs @@ -0,0 +1,152 @@ +import config from 'config'; +import mongo from './mongo.mjs'; +import logger from './logger.mjs'; +import { + getContractInstance, + getContractInterface, +} from './contract.mjs'; +import { recordNullifiers } from './commitment-storage.mjs'; +import Web3 from './web3.mjs'; + +const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION } = config; +const web3 = Web3.connection(); +const NULLIFIER_EVENT_NAME = 'Nullifiers'; +const NULLIFIER_SYNC_COLLECTION = `${COMMITMENTS_COLLECTION}_sync`; + +let nullifierReconciliationPromise = null; + +async function getNullifierSyncCollection() { + const connection = await mongo.connection(MONGO_URL); + const db = connection.db(COMMITMENTS_DB); + return db.collection(NULLIFIER_SYNC_COLLECTION); +} + +async function getDeployedContractMetadata() { + try { + const contractInterface = await getContractInterface('CONTRACT_NAME'); + const networkId = (await web3.eth.net.getId()).toString(); + const deployment = contractInterface.networks?.[networkId]; + if (!deployment?.address) return null; + return { + address: deployment.address, + blockNumber: deployment.blockNumber || 1, + networkId, + }; + } catch (error) { + logger.debug( + `Skipping commitment nullifier reconciliation: ${error.message}`, + ); + return null; + } +} + +function requireNullifierEvent(instance) { + const hasNullifierEvent = instance?._jsonInterface?.some( + item => item.type === 'event' && item.name === NULLIFIER_EVENT_NAME, + ); + if (!hasNullifierEvent) { + throw new Error( + 'Contract ABI does not include the Nullifiers event required for nullifier reconciliation.', + ); + } +} + +function getNullifiersFromEvent(eventData) { + const nullifiers = + eventData?.returnValues?.nullifiers ?? eventData?.returnValues?.[0] ?? []; + return Array.isArray(nullifiers) ? nullifiers : [nullifiers]; +} + +export async function processNullifierEventData(eventData) { + return recordNullifiers(getNullifiersFromEvent(eventData), { + blockNumber: eventData?.blockNumber, + transactionHash: eventData?.transactionHash, + }); +} + +async function reconcileNullifiedCommitmentsFromEvents(instance, deployment) { + const currentBlock = Number(await web3.eth.getBlockNumber()); + const syncCollection = await getNullifierSyncCollection(); + const syncId = [ + 'CONTRACT_NAME', + deployment.networkId, + deployment.address.toLowerCase(), + 'nullifiers', + ].join(':'); + const syncState = await syncCollection.findOne({ _id: syncId }); + const deploymentBlock = Number(deployment.blockNumber || 1); + const fromBlock = + syncState?.lastCheckedBlock === undefined + ? deploymentBlock + : Number(syncState.lastCheckedBlock) + 1; + + if (fromBlock > currentBlock) { + return { + fromBlock, + toBlock: currentBlock, + eventCount: 0, + nullifierCount: 0, + modifiedCount: 0, + lastCheckedBlock: currentBlock, + }; + } + + const nullifierEvents = await instance.getPastEvents(NULLIFIER_EVENT_NAME, { + fromBlock, + toBlock: currentBlock, + }); + + let nullifierCount = 0; + let modifiedCount = 0; + for (const eventData of nullifierEvents) { + // eslint-disable-next-line no-await-in-loop + const result = await processNullifierEventData(eventData); + nullifierCount += result.nullifierCount; + modifiedCount += result.modifiedCount; + } + + await syncCollection.updateOne( + { _id: syncId }, + { + $set: { + contractName: 'CONTRACT_NAME', + contractAddress: deployment.address, + networkId: deployment.networkId, + lastCheckedBlock: currentBlock, + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { upsert: true }, + ); + + return { + fromBlock, + toBlock: currentBlock, + eventCount: nullifierEvents.length, + nullifierCount, + modifiedCount, + lastCheckedBlock: currentBlock, + }; +} + +export async function reconcileNullifiedCommitments() { + if (!nullifierReconciliationPromise) { + nullifierReconciliationPromise = (async () => { + const deployment = await getDeployedContractMetadata(); + if (!deployment?.address) return null; + const instance = await getContractInstance( + 'CONTRACT_NAME', + deployment.address, + ); + requireNullifierEvent(instance); + return reconcileNullifiedCommitmentsFromEvents(instance, deployment); + })().finally(() => { + nullifierReconciliationPromise = null; + }); + } + + return nullifierReconciliationPromise; +} diff --git a/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts b/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts index ab0519664..4ca23bc8e 100644 --- a/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts +++ b/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts @@ -52,6 +52,7 @@ class ContractBoilerplateGenerator { `] : []), ...nullifiersRequired ? [` + event Nullifiers(uint256[] nullifiers); mapping(uint256 => uint256) public nullifiers;`] : [], ...(oldCommitmentAccessRequired ? [` @@ -194,6 +195,9 @@ class ContractBoilerplateGenerator { uint n = newNullifiers[i]; require(nullifiers[n] == 0, "Nullifier already exists"); nullifiers[n] = n; + } + if (newNullifiers.length > 0) { + emit Nullifiers(newNullifiers); }`] : []), ...(checkNullifiers ? [` diff --git a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts index 2ce73f0bd..264e90274 100644 --- a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts @@ -516,6 +516,16 @@ export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { stateVariables }; } + + case 'BackupEncryptedListenerBoilerplate': { + const { contractName, nullifiersRequired } = fields; + return { + nodeType, + contractName, + nullifiersRequired, + }; + } + case 'IntegrationTestFunction': { const { name, @@ -580,25 +590,21 @@ export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { } case 'BackupDataRetrieverBoilerplate': { - const { - contractName, - privateStates = [], - } = fields; + const { contractName, nullifiersRequired, privateStates = [] } = fields; return { nodeType, contractName, + nullifiersRequired, privateStates, }; } case 'BackupVariableBoilerplate': { - const { - contractName, - privateStates = [], - } = fields; + const { contractName, nullifiersRequired, privateStates = [] } = fields; return { nodeType, contractName, + nullifiersRequired, privateStates, }; } diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index 260f0c2b8..5c79151d9 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -43,6 +43,9 @@ class BoilerplateGenerator { ${stateVarIds.join('\n')} \nlet ${stateName}_commitmentExists = true; \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId); + \nif (!${stateName}_commitment) { + \n\tthrow new Error('Unable to find a non-nullified ${stateName} commitment with state id ' + ${stateName}_stateVarId + '.'); + \n} \nconst ${stateName}_preimage = ${stateName}_commitment.preimage; \nconst ${stateName} = generalise(${stateName}_preimage.value);`]; default: @@ -1093,8 +1096,8 @@ zappFilesBoilerplate = () => { generic: false, }, { - readPath: pathPrefix + '/backup-encrypted-data-listener.mjs', - writePath: './orchestration/common/backup-encrypted-data-listener.mjs', + readPath: pathPrefix + '/nullifier-reconciliation.mjs', + writePath: './orchestration/common/nullifier-reconciliation.mjs', generic: false, }, ]; diff --git a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts index 78d6dfdbc..0fb156f1a 100644 --- a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts +++ b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts @@ -371,15 +371,14 @@ export const preimageBoilerPlate = (node: any) => { default: // TODO - this is the case where the owner is an admin (state var) // we have to let the user submit the key and check it in the contract - if (!stateNode.ownerIsSecret && !stateNode.ownerIsParam) { + if (stateNode.isSharedSecret) { + newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? sharedPublicKey : ${privateStateName}_newOwnerPublicKey;`; + } else if (!stateNode.ownerIsSecret && !stateNode.ownerIsParam) { newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? generalise(await instance.methods.zkpPublicKeys(await instance.methods.${newOwner}().call()).call()) : ${privateStateName}_newOwnerPublicKey;`; } else if (stateNode.ownerIsParam && newOwner) { newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? ${newOwner} : ${privateStateName}_newOwnerPublicKey;`; } else { // is secret - we just use the users to avoid revealing the secret owner - if(stateNode.isSharedSecret) - newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? sharedPublicKey : ${privateStateName}_newOwnerPublicKey;`; - else newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? publicKey : ${privateStateName}_newOwnerPublicKey;` // BELOW reveals the secret owner as we check the public key in the contract diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 669bac9c1..1718119a4 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -393,6 +393,39 @@ file.file = file.file.replace(/ENCRYPTEDVARIABLE_CODE/g, encryptedCode); return file.file; } +const prepareBackupEncryptedDataListener = (node: any) => { + const readPath = path.resolve( + fileURLToPath(import.meta.url), + '../../../../../src/boilerplate/common/backup-encrypted-data-listener.mjs', + ); + const file = { + filepath: 'orchestration/backup-encrypted-data-listener.mjs', + file: fs.readFileSync(readPath, 'utf8'), + }; + file.file = file.file.replace(/CONTRACT_NAME/g, node.contractName); + let nullifierReconciliationCodeFirst = ``; + if (node.nullifiersRequired) { + nullifierReconciliationCodeFirst += ` + const nullifierSync = await reconcileNullifiedCommitments(); + await this.startNullifierEventListener( + (nullifierSync?.lastCheckedBlock || startBlock - 1) + 1, + );`; + } + file.file = file.file.replace( + /NULLIFIER_RECONCILIATION_CODE_FIRST/g, + nullifierReconciliationCodeFirst, + ); + let nullifierReconciliationCodeSecond = ``; + if (node.nullifiersRequired) { + nullifierReconciliationCodeSecond += `await reconcileNullifiedCommitments();`; + } + file.file = file.file.replace( + /NULLIFIER_RECONCILIATION_CODE_SECOND/g, + nullifierReconciliationCodeSecond, + ); + return file.file; +}; + /** * @param file - a generic migrations file skeleton to mutate * @param contextDirPath - a SetupCommonFilesBoilerplate node @@ -619,6 +652,9 @@ const prepareStartupScript = (file: localFile, node: any) => { } const prepareBackupVariable = (node: any) => { + const reconcileNullifiers = node.nullifiersRequired + ? `await reconcileNullifiedCommitments();` + : ``; // import generic test skeleton let genericApiServiceFile: any = `/* eslint-disable prettier/prettier, camelcase, prefer-const, no-unused-vars */ import config from "config"; @@ -630,8 +666,9 @@ const prepareBackupVariable = (node: any) => { import { storeCommitment, markNullified, - deleteCommitmentsByState + deleteCommitmentsByState, } from "./common/commitment-storage.mjs"; + import { reconcileNullifiedCommitments } from "./common/nullifier-reconciliation.mjs"; import { getContractInstance, getContractAddress } from "./common/contract.mjs"; @@ -645,7 +682,7 @@ const prepareBackupVariable = (node: any) => { scalarMult, } from "./common/number-theory.mjs"; import { getLeafIndex} from "./common/timber.mjs"; - import { waitForBackupListenerIdle } from "./common/backup-encrypted-data-listener.mjs"; + import { waitForBackupListenerIdle } from "./backup-encrypted-data-listener.mjs"; const { generalise } = GN; const web3 = Web3.connection(); @@ -661,9 +698,10 @@ const prepareBackupVariable = (node: any) => { await waitForBackupListenerIdle(); deleteCommitmentsByState(requestedName, null); - const instance = await getContractInstance("CONTRACT_NAME"); + const instance = await getContractInstance("CONTRACT_NAME");` + + reconcileNullifiers + ` - const backDataEvent = await instance.getPastEvents("EncryptedBackupData", { + const backDataEvent = await instance.getPastEvents("EncryptedBackupData", { fromBlock: 0, toBlock: "latest", }); @@ -789,27 +827,6 @@ const prepareBackupVariable = (node: any) => { ", Possibly this commitment has a different public key and so decryption failed."); continue; } - let nullifier = poseidonHash([ - BigInt(stateVarId.hex(32)), - BigInt(kp.secretKey.hex(32)), - BigInt(salt.hex(32)) - ]); - let isNullified = false; - // Check if nullifiers method exists on the contract - if (instance.methods.nullifiers) { - let nullification = await instance.methods.nullifiers(nullifier.integer).call(); - if (nullification === 0n) { - isNullified = false; - } else if (nullification === BigInt(nullifier.integer)) { - isNullified = true; - } else { - throw new Error("The nullifier value: " + nullifier.integer + - " does not match the on-chain nullifier: " + nullification); - } - } else { - console.log("Contract does not have nullifiers method, assuming not nullified"); - isNullified = false; - } await storeCommitment({ hash: newCommitment, name: name, @@ -824,7 +841,7 @@ const prepareBackupVariable = (node: any) => { publicKey: kp.publicKey, }, secretKey: kp.secretKey, - isNullified: isNullified, + isNullified: false, }); console.log("Added commitment", newCommitment.hex(32)); } @@ -841,6 +858,9 @@ const prepareBackupVariable = (node: any) => { }; const prepareBackupDataRetriever = (node: any) => { + const reconcileNullifiers = node.nullifiersRequired + ? `await reconcileNullifiedCommitments();` + : ``; // import generic test skeleton let genericApiServiceFile: any = `/* eslint-disable prettier/prettier, camelcase, prefer-const, no-unused-vars */ import config from "config"; @@ -853,6 +873,7 @@ const prepareBackupDataRetriever = (node: any) => { storeCommitment, markNullified, } from "./common/commitment-storage.mjs"; + import { reconcileNullifiedCommitments } from "./common/nullifier-reconciliation.mjs"; import { getContractInstance, @@ -870,7 +891,7 @@ const prepareBackupDataRetriever = (node: any) => { } from "./common/number-theory.mjs"; import { getLeafIndex} from "./common/timber.mjs"; - import { waitForBackupListenerIdle } from "./common/backup-encrypted-data-listener.mjs"; + import { waitForBackupListenerIdle } from "./backup-encrypted-data-listener.mjs"; const { generalise } = GN; const web3 = Web3.connection(); @@ -900,8 +921,9 @@ const prepareBackupDataRetriever = (node: any) => { console.error("Error emptying database:", err); } - const instance = await getContractInstance("CONTRACT_NAME"); - + const instance = await getContractInstance("CONTRACT_NAME");` + + reconcileNullifiers + ` + const contractAddr = await getContractAddress("CONTRACT_NAME"); const backDataEvent = await instance.getPastEvents('EncryptedBackupData',{fromBlock: 0, toBlock: 'latest'} ); @@ -1024,27 +1046,6 @@ const prepareBackupDataRetriever = (node: any) => { ", Possibly this commitment has a different public key and so decryption failed."); continue; } - let nullifier = poseidonHash([ - BigInt(stateVarId.hex(32)), - BigInt(kp.secretKey.hex(32)), - BigInt(salt.hex(32)) - ]) - let isNullified = false; - // Check if nullifiers method exists on the contract - if (instance.methods.nullifiers) { - let nullification = await instance.methods.nullifiers(nullifier.integer).call(); - if (nullification === 0n) { - isNullified = false; - } else if (nullification === BigInt(nullifier.integer)) { - isNullified = true; - } else { - throw new Error("The nullifier value: " + nullifier.integer + - " does not match the on-chain nullifier: " + nullification); - } - } else { - console.log("Contract does not have nullifiers method, assuming not nullified"); - isNullified = false; - } await storeCommitment({ hash: newCommitment, name: name, @@ -1059,7 +1060,7 @@ const prepareBackupDataRetriever = (node: any) => { publicKey: kp.publicKey, }, secretKey: kp.secretKey, - isNullified: isNullified, + isNullified: false, }); console.log("Added commitment", newCommitment.hex(32)); } @@ -1199,9 +1200,14 @@ export default function fileGenerator(node: any) { } case 'IntegrationEncryptedListenerBoilerplate': { - const encryptedListener = prepareIntegrationEncryptedListener(node); + const encryptedListener = prepareIntegrationEncryptedListener(node); return encryptedListener; } + + case 'BackupEncryptedListenerBoilerplate': { + const backupEncryptedListener = prepareBackupEncryptedDataListener(node); + return backupEncryptedListener; + } default: throw new TypeError(`I dont recognise this type: ${node.nodeType}`); } diff --git a/src/codeGenerators/orchestration/nodejs/toOrchestration.ts b/src/codeGenerators/orchestration/nodejs/toOrchestration.ts index 522a2c3c7..139ccf590 100644 --- a/src/codeGenerators/orchestration/nodejs/toOrchestration.ts +++ b/src/codeGenerators/orchestration/nodejs/toOrchestration.ts @@ -330,8 +330,10 @@ export default function codeGenerator(node: any, options: any = {}): any { case 'BackupVariableBoilerplate': // Separate files are handled by the fileGenerator return fileGenerator(node); - case 'IntegrationEncryptedListenerBoilerplate': - return fileGenerator(node); + case 'IntegrationEncryptedListenerBoilerplate': + return fileGenerator(node); + case 'BackupEncryptedListenerBoilerplate': + return fileGenerator(node); case 'InitialisePreimage': case 'InitialiseKeys': case 'ReadPreimage': diff --git a/src/transformers/visitors/toOrchestrationVisitor.ts b/src/transformers/visitors/toOrchestrationVisitor.ts index a4ce417b2..0c2bf9d09 100644 --- a/src/transformers/visitors/toOrchestrationVisitor.ts +++ b/src/transformers/visitors/toOrchestrationVisitor.ts @@ -373,6 +373,7 @@ const visitor = { node._newASTPointer = parent._newASTPointer; const contractName = `${node.name}Shield`; state.fullyPublicContract = true; + const { nullifiersRequired } = path.scope.indicators; if (scope.indicators.zkSnarkVerificationRequired) { const newNode = buildNode('File', { fileName: 'test', @@ -419,6 +420,7 @@ const visitor = { nodes: [ buildNode('BackupDataRetrieverBoilerplate', { contractName, + nullifiersRequired, privateStates: [], }), ], @@ -430,6 +432,7 @@ const visitor = { nodes: [ buildNode('BackupVariableBoilerplate', { contractName, + nullifiersRequired, privateStates: [], }), ], @@ -447,6 +450,17 @@ const visitor = { }); node._newASTPointer.push(newNode); } + newNode = buildNode('File', { + fileName: 'backup-encrypted-data-listener', + fileExtension: '.mjs', + nodes: [ + buildNode('BackupEncryptedListenerBoilerplate', { + contractName, + nullifiersRequired, + }), + ], + }); + node._newASTPointer.push(newNode); if (scope.indicators.newCommitmentsRequired) { const newNode = buildNode('EditableCommitmentCommonFilesBoilerplate'); node._newASTPointer.push(newNode); diff --git a/src/types/orchestration-types.ts b/src/types/orchestration-types.ts index 30cd38185..76deaef92 100644 --- a/src/types/orchestration-types.ts +++ b/src/types/orchestration-types.ts @@ -295,6 +295,7 @@ export default function buildNode(nodeType: string, fields: any = {}): any { case 'BackupDataRetrieverBoilerplate': case 'BackupVariableBoilerplate': case 'IntegrationEncryptedListenerBoilerplate': + case 'BackupEncryptedListenerBoilerplate': case 'IntegrationTestFunction': case 'IntegrationApiServiceFunction': case 'IntegrationPublicApiServiceFunction':