From d2654343dc0b1b942ddf00dadb1200814313583d Mon Sep 17 00:00:00 2001 From: sklppy88 Date: Thu, 6 Jun 2024 13:59:01 +0000 Subject: [PATCH] Init Initial --- .../aztec.js/src/rpc_clients/pxe_client.ts | 2 +- .../aztec.js/src/wallet/base_wallet.ts | 13 ++++- .../src/contract-interface-gen/typescript.ts | 35 ++++++------ .../circuit-types/src/interfaces/pxe.ts | 32 +++++++++-- .../end-to-end/src/e2e_event_logs.test.ts | 53 ++++++++++++++++++- .../pxe/src/pxe_service/pxe_service.ts | 46 +++++++++++++++- 6 files changed, 156 insertions(+), 25 deletions(-) diff --git a/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts b/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts index 3b3996ccad81..c54a9674c4d2 100644 --- a/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts +++ b/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts @@ -58,4 +58,4 @@ export const createPXEClient = (url: string, fetch = makeFetch([1, 2, 3], false) false, 'pxe', fetch, - ); + ) as PXE; diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index a2bd9a6f5b3b..052a3328a8e0 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -1,5 +1,6 @@ import { type AuthWitness, + type EventMetadata, type ExtendedNote, type FunctionCall, type GetUnencryptedLogsResponse, @@ -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'; @@ -181,4 +189,7 @@ export abstract class BaseWallet implements Wallet { getPXEInfo(): Promise { return this.pxe.getPXEInfo(); } + getEvents(from: number, limit: number, eventMetadata: EventMetadata, ivpk: Point): Promise { + return this.pxe.getEvents(from, limit, eventMetadata, ivpk); + } } diff --git a/yarn-project/builder/src/contract-interface-gen/typescript.ts b/yarn-project/builder/src/contract-interface-gen/typescript.ts index 317f52e8c1a1..614cc21e631c 100644 --- a/yarn-project/builder/src/contract-interface-gen/typescript.ts +++ b/yarn-project/builder/src/contract-interface-gen/typescript.ts @@ -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(fieldsLength: number, functionSignature: string, fields: string[]): (payload: L1EventPayload | undefined) => T | undefined { + private static decodeEvent(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), ) ) { @@ -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')} }; } `, diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index 4df12dc83957..302ada613f1a 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -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, @@ -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'; @@ -355,9 +362,28 @@ export interface PXE { * TODO(@spalladino): Same notes as above. */ isContractPubliclyDeployed(address: AztecAddress): Promise; + + /** + * 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(from: number, limit: number, eventMetadata: EventMetadata, ivpk: Point): Promise; } // docs:end:pxe-interface +/** + * The shape of the event generated on the Contract. + */ +export interface EventMetadata { + decode(payload: L1EventPayload): T | undefined; + functionSelector: FunctionSelector; + fieldNames: string[]; +} + /** * Provides basic information about the running PXE. */ diff --git a/yarn-project/end-to-end/src/e2e_event_logs.test.ts b/yarn-project/end-to-end/src/e2e_event_logs.test.ts index ac9247aad220..bc43b3884cc2 100644 --- a/yarn-project/end-to-end/src/e2e_event_logs.test.ts +++ b/yarn-project/end-to-end/src/e2e_event_logs.test.ts @@ -1,6 +1,14 @@ -import { type AccountWalletWithSecretKey, type AztecNode, Fr, L1EventPayload, TaggedLog } from '@aztec/aztec.js'; +import { + type AccountWalletWithSecretKey, + type AztecNode, + Fr, + L1EventPayload, + type PXE, + TaggedLog, +} from '@aztec/aztec.js'; import { deriveMasterIncomingViewingSecretKey } from '@aztec/circuits.js'; import { makeTuple } from '@aztec/foundation/array'; +import { type Tuple } from '@aztec/foundation/serialize'; import { TestLogContract } from '@aztec/noir-contracts.js'; import { jest } from '@jest/globals'; @@ -15,11 +23,12 @@ describe('Logs', () => { let wallets: AccountWalletWithSecretKey[]; let node: AztecNode; + let pxe: PXE; let teardown: () => Promise; beforeAll(async () => { - ({ teardown, wallets, aztecNode: node } = await setup(2)); + ({ teardown, wallets, aztecNode: node, pxe } = await setup(2)); await publicDeployAccounts(wallets[0], wallets.slice(0, 2)); @@ -110,5 +119,45 @@ 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, 5>; + const preimage = makeTuple(5, makeTuple.bind(undefined, 4, Fr.random)) as Tuple, 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 pxe.getEvents( + firstTx.blockNumber!, + lastTx.blockNumber! - firstTx.blockNumber! + 1, + TestLogContract.events.ExampleEvent0, + wallets[0].getCompleteAddress().publicKeys.masterIncomingViewingPublicKey, + ); + + const collectedEvent1s = await pxe.getEvents( + firstTx.blockNumber!, + lastTx.blockNumber! - firstTx.blockNumber! + 1, + TestLogContract.events.ExampleEvent1, + wallets[0].getCompleteAddress().publicKeys.masterIncomingViewingPublicKey, + ); + + for (let i = 0; i < 5; ++i) { + expect(collectedEvent0s[i]).toStrictEqual({ + value0: preimage[i][0], + value1: preimage[i][1], + }); + + expect(collectedEvent1s[i]).toStrictEqual({ + value2: preimage[i][2], + value3: preimage[i][3], + }); + } + }); }); }); diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index cf329063fbd1..70b8ed85aad2 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -3,9 +3,11 @@ import { type AztecNode, EncryptedNoteTxL2Logs, EncryptedTxL2Logs, + type EventMetadata, ExtendedNote, type FunctionCall, type GetUnencryptedLogsResponse, + L1EventPayload, type L2Block, type LogFilter, MerkleTreeId, @@ -15,6 +17,7 @@ import { type ProofCreator, SimulatedTx, SimulationError, + TaggedLog, Tx, type TxEffect, type TxExecutionRequest, @@ -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'; @@ -756,4 +759,45 @@ export class PXEService implements PXE { public async isContractPubliclyDeployed(address: AztecAddress): Promise { return !!(await this.node.getContract(address)); } + + public async getEvents(from: number, limit: number, eventMetadata: EventMetadata, ivpk: Point): Promise { + 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[]; + + 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; + } }