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
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/rpc_clients/pxe_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ export const createPXEClient = (url: string, fetch = makeFetch([1, 2, 3], false)
false,
'pxe',
fetch,
);
) as PXE;
18 changes: 17 additions & 1 deletion yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type AuthWitness,
type EventMetadata,
type ExtendedNote,
type FunctionCall,
type GetUnencryptedLogsResponse,
Expand All @@ -17,7 +18,14 @@ import {
type TxReceipt,
} from '@aztec/circuit-types';
import { type NoteProcessorStats } from '@aztec/circuit-types/stats';
import { type AztecAddress, type CompleteAddress, type Fq, type Fr, type PartialAddress } from '@aztec/circuits.js';
import {
type AztecAddress,
type CompleteAddress,
type Fq,
type Fr,
type PartialAddress,
type Point,
} from '@aztec/circuits.js';
import { type ContractArtifact } from '@aztec/foundation/abi';
import { type ContractClassWithId, type ContractInstanceWithAddress } from '@aztec/types/contracts';
import { type NodeInfo } from '@aztec/types/interfaces';
Expand Down Expand Up @@ -181,4 +189,12 @@ export abstract class BaseWallet implements Wallet {
getPXEInfo(): Promise<PXEInfo> {
return this.pxe.getPXEInfo();
}
getEvents<T>(
from: number,
limit: number,
eventMetadata: EventMetadata<T>,
ivpk: Point = this.getCompleteAddress().publicKeys.masterIncomingViewingPublicKey,
): Promise<T[]> {
return this.pxe.getEvents(from, limit, eventMetadata, ivpk);
}
}
35 changes: 18 additions & 17 deletions yarn-project/builder/src/contract-interface-gen/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,50 +239,51 @@ function generateNotesGetter(input: ContractArtifact) {
`;
}

// This is of type AbiType
// events is of type AbiType
function generateEvents(events: any[] | undefined) {
if (events === undefined) {
return { events: '', eventDefs: '' };
}

const eventsStrings = events.map(event => {
const eventName = event.path.split('::')[1];
const eventsMetadata = events.map(event => {
const eventName = event.path.split('::').at(-1);

const eventDefProps = event.fields.map((field: any) => `${field.name}: Fr`);
const eventDefs = `
const eventDef = `
export type ${eventName} = {
${eventDefProps.join('\n')}
}
`;

const fieldNames = event.fields.map((field: any) => `"${field.name}"`);
const eventsType = `${eventName}: {decode: (payload: L1EventPayload | undefined) => ${eventName} | undefined }`;
const eventType = `${eventName}: {decode: (payload: L1EventPayload | undefined) => ${eventName} | undefined, functionSelector: FunctionSelector, fieldNames: string[] }`;

@benesjan benesjan Jun 18, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@sklppy88 There is an EventSelector class and it should be used here instead of FunctionSelector. Could you please do the changes?

Created an issue for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will do, thank you for the heads up.


// Get the last item in path
const eventDecode = `${event.path.split('::').at(-1)}: {
decode: this.decodeEvent(${event.fields.length}, '${eventName}(${event.fields
const eventImpl = `${eventName}: {
decode: this.decodeEvent(${event.fields.length}, FunctionSelector.fromSignature('${eventName}(${event.fields
.map(() => 'Field')
.join(',')})', [${fieldNames}])
.join(',')})'), [${fieldNames}]),
functionSelector: FunctionSelector.fromSignature('${eventName}(${event.fields.map(() => 'Field').join(',')})'),
fieldNames: [${fieldNames}],
}`;

return {
eventDefs,
eventsType,
eventDecode,
eventDef,
eventType,
eventImpl,
};
});

return {
eventDefs: eventsStrings.map(({ eventDefs }) => eventDefs).join('\n'),
eventDefs: eventsMetadata.map(({ eventDef }) => eventDef).join('\n'),
events: `
// Partial application is chosen is to avoid the duplication of so much codegen.
private static decodeEvent<T>(fieldsLength: number, functionSignature: string, fields: string[]): (payload: L1EventPayload | undefined) => T | undefined {
private static decodeEvent<T>(fieldsLength: number, functionSelector: FunctionSelector, fields: string[]): (payload: L1EventPayload | undefined) => T | undefined {
return (payload: L1EventPayload | undefined): T | undefined => {
if (payload === undefined) {
return undefined;
}
if (
!FunctionSelector.fromSignature(functionSignature).equals(
!functionSelector.equals(
FunctionSelector.fromField(payload.eventTypeId),
)
) {
Expand All @@ -304,9 +305,9 @@ function generateEvents(events: any[] | undefined) {
};
}

public static get events(): { ${eventsStrings.map(({ eventsType }) => eventsType).join(', ')} } {
public static get events(): { ${eventsMetadata.map(({ eventType }) => eventType).join(', ')} } {
return {
${eventsStrings.map(({ eventDecode }) => eventDecode).join(',\n')}
${eventsMetadata.map(({ eventImpl }) => eventImpl).join(',\n')}
};
}
`,
Expand Down
32 changes: 29 additions & 3 deletions yarn-project/circuit-types/src/interfaces/pxe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { type AztecAddress, type CompleteAddress, type Fq, type Fr, type PartialAddress } from '@aztec/circuits.js';
import { type ContractArtifact } from '@aztec/foundation/abi';
import {
type AztecAddress,
type CompleteAddress,
type Fq,
type Fr,
type PartialAddress,
type Point,
} from '@aztec/circuits.js';
import { type ContractArtifact, type FunctionSelector } from '@aztec/foundation/abi';
import {
type ContractClassWithId,
type ContractInstanceWithAddress,
Expand All @@ -9,7 +16,7 @@ import { type NodeInfo } from '@aztec/types/interfaces';

import { type AuthWitness } from '../auth_witness.js';
import { type L2Block } from '../l2_block.js';
import { type GetUnencryptedLogsResponse, type LogFilter } from '../logs/index.js';
import { type GetUnencryptedLogsResponse, type L1EventPayload, type LogFilter } from '../logs/index.js';
import { type ExtendedNote } from '../notes/index.js';
import { type NoteFilter } from '../notes/note_filter.js';
import { type NoteProcessorStats } from '../stats/stats.js';
Expand Down Expand Up @@ -355,9 +362,28 @@ export interface PXE {
* TODO(@spalladino): Same notes as above.
*/
isContractPubliclyDeployed(address: AztecAddress): Promise<boolean>;

/**
* Returns the events of a specified type.
* @param from - The block number to search from.
* @param limit - The amount of blocks to search.
* @param eventMetadata - Identifier of the event. This should be the class generated from the contract. e.g. Contract.events.Event
* @param ivpk - The incoming viewing public key that corresponds to the incoming viewing secret key that can decrypt the log.
* @returns - The deserialized events.
*/
getEvents<T>(from: number, limit: number, eventMetadata: EventMetadata<T>, ivpk: Point): Promise<T[]>;
}
// docs:end:pxe-interface

/**
* The shape of the event generated on the Contract.
*/
export interface EventMetadata<T> {
decode(payload: L1EventPayload): T | undefined;
functionSelector: FunctionSelector;
fieldNames: string[];
}

/**
* Provides basic information about the running PXE.
*/
Expand Down
44 changes: 43 additions & 1 deletion yarn-project/end-to-end/src/e2e_event_logs.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type AccountWalletWithSecretKey, type AztecNode, Fr, L1EventPayload, TaggedLog } from '@aztec/aztec.js';
import { deriveMasterIncomingViewingSecretKey } from '@aztec/circuits.js';
import { makeTuple } from '@aztec/foundation/array';
import { TestLogContract } from '@aztec/noir-contracts.js';
import { type Tuple } from '@aztec/foundation/serialize';
import { type ExampleEvent0, type ExampleEvent1, TestLogContract } from '@aztec/noir-contracts.js';

import { jest } from '@jest/globals';

Expand Down Expand Up @@ -110,5 +111,46 @@ describe('Logs', () => {
const badEvent1 = TestLogContract.events.ExampleEvent0.decode(decryptedLog1!.payload);
expect(badEvent1).toBe(undefined);
});

it('emits multiple events as encrypted logs and decodes them', async () => {
const randomness = makeTuple(5, makeTuple.bind(undefined, 2, Fr.random)) as Tuple<Tuple<Fr, 2>, 5>;
const preimage = makeTuple(5, makeTuple.bind(undefined, 4, Fr.random)) as Tuple<Tuple<Fr, 4>, 5>;

let i = 0;
const firstTx = await testLogContract.methods.emit_encrypted_events(randomness[i], preimage[i]).send().wait();
await Promise.all(
[...new Array(3)].map(() =>
testLogContract.methods.emit_encrypted_events(randomness[++i], preimage[i]).send().wait(),
),
);
const lastTx = await testLogContract.methods.emit_encrypted_events(randomness[++i], preimage[i]).send().wait();

const collectedEvent0s = await wallets[0].getEvents(
firstTx.blockNumber!,
lastTx.blockNumber! - firstTx.blockNumber! + 1,
TestLogContract.events.ExampleEvent0,
);

const collectedEvent1s = await wallets[0].getEvents(
firstTx.blockNumber!,
lastTx.blockNumber! - firstTx.blockNumber! + 1,
TestLogContract.events.ExampleEvent1,
// This function can also be called specifying the incoming viewing public key associated with the encrypted event.
wallets[0].getCompleteAddress().publicKeys.masterIncomingViewingPublicKey,
);

expect(collectedEvent0s.length).toBe(5);
expect(collectedEvent1s.length).toBe(5);

const exampleEvent0Sort = (a: ExampleEvent0, b: ExampleEvent0) => (a.value0 > b.value0 ? 1 : -1);
expect(collectedEvent0s.sort(exampleEvent0Sort)).toStrictEqual(
preimage.map(preimage => ({ value0: preimage[0], value1: preimage[1] })).sort(exampleEvent0Sort),
);

const exampleEvent1Sort = (a: ExampleEvent1, b: ExampleEvent1) => (a.value2 > b.value2 ? 1 : -1);
expect(collectedEvent1s.sort(exampleEvent1Sort)).toStrictEqual(
preimage.map(preimage => ({ value2: preimage[2], value3: preimage[3] })).sort(exampleEvent1Sort),
);
});
});
});
46 changes: 45 additions & 1 deletion yarn-project/pxe/src/pxe_service/pxe_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
type AztecNode,
EncryptedNoteTxL2Logs,
EncryptedTxL2Logs,
type EventMetadata,
ExtendedNote,
type FunctionCall,
type GetUnencryptedLogsResponse,
L1EventPayload,
type L2Block,
type LogFilter,
MerkleTreeId,
Expand All @@ -15,6 +17,7 @@ import {
type ProofCreator,
SimulatedTx,
SimulationError,
TaggedLog,
Tx,
type TxEffect,
type TxExecutionRequest,
Expand All @@ -32,7 +35,7 @@ import {
} from '@aztec/circuits.js';
import { computeNoteHashNonce, siloNullifier } from '@aztec/circuits.js/hash';
import { type ContractArtifact, type DecodedReturn, FunctionSelector, encodeArguments } from '@aztec/foundation/abi';
import { type Fq, Fr } from '@aztec/foundation/fields';
import { type Fq, Fr, type Point } from '@aztec/foundation/fields';
import { SerialQueue } from '@aztec/foundation/fifo';
import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { type KeyStore } from '@aztec/key-store';
Expand Down Expand Up @@ -756,4 +759,45 @@ export class PXEService implements PXE {
public async isContractPubliclyDeployed(address: AztecAddress): Promise<boolean> {
return !!(await this.node.getContract(address));
}

public async getEvents<T>(from: number, limit: number, eventMetadata: EventMetadata<T>, ivpk: Point): Promise<T[]> {
const blocks = await this.node.getBlocks(from, limit);

const txEffects = blocks.flatMap(block => block.body.txEffects);
const encryptedTxLogs = txEffects.flatMap(txEffect => txEffect.encryptedLogs);

const encryptedLogs = encryptedTxLogs.flatMap(encryptedTxLog => encryptedTxLog.unrollLogs());

const ivsk = await this.keyStore.getMasterSecretKey(ivpk);

const visibleEvents = encryptedLogs
.map(encryptedLog => TaggedLog.decryptAsIncoming(encryptedLog, ivsk, L1EventPayload))
.filter(item => item !== undefined) as TaggedLog<L1EventPayload>[];

const decodedEvents = visibleEvents
.map(visibleEvent => {
if (visibleEvent.payload === undefined) {
return undefined;
}
if (!FunctionSelector.fromField(visibleEvent.payload.eventTypeId).equals(eventMetadata.functionSelector)) {
return undefined;
}
if (visibleEvent.payload.event.items.length !== eventMetadata.fieldNames.length) {
throw new Error(
'Something is weird here, we have matching FunctionSelectors, but the actual payload has mismatched length',
);
}

return eventMetadata.fieldNames.reduce(
(acc, curr, i) => ({
...acc,
[curr]: visibleEvent.payload.event.items[i],
}),
{} as T,
);
})
.filter(visibleEvent => visibleEvent !== undefined) as T[];

return decodedEvents;
}
}