Skip to content

Commit bbbf38b

Browse files
authored
refactor: getLogsByTags request batching in syncTaggedLogs (#10716)
1 parent 1b1306c commit bbbf38b

File tree

5 files changed

+149
-148
lines changed

5 files changed

+149
-148
lines changed

yarn-project/circuit-types/src/interfaces/aztec-node.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@ export interface AztecNode
315315
* Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
316316
* @param tags - The tags to filter the logs by.
317317
* @returns For each received tag, an array of matching logs and metadata (e.g. tx hash) is returned. An empty
318-
array implies no logs match that tag.
318+
* array implies no logs match that tag. There can be multiple logs for 1 tag because tag reuse can happen
319+
* --> e.g. when sending a note from multiple unsynched devices.
319320
*/
320321
getLogsByTags(tags: Fr[]): Promise<TxScopedL2Log[][]>;
321322

yarn-project/circuits.js/src/structs/indexed_tagging_secret.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { poseidon2Hash } from '@aztec/foundation/crypto';
33
import { Fr } from '@aztec/foundation/fields';
44

55
export class IndexedTaggingSecret {
6-
constructor(public appTaggingSecret: Fr, public index: number) {}
6+
constructor(public appTaggingSecret: Fr, public index: number) {
7+
if (index < 0) {
8+
throw new Error('IndexedTaggingSecret index out of bounds');
9+
}
10+
}
711

812
toFields(): Fr[] {
913
return [this.appTaggingSecret, new Fr(this.index)];

yarn-project/pxe/src/simulator_oracle/index.ts

+111-90
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { type IncomingNoteDao } from '../database/incoming_note_dao.js';
3838
import { type PxeDatabase } from '../database/index.js';
3939
import { produceNoteDaos } from '../note_decryption_utils/produce_note_daos.js';
4040
import { getAcirSimulator } from '../simulator/index.js';
41-
import { getInitialIndexes, getLeftMostIndexedTaggingSecrets, getRightMostIndexes } from './tagging_utils.js';
41+
import { getIndexedTaggingSecretsForTheWindow, getInitialIndexesMap } from './tagging_utils.js';
4242

4343
/**
4444
* A data oracle that provides information needed for simulating a transaction.
@@ -424,116 +424,137 @@ export class SimulatorOracle implements DBOracle {
424424
// Half the size of the window we slide over the tagging secret indexes.
425425
const WINDOW_HALF_SIZE = 10;
426426

427+
// Ideally this algorithm would be implemented in noir, exposing its building blocks as oracles.
428+
// However it is impossible at the moment due to the language not supporting nested slices.
429+
// This nesting is necessary because for a given set of tags we don't
430+
// know how many logs we will get back. Furthermore, these logs are of undetermined
431+
// length, since we don't really know the note they correspond to until we decrypt them.
432+
427433
const recipients = scopes ? scopes : await this.keyStore.getAccounts();
428-
// A map of never-before-seen logs going from recipient address to logs
429-
const newLogsMap = new Map<string, TxScopedL2Log[]>();
434+
// A map of logs going from recipient address to logs. Note that the logs might have been processed before
435+
// due to us having a sliding window that "looks back" for logs as well. (We look back as there is no guarantee
436+
// that a logs will be received ordered by a given tax index and that the tags won't be reused).
437+
const logsMap = new Map<string, TxScopedL2Log[]>();
430438
const contractName = await this.contractDataOracle.getDebugContractName(contractAddress);
431439
for (const recipient of recipients) {
432-
const logs: TxScopedL2Log[] = [];
433-
// Ideally this algorithm would be implemented in noir, exposing its building blocks as oracles.
434-
// However it is impossible at the moment due to the language not supporting nested slices.
435-
// This nesting is necessary because for a given set of tags we don't
436-
// know how many logs we will get back. Furthermore, these logs are of undetermined
437-
// length, since we don't really know the note they correspond to until we decrypt them.
438-
439-
// 1. Get all the secrets for the recipient and sender pairs (#9365)
440-
const indexedTaggingSecrets = await this.#getIndexedTaggingSecretsForContacts(contractAddress, recipient);
441-
442-
// 1.1 Set up a sliding window with an offset. Chances are the sender might have messed up
443-
// and inadvertently incremented their index without us getting any logs (for example, in case
444-
// of a revert). If we stopped looking for logs the first time we don't receive any logs for a tag,
445-
// we might never receive anything from that sender again.
446-
// Also there's a possibility that we have advanced our index, but the sender has reused it,
447-
// so we might have missed some logs. For these reasons, we have to look both back and ahead of
448-
// the stored index.
449-
450-
// App tagging secrets along with an index in a window to check in the current iteration. Called current because
451-
// this value will be updated as we iterate through the window.
452-
let currentSecrets = getLeftMostIndexedTaggingSecrets(indexedTaggingSecrets, WINDOW_HALF_SIZE);
453-
// Right-most indexes in a window to check stored in a key-value map where key is the app tagging secret
454-
// and value is the index to check (the right-most index in the window).
455-
const rightMostIndexesMap = getRightMostIndexes(indexedTaggingSecrets, WINDOW_HALF_SIZE);
440+
const logsForRecipient: TxScopedL2Log[] = [];
441+
442+
// Get all the secrets for the recipient and sender pairs (#9365)
443+
const secrets = await this.#getIndexedTaggingSecretsForContacts(contractAddress, recipient);
444+
445+
// We fetch logs for a window of indexes in a range:
446+
// <latest_log_index - WINDOW_HALF_SIZE, latest_log_index + WINDOW_HALF_SIZE>.
447+
//
448+
// We use this window approach because it could happen that a sender might have messed up and inadvertently
449+
// incremented their index without us getting any logs (for example, in case of a revert). If we stopped looking
450+
// for logs the first time we don't receive any logs for a tag, we might never receive anything from that sender again.
451+
// Also there's a possibility that we have advanced our index, but the sender has reused it, so we might have missed
452+
// some logs. For these reasons, we have to look both back and ahead of the stored index.
453+
let secretsAndWindows = secrets.map(secret => {
454+
return {
455+
appTaggingSecret: secret.appTaggingSecret,
456+
leftMostIndex: Math.max(0, secret.index - WINDOW_HALF_SIZE),
457+
rightMostIndex: secret.index + WINDOW_HALF_SIZE,
458+
};
459+
});
460+
461+
// As we iterate we store the largest index we have seen for a given secret to later on store it in the db.
462+
const newLargestIndexMapToStore: { [k: string]: number } = {};
463+
456464
// The initial/unmodified indexes of the secrets stored in a key-value map where key is the app tagging secret.
457-
const initialIndexesMap = getInitialIndexes(indexedTaggingSecrets);
458-
// A map of indexes to increment for secrets for which we have found logs with an index higher than the one
459-
// stored.
460-
const indexesToIncrementMap: { [k: string]: number } = {};
461-
462-
while (currentSecrets.length > 0) {
463-
// 2. Compute tags using the secrets, recipient and index. Obtain logs for each tag (#9380)
464-
const currentTags = currentSecrets.map(secret =>
465-
// We compute the siloed tags since we need the tags as they appear in the log.
465+
const initialIndexesMap = getInitialIndexesMap(secrets);
466+
467+
while (secretsAndWindows.length > 0) {
468+
const secretsForTheWholeWindow = getIndexedTaggingSecretsForTheWindow(secretsAndWindows);
469+
const tagsForTheWholeWindow = secretsForTheWholeWindow.map(secret =>
466470
secret.computeSiloedTag(recipient, contractAddress),
467471
);
468472

473+
// We store the new largest indexes we find in the iteration in the following map to later on construct
474+
// a new set of secrets and windows to fetch logs for.
475+
const newLargestIndexMapForIteration: { [k: string]: number } = {};
476+
469477
// Fetch the logs for the tags and iterate over them
470-
const logsByTags = await this.aztecNode.getLogsByTags(currentTags);
471-
const secretsWithNewIndex: IndexedTaggingSecret[] = [];
478+
const logsByTags = await this.aztecNode.getLogsByTags(tagsForTheWholeWindow);
479+
472480
logsByTags.forEach((logsByTag, logIndex) => {
473-
const { appTaggingSecret: currentSecret, index: currentIndex } = currentSecrets[logIndex];
474-
const currentSecretAsStr = currentSecret.toString();
475-
this.log.debug(`Syncing logs for recipient ${recipient} at contract ${contractName}(${contractAddress})`, {
476-
recipient,
477-
secret: currentSecret,
478-
index: currentIndex,
479-
contractName,
480-
contractAddress,
481-
});
482-
// 3.1. Append logs to the list and increment the index for the tags that have logs (#9380)
483481
if (logsByTag.length > 0) {
484-
const newIndex = currentIndex + 1;
485-
this.log.debug(
486-
`Found ${logsByTag.length} logs as recipient ${recipient}. Incrementing index to ${newIndex} at contract ${contractName}(${contractAddress})`,
487-
{
488-
recipient,
489-
secret: currentSecret,
490-
newIndex,
491-
contractName,
492-
contractAddress,
493-
},
494-
);
495-
logs.push(...logsByTag);
496-
497-
if (currentIndex >= initialIndexesMap[currentSecretAsStr]) {
498-
// 3.2. We found an index higher than the stored/initial one so we update it in the db later on (#9380)
499-
indexesToIncrementMap[currentSecretAsStr] = newIndex;
500-
// 3.3. We found an index higher than the initial one so we slide the window.
501-
rightMostIndexesMap[currentSecretAsStr] = currentIndex + WINDOW_HALF_SIZE;
482+
// The logs for the given tag exist so we store them for later processing
483+
logsForRecipient.push(...logsByTag);
484+
485+
// We retrieve the indexed tagging secret corresponding to the log as I need that to evaluate whether
486+
// a new largest index have been found.
487+
const secretCorrespondingToLog = secretsForTheWholeWindow[logIndex];
488+
const initialIndex = initialIndexesMap[secretCorrespondingToLog.appTaggingSecret.toString()];
489+
490+
this.log.debug(`Found ${logsByTag.length} logs as recipient ${recipient}`, {
491+
recipient,
492+
secret: secretCorrespondingToLog.appTaggingSecret,
493+
contractName,
494+
contractAddress,
495+
});
496+
497+
if (
498+
secretCorrespondingToLog.index >= initialIndex &&
499+
(newLargestIndexMapForIteration[secretCorrespondingToLog.appTaggingSecret.toString()] === undefined ||
500+
secretCorrespondingToLog.index >=
501+
newLargestIndexMapForIteration[secretCorrespondingToLog.appTaggingSecret.toString()])
502+
) {
503+
// We have found a new largest index so we store it for later processing (storing it in the db + fetching
504+
// the difference of the window sets of current and the next iteration)
505+
newLargestIndexMapForIteration[secretCorrespondingToLog.appTaggingSecret.toString()] =
506+
secretCorrespondingToLog.index + 1;
507+
508+
this.log.debug(
509+
`Incrementing index to ${
510+
secretCorrespondingToLog.index + 1
511+
} at contract ${contractName}(${contractAddress})`,
512+
);
502513
}
503514
}
504-
// 3.4 Keep increasing the index (inside the window) temporarily for the tags that have no logs
505-
// There's a chance the sender missed some and we want to catch up
506-
if (currentIndex < rightMostIndexesMap[currentSecretAsStr]) {
507-
const newTaggingSecret = new IndexedTaggingSecret(currentSecret, currentIndex + 1);
508-
secretsWithNewIndex.push(newTaggingSecret);
509-
}
510515
});
511516

512-
// We store the new indexes for the secrets that have logs with an index higher than the one stored.
513-
await this.db.setTaggingSecretsIndexesAsRecipient(
514-
Object.keys(indexesToIncrementMap).map(
515-
secret => new IndexedTaggingSecret(Fr.fromHexString(secret), indexesToIncrementMap[secret]),
516-
),
517-
);
517+
// Now based on the new largest indexes we found, we will construct a new secrets and windows set to fetch logs
518+
// for. Note that it's very unlikely that a new log from the current window would appear between the iterations
519+
// so we fetch the logs only for the difference of the window sets.
520+
const newSecretsAndWindows = [];
521+
for (const [appTaggingSecret, newIndex] of Object.entries(newLargestIndexMapForIteration)) {
522+
const secret = secrets.find(secret => secret.appTaggingSecret.toString() === appTaggingSecret);
523+
if (secret) {
524+
newSecretsAndWindows.push({
525+
appTaggingSecret: secret.appTaggingSecret,
526+
// We set the left most index to the new index to avoid fetching the same logs again
527+
leftMostIndex: newIndex,
528+
rightMostIndex: newIndex + WINDOW_HALF_SIZE,
529+
});
530+
531+
// We store the new largest index in the map to later store it in the db.
532+
newLargestIndexMapToStore[appTaggingSecret] = newIndex;
533+
} else {
534+
throw new Error(
535+
`Secret not found for appTaggingSecret ${appTaggingSecret}. This is a bug as it should never happen!`,
536+
);
537+
}
538+
}
518539

519-
// We've processed all the current secret-index pairs so we proceed to the next iteration.
520-
currentSecrets = secretsWithNewIndex;
540+
// Now we set the new secrets and windows and proceed to the next iteration.
541+
secretsAndWindows = newSecretsAndWindows;
521542
}
522543

523-
newLogsMap.set(
544+
// We filter the logs by block number and store them in the map.
545+
logsMap.set(
524546
recipient.toString(),
525-
// Remove logs with a block number higher than the max block number
526-
// Duplicates are likely to happen due to the sliding window, so we also filter them out
527-
logs.filter(
528-
(log, index, self) =>
529-
// The following condition is true if the log has small enough block number and is unique
530-
// --> the right side of the && is true if the index of the current log is the first occurrence
531-
// of the log in the array --> that way we ensure uniqueness.
532-
log.blockNumber <= maxBlockNumber && index === self.findIndex(otherLog => otherLog.equals(log)),
547+
logsForRecipient.filter(log => log.blockNumber <= maxBlockNumber),
548+
);
549+
550+
// At this point we have processed all the logs for the recipient so we store the new largest indexes in the db.
551+
await this.db.setTaggingSecretsIndexesAsRecipient(
552+
Object.entries(newLargestIndexMapToStore).map(
553+
([appTaggingSecret, index]) => new IndexedTaggingSecret(Fr.fromHexString(appTaggingSecret), index),
533554
),
534555
);
535556
}
536-
return newLogsMap;
557+
return logsMap;
537558
}
538559

539560
/**

0 commit comments

Comments
 (0)