@aztec/pxe 0.0.1-commit.321f6a9 → 0.0.1-commit.330febf

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 (124) hide show
  1. package/dest/bin/check_oracle_version.js +2 -38
  2. package/dest/bin/index.d.ts +2 -2
  3. package/dest/bin/index.d.ts.map +1 -1
  4. package/dest/bin/index.js +1 -1
  5. package/dest/bin/oracle_version_helpers.d.ts +1 -15
  6. package/dest/bin/oracle_version_helpers.d.ts.map +1 -1
  7. package/dest/bin/oracle_version_helpers.js +0 -21
  8. package/dest/block_synchronizer/block_synchronizer.d.ts +3 -5
  9. package/dest/block_synchronizer/block_synchronizer.d.ts.map +1 -1
  10. package/dest/block_synchronizer/block_synchronizer.js +5 -18
  11. package/dest/contract/contract_class_service.d.ts +1 -8
  12. package/dest/contract/contract_class_service.d.ts.map +1 -1
  13. package/dest/contract/contract_class_service.js +8 -40
  14. package/dest/contract_function_simulator/contract_function_simulator.d.ts +1 -8
  15. package/dest/contract_function_simulator/contract_function_simulator.d.ts.map +1 -1
  16. package/dest/contract_function_simulator/contract_function_simulator.js +0 -17
  17. package/dest/contract_function_simulator/oracle/oracle_type_mappings.d.ts +1 -1
  18. package/dest/contract_function_simulator/oracle/oracle_type_mappings.d.ts.map +1 -1
  19. package/dest/contract_function_simulator/oracle/oracle_type_mappings.js +11 -1
  20. package/dest/contract_function_simulator/oracle/private_execution_oracle.d.ts +1 -1
  21. package/dest/contract_function_simulator/oracle/private_execution_oracle.d.ts.map +1 -1
  22. package/dest/contract_function_simulator/oracle/private_execution_oracle.js +6 -2
  23. package/dest/contract_function_simulator/oracle/utility_execution_oracle.d.ts +10 -2
  24. package/dest/contract_function_simulator/oracle/utility_execution_oracle.d.ts.map +1 -1
  25. package/dest/contract_function_simulator/oracle/utility_execution_oracle.js +128 -38
  26. package/dest/events/event_service.d.ts +13 -6
  27. package/dest/events/event_service.d.ts.map +1 -1
  28. package/dest/events/event_service.js +13 -13
  29. package/dest/logs/log_service.d.ts +13 -5
  30. package/dest/logs/log_service.d.ts.map +1 -1
  31. package/dest/logs/log_service.js +20 -36
  32. package/dest/messages/tx_resolver_service.d.ts +18 -7
  33. package/dest/messages/tx_resolver_service.d.ts.map +1 -1
  34. package/dest/messages/tx_resolver_service.js +9 -8
  35. package/dest/node/benchmarked_node.d.ts +20 -0
  36. package/dest/node/benchmarked_node.d.ts.map +1 -0
  37. package/dest/node/benchmarked_node.js +94 -0
  38. package/dest/node/caching_aztec_node.d.ts +24 -0
  39. package/dest/node/caching_aztec_node.d.ts.map +1 -0
  40. package/dest/node/caching_aztec_node.js +267 -0
  41. package/dest/notes/note_service.d.ts +12 -6
  42. package/dest/notes/note_service.d.ts.map +1 -1
  43. package/dest/notes/note_service.js +9 -9
  44. package/dest/oracle_version.js +2 -2
  45. package/dest/pxe.d.ts +1 -1
  46. package/dest/pxe.d.ts.map +1 -1
  47. package/dest/pxe.js +31 -14
  48. package/dest/storage/contract_store/contract_store.d.ts +1 -1
  49. package/dest/storage/contract_store/contract_store.d.ts.map +1 -1
  50. package/dest/storage/contract_store/contract_store.js +2 -2
  51. package/dest/storage/fact_store/origin_state.d.ts +1 -1
  52. package/dest/storage/fact_store/origin_state.js +1 -1
  53. package/dest/storage/tagging_store/sender_tagging_store.d.ts +23 -8
  54. package/dest/storage/tagging_store/sender_tagging_store.d.ts.map +1 -1
  55. package/dest/storage/tagging_store/sender_tagging_store.js +33 -18
  56. package/dest/tagging/constants.d.ts +9 -1
  57. package/dest/tagging/constants.d.ts.map +1 -1
  58. package/dest/tagging/constants.js +10 -0
  59. package/dest/tagging/get_all_logs_by_tags.d.ts +29 -8
  60. package/dest/tagging/get_all_logs_by_tags.d.ts.map +1 -1
  61. package/dest/tagging/get_all_logs_by_tags.js +29 -10
  62. package/dest/tagging/index.d.ts +2 -2
  63. package/dest/tagging/index.d.ts.map +1 -1
  64. package/dest/tagging/index.js +1 -1
  65. package/dest/tagging/recipient_sync/sync_tagged_private_logs.d.ts +2 -2
  66. package/dest/tagging/recipient_sync/sync_tagged_private_logs.d.ts.map +1 -1
  67. package/dest/tagging/recipient_sync/sync_tagged_private_logs.js +13 -17
  68. package/dest/tagging/sender_sync/sync_sender_tagging_indexes.d.ts +6 -3
  69. package/dest/tagging/sender_sync/sync_sender_tagging_indexes.d.ts.map +1 -1
  70. package/dest/tagging/sender_sync/sync_sender_tagging_indexes.js +23 -38
  71. package/dest/tagging/sender_sync/utils/classify_pending_txs.d.ts +22 -0
  72. package/dest/tagging/sender_sync/utils/classify_pending_txs.d.ts.map +1 -0
  73. package/dest/tagging/sender_sync/utils/classify_pending_txs.js +61 -0
  74. package/dest/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.d.ts +15 -5
  75. package/dest/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.d.ts.map +1 -1
  76. package/dest/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.js +30 -33
  77. package/dest/tagging/sender_sync/utils/resolve_pending_txs.d.ts +29 -0
  78. package/dest/tagging/sender_sync/utils/resolve_pending_txs.d.ts.map +1 -0
  79. package/dest/tagging/sender_sync/utils/resolve_pending_txs.js +41 -0
  80. package/dest/tagging/sender_sync/utils/test_utils.d.ts +16 -0
  81. package/dest/tagging/sender_sync/utils/test_utils.d.ts.map +1 -0
  82. package/dest/tagging/sender_sync/utils/test_utils.js +24 -0
  83. package/package.json +17 -17
  84. package/src/bin/check_oracle_version.ts +2 -46
  85. package/src/bin/index.ts +1 -1
  86. package/src/bin/oracle_version_helpers.ts +0 -24
  87. package/src/block_synchronizer/block_synchronizer.ts +6 -23
  88. package/src/contract/contract_class_service.ts +8 -46
  89. package/src/contract_function_simulator/contract_function_simulator.ts +0 -17
  90. package/src/contract_function_simulator/oracle/oracle_type_mappings.ts +16 -1
  91. package/src/contract_function_simulator/oracle/private_execution_oracle.ts +7 -2
  92. package/src/contract_function_simulator/oracle/utility_execution_oracle.ts +130 -40
  93. package/src/events/event_service.ts +24 -14
  94. package/src/logs/log_service.ts +37 -44
  95. package/src/messages/tx_resolver_service.ts +22 -11
  96. package/src/node/benchmarked_node.ts +140 -0
  97. package/src/node/caching_aztec_node.ts +406 -0
  98. package/src/notes/note_service.ts +19 -13
  99. package/src/oracle_version.ts +2 -2
  100. package/src/pxe.ts +32 -16
  101. package/src/storage/contract_store/contract_store.ts +2 -1
  102. package/src/storage/fact_store/origin_state.ts +1 -1
  103. package/src/storage/tagging_store/sender_tagging_store.ts +51 -26
  104. package/src/tagging/constants.ts +12 -0
  105. package/src/tagging/get_all_logs_by_tags.ts +47 -11
  106. package/src/tagging/index.ts +6 -1
  107. package/src/tagging/recipient_sync/sync_tagged_private_logs.ts +19 -22
  108. package/src/tagging/sender_sync/sync_sender_tagging_indexes.ts +27 -57
  109. package/src/tagging/sender_sync/utils/classify_pending_txs.ts +91 -0
  110. package/src/tagging/sender_sync/utils/load_and_store_new_tagging_indexes.ts +40 -38
  111. package/src/tagging/sender_sync/utils/resolve_pending_txs.ts +64 -0
  112. package/src/tagging/sender_sync/utils/test_utils.ts +46 -0
  113. package/dest/contract_function_simulator/aztec_node_read_cache.d.ts +0 -39
  114. package/dest/contract_function_simulator/aztec_node_read_cache.d.ts.map +0 -1
  115. package/dest/contract_function_simulator/aztec_node_read_cache.js +0 -60
  116. package/dest/contract_function_simulator/benchmarked_node.d.ts +0 -9
  117. package/dest/contract_function_simulator/benchmarked_node.d.ts.map +0 -1
  118. package/dest/contract_function_simulator/benchmarked_node.js +0 -77
  119. package/dest/tagging/sender_sync/utils/get_status_change_of_pending.d.ts +0 -17
  120. package/dest/tagging/sender_sync/utils/get_status_change_of_pending.d.ts.map +0 -1
  121. package/dest/tagging/sender_sync/utils/get_status_change_of_pending.js +0 -60
  122. package/src/contract_function_simulator/aztec_node_read_cache.ts +0 -89
  123. package/src/contract_function_simulator/benchmarked_node.ts +0 -103
  124. package/src/tagging/sender_sync/utils/get_status_change_of_pending.ts +0 -66
