@aztec/pxe 0.0.1-commit.e6bd8901 → 0.0.1-commit.f146247c

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 (134) hide show
  1. package/dest/block_synchronizer/block_synchronizer.d.ts +3 -3
  2. package/dest/block_synchronizer/block_synchronizer.d.ts.map +1 -1
  3. package/dest/block_synchronizer/block_synchronizer.js +5 -5
  4. package/dest/contract_function_simulator/contract_function_simulator.d.ts +2 -4
  5. package/dest/contract_function_simulator/contract_function_simulator.d.ts.map +1 -1
  6. package/dest/contract_function_simulator/contract_function_simulator.js +7 -9
  7. package/dest/contract_function_simulator/oracle/interfaces.d.ts +8 -8
  8. package/dest/contract_function_simulator/oracle/interfaces.d.ts.map +1 -1
  9. package/dest/contract_function_simulator/oracle/oracle.d.ts +3 -3
  10. package/dest/contract_function_simulator/oracle/oracle.d.ts.map +1 -1
  11. package/dest/contract_function_simulator/oracle/oracle.js +15 -15
  12. package/dest/contract_function_simulator/oracle/private_execution_oracle.d.ts +2 -3
  13. package/dest/contract_function_simulator/oracle/private_execution_oracle.d.ts.map +1 -1
  14. package/dest/contract_function_simulator/oracle/private_execution_oracle.js +5 -7
  15. package/dest/contract_function_simulator/oracle/utility_execution_oracle.d.ts +20 -16
  16. package/dest/contract_function_simulator/oracle/utility_execution_oracle.d.ts.map +1 -1
  17. package/dest/contract_function_simulator/oracle/utility_execution_oracle.js +23 -19
  18. package/dest/contract_sync/index.d.ts +1 -1
  19. package/dest/contract_sync/index.d.ts.map +1 -1
  20. package/dest/contract_sync/index.js +1 -3
  21. package/dest/debug/pxe_debug_utils.d.ts +16 -6
  22. package/dest/debug/pxe_debug_utils.d.ts.map +1 -1
  23. package/dest/debug/pxe_debug_utils.js +17 -8
  24. package/dest/entrypoints/client/bundle/utils.d.ts +1 -1
  25. package/dest/entrypoints/client/bundle/utils.d.ts.map +1 -1
  26. package/dest/entrypoints/client/bundle/utils.js +12 -6
  27. package/dest/entrypoints/client/lazy/utils.d.ts +2 -2
  28. package/dest/entrypoints/client/lazy/utils.d.ts.map +1 -1
  29. package/dest/entrypoints/client/lazy/utils.js +13 -7
  30. package/dest/entrypoints/pxe_creation_options.d.ts +3 -2
  31. package/dest/entrypoints/pxe_creation_options.d.ts.map +1 -1
  32. package/dest/entrypoints/server/utils.d.ts +1 -1
  33. package/dest/entrypoints/server/utils.d.ts.map +1 -1
  34. package/dest/entrypoints/server/utils.js +19 -8
  35. package/dest/events/event_service.d.ts +4 -5
  36. package/dest/events/event_service.d.ts.map +1 -1
  37. package/dest/events/event_service.js +5 -6
  38. package/dest/job_coordinator/job_coordinator.d.ts +3 -2
  39. package/dest/job_coordinator/job_coordinator.d.ts.map +1 -1
  40. package/dest/job_coordinator/job_coordinator.js +3 -2
  41. package/dest/logs/log_service.d.ts +5 -4
  42. package/dest/logs/log_service.d.ts.map +1 -1
  43. package/dest/logs/log_service.js +8 -12
  44. package/dest/notes/note_service.d.ts +4 -5
  45. package/dest/notes/note_service.d.ts.map +1 -1
  46. package/dest/notes/note_service.js +6 -8
  47. package/dest/oracle_version.d.ts +3 -3
  48. package/dest/oracle_version.d.ts.map +1 -1
  49. package/dest/oracle_version.js +2 -2
  50. package/dest/private_kernel/private_kernel_execution_prover.d.ts +3 -2
  51. package/dest/private_kernel/private_kernel_execution_prover.d.ts.map +1 -1
  52. package/dest/private_kernel/private_kernel_execution_prover.js +2 -2
  53. package/dest/private_kernel/private_kernel_oracle.d.ts +3 -3
  54. package/dest/private_kernel/private_kernel_oracle.d.ts.map +1 -1
  55. package/dest/pxe.d.ts +1 -1
  56. package/dest/pxe.d.ts.map +1 -1
  57. package/dest/pxe.js +9 -8
  58. package/dest/storage/address_store/address_store.d.ts +1 -1
  59. package/dest/storage/address_store/address_store.d.ts.map +1 -1
  60. package/dest/storage/address_store/address_store.js +12 -11
  61. package/dest/storage/anchor_block_store/anchor_block_store.d.ts +9 -1
  62. package/dest/storage/anchor_block_store/anchor_block_store.d.ts.map +1 -1
  63. package/dest/storage/anchor_block_store/anchor_block_store.js +8 -1
  64. package/dest/storage/capsule_store/capsule_store.js +6 -8
  65. package/dest/storage/contract_store/contract_store.d.ts +1 -1
  66. package/dest/storage/contract_store/contract_store.d.ts.map +1 -1
  67. package/dest/storage/contract_store/contract_store.js +22 -13
  68. package/dest/storage/metadata.d.ts +1 -1
  69. package/dest/storage/metadata.js +1 -1
  70. package/dest/storage/note_store/note_store.d.ts +11 -1
  71. package/dest/storage/note_store/note_store.d.ts.map +1 -1
  72. package/dest/storage/note_store/note_store.js +143 -105
  73. package/dest/storage/private_event_store/private_event_store.d.ts +1 -1
  74. package/dest/storage/private_event_store/private_event_store.d.ts.map +1 -1
  75. package/dest/storage/private_event_store/private_event_store.js +84 -61
  76. package/dest/storage/private_event_store/stored_private_event.d.ts +4 -4
  77. package/dest/storage/private_event_store/stored_private_event.d.ts.map +1 -1
  78. package/dest/storage/private_event_store/stored_private_event.js +2 -2
  79. package/dest/storage/tagging_store/recipient_tagging_store.d.ts +1 -1
  80. package/dest/storage/tagging_store/recipient_tagging_store.d.ts.map +1 -1
  81. package/dest/storage/tagging_store/recipient_tagging_store.js +31 -19
  82. package/dest/storage/tagging_store/sender_address_book_store.d.ts +1 -1
  83. package/dest/storage/tagging_store/sender_address_book_store.d.ts.map +1 -1
  84. package/dest/storage/tagging_store/sender_address_book_store.js +20 -14
  85. package/dest/storage/tagging_store/sender_tagging_store.d.ts +1 -1
  86. package/dest/storage/tagging_store/sender_tagging_store.d.ts.map +1 -1
  87. package/dest/storage/tagging_store/sender_tagging_store.js +183 -113
  88. package/dest/tagging/get_all_logs_by_tags.d.ts +4 -4
  89. package/dest/tagging/get_all_logs_by_tags.d.ts.map +1 -1
  90. package/dest/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.d.ts +3 -3
  91. package/dest/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.d.ts.map +1 -1
  92. package/dest/tagging/recipient_sync/utils/load_logs_for_range.d.ts +3 -3
  93. package/dest/tagging/recipient_sync/utils/load_logs_for_range.d.ts.map +1 -1
  94. package/dest/tagging/sender_sync/sync_sender_tagging_indexes.d.ts +3 -3
  95. package/dest/tagging/sender_sync/sync_sender_tagging_indexes.d.ts.map +1 -1
  96. package/dest/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.d.ts +3 -3
  97. package/dest/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.d.ts.map +1 -1
  98. package/package.json +16 -16
  99. package/src/block_synchronizer/block_synchronizer.ts +17 -19
  100. package/src/contract_function_simulator/contract_function_simulator.ts +6 -6
  101. package/src/contract_function_simulator/oracle/interfaces.ts +10 -10
  102. package/src/contract_function_simulator/oracle/oracle.ts +27 -17
  103. package/src/contract_function_simulator/oracle/private_execution_oracle.ts +2 -8
  104. package/src/contract_function_simulator/oracle/utility_execution_oracle.ts +32 -26
  105. package/src/contract_sync/index.ts +1 -3
  106. package/src/debug/pxe_debug_utils.ts +23 -9
  107. package/src/entrypoints/client/bundle/utils.ts +7 -14
  108. package/src/entrypoints/client/lazy/utils.ts +8 -14
  109. package/src/entrypoints/pxe_creation_options.ts +2 -1
  110. package/src/entrypoints/server/utils.ts +15 -19
  111. package/src/events/event_service.ts +4 -6
  112. package/src/job_coordinator/job_coordinator.ts +4 -3
  113. package/src/logs/log_service.ts +12 -13
  114. package/src/notes/note_service.ts +5 -8
  115. package/src/oracle_version.ts +2 -2
  116. package/src/private_kernel/private_kernel_execution_prover.ts +6 -3
  117. package/src/private_kernel/private_kernel_oracle.ts +2 -2
  118. package/src/pxe.ts +16 -9
  119. package/src/storage/address_store/address_store.ts +15 -15
  120. package/src/storage/anchor_block_store/anchor_block_store.ts +8 -0
  121. package/src/storage/capsule_store/capsule_store.ts +8 -8
  122. package/src/storage/contract_store/contract_store.ts +22 -11
  123. package/src/storage/metadata.ts +1 -1
  124. package/src/storage/note_store/note_store.ts +159 -129
  125. package/src/storage/private_event_store/private_event_store.ts +102 -81
  126. package/src/storage/private_event_store/stored_private_event.ts +3 -3
  127. package/src/storage/tagging_store/recipient_tagging_store.ts +31 -21
  128. package/src/storage/tagging_store/sender_address_book_store.ts +20 -14
  129. package/src/storage/tagging_store/sender_tagging_store.ts +210 -126
  130. package/src/tagging/get_all_logs_by_tags.ts +3 -3
  131. package/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts +2 -2
  132. package/src/tagging/recipient_sync/utils/load_logs_for_range.ts +2 -2
  133. package/src/tagging/sender_sync/sync_sender_tagging_indexes.ts +2 -2
  134. package/src/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.ts +3 -3
