@aztec/pxe 0.0.1-commit.e61ad554 → 0.0.1-commit.ec5f612

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 (205) hide show
  1. package/dest/access_scopes.d.ts +9 -0
  2. package/dest/access_scopes.d.ts.map +1 -0
  3. package/dest/access_scopes.js +6 -0
  4. package/dest/block_synchronizer/block_synchronizer.d.ts +5 -3
  5. package/dest/block_synchronizer/block_synchronizer.d.ts.map +1 -1
  6. package/dest/block_synchronizer/block_synchronizer.js +11 -5
  7. package/dest/config/package_info.js +1 -1
  8. package/dest/contract_function_simulator/contract_function_simulator.d.ts +54 -30
  9. package/dest/contract_function_simulator/contract_function_simulator.d.ts.map +1 -1
  10. package/dest/contract_function_simulator/contract_function_simulator.js +174 -70
  11. package/dest/contract_function_simulator/execution_tagging_index_cache.d.ts +5 -5
  12. package/dest/contract_function_simulator/execution_tagging_index_cache.d.ts.map +1 -1
  13. package/dest/contract_function_simulator/execution_tagging_index_cache.js +3 -3
  14. package/dest/contract_function_simulator/noir-structs/event_validation_request.js +1 -1
  15. package/dest/contract_function_simulator/noir-structs/note_validation_request.d.ts +2 -2
  16. package/dest/contract_function_simulator/noir-structs/note_validation_request.d.ts.map +1 -1
  17. package/dest/contract_function_simulator/noir-structs/note_validation_request.js +1 -1
  18. package/dest/contract_function_simulator/oracle/interfaces.d.ts +10 -10
  19. package/dest/contract_function_simulator/oracle/interfaces.d.ts.map +1 -1
  20. package/dest/contract_function_simulator/oracle/oracle.d.ts +5 -5
  21. package/dest/contract_function_simulator/oracle/oracle.d.ts.map +1 -1
  22. package/dest/contract_function_simulator/oracle/oracle.js +38 -26
  23. package/dest/contract_function_simulator/oracle/private_execution_oracle.d.ts +36 -36
  24. package/dest/contract_function_simulator/oracle/private_execution_oracle.d.ts.map +1 -1
  25. package/dest/contract_function_simulator/oracle/private_execution_oracle.js +74 -29
  26. package/dest/contract_function_simulator/oracle/utility_execution_oracle.d.ts +53 -29
  27. package/dest/contract_function_simulator/oracle/utility_execution_oracle.d.ts.map +1 -1
  28. package/dest/contract_function_simulator/oracle/utility_execution_oracle.js +84 -63
  29. package/dest/contract_logging.d.ts +22 -0
  30. package/dest/contract_logging.d.ts.map +1 -0
  31. package/dest/contract_logging.js +23 -0
  32. package/dest/contract_sync/contract_sync_service.d.ts +43 -0
  33. package/dest/contract_sync/contract_sync_service.d.ts.map +1 -0
  34. package/dest/contract_sync/contract_sync_service.js +97 -0
  35. package/dest/contract_sync/helpers.d.ts +29 -0
  36. package/dest/contract_sync/helpers.d.ts.map +1 -0
  37. package/dest/contract_sync/{index.js → helpers.js} +13 -12
  38. package/dest/debug/pxe_debug_utils.d.ts +24 -10
  39. package/dest/debug/pxe_debug_utils.d.ts.map +1 -1
  40. package/dest/debug/pxe_debug_utils.js +28 -18
  41. package/dest/entrypoints/client/bundle/index.d.ts +4 -1
  42. package/dest/entrypoints/client/bundle/index.d.ts.map +1 -1
  43. package/dest/entrypoints/client/bundle/index.js +3 -0
  44. package/dest/entrypoints/client/bundle/utils.d.ts +1 -1
  45. package/dest/entrypoints/client/bundle/utils.d.ts.map +1 -1
  46. package/dest/entrypoints/client/bundle/utils.js +21 -7
  47. package/dest/entrypoints/client/lazy/index.d.ts +4 -1
  48. package/dest/entrypoints/client/lazy/index.d.ts.map +1 -1
  49. package/dest/entrypoints/client/lazy/index.js +3 -0
  50. package/dest/entrypoints/client/lazy/utils.d.ts +2 -2
  51. package/dest/entrypoints/client/lazy/utils.d.ts.map +1 -1
  52. package/dest/entrypoints/client/lazy/utils.js +22 -8
  53. package/dest/entrypoints/pxe_creation_options.d.ts +3 -2
  54. package/dest/entrypoints/pxe_creation_options.d.ts.map +1 -1
  55. package/dest/entrypoints/server/index.d.ts +4 -2
  56. package/dest/entrypoints/server/index.d.ts.map +1 -1
  57. package/dest/entrypoints/server/index.js +3 -1
  58. package/dest/entrypoints/server/utils.d.ts +1 -1
  59. package/dest/entrypoints/server/utils.d.ts.map +1 -1
  60. package/dest/entrypoints/server/utils.js +28 -9
  61. package/dest/events/event_service.d.ts +4 -5
  62. package/dest/events/event_service.d.ts.map +1 -1
  63. package/dest/events/event_service.js +5 -6
  64. package/dest/job_coordinator/job_coordinator.d.ts +3 -2
  65. package/dest/job_coordinator/job_coordinator.d.ts.map +1 -1
  66. package/dest/job_coordinator/job_coordinator.js +3 -2
  67. package/dest/logs/log_service.d.ts +7 -5
  68. package/dest/logs/log_service.d.ts.map +1 -1
  69. package/dest/logs/log_service.js +19 -29
  70. package/dest/notes/note_service.d.ts +7 -7
  71. package/dest/notes/note_service.d.ts.map +1 -1
  72. package/dest/notes/note_service.js +9 -9
  73. package/dest/notes_filter.d.ts +25 -0
  74. package/dest/notes_filter.d.ts.map +1 -0
  75. package/dest/notes_filter.js +4 -0
  76. package/dest/oracle_version.d.ts +3 -3
  77. package/dest/oracle_version.d.ts.map +1 -1
  78. package/dest/oracle_version.js +2 -2
  79. package/dest/private_kernel/hints/compute_tx_expiration_timestamp.d.ts +4 -0
  80. package/dest/private_kernel/hints/compute_tx_expiration_timestamp.d.ts.map +1 -0
  81. package/dest/private_kernel/hints/{compute_tx_include_by_timestamp.js → compute_tx_expiration_timestamp.js} +12 -12
  82. package/dest/private_kernel/hints/index.d.ts +1 -1
  83. package/dest/private_kernel/hints/index.js +1 -1
  84. package/dest/private_kernel/hints/private_kernel_reset_private_inputs_builder.d.ts +4 -3
  85. package/dest/private_kernel/hints/private_kernel_reset_private_inputs_builder.d.ts.map +1 -1
  86. package/dest/private_kernel/hints/private_kernel_reset_private_inputs_builder.js +129 -68
  87. package/dest/private_kernel/hints/test_utils.d.ts +122 -0
  88. package/dest/private_kernel/hints/test_utils.d.ts.map +1 -0
  89. package/dest/private_kernel/hints/test_utils.js +203 -0
  90. package/dest/private_kernel/private_kernel_execution_prover.d.ts +3 -2
  91. package/dest/private_kernel/private_kernel_execution_prover.d.ts.map +1 -1
  92. package/dest/private_kernel/private_kernel_execution_prover.js +21 -13
  93. package/dest/private_kernel/private_kernel_oracle.d.ts +8 -4
  94. package/dest/private_kernel/private_kernel_oracle.d.ts.map +1 -1
  95. package/dest/private_kernel/private_kernel_oracle.js +7 -3
  96. package/dest/pxe.d.ts +69 -23
  97. package/dest/pxe.d.ts.map +1 -1
  98. package/dest/pxe.js +98 -63
  99. package/dest/storage/address_store/address_store.d.ts +1 -1
  100. package/dest/storage/address_store/address_store.d.ts.map +1 -1
  101. package/dest/storage/address_store/address_store.js +12 -11
  102. package/dest/storage/anchor_block_store/anchor_block_store.d.ts +9 -1
  103. package/dest/storage/anchor_block_store/anchor_block_store.d.ts.map +1 -1
  104. package/dest/storage/anchor_block_store/anchor_block_store.js +8 -1
  105. package/dest/storage/capsule_store/capsule_store.js +6 -8
  106. package/dest/storage/contract_store/contract_store.d.ts +42 -15
  107. package/dest/storage/contract_store/contract_store.d.ts.map +1 -1
  108. package/dest/storage/contract_store/contract_store.js +157 -72
  109. package/dest/storage/metadata.d.ts +1 -1
  110. package/dest/storage/metadata.js +1 -1
  111. package/dest/storage/note_store/note_store.d.ts +13 -3
  112. package/dest/storage/note_store/note_store.d.ts.map +1 -1
  113. package/dest/storage/note_store/note_store.js +147 -107
  114. package/dest/storage/private_event_store/private_event_store.d.ts +1 -1
  115. package/dest/storage/private_event_store/private_event_store.d.ts.map +1 -1
  116. package/dest/storage/private_event_store/private_event_store.js +84 -61
  117. package/dest/storage/private_event_store/stored_private_event.d.ts +4 -4
  118. package/dest/storage/private_event_store/stored_private_event.d.ts.map +1 -1
  119. package/dest/storage/private_event_store/stored_private_event.js +2 -2
  120. package/dest/storage/tagging_store/recipient_tagging_store.d.ts +6 -6
  121. package/dest/storage/tagging_store/recipient_tagging_store.d.ts.map +1 -1
  122. package/dest/storage/tagging_store/recipient_tagging_store.js +31 -19
  123. package/dest/storage/tagging_store/sender_address_book_store.d.ts +1 -1
  124. package/dest/storage/tagging_store/sender_address_book_store.d.ts.map +1 -1
  125. package/dest/storage/tagging_store/sender_address_book_store.js +20 -14
  126. package/dest/storage/tagging_store/sender_tagging_store.d.ts +5 -5
  127. package/dest/storage/tagging_store/sender_tagging_store.d.ts.map +1 -1
  128. package/dest/storage/tagging_store/sender_tagging_store.js +184 -114
  129. package/dest/tagging/get_all_logs_by_tags.d.ts +4 -4
  130. package/dest/tagging/get_all_logs_by_tags.d.ts.map +1 -1
  131. package/dest/tagging/get_all_logs_by_tags.js +17 -3
  132. package/dest/tagging/index.d.ts +2 -2
  133. package/dest/tagging/index.d.ts.map +1 -1
  134. package/dest/tagging/index.js +1 -1
  135. package/dest/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.d.ts +5 -6
  136. package/dest/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.d.ts.map +1 -1
  137. package/dest/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.js +7 -7
  138. package/dest/tagging/recipient_sync/utils/find_highest_indexes.js +2 -2
  139. package/dest/tagging/recipient_sync/utils/load_logs_for_range.d.ts +7 -8
  140. package/dest/tagging/recipient_sync/utils/load_logs_for_range.d.ts.map +1 -1
  141. package/dest/tagging/recipient_sync/utils/load_logs_for_range.js +12 -11
  142. package/dest/tagging/sender_sync/sync_sender_tagging_indexes.d.ts +5 -9
  143. package/dest/tagging/sender_sync/sync_sender_tagging_indexes.d.ts.map +1 -1
  144. package/dest/tagging/sender_sync/sync_sender_tagging_indexes.js +3 -6
  145. package/dest/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.d.ts +5 -8
  146. package/dest/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.d.ts.map +1 -1
  147. package/dest/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.js +14 -15
  148. package/package.json +25 -16
  149. package/src/access_scopes.ts +9 -0
  150. package/src/block_synchronizer/block_synchronizer.ts +23 -19
  151. package/src/config/package_info.ts +1 -1
  152. package/src/contract_function_simulator/contract_function_simulator.ts +323 -128
  153. package/src/contract_function_simulator/execution_tagging_index_cache.ts +5 -5
  154. package/src/contract_function_simulator/noir-structs/event_validation_request.ts +1 -1
  155. package/src/contract_function_simulator/noir-structs/note_validation_request.ts +1 -1
  156. package/src/contract_function_simulator/oracle/interfaces.ts +12 -12
  157. package/src/contract_function_simulator/oracle/oracle.ts +41 -24
  158. package/src/contract_function_simulator/oracle/private_execution_oracle.ts +100 -110
  159. package/src/contract_function_simulator/oracle/utility_execution_oracle.ts +139 -70
  160. package/src/contract_logging.ts +39 -0
  161. package/src/contract_sync/contract_sync_service.ts +152 -0
  162. package/src/contract_sync/{index.ts → helpers.ts} +21 -21
  163. package/src/debug/pxe_debug_utils.ts +63 -19
  164. package/src/entrypoints/client/bundle/index.ts +3 -0
  165. package/src/entrypoints/client/bundle/utils.ts +16 -15
  166. package/src/entrypoints/client/lazy/index.ts +3 -0
  167. package/src/entrypoints/client/lazy/utils.ts +17 -15
  168. package/src/entrypoints/pxe_creation_options.ts +2 -1
  169. package/src/entrypoints/server/index.ts +3 -1
  170. package/src/entrypoints/server/utils.ts +22 -26
  171. package/src/events/event_service.ts +4 -6
  172. package/src/job_coordinator/job_coordinator.ts +4 -3
  173. package/src/logs/log_service.ts +31 -38
  174. package/src/notes/note_service.ts +9 -10
  175. package/src/notes_filter.ts +26 -0
  176. package/src/oracle_version.ts +2 -2
  177. package/src/private_kernel/hints/{compute_tx_include_by_timestamp.ts → compute_tx_expiration_timestamp.ts} +13 -13
  178. package/src/private_kernel/hints/index.ts +1 -1
  179. package/src/private_kernel/hints/private_kernel_reset_private_inputs_builder.ts +164 -117
  180. package/src/private_kernel/hints/test_utils.ts +325 -0
  181. package/src/private_kernel/private_kernel_execution_prover.ts +25 -15
  182. package/src/private_kernel/private_kernel_oracle.ts +9 -9
  183. package/src/pxe.ts +191 -115
  184. package/src/storage/address_store/address_store.ts +15 -15
  185. package/src/storage/anchor_block_store/anchor_block_store.ts +8 -0
  186. package/src/storage/capsule_store/capsule_store.ts +8 -8
  187. package/src/storage/contract_store/contract_store.ts +186 -76
  188. package/src/storage/metadata.ts +1 -1
  189. package/src/storage/note_store/note_store.ts +169 -132
  190. package/src/storage/private_event_store/private_event_store.ts +102 -81
  191. package/src/storage/private_event_store/stored_private_event.ts +3 -3
  192. package/src/storage/tagging_store/recipient_tagging_store.ts +38 -24
  193. package/src/storage/tagging_store/sender_address_book_store.ts +20 -14
  194. package/src/storage/tagging_store/sender_tagging_store.ts +214 -130
  195. package/src/tagging/get_all_logs_by_tags.ts +31 -7
  196. package/src/tagging/index.ts +1 -1
  197. package/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts +9 -12
  198. package/src/tagging/recipient_sync/utils/find_highest_indexes.ts +2 -2
  199. package/src/tagging/recipient_sync/utils/load_logs_for_range.ts +12 -17
  200. package/src/tagging/sender_sync/sync_sender_tagging_indexes.ts +6 -11
  201. package/src/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.ts +14 -23
  202. package/dest/contract_sync/index.d.ts +0 -23
  203. package/dest/contract_sync/index.d.ts.map +0 -1
  204. package/dest/private_kernel/hints/compute_tx_include_by_timestamp.d.ts +0 -4
  205. package/dest/private_kernel/hints/compute_tx_include_by_timestamp.d.ts.map +0 -1
