From 755c70ab55c768681349179e777bb0391c381420 Mon Sep 17 00:00:00 2001 From: Leila Wang Date: Thu, 31 Oct 2024 15:59:14 +0000 Subject: [PATCH] feat: fixed private log size (#9585) ### Description This PR will limit the private logs to have at most 8 fields. - For notes: 1 for storage slot, 1 for note type id, and 6 for custom fields. - For events: 1 for storage slot, 1 for event type id, 1 for address tag, and 5 for custom fields. This is to make it more difficult to figure out what a tx is about when all the logs are the same length. In the current codebase, the max number of custom fields is 4 for note, and 5 for event. We can adjust the limit to allow more custom fields if necessary. ### The implementation - Make all private logs (logs emitted from private: encrypted note logs and encrypted logs) the same size by extending the incoming cipher text to a fixed length: - [1 byte for the actual plaintext length][the actual plaintext][random bytes] - The length of the extended cipher text for event log is 1 field less, because we will append the address tag to it, which will then make it the same length as note log. - Remove the first byte of private logs, which used to indicate the number of public values. It was always 0 for private logs because only public logs (unencrypted logs) have public values. Note: the gate counts increases significantly because we are hashing more data. This is just temporary. We will stop hashing logs soon and propagate them to the databus. --------- Co-authored-by: sirasistant --- .../src/core/libraries/ConstantsGen.sol | 1 + .../encrypted_event_emission.nr | 13 +- .../encrypted_logs/encrypted_note_emission.nr | 11 +- .../aztec/src/encrypted_logs/payload.nr | 182 +++++++++++++++--- .../aztec-nr/aztec/src/macros/notes/mod.nr | 40 ++-- .../crates/types/src/constants.nr | 1 + .../l1_payload/encrypted_log_payload.test.ts | 27 ++- .../logs/l1_payload/encrypted_log_payload.ts | 61 +++++- .../src/logs/l1_payload/l1_note_payload.ts | 42 ++-- yarn-project/circuits.js/src/constants.gen.ts | 1 + .../src/note_processor/note_processor.test.ts | 4 +- .../pxe/src/note_processor/note_processor.ts | 9 +- 12 files changed, 291 insertions(+), 101 deletions(-) diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 8c42fe769d9..3c2ae9ebf1d 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -92,6 +92,7 @@ library Constants { uint256 internal constant FUNCTION_SELECTOR_NUM_BYTES = 4; uint256 internal constant INITIALIZATION_SLOT_SEPARATOR = 1000000000; uint256 internal constant INITIAL_L2_BLOCK_NUM = 1; + uint256 internal constant PRIVATE_LOG_SIZE_IN_BYTES = 576; uint256 internal constant BLOB_SIZE_IN_BYTES = 126976; uint256 internal constant ETHEREUM_SLOT_DURATION = 12; uint256 internal constant AZTEC_SLOT_DURATION = 24; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/encrypted_event_emission.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/encrypted_event_emission.nr index 920ef2ee5cc..c8e2bfe6ebe 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/encrypted_event_emission.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/encrypted_event_emission.nr @@ -2,7 +2,10 @@ use crate::{ context::PrivateContext, encrypted_logs::payload::compute_private_log_payload, event::event_interface::EventInterface, keys::getters::get_ovsk_app, oracle::random::random, }; -use dep::protocol_types::{address::AztecAddress, hash::sha256_to_field, public_keys::OvpkM}; +use dep::protocol_types::{ + address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_BYTES, hash::sha256_to_field, + public_keys::OvpkM, +}; /// Computes private event log payload and a log hash fn compute_payload_and_hash( @@ -13,22 +16,20 @@ fn compute_payload_and_hash( ovpk: OvpkM, recipient: AztecAddress, sender: AztecAddress, -) -> ([u8; 384 + N * 32], Field) +) -> ([u8; PRIVATE_LOG_SIZE_IN_BYTES], Field) where Event: EventInterface, { let contract_address: AztecAddress = context.this_address(); let plaintext = event.private_to_be_bytes(randomness); - // For event logs we never include public values prefix as there are never any public values - let encrypted_log: [u8; 384 + N * 32] = compute_private_log_payload( + let encrypted_log = compute_private_log_payload( contract_address, ovsk_app, ovpk, recipient, sender, plaintext, - false, ); let log_hash = sha256_to_field(encrypted_log); (encrypted_log, log_hash) @@ -41,7 +42,7 @@ unconstrained fn compute_payload_and_hash_unconstrained( ovpk: OvpkM, recipient: AztecAddress, sender: AztecAddress, -) -> ([u8; 384 + N * 32], Field) +) -> ([u8; PRIVATE_LOG_SIZE_IN_BYTES], Field) where Event: EventInterface, { diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/encrypted_note_emission.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/encrypted_note_emission.nr index 0faf7cf2ef0..2b7f58b41bb 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/encrypted_note_emission.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/encrypted_note_emission.nr @@ -5,7 +5,8 @@ use crate::{ note::{note_emission::NoteEmission, note_interface::NoteInterface}, }; use dep::protocol_types::{ - abis::note_hash::NoteHash, address::AztecAddress, hash::sha256_to_field, public_keys::OvpkM, + abis::note_hash::NoteHash, address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_BYTES, + hash::sha256_to_field, public_keys::OvpkM, }; /// Computes private note log payload and a log hash @@ -16,7 +17,7 @@ fn compute_payload_and_hash( ovpk: OvpkM, recipient: AztecAddress, sender: AztecAddress, -) -> (u32, [u8; 385 + N * 32], Field) +) -> (u32, [u8; PRIVATE_LOG_SIZE_IN_BYTES], Field) where Note: NoteInterface, { @@ -32,15 +33,13 @@ where let plaintext = note.to_be_bytes(storage_slot); - // For note logs we always include public values prefix - let encrypted_log: [u8; 385 + N * 32] = compute_private_log_payload( + let encrypted_log = compute_private_log_payload( contract_address, ovsk_app, ovpk, recipient, sender, plaintext, - true, ); let log_hash = sha256_to_field(encrypted_log); @@ -53,7 +52,7 @@ unconstrained fn compute_payload_and_hash_unconstrained( ovpk: OvpkM, recipient: AztecAddress, sender: AztecAddress, -) -> (u32, [u8; 385 + N * 32], Field) +) -> (u32, [u8; PRIVATE_LOG_SIZE_IN_BYTES], Field) where Note: NoteInterface, { diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/payload.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/payload.nr index 8e9d0001910..8f4d621c54d 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/payload.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/payload.nr @@ -1,6 +1,10 @@ use dep::protocol_types::{ - address::AztecAddress, constants::GENERATOR_INDEX__SYMMETRIC_KEY, - hash::poseidon2_hash_with_separator, point::Point, public_keys::OvpkM, scalar::Scalar, + address::AztecAddress, + constants::{GENERATOR_INDEX__SYMMETRIC_KEY, PRIVATE_LOG_SIZE_IN_BYTES}, + hash::poseidon2_hash_with_separator, + point::Point, + public_keys::OvpkM, + scalar::Scalar, }; use std::{ aes128::aes128_encrypt, embedded_curve_ops::fixed_base_scalar_mul as derive_public_key, @@ -14,14 +18,97 @@ use crate::{ }; use protocol_types::public_keys::AddressPoint; -fn compute_private_log_payload( +pub comptime global PRIVATE_LOG_OVERHEAD_IN_BYTES: u32 = 304; + +// 1 byte for storage slot, 1 byte for note type id, allowing 6 bytes for custom note fields. +global MAX_PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES: u32 = 8 * 32; + +global MAX_PRIVATE_EVENT_LOG_PLAINTEXT_SIZE_IN_BYTES: u32 = + MAX_PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES - 32; // Reserve 1 field for address tag. + +// PRIVATE_LOG_SIZE_IN_BYTES +// - PRIVATE_LOG_OVERHEAD_IN_BYTES, consisting of: +// - 32 bytes for incoming_tag +// - 32 bytes for eph_pk +// - 48 bytes for incoming_header +// - 48 bytes for outgoing_header +// - 144 bytes for outgoing_body +// - 16 + MAX_PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES for incoming_body, consisting of: +// - 1 byte for plaintext length +// - MAX_PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES for the actual plaintext and padded random values +// - 15 bytes for AES padding + +// Note: Update PRIVATE_LOG_SIZE_IN_BYTES in `constants.nr` if any of the above fields change. +// This value ideally should be set by the protocol, allowing users (or `aztec-nr`) to fit data within the defined size limits. +// Currently, we adjust this value as the structure changes, then update `constants.nr` to match. +// Once the structure is finalized with defined overhead and max note field sizes, this value will be fixed and should remain unaffected by further payload composition changes. + +pub fn compute_private_log_payload( + contract_address: AztecAddress, + ovsk_app: Field, + ovpk: OvpkM, + recipient: AztecAddress, + sender: AztecAddress, + plaintext: [u8; P], +) -> [u8; PRIVATE_LOG_SIZE_IN_BYTES] { + let extended_plaintext: [u8; MAX_PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES + 1] = + extend_private_log_plaintext(plaintext); + compute_encrypted_log( + contract_address, + ovsk_app, + ovpk, + recipient, + sender, + extended_plaintext, + ) +} + +pub fn compute_event_log_payload( + contract_address: AztecAddress, + ovsk_app: Field, + ovpk: OvpkM, + recipient: AztecAddress, + sender: AztecAddress, + plaintext: [u8; P], +) -> [u8; PRIVATE_LOG_SIZE_IN_BYTES] { + let extended_plaintext: [u8; MAX_PRIVATE_EVENT_LOG_PLAINTEXT_SIZE_IN_BYTES + 1] = + extend_private_log_plaintext(plaintext); + compute_encrypted_log( + contract_address, + ovsk_app, + ovpk, + recipient, + sender, + extended_plaintext, + ) +} + +pub fn compute_partial_public_log_payload( + contract_address: AztecAddress, + ovsk_app: Field, + ovpk: OvpkM, + recipient: AztecAddress, + sender: AztecAddress, + plaintext: [u8; P], +) -> [u8; M] { + let extended_plaintext: [u8; P + 1] = extend_private_log_plaintext(plaintext); + compute_encrypted_log( + contract_address, + ovsk_app, + ovpk, + recipient, + sender, + extended_plaintext, + ) +} + +fn compute_encrypted_log( contract_address: AztecAddress, ovsk_app: Field, ovpk: OvpkM, recipient: AztecAddress, sender: AztecAddress, plaintext: [u8; P], - include_public_values_prefix: bool, ) -> [u8; M] { let (eph_sk, eph_pk) = generate_ephemeral_key_pair(); @@ -35,30 +122,35 @@ fn compute_private_log_payload( let outgoing_body_ciphertext: [u8; 144] = compute_outgoing_body_ciphertext(recipient, fr_to_fq(ovsk_app), eph_sk, eph_pk); - // If we include the prefix for number of public values, we need to add 1 byte to the offset - let mut offset = if include_public_values_prefix { 1 } else { 0 }; + let mut encrypted_bytes = [0; M]; + let mut offset = 0; - let mut encrypted_bytes: [u8; M] = [0; M]; // @todo We ignore the tags for now + // incoming_tag offset += 32; + // eph_pk let eph_pk_bytes = point_to_bytes(eph_pk); for i in 0..32 { encrypted_bytes[offset + i] = eph_pk_bytes[i]; } - offset += 32; + + // incoming_header + // outgoing_header for i in 0..48 { encrypted_bytes[offset + i] = incoming_header_ciphertext[i]; encrypted_bytes[offset + 48 + i] = outgoing_header_ciphertext[i]; } - offset += 48 * 2; + + // outgoing_body for i in 0..144 { encrypted_bytes[offset + i] = outgoing_body_ciphertext[i]; } - offset += 144; + + // incoming_body // Then we fill in the rest as the incoming body ciphertext let size = M - offset; assert_eq(size, incoming_body_ciphertext.len(), "ciphertext length mismatch"); @@ -66,19 +158,35 @@ fn compute_private_log_payload( encrypted_bytes[offset + i] = incoming_body_ciphertext[i]; } - // Current unoptimized size of the encrypted log - // empty_prefix (1 byte) - // incoming_tag (32 bytes) - // outgoing_tag (32 bytes) - // eph_pk (32 bytes) - // incoming_header (48 bytes) - // outgoing_header (48 bytes) - // outgoing_body (144 bytes) - // incoming_body_fixed (64 bytes) - // incoming_body_variable (P + 16 bytes padding) encrypted_bytes } +// Prepend the plaintext length as the first byte, then copy the plaintext itself starting from the second byte. +// Fill the remaining bytes with random values to reach a fixed length of N. +fn extend_private_log_plaintext(plaintext: [u8; P]) -> [u8; N] { + let mut padded = unsafe { get_random_bytes() }; + padded[0] = P as u8; + for i in 0..P { + padded[i + 1] = plaintext[i]; + } + padded +} + +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 +} + /// Converts a base field element to scalar field element. /// This is fine because modulus of the base field is smaller than the modulus of the scalar field. fn fr_to_fq(r: Field) -> Scalar { @@ -167,7 +275,7 @@ pub fn compute_outgoing_body_ciphertext( mod test { use crate::encrypted_logs::payload::{ compute_incoming_body_ciphertext, compute_outgoing_body_ciphertext, - compute_private_log_payload, + compute_private_log_payload, MAX_PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES, }; use dep::protocol_types::{ address::AztecAddress, point::Point, public_keys::OvpkM, scalar::Scalar, @@ -178,7 +286,7 @@ mod test { #[test] unconstrained fn test_encrypted_log_matches_typescript() { - // All the values in this test were copied over from `tagged_log.test.ts` + // All the values in this test were copied over from `encrypted_log_payload.test.ts` let contract_address = AztecAddress::from_field( 0x10f48cd9eff7ae5b209c557c70de2e657ee79166868676b787e9417e19260e04, ); @@ -200,8 +308,13 @@ mod test { 101, 153, 0, 0, 16, 39, ]; + let randomness = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f; + let _ = OracleMock::mock("getRandomField").returns(randomness).times( + (MAX_PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES as u64 + 1 + 30) / 31, + ); + let eph_sk = 0x1358d15019d4639393d62b97e1588c095957ce74a1c32d6ec7d62fe6705d9538; - let _ = OracleMock::mock("getRandomField").returns(eph_sk); + let _ = OracleMock::mock("getRandomField").returns(eph_sk).times(1); let recipient = AztecAddress::from_field( 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, @@ -218,7 +331,6 @@ mod test { recipient, sender, plaintext, - false, ); // The following value was generated by `encrypted_log_payload.test.ts` @@ -239,13 +351,21 @@ mod test { 208, 176, 145, 50, 180, 152, 245, 55, 112, 40, 153, 180, 78, 54, 102, 119, 98, 56, 235, 246, 51, 179, 86, 45, 127, 18, 77, 187, 168, 41, 24, 232, 113, 149, 138, 148, 33, 143, 215, 150, 188, 105, 131, 254, 236, 199, 206, 56, 44, 130, 134, 29, 99, 254, 69, 153, - 146, 68, 234, 148, 148, 178, 38, 221, 182, 148, 178, 100, 13, 206, 0, 91, 71, 58, 207, - 26, 227, 190, 21, 143, 85, 138, 209, 202, 34, 142, 159, 121, 61, 9, 57, 2, 48, 162, 89, - 126, 14, 83, 173, 40, 247, 170, 154, 112, 12, 204, 48, 38, 7, 173, 108, 38, 234, 20, 16, - 115, 91, 106, 140, 121, 63, 99, 23, 247, 0, 148, 9, 163, 145, 43, 21, 238, 47, 40, 204, - 241, 124, 246, 201, 75, 114, 3, 1, 229, 197, 130, 109, 227, 158, 133, 188, 125, 179, - 220, 51, 170, 121, 175, 202, 243, 37, 103, 13, 27, 53, 157, 8, 177, 11, 208, 120, 64, - 211, 148, 201, 240, 56, + 146, 68, 234, 148, 148, 178, 38, 221, 182, 103, 252, 139, 7, 246, 132, 29, 232, 78, 102, + 126, 28, 136, 8, 219, 180, 162, 14, 62, 71, 118, 40, 147, 93, 87, 188, 231, 32, 93, 56, + 193, 194, 197, 120, 153, 164, 139, 114, 18, 149, 2, 226, 19, 170, 250, 249, 128, 56, + 236, 93, 14, 101, 115, 20, 173, 73, 192, 53, 229, 7, 23, 59, 11, 176, 9, 147, 175, 168, + 206, 48, 127, 126, 76, 51, 211, 66, 232, 16, 132, 243, 14, 196, 181, 118, 12, 71, 236, + 250, 253, 71, 249, 122, 30, 23, 23, 19, 89, 47, 193, 69, 240, 164, 34, 128, 110, 13, + 133, 198, 7, 165, 14, 31, 239, 210, 146, 78, 67, 86, 32, 159, 244, 214, 246, 121, 246, + 233, 252, 20, 131, 221, 28, 146, 222, 119, 222, 162, 250, 252, 189, 18, 147, 12, 142, + 177, 222, 178, 122, 248, 113, 197, 40, 199, 152, 251, 91, 81, 243, 25, 156, 241, 141, + 60, 12, 99, 103, 169, 97, 32, 112, 37, 244, 255, 126, 46, 114, 226, 113, 223, 249, 27, + 3, 31, 41, 233, 28, 8, 23, 84, 99, 25, 186, 65, 33, 9, 35, 74, 16, 52, 169, 48, 161, + 134, 233, 242, 136, 39, 162, 105, 205, 43, 253, 183, 36, 138, 186, 87, 31, 7, 248, 125, + 227, 193, 172, 155, 98, 33, 61, 186, 158, 241, 192, 23, 28, 186, 100, 222, 174, 19, 64, + 224, 113, 251, 143, 45, 152, 81, 67, 116, 16, 95, 189, 83, 31, 124, 39, 155, 142, 66, 0, + 120, 197, 221, 161, 62, 75, 192, 255, 186, 200, 10, 135, 7, ]; assert_eq(encrypted_log_from_typescript, log); } diff --git a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr index 3e9c6ce2347..0c10b6377c3 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr @@ -1,4 +1,8 @@ -use crate::{note::{note_getter_options::PropertySelector, note_header::NoteHeader}, prelude::Point}; +use crate::{ + encrypted_logs::payload::PRIVATE_LOG_OVERHEAD_IN_BYTES, + note::{note_getter_options::PropertySelector, note_header::NoteHeader}, + prelude::Point, +}; use protocol_types::meta::{flatten_to_fields, pack_from_fields}; use std::{ collections::umap::UHashMap, @@ -7,8 +11,6 @@ use std::{ }; comptime global NOTE_HEADER_TYPE = type_of(NoteHeader::empty()); -// The following is a fixed ciphertext overhead as defined by `compute_private_log_payload` -comptime global NOTE_CIPHERTEXT_OVERHEAD: u32 = 321; /// A map from note type to (note_struct_definition, serialized_note_length, note_type_id, fields). /// `fields` is an array of tuples where each tuple contains the name of the field/struct member (e.g. `amount` @@ -392,7 +394,7 @@ comptime fn generate_multi_scalar_mul( /// fn encrypt_log(self, context: &mut PrivateContext, recipient_keys: aztec::protocol_types::public_keys::PublicKeys, recipient: aztec::protocol_types::address::AztecAddress) -> [Field; 17] { /// let ovsk_app: Field = context.request_ovsk_app(recipient_keys.ovpk_m.hash()); /// -/// let encrypted_log_bytes: [u8; 513] = aztec::encrypted_logs::payload::compute_private_log_payload( +/// let encrypted_log_bytes: [u8; 513] = aztec::encrypted_logs::payload::compute_partial_public_log_payload( /// context.this_address(), /// ovsk_app, /// recipient_keys.ovpk_m, @@ -436,7 +438,10 @@ comptime fn generate_setup_payload( get_setup_log_plaintext_body(s, log_plaintext_length, indexed_nullable_fields); // Then we compute values for `encrypt_log(...)` function - let encrypted_log_byte_length = NOTE_CIPHERTEXT_OVERHEAD + log_plaintext_length; + let encrypted_log_byte_length = PRIVATE_LOG_OVERHEAD_IN_BYTES + + log_plaintext_length /* log_plaintext */ + + 1 /* log_plaintext_length */ + + 15 /* AES padding */; // Each field contains 31 bytes so the length in fields is computed as ceil(encrypted_log_byte_length / 31) // --> we achieve rouding by adding 30 and then dividing without remainder let encrypted_log_field_length = (encrypted_log_byte_length + 30) / 31; @@ -466,14 +471,13 @@ comptime fn generate_setup_payload( fn encrypt_log(self, context: &mut PrivateContext, ovpk: aztec::protocol_types::public_keys::OvpkM, recipient: aztec::protocol_types::address::AztecAddress, sender: aztec::protocol_types::address::AztecAddress) -> [Field; $encrypted_log_field_length] { let ovsk_app: Field = context.request_ovsk_app(ovpk.hash()); - let encrypted_log_bytes: [u8; $encrypted_log_byte_length] = aztec::encrypted_logs::payload::compute_private_log_payload( + let encrypted_log_bytes: [u8; $encrypted_log_byte_length] = aztec::encrypted_logs::payload::compute_partial_public_log_payload( context.this_address(), ovsk_app, ovpk, recipient, sender, self.log_plaintext, - true ); aztec::utils::bytes::bytes_to_fields(encrypted_log_bytes) @@ -657,11 +661,16 @@ comptime fn generate_finalization_payload( // Then we compute values for `encrypt_log(...)` function let setup_log_plaintext_length = indexed_fixed_fields.len() * 32 + 64; - let setup_log_byte_length = NOTE_CIPHERTEXT_OVERHEAD + setup_log_plaintext_length; + let setup_log_byte_length = PRIVATE_LOG_OVERHEAD_IN_BYTES + + setup_log_plaintext_length + + 1 /* log_plaintext_length */ + + 15 /* AES padding */; // Each field contains 31 bytes so the length in fields is computed as ceil(setup_log_byte_length / 31) // --> we achieve rouding by adding 30 and then dividing without remainder let setup_log_field_length = (setup_log_byte_length + 30) / 31; - let finalization_log_byte_length = setup_log_byte_length + public_values_length * 32; + let public_values_field_length = public_values_length * 32; + let finalization_log_byte_length = + 1 /* public_values_length */ + setup_log_byte_length + public_values_field_length; ( quote { @@ -721,22 +730,23 @@ comptime fn generate_finalization_payload( // We append the public value to the log and emit it as unencrypted log let mut finalization_log = [0; $finalization_log_byte_length]; + // Iterate over the partial log and copy it to the final log - for i in 1..setup_log.len() { - finalization_log[i] = setup_log[i]; + for i in 0..setup_log.len() { + finalization_log[i + 1] = setup_log[i]; } - // Now we populate the first byte with number of public values - finalization_log[0] = $public_values_length; - // Iterate over the public values and append them to the log for i in 0..$public_values_length { let public_value_bytes: [u8; 32] = self.public_values[i].to_be_bytes(); for j in 0..public_value_bytes.len() { - finalization_log[$setup_log_byte_length + i * 32 + j] = public_value_bytes[j]; + finalization_log[1 + $setup_log_byte_length + i * 32 + j] = public_value_bytes[j]; } } + // Populate the first byte with number of public values + finalization_log[0] = $public_values_length; + // We emit the finalization log via the unencrypted logs stream self.context.emit_unencrypted_log(finalization_log); diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index d132ebc7a8b..b15dd179f17 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -133,6 +133,7 @@ global FUNCTION_SELECTOR_NUM_BYTES: Field = 4; // to be large enough so that it's ensured that it doesn't collide with storage slots of other variables. global INITIALIZATION_SLOT_SEPARATOR: Field = 1000_000_000; global INITIAL_L2_BLOCK_NUM: Field = 1; +global PRIVATE_LOG_SIZE_IN_BYTES: u32 = 576; // This is currently defined by aztec-nr/aztec/src/encrypted_logs/payload.nr. See the comment there for how this value is calculated. global BLOB_SIZE_IN_BYTES: Field = 31 * 4096; global ETHEREUM_SLOT_DURATION: u32 = 12; // AZTEC_SLOT_DURATION should be a multiple of ETHEREUM_SLOT_DURATION diff --git a/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.test.ts index 04cc25cc25c..95275e90329 100644 --- a/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.test.ts +++ b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.test.ts @@ -2,6 +2,7 @@ import { AztecAddress, CompleteAddress, KeyValidationRequest, + PRIVATE_LOG_SIZE_IN_BYTES, computeAddressSecret, computeOvskApp, computePoint, @@ -62,7 +63,7 @@ describe('EncryptedLogPayload', () => { }); }); - it('outgoing ciphertest matches Noir', () => { + it('outgoing cipher text matches Noir', () => { const ephSk = GrumpkinScalar.fromHighLow( new Fr(0x000000000000000000000000000000000f096b423017226a18461115fa8d34bbn), new Fr(0x00000000000000000000000000000000d0d302ee245dfaf2807e604eec4715fen), @@ -122,14 +123,26 @@ describe('EncryptedLogPayload', () => { '0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c138af8799f2fba962549802469e12e3b7ba4c5f9c999c6421e05c73f45ec68481970dd8ce0250b677759dfc040f6edaf77c5827a7bcd425e66bcdec3fa7e59bc18dd22d6a4032eefe3a7a55703f583396596235f7c186e450c92981186ee74042e49e00996565114016a1a478309842ecbaf930fb716c3f498e7e10370631d7507f696b8b233de2c1935e43c793399586f532da5ff7c0356636a75acb862e964156e8a3e42bfca3663936ba98c7fd26386a14657c23b5f5146f1a94b6c4651542685ea16f17c580a7cc7c8ff2688dce9bde8bf1f50475f4c3281e1c33404ee0025f50db0733f719462b22eff03cec746bb9e3829ae3636c84fbccd2754b5a5a92087a5f41ccf94a03a2671cd341ba3264c45147e75d4ea96e3b1a58498550b89', ); - const encrypted = log.encrypt(ephSk, recipientCompleteAddress.address, ovKeys).toString('hex'); - expect(encrypted).toMatchInlineSnapshot( - `"00000000000000000000000000000000000000000000000000000000000000008d460c0e434d846ec1ea286e4090eb56376ff27bddc1aacae1d856549f701fa70577790aeabcc2d81ec8d0c99e7f5d2bf2f1452025dc777a178404f851d93de818923f85187871d99bdf95d695eff0a9e09ba15153fc9b4d224b6e1e71dfbdcaab06c09d5b3c749bfebe1c0407eccd04f51bbb59142680c8a091b97fc6cbcf61f6c2af9b8ebc8f78537ab23fd0c5e818e4d42d459d265adb77c2ef829bf68f87f2c47b478bb57ae7e41a07643f65c353083d557b94e31da4a2a13127498d2eb3f0346da5eed2e9bc245aaf022a954ed0b09132b498f537702899b44e3666776238ebf633b3562d7f124dbba82918e871958a94218fd796bc6983feecc7ce382c82861d63fe45999244ea9494b226ddb694b2640dce005b473acf1ae3be158f558ad1ca228e9f793d09390230a2597e0e53ad28f7aa9a700ccc302607ad6c26ea1410735b6a8c793f6317f7009409a3912b15ee2f28ccf17cf6c94b720301e5c5826de39e85bc7db3dc33aa79afcaf325670d1b359d08b10bd07840d394c9f038"`, + const fixedRand = (len: number) => { + // The random values in the noir test file after the overhead are [1, 2, ..., 31, 0, 1, 2, ..., 31]. + const offset = plaintext.length + 1; + return Buffer.from( + Array(len) + .fill(0) + .map((_, i) => 1 + ((offset + i) % 31)), + ); + }; + + const encrypted = log.encrypt(ephSk, recipientCompleteAddress.address, ovKeys, fixedRand); + expect(encrypted.length).toBe(PRIVATE_LOG_SIZE_IN_BYTES); + + const encryptedStr = encrypted.toString('hex'); + expect(encryptedStr).toMatchInlineSnapshot( + `"00000000000000000000000000000000000000000000000000000000000000008d460c0e434d846ec1ea286e4090eb56376ff27bddc1aacae1d856549f701fa70577790aeabcc2d81ec8d0c99e7f5d2bf2f1452025dc777a178404f851d93de818923f85187871d99bdf95d695eff0a9e09ba15153fc9b4d224b6e1e71dfbdcaab06c09d5b3c749bfebe1c0407eccd04f51bbb59142680c8a091b97fc6cbcf61f6c2af9b8ebc8f78537ab23fd0c5e818e4d42d459d265adb77c2ef829bf68f87f2c47b478bb57ae7e41a07643f65c353083d557b94e31da4a2a13127498d2eb3f0346da5eed2e9bc245aaf022a954ed0b09132b498f537702899b44e3666776238ebf633b3562d7f124dbba82918e871958a94218fd796bc6983feecc7ce382c82861d63fe45999244ea9494b226ddb667fc8b07f6841de84e667e1c8808dbb4a20e3e477628935d57bce7205d38c1c2c57899a48b72129502e213aafaf98038ec5d0e657314ad49c035e507173b0bb00993afa8ce307f7e4c33d342e81084f30ec4b5760c47ecfafd47f97a1e171713592fc145f0a422806e0d85c607a50e1fefd2924e4356209ff4d6f679f6e9fc1483dd1c92de77dea2fafcbd12930c8eb1deb27af871c528c798fb5b51f3199cf18d3c0c6367a961207025f4ff7e2e72e271dff91b031f29e91c0817546319ba412109234a1034a930a186e9f28827a269cd2bfdb7248aba571f07f87de3c1ac9b62213dba9ef1c0171cba64deae1340e071fb8f2d98514374105fbd531f7c279b8e420078c5dda13e4bc0ffbac80a8707"`, ); - const byteArrayString = `[${encrypted.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16))}]`; - // Run with AZTEC_GENERATE_TEST_DATA=1 to update noir test data + const byteArrayString = `[${encryptedStr.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16))}]`; updateInlineTestData( 'noir-projects/aztec-nr/aztec/src/encrypted_logs/payload.nr', 'encrypted_log_from_typescript', @@ -139,7 +152,7 @@ describe('EncryptedLogPayload', () => { const ivskM = new GrumpkinScalar(0x0d6e27b21c89a7632f7766e35cc280d43f75bea3898d7328400a5fefc804d462n); const addressSecret = computeAddressSecret(recipientCompleteAddress.getPreaddress(), ivskM); - const recreated = EncryptedLogPayload.decryptAsIncoming(Buffer.from(encrypted, 'hex'), addressSecret); + const recreated = EncryptedLogPayload.decryptAsIncoming(encrypted, addressSecret); expect(recreated?.toBuffer()).toEqual(log.toBuffer()); }); diff --git a/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.ts index e809c547c41..b5c03d26396 100644 --- a/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.ts +++ b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.ts @@ -4,13 +4,15 @@ import { GrumpkinScalar, type KeyValidationRequest, NotOnCurveError, + PRIVATE_LOG_SIZE_IN_BYTES, Point, type PublicKey, computeOvskApp, computePoint, derivePublicKeyFromSecretKey, } from '@aztec/circuits.js'; -import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { randomBytes } from '@aztec/foundation/crypto'; +import { BufferReader, numToUInt8, serializeToBuffer } from '@aztec/foundation/serialize'; import { decrypt, encrypt } from './encryption_util.js'; import { derivePoseidonAESSecret } from './shared_secret_derivation.js'; @@ -23,6 +25,15 @@ const HEADER_SIZE = 48; // 128 bytes for the secret key, address and public key, and 16 bytes padding to follow PKCS#7 const OUTGOING_BODY_SIZE = 144; +const ENCRYPTED_LOG_CIPHERTEXT_OVERHEAD_SIZE = + 32 /* incoming_tag */ + + 32 /* eph_pk */ + + HEADER_SIZE /* incoming_header */ + + HEADER_SIZE /* outgoing_header */ + + OUTGOING_BODY_SIZE; /* outgoing_body */ + +const INCOMING_BODY_SIZE = PRIVATE_LOG_SIZE_IN_BYTES - ENCRYPTED_LOG_CIPHERTEXT_OVERHEAD_SIZE; + /** * Encrypted log payload with a tag used for retrieval by clients. */ @@ -42,7 +53,12 @@ export class EncryptedLogPayload { public readonly incomingBodyPlaintext: Buffer, ) {} - public encrypt(ephSk: GrumpkinScalar, recipient: AztecAddress, ovKeys: KeyValidationRequest): Buffer { + public encrypt( + ephSk: GrumpkinScalar, + recipient: AztecAddress, + ovKeys: KeyValidationRequest, + rand: (len: number) => Buffer = randomBytes, + ): Buffer { const addressPoint = computePoint(recipient); const ephPk = derivePublicKeyFromSecretKey(ephSk); @@ -56,7 +72,6 @@ export class EncryptedLogPayload { throw new Error(`Invalid outgoing header size: ${outgoingHeaderCiphertext.length}`); } - const incomingBodyCiphertext = encrypt(this.incomingBodyPlaintext, ephSk, addressPoint); // The serialization of Fq is [high, low] check `outgoing_body.nr` const outgoingBodyPlaintext = serializeToBuffer(ephSk.hi, ephSk.lo, recipient, addressPoint.toCompressedBuffer()); const outgoingBodyCiphertext = encrypt( @@ -65,19 +80,42 @@ export class EncryptedLogPayload { ephPk, derivePoseidonAESSecret, ); - if (outgoingBodyCiphertext.length !== OUTGOING_BODY_SIZE) { throw new Error(`Invalid outgoing body size: ${outgoingBodyCiphertext.length}`); } - return serializeToBuffer( + const overhead = serializeToBuffer( this.tag, ephPk.toCompressedBuffer(), incomingHeaderCiphertext, outgoingHeaderCiphertext, outgoingBodyCiphertext, - incomingBodyCiphertext, ); + if (overhead.length !== ENCRYPTED_LOG_CIPHERTEXT_OVERHEAD_SIZE) { + throw new Error( + `Invalid ciphertext overhead size. Expected ${ENCRYPTED_LOG_CIPHERTEXT_OVERHEAD_SIZE}. Got ${overhead.length}.`, + ); + } + + const numPaddedBytes = + PRIVATE_LOG_SIZE_IN_BYTES - + ENCRYPTED_LOG_CIPHERTEXT_OVERHEAD_SIZE - + 1 /* 1 byte for this.incomingBodyPlaintext.length */ - + 15 /* aes padding */ - + this.incomingBodyPlaintext.length; + const paddedIncomingBodyPlaintextWithLength = Buffer.concat([ + numToUInt8(this.incomingBodyPlaintext.length), + this.incomingBodyPlaintext, + rand(numPaddedBytes), + ]); + const incomingBodyCiphertext = encrypt(paddedIncomingBodyPlaintextWithLength, ephSk, addressPoint); + if (incomingBodyCiphertext.length !== INCOMING_BODY_SIZE) { + throw new Error( + `Invalid incoming body size. Expected ${INCOMING_BODY_SIZE}. Got ${incomingBodyCiphertext.length}`, + ); + } + + return serializeToBuffer(overhead, incomingBodyCiphertext); } /** @@ -110,7 +148,10 @@ export class EncryptedLogPayload { reader.readBytes(OUTGOING_BODY_SIZE); // The incoming can be of variable size, so we read until the end - const incomingBodyPlaintext = decrypt(reader.readToEnd(), addressSecret, ephPk); + const ciphertext = reader.readToEnd(); + const decrypted = decrypt(ciphertext, addressSecret, ephPk); + const length = decrypted.readUint8(0); + const incomingBodyPlaintext = decrypted.subarray(1, 1 + length); return new EncryptedLogPayload(tag, AztecAddress.fromBuffer(incomingHeader), incomingBodyPlaintext); } catch (e: any) { @@ -173,8 +214,10 @@ export class EncryptedLogPayload { recipientAddressPoint = Point.fromCompressedBuffer(obReader.readBytes(Point.COMPRESSED_SIZE_IN_BYTES)); } - // Now we decrypt the incoming body using the ephSk and recipientAddressPoint - const incomingBody = decrypt(reader.readToEnd(), ephSk, recipientAddressPoint); + // Now we decrypt the incoming body using the ephSk and recipientIvpk + const decryptedIncomingBody = decrypt(reader.readToEnd(), ephSk, recipientAddressPoint); + const length = decryptedIncomingBody.readUint8(0); + const incomingBody = decryptedIncomingBody.subarray(1, 1 + length); return new EncryptedLogPayload(tag, contractAddress, incomingBody); } catch (e: any) { diff --git a/yarn-project/circuit-types/src/logs/l1_payload/l1_note_payload.ts b/yarn-project/circuit-types/src/logs/l1_payload/l1_note_payload.ts index 122f81a07ec..b92f9be282f 100644 --- a/yarn-project/circuit-types/src/logs/l1_payload/l1_note_payload.ts +++ b/yarn-project/circuit-types/src/logs/l1_payload/l1_note_payload.ts @@ -4,7 +4,6 @@ import { randomInt } from '@aztec/foundation/crypto'; import { type Fq, Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; -import { EncryptedL2NoteLog } from '../encrypted_l2_note_log.js'; import { EncryptedLogPayload } from './encrypted_log_payload.js'; /** @@ -60,9 +59,9 @@ export class L1NotePayload { } } - static decryptAsIncoming(log: Buffer, sk: Fq): L1NotePayload | undefined { - const { publicValues, encryptedLog } = parseLog(log); - const decryptedLog = EncryptedLogPayload.decryptAsIncoming(encryptedLog.data, sk); + static decryptAsIncoming(log: Buffer, sk: Fq, isFromPublic = false): L1NotePayload | undefined { + const { publicValues, encryptedLog } = parseLog(log, isFromPublic); + const decryptedLog = EncryptedLogPayload.decryptAsIncoming(encryptedLog, sk); if (!decryptedLog) { return undefined; } @@ -74,9 +73,9 @@ export class L1NotePayload { ); } - static decryptAsOutgoing(log: Buffer, sk: Fq): L1NotePayload | undefined { - const { publicValues, encryptedLog } = parseLog(log); - const decryptedLog = EncryptedLogPayload.decryptAsOutgoing(encryptedLog.data, sk); + static decryptAsOutgoing(log: Buffer, sk: Fq, isFromPublic = false): L1NotePayload | undefined { + const { publicValues, encryptedLog } = parseLog(log, isFromPublic); + const decryptedLog = EncryptedLogPayload.decryptAsOutgoing(encryptedLog, sk); if (!decryptedLog) { return undefined; } @@ -103,10 +102,10 @@ export class L1NotePayload { * @returns A random L1NotePayload object. */ static random(contract = AztecAddress.random()) { - const numPrivateNoteValues = randomInt(10) + 1; + const numPrivateNoteValues = randomInt(2) + 1; const privateNoteValues = Array.from({ length: numPrivateNoteValues }, () => Fr.random()); - const numPublicNoteValues = randomInt(10) + 1; + const numPublicNoteValues = randomInt(2) + 1; const publicNoteValues = Array.from({ length: numPublicNoteValues }, () => Fr.random()); return new L1NotePayload(contract, Fr.random(), NoteSelector.random(), privateNoteValues, publicNoteValues); @@ -150,20 +149,20 @@ export class L1NotePayload { * @param log - Log to be parsed. * @returns An object containing the public values and the encrypted log. */ -function parseLog(log: Buffer) { +function parseLog(log: Buffer, isFromPublic: boolean) { // First we remove padding bytes - const processedLog = removePaddingBytes(log); + const processedLog = isFromPublic ? removePaddingBytes(log) : log; const reader = new BufferReader(processedLog); // Then we extract public values from the log - const numPublicValues = reader.readUInt8(); + const numPublicValues = isFromPublic ? reader.readUInt8() : 0; const publicValuesLength = numPublicValues * Fr.SIZE_IN_BYTES; const encryptedLogLength = reader.remainingBytes() - publicValuesLength; // Now we get the buffer corresponding to the encrypted log - const encryptedLog = new EncryptedL2NoteLog(reader.readBytes(encryptedLogLength)); + const encryptedLog = reader.readBytes(encryptedLogLength); // At last we load the public values const publicValues = reader.readArray(numPublicValues, Fr); @@ -180,16 +179,15 @@ function parseLog(log: Buffer) { function removePaddingBytes(unprocessedLog: Buffer) { // Determine whether first 31 bytes of each 32 bytes block of bytes are 0 const is1FieldPerByte = unprocessedLog.every((byte, index) => index % 32 === 31 || byte === 0); + if (!is1FieldPerByte) { + return unprocessedLog; + } - if (is1FieldPerByte) { - // We take every 32nd byte from the log and return the result - const processedLog = Buffer.alloc(unprocessedLog.length / 32); - for (let i = 0; i < processedLog.length; i++) { - processedLog[i] = unprocessedLog[31 + i * 32]; - } - - return processedLog; + // We take every 32nd byte from the log and return the result + const processedLog = Buffer.alloc(unprocessedLog.length / 32); + for (let i = 0; i < processedLog.length; i++) { + processedLog[i] = unprocessedLog[31 + i * 32]; } - return unprocessedLog; + return processedLog; } diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index bc7b31cc912..b0cd1aecdc2 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -79,6 +79,7 @@ export const PRIVATE_KERNEL_RESET_INDEX = 20; export const FUNCTION_SELECTOR_NUM_BYTES = 4; export const INITIALIZATION_SLOT_SEPARATOR = 1000000000; export const INITIAL_L2_BLOCK_NUM = 1; +export const PRIVATE_LOG_SIZE_IN_BYTES = 576; export const BLOB_SIZE_IN_BYTES = 126976; export const ETHEREUM_SLOT_DURATION = 12; export const AZTEC_SLOT_DURATION = 24; diff --git a/yarn-project/pxe/src/note_processor/note_processor.test.ts b/yarn-project/pxe/src/note_processor/note_processor.test.ts index 1ff8b41df33..a5d7c77f0d8 100644 --- a/yarn-project/pxe/src/note_processor/note_processor.test.ts +++ b/yarn-project/pxe/src/note_processor/note_processor.test.ts @@ -64,9 +64,7 @@ class MockNoteRequest { encrypt(): EncryptedL2NoteLog { const ephSk = GrumpkinScalar.random(); - const logWithoutNumPublicValues = this.logPayload.encrypt(ephSk, this.recipient, this.ovKeys); - // We prefix the log with an empty byte indicating there are 0 public values. - const log = Buffer.concat([Buffer.alloc(1), logWithoutNumPublicValues]); + const log = this.logPayload.encrypt(ephSk, this.recipient, this.ovKeys); return new EncryptedL2NoteLog(log); } diff --git a/yarn-project/pxe/src/note_processor/note_processor.ts b/yarn-project/pxe/src/note_processor/note_processor.ts index 3540cc9e596..128b58b8104 100644 --- a/yarn-project/pxe/src/note_processor/note_processor.ts +++ b/yarn-project/pxe/src/note_processor/note_processor.ts @@ -147,11 +147,16 @@ export class NoteProcessor { // We iterate over both encrypted and unencrypted logs to decrypt the notes since partial notes are passed // via the unencrypted logs stream. for (const txFunctionLogs of [encryptedTxFunctionLogs, unencryptedTxFunctionLogs]) { + const isFromPublic = txFunctionLogs === unencryptedTxFunctionLogs; for (const functionLogs of txFunctionLogs) { for (const unprocessedLog of functionLogs.logs) { this.stats.seen++; - const incomingNotePayload = L1NotePayload.decryptAsIncoming(unprocessedLog.data, addressSecret); - const outgoingNotePayload = L1NotePayload.decryptAsOutgoing(unprocessedLog.data, ovskM); + const incomingNotePayload = L1NotePayload.decryptAsIncoming( + unprocessedLog.data, + addressSecret, + isFromPublic, + ); + const outgoingNotePayload = L1NotePayload.decryptAsOutgoing(unprocessedLog.data, ovskM, isFromPublic); if (incomingNotePayload || outgoingNotePayload) { if (incomingNotePayload && outgoingNotePayload && !incomingNotePayload.equals(outgoingNotePayload)) {