diff --git a/noir-projects/aztec-nr/aztec/src/context/private_context.nr b/noir-projects/aztec-nr/aztec/src/context/private_context.nr index aeba73e0554a..61475474ead8 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -10,7 +10,7 @@ use crate::{ key_validation_request::get_key_validation_request, arguments, returns::pack_returns, call_private_function::call_private_function_internal, header::get_header_at, logs::{ - emit_encrypted_log, emit_encrypted_note_log, compute_encrypted_log, + emit_encrypted_note_log, emit_encrypted_event_log, compute_encrypted_event_log, emit_contract_class_unencrypted_log_private_internal, emit_unencrypted_log_private_internal }, logs_traits::{LensForEncryptedLog, ToBytesForUnencryptedLog}, @@ -311,7 +311,7 @@ impl PrivateContext { // NB: A randomness value of 0 signals that the kernels should not mask the contract address // used in siloing later on e.g. 'handshaking' contract w/ known address. - pub fn encrypt_and_emit_log( + pub fn encrypt_and_emit_event( &mut self, contract_address: AztecAddress, randomness: Field, // Secret random value used later for masked_contract_address @@ -324,7 +324,7 @@ impl PrivateContext { // We are currently just encrypting it unconstrained, but otherwise the same way as if it was a note. let counter = self.next_counter(); - let encrypted_log: [u8; M] = compute_encrypted_log( + let encrypted_log: [u8; M] = compute_encrypted_event_log( contract_address, randomness, event_type_id, @@ -333,7 +333,7 @@ impl PrivateContext { ivpk_m, preimage ); - emit_encrypted_log(contract_address, randomness, encrypted_log, counter); + emit_encrypted_event_log(contract_address, randomness, encrypted_log, counter); let len = 32 + 32 + 64 + 48 + 48 + 176 + 64 + (preimage.len() as Field * 32) + 16 + 4; let log_hash = sha256_to_field(encrypted_log); let side_effect = EncryptedLogHash { value: log_hash, counter, length: len, randomness }; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr index 80e6ff01eec4..920ad95dd4f3 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr @@ -1,4 +1,5 @@ -use crate::note::{note_interface::NoteInterface}; +use crate::note::note_interface::NoteInterface; +use crate::event::event_interface::EventInterface; use dep::protocol_types::{grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint}; use dep::std::aes128::aes128_encrypt; @@ -14,6 +15,11 @@ impl EncryptedLogIncomingBody { EncryptedLogIncomingBody { plaintext } } + pub fn from_event(event: T, randomness: Field) -> Self where T: EventInterface { + let mut plaintext = event.to_be_bytes(randomness); + EncryptedLogIncomingBody { plaintext } + } + pub fn compute_ciphertext(self, eph_sk: GrumpkinPrivateKey, ivpk_app: GrumpkinPoint) -> [u8] { let full_key = point_to_symmetric_key(eph_sk, ivpk_app); let mut sym_key = [0; 16]; @@ -31,12 +37,14 @@ mod test { use crate::encrypted_logs::incoming_body::EncryptedLogIncomingBody; use dep::protocol_types::{ address::AztecAddress, traits::Empty, constants::GENERATOR_INDEX__NOTE_NULLIFIER, - grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint + grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint, traits::Serialize, + abis::function_selector::FunctionSelector }; use crate::{ note::{note_header::NoteHeader, note_interface::NoteInterface, utils::compute_note_hash_for_consumption}, - oracle::unsafe_rand::unsafe_rand, context::PrivateContext + event::event_interface::EventInterface, oracle::unsafe_rand::unsafe_rand, + context::PrivateContext }; struct AddressNote { @@ -100,7 +108,7 @@ mod test { } #[test] - fn test_encrypted_log_incoming_body() { + fn test_encrypted_note_log_incoming_body() { let note = AddressNote::new( AztecAddress::from_field(0x1), AztecAddress::from_field(0x2), @@ -122,14 +130,89 @@ mod test { let ciphertext = body.compute_ciphertext(eph_sk, ivpk_app); - let expected_body_ciphertext = [ + let expected_note_body_ciphertext = [ 131, 119, 105, 129, 244, 32, 151, 205, 12, 99, 93, 62, 10, 180, 72, 21, 47, 232, 95, 17, 240, 230, 80, 129, 174, 158, 23, 76, 114, 185, 43, 18, 254, 148, 147, 230, 66, 216, 167, 62, 180, 213, 238, 33, 108, 29, 84, 139, 99, 206, 212, 253, 92, 116, 137, 31, 0, 104, 45, 91, 250, 109, 141, 114, 189, 53, 35, 60, 108, 156, 170, 206, 150, 114, 150, 187, 198, 13, 62, 153, 133, 13, 169, 167, 242, 221, 40, 168, 186, 203, 104, 82, 47, 238, 142, 179, 90, 37, 9, 70, 245, 176, 122, 247, 42, 87, 75, 7, 20, 89, 166, 123, 14, 26, 230, 156, 49, 94, 0, 94, 72, 58, 171, 239, 115, 174, 155, 7, 151, 17, 60, 206, 193, 134, 70, 87, 215, 88, 21, 194, 63, 26, 106, 105, 124, 213, 252, 152, 192, 71, 115, 13, 181, 5, 169, 15, 170, 196, 174, 228, 170, 192, 91, 76, 110, 220, 89, 47, 248, 144, 189, 251, 167, 149, 248, 226 ]; - assert_eq(expected_body_ciphertext.len(), ciphertext.len()); + assert_eq(expected_note_body_ciphertext.len(), ciphertext.len()); + + for i in 0..expected_note_body_ciphertext.len() { + assert_eq(ciphertext[i], expected_note_body_ciphertext[i]); + } + } + + struct TestEvent { + value0: Field, + value1: Field, + value2: Field, + } + + impl Serialize<3> for TestEvent { + fn serialize(self) -> [Field; 3] { + [self.value0, self.value1, self.value2] + } + } + + global TEST_EVENT_LEN: Field = 3; + global TEST_EVENT_BYTES_LEN = 32 * 3 + 64; + + impl EventInterface for TestEvent { + fn _selector(self) -> FunctionSelector { + FunctionSelector::from_signature("TestEvent(Field,Field,Field)") + } + + fn to_be_bytes(self, randomness: Field) -> [u8; TEST_EVENT_BYTES_LEN] { + let mut buffer: [u8; TEST_EVENT_BYTES_LEN] = [0; TEST_EVENT_BYTES_LEN]; + + let randomness_bytes = randomness.to_be_bytes(32); + let event_type_id_bytes = self._selector().to_field().to_be_bytes(32); + + for i in 0..32 { + buffer[i] = randomness_bytes[i]; + buffer[32 + i] = event_type_id_bytes[i]; + } + + let serialized_event = self.serialize(); + + for i in 0..serialized_event.len() { + let bytes = serialized_event[i].to_be_bytes(32); + for j in 0..32 { + buffer[64 + i * 32 + j] = bytes[j]; + } + } + + buffer + } + } + + #[test] + fn test_encrypted_log_event_incoming_body() { + let test_event = TestEvent { value0: 1, value1: 2, value2: 3 }; + + let eph_sk = GrumpkinPrivateKey::new( + 0x0000000000000000000000000000000023b3127c127b1f29a7adff5cccf8fb06, + 0x00000000000000000000000000000000649e7ca01d9de27b21624098b897babd + ); + + let ivpk_app = GrumpkinPoint::new( + 0x2688431c705a5ff3e6c6f2573c9e3ba1c1026d2251d0dbbf2d810aa53fd1d186, + 0x1e96887b117afca01c00468264f4f80b5bb16d94c1808a448595f115556e5c8e + ); + + let randomness = 2; + + let body = EncryptedLogIncomingBody::from_event(test_event, randomness); + + let ciphertext = body.compute_ciphertext(eph_sk, ivpk_app); + + let expected_event_body_ciphertext = [ + 131, 119, 105, 129, 244, 32, 151, 205, 12, 99, 93, 62, 10, 180, 72, 21, 47, 232, 95, 17, 240, 230, 80, 129, 174, 158, 23, 76, 114, 185, 43, 18, 254, 148, 147, 230, 66, 216, 167, 62, 180, 213, 238, 33, 108, 29, 84, 139, 157, 165, 187, 138, 35, 3, 236, 75, 197, 105, 102, 247, 224, 253, 13, 217, 145, 62, 96, 167, 93, 23, 18, 198, 187, 91, 8, 3, 197, 195, 127, 9, 218, 111, 125, 97, 141, 129, 142, 1, 230, 108, 35, 211, 170, 170, 170, 249, 249, 104, 68, 191, 245, 207, 182, 245, 248, 82, 175, 83, 155, 138, 208, 65, 31, 129, 251, 242, 219, 76, 17, 61, 178, 187, 108, 114, 177, 215, 175, 189, 166, 221, 94, 9, 22, 57, 151, 204, 57, 220, 129, 243, 217, 18, 101, 128, 229, 40, 254, 175, 2, 21, 31, 198, 18, 152, 169, 32, 113, 92, 37, 65, 169, 119, 95, 149, 239, 8, 23, 182, 22, 209, 207, 120, 133, 90, 252, 106 + ]; + + assert_eq(expected_event_body_ciphertext.len(), ciphertext.len()); - for i in 0..expected_body_ciphertext.len() { - assert_eq(ciphertext[i], expected_body_ciphertext[i]); + for i in 0..expected_event_body_ciphertext.len() { + assert_eq(ciphertext[i], expected_event_body_ciphertext[i]); } } } diff --git a/noir-projects/aztec-nr/aztec/src/event.nr b/noir-projects/aztec-nr/aztec/src/event.nr new file mode 100644 index 000000000000..e77edfe3735b --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/event.nr @@ -0,0 +1 @@ +mod event_interface; diff --git a/noir-projects/aztec-nr/aztec/src/event/event_interface.nr b/noir-projects/aztec-nr/aztec/src/event/event_interface.nr new file mode 100644 index 000000000000..fe4b63fedd7f --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/event/event_interface.nr @@ -0,0 +1,9 @@ +use crate::context::PrivateContext; +use crate::note::note_header::NoteHeader; +use dep::protocol_types::{grumpkin_point::GrumpkinPoint, abis::function_selector::FunctionSelector}; + +trait EventInterface { + // Should be autogenerated by the #[aztec(event)] macro unless it is overridden by a custom implementation + fn _selector(self) -> FunctionSelector; + fn to_be_bytes(self, randomness: Field) -> [u8; N]; +} diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 95bb3f2f9ca5..7503e47d8a4d 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -6,6 +6,7 @@ mod initializer; mod keys; mod messaging; mod note; +mod event; mod oracle; mod state_vars; mod prelude; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/logs.nr b/noir-projects/aztec-nr/aztec/src/oracle/logs.nr index 9f184a339a84..220fe2a13ce9 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/logs.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/logs.nr @@ -16,26 +16,26 @@ unconstrained pub fn emit_encrypted_note_log( emit_encrypted_note_log_oracle(note_hash_counter, encrypted_note, counter) } -#[oracle(emitEncryptedLog)] -fn emit_encrypted_log_oracle( +#[oracle(emitEncryptedEventLog)] +fn emit_encrypted_event_log_oracle( _contract_address: AztecAddress, _randomness: Field, - _encrypted_note: [u8; M], + _encrypted_event: [u8; M], _counter: u32 ) {} -unconstrained pub fn emit_encrypted_log( +unconstrained pub fn emit_encrypted_event_log( contract_address: AztecAddress, randomness: Field, - encrypted_note: [u8; M], + encrypted_event: [u8; M], counter: u32 ) { - emit_encrypted_log_oracle(contract_address, randomness, encrypted_note, counter) + emit_encrypted_event_log_oracle(contract_address, randomness, encrypted_event, counter) } // = 480 + 32 * N bytes -#[oracle(computeEncryptedLog)] -fn compute_encrypted_log_oracle( +#[oracle(computeEncryptedNoteLog)] +fn compute_encrypted_note_log_oracle( _contract_address: AztecAddress, _storage_slot: Field, _note_type_id: Field, @@ -45,7 +45,7 @@ fn compute_encrypted_log_oracle( _preimage: [Field; N] ) -> [u8; M] {} -unconstrained pub fn compute_encrypted_log( +unconstrained pub fn compute_encrypted_note_log( contract_address: AztecAddress, storage_slot: Field, note_type_id: Field, @@ -54,7 +54,7 @@ unconstrained pub fn compute_encrypted_log( ivpk_m: GrumpkinPoint, preimage: [Field; N] ) -> [u8; M] { - compute_encrypted_log_oracle( + compute_encrypted_note_log_oracle( contract_address, storage_slot, note_type_id, @@ -65,6 +65,38 @@ unconstrained pub fn compute_encrypted_log( ) } +// = 480 + 32 * N bytes +#[oracle(computeEncryptedEventLog)] +fn compute_encrypted_event_log_oracle( + _contract_address: AztecAddress, + _randomness: Field, + _event_type_id: Field, + _ovsk_app: Field, + _ovpk_m: GrumpkinPoint, + _ivpk_m: GrumpkinPoint, + _preimage: [Field; N] +) -> [u8; M] {} + +unconstrained pub fn compute_encrypted_event_log( + contract_address: AztecAddress, + randomness: Field, + event_type_id: Field, + ovsk_app: Field, + ovpk_m: GrumpkinPoint, + ivpk_m: GrumpkinPoint, + preimage: [Field; N] +) -> [u8; M] { + compute_encrypted_event_log_oracle( + contract_address, + randomness, + event_type_id, + ovsk_app, + ovpk_m, + ivpk_m, + preimage + ) +} + #[oracle(emitUnencryptedLog)] fn emit_unencrypted_log_oracle_private( _contract_address: AztecAddress, diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index e8c4347f8555..77fbe7884293 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -33,6 +33,7 @@ members = [ "contracts/schnorr_single_key_account_contract", "contracts/stateful_test_contract", "contracts/test_contract", + "contracts/test_log_contract", "contracts/token_contract", "contracts/token_blacklist_contract", "contracts/token_bridge_contract", 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 d1329c33dd04..f34977b80c7a 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -269,7 +269,7 @@ contract Test { let header = context.get_header(); let outgoing_viewer_ovpk_m = header.get_ovpk_m(&mut context, outgoing_viewer); let owner_ivpk_m = header.get_ivpk_m(&mut context, owner); - context.encrypt_and_emit_log( + context.encrypt_and_emit_event( context.this_address(), 5, // testing only - this should be a secret random value to salt the addr 1, @@ -281,7 +281,7 @@ contract Test { // to test nested and non nested encrypted logs if nest { Test::at(context.this_address()).emit_array_as_encrypted_log([0, 0, 0, 0, 0], owner, outgoing_viewer, false).call(&mut context); - context.encrypt_and_emit_log( + context.encrypt_and_emit_event( context.this_address(), 0, // testing only - this signals to the kerels to not mask the address 1, diff --git a/noir-projects/noir-contracts/contracts/test_log_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test_log_contract/Nargo.toml new file mode 100644 index 000000000000..31969188f9cf --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test_log_contract/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "test_log_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } +value_note = { path = "../../../aztec-nr/value-note" } diff --git a/noir-projects/noir-contracts/contracts/test_log_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_log_contract/src/main.nr new file mode 100644 index 000000000000..a357f2890a84 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test_log_contract/src/main.nr @@ -0,0 +1,129 @@ +contract TestLog { + use dep::aztec::prelude::PrivateSet; + use dep::aztec::protocol_types::{ + traits::Serialize, grumpkin_point::GrumpkinPoint, grumpkin_private_key::GrumpkinPrivateKey, + abis::function_selector::FunctionSelector + }; + use dep::value_note::value_note::ValueNote; + use dep::aztec::encrypted_logs::incoming_body::EncryptedLogIncomingBody; + use dep::aztec::event::event_interface::EventInterface; + + #[aztec(event)] + struct ExampleEvent0 { + value0: Field, + value1: Field, + } + + // This should be autogenerated by the macros + global EXAMPLE_EVENT_0_BYTES_LEN = 32 * 2 + 32 + 32; + + impl EventInterface for ExampleEvent0 { + fn _selector(self) -> FunctionSelector { + FunctionSelector::from_signature("TestEvent(Field,Field,Field)") + } + + fn to_be_bytes(self, randomness: Field) -> [u8; EXAMPLE_EVENT_0_BYTES_LEN] { + let mut buffer: [u8; EXAMPLE_EVENT_0_BYTES_LEN] = [0; EXAMPLE_EVENT_0_BYTES_LEN]; + + let randomness_bytes = randomness.to_be_bytes(32); + let event_type_id_bytes = self._selector().to_field().to_be_bytes(32); + + for i in 0..32 { + buffer[i] = randomness_bytes[i]; + buffer[32 + i] = event_type_id_bytes[i]; + } + + let serialized_event = self.serialize(); + + for i in 0..serialized_event.len() { + let bytes = serialized_event[i].to_be_bytes(32); + for j in 0..32 { + buffer[64 + i * 32 + j] = bytes[j]; + } + } + + buffer + } + } + + #[aztec(event)] + struct ExampleEvent1 { + value2: Field, + value3: Field, + } + + impl Serialize<2> for ExampleEvent0 { + fn serialize(self) -> [Field; 2] { + [self.value0, self.value1] + } + } + + impl Serialize<2> for ExampleEvent1 { + fn serialize(self) -> [Field; 2] { + [self.value2, self.value3] + } + } + + #[aztec(storage)] + struct Storage { + example_set: PrivateSet, + } + + // EXAMPLE_EVENT_0_BYTES_LEN + 16 + global EXAMPLE_EVENT_0_CIPHERTEXT_BYTES_LEN = 144; + + #[aztec(private)] + fn compute_incoming_log_body_ciphertext( + secret: GrumpkinPrivateKey, + point: GrumpkinPoint, + randomness: Field, + event_type_id: Field, + preimage: [Field; 2] + ) -> [u8; EXAMPLE_EVENT_0_CIPHERTEXT_BYTES_LEN] { + EncryptedLogIncomingBody::from_event( + ExampleEvent0 { value0: preimage[0], value1: preimage[1] }, + randomness + ).compute_ciphertext(secret, point).as_array() + } + + #[aztec(private)] + fn emit_encrypted_log(randomness: Field, event_type_id: Field, preimage: [Field; 6]) { + let header = context.get_header(); + let msg_sender_ivpk_m = header.get_ivpk_m(&mut context, context.msg_sender()); + let msg_sender_ovpk_m = header.get_ovpk_m(&mut context, context.msg_sender()); + + context.encrypt_and_emit_event( + context.this_address(), + randomness, + event_type_id, + msg_sender_ovpk_m, + msg_sender_ivpk_m, + preimage + ); + } + + #[aztec(private)] + fn emit_encrypted_events(randomness: [Field; 2], preimages: [Field; 4]) { + let header = context.get_header(); + let msg_sender_ivpk_m = header.get_ivpk_m(&mut context, context.msg_sender()); + let msg_sender_ovpk_m = header.get_ovpk_m(&mut context, context.msg_sender()); + + context.encrypt_and_emit_event( + context.this_address(), + randomness[0], + ExampleEvent0::selector().to_field(), + msg_sender_ovpk_m, + msg_sender_ivpk_m, + ExampleEvent0 { value0: preimages[0], value1: preimages[1] }.serialize() + ); + + context.encrypt_and_emit_event( + context.this_address(), + randomness[1], + ExampleEvent1::selector().to_field(), + msg_sender_ovpk_m, + msg_sender_ivpk_m, + ExampleEvent1 { value2: preimages[2], value3: preimages[3] }.serialize() + ); + } +} diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index d5eb458db246..79eef3043707 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -95,7 +95,7 @@ export { CompleteAddress, EncryptedL2BlockL2Logs, EncryptedLogHeader, - EncryptedLogIncomingBody, + EncryptedNoteLogIncomingBody, EncryptedLogOutgoingBody, ExtendedNote, FunctionCall, @@ -127,6 +127,9 @@ export { createAztecNodeClient, merkleTreeIds, mockTx, + TaggedLog, + L1NotePayload, + L1EventPayload, } from '@aztec/circuit-types'; export { NodeInfo } from '@aztec/types/interfaces'; diff --git a/yarn-project/builder/src/contract-interface-gen/typescript.ts b/yarn-project/builder/src/contract-interface-gen/typescript.ts index 4c2911fba97b..317f52e8c1a1 100644 --- a/yarn-project/builder/src/contract-interface-gen/typescript.ts +++ b/yarn-project/builder/src/contract-interface-gen/typescript.ts @@ -239,6 +239,80 @@ function generateNotesGetter(input: ContractArtifact) { `; } +// This is of type AbiType +function generateEvents(events: any[] | undefined) { + if (events === undefined) { + return { events: '', eventDefs: '' }; + } + + const eventsStrings = events.map(event => { + const eventName = event.path.split('::')[1]; + + const eventDefProps = event.fields.map((field: any) => `${field.name}: Fr`); + const eventDefs = ` + export type ${eventName} = { + ${eventDefProps.join('\n')} + } + `; + + const fieldNames = event.fields.map((field: any) => `"${field.name}"`); + const eventsType = `${eventName}: {decode: (payload: L1EventPayload | undefined) => ${eventName} | undefined }`; + + // Get the last item in path + const eventDecode = `${event.path.split('::').at(-1)}: { + decode: this.decodeEvent(${event.fields.length}, '${eventName}(${event.fields + .map(() => 'Field') + .join(',')})', [${fieldNames}]) + }`; + + return { + eventDefs, + eventsType, + eventDecode, + }; + }); + + return { + eventDefs: eventsStrings.map(({ eventDefs }) => eventDefs).join('\n'), + events: ` + // Partial application is chosen is to avoid the duplication of so much codegen. + private static decodeEvent(fieldsLength: number, functionSignature: string, fields: string[]): (payload: L1EventPayload | undefined) => T | undefined { + return (payload: L1EventPayload | undefined): T | undefined => { + if (payload === undefined) { + return undefined; + } + if ( + !FunctionSelector.fromSignature(functionSignature).equals( + FunctionSelector.fromField(payload.eventTypeId), + ) + ) { + return undefined; + } + if (payload.event.items.length !== fieldsLength) { + throw new Error( + 'Something is weird here, we have matching FunctionSelectors, but the actual payload has mismatched length', + ); + } + + return fields.reduce( + (acc, curr, i) => ({ + ...acc, + [curr]: payload.event.items[i], + }), + {} as T, + ); + }; + } + + public static get events(): { ${eventsStrings.map(({ eventsType }) => eventsType).join(', ')} } { + return { + ${eventsStrings.map(({ eventDecode }) => eventDecode).join(',\n')} + }; + } + `, + }; +} + /** * Generates the typescript code to represent a contract. * @param input - The compiled Noir artifact. @@ -254,6 +328,7 @@ export function generateTypescriptContractInterface(input: ContractArtifact, art const artifactGetter = artifactImportPath && generateArtifactGetter(input.name); const storageLayoutGetter = artifactImportPath && generateStorageLayoutGetter(input); const notesGetter = artifactImportPath && generateNotesGetter(input); + const { eventDefs, events } = generateEvents(input.outputs.structs?.events); return ` /* Autogenerated file, do not edit! */ @@ -276,7 +351,9 @@ import { EthAddressLike, FieldLike, Fr, + FunctionSelector, FunctionSelectorLike, + L1EventPayload, loadContractArtifact, NoirCompiledContract, Point, @@ -286,6 +363,8 @@ import { } from '@aztec/aztec.js'; ${artifactStatement} +${eventDefs} + /** * Type-safe interface for contract ${input.name}; */ @@ -306,6 +385,8 @@ export class ${input.name}Contract extends ContractBase { public override methods!: { ${methods.join('\n')} }; + + ${events} } `; } diff --git a/yarn-project/circuit-types/src/logs/index.ts b/yarn-project/circuit-types/src/logs/index.ts index a5a4bc911c99..14cb33aa0068 100644 --- a/yarn-project/circuit-types/src/logs/index.ts +++ b/yarn-project/circuit-types/src/logs/index.ts @@ -7,10 +7,7 @@ export * from './l2_logs_source.js'; export * from './log_id.js'; export * from './log_type.js'; export * from './log_filter.js'; -export * from './l1_note_payload/index.js'; +export * from './l1_payload/index.js'; export * from './tx_l2_logs.js'; export * from './unencrypted_l2_log.js'; export * from './extended_unencrypted_l2_log.js'; -export * from './l1_note_payload/encrypted_log_header.js'; -export * from './l1_note_payload/encrypted_log_incoming_body.js'; -export * from './l1_note_payload/encrypted_log_outgoing_body.js'; diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/index.ts b/yarn-project/circuit-types/src/logs/l1_note_payload/index.ts deleted file mode 100644 index a91bee5dff00..000000000000 --- a/yarn-project/circuit-types/src/logs/l1_note_payload/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './encrypt_buffer.js'; -export * from './note.js'; -export * from './l1_note_payload.js'; -export * from './tagged_note.js'; diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/l1_note_payload.ts b/yarn-project/circuit-types/src/logs/l1_note_payload/l1_note_payload.ts deleted file mode 100644 index a4e7db0f50c4..000000000000 --- a/yarn-project/circuit-types/src/logs/l1_note_payload/l1_note_payload.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { - AztecAddress, - type GrumpkinPrivateKey, - type KeyValidationRequest, - type PublicKey, - computeIvpkApp, - computeIvskApp, - computeOvskApp, - derivePublicKeyFromSecretKey, -} from '@aztec/circuits.js'; -import { Fr, Point } from '@aztec/foundation/fields'; -import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; - -import { EncryptedLogHeader } from './encrypted_log_header.js'; -import { EncryptedLogIncomingBody } from './encrypted_log_incoming_body.js'; -import { EncryptedLogOutgoingBody } from './encrypted_log_outgoing_body.js'; -import { Note } from './note.js'; - -// Both the incoming and the outgoing header are 48 bytes. -// 32 bytes for the address, and 16 bytes padding to follow PKCS#7 -const HEADER_SIZE = 48; - -// The outgoing body is constant size of 176 bytes. -// 160 bytes for the secret key, address, and public key, and 16 bytes padding to follow PKCS#7 -const OUTGOING_BODY_SIZE = 176; -/** - * 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( - /** - * A note as emitted from Noir contract. Can be used along with private key to compute nullifier. - */ - public note: Note, - /** - * 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: Fr, - ) {} - - /** - * Deserializes the L1NotePayload object from a Buffer. - * @param buffer - Buffer or BufferReader object to deserialize. - * @returns An instance of L1NotePayload. - */ - static fromBuffer(buffer: Buffer | BufferReader): L1NotePayload { - const reader = BufferReader.asReader(buffer); - return new L1NotePayload( - reader.readObject(Note), - reader.readObject(AztecAddress), - Fr.fromBuffer(reader), - Fr.fromBuffer(reader), - ); - } - - /** - * Serializes the L1NotePayload object into a Buffer. - * @returns Buffer representation of the L1NotePayload object. - */ - toBuffer() { - return serializeToBuffer([this.note, this.contractAddress, this.storageSlot, this.noteTypeId]); - } - - /** - * 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 random(contract = AztecAddress.random()) { - return new L1NotePayload(Note.random(), contract, Fr.random(), Fr.random()); - } - - /** - * Encrypts a note payload for a given recipient and sender. - * Creates an incoming log the the recipient using the recipient's ivsk, and - * an outgoing log for the sender using the sender's ovsk. - * - * @param ephSk - An ephemeral secret key used for the encryption - * @param recipient - The recipient address, retrievable by the sender for his logs - * @param ivpk - The incoming viewing public key of the recipient - * @param ovKeys - The outgoing viewing keys of the sender - * @returns A buffer containing the encrypted log payload - * @throws If the ivpk is zero. - */ - public encrypt(ephSk: GrumpkinPrivateKey, recipient: AztecAddress, ivpk: PublicKey, ovKeys: KeyValidationRequest) { - if (ivpk.isZero()) { - throw new Error( - `Attempting to encrypt with a zero ivpk. You have probably passed a zero value in your Noir code somewhere thinking that the note won't broadcasted... but it was.`, - ); - } - - const ephPk = derivePublicKeyFromSecretKey(ephSk); - - const header = new EncryptedLogHeader(this.contractAddress); - - const incomingHeaderCiphertext = header.computeCiphertext(ephSk, ivpk); - const outgoingHeaderCiphertext = header.computeCiphertext(ephSk, ovKeys.pkM); - - const ivpkApp = computeIvpkApp(ivpk, this.contractAddress); - - const incomingBodyCiphertext = new EncryptedLogIncomingBody( - this.storageSlot, - this.noteTypeId, - this.note, - ).computeCiphertext(ephSk, ivpkApp); - - const outgoingBodyCiphertext = new EncryptedLogOutgoingBody(ephSk, recipient, ivpkApp).computeCiphertext( - ovKeys.skAppAsGrumpkinPrivateKey, - ephPk, - ); - - return Buffer.concat([ - ephPk.toBuffer(), - incomingHeaderCiphertext, - outgoingHeaderCiphertext, - outgoingBodyCiphertext, - incomingBodyCiphertext, - ]); - } - - /** - * Decrypts a ciphertext as an incoming log. - * - * This is executable by the recipient of the note, and uses the ivsk to decrypt the payload. - * The outgoing parts of the log are ignored entirely. - * - * Produces the same output as `decryptAsOutgoing`. - * - * @param ciphertext - The ciphertext for the log - * @param ivsk - The incoming viewing secret key, used to decrypt the logs - * @returns The decrypted log payload - */ - public static decryptAsIncoming(ciphertext: Buffer | bigint[], ivsk: GrumpkinPrivateKey) { - const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); - const reader = BufferReader.asReader(input); - - const ephPk = reader.readObject(Point); - - const incomingHeader = EncryptedLogHeader.fromCiphertext(reader.readBytes(HEADER_SIZE), ivsk, ephPk); - - // Skipping the outgoing header and body - reader.readBytes(HEADER_SIZE); - reader.readBytes(OUTGOING_BODY_SIZE); - - // The incoming can be of variable size, so we read until the end - const incomingBodySlice = reader.readToEnd(); - - const ivskApp = computeIvskApp(ivsk, incomingHeader.address); - const incomingBody = EncryptedLogIncomingBody.fromCiphertext(incomingBodySlice, ivskApp, ephPk); - - return new L1NotePayload( - incomingBody.note, - incomingHeader.address, - incomingBody.storageSlot, - incomingBody.noteTypeId, - ); - } - - /** - * Decrypts a ciphertext as an outgoing log. - * - * This is executable by the sender of the note, and uses the ovsk to decrypt the payload. - * The outgoing parts are decrypted to retrieve information that allows the sender to - * decrypt the incoming log, and learn about the note contents. - * - * Produces the same output as `decryptAsIncoming`. - * - * @param ciphertext - The ciphertext for the log - * @param ovsk - The outgoing viewing secret key, used to decrypt the logs - * @returns The decrypted log payload - */ - public static decryptAsOutgoing(ciphertext: Buffer | bigint[], ovsk: GrumpkinPrivateKey) { - const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); - const reader = BufferReader.asReader(input); - - const ephPk = reader.readObject(Point); - - // Skip the incoming header - reader.readBytes(HEADER_SIZE); - - const outgoingHeader = EncryptedLogHeader.fromCiphertext(reader.readBytes(HEADER_SIZE), ovsk, ephPk); - - const ovskApp = computeOvskApp(ovsk, outgoingHeader.address); - const outgoingBody = EncryptedLogOutgoingBody.fromCiphertext(reader.readBytes(OUTGOING_BODY_SIZE), ovskApp, ephPk); - - // The incoming can be of variable size, so we read until the end - const incomingBodySlice = reader.readToEnd(); - - const incomingBody = EncryptedLogIncomingBody.fromCiphertext( - incomingBodySlice, - outgoingBody.ephSk, - outgoingBody.recipientIvpkApp, - ); - - return new L1NotePayload( - incomingBody.note, - outgoingHeader.address, - incomingBody.storageSlot, - incomingBody.noteTypeId, - ); - } - - public equals(other: L1NotePayload) { - return ( - this.note.equals(other.note) && - this.contractAddress.equals(other.contractAddress) && - this.storageSlot.equals(other.storageSlot) && - this.noteTypeId.equals(other.noteTypeId) - ); - } -} diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/tagged_note.test.ts b/yarn-project/circuit-types/src/logs/l1_note_payload/tagged_note.test.ts deleted file mode 100644 index 4f4e48593b86..000000000000 --- a/yarn-project/circuit-types/src/logs/l1_note_payload/tagged_note.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { AztecAddress, KeyValidationRequest, computeOvskApp, derivePublicKeyFromSecretKey } from '@aztec/circuits.js'; -import { GrumpkinScalar } from '@aztec/foundation/fields'; - -import { L1NotePayload } from './l1_note_payload.js'; -import { TaggedNote } from './tagged_note.js'; - -describe('L1 Note Payload', () => { - it('convert to and from buffer', () => { - const payload = L1NotePayload.random(); - const taggedNote = new TaggedNote(payload); - const buf = taggedNote.toBuffer(); - expect(TaggedNote.fromBuffer(buf).notePayload).toEqual(taggedNote.notePayload); - }); - - describe('encrypt and decrypt a full log', () => { - let ovskM: GrumpkinScalar; - let ivskM: GrumpkinScalar; - - let taggedNote: TaggedNote; - let encrypted: Buffer; - - beforeAll(() => { - const payload = L1NotePayload.random(); - - ovskM = GrumpkinScalar.random(); - ivskM = GrumpkinScalar.random(); - - const ovKeys = getKeyValidationRequest(ovskM, payload.contractAddress); - - const ephSk = GrumpkinScalar.random(); - - const recipientAddress = AztecAddress.random(); - const ivpk = derivePublicKeyFromSecretKey(ivskM); - - taggedNote = new TaggedNote(payload); - - encrypted = taggedNote.encrypt(ephSk, recipientAddress, ivpk, ovKeys); - }); - - it('decrypt a log as incoming', () => { - const recreated = TaggedNote.decryptAsIncoming(encrypted, ivskM); - - expect(recreated?.toBuffer()).toEqual(taggedNote.toBuffer()); - }); - - it('decrypt a log as outgoing', () => { - const recreated = TaggedNote.decryptAsOutgoing(encrypted, ovskM); - - expect(recreated?.toBuffer()).toEqual(taggedNote.toBuffer()); - }); - }); - - const getKeyValidationRequest = (ovskM: GrumpkinScalar, app: AztecAddress) => { - const ovskApp = computeOvskApp(ovskM, app); - const ovpkM = derivePublicKeyFromSecretKey(ovskM); - - return new KeyValidationRequest(ovpkM, ovskApp); - }; -}); diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/tagged_note.ts b/yarn-project/circuit-types/src/logs/l1_note_payload/tagged_note.ts deleted file mode 100644 index 4889b5cab25e..000000000000 --- a/yarn-project/circuit-types/src/logs/l1_note_payload/tagged_note.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { AztecAddress, type GrumpkinPrivateKey, type KeyValidationRequest, type PublicKey } from '@aztec/circuits.js'; -import { Fr } from '@aztec/foundation/fields'; -import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; - -import { L1NotePayload } from './l1_note_payload.js'; - -// placeholder value until tagging is implemented -const PLACEHOLDER_TAG = new Fr(33); - -/** - * Encrypted note payload with a tag used for retrieval by clients. - */ -export class TaggedNote { - constructor( - public notePayload: L1NotePayload, - public incomingTag = PLACEHOLDER_TAG, - public outgoingTag = PLACEHOLDER_TAG, - ) {} - - /** - * Deserializes the TaggedNote object from a Buffer. - * @param buffer - Buffer or BufferReader object to deserialize. - * @returns An instance of TaggedNote. - */ - static fromBuffer(buffer: Buffer | BufferReader): TaggedNote { - const reader = BufferReader.asReader(buffer); - const incomingTag = Fr.fromBuffer(reader); - const outgoingTag = Fr.fromBuffer(reader); - const payload = L1NotePayload.fromBuffer(reader); - return new TaggedNote(payload, incomingTag, outgoingTag); - } - - /** - * Serializes the TaggedNote object into a Buffer. - * @returns Buffer representation of the TaggedNote object (unencrypted). - */ - public toBuffer(): Buffer { - return serializeToBuffer(this.incomingTag, this.outgoingTag, this.notePayload); - } - - /** - * Create a random TaggedNote (useful for testing purposes). - * @param contract - The address of a contract the note was emitted from. - * @returns A random TaggedNote object. - */ - static random(contract = AztecAddress.random()): TaggedNote { - return new TaggedNote(L1NotePayload.random(contract)); - } - - public encrypt( - ephSk: GrumpkinPrivateKey, - recipient: AztecAddress, - ivpk: PublicKey, - ovKeys: KeyValidationRequest, - ): Buffer { - return serializeToBuffer( - this.incomingTag, - this.outgoingTag, - this.notePayload.encrypt(ephSk, recipient, ivpk, ovKeys), - ); - } - - static decryptAsIncoming(data: Buffer | bigint[], ivsk: GrumpkinPrivateKey) { - // Right now heavily abusing that we will likely fail if bad decryption - // as some field will likely end up not being in the field etc. - try { - const input = Buffer.isBuffer(data) ? data : Buffer.from(data.map((x: bigint) => Number(x))); - const reader = BufferReader.asReader(input); - const incomingTag = Fr.fromBuffer(reader); - const outgoingTag = Fr.fromBuffer(reader); - const payload = L1NotePayload.decryptAsIncoming(reader.readToEnd(), ivsk); - return new TaggedNote(payload, incomingTag, outgoingTag); - } catch (e) { - return; - } - } - - static decryptAsOutgoing(data: Buffer | bigint[], ovsk: GrumpkinPrivateKey) { - // Right now heavily abusing that we will likely fail if bad decryption - // as some field will likely end up not being in the field etc. - try { - const input = Buffer.isBuffer(data) ? data : Buffer.from(data.map((x: bigint) => Number(x))); - const reader = BufferReader.asReader(input); - const incomingTag = Fr.fromBuffer(reader); - const outgoingTag = Fr.fromBuffer(reader); - const payload = L1NotePayload.decryptAsOutgoing(reader.readToEnd(), ovsk); - return new TaggedNote(payload, incomingTag, outgoingTag); - } catch (e) { - return; - } - } -} diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/browserify-cipher.d.ts b/yarn-project/circuit-types/src/logs/l1_payload/browserify-cipher.d.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/browserify-cipher.d.ts rename to yarn-project/circuit-types/src/logs/l1_payload/browserify-cipher.d.ts diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypt_buffer.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypt_buffer.test.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encrypt_buffer.test.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encrypt_buffer.test.ts diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypt_buffer.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypt_buffer.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encrypt_buffer.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encrypt_buffer.ts diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_header.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_header.test.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_header.test.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_header.test.ts diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_header.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_header.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_header.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_header.ts diff --git a/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_event_log_incoming_body.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_event_log_incoming_body.test.ts new file mode 100644 index 000000000000..3ff13a7b8675 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_event_log_incoming_body.test.ts @@ -0,0 +1,66 @@ +import { Fr, FunctionSelector, GrumpkinScalar } from '@aztec/circuits.js'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; +import { updateInlineTestData } from '@aztec/foundation/testing'; + +import { Event } from '../payload.js'; +import { EncryptedEventLogIncomingBody } from './encrypted_event_log_incoming_body.js'; + +describe('encrypt log incoming body', () => { + let grumpkin: Grumpkin; + + beforeAll(() => { + grumpkin = new Grumpkin(); + }); + + it('encrypt and decrypt an event log incoming body', () => { + const ephSecretKey = GrumpkinScalar.random(); + const viewingSecretKey = GrumpkinScalar.random(); + + const ephPubKey = grumpkin.mul(Grumpkin.generator, ephSecretKey); + const viewingPubKey = grumpkin.mul(Grumpkin.generator, viewingSecretKey); + + const event = Event.random(); + const randomness = Fr.random(); + const eventTypeId = Fr.random(); + + const body = new EncryptedEventLogIncomingBody(randomness, eventTypeId, event); + + const encrypted = body.computeCiphertext(ephSecretKey, viewingPubKey); + + const recreated = EncryptedEventLogIncomingBody.fromCiphertext(encrypted, viewingSecretKey, ephPubKey); + + expect(recreated.toBuffer()).toEqual(body.toBuffer()); + }); + + it('encrypt an event log incoming body, generate input for noir test', () => { + // The following 2 are arbitrary fixed values - fixed in order to test a match with Noir + const ephSecretKey: GrumpkinScalar = new GrumpkinScalar( + 0x23b3127c127b1f29a7adff5cccf8fb06649e7ca01d9de27b21624098b897babdn, + ); + const viewingSecretKey: GrumpkinScalar = new GrumpkinScalar( + 0x1fdd0dd8c99b21af8e00d2d130bdc263b36dadcbea84ac5ec9293a0660deca01n, + ); + + const viewingPubKey = grumpkin.mul(Grumpkin.generator, viewingSecretKey); + + const event = new Event([new Fr(1), new Fr(2), new Fr(3)]); + const eventTypeId = FunctionSelector.fromSignature('TestEvent(Field,Field,Field)').toField(); + const randomness = new Fr(2); + + const body = new EncryptedEventLogIncomingBody(randomness, eventTypeId, event); + + const encrypted = body.computeCiphertext(ephSecretKey, viewingPubKey); + + const byteArrayString = `[${encrypted + .toString('hex') + .match(/.{1,2}/g)! + .map(byte => parseInt(byte, 16))}]`; + + // Run with AZTEC_GENERATE_TEST_DATA=1 to update noir test data + updateInlineTestData( + 'noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr', + 'expected_event_body_ciphertext', + byteArrayString, + ); + }); +}); diff --git a/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_event_log_incoming_body.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_event_log_incoming_body.ts new file mode 100644 index 000000000000..077e56490b04 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_event_log_incoming_body.ts @@ -0,0 +1,60 @@ +import { Fr, type GrumpkinPrivateKey, type PublicKey } from '@aztec/circuits.js'; +import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; + +import { Event } from '../payload.js'; +import { EncryptedLogIncomingBody } from './encrypted_log_incoming_body.js'; + +export class EncryptedEventLogIncomingBody extends EncryptedLogIncomingBody { + constructor(public randomness: Fr, public eventTypeId: Fr, public event: Event) { + super(); + } + + /** + * Serializes the log body to a buffer WITHOUT the length of the event buffer + * + * @returns The serialized log body + */ + public toBuffer(): Buffer { + const eventBufferWithoutLength = this.event.toBuffer().subarray(4); + return serializeToBuffer(this.randomness, this.eventTypeId, eventBufferWithoutLength); + } + + /** + * Deserialized the log body from a buffer WITHOUT the length of the event buffer + * + * @param buf - The buffer to deserialize + * @returns The deserialized log body + */ + public static fromBuffer(buf: Buffer): EncryptedEventLogIncomingBody { + const reader = BufferReader.asReader(buf); + const randomness = Fr.fromBuffer(reader); + const eventTypeId = Fr.fromBuffer(reader); + + // 2 Fields (randomness and event type id) are not included in the event buffer + const fieldsInEvent = reader.getLength() / 32 - 2; + const event = new Event(reader.readArray(fieldsInEvent, Fr)); + + return new EncryptedEventLogIncomingBody(randomness, eventTypeId, event); + } + + /** + * Decrypts a log body + * + * @param ciphertext - The ciphertext buffer + * @param ivskAppOrEphSk - The private key matching the public key used in encryption (the viewing key secret or) + * @param ephPkOrIvpkApp - The public key generated with the ephemeral secret key used in encryption + * + * The "odd" input stems from ivskApp * ephPk == ivpkApp * ephSk producing the same value. + * This is used to allow for the same decryption function to be used by both the sender and the recipient. + * + * @returns The decrypted log body + */ + public static fromCiphertext( + ciphertext: Buffer | bigint[], + ivskAppOrEphSk: GrumpkinPrivateKey, + ephPkOrIvpkApp: PublicKey, + ): EncryptedEventLogIncomingBody { + const buffer = super.fromCiphertextToBuffer(ciphertext, ivskAppOrEphSk, ephPkOrIvpkApp); + return EncryptedEventLogIncomingBody.fromBuffer(buffer); + } +} diff --git a/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_log_incoming_body.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_log_incoming_body.ts new file mode 100644 index 000000000000..6a8b5566e4f1 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_log_incoming_body.ts @@ -0,0 +1,54 @@ +import { type GrumpkinPrivateKey, type PublicKey } from '@aztec/circuits.js'; +import { Aes128 } from '@aztec/circuits.js/barretenberg'; + +import { deriveAESSecret } from '../encryption_utils.js'; + +export abstract class EncryptedLogIncomingBody { + public abstract toBuffer(): Buffer; + + /** + * Decrypts a log body + * + * @param ciphertext - The ciphertext buffer + * @param ivskAppOrEphSk - The private key matching the public key used in encryption (the viewing key secret or) + * @param ephPkOrIvpkApp - The public key generated with the ephemeral secret key used in encryption + * + * The "odd" input stems from ivskApp * ephPk == ivpkApp * ephSk producing the same value. + * This is used to allow for the same decryption function to be used by both the sender and the recipient. + * + * @returns The decrypted log body as a buffer + */ + protected static fromCiphertextToBuffer( + ciphertext: Buffer | bigint[], + ivskAppOrEphSk: GrumpkinPrivateKey, + ephPkOrIvpkApp: PublicKey, + ): Buffer { + const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); + + const aesSecret = deriveAESSecret(ivskAppOrEphSk, ephPkOrIvpkApp); + const key = aesSecret.subarray(0, 16); + const iv = aesSecret.subarray(16, 32); + + const buffer = new Aes128().decryptBufferCBC(input, iv, key); + return buffer; + } + + /** + * Encrypts a log body + * + * @param ephSk - The ephemeral secret key + * @param ivpkApp - The application scoped incoming viewing key for the recipient of this log + * + * @returns The ciphertext of the encrypted log body + */ + public computeCiphertext(ephSk: GrumpkinPrivateKey, ivpkApp: PublicKey) { + const aesSecret = deriveAESSecret(ephSk, ivpkApp); + const key = aesSecret.subarray(0, 16); + const iv = aesSecret.subarray(16, 32); + + const aes128 = new Aes128(); + const buffer = this.toBuffer(); + + return aes128.encryptBufferCBC(buffer, iv, key); + } +} diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_incoming_body.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_note_log_incoming_body.test.ts similarity index 76% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_incoming_body.test.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_note_log_incoming_body.test.ts index 934621b508ed..939ca41ea65e 100644 --- a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_incoming_body.test.ts +++ b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_note_log_incoming_body.test.ts @@ -2,8 +2,8 @@ import { Fr, GrumpkinScalar } from '@aztec/circuits.js'; import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { updateInlineTestData } from '@aztec/foundation/testing'; -import { EncryptedLogIncomingBody } from './encrypted_log_incoming_body.js'; -import { Note } from './note.js'; +import { Note } from '../payload.js'; +import { EncryptedNoteLogIncomingBody } from './encrypted_note_log_incoming_body.js'; describe('encrypt log incoming body', () => { let grumpkin: Grumpkin; @@ -12,7 +12,7 @@ describe('encrypt log incoming body', () => { grumpkin = new Grumpkin(); }); - it('encrypt and decrypt a log incoming body', () => { + it('encrypt and decrypt a note log incoming body', () => { const ephSecretKey = GrumpkinScalar.random(); const viewingSecretKey = GrumpkinScalar.random(); @@ -23,21 +23,21 @@ describe('encrypt log incoming body', () => { const noteTypeId = Fr.random(); const storageSlot = Fr.random(); - const body = new EncryptedLogIncomingBody(noteTypeId, storageSlot, note); + const body = new EncryptedNoteLogIncomingBody(noteTypeId, storageSlot, note); const encrypted = body.computeCiphertext(ephSecretKey, viewingPubKey); - const recreated = EncryptedLogIncomingBody.fromCiphertext(encrypted, viewingSecretKey, ephPubKey); + const recreated = EncryptedNoteLogIncomingBody.fromCiphertext(encrypted, viewingSecretKey, ephPubKey); expect(recreated.toBuffer()).toEqual(body.toBuffer()); }); - it('encrypt a log incoming body, generate input for noir test', () => { + it('encrypt a note log incoming body, generate input for noir test', () => { // The following 2 are arbitrary fixed values - fixed in order to test a match with Noir - const viewingSecretKey: GrumpkinScalar = new GrumpkinScalar( + const ephSecretKey: GrumpkinScalar = new GrumpkinScalar( 0x23b3127c127b1f29a7adff5cccf8fb06649e7ca01d9de27b21624098b897babdn, ); - const ephSecretKey: GrumpkinScalar = new GrumpkinScalar( + const viewingSecretKey: GrumpkinScalar = new GrumpkinScalar( 0x1fdd0dd8c99b21af8e00d2d130bdc263b36dadcbea84ac5ec9293a0660deca01n, ); @@ -47,7 +47,7 @@ describe('encrypt log incoming body', () => { const noteTypeId = new Fr(1); const storageSlot = new Fr(2); - const body = new EncryptedLogIncomingBody(storageSlot, noteTypeId, note); + const body = new EncryptedNoteLogIncomingBody(storageSlot, noteTypeId, note); const encrypted = body.computeCiphertext(ephSecretKey, viewingPubKey); @@ -59,7 +59,7 @@ describe('encrypt log incoming body', () => { // Run with AZTEC_GENERATE_TEST_DATA=1 to update noir test data updateInlineTestData( 'noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr', - 'expected_body_ciphertext', + 'expected_note_body_ciphertext', byteArrayString, ); }); diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_incoming_body.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_note_log_incoming_body.ts similarity index 56% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_incoming_body.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_note_log_incoming_body.ts index 3001d6963de2..83bd9edb4794 100644 --- a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_incoming_body.ts +++ b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/encrypted_note_log_incoming_body.ts @@ -1,12 +1,13 @@ import { Fr, type GrumpkinPrivateKey, type PublicKey } from '@aztec/circuits.js'; -import { Aes128 } from '@aztec/circuits.js/barretenberg'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; -import { deriveAESSecret } from './encryption_utils.js'; -import { Note } from './note.js'; +import { Note } from '../payload.js'; +import { EncryptedLogIncomingBody } from './encrypted_log_incoming_body.js'; -export class EncryptedLogIncomingBody { - constructor(public storageSlot: Fr, public noteTypeId: Fr, public note: Note) {} +export class EncryptedNoteLogIncomingBody extends EncryptedLogIncomingBody { + constructor(public storageSlot: Fr, public noteTypeId: Fr, public note: Note) { + super(); + } /** * Serializes the log body to a buffer WITHOUT the length of the note buffer @@ -24,7 +25,7 @@ export class EncryptedLogIncomingBody { * @param buf - The buffer to deserialize * @returns The deserialized log body */ - public static fromBuffer(buf: Buffer): EncryptedLogIncomingBody { + public static fromBuffer(buf: Buffer): EncryptedNoteLogIncomingBody { const reader = BufferReader.asReader(buf); const storageSlot = Fr.fromBuffer(reader); const noteTypeId = Fr.fromBuffer(reader); @@ -33,26 +34,7 @@ export class EncryptedLogIncomingBody { const fieldsInNote = reader.getLength() / 32 - 2; const note = new Note(reader.readArray(fieldsInNote, Fr)); - return new EncryptedLogIncomingBody(storageSlot, noteTypeId, note); - } - - /** - * Encrypts a log body - * - * @param ephSk - The ephemeral secret key - * @param ivpkApp - The application scoped incoming viewing key for the recipient of this log - * - * @returns The ciphertext of the encrypted log body - */ - public computeCiphertext(ephSk: GrumpkinPrivateKey, ivpkApp: PublicKey) { - const aesSecret = deriveAESSecret(ephSk, ivpkApp); - const key = aesSecret.subarray(0, 16); - const iv = aesSecret.subarray(16, 32); - - const aes128 = new Aes128(); - const buffer = this.toBuffer(); - - return aes128.encryptBufferCBC(buffer, iv, key); + return new EncryptedNoteLogIncomingBody(storageSlot, noteTypeId, note); } /** @@ -71,15 +53,8 @@ export class EncryptedLogIncomingBody { ciphertext: Buffer | bigint[], ivskAppOrEphSk: GrumpkinPrivateKey, ephPkOrIvpkApp: PublicKey, - ): EncryptedLogIncomingBody { - const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); - - const aesSecret = deriveAESSecret(ivskAppOrEphSk, ephPkOrIvpkApp); - const key = aesSecret.subarray(0, 16); - const iv = aesSecret.subarray(16, 32); - - const aes128 = new Aes128(); - const buffer = aes128.decryptBufferCBC(input, iv, key); - return EncryptedLogIncomingBody.fromBuffer(buffer); + ): EncryptedNoteLogIncomingBody { + const buffer = super.fromCiphertextToBuffer(ciphertext, ivskAppOrEphSk, ephPkOrIvpkApp); + return EncryptedNoteLogIncomingBody.fromBuffer(buffer); } } diff --git a/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/index.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/index.ts new file mode 100644 index 000000000000..6a8e480e25fe --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_incoming_body/index.ts @@ -0,0 +1,3 @@ +export * from './encrypted_event_log_incoming_body.js'; +export * from './encrypted_note_log_incoming_body.js'; +export * from './encrypted_log_incoming_body.js'; diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_outgoing_body.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_outgoing_body.test.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_outgoing_body.test.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_outgoing_body.test.ts diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_outgoing_body.ts b/yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_outgoing_body.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encrypted_log_outgoing_body.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_outgoing_body.ts diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/encryption_utils.ts b/yarn-project/circuit-types/src/logs/l1_payload/encryption_utils.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/encryption_utils.ts rename to yarn-project/circuit-types/src/logs/l1_payload/encryption_utils.ts diff --git a/yarn-project/circuit-types/src/logs/l1_payload/index.ts b/yarn-project/circuit-types/src/logs/l1_payload/index.ts new file mode 100644 index 000000000000..a8fe71dbc5d4 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/index.ts @@ -0,0 +1,8 @@ +export * from './encrypt_buffer.js'; +export * from './payload.js'; +export * from './l1_event_payload.js'; +export * from './l1_note_payload.js'; +export * from './tagged_log.js'; +export * from './encrypted_log_incoming_body/index.js'; +export * from './encrypted_log_header.js'; +export * from './encrypted_log_outgoing_body.js'; diff --git a/yarn-project/circuit-types/src/logs/l1_payload/l1_event_payload.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/l1_event_payload.test.ts new file mode 100644 index 000000000000..ea7f49391b6e --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/l1_event_payload.test.ts @@ -0,0 +1,67 @@ +import { AztecAddress, KeyValidationRequest, computeOvskApp, derivePublicKeyFromSecretKey } from '@aztec/circuits.js'; +import { pedersenHash } from '@aztec/foundation/crypto'; +import { Fr, GrumpkinScalar } from '@aztec/foundation/fields'; + +import { EncryptedL2Log } from '../encrypted_l2_log.js'; +import { L1EventPayload } from './l1_event_payload.js'; +import { Event } from './payload.js'; + +describe('L1 Event Payload', () => { + it('convert to and from buffer', () => { + const payload = L1EventPayload.random(); + const buf = payload.toBuffer(); + expect(L1EventPayload.fromBuffer(buf)).toEqual(payload); + }); + + describe('encrypt and decrypt a full log', () => { + let ovskM: GrumpkinScalar; + let ivskM: GrumpkinScalar; + + let payload: L1EventPayload; + let encrypted: Buffer; + let encryptedL2Log: EncryptedL2Log; + let maskedContractAddress: AztecAddress; + let contractAddress: AztecAddress; + let randomness: Fr; + + beforeAll(() => { + contractAddress = AztecAddress.random(); + randomness = Fr.random(); + maskedContractAddress = pedersenHash([contractAddress, randomness], 0); + + payload = new L1EventPayload(Event.random(), contractAddress, randomness, Fr.random()); + + ovskM = GrumpkinScalar.random(); + ivskM = GrumpkinScalar.random(); + + const ovKeys = getKeyValidationRequest(ovskM, payload.contractAddress); + + const ephSk = GrumpkinScalar.random(); + + const recipientAddress = AztecAddress.random(); + const ivpk = derivePublicKeyFromSecretKey(ivskM); + + encrypted = payload.encrypt(ephSk, recipientAddress, ivpk, ovKeys); + const tag = Fr.random().toBuffer(); + encryptedL2Log = new EncryptedL2Log(Buffer.concat([tag, tag, encrypted]), maskedContractAddress); + }); + + it('decrypt a log as incoming', () => { + const recreated = L1EventPayload.decryptAsIncoming(encryptedL2Log, ivskM); + + expect(recreated.toBuffer()).toEqual(payload.toBuffer()); + }); + + it('decrypt a log as outgoing', () => { + const recreated = L1EventPayload.decryptAsOutgoing(encryptedL2Log, ovskM); + + expect(recreated.toBuffer()).toEqual(payload.toBuffer()); + }); + }); + + const getKeyValidationRequest = (ovskM: GrumpkinScalar, app: AztecAddress) => { + const ovskApp = computeOvskApp(ovskM, app); + const ovpkM = derivePublicKeyFromSecretKey(ovskM); + return new KeyValidationRequest(ovpkM, ovskApp); + }; +}); diff --git a/yarn-project/circuit-types/src/logs/l1_payload/l1_event_payload.ts b/yarn-project/circuit-types/src/logs/l1_payload/l1_event_payload.ts new file mode 100644 index 000000000000..e3cd80ba0610 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/l1_event_payload.ts @@ -0,0 +1,138 @@ +import { AztecAddress, type GrumpkinPrivateKey, type KeyValidationRequest, type PublicKey } from '@aztec/circuits.js'; +import { Fr } from '@aztec/foundation/fields'; +import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; + +import { type EncryptedL2Log } from '../encrypted_l2_log.js'; +import { EncryptedEventLogIncomingBody } from './encrypted_log_incoming_body/index.js'; +import { L1Payload } from './l1_payload.js'; +import { Event } from './payload.js'; + +/** + * A class which wraps event data which is pushed on L1. + */ +export class L1EventPayload extends L1Payload { + constructor( + /** + * An encrypted event as emitted from Noir contract. + */ + public event: Event, + /** + * Address of the contract that emitted this event log. + */ + public contractAddress: AztecAddress, + /** + * Randomness used to mask the contract address. + */ + public randomness: Fr, + /** + * Type identifier for the underlying event, (calculated as a function selector). + */ + public eventTypeId: Fr, + ) { + super(); + } + + /** + * Deserializes the L1EventPayload object from a Buffer. + * @param buffer - Buffer or BufferReader object to deserialize. + * @returns An instance of L1EventPayload. + */ + static fromBuffer(buffer: Buffer | BufferReader): L1EventPayload { + const reader = BufferReader.asReader(buffer); + return new L1EventPayload( + reader.readObject(Event), + reader.readObject(AztecAddress), + Fr.fromBuffer(reader), + Fr.fromBuffer(reader), + ); + } + + /** + * Serializes the L1EventPayload object into a Buffer. + * @returns Buffer representation of the L1EventPayload object. + */ + toBuffer() { + return serializeToBuffer([this.event, this.contractAddress, this.randomness, this.eventTypeId]); + } + + /** + * Create a random L1EventPayload object (useful for testing purposes). + * @returns A random L1EventPayload object. + */ + static random() { + return new L1EventPayload(Event.random(), AztecAddress.random(), Fr.random(), Fr.random()); + } + + public encrypt(ephSk: GrumpkinPrivateKey, recipient: AztecAddress, ivpk: PublicKey, ovKeys: KeyValidationRequest) { + return super._encrypt( + this.contractAddress, + ephSk, + recipient, + ivpk, + ovKeys, + new EncryptedEventLogIncomingBody(this.randomness, this.eventTypeId, this.event), + ); + } + + /** + * Decrypts a ciphertext as an incoming log. + * + * This is executable by the recipient of the event, and uses the ivsk to decrypt the payload. + * The outgoing parts of the log are ignored entirely. + * + * Produces the same output as `decryptAsOutgoing`. + * + * @param encryptedLog - The encrypted log. This encrypted log is assumed to always have tags. + * @param ivsk - The incoming viewing secret key, used to decrypt the logs + * @returns The decrypted log payload + * @remarks The encrypted log is assumed to always have tags. + */ + public static decryptAsIncoming(encryptedLog: EncryptedL2Log, ivsk: GrumpkinPrivateKey) { + const reader = BufferReader.asReader(encryptedLog.data); + + // We skip the tags + Fr.fromBuffer(reader); + Fr.fromBuffer(reader); + + const [address, incomingBody] = super._decryptAsIncoming( + reader.readToEnd(), + ivsk, + EncryptedEventLogIncomingBody.fromCiphertext, + ); + + this.ensureMatchedMaskedContractAddress(address, incomingBody.randomness, encryptedLog.maskedContractAddress); + + return new L1EventPayload(incomingBody.event, address, incomingBody.randomness, incomingBody.eventTypeId); + } + + /** + * Decrypts a ciphertext as an outgoing log. + * + * This is executable by the sender of the event, and uses the ovsk to decrypt the payload. + * The outgoing parts are decrypted to retrieve information that allows the sender to + * decrypt the incoming log, and learn about the event contents. + * + * Produces the same output as `decryptAsIncoming`. + * + * @param ciphertext - The ciphertext for the log + * @param ovsk - The outgoing viewing secret key, used to decrypt the logs + * @returns The decrypted log payload + */ + public static decryptAsOutgoing(encryptedLog: EncryptedL2Log, ovsk: GrumpkinPrivateKey) { + const reader = BufferReader.asReader(encryptedLog.data); + + // Skip the tags + Fr.fromBuffer(reader); + Fr.fromBuffer(reader); + + const [address, incomingBody] = super._decryptAsOutgoing( + reader.readToEnd(), + ovsk, + EncryptedEventLogIncomingBody.fromCiphertext, + ); + + this.ensureMatchedMaskedContractAddress(address, incomingBody.randomness, encryptedLog.maskedContractAddress); + + return new L1EventPayload(incomingBody.event, address, incomingBody.randomness, incomingBody.eventTypeId); + } +} diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/l1_note_payload.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/l1_note_payload.test.ts similarity index 100% rename from yarn-project/circuit-types/src/logs/l1_note_payload/l1_note_payload.test.ts rename to yarn-project/circuit-types/src/logs/l1_payload/l1_note_payload.test.ts 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 new file mode 100644 index 000000000000..ee28010c1bfb --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/l1_note_payload.ts @@ -0,0 +1,138 @@ +import { AztecAddress, type GrumpkinPrivateKey, type KeyValidationRequest, type PublicKey } from '@aztec/circuits.js'; +import { Fr } from '@aztec/foundation/fields'; +import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; + +import { EncryptedNoteLogIncomingBody } from './encrypted_log_incoming_body/index.js'; +import { L1Payload } from './l1_payload.js'; +import { Note } from './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 extends L1Payload { + constructor( + /** + * A note as emitted from Noir contract. Can be used along with private key to compute nullifier. + */ + public note: Note, + /** + * 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: Fr, + ) { + super(); + } + + /** + * Deserializes the L1NotePayload object from a Buffer. + * @param buffer - Buffer or BufferReader object to deserialize. + * @returns An instance of L1NotePayload. + */ + static fromBuffer(buffer: Buffer | BufferReader): L1NotePayload { + const reader = BufferReader.asReader(buffer); + return new L1NotePayload( + reader.readObject(Note), + reader.readObject(AztecAddress), + Fr.fromBuffer(reader), + Fr.fromBuffer(reader), + ); + } + + /** + * Serializes the L1NotePayload object into a Buffer. + * @returns Buffer representation of the L1NotePayload object. + */ + toBuffer() { + return serializeToBuffer([this.note, this.contractAddress, this.storageSlot, this.noteTypeId]); + } + + /** + * 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 random(contract = AztecAddress.random()) { + return new L1NotePayload(Note.random(), contract, Fr.random(), Fr.random()); + } + + public encrypt(ephSk: GrumpkinPrivateKey, recipient: AztecAddress, ivpk: PublicKey, ovKeys: KeyValidationRequest) { + return super._encrypt( + this.contractAddress, + ephSk, + recipient, + ivpk, + ovKeys, + new EncryptedNoteLogIncomingBody(this.storageSlot, this.noteTypeId, this.note), + ); + } + + /** + * Decrypts a ciphertext as an incoming log. + * + * This is executable by the recipient of the note, and uses the ivsk to decrypt the payload. + * The outgoing parts of the log are ignored entirely. + * + * Produces the same output as `decryptAsOutgoing`. + * + * @param ciphertext - The ciphertext for the log + * @param ivsk - The incoming viewing secret key, used to decrypt the logs + * @returns The decrypted log payload + */ + public static decryptAsIncoming(ciphertext: Buffer | bigint[], ivsk: GrumpkinPrivateKey) { + const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); + const reader = BufferReader.asReader(input); + + const [address, incomingBody] = super._decryptAsIncoming( + reader.readToEnd(), + ivsk, + EncryptedNoteLogIncomingBody.fromCiphertext, + ); + + return new L1NotePayload(incomingBody.note, address, incomingBody.storageSlot, incomingBody.noteTypeId); + } + + /** + * Decrypts a ciphertext as an outgoing log. + * + * This is executable by the sender of the note, and uses the ovsk to decrypt the payload. + * The outgoing parts are decrypted to retrieve information that allows the sender to + * decrypt the incoming log, and learn about the note contents. + * + * Produces the same output as `decryptAsIncoming`. + * + * @param ciphertext - The ciphertext for the log + * @param ovsk - The outgoing viewing secret key, used to decrypt the logs + * @returns The decrypted log payload + */ + public static decryptAsOutgoing(ciphertext: Buffer | bigint[], ovsk: GrumpkinPrivateKey) { + const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); + const reader = BufferReader.asReader(input); + + const [address, incomingBody] = super._decryptAsOutgoing( + reader.readToEnd(), + ovsk, + EncryptedNoteLogIncomingBody.fromCiphertext, + ); + + return new L1NotePayload(incomingBody.note, address, incomingBody.storageSlot, incomingBody.noteTypeId); + } + + public equals(other: L1NotePayload) { + return ( + this.note.equals(other.note) && + this.contractAddress.equals(other.contractAddress) && + this.storageSlot.equals(other.storageSlot) && + this.noteTypeId.equals(other.noteTypeId) + ); + } +} diff --git a/yarn-project/circuit-types/src/logs/l1_payload/l1_payload.ts b/yarn-project/circuit-types/src/logs/l1_payload/l1_payload.ts new file mode 100644 index 000000000000..4e6b026840a1 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/l1_payload.ts @@ -0,0 +1,169 @@ +import { + type AztecAddress, + type GrumpkinPrivateKey, + type KeyValidationRequest, + type PublicKey, + computeIvpkApp, + computeIvskApp, + computeOvskApp, + derivePublicKeyFromSecretKey, +} from '@aztec/circuits.js'; +import { pedersenHash } from '@aztec/foundation/crypto'; +import { type Fr, Point } from '@aztec/foundation/fields'; +import { BufferReader } from '@aztec/foundation/serialize'; + +import { EncryptedLogHeader } from './encrypted_log_header.js'; +import { type EncryptedLogIncomingBody } from './encrypted_log_incoming_body/index.js'; +import { EncryptedLogOutgoingBody } from './encrypted_log_outgoing_body.js'; + +// Both the incoming and the outgoing header are 48 bytes. +// 32 bytes for the address, and 16 bytes padding to follow PKCS#7 +const HEADER_SIZE = 48; + +// The outgoing body is constant size of 176 bytes. +// 160 bytes for the secret key, address, and public key, and 16 bytes padding to follow PKCS#7 +const OUTGOING_BODY_SIZE = 176; +/** + * A class which wraps event data which is pushed on L1. + */ +export abstract class L1Payload { + /** + * Serializes the L1EventPayload object into a Buffer. + * @returns Buffer representation of the L1EventPayload object. + */ + abstract toBuffer(): Buffer; + + /** + * Encrypts an event payload for a given recipient and sender. + * Creates an incoming log the the recipient using the recipient's ivsk, and + * an outgoing log for the sender using the sender's ovsk. + * + * @param ephSk - An ephemeral secret key used for the encryption + * @param recipient - The recipient address, retrievable by the sender for his logs + * @param ivpk - The incoming viewing public key of the recipient + * @param ovKeys - The outgoing viewing keys of the sender + * @returns A buffer containing the encrypted log payload + * @throws If the ivpk is zero. + */ + protected _encrypt( + contractAddress: AztecAddress, + ephSk: GrumpkinPrivateKey, + recipient: AztecAddress, + ivpk: PublicKey, + ovKeys: KeyValidationRequest, + incomingBody: T, + ) { + if (ivpk.isZero()) { + throw new Error(`Attempting to encrypt an event log with a zero ivpk.`); + } + + const ephPk = derivePublicKeyFromSecretKey(ephSk); + + const header = new EncryptedLogHeader(contractAddress); + + const incomingHeaderCiphertext = header.computeCiphertext(ephSk, ivpk); + const outgoingHeaderCiphertext = header.computeCiphertext(ephSk, ovKeys.pkM); + + const ivpkApp = computeIvpkApp(ivpk, contractAddress); + + const incomingBodyCiphertext = incomingBody.computeCiphertext(ephSk, ivpkApp); + + const outgoingBodyCiphertext = new EncryptedLogOutgoingBody(ephSk, recipient, ivpkApp).computeCiphertext( + ovKeys.skAppAsGrumpkinPrivateKey, + ephPk, + ); + + return Buffer.concat([ + ephPk.toBuffer(), + incomingHeaderCiphertext, + outgoingHeaderCiphertext, + outgoingBodyCiphertext, + incomingBodyCiphertext, + ]); + } + + /** + * Decrypts a ciphertext as an incoming log. + * + * This is executable by the recipient of the event, and uses the ivsk to decrypt the payload. + * The outgoing parts of the log are ignored entirely. + * + * Produces the same output as `decryptAsOutgoing`. + * + * @param encryptedLog - The encrypted log. This encrypted log is assumed to always have tags. + * @param ivsk - The incoming viewing secret key, used to decrypt the logs + * @returns The decrypted log payload + */ + protected static _decryptAsIncoming( + data: Buffer, + ivsk: GrumpkinPrivateKey, + fromCiphertext: (incomingBodySlice: Buffer, ivskApp: GrumpkinPrivateKey, ephPk: Point) => T, + ): [AztecAddress, T] { + const reader = BufferReader.asReader(data); + + const ephPk = reader.readObject(Point); + + const incomingHeader = EncryptedLogHeader.fromCiphertext(reader.readBytes(HEADER_SIZE), ivsk, ephPk); + + // Skipping the outgoing header and body + reader.readBytes(HEADER_SIZE); + reader.readBytes(OUTGOING_BODY_SIZE); + + // The incoming can be of variable size, so we read until the end + const incomingBodySlice = reader.readToEnd(); + + const ivskApp = computeIvskApp(ivsk, incomingHeader.address); + const incomingBody = fromCiphertext(incomingBodySlice, ivskApp, ephPk); + + return [incomingHeader.address, incomingBody]; + } + + /** + * Decrypts a ciphertext as an outgoing log. + * + * This is executable by the sender of the event, and uses the ovsk to decrypt the payload. + * The outgoing parts are decrypted to retrieve information that allows the sender to + * decrypt the incoming log, and learn about the event contents. + * + * Produces the same output as `decryptAsIncoming`. + * + * @param ciphertext - The ciphertext for the log + * @param ovsk - The outgoing viewing secret key, used to decrypt the logs + * @returns The decrypted log payload + */ + protected static _decryptAsOutgoing( + data: Buffer, + ovsk: GrumpkinPrivateKey, + fromCiphertext: (incomingBodySlice: Buffer, ivskApp: GrumpkinPrivateKey, ephPk: Point) => T, + ): [AztecAddress, T] { + const reader = BufferReader.asReader(data); + + const ephPk = reader.readObject(Point); + + reader.readBytes(HEADER_SIZE); + + const outgoingHeader = EncryptedLogHeader.fromCiphertext(reader.readBytes(HEADER_SIZE), ovsk, ephPk); + + const ovskApp = computeOvskApp(ovsk, outgoingHeader.address); + const outgoingBody = EncryptedLogOutgoingBody.fromCiphertext(reader.readBytes(OUTGOING_BODY_SIZE), ovskApp, ephPk); + + // The incoming can be of variable size, so we read until the end + const incomingBodySlice = reader.readToEnd(); + + const incomingBody = fromCiphertext(incomingBodySlice, outgoingBody.ephSk, outgoingBody.recipientIvpkApp); + + return [outgoingHeader.address, incomingBody]; + } + + protected static ensureMatchedMaskedContractAddress( + contractAddress: AztecAddress, + randomness: Fr, + maskedContractAddress: Fr, + ) { + if (!pedersenHash([contractAddress, randomness], 0).equals(maskedContractAddress)) { + throw new Error( + 'The provided masked contract address does not match with the incoming address from header and randomness from body', + ); + } + } +} diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/note.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/payload.test.ts similarity index 50% rename from yarn-project/circuit-types/src/logs/l1_note_payload/note.test.ts rename to yarn-project/circuit-types/src/logs/l1_payload/payload.test.ts index fd55db8121bf..a4fa27d833a5 100644 --- a/yarn-project/circuit-types/src/logs/l1_note_payload/note.test.ts +++ b/yarn-project/circuit-types/src/logs/l1_payload/payload.test.ts @@ -1,6 +1,6 @@ import { Fr } from '@aztec/foundation/fields'; -import { Note } from './note.js'; +import { Event, Note } from './payload.js'; describe('note', () => { it('convert to and from buffer', () => { @@ -10,3 +10,12 @@ describe('note', () => { expect(Note.fromBuffer(buf)).toEqual(note); }); }); + +describe('event', () => { + it('convert to and from buffer', () => { + const fields = Array.from({ length: 5 }).map(() => Fr.random()); + const note = new Event(fields); + const buf = note.toBuffer(); + expect(Event.fromBuffer(buf)).toEqual(note); + }); +}); diff --git a/yarn-project/circuit-types/src/logs/l1_note_payload/note.ts b/yarn-project/circuit-types/src/logs/l1_payload/payload.ts similarity index 89% rename from yarn-project/circuit-types/src/logs/l1_note_payload/note.ts rename to yarn-project/circuit-types/src/logs/l1_payload/payload.ts index ea59a372d716..08e02e56f118 100644 --- a/yarn-project/circuit-types/src/logs/l1_note_payload/note.ts +++ b/yarn-project/circuit-types/src/logs/l1_payload/payload.ts @@ -8,7 +8,7 @@ import { BufferReader } from '@aztec/foundation/serialize'; * This data also represents a preimage to a note hash. This class extends the Vector class, which allows for * additional operations on the underlying field elements. */ -export class Note extends Vector { +export class Payload extends Vector { /** * Create a Note instance from a Buffer or BufferReader. * The input 'buffer' can be either a Buffer containing the serialized Fr elements or a BufferReader instance. @@ -19,7 +19,7 @@ export class Note extends Vector { */ static fromBuffer(buffer: Buffer | BufferReader) { const reader = BufferReader.asReader(buffer); - return new Note(reader.readVector(Fr)); + return new Payload(reader.readVector(Fr)); } /** @@ -32,7 +32,7 @@ export class Note extends Vector { static random() { const numItems = randomInt(10) + 1; const items = Array.from({ length: numItems }, () => Fr.random()); - return new Note(items); + return new Payload(items); } /** @@ -50,7 +50,7 @@ export class Note extends Vector { */ static fromString(str: string) { const hex = str.replace(/^0x/, ''); - return Note.fromBuffer(Buffer.from(hex, 'hex')); + return Payload.fromBuffer(Buffer.from(hex, 'hex')); } get length() { @@ -61,3 +61,7 @@ export class Note extends Vector { return this.items.every((item, index) => item.equals(other.items[index])); } } + +export class Event extends Payload {} + +export class Note extends Payload {} diff --git a/yarn-project/circuit-types/src/logs/l1_payload/tagged_log.test.ts b/yarn-project/circuit-types/src/logs/l1_payload/tagged_log.test.ts new file mode 100644 index 000000000000..c5c7968c965d --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/tagged_log.test.ts @@ -0,0 +1,126 @@ +import { AztecAddress, KeyValidationRequest, computeOvskApp, derivePublicKeyFromSecretKey } from '@aztec/circuits.js'; +import { pedersenHash } from '@aztec/foundation/crypto'; +import { Fr, GrumpkinScalar } from '@aztec/foundation/fields'; + +import { EncryptedL2Log } from '../encrypted_l2_log.js'; +import { L1EventPayload } from './l1_event_payload.js'; +import { L1NotePayload } from './l1_note_payload.js'; +import { Event } from './payload.js'; +import { TaggedLog } from './tagged_log.js'; + +describe('L1 Note Payload', () => { + it('convert to and from buffer', () => { + const payload = L1NotePayload.random(); + const taggedLog = new TaggedLog(payload); + const buf = taggedLog.toBuffer(); + expect(TaggedLog.fromBuffer(buf).payload).toEqual(taggedLog.payload); + }); + + describe('encrypt and decrypt a full log', () => { + let ovskM: GrumpkinScalar; + let ivskM: GrumpkinScalar; + + let taggedLog: TaggedLog; + let encrypted: Buffer; + + beforeAll(() => { + const payload = L1NotePayload.random(); + + ovskM = GrumpkinScalar.random(); + ivskM = GrumpkinScalar.random(); + + const ovKeys = getKeyValidationRequest(ovskM, payload.contractAddress); + + const ephSk = GrumpkinScalar.random(); + + const recipientAddress = AztecAddress.random(); + const ivpk = derivePublicKeyFromSecretKey(ivskM); + + taggedLog = new TaggedLog(payload); + + encrypted = taggedLog.encrypt(ephSk, recipientAddress, ivpk, ovKeys); + }); + + it('decrypt a log as incoming', () => { + const recreated = TaggedLog.decryptAsIncoming(encrypted, ivskM); + + expect(recreated?.toBuffer()).toEqual(taggedLog.toBuffer()); + }); + + it('decrypt a log as outgoing', () => { + const recreated = TaggedLog.decryptAsOutgoing(encrypted, ovskM); + + expect(recreated?.toBuffer()).toEqual(taggedLog.toBuffer()); + }); + }); + + const getKeyValidationRequest = (ovskM: GrumpkinScalar, app: AztecAddress) => { + const ovskApp = computeOvskApp(ovskM, app); + const ovpkM = derivePublicKeyFromSecretKey(ovskM); + + return new KeyValidationRequest(ovpkM, ovskApp); + }; +}); + +describe('L1 Event Payload', () => { + it('convert to and from buffer', () => { + const payload = L1EventPayload.random(); + const taggedLog = new TaggedLog(payload); + const buf = taggedLog.toBuffer(); + expect(TaggedLog.fromBuffer(buf, L1EventPayload).payload).toEqual(taggedLog.payload); + }); + + describe('encrypt and decrypt a full log', () => { + let ovskM: GrumpkinScalar; + let ivskM: GrumpkinScalar; + + let taggedLog: TaggedLog; + let encrypted: Buffer; + let maskedContractAddress: AztecAddress; + let contractAddress: AztecAddress; + let randomness: Fr; + let encryptedL2Log: EncryptedL2Log; + + beforeAll(() => { + contractAddress = AztecAddress.random(); + randomness = Fr.random(); + maskedContractAddress = pedersenHash([contractAddress, randomness], 0); + + const payload = new L1EventPayload(Event.random(), contractAddress, randomness, Fr.random()); + + ovskM = GrumpkinScalar.random(); + ivskM = GrumpkinScalar.random(); + + const ovKeys = getKeyValidationRequest(ovskM, payload.contractAddress); + + const ephSk = GrumpkinScalar.random(); + + const recipientAddress = AztecAddress.random(); + const ivpk = derivePublicKeyFromSecretKey(ivskM); + + taggedLog = new TaggedLog(payload); + + encrypted = taggedLog.encrypt(ephSk, recipientAddress, ivpk, ovKeys); + encryptedL2Log = new EncryptedL2Log(encrypted, maskedContractAddress); + }); + + it('decrypt a log as incoming', () => { + const recreated = TaggedLog.decryptAsIncoming(encryptedL2Log, ivskM, L1EventPayload); + + expect(recreated?.toBuffer()).toEqual(taggedLog.toBuffer()); + }); + + it('decrypt a log as outgoing', () => { + const recreated = TaggedLog.decryptAsOutgoing(encryptedL2Log, ovskM, L1EventPayload); + + expect(recreated?.toBuffer()).toEqual(taggedLog.toBuffer()); + }); + }); + + const getKeyValidationRequest = (ovskM: GrumpkinScalar, app: AztecAddress) => { + const ovskApp = computeOvskApp(ovskM, app); + const ovpkM = derivePublicKeyFromSecretKey(ovskM); + + return new KeyValidationRequest(ovpkM, ovskApp); + }; +}); diff --git a/yarn-project/circuit-types/src/logs/l1_payload/tagged_log.ts b/yarn-project/circuit-types/src/logs/l1_payload/tagged_log.ts new file mode 100644 index 000000000000..4904479acdb2 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/l1_payload/tagged_log.ts @@ -0,0 +1,140 @@ +import { AztecAddress, type GrumpkinPrivateKey, type KeyValidationRequest, type PublicKey } from '@aztec/circuits.js'; +import { Fr } from '@aztec/foundation/fields'; +import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; + +import { type EncryptedL2Log } from '../encrypted_l2_log.js'; +import { L1EventPayload } from './l1_event_payload.js'; +import { L1NotePayload } from './l1_note_payload.js'; + +// placeholder value until tagging is implemented +const PLACEHOLDER_TAG = new Fr(33); + +/** + * Encrypted log payload with a tag used for retrieval by clients. + */ +export class TaggedLog { + constructor(public payload: Payload, public incomingTag = PLACEHOLDER_TAG, public outgoingTag = PLACEHOLDER_TAG) {} + + /** + * Deserializes the TaggedLog object from a Buffer. + * @param buffer - Buffer or BufferReader object to deserialize. + * @returns An instance of TaggedLog. + */ + static fromBuffer(buffer: Buffer | BufferReader, payloadType: typeof L1EventPayload): TaggedLog; + static fromBuffer(buffer: Buffer | BufferReader, payloadType?: typeof L1NotePayload): TaggedLog; + static fromBuffer( + buffer: Buffer | BufferReader, + payloadType: typeof L1NotePayload | typeof L1EventPayload = L1NotePayload, + ) { + const reader = BufferReader.asReader(buffer); + const incomingTag = Fr.fromBuffer(reader); + const outgoingTag = Fr.fromBuffer(reader); + const payload = + payloadType === L1NotePayload ? L1NotePayload.fromBuffer(reader) : L1EventPayload.fromBuffer(reader); + + return new TaggedLog(payload, incomingTag, outgoingTag); + } + + /** + * Serializes the TaggedLog object into a Buffer. + * @returns Buffer representation of the TaggedLog object (unencrypted). + */ + public toBuffer(): Buffer { + return serializeToBuffer(this.incomingTag, this.outgoingTag, this.payload); + } + + static random(payloadType?: typeof L1NotePayload, contract?: AztecAddress): TaggedLog; + static random(payloadType: typeof L1EventPayload): TaggedLog; + static random( + payloadType: typeof L1NotePayload | typeof L1EventPayload = L1NotePayload, + contract = AztecAddress.random(), + ): TaggedLog { + return payloadType === L1NotePayload + ? new TaggedLog(L1NotePayload.random(contract)) + : new TaggedLog(L1EventPayload.random()); + } + + public encrypt( + ephSk: GrumpkinPrivateKey, + recipient: AztecAddress, + ivpk: PublicKey, + ovKeys: KeyValidationRequest, + ): Buffer { + return serializeToBuffer(this.incomingTag, this.outgoingTag, this.payload.encrypt(ephSk, recipient, ivpk, ovKeys)); + } + + static decryptAsIncoming( + encryptedLog: EncryptedL2Log, + ivsk: GrumpkinPrivateKey, + payloadType: typeof L1EventPayload, + ): TaggedLog | undefined; + static decryptAsIncoming( + data: Buffer | bigint[], + ivsk: GrumpkinPrivateKey, + payloadType?: typeof L1NotePayload, + ): TaggedLog | undefined; + static decryptAsIncoming( + data: Buffer | bigint[] | EncryptedL2Log, + ivsk: GrumpkinPrivateKey, + payloadType: typeof L1NotePayload | typeof L1EventPayload = L1NotePayload, + ): TaggedLog | undefined { + // Right now heavily abusing that we will likely fail if bad decryption + // as some field will likely end up not being in the field etc. + try { + if (payloadType === L1EventPayload) { + const reader = BufferReader.asReader((data as EncryptedL2Log).data); + const incomingTag = Fr.fromBuffer(reader); + const outgoingTag = Fr.fromBuffer(reader); + // We must pass the entire encrypted log in. The tags are not stripped here from the original data + const payload = L1EventPayload.decryptAsIncoming(data as EncryptedL2Log, ivsk); + return new TaggedLog(payload, incomingTag, outgoingTag); + } else { + const input = Buffer.isBuffer(data) ? data : Buffer.from((data as bigint[]).map((x: bigint) => Number(x))); + const reader = BufferReader.asReader(input); + const incomingTag = Fr.fromBuffer(reader); + const outgoingTag = Fr.fromBuffer(reader); + const payload = L1NotePayload.decryptAsIncoming(reader.readToEnd(), ivsk); + return new TaggedLog(payload, incomingTag, outgoingTag); + } + } catch (e) { + return; + } + } + + static decryptAsOutgoing( + encryptedLog: EncryptedL2Log, + ivsk: GrumpkinPrivateKey, + payloadType: typeof L1EventPayload, + ): TaggedLog | undefined; + static decryptAsOutgoing( + data: Buffer | bigint[], + ivsk: GrumpkinPrivateKey, + payloadType?: typeof L1NotePayload, + ): TaggedLog | undefined; + static decryptAsOutgoing( + data: Buffer | bigint[] | EncryptedL2Log, + ovsk: GrumpkinPrivateKey, + payloadType: typeof L1NotePayload | typeof L1EventPayload = L1NotePayload, + ) { + // Right now heavily abusing that we will likely fail if bad decryption + // as some field will likely end up not being in the field etc. + try { + if (payloadType === L1EventPayload) { + const reader = BufferReader.asReader((data as EncryptedL2Log).data); + const incomingTag = Fr.fromBuffer(reader); + const outgoingTag = Fr.fromBuffer(reader); + const payload = L1EventPayload.decryptAsOutgoing(data as EncryptedL2Log, ovsk); + return new TaggedLog(payload, incomingTag, outgoingTag); + } else { + const input = Buffer.isBuffer(data) ? data : Buffer.from((data as bigint[]).map((x: bigint) => Number(x))); + const reader = BufferReader.asReader(input); + const incomingTag = Fr.fromBuffer(reader); + const outgoingTag = Fr.fromBuffer(reader); + const payload = L1NotePayload.decryptAsOutgoing(reader.readToEnd(), ovsk); + return new TaggedLog(payload, incomingTag, outgoingTag); + } + } catch (e) { + return; + } + } +} diff --git a/yarn-project/circuit-types/src/notes/extended_note.ts b/yarn-project/circuit-types/src/notes/extended_note.ts index 48b9e53b0f48..caee60e8be9c 100644 --- a/yarn-project/circuit-types/src/notes/extended_note.ts +++ b/yarn-project/circuit-types/src/notes/extended_note.ts @@ -1,7 +1,7 @@ import { AztecAddress, Fr } from '@aztec/circuits.js'; import { BufferReader } from '@aztec/foundation/serialize'; -import { Note } from '../logs/l1_note_payload/note.js'; +import { Note } from '../logs/l1_payload/payload.js'; import { TxHash } from '../tx/tx_hash.js'; /** diff --git a/yarn-project/end-to-end/Earthfile b/yarn-project/end-to-end/Earthfile index 143458fe9e2c..10db3c25b4a8 100644 --- a/yarn-project/end-to-end/Earthfile +++ b/yarn-project/end-to-end/Earthfile @@ -163,6 +163,9 @@ e2e-public-to-private-messaging: e2e-state-vars: DO +E2E_TEST --test=./src/e2e_state_vars.test.ts +e2e-logs: + DO +E2E_TEST --test=./src/e2e_event_logs.test.ts + e2e-static-calls: DO +E2E_TEST --test=./src/e2e_static_calls.test.ts 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 538bacf920f7..9a308a2e523a 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 @@ -6,6 +6,7 @@ import { ContractFunctionInteraction, type DebugLogger, Fr, + L1NotePayload, type PXE, type Wallet, deriveKeys, @@ -18,7 +19,7 @@ import { TokenContract } from '@aztec/noir-contracts.js/Token'; import 'jest-extended'; -import { TaggedNote } from '../../circuit-types/src/logs/l1_note_payload/tagged_note.js'; +import { TaggedLog } from '../../circuit-types/src/logs/l1_payload/tagged_log.js'; import { DUPLICATE_NULLIFIER_ERROR } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; @@ -281,8 +282,8 @@ describe('e2e_block_building', () => { expect(rct.status).toEqual('success'); const decryptedLogs = tx.noteEncryptedLogs .unrollLogs() - .map(l => TaggedNote.decryptAsIncoming(l.data, keys.masterIncomingViewingSecretKey)); - const notevalues = decryptedLogs.map(l => l?.notePayload.note.items[0]); + .map(l => TaggedLog.decryptAsIncoming(l.data, keys.masterIncomingViewingSecretKey, L1NotePayload)); + const notevalues = decryptedLogs.map(l => l?.payload.note.items[0]); expect(notevalues[0]).toEqual(new Fr(10)); expect(notevalues[1]).toEqual(new Fr(11)); expect(notevalues[2]).toEqual(new Fr(12)); diff --git a/yarn-project/end-to-end/src/e2e_encryption.test.ts b/yarn-project/end-to-end/src/e2e_encryption.test.ts index a87653fdc89a..6ca15a59e346 100644 --- a/yarn-project/end-to-end/src/e2e_encryption.test.ts +++ b/yarn-project/end-to-end/src/e2e_encryption.test.ts @@ -1,8 +1,8 @@ import { AztecAddress, EncryptedLogHeader, - EncryptedLogIncomingBody, EncryptedLogOutgoingBody, + EncryptedNoteLogIncomingBody, Fr, GrumpkinScalar, Note, @@ -94,7 +94,7 @@ describe('e2e_encryption', () => { const value = Fr.random(); const note = new Note([value]); - const body = new EncryptedLogIncomingBody(storageSlot, noteTypeId, note); + const body = new EncryptedNoteLogIncomingBody(storageSlot, noteTypeId, note); const encrypted = await contract.methods .compute_incoming_log_body_ciphertext(ephSecretKey, viewingPubKey, storageSlot, value) @@ -104,7 +104,7 @@ describe('e2e_encryption', () => { body.computeCiphertext(ephSecretKey, viewingPubKey), ); - const recreated = EncryptedLogIncomingBody.fromCiphertext(encrypted, viewingSecretKey, ephPubKey); + const recreated = EncryptedNoteLogIncomingBody.fromCiphertext(encrypted, viewingSecretKey, ephPubKey); expect(recreated.toBuffer()).toEqual(body.toBuffer()); }); diff --git a/yarn-project/end-to-end/src/e2e_event_logs.test.ts b/yarn-project/end-to-end/src/e2e_event_logs.test.ts new file mode 100644 index 000000000000..ac9247aad220 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_event_logs.test.ts @@ -0,0 +1,114 @@ +import { type AccountWalletWithSecretKey, type AztecNode, Fr, L1EventPayload, TaggedLog } from '@aztec/aztec.js'; +import { deriveMasterIncomingViewingSecretKey } from '@aztec/circuits.js'; +import { makeTuple } from '@aztec/foundation/array'; +import { TestLogContract } from '@aztec/noir-contracts.js'; + +import { jest } from '@jest/globals'; + +import { publicDeployAccounts, setup } from './fixtures/utils.js'; + +const TIMEOUT = 120_000; + +describe('Logs', () => { + let testLogContract: TestLogContract; + jest.setTimeout(TIMEOUT); + + let wallets: AccountWalletWithSecretKey[]; + let node: AztecNode; + + let teardown: () => Promise; + + beforeAll(async () => { + ({ teardown, wallets, aztecNode: node } = await setup(2)); + + await publicDeployAccounts(wallets[0], wallets.slice(0, 2)); + + testLogContract = await TestLogContract.deploy(wallets[0]).send().deployed(); + }); + + afterAll(() => teardown()); + + describe('functionality around emitting an encrypted log', () => { + it('emits a generic encrypted log and checks for correctness', async () => { + const randomness = Fr.random(); + const eventTypeId = Fr.random(); + const preimage = makeTuple(6, Fr.random); + + const tx = await testLogContract.methods.emit_encrypted_log(randomness, eventTypeId, preimage).send().wait(); + + const txEffect = await node.getTxEffect(tx.txHash); + + const encryptedLogs = txEffect!.encryptedLogs.unrollLogs(); + expect(encryptedLogs.length).toBe(1); + + const decryptedLog = TaggedLog.decryptAsIncoming( + encryptedLogs[0], + deriveMasterIncomingViewingSecretKey(wallets[0].getSecretKey()), + L1EventPayload, + ); + + expect(decryptedLog?.payload.contractAddress).toStrictEqual(testLogContract.address); + expect(decryptedLog?.payload.randomness).toStrictEqual(randomness); + expect(decryptedLog?.payload.eventTypeId).toStrictEqual(eventTypeId); + expect(decryptedLog?.payload.event.items).toStrictEqual(preimage); + }); + + it('emits multiple events as encrypted logs and decodes them', async () => { + const randomness = makeTuple(2, Fr.random); + const preimage = makeTuple(4, Fr.random); + + const tx = await testLogContract.methods.emit_encrypted_events(randomness, preimage).send().wait(); + + const txEffect = await node.getTxEffect(tx.txHash); + + const encryptedLogs = txEffect!.encryptedLogs.unrollLogs(); + expect(encryptedLogs.length).toBe(2); + + const decryptedLog0 = TaggedLog.decryptAsIncoming( + encryptedLogs[0], + deriveMasterIncomingViewingSecretKey(wallets[0].getSecretKey()), + L1EventPayload, + ); + + expect(decryptedLog0?.payload.contractAddress).toStrictEqual(testLogContract.address); + expect(decryptedLog0?.payload.randomness).toStrictEqual(randomness[0]); + expect(decryptedLog0?.payload.eventTypeId).toStrictEqual( + new Fr(0x00000000000000000000000000000000000000000000000000000000aa533f60), + ); + + // We decode our event into the event type + const event0 = TestLogContract.events.ExampleEvent0.decode(decryptedLog0!.payload); + + // We check that the event was decoded correctly + expect(event0?.value0).toStrictEqual(preimage[0]); + expect(event0?.value1).toStrictEqual(preimage[1]); + + // We check that an event that does not match, is not decoded correctly due to an event type id mismatch + const badEvent0 = TestLogContract.events.ExampleEvent1.decode(decryptedLog0!.payload); + expect(badEvent0).toBe(undefined); + + const decryptedLog1 = TaggedLog.decryptAsIncoming( + encryptedLogs[1], + deriveMasterIncomingViewingSecretKey(wallets[0].getSecretKey()), + L1EventPayload, + ); + + expect(decryptedLog1?.payload.contractAddress).toStrictEqual(testLogContract.address); + expect(decryptedLog1?.payload.randomness).toStrictEqual(randomness[1]); + expect(decryptedLog1?.payload.eventTypeId).toStrictEqual( + new Fr(0x00000000000000000000000000000000000000000000000000000000d1be0447), + ); + + // We check our second event, which is a different type + const event1 = TestLogContract.events.ExampleEvent1.decode(decryptedLog1!.payload); + + // We expect the fields to have been populated correctly + expect(event1?.value2).toStrictEqual(preimage[2]); + expect(event1?.value3).toStrictEqual(preimage[3]); + + // Again, trying to decode another event with mismatching data does not yield anything + const badEvent1 = TestLogContract.events.ExampleEvent0.decode(decryptedLog1!.payload); + expect(badEvent1).toBe(undefined); + }); + }); +}); 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 fab94c23c695..59f349b4d0b1 100644 --- a/yarn-project/pxe/src/note_processor/note_processor.test.ts +++ b/yarn-project/pxe/src/note_processor/note_processor.test.ts @@ -1,4 +1,4 @@ -import { type AztecNode, EncryptedL2NoteLog, L2Block, TaggedNote } from '@aztec/circuit-types'; +import { type AztecNode, EncryptedL2NoteLog, L1NotePayload, L2Block, TaggedLog } from '@aztec/circuit-types'; import { AztecAddress, CompleteAddress, @@ -33,7 +33,7 @@ const NUM_NOTE_HASHES_PER_BLOCK = TXS_PER_BLOCK * MAX_NEW_NOTE_HASHES_PER_TX; class MockNoteRequest { constructor( /** Note we want to insert into a block. */ - public readonly note: TaggedNote, + public readonly note: TaggedLog, /** Block number this note corresponds to. */ public readonly blockNumber: number, /** Index of a tx within a block this note corresponds to. */ @@ -107,7 +107,7 @@ describe('Note Processor', () => { // Then we update the relevant note hashes to match the note requests for (const request of noteRequestsForBlock) { const note = request.note; - const noteHash = pedersenHash(note.notePayload.note.items); + const noteHash = pedersenHash(note.payload.note.items); block.body.txEffects[request.txIndex].noteHashes[request.noteHashIndex] = noteHash; // Now we populate the log - to simplify we say that there is only 1 function invocation in each tx @@ -182,7 +182,14 @@ describe('Note Processor', () => { }); it('should store an incoming note that belongs to us', async () => { - const request = new MockNoteRequest(TaggedNote.random(app), 4, 0, 2, ownerIvpkM, KeyValidationRequest.random()); + const request = new MockNoteRequest( + TaggedLog.random(L1NotePayload, app), + 4, + 0, + 2, + ownerIvpkM, + KeyValidationRequest.random(), + ); const blocks = mockBlocks([request]); await noteProcessor.process(blocks); @@ -191,7 +198,7 @@ describe('Note Processor', () => { expect(addNotesSpy).toHaveBeenCalledWith( [ expect.objectContaining({ - ...request.note.notePayload, + ...request.note.payload, index: request.indexWithinNoteHashTree, }), ], @@ -200,23 +207,23 @@ describe('Note Processor', () => { }, 25_000); it('should store an outgoing note that belongs to us', async () => { - const request = new MockNoteRequest(TaggedNote.random(app), 4, 0, 2, Point.random(), ownerOvKeys); + const request = new MockNoteRequest(TaggedLog.random(L1NotePayload, app), 4, 0, 2, Point.random(), ownerOvKeys); const blocks = mockBlocks([request]); await noteProcessor.process(blocks); expect(addNotesSpy).toHaveBeenCalledTimes(1); // For outgoing notes, the resulting DAO does not contain index. - expect(addNotesSpy).toHaveBeenCalledWith([], [expect.objectContaining(request.note.notePayload)]); + expect(addNotesSpy).toHaveBeenCalledWith([], [expect.objectContaining(request.note.payload)]); }, 25_000); it('should store multiple notes that belong to us', async () => { const requests = [ - new MockNoteRequest(TaggedNote.random(app), 1, 1, 1, ownerIvpkM, ownerOvKeys), - new MockNoteRequest(TaggedNote.random(app), 2, 3, 0, Point.random(), ownerOvKeys), - new MockNoteRequest(TaggedNote.random(app), 6, 3, 2, ownerIvpkM, KeyValidationRequest.random()), - new MockNoteRequest(TaggedNote.random(app), 9, 3, 2, Point.random(), KeyValidationRequest.random()), - new MockNoteRequest(TaggedNote.random(app), 12, 3, 2, ownerIvpkM, ownerOvKeys), + new MockNoteRequest(TaggedLog.random(L1NotePayload, app), 1, 1, 1, ownerIvpkM, ownerOvKeys), + new MockNoteRequest(TaggedLog.random(L1NotePayload, app), 2, 3, 0, Point.random(), ownerOvKeys), + new MockNoteRequest(TaggedLog.random(L1NotePayload, app), 6, 3, 2, ownerIvpkM, KeyValidationRequest.random()), + new MockNoteRequest(TaggedLog.random(L1NotePayload, app), 9, 3, 2, Point.random(), KeyValidationRequest.random()), + new MockNoteRequest(TaggedLog.random(L1NotePayload, app), 12, 3, 2, ownerIvpkM, ownerOvKeys), ]; const blocks = mockBlocks(requests); @@ -227,23 +234,23 @@ describe('Note Processor', () => { // Incoming should contain notes from requests 0, 2, 4 because in those requests we set owner ivpk. [ expect.objectContaining({ - ...requests[0].note.notePayload, + ...requests[0].note.payload, index: requests[0].indexWithinNoteHashTree, }), expect.objectContaining({ - ...requests[2].note.notePayload, + ...requests[2].note.payload, index: requests[2].indexWithinNoteHashTree, }), expect.objectContaining({ - ...requests[4].note.notePayload, + ...requests[4].note.payload, index: requests[4].indexWithinNoteHashTree, }), ], // Outgoing should contain notes from requests 0, 1, 4 because in those requests we set owner ovKeys. [ - expect.objectContaining(requests[0].note.notePayload), - expect.objectContaining(requests[1].note.notePayload), - expect.objectContaining(requests[4].note.notePayload), + expect.objectContaining(requests[0].note.payload), + expect.objectContaining(requests[1].note.payload), + expect.objectContaining(requests[4].note.payload), ], ); }, 30_000); @@ -251,8 +258,8 @@ describe('Note Processor', () => { 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 blocks = mockBlocks([ - new MockNoteRequest(TaggedNote.random(), 2, 1, 1, Point.random(), KeyValidationRequest.random()), - new MockNoteRequest(TaggedNote.random(), 2, 3, 0, Point.random(), KeyValidationRequest.random()), + new MockNoteRequest(TaggedLog.random(), 2, 1, 1, Point.random(), KeyValidationRequest.random()), + new MockNoteRequest(TaggedLog.random(), 2, 3, 0, Point.random(), KeyValidationRequest.random()), ]); await noteProcessor.process(blocks); @@ -260,8 +267,8 @@ describe('Note Processor', () => { }); it('should be able to recover two note payloads containing the same note', async () => { - const note = TaggedNote.random(app); - const note2 = TaggedNote.random(app); + const note = TaggedLog.random(L1NotePayload, app); + const note2 = TaggedLog.random(L1NotePayload, app); // All note payloads except one have the same contract address, storage slot, and the actual note. const requests = [ new MockNoteRequest(note, 3, 0, 0, ownerIvpkM, ownerOvKeys), @@ -278,11 +285,11 @@ describe('Note Processor', () => { { const addedIncoming: IncomingNoteDao[] = addNotesSpy.mock.calls[0][0]; expect(addedIncoming.map(dao => dao)).toEqual([ - expect.objectContaining({ ...requests[0].note.notePayload, index: requests[0].indexWithinNoteHashTree }), - expect.objectContaining({ ...requests[1].note.notePayload, index: requests[1].indexWithinNoteHashTree }), - expect.objectContaining({ ...requests[2].note.notePayload, index: requests[2].indexWithinNoteHashTree }), - expect.objectContaining({ ...requests[3].note.notePayload, index: requests[3].indexWithinNoteHashTree }), - expect.objectContaining({ ...requests[4].note.notePayload, index: requests[4].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[0].note.payload, index: requests[0].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[1].note.payload, index: requests[1].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[2].note.payload, index: requests[2].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[3].note.payload, index: requests[3].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[4].note.payload, index: requests[4].indexWithinNoteHashTree }), ]); // Check that every note has a different nonce. @@ -295,11 +302,11 @@ describe('Note Processor', () => { { const addedOutgoing: OutgoingNoteDao[] = addNotesSpy.mock.calls[0][1]; expect(addedOutgoing.map(dao => dao)).toEqual([ - expect.objectContaining(requests[0].note.notePayload), - expect.objectContaining(requests[1].note.notePayload), - expect.objectContaining(requests[2].note.notePayload), - expect.objectContaining(requests[3].note.notePayload), - expect.objectContaining(requests[4].note.notePayload), + expect.objectContaining(requests[0].note.payload), + expect.objectContaining(requests[1].note.payload), + expect.objectContaining(requests[2].note.payload), + expect.objectContaining(requests[3].note.payload), + expect.objectContaining(requests[4].note.payload), ]); // Outgoing note daos do not have a nonce so we don't check it. @@ -307,7 +314,7 @@ describe('Note Processor', () => { }); it('advances the block number', async () => { - const request = new MockNoteRequest(TaggedNote.random(), 6, 0, 2, ownerIvpkM, ownerOvKeys); + const request = new MockNoteRequest(TaggedLog.random(), 6, 0, 2, ownerIvpkM, ownerOvKeys); const blocks = mockBlocks([request]); await noteProcessor.process(blocks); @@ -316,7 +323,7 @@ describe('Note Processor', () => { }); it('should restore the last block number processed and ignore the starting block', async () => { - const request = new MockNoteRequest(TaggedNote.random(), 6, 0, 2, Point.random(), KeyValidationRequest.random()); + const request = new MockNoteRequest(TaggedLog.random(), 6, 0, 2, Point.random(), KeyValidationRequest.random()); const blocks = mockBlocks([request]); await noteProcessor.process(blocks); diff --git a/yarn-project/pxe/src/note_processor/note_processor.ts b/yarn-project/pxe/src/note_processor/note_processor.ts index 6ea5a55e7fe3..e7a2d4d95e70 100644 --- a/yarn-project/pxe/src/note_processor/note_processor.ts +++ b/yarn-project/pxe/src/note_processor/note_processor.ts @@ -1,4 +1,4 @@ -import { type AztecNode, L1NotePayload, type L2Block, TaggedNote } from '@aztec/circuit-types'; +import { type AztecNode, L1NotePayload, type L2Block, TaggedLog } from '@aztec/circuit-types'; import { type NoteProcessorStats } from '@aztec/circuit-types/stats'; import { type AztecAddress, @@ -147,19 +147,19 @@ export class NoteProcessor { for (const functionLogs of txFunctionLogs) { for (const log of functionLogs.logs) { this.stats.seen++; - const incomingTaggedNote = TaggedNote.decryptAsIncoming(log.data, ivskM)!; - const outgoingTaggedNote = TaggedNote.decryptAsOutgoing(log.data, ovskM)!; + const incomingTaggedNote = TaggedLog.decryptAsIncoming(log.data, ivskM)!; + const outgoingTaggedNote = TaggedLog.decryptAsOutgoing(log.data, ovskM)!; if (incomingTaggedNote || outgoingTaggedNote) { if ( incomingTaggedNote && outgoingTaggedNote && - !incomingTaggedNote.notePayload.equals(outgoingTaggedNote.notePayload) + !incomingTaggedNote.payload.equals(outgoingTaggedNote.payload) ) { throw new Error('Incoming and outgoing note payloads do not match.'); } - const payload = incomingTaggedNote?.notePayload || outgoingTaggedNote?.notePayload; + const payload = incomingTaggedNote?.payload || outgoingTaggedNote?.payload; const txHash = block.body.txEffects[indexOfTxInABlock].txHash; const { incomingNote, outgoingNote, incomingDeferredNote } = await produceNoteDaos( diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 1bd7a445ac65..590f0542e1b6 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -289,15 +289,15 @@ export class Oracle { return newValues.map(toACVMField); } - emitEncryptedLog( + emitEncryptedEventLog( [contractAddress]: ACVMField[], [randomness]: ACVMField[], - encryptedLog: ACVMField[], + encryptedEvent: ACVMField[], [counter]: ACVMField[], ): void { // Convert each field to a number and then to a buffer (1 byte is stored in 1 field) - const processedInput = Buffer.from(encryptedLog.map(fromACVMField).map(f => f.toNumber())); - this.typedOracle.emitEncryptedLog( + const processedInput = Buffer.from(encryptedEvent.map(fromACVMField).map(f => f.toNumber())); + this.typedOracle.emitEncryptedEventLog( AztecAddress.fromString(contractAddress), Fr.fromString(randomness), processedInput, @@ -311,7 +311,36 @@ export class Oracle { this.typedOracle.emitEncryptedNoteLog(+noteHashCounter, processedInput, +counter); } - computeEncryptedLog( + computeEncryptedEventLog( + [contractAddress]: ACVMField[], + [randomness]: ACVMField[], + [eventTypeId]: ACVMField[], + [ovskApp]: ACVMField[], + [ovpkMX]: ACVMField[], + [ovpkMY]: ACVMField[], + [ivpkMX]: ACVMField[], + [ivpkMY]: ACVMField[], + preimage: ACVMField[], + ): ACVMField[] { + const ovpkM = new Point(fromACVMField(ovpkMX), fromACVMField(ovpkMY)); + const ovKeys = new KeyValidationRequest(ovpkM, Fr.fromString(ovskApp)); + const ivpkM = new Point(fromACVMField(ivpkMX), fromACVMField(ivpkMY)); + const encLog = this.typedOracle.computeEncryptedEventLog( + AztecAddress.fromString(contractAddress), + Fr.fromString(randomness), + Fr.fromString(eventTypeId), + ovKeys, + ivpkM, + preimage.map(fromACVMField), + ); + const bytes: ACVMField[] = []; + encLog.forEach(v => { + bytes.push(toACVMField(v)); + }); + return bytes; + } + + computeEncryptedNoteLog( [contractAddress]: ACVMField[], [storageSlot]: ACVMField[], [noteTypeId]: ACVMField[], @@ -325,7 +354,7 @@ export class Oracle { const ovpkM = new Point(fromACVMField(ovpkMX), fromACVMField(ovpkMY)); const ovKeys = new KeyValidationRequest(ovpkM, Fr.fromString(ovskApp)); const ivpkM = new Point(fromACVMField(ivpkMX), fromACVMField(ivpkMY)); - const encLog = this.typedOracle.computeEncryptedLog( + const encLog = this.typedOracle.computeEncryptedNoteLog( AztecAddress.fromString(contractAddress), Fr.fromString(storageSlot), Fr.fromString(noteTypeId), diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 230b82b180a1..41fd2f7e37b9 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -184,15 +184,31 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('storageWrite'); } - emitEncryptedLog(_contractAddress: AztecAddress, _randomness: Fr, _encryptedNote: Buffer, _counter: number): void { - throw new OracleMethodNotAvailableError('emitEncryptedLog'); + emitEncryptedEventLog( + _contractAddress: AztecAddress, + _randomness: Fr, + _encryptedEvent: Buffer, + _counter: number, + ): void { + throw new OracleMethodNotAvailableError('emitEncryptedEventLog'); } emitEncryptedNoteLog(_noteHashCounter: number, _encryptedNote: Buffer, _counter: number): void { throw new OracleMethodNotAvailableError('emitEncryptedNoteLog'); } - computeEncryptedLog( + computeEncryptedEventLog( + _contractAddress: AztecAddress, + _randomness: Fr, + _eventTypeId: Fr, + _ovKeys: KeyValidationRequest, + _ivpkM: PublicKey, + _preimage: Fr[], + ): Buffer { + throw new OracleMethodNotAvailableError('computeEncryptedEventLog'); + } + + computeEncryptedNoteLog( _contractAddress: AztecAddress, _storageSlot: Fr, _noteTypeId: Fr, @@ -200,7 +216,7 @@ export abstract class TypedOracle { _ivpkM: PublicKey, _preimage: Fr[], ): Buffer { - throw new OracleMethodNotAvailableError('computeEncryptedLog'); + throw new OracleMethodNotAvailableError('computeEncryptedNoteLog'); } emitUnencryptedLog(_log: UnencryptedL2Log, _counter: number): void { diff --git a/yarn-project/simulator/src/client/client_execution_context.ts b/yarn-project/simulator/src/client/client_execution_context.ts index 1309dc26323e..161a4f06c532 100644 --- a/yarn-project/simulator/src/client/client_execution_context.ts +++ b/yarn-project/simulator/src/client/client_execution_context.ts @@ -3,10 +3,12 @@ import { type AztecNode, EncryptedL2Log, EncryptedL2NoteLog, + Event, + L1EventPayload, L1NotePayload, Note, type NoteStatus, - TaggedNote, + TaggedLog, type UnencryptedL2Log, } from '@aztec/circuit-types'; import { @@ -330,13 +332,15 @@ export class ClientExecutionContext extends ViewDataOracle { /** * Emit encrypted data - * @param encryptedNote - The encrypted data. + * @param contractAddress - The contract emitting the encrypted event. + * @param randomness - A value used to mask the contract address we are siloing with. + * @param encryptedEvent - The encrypted event data. * @param counter - The effects counter. */ - public override emitEncryptedLog( + public override emitEncryptedEventLog( contractAddress: AztecAddress, randomness: Fr, - encryptedData: Buffer, + encryptedEvent: Buffer, counter: number, ) { // In some cases, we actually want to reveal the contract address we are siloing with: @@ -345,7 +349,7 @@ export class ClientExecutionContext extends ViewDataOracle { const maskedContractAddress = randomness.isZero() ? contractAddress.toField() : pedersenHash([contractAddress, randomness], 0); - const encryptedLog = new CountedLog(new EncryptedL2Log(encryptedData, maskedContractAddress), counter); + const encryptedLog = new CountedLog(new EncryptedL2Log(encryptedEvent, maskedContractAddress), counter); this.encryptedLogs.push(encryptedLog); } @@ -360,6 +364,34 @@ export class ClientExecutionContext extends ViewDataOracle { this.noteEncryptedLogs.push(encryptedLog); } + /** + * Encrypt an event + * @param contractAddress - The contract emitting the encrypted event. + * @param randomness - A value used to mask the contract address we are siloing with. + * @param eventTypeId - The type ID of the event (function selector). + * @param ovKeys - The outgoing viewing keys to use to encrypt. + * @param ivpkM - The master incoming viewing public key. + * @param preimage - The event preimage. + */ + public override computeEncryptedEventLog( + contractAddress: AztecAddress, + randomness: Fr, + eventTypeId: Fr, + ovKeys: KeyValidationRequest, + ivpkM: Point, + preimage: Fr[], + ) { + const event = new Event(preimage); + const l1EventPayload = new L1EventPayload(event, contractAddress, randomness, eventTypeId); + const taggedEvent = new TaggedLog(l1EventPayload); + + const ephSk = GrumpkinScalar.random(); + + const recipient = AztecAddress.random(); + + return taggedEvent.encrypt(ephSk, recipient, ivpkM, ovKeys); + } + /** * Encrypt a note * @param contractAddress - The contract address of the note. @@ -369,7 +401,7 @@ export class ClientExecutionContext extends ViewDataOracle { * @param ivpkM - The master incoming viewing public key. * @param preimage - The note preimage. */ - public override computeEncryptedLog( + public override computeEncryptedNoteLog( contractAddress: AztecAddress, storageSlot: Fr, noteTypeId: Fr, @@ -379,7 +411,7 @@ export class ClientExecutionContext extends ViewDataOracle { ) { const note = new Note(preimage); const l1NotePayload = new L1NotePayload(note, contractAddress, storageSlot, noteTypeId); - const taggedNote = new TaggedNote(l1NotePayload); + const taggedNote = new TaggedLog(l1NotePayload); const ephSk = GrumpkinScalar.random();