@@ -1,12 +1,12 @@
1
- import { toArray } from '@aztec/foundation/iterable';
2
1
  import { Semaphore } from '@aztec/foundation/queue';
3
2
  import type { Fr } from '@aztec/foundation/schemas';
4
3
  import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncMultiMap } from '@aztec/kv-store';
5
4
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
6
5
  import type { DataInBlock } from '@aztec/stdlib/block';
7
- import { NoteDao, NoteStatus, type NotesFilter } from '@aztec/stdlib/note';
6
+ import { NoteDao, NoteStatus } from '@aztec/stdlib/note';
8
7
 
9
8
  import type { StagedStore } from '../../job_coordinator/job_coordinator.js';
9
+ import type { NotesFilter } from '../../notes_filter.js';
10
10
  import { StoredNote } from './stored_note.js';
11
11
 
12
12
  /**
@@ -68,32 +68,26 @@ export class NoteStore implements StagedStore {
68
68
  * @param jobId - The job context for staged writes
69
69
  */
70
70
  public addNotes(notes: NoteDao[], scope: AztecAddress, jobId: string): Promise<void[]> {
71
- return this.#withJobLock(jobId, () => Promise.all(notes.map(noteDao => this.#addNote(noteDao, scope, jobId))));
72
- }
73
-
74
- async #addNote(note: NoteDao, scope: AztecAddress, jobId: string) {
75
- const noteForJob =
76
- (await this.#readNote(note.siloedNullifier.toString(), jobId)) ?? new StoredNote(note, new Set());
77
-
78
- // Make sure the note is linked to the scope and staged for this job
79
- noteForJob.addScope(scope.toString());
80
- this.#writeNote(noteForJob, jobId);
71
+ return this.#withJobLock(jobId, () =>
72
+ this.#store.transactionAsync(() =>
73
+ Promise.all(
74
+ notes.map(async note => {
75
+ const noteForJob =
76
+ (await this.#readNote(note.siloedNullifier.toString(), jobId)) ?? new StoredNote(note, new Set());
77
+ noteForJob.addScope(scope.toString());
78
+ this.#writeNote(noteForJob, jobId);
79
+ }),
80
+ ),
81
+ ),
82
+ );
81
83
  }
