From 2721ba1ca0964177b0a2f39f61011e94adcad695 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 12 Mar 2025 19:33:12 +0000 Subject: [PATCH 01/20] WIP --- .../aztec-nr/aztec/src/discovery/mod.nr | 14 +- .../aztec/src/discovery/private_logs.nr | 23 +- .../arithmetic_generics_utils.nr | 85 +++ .../default_aes128/event.nr | 40 +- .../default_aes128/mod.nr | 2 + .../default_aes128/note.nr | 546 ------------------ .../default_aes128/note/encryption.nr | 352 +++++++++++ .../default_aes128/note/mod.nr | 7 + .../default_aes128/note/note.nr | 222 +++++++ .../default_aes128/utils_to_nuke.nr | 96 +++ .../aztec-nr/aztec/src/macros/mod.nr | 6 +- .../aztec/src/oracle/aes128_decrypt.nr | 18 +- .../aztec-nr/aztec/src/oracle/mod.nr | 1 + .../aztec-nr/aztec/src/oracle/random.nr | 14 + .../aztec/src/oracle/shared_secret.nr | 16 + .../aztec-nr/aztec/src/utils/point.nr | 40 +- .../contracts/test_contract/src/main.nr | 21 - .../crates/types/src/address/aztec_address.nr | 2 +- .../archiver/kv_archiver_store/log_store.ts | 20 +- .../memory_archiver_store.ts | 4 +- yarn-project/aztec.js/src/index.ts | 2 +- .../end-to-end/src/e2e_block_building.test.ts | 24 - yarn-project/foundation/src/fields/fields.ts | 7 + .../pxe_oracle_interface.test.ts | 259 +-------- .../pxe_oracle_interface.ts | 106 ++-- .../simulator/src/private/acvm/deserialize.ts | 33 ++ .../src/private/acvm/oracle/oracle.ts | 40 +- .../src/private/acvm/oracle/typed_oracle.ts | 6 +- .../simulator/src/private/acvm/serialize.ts | 28 + .../src/private/execution_data_provider.ts | 13 +- .../src/private/private_execution_oracle.ts | 6 +- .../private/unconstrained_execution_oracle.ts | 12 +- .../stdlib/src/interfaces/archiver.test.ts | 4 +- .../stdlib/src/interfaces/aztec-node.test.ts | 4 +- .../src/interfaces/get_logs_response.test.ts | 4 +- .../stdlib/src/logs/l1_payload/index.ts | 2 +- .../src/logs/l1_payload/l1_note_payload.ts | 117 ---- yarn-project/stdlib/src/logs/private_log.ts | 16 +- yarn-project/stdlib/src/logs/public_log.ts | 12 +- .../stdlib/src/logs/tx_scoped_l2_log.ts | 53 +- yarn-project/stdlib/src/tx/tx_effect.ts | 2 +- yarn-project/txe/src/node/txe_node.ts | 16 +- yarn-project/txe/src/oracle/txe_oracle.ts | 21 +- .../txe/src/txe_service/txe_service.ts | 29 +- yarn-project/txe/src/util/encoding.ts | 20 +- 45 files changed, 1221 insertions(+), 1144 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/arithmetic_generics_utils.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr create mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr create mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/mod.nr create mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr create mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils_to_nuke.nr create mode 100644 noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr delete mode 100644 yarn-project/stdlib/src/logs/l1_payload/l1_note_payload.ts diff --git a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr index c03a695c75ba..ff5b45f6e60c 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr @@ -1,6 +1,5 @@ -use dep::protocol_types::{ - address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, debug_log::debug_log, -}; +use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; +use dep::protocol_types::{address::AztecAddress, debug_log::debug_log}; pub mod private_logs; pub mod partial_notes; @@ -10,11 +9,10 @@ pub mod nonce_discovery; /// one for the combined log and note type ID. global NOTE_PRIVATE_LOG_RESERVED_FIELDS: u32 = 2; -/// The maximum length of the packed representation of a note's contents. This is limited by private log size and extra -/// fields in the log (e.g. the combined log and note type ID). -// TODO (#11634): we're assuming here that the entire log is plaintext, which is not true due to headers, encryption -// padding, etc. Notes can't actually be this large. -pub global MAX_NOTE_PACKED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; +/// The maximum length of the packed representation of a note's contents. This is limited by private log size, encryption +/// overhead and extra fields in the log (e.g. the combined log and note type ID). +pub global MAX_NOTE_PACKED_LEN: u32 = + PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; pub struct NoteHashAndNullifier { /// The result of NoteHash::compute_note_hash 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 99ac9dbc33b3..81de263f99dc 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -21,6 +21,9 @@ use crate::discovery::{ DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, DeliveredPendingPartialNote, }, }; +use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::decrypt_log; +use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; +use crate::encrypted_logs::log_assembly_strategies::default_aes128::utils_to_nuke::be_bytes_32_to_fields; pub global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; /// Partial notes have a maximum packed length of their private fields bound by extra content in their private log (i.e. @@ -42,22 +45,24 @@ pub unconstrained fn fetch_and_process_private_tagged_logs( sync_notes(); } -/// Processes a log's plaintext, searching for private notes or partial notes. Private notes result in nonce discovery -/// being performed prior to delivery, which requires knowledge of the transaction hash in which the notes would've been -/// created (typically the same transaction in which the log was emitted), along with the list of unique note hashes in -/// said transaction and the `compute_note_hash_and_nullifier` function. +/// Processes a log's ciphertext by decrypting it and then searching the plaintext for private notes or partial notes. Private +/// notes result in nonce discovery being performed prior to delivery, which requires knowledge of the transaction hash in +/// which the notes would've been created (typically the same transaction in which the log was emitted), along with the +/// list of unique note hashes in said transaction and the `compute_note_hash_and_nullifier` function. pub unconstrained fn do_process_log( contract_address: AztecAddress, - log_plaintext: BoundedVec, + log: BoundedVec, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: AztecAddress, compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, ) { - // The first thing to do is to determine what type of private log we're processing. We currently just have two log - // types: 0 for private notes and 1 for partial notes. This will likely be expanded and improved upon in the future - // to also handle events, etc. + let log_plaintext = decrypt_log(log, recipient); + + // The first thing to do after decrypting the log is to determine what type of private log we're processing. We + // currently just have two log types: 0 for private notes and 1 for partial notes. This will likely be expanded and + // improved upon in the future to also handle events, etc. let (storage_slot, note_type_id, log_type_id, log_payload) = destructure_log_plaintext(log_plaintext); @@ -92,7 +97,7 @@ pub unconstrained fn do_process_log( } unconstrained fn destructure_log_plaintext( - log_plaintext: BoundedVec, + log_plaintext: BoundedVec, ) -> (Field, Field, Field, BoundedVec) { assert(log_plaintext.len() >= NOTE_PRIVATE_LOG_RESERVED_FIELDS); diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/arithmetic_generics_utils.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/arithmetic_generics_utils.nr new file mode 100644 index 000000000000..6d3c4b05e983 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/arithmetic_generics_utils.nr @@ -0,0 +1,85 @@ +/********************************************************/ +// 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/event.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr index 81783c6a3a66..881cf7d6d783 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 @@ -2,9 +2,11 @@ use crate::{ context::PrivateContext, encrypted_logs::{ encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, - log_assembly_strategies::default_aes128::note::{ - get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, - HEADER_CIPHERTEXT_SIZE_IN_BYTES, + log_assembly_strategies::default_aes128::{ + arithmetic_generics_utils::{ + get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, + }, + note::encryption::HEADER_CIPHERTEXT_SIZE_IN_BYTES, }, }, event::event_interface::EventInterface, @@ -14,7 +16,7 @@ use crate::{ }, oracle::{ notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, - random::{get_random_bytes, random}, + random::{get_random_bytes, random_with_max_byte_size}, }, utils::{conversion::bytes_to_fields::bytes_to_fields, point::get_sign_of_point}, }; @@ -60,31 +62,26 @@ use std::aes128::aes128_encrypt; /// 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 `+ 32` bytes is for the event_type_id: -fn compute_event_plaintext_for_this_strategy(event: Event) -> [u8; N * 32 + 32] +/// NB: The extra `+ 1` is for the event_type_id: +fn compute_event_plaintext_for_this_strategy(event: Event) -> [u8; (N + 1) * 32] where Event: EventInterface, { let serialized_event = Serialize::::serialize(event); - let event_type_id_bytes: [u8; 32] = Event::get_event_type_id().to_field().to_be_bytes(); - - let mut plaintext_bytes = [0 as u8; N * 32 + 32]; - - for i in 0..32 { - plaintext_bytes[i] = event_type_id_bytes[i]; - } - + let mut fields = [0; N + 1]; + fields[0] = Event::get_event_type_id().to_field(); for i in 0..serialized_event.len() { - let bytes: [u8; 32] = serialized_event[i].to_be_bytes(); - for j in 0..32 { - plaintext_bytes[32 + i * 32 + j] = bytes[j]; - } + fields[i + 1] = serialized_event[i]; } - plaintext_bytes + fields_to_be_bytes_32(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, @@ -236,9 +233,12 @@ where 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). + // 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. - final_log[i] = unsafe { random() }; + final_log[i] = unsafe { random_with_max_byte_size::<31>() }; } final_log 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 fc06cf1fa360..acf7c9147c5c 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 @@ -1,2 +1,4 @@ +pub mod arithmetic_generics_utils; pub mod event; pub mod note; +pub mod utils_to_nuke; 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 deleted file mode 100644 index b3b62adebaf2..000000000000 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr +++ /dev/null @@ -1,546 +0,0 @@ -use crate::{ - context::PrivateContext, - encrypted_logs::encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, - keys::{ - ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, - ephemeral::generate_ephemeral_key_pair, - }, - note::{note_emission::NoteEmission, note_interface::NoteType}, - oracle::{ - notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, - random::{get_random_bytes, random}, - }, - utils::{conversion::bytes_to_fields::bytes_to_fields, point::get_sign_of_point}, -}; -use dep::protocol_types::{ - abis::note_hash::NoteHash, - address::AztecAddress, - constants::PRIVATE_LOG_SIZE_IN_FIELDS, - traits::{Packable, ToField}, -}; -use std::aes128::aes128_encrypt; - -pub(crate) global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 48; // contract_address (32) + ciphertext_length (2) + 16 bytes pkcs#7 AES padding. - -/* - * 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. - */ - -/* - * 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 - * - * Note-specific: - * derive_note_randomness_from_shared_secret: false - * - */ - -/* - * LOG LAYOUT CHOICE: - * - * Short explanation: - * log = [tag, epk, header_ct=[[contract_address, ct_len], pkcs7_pad], ct=[[pt], pkcs7_pad], some bytes padding, some fields padding] - * - * Long explanation: - * tag: Field - * epk: [Field, u8] - * header_ct: [[u8; 32], [u8; 2], [u8; 16]] - * ct: [[u8; 2], [u8; x], [u8; y]] - * - * More precisely (in pseudocode): - * - * log = [ - * tag: Field, - * Epk: Field, - * - * le_bytes_31_to_fields( - * - * log_bytes: [ - * eph_pk_sign: [u8; 1], - * - * header_ciphertext: aes_encrypt( - * contract_address: [u8; 32], - * ct_length: [u8; 2], - * - * // the aes128_encrypt fn automatically inserts padding: - * header_pt_aes_padding: [u8; 14], // `16 - (input.len() % 16)` - - * ): [u8; 48], - * - * ciphertext: aes_encrypt( - * final_pt: [ - * pt: { - * note_bytes: { - * storage_slot: [u8; 32], - * note_type_id: [u8; 32], - * ...note: [u8; N * 32], - * }: [u8; N * 32 + 64], - * }: [u8; N * 32 + 64], - - * ]: [u8; N * 32 + 64], - * - * // the aes128_encrypt fn automatically inserts padding: - * pt_aes_padding: [u8; 16 - ( (|pt_length| + |pt|) % 16 )] - * - * ): [u8; |pt| + |pt_aes_padding|] - * [u8; |ct|] - * - * log_bytes_padding_to_mult_31: [u8; 31 * ceil((1 + 48 + |ct|)/31) - (1 + 48 + |ct|)], - * [u8; p] - * - * ]: [u8; 1 + 48 + |ct| + p] - * - * ): [Field; (1 + 48 + |ct| + p) / 31] - * - * log_fields_padding: [Field; PRIVATE_LOG_SIZE_IN_FIELDS - 2 - (1 + 48 + |ct| + p) / 31], - * - * ]: [Field; PRIVATE_LOG_SIZE_IN_FIELDS] - * - * - */ - -/********************************************************/ -// 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 -} - -/********************************************************/ -// End of disgusting arithmetic on generics -/********************************************************/ - -// TODO: it feels like this existence check is in the wrong place. In fact, why is it needed at all? Under what circumstances have we found a non-existent note being emitted accidentally? -fn assert_note_exists(context: PrivateContext, note_hash_counter: u32) { - // TODO(#8589): use typesystem to skip this check when not needed - let note_exists = - context.note_hashes.storage().any(|n: NoteHash| n.counter == note_hash_counter); - assert(note_exists, "Can only emit a note log for an existing note."); -} - -/// This particular log assembly strategy (AES 128) requires the note (and the -/// note_id and the storage_slot) to be converted into bytes, because the aes function -/// operates on bytes; not fields. -/// NB: The extra `+ 64` bytes is for the note_id and the storage_slot of the note: -fn compute_note_plaintext_for_this_strategy( - note: Note, - storage_slot: Field, -) -> [u8; N * 32 + 64] -where - Note: NoteType + Packable, -{ - let packed_note = note.pack(); - - let storage_slot_bytes: [u8; 32] = storage_slot.to_be_bytes(); - - // TODO(#10952): The following can be reduced to 7 bits - let note_type_id_bytes: [u8; 32] = Note::get_id().to_be_bytes(); - - // We combine all the bytes into plaintext_bytes: - let mut plaintext_bytes: [u8; N * 32 + 64] = [0; N * 32 + 64]; - for i in 0..32 { - plaintext_bytes[i] = storage_slot_bytes[i]; - plaintext_bytes[32 + i] = note_type_id_bytes[i]; - } - - for i in 0..packed_note.len() { - let bytes: [u8; 32] = packed_note[i].to_be_bytes(); - for j in 0..32 { - plaintext_bytes[64 + i * 32 + j] = bytes[j]; - } - } - - plaintext_bytes -} - -fn compute_log( - context: PrivateContext, - note: Note, - storage_slot: Field, - recipient: AztecAddress, - sender: AztecAddress, -) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] -where - Note: NoteType + Packable, -{ - // ***************************************************************************** - // 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); // not to be confused with the tagging shared secret - - // TODO: also use this shared secret for deriving note randomness. - - // ***************************************************************************** - // Compute the plaintext - // ***************************************************************************** - - let final_plaintext_bytes = compute_note_plaintext_for_this_strategy(note, storage_slot); - - // ***************************************************************************** - // 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_bytes, iv, sym_key); - - // |full_pt| = |pt_length| + |pt| - // |pt_aes_padding| = 16 - (|full_pt| % 16) - // or... since a % b is the same as a - b * (a // b) (integer division), so: - // |pt_aes_padding| = 16 - (|full_pt| - 16 * (|full_pt| // 16)) - // |ct| = |full_pt| + |pt_aes_padding| - // = |full_pt| + 16 - (|full_pt| - 16 * (|full_pt| // 16)) - // = 16 + 16 * (|full_pt| // 16) - // = 16 * (1 + |full_pt| // 16) - assert(ciphertext_bytes.len() == 16 * (1 + ((N * 32) + 64) / 16)); - - // ***************************************************************************** - // Compute the header ciphertext - // ***************************************************************************** - - 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) + 64>(); - - // 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 mut log_bytes = get_arr_of_size__log_bytes__from_PT::<(N * 32) + 64>(); - - assert( - log_bytes.len() % 31 == 0, - "Unexpected error: log_bytes.len() should be divisible by 31, by construction.", - ); - - 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]; - } - - assert( - offset + log_bytes_padding_to_mult_31.len() == log_bytes.len(), - "Something has gone wrong", - ); - - // ***************************************************************************** - // Convert the encrypted bytes to fields, because logs are field-based - // ***************************************************************************** - - 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] - - // 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 final_log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS] = [0; PRIVATE_LOG_SIZE_IN_FIELDS]; - - final_log[0] = tag; - 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 { - // 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. - final_log[i] = unsafe { random() }; - } - - final_log -} - -unconstrained fn compute_log_unconstrained( - context: PrivateContext, - note: Note, - storage_slot: Field, - recipient: AztecAddress, - sender: AztecAddress, -) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] -where - Note: NoteType + Packable, -{ - compute_log(context, note, storage_slot, recipient, sender) -} - -/// Sends an encrypted message to `recipient` with the content of the note, which they will discover when processing -/// private logs. -pub fn encode_and_encrypt_note( - context: &mut PrivateContext, - recipient: AztecAddress, - // We need this because to compute a tagging secret, we require a sender: - sender: AztecAddress, -) -> fn[(&mut PrivateContext, AztecAddress, AztecAddress)](NoteEmission) -> () -where - Note: NoteType + Packable, -{ - |e: NoteEmission| { - let note = e.note; - let storage_slot = e.storage_slot; - let note_hash_counter = e.note_hash_counter; - assert_note_exists(*context, note_hash_counter); - - let encrypted_log = compute_log(*context, note, storage_slot, recipient, sender); - context.emit_raw_note_log(encrypted_log, note_hash_counter); - } -} - -/// Same as `encode_and_encrypt_note`, except encryption is unconstrained. This means that the sender is free to make -/// the log contents be whatever they wish, potentially resulting in scenarios in which the recipient is unable to -/// decrypt and process the payload, **leading to the note being lost**. -/// -/// Only use this function in scenarios where the recipient not receiving the note is an acceptable outcome. -pub fn encode_and_encrypt_note_unconstrained( - context: &mut PrivateContext, - recipient: AztecAddress, - // We need this because to compute a tagging secret, we require a sender: - sender: AztecAddress, -) -> fn[(&mut PrivateContext, AztecAddress, AztecAddress)](NoteEmission) -> () -where - Note: NoteType + Packable, -{ - |e: NoteEmission| { - let note = e.note; - let storage_slot = e.storage_slot; - let note_hash_counter = e.note_hash_counter; - - assert_note_exists(*context, note_hash_counter); - - // Safety: this function does not constrain the encryption of the log, as explainted on its description. - let encrypted_log = - unsafe { compute_log_unconstrained(*context, note, storage_slot, recipient, sender) }; - context.emit_raw_note_log(encrypted_log, note_hash_counter); - } -} - -mod test { - use crate::test::{helpers::test_environment::TestEnvironment, mocks::mock_note::MockNote}; - use dep::protocol_types::address::AztecAddress; - use dep::protocol_types::traits::FromField; - use std::test::OracleMock; - - #[test] - unconstrained fn test_encrypted_log_matches_typescript() { - let mut env = TestEnvironment::new(); - // Advance 1 block so we can read historic state from private - env.advance_block_by(1); - let mut context = env.private(); - - // I'm not sure how to initialise an `env` or `context` with a consistent contract address for every run of this test; the value kept changing each time. So I'm going to overwrite it now: - context.inputs.call_context.contract_address = AztecAddress::from_field( - 0x10f48cd9eff7ae5b209c557c70de2e657ee79166868676b787e9417e19260e04, - ); // This is an address copied to match the typescript one. - - let storage_slot = 42; - let note = MockNote::new(1234).build_note(); - - // All the values in this test were copied over from `encrypted_log_payload.test.ts` - - let eph_sk = 0x1358d15019d4639393d62b97e1588c095957ce74a1c32d6ec7d62fe6705d9538; - let _ = OracleMock::mock("getRandomField").returns(eph_sk).times(1); - - let randomness = 0x0101010101010101010101010101010101010101010101010101010101010101; - let _ = OracleMock::mock("getRandomField").returns(randomness).times(1000000); - - let recipient = AztecAddress::from_field( - 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, - ); - - let sender = AztecAddress::from_field( - 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, - ); - - let _ = OracleMock::mock("getIndexedTaggingSecretAsSender").returns([69420, 1337]); - - let _ = OracleMock::mock("incrementAppTaggingSecretIndexAsSender").returns(()); - - let payload = super::compute_log(context, note, storage_slot, recipient, sender); - - // The following value was generated by `encrypted_log_payload.test.ts` - // --> Run the test with AZTEC_GENERATE_TEST_DATA=1 flag to update test data. - let private_log_payload_from_typescript = [ - 0x0e9cffc3ddd746affb02410d8f0a823e89939785bcc8e88ee4f3cae05e737c36, - 0x0d460c0e434d846ec1ea286e4090eb56376ff27bddc1aacae1d856549f701fa7, - 0x000194e6d7872db8f61e8e59f23580f4db45d13677f873ec473a409cf61fd04d, - 0x00334e5fb6083721f3eb4eef500876af3c9acfab0a1cb1804b930606fdb0b283, - 0x00af91db798fa320746831a59b74362dfd0cf9e7c239f6aad11a4b47d0d870ee, - 0x00d25a054613a83be7be8512f2c09664bc4f7ab60a127b06584f476918581b8a, - 0x003840d100d8c1d78d4b68b787ed353ebfb8cd2987503d3b472f614f25799a18, - 0x003f38322629d401010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - 0x0101010101010101010101010101010101010101010101010101010101010101, - ]; - - assert_eq(payload, private_log_payload_from_typescript); - } -} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr new file mode 100644 index 000000000000..47bce16360b7 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr @@ -0,0 +1,352 @@ +use crate::{ + encrypted_logs::encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, + keys::{ + ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, + ephemeral::generate_ephemeral_key_pair, + }, + oracle::{ + notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, + random::random_with_max_byte_size, + }, + utils::{array, bytes::{be_bytes_31_to_fields, get_random_bytes}, point::get_sign_of_point}, +}; +use crate::encrypted_logs::log_assembly_strategies::default_aes128::{ + arithmetic_generics_utils::{ + get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, + }, + utils_to_nuke, +}; +use crate::encrypted_logs::log_assembly_strategies::default_aes128::utils_to_nuke::be_bytes_32_to_fields; +use crate::oracle::aes128_decrypt::aes128_decrypt_oracle; +use crate::oracle::shared_secret::get_shared_secret; +use crate::utils::bytes::fields_to_be_bytes_32; +use crate::utils::point::point_from_x_coord_and_sign; +use dep::protocol_types::{ + address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::ToField, +}; +use std::aes128::aes128_encrypt; + +pub(crate) global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 48; // contract_address (32) + ciphertext_length (2) + 16 bytes pkcs#7 AES padding. + +global TAG_AND_EPH_PK_X_SIZE_IN_FIELDS: u32 = 2; +global EPH_PK_SIGN_BYTE_SIZE_IN_BYTES: u32 = 1; +pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES: u32 = ( + PRIVATE_LOG_SIZE_IN_FIELDS - TAG_AND_EPH_PK_X_SIZE_IN_FIELDS +) + * 31 + - HEADER_CIPHERTEXT_SIZE_IN_BYTES + - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; +// Each field of the original note log was serialized to 32 bytes. Below we convert the bytes back to fields. +pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS: u32 = + (PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES + 31) / 32; + +/// Computes an encrypted log using AES-128 encryption in CBC mode. +/// +/// The resulting log has the following format: +/// ```text +/// [ +/// tag: Field, // Tag for note discovery, derived from sender/recipient +/// epk_x: Field, // X coordinate of ephemeral public key +/// log_bytes: [Field], // Encrypted data converted from bytes to fields, containing: +/// [ +/// epk_sign: u8, // Sign bit of ephemeral public key Y coordinate +/// header_ciphertext: [u8], // AES encrypted header containing: +/// [ +/// contract_address: [u8; 32], // Contract address that emitted the note +/// ciphertext_length: [u8; 2], // Length of main ciphertext in bytes +/// padding: [u8; 14] // PKCS#7 padding to AES block size +/// ], +/// ciphertext: [u8], // AES encrypted note data containing: +/// [ +/// plaintext_bytes: [u8], // The plaintext +/// padding: [u8] // PKCS#7 padding to AES block size +/// ], +/// padding: [u8] // Random padding to make log_bytes multiple of 31 +/// ], +/// padding: [Field] // Random padding to PRIVATE_LOG_SIZE_IN_FIELDS +/// ] +/// ``` +/// +/// The encryption process: +/// 1. Generate ephemeral key-pair and ECDH shared secret with recipient +/// 2. Derive AES key and IV from shared secret using SHA-256 +/// 3. Encrypt header and note data separately using AES-128-CBC +/// 4. Format into final log structure with padding +pub fn encrypt_log( + contract_address: AztecAddress, + plaintext: [Field; PT], + recipient: AztecAddress, + sender: AztecAddress, +) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] { + // AES 128 operates on bytes, not fields, so we need to convert the fields to bytes. + // (This process is then reversed when processing the log in `do_process_log`) + let plaintext_bytes = fields_to_be_bytes_32(plaintext); + + // ***************************************************************************** + // 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; + + // (not to be confused with the tagging shared secret) + let ciphertext_shared_secret = derive_ecdh_shared_secret_using_aztec_address(eph_sk, recipient); + + // TODO: also use this shared secret for deriving note randomness. + + // ***************************************************************************** + // 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(plaintext_bytes, iv, sym_key); + + // |full_pt| = |pt_length| + |pt| + // |pt_aes_padding| = 16 - (|full_pt| % 16) + // or... since a % b is the same as a - b * (a // b) (integer division), so: + // |pt_aes_padding| = 16 - (|full_pt| - 16 * (|full_pt| // 16)) + // |ct| = |full_pt| + |pt_aes_padding| + // = |full_pt| + 16 - (|full_pt| - 16 * (|full_pt| // 16)) + // = 16 + 16 * (|full_pt| // 16) + // = 16 * (1 + |full_pt| // 16) + assert(ciphertext_bytes.len() == 16 * (1 + (PT * 32) / 16)); + + // ***************************************************************************** + // Compute the header ciphertext + // ***************************************************************************** + + 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::(); + // Safety: this randomness won't be constrained to be random. It's in the + // interest of the executor of this fn to encrypt with random bytes. + log_bytes_padding_to_mult_31 = unsafe { get_random_bytes() }; + + let mut log_bytes = get_arr_of_size__log_bytes__from_PT::(); + + assert( + log_bytes.len() % 31 == 0, + "Unexpected error: log_bytes.len() should be divisible by 31, by construction.", + ); + + 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]; + } + + assert( + offset + log_bytes_padding_to_mult_31.len() == log_bytes.len(), + "Something has gone wrong", + ); + + // ***************************************************************************** + // Convert bytes back to fields + // ***************************************************************************** + + let log_bytes_as_fields = be_bytes_31_to_fields(log_bytes); + + // ***************************************************************************** + // Prepend / append fields, to create the final log + // ***************************************************************************** + + // In this strategy, we prepend [tag, eph_pk.x] + + // 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 final_log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS] = [0; PRIVATE_LOG_SIZE_IN_FIELDS]; + + final_log[0] = tag; + 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). + // Safety: randomness cannot be constrained. + final_log[i] = unsafe { random_with_max_byte_size::<31>() }; + } + + final_log +} + +pub unconstrained fn decrypt_log( + log: BoundedVec, + recipient: AztecAddress, +) -> BoundedVec { + // let tag = log.get(0); + let eph_pk_x = log.get(1); + + let log_ciphertext_fields = array::subbvec::( + log, + TAG_AND_EPH_PK_X_SIZE_IN_FIELDS, + ); + + // Convert the ciphertext represented as fields to a byte representation (its original format) + let log_ciphertext = utils_to_nuke::fields_to_be_bytes_31(log_ciphertext_fields); + + // First byte of the ciphertext represents the ephemeral public key sign + let eph_pk_sign_bool = log_ciphertext.get(0) as bool; + // With the sign and the x-coordinate of the ephemeral public key, we can reconstruct the point + let eph_pk = point_from_x_coord_and_sign(eph_pk_x, eph_pk_sign_bool); + + // Derive shared secret and symmetric key + let ciphertext_shared_secret = get_shared_secret(recipient, eph_pk); + let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( + ciphertext_shared_secret, + ); + + // Extract the header ciphertext + let header_start = EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; // Skip eph_pk_sign byte + let header_ciphertext: [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES] = + array::subarray(log_ciphertext.storage(), header_start); + // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to work + // with logs with unknown length at compile time. This would not be necessary here as the header ciphertext length + // is fixed. But we do it anyway to not have to have duplicate oracles. + let header_ciphertext_bvec = + BoundedVec::::from_array(header_ciphertext); + + // Decrypt header + let header_plaintext = aes128_decrypt_oracle(header_ciphertext_bvec, iv, sym_key); + + // Extract ciphertext length from header (2 bytes, big-endian) + let ciphertext_length = + ((header_plaintext.get(32) as u32) << 8) | (header_plaintext.get(33) as u32); + + // Extract and decrypt main ciphertext + let ciphertext_start = header_start + HEADER_CIPHERTEXT_SIZE_IN_BYTES; + let ciphertext_with_padding: [u8; (PRIVATE_LOG_SIZE_IN_FIELDS - TAG_AND_EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES] = + array::subarray(log_ciphertext.storage(), ciphertext_start); + let ciphertext: BoundedVec = + BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length); + + // Decrypt main ciphertext and return it + let log_plaintext_bytes = aes128_decrypt_oracle(ciphertext, iv, sym_key); + + // Each field of the original note log was serialized to 32 bytes so we convert the bytes back to fields. + be_bytes_32_to_fields(log_plaintext_bytes) +} + +mod test { + use crate::keys::ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address; + use crate::test::helpers::test_environment::TestEnvironment; + use super::{decrypt_log, encrypt_log, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS}; + use dep::protocol_types::address::AztecAddress; + use dep::protocol_types::traits::FromField; + use protocol_types::traits::Serialize; + use std::test::OracleMock; + + #[test] + unconstrained fn test_encrypt_decrypt_log() { + let mut env = TestEnvironment::new(); + // Advance 1 block so we can read historic state from private + env.advance_block_by(1); + + let contract_address = AztecAddress::from_field( + 0x10f48cd9eff7ae5b209c557c70de2e657ee79166868676b787e9417e19260e04, + ); + + let plaintext = [1, 2, 3]; + + let recipient = AztecAddress::from_field( + 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, + ); + + let sender = AztecAddress::from_field( + 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, + ); + + // Mock random values for deterministic test + let eph_sk = 0x1358d15019d4639393d62b97e1588c095957ce74a1c32d6ec7d62fe6705d9538; + let _ = OracleMock::mock("getRandomField").returns(eph_sk).times(1); + + let randomness = 0x0101010101010101010101010101010101010101010101010101010101010101; + let _ = OracleMock::mock("getRandomField").returns(randomness).times(1000000); + + let _ = OracleMock::mock("getIndexedTaggingSecretAsSender").returns([69420, 1337]); + let _ = OracleMock::mock("incrementAppTaggingSecretIndexAsSender").returns(()); + + // Encrypt the log + let encrypted_log = + BoundedVec::from_array(encrypt_log(contract_address, plaintext, recipient, sender)); + + // Mock shared secret for deterministic test + let shared_secret = derive_ecdh_shared_secret_using_aztec_address( + std::hash::from_field_unsafe(eph_sk), + recipient, + ); + let _ = OracleMock::mock("getSharedSecret").returns(shared_secret.serialize()); + + // Decrypt the log + let decrypted = decrypt_log(encrypted_log, recipient); + + // The decryption function spits out a BoundedVec because it's designed to work with logs with unknown length + // at compile time. For this reason we need to convert the original input to a BoundedVec. + let plaintext_bvec = + BoundedVec::::from_array(plaintext); + + // Verify decryption matches original plaintext + assert_eq(decrypted, plaintext_bvec, "Decrypted bytes should match original plaintext"); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/mod.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/mod.nr new file mode 100644 index 000000000000..9aa3d138d205 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/mod.nr @@ -0,0 +1,7 @@ +pub mod encryption; +pub mod note; + +pub use note::{ + compute_log, compute_log_unconstrained, encode_and_encrypt_note, + encode_and_encrypt_note_unconstrained, +}; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr new file mode 100644 index 000000000000..bc661e440acd --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr @@ -0,0 +1,222 @@ +use crate::{ + context::PrivateContext, + encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::encrypt_log, + note::{note_emission::NoteEmission, note_interface::NoteType}, + utils::bytes::fields_to_be_bytes_32, +}; +use dep::protocol_types::{ + abis::note_hash::NoteHash, address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, + traits::Packable, +}; + +/* + * 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. + */ + +/* + * 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 + * + * Note-specific: + * derive_note_randomness_from_shared_secret: false + * + */ + +/* + * LOG LAYOUT CHOICE: + * + * Short explanation: + * log = [tag, epk, header_ct=[[contract_address, ct_len], pkcs7_pad], ct=[[pt], pkcs7_pad], some bytes padding, some fields padding] + * + * Long explanation: + * tag: Field + * epk: [Field, u8] + * header_ct: [[u8; 32], [u8; 2], [u8; 16]] + * ct: [[u8; 2], [u8; x], [u8; y]] + * + * More precisely (in pseudocode): + * + * log = [ + * tag: Field, + * Epk: Field, + * + * le_bytes_31_to_fields( + * + * log_bytes: [ + * eph_pk_sign: [u8; 1], + * + * header_ciphertext: aes_encrypt( + * contract_address: [u8; 32], + * ct_length: [u8; 2], + * + * // the aes128_encrypt fn automatically inserts padding: + * header_pt_aes_padding: [u8; 14], // `16 - (input.len() % 16)` + + * ): [u8; 48], + * + * ciphertext: aes_encrypt( + * final_pt: [ + * pt: { + * note_bytes: { + * storage_slot: [u8; 32], + * note_type_id: [u8; 32], + * ...note: [u8; N * 32], + * }: [u8; N * 32 + 64], + * }: [u8; N * 32 + 64], + + * ]: [u8; N * 32 + 64], + * + * // the aes128_encrypt fn automatically inserts padding: + * pt_aes_padding: [u8; 16 - ( (|pt_length| + |pt|) % 16 )] + * + * ): [u8; |pt| + |pt_aes_padding|] + * [u8; |ct|] + * + * log_bytes_padding_to_mult_31: [u8; 31 * ceil((1 + 48 + |ct|)/31) - (1 + 48 + |ct|)], + * [u8; p] + * + * ]: [u8; 1 + 48 + |ct| + p] + * + * ): [Field; (1 + 48 + |ct| + p) / 31] + * + * log_fields_padding: [Field; PRIVATE_LOG_SIZE_IN_FIELDS - 2 - (1 + 48 + |ct| + p) / 31], + * + * ]: [Field; PRIVATE_LOG_SIZE_IN_FIELDS] + * + * + */ + +/********************************************************/ +// End of disgusting arithmetic on generics +/********************************************************/ + +// TODO: it feels like this existence check is in the wrong place. In fact, why is it needed at all? Under what circumstances have we found a non-existent note being emitted accidentally? +fn assert_note_exists(context: PrivateContext, note_hash_counter: u32) { + // TODO(#8589): use typesystem to skip this check when not needed + let note_exists = + context.note_hashes.storage().any(|n: NoteHash| n.counter == note_hash_counter); + assert(note_exists, "Can only emit a note log for an existing note."); +} + +/// This particular log assembly strategy (AES 128) requires the note (and the +/// note_id and the storage_slot) to be converted into bytes, because the aes function +/// operates on bytes; not fields. +/// NB: The "2" in "N + 2" is for the note_id and the storage_slot of the note: +fn compute_note_plaintext_for_this_strategy( + note: Note, + storage_slot: Field, +) -> [Field; (N + 2)] +where + Note: NoteType + Packable, +{ + let packed_note = note.pack(); + + let mut fields = [0; N + 2]; + fields[0] = storage_slot; + fields[1] = Note::get_id(); // TODO(#10952): The note type ID can be reduced to 7 bits + for i in 0..packed_note.len() { + fields[i + 2] = packed_note[i]; + } + + fields +} + +pub fn compute_log( + context: PrivateContext, + note: Note, + storage_slot: Field, + recipient: AztecAddress, + sender: AztecAddress, +) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] +where + Note: NoteType + Packable, +{ + let final_plaintext = compute_note_plaintext_for_this_strategy(note, storage_slot); + + encrypt_log(context.this_address(), final_plaintext, recipient, sender) +} + +pub unconstrained fn compute_log_unconstrained( + context: PrivateContext, + note: Note, + storage_slot: Field, + recipient: AztecAddress, + sender: AztecAddress, +) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] +where + Note: NoteType + Packable, +{ + compute_log(context, note, storage_slot, recipient, sender) +} + +// This function seems to be affected by the following Noir bug: +// https://github.com/noir-lang/noir/issues/5771 +// If you get weird behavior it might be because of it. +pub fn encode_and_encrypt_note( + context: &mut PrivateContext, + recipient: AztecAddress, + // We need this because to compute a tagging secret, we require a sender: + sender: AztecAddress, +) -> fn[(&mut PrivateContext, AztecAddress, AztecAddress)](NoteEmission) -> () +where + Note: NoteType + Packable, +{ + |e: NoteEmission| { + let note = e.note; + let storage_slot = e.storage_slot; + let note_hash_counter = e.note_hash_counter; + assert_note_exists(*context, note_hash_counter); + + let encrypted_log = compute_log(*context, note, storage_slot, recipient, sender); + context.emit_raw_note_log(encrypted_log, note_hash_counter); + } +} + +// Important note: this function -- although called "unconstrained" -- the +// function is not labelled as `unconstrained`, because we pass a reference to the +// context. +pub fn encode_and_encrypt_note_unconstrained( + context: &mut PrivateContext, + recipient: AztecAddress, + // We need this because to compute a tagging secret, we require a sender: + sender: AztecAddress, +) -> fn[(&mut PrivateContext, AztecAddress, AztecAddress)](NoteEmission) -> () +where + Note: NoteType + Packable, +{ + |e: NoteEmission| { + let note = e.note; + let storage_slot = e.storage_slot; + let note_hash_counter = e.note_hash_counter; + + assert_note_exists(*context, note_hash_counter); + + // Unconstrained logs have both their content and encryption unconstrained - it could occur that the + // recipient is unable to decrypt the payload. + // Regarding the note hash counter, this is used for squashing. The kernel assumes that a given note can have + // more than one log and removes all of the matching ones, so all a malicious sender could do is either: cause + // for the log to be deleted when it shouldn't have (which is fine - they can already make the content be + // whatever), or cause for the log to not be deleted when it should have (which is also fine - it'll be a log + // for a note that doesn't exist). + // It's important here that we do not + // return the log from this function to the app, otherwise it could try to do stuff with it and then that might + // be wrong. + let encrypted_log = + unsafe { compute_log_unconstrained(*context, note, storage_slot, recipient, sender) }; + context.emit_raw_note_log(encrypted_log, note_hash_counter); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils_to_nuke.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils_to_nuke.nr new file mode 100644 index 000000000000..fb259f834e61 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils_to_nuke.nr @@ -0,0 +1,96 @@ +// TODO: NUKE THIS FILE - COPIED HERE TO UNBLOCK MYSELF +pub fn be_bytes_31_to_fields( + bytes: BoundedVec, +) -> BoundedVec { + let mut fields = BoundedVec::new(); + + // There are "bytes.len() / 31" whole fields that can be populated. + for i in 0..bytes.len() / 31 { + let mut field = 0; + for j in 0..31 { + // Shift the existing value left by 8 bits and add the new byte + field = field * 256 + bytes.get(i * 31 + j) as Field; + } + fields.push(field); + } + + // Note: if 31 divides bytes.len(), then this loop does not execute. + // ceil(bytes.len()/31) - floor(bytes.len()/31) = 1, unless 31 divides bytes.len(), in which case it's 0. + for _ in 0..((bytes.len() + 30) / 31) - (bytes.len() / 31) { + let mut final_field = 0; + let final_field_idx = fields.len(); + let final_offset = final_field_idx * 31; + // bytes.len() - ((bytes.len() / 31) * 31) = bytes.len() - floor(bytes.len()/31) * 31 = the number of bytes + // to go in the final field. + for j in 0..bytes.len() - ((bytes.len() / 31) * 31) { + // Shift the existing value left by 8 bits and add the new byte + final_field = final_field * 256 + bytes.get(final_offset + j) as Field; + } + + fields.push(final_field); + } + + fields +} + +/// Converts an input BoundedVec of fields into a BoundedVec of bytes in big-endian order. +/// +/// Each input field must contain at most 31 bytes (this is constrained to be so). +/// Each field is converted into 31 big-endian bytes, and the resulting 31-byte chunks are concatenated +/// back together in the order of the original fields. +/// +/// This function is expected to be used along with `be_bytes_31_to_fields` to convert a BoundedVec of bytes into +/// a BoundedVec of fields and then back to bytes. +pub fn fields_to_be_bytes_31(fields: BoundedVec) -> BoundedVec { + let mut bytes = BoundedVec::new(); + + for i in 0..fields.len() { + let field = fields.get(i); + + // We expect that the field contains at most 31 bytes of information. + field.assert_max_bit_size::<248>(); + + // Now we can safely convert the field to 31 bytes. + let field_as_bytes: [u8; 31] = field.to_be_bytes(); + + for j in 0..31 { + bytes.push(field_as_bytes[j]); + } + } + + bytes +} + +pub fn be_bytes_32_to_fields( + bytes: BoundedVec, +) -> BoundedVec { + let mut fields = BoundedVec::new(); + + // There are "bytes.len() / 32" whole fields that can be populated. + for i in 0..bytes.len() / 32 { + let mut field = 0; + for j in 0..32 { + // Shift the existing value left by 8 bits and add the new byte + field = field * 256 + bytes.get(i * 32 + j) as Field; + } + fields.push(field); + } + + // Note: if 32 divides bytes.len(), then this loop does not execute. + // ceil(bytes.len()/32) - floor(bytes.len()/32) = 1, unless 32 divides bytes.len(), in which case it's 0. + for _ in 0..((bytes.len() + 31) / 32) - (bytes.len() / 32) { + let mut final_field = 0; + let final_field_idx = fields.len(); + let final_offset = final_field_idx * 32; + // bytes.len() - ((bytes.len() / 32) * 32) = bytes.len() - floor(bytes.len()/32) * 32 = the number of bytes + // to go in the final field. + for j in 0..bytes.len() - ((bytes.len() / 32) * 32) { + // Shift the existing value left by 8 bits and add the new byte + final_field = final_field * 256 + bytes.get(final_offset + j) as Field; + } + + fields.push(final_field); + } + + fields +} diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index b6867b107484..53c37cb92430 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -273,7 +273,7 @@ comptime fn generate_process_log() -> Quoted { if notes.len() > 0 { quote { unconstrained fn process_log( - log_plaintext: BoundedVec, + log_ciphertext: BoundedVec, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, @@ -289,7 +289,7 @@ comptime fn generate_process_log() -> Quoted { aztec::discovery::private_logs::do_process_log( contract_address, - log_plaintext, + log_ciphertext, tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, @@ -303,7 +303,7 @@ comptime fn generate_process_log() -> Quoted { // simply throws immediately. quote { unconstrained fn process_log( - _log_plaintext: BoundedVec, + _log_ciphertext: BoundedVec, _tx_hash: Field, _unique_note_hashes_in_tx: BoundedVec, _first_nullifier_in_tx: Field, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr index 3b86686e752d..53a57382a6d8 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr @@ -4,9 +4,13 @@ /// /// It's up to the calling function to determine whether decryption succeeded or failed. /// See the tests below for an example of how. +/// +/// Note that we accept ciphertext as a BoundedVec, not as an array. This is because this function is typically used +/// when processing logs and at that point we don't have a comptime information about the length of the ciphertext +/// as the log is not specific to any individual note. #[oracle(aes128Decrypt)] pub unconstrained fn aes128_decrypt_oracle( - ciphertext: [u8; N], + ciphertext: BoundedVec, iv: [u8; 16], sym_key: [u8; 16], ) -> BoundedVec {} @@ -35,7 +39,12 @@ mod test { let ciphertext: [u8; TEST_CIPHERTEXT_LENGTH] = aes128_encrypt(plaintext, iv, sym_key); - let received_plaintext = aes128_decrypt_oracle(ciphertext, iv, sym_key); + // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to work + // with logs with unknown length at compile time. This would not be necessary here as the header ciphertext length + // is fixed. But we do it anyway to not have to have duplicate oracles. + let ciphertext_bvec = BoundedVec::::from_array(ciphertext); + + let received_plaintext = aes128_decrypt_oracle(ciphertext_bvec, iv, sym_key); assert_eq(received_plaintext.len(), TEST_PLAINTEXT_LENGTH); assert_eq(received_plaintext.max_len(), TEST_CIPHERTEXT_LENGTH); @@ -113,7 +122,10 @@ mod test { let mut bad_sym_key = sym_key; bad_sym_key[0] = 0; - let received_plaintext = aes128_decrypt_oracle(ciphertext, iv, bad_sym_key); + // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's design to work + // with logs with unknown length. + let ciphertext_bvec = BoundedVec::::from_array(ciphertext); + let received_plaintext = aes128_decrypt_oracle(ciphertext_bvec, iv, bad_sym_key); let extracted_mac_as_bytes: [u8; TEST_MAC_LENGTH] = subarray(received_plaintext.storage(), TEST_PLAINTEXT_LENGTH); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 5d2a69418197..044d7cd87e81 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -20,6 +20,7 @@ pub mod logs; pub mod note_discovery; pub mod notes; pub mod random; +pub mod shared_secret; pub mod storage; // debug_log oracle is used by both noir-protocol-circuits and this crate and for this reason we just re-export it diff --git a/noir-projects/aztec-nr/aztec/src/oracle/random.nr b/noir-projects/aztec-nr/aztec/src/oracle/random.nr index e857090fe84d..c79f7a504cc6 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/random.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/random.nr @@ -22,5 +22,19 @@ pub unconstrained fn get_random_bytes() -> [u8; N] { bytes } +/// Just like `random`, but returns a field that fits in `N` bytes. +pub unconstrained fn random_with_max_byte_size() -> Field { + let random_field = rand_oracle(); + + // We convert the field to the desired number of bytes, and then back to a field + let random_field_as_bytes: [u8; N] = random_field.to_be_bytes(); + let reconstructed_field = Field::from_be_bytes::(random_field_as_bytes); + + // We assert that the max byte size is as expected + reconstructed_field.assert_max_bit_size::<8 * N>(); + + reconstructed_field +} + #[oracle(getRandomField)] unconstrained fn rand_oracle() -> Field {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr new file mode 100644 index 000000000000..d74d7e88dfa4 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr @@ -0,0 +1,16 @@ +use dep::protocol_types::{ + address::aztec_address::AztecAddress, + point::{Point, POINT_LENGTH}, + traits::Deserialize, +}; + +#[oracle(getSharedSecret)] +pub unconstrained fn get_shared_secret_oracle( + address: AztecAddress, + ephPk: Point, +) -> [Field; POINT_LENGTH] {} + +pub unconstrained fn get_shared_secret(address: AztecAddress, ephPk: Point) -> Point { + let fields = get_shared_secret_oracle(address, ephPk); + Point::deserialize(fields) +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/point.nr b/noir-projects/aztec-nr/aztec/src/utils/point.nr index c8055bef2357..ed308df36c74 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/point.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/point.nr @@ -46,8 +46,25 @@ pub fn point_from_x_coord(x: Field) -> Point { Point { x, y, is_infinite: false } } +/// Uses the x coordinate and sign flag (+/-) to reconstruct the point. +/// The y coordinate can be derived from the x coordinate and the "sign" flag by solving the grumpkin curve +/// equation for y. +/// @param x - The x coordinate of the point +/// @param sign - The "sign" of the y coordinate - determines whether y <= (Fr.MODULUS - 1) / 2 +pub fn point_from_x_coord_and_sign(x: Field, sign: bool) -> Point { + // y ^ 2 = x ^ 3 - 17 + let rhs = x * x * x - 17; + let y = sqrt(rhs).unwrap(); + + // If y > MOD_DIV_2 and we want positive sign (or vice versa), negate y + let y_is_positive = !BN254_FR_MODULUS_DIV_2.lt(y); + let final_y = if y_is_positive == sign { y } else { -y }; + + Point { x, y: final_y, is_infinite: false } +} + mod test { - use crate::utils::point::point_to_bytes; + use crate::utils::point::{point_from_x_coord_and_sign, point_to_bytes}; use dep::protocol_types::point::Point; #[test] @@ -84,4 +101,25 @@ mod test { assert_eq(expected_compressed_point_negative_sign, compressed_point); } + + #[test] + unconstrained fn test_point_from_x_coord_and_sign() { + // Test positive y coordinate + let x = 0x1af41f5de96446dc3776a1eb2d98bb956b7acd9979a67854bec6fa7c2973bd73; + let sign = true; + let p = point_from_x_coord_and_sign(x, sign); + + assert_eq(p.x, x); + assert_eq(p.y, 0x07fc22c7f2c7057571f137fe46ea9c95114282bc95d37d71ec4bfb88de457d4a); + assert_eq(p.is_infinite, false); + + // Test negative y coordinate + let x2 = 0x247371652e55dd74c9af8dbe9fb44931ba29a9229994384bd7077796c14ee2b5; + let sign2 = false; + let p2 = point_from_x_coord_and_sign(x2, sign2); + + assert_eq(p2.x, x2); + assert_eq(p2.y, 0x26441aec112e1ae4cee374f42556932001507ad46e255ffb27369c7e3766e5c0); + assert_eq(p2.is_infinite, false); + } } diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index 2d108366d0ff..6d30ee2a573f 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -374,27 +374,6 @@ pub contract Test { } } - #[private] - fn emit_encrypted_logs_nested(value: Field, owner: AztecAddress, sender: AztecAddress) { - let mut storage_slot = - aztec::state_vars::storage::Storage::get_storage_slot(storage.example_constant) + 1; - Test::at(context.this_address()).call_create_note(value, owner, sender, storage_slot).call( - &mut context, - ); - storage_slot += 1; - - let note = ValueNote::new(value + 1, owner); - create_note(&mut context, storage_slot, note).emit(encode_and_encrypt_note( - &mut context, - owner, - sender, - )); - storage_slot += 1; - Test::at(context.this_address()) - .call_create_note(value + 2, owner, sender, storage_slot) - .call(&mut context); - } - // docs:start:is-time-equal #[public] fn is_time_equal(time: u64) -> u64 { diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr b/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr index eeb33844923c..e8e4c7535b7e 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr @@ -116,7 +116,7 @@ impl AztecAddress { // they can recover our full point using the x-coordinate (our address itself). To do this, they recompute the y-coordinate according to the equation y^2 = x^3 - 17. // When they do this, they may get a positive y-coordinate (a value that is less than or equal to MAX_FIELD_VALUE / 2) or // a negative y-coordinate (a value that is more than MAX_FIELD_VALUE), and we cannot dictate which one they get and hence the recovered point may sometimes be different than the one - // our secrect can decrypt. Regardless though, they should and will always encrypt using point with the positive y-coordinate by convention. + // our secret can decrypt. Regardless though, they should and will always encrypt using point with the positive y-coordinate by convention. // This ensures that everyone encrypts to the same point given an arbitrary x-coordinate (address). This is allowed because even though our original point may not have a positive y-coordinate, // with our original secret, we will be able to derive the secret to the point with the flipped (and now positive) y-coordinate that everyone encrypts to. AztecAddress::from_field(address_point.x) diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts index d4a4b7897275..11b6daadde42 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts @@ -54,15 +54,7 @@ export class LogStore { this.#log.debug(`Found private log with tag ${tag.toString()} in block ${block.number}`); const currentLogs = taggedLogs.get(tag.toString()) ?? []; - currentLogs.push( - new TxScopedL2Log( - txHash, - dataStartIndexForTx, - block.number, - /* isFromPublic */ false, - log.toBuffer(), - ).toBuffer(), - ); + currentLogs.push(new TxScopedL2Log(txHash, dataStartIndexForTx, block.number, log).toBuffer()); taggedLogs.set(tag.toString(), currentLogs); }); @@ -71,15 +63,7 @@ export class LogStore { this.#log.debug(`Found public log with tag ${tag.toString()} in block ${block.number}`); const currentLogs = taggedLogs.get(tag.toString()) ?? []; - currentLogs.push( - new TxScopedL2Log( - txHash, - dataStartIndexForTx, - block.number, - /* isFromPublic */ true, - log.toBuffer(), - ).toBuffer(), - ); + currentLogs.push(new TxScopedL2Log(txHash, dataStartIndexForTx, block.number, log).toBuffer()); taggedLogs.set(tag.toString(), currentLogs); }); }); diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index b55ef2e7d7f5..15e09d3714e0 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -288,7 +288,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { const currentLogs = this.taggedLogs.get(tag.toString()) || []; this.taggedLogs.set(tag.toString(), [ ...currentLogs, - new TxScopedL2Log(txHash, dataStartIndexForTx, block.number, /* isFromPublic */ false, log.toBuffer()), + new TxScopedL2Log(txHash, dataStartIndexForTx, block.number, log), ]); const currentTagsInBlock = this.logTagsPerBlock.get(block.number) || []; this.logTagsPerBlock.set(block.number, [...currentTagsInBlock, tag]); @@ -301,7 +301,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { const currentLogs = this.taggedLogs.get(tag.toString()) || []; this.taggedLogs.set(tag.toString(), [ ...currentLogs, - new TxScopedL2Log(txHash, dataStartIndexForTx, block.number, /* isFromPublic */ true, log.toBuffer()), + new TxScopedL2Log(txHash, dataStartIndexForTx, block.number, log), ]); const currentTagsInBlock = this.logTagsPerBlock.get(block.number) || []; this.logTagsPerBlock.set(block.number, [...currentTagsInBlock, tag]); diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 6f51e88a49c3..e505e00f6754 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -48,7 +48,7 @@ export { GlobalVariables, } from '@aztec/stdlib/tx'; export { Body, L2Block } from '@aztec/stdlib/block'; -export { L1NotePayload, LogId, type LogFilter, EncryptedLogPayload } from '@aztec/stdlib/logs'; +export { LogId, type LogFilter, EncryptedLogPayload } from '@aztec/stdlib/logs'; export { L1EventPayload, EventMetadata } from '@aztec/stdlib/event'; export { L1ToL2Message, L2Actor, L1Actor } from '@aztec/stdlib/messaging'; export { UniqueNote, ExtendedNote, Comparator, Note } from '@aztec/stdlib/note'; diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts index ccbdd50fcdbe..b0b390e4371d 100644 --- a/yarn-project/end-to-end/src/e2e_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts @@ -10,7 +10,6 @@ import { Fr, type GlobalVariables, L1EventPayload, - L1NotePayload, type Logger, type PXE, TxStatus, @@ -415,29 +414,6 @@ describe('e2e_block_building', () => { afterAll(() => teardown()); - it('calls a method with nested note encrypted logs', async () => { - const thisWallet = new AccountWalletWithSecretKey(pxe, ownerWallet, owner.secret, owner.salt); - const address = owner.address; - - // call test contract - const action = testContract.methods.emit_encrypted_logs_nested(10, address, address); - const tx = await action.prove(); - const rct = await tx.send().wait(); - - // compare logs - expect(rct.status).toEqual('success'); - const noteValues = await Promise.all( - tx.data.getNonEmptyPrivateLogs().map(async log => { - const notePayload = await L1NotePayload.decryptAsIncoming(log, await thisWallet.getEncryptionSecret()); - // In this test we care only about the privately delivered values - return notePayload?.privateNoteValues[0]; - }), - ); - expect(noteValues[0]).toEqual(new Fr(10)); - expect(noteValues[1]).toEqual(new Fr(11)); - expect(noteValues[2]).toEqual(new Fr(12)); - }, 30_000); - it('calls a method with nested encrypted logs', async () => { const thisWallet = new AccountWalletWithSecretKey(pxe, ownerWallet, owner.secret, owner.salt); const address = owner.address; diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index 6cd242cdbeda..7f400763f231 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -437,6 +437,13 @@ export class Fq extends BaseField { return this.toString(); } + toFields() { + // The following has to match the order of the limbs in EmbeddedCurveScalar struct in noir::std. This is because + // this function is used when returning Scalar from the getAddressSecret oracle and in Noir the values get deserialized + // using the intrinsic serialization of Noir (which follows the order of the fields/members in the struct). + return [this.lo, this.hi]; + } + static get schema() { return hexSchemaFor(Fq); } 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 81ffa6aba0f9..19b9db38c1da 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 @@ -1,18 +1,17 @@ -import { INITIAL_L2_BLOCK_NUM, MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS } from '@aztec/constants'; +import { PUBLIC_LOG_DATA_SIZE_IN_FIELDS } from '@aztec/constants'; import { timesParallel } from '@aztec/foundation/collection'; -import { pedersenHash, poseidon2Hash } from '@aztec/foundation/crypto'; -import { Fq, Fr, GrumpkinScalar } from '@aztec/foundation/fields'; +import { poseidon2Hash } from '@aztec/foundation/crypto'; +import { Fq, Fr } from '@aztec/foundation/fields'; import { KeyStore } from '@aztec/key-store'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { type AcirSimulator, type SimulationProvider, WASMSimulator } from '@aztec/simulator/client'; import { type FunctionArtifact, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { L2Block, randomInBlock, wrapInBlock } from '@aztec/stdlib/block'; +import { randomInBlock } from '@aztec/stdlib/block'; import { CompleteAddress } from '@aztec/stdlib/contract'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { computeAddress, computeTaggingSecretPoint, deriveKeys } from '@aztec/stdlib/keys'; -import { EncryptedLogPayload, IndexedTaggingSecret, L1NotePayload, PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs'; -import { Note } from '@aztec/stdlib/note'; +import { IndexedTaggingSecret, PrivateLog, PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs'; import { randomContractArtifact, randomContractInstanceWithAddress } from '@aztec/stdlib/testing'; import { TxEffect, TxHash } from '@aztec/stdlib/tx'; @@ -29,70 +28,8 @@ import { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_da import { PXEOracleInterface } from './pxe_oracle_interface.js'; import { WINDOW_HALF_SIZE } from './tagging_utils.js'; -const TXS_PER_BLOCK = 4; -const NUM_NOTE_HASHES_PER_BLOCK = TXS_PER_BLOCK * MAX_NOTE_HASHES_PER_TX; - jest.setTimeout(30_000); -async function getRandomNoteLogPayload(tag = Fr.random(), app?: AztecAddress): Promise { - return new EncryptedLogPayload( - tag, - app ?? (await AztecAddress.random()), - (await L1NotePayload.random(app)).toIncomingBodyPlaintext(), - ); -} - -/** A wrapper containing info about a note we want to mock and insert into a block. */ -class MockNoteRequest { - constructor( - /** Log payload corresponding to a note we want to insert into a block. */ - public readonly logPayload: EncryptedLogPayload, - /** Block number this note corresponds to. */ - public readonly blockNumber: number, - /** Index of a tx within a block this note corresponds to. */ - public readonly txIndex: number, - /** Index of a note hash within a list of note hashes for 1 tx. */ - public readonly noteHashIndex: number, - /** Address point we use when encrypting a note. */ - public readonly recipient: AztecAddress, - ) { - if (blockNumber < INITIAL_L2_BLOCK_NUM) { - throw new Error(`Block number should be greater than or equal to ${INITIAL_L2_BLOCK_NUM}.`); - } - if (noteHashIndex >= MAX_NOTE_HASHES_PER_TX) { - throw new Error(`Data index should be less than ${MAX_NOTE_HASHES_PER_TX}.`); - } - if (txIndex >= TXS_PER_BLOCK) { - throw new Error(`Tx index should be less than ${TXS_PER_BLOCK}.`); - } - } - - async encrypt(): Promise { - const ephSk = GrumpkinScalar.random(); - const log = await this.logPayload.generatePayload(ephSk, this.recipient); - return log.toBuffer(); - } - - get indexWithinNoteHashTree(): bigint { - return BigInt( - (this.blockNumber - 1) * NUM_NOTE_HASHES_PER_BLOCK + this.txIndex * MAX_NOTE_HASHES_PER_TX + this.noteHashIndex, - ); - } - - get snippetOfNoteDao() { - const payload = L1NotePayload.fromIncomingBodyPlaintextContract( - this.logPayload.incomingBodyPlaintext, - this.logPayload.contractAddress, - )!; - return { - note: new Note(payload.privateNoteValues), - contractAddress: payload.contractAddress, - storageSlot: payload.storageSlot, - noteTypeId: payload.noteTypeId, - }; - } -} - async function computeSiloedTagForIndex( sender: { completeAddress: CompleteAddress; ivsk: Fq }, recipient: AztecAddress, @@ -167,14 +104,7 @@ describe('PXEOracleInterface', () => { for (const sender of senders) { const tag = await computeSiloedTagForIndex(sender, recipient.address, contractAddress, tagIndex); const blockNumber = 1; - const randomNote = new MockNoteRequest( - await getRandomNoteLogPayload(tag, contractAddress), - blockNumber, - 1, - 1, - recipient.address, - ); - const log = new TxScopedL2Log(TxHash.random(), 0, blockNumber, false, await randomNote.encrypt()); + const log = new TxScopedL2Log(TxHash.random(), 0, blockNumber, PrivateLog.random(tag)); logs[tag.toString()] = [log]; } // Accumulated logs intended for recipient: NUM_SENDERS @@ -183,9 +113,7 @@ describe('PXEOracleInterface', () => { // Compute the tag as sender (knowledge of preaddress and ivsk) const firstSender = senders[0]; const tag = await computeSiloedTagForIndex(firstSender, recipient.address, contractAddress, tagIndex); - const payload = await getRandomNoteLogPayload(tag, contractAddress); - const logData = (await payload.generatePayload(GrumpkinScalar.random(), recipient.address)).toBuffer(); - const log = new TxScopedL2Log(TxHash.random(), 1, 0, false, logData); + const log = new TxScopedL2Log(TxHash.random(), 1, 0, PrivateLog.random(tag)); logs[tag.toString()].push(log); // Accumulated logs intended for recipient: NUM_SENDERS + 1 @@ -195,14 +123,7 @@ describe('PXEOracleInterface', () => { const sender = senders[i]; const tag = await computeSiloedTagForIndex(sender, recipient.address, contractAddress, tagIndex + 1); const blockNumber = 2; - const randomNote = new MockNoteRequest( - await getRandomNoteLogPayload(tag, contractAddress), - blockNumber, - 1, - 1, - recipient.address, - ); - const log = new TxScopedL2Log(TxHash.random(), 0, blockNumber, false, await randomNote.encrypt()); + const log = new TxScopedL2Log(TxHash.random(), 0, blockNumber, PrivateLog.random(tag)); logs[tag.toString()] = [log]; } // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 @@ -215,14 +136,7 @@ describe('PXEOracleInterface', () => { const randomRecipient = await computeAddress(keys.publicKeys, partialAddress); const tag = await computeSiloedTagForIndex(sender, randomRecipient, contractAddress, tagIndex); const blockNumber = 3; - const randomNote = new MockNoteRequest( - await getRandomNoteLogPayload(tag, contractAddress), - blockNumber, - 1, - 1, - randomRecipient, - ); - const log = new TxScopedL2Log(TxHash.random(), 0, blockNumber, false, await randomNote.encrypt()); + const log = new TxScopedL2Log(TxHash.random(), 0, blockNumber, PrivateLog.random(tag)); logs[tag.toString()] = [log]; } // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 @@ -513,18 +427,23 @@ describe('PXEOracleInterface', () => { expect(syncedLogs.get(recipient.address.toString())).toHaveLength(NUM_SENDERS + 1); }); + // IMPORTANT: This test has a misleading name and behavior. When TxScopedL2Log was updated to work with + // PrivateLog and PublicLog types directly, the `isFromPublic` getter was initially left unimplemented. + // At that time, this test appeared to pass - not because it was correctly filtering logs by contract address, + // but because `isFromPublic` was returning undefined (falsey). As a result, the test is actually filtering + // based on the log type (public vs private) rather than the intended contract address check. it('should not sync public tagged logs with incorrect contract address', async () => { const logs: { [k: string]: TxScopedL2Log[] } = {}; const tag = await computeSiloedTagForIndex(senders[0], recipient.address, contractAddress, 0); // Create a public log with an address which doesn't match the tag - const logData = PublicLog.fromFields([ + const log = PublicLog.fromFields([ AztecAddress.fromNumber(2).toField(), Fr.ONE, tag, ...Array(PUBLIC_LOG_DATA_SIZE_IN_FIELDS - 2).fill(Fr.random()), - ]).toBuffer(); - const log = new TxScopedL2Log(TxHash.random(), 1, 0, true, logData); - logs[tag.toString()] = [log]; + ]); + const scopedLog = new TxScopedL2Log(TxHash.random(), 1, 0, log); + logs[tag.toString()] = [scopedLog]; aztecNode.getLogsByTags.mockImplementation(tags => { return Promise.resolve(tags.map(tag => logs[tag.toString()] ?? [])); }); @@ -535,10 +454,7 @@ describe('PXEOracleInterface', () => { }); }); - describe('Process notes', () => { - let addNotesSpy: any; - let getNotesSpy: any; - let removeNullifiedNotesSpy: any; + describe('Process logs', () => { let simulator: MockProxy; let runUnconstrainedSpy: any; @@ -567,103 +483,29 @@ describe('PXEOracleInterface', () => { await contractDataProvider.addContractArtifact(contractInstance.currentContractClassId, contractArtifact); contractAddress = contractInstance.address; - addNotesSpy = jest.spyOn(noteDataProvider, 'addNotes'); - getNotesSpy = jest.spyOn(noteDataProvider, 'getNotes'); - removeNullifiedNotesSpy = jest.spyOn(noteDataProvider, 'removeNullifiedNotes'); - removeNullifiedNotesSpy.mockImplementation(() => Promise.resolve([])); simulator = mock(); simulator.runUnconstrained.mockImplementation(() => Promise.resolve({})); runUnconstrainedSpy = jest.spyOn(simulator, 'runUnconstrained'); - }); - afterEach(() => { - addNotesSpy.mockReset(); - getNotesSpy.mockReset(); - removeNullifiedNotesSpy.mockReset(); - aztecNode.getTxEffect.mockReset(); + aztecNode.getTxEffect.mockResolvedValue(randomInBlock(await TxEffect.random())); }); - async function mockTaggedLogs(requests: MockNoteRequest[], nullifiers: number = 0) { - const txEffectsMap: { [k: string]: { noteHashes: Fr[]; txHash: TxHash; nullifiers: Fr[] } } = {}; - const taggedLogs: TxScopedL2Log[] = []; - const groupedByTx = requests.reduce<{ [i: number]: { [j: number]: MockNoteRequest[] } }>((acc, request) => { - if (!acc[request.blockNumber]) { - acc[request.blockNumber] = {}; - } - if (!acc[request.blockNumber][request.txIndex]) { - acc[request.blockNumber][request.txIndex] = []; - } - acc[request.blockNumber][request.txIndex].push(request); - return acc; - }, {}); - for (const blockNumberKey in groupedByTx) { - const blockNumber = parseInt(blockNumberKey); - for (const txIndexKey in groupedByTx[blockNumber]) { - const txIndex = parseInt(txIndexKey); - const requestsInTx = groupedByTx[blockNumber][txIndex]; - const maxNoteIndex = Math.max(...requestsInTx.map(request => request.noteHashIndex)); - const txHash = TxHash.random(); - for (const request of requestsInTx) { - if (!txEffectsMap[txHash.toString()]) { - txEffectsMap[txHash.toString()] = { - txHash, - nullifiers: [new Fr(txHash.hash.toBigInt() + 27n)], - noteHashes: Array(maxNoteIndex + 1) - .fill(0) - .map(() => Fr.random()), - }; - } - const dataStartIndex = - (request.blockNumber - 1) * NUM_NOTE_HASHES_PER_BLOCK + request.txIndex * MAX_NOTE_HASHES_PER_TX; - const taggedLog = new TxScopedL2Log(txHash, dataStartIndex, blockNumber, false, await request.encrypt()); - const note = request.snippetOfNoteDao.note; - const noteHash = await pedersenHash(note.items); - txEffectsMap[txHash.toString()].noteHashes[request.noteHashIndex] = noteHash; - taggedLogs.push(taggedLog); - } - } - } - - aztecNode.getTxEffect.mockImplementation(txHash => { - return Promise.resolve(randomInBlock(txEffectsMap[txHash.toString()] as TxEffect)); - }); - aztecNode.findNullifiersIndexesWithBlock.mockImplementation((_blockNumber, requestedNullifiers) => - Promise.resolve( - Array(requestedNullifiers.length - nullifiers) - .fill(undefined) - .concat(Array(nullifiers).fill({ data: 1n, l2BlockNumber: 1n, l2BlockHash: '0x' })), - ), - ); - return taggedLogs; + function mockTaggedLogs(numLogs: number) { + return Array(numLogs) + .fill(0) + .map(() => new TxScopedL2Log(TxHash.random(), 0, 0, PrivateLog.random(Fr.random()))); } - it('should call processLog on multiple notes', async () => { - const requests = [ - new MockNoteRequest(await getRandomNoteLogPayload(Fr.random(), contractAddress), 1, 1, 1, recipient.address), - new MockNoteRequest( - await getRandomNoteLogPayload(Fr.random(), contractAddress), - 2, - 3, - 0, - await AztecAddress.random(), - ), - new MockNoteRequest(await getRandomNoteLogPayload(Fr.random(), contractAddress), 6, 3, 2, recipient.address), - new MockNoteRequest( - await getRandomNoteLogPayload(Fr.random(), contractAddress), - 9, - 3, - 2, - await AztecAddress.random(), - ), - new MockNoteRequest(await getRandomNoteLogPayload(Fr.random(), contractAddress), 12, 3, 2, recipient.address), - ]; - - const taggedLogs = await mockTaggedLogs(requests); - - await pxeOracleInterface.processTaggedLogs(taggedLogs, recipient.address, simulator); + + it('should call processLog on multiple logs', async () => { + const numLogs = 3; + + const taggedLogs = mockTaggedLogs(numLogs); + + 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 - expect(runUnconstrainedSpy).toHaveBeenCalledTimes(3); + expect(runUnconstrainedSpy).toHaveBeenCalledTimes(numLogs); expect(runUnconstrainedSpy).toHaveBeenCalledWith( expect.anything(), contractAddress, @@ -671,42 +513,5 @@ describe('PXEOracleInterface', () => { [], ); }, 30_000); - - it('should not store notes that do not belong to us', async () => { - // Both notes should be ignored because the encryption keys do not belong to owner (they are random). - const requests = [ - new MockNoteRequest(await getRandomNoteLogPayload(), 2, 1, 1, await AztecAddress.random()), - new MockNoteRequest(await getRandomNoteLogPayload(), 2, 3, 0, await AztecAddress.random()), - ]; - - const taggedLogs = await mockTaggedLogs(requests); - - await pxeOracleInterface.processTaggedLogs(taggedLogs, recipient.address, simulator); - - expect(addNotesSpy).toHaveBeenCalledTimes(0); - }); - - it('should remove nullified notes', async () => { - const requests = [ - new MockNoteRequest(await getRandomNoteLogPayload(Fr.random(), contractAddress), 1, 1, 1, recipient.address), - new MockNoteRequest(await getRandomNoteLogPayload(Fr.random(), contractAddress), 6, 3, 2, recipient.address), - new MockNoteRequest(await getRandomNoteLogPayload(Fr.random(), contractAddress), 12, 3, 2, recipient.address), - ]; - - getNotesSpy.mockResolvedValueOnce( - Promise.resolve(requests.map(request => ({ siloedNullifier: Fr.random(), ...request.snippetOfNoteDao }))), - ); - let requestedNullifier; - aztecNode.findNullifiersIndexesWithBlock.mockImplementationOnce(async (_blockNumber, nullifiers) => { - const block = await L2Block.random(2); - requestedNullifier = await wrapInBlock(nullifiers[0], block); - return [await wrapInBlock(1n, await L2Block.random(2)), undefined, undefined]; - }); - - await pxeOracleInterface.removeNullifiedNotes(contractAddress); - - expect(removeNullifiedNotesSpy).toHaveBeenCalledTimes(1); - expect(removeNullifiedNotesSpy).toHaveBeenCalledWith([requestedNullifier], recipient.address); - }, 30_000); }); }); 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 bc0a6187b85b..95827081d0c3 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,14 +1,8 @@ -import { - type L1_TO_L2_MSG_TREE_HEIGHT, - MAX_NOTE_HASHES_PER_TX, - PRIVATE_LOG_SIZE_IN_FIELDS, - PUBLIC_LOG_DATA_SIZE_IN_FIELDS, -} from '@aztec/constants'; +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 { Fr } from '@aztec/foundation/fields'; +import { Fr, Point } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; -import { BufferReader } from '@aztec/foundation/serialize'; import type { KeyStore } from '@aztec/key-store'; import { AcirSimulator, @@ -31,7 +25,7 @@ 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, L1NotePayload, LogWithTxData, PrivateLog, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { IndexedTaggingSecret, LogWithTxData, 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'; @@ -595,78 +589,36 @@ export class PXEOracleInterface implements ExecutionDataProvider { return logsMap; } - /** - * Decrypts logs tagged for a recipient and returns them. - * @param scopedLogs - The logs to decrypt. - * @param recipient - The recipient of the logs. - * @returns The decrypted notes. - */ - async #decryptTaggedLogs(scopedLogs: TxScopedL2Log[], recipient: AztecAddress) { - const recipientCompleteAddress = await this.getCompleteAddress(recipient); - const ivskM = await this.keyStore.getMasterSecretKey( - recipientCompleteAddress.publicKeys.masterIncomingViewingPublicKey, - ); - const addressSecret = await computeAddressSecret(await recipientCompleteAddress.getPreaddress(), ivskM); - - // Since we could have notes with the same index for different txs, we need - // to keep track of them scoping by txHash - const excludedIndices: Map> = new Map(); - const decrypted = []; - - for (const scopedLog of scopedLogs) { - if (scopedLog.isFromPublic) { - throw new Error('Attempted to decrypt public log'); - } - - const payload = await L1NotePayload.decryptAsIncoming(PrivateLog.fromBuffer(scopedLog.logData), addressSecret); - - if (!payload) { - this.log.warn('Unable to decrypt tagged log - was it not meant for us?'); - continue; - } - - if (!excludedIndices.has(scopedLog.txHash.toString())) { - excludedIndices.set(scopedLog.txHash.toString(), new Set()); - } - - const plaintext = [payload.storageSlot, payload.noteTypeId.toField(), ...payload.privateNoteValues]; - - decrypted.push({ plaintext, txHash: scopedLog.txHash, contractAddress: payload.contractAddress }); - } - - return decrypted; - } - /** * Processes the tagged logs returned by syncTaggedLogs by decrypting them and storing them in the database. + * @param contractAddress - The address of the contract that the logs are tagged for. * @param logs - The logs to process. * @param recipient - The recipient of the logs. */ public async processTaggedLogs( + contractAddress: AztecAddress, logs: TxScopedL2Log[], recipient: AztecAddress, simulator?: AcirSimulator, ): Promise { - const decryptedLogs = await this.#decryptTaggedLogs(logs, recipient); + for (const scopedLog of logs) { + if (scopedLog.isFromPublic) { + throw new Error('Attempted to decrypt public log'); + } - // We've produced the full NoteDao, which we'd be able to simply insert into the database. However, this is - // only a temporary measure as we migrate from the PXE-driven discovery into the new contract-driven approach. We - // discard most of the work done up to this point and reconstruct the note plaintext to then hand over to the - // contract for further processing. - for (const decryptedLog of decryptedLogs) { // Log processing requires the note hashes in the tx in which the note was created. We are now assuming that the // note was included in the same block in which the log was delivered - note that partial notes will not work this // way. - const txEffect = await this.aztecNode.getTxEffect(decryptedLog.txHash); + const txEffect = await this.aztecNode.getTxEffect(scopedLog.txHash); if (!txEffect) { - throw new Error(`Could not find tx effect for tx hash ${decryptedLog.txHash}`); + throw new Error(`Could not find tx effect for tx hash ${scopedLog.txHash}`); } // This will trigger calls to the deliverNote oracle await this.callProcessLog( - decryptedLog.contractAddress, - decryptedLog.plaintext, - decryptedLog.txHash, + contractAddress, + scopedLog.log.toFields(), + scopedLog.txHash, txEffect.data.noteHashes, txEffect.data.nullifiers[0], recipient, @@ -786,28 +738,28 @@ export class PXEOracleInterface implements ExecutionDataProvider { ); } - const log = logsForTag[0]; + const scopedLog = logsForTag[0]; // getLogsByTag doesn't have all of the information that we need (notably note hashes and the first nullifier), so // we need to make a second call to the node for `getTxEffect`. // TODO(#9789): bundle this information in the `getLogsByTag` call. - const txEffect = await this.aztecNode.getTxEffect(log.txHash); + const txEffect = await this.aztecNode.getTxEffect(scopedLog.txHash); if (txEffect == undefined) { - throw new Error(`Unexpected: failed to retrieve tx effects for tx ${log.txHash} which is known to exist`); + throw new Error(`Unexpected: failed to retrieve tx effects for tx ${scopedLog.txHash} which is known to exist`); } - const reader = BufferReader.asReader(log.logData); - const logArray = reader.readArray(PUBLIC_LOG_DATA_SIZE_IN_FIELDS, Fr); - // Public logs always take up all available fields by padding with zeroes, and the length of the originally emitted // log is lost. Until this is improved, we simply remove all of the zero elements (which are expected to be at the // end). // TODO(#11636): use the actual log length. - const trimmedLog = logArray.filter(x => !x.isZero()); + const trimmedLog = scopedLog.log.toFields().filter(x => !x.isZero()); - return new LogWithTxData(trimmedLog, log.txHash.hash, txEffect.data.noteHashes, txEffect.data.nullifiers[0]); + return new LogWithTxData(trimmedLog, scopedLog.txHash.hash, txEffect.data.noteHashes, txEffect.data.nullifiers[0]); } + // TODO(#12553): nuke this as part of tackling that issue. This function is no longer unit tested as I had to remove + // it from pxe_oracle_interface.test.ts when moving decryption to Noir (at that point we could not get a hold of + // the decrypted note in the test as TS decryption no longer existed). public async removeNullifiedNotes(contractAddress: AztecAddress) { this.log.verbose('Searching for nullifiers of known notes', { contract: contractAddress }); @@ -837,7 +789,7 @@ export class PXEOracleInterface implements ExecutionDataProvider { async callProcessLog( contractAddress: AztecAddress, - logPlaintext: Fr[], + logCiphertext: Fr[], txHash: TxHash, noteHashes: Fr[], firstNullifier: Fr, @@ -862,7 +814,7 @@ export class PXEOracleInterface implements ExecutionDataProvider { type: FunctionType.UNCONSTRAINED, isStatic: artifact.isStatic, args: encodeArguments(artifact, [ - toBoundedVec(logPlaintext, PRIVATE_LOG_SIZE_IN_FIELDS), + toBoundedVec(logCiphertext, PRIVATE_LOG_SIZE_IN_FIELDS), txHash.toString(), toBoundedVec(noteHashes, MAX_NOTE_HASHES_PER_TX), firstNullifier, @@ -894,6 +846,16 @@ export class PXEOracleInterface implements ExecutionDataProvider { copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { return this.capsuleDataProvider.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries); } + + async getSharedSecret(address: AztecAddress, ephPk: Point): Promise { + // TODO(#12656): return an app-siloed secret + const recipientCompleteAddress = await this.getCompleteAddress(address); + const ivskM = await this.keyStore.getMasterSecretKey( + recipientCompleteAddress.publicKeys.masterIncomingViewingPublicKey, + ); + const addressSecret = await computeAddressSecret(await recipientCompleteAddress.getPreaddress(), ivskM); + return deriveEcdhSharedSecret(addressSecret, ephPk); + } } function toBoundedVec(array: Fr[], maxLength: number) { diff --git a/yarn-project/simulator/src/private/acvm/deserialize.ts b/yarn-project/simulator/src/private/acvm/deserialize.ts index f788ea12bf7b..fb970e304b9d 100644 --- a/yarn-project/simulator/src/private/acvm/deserialize.ts +++ b/yarn-project/simulator/src/private/acvm/deserialize.ts @@ -1,4 +1,5 @@ import { Fr } from '@aztec/foundation/fields'; +import { hexToBuffer } from '@aztec/foundation/string'; import type { ACVMField, ACVMWitness } from './acvm_types.js'; @@ -41,6 +42,24 @@ export function fromBoundedVec(storage: ACVMField[], length: ACVMField): Fr[] { return storage.slice(0, frToNumber(fromACVMField(length))).map(fromACVMField); } +/** + * Converts a Noir BoundedVec of unsigned integers into a Buffer. Note that BoundedVecs are structs, and therefore + * translated as two separate ACVMField values (an array and a single field). + * + * @param storage The array with the BoundedVec's storage (i.e. BoundedVec::storage()) + * @param length The length of the BoundedVec (i.e. BoundedVec::len()) + * @param uintBitSize If it's an array of Noir u8's, put `8`, etc. + * @returns A buffer containing the unsigned integers tightly packed + */ +export function fromUintBoundedVec(storage: ACVMField[], length: ACVMField, uintBitSize: number): Buffer { + if (uintBitSize % 8 !== 0) { + throw new Error(`u${uintBitSize} is not a supported type in Noir`); + } + const uintByteSize = uintBitSize / 8; + const boundedStorage = storage.slice(0, frToNumber(fromACVMField(length))); + return Buffer.concat(boundedStorage.map(str => hexToBuffer(str).subarray(-uintByteSize))); +} + /** * Transforms a witness map to its field elements. * @param witness - The witness to extract from. @@ -50,3 +69,17 @@ export function witnessMapToFields(witness: ACVMWitness): Fr[] { const sortedKeys = [...witness.keys()].sort((a, b) => a - b); return sortedKeys.map(key => witness.get(key)!).map(fromACVMField); } + +/** + * Converts an array of Noir unsigned integers to a single tightly-packed buffer. + * @param uintBitSize If it's an array of Noir u8's, put `8`, etc. + * @returns A buffer where each byte is correctly represented as a single byte in the buffer. + * Copy of the function in txe/src/util/encoding.ts. + */ +export function fromUintArray(obj: ACVMField[], uintBitSize: number): Buffer { + if (uintBitSize % 8 !== 0) { + throw new Error(`u${uintBitSize} is not a supported type in Noir`); + } + const uintByteSize = uintBitSize / 8; + return Buffer.concat(obj.map(str => hexToBuffer(str).slice(-uintByteSize))); +} diff --git a/yarn-project/simulator/src/private/acvm/oracle/oracle.ts b/yarn-project/simulator/src/private/acvm/oracle/oracle.ts index c0792338d1e8..ba1cdb767ea3 100644 --- a/yarn-project/simulator/src/private/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/private/acvm/oracle/oracle.ts @@ -1,12 +1,19 @@ -import { Fr } from '@aztec/foundation/fields'; +import { Fr, Point } from '@aztec/foundation/fields'; import { 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 type { ACVMField } from '../acvm_types.js'; -import { frToBoolean, frToNumber, fromACVMField, fromBoundedVec } from '../deserialize.js'; -import { toACVMField, toACVMFieldSingleOrArray } from '../serialize.js'; +import { + frToBoolean, + frToNumber, + fromACVMField, + fromBoundedVec, + fromUintArray, + fromUintBoundedVec, +} from '../deserialize.js'; +import { bufferToBoundedVec, toACVMField, toACVMFieldSingleOrArray } from '../serialize.js'; import type { TypedOracle } from './typed_oracle.js'; /** @@ -452,4 +459,31 @@ export class Oracle { frToNumber(fromACVMField(numEntries)), ); } + + async aes128Decrypt( + ciphertextBVecStorage: ACVMField[], + [ciphertextLength]: ACVMField[], + iv: ACVMField[], + symKey: ACVMField[], + ): Promise<(ACVMField | ACVMField[])[]> { + const ciphertext = fromUintBoundedVec(ciphertextBVecStorage, ciphertextLength, 8); + const ivBuffer = fromUintArray(iv, 8); + const symKeyBuffer = fromUintArray(symKey, 8); + + const plaintext = await this.typedOracle.aes128Decrypt(ciphertext, ivBuffer, symKeyBuffer); + return bufferToBoundedVec(plaintext, ciphertextBVecStorage.length); + } + + async getSharedSecret( + [address]: ACVMField[], + [ephPKField0]: ACVMField[], + [ephPKField1]: ACVMField[], + [ephPKField2]: ACVMField[], + ): Promise { + const secret = await this.typedOracle.getSharedSecret( + AztecAddress.fromField(fromACVMField(address)), + Point.fromFields([ephPKField0, ephPKField1, ephPKField2].map(fromACVMField)), + ); + return secret.toFields().map(toACVMField); + } } 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 a54019ec7a7b..a94d9af06970 100644 --- a/yarn-project/simulator/src/private/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/private/acvm/oracle/typed_oracle.ts @@ -1,5 +1,5 @@ import type { L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/constants'; -import { Fr } from '@aztec/foundation/fields'; +import { Fr, Point } from '@aztec/foundation/fields'; import type { FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { CompleteAddress, ContractInstance } from '@aztec/stdlib/contract'; @@ -256,4 +256,8 @@ export abstract class TypedOracle { aes128Decrypt(_ciphertext: Buffer, _iv: Buffer, _symKey: Buffer): Promise { return Promise.reject(new OracleMethodNotAvailableError('aes128Decrypt')); } + + getSharedSecret(_address: AztecAddress, _ephPk: Point): Promise { + return Promise.reject(new OracleMethodNotAvailableError('getSharedSecret')); + } } diff --git a/yarn-project/simulator/src/private/acvm/serialize.ts b/yarn-project/simulator/src/private/acvm/serialize.ts index 21552ad04e56..599a993e637b 100644 --- a/yarn-project/simulator/src/private/acvm/serialize.ts +++ b/yarn-project/simulator/src/private/acvm/serialize.ts @@ -58,3 +58,31 @@ export function toACVMWitness(witnessStartIndex: number, fields: Parameters()); } + +export function bufferToU8Array(buffer: Buffer): ACVMField[] { + return Array.from(buffer).map(byte => toACVMField(BigInt(byte))); +} + +export function bufferToBoundedVec(buffer: Buffer, maxLen: number): [ACVMField[], ACVMField] { + const u8Array = bufferToU8Array(buffer); + return arrayToBoundedVec(u8Array, maxLen); +} + +/** + * Converts a ForeignCallArray into a tuple which represents a nr BoundedVec. + * If the input array is shorter than the maxLen, it pads the result with zeros, + * so that nr can correctly coerce this result into a BoundedVec. + * @param array + * @param maxLen - the max length of the BoundedVec. + * @returns a tuple representing a BoundedVec. + */ +export function arrayToBoundedVec(array: ACVMField[], maxLen: number): [ACVMField[], ACVMField] { + if (array.length > maxLen) { + throw new Error(`Array of length ${array.length} larger than maxLen ${maxLen}`); + } + const lengthDiff = maxLen - array.length; + const zeroPaddingArray = Array(lengthDiff).fill(toACVMField(BigInt(0))); + const storage = array.concat(zeroPaddingArray); + const len = toACVMField(BigInt(array.length)); + return [storage, len]; +} diff --git a/yarn-project/simulator/src/private/execution_data_provider.ts b/yarn-project/simulator/src/private/execution_data_provider.ts index 0499ba695e36..45657109f9fa 100644 --- a/yarn-project/simulator/src/private/execution_data_provider.ts +++ b/yarn-project/simulator/src/private/execution_data_provider.ts @@ -1,4 +1,4 @@ -import type { Fr } from '@aztec/foundation/fields'; +import type { Fr, Point } from '@aztec/foundation/fields'; import type { FunctionArtifact, FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2Block } from '@aztec/stdlib/block'; @@ -239,10 +239,11 @@ export interface ExecutionDataProvider extends CommitmentsDBInterface { /** * Processes the tagged logs returned by syncTaggedLogs by decrypting them and storing them in the database. + * @param contractAddress - The address of the contract that emitted the logs. * @param logs - The logs to process. * @param recipient - The recipient of the logs. */ - processTaggedLogs(logs: TxScopedL2Log[], recipient: AztecAddress): Promise; + processTaggedLogs(contractAddress: AztecAddress, logs: TxScopedL2Log[], recipient: AztecAddress): Promise; /** * Delivers the preimage and metadata of a committed note so that it can be later requested via the `getNotes` @@ -320,4 +321,12 @@ export interface ExecutionDataProvider extends CommitmentsDBInterface { * @param numEntries - The number of entries to copy. */ copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise; + + /** + * Retrieves the shared secret for a given address and ephemeral public key. + * @param address - The address to get the secret for. + * @param ephPk - The ephemeral public key to get the secret for. + * @returns The secret for the given address. + */ + getSharedSecret(address: AztecAddress, ephPk: Point): Promise; } diff --git a/yarn-project/simulator/src/private/private_execution_oracle.ts b/yarn-project/simulator/src/private/private_execution_oracle.ts index d033e02176b2..59cdac3d0abb 100644 --- a/yarn-project/simulator/src/private/private_execution_oracle.ts +++ b/yarn-project/simulator/src/private/private_execution_oracle.ts @@ -606,7 +606,11 @@ export class PrivateExecutionOracle extends UnconstrainedExecutionOracle { this.scopes, ); for (const [recipient, taggedLogs] of taggedLogsByRecipient.entries()) { - await this.executionDataProvider.processTaggedLogs(taggedLogs, AztecAddress.fromString(recipient)); + await this.executionDataProvider.processTaggedLogs( + this.contractAddress, + taggedLogs, + AztecAddress.fromString(recipient), + ); } await this.executionDataProvider.removeNullifiedNotes(this.contractAddress); diff --git a/yarn-project/simulator/src/private/unconstrained_execution_oracle.ts b/yarn-project/simulator/src/private/unconstrained_execution_oracle.ts index 2bd39ca6f181..fbd50fa5ce5e 100644 --- a/yarn-project/simulator/src/private/unconstrained_execution_oracle.ts +++ b/yarn-project/simulator/src/private/unconstrained_execution_oracle.ts @@ -1,5 +1,5 @@ import { Aes128 } from '@aztec/foundation/crypto'; -import { Fr } from '@aztec/foundation/fields'; +import { Fr, Point } from '@aztec/foundation/fields'; import { applyStringFormatting, createLogger } from '@aztec/foundation/log'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -288,7 +288,11 @@ export class UnconstrainedExecutionOracle extends TypedOracle { ); for (const [recipient, taggedLogs] of taggedLogsByRecipient.entries()) { - await this.executionDataProvider.processTaggedLogs(taggedLogs, AztecAddress.fromString(recipient)); + await this.executionDataProvider.processTaggedLogs( + this.contractAddress, + taggedLogs, + AztecAddress.fromString(recipient), + ); } await this.executionDataProvider.removeNullifiedNotes(this.contractAddress); @@ -371,4 +375,8 @@ export class UnconstrainedExecutionOracle extends TypedOracle { const aes128 = new Aes128(); return aes128.decryptBufferCBC(ciphertext, iv, symKey); } + + public override getSharedSecret(address: AztecAddress, ephPk: Point): Promise { + return this.executionDataProvider.getSharedSecret(address, ephPk); + } } diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index bd94e0aaeb2e..06a52581b397 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -329,9 +329,9 @@ class MockArchiver implements ArchiverApi { getPrivateLogs(_from: number, _limit: number): Promise { return Promise.resolve([PrivateLog.random()]); } - getLogsByTags(tags: Fr[]): Promise { + async getLogsByTags(tags: Fr[]): Promise { expect(tags[0]).toBeInstanceOf(Fr); - return Promise.resolve([Array.from({ length: tags.length }, () => TxScopedL2Log.random())]); + return [await Promise.all(tags.map(() => TxScopedL2Log.random()))]; } async getPublicLogs(filter: LogFilter): Promise { expect(filter.txHash).toBeInstanceOf(TxHash); diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 30353f66da93..645f20ff28a8 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -561,10 +561,10 @@ class MockAztecNode implements AztecNode { expect(filter.contractAddress).toBeInstanceOf(AztecAddress); return Promise.resolve({ logs: [await ExtendedContractClassLog.random()], maxLogsHit: true }); } - getLogsByTags(tags: Fr[]): Promise { + async getLogsByTags(tags: Fr[]): Promise { expect(tags).toHaveLength(1); expect(tags[0]).toBeInstanceOf(Fr); - return Promise.resolve([[TxScopedL2Log.random()]]); + return [[await TxScopedL2Log.random()]]; } sendTx(tx: Tx): Promise { expect(tx).toBeInstanceOf(Tx); diff --git a/yarn-project/stdlib/src/interfaces/get_logs_response.test.ts b/yarn-project/stdlib/src/interfaces/get_logs_response.test.ts index bd30e6ead385..065b16d75069 100644 --- a/yarn-project/stdlib/src/interfaces/get_logs_response.test.ts +++ b/yarn-project/stdlib/src/interfaces/get_logs_response.test.ts @@ -3,8 +3,8 @@ import { jsonStringify } from '@aztec/foundation/json-rpc'; import { TxScopedL2Log } from '../logs/tx_scoped_l2_log.js'; describe('TxScopedL2Log', () => { - it('serializes to JSON', () => { - const log = TxScopedL2Log.random(); + it('serializes to JSON', async () => { + const log = await TxScopedL2Log.random(); expect(TxScopedL2Log.schema.parse(JSON.parse(jsonStringify(log)))).toEqual(log); }); }); diff --git a/yarn-project/stdlib/src/logs/l1_payload/index.ts b/yarn-project/stdlib/src/logs/l1_payload/index.ts index c01143fb9986..8506df7236f7 100644 --- a/yarn-project/stdlib/src/logs/l1_payload/index.ts +++ b/yarn-project/stdlib/src/logs/l1_payload/index.ts @@ -1,2 +1,2 @@ export * from './encrypted_log_payload.js'; -export * from './l1_note_payload.js'; +export * from './shared_secret_derivation.js'; diff --git a/yarn-project/stdlib/src/logs/l1_payload/l1_note_payload.ts b/yarn-project/stdlib/src/logs/l1_payload/l1_note_payload.ts deleted file mode 100644 index 4c9ab923d33f..000000000000 --- a/yarn-project/stdlib/src/logs/l1_payload/l1_note_payload.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { randomInt } from '@aztec/foundation/crypto'; -import { type Fq, Fr } from '@aztec/foundation/fields'; -import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; - -import { NoteSelector } from '../../abi/note_selector.js'; -import { AztecAddress } from '../../aztec-address/index.js'; -import { Vector } from '../../types/index.js'; -import type { PrivateLog } from '../private_log.js'; -import { EncryptedLogPayload } from './encrypted_log_payload.js'; - -/** - * A class which wraps note data which is pushed on L1. - * @remarks This data is required to compute a nullifier/to spend a note. Along with that this class contains - * the necessary functionality to encrypt and decrypt the data. - */ -export class L1NotePayload { - constructor( - /** - * Address of the contract this tx is interacting with. - */ - public contractAddress: AztecAddress, - /** - * Storage slot of the underlying note. - */ - public storageSlot: Fr, - /** - * Type identifier for the underlying note, required to determine how to compute its hash and nullifier. - */ - public noteTypeId: NoteSelector, - /** - * Note values delivered encrypted. - */ - public privateNoteValues: Fr[], - ) {} - - static fromIncomingBodyPlaintextContract( - plaintext: Buffer, - contractAddress: AztecAddress, - ): L1NotePayload | undefined { - try { - const reader = BufferReader.asReader(plaintext); - const fields = reader.readArray(plaintext.length / Fr.SIZE_IN_BYTES, Fr); - - const storageSlot = fields[0]; - const noteTypeId = NoteSelector.fromField(fields[1]); - - const privateNoteValues = fields.slice(2); - - return new L1NotePayload(contractAddress, storageSlot, noteTypeId, privateNoteValues); - } catch (e) { - return undefined; - } - } - - static async decryptAsIncoming(log: PrivateLog, sk: Fq): Promise { - const decryptedLog = await EncryptedLogPayload.decryptAsIncoming(log.fields, sk); - if (!decryptedLog) { - return undefined; - } - - return this.fromIncomingBodyPlaintextContract(decryptedLog.incomingBodyPlaintext, decryptedLog.contractAddress); - } - - /** - * Serializes the L1NotePayload object into a Buffer. - * @returns Buffer representation of the L1NotePayload object. - */ - toIncomingBodyPlaintext() { - const fields = [this.storageSlot, this.noteTypeId.toField(), ...this.privateNoteValues]; - return serializeToBuffer(fields); - } - - /** - * Create a random L1NotePayload object (useful for testing purposes). - * @param contract - The address of a contract the note was emitted from. - * @returns A random L1NotePayload object. - */ - static async random(contract?: AztecAddress) { - const numPrivateNoteValues = randomInt(2) + 1; - const privateNoteValues = Array.from({ length: numPrivateNoteValues }, () => Fr.random()); - - return new L1NotePayload( - contract ?? (await AztecAddress.random()), - Fr.random(), - NoteSelector.random(), - privateNoteValues, - ); - } - - public equals(other: L1NotePayload) { - return ( - this.contractAddress.equals(other.contractAddress) && - this.storageSlot.equals(other.storageSlot) && - this.noteTypeId.equals(other.noteTypeId) && - this.privateNoteValues.every((value, index) => value.equals(other.privateNoteValues[index])) - ); - } - - toBuffer() { - return serializeToBuffer( - this.contractAddress, - this.storageSlot, - this.noteTypeId, - new Vector(this.privateNoteValues), - ); - } - - static fromBuffer(buffer: Buffer | BufferReader) { - const reader = BufferReader.asReader(buffer); - return new L1NotePayload( - reader.readObject(AztecAddress), - reader.readObject(Fr), - reader.readObject(NoteSelector), - reader.readVector(Fr), - ); - } -} diff --git a/yarn-project/stdlib/src/logs/private_log.ts b/yarn-project/stdlib/src/logs/private_log.ts index 4e3247df2dd0..4a63358675c7 100644 --- a/yarn-project/stdlib/src/logs/private_log.ts +++ b/yarn-project/stdlib/src/logs/private_log.ts @@ -1,4 +1,4 @@ -import { PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; +import { PRIVATE_LOG_SIZE_IN_FIELDS, PUBLIC_LOG_DATA_SIZE_IN_FIELDS } from '@aztec/constants'; import { makeTuple } from '@aztec/foundation/array'; import { Fr } from '@aztec/foundation/fields'; import { schemas } from '@aztec/foundation/schemas'; @@ -38,8 +38,8 @@ export class PrivateLog { return new PrivateLog(reader.readArray(PRIVATE_LOG_SIZE_IN_FIELDS, Fr)); } - static random() { - return new PrivateLog(makeTuple(PRIVATE_LOG_SIZE_IN_FIELDS, Fr.random)); + static random(tag = Fr.random()) { + return PrivateLog.fromFields([tag, ...Array.from({ length: PRIVATE_LOG_SIZE_IN_FIELDS - 1 }, () => Fr.random())]); } getEmittedLength() { @@ -54,6 +54,12 @@ export class PrivateLog { } static get schema() { + if (PUBLIC_LOG_DATA_SIZE_IN_FIELDS + 1 == PRIVATE_LOG_SIZE_IN_FIELDS) { + throw new Error( + 'Constants got updated and schema for PublicLog matches that of PrivateLog. This needs to be updated now as Zod is no longer able to differentiate the 2 in TxScopedL2Log.', + ); + } + return z .object({ fields: z.array(schemas.Fr), @@ -70,4 +76,8 @@ export class PrivateLog { fields: [${this.fields.map(x => inspect(x)).join(', ')}], }`; } + + equals(other: PrivateLog) { + return this.fields.every((field, index) => field.equals(other.fields[index])); + } } diff --git a/yarn-project/stdlib/src/logs/public_log.ts b/yarn-project/stdlib/src/logs/public_log.ts index fda607b41820..59112bd5e51d 100644 --- a/yarn-project/stdlib/src/logs/public_log.ts +++ b/yarn-project/stdlib/src/logs/public_log.ts @@ -1,4 +1,8 @@ -import { PUBLIC_LOG_DATA_SIZE_IN_FIELDS, PUBLIC_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; +import { + PRIVATE_LOG_SIZE_IN_FIELDS, + PUBLIC_LOG_DATA_SIZE_IN_FIELDS, + PUBLIC_LOG_SIZE_IN_FIELDS, +} from '@aztec/constants'; import { type FieldsOf, makeTuple } from '@aztec/foundation/array'; import { Fr } from '@aztec/foundation/fields'; import { type ZodFor, schemas } from '@aztec/foundation/schemas'; @@ -78,6 +82,12 @@ export class PublicLog { } static get schema(): ZodFor { + if (PUBLIC_LOG_DATA_SIZE_IN_FIELDS + 1 == PRIVATE_LOG_SIZE_IN_FIELDS) { + throw new Error( + 'Constants got updated and schema for PrivateLog matches that of PublicLog. This needs to be updated now as Zod is no longer able to differentiate the 2 in TxScopedL2Log.', + ); + } + return z .object({ contractAddress: AztecAddress.schema, diff --git a/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts b/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts index 5cde328ba3f7..7b0df2d89a73 100644 --- a/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts +++ b/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts @@ -1,10 +1,10 @@ -import { Fr } from '@aztec/foundation/fields'; import { BufferReader, boolToBuffer, numToUInt32BE } from '@aztec/foundation/serialize'; import { z } from 'zod'; -import { schemas } from '../schemas/schemas.js'; import { TxHash } from '../tx/tx_hash.js'; +import { PrivateLog } from './private_log.js'; +import { PublicLog } from './public_log.js'; export class TxScopedL2Log { constructor( @@ -22,53 +22,55 @@ export class TxScopedL2Log { */ public blockNumber: number, /* - * Indicates if the log comes from the public logs stream (partial note) + * The log data as either a PrivateLog or PublicLog */ - public isFromPublic: boolean, - /* - * The log data - */ - public logData: Buffer, + public log: PrivateLog | PublicLog, ) {} + get isFromPublic() { + return this.log instanceof PublicLog; + } + static get schema() { return z .object({ txHash: TxHash.schema, dataStartIndexForTx: z.number(), blockNumber: z.number(), - isFromPublic: z.boolean(), - logData: schemas.Buffer, + log: z.union([PrivateLog.schema, PublicLog.schema]), }) .transform( - ({ txHash, dataStartIndexForTx, blockNumber, isFromPublic, logData }) => - new TxScopedL2Log(txHash, dataStartIndexForTx, blockNumber, isFromPublic, logData), + ({ txHash, dataStartIndexForTx, blockNumber, log }) => + new TxScopedL2Log(txHash, dataStartIndexForTx, blockNumber, log), ); } toBuffer() { + const isFromPublic = this.log instanceof PublicLog; return Buffer.concat([ this.txHash.toBuffer(), numToUInt32BE(this.dataStartIndexForTx), numToUInt32BE(this.blockNumber), - boolToBuffer(this.isFromPublic), - this.logData, + boolToBuffer(isFromPublic), + this.log.toBuffer(), ]); } static fromBuffer(buffer: Buffer) { const reader = BufferReader.asReader(buffer); - return new TxScopedL2Log( - reader.readObject(TxHash), - reader.readNumber(), - reader.readNumber(), - reader.readBoolean(), - reader.readToEnd(), - ); + const txHash = reader.readObject(TxHash); + const dataStartIndexForTx = reader.readNumber(); + const blockNumber = reader.readNumber(); + const isFromPublic = reader.readBoolean(); + const log = isFromPublic ? PublicLog.fromBuffer(reader) : PrivateLog.fromBuffer(reader); + + return new TxScopedL2Log(txHash, dataStartIndexForTx, blockNumber, log); } - static random() { - return new TxScopedL2Log(TxHash.random(), 1, 1, false, Fr.random().toBuffer()); + static async random() { + const isFromPublic = Math.random() < 0.5; + const log = isFromPublic ? await PublicLog.random() : PrivateLog.random(); + return new TxScopedL2Log(TxHash.random(), 1, 1, log); } equals(other: TxScopedL2Log) { @@ -76,8 +78,9 @@ export class TxScopedL2Log { this.txHash.equals(other.txHash) && this.dataStartIndexForTx === other.dataStartIndexForTx && this.blockNumber === other.blockNumber && - this.isFromPublic === other.isFromPublic && - this.logData.equals(other.logData) + ((this.log instanceof PublicLog && other.log instanceof PublicLog) || + (this.log instanceof PrivateLog && other.log instanceof PrivateLog)) && + this.log.equals(other.log as any) ); } } diff --git a/yarn-project/stdlib/src/tx/tx_effect.ts b/yarn-project/stdlib/src/tx/tx_effect.ts index 3f5762696132..19a651c0f9b1 100644 --- a/yarn-project/stdlib/src/tx/tx_effect.ts +++ b/yarn-project/stdlib/src/tx/tx_effect.ts @@ -245,7 +245,7 @@ export class TxEffect { makeTuple(MAX_L2_TO_L1_MSGS_PER_TX, Fr.random), makeTuple(MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, () => new PublicDataWrite(Fr.random(), Fr.random())), makeTuple(MAX_PRIVATE_LOGS_PER_TX, () => new PrivateLog(makeTuple(PRIVATE_LOG_SIZE_IN_FIELDS, Fr.random))), - await makeTupleAsync(numPublicCallsPerTx * numPublicLogsPerCall, PublicLog.random), + await makeTupleAsync(numPublicCallsPerTx * numPublicLogsPerCall, async () => await PublicLog.random()), await makeTupleAsync(MAX_CONTRACT_CLASS_LOGS_PER_TX, ContractClassLog.random), ); } diff --git a/yarn-project/txe/src/node/txe_node.ts b/yarn-project/txe/src/node/txe_node.ts index 747e4c74bafd..b372dc8bd7c0 100644 --- a/yarn-project/txe/src/node/txe_node.ts +++ b/yarn-project/txe/src/node/txe_node.ts @@ -186,13 +186,7 @@ export class TXENode implements AztecNode { this.#logger.verbose(`Found private log with tag ${tag.toString()} in block ${this.getBlockNumber()}`); const currentLogs = this.#logsByTags.get(tag.toString()) ?? []; - const scopedLog = new TxScopedL2Log( - new TxHash(new Fr(blockNumber)), - this.#noteIndex, - blockNumber, - false, - log.toBuffer(), - ); + const scopedLog = new TxScopedL2Log(new TxHash(new Fr(blockNumber)), this.#noteIndex, blockNumber, log); currentLogs.push(scopedLog); this.#logsByTags.set(tag.toString(), currentLogs); }); @@ -211,13 +205,7 @@ export class TXENode implements AztecNode { this.#logger.verbose(`Found public log with tag ${tag.toString()} in block ${this.getBlockNumber()}`); const currentLogs = this.#logsByTags.get(tag.toString()) ?? []; - const scopedLog = new TxScopedL2Log( - new TxHash(new Fr(blockNumber)), - this.#noteIndex, - blockNumber, - true, - log.toBuffer(), - ); + const scopedLog = new TxScopedL2Log(new TxHash(new Fr(blockNumber)), this.#noteIndex, blockNumber, log); currentLogs.push(scopedLog); this.#logsByTags.set(tag.toString(), currentLogs); diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index dea7ddd09e93..4c4e231b9865 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -12,7 +12,7 @@ import { } from '@aztec/constants'; import { padArrayEnd } from '@aztec/foundation/collection'; import { Aes128, Schnorr, poseidon2Hash } from '@aztec/foundation/crypto'; -import { Fr } from '@aztec/foundation/fields'; +import { Fr, Point } from '@aztec/foundation/fields'; import { type Logger, applyStringFormatting } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import { KeyStore } from '@aztec/key-store'; @@ -75,8 +75,13 @@ import { import type { MerkleTreeReadOperations, MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; import { type KeyValidationRequest, PrivateContextInputs } from '@aztec/stdlib/kernel'; import { deriveKeys } from '@aztec/stdlib/keys'; -import { ContractClassLog, LogWithTxData } from '@aztec/stdlib/logs'; -import { IndexedTaggingSecret, type PrivateLog, type PublicLog } from '@aztec/stdlib/logs'; +import { + ContractClassLog, + IndexedTaggingSecret, + LogWithTxData, + type PrivateLog, + type PublicLog, +} from '@aztec/stdlib/logs'; import type { NoteStatus } from '@aztec/stdlib/note'; import type { CircuitWitnessGenerationStats } from '@aztec/stdlib/stats'; import { @@ -1086,7 +1091,11 @@ export class TXE implements TypedOracle { ); for (const [recipient, taggedLogs] of taggedLogsByRecipient.entries()) { - await this.pxeOracleInterface.processTaggedLogs(taggedLogs, AztecAddress.fromString(recipient)); + await this.pxeOracleInterface.processTaggedLogs( + this.contractAddress, + taggedLogs, + AztecAddress.fromString(recipient), + ); } await this.pxeOracleInterface.removeNullifiedNotes(this.contractAddress); @@ -1247,4 +1256,8 @@ export class TXE implements TypedOracle { const aes128 = new Aes128(); return aes128.decryptBufferCBC(ciphertext, iv, symKey); } + + getSharedSecret(address: AztecAddress, ephPk: Point): Promise { + return this.pxeOracleInterface.getSharedSecret(address, ephPk); + } } diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 40c26e2479a1..41bee41fca7f 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -1,4 +1,4 @@ -import { type ContractInstanceWithAddress, Fr } from '@aztec/aztec.js'; +import { type ContractInstanceWithAddress, Fr, Point } 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'; @@ -24,6 +24,7 @@ import { fromArray, fromSingle, fromUintArray, + fromUintBoundedVec, toArray, toForeignCallResult, toSingle, @@ -604,15 +605,31 @@ export class TXEService { return toForeignCallResult([]); } - // TODO: I forgot to add a corresponding function here, when I introduced an oracle method to txe_oracle.ts. The compiler didn't throw an error, so it took me a while to learn of the existence of this file, and that I need to implement this function here. Isn't there a way to programmatically identify that this is missing, given the existence of a txe_oracle method? - async aes128Decrypt(ciphertext: ForeignCallArray, iv: ForeignCallArray, symKey: ForeignCallArray) { - const ciphertextBuffer = fromUintArray(ciphertext, 8); + // TODO: I forgot to add a corresponding function here, when I introduced an oracle method to txe_oracle.ts. + // The compiler didn't throw an error, so it took me a while to learn of the existence of this file, and that I need + // to implement this function here. Isn't there a way to programmatically identify that this is missing, given the + // existence of a txe_oracle method? + async aes128Decrypt( + ciphertextBVecStorage: ForeignCallArray, + ciphertextLength: ForeignCallSingle, + iv: ForeignCallArray, + symKey: ForeignCallArray, + ) { + const ciphertext = fromUintBoundedVec(ciphertextBVecStorage, ciphertextLength, 8); const ivBuffer = fromUintArray(iv, 8); const symKeyBuffer = fromUintArray(symKey, 8); - const plaintextBuffer = await this.typedOracle.aes128Decrypt(ciphertextBuffer, ivBuffer, symKeyBuffer); + const plaintextBuffer = await this.typedOracle.aes128Decrypt(ciphertext, ivBuffer, symKeyBuffer); + + return toForeignCallResult(arrayToBoundedVec(bufferToU8Array(plaintextBuffer), ciphertextBVecStorage.length)); + } - return toForeignCallResult(arrayToBoundedVec(bufferToU8Array(plaintextBuffer), ciphertextBuffer.length)); + async getSharedSecret(address: ForeignCallSingle, ephPk: ForeignCallArray) { + const secret = await this.typedOracle.getSharedSecret( + AztecAddress.fromField(fromSingle(address)), + Point.fromFields(fromArray(ephPk)), + ); + return toForeignCallResult([toArray(secret.toFields())]); } // AVM opcodes diff --git a/yarn-project/txe/src/util/encoding.ts b/yarn-project/txe/src/util/encoding.ts index 2a9c65cb41a0..e0b9e768e986 100644 --- a/yarn-project/txe/src/util/encoding.ts +++ b/yarn-project/txe/src/util/encoding.ts @@ -31,7 +31,7 @@ export function fromArray(obj: ForeignCallArray) { /** * Converts an array of Noir unsigned integers to a single tightly-packed buffer. * @param uintBitSize If it's an array of Noir u8's, put `8`, etc. - * @returns + * @returns A buffer where each byte is correctly represented as a single byte in the buffer. */ export function fromUintArray(obj: ForeignCallArray, uintBitSize: number): Buffer { if (uintBitSize % 8 !== 0) { @@ -41,6 +41,24 @@ export function fromUintArray(obj: ForeignCallArray, uintBitSize: number): Buffe return Buffer.concat(obj.map(str => hexToBuffer(str).slice(-uintByteSize))); } +/** + * Converts a Noir BoundedVec of unsigned integers into a Buffer. Note that BoundedVecs are structs, and therefore + * translated as two separate ForeignCallArray and ForeignCallSingle values (an array and a single field). + * + * @param storage The array with the BoundedVec's storage (i.e. BoundedVec::storage()) + * @param length The length of the BoundedVec (i.e. BoundedVec::len()) + * @param uintBitSize If it's an array of Noir u8's, put `8`, etc. + * @returns A buffer containing the unsigned integers tightly packed + */ +export function fromUintBoundedVec(storage: ForeignCallArray, length: ForeignCallSingle, uintBitSize: number): Buffer { + if (uintBitSize % 8 !== 0) { + throw new Error(`u${uintBitSize} is not a supported type in Noir`); + } + const uintByteSize = uintBitSize / 8; + const boundedStorage = storage.slice(0, fromSingle(length).toNumber()); + return Buffer.concat(boundedStorage.map(str => hexToBuffer(str).slice(-uintByteSize))); +} + export function toSingle(obj: Fr | AztecAddress): ForeignCallSingle { return obj.toString().slice(2); } From ecba8e73c717ffc6a1bb3cdb76a800fdb4436ae6 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 12 Mar 2025 19:45:32 +0000 Subject: [PATCH 02/20] cute imports --- .../default_aes128/note/encryption.nr | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr index 47bce16360b7..fd333d7702d1 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr @@ -1,32 +1,34 @@ use crate::{ - encrypted_logs::encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, + encrypted_logs::{ + encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, + log_assembly_strategies::default_aes128::{ + arithmetic_generics_utils::{ + get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, + }, + utils_to_nuke::{self, be_bytes_32_to_fields}, + }, + }, keys::{ ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, ephemeral::generate_ephemeral_key_pair, }, oracle::{ + aes128_decrypt::aes128_decrypt_oracle, notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, random::random_with_max_byte_size, + shared_secret::get_shared_secret, }, - utils::{array, bytes::{be_bytes_31_to_fields, get_random_bytes}, point::get_sign_of_point}, -}; -use crate::encrypted_logs::log_assembly_strategies::default_aes128::{ - arithmetic_generics_utils::{ - get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, + utils::{ + array, + bytes::{be_bytes_31_to_fields, fields_to_be_bytes_32, get_random_bytes}, + point::{get_sign_of_point, point_from_x_coord_and_sign}, }, - utils_to_nuke, -}; -use crate::encrypted_logs::log_assembly_strategies::default_aes128::utils_to_nuke::be_bytes_32_to_fields; -use crate::oracle::aes128_decrypt::aes128_decrypt_oracle; -use crate::oracle::shared_secret::get_shared_secret; -use crate::utils::bytes::fields_to_be_bytes_32; -use crate::utils::point::point_from_x_coord_and_sign; -use dep::protocol_types::{ - address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::ToField, }; +use protocol_types::{address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::ToField}; use std::aes128::aes128_encrypt; -pub(crate) global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 48; // contract_address (32) + ciphertext_length (2) + 16 bytes pkcs#7 AES padding. +// contract_address (32) + ciphertext_length (2) + 16 bytes pkcs#7 AES padding. +pub(crate) global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 48; global TAG_AND_EPH_PK_X_SIZE_IN_FIELDS: u32 = 2; global EPH_PK_SIGN_BYTE_SIZE_IN_BYTES: u32 = 1; @@ -289,12 +291,12 @@ pub unconstrained fn decrypt_log( } mod test { - use crate::keys::ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address; - use crate::test::helpers::test_environment::TestEnvironment; + use crate::{ + keys::ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, + test::helpers::test_environment::TestEnvironment, + }; use super::{decrypt_log, encrypt_log, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS}; - use dep::protocol_types::address::AztecAddress; - use dep::protocol_types::traits::FromField; - use protocol_types::traits::Serialize; + use protocol_types::{address::AztecAddress, traits::{FromField, Serialize}}; use std::test::OracleMock; #[test] From 80b4bce0d48891a1b9a6d10fcda847599d825fa7 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 13 Mar 2025 18:54:56 +0000 Subject: [PATCH 03/20] test fix --- .../aztec-nr/aztec/src/discovery/private_logs.nr | 7 +++++-- .../src/archiver/archiver_store_test_suite.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) 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 81de263f99dc..51ae8ad3edb0 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -23,7 +23,6 @@ use crate::discovery::{ }; use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::decrypt_log; use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; -use crate::encrypted_logs::log_assembly_strategies::default_aes128::utils_to_nuke::be_bytes_32_to_fields; pub global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; /// Partial notes have a maximum packed length of their private fields bound by extra content in their private log (i.e. @@ -92,7 +91,11 @@ pub unconstrained fn do_process_log( recipient, ); } else { - panic(f"Unknown log type id {log_type_id}"); + debug_log_format( + "Unknown log type id {0} (probably belonging to an event log)", + [log_type_id], + ); + // panic(f"Unknown log type id {log_type_id}"); } } diff --git a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts index 4b5b8796a71a..350bc2ebb09a 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -504,14 +504,14 @@ export function describeArchiverDataStore( [ expect.objectContaining({ blockNumber: 1, - logData: makePrivateLog(tags[0]).toBuffer(), + log: makePrivateLog(tags[0]), isFromPublic: false, }), ], [ expect.objectContaining({ blockNumber: 0, - logData: makePrivateLog(tags[1]).toBuffer(), + log: makePrivateLog(tags[1]), isFromPublic: false, }), ], @@ -528,12 +528,12 @@ export function describeArchiverDataStore( [ expect.objectContaining({ blockNumber: 0, - logData: makePrivateLog(tags[0]).toBuffer(), + log: makePrivateLog(tags[0]), isFromPublic: false, }), expect.objectContaining({ blockNumber: 0, - logData: makePublicLog(tags[0]).toBuffer(), + log: makePublicLog(tags[0]), isFromPublic: true, }), ], @@ -558,12 +558,12 @@ export function describeArchiverDataStore( [ expect.objectContaining({ blockNumber: 1, - logData: makePrivateLog(tags[0]).toBuffer(), + log: makePrivateLog(tags[0]), isFromPublic: false, }), expect.objectContaining({ blockNumber: newBlockNumber, - logData: newLog.toBuffer(), + log: newLog, isFromPublic: false, }), ], @@ -582,7 +582,7 @@ export function describeArchiverDataStore( [ expect.objectContaining({ blockNumber: 1, - logData: makePrivateLog(tags[1]).toBuffer(), + log: makePrivateLog(tags[1]), isFromPublic: false, }), ], From 597e5e64b7d0854c17d893cf06d2f4a4ba118693 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 13 Mar 2025 19:36:29 +0000 Subject: [PATCH 04/20] fixes --- .../default_aes128/event.nr | 7 +- .../default_aes128/mod.nr | 1 - .../default_aes128/note/encryption.nr | 25 +++-- .../default_aes128/note/note.nr | 2 +- .../default_aes128/utils_to_nuke.nr | 96 ------------------- yarn-project/stdlib/src/logs/private_log.ts | 4 - 6 files changed, 18 insertions(+), 117 deletions(-) delete mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils_to_nuke.nr 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 881cf7d6d783..939f090d1fba 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 @@ -18,7 +18,10 @@ use crate::{ notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, random::{get_random_bytes, random_with_max_byte_size}, }, - utils::{conversion::bytes_to_fields::bytes_to_fields, point::get_sign_of_point}, + utils::{ + conversion::{bytes_to_fields::bytes_to_fields, fields_to_bytes::fields_to_bytes}, + point::get_sign_of_point, + }, }; use dep::protocol_types::{ address::AztecAddress, @@ -75,7 +78,7 @@ where fields[i + 1] = serialized_event[i]; } - fields_to_be_bytes_32(fields) + fields_to_bytes(fields) } // Note: This function is basically a copy of ./note/encryption.nr::encrypt_log. TODO: Merge the functions once 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 acf7c9147c5c..eeda21a1875d 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 @@ -1,4 +1,3 @@ pub mod arithmetic_generics_utils; pub mod event; pub mod note; -pub mod utils_to_nuke; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr index fd333d7702d1..a24439e4ef12 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr @@ -1,11 +1,8 @@ use crate::{ encrypted_logs::{ encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, - log_assembly_strategies::default_aes128::{ - arithmetic_generics_utils::{ - get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, - }, - utils_to_nuke::{self, be_bytes_32_to_fields}, + log_assembly_strategies::default_aes128::arithmetic_generics_utils::{ + get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, }, }, keys::{ @@ -15,12 +12,15 @@ use crate::{ oracle::{ aes128_decrypt::aes128_decrypt_oracle, notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, - random::random_with_max_byte_size, + random::{get_random_bytes, random_with_max_byte_size}, shared_secret::get_shared_secret, }, utils::{ array, - bytes::{be_bytes_31_to_fields, fields_to_be_bytes_32, get_random_bytes}, + conversion::{ + bytes_to_fields::{bytes_from_fields, bytes_to_fields}, + fields_to_bytes::{fields_from_bytes, fields_to_bytes}, + }, point::{get_sign_of_point, point_from_x_coord_and_sign}, }, }; @@ -39,8 +39,7 @@ pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES: u32 = ( - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; // Each field of the original note log was serialized to 32 bytes. Below we convert the bytes back to fields. -pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS: u32 = - (PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES + 31) / 32; +pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS: u32 = PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES / 32; /// Computes an encrypted log using AES-128 encryption in CBC mode. /// @@ -82,7 +81,7 @@ pub fn encrypt_log( ) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] { // AES 128 operates on bytes, not fields, so we need to convert the fields to bytes. // (This process is then reversed when processing the log in `do_process_log`) - let plaintext_bytes = fields_to_be_bytes_32(plaintext); + let plaintext_bytes = fields_to_bytes(plaintext); // ***************************************************************************** // Compute the shared secret @@ -198,7 +197,7 @@ pub fn encrypt_log( // Convert bytes back to fields // ***************************************************************************** - let log_bytes_as_fields = be_bytes_31_to_fields(log_bytes); + let log_bytes_as_fields = bytes_to_fields(log_bytes); // ***************************************************************************** // Prepend / append fields, to create the final log @@ -246,7 +245,7 @@ pub unconstrained fn decrypt_log( ); // Convert the ciphertext represented as fields to a byte representation (its original format) - let log_ciphertext = utils_to_nuke::fields_to_be_bytes_31(log_ciphertext_fields); + let log_ciphertext = bytes_from_fields(log_ciphertext_fields); // First byte of the ciphertext represents the ephemeral public key sign let eph_pk_sign_bool = log_ciphertext.get(0) as bool; @@ -287,7 +286,7 @@ pub unconstrained fn decrypt_log( let log_plaintext_bytes = aes128_decrypt_oracle(ciphertext, iv, sym_key); // Each field of the original note log was serialized to 32 bytes so we convert the bytes back to fields. - be_bytes_32_to_fields(log_plaintext_bytes) + fields_from_bytes(log_plaintext_bytes) } mod test { diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr index bc661e440acd..e5132a5f2f5a 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr @@ -2,7 +2,7 @@ use crate::{ context::PrivateContext, encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::encrypt_log, note::{note_emission::NoteEmission, note_interface::NoteType}, - utils::bytes::fields_to_be_bytes_32, + utils::conversion::bytes_to_fields::bytes_to_fields, }; use dep::protocol_types::{ abis::note_hash::NoteHash, address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils_to_nuke.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils_to_nuke.nr deleted file mode 100644 index fb259f834e61..000000000000 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/utils_to_nuke.nr +++ /dev/null @@ -1,96 +0,0 @@ -// TODO: NUKE THIS FILE - COPIED HERE TO UNBLOCK MYSELF -pub fn be_bytes_31_to_fields( - bytes: BoundedVec, -) -> BoundedVec { - let mut fields = BoundedVec::new(); - - // There are "bytes.len() / 31" whole fields that can be populated. - for i in 0..bytes.len() / 31 { - let mut field = 0; - for j in 0..31 { - // Shift the existing value left by 8 bits and add the new byte - field = field * 256 + bytes.get(i * 31 + j) as Field; - } - fields.push(field); - } - - // Note: if 31 divides bytes.len(), then this loop does not execute. - // ceil(bytes.len()/31) - floor(bytes.len()/31) = 1, unless 31 divides bytes.len(), in which case it's 0. - for _ in 0..((bytes.len() + 30) / 31) - (bytes.len() / 31) { - let mut final_field = 0; - let final_field_idx = fields.len(); - let final_offset = final_field_idx * 31; - // bytes.len() - ((bytes.len() / 31) * 31) = bytes.len() - floor(bytes.len()/31) * 31 = the number of bytes - // to go in the final field. - for j in 0..bytes.len() - ((bytes.len() / 31) * 31) { - // Shift the existing value left by 8 bits and add the new byte - final_field = final_field * 256 + bytes.get(final_offset + j) as Field; - } - - fields.push(final_field); - } - - fields -} - -/// Converts an input BoundedVec of fields into a BoundedVec of bytes in big-endian order. -/// -/// Each input field must contain at most 31 bytes (this is constrained to be so). -/// Each field is converted into 31 big-endian bytes, and the resulting 31-byte chunks are concatenated -/// back together in the order of the original fields. -/// -/// This function is expected to be used along with `be_bytes_31_to_fields` to convert a BoundedVec of bytes into -/// a BoundedVec of fields and then back to bytes. -pub fn fields_to_be_bytes_31(fields: BoundedVec) -> BoundedVec { - let mut bytes = BoundedVec::new(); - - for i in 0..fields.len() { - let field = fields.get(i); - - // We expect that the field contains at most 31 bytes of information. - field.assert_max_bit_size::<248>(); - - // Now we can safely convert the field to 31 bytes. - let field_as_bytes: [u8; 31] = field.to_be_bytes(); - - for j in 0..31 { - bytes.push(field_as_bytes[j]); - } - } - - bytes -} - -pub fn be_bytes_32_to_fields( - bytes: BoundedVec, -) -> BoundedVec { - let mut fields = BoundedVec::new(); - - // There are "bytes.len() / 32" whole fields that can be populated. - for i in 0..bytes.len() / 32 { - let mut field = 0; - for j in 0..32 { - // Shift the existing value left by 8 bits and add the new byte - field = field * 256 + bytes.get(i * 32 + j) as Field; - } - fields.push(field); - } - - // Note: if 32 divides bytes.len(), then this loop does not execute. - // ceil(bytes.len()/32) - floor(bytes.len()/32) = 1, unless 32 divides bytes.len(), in which case it's 0. - for _ in 0..((bytes.len() + 31) / 32) - (bytes.len() / 32) { - let mut final_field = 0; - let final_field_idx = fields.len(); - let final_offset = final_field_idx * 32; - // bytes.len() - ((bytes.len() / 32) * 32) = bytes.len() - floor(bytes.len()/32) * 32 = the number of bytes - // to go in the final field. - for j in 0..bytes.len() - ((bytes.len() / 32) * 32) { - // Shift the existing value left by 8 bits and add the new byte - final_field = final_field * 256 + bytes.get(final_offset + j) as Field; - } - - fields.push(final_field); - } - - fields -} diff --git a/yarn-project/stdlib/src/logs/private_log.ts b/yarn-project/stdlib/src/logs/private_log.ts index 4a63358675c7..381c18849240 100644 --- a/yarn-project/stdlib/src/logs/private_log.ts +++ b/yarn-project/stdlib/src/logs/private_log.ts @@ -76,8 +76,4 @@ export class PrivateLog { fields: [${this.fields.map(x => inspect(x)).join(', ')}], }`; } - - equals(other: PrivateLog) { - return this.fields.every((field, index) => field.equals(other.fields[index])); - } } From 8837569f826dbb78079476a8c4ca5d6fdd1c5a39 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 13 Mar 2025 19:52:41 +0000 Subject: [PATCH 05/20] e2e_offchain_note_delivery test fix --- .../aztec/src/discovery/private_logs.nr | 4 +++- .../contracts/test_contract/src/main.nr | 18 ++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) 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 51ae8ad3edb0..b17a7c7c6daf 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -127,7 +127,9 @@ unconstrained fn destructure_log_plaintext( (storage_slot, note_type_id, log_type_id, log_payload) } -unconstrained fn process_private_note_log( +// This function was exposed only because of e2e_offchain_note_delivery test. This is not great, but it was the best +// thing to do at the time. +pub unconstrained fn process_private_note_log( contract_address: AztecAddress, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index 6d30ee2a573f..3bf1613e5f93 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -32,7 +32,7 @@ pub contract Test { use dep::aztec::{ deploy::deploy_contract as aztec_deploy_contract, - discovery::private_logs::do_process_log, + discovery::private_logs::process_private_note_log, event::event_interface::EventInterface, hash::{ArgsHasher, pedersen_hash}, history::note_inclusion::ProveNoteInclusion, @@ -461,23 +461,21 @@ pub contract Test { first_nullifier_in_tx: Field, recipient: AztecAddress, ) { - // do_process_log expects a standard aztec-nr encoded note, which has the following shape: - // [ storage_slot, note_type_id, ...packed_note ] - let note = TestNote::new(value); - let log_plaintext = BoundedVec::from_array(array_concat( - [Test::storage_layout().example_constant.slot, TestNote::get_id()], - note.pack(), - )); + let storage_slot = Test::storage_layout().example_constant.slot; + let note_type_id = TestNote::get_id(); + let log_payload = BoundedVec::from_array(note.pack()); - do_process_log( + process_private_note_log( contract_address, - log_plaintext, tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, _compute_note_hash_and_nullifier, + storage_slot, + note_type_id, + log_payload, ); } From 5ea301491e5d965d4698ec6a7df512f3a4733e9c Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 13 Mar 2025 21:40:17 +0000 Subject: [PATCH 06/20] fix --- .../token_blacklist_contract/src/main.nr | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr index b2ee256a44cf..29e9269b82ec 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr @@ -18,7 +18,7 @@ use dep::aztec::macros::aztec; pub contract TokenBlacklist { // Libs use dep::aztec::{ - discovery::private_logs::do_process_log, + discovery::private_logs::process_private_note_log, encrypted_logs::log_assembly_strategies::default_aes128::note::{ encode_and_encrypt_note, encode_and_encrypt_note_unconstrained, }, @@ -310,22 +310,21 @@ pub contract TokenBlacklist { ) { // docs:end:deliver_note_contract_method - // do_process_log expects a standard aztec-nr encoded note, which has the following shape: - // [ storage_slot, note_type_id, ...packed_note ] let note = TransparentNote::new(amount, secret_hash); - let log_plaintext = BoundedVec::from_array(array_concat( - [TokenBlacklist::storage_layout().pending_shields.slot, TransparentNote::get_id()], - note.pack(), - )); + let storage_slot = TokenBlacklist::storage_layout().pending_shields.slot; + let note_type_id = TransparentNote::get_id(); + let packed_note = BoundedVec::from_array(note.pack()); - do_process_log( + process_private_note_log( contract_address, - log_plaintext, tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, _compute_note_hash_and_nullifier, + storage_slot, + note_type_id, + packed_note, ); } } From 2124fb929f72c0b1526be60c479665893b9ffd74 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 13 Mar 2025 22:16:14 +0000 Subject: [PATCH 07/20] sf --- .../simulator/src/private/acvm/serialize.ts | 14 +++++++------- yarn-project/txe/src/util/encoding.ts | 17 ++++++++++------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/yarn-project/simulator/src/private/acvm/serialize.ts b/yarn-project/simulator/src/private/acvm/serialize.ts index 599a993e637b..b77352f57bdc 100644 --- a/yarn-project/simulator/src/private/acvm/serialize.ts +++ b/yarn-project/simulator/src/private/acvm/serialize.ts @@ -72,17 +72,17 @@ export function bufferToBoundedVec(buffer: Buffer, maxLen: number): [ACVMField[] * Converts a ForeignCallArray into a tuple which represents a nr BoundedVec. * If the input array is shorter than the maxLen, it pads the result with zeros, * so that nr can correctly coerce this result into a BoundedVec. - * @param array + * @param bVecStorage - The array underlying the BoundedVec. * @param maxLen - the max length of the BoundedVec. * @returns a tuple representing a BoundedVec. */ -export function arrayToBoundedVec(array: ACVMField[], maxLen: number): [ACVMField[], ACVMField] { - if (array.length > maxLen) { - throw new Error(`Array of length ${array.length} larger than maxLen ${maxLen}`); +export function arrayToBoundedVec(bVecStorage: ACVMField[], maxLen: number): [ACVMField[], ACVMField] { + if (bVecStorage.length > maxLen) { + throw new Error(`Array of length ${bVecStorage.length} larger than maxLen ${maxLen}`); } - const lengthDiff = maxLen - array.length; + const lengthDiff = maxLen - bVecStorage.length; const zeroPaddingArray = Array(lengthDiff).fill(toACVMField(BigInt(0))); - const storage = array.concat(zeroPaddingArray); - const len = toACVMField(BigInt(array.length)); + const storage = bVecStorage.concat(zeroPaddingArray); + const len = toACVMField(BigInt(bVecStorage.length)); return [storage, len]; } diff --git a/yarn-project/txe/src/util/encoding.ts b/yarn-project/txe/src/util/encoding.ts index e0b9e768e986..a97812a50bd7 100644 --- a/yarn-project/txe/src/util/encoding.ts +++ b/yarn-project/txe/src/util/encoding.ts @@ -79,21 +79,24 @@ export function bufferToU8Array(buffer: Buffer): ForeignCallArray { * Converts a ForeignCallArray into a tuple which represents a nr BoundedVec. * If the input array is shorter than the maxLen, it pads the result with zeros, * so that nr can correctly coerce this result into a BoundedVec. - * @param array + * @param bVecStorage - The array underlying the BoundedVec. * @param maxLen - the max length of the BoundedVec. * @returns a tuple representing a BoundedVec. */ -export function arrayToBoundedVec(array: ForeignCallArray, maxLen: number): [ForeignCallArray, ForeignCallSingle] { - if (array.length > maxLen) { - throw new Error(`Array of length ${array.length} larger than maxLen ${maxLen}`); +export function arrayToBoundedVec( + bVecStorage: ForeignCallArray, + maxLen: number, +): [ForeignCallArray, ForeignCallSingle] { + if (bVecStorage.length > maxLen) { + throw new Error(`Array of length ${bVecStorage.length} larger than maxLen ${maxLen}`); } - const lengthDiff = maxLen - array.length; + const lengthDiff = maxLen - bVecStorage.length; // We pad the array to the maxLen of the BoundedVec. const zeroPaddingArray = toArray(Array(lengthDiff).fill(new Fr(0))); // These variable names match with the BoundedVec members in nr: - const storage = array.concat(zeroPaddingArray); - const len = toSingle(new Fr(array.length)); + const storage = bVecStorage.concat(zeroPaddingArray); + const len = toSingle(new Fr(bVecStorage.length)); return [storage, len]; } From 0fc4a7029072a6caff0838dcf12bffac85a9804c Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 14 Mar 2025 15:13:16 +0000 Subject: [PATCH 08/20] moving non-oracle stuff to utils --- .../default_aes128/event.nr | 6 ++-- .../default_aes128/note/encryption.nr | 2 +- .../aztec-nr/aztec/src/oracle/random.nr | 30 ------------------ noir-projects/aztec-nr/aztec/src/utils/mod.nr | 1 + .../aztec-nr/aztec/src/utils/random.nr | 31 +++++++++++++++++++ 5 files changed, 35 insertions(+), 35 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/utils/random.nr 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 939f090d1fba..f6adc686f7ef 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 @@ -14,13 +14,11 @@ use crate::{ ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, ephemeral::generate_ephemeral_key_pair, }, - oracle::{ - notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, - random::{get_random_bytes, random_with_max_byte_size}, - }, + oracle::notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, utils::{ conversion::{bytes_to_fields::bytes_to_fields, fields_to_bytes::fields_to_bytes}, point::get_sign_of_point, + random::{get_random_bytes, random_with_max_byte_size}, }, }; use dep::protocol_types::{ diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr index a24439e4ef12..86b16aa6e910 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr @@ -12,7 +12,6 @@ use crate::{ oracle::{ aes128_decrypt::aes128_decrypt_oracle, notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, - random::{get_random_bytes, random_with_max_byte_size}, shared_secret::get_shared_secret, }, utils::{ @@ -22,6 +21,7 @@ use crate::{ fields_to_bytes::{fields_from_bytes, fields_to_bytes}, }, point::{get_sign_of_point, point_from_x_coord_and_sign}, + random::{get_random_bytes, random_with_max_byte_size}, }, }; use protocol_types::{address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::ToField}; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/random.nr b/noir-projects/aztec-nr/aztec/src/oracle/random.nr index c79f7a504cc6..e01affe2d1a2 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/random.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/random.nr @@ -6,35 +6,5 @@ pub unconstrained fn random() -> Field { rand_oracle() } -/// Returns as many random bytes as specified through N. -pub unconstrained fn get_random_bytes() -> [u8; N] { - let mut bytes = [0; N]; - let mut idx = 32; - let mut randomness = [0; 32]; - for i in 0..N { - if idx == 32 { - randomness = random().to_be_bytes(); - idx = 1; // Skip the first byte as it's always 0. - } - bytes[i] = randomness[idx]; - idx += 1; - } - bytes -} - -/// Just like `random`, but returns a field that fits in `N` bytes. -pub unconstrained fn random_with_max_byte_size() -> Field { - let random_field = rand_oracle(); - - // We convert the field to the desired number of bytes, and then back to a field - let random_field_as_bytes: [u8; N] = random_field.to_be_bytes(); - let reconstructed_field = Field::from_be_bytes::(random_field_as_bytes); - - // We assert that the max byte size is as expected - reconstructed_field.assert_max_bit_size::<8 * N>(); - - reconstructed_field -} - #[oracle(getRandomField)] unconstrained fn rand_oracle() -> Field {} diff --git a/noir-projects/aztec-nr/aztec/src/utils/mod.nr b/noir-projects/aztec-nr/aztec/src/utils/mod.nr index 4354d4b5cb86..9465f032e652 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/mod.nr @@ -3,6 +3,7 @@ pub mod comparison; pub mod conversion; pub mod field; pub mod point; +pub mod random; pub mod to_bytes; pub mod secrets; pub mod with_hash; diff --git a/noir-projects/aztec-nr/aztec/src/utils/random.nr b/noir-projects/aztec-nr/aztec/src/utils/random.nr new file mode 100644 index 000000000000..1fd9517e197b --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/utils/random.nr @@ -0,0 +1,31 @@ +use crate::oracle::random::random; + +/// Returns as many random bytes as specified through N. +pub unconstrained fn get_random_bytes() -> [u8; N] { + let mut bytes = [0; N]; + let mut idx = 32; + let mut randomness = [0; 32]; + for i in 0..N { + if idx == 32 { + randomness = random().to_be_bytes(); + idx = 1; // Skip the first byte as it's always 0. + } + bytes[i] = randomness[idx]; + idx += 1; + } + bytes +} + +/// Just like `random`, but returns a field that fits in `N` bytes. +pub unconstrained fn random_with_max_byte_size() -> Field { + let random_field = random(); + + // We convert the field to the desired number of bytes, and then back to a field + let random_field_as_bytes: [u8; N] = random_field.to_be_bytes(); + let reconstructed_field = Field::from_be_bytes::(random_field_as_bytes); + + // We assert that the max byte size is as expected + reconstructed_field.assert_max_bit_size::<8 * N>(); + + reconstructed_field +} From 1184bbb4e0d25664ea3aa8a02f835e1b68891d97 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 14 Mar 2025 16:45:58 +0000 Subject: [PATCH 09/20] linking a todo --- .../log_assembly_strategies/default_aes128/event.nr | 3 +++ .../log_assembly_strategies/default_aes128/note/encryption.nr | 3 +++ 2 files changed, 6 insertions(+) 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 f6adc686f7ef..bf24402ff29d 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 @@ -208,6 +208,9 @@ where // 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); // ***************************************************************************** diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr index 86b16aa6e910..b5cb28f84c51 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr @@ -197,6 +197,9 @@ pub fn encrypt_log( // Convert bytes back to fields // ***************************************************************************** + // 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); // ***************************************************************************** From 43b7c79069f4342a95f7265f569df1c495f257e5 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 14 Mar 2025 16:51:53 +0000 Subject: [PATCH 10/20] more TODOs --- .../log_assembly_strategies/default_aes128/event.nr | 1 + .../log_assembly_strategies/default_aes128/note/encryption.nr | 1 + noir-projects/aztec-nr/aztec/src/utils/random.nr | 1 + 3 files changed, 3 insertions(+) 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 bf24402ff29d..1185a46b67b6 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 @@ -239,6 +239,7 @@ where 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. diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr index b5cb28f84c51..949b5ba46ef5 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr @@ -228,6 +228,7 @@ pub fn encrypt_log( 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: randomness cannot be constrained. final_log[i] = unsafe { random_with_max_byte_size::<31>() }; } diff --git a/noir-projects/aztec-nr/aztec/src/utils/random.nr b/noir-projects/aztec-nr/aztec/src/utils/random.nr index 1fd9517e197b..3002a9941041 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/random.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/random.nr @@ -17,6 +17,7 @@ pub unconstrained fn get_random_bytes() -> [u8; N] { } /// Just like `random`, but returns a field that fits in `N` bytes. +/// TODO(#12749): Nuke this function. pub unconstrained fn random_with_max_byte_size() -> Field { let random_field = random(); From 6a55c086431293a8bdbcc0c8f2a551b1f72501eb Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 14 Mar 2025 16:59:38 +0000 Subject: [PATCH 11/20] nuking random_with_max_byte_size --- .../default_aes128/event.nr | 5 +++-- .../default_aes128/note/encryption.nr | 5 +++-- noir-projects/aztec-nr/aztec/src/utils/random.nr | 15 --------------- 3 files changed, 6 insertions(+), 19 deletions(-) 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 1185a46b67b6..f903b1598f31 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 @@ -18,7 +18,7 @@ use crate::{ utils::{ conversion::{bytes_to_fields::bytes_to_fields, fields_to_bytes::fields_to_bytes}, point::get_sign_of_point, - random::{get_random_bytes, random_with_max_byte_size}, + random::get_random_bytes, }, }; use dep::protocol_types::{ @@ -243,7 +243,8 @@ where // 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. - final_log[i] = unsafe { random_with_max_byte_size::<31>() }; + let field_bytes = unsafe { get_random_bytes::<31>() }; + final_log[i] = Field::from_be_bytes::<31>(field_bytes); } final_log diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr index 949b5ba46ef5..40a1ed1c1d9a 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr @@ -21,7 +21,7 @@ use crate::{ fields_to_bytes::{fields_from_bytes, fields_to_bytes}, }, point::{get_sign_of_point, point_from_x_coord_and_sign}, - random::{get_random_bytes, random_with_max_byte_size}, + random::get_random_bytes, }, }; use protocol_types::{address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::ToField}; @@ -230,7 +230,8 @@ pub fn encrypt_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: randomness cannot be constrained. - final_log[i] = unsafe { random_with_max_byte_size::<31>() }; + let field_bytes = unsafe { get_random_bytes::<31>() }; + final_log[i] = Field::from_be_bytes::<31>(field_bytes); } final_log diff --git a/noir-projects/aztec-nr/aztec/src/utils/random.nr b/noir-projects/aztec-nr/aztec/src/utils/random.nr index 3002a9941041..6cb65f361058 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/random.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/random.nr @@ -15,18 +15,3 @@ pub unconstrained fn get_random_bytes() -> [u8; N] { } bytes } - -/// Just like `random`, but returns a field that fits in `N` bytes. -/// TODO(#12749): Nuke this function. -pub unconstrained fn random_with_max_byte_size() -> Field { - let random_field = random(); - - // We convert the field to the desired number of bytes, and then back to a field - let random_field_as_bytes: [u8; N] = random_field.to_be_bytes(); - let reconstructed_field = Field::from_be_bytes::(random_field_as_bytes); - - // We assert that the max byte size is as expected - reconstructed_field.assert_max_bit_size::<8 * N>(); - - reconstructed_field -} From 18c3f8e0b47137b412f2ffffb07fa0c50ec45311 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 14 Mar 2025 17:03:51 +0000 Subject: [PATCH 12/20] more TODOs --- noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr | 1 + 1 file changed, 1 insertion(+) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr index d74d7e88dfa4..9633a3a028c7 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr @@ -4,6 +4,7 @@ use dep::protocol_types::{ traits::Deserialize, }; +// TODO(#12656): return an app-siloed secret + document this #[oracle(getSharedSecret)] pub unconstrained fn get_shared_secret_oracle( address: AztecAddress, From b0dfb2a9832bc785044a8da8ca217ddd9928eef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Fri, 14 Mar 2025 20:19:31 +0100 Subject: [PATCH 13/20] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolás Venturo --- noir-projects/aztec-nr/aztec/src/discovery/mod.nr | 1 + noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr index ff5b45f6e60c..0155e087b679 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr @@ -1,3 +1,4 @@ +// TODO(#12750): don't make this value assume we're using AES. use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; use dep::protocol_types::{address::AztecAddress, debug_log::debug_log}; 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 b17a7c7c6daf..d3180f15da24 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -22,6 +22,7 @@ use crate::discovery::{ }, }; use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::decrypt_log; +// TODO(#12750): don't make this value assume we're using AES. use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; pub global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; @@ -91,11 +92,11 @@ pub unconstrained fn do_process_log( recipient, ); } else { + // TODO(#11569): handle events debug_log_format( "Unknown log type id {0} (probably belonging to an event log)", [log_type_id], ); - // panic(f"Unknown log type id {log_type_id}"); } } @@ -127,8 +128,8 @@ unconstrained fn destructure_log_plaintext( (storage_slot, note_type_id, log_type_id, log_payload) } -// This function was exposed only because of e2e_offchain_note_delivery test. This is not great, but it was the best -// thing to do at the time. +/// Attempts discovery of a note given information about its contents and the transaction in which it is +/// suspected the note was created. pub unconstrained fn process_private_note_log( contract_address: AztecAddress, tx_hash: Field, From fc7f0a83a40c285220b42fe6bf83ceeed8b4dd86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Fri, 14 Mar 2025 20:20:26 +0100 Subject: [PATCH 14/20] Update noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolás Venturo --- .../log_assembly_strategies/default_aes128/note/note.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr index e5132a5f2f5a..1b833990e19f 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr @@ -127,7 +127,7 @@ where let mut fields = [0; N + 2]; fields[0] = storage_slot; - fields[1] = Note::get_id(); // TODO(#10952): The note type ID can be reduced to 7 bits + fields[1] = Note::get_id(); // Note that the note id only uses 7 bits of this field. for i in 0..packed_note.len() { fields[i + 2] = packed_note[i]; } From 69ea4b2db30beabb4b1d9f97bcbaafd7aea49bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Fri, 14 Mar 2025 20:20:49 +0100 Subject: [PATCH 15/20] Update noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolás Venturo --- noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr index 9633a3a028c7..1d1bdc741ea3 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr @@ -6,7 +6,7 @@ use dep::protocol_types::{ // TODO(#12656): return an app-siloed secret + document this #[oracle(getSharedSecret)] -pub unconstrained fn get_shared_secret_oracle( +unconstrained fn get_shared_secret_oracle( address: AztecAddress, ephPk: Point, ) -> [Field; POINT_LENGTH] {} From 538e90e8c04ffbf70e68e3f0868c8b3e5a18bae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Fri, 14 Mar 2025 20:21:17 +0100 Subject: [PATCH 16/20] Update noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolás Venturo --- noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr index 1d1bdc741ea3..cd35ab7f8669 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr @@ -11,6 +11,15 @@ unconstrained fn get_shared_secret_oracle( ephPk: Point, ) -> [Field; POINT_LENGTH] {} +/// Returns an app-siloed shared secret between `address` and someone who knows the secret key behind an +/// ephimeral public pkey `ephPk`. The app-siloing means that contracts cannot retrieve secrets that belong to +/// other contracts, and therefore cannot e.g. decrypt their messages. This is an important security consideration +/// given that both the `address` and `ephPk` are public information. +/// +/// The shared secret `S` is computed as: +/// `let S = (ivsk + h) * ephPk` +/// where `ivsk + h` is the 'preaddress' i.e. the preimage of the address, also callded the address secret. +/// TODO(#12656): app-silo this secret pub unconstrained fn get_shared_secret(address: AztecAddress, ephPk: Point) -> Point { let fields = get_shared_secret_oracle(address, ephPk); Point::deserialize(fields) From 8ce8276e5b12a70113d24622359f2531ddd8ad15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Fri, 14 Mar 2025 20:21:46 +0100 Subject: [PATCH 17/20] Update noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolás Venturo --- noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr index 53a57382a6d8..2b80c872d4a8 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr @@ -122,8 +122,8 @@ mod test { let mut bad_sym_key = sym_key; bad_sym_key[0] = 0; - // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's design to work - // with logs with unknown length. + // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to work + // with logs of unknown length. let ciphertext_bvec = BoundedVec::::from_array(ciphertext); let received_plaintext = aes128_decrypt_oracle(ciphertext_bvec, iv, bad_sym_key); From e1de1aacdb80b8be930db7977adb42cbc0bb57ca Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 14 Mar 2025 19:24:25 +0000 Subject: [PATCH 18/20] typo fix --- noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr index cd35ab7f8669..4462f4b04e2d 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr @@ -12,13 +12,13 @@ unconstrained fn get_shared_secret_oracle( ) -> [Field; POINT_LENGTH] {} /// Returns an app-siloed shared secret between `address` and someone who knows the secret key behind an -/// ephimeral public pkey `ephPk`. The app-siloing means that contracts cannot retrieve secrets that belong to +/// ephemeral public key `ephPk`. The app-siloing means that contracts cannot retrieve secrets that belong to /// other contracts, and therefore cannot e.g. decrypt their messages. This is an important security consideration /// given that both the `address` and `ephPk` are public information. /// /// The shared secret `S` is computed as: /// `let S = (ivsk + h) * ephPk` -/// where `ivsk + h` is the 'preaddress' i.e. the preimage of the address, also callded the address secret. +/// where `ivsk + h` is the 'preaddress' i.e. the preimage of the address, also called the address secret. /// TODO(#12656): app-silo this secret pub unconstrained fn get_shared_secret(address: AztecAddress, ephPk: Point) -> Point { let fields = get_shared_secret_oracle(address, ephPk); From f09095bf4b28096fd6878570680f8769a5c29cd7 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 14 Mar 2025 19:28:59 +0000 Subject: [PATCH 19/20] fixes after rebase --- .../log_assembly_strategies/default_aes128/note/encryption.nr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr index 40a1ed1c1d9a..b6970235f00e 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr @@ -229,7 +229,9 @@ pub fn encrypt_log( // 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: randomness cannot be constrained. + + // 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); } From 89abba916b457b9162639253bc71bdcf6c91c0e9 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 14 Mar 2025 19:33:28 +0000 Subject: [PATCH 20/20] process_private_note_log --> attempt_note_discovery --- noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr | 4 ++-- noir-projects/aztec-nr/aztec/src/macros/mod.nr | 2 +- .../noir-contracts/contracts/test_contract/src/main.nr | 4 ++-- .../contracts/token_blacklist_contract/src/main.nr | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) 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 d3180f15da24..5a36e2c18a80 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -70,7 +70,7 @@ pub unconstrained fn do_process_log( if log_type_id == 0 { debug_log("Processing private note log"); - process_private_note_log( + attempt_note_discovery( contract_address, tx_hash, unique_note_hashes_in_tx, @@ -130,7 +130,7 @@ unconstrained fn destructure_log_plaintext( /// Attempts discovery of a note given information about its contents and the transaction in which it is /// suspected the note was created. -pub unconstrained fn process_private_note_log( +pub unconstrained fn attempt_note_discovery( contract_address: AztecAddress, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 53c37cb92430..3c2fd7c460f2 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -222,7 +222,7 @@ comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() - /// /// The signature of this function notably matches the `aztec::discovery::ComputeNoteHashAndNullifier` type, /// and so it can be used to call functions from that module such as `discover_new_notes`, `do_process_log` - /// and `process_private_note_log`. + /// and `attempt_note_discovery`. /// /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index 3bf1613e5f93..ad2cb0f4015f 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -32,7 +32,7 @@ pub contract Test { use dep::aztec::{ deploy::deploy_contract as aztec_deploy_contract, - discovery::private_logs::process_private_note_log, + discovery::private_logs::attempt_note_discovery, event::event_interface::EventInterface, hash::{ArgsHasher, pedersen_hash}, history::note_inclusion::ProveNoteInclusion, @@ -466,7 +466,7 @@ pub contract Test { let note_type_id = TestNote::get_id(); let log_payload = BoundedVec::from_array(note.pack()); - process_private_note_log( + attempt_note_discovery( contract_address, tx_hash, unique_note_hashes_in_tx, diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr index 29e9269b82ec..f19053725d7a 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr @@ -18,7 +18,7 @@ use dep::aztec::macros::aztec; pub contract TokenBlacklist { // Libs use dep::aztec::{ - discovery::private_logs::process_private_note_log, + discovery::private_logs::attempt_note_discovery, encrypted_logs::log_assembly_strategies::default_aes128::note::{ encode_and_encrypt_note, encode_and_encrypt_note_unconstrained, }, @@ -315,7 +315,7 @@ pub contract TokenBlacklist { let note_type_id = TransparentNote::get_id(); let packed_note = BoundedVec::from_array(note.pack()); - process_private_note_log( + attempt_note_discovery( contract_address, tx_hash, unique_note_hashes_in_tx,