diff --git a/docs/docs/migration_notes.md b/docs/docs/migration_notes.md index 986ccef45b1a..8bd3ac077e69 100644 --- a/docs/docs/migration_notes.md +++ b/docs/docs/migration_notes.md @@ -8,7 +8,18 @@ Aztec is in full-speed development. Literally every version breaks compatibility ## TBD -## [aztec.js] AztecNode.findLeavesIndexes returns indexes with block metadata +### [aztec.js] AztecNode.getPrivateEvents API change + +The `getPrivateEvents` method signature has changed to require an address of a contract that emitted the event and use recipient addresses instead of viewing public keys: + +```diff +- const events = await wallet.getPrivateEvents(TokenContract.events.Transfer, 1, 1, [recipient.getCompleteAddress().publicKeys.masterIncomingViewingPublicKey()]); ++ const events = await wallet.getPrivateEvents(token.address, TokenContract.events.Transfer, 1, 1, [recipient.getAddress()]); +``` + +## 0.82.0 + +### [aztec.js] AztecNode.findLeavesIndexes returns indexes with block metadata It's common that we need block metadata of a block in which leaves where inserted when querying indexes of these tree leaves. For this reason we now return that information along with the indexes. @@ -16,11 +27,11 @@ This allows us to reduce the number of individual AztecNode queries. Along this change `findNullifiersIndexesWithBlock` and `findBlockNumbersForIndexes` functions wer removed as all its uses can now be replaced with the newly modified `findLeavesIndexes` function. -## [aztec.js] AztecNode.getPublicDataTreeWitness renamed as AztecNode.getPublicDataWitness +### [aztec.js] AztecNode.getPublicDataTreeWitness renamed as AztecNode.getPublicDataWitness This change was done to have consistent naming across codebase. -## [aztec.js] Wallet interface and Authwit management +### [aztec.js] Wallet interface and Authwit management The `Wallet` interface in `aztec.js` is undergoing transformations, trying to be friendlier to wallet builders and reducing the surface of its API. This means `Wallet` no longer extends `PXE`, and instead just implements a subset of the methods of the former. This is NOT going to be its final form, but paves the way towards better interfaces and starts to clarify what the responsibilities of the wallet are: diff --git a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr index 6cc1673f659a..c53dc8914471 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr @@ -1,10 +1,10 @@ use crate::encrypted_logs::log_encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; use protocol_types::{address::AztecAddress, debug_log::debug_log}; +pub mod nonce_discovery; +pub mod partial_notes; pub mod private_logs; pub mod private_notes; -pub mod partial_notes; -pub mod nonce_discovery; /// We reserve two fields in the note private log that are not part of the note content: one for the storage slot, and /// one for the combined log and note type ID. diff --git a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr index f19af033700a..6b6cf1399623 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -7,17 +7,21 @@ use crate::{ encrypted_logs::{ log_assembly_strategies::default_aes128::aes128::AES128, log_encryption::LogEncryption, - log_type::{PARTIAL_NOTE_PRIVATE_LOG_TYPE_ID, PRIVATE_NOTE_LOG_TYPE_ID}, + log_type::{ + PARTIAL_NOTE_PRIVATE_LOG_TYPE_ID, PRIVATE_EVENT_LOG_TYPE_ID, PRIVATE_NOTE_LOG_TYPE_ID, + }, metadata_packing::from_expanded_metadata, }, - oracle::message_discovery::sync_notes, + oracle::{logs::store_private_event_log, message_discovery::sync_notes}, utils::array, }; use protocol_types::{ + abis::event_selector::EventSelector, address::AztecAddress, constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, debug_log::{debug_log, debug_log_format}, + traits::FromField, }; // TODO(#12750): don't make these values assume we're using AES. @@ -52,14 +56,14 @@ pub unconstrained fn do_process_log( tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, + log_index_in_tx: Field, recipient: AztecAddress, compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, ) { debug_log_format("Processing log with tag {0}", [log.get(0)]); - // The tag is ignored for now. + // The first field of the log is the tag, which we don't need right now, and the rest is all ciphertext. let ciphertext = array::subbvec(log, 1); - let log_plaintext = AES128::decrypt_log(ciphertext, recipient); // The first thing to do after decrypting the log is to determine what type of private log we're processing. We @@ -84,6 +88,20 @@ pub unconstrained fn do_process_log( debug_log("Processing partial note private log"); process_partial_note_private_log(contract_address, recipient, log_metadata, log_content); + } else if log_type_id == PRIVATE_EVENT_LOG_TYPE_ID { + debug_log("Processing private event log"); + + // In the case of event logs, the log metadata is the event selector. + let event_selector = EventSelector::from_field(log_metadata as Field); + + store_private_event_log( + contract_address, + recipient, + event_selector, + log_content, + tx_hash, + log_index_in_tx, + ); } else { // TODO(#11569): handle events debug_log_format( diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr index 2527164230b4..dcbd7121a3c0 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr @@ -1,247 +1,56 @@ use crate::{ context::PrivateContext, - encrypted_logs::encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, - event::event_interface::EventInterface, - keys::{ - ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, - ephemeral::generate_ephemeral_key_pair, - }, - utils::{ - conversion::{bytes_to_fields::bytes_to_fields, fields_to_bytes::fields_to_bytes}, - point::get_sign_of_point, - random::get_random_bytes, + encrypted_logs::{ + log_assembly_strategies::default_aes128::{aes128::AES128, utils::prefix_with_tag}, + log_encryption::LogEncryption, + log_type::PRIVATE_EVENT_LOG_TYPE_ID, + metadata_packing::to_expanded_metadata, }, + event::event_interface::EventInterface, + prelude::AztecAddress, }; -use dep::protocol_types::{ - address::AztecAddress, - constants::PRIVATE_LOG_SIZE_IN_FIELDS, - traits::{Serialize, ToField}, -}; -use std::aes128::aes128_encrypt; - -global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 48; +use protocol_types::{constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::{Serialize, ToField}}; -/* - * WHY IS THERE LOTS OF CODE DUPLICATION BETWEEN event.nr and note.nr? - * It's because there are a few more optimisations that can be done for notes, - * and so the stuff that looks like duplicated code currently, won't be - * the same for long. - * To modularise now feels premature, because we might get that modularisation wrong. - * Much better (imo) to have a flattened templates for log assembly, because it - * makes it much easier for us all to follow, it serves as a nice example for the - * community to follow (if they wish to roll their own log layouts), and it gives - * us more time to spot common patterns across all kinds of log layouts. - * - * There is some minimal de-duplication in common.nr. - */ - -/* - * LOG CONFIGURATION CHOICES: - * - * deliver_to: INPUT as recipient: AztecAddress - * encrypt_with: aes128 CBC (Cipher Block Chaining) - * shared_secret: ephemeral - * shared_secret_randomness_extraction_hash: sha256 - * tag: true - * tag_from: INPUT as sender: AztecAddress - * - */ - -/* - * LOG LAYOUT CHOICE: - * - * As per ./note.nr, but replace `note_bytes` with `event_bytes`. - */ - -/// This particular log assembly strategy (AES 128) requires the event (and the -/// event_type_id) to be converted into bytes, because the aes function -/// operates on bytes; not fields. -/// NB: The extra `+ 1` is for the event_type_id: -fn compute_event_plaintext_for_this_strategy(event: Event) -> [u8; (N + 1) * 32] +fn compute_event_plaintext_for_this_strategy(event: Event) -> [Field; N + 1] where Event: EventInterface, { + // TODO(#11571): with decryption happening in Noir we can now use the Packable trait instead. let serialized_event = Serialize::::serialize(event); let mut fields = [0; N + 1]; - fields[0] = Event::get_event_type_id().to_field(); + + // We pack log type id and log metadata into the first field. Search for `decode_log_plaintext` function to see + // where the value gets decoded. + fields[0] = to_expanded_metadata( + Event::get_event_type_id().to_field() as u64, + PRIVATE_EVENT_LOG_TYPE_ID, + ); for i in 0..serialized_event.len() { fields[i + 1] = serialized_event[i]; } - fields_to_bytes(fields) + fields } -// Note: This function is basically a copy of ./note/encryption.nr::encrypt_log. TODO: Merge the functions once -// the final note and event log layout is clear. Seems to me that the functions should be the same as encrypt_log -// is quite general and takes in an arbitrary plaintext. The note specific thing seems to be that in that function -// we perform some note-specific log length assertions. fn compute_log( - context: PrivateContext, event: Event, recipient: AztecAddress, - _sender: AztecAddress, + sender: AztecAddress, ) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] where Event: EventInterface, { - // ***************************************************************************** - // Compute the shared secret - // ***************************************************************************** - - let (eph_sk, eph_pk) = generate_ephemeral_key_pair(); - - let eph_pk_sign_byte: u8 = get_sign_of_point(eph_pk) as u8; - - let ciphertext_shared_secret = derive_ecdh_shared_secret_using_aztec_address(eph_sk, recipient); - - // TODO: also use this shared secret for deriving note randomness. - - // ***************************************************************************** - // Optionally convert some/all of the event to bytes, so that we may then - // prepend/append extra bytes, in a tightly-packed way. - // ***************************************************************************** - - // This includes the note_id and the storage_slot of the note: - // TODO: the note_id doesn't need to be a full field; it can be a single byte. - let event_bytes = compute_event_plaintext_for_this_strategy(event); - - // ***************************************************************************** - // Prepend/append extra bytes - // ***************************************************************************** - - // "Proper" meaning the main meaty stuff that we care about. - let proper_plaintext = event_bytes; - let final_plaintext = proper_plaintext; - - // ***************************************************************************** - // Convert the plaintext into whatever format the encryption function expects - // ***************************************************************************** - - // Already done for this strategy: AES expects bytes. - - // ***************************************************************************** - // Encrypt the plaintext - // ***************************************************************************** - - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( - ciphertext_shared_secret, - ); - - let ciphertext_bytes = aes128_encrypt(final_plaintext, iv, sym_key); - - assert(ciphertext_bytes.len() == 16 * (1 + ((N * 32) + 32) / 16)); - - // ***************************************************************************** - // Compute the header ciphertext - // ***************************************************************************** - - // TODO: this header section will be deleted soon. - let contract_address = context.this_address(); - let contract_address_bytes = contract_address.to_field().to_be_bytes::<32>(); - - let mut header_plaintext: [u8; 32 + 2] = [0; 32 + 2]; - for i in 0..32 { - header_plaintext[i] = contract_address_bytes[i]; - } - let offset = 32; - let ciphertext_bytes_length = ciphertext_bytes.len(); - header_plaintext[offset] = (ciphertext_bytes_length >> 8) as u8; - header_plaintext[offset + 1] = ciphertext_bytes_length as u8; - - // TODO: this is insecure and wasteful: - // "Insecure", because the esk shouldn't be used twice (once for the header, - // and again for the proper ciphertext) (at least, I never got the - // "go ahead" that this would be safe, unfortunately). - // "Wasteful", because the exact same computation is happening further down. - // I'm leaving that 2nd computation where it is, because this 1st computation - // will be imminently deleted, when the header logic is deleted. - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( - ciphertext_shared_secret, - ); - - // Note: the aes128_encrypt builtin fn automatically appends bytes to the - // input, according to pkcs#7; hence why the output `header_ciphertext_bytes` is 16 - // bytes larger than the input in this case. - let header_ciphertext_bytes = aes128_encrypt(header_plaintext, iv, sym_key); - // I recall that converting a slice to an array incurs constraints, so I'll check the length this way instead: - assert(header_ciphertext_bytes.len() == HEADER_CIPHERTEXT_SIZE_IN_BYTES); - - // ***************************************************************************** - // Prepend / append more bytes of data to the ciphertext, before converting back - // to fields. - // ***************************************************************************** - - let mut log_bytes_padding_to_mult_31 = - get_arr_of_size__log_bytes_padding__from_PT::<(N * 32) + 32>(); + let plaintext = compute_event_plaintext_for_this_strategy(event); - // Safety: we assume that the sender wants for the log to be private - a malicious one could simply reveal its - // contents publicly. It is therefore fine to trust the sender to provide random padding. - log_bytes_padding_to_mult_31 = unsafe { get_random_bytes() }; + let ciphertext = AES128::encrypt_log(plaintext, recipient); - let mut log_bytes = get_arr_of_size__log_bytes__from_PT::<(N * 32) + 32>(); + let log = prefix_with_tag(ciphertext, sender, recipient); - log_bytes[0] = eph_pk_sign_byte; - let mut offset = 1; - for i in 0..header_ciphertext_bytes.len() { - log_bytes[offset + i] = header_ciphertext_bytes[i]; - } - offset += header_ciphertext_bytes.len(); - - for i in 0..ciphertext_bytes.len() { - log_bytes[offset + i] = ciphertext_bytes[i]; - } - offset += ciphertext_bytes.len(); - - for i in 0..log_bytes_padding_to_mult_31.len() { - log_bytes[offset + i] = log_bytes_padding_to_mult_31[i]; - } - - // ***************************************************************************** - // Convert the encrypted bytes to fields, because logs are field-based - // ***************************************************************************** - - // TODO(#12749): As Mike pointed out, we need to make logs produced by different encryption schemes - // indistinguishable from each other and for this reason the output here and in the last for-loop of this function - // should cover a full field. - let log_bytes_as_fields = bytes_to_fields(log_bytes); - - // ***************************************************************************** - // Prepend / append fields, to create the final log - // ***************************************************************************** - - // In this strategy, we prepend [tag, eph_pk.x] - - let mut final_log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS] = [0; PRIVATE_LOG_SIZE_IN_FIELDS]; - - // At this point we feed in an incorrect tag because we don't want the event logs to be processed when invoking - // `sync_notes`. This currently fine as the events are solely to be obtain via the getPrivateEvents function - // that doesn't work with the tag. - final_log[0] = 1234; - final_log[1] = eph_pk.x; - - let mut offset = 2; - for i in 0..log_bytes_as_fields.len() { - final_log[offset + i] = log_bytes_as_fields[i]; - } - offset += log_bytes_as_fields.len(); - - for i in offset..PRIVATE_LOG_SIZE_IN_FIELDS { - // We need to get a random value that fits in 31 bytes to not leak information about the size of the log - // (all the "real" log fields contain at most 31 bytes because of the way we convert the bytes to fields). - // TODO(#12749): Long term, this is not a good solution. - - // Safety: we assume that the sender wants for the log to be private - a malicious one could simply reveal its - // contents publicly. It is therefore fine to trust the sender to provide random padding. - let field_bytes = unsafe { get_random_bytes::<31>() }; - final_log[i] = Field::from_be_bytes::<31>(field_bytes); - } - - final_log + log } -unconstrained fn compute_log_unconstrained( - context: PrivateContext, +fn compute_log_unconstrained( event: Event, recipient: AztecAddress, sender: AztecAddress, @@ -249,7 +58,7 @@ unconstrained fn compute_log_unconstrained( where Event: EventInterface, { - compute_log(context, event, recipient, sender) + compute_log(event, recipient, sender) } /// Sends an encrypted message to `recipient` with the content of the event, which they will discover when processing @@ -258,12 +67,12 @@ pub fn encode_and_encrypt_event( context: &mut PrivateContext, recipient: AztecAddress, sender: AztecAddress, -) -> fn[(&mut PrivateContext, AztecAddress, AztecAddress)](Event) -> () +) -> fn[(AztecAddress, AztecAddress, &mut PrivateContext)](Event) -> () where Event: EventInterface, { |e: Event| { - let encrypted_log = compute_log(*context, e, recipient, sender); + let encrypted_log = compute_log(e, recipient, sender); context.emit_private_log(encrypted_log); } } @@ -277,102 +86,13 @@ pub fn encode_and_encrypt_event_unconstrained( context: &mut PrivateContext, recipient: AztecAddress, sender: AztecAddress, -) -> fn[(&mut PrivateContext, AztecAddress, AztecAddress)](Event) -> () +) -> fn[(AztecAddress, AztecAddress, &mut PrivateContext)](Event) -> () where Event: EventInterface, { |e: Event| { - // Safety: this function does not constrain the encryption of the log, as explainted on its description. - let encrypted_log = unsafe { compute_log_unconstrained(*context, e, recipient, sender) }; + // Safety: this function does not constrain the encryption of the log, as explained on its description. + let encrypted_log = compute_log_unconstrained(e, recipient, sender); context.emit_private_log(encrypted_log); } } - -// These utils got copied here because note encryption diverged from the event one. The encryption functionality -// -// in this file is soon to be removed and replaced with the standard AES128 encryption from ./aes128.nr. -/********************************************************/ -// Disgusting arithmetic on generics -/********************************************************/ - -// In this section, instead of initialising arrays with very complicated generic -// arithmetic, such as: -// let my_arr: [u8; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))] = [0; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))]; -//... we instead do the arithmetic a little bit at a time, so that the computation -// can be audited and understood. Now, we can't do arithmetic on generics in the body -// of a function, so we abusing functions in the following way: - -// |full_pt| = |pt| = (N * 32) + 64 -fn get_arr_of_size__full_plaintext() -> [u8; PT] { - [0; PT] -} - -// |pt_aes_padding| = 16 - (|full_pt| % 16) -fn get_arr_of_size__plaintext_aes_padding( - _full_pt: [u8; FULL_PT], -) -> [u8; 16 - (FULL_PT % 16)] { - [0; 16 - (FULL_PT % 16)] -} - -// |ct| = |full_pt| + |pt_aes_padding| -fn get_arr_of_size__ciphertext( - _full_pt: [u8; FULL_PT], - _pt_aes_padding: [u8; PT_AES_PADDING], -) -> [u8; FULL_PT + PT_AES_PADDING] { - [0; FULL_PT + PT_AES_PADDING] -} - -// Ok, so we have the following bytes: -// eph_pk_sign, header_ciphertext, ciphertext: -// Let lbwop = 1 + 48 + |ct| // aka log bytes without padding -fn get_arr_of_size__log_bytes_without_padding(_ct: [u8; CT]) -> [u8; 1 + 48 + CT] { - [0; 1 + 48 + CT] -} - -// Recall: -// lbwop := 1 + 48 + |ct| // aka log bytes without padding -// We now want to pad b to the next multiple of 31, so as to "fill" fields. -// Let p be that padding. -// p = 31 * ceil(lbwop / 31) - lbwop -// = 31 * ((lbwop + 30) // 31) - lbwop -// (because ceil(x / y) = (x + y - 1) // y ). -fn get_arr_of_size__log_bytes_padding( - _lbwop: [u8; LBWOP], -) -> [u8; (31 * ((LBWOP + 30) / 31)) - LBWOP] { - [0; (31 * ((LBWOP + 30) / 31)) - LBWOP] -} - -// |log_bytes| = 1 + 48 + |ct| + p // aka log bytes (with padding) -// Recall: -// lbwop := 1 + 48 + |ct| -// p is the padding -fn get_arr_of_size__log_bytes( - _lbwop: [u8; LBWOP], - _p: [u8; P], -) -> [u8; LBWOP + P] { - [0; LBWOP + P] -} - -// The return type is pasted from the LSP's expectation, because it was too difficult -// to match its weird way of doing algebra. It doesn't know all rules of arithmetic. -// PT is the plaintext length. -pub(crate) fn get_arr_of_size__log_bytes_padding__from_PT() -> [u8; ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49))] { - let full_pt = get_arr_of_size__full_plaintext::(); - let pt_aes_padding = get_arr_of_size__plaintext_aes_padding(full_pt); - let ct = get_arr_of_size__ciphertext(full_pt, pt_aes_padding); - let lbwop = get_arr_of_size__log_bytes_without_padding(ct); - let p = get_arr_of_size__log_bytes_padding(lbwop); - p -} - -// The return type is pasted from the LSP's expectation, because it was too difficult -// to match its weird way of doing algebra. It doesn't know all rules of arithmetic. -pub(crate) fn get_arr_of_size__log_bytes__from_PT() -> [u8; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))] { - let full_pt = get_arr_of_size__full_plaintext::(); - let pt_aes_padding = get_arr_of_size__plaintext_aes_padding(full_pt); - let ct = get_arr_of_size__ciphertext(full_pt, pt_aes_padding); - let lbwop = get_arr_of_size__log_bytes_without_padding(ct); - let p = get_arr_of_size__log_bytes_padding(lbwop); - let log_bytes = get_arr_of_size__log_bytes(lbwop, p); - log_bytes -} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/mod.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/mod.nr index 4b1e58b3028f..0c1d47d34718 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/mod.nr @@ -2,3 +2,4 @@ pub mod aes128; pub mod arithmetic_generics_utils; pub mod event; pub mod note; +pub mod utils; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr index 46fb4b5a9bfb..e18f2916020a 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr @@ -1,15 +1,14 @@ use crate::{ context::PrivateContext, encrypted_logs::{ - log_assembly_strategies::default_aes128::aes128::AES128, + log_assembly_strategies::default_aes128::{aes128::AES128, utils::prefix_with_tag}, log_encryption::LogEncryption, log_type::{PARTIAL_NOTE_PRIVATE_LOG_TYPE_ID, PRIVATE_NOTE_LOG_TYPE_ID}, metadata_packing::to_expanded_metadata, }, note::{note_emission::NoteEmission, note_interface::NoteType}, - oracle::notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, }; -use dep::protocol_types::{ +use protocol_types::{ abis::note_hash::NoteHash, address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::Packable, }; @@ -104,27 +103,6 @@ where log } -fn prefix_with_tag( - log_without_tag: [Field; L], - sender: AztecAddress, - recipient: AztecAddress, -) -> [Field; L + 1] { - // Safety: we assume that the sender wants for the recipient to find the tagged note, and therefore that they will - // cooperate and use the correct tag. Usage of a bad tag will result in the recipient not being able to find the - // note automatically. - let tag = unsafe { get_app_tag_as_sender(sender, recipient) }; - increment_app_tagging_secret_index_as_sender(sender, recipient); - - let mut log_with_tag = [0; L + 1]; - - log_with_tag[0] = tag; - for i in 0..log_without_tag.len() { - log_with_tag[i + 1] = log_without_tag[i]; - } - - log_with_tag -} - pub unconstrained fn compute_note_log_unconstrained( note: Note, storage_slot: Field, @@ -186,38 +164,3 @@ where context.emit_raw_note_log(encrypted_log, note_hash_counter); } } - -mod test { - use super::prefix_with_tag; - use protocol_types::{ - address::AztecAddress, - indexed_tagging_secret::IndexedTaggingSecret, - traits::{Deserialize, FromField}, - }; - use std::test::OracleMock; - - #[test] - unconstrained fn prefixing_with_tag() { - let sender = AztecAddress::from_field(1); - let recipient = AztecAddress::from_field(2); - - let app_tagging_secret = 42; - let index = 5; - - // I am using the deserialize trait instead of directly instantiating the IndexedTaggingSecret struct because - // direct instantiation functionality is not exposed. - let indexed_tagging_secret = IndexedTaggingSecret::deserialize([app_tagging_secret, index]); - - // Mock the tagging oracles - let _ = OracleMock::mock("getIndexedTaggingSecretAsSender").returns(indexed_tagging_secret); - let _ = OracleMock::mock("incrementAppTaggingSecretIndexAsSender").returns(()); - - let log_without_tag = [1, 2, 3]; - let log_with_tag = prefix_with_tag(log_without_tag, sender, recipient); - - let expected_result = [indexed_tagging_secret.compute_tag(recipient), 1, 2, 3]; - - // Check tag was prefixed correctly - assert_eq(log_with_tag, expected_result, "Tag was not prefixed correctly"); - } -} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils.nr new file mode 100644 index 000000000000..0bc37d56baea --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils.nr @@ -0,0 +1,60 @@ +use crate::{ + oracle::notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, + prelude::AztecAddress, +}; + +pub(crate) fn prefix_with_tag( + log_without_tag: [Field; L], + sender: AztecAddress, + recipient: AztecAddress, +) -> [Field; L + 1] { + // Safety: we assume that the sender wants for the recipient to find the tagged note, and therefore that they will + // cooperate and use the correct tag. Usage of a bad tag will result in the recipient not being able to find the + // note automatically. + let tag = unsafe { get_app_tag_as_sender(sender, recipient) }; + increment_app_tagging_secret_index_as_sender(sender, recipient); + + let mut log_with_tag = [0; L + 1]; + + log_with_tag[0] = tag; + for i in 0..log_without_tag.len() { + log_with_tag[i + 1] = log_without_tag[i]; + } + + log_with_tag +} + +mod test { + use super::prefix_with_tag; + use protocol_types::{ + address::AztecAddress, + indexed_tagging_secret::IndexedTaggingSecret, + traits::{Deserialize, FromField}, + }; + use std::test::OracleMock; + + #[test] + unconstrained fn prefixing_with_tag() { + let sender = AztecAddress::from_field(1); + let recipient = AztecAddress::from_field(2); + + let app_tagging_secret = 42; + let index = 5; + + // I am using the deserialize trait instead of directly instantiating the IndexedTaggingSecret struct because + // direct instantiation functionality is not exposed. + let indexed_tagging_secret = IndexedTaggingSecret::deserialize([app_tagging_secret, index]); + + // Mock the tagging oracles + let _ = OracleMock::mock("getIndexedTaggingSecretAsSender").returns(indexed_tagging_secret); + let _ = OracleMock::mock("incrementAppTaggingSecretIndexAsSender").returns(()); + + let log_without_tag = [1, 2, 3]; + let log_with_tag = prefix_with_tag(log_without_tag, sender, recipient); + + let expected_result = [indexed_tagging_secret.compute_tag(recipient), 1, 2, 3]; + + // Check tag was prefixed correctly + assert_eq(log_with_tag, expected_result, "Tag was not prefixed correctly"); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/event/event_interface.nr b/noir-projects/aztec-nr/aztec/src/event/event_interface.nr index 7e3d22ce2c76..704b0affbed4 100644 --- a/noir-projects/aztec-nr/aztec/src/event/event_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/event/event_interface.nr @@ -1,6 +1,7 @@ use dep::protocol_types::abis::event_selector::EventSelector; use dep::protocol_types::traits::Serialize; +// TODO(#11571): Make events implement Packable instead of Serialize. pub trait EventInterface: Serialize { fn get_event_type_id() -> EventSelector; fn emit(self, emit: fn[Env](Self) -> ()); diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index 56a0fb57c477..9824f990c2c4 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -272,6 +272,7 @@ comptime fn generate_process_log() -> Quoted { tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, + log_index_in_tx: Field, recipient: aztec::protocol_types::address::AztecAddress, ) { // Because this unconstrained function is injected after the contract is processed by the macros, it'll @@ -288,6 +289,7 @@ comptime fn generate_process_log() -> Quoted { tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, + log_index_in_tx, recipient, _compute_note_hash_and_nullifier, ); @@ -302,6 +304,7 @@ comptime fn generate_process_log() -> Quoted { _tx_hash: Field, _unique_note_hashes_in_tx: BoundedVec, _first_nullifier_in_tx: Field, + _log_index_in_tx: Field, _recipient: aztec::protocol_types::address::AztecAddress, ) { panic(f"This contract does not use private notes") diff --git a/noir-projects/aztec-nr/aztec/src/oracle/logs.nr b/noir-projects/aztec-nr/aztec/src/oracle/logs.nr index 8ef03f1b155b..aa2c972d12b0 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/logs.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/logs.nr @@ -1,4 +1,5 @@ -use dep::protocol_types::address::AztecAddress; +use crate::discovery::MAX_LOG_CONTENT_LEN; +use protocol_types::{abis::event_selector::EventSelector, address::AztecAddress}; /// The below only exists to broadcast the raw log, so we can provide it to the base rollup later to be constrained. pub unconstrained fn notify_created_contract_class_log( @@ -15,3 +16,31 @@ unconstrained fn notify_created_contract_class_log_private_oracle( message: [Field; N], counter: u32, ) {} + +pub unconstrained fn store_private_event_log( + contract_address: AztecAddress, + recipient: AztecAddress, + event_selector: EventSelector, + log_content: BoundedVec, + tx_hash: Field, + log_index_in_tx: Field, +) { + store_private_event_log_oracle( + contract_address, + recipient, + event_selector, + log_content, + tx_hash, + log_index_in_tx, + ) +} + +#[oracle(storePrivateEventLog)] +unconstrained fn store_private_event_log_oracle( + contract_address: AztecAddress, + recipient: AztecAddress, + event_selector: EventSelector, + log_content: BoundedVec, + tx_hash: Field, + log_index_in_tx: Field, +) {} diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index d785e721de4e..ca3a1a6f78b5 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -1,6 +1,6 @@ import type { FeeOptions, TxExecutionOptions } from '@aztec/entrypoints/interfaces'; import type { ExecutionPayload } from '@aztec/entrypoints/payload'; -import type { Fr, Point } from '@aztec/foundation/fields'; +import type { Fr } from '@aztec/foundation/fields'; import type { AbiDecoded, ContractArtifact } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -124,12 +124,13 @@ export abstract class BaseWallet implements Wallet { } getPrivateEvents( + contractAddress: AztecAddress, event: EventMetadataDefinition, from: number, limit: number, - vpks: Point[] = [this.getCompleteAddress().publicKeys.masterIncomingViewingPublicKey], + recipients: AztecAddress[] = [this.getCompleteAddress().address], ): Promise { - return this.pxe.getPrivateEvents(event, from, limit, vpks); + return this.pxe.getPrivateEvents(contractAddress, event, from, limit, recipients); } getPublicEvents(event: EventMetadataDefinition, from: number, limit: number): Promise { return this.pxe.getPublicEvents(event, from, limit); 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 03ab8c625eda..6be213eae194 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,16 +1,8 @@ -import { - type AccountWalletWithSecretKey, - AztecAddress, - type AztecNode, - EventMetadata, - Fr, - L1EventPayload, -} from '@aztec/aztec.js'; +import { type AccountWalletWithSecretKey, AztecAddress, Fr } from '@aztec/aztec.js'; import { makeTuple } from '@aztec/foundation/array'; import { timesParallel } from '@aztec/foundation/collection'; import type { Tuple } from '@aztec/foundation/serialize'; import { type ExampleEvent0, type ExampleEvent1, TestLogContract } from '@aztec/noir-contracts.js/TestLog'; -import { EventSelector } from '@aztec/stdlib/abi'; import { jest } from '@jest/globals'; @@ -23,12 +15,11 @@ describe('Logs', () => { jest.setTimeout(TIMEOUT); let wallets: AccountWalletWithSecretKey[]; - let node: AztecNode; let teardown: () => Promise; beforeAll(async () => { - ({ teardown, wallets, aztecNode: node } = await setup(2)); + ({ teardown, wallets } = await setup(2)); await ensureAccountsPubliclyDeployed(wallets[0], wallets.slice(0, 2)); @@ -38,115 +29,63 @@ describe('Logs', () => { afterAll(() => teardown()); describe('functionality around emitting an encrypted log', () => { - it('emits multiple events as encrypted logs and decodes them one manually', async () => { - const preimage = makeTuple(4, Fr.random); - - const tx = await testLogContract.methods.emit_encrypted_events(wallets[1].getAddress(), preimage).send().wait(); - - const txEffect = await node.getTxEffect(tx.txHash); - - const privateLogs = txEffect!.data.privateLogs; - expect(privateLogs.length).toBe(3); - - const decryptedEvent0 = (await L1EventPayload.decryptAsIncoming( - privateLogs[0], - await wallets[0].getEncryptionSecret(), - ))!; - - expect(decryptedEvent0.contractAddress).toStrictEqual(testLogContract.address); - expect(decryptedEvent0.eventTypeId).toStrictEqual( - await EventSelector.fromSignature('ExampleEvent0(Field,Field)'), - ); - - // We decode our event into the event type - const event0Metadata = new EventMetadata(TestLogContract.events.ExampleEvent0); - const event0 = event0Metadata.decode(decryptedEvent0); - - // We check that the event was decoded correctly - expect(event0?.value0).toStrictEqual(preimage[0].toBigInt()); - expect(event0?.value1).toStrictEqual(preimage[1].toBigInt()); - - const decryptedEvent1 = (await L1EventPayload.decryptAsIncoming( - privateLogs[2], - await wallets[0].getEncryptionSecret(), - ))!; - - const event1Metadata = new EventMetadata(TestLogContract.events.ExampleEvent1); - - // We check our second event, which is a different type - const event1 = event1Metadata.decode(decryptedEvent1); - - // We check that an event that does not match, is not decoded correctly due to an event type id mismatch - const badEvent0 = event1Metadata.decode(decryptedEvent0); - expect(badEvent0).toBe(undefined); - - expect(decryptedEvent1.contractAddress).toStrictEqual(testLogContract.address); - expect(decryptedEvent1.eventTypeId).toStrictEqual(await EventSelector.fromSignature('ExampleEvent1((Field),u8)')); - - // We expect the fields to have been populated correctly - expect(event1?.value2).toStrictEqual(new AztecAddress(preimage[2])); - // We get the last byte here because value3 is of type u8 - expect(event1?.value3).toStrictEqual(BigInt(preimage[3].toBuffer().subarray(31).readUint8())); - - // Again, trying to decode another event with mismatching data does not yield anything - const badEvent1 = event0Metadata.decode(decryptedEvent1); - expect(badEvent1).toBe(undefined); - }); - it('emits multiple events as private logs and decodes them', async () => { const preimages = makeTuple(5, makeTuple.bind(undefined, 4, Fr.random)) as Tuple, 5>; - const txs = await Promise.all( - preimages.map(preimage => - testLogContract.methods.emit_encrypted_events(wallets[1].getAddress(), preimage).send().wait(), - ), - ); + // TODO(benesjan): Sending the txs is sequence here instead of in parallel as they were sent before because + // with the processing of events in Aztec.nr this revealed a bug in log processing. + // const txs = await Promise.all( + // preimages.map(preimage => + // testLogContract.methods.emit_encrypted_events(wallets[1].getAddress(), preimage).send().wait(), + // ), + // ); + const txs = []; + for (const preimage of preimages) { + const tx = await testLogContract.methods.emit_encrypted_events(wallets[1].getAddress(), preimage).send().wait(); + txs.push(tx); + } const firstBlockNumber = Math.min(...txs.map(tx => tx.blockNumber!)); const lastBlockNumber = Math.max(...txs.map(tx => tx.blockNumber!)); const numBlocks = lastBlockNumber - firstBlockNumber + 1; - // We get all the events we can decrypt with our incoming viewing keys - + // Each emit_encrypted_events call emits 2 ExampleEvent0s and 1 ExampleEvent1 + // So with 5 calls we expect 10 ExampleEvent0s and 5 ExampleEvent1s const collectedEvent0s = await wallets[0].getPrivateEvents( + testLogContract.address, TestLogContract.events.ExampleEvent0, firstBlockNumber, numBlocks, - ); - - const collectedEvent0sWithIncoming = await wallets[0].getPrivateEvents( - TestLogContract.events.ExampleEvent0, - firstBlockNumber, - numBlocks, - // This function can be called specifying the viewing public keys associated with the encrypted event. - [wallets[0].getCompleteAddress().publicKeys.masterIncomingViewingPublicKey], + [wallets[0].getAddress(), wallets[1].getAddress()], ); const collectedEvent1s = await wallets[0].getPrivateEvents( + testLogContract.address, TestLogContract.events.ExampleEvent1, firstBlockNumber, numBlocks, - [wallets[0].getCompleteAddress().publicKeys.masterIncomingViewingPublicKey], + [wallets[0].getAddress(), wallets[1].getAddress()], ); - expect(collectedEvent0sWithIncoming.length).toBe(5); - expect(collectedEvent0s.length).toBe(5); - expect(collectedEvent1s.length).toBe(5); + expect(collectedEvent0s.length).toBe(10); // 2 events per tx * 5 txs + expect(collectedEvent1s.length).toBe(5); // 1 event per tx * 5 txs const emptyEvent1s = await wallets[0].getPrivateEvents( + testLogContract.address, TestLogContract.events.ExampleEvent1, firstBlockNumber, numBlocks, - [wallets[0].getCompleteAddress().publicKeys.masterOutgoingViewingPublicKey], + [wallets[0].getAddress()], ); - expect(emptyEvent1s.length).toBe(0); + expect(emptyEvent1s.length).toBe(5); // Events sent to msg_sender() const exampleEvent0Sort = (a: ExampleEvent0, b: ExampleEvent0) => (a.value0 > b.value0 ? 1 : -1); - expect(collectedEvent0sWithIncoming.sort(exampleEvent0Sort)).toStrictEqual( - preimages - .map(preimage => ({ value0: preimage[0].toBigInt(), value1: preimage[1].toBigInt() })) - .sort(exampleEvent0Sort), - ); + // Each preimage is used twice for ExampleEvent0 + const expectedEvent0s = [...preimages, ...preimages].map(preimage => ({ + value0: preimage[0].toBigInt(), + value1: preimage[1].toBigInt(), + })); + expect(collectedEvent0s.sort(exampleEvent0Sort)).toStrictEqual(expectedEvent0s.sort(exampleEvent0Sort)); const exampleEvent1Sort = (a: ExampleEvent1, b: ExampleEvent1) => (a.value2 > b.value2 ? 1 : -1); expect(collectedEvent1s.sort(exampleEvent1Sort)).toStrictEqual( diff --git a/yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts index 5d987ac303f6..434691429444 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts @@ -32,7 +32,13 @@ describe('e2e_token_contract private transfer recursion', () => { // We should have created a single new note, for the recipient expect(txEffects!.data.noteHashes.length).toBe(1); - const events = await wallets[1].getPrivateEvents(TokenContract.events.Transfer, tx.blockNumber!, 1); + const events = await wallets[1].getPrivateEvents( + asset.address, + TokenContract.events.Transfer, + tx.blockNumber!, + 1, + [wallets[1].getAddress()], + ); expect(events[0]).toEqual({ from: accounts[0].address, @@ -59,7 +65,13 @@ describe('e2e_token_contract private transfer recursion', () => { const senderBalance = await asset.methods.balance_of_private(accounts[0].address).simulate(); expect(senderBalance).toEqual(expectedChange); - const events = await wallets[1].getPrivateEvents(TokenContract.events.Transfer, tx.blockNumber!, 1); + const events = await wallets[1].getPrivateEvents( + asset.address, + TokenContract.events.Transfer, + tx.blockNumber!, + 1, + [wallets[1].getAddress()], + ); expect(events[0]).toEqual({ from: accounts[0].address, diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer.test.ts index 8de87fad24cf..adc265637e18 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer.test.ts @@ -30,7 +30,13 @@ describe('e2e_token_contract transfer private', () => { const tx = await asset.methods.transfer(accounts[1].address, amount).send().wait(); tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount); - const events = await wallets[1].getPrivateEvents(TokenContract.events.Transfer, tx.blockNumber!, 1); + const events = await wallets[1].getPrivateEvents( + asset.address, + TokenContract.events.Transfer, + tx.blockNumber!, + 1, + [wallets[1].getAddress()], + ); expect(events[0]).toEqual({ from: accounts[0].address, diff --git a/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.test.ts b/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.test.ts index 42978047f95e..4ed6f4b374d2 100644 --- a/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.test.ts +++ b/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.test.ts @@ -23,6 +23,7 @@ import { AddressDataProvider } from '../storage/address_data_provider/address_da import { CapsuleDataProvider } from '../storage/capsule_data_provider/capsule_data_provider.js'; import { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; import { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js'; +import { PrivateEventDataProvider } from '../storage/private_event_data_provider/private_event_data_provider.js'; import { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; import { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; import { PXEOracleInterface } from './pxe_oracle_interface.js'; @@ -46,6 +47,7 @@ describe('PXEOracleInterface', () => { let aztecNode: MockProxy; let addressDataProvider: AddressDataProvider; + let privateEventDataProvider: PrivateEventDataProvider; let contractDataProvider: ContractDataProvider; let noteDataProvider: NoteDataProvider; let syncDataProvider: SyncDataProvider; @@ -66,6 +68,7 @@ describe('PXEOracleInterface', () => { jest.spyOn(contractDataProvider, 'getDebugContractName').mockImplementation(() => Promise.resolve('TestContract')); addressDataProvider = new AddressDataProvider(store); + privateEventDataProvider = new PrivateEventDataProvider(store); noteDataProvider = await NoteDataProvider.create(store); syncDataProvider = new SyncDataProvider(store); taggingDataProvider = new TaggingDataProvider(store); @@ -82,6 +85,7 @@ describe('PXEOracleInterface', () => { syncDataProvider, taggingDataProvider, addressDataProvider, + privateEventDataProvider, ); // Set up contract address contractAddress = await AztecAddress.random(); // Set up recipient account @@ -480,8 +484,6 @@ describe('PXEOracleInterface', () => { simulator.runUnconstrained.mockImplementation(() => Promise.resolve({})); runUnconstrainedSpy = jest.spyOn(simulator, 'runUnconstrained'); - - aztecNode.getTxEffect.mockResolvedValue(randomInBlock(await TxEffect.random())); }); function mockTaggedLogs(numLogs: number) { @@ -495,6 +497,13 @@ describe('PXEOracleInterface', () => { const taggedLogs = mockTaggedLogs(numLogs); + // Mock getTxEffect to return a TxEffect containing the private logs + aztecNode.getTxEffect.mockImplementation(async () => { + const txEffect = await TxEffect.random(); + txEffect.privateLogs = taggedLogs.map(log => log.log as PrivateLog); + return randomInBlock(txEffect); + }); + await pxeOracleInterface.processTaggedLogs(contractAddress, taggedLogs, recipient.address, simulator); // We test that a call to `processLog` is made with the correct function artifact and contract address diff --git a/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.ts b/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.ts index b69442162629..fbc0c394f98f 100644 --- a/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.ts +++ b/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.ts @@ -1,6 +1,6 @@ import { type L1_TO_L2_MSG_TREE_HEIGHT, MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; import { timesParallel } from '@aztec/foundation/collection'; -import { poseidon2Hash } from '@aztec/foundation/crypto'; +import { poseidon2Hash, randomInt } from '@aztec/foundation/crypto'; import { Fr, Point } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; import type { KeyStore } from '@aztec/key-store'; @@ -11,6 +11,7 @@ import { type SimulationProvider, } from '@aztec/simulator/client'; import { + EventSelector, type FunctionArtifact, type FunctionArtifactWithContractName, FunctionCall, @@ -26,7 +27,13 @@ import { computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/stdli import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { computeAddressSecret, computeTaggingSecretPoint } from '@aztec/stdlib/keys'; -import { IndexedTaggingSecret, LogWithTxData, TxScopedL2Log, deriveEcdhSharedSecret } from '@aztec/stdlib/logs'; +import { + IndexedTaggingSecret, + LogWithTxData, + PrivateLog, + TxScopedL2Log, + deriveEcdhSharedSecret, +} from '@aztec/stdlib/logs'; import { getNonNullifiedL1ToL2MessageWitness } from '@aztec/stdlib/messaging'; import { Note, type NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; @@ -38,6 +45,7 @@ import type { CapsuleDataProvider } from '../storage/capsule_data_provider/capsu import type { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; import { NoteDao } from '../storage/note_data_provider/note_dao.js'; import type { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js'; +import type { PrivateEventDataProvider } from '../storage/private_event_data_provider/private_event_data_provider.js'; import type { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; import type { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; import { WINDOW_HALF_SIZE, getIndexedTaggingSecretsForTheWindow, getInitialIndexesMap } from './tagging_utils.js'; @@ -56,6 +64,7 @@ export class PXEOracleInterface implements ExecutionDataProvider { private syncDataProvider: SyncDataProvider, private taggingDataProvider: TaggingDataProvider, private addressDataProvider: AddressDataProvider, + private privateEventDataProvider: PrivateEventDataProvider, private log = createLogger('pxe:pxe_oracle_interface'), ) {} @@ -608,6 +617,19 @@ export class PXEOracleInterface implements ExecutionDataProvider { throw new Error(`Could not find tx effect for tx hash ${scopedLog.txHash}`); } + // TODO(#13155): Handle multiple found indexes for the same log. + let logIndexInTx = txEffect.data.privateLogs.findIndex(log => log.equals(scopedLog.log as PrivateLog)); + + // TODO(#13137): The following is a workaround to disable the logIndexInTx check for TXE tests as TXE currently + // returns nonsensical tx effects and the tx has is incremented from 0 up (so it never crosses a 1000). + if (scopedLog.txHash.toBigInt() < 1000n) { + logIndexInTx = randomInt(10); + } + + if (logIndexInTx === -1) { + throw new Error(`Could not find log in tx effect for tx hash ${scopedLog.txHash}`); + } + // This will trigger calls to the deliverNote oracle await this.callProcessLog( contractAddress, @@ -615,6 +637,7 @@ export class PXEOracleInterface implements ExecutionDataProvider { scopedLog.txHash, txEffect.data.noteHashes, txEffect.data.nullifiers[0], + logIndexInTx, recipient, simulator, ); @@ -781,6 +804,7 @@ export class PXEOracleInterface implements ExecutionDataProvider { txHash: TxHash, noteHashes: Fr[], firstNullifier: Fr, + logIndexInTx: number, recipient: AztecAddress, simulator?: AcirSimulator, ) { @@ -806,6 +830,7 @@ export class PXEOracleInterface implements ExecutionDataProvider { txHash.toString(), toBoundedVec(noteHashes, MAX_NOTE_HASHES_PER_TX), firstNullifier, + logIndexInTx, recipient, ]), returnTypes: artifact.returnTypes, @@ -844,6 +869,36 @@ export class PXEOracleInterface implements ExecutionDataProvider { const addressSecret = await computeAddressSecret(await recipientCompleteAddress.getPreaddress(), ivskM); return deriveEcdhSharedSecret(addressSecret, ephPk); } + + async storePrivateEventLog( + contractAddress: AztecAddress, + recipient: AztecAddress, + eventSelector: EventSelector, + logContent: Fr[], + txHash: TxHash, + logIndexInTx: number, + ): Promise { + const txReceipt = await this.aztecNode.getTxReceipt(txHash); + const blockNumber = txReceipt.blockNumber; + if (blockNumber === undefined) { + throw new Error(`Block number is undefined for tx ${txHash} in storePrivateEventLog`); + } + const historicalBlockNumber = await this.syncDataProvider.getBlockNumber(); + if (blockNumber > historicalBlockNumber) { + throw new Error( + `Attempting to store private event log from a block newer than the historical block of the simulation. Log block number: ${blockNumber}, historical block number: ${historicalBlockNumber}`, + ); + } + return this.privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + blockNumber, + ); + } } function toBoundedVec(array: Fr[], maxLength: number) { diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index e637ac39e023..541cad281d91 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -1,5 +1,5 @@ import { L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/constants'; -import { Fr, type Point } from '@aztec/foundation/fields'; +import { Fr } from '@aztec/foundation/fields'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { SerialQueue } from '@aztec/foundation/queue'; import { Timer } from '@aztec/foundation/timer'; @@ -20,6 +20,7 @@ import { FunctionCall, FunctionSelector, FunctionType, + decodeFromAbi, decodeFunctionSignature, encodeArguments, } from '@aztec/stdlib/abi'; @@ -36,7 +37,7 @@ import { getContractClassFromArtifact, } from '@aztec/stdlib/contract'; import { SimulationError } from '@aztec/stdlib/errors'; -import { EventMetadata, L1EventPayload } from '@aztec/stdlib/event'; +import { EventMetadata } from '@aztec/stdlib/event'; import type { GasFees } from '@aztec/stdlib/gas'; import { siloNullifier } from '@aztec/stdlib/hash'; import type { @@ -49,7 +50,6 @@ import type { PrivateKernelProver, } from '@aztec/stdlib/interfaces/client'; import type { PrivateKernelExecutionProofOutput, PrivateKernelTailCircuitPublicInputs } from '@aztec/stdlib/kernel'; -import { computeAddressSecret } from '@aztec/stdlib/keys'; import type { LogFilter } from '@aztec/stdlib/logs'; import { getNonNullifiedL1ToL2MessageWitness } from '@aztec/stdlib/messaging'; import { type NotesFilter, UniqueNote } from '@aztec/stdlib/note'; @@ -82,6 +82,7 @@ import { AddressDataProvider } from '../storage/address_data_provider/address_da import { CapsuleDataProvider } from '../storage/capsule_data_provider/capsule_data_provider.js'; import { ContractDataProvider } from '../storage/contract_data_provider/contract_data_provider.js'; import { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js'; +import { PrivateEventDataProvider } from '../storage/private_event_data_provider/private_event_data_provider.js'; import { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js'; import { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js'; import { Synchronizer } from '../synchronizer/index.js'; @@ -101,6 +102,7 @@ export class PXEService implements PXE { private syncDataProvider: SyncDataProvider, private taggingDataProvider: TaggingDataProvider, private addressDataProvider: AddressDataProvider, + private privateEventDataProvider: PrivateEventDataProvider, private simulator: AcirSimulator, private packageVersion: string, private proverEnabled: boolean, @@ -134,6 +136,7 @@ export class PXEService implements PXE { const packageVersion = getPackageInfo().version; const proverEnabled = !!config.proverEnabled; const addressDataProvider = new AddressDataProvider(store); + const privateEventDataProvider = new PrivateEventDataProvider(store); const contractDataProvider = new ContractDataProvider(store); const noteDataProvider = await NoteDataProvider.create(store); const syncDataProvider = new SyncDataProvider(store); @@ -160,6 +163,7 @@ export class PXEService implements PXE { syncDataProvider, taggingDataProvider, addressDataProvider, + privateEventDataProvider, log, ); const simulator = new AcirSimulator(pxeOracleInterface, simulationProvider); @@ -175,6 +179,7 @@ export class PXEService implements PXE { syncDataProvider, taggingDataProvider, addressDataProvider, + privateEventDataProvider, simulator, packageVersion, proverEnabled, @@ -881,72 +886,30 @@ export class PXEService implements PXE { } public async getPrivateEvents( + contractAddress: AztecAddress, eventMetadataDef: EventMetadataDefinition, from: number, - limit: number, - // TODO (#9272): Make this better, we should be able to only pass an address now - vpks: Point[], + numBlocks: number, + recipients: AztecAddress[], ): Promise { - const eventMetadata = new EventMetadata(eventMetadataDef); - if (vpks.length === 0) { - throw new Error('Tried to get encrypted events without supplying any viewing public keys'); + if (recipients.length === 0) { + throw new Error('Recipients are required to get private events'); } - const blocks = await this.node.getBlocks(from, limit); - - const txEffects = blocks.flatMap(block => block.body.txEffects); - const privateLogs = txEffects.flatMap(txEffect => txEffect.privateLogs); + this.log.verbose(`Getting private events for ${contractAddress.toString()} from ${from} to ${from + numBlocks}`); - const vsks = await Promise.all( - vpks.map(async vpk => { - const [keyPrefix, account] = await this.keyStore.getKeyPrefixAndAccount(vpk); - let secretKey = await this.keyStore.getMasterSecretKey(vpk); - if (keyPrefix === 'iv') { - const registeredAccount = (await this.getRegisteredAccounts()).find(completeAddress => - completeAddress.address.equals(account), - ); - if (!registeredAccount) { - throw new Error('No registered account'); - } + // TODO(#13113): This is a temporary hack to ensure that the notes are synced before getting the events. + await this.simulateUnconstrained('sync_notes', [], contractAddress); - const preaddress = await registeredAccount.getPreaddress(); - - secretKey = await computeAddressSecret(preaddress, secretKey); - } - - return secretKey; - }), + const events = await this.privateEventDataProvider.getPrivateEvents( + contractAddress, + from, + numBlocks, + recipients, + eventMetadataDef.eventSelector, ); - const visibleEvents = ( - await Promise.all( - privateLogs.map(async log => { - for (const sk of vsks) { - // TODO: Verify that the first field of the log is the tag siloed with contract address. - // Or use tags to query logs, like we do with notes. - const decryptedEvent = await L1EventPayload.decryptAsIncoming(log, sk); - if (decryptedEvent !== undefined) { - return [decryptedEvent]; - } - } - - return []; - }), - ) - ).flat(); - - const decodedEvents = visibleEvents - .map(visibleEvent => { - if (visibleEvent === undefined) { - return undefined; - } - if (!visibleEvent.eventTypeId.equals(eventMetadata.eventSelector)) { - return undefined; - } - - return eventMetadata.decode(visibleEvent); - }) - .filter(visibleEvent => visibleEvent !== undefined) as T[]; + const decodedEvents = events.map((event: Fr[]): T => decodeFromAbi([eventMetadataDef.abiType], event) as T); return decodedEvents; } diff --git a/yarn-project/pxe/src/storage/index.ts b/yarn-project/pxe/src/storage/index.ts index ea1ee388f902..4d66f9c892e5 100644 --- a/yarn-project/pxe/src/storage/index.ts +++ b/yarn-project/pxe/src/storage/index.ts @@ -6,3 +6,4 @@ export * from './sync_data_provider/index.js'; export * from './tagging_data_provider/index.js'; export * from './data_provider.js'; export * from './metadata.js'; +export * from './private_event_data_provider/private_event_data_provider.js'; diff --git a/yarn-project/pxe/src/storage/private_event_data_provider/private_event_data_provider.test.ts b/yarn-project/pxe/src/storage/private_event_data_provider/private_event_data_provider.test.ts new file mode 100644 index 000000000000..890199940c3c --- /dev/null +++ b/yarn-project/pxe/src/storage/private_event_data_provider/private_event_data_provider.test.ts @@ -0,0 +1,233 @@ +import { randomInt } from '@aztec/foundation/crypto'; +import { Fr } from '@aztec/foundation/fields'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { EventSelector } from '@aztec/stdlib/abi'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { TxHash } from '@aztec/stdlib/tx'; + +import { PrivateEventDataProvider } from './private_event_data_provider.js'; + +const getRandomLogContent = () => { + return [Fr.random(), Fr.random(), Fr.random()]; +}; + +describe('PrivateEventDataProvider', () => { + let privateEventDataProvider: PrivateEventDataProvider; + let contractAddress: AztecAddress; + let recipient: AztecAddress; + let logContent: Fr[]; + let blockNumber: number; + let eventSelector: EventSelector; + let txHash: TxHash; + let logIndexInTx: number; + + beforeEach(async () => { + const store = await openTmpStore('private_event_data_provider_test'); + privateEventDataProvider = new PrivateEventDataProvider(store); + contractAddress = await AztecAddress.random(); + recipient = await AztecAddress.random(); + logContent = getRandomLogContent(); + blockNumber = 123; + eventSelector = EventSelector.random(); + txHash = TxHash.random(); + logIndexInTx = randomInt(10); + }); + + it('stores and retrieves private events', async () => { + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + blockNumber, + ); + const events = await privateEventDataProvider.getPrivateEvents( + contractAddress, + blockNumber, + 1, + [recipient], + eventSelector, + ); + expect(events).toEqual([logContent]); + }); + + it('ignores duplicate events with same txHash and logIndexInTx', async () => { + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + blockNumber, + ); + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + blockNumber, + ); + const events = await privateEventDataProvider.getPrivateEvents( + contractAddress, + blockNumber, + 1, + [recipient], + eventSelector, + ); + expect(events).toEqual([logContent]); + }); + + it('allows multiple events with same content but different txHash', async () => { + const otherTxHash = TxHash.random(); + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + blockNumber, + ); + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + otherTxHash, + logIndexInTx, + blockNumber, + ); + const events = await privateEventDataProvider.getPrivateEvents( + contractAddress, + blockNumber, + 1, + [recipient], + eventSelector, + ); + expect(events).toEqual([logContent, logContent]); + }); + + it('filters events by block range', async () => { + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + getRandomLogContent(), + TxHash.random(), + logIndexInTx, + 100, + ); + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + TxHash.random(), + logIndexInTx, + 200, + ); + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + getRandomLogContent(), + TxHash.random(), + logIndexInTx, + 300, + ); + + const events = await privateEventDataProvider.getPrivateEvents( + contractAddress, + 150, + 100, + [recipient], + eventSelector, + ); + + expect(events).toEqual([logContent]); // Only includes event from block 200 + }); + + it('filters events by recipient', async () => { + const otherRecipient = await AztecAddress.random(); + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + blockNumber, + ); + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + otherRecipient, + eventSelector, + logContent, + TxHash.random(), + logIndexInTx, + blockNumber, + ); + + const events = await privateEventDataProvider.getPrivateEvents( + contractAddress, + blockNumber, + 1, + [recipient], + eventSelector, + ); + expect(events).toEqual([logContent]); + }); + + it('returns empty array when no events match criteria', async () => { + const events = await privateEventDataProvider.getPrivateEvents( + contractAddress, + blockNumber, + 1, + [recipient], + eventSelector, + ); + expect(events).toEqual([]); + }); + + it('tracks size correctly', async () => { + expect(await privateEventDataProvider.getSize()).toBe(0); + + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + blockNumber, + ); + expect(await privateEventDataProvider.getSize()).toBe(1); + + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + blockNumber, + ); + expect(await privateEventDataProvider.getSize()).toBe(1); // Duplicate event not stored + + await privateEventDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + TxHash.random(), + logIndexInTx, + blockNumber, + ); + expect(await privateEventDataProvider.getSize()).toBe(2); + }); +}); diff --git a/yarn-project/pxe/src/storage/private_event_data_provider/private_event_data_provider.ts b/yarn-project/pxe/src/storage/private_event_data_provider/private_event_data_provider.ts new file mode 100644 index 000000000000..2df4f8ee4106 --- /dev/null +++ b/yarn-project/pxe/src/storage/private_event_data_provider/private_event_data_provider.ts @@ -0,0 +1,128 @@ +import { Fr } from '@aztec/foundation/fields'; +import { createLogger } from '@aztec/foundation/log'; +import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import type { AztecAsyncArray, AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; +import type { EventSelector } from '@aztec/stdlib/abi'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { TxHash } from '@aztec/stdlib/tx'; + +import type { DataProvider } from '../data_provider.js'; + +interface PrivateEventEntry { + logContent: Buffer; + blockNumber: number; +} + +/** + * Stores decrypted private event logs. + */ +export class PrivateEventDataProvider implements DataProvider { + #store: AztecAsyncKVStore; + /** Array storing the actual private event log entries containing the log content and block number */ + #eventLogs: AztecAsyncArray; + /** Map from contract_address_recipient_eventSelector to array of indices into #eventLogs for efficient lookup */ + #eventLogIndex: AztecAsyncMap; + /** + * Map from txHash_logIndexInTx to boolean indicating if log has been seen. + * @dev A single transaction can have multiple logs. + */ + #seenLogs: AztecAsyncMap; + + logger = createLogger('private_event_data_provider'); + + constructor(store: AztecAsyncKVStore) { + this.#store = store; + this.#eventLogs = this.#store.openArray('private_event_logs'); + this.#eventLogIndex = this.#store.openMap('private_event_log_index'); + this.#seenLogs = this.#store.openMap('seen_logs'); + } + + /** + * Store a private event log. + * @param contractAddress - The address of the contract that emitted the event. + * @param recipient - The recipient of the event. + * @param eventSelector - The event selector of the event. + * @param logContent - The content of the event. + * @param txHash - The transaction hash of the event log. + * @param logIndexInTx - The index of the log within the transaction. + * @param blockNumber - The block number in which the event was emitted. + */ + storePrivateEventLog( + contractAddress: AztecAddress, + recipient: AztecAddress, + eventSelector: EventSelector, + logContent: Fr[], + txHash: TxHash, + logIndexInTx: number, + blockNumber: number, + ): Promise { + return this.#store.transactionAsync(async () => { + const key = `${contractAddress.toString()}_${recipient.toString()}_${eventSelector.toString()}`; + + // We identify a unique log by its transaction hash and index within that transaction + const txKey = `${txHash.toString()}_${logIndexInTx}`; + + // Check if this exact log has already been stored + const hasBeenSeen = await this.#seenLogs.getAsync(txKey); + if (hasBeenSeen) { + this.logger.verbose('Ignoring duplicate event log', { txHash: txHash.toString(), logIndexInTx }); + return; + } + + this.logger.verbose('storing private event log', { contractAddress, recipient, logContent, blockNumber }); + + const index = await this.#eventLogs.lengthAsync(); + await this.#eventLogs.push({ logContent: serializeToBuffer(logContent), blockNumber }); + + const existingIndices = (await this.#eventLogIndex.getAsync(key)) || []; + await this.#eventLogIndex.set(key, [...existingIndices, index]); + + // Mark this log as seen + await this.#seenLogs.set(txKey, true); + }); + } + + /** + * Returns the private events given search parameters. + * @param contractAddress - The address of the contract to get events from. + * @param from - The block number to search from. + * @param numBlocks - The amount of blocks to search. + * @param recipients - The addresses that decrypted the logs. + * @param eventSelector - The event selector to filter by. + * @returns - The event log contents. + */ + public async getPrivateEvents( + contractAddress: AztecAddress, + from: number, + numBlocks: number, + recipients: AztecAddress[], + eventSelector: EventSelector, + ): Promise { + const events: Fr[][] = []; + + for (const recipient of recipients) { + const key = `${contractAddress.toString()}_${recipient.toString()}_${eventSelector.toString()}`; + const indices = (await this.#eventLogIndex.getAsync(key)) || []; + + for (const index of indices) { + const entry = await this.#eventLogs.atAsync(index); + if (!entry || entry.blockNumber < from || entry.blockNumber >= from + numBlocks) { + continue; + } + + // Convert buffer back to Fr array + const reader = BufferReader.asReader(entry.logContent); + const numFields = entry.logContent.length / Fr.SIZE_IN_BYTES; + const logContent = reader.readArray(numFields, Fr); + + events.push(logContent); + } + } + + return events; + } + + getSize(): Promise { + return this.#eventLogs.lengthAsync(); + } +} diff --git a/yarn-project/simulator/src/private/acvm/oracle/oracle.ts b/yarn-project/simulator/src/private/acvm/oracle/oracle.ts index 1ea0cc579112..7715f95343f1 100644 --- a/yarn-project/simulator/src/private/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/private/acvm/oracle/oracle.ts @@ -1,8 +1,9 @@ import { Fr, Point } from '@aztec/foundation/fields'; -import { FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; +import { EventSelector, FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { ContractClassLog, LogWithTxData } from '@aztec/stdlib/logs'; import { MerkleTreeId } from '@aztec/stdlib/trees'; +import { TxHash } from '@aztec/stdlib/tx'; import type { ACVMField } from '../acvm_types.js'; import { fromBoundedVec, fromUintArray, fromUintBoundedVec } from '../deserialize.js'; @@ -490,4 +491,24 @@ export class Oracle { ); return secret.toFields().map(toACVMField); } + + async storePrivateEventLog( + [contractAddress]: ACVMField[], + [recipient]: ACVMField[], + [eventSelector]: ACVMField[], + logContentBVecStorage: ACVMField[], + [logContentLength]: ACVMField[], + [txHash]: ACVMField[], + [logIndexInTx]: ACVMField[], + ) { + await this.typedOracle.storePrivateEventLog( + AztecAddress.fromField(Fr.fromString(contractAddress)), + AztecAddress.fromField(Fr.fromString(recipient)), + EventSelector.fromField(Fr.fromString(eventSelector)), + fromBoundedVec(logContentBVecStorage, logContentLength), + new TxHash(Fr.fromString(txHash)), + Fr.fromString(logIndexInTx).toNumber(), + ); + return []; + } } diff --git a/yarn-project/simulator/src/private/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/private/acvm/oracle/typed_oracle.ts index ab2b1a65dffb..4d949bc2d025 100644 --- a/yarn-project/simulator/src/private/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/private/acvm/oracle/typed_oracle.ts @@ -1,13 +1,13 @@ import type { L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/constants'; import { Fr, Point } from '@aztec/foundation/fields'; -import type { FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; +import type { EventSelector, FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { CompleteAddress, ContractInstance } from '@aztec/stdlib/contract'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; import type { ContractClassLog, IndexedTaggingSecret, LogWithTxData } from '@aztec/stdlib/logs'; import type { Note, NoteStatus } from '@aztec/stdlib/note'; import { type MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; -import type { BlockHeader } from '@aztec/stdlib/tx'; +import type { BlockHeader, TxHash } from '@aztec/stdlib/tx'; import type { MessageLoadOracleInputs } from '../../../common/message_load_oracle_inputs.js'; @@ -258,4 +258,15 @@ export abstract class TypedOracle { getSharedSecret(_address: AztecAddress, _ephPk: Point): Promise { return Promise.reject(new OracleMethodNotAvailableError('getSharedSecret')); } + + storePrivateEventLog( + _contractAddress: AztecAddress, + _recipient: AztecAddress, + _eventSelector: EventSelector, + _logContent: Fr[], + _txHash: TxHash, + _logIndexInTx: number, + ): Promise { + return Promise.reject(new OracleMethodNotAvailableError('storePrivateEventLog')); + } } diff --git a/yarn-project/simulator/src/private/execution_data_provider.ts b/yarn-project/simulator/src/private/execution_data_provider.ts index d409e3ecc5b0..0954fd8b74a2 100644 --- a/yarn-project/simulator/src/private/execution_data_provider.ts +++ b/yarn-project/simulator/src/private/execution_data_provider.ts @@ -1,5 +1,10 @@ import type { Fr, Point } from '@aztec/foundation/fields'; -import type { FunctionArtifact, FunctionArtifactWithContractName, FunctionSelector } from '@aztec/stdlib/abi'; +import type { + EventSelector, + FunctionArtifact, + FunctionArtifactWithContractName, + FunctionSelector, +} from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2Block } from '@aztec/stdlib/block'; import type { CompleteAddress, ContractInstance } from '@aztec/stdlib/contract'; @@ -7,7 +12,7 @@ import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { IndexedTaggingSecret, LogWithTxData, TxScopedL2Log } from '@aztec/stdlib/logs'; import type { NoteStatus } from '@aztec/stdlib/note'; import { type MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; -import type { BlockHeader } from '@aztec/stdlib/tx'; +import type { BlockHeader, TxHash } from '@aztec/stdlib/tx'; import type { CommitmentsDBInterface } from '../common/db_interfaces.js'; import type { NoteData } from './acvm/index.js'; @@ -325,4 +330,22 @@ export interface ExecutionDataProvider extends CommitmentsDBInterface { * @returns The secret for the given address. */ getSharedSecret(address: AztecAddress, ephPk: Point): Promise; + + /** + * Stores an event log in the database. + * @param contractAddress - The address of the contract that emitted the log. + * @param recipient - The address of the recipient. + * @param eventSelector - The event selector of the event. + * @param logContent - The content of the private event log. + * @param txHash - The hash of the transaction that emitted the log. + * @param logIndexInTx - The index of the log within the transaction. + */ + storePrivateEventLog( + contractAddress: AztecAddress, + recipient: AztecAddress, + eventSelector: EventSelector, + logContent: Fr[], + txHash: TxHash, + logIndexInTx: number, + ): Promise; } diff --git a/yarn-project/simulator/src/private/unconstrained_execution_oracle.ts b/yarn-project/simulator/src/private/unconstrained_execution_oracle.ts index 5a00e623b534..d14d648b2d18 100644 --- a/yarn-project/simulator/src/private/unconstrained_execution_oracle.ts +++ b/yarn-project/simulator/src/private/unconstrained_execution_oracle.ts @@ -1,6 +1,7 @@ import { Aes128 } from '@aztec/foundation/crypto'; import { Fr, Point } from '@aztec/foundation/fields'; import { applyStringFormatting, createLogger } from '@aztec/foundation/log'; +import type { EventSelector } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { CompleteAddress, ContractInstance } from '@aztec/stdlib/contract'; @@ -9,7 +10,7 @@ import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { IndexedTaggingSecret, LogWithTxData } from '@aztec/stdlib/logs'; import type { NoteStatus } from '@aztec/stdlib/note'; import { type MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; -import type { BlockHeader, Capsule } from '@aztec/stdlib/tx'; +import type { BlockHeader, Capsule, TxHash } from '@aztec/stdlib/tx'; import { type NoteData, TypedOracle } from './acvm/index.js'; import type { ExecutionDataProvider } from './execution_data_provider.js'; @@ -376,4 +377,22 @@ export class UnconstrainedExecutionOracle extends TypedOracle { public override getSharedSecret(address: AztecAddress, ephPk: Point): Promise { return this.executionDataProvider.getSharedSecret(address, ephPk); } + + public override storePrivateEventLog( + contractAddress: AztecAddress, + recipient: AztecAddress, + eventSelector: EventSelector, + logContent: Fr[], + txHash: TxHash, + logIndexInTx: number, + ): Promise { + return this.executionDataProvider.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + ); + } } diff --git a/yarn-project/stdlib/src/interfaces/pxe.test.ts b/yarn-project/stdlib/src/interfaces/pxe.test.ts index e1135bdd28dc..a96da502cd45 100644 --- a/yarn-project/stdlib/src/interfaces/pxe.test.ts +++ b/yarn-project/stdlib/src/interfaces/pxe.test.ts @@ -2,7 +2,7 @@ import { L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/constants'; import { type L1ContractAddresses, L1ContractsNames } from '@aztec/ethereum/l1-contract-addresses'; import { memoize } from '@aztec/foundation/decorators'; import { EthAddress } from '@aztec/foundation/eth-address'; -import { Fr, Point } from '@aztec/foundation/fields'; +import { Fr } from '@aztec/foundation/fields'; import { type JsonRpcTestContext, createJsonRpcTestSetup } from '@aztec/foundation/json-rpc/test'; import { SiblingPath } from '@aztec/foundation/trees'; @@ -280,10 +280,11 @@ describe('PXESchema', () => { it('getPrivateEvents', async () => { const result = await context.client.getPrivateEvents<{ value: bigint }>( + address, { abiType: { kind: 'boolean' }, eventSelector: EventSelector.random(), fieldNames: ['name'] }, 1, 1, - [await Point.random()], + [await AztecAddress.random()], ); expect(result).toEqual([{ value: 1n }]); }); @@ -501,14 +502,15 @@ class MockPXE implements PXE { }); } getPrivateEvents( + _contractAddress: AztecAddress, _eventMetadata: EventMetadataDefinition, from: number, limit: number, - vpks: Point[], + _recipients: AztecAddress[], ): Promise { expect(from).toBe(1); expect(limit).toBe(1); - expect(vpks[0]).toBeInstanceOf(Point); + expect(_recipients[0]).toBeInstanceOf(AztecAddress); return Promise.resolve([{ value: 1n } as T]); } getPublicEvents(_eventMetadata: EventMetadataDefinition, from: number, limit: number): Promise { diff --git a/yarn-project/stdlib/src/interfaces/pxe.ts b/yarn-project/stdlib/src/interfaces/pxe.ts index 987c19ca5f77..7bdb765c95bc 100644 --- a/yarn-project/stdlib/src/interfaces/pxe.ts +++ b/yarn-project/stdlib/src/interfaces/pxe.ts @@ -1,5 +1,5 @@ import { L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/constants'; -import type { Fr, Point } from '@aztec/foundation/fields'; +import type { Fr } from '@aztec/foundation/fields'; import type { ApiSchemaFor, ZodFor } from '@aztec/foundation/schemas'; import { SiblingPath } from '@aztec/foundation/trees'; @@ -354,13 +354,20 @@ export interface PXE { /** * Returns the private events given search parameters. + * @param contractAddress - The address of the contract to get events from. * @param eventMetadata - Metadata 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 vpks - The incoming viewing public keys that can decrypt the log. + * @param numBlocks - The amount of blocks to search. + * @param recipients - The addresses that decrypted the logs. * @returns - The deserialized events. */ - getPrivateEvents(eventMetadata: EventMetadataDefinition, from: number, limit: number, vpks: Point[]): Promise; + getPrivateEvents( + contractAddress: AztecAddress, + eventMetadata: EventMetadataDefinition, + from: number, + numBlocks: number, + recipients: AztecAddress[], + ): Promise; /** * Returns the public events given search parameters. @@ -505,7 +512,7 @@ export const PXESchema: ApiSchemaFor = { getContractClassMetadata: z.function().args(schemas.Fr, optional(z.boolean())).returns(ContractClassMetadataSchema), getPrivateEvents: z .function() - .args(EventMetadataDefinitionSchema, z.number(), z.number(), z.array(schemas.Point)) + .args(schemas.AztecAddress, EventMetadataDefinitionSchema, z.number(), z.number(), z.array(schemas.AztecAddress)) .returns(z.array(AbiDecodedSchema)), getPublicEvents: z .function() diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index ef203667cec4..a2fb99936482 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -23,6 +23,7 @@ import { ContractDataProvider, NoteDataProvider, PXEOracleInterface, + PrivateEventDataProvider, SyncDataProvider, TaggingDataProvider, enrichPublicSimulationError, @@ -52,6 +53,7 @@ import { } from '@aztec/simulator/server'; import { type ContractArtifact, + EventSelector, type FunctionAbi, FunctionSelector, type NoteSelector, @@ -152,6 +154,7 @@ export class TXE implements TypedOracle { private syncDataProvider: SyncDataProvider, private taggingDataProvider: TaggingDataProvider, private addressDataProvider: AddressDataProvider, + private privateEventDataProvider: PrivateEventDataProvider, private accountDataProvider: TXEAccountDataProvider, private executionCache: HashedValuesCache, private contractAddress: AztecAddress, @@ -174,6 +177,7 @@ export class TXE implements TypedOracle { this.syncDataProvider, this.taggingDataProvider, this.addressDataProvider, + this.privateEventDataProvider, this.logger, ); } @@ -184,6 +188,7 @@ export class TXE implements TypedOracle { const baseFork = await nativeWorldStateService.fork(); const addressDataProvider = new AddressDataProvider(store); + const privateEventDataProvider = new PrivateEventDataProvider(store); const contractDataProvider = new ContractDataProvider(store); const noteDataProvider = await NoteDataProvider.create(store); const syncDataProvider = new SyncDataProvider(store); @@ -208,6 +213,7 @@ export class TXE implements TypedOracle { syncDataProvider, taggingDataProvider, addressDataProvider, + privateEventDataProvider, accountDataProvider, executionCache, await AztecAddress.random(), @@ -1255,4 +1261,22 @@ export class TXE implements TypedOracle { getSharedSecret(address: AztecAddress, ephPk: Point): Promise { return this.pxeOracleInterface.getSharedSecret(address, ephPk); } + + storePrivateEventLog( + contractAddress: AztecAddress, + recipient: AztecAddress, + eventSelector: EventSelector, + logContent: Fr[], + txHash: TxHash, + logIndexInTx: number, + ): Promise { + return this.pxeOracleInterface.storePrivateEventLog( + contractAddress, + recipient, + eventSelector, + logContent, + txHash, + logIndexInTx, + ); + } } diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 751434bc658e..5e9daabecd25 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -1,11 +1,11 @@ -import { type ContractInstanceWithAddress, Fr, Point } from '@aztec/aztec.js'; +import { type ContractInstanceWithAddress, Fr, Point, TxHash } from '@aztec/aztec.js'; import { DEPLOYER_CONTRACT_ADDRESS } from '@aztec/constants'; import type { Logger } from '@aztec/foundation/log'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import type { ProtocolContract } from '@aztec/protocol-contracts'; import { enrichPublicSimulationError } from '@aztec/pxe/server'; import type { TypedOracle } from '@aztec/simulator/client'; -import { type ContractArtifact, FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; +import { type ContractArtifact, EventSelector, FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import { PublicDataWrite } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { computePartialAddress } from '@aztec/stdlib/contract'; @@ -630,6 +630,25 @@ export class TXEService { return toForeignCallResult(secret.toFields().map(toSingle)); } + async storePrivateEventLog( + contractAddress: ForeignCallSingle, + recipient: ForeignCallSingle, + eventSelector: ForeignCallSingle, + logContent: ForeignCallArray, + txHash: ForeignCallSingle, + logIndexInTx: ForeignCallSingle, + ) { + await this.typedOracle.storePrivateEventLog( + AztecAddress.fromField(fromSingle(contractAddress)), + AztecAddress.fromField(fromSingle(recipient)), + EventSelector.fromField(fromSingle(eventSelector)), + fromArray(logContent), + new TxHash(fromSingle(txHash)), + fromSingle(logIndexInTx).toNumber(), + ); + return toForeignCallResult([]); + } + // AVM opcodes avmOpcodeEmitUnencryptedLog(_message: ForeignCallArray) {