@@ -10,6 +10,14 @@ export class AnchorBlockStore {
10
10
  this.#synchronizedHeader = this.#store.openSingleton('header');
11
11
  }
12
12
 
13
+ /**
14
+ * Sets the currently synchronized block
15
+ *
16
+ * Important: this method is only called from BlockSynchronizer, and since we need it to run atomically with other
17
+ * stores in the case of a reorg, it MUST NOT be wrapped in a `transactionAsync` call. Doing so would result in a
18
+ * deadlock when the backend is IndexedDB, because `transactionAsync` is not designed to support reentrancy.
19
+ *
20
+ */
13
21
  async setHeader(header: BlockHeader): Promise<void> {
14
22
  await this.#synchronizedHeader.set(header.toBuffer());
15
23
  }
@@ -57,14 +57,14 @@ export class CapsuleStore implements StagedStore {
57
57
  */
58
58
  async #getFromStage(jobId: string, dbSlotKey: string): Promise<Buffer | null | undefined> {
59
59
  const jobStagedCapsules = this.#getJobStagedCapsules(jobId);
60
- let staged: Buffer | null | undefined = jobStagedCapsules.get(dbSlotKey);
61
- // Note that if staged === null, we marked it for deletion, so we don't want to
62
- // re-read it from DB
63
- if (staged === undefined) {
64
- // If we don't have a staged version of this dbSlotKey, first we check if there's one in DB
65
- staged = await this.#loadCapsuleFromDb(dbSlotKey);
66
- }
67
- return staged;
60
+ const staged: Buffer | null | undefined = jobStagedCapsules.get(dbSlotKey);
61
+
62
+ // Always issue DB read to keep IndexedDB transaction alive, even if the value is in the job staged data. This
63
+ // keeps IndexedDB transactions alive (they auto-commit when a new micro-task starts and there are no pending read
64
+ // requests). The staged value still takes precedence if it exists (including null for deletions).
65
+ const dbValue = await this.#loadCapsuleFromDb(dbSlotKey);
66
+
67
+ return staged !== undefined ? staged : dbValue;
68
68
  }