82
84
 
83
85
  async #readNote(nullifier: string, jobId: string): Promise<StoredNote | undefined> {
84
- // First check staged notes for this job
85
- const noteForJob = this.#getNotesForJob(jobId).get(nullifier);
86
- if (noteForJob) {
87
- return noteForJob;
88
- }
89
-
90
- // Then check persistent storage
86
+ // Always issue DB read to keep IndexedDB transaction alive (they auto-commit when a new micro-task starts and there
87
+ // are no pending read requests). The staged value still takes precedence if it exists.
91
88
  const noteBuffer = await this.#notes.getAsync(nullifier);
92
- if (noteBuffer) {
93
- return StoredNote.fromBuffer(noteBuffer);
94
- }
95
-
96
- return undefined;
89
+ const noteForJob = this.#getNotesForJob(jobId).get(nullifier);
90
+ return noteForJob ?? (noteBuffer ? StoredNote.fromBuffer(noteBuffer) : undefined);
97
91
  }
98
92
 
99
93
  #writeNote(note: StoredNote, jobId: string) {
@@ -110,59 +104,102 @@ export class NoteStore implements StagedStore {
110
104
  * @params jobId - the job context to read from.
111
105
  * @returns Filtered and deduplicated notes (a note might be present in multiple scopes - we ensure it is only
112
106
  * returned once if this is the case)
113
- * @throws If filtering by an empty scopes array. Scopes have to be set to undefined or to a non-empty array.
114
107
  */
115
- async getNotes(filter: NotesFilter, jobId: string): Promise<NoteDao[]> {
116
- if (filter.scopes !== undefined && filter.scopes.length === 0) {
117
- throw new Error('Trying to get notes with an empty scopes array');
108
+ getNotes(filter: NotesFilter, jobId: string): Promise<NoteDao[]> {
109
+ if (filter.scopes !== 'ALL_SCOPES' && filter.scopes.length === 0) {
110
+ return Promise.resolve([]);
118
111
  }
119
112
 
120
- const targetStatus = filter.status ?? NoteStatus.ACTIVE;
113
+ return this.#store.transactionAsync(async () => {
114
+ const targetStatus = filter.status ?? NoteStatus.ACTIVE;
115
+
116
+ // The code below might read a bit unnatural, the reason is that we need to be careful in how we use `await` inside
117
+ // `transactionAsync`, otherwise browsers might choose to auto-commit the IndexedDB transaction forcing us to
118
+ // explicitly handle that condition. The rule we need to honor is: do not await unless you generate a database
119
+ // read or write or you're done using the DB for the remainder of the transaction. The following sequence is
120
+ // unsafe in IndexedDB:
121
+ //
122
+ // 1. start transactionAsync()
123
+ // 2. await readDb() <-- OK, transaction alive because we issued DB ops
124
+ // 3. run a bunch of computations (no await involved) <-- OK, tx alive because we are in the same microtask
125
+ // 4. await doSthNotInDb() <-- no DB ops issued in this task, browser's free to decide to commit the tx
126
+ // 5. await readDb() <-- BOOM, TransactionInactiveError
127
+ //
128
+ // Note that the real issue is in step number 5: we try to continue using a transaction that the browser might
129
+ // have already committed.
130
+ //
131
+ // We need to read candidate notes which are either indexed by contract address in the DB (in
132
+ // #nullifiersByContractAddress), or lie in memory for the not yet committed `jobId`.
133
+ // So we collect promises based on both sources without awaiting for them.
134
+ const noteReadPromises: Map<string, Promise<StoredNote | undefined>> = new Map();
135
+
136
+ // Awaiting the getValuesAsync iterator is fine because it's reading from the DB
137
+ for await (const nullifier of this.#nullifiersByContractAddress.getValuesAsync(
138
+ filter.contractAddress.toString(),
139
+ )) {
140
+ // Each #readNote will perform a DB read
141
+ noteReadPromises.set(nullifier, this.#readNote(nullifier, jobId));
142
+ }
121
143
 
122
- const foundNotes: Map<string, NoteDao> = new Map();
144
+ // Add staged nullifiers from job, no awaits involved, so we are fine
145
+ for (const storedNote of this.#getNotesForJob(jobId).values()) {
146
+ if (storedNote.noteDao.contractAddress.equals(filter.contractAddress)) {
147
+ const nullifier = storedNote.noteDao.siloedNullifier.toString();
148
+ if (!noteReadPromises.has(nullifier)) {
149
+ noteReadPromises.set(nullifier, Promise.resolve(storedNote));
150
+ }
151
+ }
152
+ }
123
153
 
124
- const nullifiersOfContract = await this.#nullifiersOfContract(filter.contractAddress, jobId);
125
- for (const nullifier of nullifiersOfContract) {
126
- const note = await this.#readNote(nullifier, jobId);
154
+ // By now we have pending DB requests from all the #readNote calls. Await them all together.
155
+ const notes = await Promise.all(noteReadPromises.values());
127
156
 
128
- // Defensive: hitting this case means we're mishandling contract indices or in-memory job data
129
- if (!note) {
130
- throw new Error('PXE note database is corrupted.');
131
- }
157
+ // The rest of the function is await-free, and just deals with filtering and sorting our findings.
158
+ const foundNotes: Map<string, NoteDao> = new Map();
132
159
 
133
- // Apply filters
134
- if (targetStatus === NoteStatus.ACTIVE && note.isNullified()) {
135
- continue;
136
- }
160
+ for (const note of notes) {
161
+ // Defensive: hitting this case means we're mishandling contract indices or in-memory job data
162
+ if (!note) {
163
+ throw new Error('PXE note database is corrupted.');
164
+ }
137
165
 
138
- if (filter.owner && !note.noteDao.owner.equals(filter.owner)) {
139
- continue;
140
- }
166
+ // Apply filters
167
+ if (targetStatus === NoteStatus.ACTIVE && note.isNullified()) {
168
+ continue;
169
+ }
141
170
 
142
- if (filter.storageSlot && !note.noteDao.storageSlot.equals(filter.storageSlot)) {
143
- continue;
144
- }
171
+ if (filter.owner && !note.noteDao.owner.equals(filter.owner)) {
172
+ continue;
173
+ }
145
174
 
146
- if (filter.siloedNullifier && !note.noteDao.siloedNullifier.equals(filter.siloedNullifier)) {
147
- continue;
148
- }
175
+ if (filter.storageSlot && !note.noteDao.storageSlot.equals(filter.storageSlot)) {
176
+ continue;
177
+ }
149
178
 
150
- if (filter.scopes && note.scopes.intersection(new Set(filter.scopes.map(s => s.toString()))).size === 0) {
151
- continue;
152
- }
179
+ if (filter.siloedNullifier && !note.noteDao.siloedNullifier.equals(filter.siloedNullifier)) {
180
+ continue;
181
+ }
153
182
 
154
- foundNotes.set(note.noteDao.siloedNullifier.toString(), note.noteDao);
155
- }
183
+ if (
184
+ filter.scopes !== 'ALL_SCOPES' &&
185
+ note.scopes.intersection(new Set(filter.scopes.map(s => s.toString()))).size === 0
186
+ ) {
187
+ continue;
188
+ }
156
189
 
157
- // Sort by block number, then by tx index within block, then by note index within tx
158
- return [...foundNotes.values()].sort((a, b) => {
159
- if (a.l2BlockNumber !== b.l2BlockNumber) {
160
- return a.l2BlockNumber - b.l2BlockNumber;
161
- }
162
- if (a.txIndexInBlock !== b.txIndexInBlock) {
163
- return a.txIndexInBlock - b.txIndexInBlock;
190
+ foundNotes.set(note.noteDao.siloedNullifier.toString(), note.noteDao);
164
191
  }
165
- return a.noteIndexInTx - b.noteIndexInTx;
192
+
193
+ // Sort by block number, then by tx index within block, then by note index within tx
194
+ return [...foundNotes.values()].sort((a, b) => {
195
+ if (a.l2BlockNumber !== b.l2BlockNumber) {
196
+ return a.l2BlockNumber - b.l2BlockNumber;
197
+ }
198
+ if (a.txIndexInBlock !== b.txIndexInBlock) {
199
+ return a.txIndexInBlock - b.txIndexInBlock;
200
+ }
201
+ return a.noteIndexInTx - b.noteIndexInTx;
202
+ });
166
203
  });
167
204
  }
168
205
 
@@ -182,12 +219,16 @@ export class NoteStore implements StagedStore {
182
219
  * @throws Error if any nullifier is not found in this notes store
183
220
  */
184
221
  applyNullifiers(nullifiers: DataInBlock<Fr>[], jobId: string): Promise<NoteDao[]> {
222
+ if (nullifiers.length === 0) {
223
+ return Promise.resolve([]);
224
+ }
225
+
226
+ if (nullifiers.some(n => n.l2BlockNumber === 0)) {
227
+ return Promise.reject(new Error('applyNullifiers: nullifiers cannot have been emitted at block 0'));
228
+ }
229
+
185
230
  return this.#withJobLock(jobId, () =>
186
231
  this.#store.transactionAsync(async () => {
187
- if (nullifiers.length === 0) {
188
- return [];
189
- }
190
-
191
232
  const notesToNullify = await Promise.all(
192
233
  nullifiers.map(async nullifierInBlock => {
193
234
  const nullifier = nullifierInBlock.data.toString();
@@ -197,14 +238,13 @@ export class NoteStore implements StagedStore {
197
238
  throw new Error(`Attempted to mark a note as nullified which does not exist in PXE DB`);
198
239
  }
199
240
 
200
- return { storedNote: await this.#readNote(nullifier, jobId), blockNumber: nullifierInBlock.l2BlockNumber };
241
+ return { storedNote, blockNumber: nullifierInBlock.l2BlockNumber };
201
242
  }),
202
243
  );
203
244
 
204
245
  const notesNullifiedInThisCall: Map<string, NoteDao> = new Map();
205
246
  for (const noteToNullify of notesToNullify) {
206
- // Safe to coerce (!) because we throw if we find any undefined above
207
- const note = noteToNullify.storedNote!;
247
+ const note = noteToNullify.storedNote;
208
248
 
209
249
  // Skip already nullified notes
210
250
  if (note.isNullified()) {
@@ -249,18 +289,23 @@ export class NoteStore implements StagedStore {
249
289
  * @param blockNumber - Notes created after this block number will be deleted
250
290
  */
251
291
  async #deleteActiveNotesAfterBlock(blockNumber: number): Promise<void> {
252
- const notes = await toArray(this.#notes.valuesAsync());
253
- for (const noteBuffer of notes) {
292
+ // Collect notes to delete during iteration to keep IndexedDB transaction alive.
293
+ const notesToDelete: { nullifier: string; contractAddress: string }[] = [];
294
+ for await (const noteBuffer of this.#notes.valuesAsync()) {
254
295
  const storedNote = StoredNote.fromBuffer(noteBuffer);
255
296
  if (storedNote.noteDao.l2BlockNumber > blockNumber) {
256
- const noteNullifier = storedNote.noteDao.siloedNullifier.toString();
257
- await this.#notes.delete(noteNullifier);
258
- await this.#nullifiersByContractAddress.deleteValue(
259
- storedNote.noteDao.contractAddress.toString(),
260
- noteNullifier,
261
- );
297
+ notesToDelete.push({
298
+ nullifier: storedNote.noteDao.siloedNullifier.toString(),
299
+ contractAddress: storedNote.noteDao.contractAddress.toString(),
300
+ });
262
301
  }
263
302
  }
303
+
304
+ // Delete all collected notes. Each delete is a DB operation that keeps the transaction alive.
305
+ for (const { nullifier, contractAddress } of notesToDelete) {
306
+ await this.#notes.delete(nullifier);
307
+ await this.#nullifiersByContractAddress.deleteValue(contractAddress, nullifier);
308
+ }
264
309
  }
265
310
 
266
311
  /**
@@ -273,62 +318,69 @@ export class NoteStore implements StagedStore {
273
318
  * @param anchorBlockNumber - Upper bound for the block range to process
274
319
  */
275
320
  async #rewindNullifiedNotesAfterBlock(blockNumber: number, anchorBlockNumber: number): Promise<void> {
276
- const currentBlockNumber = blockNumber + 1;
277
- for (let i = currentBlockNumber; i <= anchorBlockNumber; i++) {
278
- const noteNullifiersToReinsert: string[] = await toArray(
279
- this.#nullifiersByNullificationBlockNumber.getValuesAsync(i),
280
- );
281
-
282
- const nullifiedNoteBuffers = await Promise.all(
283
- noteNullifiersToReinsert.map(async noteNullifier => {
284
- const note = await this.#notes.getAsync(noteNullifier);
285
-
286
- if (!note) {
287
- throw new Error(`PXE DB integrity error: no note found with nullifier ${noteNullifier}`);
288
- }
289
-
290
- return note;
291
- }),
292
- );
293
-
294
- const storedNotes = nullifiedNoteBuffers.map(buffer => StoredNote.fromBuffer(buffer));
321
+ // First pass: collect all nullifiers for all blocks, starting reads during iteration to keep tx alive.
322
+ const nullifiersByBlock: Map<number, { nullifier: string; noteReadPromise: Promise<Buffer | undefined> }[]> =
323
+ new Map();
324
+
325
+ for (let i = blockNumber + 1; i <= anchorBlockNumber; i++) {
326
+ const blockNullifiers: { nullifier: string; noteReadPromise: Promise<Buffer | undefined> }[] = [];
327
+ for await (const nullifier of this.#nullifiersByNullificationBlockNumber.getValuesAsync(i)) {
328
+ // Start read immediately during iteration to keep IndexedDB transaction alive
329
+ blockNullifiers.push({ nullifier, noteReadPromise: this.#notes.getAsync(nullifier) });
330
+ }
331
+ if (blockNullifiers.length > 0) {
332
+ nullifiersByBlock.set(i, blockNullifiers);
333
+ }
334
+ }
295
335
 
296
- for (const storedNote of storedNotes) {
297
- const noteNullifier = storedNote.noteDao.siloedNullifier.toString();
298
- const scopes = storedNote.scopes;
336
+ // Second pass: await reads and perform writes
337
+ for (const [block, nullifiers] of nullifiersByBlock) {
338
+ for (const { nullifier, noteReadPromise } of nullifiers) {
339
+ const noteBuffer = await noteReadPromise;
340
+ if (!noteBuffer) {
341
+ throw new Error(`PXE DB integrity error: no note found with nullifier ${nullifier}`);
342
+ }
299
343
 
300
- if (scopes.size === 0) {
301
- // We should never run into this error because notes always have a scope assigned to them - either on initial
302
- // insertion via `addNotes` or when removing their nullifiers.
303
- throw new Error(`No scopes found for nullified note with nullifier ${noteNullifier}`);
344
+ const storedNote = StoredNote.fromBuffer(noteBuffer);
345
+ if (storedNote.scopes.size === 0) {
346
+ throw new Error(`No scopes found for nullified note with nullifier ${nullifier}`);
304
347
  }
305
348
 
306
349
  storedNote.markAsActive();
307
350
 
308
351
  await Promise.all([
309
- this.#notes.set(noteNullifier, storedNote.toBuffer()),
310
- this.#nullifiersByNullificationBlockNumber.deleteValue(i, noteNullifier),
352
+ this.#notes.set(nullifier, storedNote.toBuffer()),
353
+ this.#nullifiersByNullificationBlockNumber.deleteValue(block, nullifier),
311
354
  ]);
312
355
  }
313
356
  }
314
357
  }
315
358
 
316
- commit(jobId: string): Promise<void> {
317
- return this.#withJobLock(jobId, async () => {
318
- for (const [nullifier, storedNote] of this.#getNotesForJob(jobId)) {
319
- await this.#notes.set(nullifier, storedNote.toBuffer());
320
- await this.#nullifiersByContractAddress.set(storedNote.noteDao.contractAddress.toString(), nullifier);
321
- if (storedNote.nullifiedAt !== undefined) {
322
- await this.#nullifiersByNullificationBlockNumber.set(storedNote.nullifiedAt, nullifier);
323
- }
359
+ /**
360
+ * Commits in memory job data to persistent storage.
361
+ *
362
+ * Called by JobCoordinator when a job completes successfully.
363
+ *
364
+ * Note: JobCoordinator wraps all commits in a single transaction, so we don't need our own transactionAsync here
365
+ * (and using one would throw on IndexedDB as it does not support nested txs).
366
+ *
367
+ * @param jobId - The jobId identifying which staged data to commit
368
+ */
369
+ async commit(jobId: string): Promise<void> {
370
+ for (const [nullifier, storedNote] of this.#getNotesForJob(jobId)) {
371
+ await this.#notes.set(nullifier, storedNote.toBuffer());
372
+ await this.#nullifiersByContractAddress.set(storedNote.noteDao.contractAddress.toString(), nullifier);
373
+ if (storedNote.nullifiedAt !== undefined) {
374
+ await this.#nullifiersByNullificationBlockNumber.set(storedNote.nullifiedAt, nullifier);
324
375
  }
376
+ }
325
377
 
326
- this.#clearJobData(jobId);
327
- });
378
+ this.#clearJobData(jobId);
328
379
  }
329
380
 
330
381
  discardStaged(jobId: string): Promise<void> {
331
- return this.#withJobLock(jobId, () => Promise.resolve(this.#clearJobData(jobId)));
382
+ this.#clearJobData(jobId);
383
+ return Promise.resolve();
332
384
  }
333
385
 
334
386
  #clearJobData(jobId: string) {
@@ -363,19 +415,4 @@ export class NoteStore implements StagedStore {
363
415
  }
364
416
  return notesForJob;
365
417
  }
366
-
367
- async #nullifiersOfContract(contractAddress: AztecAddress, jobId: string): Promise<Set<string>> {
368
- // Collect persisted nullifiers for this contract
369
- const persistedNullifiers: string[] = await toArray(
370
- this.#nullifiersByContractAddress.getValuesAsync(contractAddress.toString()),
371
- );
372
-
373
- // Collect staged nullifiers from the job where the note's contract matches
374
- const stagedNullifiers = this.#getNotesForJob(jobId)
375
- .values()
376
- .filter(storedNote => storedNote.noteDao.contractAddress.equals(contractAddress))
377
- .map(storedNote => storedNote.noteDao.siloedNullifier.toString());
378
-
379
- return new Set([...persistedNullifiers, ...stagedNullifiers]);
380
- }
381
418
  }
@@ -1,6 +1,5 @@
1
1
  import { BlockNumber } from '@aztec/foundation/branded-types';
2
2
  import { Fr } from '@aztec/foundation/curves/bn254';
3
- import { toArray } from '@aztec/foundation/iterable';
4
3
  import { createLogger } from '@aztec/foundation/log';
5
4
  import { Semaphore } from '@aztec/foundation/queue';
6
5
  import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncMultiMap } from '@aztec/kv-store';
@@ -81,43 +80,45 @@ export class PrivateEventStore implements StagedStore {
81
80
  metadata: PrivateEventMetadata,
82
81
  jobId: string,
83
82
  ) {
84
- return this.#withJobLock(jobId, async () => {
85
- const { contractAddress, scope, txHash, l2BlockNumber, l2BlockHash, txIndexInBlock, eventIndexInTx } = metadata;
86
- const eventId = siloedEventCommitment.toString();
87
-
88
- this.logger.verbose('storing private event log (job stage)', {
89
- eventId,
90
- contractAddress,
91
- scope,
92
- msgContent,
93
- l2BlockNumber,
94
- });
95
-
96
- const existing = await this.#readEvent(eventId, jobId);
83
+ return this.#withJobLock(jobId, () =>
84
+ this.#store.transactionAsync(async () => {
85
+ const { contractAddress, scope, txHash, l2BlockNumber, l2BlockHash, txIndexInBlock, eventIndexInTx } = metadata;
86
+ const eventId = siloedEventCommitment.toString();
97
87
 
98
- if (existing) {
99
- // If we already stored this event, we still want to make sure to track it for the given scope
100
- existing.addScope(scope.toString());
101
- this.#writeEvent(eventId, existing, jobId);
102
- } else {
103
- this.#writeEvent(
88
+ this.logger.verbose('storing private event log (job stage)', {
104
89
  eventId,
105
- new StoredPrivateEvent(
106
- randomness,
107
- msgContent,
108
- l2BlockNumber,
109
- l2BlockHash,
110
- txHash,
111
- txIndexInBlock,
112
- eventIndexInTx,
113
- contractAddress,
114
- eventSelector,
115
- new Set([scope.toString()]),
116
- ),
117
- jobId,
118
- );
119
- }
120
- });
90
+ contractAddress,
91
+ scope,
92
+ msgContent,
93
+ l2BlockNumber,
94
+ });
95
+
96
+ const existing = await this.#readEvent(eventId, jobId);
97
+
98
+ if (existing) {
99
+ // If we already stored this event, we still want to make sure to track it for the given scope
100
+ existing.addScope(scope.toString());
101
+ this.#writeEvent(eventId, existing, jobId);
102
+ } else {
103
+ this.#writeEvent(
104
+ eventId,
105
+ new StoredPrivateEvent(
106
+ randomness,
107
+ msgContent,
108
+ l2BlockNumber,
109
+ l2BlockHash,
110
+ txHash,
111
+ txIndexInBlock,
112
+ eventIndexInTx,
113
+ contractAddress,
114
+ eventSelector,
115
+ new Set([scope.toString()]),
116
+ ),
117
+ jobId,
118
+ );
119
+ }
120
+ }),
121
+ );
121
122
  }
122
123
 
123
124
  /**
@@ -136,6 +137,20 @@ export class PrivateEventStore implements StagedStore {
136
137
  filter: PrivateEventStoreFilter,
137
138
  ): Promise<PackedPrivateEvent[]> {
138
139
  return this.#store.transactionAsync(async () => {
140
+ const key = this.#keyFor(filter.contractAddress, eventSelector);
141
+ const targetScopes = new Set(filter.scopes.map(s => s.toString()));
142
+
143
+ // Map from eventId to the promise that reads the event buffer.
144
+ // We start reads during iteration to keep DB requests pending and avoid IndexedDB auto-commit.
145
+ const eventReadPromises: Map<string, Promise<Buffer | undefined>> = new Map();
146
+
147
+ for await (const eventId of this.#eventsByContractAndEventSelector.getValuesAsync(key)) {
148
+ eventReadPromises.set(eventId, this.#events.getAsync(eventId));
149
+ }
150
+
151
+ const eventIds = [...eventReadPromises.keys()];
152
+ const eventBuffers = await Promise.all(eventReadPromises.values());
153
+
139
154
  const events: Array<{
140
155
  l2BlockNumber: number;
141
156
  txIndexInBlock: number;
@@ -143,13 +158,9 @@ export class PrivateEventStore implements StagedStore {
143
158
  event: PackedPrivateEvent;
144
159
  }> = [];
145
160
 
146
- const key = this.#keyFor(filter.contractAddress, eventSelector);
147
- const targetScopes = new Set(filter.scopes.map(s => s.toString()));
148
-
149
- const eventIds: string[] = await toArray(this.#eventsByContractAndEventSelector.getValuesAsync(key));
150
-
151
- for (const eventId of eventIds) {
152
- const eventBuffer = await this.#events.getAsync(eventId);
161
+ for (let i = 0; i < eventIds.length; i++) {
162
+ const eventId = eventIds[i];
163
+ const eventBuffer = eventBuffers[i];
153
164
 
154
165
  // Defensive, if it happens, there's a problem with how we're handling #eventsByContractAndEventSelector
155
166
  if (!eventBuffer) {
@@ -223,29 +234,39 @@ export class PrivateEventStore implements StagedStore {
223
234
  * IMPORTANT: This method must be called within a transaction to ensure atomicity.
224
235
  */
225
236
  public async rollback(blockNumber: number, synchedBlockNumber: number): Promise<void> {
226
- let removedCount = 0;
237
+ // First pass: collect all event IDs for all blocks, starting reads during iteration to keep tx alive.
238
+ const eventsByBlock: Map<number, { eventId: string; eventReadPromise: Promise<Buffer | undefined> }[]> = new Map();
227
239
 
228
240
  for (let block = blockNumber + 1; block <= synchedBlockNumber; block++) {
229
- const eventIds: string[] = await toArray(this.#eventsByBlockNumber.getValuesAsync(block));
241
+ const blockEvents: { eventId: string; eventReadPromise: Promise<Buffer | undefined> }[] = [];
242
+ for await (const eventId of this.#eventsByBlockNumber.getValuesAsync(block)) {
243
+ // Start read immediately during iteration to keep IndexedDB transaction alive
244
+ blockEvents.push({ eventId, eventReadPromise: this.#events.getAsync(eventId) });
245
+ }
246
+ if (blockEvents.length > 0) {
247
+ eventsByBlock.set(block, blockEvents);
248
+ }
249
+ }
230
250
 
231
- if (eventIds.length > 0) {
232
- await this.#eventsByBlockNumber.delete(block);
251
+ // Second pass: await reads and perform deletes
252
+ let removedCount = 0;
253
+ for (const [block, events] of eventsByBlock) {
254
+ await this.#eventsByBlockNumber.delete(block);
233
255
 
234
- for (const eventId of eventIds) {
235
- const buffer = await this.#events.getAsync(eventId);
236
- if (!buffer) {
237
- throw new Error(`Event not found for eventId ${eventId}`);
238
- }
256
+ for (const { eventId, eventReadPromise } of events) {
257
+ const buffer = await eventReadPromise;
258
+ if (!buffer) {
259
+ throw new Error(`Event not found for eventId ${eventId}`);
260
+ }
239
261
 
240
- const entry = StoredPrivateEvent.fromBuffer(buffer);
241
- await this.#events.delete(eventId);
242
- await this.#eventsByContractAndEventSelector.deleteValue(
243
- this.#keyFor(entry.contractAddress, entry.eventSelector),
244
- eventId,
245
- );
262
+ const entry = StoredPrivateEvent.fromBuffer(buffer);
263
+ await this.#events.delete(eventId);
264
+ await this.#eventsByContractAndEventSelector.deleteValue(
265
+ this.#keyFor(entry.contractAddress, entry.eventSelector),
266
+ eventId,
267
+ );
246
268
 
247
- removedCount++;
248
- }
269
+ removedCount++;
249
270
  }
250
271
  }
251
272
 
@@ -262,28 +283,30 @@ export class PrivateEventStore implements StagedStore {
262
283
  *
263
284
  * @param jobId - The jobId identifying which staged data to commit
264
285
  */
265
- commit(jobId: string): Promise<void> {
266
- return this.#withJobLock(jobId, async () => {
267
- for (const [eventId, entry] of this.#getEventsForJob(jobId).entries()) {
268
- const lookupKey = this.#keyFor(entry.contractAddress, entry.eventSelector);
269
- this.logger.verbose('storing private event log', { eventId, lookupKey });
270
-
271
- await Promise.all([
272
- this.#events.set(eventId, entry.toBuffer()),
273
- this.#eventsByContractAndEventSelector.set(lookupKey, eventId),
274
- this.#eventsByBlockNumber.set(entry.l2BlockNumber, eventId),
275
- ]);
276
- }
286
+ async commit(jobId: string): Promise<void> {
287
+ // Note: Don't use #withJobLock here - commit runs within JobCoordinator's transactionAsync,
288
+ // and awaiting the lock would create a microtask boundary with no pending DB request,
289
+ // causing IndexedDB to auto-commit the transaction.
290
+ for (const [eventId, entry] of this.#getEventsForJob(jobId).entries()) {
291
+ const lookupKey = this.#keyFor(entry.contractAddress, entry.eventSelector);
292
+ this.logger.verbose('storing private event log', { eventId, lookupKey });
293
+
294
+ await Promise.all([
295
+ this.#events.set(eventId, entry.toBuffer()),
296
+ this.#eventsByContractAndEventSelector.set(lookupKey, eventId),
297
+ this.#eventsByBlockNumber.set(entry.l2BlockNumber, eventId),
298
+ ]);
299
+ }
277
300
 
278
- this.#clearJobData(jobId);
279
- });
301
+ this.#clearJobData(jobId);
280
302
  }
281
303
 
282
304
  /**
283
305
  * Discards in memory job data without persisting it.
284
306
  */
285
307
  discardStaged(jobId: string): Promise<void> {
286
- return this.#withJobLock(jobId, () => Promise.resolve(this.#clearJobData(jobId)));
308
+ this.#clearJobData(jobId);
309
+ return Promise.resolve();
287
310
  }
288
311
 
289
312
  /**
@@ -292,13 +315,11 @@ export class PrivateEventStore implements StagedStore {
292
315
  * Returns undefined if the event does not exist in the store overall.
293
316
  */
294
317
  async #readEvent(eventId: string, jobId: string): Promise<StoredPrivateEvent | undefined> {
295
- const eventForJob = this.#getEventsForJob(jobId).get(eventId);
296
- if (eventForJob) {
297
- return eventForJob;
298
- }
299
-
318
+ // Always issue DB read to keep IndexedDB transaction alive (they auto-commit when a new micro-task starts and there
319
+ // are no pending read requests). The staged value still takes precedence if it exists.
300
320
  const buffer = await this.#events.getAsync(eventId);
301
- return buffer ? StoredPrivateEvent.fromBuffer(buffer) : undefined;
321
+ const eventForJob = this.#getEventsForJob(jobId).get(eventId);
322
+ return eventForJob ?? (buffer ? StoredPrivateEvent.fromBuffer(buffer) : undefined);
302
323
  }
303
324
 
304
325
  /**