Skip to content

Commit

Permalink
feat: pxe can filter on emitted events (#6947)
Browse files Browse the repository at this point in the history
Please read [contributing guidelines](CONTRIBUTING.md) and remove this
line.
  • Loading branch information
sklppy88 authored Jun 12, 2024
1 parent 7a68be4 commit ee45fda
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 24 deletions.
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 @@ -184,4 +192,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[] }`;

// 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 @@ -365,9 +372,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 @@ -808,4 +811,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;
}
}

0 comments on commit ee45fda

Please sign in to comment.