69
69
 
70
70
  /**
@@ -42,10 +42,12 @@ export class ContractStore {
42
42
  /** Map from contract address to contract class id */
43
43
  #contractClassIdMap: Map<string, Fr> = new Map();
44
44
 
45
+ #store: AztecAsyncKVStore;
45
46
  #contractArtifacts: AztecAsyncMap<string, Buffer>;
46
47
  #contractInstances: AztecAsyncMap<string, Buffer>;
47
48
 
48
49
  constructor(store: AztecAsyncKVStore) {
50
+ this.#store = store;
49
51
  this.#contractArtifacts = store.openMap('contract_artifacts');
50
52
  this.#contractInstances = store.openMap('contracts_instances');
51
53
  }
@@ -53,6 +55,7 @@ export class ContractStore {
53
55
  // Setters
54
56
 
55
57
  public async addContractArtifact(id: Fr, contract: ContractArtifact): Promise<void> {
58
+ // Validation outside transactionAsync - these are not DB operations
56
59
  const privateFunctions = contract.functions.filter(
57
60
  functionArtifact => functionArtifact.functionType === FunctionType.PRIVATE,
58
61
  );
@@ -69,7 +72,9 @@ export class ContractStore {
69
72
  throw new Error('Repeated function selectors of private functions');
70
73
  }
71
74
 
72
- await this.#contractArtifacts.set(id.toString(), contractArtifactToBuffer(contract));
75
+ await this.#store.transactionAsync(() =>
76
+ this.#contractArtifacts.set(id.toString(), contractArtifactToBuffer(contract)),
77
+ );
73
78
  }
