diff --git a/noir-projects/aztec-nr/aztec/src/context/public_context.nr b/noir-projects/aztec-nr/aztec/src/context/public_context.nr index 87b17aa92d2a..57d2380e6cf4 100644 --- a/noir-projects/aztec-nr/aztec/src/context/public_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/public_context.nr @@ -17,17 +17,15 @@ impl PublicContext { pub fn storage_address(self) -> AztecAddress { storage_address() } + pub fn fee_per_l2_gas(self) -> Field { fee_per_l2_gas() } + pub fn fee_per_da_gas(self) -> Field { fee_per_da_gas() } - /** - * Emit a log with the given event selector and message. - * @param event_selector The event selector for the log. - * @param message The message to emit in the log. - */ + pub fn emit_unencrypted_log(&mut self, log: T) where T: Serialize { emit_unencrypted_log(Serialize::serialize(log).as_slice()); } @@ -35,6 +33,7 @@ impl PublicContext { pub fn note_hash_exists(self, note_hash: Field, leaf_index: Field) -> bool { note_hash_exists(note_hash, leaf_index) == 1 } + pub fn l1_to_l2_msg_exists(self, msg_hash: Field, msg_leaf_index: Field) -> bool { l1_to_l2_msg_exists(msg_hash, msg_leaf_index) == 1 } diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 7e7b1af2a8a6..e569b892e1ae 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -12,6 +12,7 @@ mod state_vars; mod prelude; mod public_storage; mod encrypted_logs; +mod unencrypted_logs; use dep::protocol_types; mod utils; diff --git a/noir-projects/aztec-nr/aztec/src/unencrypted_logs.nr b/noir-projects/aztec-nr/aztec/src/unencrypted_logs.nr new file mode 100644 index 000000000000..3eae1f8dc8e5 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/unencrypted_logs.nr @@ -0,0 +1 @@ +mod unencrypted_event_emission; diff --git a/noir-projects/aztec-nr/aztec/src/unencrypted_logs/unencrypted_event_emission.nr b/noir-projects/aztec-nr/aztec/src/unencrypted_logs/unencrypted_event_emission.nr new file mode 100644 index 000000000000..f374a2a1195d --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/unencrypted_logs/unencrypted_event_emission.nr @@ -0,0 +1,58 @@ +use crate::{ + context::{PrivateContext, PublicContext}, event::event_interface::EventInterface, + encrypted_logs::payload::compute_encrypted_event_log, oracle::logs_traits::LensForEncryptedEvent +}; +use dep::protocol_types::{address::AztecAddress, grumpkin_point::GrumpkinPoint, traits::Serialize}; + +fn emit( + context: &mut PublicContext, + event: Event +) where Event: EventInterface, Event: Serialize, [Field; N]: LensForEventSelector { + let selector = Event::get_event_type_id(); + + let serialized_event = event.serialize(); + let mut emitted_log = [0; M]; + + // We put the selector in the "last" place, to avoid reading or assigning to an expression in an index + for i in 0..serialized_event.len() { + emitted_log[i] = serialized_event[i]; + } + + emitted_log[serialized_event.len()] = selector.to_field(); + + context.emit_unencrypted_log(emitted_log); +} + +pub fn encode_event(context: &mut PublicContext) -> fn[(&mut PublicContext,)](Event) -> () where Event: EventInterface, Event: Serialize, [Field; N]: LensForEventSelector { + | e: Event | { + emit( + context, + e, + ); + } +} + +trait LensForEventSelector { + // N = event preimage input in fields + // M = event preimage input in fields + event selector as field + fn output(self: [Field; N]) -> [Field; M]; +} + +impl LensForEventSelector<1, 2> for [Field; 1] { + fn output(self) -> [Field; 2] {[self[0] as Field; 2]} +} +impl LensForEventSelector<2, 3> for [Field; 2] { + fn output(self) -> [Field; 3] {[self[0] as Field; 3]} +} +impl LensForEventSelector<3, 4> for [Field; 3] { + fn output(self) -> [Field; 4] {[self[0] as Field; 4]} +} +impl LensForEventSelector<4, 5> for [Field; 4] { + fn output(self) -> [Field; 5] {[self[0] as Field; 5]} +} +impl LensForEventSelector<5, 6> for [Field; 5] { + fn output(self) -> [Field; 6] {[self[0] as Field; 6]} +} +impl LensForEventSelector<6, 7> for [Field; 6] { + fn output(self) -> [Field; 7] {[self[0] as Field; 7]} +} diff --git a/noir-projects/noir-contracts/contracts/test_log_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_log_contract/src/main.nr index f42cb2ffd7a7..ef63b3306039 100644 --- a/noir-projects/noir-contracts/contracts/test_log_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_log_contract/src/main.nr @@ -5,6 +5,7 @@ contract TestLog { use dep::aztec::encrypted_logs::incoming_body::EncryptedLogIncomingBody; use dep::aztec::event::event_interface::EventInterface; use dep::aztec::encrypted_logs::encrypted_event_emission::{encode_and_encrypt_event, encode_and_encrypt_event_with_keys}; + use dep::aztec::unencrypted_logs::unencrypted_event_emission::encode_event; #[aztec(event)] struct ExampleEvent0 { @@ -64,4 +65,15 @@ contract TestLog { ) ); } + + #[aztec(public)] + fn emit_unencrypted_events(preimages: [Field; 4]) { + let event0 = ExampleEvent0 { value0: preimages[0], value1: preimages[1] }; + + event0.emit(encode_event(&mut context)); + + let event1 = ExampleEvent1 { value2: preimages[2], value3: preimages[3] }; + + event1.emit(encode_event(&mut context)); + } } diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 4e3d71e4d8f2..38cf7e986bce 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -99,6 +99,7 @@ export { EncryptedLogHeader, EncryptedNoteLogIncomingBody, EncryptedLogOutgoingBody, + EventType, ExtendedNote, FunctionCall, GrumpkinPrivateKey, diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index 247b509fbafb..737247c2270e 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -1,6 +1,7 @@ import { type AuthWitness, type EventMetadata, + type EventType, type ExtendedNote, type GetUnencryptedLogsResponse, type IncomingNotesFilter, @@ -182,11 +183,12 @@ export abstract class BaseWallet implements Wallet { return this.pxe.getPXEInfo(); } getEvents( + type: EventType, + eventMetadata: EventMetadata, from: number, limit: number, - eventMetadata: EventMetadata, ivpk: Point = this.getCompleteAddress().publicKeys.masterIncomingViewingPublicKey, - ): Promise { - return this.pxe.getEvents(from, limit, eventMetadata, ivpk); + ) { + return this.pxe.getEvents(type, eventMetadata, from, limit, ivpk); } } diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index 6092eda27800..1cdc9d9f6fe2 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -381,14 +381,21 @@ export interface PXE { isContractPubliclyDeployed(address: AztecAddress): Promise; /** - * Returns the events of a specified type. + * Returns the events of a specified type given search parameters. + * @param type - The type of the event to search for—Encrypted, or Unencrypted. + * @param eventMetadata - Identifier of the event. This should be the class generated from the contract. e.g. Contract.events.Event * @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. + * @param ivpk - (Used for encrypted logs only) 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; + getEvents( + type: EventType, + eventMetadata: EventMetadata, + from: number, + limit: number, + ivpk: Point, + ): Promise; } // docs:end:pxe-interface @@ -401,6 +408,14 @@ export interface EventMetadata { fieldNames: string[]; } +/** + * This is used in getting events via the filter + */ +export enum EventType { + Encrypted = 'Encrypted', + Unencrypted = 'Unencrypted', +} + /** * 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 02122e165d12..8421e87b8dcb 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,4 +1,11 @@ -import { type AccountWalletWithSecretKey, type AztecNode, Fr, L1EventPayload, TaggedLog } from '@aztec/aztec.js'; +import { + type AccountWalletWithSecretKey, + type AztecNode, + EventType, + Fr, + L1EventPayload, + TaggedLog, +} from '@aztec/aztec.js'; import { deriveMasterIncomingViewingSecretKey } from '@aztec/circuits.js'; import { EventSelector } from '@aztec/foundation/abi'; import { makeTuple } from '@aztec/foundation/array'; @@ -31,7 +38,7 @@ describe('Logs', () => { afterAll(() => teardown()); describe('functionality around emitting an encrypted log', () => { - it('emits multiple events as encrypted logs and decodes them', async () => { + it('emits multiple events as encrypted logs and decodes a single one manually', async () => { const randomness = makeTuple(2, Fr.random); const preimage = makeTuple(4, Fr.random); @@ -103,15 +110,17 @@ describe('Logs', () => { const lastTx = await testLogContract.methods.emit_encrypted_events(randomness[++i], preimage[i]).send().wait(); const collectedEvent0s = await wallets[0].getEvents( + EventType.Encrypted, + TestLogContract.events.ExampleEvent0, firstTx.blockNumber!, lastTx.blockNumber! - firstTx.blockNumber! + 1, - TestLogContract.events.ExampleEvent0, ); const collectedEvent1s = await wallets[0].getEvents( + EventType.Encrypted, + TestLogContract.events.ExampleEvent1, 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, ); @@ -129,5 +138,43 @@ describe('Logs', () => { preimage.map(preimage => ({ value2: preimage[2], value3: preimage[3] })).sort(exampleEvent1Sort), ); }); + + it('emits multiple events as unencrypted logs and decodes them', async () => { + const preimage = makeTuple(5, makeTuple.bind(undefined, 4, Fr.random)) as Tuple, 5>; + + let i = 0; + const firstTx = await testLogContract.methods.emit_unencrypted_events(preimage[i]).send().wait(); + await Promise.all( + [...new Array(3)].map(() => testLogContract.methods.emit_unencrypted_events(preimage[++i]).send().wait()), + ); + const lastTx = await testLogContract.methods.emit_unencrypted_events(preimage[++i]).send().wait(); + + const collectedEvent0s = await wallets[0].getEvents( + EventType.Unencrypted, + TestLogContract.events.ExampleEvent0, + firstTx.blockNumber!, + lastTx.blockNumber! - firstTx.blockNumber! + 1, + ); + + const collectedEvent1s = await wallets[0].getEvents( + EventType.Unencrypted, + TestLogContract.events.ExampleEvent1, + firstTx.blockNumber!, + lastTx.blockNumber! - firstTx.blockNumber! + 1, + ); + + 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), + ); + }); }); }); diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index cf351c5bf9bf..e9b76b6d07c0 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -4,6 +4,7 @@ import { EncryptedNoteTxL2Logs, EncryptedTxL2Logs, type EventMetadata, + EventType, ExtendedNote, type FunctionCall, type GetUnencryptedLogsResponse, @@ -35,8 +36,14 @@ import { getContractClassFromArtifact, } 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, type Point } from '@aztec/foundation/fields'; +import { + type ContractArtifact, + type DecodedReturn, + EventSelector, + FunctionSelector, + encodeArguments, +} from '@aztec/foundation/abi'; +import { type Fq, Fr, 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'; @@ -834,7 +841,34 @@ export class PXEService implements PXE { return !!(await this.node.getContract(address)); } - public async getEvents(from: number, limit: number, eventMetadata: EventMetadata, ivpk: Point): Promise { + public getEvents( + type: EventType.Encrypted, + eventMetadata: EventMetadata, + from: number, + limit: number, + ivpk: Point, + ): Promise; + public getEvents( + type: EventType.Unencrypted, + eventMetadata: EventMetadata, + from: number, + limit: number, + ): Promise; + public getEvents( + type: EventType, + eventMetadata: EventMetadata, + from: number, + limit: number, + ivpk: Point = Point.ZERO, + ): Promise { + if (type.includes(EventType.Encrypted)) { + return this.getEncryptedEvents(from, limit, eventMetadata, ivpk); + } + + return this.getUnencryptedEvents(from, limit, eventMetadata); + } + + async getEncryptedEvents(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); @@ -874,4 +908,40 @@ export class PXEService implements PXE { return decodedEvents; } + + async getUnencryptedEvents(from: number, limit: number, eventMetadata: EventMetadata): Promise { + const { logs: unencryptedLogs } = await this.node.getUnencryptedLogs({ + fromBlock: from, + toBlock: from + limit, + }); + + const decodedEvents = unencryptedLogs + .map(unencryptedLog => { + const unencryptedLogBuf = unencryptedLog.log.data; + if ( + !EventSelector.fromBuffer(unencryptedLogBuf.subarray(unencryptedLogBuf.byteLength - 4)).equals( + eventMetadata.eventSelector, + ) + ) { + return undefined; + } + + if (unencryptedLogBuf.byteLength !== eventMetadata.fieldNames.length * 32 + 32) { + 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]: new Fr(unencryptedLogBuf.subarray(i * 32, i * 32 + 32)), + }), + {} as T, + ); + }) + .filter(unencryptedLog => unencryptedLog !== undefined) as T[]; + + return decodedEvents; + } }