package/src/pxe.ts CHANGED
@@ -62,7 +62,6 @@ import { BlockSynchronizer } from './block_synchronizer/index.js';
62
62
  import type { PXEConfig } from './config/index.js';
63
63
  import { ContractClassService } from './contract/contract_class_service.js';
64
64
  import { ContractSyncService } from './contract/contract_sync_service.js';
65
- import { BenchmarkedNodeFactory } from './contract_function_simulator/benchmarked_node.js';
66
65
  import {
67
66
  ContractFunctionSimulator,
68
67
  generateSimulatedProvingResult,
@@ -74,6 +73,7 @@ import { PrivateEventFilterValidator } from './events/private_event_filter_valid
74
73
  import type { ExecutionHooks } from './hooks/index.js';
75
74
  import { JobCoordinator } from './job_coordinator/job_coordinator.js';
76
75
  import { TxResolverService } from './messages/tx_resolver_service.js';
76
+ import { type CachingAztecNode, withCache } from './node/caching_aztec_node.js';
77
77
  import {
78
78
  PrivateKernelExecutionProver,
79
79
  type PrivateKernelExecutionProverConfig,
@@ -230,7 +230,7 @@ export type RegisteredTaggingSecretSource =
230
230
  */
231
231
  export class PXE {
232
232
  private constructor(
233
- private node: AztecNode,
233
+ private node: CachingAztecNode,
234
234
  private nodeDebug: AztecNodeDebug | undefined,
235
235
  private db: AztecAsyncKVStore,
236
236
  private blockStateSynchronizer: BlockSynchronizer,
@@ -315,18 +315,22 @@ export class PXE {
315
315
  l2TipsStore,
316
316
  factStore,
317
317
  } = openPxeStores(store, initialBlockHash);
318
- const contractClassService = new ContractClassService(node, contractStore);
318
+ // Every PXE consumer reads through this one wrapper, so a read cached by one is served to the rest. Only
319
+ // immutable, hash-pinned reads are cached (the rule lives on `withCache`), which makes it safe regardless of
320
+ // the consumer's anchor block; the block synchronizer wipes it on anchor updates to bound memory.
321
+ const readCachedNode = withCache(node);
322
+ const contractClassService = new ContractClassService(readCachedNode, contractStore);
319
323
  const contractSyncService = new ContractSyncService(
320
- node,
324
+ readCachedNode,
321
325
  contractStore,
322
326
  contractClassService,
323
327
  noteStore,
324
328
  createLogger('pxe:contract_sync', bindings),
325
329
  );
326
- const txResolver = new TxResolverService(node);
330
+ const txResolver = new TxResolverService(readCachedNode);
327
331
 
328
332
  const synchronizer = new BlockSynchronizer(
329
- node,
333
+ readCachedNode,
330
334
  store,
331
335
  anchorBlockStore,
332
336
  noteStore,
@@ -334,7 +338,6 @@ export class PXE {
334
338
  factStore,
335
339
  l2TipsStore,
336
340
  contractSyncService,
337
- contractClassService,
338
341
  config,
339
342
  bindings,
340
343
  );
@@ -355,7 +358,7 @@ export class PXE {
355
358
  const jobQueue = new SerialQueue();
356
359
 
357
360
  const pxe = new PXE(
358
- node,
361
+ readCachedNode,
359
362
  nodeDebug,
360
363
  store,
361
364
  synchronizer,
@@ -410,7 +413,7 @@ export class PXE {
410
413
  noteStore: this.noteStore,
411
414
  keyStore: this.keyStore,
412
415
  addressStore: this.addressStore,
413
- aztecNode: BenchmarkedNodeFactory.create(this.node),
416
+ aztecNode: this.node,
414
417
  l2TipsStore: this.l2TipsStore,
415
418
  senderTaggingStore: this.senderTaggingStore,
416
419
  recipientTaggingStore: this.recipientTaggingStore,
@@ -1016,6 +1019,7 @@ export class PXE {
1016
1019
  // computationally demanding that it'd be rare for someone to try to do it concurrently regardless.
1017
1020
  return this.#putInJobQueue(async jobId => {
1018
1021
  const totalTimer = new Timer();
1022
+ const recording = this.node.startRecording();
1019
1023
  try {
1020
1024
  const syncTimer = new Timer();
1021
1025
  await this.#maybeSync();
@@ -1063,7 +1067,7 @@ export class PXE {
1063
1067
 
1064
1068
  const txProvingResult = new TxProvingResult(privateExecutionResult, publicInputs, chonkProof!, {
1065
1069
  timings,
1066
- nodeRPCCalls: contractFunctionSimulator?.getStats().nodeRPCCalls,
1070
+ nodeRPCCalls: recording.stop(),
1067
1071
  });
1068
1072
 
1069
1073
  // We keep track of which tagging indices we've used in this tx so that we don't repeat them in future txs
@@ -1084,6 +1088,9 @@ export class PXE {
1084
1088
  return txProvingResult;
1085
1089
  } catch (err: any) {
1086
1090
  throw this.#contextualizeError(err, inspect(txRequest), inspect(privateExecutionResult));
1091
+ } finally {
1092
+ // Idempotent cleanup for the error and early-exit paths. The success path already stopped the recording.
1093
+ recording.stop();
1087
1094
  }
1088
1095
  });
1089
1096
  }
@@ -1101,6 +1108,7 @@ export class PXE {
1101
1108
  // We disable concurrent profiles for consistency with simulateTx.
1102
1109
  return this.#putInJobQueue(async jobId => {
1103
1110
  const totalTimer = new Timer();
1111
+ const recording = this.node.startRecording();
1104
1112
  try {
1105
1113
  const txInfo = {
1106
1114
  origin: txRequest.origin,
@@ -1166,10 +1174,12 @@ export class PXE {
1166
1174
  total - ((syncTime ?? 0) + (proving ?? 0) + perFunction.reduce((acc, { time }) => acc + time, 0)),
1167
1175
  };
1168
1176
 
1169
- const simulatorStats = contractFunctionSimulator.getStats();
1170
- return new TxProfileResult(executionSteps, { timings, nodeRPCCalls: simulatorStats.nodeRPCCalls });
1177
+ return new TxProfileResult(executionSteps, { timings, nodeRPCCalls: recording.stop() });
1171
1178
  } catch (err: any) {
1172
1179
  throw this.#contextualizeError(err, inspect(txRequest), `profileMode=${profileMode}`);
1180
+ } finally {
1181
+ // Idempotent cleanup for the error and early-exit paths. The success path already stopped the recording.
1182
+ recording.stop();
1173
1183
  }
1174
1184
  });
1175
1185
  }
@@ -1208,6 +1218,7 @@ export class PXE {
1208
1218
  // to the capsules), and we need to prevent concurrent runs from interfering with one another (e.g. attempting to
1209
1219
  // delete the same read value, or reading values that another simulation is currently modifying).
1210
1220
  return this.#putInJobQueue(async jobId => {
1221
+ const recording = this.node.startRecording();
1211
1222
  try {
1212
1223
  const totalTimer = new Timer();
1213
1224
  const txInfo = {
@@ -1328,10 +1339,9 @@ export class PXE {
1328
1339
  : {}),
1329
1340
  });
1330
1341
 
1331
- const simulatorStats = contractFunctionSimulator.getStats();
1332
1342
  return TxSimulationResult.fromPrivateSimulationResultAndPublicOutput(privateSimulationResult, publicOutput, {
1333
1343
  timings,
1334
- nodeRPCCalls: simulatorStats.nodeRPCCalls,
1344
+ nodeRPCCalls: recording.stop(),
1335
1345
  });
1336
1346
  } catch (err: any) {
1337
1347
  throw this.#contextualizeError(
@@ -1341,6 +1351,9 @@ export class PXE {
1341
1351
  `skipTxValidation=${skipTxValidation}`,
1342
1352
  `scopes=${scopes.map(s => s.toString()).join(', ')}`,
1343
1353
  );
1354
+ } finally {
1355
+ // Idempotent cleanup for the error and early-exit paths. The success path already stopped the recording.
1356
+ recording.stop();
1344
1357
  }
1345
1358
  });
1346
1359
  }
@@ -1357,6 +1370,7 @@ export class PXE {
1357
1370
  // to the capsules), and we need to prevent concurrent runs from interfering with one another (e.g. attempting to
1358
1371
  // delete the same read value, or reading values that another execution is currently modifying).
1359
1372
  return this.#putInJobQueue(async jobId => {
1373
+ const recording = this.node.startRecording();
1360
1374
  try {
1361
1375
  const totalTimer = new Timer();
1362
1376
  const syncTimer = new Timer();
@@ -1396,12 +1410,11 @@ export class PXE {
1396
1410
  unaccounted: totalTime - (syncTime + perFunction.reduce((acc, { time }) => acc + time, 0)),
1397
1411
  };
1398
1412
 
1399
- const simulationStats = contractFunctionSimulator.getStats();
1400
1413
  return {
1401
1414
  result: executionResult,
1402
1415
  offchainEffects,
1403
1416
  anchorBlockTimestamp: anchorBlockHeader.globalVariables.timestamp,
1404
- stats: { timings, nodeRPCCalls: simulationStats.nodeRPCCalls },
1417
+ stats: { timings, nodeRPCCalls: recording.stop() },
1405
1418
  };
1406
1419
  } catch (err: any) {
1407
1420
  const { to, name, args } = call;
@@ -1411,6 +1424,9 @@ export class PXE {
1411
1424
  `executeUtility ${to}:${name}(${stringifiedArgs})`,
1412
1425
  `scopes=${scopes.map(s => s.toString()).join(', ')}`,
1413
1426
  );
1427
+ } finally {
1428
+ // Idempotent cleanup for the error and early-exit paths. The success path already stopped the recording.
1429
+ recording.stop();
1414
1430
  }
1415
1431
  });
1416
1432
  }
@@ -17,6 +17,7 @@ import {
17
17
  findFunctionAbiBySelector,
18
18
  findFunctionArtifactBySelector,
19
19
  getFunctionDebugMetadata,
20
+ getFunctionReturnType,
20
21
  } from '@aztec/stdlib/abi';
21
22
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
22
23
  import {
@@ -371,7 +372,7 @@ export class ContractStore {
371
372
  hideMsgSender: false,
372
373
  isStatic: functionDao.isStatic,
373
374
  args: encodeArguments(functionDao, args),
374
- returnTypes: functionDao.returnTypes,
375
+ returnType: getFunctionReturnType(functionDao),
375
376
  });
376
377
  }
377
378
  }
