Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub contract TestLog {
event::event_emission::{emit_event_in_private, emit_event_in_public},
macros::{events::event, functions::{private, public}, storage::storage},
messages::message_delivery::MessageDelivery,
oracle::random::random,
protocol_types::{address::AztecAddress, traits::FromField},
state_vars::PrivateSet,
};
Expand Down Expand Up @@ -75,4 +76,36 @@ pub contract TestLog {
&mut context,
);
}

#[private]
fn emit_encrypted_events_nested(other: AztecAddress, num_nested_calls: u32) {
// Safety: We use the following just as an arbitrary test value
let random_value_0 = unsafe { random() };
// Safety: We use the following just as an arbitrary test value
let random_value_1 = unsafe { random() };
// Safety: We use the following just as an arbitrary test value
let random_value_2 = unsafe { random() };
// Safety: We use the following just as an arbitrary test value
let random_value_3 = unsafe { random() };

emit_event_in_private(
ExampleEvent0 { value0: random_value_0, value1: random_value_1 },
&mut context,
other,
MessageDelivery.UNCONSTRAINED_ONCHAIN,
);

emit_event_in_private(
ExampleEvent0 { value0: random_value_2, value1: random_value_3 },
&mut context,
other,
MessageDelivery.UNCONSTRAINED_ONCHAIN,
);

if num_nested_calls > 0 {
TestLog::at(context.this_address())
.emit_encrypted_events_nested(other, num_nested_calls - 1)
.call(&mut context);
}
}
}
58 changes: 58 additions & 0 deletions yarn-project/end-to-end/src/e2e_event_logs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,63 @@ describe('Logs', () => {
.sort(exampleEvent1Sort),
);
});

