Skip to content

Commit d22a2af

Browse files
committed
chore(orchestration): update commitment DB based on whether the commitments where nullified on-chain
1 parent f169af4 commit d22a2af

7 files changed

Lines changed: 371 additions & 106 deletions

File tree

src/boilerplate/common/backup-encrypted-data-listener.mjs

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import {
88
registerKey,
99
} from './contract.mjs';
1010
import { storeCommitment } from './commitment-storage.mjs';
11+
import {
12+
processNullifierEventData,
13+
reconcileNullifiedCommitments,
14+
} from './nullifier-reconciliation.mjs';
1115
import {
1216
decompressStarlightKey,
1317
decrypt,
@@ -45,6 +49,7 @@ export default class BackupEncryptedDataEventListener {
4549
this.ethAddress = generalise(config.web3.options.defaultAccount);
4650
this.contractMetadata = {};
4751
this.eventSubscription = null; // Store as class property to prevent garbage collection
52+
this.nullifierEventSubscription = null;
4853
this.heartbeatInterval = null;
4954
this.lastEventReceived = Date.now();
5055
this.lastProcessedBlock = 0; // Track last block we processed
@@ -115,6 +120,11 @@ export default class BackupEncryptedDataEventListener {
115120
`[BACKUP] Starting backup event listener from block ${startBlock}`,
116121
);
117122

123+
const nullifierSync = await reconcileNullifiedCommitments();
124+
await this.startNullifierEventListener(
125+
(nullifierSync?.lastCheckedBlock || startBlock - 1) + 1,
126+
);
127+
118128
// Store as class property to prevent garbage collection
119129
this.eventSubscription = this.instance.events[eventName]({
120130
fromBlock: startBlock,
@@ -245,6 +255,48 @@ export default class BackupEncryptedDataEventListener {
245255
}
246256
}
247257

258+
async startNullifierEventListener(fromBlock) {
259+
const eventName = 'Nullifiers';
260+
const eventJsonInterface = this.instance._jsonInterface.find(
261+
o => o.name === eventName && o.type === 'event',
262+
);
263+
if (!eventJsonInterface) {
264+
throw new Error(
265+
'[BACKUP] Contract ABI does not include the Nullifiers event required for nullifier reconciliation.',
266+
);
267+
}
268+
269+
this.nullifierEventSubscription = this.instance.events[eventName]({
270+
fromBlock,
271+
topics: [eventJsonInterface.signature],
272+
});
273+
274+
this.nullifierEventSubscription.on('connected', subscriptionId => {
275+
console.log(
276+
`[BACKUP] Nullifier listener connected, ID: ${subscriptionId}`,
277+
);
278+
});
279+
280+
this.nullifierEventSubscription.on('data', async eventData => {
281+
try {
282+
await processNullifierEventData(eventData);
283+
await reconcileNullifiedCommitments();
284+
} catch (error) {
285+
console.error('[BACKUP] Error processing nullifier event:', error);
286+
}
287+
});
288+
289+
this.nullifierEventSubscription.on('error', async error => {
290+
console.error('[BACKUP] Nullifier subscription error:', error);
291+
await this.reconnect();
292+
});
293+
294+
this.nullifierEventSubscription.on('close', async () => {
295+
console.log('[BACKUP] Nullifier subscription closed, reconnecting...');
296+
await this.reconnect();
297+
});
298+
}
299+
248300
async processBackupEventData(eventData) {
249301
activeBackupProcesses += 1;
250302
try {
@@ -367,33 +419,6 @@ export default class BackupEncryptedDataEventListener {
367419
);
368420
continue; // eslint-disable-line no-continue
369421
}
370-
const nullifier = poseidonHash([
371-
BigInt(stateVarId.hex(32)),
372-
BigInt(kp.secretKey.hex(32)),
373-
BigInt(salt.hex(32)),
374-
]);
375-
let isNullified = false;
376-
// Check if nullifiers method exists on the contract
377-
if (this.instance.methods.nullifiers) {
378-
// eslint-disable-next-line no-await-in-loop
379-
const nullification = await this.instance.methods
380-
.nullifiers(nullifier.integer)
381-
.call();
382-
if (nullification === 0n) {
383-
isNullified = false;
384-
} else if (nullification === BigInt(nullifier.integer)) {
385-
isNullified = true;
386-
} else {
387-
throw new Error(
388-
`The nullifier value: ${nullifier.integer} does not match the on-chain nullifier: ${nullification}`,
389-
);
390-
}
391-
} else {
392-
console.log(
393-
'Contract does not have nullifiers method, assuming not nullified',
394-
);
395-
isNullified = false;
396-
}
397422
try {
398423
// eslint-disable-next-line no-await-in-loop
399424
await storeCommitment({
@@ -410,7 +435,7 @@ export default class BackupEncryptedDataEventListener {
410435
publicKey: kp.publicKey,
411436
},
412437
secretKey: kp.secretKey,
413-
isNullified,
438+
isNullified: false,
414439
});
415440
console.log('Added commitment', newCommitment.hex(32));
416441
} catch (e) {
@@ -423,6 +448,7 @@ export default class BackupEncryptedDataEventListener {
423448
}
424449
}
425450
}
451+
await reconcileNullifiedCommitments();
426452
} finally {
427453
activeBackupProcesses = Math.max(0, activeBackupProcesses - 1);
428454
}
@@ -451,6 +477,19 @@ export default class BackupEncryptedDataEventListener {
451477
this.eventSubscription = null;
452478
}
453479

480+
if (this.nullifierEventSubscription) {
481+
try {
482+
console.log('[BACKUP] Unsubscribing from nullifier subscription...');
483+
await this.nullifierEventSubscription.unsubscribe();
484+
} catch (e) {
485+
console.log(
486+
'[BACKUP] Error unsubscribing nullifier subscription:',
487+
e.message,
488+
);
489+
}
490+
this.nullifierEventSubscription = null;
491+
}
492+
454493
// Reset last event timestamp
455494
this.lastEventReceived = Date.now();
456495

src/boilerplate/common/commitment-storage.mjs

Lines changed: 131 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,114 @@ const keyDb =
2222
process.env.KEY_DB_PATH || '/app/orchestration/common/db/key.json';
2323

2424
const PRINTABLE_ASCII_REGEX = /^[\x20-\x7E]*$/;
25+
const NULLIFIER_EVENTS_COLLECTION = `${COMMITMENTS_COLLECTION}_nullifiers`;
26+
27+
async function getCommitmentsDb() {
28+
const connection = await mongo.connection(MONGO_URL);
29+
return connection.db(COMMITMENTS_DB);
30+
}
31+
32+
async function getCommitmentsCollection() {
33+
const db = await getCommitmentsDb();
34+
return db.collection(COMMITMENTS_COLLECTION);
35+
}
36+
37+
async function getNullifierEventsCollection() {
38+
const db = await getCommitmentsDb();
39+
return db.collection(NULLIFIER_EVENTS_COLLECTION);
40+
}
41+
42+
function normaliseNullifier(nullifier) {
43+
const normalised = generalise(nullifier);
44+
return {
45+
hex: normalised.hex(32),
46+
integer: normalised.bigInt.toString(),
47+
};
48+
}
49+
50+
function normaliseNullifiers(nullifiers) {
51+
const uniqueNullifiers = new Map();
52+
for (const nullifier of nullifiers) {
53+
if (nullifier === null || nullifier === undefined || nullifier === '')
54+
continue; // eslint-disable-line no-continue
55+
const normalised = normaliseNullifier(nullifier);
56+
if (BigInt(normalised.integer) === 0n) continue; // eslint-disable-line no-continue
57+
uniqueNullifiers.set(normalised.hex, normalised);
58+
}
59+
return Array.from(uniqueNullifiers.values());
60+
}
61+
62+
async function hasSeenNullifier(nullifier) {
63+
if (!nullifier) return false;
64+
const nullifierEventsCollection = await getNullifierEventsCollection();
65+
const normalised = normaliseNullifier(nullifier);
66+
const knownNullifier = await nullifierEventsCollection.findOne({
67+
_id: normalised.hex,
68+
});
69+
return !!knownNullifier;
70+
}
71+
72+
export async function recordNullifiers(nullifiers, metadata = {}) {
73+
const normalisedNullifiers = normaliseNullifiers(nullifiers);
74+
if (normalisedNullifiers.length === 0) {
75+
return {
76+
nullifierCount: 0,
77+
modifiedCount: 0,
78+
};
79+
}
80+
81+
const nullifierEventsCollection = await getNullifierEventsCollection();
82+
const commitmentsCollection = await getCommitmentsCollection();
83+
const now = new Date();
84+
const blockNumber =
85+
metadata.blockNumber === undefined || metadata.blockNumber === null
86+
? null
87+
: Number(metadata.blockNumber);
88+
const transactionHash = metadata.transactionHash ?? null;
89+
90+
await nullifierEventsCollection.bulkWrite(
91+
normalisedNullifiers.map(nullifier => ({
92+
updateOne: {
93+
filter: { _id: nullifier.hex },
94+
update: {
95+
$set: {
96+
nullifier: nullifier.hex,
97+
integer: nullifier.integer,
98+
blockNumber,
99+
transactionHash,
100+
updatedAt: now,
101+
},
102+
$setOnInsert: {
103+
createdAt: now,
104+
},
105+
},
106+
upsert: true,
107+
},
108+
})),
109+
{ ordered: false },
110+
);
111+
112+
const nullifierValues = normalisedNullifiers.flatMap(nullifier => [
113+
nullifier.hex,
114+
nullifier.integer,
115+
]);
116+
const updateResult = await commitmentsCollection.updateMany(
117+
{
118+
isNullified: false,
119+
nullifier: { $in: nullifierValues },
120+
},
121+
{
122+
$set: {
123+
isNullified: true,
124+
},
125+
},
126+
);
127+
128+
return {
129+
nullifierCount: normalisedNullifiers.length,
130+
modifiedCount: updateResult.modifiedCount || 0,
131+
};
132+
}
25133

26134
const formatPreimageValue = (rawValue, typeName = null) => {
27135
const generalisedValue = generalise(rawValue);
@@ -102,9 +210,15 @@ export function formatCommitment(commitment) {
102210
}
103211

104212
export async function persistCommitment (data) {
105-
const connection = await mongo.connection(MONGO_URL)
106-
const db = connection.db(COMMITMENTS_DB)
107-
return db.collection(COMMITMENTS_COLLECTION).insertOne(data)
213+
const connection = await mongo.connection(MONGO_URL);
214+
const db = connection.db(COMMITMENTS_DB);
215+
const commitmentData = { ...data };
216+
if (commitmentData?.nullifier && !commitmentData.isNullified) {
217+
commitmentData.isNullified = await hasSeenNullifier(
218+
commitmentData.nullifier,
219+
);
220+
}
221+
return db.collection(COMMITMENTS_COLLECTION).insertOne(commitmentData);
108222
}
109223
// function to format a commitment for a mongo db and store it
110224
export async function storeCommitment (commitment) {
@@ -114,20 +228,17 @@ export async function storeCommitment (commitment) {
114228

115229
// function to retrieve commitment with a specified stateVarId
116230
export async function getCommitmentsById(id) {
117-
const connection = await mongo.connection(MONGO_URL);
118-
const db = connection.db(COMMITMENTS_DB);
119-
const commitments = await db
120-
.collection(COMMITMENTS_COLLECTION)
231+
const commitmentsCollection = await getCommitmentsCollection();
232+
const commitments = await commitmentsCollection
121233
.find({ 'preimage.stateVarId': generalise(id).hex(32) })
122234
.toArray();
123235
return commitments;
124236
}
125237

126238
// function to retrieve commitment with a specified stateVarId
127239
export async function getCurrentWholeCommitment(id) {
128-
const connection = await mongo.connection(MONGO_URL);
129-
const db = connection.db(COMMITMENTS_DB);
130-
const commitment = await db.collection(COMMITMENTS_COLLECTION).findOne({
240+
const commitmentsCollection = await getCommitmentsCollection();
241+
const commitment = await commitmentsCollection.findOne({
131242
'preimage.stateVarId': generalise(id).hex(32),
132243
isNullified: false,
133244
});
@@ -136,12 +247,10 @@ export async function getCurrentWholeCommitment(id) {
136247

137248
// function to retrieve commitment with a specified stateName
138249
export async function getCommitmentsByState(name, mappingKey = null) {
139-
const connection = await mongo.connection(MONGO_URL);
140-
const db = connection.db(COMMITMENTS_DB);
250+
const commitmentsCollection = await getCommitmentsCollection();
141251
const query = { name: name };
142252
if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer;
143-
const commitments = await db
144-
.collection(COMMITMENTS_COLLECTION)
253+
const commitments = await commitmentsCollection
145254
.find(query)
146255
.toArray();
147256
return commitments;
@@ -161,10 +270,8 @@ export async function deleteCommitmentsByState(name, mappingKey = null) {
161270

162271
// function to retrieve all known nullified commitments
163272
export async function getNullifiedCommitments() {
164-
const connection = await mongo.connection(MONGO_URL);
165-
const db = connection.db(COMMITMENTS_DB);
166-
const commitments = await db
167-
.collection(COMMITMENTS_COLLECTION)
273+
const commitmentsCollection = await getCommitmentsCollection();
274+
const commitments = await commitmentsCollection
168275
.find({ isNullified: true })
169276
.toArray();
170277
return commitments;
@@ -174,10 +281,8 @@ export async function getNullifiedCommitments() {
174281
* @returns {Promise<number>} The sum of the values ​​of all non-nullified commitments
175282
*/
176283
export async function getBalance() {
177-
const connection = await mongo.connection(MONGO_URL);
178-
const db = connection.db(COMMITMENTS_DB);
179-
const commitments = await db
180-
.collection(COMMITMENTS_COLLECTION)
284+
const commitmentsCollection = await getCommitmentsCollection();
285+
const commitments = await commitmentsCollection
181286
.find({ isNullified: false }) // no nullified
182287
.toArray();
183288

@@ -189,12 +294,10 @@ export async function getBalance() {
189294
}
190295

191296
export async function getBalanceByState(name, mappingKey = null) {
192-
const connection = await mongo.connection(MONGO_URL);
193-
const db = connection.db(COMMITMENTS_DB);
297+
const commitmentsCollection = await getCommitmentsCollection();
194298
const query = { name: name };
195299
if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer;
196-
const commitments = await db
197-
.collection(COMMITMENTS_COLLECTION)
300+
const commitments = await commitmentsCollection
198301
.find(query)
199302
.toArray();
200303
let sumOfValues = 0;
@@ -210,10 +313,8 @@ export async function getBalanceByState(name, mappingKey = null) {
210313
* @returns all the commitments existent in this database.
211314
*/
212315
export async function getAllCommitments() {
213-
const connection = await mongo.connection(MONGO_URL);
214-
const db = connection.db(COMMITMENTS_DB);
215-
const allCommitments = await db
216-
.collection(COMMITMENTS_COLLECTION)
316+
const commitmentsCollection = await getCommitmentsCollection();
317+
const allCommitments = await commitmentsCollection
217318
.find()
218319
.toArray();
219320
return allCommitments;

src/boilerplate/common/encrypted-data-listener.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import config from 'config';
44
import { generalise } from 'general-number';
55
import { getContractAddress, getContractInstance, registerKey } from './common/contract.mjs';
66
import { storeCommitment, formatCommitment, persistCommitment } from './common/commitment-storage.mjs';
7+
import { processNullifierEventData } from './common/nullifier-reconciliation.mjs';
78
import { decrypt, poseidonHash, } from './common/number-theory.mjs';
89

910
const keyDb =
@@ -74,6 +75,7 @@ export default class EncryptedDataEventListener {
7475
const nullifierEvents = await instance.getPastEvents('Nullifiers', {
7576
fromBlock: this.contractMetadata.blockNumber || 1
7677
})
78+
await Promise.all(nullifierEvents.map(processNullifierEventData))
7779
const nullifiers = nullifierEvents
7880
.flatMap(e => e.returnValues.nullifiers)
7981
return Promise.all(

0 commit comments

Comments
 (0)