@@ -12,7 +12,7 @@ import type { Fact } from './stored_fact.js';
12
12
  * - `Finalized`: L1-finalized.
13
13
  *
14
14
  * The numeric discriminants must stay in sync with the Noir `OriginBlockState` in
15
- * `noir-projects/aztec-nr/aztec/src/facts/origin_state.nr`: PXE serializes this value into the `Fact` oracle response
15
+ * `noir-projects/labs/aztec-nr/aztec/src/facts/origin_state.nr`: PXE serializes this value into the `Fact` oracle response
16
16
  * and Noir deserializes it via `from_u8`, which rejects any value outside this set.
17
17
  */
18
18
  export enum OriginBlockState {
@@ -3,10 +3,13 @@ import { AppTaggingSecret, SiloedTag, type TaggingIndexRange } from '@aztec/stdl
3
3
  import { TxEffect, TxHash } from '@aztec/stdlib/tx';
4
4
 
5
5
  import type { StagedStore } from '../../job_coordinator/job_coordinator.js';
6
- import { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../../tagging/constants.js';
6
+ import { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN, unfinalizedTaggingIndexesWindowEnd } from '../../tagging/constants.js';
7
+
8
+ /** A tx still awaiting finalization, and the highest tagging index it used for one secret. */
9
+ export type PendingTx = { txHash: string; highestIndex: number };
7
10
 
8
11
  /** Internal representation of a pending index range entry. */
9
- type PendingIndexesEntry = { lowestIndex: number; highestIndex: number; txHash: string };
12
+ type PendingIndexesEntry = PendingTx & { lowestIndex: number };
10
13
 
11
14
  /**
12
15
  * Data provider of tagging data used when syncing the sender tagging indexes. The recipient counterpart of this class
@@ -195,13 +198,9 @@ export class SenderTaggingStore implements StagedStore {
195
198
 
196
199
  // Process in memory and validate
197
200
  for (const { range, secretStr, pendingData, finalizedIndex } of rangeData) {
198
- // Check that the highest index is not further than window length from the highest finalized index.
199
- if (range.highestIndex > (finalizedIndex ?? 0) + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN) {
200
- throw new Error(
201
- `Highest used index ${range.highestIndex} is further than window length from the highest finalized index ${finalizedIndex ?? 0}.
202
- Tagging window length ${UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN} is configured too low. Contact the Aztec team
203
- to increase it!`,
204
- );
201
+ const windowEnd = unfinalizedTaggingIndexesWindowEnd(finalizedIndex);
202
+ if (range.highestIndex >= windowEnd) {
203
+ throw windowExceededError(range.highestIndex, windowEnd, finalizedIndex);
205
204
  }
206
205
 
207
206
  // Throw if the lowest index is lower than or equal to the last finalized index
@@ -255,27 +254,20 @@ export class SenderTaggingStore implements StagedStore {
255
254
  }
256
255
 
257
256
  /**
258
- * Returns the transaction hashes of all pending transactions that contain highest indexes within a specified range
259
- * for a given directional app tagging secret. We check based on the highest indexes only as that is the relevant
260
- * information for the caller of this function.
257
+ * Returns the pending txs whose highest index falls within [startIndex, endIndex) for a given directional app
258
+ * tagging secret. The highest index is what decides whether a tx belongs to the window, so it alone is matched
259
+ * against the bounds, and it is also the only index a caller needs: a tx whose highest index is onchain has every
260
+ * lower one onchain too. A secret holds at most one entry per tx hash, so no tx hash appears twice in the result.
261
261
  * @param secret - The directional app tagging secret to query pending indexes for.
262
262
  * @param startIndex - The lower bound of the index range (inclusive).
263
263
  * @param endIndex - The upper bound of the index range (exclusive).
264
- * @returns An array of unique transaction hashes for pending transactions that contain indexes in the range
265
- * [startIndex, endIndex). Returns an empty array if no pending indexes exist in the range.
266
264
  */
267
- getTxHashesOfPendingIndexes(
268
- secret: AppTaggingSecret,
269
- startIndex: number,
270
- endIndex: number,
271
- jobId: string,
272
- ): Promise<TxHash[]> {
265
+ getPendingTxs(secret: AppTaggingSecret, startIndex: number, endIndex: number, jobId: string): Promise<PendingTx[]> {
273
266
  return this.#store.transactionAsync(async () => {
274
267
  const existing = await this.#readPendingIndexes(jobId, secret.toString());
275
- const txHashes = existing
268
+ return existing
276
269
  .filter(entry => entry.highestIndex >= startIndex && entry.highestIndex < endIndex)
277
- .map(entry => entry.txHash);
278
- return Array.from(new Set(txHashes)).map(TxHash.fromString);
270
+ .map(entry => ({ txHash: entry.txHash, highestIndex: entry.highestIndex }));
279
271
  });
280
272
  }
281
273
 
@@ -405,15 +397,33 @@ export class SenderTaggingStore implements StagedStore {
405
397
 
406
398
  /**
407
399
  * Updates pending indexes corresponding to the given transaction hashes to be finalized and prunes any lower pending
408
- * indexes.
400
+ * indexes. Applies to every secret the txs used, so the caller must hold tx-level evidence that the whole tx
401
+ * finalized. Callers holding evidence about a single secret must use {@link finalizePendingIndexesOfSecret} instead.
409
402
  */
410
- async finalizePendingIndexes(txHashes: TxHash[], jobId: string): Promise<void> {
403
+ finalizePendingIndexes(txHashes: TxHash[], jobId: string): Promise<void> {
404
+ return this.#finalizePendingIndexes(txHashes, jobId);
405
+ }
406
+
407
+ /**
408
+ * Same as {@link finalizePendingIndexes}, but restricted to the pending indexes of a single secret.
409
+ *
410
+ * Finalizing every secret off single-secret evidence would be unsound: a tx whose execution partially reverted can
411
+ * have all of one secret's tags onchain and none of another's, and the second secret's indexes must not be recorded
412
+ * as finalized when they never reached the chain.
413
+ */
414
+ finalizePendingIndexesOfSecret(secret: AppTaggingSecret, txHashes: TxHash[], jobId: string): Promise<void> {
415
+ return this.#finalizePendingIndexes(txHashes, jobId, secret.toString());
416
+ }
417
+
418
+ async #finalizePendingIndexes(txHashes: TxHash[], jobId: string, onlySecret?: string): Promise<void> {
411
419
  if (txHashes.length === 0) {
412
420
  return;
413
421
  }
414
422
 
415
423
  const txHashStrings = new Set(txHashes.map(tx => tx.toString()));
416
- const secretsWithData = await this.#getSecretsWithPendingData(jobId);
424
+ const secretsWithData = (await this.#getSecretsWithPendingData(jobId)).filter(
425
+ ({ secret }) => onlySecret === undefined || secret === onlySecret,
426
+ );
417
427
 
418
428
  for (const { secret, pendingData, lastFinalized } of secretsWithData) {
419
429
  let currentPending = pendingData;
@@ -521,3 +531,18 @@ export class SenderTaggingStore implements StagedStore {
521
531
  }
522
532
  }
523
533
  }
534
+
535
+ /** Builds the error thrown when a pending tag index is at or past the unfinalized tagging window end. */
536
+ export function windowExceededError(
537
+ highestIndex: number,
538
+ windowEnd: number,
539
+ finalizedIndex: number | undefined,
540
+ ): Error {
541
+ const finalizedDescription =
542
+ finalizedIndex === undefined ? 'no index finalized yet' : `highest finalized index ${finalizedIndex}`;
543
+ return new Error(
544
+ `Highest used index ${highestIndex} is at or past the window end ${windowEnd} (${finalizedDescription}). ` +
545
+ `Tagging window length ${UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN} is configured too low. ` +
546
+ `Contact the Aztec team to increase it!`,
547
+ );
548
+ }
@@ -19,6 +19,18 @@ import { MAX_PRIVATE_LOGS_PER_TX } from '@aztec/constants';
19
19
  // MAX_PRIVATE_LOGS_PER_TX. No fixed window value closes that gap.
20
20
  export const UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN = MAX_PRIVATE_LOGS_PER_TX + 20;
21
21
 
22
+ /**
23
+ * Exclusive upper bound of the tag indexes that can exist for a secret whose highest finalized index is
24
+ * `finalizedIndex` (undefined when nothing is finalized yet). The sender store refuses pending indexes at or past it,
25
+ * and both sender and recipient sync scan exactly up to it. Every absolute window bound must come from this helper:
26
+ * a site with a wider or narrower bound lets a tx land at an index the syncs never scan, so two stores sharing the
27
+ * secret could later pick a colliding index.
28
+ */
29
+ export function unfinalizedTaggingIndexesWindowEnd(finalizedIndex: number | undefined): number {
30
+ const windowStart = finalizedIndex === undefined ? 0 : finalizedIndex + 1;
31
+ return windowStart + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN;
32
+ }
33
+
22
34
  // The number of tags probed per constrained secret in the first round.
23
35
  //
24
36
  // The probe doubles each round (2, 4, 8, ..., capped at UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN) while every probed
@@ -1,4 +1,4 @@
1
- import type { BlockNumber } from '@aztec/foundation/branded-types';
1
+ import { BlockNumber } from '@aztec/foundation/branded-types';
2
2
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
3
3
  import type { BlockHash } from '@aztec/stdlib/block';
4
4
  import { MAX_RPC_LEN } from '@aztec/stdlib/interfaces/api-limit';
@@ -11,6 +11,31 @@ import {
11
11
  queryAllPrivateLogsByTags,
12
12
  queryAllPublicLogsByTags,
13
13
  } from '@aztec/stdlib/logs';
14
+ import type { BlockHeader } from '@aztec/stdlib/tx';
15
+
16
+ /**
17
+ * The block a tag query is anchored to.
18
+ *
19
+ * Both fields come from one header (see {@link logQueryAnchorOf}), which is what keeps the bound and the reorg check
20
+ * talking about the same block.
21
+ */
22
+ export type LogQueryAnchor = {
23
+ /**
24
+ * Hash of the anchor block, naming the chain the query must be answered on: the node throws if that block is gone,
25
+ * which is how a reorg surfaces.
26
+ */
27
+ hash: BlockHash;
28
+ /** Height of the anchor block, bounding the query to blocks at or below it. */
29
+ number: BlockNumber;
30
+ };
31
+
32
+ /**
33
+ * The {@link LogQueryAnchor} naming `anchorBlockHeader`: its hash and its height. Callers build it once and reuse
34
+ * it across every query anchored to that block, so the header is hashed only once.
35
+ */
36
+ export async function logQueryAnchorOf(anchorBlockHeader: BlockHeader): Promise<LogQueryAnchor> {
37
+ return { hash: await anchorBlockHeader.hash(), number: anchorBlockHeader.getBlockNumber() };
38
+ }
14
39
 
15
40
  /** Optional block-range, effects opt-in, and pagination cap shared by both wrappers. */
16
41
  export type GetAllLogsByTagsOptions = {
@@ -61,15 +86,15 @@ async function getAllPagesInBatches<T extends Tag | SiloedTag, Opts extends LogI
61
86
  *
62
87
  * @param aztecNode - The Aztec node to query.
63
88
  * @param tags - The siloed tags to search for.
64
- * @param anchorBlockHash - Reference block for the Aztec node query, throws if block is not found there (typically
65
- * because of reorgs).
89
+ * @param anchor - Block the query is anchored to: results stop there, and the call throws if that block is not found
90
+ * (typically because of reorgs).
66
91
  * @param options - Optional `fromBlock`/`toBlock` range and `includeEffects` opt-in.
67
92
  * @returns An array of log arrays, one per tag, containing all logs across all pages.
68
93
  */
69
94
  export function getAllPrivateLogsByTags<Opts extends GetAllLogsByTagsOptions = GetAllLogsByTagsOptions>(
70
95
  aztecNode: AztecNode,
71
96
  tags: SiloedTag[],
72
- anchorBlockHash: BlockHash,
97
+ anchor: LogQueryAnchor,
73
98
  options: Opts = {} as Opts,
74
99
  ): Promise<LogResult<Opts>[][]> {
75
100
  return getAllPagesInBatches<SiloedTag, Opts>(
@@ -77,9 +102,9 @@ export function getAllPrivateLogsByTags<Opts extends GetAllLogsByTagsOptions = G
77
102
  batch =>
78
103
  queryAllPrivateLogsByTags(aztecNode, {
79
104
  tags: batch,
80
- referenceBlock: anchorBlockHash,
105
+ referenceBlock: anchor.hash,
81
106
  fromBlock: options.fromBlock,
82
- toBlock: options.toBlock,
107
+ toBlock: exclusiveUpperBound(anchor, options.toBlock),
83
108
  includeEffects: options.includeEffects ?? false,
84
109
  limitPerTag: options.limitPerTag,
85
110
  }) as Promise<LogResult<Opts>[][]>,
@@ -92,8 +117,8 @@ export function getAllPrivateLogsByTags<Opts extends GetAllLogsByTagsOptions = G
92
117
  * @param aztecNode - The Aztec node to query.
93
118
  * @param contractAddress - The contract address to search logs for.
94
119
  * @param tags - The tags to search for.
95
- * @param anchorBlockHash - Reference block for the Aztec node query, throws if block is not found there (typically
96
- * because of reorgs).
120
+ * @param anchor - Block the query is anchored to: results stop there, and the call throws if that block is not found
121
+ * (typically because of reorgs).
97
122
  * @param options - Optional `fromBlock`/`toBlock` range and `includeEffects` opt-in.
98
123
  * @returns An array of log arrays, one per tag, containing all logs across all pages.
99
124
  */
@@ -101,7 +126,7 @@ export function getAllPublicLogsByTagsFromContract<Opts extends GetAllLogsByTags
101
126
  aztecNode: AztecNode,
102
127
  contractAddress: AztecAddress,
103
128
  tags: Tag[],
104
- anchorBlockHash: BlockHash,
129
+ anchor: LogQueryAnchor,
105
130
  options: Opts = {} as Opts,
106
131
  ): Promise<LogResult<Opts>[][]> {
107
132
  return getAllPagesInBatches<Tag, Opts>(
@@ -110,11 +135,22 @@ export function getAllPublicLogsByTagsFromContract<Opts extends GetAllLogsByTags
110
135
  queryAllPublicLogsByTags(aztecNode, {
111
136
  contractAddress,
112
137
  tags: batch,
113
- referenceBlock: anchorBlockHash,
138
+ referenceBlock: anchor.hash,
114
139
  fromBlock: options.fromBlock,
115
- toBlock: options.toBlock,
140
+ toBlock: exclusiveUpperBound(anchor, options.toBlock),
116
141
  includeEffects: options.includeEffects ?? false,
117
142
  limitPerTag: options.limitPerTag,
118
143
  }) as Promise<LogResult<Opts>[][]>,
119
144
  );
120
145
  }
146
+
147
+ /**
148
+ * Exclusive upper block bound for a query anchored at `anchor`: one past the anchor block, or the caller's own
149
+ * `toBlock` when that is tighter.
150
+ *
151
+ * The bound is spelled out in the request rather than left implicit in `referenceBlock`, so the request itself
152
+ * names a fixed block range that no later block can widen.
153
+ */
154
+ function exclusiveUpperBound(anchor: LogQueryAnchor, toBlock: BlockNumber | undefined): BlockNumber {
155
+ return BlockNumber(Math.min(toBlock ?? Infinity, anchor.number + 1));
156
+ }
@@ -13,7 +13,12 @@ export { syncTaggedPrivateLogs } from './recipient_sync/sync_tagged_private_logs
13
13
  export { syncSenderTaggingIndexes } from './sender_sync/sync_sender_tagging_indexes.js';
14
14
  export { persistSenderTaggingIndexRangesForTx } from './persist_sender_tagging_index_ranges.js';
15
15
  export { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN, INITIAL_CONSTRAINED_PROBE_LEN } from './constants.js';
16
- export { getAllPrivateLogsByTags, getAllPublicLogsByTagsFromContract } from './get_all_logs_by_tags.js';
16
+ export {
17
+ type LogQueryAnchor,
18
+ getAllPrivateLogsByTags,
19
+ getAllPublicLogsByTagsFromContract,
20
+ logQueryAnchorOf,
21
+ } from './get_all_logs_by_tags.js';
17
22
 
18
23
  // Re-export tagging-related types from stdlib
19
24
  export { AppTaggingSecret, Tag, SiloedTag } from '@aztec/stdlib/logs';
@@ -1,14 +1,17 @@
1
- import { BlockNumber } from '@aztec/foundation/branded-types';
1
+ import type { BlockNumber } from '@aztec/foundation/branded-types';
2
2
  import { isDefined } from '@aztec/foundation/types';
3
- import type { BlockHash } from '@aztec/stdlib/block';
4
3
  import type { AztecNode } from '@aztec/stdlib/interfaces/client';
5
4
  import type { AppTaggingSecret, LogResult } from '@aztec/stdlib/logs';
6
5
  import { AppTaggingSecretKind, SiloedTag } from '@aztec/stdlib/logs';
7
6
  import type { BlockHeader } from '@aztec/stdlib/tx';
8
7
 
9
8
  import type { RecipientTaggingStore } from '../../storage/tagging_store/recipient_tagging_store.js';
10
- import { INITIAL_CONSTRAINED_PROBE_LEN, UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../constants.js';
11
- import { getAllPrivateLogsByTags } from '../get_all_logs_by_tags.js';
9
+ import {
10
+ INITIAL_CONSTRAINED_PROBE_LEN,
11
+ UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN,
12
+ unfinalizedTaggingIndexesWindowEnd,
13
+ } from '../constants.js';
14
+ import { type LogQueryAnchor, getAllPrivateLogsByTags, logQueryAnchorOf } from '../get_all_logs_by_tags.js';
12
15
  import { findHighestIndexes } from './utils/find_highest_indexes.js';
13
16
 
14
17
  /**
@@ -90,8 +93,7 @@ export async function syncTaggedPrivateLogs(
90
93
  return [];
91
94
  }
92
95
 
93
- const anchorBlockNumber = anchorBlockHeader.getBlockNumber();
94
- const anchorBlockHash = await anchorBlockHeader.hash();
96
+ const anchor = await logQueryAnchorOf(anchorBlockHeader);
95
97
  const currentTimestamp = anchorBlockHeader.globalVariables.timestamp;
96
98
 
97
99
  // Read stored indexes from the db and compute the initial [start, end) range for each secret
@@ -100,7 +102,7 @@ export async function syncTaggedPrivateLogs(
100
102
 
101
103
  while (pending.length > 0) {
102
104
  // Compute tags for all pending secrets and fetch logs in batched RPC calls
103
- const logsPerSecret = await fetchLogsForSecrets(pending, aztecNode, anchorBlockNumber, anchorBlockHash);
105
+ const logsPerSecret = await fetchLogsForSecrets(pending, aztecNode, anchor);
104
106
 
105
107
  const nextRound = await Promise.all(
106
108
  pending.map(async (pendingSecret, i) => {
@@ -149,7 +151,7 @@ function getIndexRangesForSecrets(
149
151
  return Promise.all(
150
152
  secrets.map(async (secret): Promise<PendingSecret> => {
151
153
  const currentHighestFinalizedIndex = await taggingStore.getHighestFinalizedIndex(secret, jobId);
152
- const boundEnd = (currentHighestFinalizedIndex ?? 0) + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1;
154
+ const boundEnd = unfinalizedTaggingIndexesWindowEnd(currentHighestFinalizedIndex);
153
155
 
154
156
  if (secret.kind === AppTaggingSecretKind.CONSTRAINED) {
155
157
  // Constrained streams are gapless and resume at the finalized index, so probe a small initial window and stop
@@ -181,8 +183,7 @@ function getIndexRangesForSecrets(
181
183
  async function fetchLogsForSecrets(
182
184
  pending: PendingSecret[],
183
185
  aztecNode: AztecNode,
184
- anchorBlockNumber: BlockNumber,
185
- anchorBlockHash: BlockHash,
186
+ anchor: LogQueryAnchor,
186
187
  ): Promise<LogWithIndex[][]> {
187
188
  // Determine the index range for each secret
188
189
  const indexesPerSecret = pending.map(({ start, end }) => Array.from({ length: end - start }, (_, i) => start + i));
@@ -196,14 +197,10 @@ async function fetchLogsForSecrets(
196
197
 
197
198
  const allTags = tagsPerSecret.flat();
198
199
 
199
- // getAllPrivateLogsByTags handles MAX_RPC_LEN chunking internally. Recipient sync builds `PendingTaggedLog` from
200
- // each log's note hashes and first nullifier, so we opt into effects. The `toBlock` cap (anchor block + 1,
201
- // exclusive) tells the node to skip any logs in blocks past the anchor — the same guard previously enforced
202
- // by an in-memory filter on the response.
203
- const allResults = await getAllPrivateLogsByTags(aztecNode, allTags, anchorBlockHash, {
204
- includeEffects: true,
205
- toBlock: BlockNumber(anchorBlockNumber + 1),
206
- });
200
+ // getAllPrivateLogsByTags handles MAX_RPC_LEN chunking internally, and bounds the query at the anchor block so
201
+ // logs from later blocks are never returned. Recipient sync builds `PendingTaggedLog` from each log's note hashes
202
+ // and first nullifier, so we opt into effects.
203
+ const allResults = await getAllPrivateLogsByTags(aztecNode, allTags, anchor, { includeEffects: true });
207
204
 
208
205
  // Split flat results back per secret using the known lengths
209
206
  const logsPerSecret: LogWithIndex[][] = [];
@@ -257,7 +254,7 @@ async function processConstrainedResults(
257
254
  const probeFullyConsumed = firstMissingIndex >= pending.end;
258
255
  const boundEnd =
259
256
  highestFinalizedIndex !== undefined
260
- ? Math.max(pending.boundEnd, highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1)
257
+ ? Math.max(pending.boundEnd, unfinalizedTaggingIndexesWindowEnd(highestFinalizedIndex))
261
258
  : pending.boundEnd;
262
259
 
263
260
  // Double the probe each round, capped at the window (see INITIAL_CONSTRAINED_PROBE_LEN in ../constants.ts).
@@ -319,7 +316,7 @@ async function processUnconstrainedResults(
319
316
 
320
317
  // For the next iteration we want to look only at indexes for which we have not yet fetched logs while
321
318
  // ensuring that we do not look further than WINDOW_LEN ahead of the highest finalized index.
322
- const end = highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1;
319
+ const end = unfinalizedTaggingIndexesWindowEnd(highestFinalizedIndex);
323
320
  return {
324
321
  kind: AppTaggingSecretKind.UNCONSTRAINED,
325
322
  secret: pending.secret,
@@ -338,8 +335,8 @@ type PendingSecretBase = {
338
335
  /** Constrained scan state, carrying the doubling-probe bookkeeping that only the gapless constrained scan uses. */
339
336
  type PendingConstrained = PendingSecretBase & {
340
337
  kind: typeof AppTaggingSecretKind.CONSTRAINED;
341
- // Exclusive upper bound on the indexes this secret may probe this sync (highest finalized index + WINDOW_LEN + 1, the
342
- // protocol bound on how far ahead of finalized a log can sit). `end` grows toward it each round.
338
+ // Exclusive upper bound on the indexes this secret may probe this sync, from `unfinalizedTaggingIndexesWindowEnd`:
339
+ // the protocol bound on how far ahead of finalized a log can sit. `end` grows toward it each round.
343
340
  boundEnd: number;
344
341
  // Intended probe length for the round that produced `end`. The queried span can be smaller when boundEnd caps it.
345
342
  probeLen: number;