74
79
 
75
80
  async addContractInstance(contract: ContractInstanceWithAddress): Promise<void> {
@@ -123,21 +128,27 @@ export class ContractStore {
123
128
 
124
129
  // Public getters
125
130
 
126
- async getContractsAddresses(): Promise<AztecAddress[]> {
127
- const keys = await toArray(this.#contractInstances.keysAsync());
128
- return keys.map(AztecAddress.fromString);
131
+ getContractsAddresses(): Promise<AztecAddress[]> {
132
+ return this.#store.transactionAsync(async () => {
133
+ const keys = await toArray(this.#contractInstances.keysAsync());
134
+ return keys.map(AztecAddress.fromString);
135
+ });
129
136
  }
130
137
 
131
138
  /** Returns a contract instance for a given address. Throws if not found. */
132
- public async getContractInstance(contractAddress: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
133
- const contract = await this.#contractInstances.getAsync(contractAddress.toString());
134
- return contract && SerializableContractInstance.fromBuffer(contract).withAddress(contractAddress);
139
+ public getContractInstance(contractAddress: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
140
+ return this.#store.transactionAsync(async () => {
141
+ const contract = await this.#contractInstances.getAsync(contractAddress.toString());
142
+ return contract && SerializableContractInstance.fromBuffer(contract).withAddress(contractAddress);
143
+ });
135
144
  }
136
145
 
137
- public async getContractArtifact(contractClassId: Fr): Promise<ContractArtifact | undefined> {
138
- const contract = await this.#contractArtifacts.getAsync(contractClassId.toString());
139
- // TODO(@spalladino): AztecAsyncMap lies and returns Uint8Arrays instead of Buffers, hence the extra Buffer.from.
140
- return contract && contractArtifactFromBuffer(Buffer.from(contract));
146
+ public getContractArtifact(contractClassId: Fr): Promise<ContractArtifact | undefined> {
147
+ return this.#store.transactionAsync(async () => {
148
+ const contract = await this.#contractArtifacts.getAsync(contractClassId.toString());
149
+ // TODO(@spalladino): AztecAsyncMap lies and returns Uint8Arrays instead of Buffers, hence the extra Buffer.from.
150
+ return contract && contractArtifactFromBuffer(Buffer.from(contract));
151
+ });
141
152
  }
142
153
 
143
154
  /** Returns a contract class for a given class id. Throws if not found. */
@@ -1 +1 @@
1
- export const PXE_DATA_SCHEMA_VERSION = 2;
1
+ export const PXE_DATA_SCHEMA_VERSION = 3;
@@ -1,4 +1,3 @@
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';
@@ -68,32 +67,26 @@ export class NoteStore implements StagedStore {
68
67
  * @param jobId - The job context for staged writes
69
68
  */
70
69
  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);
70
+ return this.#withJobLock(jobId, () =>
71
+ this.#store.transactionAsync(() =>
72
+ Promise.all(
73
+ notes.map(async note => {
74
+ const noteForJob =
75
+ (await this.#readNote(note.siloedNullifier.toString(), jobId)) ?? new StoredNote(note, new Set());
76
+ noteForJob.addScope(scope.toString());
77
+ this.#writeNote(noteForJob, jobId);
78
+ }),
79
+ ),
80
+ ),
81
+ );
81
82
  }
82
83
 
83
84
  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
85
+ // Always issue DB read to keep IndexedDB transaction alive (they auto-commit when a new micro-task starts and there
86
+ // are no pending read requests). The staged value still takes precedence if it exists.
91
87
  const noteBuffer = await this.#notes.getAsync(nullifier);
92
- if (noteBuffer) {
93
- return StoredNote.fromBuffer(noteBuffer);
94
- }
95
-
96
- return undefined;
88
+ const noteForJob = this.#getNotesForJob(jobId).get(nullifier);
89
+ return noteForJob ?? (noteBuffer ? StoredNote.fromBuffer(noteBuffer) : undefined);
97
90
  }