// This test verifies that tags remain unique:
// 1. Across nested calls within the same contract, confirming proper propagation of the ExecutionTaggingIndexCache
// between calls,
// 2. across separate transactions that interact with the same contract function, confirming proper persistence
// of the cache contents in the database (TaggingDataProvider) after transaction proving completes.
it('produces unique tags for encrypted logs across nested calls and different transactions', async () => {
let tx1Tags: string[];
// With 4 nestings we have 5 total calls, each emitting 2 logs => 10 logs
const tx1NumLogs = 10;
{
// Call the private function that emits two encrypted logs per call and recursively nests 4 times
const tx = await testLogContract.methods
.emit_encrypted_events_nested(account2Address, 4)
.send({ from: account1Address })
.wait();

const blockNumber = tx.blockNumber!;

// Fetch raw private logs for that block and check tag uniqueness
const privateLogs = await aztecNode.getPrivateLogs(blockNumber, 1);
const logs = privateLogs.filter(l => !l.isEmpty());

expect(logs.length).toBe(tx1NumLogs);

const tags = logs.map(l => l.fields[0].toString());
expect(new Set(tags).size).toBe(tx1NumLogs);
tx1Tags = tags;
}

let tx2Tags: string[];
// With 2 nestings we have 3 total calls, each emitting 2 logs => 6 logs
const tx2NumLogs = 6;
{
// Call the private function that emits two encrypted logs per call and recursively nests 2 times
const tx = await testLogContract.methods
.emit_encrypted_events_nested(account2Address, 2)
.send({ from: account1Address })
.wait();

const blockNumber = tx.blockNumber!;

// Fetch raw private logs for that block and check tag uniqueness
const privateLogs = await aztecNode.getPrivateLogs(blockNumber, 1);
const logs = privateLogs.filter(l => !l.isEmpty());

expect(logs.length).toBe(tx2NumLogs);

const tags = logs.map(l => l.fields[0].toString());
expect(new Set(tags).size).toBe(tx2NumLogs);
tx2Tags = tags;
}

// Now we create a set from both tx1Tags and tx2Tags and expect it to be the same size as the sum of the number
// of logs in both transactions
const allTags = new Set([...tx1Tags, ...tx2Tags]);
expect(allTags.size).toBe(tx1NumLogs + tx2NumLogs);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
import type { ContractDataProvider } from '../storage/index.js';
import type { ExecutionDataProvider } from './execution_data_provider.js';
import { ExecutionNoteCache } from './execution_note_cache.js';
import { ExecutionTaggingIndexCache } from './execution_tagging_index_cache.js';
import { HashedValuesCache } from './hashed_values_cache.js';
import { Oracle } from './oracle/oracle.js';
import { executePrivateFunction, verifyCurrentClassId } from './oracle/private_execution.js';
Expand Down Expand Up @@ -133,6 +134,7 @@ export class ContractFunctionSimulator {

const txRequestHash = await request.toTxRequest().hash();
const noteCache = new ExecutionNoteCache(txRequestHash);
const taggingIndexCache = new ExecutionTaggingIndexCache();

const privateExecutionOracle = new PrivateExecutionOracle(
request.firstCallArgsHash,
Expand All @@ -143,6 +145,7 @@ export class ContractFunctionSimulator {
request.capsules,
HashedValuesCache.create(request.argsOfCalls),
noteCache,
taggingIndexCache,
this.executionDataProvider,
0, // totalPublicArgsCount
startSideEffectCounter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ 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';

Expand Down Expand Up @@ -240,12 +239,11 @@ export interface ExecutionDataProvider {
syncTaggedLogsAsSender(secret: DirectionalAppTaggingSecret, contractAddress: AztecAddress): Promise<void>;

/**
* 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.
* 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.
*/
getNextAppTagAsSender(secret: DirectionalAppTaggingSecret): Promise<Tag>;
getNextIndexAsSender(secret: DirectionalAppTaggingSecret): Promise<number>;

/**
* Synchronizes the private logs tagged with scoped addresses and all the senders in the address book. Stores the found
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DirectionalAppTaggingSecret, type IndexedTaggingSecret } from '@aztec/stdlib/logs';

/**
* A map that stores the tagging index for a given directional app tagging secret.
* Note: The directional app tagging secret is unique for a (sender, recipient, contract) tuple while the direction
* of sender -> recipient matters.
*/
export class ExecutionTaggingIndexCache {
private taggingIndexMap: Map<string, number> = new Map();

public getTaggingIndex(secret: DirectionalAppTaggingSecret): number | undefined {
return this.taggingIndexMap.get(secret.toString());
}

public setTaggingIndex(secret: DirectionalAppTaggingSecret, index: number) {
const currentValue = this.taggingIndexMap.get(secret.toString());
if (currentValue !== undefined && currentValue !== index - 1) {
throw new Error(`Invalid tagging index update. Current value: ${currentValue}, new value: ${index}`);
}
this.taggingIndexMap.set(secret.toString(), index);
}

public getIndexedTaggingSecrets(): IndexedTaggingSecret[] {
return Array.from(this.taggingIndexMap.entries()).map(([secret, index]) => ({
secret: DirectionalAppTaggingSecret.fromString(secret),
index,
}));
}
}
1 change: 1 addition & 0 deletions yarn-project/pxe/src/contract_function_simulator/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { ExecutionNoteCache } from './execution_note_cache.js';
export { ExecutionTaggingIndexCache } from './execution_tagging_index_cache.js';
export { HashedValuesCache } from './hashed_values_cache.js';
export { pickNotes } from './pick_notes.js';
export type { NoteData, IMiscOracle, IUtilityExecutionOracle, IPrivateExecutionOracle } from './oracle/interfaces.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ 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';
Expand Down Expand Up @@ -303,8 +302,8 @@ describe('Private Execution test suite', () => {
throw new Error(`Unknown address: ${address}. Recipient: ${recipient}, Owner: ${owner}`);
});

executionDataProvider.getNextAppTagAsSender.mockImplementation((_secret: DirectionalAppTaggingSecret) => {
return Promise.resolve(Tag.compute({ secret: _secret, index: 0 }));
executionDataProvider.getNextIndexAsSender.mockImplementation((_secret: DirectionalAppTaggingSecret) => {
return Promise.resolve(0);
});
executionDataProvider.getFunctionArtifact.mockImplementation(async (address, selector) => {
const contract = contracts[address.toString()];
Expand All @@ -331,8 +330,12 @@ describe('Private Execution test suite', () => {
});

executionDataProvider.syncTaggedLogs.mockImplementation((_, __) => Promise.resolve());
executionDataProvider.calculateDirectionalAppTaggingSecret.mockResolvedValue(
DirectionalAppTaggingSecret.fromString('0x1'),
// Provide tagging-related mocks expected by private log emission
executionDataProvider.calculateDirectionalAppTaggingSecret.mockImplementation((_contract, _sender, _recipient) => {
return Promise.resolve(DirectionalAppTaggingSecret.fromString('0x1'));
});
executionDataProvider.syncTaggedLogsAsSender.mockImplementation((_directionalAppTaggingSecret, _contractAddress) =>
Promise.resolve(),
);
executionDataProvider.loadCapsule.mockImplementation((_, __) => Promise.resolve(null));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export async function executePrivateFunction(
const newNotes = privateExecutionOracle.getNewNotes();
const noteHashNullifierCounterMap = privateExecutionOracle.getNoteHashNullifierCounterMap();
const offchainEffects = privateExecutionOracle.getOffchainEffects();
const indexedTaggingSecrets = privateExecutionOracle.getIndexedTaggingSecrets();
const nestedExecutionResults = privateExecutionOracle.getNestedExecutionResults();

let timerSubtractionList = nestedExecutionResults;
Expand All @@ -111,6 +112,7 @@ export async function executePrivateFunction(
noteHashNullifierCounterMap,
rawReturnValues,
offchainEffects,
indexedTaggingSecrets,
nestedExecutionResults,
contractClassLogs,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { AuthWitness } from '@aztec/stdlib/auth-witness';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/stdlib/hash';
import { PrivateContextInputs } from '@aztec/stdlib/kernel';
import type { ContractClassLog } from '@aztec/stdlib/logs';
import type { ContractClassLog, IndexedTaggingSecret } from '@aztec/stdlib/logs';
import { Note, type NoteStatus } from '@aztec/stdlib/note';
import {
type BlockHeader,
Expand All @@ -26,9 +26,10 @@ import {
type TxContext,
} from '@aztec/stdlib/tx';

import type { Tag } from '../../tagging/tag.js';
import { Tag } from '../../tagging/tag.js';
import type { ExecutionDataProvider } from '../execution_data_provider.js';
import type { ExecutionNoteCache } from '../execution_note_cache.js';
import { ExecutionTaggingIndexCache } from '../execution_tagging_index_cache.js';
import type { HashedValuesCache } from '../hashed_values_cache.js';
import { pickNotes } from '../pick_notes.js';
import type { IPrivateExecutionOracle, NoteData } from './interfaces.js';
Expand Down Expand Up @@ -76,6 +77,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
capsules: Capsule[],
private readonly executionCache: HashedValuesCache,
private readonly noteCache: ExecutionNoteCache,
private readonly taggingIndexCache: ExecutionTaggingIndexCache,
executionDataProvider: ExecutionDataProvider,
private totalPublicCalldataCount: number = 0,
protected sideEffectCounter: number = 0,
Expand Down Expand Up @@ -149,6 +151,13 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
return this.offchainEffects;
}

/**
* Return the tagging indexes incremented by this execution along with the directional app tagging secrets.
*/
public getIndexedTaggingSecrets(): IndexedTaggingSecret[] {
return this.taggingIndexCache.getIndexedTaggingSecrets();
}
Comment on lines +155 to +159
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solidified me not liking the 'indexed tagging secret' name - I just don't really know what it means. The 'tagging secret' part is particuarly annoying because it hides a lot of stuff and makes me have to think about the complexity of the tag derivation. Like, the tagging secret is not really indexed - that'd be the tag. What this is is the 'app siloed sender/recipient shared secret'. Perhaps we need to come up with a name for the thing that we combine with an index to get a tag, e.g. the 'pretag', 'tag seed', or whatever.

At any rate, I'd also change the name of this function, which is not very descriptive. Get which tagging secrets? I'd call it 'getUsedTags' or something nicer that conveys that these are the tags that this execution consumed and should be a) tracked by tx and b) never used again.

Copy link
Contributor Author

@benesjan benesjan Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a point. The IndexedTaggingSecret name is indeed bad as the point of it is not indexing a secret (indexing in a sense of it being easily obtainable from a data store) but it is either used as a preimage of a tag or as a data container to send to the db to update the next index to be used for a given directional app tagging secret.

As to the name of the function it's quite confusing here because it's not the last used indexes but it's the indexes that are to be used in a tx. This is done because it makes the tagging data provider simpler as then we never need to return undefined from the getNextIndexAsSender function as by returning next we can just return 0 in the beginning:

Image

But anyway I think it would be a good way to change this as handling undefined in 1 place is better then having all these function confusing.

I propose the following:

  1. Rename IndexedTaggingSecret as PreTag- it's a preimage of a tag so I think this name is clear,
  2. refactor all the functions to return last used tag instead of the next one as it's clearer,
  3. rename this function to getUsedPreTags.

Doing all these refactor would explode the scope of this PR so I would do that in a followup one.

Sounds good?


/**
* Return the nested execution results during this execution.
*/
Expand Down Expand Up @@ -193,21 +202,40 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
* @returns An app tag to be used in a log.
*/
public async privateGetNextAppTagAsSender(sender: AztecAddress, recipient: AztecAddress): Promise<Tag> {
const directionalAppTaggingSecret = await this.executionDataProvider.calculateDirectionalAppTaggingSecret(
const secret = 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.
// If we have the tagging index in the cache, we use it. If not we obtain it from the execution data provider.
// TODO(benesjan): Make this be `getLastUsedIndex` and refactor this function to look as proposed in this comment:
// https://github.com/AztecProtocol/aztec-packages/pull/17445#discussion_r2400365845
const maybeTaggingIndex = this.taggingIndexCache.getTaggingIndex(secret);
let taggingIndex: number;

if (maybeTaggingIndex !== undefined) {
taggingIndex = maybeTaggingIndex;
} else {
// This is a tagging secret we've not yet used in this tx, so first sync our store to make sure its indices
// are up to date. We do this here because this store is not synced as part of the global sync because
// that'd be wasteful as most tagging secrets are not used in each tx.
this.log.debug(`Syncing tagged logs as sender ${sender} for contract ${this.contractAddress}`, {
directionalAppTaggingSecret: secret,
recipient,
});
await this.executionDataProvider.syncTaggedLogsAsSender(secret, this.contractAddress);
taggingIndex = await this.executionDataProvider.getNextIndexAsSender(secret);
}

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);
// Now we increment the index by 1 and store it in the cache.
const nextTaggingIndex = taggingIndex + 1;
this.log.debug(
`Incrementing tagging index for sender: ${sender}, recipient: ${recipient}, contract: ${this.contractAddress} to ${nextTaggingIndex}`,
);
this.taggingIndexCache.setTaggingIndex(secret, nextTaggingIndex);

return Tag.compute({ secret, index: taggingIndex });
}

/**
Expand Down Expand Up @@ -480,6 +508,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
this.capsules,
this.executionCache,
this.noteCache,
this.taggingIndexCache,
this.executionDataProvider,
this.totalPublicCalldataCount,
sideEffectCounter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,32 +269,8 @@ export class PXEOracleInterface implements ExecutionDataProvider {
return this.taggingDataProvider.getSenderAddresses();
}

/**
* Returns the next app tag for a given sender and recipient pair.
* @param contractAddress - The contract address emitting the log.
* @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(secret: DirectionalAppTaggingSecret): Promise<Tag> {
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})`, {
// directionalAppTaggingSecret,
// sender,
// recipient,
// contractName,
// contractAddress,
// });
await this.taggingDataProvider.setNextIndexesAsSender([{ secret, index: index + 1 }]);

// Compute and return the tag using the current index
return Tag.compute({ secret, index });
public getNextIndexAsSender(secret: DirectionalAppTaggingSecret): Promise<number> {
return this.taggingDataProvider.getNextIndexAsSender(secret);
}

public async calculateDirectionalAppTaggingSecret(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('Private Kernel Sequencer', () => {
new Map(),
[],
[],
[],
(dependencies[fnName] || []).map(name => createCallExecutionResult(name)),
[],
);
Expand Down
Loading
Loading