From 0cc4a4881ea3d61d4ab0bad7594da4f610746f4f Mon Sep 17 00:00:00 2001 From: Gregorio Juliana Date: Thu, 31 Oct 2024 08:25:10 +0100 Subject: [PATCH] feat: sync tagged logs (#9595) Closes: https://github.com/AztecProtocol/aztec-packages/issues/9380 Implements the logic to sync tagged note logs from the node, taking into account the indices of "last seen" ones. Unfortunately, the approach of modularizing it as oracles and putting the logic in the contract itself was met with limitations in noir (namely, nested slices) and even the code is ready and close to becoming separate oracles, it cannot be done (yet! or at least without major caveats and performance implications) --- .../aztec-nr/aztec/src/oracle/notes.nr | 4 +- .../types/src/indexed_tagging_secret.nr | 4 +- yarn-project/aztec.js/src/utils/account.ts | 3 +- .../src/logs/encrypted_l2_note_log.ts | 4 +- .../circuits.js/src/keys/derivation.ts | 5 +- yarn-project/circuits.js/src/structs/index.ts | 2 +- .../src/structs/indexed_tagging_secret.ts | 9 - .../circuits.js/src/structs/tagging_secret.ts | 20 +++ yarn-project/pxe/package.json | 2 + .../pxe/src/database/kv_pxe_database.ts | 28 +-- yarn-project/pxe/src/database/pxe_database.ts | 18 +- yarn-project/pxe/src/index.ts | 2 +- .../pxe/src/simulator_oracle/index.ts | 62 ++++++- .../simulator_oracle/simulator_oracle.test.ts | 165 ++++++++++++++++++ yarn-project/txe/src/oracle/txe_oracle.ts | 13 +- yarn-project/yarn.lock | 4 +- 16 files changed, 301 insertions(+), 44 deletions(-) delete mode 100644 yarn-project/circuits.js/src/structs/indexed_tagging_secret.ts create mode 100644 yarn-project/circuits.js/src/structs/tagging_secret.ts create mode 100644 yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index 00bb2f14ab1..39ed994516f 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -231,11 +231,11 @@ pub unconstrained fn get_app_tagging_secrets_for_senders( let results = get_app_tagging_secrets_for_senders_oracle(recipient); let mut indexed_tagging_secrets = &[]; for i in 0..results.len() { - if i % 2 != 0 { + if i % 3 != 0 { continue; } indexed_tagging_secrets = indexed_tagging_secrets.push_back( - IndexedTaggingSecret::deserialize([results[i], results[i + 1]]), + IndexedTaggingSecret::deserialize([results[i], results[i + 1], results[i + 2]]), ); } indexed_tagging_secrets diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/indexed_tagging_secret.nr b/noir-projects/noir-protocol-circuits/crates/types/src/indexed_tagging_secret.nr index 1685fdf38e8..c3191eb9e08 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/indexed_tagging_secret.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/indexed_tagging_secret.nr @@ -1,10 +1,12 @@ use crate::traits::Deserialize; +use super::address::aztec_address::AztecAddress; use std::meta::derive; -pub global INDEXED_TAGGING_SECRET_LENGTH: u32 = 2; +pub global INDEXED_TAGGING_SECRET_LENGTH: u32 = 3; #[derive(Deserialize)] pub struct IndexedTaggingSecret { secret: Field, + recipient: AztecAddress, index: u32, } diff --git a/yarn-project/aztec.js/src/utils/account.ts b/yarn-project/aztec.js/src/utils/account.ts index cfc1c5bf479..5f11c192ede 100644 --- a/yarn-project/aztec.js/src/utils/account.ts +++ b/yarn-project/aztec.js/src/utils/account.ts @@ -1,4 +1,5 @@ -import { type CompleteAddress, type PXE } from '@aztec/circuit-types'; +import { type PXE } from '@aztec/circuit-types'; +import { type CompleteAddress } from '@aztec/circuits.js'; import { retryUntil } from '@aztec/foundation/retry'; import { DefaultWaitOpts, type WaitOpts } from '../contract/sent_tx.js'; diff --git a/yarn-project/circuit-types/src/logs/encrypted_l2_note_log.ts b/yarn-project/circuit-types/src/logs/encrypted_l2_note_log.ts index 3202155a858..095fe870b29 100644 --- a/yarn-project/circuit-types/src/logs/encrypted_l2_note_log.ts +++ b/yarn-project/circuit-types/src/logs/encrypted_l2_note_log.ts @@ -60,10 +60,10 @@ export class EncryptedL2NoteLog { * Crates a random log. * @returns A random log. */ - public static random(): EncryptedL2NoteLog { + public static random(tag: Fr = Fr.random()): EncryptedL2NoteLog { const randomEphPubKey = Point.random(); const randomLogContent = randomBytes(144 - Point.COMPRESSED_SIZE_IN_BYTES); - const data = Buffer.concat([Fr.random().toBuffer(), randomLogContent, randomEphPubKey.toCompressedBuffer()]); + const data = Buffer.concat([tag.toBuffer(), randomLogContent, randomEphPubKey.toCompressedBuffer()]); return new EncryptedL2NoteLog(data); } diff --git a/yarn-project/circuits.js/src/keys/derivation.ts b/yarn-project/circuits.js/src/keys/derivation.ts index 1fa9276e3f3..337091c197a 100644 --- a/yarn-project/circuits.js/src/keys/derivation.ts +++ b/yarn-project/circuits.js/src/keys/derivation.ts @@ -134,5 +134,8 @@ export function computeTaggingSecret(knownAddress: CompleteAddress, ivsk: Fq, ex const curve = new Grumpkin(); // Given A (known complete address) -> B (external address) and h == preaddress // Compute shared secret as S = (h_A + ivsk_A) * Addr_Point_B - return curve.mul(externalAddressPoint, ivsk.add(new Fq(knownPreaddress.toBigInt()))); + + // Beware! h_a + ivsk_a (also known as the address secret) can lead to an address point with a negative y-coordinate, since there's two possible candidates + // computeAddressSecret takes care of selecting the one that leads to a positive y-coordinate, which is the only valid address point + return curve.mul(externalAddressPoint, computeAddressSecret(knownPreaddress, ivsk)); } diff --git a/yarn-project/circuits.js/src/structs/index.ts b/yarn-project/circuits.js/src/structs/index.ts index 4e047dfc520..7bc2702e0a5 100644 --- a/yarn-project/circuits.js/src/structs/index.ts +++ b/yarn-project/circuits.js/src/structs/index.ts @@ -13,7 +13,7 @@ export * from './gas_fees.js'; export * from './gas_settings.js'; export * from './global_variables.js'; export * from './header.js'; -export * from './indexed_tagging_secret.js'; +export * from './tagging_secret.js'; export * from './kernel/combined_accumulated_data.js'; export * from './kernel/combined_constant_data.js'; export * from './kernel/enqueued_call_data.js'; diff --git a/yarn-project/circuits.js/src/structs/indexed_tagging_secret.ts b/yarn-project/circuits.js/src/structs/indexed_tagging_secret.ts deleted file mode 100644 index ab218b5b7ee..00000000000 --- a/yarn-project/circuits.js/src/structs/indexed_tagging_secret.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Fr } from '@aztec/foundation/fields'; - -export class IndexedTaggingSecret { - constructor(public secret: Fr, public index: number) {} - - toFields(): Fr[] { - return [this.secret, new Fr(this.index)]; - } -} diff --git a/yarn-project/circuits.js/src/structs/tagging_secret.ts b/yarn-project/circuits.js/src/structs/tagging_secret.ts new file mode 100644 index 00000000000..b43e8cb8030 --- /dev/null +++ b/yarn-project/circuits.js/src/structs/tagging_secret.ts @@ -0,0 +1,20 @@ +import { type AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr } from '@aztec/foundation/fields'; + +export class TaggingSecret { + constructor(public secret: Fr, public recipient: AztecAddress) {} + + toFields(): Fr[] { + return [this.secret, this.recipient]; + } +} + +export class IndexedTaggingSecret extends TaggingSecret { + constructor(secret: Fr, recipient: AztecAddress, public index: number) { + super(secret, recipient); + } + + override toFields(): Fr[] { + return [this.secret, this.recipient, new Fr(this.index)]; + } +} diff --git a/yarn-project/pxe/package.json b/yarn-project/pxe/package.json index b36c544458d..2d84ed81f13 100644 --- a/yarn-project/pxe/package.json +++ b/yarn-project/pxe/package.json @@ -87,9 +87,11 @@ "@jest/globals": "^29.5.0", "@types/jest": "^29.5.0", "@types/lodash.omit": "^4.5.7", + "@types/lodash.times": "^4.3.9", "@types/node": "^18.7.23", "jest": "^29.5.0", "jest-mock-extended": "^3.0.3", + "lodash.times": "^4.3.2", "ts-node": "^10.9.1", "typescript": "^5.0.4" }, diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts index 95a5da82e45..6d84f65c567 100644 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ b/yarn-project/pxe/src/database/kv_pxe_database.ts @@ -1,16 +1,12 @@ -import { - type IncomingNotesFilter, - MerkleTreeId, - NoteStatus, - type OutgoingNotesFilter, - type PublicKey, -} from '@aztec/circuit-types'; +import { type IncomingNotesFilter, MerkleTreeId, NoteStatus, type OutgoingNotesFilter } from '@aztec/circuit-types'; import { AztecAddress, CompleteAddress, type ContractInstanceWithAddress, Header, + type PublicKey, SerializableContractInstance, + type TaggingSecret, computePoint, } from '@aztec/circuits.js'; import { type ContractArtifact } from '@aztec/foundation/abi'; @@ -576,19 +572,23 @@ export class KVPxeDatabase implements PxeDatabase { return incomingNotesSize + outgoingNotesSize + treeRootsSize + authWitsSize + addressesSize; } - async incrementTaggingSecretsIndexes(appTaggingSecrets: Fr[]): Promise { - const indexes = await this.getTaggingSecretsIndexes(appTaggingSecrets); + async incrementTaggingSecretsIndexes(appTaggingSecretsWithRecipient: TaggingSecret[]): Promise { + const indexes = await this.getTaggingSecretsIndexes(appTaggingSecretsWithRecipient); await this.db.transaction(() => { - indexes.forEach(index => { - const nextIndex = index ? index + 1 : 1; - void this.#taggingSecretIndexes.set(appTaggingSecrets.toString(), nextIndex); + indexes.forEach((taggingSecretIndex, listIndex) => { + const nextIndex = taggingSecretIndex ? taggingSecretIndex + 1 : 1; + const { secret, recipient } = appTaggingSecretsWithRecipient[listIndex]; + const key = `${secret.toString()}-${recipient.toString()}`; + void this.#taggingSecretIndexes.set(key, nextIndex); }); }); } - getTaggingSecretsIndexes(appTaggingSecrets: Fr[]): Promise { + getTaggingSecretsIndexes(appTaggingSecretsWithRecipient: TaggingSecret[]): Promise { return this.db.transaction(() => - appTaggingSecrets.map(secret => this.#taggingSecretIndexes.get(secret.toString()) ?? 0), + appTaggingSecretsWithRecipient.map( + ({ secret, recipient }) => this.#taggingSecretIndexes.get(`${secret.toString()}-${recipient.toString()}`) ?? 0, + ), ); } } diff --git a/yarn-project/pxe/src/database/pxe_database.ts b/yarn-project/pxe/src/database/pxe_database.ts index db845d06828..961301501de 100644 --- a/yarn-project/pxe/src/database/pxe_database.ts +++ b/yarn-project/pxe/src/database/pxe_database.ts @@ -4,6 +4,7 @@ import { type ContractInstanceWithAddress, type Header, type PublicKey, + type TaggingSecret, } from '@aztec/circuits.js'; import { type ContractArtifact } from '@aztec/foundation/abi'; import { type AztecAddress } from '@aztec/foundation/aztec-address'; @@ -186,7 +187,20 @@ export interface PxeDatabase extends ContractArtifactDatabase, ContractInstanceD */ estimateSize(): Promise; - getTaggingSecretsIndexes(appTaggingSecrets: Fr[]): Promise; + /** + * Returns the last seen indexes for the provided app siloed tagging secrets or 0 if they've never been seen. + * The recipient must also be provided to convey "directionality" of the secret and index pair, or in other words + * whether the index was used to tag a sent or received note. + * @param appTaggingSecrets - The app siloed tagging secrets. + * @returns The indexes for the provided secrets, 0 if they've never been seen. + */ + getTaggingSecretsIndexes(appTaggingSecretsWithRecipient: TaggingSecret[]): Promise; - incrementTaggingSecretsIndexes(appTaggingSecrets: Fr[]): Promise; + /** + * Increments the index for the provided app siloed tagging secrets. + * The recipient must also be provided to convey "directionality" of the secret and index pair, or in other words + * whether the index was used to tag a sent or received note. + * @param appTaggingSecrets - The app siloed tagging secrets. + */ + incrementTaggingSecretsIndexes(appTaggingSecretsWithRecipient: TaggingSecret[]): Promise; } diff --git a/yarn-project/pxe/src/index.ts b/yarn-project/pxe/src/index.ts index 86b3f1205e7..d6e46f5ad84 100644 --- a/yarn-project/pxe/src/index.ts +++ b/yarn-project/pxe/src/index.ts @@ -4,7 +4,7 @@ export * from './config/index.js'; export { Tx, TxHash } from '@aztec/circuit-types'; -export { TxRequest, PartialAddress } from '@aztec/circuits.js'; +export { TxRequest } from '@aztec/circuits.js'; export * from '@aztec/foundation/fields'; export * from '@aztec/foundation/eth-address'; export * from '@aztec/foundation/aztec-address'; diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 4295bc1e555..9f4b278ecba 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -1,5 +1,6 @@ import { type AztecNode, + type EncryptedL2NoteLog, type L2Block, MerkleTreeId, type NoteStatus, @@ -17,6 +18,7 @@ import { IndexedTaggingSecret, type KeyValidationRequest, type L1_TO_L2_MSG_TREE_HEIGHT, + TaggingSecret, computeTaggingSecret, } from '@aztec/circuits.js'; import { type FunctionArtifact, getFunctionArtifact } from '@aztec/foundation/abi'; @@ -247,9 +249,11 @@ export class SimulatorOracle implements DBOracle { const senderIvsk = await this.keyStore.getMasterIncomingViewingSecretKey(sender); const sharedSecret = computeTaggingSecret(senderCompleteAddress, senderIvsk, recipient); // Silo the secret to the app so it can't be used to track other app's notes - const secret = poseidon2Hash([sharedSecret.x, sharedSecret.y, contractAddress]); - const [index] = await this.db.getTaggingSecretsIndexes([secret]); - return new IndexedTaggingSecret(secret, index); + const siloedSecret = poseidon2Hash([sharedSecret.x, sharedSecret.y, contractAddress]); + // Get the index of the secret, ensuring the directionality (sender -> recipient) + const directionalSecret = new TaggingSecret(siloedSecret, recipient); + const [index] = await this.db.getTaggingSecretsIndexes([directionalSecret]); + return new IndexedTaggingSecret(siloedSecret, recipient, index); } /** @@ -274,7 +278,55 @@ export class SimulatorOracle implements DBOracle { const sharedSecret = computeTaggingSecret(recipientCompleteAddress, recipientIvsk, sender); return poseidon2Hash([sharedSecret.x, sharedSecret.y, contractAddress]); }); - const indexes = await this.db.getTaggingSecretsIndexes(secrets); - return secrets.map((secret, i) => new IndexedTaggingSecret(secret, indexes[i])); + // Ensure the directionality (sender -> recipient) + const directionalSecrets = secrets.map(secret => new TaggingSecret(secret, recipient)); + const indexes = await this.db.getTaggingSecretsIndexes(directionalSecrets); + return directionalSecrets.map( + ({ secret, recipient }, i) => new IndexedTaggingSecret(secret, recipient, indexes[i]), + ); + } + + /** + * Synchronizes the logs tagged with the recipient's address and all the senders in the addressbook. + * Returns the unsynched logs and updates the indexes of the secrets used to tag them until there are no more logs to sync. + * @param contractAddress - The address of the contract that the logs are tagged for + * @param recipient - The address of the recipient + * @returns A list of encrypted logs tagged with the recipient's address + */ + public async syncTaggedLogs(contractAddress: AztecAddress, recipient: AztecAddress): Promise { + // Ideally this algorithm would be implemented in noir, exposing its building blocks as oracles. + // However it is impossible at the moment due to the language not supporting nested slices. + // This nesting is necessary because for a given set of tags we don't + // know how many logs we will get back. Furthermore, these logs are of undetermined + // length, since we don't really know the note they correspond to until we decrypt them. + + // 1. Get all the secrets for the recipient and sender pairs (#9365) + let appTaggingSecrets = await this.getAppTaggingSecretsForSenders(contractAddress, recipient); + + const logs: EncryptedL2NoteLog[] = []; + while (appTaggingSecrets.length > 0) { + // 2. Compute tags using the secrets, recipient and index. Obtain logs for each tag (#9380) + const currentTags = appTaggingSecrets.map(({ secret, recipient, index }) => + poseidon2Hash([secret, recipient, index]), + ); + const logsByTags = await this.aztecNode.getLogsByTags(currentTags); + const newTaggingSecrets: IndexedTaggingSecret[] = []; + logsByTags.forEach((logsByTag, index) => { + // 3.1. Append logs to the list and increment the index for the tags that have logs (#9380) + if (logsByTag.length > 0) { + logs.push(...logsByTag); + // 3.2. Increment the index for the tags that have logs (#9380) + newTaggingSecrets.push( + new IndexedTaggingSecret(appTaggingSecrets[index].secret, recipient, appTaggingSecrets[index].index + 1), + ); + } + }); + // 4. Consolidate in db and replace initial appTaggingSecrets with the new ones (updated indexes) + await this.db.incrementTaggingSecretsIndexes( + newTaggingSecrets.map(secret => new TaggingSecret(secret.secret, recipient)), + ); + appTaggingSecrets = newTaggingSecrets; + } + return logs; } } diff --git a/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts b/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts new file mode 100644 index 00000000000..d9e8728ad95 --- /dev/null +++ b/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts @@ -0,0 +1,165 @@ +import { type AztecNode, EncryptedL2NoteLog } from '@aztec/circuit-types'; +import { + AztecAddress, + CompleteAddress, + type Fq, + Fr, + TaggingSecret, + computeAddress, + computeTaggingSecret, + deriveKeys, +} from '@aztec/circuits.js'; +import { poseidon2Hash } from '@aztec/foundation/crypto'; +import { KeyStore } from '@aztec/key-store'; +import { openTmpStore } from '@aztec/kv-store/utils'; + +import { type MockProxy, mock } from 'jest-mock-extended'; +import times from 'lodash.times'; + +import { type PxeDatabase } from '../database/index.js'; +import { KVPxeDatabase } from '../database/kv_pxe_database.js'; +import { ContractDataOracle } from '../index.js'; +import { SimulatorOracle } from './index.js'; + +function computeTagForIndex( + sender: { completeAddress: CompleteAddress; ivsk: Fq }, + recipient: AztecAddress, + contractAddress: AztecAddress, + index: number, +) { + const sharedSecret = computeTaggingSecret(sender.completeAddress, sender.ivsk, recipient); + const siloedSecret = poseidon2Hash([sharedSecret.x, sharedSecret.y, contractAddress]); + return poseidon2Hash([siloedSecret, recipient, index]); +} + +describe('Simulator oracle', () => { + let aztecNode: MockProxy; + let database: PxeDatabase; + let contractDataOracle: ContractDataOracle; + let simulatorOracle: SimulatorOracle; + let keyStore: KeyStore; + + let recipient: CompleteAddress; + let contractAddress: AztecAddress; + + const NUM_SENDERS = 10; + let senders: { completeAddress: CompleteAddress; ivsk: Fq }[]; + + beforeEach(async () => { + const db = openTmpStore(); + aztecNode = mock(); + database = new KVPxeDatabase(db); + contractDataOracle = new ContractDataOracle(database); + keyStore = new KeyStore(db); + simulatorOracle = new SimulatorOracle(contractDataOracle, database, keyStore, aztecNode); + // Set up contract address + contractAddress = AztecAddress.random(); + // Set up recipient account + recipient = await keyStore.addAccount(new Fr(69), Fr.random()); + await database.addCompleteAddress(recipient); + // Set up the address book + senders = times(NUM_SENDERS).map((_, index) => { + const keys = deriveKeys(new Fr(index)); + const partialAddress = Fr.random(); + const address = computeAddress(keys.publicKeys, partialAddress); + const completeAddress = new CompleteAddress(address, keys.publicKeys, partialAddress); + return { completeAddress, ivsk: keys.masterIncomingViewingSecretKey }; + }); + for (const sender of senders) { + await database.addCompleteAddress(sender.completeAddress); + } + + const logs: { [k: string]: EncryptedL2NoteLog[] } = {}; + + // Add a random note from every address in the address book for our account with index 0 + // Compute the tag as sender (knowledge of preaddress and ivsk) + for (const sender of senders) { + const tag = computeTagForIndex(sender, recipient.address, contractAddress, 0); + const log = EncryptedL2NoteLog.random(tag); + logs[tag.toString()] = [log]; + } + // Accumulated logs intended for recipient: NUM_SENDERS + + // Add a random note from the first sender in the address book, repeating the tag + // Compute the tag as sender (knowledge of preaddress and ivsk) + const firstSender = senders[0]; + const tag = computeTagForIndex(firstSender, recipient.address, contractAddress, 0); + const log = EncryptedL2NoteLog.random(tag); + logs[tag.toString()].push(log); + // Accumulated logs intended for recipient: NUM_SENDERS + 1 + + // Add a random note from half the address book for our account with index 1 + // Compute the tag as sender (knowledge of preaddress and ivsk) + for (let i = NUM_SENDERS / 2; i < NUM_SENDERS; i++) { + const sender = senders[i]; + const tag = computeTagForIndex(sender, recipient.address, contractAddress, 1); + const log = EncryptedL2NoteLog.random(tag); + logs[tag.toString()] = [log]; + } + // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 + + // Add a random note from every address in the address book for a random recipient with index 0 + // Compute the tag as sender (knowledge of preaddress and ivsk) + for (const sender of senders) { + const keys = deriveKeys(Fr.random()); + const partialAddress = Fr.random(); + const randomRecipient = computeAddress(keys.publicKeys, partialAddress); + const tag = computeTagForIndex(sender, randomRecipient, contractAddress, 0); + const log = EncryptedL2NoteLog.random(tag); + logs[tag.toString()] = [log]; + } + // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 + + // Set up the getTaggedLogs mock + + aztecNode.getLogsByTags.mockImplementation(tags => { + return Promise.resolve(tags.map(tag => logs[tag.toString()] ?? [])); + }); + }); + + describe('sync tagged logs', () => { + it('should sync tagged logs', async () => { + const syncedLogs = await simulatorOracle.syncTaggedLogs(contractAddress, recipient.address); + // We expect to have all logs intended for the recipient, one per sender + 1 with a duplicated tag for the first one + half of the logs for the second index + expect(syncedLogs).toHaveLength(NUM_SENDERS + 1 + NUM_SENDERS / 2); + + // Recompute the secrets (as recipient) to ensure indexes are updated + + const ivsk = await keyStore.getMasterIncomingViewingSecretKey(recipient.address); + const directionalSecrets = senders.map(sender => { + const firstSenderSharedSecret = computeTaggingSecret(recipient, ivsk, sender.completeAddress.address); + const siloedSecret = poseidon2Hash([firstSenderSharedSecret.x, firstSenderSharedSecret.y, contractAddress]); + return new TaggingSecret(siloedSecret, recipient.address); + }); + + // First sender should have 2 logs, but keep index 1 since they were built using the same tag + // Next 4 senders hould also have index 1 + // Last 5 senders should have index 2 + const indexes = await database.getTaggingSecretsIndexes(directionalSecrets); + + expect(indexes).toHaveLength(NUM_SENDERS); + expect(indexes).toEqual([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]); + }); + + it('should only sync tagged logs for which indexes are not updated', async () => { + // Recompute the secrets (as recipient) to update indexes + + const ivsk = await keyStore.getMasterIncomingViewingSecretKey(recipient.address); + const directionalSecrets = senders.map(sender => { + const firstSenderSharedSecret = computeTaggingSecret(recipient, ivsk, sender.completeAddress.address); + const siloedSecret = poseidon2Hash([firstSenderSharedSecret.x, firstSenderSharedSecret.y, contractAddress]); + return new TaggingSecret(siloedSecret, recipient.address); + }); + + await database.incrementTaggingSecretsIndexes(directionalSecrets); + + const syncedLogs = await simulatorOracle.syncTaggedLogs(contractAddress, recipient.address); + + // Only half of the logs should be synced since we start from index 1, the other half should be skipped + expect(syncedLogs).toHaveLength(NUM_SENDERS / 2); + + // We should have called the node twice, once for index 1 and once for index 2 (which should return no logs) + expect(aztecNode.getLogsByTags.mock.calls.length).toBe(2); + }); + }); +}); diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 1078e6e329e..1754fa8e8c1 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -1,5 +1,6 @@ import { AuthWitness, + type EncryptedL2NoteLog, MerkleTreeId, Note, type NoteStatus, @@ -29,6 +30,7 @@ import { PrivateContextInputs, PublicDataTreeLeaf, type PublicDataTreeLeafPreimage, + TaggingSecret, TxContext, computeContractClassId, computeTaggingSecret, @@ -90,6 +92,8 @@ export class TXE implements TypedOracle { private version: Fr = Fr.ONE; private chainId: Fr = Fr.ONE; + private logsByTags = new Map(); + constructor( private logger: Logger, private trees: MerkleTrees, @@ -758,8 +762,8 @@ export class TXE implements TypedOracle { const sharedSecret = computeTaggingSecret(senderCompleteAddress, senderIvsk, recipient); // Silo the secret to the app so it can't be used to track other app's notes const secret = poseidon2Hash([sharedSecret.x, sharedSecret.y, this.contractAddress]); - const [index] = await this.txeDatabase.getTaggingSecretsIndexes([secret]); - return new IndexedTaggingSecret(secret, index); + const [index] = await this.txeDatabase.getTaggingSecretsIndexes([new TaggingSecret(secret, recipient)]); + return new IndexedTaggingSecret(secret, recipient, index); } async getAppTaggingSecretsForSenders(recipient: AztecAddress): Promise { @@ -775,8 +779,9 @@ export class TXE implements TypedOracle { const sharedSecret = computeTaggingSecret(recipientCompleteAddress, recipientIvsk, sender); return poseidon2Hash([sharedSecret.x, sharedSecret.y, this.contractAddress]); }); - const indexes = await this.txeDatabase.getTaggingSecretsIndexes(secrets); - return secrets.map((secret, i) => new IndexedTaggingSecret(secret, indexes[i])); + const directionalSecrets = secrets.map(secret => new TaggingSecret(secret, recipient)); + const indexes = await this.txeDatabase.getTaggingSecretsIndexes(directionalSecrets); + return secrets.map((secret, i) => new IndexedTaggingSecret(secret, recipient, indexes[i])); } // AVM oracles diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index a20dfb4d126..d31f814b645 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -1022,12 +1022,14 @@ __metadata: "@noir-lang/types": "workspace:*" "@types/jest": ^29.5.0 "@types/lodash.omit": ^4.5.7 + "@types/lodash.times": ^4.3.9 "@types/node": ^18.7.23 jest: ^29.5.0 jest-mock-extended: ^3.0.3 koa: ^2.14.2 koa-router: ^12.0.0 lodash.omit: ^4.5.0 + lodash.times: ^4.3.2 sha3: ^2.1.4 ts-node: ^10.9.1 tslib: ^2.4.0 @@ -4718,7 +4720,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash.times@npm:^4.3.7": +"@types/lodash.times@npm:^4.3.7, @types/lodash.times@npm:^4.3.9": version: 4.3.9 resolution: "@types/lodash.times@npm:4.3.9" dependencies: