@aztec/pxe 5.1.0 → 5.2.0-nightly.20260724

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.
@@ -1,6 +1,6 @@
1
1
  export function getPackageInfo() {
2
2
  return {
3
- version: '5.1.0',
3
+ version: '5.2.0',
4
4
  name: '@aztec/pxe'
5
5
  };
6
6
  }
@@ -1,2 +1,3 @@
1
1
  export declare const UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN: number;
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdGFnZ2luZy9jb25zdGFudHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBbUJBLGVBQU8sTUFBTSxzQ0FBc0MsUUFBK0IsQ0FBQyJ9
2
+ export declare const INITIAL_CONSTRAINED_PROBE_LEN = 2;
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdGFnZ2luZy9jb25zdGFudHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBbUJBLGVBQU8sTUFBTSxzQ0FBc0MsUUFBK0IsQ0FBQztBQVVuRixlQUFPLE1BQU0sNkJBQTZCLElBQUksQ0FBQyJ9
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/tagging/constants.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,sCAAsC,QAA+B,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/tagging/constants.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,sCAAsC,QAA+B,CAAC;AAUnF,eAAO,MAAM,6BAA6B,IAAI,CAAC"}
@@ -17,3 +17,12 @@ import { MAX_PRIVATE_LOGS_PER_TX } from '@aztec/constants';
17
17
  // reserved at log emission time, before squashing is decided, and the kernel's reset/squash loop is not bounded by
18
18
  // MAX_PRIVATE_LOGS_PER_TX. No fixed window value closes that gap.
19
19
  export const UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN = MAX_PRIVATE_LOGS_PER_TX + 20;
20
+ // The number of tags probed per constrained secret in the first round.
21
+ //
22
+ // The probe doubles each round (2, 4, 8, ..., capped at UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN) while every probed
23
+ // index is a hit, stopping at the first missing tag. Constrained delivery is gapless, so a single missing tag proves
24
+ // the stream has ended: at steady state this turns a full WINDOW_LEN probe into two tags. A secret K logs behind
25
+ // catches up in ~log2(K) round-trips while the probe is still doubling (2, 4, 8, 16, 32), but once it saturates the cap
26
+ // and advances WINDOW_LEN tags per round, deeper catch-up is linear at ~K/WINDOW_LEN rounds. Either way it beats both
27
+ // the full window every round and one round per log.
28
+ export const INITIAL_CONSTRAINED_PROBE_LEN = 2;
@@ -11,8 +11,8 @@
11
11
  export { syncTaggedPrivateLogs } from './recipient_sync/sync_tagged_private_logs.js';
12
12
  export { syncSenderTaggingIndexes } from './sender_sync/sync_sender_tagging_indexes.js';
13
13
  export { persistSenderTaggingIndexRangesForTx } from './persist_sender_tagging_index_ranges.js';
