@aztec/pxe 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dest/database/deferred_note_dao.d.ts +4 -0
  2. package/dest/database/deferred_note_dao.d.ts.map +1 -1
  3. package/dest/database/deferred_note_dao.js +6 -3
  4. package/dest/database/note_dao.d.ts +4 -0
  5. package/dest/database/note_dao.d.ts.map +1 -1
  6. package/dest/database/note_dao.js +7 -2
  7. package/dest/database/pxe_database_test_suite.js +2 -2
  8. package/dest/kernel_prover/kernel_prover.js +9 -9
  9. package/dest/kernel_prover/proof_creator.d.ts +10 -10
  10. package/dest/kernel_prover/proof_creator.d.ts.map +1 -1
  11. package/dest/kernel_prover/proof_creator.js +4 -4
  12. package/dest/note_processor/note_processor.d.ts.map +1 -1
  13. package/dest/note_processor/note_processor.js +4 -4
  14. package/dest/note_processor/produce_note_dao.d.ts.map +1 -1
  15. package/dest/note_processor/produce_note_dao.js +4 -4
  16. package/dest/pxe_service/create_pxe_service.d.ts.map +1 -1
  17. package/dest/pxe_service/create_pxe_service.js +4 -1
  18. package/dest/pxe_service/pxe_service.d.ts.map +1 -1
  19. package/dest/pxe_service/pxe_service.js +5 -5
  20. package/package.json +13 -12
  21. package/src/bin/index.ts +42 -0
  22. package/src/config/index.ts +40 -0
  23. package/src/contract_data_oracle/index.ts +185 -0
  24. package/src/contract_data_oracle/private_functions_tree.ts +127 -0
  25. package/src/contract_database/index.ts +1 -0
  26. package/src/contract_database/memory_contract_database.ts +58 -0
  27. package/src/database/contracts/contract_artifact_db.ts +19 -0
  28. package/src/database/contracts/contract_instance_db.ts +18 -0
  29. package/src/database/deferred_note_dao.ts +55 -0
  30. package/src/database/index.ts +1 -0
  31. package/src/database/kv_pxe_database.ts +409 -0
  32. package/src/database/note_dao.ts +97 -0
  33. package/src/database/pxe_database.ts +162 -0
  34. package/src/database/pxe_database_test_suite.ts +258 -0
  35. package/src/index.ts +11 -0
  36. package/src/kernel_oracle/index.ts +61 -0
  37. package/src/kernel_prover/index.ts +2 -0
  38. package/src/kernel_prover/kernel_prover.ts +388 -0
  39. package/src/kernel_prover/proof_creator.ts +157 -0
  40. package/src/kernel_prover/proving_data_oracle.ts +76 -0
  41. package/src/note_processor/index.ts +1 -0
  42. package/src/note_processor/note_processor.ts +282 -0
  43. package/src/note_processor/produce_note_dao.ts +132 -0
  44. package/src/pxe_http/index.ts +1 -0
  45. package/src/pxe_http/pxe_http_server.ts +73 -0
  46. package/src/pxe_service/create_pxe_service.ts +52 -0
  47. package/src/pxe_service/index.ts +3 -0
  48. package/src/pxe_service/pxe_service.ts +756 -0
  49. package/src/pxe_service/test/pxe_test_suite.ts +138 -0
  50. package/src/simulator/index.ts +24 -0
  51. package/src/simulator_oracle/index.ts +210 -0
  52. package/src/synchronizer/index.ts +1 -0
  53. package/src/synchronizer/synchronizer.ts +427 -0