98
91
 
99
92
  #writeNote(note: StoredNote, jobId: string) {
@@ -112,57 +105,98 @@ export class NoteStore implements StagedStore {
112
105
  * returned once if this is the case)
113
106
  * @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[]> {
108
+ getNotes(filter: NotesFilter, jobId: string): Promise<NoteDao[]> {
116
109
  if (filter.scopes !== undefined && filter.scopes.length === 0) {
117
- throw new Error('Trying to get notes with an empty scopes array');
110
+ return Promise.reject(new Error('Trying to get notes with an empty scopes array'));
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 (filter.scopes && note.scopes.intersection(new Set(filter.scopes.map(s => s.toString()))).size === 0) {
184
+ continue;
185
+ }
156
186
 
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;
187
+ foundNotes.set(note.noteDao.siloedNullifier.toString(), note.noteDao);
164
188
  }
165
- return a.noteIndexInTx - b.noteIndexInTx;
189
+
190
+ // Sort by block number, then by tx index within block, then by note index within tx
191
+ return [...foundNotes.values()].sort((a, b) => {
192
+ if (a.l2BlockNumber !== b.l2BlockNumber) {
193
+ return a.l2BlockNumber - b.l2BlockNumber;
194
+ }
195
+ if (a.txIndexInBlock !== b.txIndexInBlock) {
196
+ return a.txIndexInBlock - b.txIndexInBlock;
197
+ }
198
+ return a.noteIndexInTx - b.noteIndexInTx;
199
+ });
166
200
  });
167
201
  }
168
202
 
