From cb1c96840ddb49908e66e35ad39b2f52a67bba06 Mon Sep 17 00:00:00 2001 From: benesjan <13470840+benesjan@users.noreply.github.com> Date: Tue, 14 Oct 2025 08:42:26 +0000 Subject: [PATCH] refactor: tagging related cleanup I started working on improving unconstrained tagging as proposed in [this forum post](https://forum.aztec.network/t/on-note-discovery-and-index-coordination/7165) and the initial step in implementing that is the following: 1. store the tagging indexes requested during execution in a cache (just like we do with notes), 2. when the execution is finished take the cache and along with a tx hash dump it into db. Given that the cache will reside in `PrivateExecutionOracle` I now need to orchestrate the sync there. When trying to do that (to be done in a [PR up the stack](https://github.com/AztecProtocol/aztec-packages/pull/17445)) I realized that the current tagging-related API was cumbersome and this made me do the following in this PR: 1. Have the API accept directionalAppTaggingSecret on the input instead of appTaggingSecret and recipient as now I can just compute it in the `PrivateExecutionOracle` and request info based on that, 2. I moved the tagging functionality from stdlib to PXE as there was no reason to have it in stdlib, 3. I simplified a bunch of random things. This PR is a bit random so if something is not clear LMK. --- .../execution_data_provider.ts | 29 ++- .../oracle/interfaces.ts | 3 +- .../oracle/oracle.ts | 2 +- .../oracle/private_execution.test.ts | 13 +- .../oracle/private_execution_oracle.ts | 19 +- .../pxe_oracle_interface.test.ts | 132 ++++++++----- .../pxe_oracle_interface.ts | 177 ++++++++++-------- .../tagging_utils.ts | 32 ---- .../tagging_data_provider.ts | 101 +++++----- yarn-project/pxe/src/tagging/constants.ts | 2 + yarn-project/pxe/src/tagging/index.ts | 6 + yarn-project/pxe/src/tagging/siloed_tag.ts | 22 +++ yarn-project/pxe/src/tagging/tag.ts | 16 ++ yarn-project/pxe/src/tagging/utils.ts | 31 +++ yarn-project/stdlib/src/keys/derivation.ts | 27 +-- .../logs/directional_app_tagging_secret.ts | 72 +++++++ yarn-project/stdlib/src/logs/index.ts | 1 + .../stdlib/src/logs/indexed_tagging_secret.ts | 61 ++---- yarn-project/txe/src/rpc_translator.ts | 2 +- 19 files changed, 449 insertions(+), 299 deletions(-) delete mode 100644 yarn-project/pxe/src/contract_function_simulator/tagging_utils.ts create mode 100644 yarn-project/pxe/src/tagging/constants.ts create mode 100644 yarn-project/pxe/src/tagging/index.ts create mode 100644 yarn-project/pxe/src/tagging/siloed_tag.ts create mode 100644 yarn-project/pxe/src/tagging/tag.ts create mode 100644 yarn-project/pxe/src/tagging/utils.ts create mode 100644 yarn-project/stdlib/src/logs/directional_app_tagging_secret.ts diff --git a/yarn-project/pxe/src/contract_function_simulator/execution_data_provider.ts b/yarn-project/pxe/src/contract_function_simulator/execution_data_provider.ts index b91b883de6a7..0eb5774452b1 100644 --- a/yarn-project/pxe/src/contract_function_simulator/execution_data_provider.ts +++ b/yarn-project/pxe/src/contract_function_simulator/execution_data_provider.ts @@ -5,10 +5,12 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2Block } from '@aztec/stdlib/block'; import type { CompleteAddress, ContractInstance } from '@aztec/stdlib/contract'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; +import type { DirectionalAppTaggingSecret } from '@aztec/stdlib/logs'; import type { NoteStatus } from '@aztec/stdlib/note'; import { type MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; import type { BlockHeader, NodeStats } from '@aztec/stdlib/tx'; +import type { Tag } from '../tagging/tag.js'; import type { NoteData } from './oracle/interfaces.js'; import type { MessageLoadOracleInputs } from './oracle/message_load_oracle_inputs.js'; @@ -214,13 +216,36 @@ export interface ExecutionDataProvider { assertCompatibleOracleVersion(version: number): void; /** - * Returns the next app tag for a given sender and recipient pair. + * Calculates the directional app tagging secret for a given contract, sender and recipient. * @param contractAddress - The contract address to silo the secret for * @param sender - The address sending the note * @param recipient - The address receiving the note + * @returns The directional app tagging secret + */ + calculateDirectionalAppTaggingSecret( + contractAddress: AztecAddress, + sender: AztecAddress, + recipient: AztecAddress, + ): Promise; + + /** + * Updates the local index of the shared tagging secret of a (sender, recipient, contract) tuple if a log with + * a larger index is found from the node. + * @param secret - The secret that's unique for (sender, recipient, contract) tuple while the direction + * of sender -> recipient matters. + * @param contractAddress - The address of the contract that the logs are tagged for. Needs to be provided to store + * because the function performs second round of siloing which is necessary because kernels do it as well (they silo + * first field of the private log which corresponds to the tag). + */ + syncTaggedLogsAsSender(secret: DirectionalAppTaggingSecret, contractAddress: AztecAddress): Promise; + + /** + * Returns the next app tag for a given directional app tagging secret. + * @param secret - The secret that's unique for (sender, recipient, contract) tuple while + * direction of sender -> recipient matters. * @returns The computed tag. */ - getNextAppTagAsSender(contractAddress: AztecAddress, sender: AztecAddress, recipient: AztecAddress): Promise; + getNextAppTagAsSender(secret: DirectionalAppTaggingSecret): Promise; /** * Synchronizes the private logs tagged with scoped addresses and all the senders in the address book. Stores the found diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index 768680b2276c..ab25b9590829 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -9,6 +9,7 @@ import type { Note, NoteStatus } from '@aztec/stdlib/note'; import { type MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; import type { BlockHeader } from '@aztec/stdlib/tx'; +import type { Tag } from '../../tagging/tag.js'; import type { UtilityContext } from '../noir-structs/utility_context.js'; import type { MessageLoadOracleInputs } from './message_load_oracle_inputs.js'; @@ -154,6 +155,6 @@ export interface IPrivateExecutionOracle { privateNotifySetMinRevertibleSideEffectCounter(minRevertibleSideEffectCounter: number): Promise; privateGetSenderForTags(): Promise; privateSetSenderForTags(senderForTags: AztecAddress): Promise; - privateGetNextAppTagAsSender(sender: AztecAddress, recipient: AztecAddress): Promise; + privateGetNextAppTagAsSender(sender: AztecAddress, recipient: AztecAddress): Promise; utilityEmitOffchainEffect(data: Fr[]): Promise; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index e26d24f5a756..ce82d64e746a 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -438,7 +438,7 @@ export class Oracle { AztecAddress.fromString(sender), AztecAddress.fromString(recipient), ); - return [toACVMField(tag)]; + return [toACVMField(tag.value)]; } async utilityFetchTaggedLogs([pendingTaggedLogArrayBaseSlot]: ACVMField[]): Promise { diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index ef168d524c1f..287c132e07b7 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -47,6 +47,7 @@ import { } from '@aztec/stdlib/hash'; import { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { computeAppNullifierSecretKey, deriveKeys } from '@aztec/stdlib/keys'; +import { DirectionalAppTaggingSecret } from '@aztec/stdlib/logs'; import { L1Actor, L1ToL2Message, L2Actor } from '@aztec/stdlib/messaging'; import { Note } from '@aztec/stdlib/note'; import { makeHeader } from '@aztec/stdlib/testing'; @@ -64,6 +65,7 @@ import { jest } from '@jest/globals'; import { Matcher, type MatcherCreator, type MockProxy, mock } from 'jest-mock-extended'; import { toFunctionSelector } from 'viem'; +import { Tag } from '../../tagging/tag.js'; import { ContractFunctionSimulator } from '../contract_function_simulator.js'; import type { ExecutionDataProvider } from '../execution_data_provider.js'; import { MessageLoadOracleInputs } from './message_load_oracle_inputs.js'; @@ -301,11 +303,9 @@ describe('Private Execution test suite', () => { throw new Error(`Unknown address: ${address}. Recipient: ${recipient}, Owner: ${owner}`); }); - executionDataProvider.getNextAppTagAsSender.mockImplementation( - (_contractAddress: AztecAddress, _sender: AztecAddress, _recipient: AztecAddress) => { - return Promise.resolve(Fr.random()); - }, - ); + executionDataProvider.getNextAppTagAsSender.mockImplementation((_secret: DirectionalAppTaggingSecret) => { + return Promise.resolve(Tag.compute({ secret: _secret, index: 0 })); + }); executionDataProvider.getFunctionArtifact.mockImplementation(async (address, selector) => { const contract = contracts[address.toString()]; if (!contract) { @@ -331,6 +331,9 @@ describe('Private Execution test suite', () => { }); executionDataProvider.syncTaggedLogs.mockImplementation((_, __) => Promise.resolve()); + executionDataProvider.calculateDirectionalAppTaggingSecret.mockResolvedValue( + DirectionalAppTaggingSecret.fromString('0x1'), + ); executionDataProvider.loadCapsule.mockImplementation((_, __) => Promise.resolve(null)); executionDataProvider.getPublicStorageAt.mockImplementation( diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts index 9910f06a22fb..0a90a283aaba 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts @@ -26,6 +26,7 @@ import { type TxContext, } from '@aztec/stdlib/tx'; +import type { Tag } from '../../tagging/tag.js'; import type { ExecutionDataProvider } from '../execution_data_provider.js'; import type { ExecutionNoteCache } from '../execution_note_cache.js'; import type { HashedValuesCache } from '../hashed_values_cache.js'; @@ -191,8 +192,22 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP * @param recipient - The address receiving the log * @returns An app tag to be used in a log. */ - public async privateGetNextAppTagAsSender(sender: AztecAddress, recipient: AztecAddress): Promise { - return await this.executionDataProvider.getNextAppTagAsSender(this.contractAddress, sender, recipient); + public async privateGetNextAppTagAsSender(sender: AztecAddress, recipient: AztecAddress): Promise { + const directionalAppTaggingSecret = await this.executionDataProvider.calculateDirectionalAppTaggingSecret( + this.contractAddress, + sender, + recipient, + ); + + // TODO(benesjan): In a follow-up PR we will load here the index from the ExecutionTaggingIndexCache if present + // and if not we will obtain it from the execution data provider. + + this.log.debug(`Syncing tagged logs as sender ${sender} for contract ${this.contractAddress}`, { + directionalAppTaggingSecret, + recipient, + }); + await this.executionDataProvider.syncTaggedLogsAsSender(directionalAppTaggingSecret, this.contractAddress); + return this.executionDataProvider.getNextAppTagAsSender(directionalAppTaggingSecret); } /** diff --git a/yarn-project/pxe/src/contract_function_simulator/pxe_oracle_interface.test.ts b/yarn-project/pxe/src/contract_function_simulator/pxe_oracle_interface.test.ts index 262173f86392..73026d9157e7 100644 --- a/yarn-project/pxe/src/contract_function_simulator/pxe_oracle_interface.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/pxe_oracle_interface.test.ts @@ -8,8 +8,8 @@ import { randomInBlock } from '@aztec/stdlib/block'; import { CompleteAddress } from '@aztec/stdlib/contract'; import { computeUniqueNoteHash, siloNoteHash, siloNullifier, siloPrivateLog } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import { computeAddress, computeAppTaggingSecret, deriveKeys } from '@aztec/stdlib/keys'; -import { IndexedTaggingSecret, PrivateLog, PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { computeAddress, deriveKeys } from '@aztec/stdlib/keys'; +import { DirectionalAppTaggingSecret, PrivateLog, PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs'; import { NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId } from '@aztec/stdlib/trees'; import { BlockHeader, GlobalVariables, TxEffect, TxHash, randomIndexedTxEffect } from '@aztec/stdlib/tx'; @@ -25,9 +25,11 @@ import { NoteDataProvider } from '../storage/note_data_provider/note_data_provid import { PrivateEventDataProvider } from '../storage/private_event_data_provider/private_event_data_provider.js'; import { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; import { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; +import { WINDOW_HALF_SIZE } from '../tagging/constants.js'; +import { SiloedTag } from '../tagging/siloed_tag.js'; +import { Tag } from '../tagging/tag.js'; import { LogRetrievalRequest } from './noir-structs/log_retrieval_request.js'; import { PXEOracleInterface } from './pxe_oracle_interface.js'; -import { WINDOW_HALF_SIZE } from './tagging_utils.js'; jest.setTimeout(30_000); @@ -37,9 +39,15 @@ async function computeSiloedTagForIndex( contractAddress: AztecAddress, index: number, ) { - const appSecret = await computeAppTaggingSecret(sender.completeAddress, sender.ivsk, recipient, contractAddress); - const indexedTaggingSecret = new IndexedTaggingSecret(appSecret, index); - return indexedTaggingSecret.computeSiloedTag(recipient, contractAddress); + const secret = await DirectionalAppTaggingSecret.compute( + sender.completeAddress, + sender.ivsk, + recipient, + contractAddress, + recipient, + ); + const tag = await Tag.compute({ secret, index }); + return SiloedTag.compute(tag, contractAddress); } describe('PXEOracleInterface', () => { @@ -110,7 +118,7 @@ describe('PXEOracleInterface', () => { // Compute the tag as sender (knowledge of preaddress and ivsk) for (const sender of senders) { const tag = await computeSiloedTagForIndex(sender, recipient.address, contractAddress, tagIndex); - const log = new TxScopedL2Log(TxHash.random(), 0, 0, MIN_BLOCK_NUMBER_OF_A_LOG, PrivateLog.random(tag)); + const log = new TxScopedL2Log(TxHash.random(), 0, 0, MIN_BLOCK_NUMBER_OF_A_LOG, PrivateLog.random(tag.value)); logs[tag.toString()] = [log]; } // Accumulated logs intended for recipient: NUM_SENDERS @@ -119,7 +127,7 @@ describe('PXEOracleInterface', () => { // Compute the tag as sender (knowledge of preaddress and ivsk) const firstSender = senders[0]; const tag = await computeSiloedTagForIndex(firstSender, recipient.address, contractAddress, tagIndex); - const log = new TxScopedL2Log(TxHash.random(), 1, 0, 0, PrivateLog.random(tag)); + const log = new TxScopedL2Log(TxHash.random(), 1, 0, 0, PrivateLog.random(tag.value)); logs[tag.toString()].push(log); // Accumulated logs intended for recipient: NUM_SENDERS + 1 @@ -129,7 +137,7 @@ describe('PXEOracleInterface', () => { const sender = senders[i]; const tag = await computeSiloedTagForIndex(sender, recipient.address, contractAddress, tagIndex + 1); const blockNumber = 2; - const log = new TxScopedL2Log(TxHash.random(), 0, 0, blockNumber, PrivateLog.random(tag)); + const log = new TxScopedL2Log(TxHash.random(), 0, 0, blockNumber, PrivateLog.random(tag.value)); logs[tag.toString()] = [log]; } // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 @@ -141,7 +149,7 @@ describe('PXEOracleInterface', () => { const partialAddress = Fr.random(); const randomRecipient = await computeAddress(keys.publicKeys, partialAddress); const tag = await computeSiloedTagForIndex(sender, randomRecipient, contractAddress, tagIndex); - const log = new TxScopedL2Log(TxHash.random(), 0, 0, MAX_BLOCK_NUMBER_OF_A_LOG, PrivateLog.random(tag)); + const log = new TxScopedL2Log(TxHash.random(), 0, 0, MAX_BLOCK_NUMBER_OF_A_LOG, PrivateLog.random(tag.value)); logs[tag.toString()] = [log]; } // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 @@ -188,14 +196,20 @@ describe('PXEOracleInterface', () => { const ivsk = await keyStore.getMasterIncomingViewingSecretKey(recipient.address); const secrets = await Promise.all( senders.map(sender => - computeAppTaggingSecret(recipient, ivsk, sender.completeAddress.address, contractAddress), + DirectionalAppTaggingSecret.compute( + recipient, + ivsk, + sender.completeAddress.address, + contractAddress, + recipient.address, + ), ), ); // First sender should have 2 logs, but keep index 1 since they were built using the same tag // Next 4 senders should also have index 1 = offset + 1 // Last 5 senders should have index 2 = offset + 2 - const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets, recipient.address); + const indexes = await taggingDataProvider.getNextIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]); @@ -216,38 +230,40 @@ describe('PXEOracleInterface', () => { // Recompute the secrets (as recipient) to ensure indexes are updated const ivsk = await keyStore.getMasterIncomingViewingSecretKey(recipient.address); - // An array of direction-less secrets for each sender-recipient pair + // An array of directional secrets for each sender-recipient pair const secrets = await Promise.all( senders.map(sender => - computeAppTaggingSecret(recipient, ivsk, sender.completeAddress.address, contractAddress), + DirectionalAppTaggingSecret.compute( + recipient, + ivsk, + sender.completeAddress.address, + contractAddress, + recipient.address, + ), ), ); - // We only get the tagging secret at index `index` for each sender because each sender only needs to track - // their own tagging secret with the recipient. The secrets array contains all sender-recipient pairs, so - // secrets[index] corresponds to the tagging secret between sender[index] and the recipient. const getTaggingSecretsIndexesAsSenderForSenders = () => - Promise.all( - senders.map((sender, index) => - taggingDataProvider.getTaggingSecretsIndexesAsSender([secrets[index]], sender.completeAddress.address), - ), - ); + Promise.all(secrets.map(secret => taggingDataProvider.getNextIndexAsSender(secret))); const indexesAsSender = await getTaggingSecretsIndexesAsSenderForSenders(); - expect(indexesAsSender).toStrictEqual([[0], [0], [0], [0], [0], [0], [0], [0], [0], [0]]); + expect(indexesAsSender).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); expect(aztecNode.getLogsByTags.mock.calls.length).toBe(0); for (let i = 0; i < senders.length; i++) { - await pxeOracleInterface.syncTaggedLogsAsSender( + const directionalAppTaggingSecret = await DirectionalAppTaggingSecret.compute( + senders[i].completeAddress, + senders[i].ivsk, + recipient.address, contractAddress, - senders[i].completeAddress.address, recipient.address, ); + await pxeOracleInterface.syncTaggedLogsAsSender(directionalAppTaggingSecret, contractAddress); } let indexesAsSenderAfterSync = await getTaggingSecretsIndexesAsSenderForSenders(); - expect(indexesAsSenderAfterSync).toStrictEqual([[1], [1], [1], [1], [1], [2], [2], [2], [2], [2]]); + expect(indexesAsSenderAfterSync).toStrictEqual([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]); // Only 1 window is obtained for each sender expect(aztecNode.getLogsByTags.mock.calls.length).toBe(NUM_SENDERS); @@ -258,15 +274,18 @@ describe('PXEOracleInterface', () => { tagIndex = 11; await generateMockLogs(tagIndex); for (let i = 0; i < senders.length; i++) { - await pxeOracleInterface.syncTaggedLogsAsSender( + const directionalAppTaggingSecret = await DirectionalAppTaggingSecret.compute( + senders[i].completeAddress, + senders[i].ivsk, + recipient.address, contractAddress, - senders[i].completeAddress.address, recipient.address, ); + await pxeOracleInterface.syncTaggedLogsAsSender(directionalAppTaggingSecret, contractAddress); } indexesAsSenderAfterSync = await getTaggingSecretsIndexesAsSenderForSenders(); - expect(indexesAsSenderAfterSync).toStrictEqual([[12], [12], [12], [12], [12], [13], [13], [13], [13], [13]]); + expect(indexesAsSenderAfterSync).toStrictEqual([12, 12, 12, 12, 12, 13, 13, 13, 13, 13]); expect(aztecNode.getLogsByTags.mock.calls.length).toBe(NUM_SENDERS * 2); }); @@ -284,14 +303,20 @@ describe('PXEOracleInterface', () => { const ivsk = await keyStore.getMasterIncomingViewingSecretKey(recipient.address); const secrets = await Promise.all( senders.map(sender => - computeAppTaggingSecret(recipient, ivsk, sender.completeAddress.address, contractAddress), + DirectionalAppTaggingSecret.compute( + recipient, + ivsk, + sender.completeAddress.address, + contractAddress, + recipient.address, + ), ), ); // First sender should have 2 logs, but keep index 6 since they were built using the same tag // Next 4 senders should also have index 6 = offset + 1 // Last 5 senders should have index 7 = offset + 2 - const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets, recipient.address); + const indexes = await taggingDataProvider.getNextIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([6, 6, 6, 6, 6, 7, 7, 7, 7, 7]); @@ -309,15 +334,18 @@ describe('PXEOracleInterface', () => { const ivsk = await keyStore.getMasterIncomingViewingSecretKey(recipient.address); const secrets = await Promise.all( senders.map(sender => - computeAppTaggingSecret(recipient, ivsk, sender.completeAddress.address, contractAddress), + DirectionalAppTaggingSecret.compute( + recipient, + ivsk, + sender.completeAddress.address, + contractAddress, + recipient.address, + ), ), ); // Increase our indexes to 2 - await taggingDataProvider.setTaggingSecretsIndexesAsRecipient( - secrets.map(secret => new IndexedTaggingSecret(secret, 2)), - recipient.address, - ); + await taggingDataProvider.setNextIndexesAsRecipient(secrets.map(secret => ({ secret, index: 2 }))); await pxeOracleInterface.syncTaggedLogs(contractAddress, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT); @@ -328,7 +356,7 @@ describe('PXEOracleInterface', () => { // First sender should have 2 logs, but keep index 2 since they were built using the same tag // Next 4 senders should also have index 2 = tagIndex + 1 // Last 5 senders should have index 3 = tagIndex + 2 - const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets, recipient.address); + const indexes = await taggingDataProvider.getNextIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([2, 2, 2, 2, 2, 3, 3, 3, 3, 3]); @@ -346,17 +374,20 @@ describe('PXEOracleInterface', () => { const ivsk = await keyStore.getMasterIncomingViewingSecretKey(recipient.address); const secrets = await Promise.all( senders.map(sender => - computeAppTaggingSecret(recipient, ivsk, sender.completeAddress.address, contractAddress), + DirectionalAppTaggingSecret.compute( + recipient, + ivsk, + sender.completeAddress.address, + contractAddress, + recipient.address, + ), ), ); // We set the indexes to WINDOW_HALF_SIZE + 1 so that it's outside the window and for this reason no updates // should be triggered. const index = WINDOW_HALF_SIZE + 1; - await taggingDataProvider.setTaggingSecretsIndexesAsRecipient( - secrets.map(secret => new IndexedTaggingSecret(secret, index)), - recipient.address, - ); + await taggingDataProvider.setNextIndexesAsRecipient(secrets.map(secret => ({ secret, index }))); await pxeOracleInterface.syncTaggedLogs(contractAddress, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT); @@ -365,7 +396,7 @@ describe('PXEOracleInterface', () => { await expectPendingTaggedLogArrayLengthToBe(contractAddress, NUM_SENDERS / 2); // Indexes should remain where we set them (window_size + 1) - const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets, recipient.address); + const indexes = await taggingDataProvider.getNextIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([index, index, index, index, index, index, index, index, index, index]); @@ -382,13 +413,18 @@ describe('PXEOracleInterface', () => { const ivsk = await keyStore.getMasterIncomingViewingSecretKey(recipient.address); const secrets = await Promise.all( senders.map(sender => - computeAppTaggingSecret(recipient, ivsk, sender.completeAddress.address, contractAddress), + DirectionalAppTaggingSecret.compute( + recipient, + ivsk, + sender.completeAddress.address, + contractAddress, + recipient.address, + ), ), ); - await taggingDataProvider.setTaggingSecretsIndexesAsRecipient( - secrets.map(secret => new IndexedTaggingSecret(secret, WINDOW_HALF_SIZE + 2)), - recipient.address, + await taggingDataProvider.setNextIndexesAsRecipient( + secrets.map(secret => ({ secret, index: WINDOW_HALF_SIZE + 2 })), ); await pxeOracleInterface.syncTaggedLogs(contractAddress, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT); @@ -410,7 +446,7 @@ describe('PXEOracleInterface', () => { // First sender should have 2 logs, but keep index 1 since they were built using the same tag // Next 4 senders should also have index 1 = offset + 1 // Last 5 senders should have index 2 = offset + 2 - const indexes = await taggingDataProvider.getTaggingSecretsIndexesAsRecipient(secrets, recipient.address); + const indexes = await taggingDataProvider.getNextIndexesAsRecipient(secrets); expect(indexes).toHaveLength(NUM_SENDERS); expect(indexes).toEqual([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]); diff --git a/yarn-project/pxe/src/contract_function_simulator/pxe_oracle_interface.ts b/yarn-project/pxe/src/contract_function_simulator/pxe_oracle_interface.ts index e1238058f602..03f9ad7ed84e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/pxe_oracle_interface.ts +++ b/yarn-project/pxe/src/contract_function_simulator/pxe_oracle_interface.ts @@ -15,9 +15,8 @@ import type { CompleteAddress, ContractInstance } from '@aztec/stdlib/contract'; import { computeUniqueNoteHash, siloNoteHash, siloNullifier, siloPrivateLog } from '@aztec/stdlib/hash'; import { type AztecNode, MAX_RPC_LEN } from '@aztec/stdlib/interfaces/client'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; -import { computeAddressSecret, computeAppTaggingSecret } from '@aztec/stdlib/keys'; +import { computeAddressSecret } from '@aztec/stdlib/keys'; import { - IndexedTaggingSecret, PendingTaggedLog, PrivateLogWithTxData, PublicLog, @@ -25,6 +24,7 @@ import { TxScopedL2Log, deriveEcdhSharedSecret, } from '@aztec/stdlib/logs'; +import type { IndexedTaggingSecret } from '@aztec/stdlib/logs'; import { getNonNullifiedL1ToL2MessageWitness } from '@aztec/stdlib/messaging'; import { Note, type NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; @@ -42,12 +42,19 @@ import type { NoteDataProvider } from '../storage/note_data_provider/note_data_p import type { PrivateEventDataProvider } from '../storage/private_event_data_provider/private_event_data_provider.js'; import type { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; import type { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; +import { + DirectionalAppTaggingSecret, + SiloedTag, + Tag, + WINDOW_HALF_SIZE, + getIndexedTaggingSecretsForTheWindow, + getInitialIndexesMap, +} from '../tagging/index.js'; import { EventValidationRequest } from './noir-structs/event_validation_request.js'; import { LogRetrievalRequest } from './noir-structs/log_retrieval_request.js'; import { LogRetrievalResponse } from './noir-structs/log_retrieval_response.js'; import { NoteValidationRequest } from './noir-structs/note_validation_request.js'; import type { ProxiedNode } from './proxied_node.js'; -import { WINDOW_HALF_SIZE, getIndexedTaggingSecretsForTheWindow, getInitialIndexesMap } from './tagging_utils.js'; /** * A data layer that provides and stores information needed for simulating/proving a transaction. @@ -268,41 +275,42 @@ export class PXEOracleInterface implements ExecutionDataProvider { * @param sender - The address sending the note * @param recipient - The address receiving the note * @returns The computed tag. + * TODO(benesjan): In a follow-up PR this will only return the index and that's it. */ - public async getNextAppTagAsSender( - contractAddress: AztecAddress, - sender: AztecAddress, - recipient: AztecAddress, - ): Promise { - await this.syncTaggedLogsAsSender(contractAddress, sender, recipient); - - const appTaggingSecret = await this.#calculateAppTaggingSecret(contractAddress, sender, recipient); - const [index] = await this.taggingDataProvider.getTaggingSecretsIndexesAsSender([appTaggingSecret], sender); + public async getNextAppTagAsSender(secret: DirectionalAppTaggingSecret): Promise { + const index = await this.taggingDataProvider.getNextIndexAsSender(secret); + // TODO(benesjan): This will be reworked in a follow-up PR where we will store the new indexes in the db once + // the execution finishes (then we dump the contents of the ExecutionTaggingIndexCache into the db) // Increment the index for next time - const contractName = await this.contractDataProvider.getDebugContractName(contractAddress); - this.log.debug(`Incrementing app tagging secret at ${contractName}(${contractAddress})`, { - appTaggingSecret, - sender, - recipient, - contractName, - contractAddress, - }); - - await this.taggingDataProvider.setTaggingSecretsIndexesAsSender( - [new IndexedTaggingSecret(appTaggingSecret, index + 1)], - sender, - ); + // const contractName = await this.contractDataProvider.getDebugContractName(contractAddress); + // this.log.debug(`Incrementing app tagging secret at ${contractName}(${contractAddress})`, { + // directionalAppTaggingSecret, + // sender, + // recipient, + // contractName, + // contractAddress, + // }); + await this.taggingDataProvider.setNextIndexesAsSender([{ secret, index: index + 1 }]); // Compute and return the tag using the current index - const indexedTaggingSecret = new IndexedTaggingSecret(appTaggingSecret, index); - return indexedTaggingSecret.computeTag(recipient); + return Tag.compute({ secret, index }); } - async #calculateAppTaggingSecret(contractAddress: AztecAddress, sender: AztecAddress, recipient: AztecAddress) { + public async calculateDirectionalAppTaggingSecret( + contractAddress: AztecAddress, + sender: AztecAddress, + recipient: AztecAddress, + ) { const senderCompleteAddress = await this.getCompleteAddress(sender); const senderIvsk = await this.keyStore.getMasterIncomingViewingSecretKey(sender); - return computeAppTaggingSecret(senderCompleteAddress, senderIvsk, recipient, contractAddress); + return DirectionalAppTaggingSecret.compute( + senderCompleteAddress, + senderIvsk, + recipient, + contractAddress, + recipient, + ); } /** @@ -327,30 +335,33 @@ export class PXEOracleInterface implements ExecutionDataProvider { ...(await this.taggingDataProvider.getSenderAddresses()), ...(await this.keyStore.getAccounts()), ].filter((address, index, self) => index === self.findIndex(otherAddress => otherAddress.equals(address))); - const appTaggingSecrets = await Promise.all( - senders.map(contact => - computeAppTaggingSecret(recipientCompleteAddress, recipientIvsk, contact, contractAddress), - ), + const secrets = await Promise.all( + senders.map(contact => { + return DirectionalAppTaggingSecret.compute( + recipientCompleteAddress, + recipientIvsk, + contact, + contractAddress, + recipient, + ); + }), ); - const indexes = await this.taggingDataProvider.getTaggingSecretsIndexesAsRecipient(appTaggingSecrets, recipient); - return appTaggingSecrets.map((secret, i) => new IndexedTaggingSecret(secret, indexes[i])); + const indexes = await this.taggingDataProvider.getNextIndexesAsRecipient(secrets); + if (indexes.length !== secrets.length) { + throw new Error('Indexes and directional app tagging secrets have different lengths'); + } + + return secrets.map((secret, i) => ({ + secret, + index: indexes[i], + })); } - /** - * Updates the local index of the shared tagging secret of a sender / recipient pair - * if a log with a larger index is found from the node. - * @param contractAddress - The address of the contract that the logs are tagged for - * @param sender - The address of the sender, we must know the sender's ivsk_m. - * @param recipient - The address of the recipient. - * TODO: This is used only withing PXEOracleInterface and tests so we most likely just want to hide this. - */ public async syncTaggedLogsAsSender( + secret: DirectionalAppTaggingSecret, contractAddress: AztecAddress, - sender: AztecAddress, - recipient: AztecAddress, ): Promise { - const appTaggingSecret = await this.#calculateAppTaggingSecret(contractAddress, sender, recipient); - const [oldIndex] = await this.taggingDataProvider.getTaggingSecretsIndexesAsSender([appTaggingSecret], sender); + const oldIndex = await this.taggingDataProvider.getNextIndexAsSender(secret); // This algorithm works such that: // 1. If we find minimum consecutive empty logs in a window of logs we set the index to the index of the last log @@ -363,13 +374,15 @@ export class PXEOracleInterface implements ExecutionDataProvider { let [numConsecutiveEmptyLogs, currentIndex] = [0, oldIndex]; do { // We compute the tags for the current window of indexes - const currentTags = await timesParallel(WINDOW_SIZE, i => { - const indexedAppTaggingSecret = new IndexedTaggingSecret(appTaggingSecret, currentIndex + i); - return indexedAppTaggingSecret.computeSiloedTag(recipient, contractAddress); + const currentTags = await timesParallel(WINDOW_SIZE, async i => { + return SiloedTag.compute(await Tag.compute({ secret, index: currentIndex + i }), contractAddress); }); // We fetch the logs for the tags - const possibleLogs = await this.#getPrivateLogsByTags(currentTags); + // TODO: The following conversion is unfortunate and we should most likely just type the #getPrivateLogsByTags + // to accept SiloedTag[] instead of Fr[]. That would result in a large change so I didn't do it yet. + const tagsAsFr = currentTags.map(tag => tag.value); + const possibleLogs = await this.#getPrivateLogsByTags(tagsAsFr); // We find the index of the last log in the window that is not empty const indexOfLastLog = possibleLogs.findLastIndex(possibleLog => possibleLog.length !== 0); @@ -388,20 +401,17 @@ export class PXEOracleInterface implements ExecutionDataProvider { const contractName = await this.contractDataProvider.getDebugContractName(contractAddress); if (currentIndex !== oldIndex) { - await this.taggingDataProvider.setTaggingSecretsIndexesAsSender( - [new IndexedTaggingSecret(appTaggingSecret, currentIndex)], - sender, - ); + await this.taggingDataProvider.setNextIndexesAsSender([{ secret, index: currentIndex }]); - this.log.debug(`Syncing logs for sender ${sender} at contract ${contractName}(${contractAddress})`, { - sender, - secret: appTaggingSecret, + this.log.debug(`Syncing logs for secret ${secret.toString()} at contract ${contractName}(${contractAddress})`, { index: currentIndex, contractName, contractAddress, }); } else { - this.log.debug(`No new logs found for sender ${sender} at contract ${contractName}(${contractAddress})`); + this.log.debug( + `No new logs found for secret ${secret.toString()} at contract ${contractName}(${contractAddress})`, + ); } } @@ -443,11 +453,11 @@ export class PXEOracleInterface implements ExecutionDataProvider { // for logs the first time we don't receive any logs for a tag, we might never receive anything from that sender again. // Also there's a possibility that we have advanced our index, but the sender has reused it, so we might have missed // some logs. For these reasons, we have to look both back and ahead of the stored index. - let secretsAndWindows = secrets.map(secret => { + let secretsAndWindows = secrets.map(indexedSecret => { return { - appTaggingSecret: secret.appTaggingSecret, - leftMostIndex: Math.max(0, secret.index - WINDOW_HALF_SIZE), - rightMostIndex: secret.index + WINDOW_HALF_SIZE, + secret: indexedSecret.secret, + leftMostIndex: Math.max(0, indexedSecret.index - WINDOW_HALF_SIZE), + rightMostIndex: indexedSecret.index + WINDOW_HALF_SIZE, }; }); @@ -460,7 +470,9 @@ export class PXEOracleInterface implements ExecutionDataProvider { while (secretsAndWindows.length > 0) { const secretsForTheWholeWindow = getIndexedTaggingSecretsForTheWindow(secretsAndWindows); const tagsForTheWholeWindow = await Promise.all( - secretsForTheWholeWindow.map(secret => secret.computeSiloedTag(recipient, contractAddress)), + secretsForTheWholeWindow.map(async indexedSecret => { + return SiloedTag.compute(await Tag.compute(indexedSecret), contractAddress); + }), ); // We store the new largest indexes we find in the iteration in the following map to later on construct @@ -468,7 +480,10 @@ export class PXEOracleInterface implements ExecutionDataProvider { const newLargestIndexMapForIteration: { [k: string]: number } = {}; // Fetch the private logs for the tags and iterate over them - const logsByTags = await this.#getPrivateLogsByTags(tagsForTheWholeWindow); + // TODO: The following conversion is unfortunate and we should most likely just type the #getPrivateLogsByTags + // to accept SiloedTag[] instead of Fr[]. That would result in a large change so I didn't do it yet. + const tagsForTheWholeWindowAsFr = tagsForTheWholeWindow.map(tag => tag.value); + const logsByTags = await this.#getPrivateLogsByTags(tagsForTheWholeWindowAsFr); this.log.debug(`Found ${logsByTags.filter(logs => logs.length > 0).length} logs as recipient ${recipient}`, { recipient, contractName, @@ -492,17 +507,17 @@ export class PXEOracleInterface implements ExecutionDataProvider { // We retrieve the indexed tagging secret corresponding to the log as I need that to evaluate whether // a new largest index have been found. const secretCorrespondingToLog = secretsForTheWholeWindow[logIndex]; - const initialIndex = initialIndexesMap[secretCorrespondingToLog.appTaggingSecret.toString()]; + const initialIndex = initialIndexesMap[secretCorrespondingToLog.secret.toString()]; if ( secretCorrespondingToLog.index >= initialIndex && - (newLargestIndexMapForIteration[secretCorrespondingToLog.appTaggingSecret.toString()] === undefined || + (newLargestIndexMapForIteration[secretCorrespondingToLog.secret.toString()] === undefined || secretCorrespondingToLog.index >= - newLargestIndexMapForIteration[secretCorrespondingToLog.appTaggingSecret.toString()]) + newLargestIndexMapForIteration[secretCorrespondingToLog.secret.toString()]) ) { // We have found a new largest index so we store it for later processing (storing it in the db + fetching // the difference of the window sets of current and the next iteration) - newLargestIndexMapForIteration[secretCorrespondingToLog.appTaggingSecret.toString()] = + newLargestIndexMapForIteration[secretCorrespondingToLog.secret.toString()] = secretCorrespondingToLog.index + 1; this.log.debug( @@ -518,21 +533,23 @@ export class PXEOracleInterface implements ExecutionDataProvider { // for. Note that it's very unlikely that a new log from the current window would appear between the iterations // so we fetch the logs only for the difference of the window sets. const newSecretsAndWindows = []; - for (const [appTaggingSecret, newIndex] of Object.entries(newLargestIndexMapForIteration)) { - const secret = secrets.find(secret => secret.appTaggingSecret.toString() === appTaggingSecret); - if (secret) { + for (const [directionalAppTaggingSecret, newIndex] of Object.entries(newLargestIndexMapForIteration)) { + const maybeIndexedSecret = secrets.find( + indexedSecret => indexedSecret.secret.toString() === directionalAppTaggingSecret, + ); + if (maybeIndexedSecret) { newSecretsAndWindows.push({ - appTaggingSecret: secret.appTaggingSecret, + secret: maybeIndexedSecret.secret, // We set the left most index to the new index to avoid fetching the same logs again leftMostIndex: newIndex, rightMostIndex: newIndex + WINDOW_HALF_SIZE, }); // We store the new largest index in the map to later store it in the db. - newLargestIndexMapToStore[appTaggingSecret] = newIndex; + newLargestIndexMapToStore[directionalAppTaggingSecret] = newIndex; } else { throw new Error( - `Secret not found for appTaggingSecret ${appTaggingSecret}. This is a bug as it should never happen!`, + `Secret not found for directionalAppTaggingSecret ${directionalAppTaggingSecret}. This is a bug as it should never happen!`, ); } } @@ -542,11 +559,11 @@ export class PXEOracleInterface implements ExecutionDataProvider { } // At this point we have processed all the logs for the recipient so we store the new largest indexes in the db. - await this.taggingDataProvider.setTaggingSecretsIndexesAsRecipient( - Object.entries(newLargestIndexMapToStore).map( - ([appTaggingSecret, index]) => new IndexedTaggingSecret(Fr.fromHexString(appTaggingSecret), index), - ), - recipient, + await this.taggingDataProvider.setNextIndexesAsRecipient( + Object.entries(newLargestIndexMapToStore).map(([directionalAppTaggingSecret, index]) => ({ + secret: DirectionalAppTaggingSecret.fromString(directionalAppTaggingSecret), + index, + })), ); } } diff --git a/yarn-project/pxe/src/contract_function_simulator/tagging_utils.ts b/yarn-project/pxe/src/contract_function_simulator/tagging_utils.ts deleted file mode 100644 index 3148fd3faa52..000000000000 --- a/yarn-project/pxe/src/contract_function_simulator/tagging_utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Fr } from '@aztec/foundation/fields'; -import { IndexedTaggingSecret } from '@aztec/stdlib/logs'; - -// Half the size of the window we slide over the tagging secret indexes. -export const WINDOW_HALF_SIZE = 10; - -export function getIndexedTaggingSecretsForTheWindow( - secretsAndWindows: { appTaggingSecret: Fr; leftMostIndex: number; rightMostIndex: number }[], -): IndexedTaggingSecret[] { - const secrets: IndexedTaggingSecret[] = []; - for (const secretAndWindow of secretsAndWindows) { - for (let i = secretAndWindow.leftMostIndex; i <= secretAndWindow.rightMostIndex; i++) { - secrets.push(new IndexedTaggingSecret(secretAndWindow.appTaggingSecret, i)); - } - } - return secrets; -} - -/** - * Creates a map from app tagging secret to initial index. - * @param indexedTaggingSecrets - The indexed tagging secrets to get the initial indexes from. - * @returns The map from app tagging secret to initial index. - */ -export function getInitialIndexesMap(indexedTaggingSecrets: IndexedTaggingSecret[]): { [k: string]: number } { - const initialIndexes: { [k: string]: number } = {}; - - for (const indexedTaggingSecret of indexedTaggingSecrets) { - initialIndexes[indexedTaggingSecret.appTaggingSecret.toString()] = indexedTaggingSecret.index; - } - - return initialIndexes; -} diff --git a/yarn-project/pxe/src/storage/tagging_data_provider/tagging_data_provider.ts b/yarn-project/pxe/src/storage/tagging_data_provider/tagging_data_provider.ts index 739002280272..2e5a97bab484 100644 --- a/yarn-project/pxe/src/storage/tagging_data_provider/tagging_data_provider.ts +++ b/yarn-project/pxe/src/storage/tagging_data_provider/tagging_data_provider.ts @@ -1,93 +1,88 @@ -import type { Fr } from '@aztec/foundation/fields'; import { toArray } from '@aztec/foundation/iterable'; import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { IndexedTaggingSecret } from '@aztec/stdlib/logs'; +import type { DirectionalAppTaggingSecret, IndexedTaggingSecret } from '@aztec/stdlib/logs'; export class TaggingDataProvider { #store: AztecAsyncKVStore; #addressBook: AztecAsyncMap; - // Stores the last index used for each tagging secret, taking direction into account - // This is necessary to avoid reusing the same index for the same secret, which happens if - // sender and recipient are the same - #taggingSecretIndexesForSenders: AztecAsyncMap; - #taggingSecretIndexesForRecipients: AztecAsyncMap; + // Stores the next index to be used for each directional app tagging secret. Taking into account whether we are + // requesting the index as a sender or as a recipient because the sender and recipient can be in the same PXE. + #nextIndexesAsSenders: AztecAsyncMap; + #nextIndexesAsRecipients: AztecAsyncMap; constructor(store: AztecAsyncKVStore) { this.#store = store; this.#addressBook = this.#store.openMap('address_book'); - this.#taggingSecretIndexesForSenders = this.#store.openMap('tagging_secret_indexes_for_senders'); - this.#taggingSecretIndexesForRecipients = this.#store.openMap('tagging_secret_indexes_for_recipients'); + this.#nextIndexesAsSenders = this.#store.openMap('next_indexes_as_senders'); + this.#nextIndexesAsRecipients = this.#store.openMap('next_indexes_as_recipients'); } - setTaggingSecretsIndexesAsSender(indexedSecrets: IndexedTaggingSecret[], sender: AztecAddress) { - return this.#setTaggingSecretsIndexes(indexedSecrets, this.#taggingSecretIndexesForSenders, sender); - } + /** + * Sets the next indexes to be used to compute tags when sending a log. + * @param indexedSecrets - The indexed secrets to set the next indexes for. + * @throws If there are duplicate secrets in the input array + */ + setNextIndexesAsSender(indexedSecrets: IndexedTaggingSecret[]) { + this.#assertUniqueSecrets(indexedSecrets, 'sender'); - setTaggingSecretsIndexesAsRecipient(indexedSecrets: IndexedTaggingSecret[], recipient: AztecAddress) { - return this.#setTaggingSecretsIndexes(indexedSecrets, this.#taggingSecretIndexesForRecipients, recipient); + return Promise.all( + indexedSecrets.map(({ secret, index }) => this.#nextIndexesAsSenders.set(secret.toString(), index)), + ); } /** - * Sets the indexes of the tagging secrets for the given app tagging secrets in the direction of the given address. - * @dev We need to specify the direction because app tagging secrets are direction-less due to the way they are generated - * but we need to guarantee that the index is stored under a uni-directional key because the tags are themselves - * uni-directional. - * @param indexedSecrets - The app tagging secrets and indexes to set. - * @param storageMap - The storage map to set the indexes in. - * @param inDirectionOf - The address that the secrets are in the direction of. + * Sets the next indexes to be used to compute tags when looking for logs. + * @param indexedSecrets - The indexed secrets to set the next indexes for. + * @throws If there are duplicate secrets in the input array */ - #setTaggingSecretsIndexes( - indexedSecrets: IndexedTaggingSecret[], - storageMap: AztecAsyncMap, - inDirectionOf: AztecAddress, - ) { + setNextIndexesAsRecipient(indexedSecrets: IndexedTaggingSecret[]) { + this.#assertUniqueSecrets(indexedSecrets, 'recipient'); + return Promise.all( - indexedSecrets.map(indexedSecret => - storageMap.set(`${indexedSecret.appTaggingSecret.toString()}_${inDirectionOf.toString()}`, indexedSecret.index), - ), + indexedSecrets.map(({ secret, index }) => this.#nextIndexesAsRecipients.set(secret.toString(), index)), ); } - getTaggingSecretsIndexesAsRecipient(appTaggingSecrets: Fr[], recipient: AztecAddress) { - return this.#getTaggingSecretsIndexes(appTaggingSecrets, this.#taggingSecretIndexesForRecipients, recipient); + // It should never happen that we would receive a duplicate secrets on the input of the setters as everywhere + // we always just apply the largest index. Hence this check is a good way to catch bugs. + #assertUniqueSecrets(indexedSecrets: IndexedTaggingSecret[], role: 'sender' | 'recipient'): void { + const secretStrings = indexedSecrets.map(({ secret }) => secret.toString()); + const uniqueSecrets = new Set(secretStrings); + if (uniqueSecrets.size !== secretStrings.length) { + throw new Error(`Duplicate secrets found when setting next indexes as ${role}`); + } } - getTaggingSecretsIndexesAsSender(appTaggingSecrets: Fr[], sender: AztecAddress) { - return this.#getTaggingSecretsIndexes(appTaggingSecrets, this.#taggingSecretIndexesForSenders, sender); + /** + * Returns the next index to be used to compute a tag when sending a log. + * @param secret - The directional app tagging secret. + * @returns The next index to be used to compute a tag for the given directional app tagging secret. + */ + async getNextIndexAsSender(secret: DirectionalAppTaggingSecret): Promise { + return (await this.#nextIndexesAsSenders.getAsync(secret.toString())) ?? 0; } /** - * Returns the indexes of the tagging secrets for the given app tagging secrets in the direction of the given address. - * @dev We need to specify the direction because app tagging secrets are direction-less due to the way they are generated - * but we need to guarantee that the index is stored under a uni-directional key because the tags are themselves - * uni-directional. - * @param appTaggingSecrets - The app tagging secrets to get the indexes for. - * @param storageMap - The storage map to get the indexes from. - * @param inDirectionOf - The address that the secrets are in the direction of. - * @returns The indexes of the tagging secrets. + * Returns the next indexes to be used to compute tags when looking for logs. + * @param secrets - The directional app tagging secrets to obtain the indexes for. + * @returns The next indexes to be used to compute tags for the given directional app tagging secrets. */ - #getTaggingSecretsIndexes( - appTaggingSecrets: Fr[], - storageMap: AztecAsyncMap, - inDirectionOf: AztecAddress, - ): Promise { + getNextIndexesAsRecipient(secrets: DirectionalAppTaggingSecret[]): Promise { return Promise.all( - appTaggingSecrets.map( - async secret => (await storageMap.getAsync(`${secret.toString()}_${inDirectionOf.toString()}`)) ?? 0, - ), + secrets.map(async secret => (await this.#nextIndexesAsRecipients.getAsync(secret.toString())) ?? 0), ); } resetNoteSyncData(): Promise { return this.#store.transactionAsync(async () => { - const recipients = await toArray(this.#taggingSecretIndexesForRecipients.keysAsync()); - await Promise.all(recipients.map(recipient => this.#taggingSecretIndexesForRecipients.delete(recipient))); - const senders = await toArray(this.#taggingSecretIndexesForSenders.keysAsync()); - await Promise.all(senders.map(sender => this.#taggingSecretIndexesForSenders.delete(sender))); + const keysForSenders = await toArray(this.#nextIndexesAsSenders.keysAsync()); + await Promise.all(keysForSenders.map(secret => this.#nextIndexesAsSenders.delete(secret))); + const keysForRecipients = await toArray(this.#nextIndexesAsRecipients.keysAsync()); + await Promise.all(keysForRecipients.map(secret => this.#nextIndexesAsRecipients.delete(secret))); }); } diff --git a/yarn-project/pxe/src/tagging/constants.ts b/yarn-project/pxe/src/tagging/constants.ts new file mode 100644 index 000000000000..f69b52f95d73 --- /dev/null +++ b/yarn-project/pxe/src/tagging/constants.ts @@ -0,0 +1,2 @@ +// Half the size of the window we slide over the tagging secret indexes. +export const WINDOW_HALF_SIZE = 10; diff --git a/yarn-project/pxe/src/tagging/index.ts b/yarn-project/pxe/src/tagging/index.ts new file mode 100644 index 000000000000..a2b34e9e5461 --- /dev/null +++ b/yarn-project/pxe/src/tagging/index.ts @@ -0,0 +1,6 @@ +export * from './tag.js'; +export * from './constants.js'; +export * from './siloed_tag.js'; +export * from './utils.js'; +export { DirectionalAppTaggingSecret } from '@aztec/stdlib/logs'; +export { type IndexedTaggingSecret } from '@aztec/stdlib/logs'; diff --git a/yarn-project/pxe/src/tagging/siloed_tag.ts b/yarn-project/pxe/src/tagging/siloed_tag.ts new file mode 100644 index 000000000000..22076a7b473b --- /dev/null +++ b/yarn-project/pxe/src/tagging/siloed_tag.ts @@ -0,0 +1,22 @@ +import { poseidon2Hash } from '@aztec/foundation/crypto'; +import type { Fr } from '@aztec/foundation/fields'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; + +import type { Tag } from './tag.js'; + +/** + * Represents a tag used in private log as it "appears on the chain" - that is the tag is siloed with a contract + * address that emitted the log. + */ +export class SiloedTag { + private constructor(public readonly value: Fr) {} + + static async compute(tag: Tag, app: AztecAddress): Promise { + const siloedTag = await poseidon2Hash([app, tag.value]); + return new SiloedTag(siloedTag); + } + + toString(): string { + return this.value.toString(); + } +} diff --git a/yarn-project/pxe/src/tagging/tag.ts b/yarn-project/pxe/src/tagging/tag.ts new file mode 100644 index 000000000000..06c3becdc2ae --- /dev/null +++ b/yarn-project/pxe/src/tagging/tag.ts @@ -0,0 +1,16 @@ +import { poseidon2Hash } from '@aztec/foundation/crypto'; +import type { Fr } from '@aztec/foundation/fields'; +import type { IndexedTaggingSecret } from '@aztec/stdlib/logs'; + +/** + * Represents a tag of a private log. This is not the tag that "appears" on the chain as this tag is first siloed + * with a contract address by kernels before being included in the final log. + */ +export class Tag { + private constructor(public readonly value: Fr) {} + + static async compute(indexedTaggingSecret: IndexedTaggingSecret): Promise { + const tag = await poseidon2Hash([indexedTaggingSecret.secret.value, indexedTaggingSecret.index]); + return new Tag(tag); + } +} diff --git a/yarn-project/pxe/src/tagging/utils.ts b/yarn-project/pxe/src/tagging/utils.ts new file mode 100644 index 000000000000..c99847e8dd01 --- /dev/null +++ b/yarn-project/pxe/src/tagging/utils.ts @@ -0,0 +1,31 @@ +import type { DirectionalAppTaggingSecret, IndexedTaggingSecret } from '@aztec/stdlib/logs'; + +// TODO(benesjan): Make this return tags instead - this will moves some complexity from syncTaggedLogs +export function getIndexedTaggingSecretsForTheWindow( + secretsAndWindows: { secret: DirectionalAppTaggingSecret; leftMostIndex: number; rightMostIndex: number }[], +): IndexedTaggingSecret[] { + const secrets = []; + for (const secretAndWindow of secretsAndWindows) { + for (let i = secretAndWindow.leftMostIndex; i <= secretAndWindow.rightMostIndex; i++) { + secrets.push({ secret: secretAndWindow.secret, index: i }); + } + } + return secrets; +} + +/** + * Creates a map from directional app tagging secret to initial index. + * @param indexedTaggingSecrets - The indexed tagging secrets to get the initial indexes from. + * @returns The map from directional app tagging secret to initial index. + */ +export function getInitialIndexesMap(indexedTaggingSecrets: IndexedTaggingSecret[]): { + [k: string]: number; +} { + const initialIndexes: { [k: string]: number } = {}; + + for (const indexedTaggingSecret of indexedTaggingSecrets) { + initialIndexes[indexedTaggingSecret.secret.toString()] = indexedTaggingSecret.index; + } + + return initialIndexes; +} diff --git a/yarn-project/stdlib/src/keys/derivation.ts b/yarn-project/stdlib/src/keys/derivation.ts index da9bd0f02e6c..a4c6c627f2cd 100644 --- a/yarn-project/stdlib/src/keys/derivation.ts +++ b/yarn-project/stdlib/src/keys/derivation.ts @@ -1,9 +1,8 @@ import { GeneratorIndex } from '@aztec/constants'; -import { Grumpkin, poseidon2Hash, poseidon2HashWithSeparator, sha512ToGrumpkinScalar } from '@aztec/foundation/crypto'; +import { Grumpkin, poseidon2HashWithSeparator, sha512ToGrumpkinScalar } from '@aztec/foundation/crypto'; import { Fq, Fr, GrumpkinScalar } from '@aztec/foundation/fields'; import { AztecAddress } from '../aztec-address/index.js'; -import type { CompleteAddress } from '../contract/complete_address.js'; import type { KeyPrefix } from './key_types.js'; import { PublicKeys } from './public_keys.js'; import { getKeyGenerator } from './utils.js'; @@ -121,27 +120,3 @@ export async function deriveKeys(secretKey: Fr) { publicKeys, }; } - -// Returns shared tagging secret computed with Diffie-Hellman key exchange. -async function computeTaggingSecretPoint(knownAddress: CompleteAddress, ivsk: Fq, externalAddress: AztecAddress) { - const knownPreaddress = await computePreaddress(await knownAddress.publicKeys.hash(), knownAddress.partialAddress); - // TODO: #8970 - Computation of address point from x coordinate might fail - const externalAddressPoint = await externalAddress.toAddressPoint(); - 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 - - // 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, await computeAddressSecret(knownPreaddress, ivsk)); -} - -export async function computeAppTaggingSecret( - knownAddress: CompleteAddress, - ivsk: Fq, - externalAddress: AztecAddress, - app: AztecAddress, -) { - const taggingSecretPoint = await computeTaggingSecretPoint(knownAddress, ivsk, externalAddress); - return poseidon2Hash([taggingSecretPoint.x, taggingSecretPoint.y, app]); -} diff --git a/yarn-project/stdlib/src/logs/directional_app_tagging_secret.ts b/yarn-project/stdlib/src/logs/directional_app_tagging_secret.ts new file mode 100644 index 000000000000..c2619c6b2eca --- /dev/null +++ b/yarn-project/stdlib/src/logs/directional_app_tagging_secret.ts @@ -0,0 +1,72 @@ +import { Grumpkin, poseidon2Hash } from '@aztec/foundation/crypto'; +import { type Fq, Fr, type Point } from '@aztec/foundation/fields'; + +import type { AztecAddress } from '../aztec-address/index.js'; +import type { CompleteAddress } from '../contract/complete_address.js'; +import { computeAddressSecret, computePreaddress } from '../keys/derivation.js'; + +/** + * Directional application tagging secret used for log tagging. + * + * "Directional" because the derived secret is bound to the recipient + * address: A→B differs from B→A even with the same participants and app. + * + * Note: It's a bit unfortunate that this type resides in `stdlib` as the rest of the tagging functionality resides + * in `pxe/src/tagging`. We need to use this type in `IndexedTaggingSecret` that in turn is used by other types + * in stdlib hence there doesn't seem to be a good way around this. + */ +export class DirectionalAppTaggingSecret { + private constructor(public readonly value: Fr) {} + + /** + * Derives shared tagging secret and from that, the app address and recipient derives the directional app tagging + * secret. + * + * @param localAddress - The complete address of entity A in the shared tagging secret derivation scheme + * @param localIvsk - The incoming viewing secret key of entity A + * @param externalAddress - The address of entity B in the shared tagging secret derivation scheme + * @param app - Contract address to silo the secret to + * @param recipient - Recipient of the log. Defines the "direction of the secret". + * @returns The secret that can be used along with an index to compute a tag to be included in a log. + */ + static async compute( + localAddress: CompleteAddress, + localIvsk: Fq, + externalAddress: AztecAddress, + app: AztecAddress, + recipient: AztecAddress, + ): Promise { + const taggingSecretPoint = await computeSharedTaggingSecret(localAddress, localIvsk, externalAddress); + const appTaggingSecret = await poseidon2Hash([taggingSecretPoint.x, taggingSecretPoint.y, app]); + const directionalAppTaggingSecret = await poseidon2Hash([appTaggingSecret, recipient]); + + return new DirectionalAppTaggingSecret(directionalAppTaggingSecret); + } + + toString(): string { + return this.value.toString(); + } + + static fromString(str: string): DirectionalAppTaggingSecret { + return new DirectionalAppTaggingSecret(Fr.fromString(str)); + } +} + +// Returns shared tagging secret computed with Diffie-Hellman key exchange. +async function computeSharedTaggingSecret( + localAddress: CompleteAddress, + localIvsk: Fq, + externalAddress: AztecAddress, +): Promise { + const knownPreaddress = await computePreaddress(await localAddress.publicKeys.hash(), localAddress.partialAddress); + // TODO: #8970 - Computation of address point from x coordinate might fail + const externalAddressPoint = await externalAddress.toAddressPoint(); + const curve = new Grumpkin(); + // Given A (local complete address) -> B (external address) and h == preaddress + // Compute shared secret as S = (h_A + local_ivsk_A) * Addr_Point_B + + // Beware! h_a + local_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, await computeAddressSecret(knownPreaddress, localIvsk)); +} diff --git a/yarn-project/stdlib/src/logs/index.ts b/yarn-project/stdlib/src/logs/index.ts index 41ccdfa7c7e6..f1cfd7dba74a 100644 --- a/yarn-project/stdlib/src/logs/index.ts +++ b/yarn-project/stdlib/src/logs/index.ts @@ -1,4 +1,5 @@ export * from './log_with_tx_data.js'; +export * from './directional_app_tagging_secret.js'; export * from './indexed_tagging_secret.js'; export * from './contract_class_log.js'; export * from './public_log.js'; diff --git a/yarn-project/stdlib/src/logs/indexed_tagging_secret.ts b/yarn-project/stdlib/src/logs/indexed_tagging_secret.ts index 515c009e9219..8992e08f0103 100644 --- a/yarn-project/stdlib/src/logs/indexed_tagging_secret.ts +++ b/yarn-project/stdlib/src/logs/indexed_tagging_secret.ts @@ -1,48 +1,13 @@ -import { poseidon2Hash } from '@aztec/foundation/crypto'; -import { Fr } from '@aztec/foundation/fields'; - -import type { AztecAddress } from '../aztec-address/index.js'; - -export class IndexedTaggingSecret { - constructor( - public appTaggingSecret: Fr, - public index: number, - ) { - if (index < 0) { - throw new Error('IndexedTaggingSecret index out of bounds'); - } - } - - toFields(): Fr[] { - return [this.appTaggingSecret, new Fr(this.index)]; - } - - static fromFields(serialized: Fr[]) { - return new this(serialized[0], serialized[1].toNumber()); - } - - /** - * Computes the tag based on the app tagging secret, recipient and index. - * @dev By including the recipient we achieve "directionality" of the tag (when sending a note in the other - * direction, the tag will be different). - * @param recipient The recipient of the note - * @returns The tag. - */ - computeTag(recipient: AztecAddress) { - return poseidon2Hash([this.appTaggingSecret, recipient, this.index]); - } - - /** - * Computes the siloed tag. - * @dev We do this second layer of siloing (one was already done as the tagging secret is app-siloed) because kernels - * do that to protect against contract impersonation attacks. This extra layer of siloing in kernels ensures that - * a malicious contract cannot emit a note with a tag corresponding to another contract. - * @param recipient The recipient of the note - * @param app The app address - * @returns The siloed tag. - */ - async computeSiloedTag(recipient: AztecAddress, app: AztecAddress) { - const tag = await this.computeTag(recipient); - return poseidon2Hash([app, tag]); - } -} +import type { DirectionalAppTaggingSecret } from './directional_app_tagging_secret.js'; + +/** + * Represents a preimage of a private log tag (see `Tag` in `pxe/src/tagging`). + * + * Note: It's a bit unfortunate that this type resides in `stdlib` as the rest of the tagging functionality resides + * in `pxe/src/tagging`. But this type is used by other types in stdlib hence there doesn't seem to be a good way + * around this. + */ +export type IndexedTaggingSecret = { + secret: DirectionalAppTaggingSecret; + index: number; +}; diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index ff2ed3df7e8e..0bb58fce4f86 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -1004,6 +1004,6 @@ export class RPCTranslator { const nextAppTag = await this.handlerAsPrivate().privateGetNextAppTagAsSender(sender, recipient); - return toForeignCallResult([toSingle(nextAppTag)]); + return toForeignCallResult([toSingle(nextAppTag.value)]); } }