@@ -0,0 +1,427 @@
1
+ import {
2
+ AztecNode,
3
+ INITIAL_L2_BLOCK_NUM,
4
+ KeyStore,
5
+ L2BlockContext,
6
+ L2BlockL2Logs,
7
+ LogType,
8
+ MerkleTreeId,
9
+ TxHash,
10
+ } from '@aztec/circuit-types';
11
+ import { NoteProcessorCaughtUpStats } from '@aztec/circuit-types/stats';
12
+ import { AztecAddress, Fr, PublicKey } from '@aztec/circuits.js';
13
+ import { SerialQueue } from '@aztec/foundation/fifo';
14
+ import { DebugLogger, createDebugLogger } from '@aztec/foundation/log';
15
+ import { RunningPromise } from '@aztec/foundation/running-promise';
16
+
17
+ import { DeferredNoteDao } from '../database/deferred_note_dao.js';
18
+ import { PxeDatabase } from '../database/index.js';
19
+ import { NoteDao } from '../database/note_dao.js';
20
+ import { NoteProcessor } from '../note_processor/index.js';
21
+
22
+ /**
23
+ * The Synchronizer class manages the synchronization of note processors and interacts with the Aztec node
24
+ * to obtain encrypted logs, blocks, and other necessary information for the accounts.
25
+ * It provides methods to start or stop the synchronization process, add new accounts, retrieve account
26
+ * details, and fetch transactions by hash. The Synchronizer ensures that it maintains the note processors
27
+ * in sync with the blockchain while handling retries and errors gracefully.
28
+ */
29
+ export class Synchronizer {
30
+ private runningPromise?: RunningPromise;
31
+ private noteProcessors: NoteProcessor[] = [];
32
+ private running = false;
33
+ private initialSyncBlockNumber = INITIAL_L2_BLOCK_NUM - 1;
34
+ private log: DebugLogger;
35
+ private noteProcessorsToCatchUp: NoteProcessor[] = [];
36
+
37
+ constructor(private node: AztecNode, private db: PxeDatabase, private jobQueue: SerialQueue, logSuffix = '') {
38
+ this.log = createDebugLogger(logSuffix ? `aztec:pxe_synchronizer_${logSuffix}` : 'aztec:pxe_synchronizer');
39
+ }
40
+
41
+ /**
42
+ * Starts the synchronization process by fetching encrypted logs and blocks from a specified position.
43
+ * Continuously processes the fetched data for all note processors until stopped. If there is no data
44
+ * available, it retries after a specified interval.
45
+ *
46
+ * @param limit - The maximum number of encrypted, unencrypted logs and blocks to fetch in each iteration.
47
+ * @param retryInterval - The time interval (in ms) to wait before retrying if no data is available.
48
+ */
49
+ public async start(limit = 1, retryInterval = 1000) {
50
+ if (this.running) {
51
+ return;
52
+ }
53
+ this.running = true;
54
+
55
+ await this.jobQueue.put(() => this.initialSync());
56
+ this.log('Initial sync complete');
57
+ this.runningPromise = new RunningPromise(() => this.sync(limit), retryInterval);
58
+ this.runningPromise.start();
59
+ this.log('Started loop');
60
+ }
61
+
62
+ protected async initialSync() {
63
+ // fast forward to the latest block
64
+ const latestHeader = await this.node.getHeader();
65
+ this.initialSyncBlockNumber = Number(latestHeader.globalVariables.blockNumber.toBigInt());
66
+ await this.db.setHeader(latestHeader);
67
+ }
68
+
69
+ /**
70
+ * Fetches encrypted logs and blocks from the Aztec node and processes them for all note processors.
71
+ * If needed, catches up note processors that are lagging behind the main sync, e.g. because we just added a new account.
72
+ *
73
+ * Uses the job queue to ensure that
74
+ * - sync does not overlap with pxe simulations.
75
+ * - one sync is running at a time.
76
+ *
77
+ * @param limit - The maximum number of encrypted, unencrypted logs and blocks to fetch in each iteration.
78
+ * @returns a promise that resolves when the sync is complete
79
+ */
80
+ protected sync(limit: number) {
81
+ return this.jobQueue.put(async () => {
82
+ let moreWork = true;
83
+ // keep external this.running flag to interrupt greedy sync
84
+ while (moreWork && this.running) {
85
+ if (this.noteProcessorsToCatchUp.length > 0) {
86
+ // There is a note processor that needs to catch up. We hijack the main loop to catch up the note processor.
87
+ moreWork = await this.workNoteProcessorCatchUp(limit);
88
+ } else {
89
+ // No note processor needs to catch up. We continue with the normal flow.
90
+ moreWork = await this.work(limit);
91
+ }
92
+ }
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Fetches encrypted logs and blocks from the Aztec node and processes them for all note processors.
98
+ *
99
+ * @param limit - The maximum number of encrypted, unencrypted logs and blocks to fetch in each iteration.
100
+ * @returns true if there could be more work, false if we're caught up or there was an error.
101
+ */
102
+ protected async work(limit = 1): Promise<boolean> {
103
+ const from = this.getSynchedBlockNumber() + 1;
104
+ try {
105
+ // Possibly improve after https://github.com/AztecProtocol/aztec-packages/issues/3870
106
+ let encryptedLogs = await this.node.getLogs(from, limit, LogType.ENCRYPTED);
107
+ if (!encryptedLogs.length) {
108
+ return false;
109
+ }
110
+
111
+ let unencryptedLogs = await this.node.getLogs(from, limit, LogType.UNENCRYPTED);
112
+ if (!unencryptedLogs.length) {
113
+ return false;
114
+ }
115
+
116
+ // Note: If less than `limit` encrypted logs is returned, then we fetch only that number of blocks.
117
+ const blocks = await this.node.getBlocks(from, encryptedLogs.length);
118
+ if (!blocks.length) {
119
+ return false;
120
+ }
121
+
122
+ if (blocks.length !== encryptedLogs.length) {
123
+ // "Trim" the encrypted logs to match the number of blocks.
124
+ encryptedLogs = encryptedLogs.slice(0, blocks.length);
125
+ }
126
+
127
+ if (blocks.length !== unencryptedLogs.length) {
128
+ // "Trim" the unencrypted logs to match the number of blocks.
129
+ unencryptedLogs = unencryptedLogs.slice(0, blocks.length);
130
+ }
131
+
132
+ // attach logs to blocks
133
+ blocks.forEach((block, i) => {
134
+ block.attachLogs(encryptedLogs[i], LogType.ENCRYPTED);
135
+ block.attachLogs(unencryptedLogs[i], LogType.UNENCRYPTED);
136
+ });
137
+
138
+ // Wrap blocks in block contexts & only keep those that match our query
139
+ const blockContexts = blocks.filter(block => block.number >= from).map(block => new L2BlockContext(block));
140
+
141
+ // Update latest tree roots from the most recent block
142
+ const latestBlock = blockContexts[blockContexts.length - 1];
143
+ await this.setHeaderFromBlock(latestBlock);
144
+
145
+ const logCount = L2BlockL2Logs.getTotalLogCount(encryptedLogs);
146
+ this.log(`Forwarding ${logCount} encrypted logs and blocks to ${this.noteProcessors.length} note processors`);
147
+ for (const noteProcessor of this.noteProcessors) {
148
+ await noteProcessor.process(blockContexts, encryptedLogs);
149
+ }
150
+ return true;
151
+ } catch (err) {
152
+ this.log.error(`Error in synchronizer work`, err);
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Catch up note processors that are lagging behind the main sync.
159
+ * e.g. because we just added a new account.
160
+ *
161
+ * @param limit - the maximum number of encrypted, unencrypted logs and blocks to fetch in each iteration.
162
+ * @returns true if there could be more work, false if there was an error which allows a retry with delay.
163
+ */
164
+ protected async workNoteProcessorCatchUp(limit = 1): Promise<boolean> {
165
+ const toBlockNumber = this.getSynchedBlockNumber();
166
+
167
+ // filter out note processors that are already caught up
168
+ // and sort them by the block number they are lagging behind in ascending order
169
+ this.noteProcessorsToCatchUp = this.noteProcessorsToCatchUp.filter(noteProcessor => {
170
+ if (noteProcessor.status.syncedToBlock >= toBlockNumber) {
171
+ // Note processor is ahead of main sync, nothing to do
172
+ this.noteProcessors.push(noteProcessor);
173
+ return false;
174
+ }
175
+ return true;
176
+ });
177
+
178
+ if (!this.noteProcessorsToCatchUp.length) {
179
+ // No note processors to catch up, nothing to do here,
180
+ // but we return true to continue with the normal flow.
181
+ return true;
182
+ }
183
+
184
+ // create a copy so that:
185
+ // 1. we can modify the original array while iterating over it
186
+ // 2. we don't need to serialize insertions into the array
187
+ const catchUpGroup = this.noteProcessorsToCatchUp
188
+ .slice()
189
+ // sort by the block number they are lagging behind
190
+ .sort((a, b) => a.status.syncedToBlock - b.status.syncedToBlock);
191
+
192
+ // grab the note processor that is lagging behind the most
193
+ const from = catchUpGroup[0].status.syncedToBlock + 1;
194
+ // Ensuring that the note processor does not sync further than the main sync.
195
+ limit = Math.min(limit, toBlockNumber - from + 1);
196
+ // this.log(`Catching up ${catchUpGroup.length} note processors by up to ${limit} blocks starting at block ${from}`);
197
+
198
+ if (limit < 1) {
199
+ throw new Error(`Unexpected limit ${limit} for note processor catch up`);
200
+ }
201
+
202
+ try {
203
+ let encryptedLogs = await this.node.getLogs(from, limit, LogType.ENCRYPTED);
204
+ if (!encryptedLogs.length) {
205
+ // This should never happen because this function should only be called when the note processor is lagging
206
+ // behind main sync.
207
+ throw new Error('No encrypted logs in processor catch up mode');
208
+ }
209
+
210
+ // Note: If less than `limit` encrypted logs is returned, then we fetch only that number of blocks.
211
+ const blocks = await this.node.getBlocks(from, encryptedLogs.length);
212
+ if (!blocks.length) {
213
+ // This should never happen because this function should only be called when the note processor is lagging
214
+ // behind main sync.
215
+ throw new Error('No blocks in processor catch up mode');
216
+ }
217
+
218
+ if (blocks.length !== encryptedLogs.length) {
219
+ // "Trim" the encrypted logs to match the number of blocks.
220
+ encryptedLogs = encryptedLogs.slice(0, blocks.length);
221
+ }
222
+
223
+ const blockContexts = blocks.map(block => new L2BlockContext(block));
224
+
225
+ const logCount = L2BlockL2Logs.getTotalLogCount(encryptedLogs);
226
+ this.log(`Forwarding ${logCount} encrypted logs and blocks to note processors in catch up mode`);
227
+
228
+ for (const noteProcessor of catchUpGroup) {
229
+ // find the index of the first block that the note processor is not yet synced to
230
+ const index = blockContexts.findIndex(block => block.block.number > noteProcessor.status.syncedToBlock);
231
+ if (index === -1) {
232
+ // Due to the limit, we might not have fetched a new enough block for the note processor.
233
+ // And since the group is sorted, we break as soon as we find a note processor
234
+ // that needs blocks newer than the newest block we fetched.
235
+ break;
236
+ }
237
+
238
+ this.log.debug(
239
+ `Catching up note processor ${noteProcessor.publicKey.toString()} by processing ${
240
+ blockContexts.length - index
241
+ } blocks`,
242
+ );
243
+ await noteProcessor.process(blockContexts.slice(index), encryptedLogs.slice(index));
244
+
245
+ if (noteProcessor.status.syncedToBlock === toBlockNumber) {
246
+ // Note processor caught up, move it to `noteProcessors` from `noteProcessorsToCatchUp`.
247
+ this.log(`Note processor for ${noteProcessor.publicKey.toString()} has caught up`, {
248
+ eventName: 'note-processor-caught-up',
249
+ publicKey: noteProcessor.publicKey.toString(),
250
+ duration: noteProcessor.timer.ms(),
251
+ dbSize: this.db.estimateSize(),
252
+ ...noteProcessor.stats,
253
+ } satisfies NoteProcessorCaughtUpStats);
254
+
255
+ this.noteProcessorsToCatchUp = this.noteProcessorsToCatchUp.filter(
256
+ np => !np.publicKey.equals(noteProcessor.publicKey),
257
+ );
258
+ this.noteProcessors.push(noteProcessor);
259
+ }
260
+ }
261
+
262
+ return true; // could be more work, immediately continue syncing
263
+ } catch (err) {
264
+ this.log.error(`Error in synchronizer workNoteProcessorCatchUp`, err);
265
+ return false;
266
+ }
267
+ }
268
+
269
+ private async setHeaderFromBlock(latestBlock: L2BlockContext) {
270
+ const { block } = latestBlock;
271
+ if (block.number < this.initialSyncBlockNumber) {
272
+ return;
273
+ }
274
+
275
+ await this.db.setHeader(block.header);
276
+ }
277
+
278
+ /**
279
+ * Stops the synchronizer gracefully, interrupting any ongoing sleep and waiting for the current
280
+ * iteration to complete before setting the running state to false. Once stopped, the synchronizer
281
+ * will no longer process blocks or encrypted logs and must be restarted using the start method.
282
+ *
283
+ * @returns A promise that resolves when the synchronizer has successfully stopped.
284
+ */
285
+ public async stop() {
286
+ this.running = false;
287
+ await this.runningPromise?.stop();
288
+ this.log('Stopped');
289
+ }
290
+
291
+ /**
292
+ * Add a new account to the Synchronizer with the specified private key.
293
+ * Creates a NoteProcessor instance for the account and pushes it into the noteProcessors array.
294
+ * The method resolves immediately after pushing the new note processor.
295
+ *
296
+ * @param publicKey - The public key for the account.
297
+ * @param keyStore - The key store.
298
+ * @param startingBlock - The block where to start scanning for notes for this accounts.
299
+ * @returns A promise that resolves once the account is added to the Synchronizer.
300
+ */
301
+ public addAccount(publicKey: PublicKey, keyStore: KeyStore, startingBlock: number) {
302
+ const predicate = (x: NoteProcessor) => x.publicKey.equals(publicKey);
303
+ const processor = this.noteProcessors.find(predicate) ?? this.noteProcessorsToCatchUp.find(predicate);
304
+ if (processor) {
305
+ return;
306
+ }
307
+
308
+ this.noteProcessorsToCatchUp.push(new NoteProcessor(publicKey, keyStore, this.db, this.node, startingBlock));
309
+ }
310
+
311
+ /**
312
+ * Checks if the specified account is synchronized.
313
+ * @param account - The aztec address for which to query the sync status.
314
+ * @returns True if the account is fully synched, false otherwise.
315
+ * @remarks Checks whether all the notes from all the blocks have been processed. If it is not the case, the
316
+ * retrieved information from contracts might be old/stale (e.g. old token balance).
317
+ * @throws If checking a sync status of account which is not registered.
318
+ */
319
+ public async isAccountStateSynchronized(account: AztecAddress) {
320
+ const completeAddress = await this.db.getCompleteAddress(account);
321
+ if (!completeAddress) {
322
+ throw new Error(`Checking if account is synched is not possible for ${account} because it is not registered.`);
323
+ }
324
+ const findByPublicKey = (x: NoteProcessor) => x.publicKey.equals(completeAddress.publicKey);
325
+ const processor = this.noteProcessors.find(findByPublicKey) ?? this.noteProcessorsToCatchUp.find(findByPublicKey);
326
+ if (!processor) {
327
+ throw new Error(
328
+ `Checking if account is synched is not possible for ${account} because it is only registered as a recipient.`,
329
+ );
330
+ }
331
+ return await processor.isSynchronized();
332
+ }
333
+
334
+ private getSynchedBlockNumber() {
335
+ return this.db.getBlockNumber() ?? this.initialSyncBlockNumber;
336
+ }
337
+
338
+ /**
339
+ * Checks whether all the blocks were processed (tree roots updated, txs updated with block info, etc.).
340
+ * @returns True if there are no outstanding blocks to be synched.
341
+ * @remarks This indicates that blocks and transactions are synched even if notes are not.
342
+ * @remarks Compares local block number with the block number from aztec node.
343
+ */
344
+ public async isGlobalStateSynchronized() {
345
+ const latest = await this.node.getBlockNumber();
346
+ return latest <= this.getSynchedBlockNumber();
347
+ }
348
+
349
+ /**
350
+ * Returns the latest block that has been synchronized by the synchronizer and each account.
351
+ * @returns The latest block synchronized for blocks, and the latest block synched for notes for each public key being tracked.
352
+ */
353
+ public getSyncStatus() {
354
+ const lastBlockNumber = this.getSynchedBlockNumber();
355
+ return {
356
+ blocks: lastBlockNumber,
357
+ notes: Object.fromEntries(this.noteProcessors.map(n => [n.publicKey.toString(), n.status.syncedToBlock])),
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Retry decoding any deferred notes for the specified contract address.
363
+ * @param contractAddress - the contract address that has just been added
364
+ */
365
+ public reprocessDeferredNotesForContract(contractAddress: AztecAddress): Promise<void> {
366
+ return this.jobQueue.put(() => this.#reprocessDeferredNotesForContract(contractAddress));
367
+ }
368
+
369
+ async #reprocessDeferredNotesForContract(contractAddress: AztecAddress): Promise<void> {
370
+ const deferredNotes = await this.db.getDeferredNotesByContract(contractAddress);
371
+
372
+ // group deferred notes by txHash to properly deal with possible duplicates
373
+ const txHashToDeferredNotes: Map<TxHash, DeferredNoteDao[]> = new Map();
374
+ for (const note of deferredNotes) {
375
+ const notesForTx = txHashToDeferredNotes.get(note.txHash) ?? [];
376
+ notesForTx.push(note);
377
+ txHashToDeferredNotes.set(note.txHash, notesForTx);
378
+ }
379
+
380
+ // keep track of decoded notes
381
+ const newNotes: NoteDao[] = [];
382
+ // now process each txHash
383
+ for (const deferredNotes of txHashToDeferredNotes.values()) {
384
+ // to be safe, try each note processor in case the deferred notes are for different accounts.
385
+ for (const processor of this.noteProcessors) {
386
+ const decodedNotes = await processor.decodeDeferredNotes(
387
+ deferredNotes.filter(n => n.publicKey.equals(processor.publicKey)),
388
+ );
389
+ newNotes.push(...decodedNotes);
390
+ }
391
+ }
392
+
393
+ // now drop the deferred notes, and add the decoded notes
394
+ await this.db.removeDeferredNotesByContract(contractAddress);
395
+ await this.db.addNotes(newNotes);
396
+
397
+ newNotes.forEach(noteDao => {
398
+ this.log(
399
+ `Decoded deferred note for contract ${noteDao.contractAddress} at slot ${
400
+ noteDao.storageSlot
401
+ } with nullifier ${noteDao.siloedNullifier.toString()}`,
402
+ );
403
+ });
404
+
405
+ // now group the decoded notes by public key
406
+ const publicKeyToNotes: Map<PublicKey, NoteDao[]> = new Map();
407
+ for (const noteDao of newNotes) {
408
+ const notesForPublicKey = publicKeyToNotes.get(noteDao.publicKey) ?? [];
409
+ notesForPublicKey.push(noteDao);
410
+ publicKeyToNotes.set(noteDao.publicKey, notesForPublicKey);
411
+ }
412
+
413
+ // now for each group, look for the nullifiers in the nullifier tree
414
+ for (const [publicKey, notes] of publicKeyToNotes.entries()) {
415
+ const nullifiers = notes.map(n => n.siloedNullifier);
416
+ const relevantNullifiers: Fr[] = [];
417
+ for (const nullifier of nullifiers) {
418
+ // NOTE: this leaks information about the nullifiers I'm interested in to the node.
419
+ const found = await this.node.findLeafIndex('latest', MerkleTreeId.NULLIFIER_TREE, nullifier);
420
+ if (found) {
421
+ relevantNullifiers.push(nullifier);
422
+ }
423
+ }
424
+ await this.db.removeNullifiedNotes(relevantNullifiers, publicKey);
425
+ }
426
+ }
427
+ }