@@ -182,12 +216,12 @@ export class NoteStore implements StagedStore {
182
216
  * @throws Error if any nullifier is not found in this notes store
183
217
  */
184
218
  applyNullifiers(nullifiers: DataInBlock<Fr>[], jobId: string): Promise<NoteDao[]> {
219
+ if (nullifiers.length === 0) {
220
+ return Promise.resolve([]);
221
+ }
222
+
185
223
  return this.#withJobLock(jobId, () =>
186
224
  this.#store.transactionAsync(async () => {
187
- if (nullifiers.length === 0) {
188
- return [];
189
- }
190
-
191
225
  const notesToNullify = await Promise.all(
192
226
  nullifiers.map(async nullifierInBlock => {
193
227
  const nullifier = nullifierInBlock.data.toString();
@@ -197,14 +231,13 @@ export class NoteStore implements StagedStore {
197
231
  throw new Error(`Attempted to mark a note as nullified which does not exist in PXE DB`);
198
232
  }
199
233
 
200
- return { storedNote: await this.#readNote(nullifier, jobId), blockNumber: nullifierInBlock.l2BlockNumber };
234
+ return { storedNote, blockNumber: nullifierInBlock.l2BlockNumber };
201
235
  }),
202
236
  );
203
237
 
204
238
  const notesNullifiedInThisCall: Map<string, NoteDao> = new Map();
205
239
  for (const noteToNullify of notesToNullify) {
206
- // Safe to coerce (!) because we throw if we find any undefined above
207
- const note = noteToNullify.storedNote!;
240
+ const note = noteToNullify.storedNote;
208
241
 
209
242
  // Skip already nullified notes
210
243
  if (note.isNullified()) {
@@ -249,18 +282,23 @@ export class NoteStore implements StagedStore {
249
282
  * @param blockNumber - Notes created after this block number will be deleted
250
283
  */
251
284
  async #deleteActiveNotesAfterBlock(blockNumber: number): Promise<void> {
252
- const notes = await toArray(this.#notes.valuesAsync());
253
- for (const noteBuffer of notes) {
285
+ // Collect notes to delete during iteration to keep IndexedDB transaction alive.
286
+ const notesToDelete: { nullifier: string; contractAddress: string }[] = [];
287
+ for await (const noteBuffer of this.#notes.valuesAsync()) {
254
288
  const storedNote = StoredNote.fromBuffer(noteBuffer);
255
289
  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
- );
290
+ notesToDelete.push({
291
+ nullifier: storedNote.noteDao.siloedNullifier.toString(),
292
+ contractAddress: storedNote.noteDao.contractAddress.toString(),
293
+ });
262
294
  }
263
295
  }
296
+
297
+ // Delete all collected notes. Each delete is a DB operation that keeps the transaction alive.
298
+ for (const { nullifier, contractAddress } of notesToDelete) {
299
+ await this.#notes.delete(nullifier);
300
+ await this.#nullifiersByContractAddress.deleteValue(contractAddress, nullifier);
301
+ }
264
302
  }
265
303
 
266
304
  /**
@@ -273,62 +311,69 @@ export class NoteStore implements StagedStore {
273
311
  * @param anchorBlockNumber - Upper bound for the block range to process
274
312
  */
275
313
  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));
314
+ // First pass: collect all nullifiers for all blocks, starting reads during iteration to keep tx alive.
315
+ const nullifiersByBlock: Map<number, { nullifier: string; noteReadPromise: Promise<Buffer | undefined> }[]> =
316
+ new Map();
317
+
318
+ for (let i = blockNumber + 1; i <= anchorBlockNumber; i++) {
319
+ const blockNullifiers: { nullifier: string; noteReadPromise: Promise<Buffer | undefined> }[] = [];
320
+ for await (const nullifier of this.#nullifiersByNullificationBlockNumber.getValuesAsync(i)) {
321
+ // Start read immediately during iteration to keep IndexedDB transaction alive
322
+ blockNullifiers.push({ nullifier, noteReadPromise: this.#notes.getAsync(nullifier) });
323
+ }
324
+ if (blockNullifiers.length > 0) {
325
+ nullifiersByBlock.set(i, blockNullifiers);
326
+ }
327
+ }
295
328
 
296
- for (const storedNote of storedNotes) {
297
- const noteNullifier = storedNote.noteDao.siloedNullifier.toString();
298
- const scopes = storedNote.scopes;
329
+ // Second pass: await reads and perform writes
330
+ for (const [block, nullifiers] of nullifiersByBlock) {
331
+ for (const { nullifier, noteReadPromise } of nullifiers) {
332
+ const noteBuffer = await noteReadPromise;
333
+ if (!noteBuffer) {
334
+ throw new Error(`PXE DB integrity error: no note found with nullifier ${nullifier}`);
335
+ }
299
336
 
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}`);
337
+ const storedNote = StoredNote.fromBuffer(noteBuffer);
338
+ if (storedNote.scopes.size === 0) {
339
+ throw new Error(`No scopes found for nullified note with nullifier ${nullifier}`);
304
340
  }
305
341
 
306
342
  storedNote.markAsActive();
307
343
 
308
344
  await Promise.all([
309
- this.#notes.set(noteNullifier, storedNote.toBuffer()),
310
- this.#nullifiersByNullificationBlockNumber.deleteValue(i, noteNullifier),
345
+ this.#notes.set(nullifier, storedNote.toBuffer()),
346
+ this.#nullifiersByNullificationBlockNumber.deleteValue(block, nullifier),
311
347
  ]);
312
348
  }
313
349
  }
314
350
  }
315
351
 
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
- }
352
+ /**
353
+ * Commits in memory job data to persistent storage.
354
+ *
355
+ * Called by JobCoordinator when a job completes successfully.
356
+ *
357
+ * Note: JobCoordinator wraps all commits in a single transaction, so we don't need our own transactionAsync here
358
+ * (and using one would throw on IndexedDB as it does not support nested txs).
359
+ *
360
+ * @param jobId - The jobId identifying which staged data to commit
361
+ */
362
+ async commit(jobId: string): Promise<void> {
363
+ for (const [nullifier, storedNote] of this.#getNotesForJob(jobId)) {
364
+ await this.#notes.set(nullifier, storedNote.toBuffer());
365
+ await this.#nullifiersByContractAddress.set(storedNote.noteDao.contractAddress.toString(), nullifier);
366
+ if (storedNote.nullifiedAt !== undefined) {
367
+ await this.#nullifiersByNullificationBlockNumber.set(storedNote.nullifiedAt, nullifier);
324
368
  }
369
+ }
325
370
 
326
- this.#clearJobData(jobId);
327
- });
371
+ this.#clearJobData(jobId);
328
372
  }
329
373
 
330
374
  discardStaged(jobId: string): Promise<void> {
331
- return this.#withJobLock(jobId, () => Promise.resolve(this.#clearJobData(jobId)));
375
+ this.#clearJobData(jobId);
376
+ return Promise.resolve();
332
377
  }
333
378
 
334
379
  #clearJobData(jobId: string) {
@@ -363,19 +408,4 @@ export class NoteStore implements StagedStore {
363
408
  }
364
409
  return notesForJob;
365
410
  }
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
411
  }