14
- export { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from './constants.js';
14
+ export { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN, INITIAL_CONSTRAINED_PROBE_LEN } from './constants.js';
15
15
  export { getAllPrivateLogsByTags, getAllPublicLogsByTagsFromContract } from './get_all_logs_by_tags.js';
16
16
  export { AppTaggingSecret, Tag, SiloedTag } from '@aztec/stdlib/logs';
17
17
  export { type PreTag, type TaggingIndexRange } from '@aztec/stdlib/logs';
18
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90YWdnaW5nL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7R0FTRztBQUVILE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLDhDQUE4QyxDQUFDO0FBQ3JGLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLDhDQUE4QyxDQUFDO0FBQ3hGLE9BQU8sRUFBRSxvQ0FBb0MsRUFBRSxNQUFNLDBDQUEwQyxDQUFDO0FBQ2hHLE9BQU8sRUFBRSxzQ0FBc0MsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ3hFLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxrQ0FBa0MsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBR3hHLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxHQUFHLEVBQUUsU0FBUyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDdEUsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFFLEtBQUssaUJBQWlCLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQyJ9
18
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90YWdnaW5nL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7R0FTRztBQUVILE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLDhDQUE4QyxDQUFDO0FBQ3JGLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLDhDQUE4QyxDQUFDO0FBQ3hGLE9BQU8sRUFBRSxvQ0FBb0MsRUFBRSxNQUFNLDBDQUEwQyxDQUFDO0FBQ2hHLE9BQU8sRUFBRSxzQ0FBc0MsRUFBRSw2QkFBNkIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ3ZHLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxrQ0FBa0MsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBR3hHLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxHQUFHLEVBQUUsU0FBUyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDdEUsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFFLEtBQUssaUJBQWlCLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQyJ9
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tagging/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AACxF,OAAO,EAAE,oCAAoC,EAAE,MAAM,0CAA0C,CAAC;AAChG,OAAO,EAAE,sCAAsC,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EAAE,uBAAuB,EAAE,kCAAkC,EAAE,MAAM,2BAA2B,CAAC;AAGxG,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tagging/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AACxF,OAAO,EAAE,oCAAoC,EAAE,MAAM,0CAA0C,CAAC;AAChG,OAAO,EAAE,sCAAsC,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,uBAAuB,EAAE,kCAAkC,EAAE,MAAM,2BAA2B,CAAC;AAGxG,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -10,7 +10,7 @@
10
10
  */ export { syncTaggedPrivateLogs } from './recipient_sync/sync_tagged_private_logs.js';
11
11
  export { syncSenderTaggingIndexes } from './sender_sync/sync_sender_tagging_indexes.js';
12
12
  export { persistSenderTaggingIndexRangesForTx } from './persist_sender_tagging_index_ranges.js';
13
- export { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from './constants.js';
13
+ export { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN, INITIAL_CONSTRAINED_PROBE_LEN } from './constants.js';
14
14
  export { getAllPrivateLogsByTags, getAllPublicLogsByTagsFromContract } from './get_all_logs_by_tags.js';
15
15
  // Re-export tagging-related types from stdlib
16
16
  export { AppTaggingSecret, Tag, SiloedTag } from '@aztec/stdlib/logs';
@@ -54,9 +54,14 @@ import type { RecipientTaggingStore } from '../../storage/tagging_store/recipien
54
54
  * - The lower bound is `highestFinalizedIndex + 1`. The `highestAgedIndex` mechanism is unnecessary because
55
55
  * the nullifier chain prevents out-of-order index usage: no device can fill in a lower index after a higher
56
56
  * one is already on-chain.
57
- * - The scan stops at the first gap in the index sequence. If all indexes in the batch have logs, the window
58
- * advances. If a gap is found, the sequence has ended and no further logs exist.
59
- * - The upper bound is the same as unconstrained: `highestFinalizedIndex + WINDOW_LEN`.
57
+ * - Because the sequence is gapless, the scan stops at the first missing index: that gap proves the sequence has
58
+ * ended and no further logs exist. We exploit this with a doubling first-miss probe (see
59
+ * `INITIAL_CONSTRAINED_PROBE_LEN` in ../constants.ts), so a
60
+ * steady-state sync with no new logs costs two tags instead of the whole window. The probe length is in-memory
61
+ * state scoped to one sync call, so every sync restarts at the initial probe.
62
+ * - The upper bound is the same as unconstrained: `highestFinalizedIndex + WINDOW_LEN`. Advancing the probe is
63
+ * decoupled from persisting the finalized index, so unfinalized logs at the top of the run are still fetched
64
+ * while only the finalized prefix is persisted.
60
65
  *
61
66
  * # Batching across secrets
62
67
  *
@@ -66,4 +71,4 @@ import type { RecipientTaggingStore } from '../../storage/tagging_store/recipien
66
71
  * advanced are kept for the next iteration.
67
72
  */
68
73
  export declare function syncTaggedPrivateLogs(secrets: AppTaggingSecret[], aztecNode: AztecNode, taggingStore: RecipientTaggingStore, anchorBlockHeader: BlockHeader, finalizedBlockNumber: BlockNumber, jobId: string): Promise<LogResult[]>;
69
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luY190YWdnZWRfcHJpdmF0ZV9sb2dzLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvdGFnZ2luZy9yZWNpcGllbnRfc3luYy9zeW5jX3RhZ2dlZF9wcml2YXRlX2xvZ3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBRzlELE9BQU8sS0FBSyxFQUFFLFNBQVMsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQ2pFLE9BQU8sS0FBSyxFQUFFLGdCQUFnQixFQUFFLFNBQVMsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRXRFLE9BQU8sS0FBSyxFQUFFLFdBQVcsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRXBELE9BQU8sS0FBSyxFQUFFLHFCQUFxQixFQUFFLE1BQU0sd0RBQXdELENBQUM7QUFLcEc7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E2REc7QUFDSCx3QkFBc0IscUJBQXFCLENBQ3pDLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxFQUMzQixTQUFTLEVBQUUsU0FBUyxFQUNwQixZQUFZLEVBQUUscUJBQXFCLEVBQ25DLGlCQUFpQixFQUFFLFdBQVcsRUFDOUIsb0JBQW9CLEVBQUUsV0FBVyxFQUNqQyxLQUFLLEVBQUUsTUFBTSxHQUNaLE9BQU8sQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQWlEdEIifQ==
74
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luY190YWdnZWRfcHJpdmF0ZV9sb2dzLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvdGFnZ2luZy9yZWNpcGllbnRfc3luYy9zeW5jX3RhZ2dlZF9wcml2YXRlX2xvZ3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBRzlELE9BQU8sS0FBSyxFQUFFLFNBQVMsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQ2pFLE9BQU8sS0FBSyxFQUFFLGdCQUFnQixFQUFFLFNBQVMsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRXRFLE9BQU8sS0FBSyxFQUFFLFdBQVcsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRXBELE9BQU8sS0FBSyxFQUFFLHFCQUFxQixFQUFFLE1BQU0sd0RBQXdELENBQUM7QUFLcEc7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQWtFRztBQUNILHdCQUFzQixxQkFBcUIsQ0FDekMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLEVBQzNCLFNBQVMsRUFBRSxTQUFTLEVBQ3BCLFlBQVksRUFBRSxxQkFBcUIsRUFDbkMsaUJBQWlCLEVBQUUsV0FBVyxFQUM5QixvQkFBb0IsRUFBRSxXQUFXLEVBQ2pDLEtBQUssRUFBRSxNQUFNLEdBQ1osT0FBTyxDQUFDLFNBQVMsRUFBRSxDQUFDLENBcUR0QiJ9
@@ -1 +1 @@
1
- {"version":3,"file":"sync_tagged_private_logs.d.ts","sourceRoot":"","sources":["../../../src/tagging/recipient_sync/sync_tagged_private_logs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAG9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wDAAwD,CAAC;AAKpG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,qBAAqB,EACnC,iBAAiB,EAAE,WAAW,EAC9B,oBAAoB,EAAE,WAAW,EACjC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,SAAS,EAAE,CAAC,CAiDtB"}
1
+ {"version":3,"file":"sync_tagged_private_logs.d.ts","sourceRoot":"","sources":["../../../src/tagging/recipient_sync/sync_tagged_private_logs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAG9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wDAAwD,CAAC;AAKpG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,qBAAqB,EACnC,iBAAiB,EAAE,WAAW,EAC9B,oBAAoB,EAAE,WAAW,EACjC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,SAAS,EAAE,CAAC,CAqDtB"}
@@ -1,7 +1,7 @@
1
1
  import { BlockNumber } from '@aztec/foundation/branded-types';
2
2
  import { isDefined } from '@aztec/foundation/types';
3
3
  import { AppTaggingSecretKind, SiloedTag } from '@aztec/stdlib/logs';
4
- import { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../constants.js';
4
+ import { INITIAL_CONSTRAINED_PROBE_LEN, UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../constants.js';
5
5
  import { getAllPrivateLogsByTags } from '../get_all_logs_by_tags.js';
6
6
  import { findHighestIndexes } from './utils/find_highest_indexes.js';
7
7
  /**
@@ -55,9 +55,14 @@ import { findHighestIndexes } from './utils/find_highest_indexes.js';
55
55
  * - The lower bound is `highestFinalizedIndex + 1`. The `highestAgedIndex` mechanism is unnecessary because
56
56
  * the nullifier chain prevents out-of-order index usage: no device can fill in a lower index after a higher
57
57
  * one is already on-chain.
58
- * - The scan stops at the first gap in the index sequence. If all indexes in the batch have logs, the window
59
- * advances. If a gap is found, the sequence has ended and no further logs exist.
60
- * - The upper bound is the same as unconstrained: `highestFinalizedIndex + WINDOW_LEN`.
58
+ * - Because the sequence is gapless, the scan stops at the first missing index: that gap proves the sequence has
59
+ * ended and no further logs exist. We exploit this with a doubling first-miss probe (see
60
+ * `INITIAL_CONSTRAINED_PROBE_LEN` in ../constants.ts), so a
61
+ * steady-state sync with no new logs costs two tags instead of the whole window. The probe length is in-memory
62
+ * state scoped to one sync call, so every sync restarts at the initial probe.
63
+ * - The upper bound is the same as unconstrained: `highestFinalizedIndex + WINDOW_LEN`. Advancing the probe is
64
+ * decoupled from persisting the finalized index, so unfinalized logs at the top of the run are still fetched
65
+ * while only the finalized prefix is persisted.
61
66
  *
62
67
  * # Batching across secrets
63
68
  *
@@ -87,8 +92,7 @@ import { findHighestIndexes } from './utils/find_highest_indexes.js';
87
92
  allLogs.push(...logsFoundWithSecret.map(({ log })=>log));
88
93
  // Persist new indexes. If the finalized index moved forward, the window advances
89
94
  // and we need another round for this secret.
90
- const processForKind = pendingSecret.secret.kind === AppTaggingSecretKind.CONSTRAINED ? processConstrainedResults : processUnconstrainedResults;
91
- return await processForKind(pendingSecret, logsFoundWithSecret, taggingStore, currentTimestamp, finalizedBlockNumber, jobId);
95
+ return pendingSecret.kind === AppTaggingSecretKind.CONSTRAINED ? await processConstrainedResults(pendingSecret, logsFoundWithSecret, taggingStore, currentTimestamp, finalizedBlockNumber, jobId) : await processUnconstrainedResults(pendingSecret, logsFoundWithSecret, taggingStore, currentTimestamp, finalizedBlockNumber, jobId);
92
96
  }));
93
97
  pending = nextRound.filter(isDefined);
94
98
  }
@@ -97,13 +101,29 @@ import { findHighestIndexes } from './utils/find_highest_indexes.js';
97
101
  /** Reads stored indexes for each secret and computes the initial index range to query. */ function getIndexRangesForSecrets(secrets, taggingStore, jobId) {
98
102
  return Promise.all(secrets.map(async (secret)=>{
99
103
  const currentHighestFinalizedIndex = await taggingStore.getHighestFinalizedIndex(secret, jobId);
100
- const highestIndexBeforeStart = secret.kind === AppTaggingSecretKind.CONSTRAINED ? currentHighestFinalizedIndex : await taggingStore.getHighestAgedIndex(secret, jobId);
101
- const start = highestIndexBeforeStart === undefined ? 0 : highestIndexBeforeStart + 1;
102
- const end = (currentHighestFinalizedIndex ?? 0) + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1;
104
+ const boundEnd = (currentHighestFinalizedIndex ?? 0) + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1;
105
+ if (secret.kind === AppTaggingSecretKind.CONSTRAINED) {
106
+ // Constrained streams are gapless and resume at the finalized index, so probe a small initial window and stop
107
+ // at the first missing tag instead of always fetching the full window. The probe grows each round up to the
108
+ // window cap.
109
+ const start = currentHighestFinalizedIndex === undefined ? 0 : currentHighestFinalizedIndex + 1;
110
+ return {
111
+ kind: AppTaggingSecretKind.CONSTRAINED,
112
+ secret,
113
+ start,
114
+ end: Math.min(boundEnd, start + INITIAL_CONSTRAINED_PROBE_LEN),
115
+ boundEnd,
116
+ probeLen: INITIAL_CONSTRAINED_PROBE_LEN
117
+ };
118
+ }
119
+ // Unconstrained secrets can have gaps, so they scan the whole window starting past the highest aged index.
120
+ const highestAgedIndex = await taggingStore.getHighestAgedIndex(secret, jobId);
121
+ const start = highestAgedIndex === undefined ? 0 : highestAgedIndex + 1;
103
122
  return {
123
+ kind: AppTaggingSecretKind.UNCONSTRAINED,
104
124
  secret,
105
125
  start,
106
- end
126
+ end: boundEnd
107
127
  };
108
128
  }));
109
129
  }
@@ -148,30 +168,43 @@ import { findHighestIndexes } from './utils/find_highest_indexes.js';
148
168
  return logsPerSecret;
149
169
  }
150
170
  /**
151
- * Processes fetched logs for a constrained secret. Constrained delivery guarantees contiguous index sequences,
152
- * so we scan from the start of the queried range and stop at the first missing index. Only `highestFinalizedIndex`
171
+ * Processes fetched logs for a constrained secret: persists the finalized index and advances the probe. See the
172
+ * "# Constrained secrets" section above for why the scan stops at the first missing index. Only `highestFinalizedIndex`
153
173
  * is tracked (no aged index).
154
174
  */ async function processConstrainedResults(pending, logsWithIndexes, taggingStore, currentTimestamp, finalizedBlockNumber, jobId) {
155
- // Find where the contiguous run of indexes ends. Constrained delivery guarantees there are no logs after the
156
- // first gap, so all logs in the batch are within this prefix.
175
+ // Find where the contiguous run of indexes ends; all logs in the batch fall within this prefix.
157
176
  const indexesWithLogs = new Set(logsWithIndexes.map((l)=>l.taggingIndex));
158
177
  let firstMissingIndex = pending.start;
159
178
  while(firstMissingIndex < pending.end && indexesWithLogs.has(firstMissingIndex)){
160
179
  firstMissingIndex++;
161
180
  }
162
- // Finalize any logs before the gap. The nullifier for a constrained-delivery log is emitted in the same
163
- // transaction, guaranteeing the log cannot disappear once included, so we persist the highest finalized
164
- // index even when a gap stops the scan. This lets the next sync round skip already-finalized indexes.
181
+ // Persist the highest finalized index found. The nullifier for a constrained-delivery log is emitted in the same
182
+ // transaction, guaranteeing the log cannot disappear once included, so we persist even when a gap stops the scan.
183
+ // This lets the next sync round skip already-finalized indexes.
165
184
  const { highestFinalizedIndex } = findHighestIndexes(logsWithIndexes, currentTimestamp, finalizedBlockNumber);
166
185
  if (highestFinalizedIndex !== undefined) {
167
186
  await taggingStore.updateHighestFinalizedIndex(pending.secret, highestFinalizedIndex, jobId);
168
- if (firstMissingIndex >= pending.end) {
169
- return {
170
- secret: pending.secret,
171
- start: pending.end,
172
- end: highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1
173
- };
174
- }
187
+ }
188
+ // Advancing the probe is decoupled from persisting the finalized index: keep scanning as long as the probe was
189
+ // entirely hits (no gap) and there is room before the protocol bound, even if none of those hits is finalized yet.
190
+ // Gating this on finalization would drop logs in unfinalized blocks, which sit at the top of the contiguous run.
191
+ // The bound re-anchors to any finalized index found this round.
192
+ const probeFullyConsumed = firstMissingIndex >= pending.end;
193
+ const boundEnd = highestFinalizedIndex !== undefined ? Math.max(pending.boundEnd, highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1) : pending.boundEnd;
194
+ // Double the probe each round, capped at the window (see INITIAL_CONSTRAINED_PROBE_LEN in ../constants.ts).
195
+ // The queried range is already bounded by boundEnd (a probe can never reach
196
+ // more than WINDOW_LEN past the finalized frontier); this cap just keeps the stored length from growing unbounded
197
+ // once the probe saturates the window.
198
+ const nextProbeLen = Math.min(pending.probeLen * 2, UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN);
199
+ if (probeFullyConsumed && pending.end < boundEnd) {
200
+ return {
201
+ kind: AppTaggingSecretKind.CONSTRAINED,
202
+ secret: pending.secret,
203
+ start: pending.end,
204
+ end: Math.min(boundEnd, pending.end + nextProbeLen),
205
+ boundEnd,
206
+ probeLen: nextProbeLen
207
+ };
175
208
  }
176
209
  return undefined;
177
210
  }
@@ -195,9 +228,11 @@ import { findHighestIndexes } from './utils/find_highest_indexes.js';
195
228
  await taggingStore.updateHighestFinalizedIndex(pending.secret, highestFinalizedIndex, jobId);
196
229
  // For the next iteration we want to look only at indexes for which we have not yet fetched logs while
197
230
  // ensuring that we do not look further than WINDOW_LEN ahead of the highest finalized index.
231
+ const end = highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1;
198
232
  return {
233
+ kind: AppTaggingSecretKind.UNCONSTRAINED,
199
234
  secret: pending.secret,
200
235
  start: pending.end,
201
- end: highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1
236
+ end
202
237
  };
203
238
  }
@@ -0,0 +1,6 @@
1
+ import { type AppTaggingSecret, type PrivateLogsQuery, SiloedTag } from '@aztec/stdlib/logs';
2
+ /** Computes the siloed tag */
3
+ export declare function computeSiloedTagForIndex(secret: AppTaggingSecret, index: number): Promise<SiloedTag>;
4
+ /** Extracts the bare-tag set from a query */
5
+ export declare function extractTags(query: PrivateLogsQuery): SiloedTag[];
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFnX3F1ZXJ5X3Rlc3RfdXRpbHMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy90YWdnaW5nL3Rlc3RpbmcvdGFnX3F1ZXJ5X3Rlc3RfdXRpbHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLEtBQUssZ0JBQWdCLEVBQUUsS0FBSyxnQkFBZ0IsRUFBRSxTQUFTLEVBQWlCLE1BQU0sb0JBQW9CLENBQUM7QUFFNUcsOEJBQThCO0FBQzlCLHdCQUFnQix3QkFBd0IsQ0FBQyxNQUFNLEVBQUUsZ0JBQWdCLEVBQUUsS0FBSyxFQUFFLE1BQU0sR0FBRyxPQUFPLENBQUMsU0FBUyxDQUFDLENBRXBHO0FBRUQsNkNBQTZDO0FBQzdDLHdCQUFnQixXQUFXLENBQUMsS0FBSyxFQUFFLGdCQUFnQixHQUFHLFNBQVMsRUFBRSxDQUVoRSJ9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag_query_test_utils.d.ts","sourceRoot":"","sources":["../../../src/tagging/testing/tag_query_test_utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAiB,MAAM,oBAAoB,CAAC;AAE5G,8BAA8B;AAC9B,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAEpG;AAED,6CAA6C;AAC7C,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,SAAS,EAAE,CAEhE"}
@@ -0,0 +1,10 @@
1
+ import { SiloedTag } from '@aztec/stdlib/logs';
2
+ /** Computes the siloed tag */ export function computeSiloedTagForIndex(secret, index) {
3
+ return SiloedTag.compute({
4
+ extendedSecret: secret,
5
+ index
6
+ });
7
+ }
8
+ /** Extracts the bare-tag set from a query */ export function extractTags(query) {
9
+ return query.tags.map((entry)=>entry instanceof SiloedTag ? entry : entry.tag);
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/pxe",
3
- "version": "5.1.0",
3
+ "version": "5.2.0-nightly.20260724",
4
4
  "type": "module",
5
5
  "typedocOptions": {
6
6
  "entryPoints": [
@@ -71,20 +71,20 @@
71
71
  ]
72
72
  },
73
73
  "dependencies": {
74
- "@aztec/bb-prover": "5.1.0",
75
- "@aztec/bb.js": "5.1.0",
76
- "@aztec/builder": "5.1.0",
77
- "@aztec/constants": "5.1.0",
78
- "@aztec/ethereum": "5.1.0",
79
- "@aztec/foundation": "5.1.0",
80
- "@aztec/key-store": "5.1.0",
81
- "@aztec/kv-store": "5.1.0",
82
- "@aztec/noir-protocol-circuits-types": "5.1.0",
83
- "@aztec/noir-types": "5.1.0",
84
- "@aztec/protocol-contracts": "5.1.0",
85
- "@aztec/simulator": "5.1.0",
86
- "@aztec/standard-contracts": "5.1.0",
87
- "@aztec/stdlib": "5.1.0",
74
+ "@aztec/bb-prover": "5.2.0-nightly.20260724",
75
+ "@aztec/bb.js": "5.2.0-nightly.20260724",
76
+ "@aztec/builder": "5.2.0-nightly.20260724",
77
+ "@aztec/constants": "5.2.0-nightly.20260724",
78
+ "@aztec/ethereum": "5.2.0-nightly.20260724",
79
+ "@aztec/foundation": "5.2.0-nightly.20260724",
80
+ "@aztec/key-store": "5.2.0-nightly.20260724",
81
+ "@aztec/kv-store": "5.2.0-nightly.20260724",
82
+ "@aztec/noir-protocol-circuits-types": "5.2.0-nightly.20260724",
83
+ "@aztec/noir-types": "5.2.0-nightly.20260724",
84
+ "@aztec/protocol-contracts": "5.2.0-nightly.20260724",
85
+ "@aztec/simulator": "5.2.0-nightly.20260724",
86
+ "@aztec/standard-contracts": "5.2.0-nightly.20260724",
87
+ "@aztec/stdlib": "5.2.0-nightly.20260724",
88
88
  "koa": "^2.16.1",
89
89
  "koa-router": "^13.1.1",
90
90
  "lodash.omit": "^4.5.0",
@@ -93,8 +93,8 @@
93
93
  "viem": "npm:@aztec/viem@2.38.2"
94
94
  },
95
95
  "devDependencies": {
96
- "@aztec/noir-test-contracts.js": "5.1.0",
97
- "@aztec/world-state": "5.1.0",
96
+ "@aztec/noir-test-contracts.js": "5.2.0-nightly.20260724",
97
+ "@aztec/world-state": "5.2.0-nightly.20260724",
98
98
  "@jest/globals": "^30.0.0",
99
99
  "@types/jest": "^30.0.0",
100
100
  "@types/lodash.omit": "^4.5.7",
@@ -1,3 +1,3 @@
1
1
  export function getPackageInfo() {
2
- return { version: '5.1.0', name: '@aztec/pxe' };
2
+ return { version: '5.2.0', name: '@aztec/pxe' };
3
3
  }
@@ -18,3 +18,13 @@ import { MAX_PRIVATE_LOGS_PER_TX } from '@aztec/constants';
18
18
  // reserved at log emission time, before squashing is decided, and the kernel's reset/squash loop is not bounded by
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
+
22
+ // The number of tags probed per constrained secret in the first round.
23
+ //
24
+ // The probe doubles each round (2, 4, 8, ..., capped at UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN) while every probed
25
+ // index is a hit, stopping at the first missing tag. Constrained delivery is gapless, so a single missing tag proves
26
+ // the stream has ended: at steady state this turns a full WINDOW_LEN probe into two tags. A secret K logs behind
27
+ // catches up in ~log2(K) round-trips while the probe is still doubling (2, 4, 8, 16, 32), but once it saturates the cap
28
+ // and advances WINDOW_LEN tags per round, deeper catch-up is linear at ~K/WINDOW_LEN rounds. Either way it beats both
29
+ // the full window every round and one round per log.
30
+ export const INITIAL_CONSTRAINED_PROBE_LEN = 2;
@@ -12,7 +12,7 @@
12
12
  export { syncTaggedPrivateLogs } from './recipient_sync/sync_tagged_private_logs.js';
13
13
  export { syncSenderTaggingIndexes } from './sender_sync/sync_sender_tagging_indexes.js';
14
14
  export { persistSenderTaggingIndexRangesForTx } from './persist_sender_tagging_index_ranges.js';
15
- export { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from './constants.js';
15
+ export { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN, INITIAL_CONSTRAINED_PROBE_LEN } from './constants.js';
16
16
  export { getAllPrivateLogsByTags, getAllPublicLogsByTagsFromContract } from './get_all_logs_by_tags.js';
17
17
 
18
18
  // Re-export tagging-related types from stdlib
@@ -7,7 +7,7 @@ import { AppTaggingSecretKind, SiloedTag } from '@aztec/stdlib/logs';
7
7
  import type { BlockHeader } from '@aztec/stdlib/tx';
8
8
 
9
9
  import type { RecipientTaggingStore } from '../../storage/tagging_store/recipient_tagging_store.js';
10
- import { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../constants.js';
10
+ import { INITIAL_CONSTRAINED_PROBE_LEN, UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../constants.js';
11
11
  import { getAllPrivateLogsByTags } from '../get_all_logs_by_tags.js';
12
12
  import { findHighestIndexes } from './utils/find_highest_indexes.js';
13
13
 
@@ -62,9 +62,14 @@ import { findHighestIndexes } from './utils/find_highest_indexes.js';
62
62
  * - The lower bound is `highestFinalizedIndex + 1`. The `highestAgedIndex` mechanism is unnecessary because
63
63
  * the nullifier chain prevents out-of-order index usage: no device can fill in a lower index after a higher
64
64
  * one is already on-chain.
65
- * - The scan stops at the first gap in the index sequence. If all indexes in the batch have logs, the window
66
- * advances. If a gap is found, the sequence has ended and no further logs exist.
67
- * - The upper bound is the same as unconstrained: `highestFinalizedIndex + WINDOW_LEN`.
65
+ * - Because the sequence is gapless, the scan stops at the first missing index: that gap proves the sequence has
66
+ * ended and no further logs exist. We exploit this with a doubling first-miss probe (see
67
+ * `INITIAL_CONSTRAINED_PROBE_LEN` in ../constants.ts), so a
68
+ * steady-state sync with no new logs costs two tags instead of the whole window. The probe length is in-memory
69
+ * state scoped to one sync call, so every sync restarts at the initial probe.
70
+ * - The upper bound is the same as unconstrained: `highestFinalizedIndex + WINDOW_LEN`. Advancing the probe is
71
+ * decoupled from persisting the finalized index, so unfinalized logs at the top of the run are still fetched
72
+ * while only the finalized prefix is persisted.
68
73
  *
69
74
  * # Batching across secrets
70
75
  *
@@ -109,19 +114,23 @@ export async function syncTaggedPrivateLogs(
109
114
 
110
115
  // Persist new indexes. If the finalized index moved forward, the window advances
111
116
  // and we need another round for this secret.
112
- const processForKind =
113
- pendingSecret.secret.kind === AppTaggingSecretKind.CONSTRAINED
114
- ? processConstrainedResults
115
- : processUnconstrainedResults;
116
-
117
- return await processForKind(
118
- pendingSecret,
119
- logsFoundWithSecret,
120
- taggingStore,
121
- currentTimestamp,
122
- finalizedBlockNumber,
123
- jobId,
124
- );
117
+ return pendingSecret.kind === AppTaggingSecretKind.CONSTRAINED
118
+ ? await processConstrainedResults(
119
+ pendingSecret,
120
+ logsFoundWithSecret,
121
+ taggingStore,
122
+ currentTimestamp,
123
+ finalizedBlockNumber,
124
+ jobId,
125
+ )
126
+ : await processUnconstrainedResults(
127
+ pendingSecret,
128
+ logsFoundWithSecret,
129
+ taggingStore,
130
+ currentTimestamp,
131
+ finalizedBlockNumber,
132
+ jobId,
133
+ );
125
134
  }),
126
135
  );
127
136
 
@@ -138,18 +147,29 @@ function getIndexRangesForSecrets(
138
147
  jobId: string,
139
148
  ): Promise<PendingSecret[]> {
140
149
  return Promise.all(
141
- secrets.map(async secret => {
150
+ secrets.map(async (secret): Promise<PendingSecret> => {
142
151
  const currentHighestFinalizedIndex = await taggingStore.getHighestFinalizedIndex(secret, jobId);
152
+ const boundEnd = (currentHighestFinalizedIndex ?? 0) + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1;
153
+
154
+ if (secret.kind === AppTaggingSecretKind.CONSTRAINED) {
155
+ // Constrained streams are gapless and resume at the finalized index, so probe a small initial window and stop
156
+ // at the first missing tag instead of always fetching the full window. The probe grows each round up to the
157
+ // window cap.
158
+ const start = currentHighestFinalizedIndex === undefined ? 0 : currentHighestFinalizedIndex + 1;
159
+ return {
160
+ kind: AppTaggingSecretKind.CONSTRAINED,
161
+ secret,
162
+ start,
163
+ end: Math.min(boundEnd, start + INITIAL_CONSTRAINED_PROBE_LEN),
164
+ boundEnd,
165
+ probeLen: INITIAL_CONSTRAINED_PROBE_LEN,
166
+ };
167
+ }
143
168
 
144
- const highestIndexBeforeStart =
145
- secret.kind === AppTaggingSecretKind.CONSTRAINED
146
- ? currentHighestFinalizedIndex
147
- : await taggingStore.getHighestAgedIndex(secret, jobId);
148
- const start = highestIndexBeforeStart === undefined ? 0 : highestIndexBeforeStart + 1;
149
-
150
- const end = (currentHighestFinalizedIndex ?? 0) + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1;
151
-
152
- return { secret, start, end };
169
+ // Unconstrained secrets can have gaps, so they scan the whole window starting past the highest aged index.
170
+ const highestAgedIndex = await taggingStore.getHighestAgedIndex(secret, jobId);
171
+ const start = highestAgedIndex === undefined ? 0 : highestAgedIndex + 1;
172
+ return { kind: AppTaggingSecretKind.UNCONSTRAINED, secret, start, end: boundEnd };
153
173
  }),
154
174
  );
155
175
  }
@@ -203,41 +223,58 @@ async function fetchLogsForSecrets(
203
223
  }
204
224
 
205
225
  /**
206
- * Processes fetched logs for a constrained secret. Constrained delivery guarantees contiguous index sequences,
207
- * so we scan from the start of the queried range and stop at the first missing index. Only `highestFinalizedIndex`
226
+ * Processes fetched logs for a constrained secret: persists the finalized index and advances the probe. See the
227
+ * "# Constrained secrets" section above for why the scan stops at the first missing index. Only `highestFinalizedIndex`
208
228
  * is tracked (no aged index).
209
229
  */
210
230
  async function processConstrainedResults(
211
- pending: PendingSecret,
231
+ pending: PendingConstrained,
212
232
  logsWithIndexes: LogWithIndex[],
213
233
  taggingStore: RecipientTaggingStore,
214
234
  currentTimestamp: bigint,
215
235
  finalizedBlockNumber: BlockNumber,
216
236
  jobId: string,
217
237
  ): Promise<PendingSecret | undefined> {
218
- // Find where the contiguous run of indexes ends. Constrained delivery guarantees there are no logs after the
219
- // first gap, so all logs in the batch are within this prefix.
238
+ // Find where the contiguous run of indexes ends; all logs in the batch fall within this prefix.
220
239
  const indexesWithLogs = new Set(logsWithIndexes.map(l => l.taggingIndex));
221
240
  let firstMissingIndex = pending.start;
222
241
  while (firstMissingIndex < pending.end && indexesWithLogs.has(firstMissingIndex)) {
223
242
  firstMissingIndex++;
224
243
  }
225
244
 
226
- // Finalize any logs before the gap. The nullifier for a constrained-delivery log is emitted in the same
227
- // transaction, guaranteeing the log cannot disappear once included, so we persist the highest finalized
228
- // index even when a gap stops the scan. This lets the next sync round skip already-finalized indexes.
245
+ // Persist the highest finalized index found. The nullifier for a constrained-delivery log is emitted in the same
246
+ // transaction, guaranteeing the log cannot disappear once included, so we persist even when a gap stops the scan.
247
+ // This lets the next sync round skip already-finalized indexes.
229
248
  const { highestFinalizedIndex } = findHighestIndexes(logsWithIndexes, currentTimestamp, finalizedBlockNumber);
230
-
231
249
  if (highestFinalizedIndex !== undefined) {
232
250
  await taggingStore.updateHighestFinalizedIndex(pending.secret, highestFinalizedIndex, jobId);
251
+ }
233
252
 
234
- if (firstMissingIndex >= pending.end) {
235
- return {
236
- secret: pending.secret,
237
- start: pending.end,
238
- end: highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1,
239
- };
240
- }
253
+ // Advancing the probe is decoupled from persisting the finalized index: keep scanning as long as the probe was
254
+ // entirely hits (no gap) and there is room before the protocol bound, even if none of those hits is finalized yet.
255
+ // Gating this on finalization would drop logs in unfinalized blocks, which sit at the top of the contiguous run.
256
+ // The bound re-anchors to any finalized index found this round.
257
+ const probeFullyConsumed = firstMissingIndex >= pending.end;
258
+ const boundEnd =
259
+ highestFinalizedIndex !== undefined
260
+ ? Math.max(pending.boundEnd, highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1)
261
+ : pending.boundEnd;
262
+
263
+ // Double the probe each round, capped at the window (see INITIAL_CONSTRAINED_PROBE_LEN in ../constants.ts).
264
+ // The queried range is already bounded by boundEnd (a probe can never reach
265
+ // more than WINDOW_LEN past the finalized frontier); this cap just keeps the stored length from growing unbounded
266
+ // once the probe saturates the window.
267
+ const nextProbeLen = Math.min(pending.probeLen * 2, UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN);
268
+
269
+ if (probeFullyConsumed && pending.end < boundEnd) {
270
+ return {
271
+ kind: AppTaggingSecretKind.CONSTRAINED,
272
+ secret: pending.secret,
273
+ start: pending.end,
274
+ end: Math.min(boundEnd, pending.end + nextProbeLen),
275
+ boundEnd,
276
+ probeLen: nextProbeLen,
277
+ };
241
278
  }
242
279
 
243
280
  return undefined;
@@ -248,7 +285,7 @@ async function processConstrainedResults(
248
285
  * if the window needs to advance, or undefined if this secret is done.
249
286
  */
250
287
  async function processUnconstrainedResults(
251
- pending: PendingSecret,
288
+ pending: PendingUnconstrained,
252
289
  logsWithIndexes: LogWithIndex[],
253
290
  taggingStore: RecipientTaggingStore,
254
291
  currentTimestamp: bigint,
@@ -282,19 +319,39 @@ async function processUnconstrainedResults(
282
319
 
283
320
  // For the next iteration we want to look only at indexes for which we have not yet fetched logs while
284
321
  // 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;
285
323
  return {
324
+ kind: AppTaggingSecretKind.UNCONSTRAINED,
286
325
  secret: pending.secret,
287
326
  start: pending.end,
288
- end: highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1,
327
+ end,
289
328
  };
290
329
  }
291
330
 
292
- type PendingSecret = {
331
+ /** Working scan state common to both secret kinds: the half-open [start, end) tag-index range to query this round. */
332
+ type PendingSecretBase = {
293
333
  secret: AppTaggingSecret;
294
334
  start: number;
295
335
  end: number;
296
336
  };
297
337
 
338
+ /** Constrained scan state, carrying the doubling-probe bookkeeping that only the gapless constrained scan uses. */
339
+ type PendingConstrained = PendingSecretBase & {
340
+ 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.
343
+ boundEnd: number;
344
+ // Intended probe length for the round that produced `end`. The queried span can be smaller when boundEnd caps it.
345
+ probeLen: number;
346
+ };
347
+
348
+ /** Unconstrained scan state: a plain window with no probe bookkeeping, since the scan always covers the full window. */
349
+ type PendingUnconstrained = PendingSecretBase & {
350
+ kind: typeof AppTaggingSecretKind.UNCONSTRAINED;
351
+ };
352
+
353
+ type PendingSecret = PendingConstrained | PendingUnconstrained;
354
+
298
355
  type LogWithIndex = {
299
356
  log: LogResult;
300
357
  taggingIndex: number;
@@ -0,0 +1,11 @@
1
+ import { type AppTaggingSecret, type PrivateLogsQuery, SiloedTag, type TagQuery } from '@aztec/stdlib/logs';
2
+
3
+ /** Computes the siloed tag */
4
+ export function computeSiloedTagForIndex(secret: AppTaggingSecret, index: number): Promise<SiloedTag> {
5
+ return SiloedTag.compute({ extendedSecret: secret, index });
6
+ }
7
+
8
+ /** Extracts the bare-tag set from a query */
9
+ export function extractTags(query: PrivateLogsQuery): SiloedTag[] {
10
+ return query.tags.map((entry: TagQuery<SiloedTag>) => (entry instanceof SiloedTag ? entry : entry.tag));
11
+ }