From cc10c2a09d77b3550738fdc0bfaab9b5222ee2ec Mon Sep 17 00:00:00 2001 From: Aztec Bot <49558828+AztecBot@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:38:28 -0400 Subject: [PATCH 01/22] feat!: scoped capsules (backport #21533) (#21986) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Backport of https://github.com/AztecProtocol/aztec-packages/pull/21533 (feat!: scoped capsules) to v4-next. The automatic cherry-pick failed due to conflicts in 7 files. This PR preserves the full cherry-pick history: 1. **Cherry-pick with conflicts** — raw cherry-pick of `3029216084` with conflict markers committed as-is 2. **Conflict resolution** — resolved all 7 conflicted files: - `docs/docs-developers/docs/resources/migration_notes.md` — added new migration notes under TBD - `noir-projects/aztec-nr/aztec/src/macros/aztec.nr` — took PR's `$offchain_inbox_sync_option` macro variable + `scope` param - `noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr` — added `scope` param to `get_private_logs` call - `yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.ts` — accepted deletion (PR removes this file) - `yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts` — renamed method + added scope param - `yarn-project/pxe/src/logs/log_service.ts` — took PR's simplified single-recipient approach, moved deduplication into `#getSecretsForSenders` - `yarn-project/pxe/src/oracle_version.ts` — took PR's oracle interface hash 3. **Build fixes** — added missing `offchain_inbox_sync_option` variable definition and updated `generate_sync_state` function signature in `aztec.nr` ClaudeBox log: https://claudebox.work/s/358dd3e35e1d8542?run=1 --- .../docs/resources/migration_notes.md | 76 ++++- .../aztec-nr/aztec/src/capsules/mod.nr | 95 +++++-- .../aztec-nr/aztec/src/macros/aztec.nr | 65 ++--- .../aztec/src/messages/discovery/mod.nr | 18 +- .../src/messages/discovery/partial_notes.nr | 12 +- .../src/messages/discovery/private_events.nr | 2 +- .../src/messages/discovery/private_notes.nr | 6 +- .../src/messages/discovery/process_message.nr | 12 +- .../processing/event_validation_request.nr | 3 - .../messages/processing/message_context.nr | 22 +- .../aztec/src/messages/processing/mod.nr | 92 +++--- .../processing/note_validation_request.nr | 3 - .../aztec/src/messages/processing/offchain.nr | 81 +++--- .../messages/processing/pending_tagged_log.nr | 5 +- .../aztec-nr/aztec/src/oracle/capsules.nr | 131 +++++---- .../aztec/src/oracle/message_processing.nr | 15 +- .../aztec-nr/aztec/src/oracle/version.nr | 2 +- .../src/test/helpers/test_environment.nr | 20 +- .../app/token_blacklist_contract/src/main.nr | 6 +- .../aztec_sublib/src/oracle/capsules.nr | 37 ++- .../src/main.nr | 2 + .../test/custom_message_contract/src/main.nr | 13 +- .../storage_proof_test_contract/src/main.nr | 22 +- .../src/e2e_offchain_effect.test.ts | 105 +++---- .../src/e2e_offchain_payment.test.ts | 61 +++- .../event_validation_request.test.ts | 5 +- .../noir-structs/event_validation_request.ts | 3 - .../noir-structs/message_tx_context.test.ts | 172 ----------- .../noir-structs/message_tx_context.ts | 55 ---- .../note_validation_request.test.ts | 5 +- .../noir-structs/note_validation_request.ts | 3 - .../oracle/interfaces.ts | 19 +- .../oracle/legacy_oracle_mappings.ts | 40 +-- .../oracle/oracle.ts | 39 ++- .../oracle/private_execution.test.ts | 29 +- .../oracle/utility_execution.test.ts | 115 ++++++-- .../oracle/utility_execution_oracle.ts | 92 ++++-- .../contract_sync_service.test.ts | 41 +-- .../contract_sync/contract_sync_service.ts | 33 ++- yarn-project/pxe/src/contract_sync/helpers.ts | 5 +- yarn-project/pxe/src/logs/log_service.ts | 86 +++--- .../messages/message_context_service.test.ts | 10 +- .../src/messages/message_context_service.ts | 7 +- yarn-project/pxe/src/notes/note_service.ts | 5 +- yarn-project/pxe/src/oracle_version.ts | 4 +- yarn-project/pxe/src/pxe.ts | 1 + .../capsule_store/capsule_store.test.ts | 269 +++++++++++------- .../storage/capsule_store/capsule_store.ts | 60 ++-- yarn-project/pxe/src/storage/metadata.ts | 2 +- .../stdlib/src/logs/message_context.test.ts | 34 ++- .../stdlib/src/logs/message_context.ts | 24 +- .../src/logs/pending_tagged_log.test.ts | 5 +- .../stdlib/src/logs/pending_tagged_log.ts | 4 +- yarn-project/stdlib/src/tx/capsule.ts | 12 +- yarn-project/txe/src/rpc_translator.ts | 40 ++- yarn-project/txe/src/state_machine/index.ts | 3 + yarn-project/txe/src/txe_session.ts | 10 +- 57 files changed, 1190 insertions(+), 948 deletions(-) delete mode 100644 yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.test.ts delete mode 100644 yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.ts diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 4ecfbf93a216..ddbc36293e6e 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,80 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.nr] Capsule operations are now addressed by scope + +All capsule operations (`store`, `load`, `delete`, `copy`) and `CapsuleArray` now require a `scope: AztecAddress` parameter. This scopes capsule storage by address, providing isolation between different accounts within the same PXE. + +Contracts that use `CapsuleArray` directly also need to update. + +**Migration:** + +```diff +- let array: CapsuleArray = CapsuleArray::at(contract_address, slot); ++ let array: CapsuleArray = CapsuleArray::at(contract_address, slot, scope); +``` + +The low-level capsule functions are similarly affected: + +```diff +- capsules::store(contract_address, slot, value); ++ capsules::store(contract_address, slot, value, scope); + +- capsules::load(contract_address, slot); ++ capsules::load(contract_address, slot, scope); + +- capsules::delete(contract_address, slot); ++ capsules::delete(contract_address, slot, scope); + +- capsules::copy(contract_address, src_slot, dst_slot, num_entries); ++ capsules::copy(contract_address, src_slot, dst_slot, num_entries, scope); +``` + +If you need to stick the old, scope-less behavior, and you are really sure that that's what you need to use, you can use `scope = AztecAddress::zero()`. + +### [Aztec.nr] `process_message` utility function removed + +The auto-generated `process_message` utility function has been removed. If you need to deliver offchain messages (messages not broadcast via onchain logs), use the `offchain_receive` utility function instead. This function is automatically injected by the `#[aztec]` macro and accepts messages into a persistent inbox scoped by recipient. These messages are then picked up and processed during `sync_state`. + +**Impact**: Contracts that explicitly called `process_message` must switch to delivering messages via `offchain_receive` and letting `sync_state` handle processing. + +### [Aztec.nr] `CustomMessageHandler` type signature changed + +The `CustomMessageHandler` function type now receives an additional `scope: AztecAddress` parameter: + +```diff + type CustomMessageHandler = unconstrained fn( + AztecAddress, // contract_address + u64, // msg_type_id + u64, // msg_metadata + BoundedVec, // msg_content + MessageContext, // message_context ++ AztecAddress, // scope + ); +``` + +**Impact**: Contracts that implement a custom message handler must update the function signature. + +### [aztec.js] `DeployMethod.send()` always returns `{ contract, receipt, instance }` + +The `returnReceipt` option in deploy wait options has been removed. `DeployMethod.send()` now always returns an object with `contract`, `receipt`, and `instance` at the top level, provided the user waits for the transaction to be included. + +The `DeployTxReceipt` and `DeployWaitOptions` types have been removed. + +**Migration:** + +```diff +- const { +- receipt: { contract, instance }, +- } = await MyContract.deploy(wallet, ...args).send({ +- from: address, +- wait: { returnReceipt: true }, +- }); + ++ const { contract, instance } = await MyContract.deploy(wallet, ...args).send({ ++ from: address, ++ }); +``` ### [aztec.js] `isContractInitialized` is now `initializationStatus` tri-state enum `ContractMetadata.isContractInitialized` has been renamed to `ContractMetadata.initializationStatus` and changed from `boolean | undefined` to a `ContractInitializationStatus` enum with values `INITIALIZED`, `UNINITIALIZED`, and `UNKNOWN`. @@ -97,7 +171,7 @@ Most contracts are not affected, as the macro-generated `sync_state` and `proces **Migration:** ```diff - attempt_note_discovery( + attempt_note_discovery( contract_address, tx_hash, unique_note_hashes_in_tx, diff --git a/noir-projects/aztec-nr/aztec/src/capsules/mod.nr b/noir-projects/aztec-nr/aztec/src/capsules/mod.nr index ec8af10da3b2..c9060b7efcc4 100644 --- a/noir-projects/aztec-nr/aztec/src/capsules/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/capsules/mod.nr @@ -10,20 +10,25 @@ pub struct CapsuleArray { /// after the base slot. For example, with base slot 5: the length is at slot 5, the first element (index 0) is at /// slot 6, the second element (index 1) is at slot 7, and so on. base_slot: Field, + /// Scope for capsule isolation. Capsule operations are scoped to the given address, allowing multiple independent + /// namespaces within the same contract. + scope: AztecAddress, } impl CapsuleArray { - /// Returns a CapsuleArray connected to a contract's capsules at a base slot. Array elements are stored in - /// contiguous slots following the base slot, so there should be sufficient space between array base slots to - /// accommodate elements. A reasonable strategy is to make the base slot a hash of a unique value. - pub unconstrained fn at(contract_address: AztecAddress, base_slot: Field) -> Self { - Self { contract_address, base_slot } + /// Returns a CapsuleArray scoped to a specific address. + /// + /// Array elements are stored in contiguous slots + /// following the base slot, so there should be sufficient space between array base slots to accommodate elements. + /// A reasonable strategy is to make the base slot a hash of a unique value. + pub unconstrained fn at(contract_address: AztecAddress, base_slot: Field, scope: AztecAddress) -> Self { + Self { contract_address, base_slot, scope } } /// Returns the number of elements stored in the array. pub unconstrained fn len(self) -> u32 { // An uninitialized array defaults to a length of 0. - capsules::load(self.contract_address, self.base_slot).unwrap_or(0) as u32 + capsules::load(self.contract_address, self.base_slot, self.scope).unwrap_or(0) as u32 } /// Stores a value at the end of the array. @@ -35,11 +40,21 @@ impl CapsuleArray { // The slot corresponding to the index `current_length` is the first slot immediately after the end of the // array, which is where we want to place the new value. - capsules::store(self.contract_address, self.slot_at(current_length), value); + capsules::store( + self.contract_address, + self.slot_at(current_length), + value, + self.scope, + ); // Then we simply update the length. let new_length = current_length + 1; - capsules::store(self.contract_address, self.base_slot, new_length); + capsules::store( + self.contract_address, + self.base_slot, + new_length, + self.scope, + ); } /// Retrieves the value stored in the array at `index`. Throws if the index is out of bounds. @@ -49,7 +64,7 @@ impl CapsuleArray { { assert(index < self.len(), "Attempted to read past the length of a CapsuleArray"); - capsules::load(self.contract_address, self.slot_at(index)).unwrap() + capsules::load(self.contract_address, self.slot_at(index), self.scope).unwrap() } /// Deletes the value stored in the array at `index`. Throws if the index is out of bounds. @@ -67,13 +82,23 @@ impl CapsuleArray { self.slot_at(index + 1), self.slot_at(index), current_length - index - 1, + self.scope, ); } // We can now delete the last element (which has either been copied to the slot immediately before it, or was // the element we meant to delete in the first place) and update the length. - capsules::delete(self.contract_address, self.slot_at(current_length - 1)); - capsules::store(self.contract_address, self.base_slot, current_length - 1); + capsules::delete( + self.contract_address, + self.slot_at(current_length - 1), + self.scope, + ); + capsules::store( + self.contract_address, + self.base_slot, + current_length - 1, + self.scope, + ); } /// Calls a function on each element of the array. @@ -124,10 +149,12 @@ impl CapsuleArray { } mod test { + use crate::protocol::address::AztecAddress; use crate::test::helpers::test_environment::TestEnvironment; use super::CapsuleArray; global SLOT: Field = 1230; + global SCOPE: AztecAddress = AztecAddress { inner: 0xface }; #[test] unconstrained fn empty_array() { @@ -135,7 +162,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array: CapsuleArray = CapsuleArray::at(contract_address, SLOT); + let array: CapsuleArray = CapsuleArray::at(contract_address, SLOT, SCOPE); assert_eq(array.len(), 0); }); } @@ -146,7 +173,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); let _: Field = array.get(0); }); } @@ -157,7 +184,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(5); assert_eq(array.len(), 1); @@ -171,7 +198,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(5); let _ = array.get(1); @@ -184,7 +211,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(5); array.remove(0); @@ -199,7 +226,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(7); array.push(8); @@ -224,7 +251,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(7); array.push(8); @@ -243,7 +270,7 @@ mod test { let env = TestEnvironment::new(); env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(4); array.push(5); @@ -266,7 +293,7 @@ mod test { let env = TestEnvironment::new(); env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(4); array.push(5); @@ -289,7 +316,7 @@ mod test { let env = TestEnvironment::new(); env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(4); array.push(5); @@ -306,7 +333,7 @@ mod test { let env = TestEnvironment::new(); env.private_context(|context| { let contract_address = context.this_address(); - let array = CapsuleArray::at(contract_address, SLOT); + let array = CapsuleArray::at(contract_address, SLOT, SCOPE); array.push(4); array.push(5); @@ -321,4 +348,28 @@ mod test { assert_eq(mock.times_called(), 0); }); } + + #[test] + unconstrained fn different_scopes_are_isolated() { + let env = TestEnvironment::new(); + env.private_context(|context| { + let contract_address = context.this_address(); + let scope_a = AztecAddress { inner: 0xaaa }; + let scope_b = AztecAddress { inner: 0xbbb }; + + let array_a = CapsuleArray::at(contract_address, SLOT, scope_a); + let array_b = CapsuleArray::at(contract_address, SLOT, scope_b); + + array_a.push(10); + array_a.push(20); + array_b.push(99); + + assert_eq(array_a.len(), 2); + assert_eq(array_a.get(0), 10); + assert_eq(array_a.get(1), 20); + + assert_eq(array_b.len(), 1); + assert_eq(array_b.get(0), 99); + }); + } } diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index 0f1c3585a935..932aeb3c93ea 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -101,8 +101,8 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { let fn_abi_exports = create_fn_abi_exports(m); // We generate `_compute_note_hash`, `_compute_note_nullifier` (and the deprecated - // `_compute_note_hash_and_nullifier` wrapper), `sync_state` and `process_message` functions only if they are not - // already implemented. If they are implemented we just insert empty quotes. + // `_compute_note_hash_and_nullifier` wrapper) and `sync_state` functions only if they are not already implemented. + // If they are implemented we just insert empty quotes. let contract_library_method_compute_note_hash_and_nullifier = if !m.functions().any(|f| { // Note that we don't test for `_compute_note_hash` or `_compute_note_nullifier` in order to make this simpler // - users must either implement all three or none. @@ -120,8 +120,12 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { quote { Option::>::none() } }; + let offchain_inbox_sync_option = quote { + Option::some(aztec::messages::processing::offchain::sync_inbox) + }; + let sync_state_fn_and_abi_export = if !m.functions().any(|f| f.name() == quote { sync_state }) { - generate_sync_state(process_custom_message_option) + generate_sync_state(process_custom_message_option, offchain_inbox_sync_option) } else { quote {} }; @@ -133,11 +137,6 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { } let offchain_receive_fn_and_abi_export = generate_offchain_receive(); - let process_message_fn_and_abi_export = if !m.functions().any(|f| f.name() == quote { process_message }) { - generate_process_message(process_custom_message_option) - } else { - quote {} - }; let (has_public_init_nullifier_fn, emit_public_init_nullifier_fn_body) = generate_emit_public_init_nullifier(m); let public_dispatch = generate_public_dispatch(m, has_public_init_nullifier_fn); @@ -150,7 +149,6 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { $contract_library_method_compute_note_hash_and_nullifier $public_dispatch $sync_state_fn_and_abi_export - $process_message_fn_and_abi_export $emit_public_init_nullifier_fn_body $offchain_receive_fn_and_abi_export } @@ -221,9 +219,11 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } /// Generates the `sync_state` utility function that performs message discovery. -comptime fn generate_sync_state(process_custom_message_option: Quoted) -> Quoted { +comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_inbox_sync_option: Quoted) -> Quoted { quote { - pub struct sync_state_parameters {} + pub struct sync_state_parameters { + pub scope: aztec::protocol::address::AztecAddress, + } #[abi(functions)] pub struct sync_state_abi { @@ -231,55 +231,20 @@ comptime fn generate_sync_state(process_custom_message_option: Quoted) -> Quoted } #[aztec::macros::internals_functions_generation::abi_attributes::abi_utility] - unconstrained fn sync_state() { + unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) { let address = aztec::context::UtilityContext::new().this_address(); aztec::messages::discovery::do_sync_state( address, _compute_note_hash, _compute_note_nullifier, $process_custom_message_option, - Option::some(aztec::messages::processing::offchain::sync_inbox), + $offchain_inbox_sync_option, + scope, ); } } } -/// Generates the `process_message` utility function that processes a single message ciphertext. -comptime fn generate_process_message(process_custom_message_option: Quoted) -> Quoted { - quote { - pub struct process_message_parameters { - pub message_ciphertext: BoundedVec, - pub message_context: aztec::messages::processing::MessageContext, - } - - #[abi(functions)] - pub struct process_message_abi { - parameters: process_message_parameters, - } - - #[aztec::macros::internals_functions_generation::abi_attributes::abi_utility] - unconstrained fn process_message( - message_ciphertext: BoundedVec, - message_context: aztec::messages::processing::MessageContext, - ) { - let address = aztec::context::UtilityContext::new().this_address(); - - aztec::messages::discovery::process_message::process_message_ciphertext( - address, - _compute_note_hash, - _compute_note_nullifier, - $process_custom_message_option, - message_ciphertext, - message_context, - ); - - // At this point, the note is pending validation and storage in the database. We must call - // validate_and_store_enqueued_notes_and_events to complete that process. - aztec::messages::processing::validate_and_store_enqueued_notes_and_events(address); - } - } -} - /// Generates an `offchain_receive` utility function that lets callers add messages to the offchain message inbox. /// /// For more details, see `aztec::messages::processing::offchain::receive`. @@ -299,6 +264,8 @@ comptime fn generate_offchain_receive() -> Quoted { /// Receives offchain messages into this contract's offchain inbox for subsequent processing. /// + /// Each message is routed to the inbox scoped to its `recipient` field. + /// /// For more details, see `aztec::messages::processing::offchain::receive`. /// /// This function is automatically injected by the `#[aztec]` macro. diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index cdc926291c07..dafa8c331f93 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -103,7 +103,8 @@ pub type CustomMessageHandler = unconstrained fn[Env]( /* msg_type_id */ u64, /* msg_metadata */ u64, /* msg_content */ BoundedVec, -/* message_context */ MessageContext); +/* message_context */ MessageContext, +/* scope */ AztecAddress); /// Synchronizes the contract's private state with the network. /// @@ -119,12 +120,13 @@ pub unconstrained fn do_sync_state( compute_note_nullifier: ComputeNoteNullifier, process_custom_message: Option>, offchain_inbox_sync: Option>, + scope: AztecAddress, ) { aztecnr_debug_log!("Performing state synchronization"); // First we process all private logs, which can contain different kinds of messages e.g. private notes, partial // notes, private events, etc. - let mut logs = get_private_logs(contract_address); + let logs = get_private_logs(contract_address, scope); logs.for_each(|i, pending_tagged_log: PendingTaggedLog| { if pending_tagged_log.log.len() == 0 { aztecnr_warn_log_format!("Skipping empty log from tx {0}")([pending_tagged_log.context.tx_hash]); @@ -141,6 +143,7 @@ pub unconstrained fn do_sync_state( process_custom_message, message_ciphertext, pending_tagged_log.context, + scope, ); } @@ -152,7 +155,7 @@ pub unconstrained fn do_sync_state( }); if offchain_inbox_sync.is_some() { - let msgs: CapsuleArray = offchain_inbox_sync.unwrap()(contract_address); + let msgs: CapsuleArray = offchain_inbox_sync.unwrap()(contract_address, scope); msgs.for_each(|i, msg| { process_message_ciphertext( contract_address, @@ -161,6 +164,7 @@ pub unconstrained fn do_sync_state( process_custom_message, msg.message_ciphertext, msg.message_context, + scope, ); // The inbox sync returns _a copy_ of messages to process, so we clear them as we do so. This is a // volatile array with the to-process message, not the actual persistent storage of them. @@ -174,11 +178,12 @@ pub unconstrained fn do_sync_state( contract_address, compute_note_hash, compute_note_nullifier, + scope, ); // Finally we validate all notes and events that were found as part of the previous processes, resulting in them // being added to PXE's database and retrievable via oracles (get_notes) and our TS API (PXE::getPrivateEvents). - validate_and_store_enqueued_notes_and_events(contract_address); + validate_and_store_enqueued_notes_and_events(contract_address, scope); } mod test { @@ -195,6 +200,8 @@ mod test { }; use crate::protocol::address::AztecAddress; + global SCOPE: AztecAddress = AztecAddress { inner: 0xcafe }; + #[test] unconstrained fn do_sync_state_does_not_panic_on_empty_logs() { let env = TestEnvironment::new(); @@ -204,7 +211,7 @@ mod test { env.utility_context_at(contract_address, |_| { let base_slot = PENDING_TAGGED_LOG_ARRAY_BASE_SLOT; - let logs: CapsuleArray = CapsuleArray::at(contract_address, base_slot); + let logs: CapsuleArray = CapsuleArray::at(contract_address, base_slot, SCOPE); logs.push(PendingTaggedLog { log: BoundedVec::new(), context: std::mem::zeroed() }); assert_eq(logs.len(), 1); @@ -216,6 +223,7 @@ mod test { dummy_compute_note_nullifier, no_handler, no_inbox_sync, + SCOPE, ); assert_eq(logs.len(), 0); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr index e57c82caa0d0..58a8a6d5e514 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr @@ -30,15 +30,14 @@ pub(crate) struct DeliveredPendingPartialNote { pub(crate) note_completion_log_tag: Field, pub(crate) note_type_id: Field, pub(crate) packed_private_note_content: BoundedVec, - pub(crate) recipient: AztecAddress, } pub(crate) unconstrained fn process_partial_note_private_msg( contract_address: AztecAddress, - recipient: AztecAddress, msg_metadata: u64, msg_content: BoundedVec, tx_hash: Field, + scope: AztecAddress, ) { let decoded = decode_partial_note_private_message(msg_metadata, msg_content); @@ -53,12 +52,12 @@ pub(crate) unconstrained fn process_partial_note_private_msg( note_completion_log_tag, note_type_id, packed_private_note_content, - recipient, }; CapsuleArray::at( contract_address, DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, + scope, ) .push(pending); } else { @@ -76,10 +75,12 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( contract_address: AztecAddress, compute_note_hash: ComputeNoteHash, compute_note_nullifier: ComputeNoteNullifier, + scope: AztecAddress, ) { let pending_partial_notes = CapsuleArray::at( contract_address, DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, + scope, ); aztecnr_debug_log_format!("{} pending partial notes")([pending_partial_notes.len() as Field]); @@ -87,7 +88,8 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( // Each of the pending partial notes might get completed by a log containing its public values. For performance // reasons, we fetch all of these logs concurrently and then process them one by one, minimizing the amount of time // waiting for the node roundtrip. - let maybe_completion_logs = get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes); + let maybe_completion_logs = + get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes, scope); // Each entry in the maybe completion logs array corresponds to the entry in the pending partial notes array at the // same index. This means we can use the same index as we iterate through the responses to get both the partial @@ -165,7 +167,7 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( discovered_note.note_hash, discovered_note.inner_nullifier, log.tx_hash, - pending_partial_note.recipient, + scope, ); }); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr index 5276b0191f7f..0e03905f20f3 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr @@ -9,10 +9,10 @@ use crate::protocol::{address::AztecAddress, logging::debug_log_format, traits:: pub(crate) unconstrained fn process_private_event_msg( contract_address: AztecAddress, - recipient: AztecAddress, msg_metadata: u64, msg_content: BoundedVec, tx_hash: Field, + recipient: AztecAddress, ) { let decoded = decode_private_event_message(msg_metadata, msg_content); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr index f2c806211fb0..c711f1e3a5d6 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr @@ -14,11 +14,11 @@ pub(crate) unconstrained fn process_private_note_msg( tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - recipient: AztecAddress, compute_note_hash: ComputeNoteHash, compute_note_nullifier: ComputeNoteNullifier, msg_metadata: u64, msg_content: BoundedVec, + recipient: AztecAddress, ) { let decoded = decode_private_note_message(msg_metadata, msg_content); @@ -30,7 +30,6 @@ pub(crate) unconstrained fn process_private_note_msg( tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, - recipient, compute_note_hash, compute_note_nullifier, owner, @@ -38,6 +37,7 @@ pub(crate) unconstrained fn process_private_note_msg( randomness, note_type_id, packed_note, + recipient, ); } else { aztecnr_debug_log_format!( @@ -55,7 +55,6 @@ pub unconstrained fn attempt_note_discovery( tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - recipient: AztecAddress, compute_note_hash: ComputeNoteHash, compute_note_nullifier: ComputeNoteNullifier, owner: AztecAddress, @@ -63,6 +62,7 @@ pub unconstrained fn attempt_note_discovery( randomness: Field, note_type_id: Field, packed_note: BoundedVec, + recipient: AztecAddress, ) { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr index 1becbd92cecd..978f3557809d 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr @@ -33,8 +33,9 @@ pub unconstrained fn process_message_ciphertext( process_custom_message: Option>, message_ciphertext: BoundedVec, message_context: MessageContext, + recipient: AztecAddress, ) { - let message_plaintext_option = AES128::decrypt(message_ciphertext, message_context.recipient); + let message_plaintext_option = AES128::decrypt(message_ciphertext, recipient); if message_plaintext_option.is_some() { process_message_plaintext( @@ -44,6 +45,7 @@ pub unconstrained fn process_message_ciphertext( process_custom_message, message_plaintext_option.unwrap(), message_context, + recipient, ); } else { aztecnr_warn_log_format!("Could not decrypt message ciphertext from tx {0}, ignoring")([message_context.tx_hash]); @@ -57,6 +59,7 @@ pub(crate) unconstrained fn process_message_plaintext( process_custom_message: Option>, message_plaintext: BoundedVec, message_context: MessageContext, + recipient: AztecAddress, ) { // The first thing to do after decrypting the message is to determine what type of message we're processing. We // have 3 message types: private notes, partial notes and events. @@ -75,31 +78,31 @@ pub(crate) unconstrained fn process_message_plaintext( message_context.tx_hash, message_context.unique_note_hashes_in_tx, message_context.first_nullifier_in_tx, - message_context.recipient, compute_note_hash, compute_note_nullifier, msg_metadata, msg_content, + recipient, ); } else if msg_type_id == PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID { aztecnr_debug_log!("Processing partial note private msg"); process_partial_note_private_msg( contract_address, - message_context.recipient, msg_metadata, msg_content, message_context.tx_hash, + recipient, ); } else if msg_type_id == PRIVATE_EVENT_MSG_TYPE_ID { aztecnr_debug_log!("Processing private event msg"); process_private_event_msg( contract_address, - message_context.recipient, msg_metadata, msg_content, message_context.tx_hash, + recipient, ); } else if msg_type_id < MIN_CUSTOM_MSG_TYPE_ID { // The message type ID falls in the range reserved for aztec.nr built-in types but wasn't matched above. @@ -117,6 +120,7 @@ pub(crate) unconstrained fn process_message_plaintext( msg_metadata, msg_content, message_context, + recipient, ); } else { // A custom message was received but no handler is configured. This likely means the contract emits custom diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr index b5337e61e507..98ef074b84e5 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr @@ -12,7 +12,6 @@ pub(crate) struct EventValidationRequest { pub serialized_event: BoundedVec, pub event_commitment: Field, pub tx_hash: Field, - pub recipient: AztecAddress, } mod test { @@ -29,7 +28,6 @@ mod test { serialized_event: BoundedVec::from_array([4, 5]), event_commitment: 6, tx_hash: 7, - recipient: AztecAddress::from_field(8), }; // We define the serialization in Noir and the deserialization in TS. If the deserialization changes from the @@ -46,7 +44,6 @@ mod test { 2, // bounded_vec_len 6, // event_commitment 7, // tx_hash - 8, // recipient ]; assert_eq(request.serialize(), expected_serialization); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr index b98b4bdeff20..4f456bf6003b 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr @@ -1,4 +1,4 @@ -use crate::protocol::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX, traits::{Deserialize, Serialize}}; +use crate::protocol::{constants::MAX_NOTE_HASHES_PER_TX, traits::{Deserialize, Serialize}}; /// Additional information needed to process a message. /// @@ -10,26 +10,11 @@ pub struct MessageContext { pub tx_hash: Field, pub unique_note_hashes_in_tx: BoundedVec, pub first_nullifier_in_tx: Field, - pub recipient: AztecAddress, -} - -/// Transaction context needed to process a message. -/// -/// Like [`MessageContext`], but `MessageTxContext` does not include the recipient. MessageTxContext's are kind of -/// adhoc: they are just the minimal data structure that the contract needs to get from a PXE oracle to prepare -/// offchain messages to be processed. We reify it with a type just because it crosses Noir<->TS boundaries. -/// The contract knows how to pair the context data with a recipient: then it is able to build a `MessageContext` for -/// subsequent processing. -#[derive(Serialize, Deserialize, Eq)] -pub(crate) struct MessageTxContext { - pub tx_hash: Field, - pub unique_note_hashes_in_tx: BoundedVec, - pub first_nullifier_in_tx: Field, } mod test { use crate::messages::processing::MessageContext; - use crate::protocol::{address::AztecAddress, traits::{Deserialize, FromField}}; + use crate::protocol::traits::Deserialize; #[test] unconstrained fn message_context_serialization_matches_typescript() { @@ -37,14 +22,12 @@ mod test { let tx_hash = 123; let unique_note_hashes = BoundedVec::from_array([4, 5]); let first_nullifier = 6; - let recipient = AztecAddress::from_field(789); // Create a MessageContext instance let message_context = MessageContext { tx_hash, unique_note_hashes_in_tx: unique_note_hashes, first_nullifier_in_tx: first_nullifier, - recipient, }; // Expected output generated from TypeScript's `MessageContext.toFields()` @@ -116,7 +99,6 @@ mod test { 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000000000000000000000000000006, - 0x0000000000000000000000000000000000000000000000000000000000000315, ]; let deserialized = MessageContext::deserialize(serialized_message_context_from_typescript); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 6adfbc1d43e9..9ab82066ffd9 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -3,7 +3,6 @@ pub mod offchain; mod message_context; pub use message_context::MessageContext; -pub(crate) use message_context::MessageTxContext; pub(crate) mod note_validation_request; pub(crate) mod log_retrieval_request; @@ -54,14 +53,16 @@ pub struct OffchainMessageWithContext { pub message_context: MessageContext, } -/// Searches for private logs emitted by `contract_address` that might contain messages for one of the local accounts, -/// and stores them in a `CapsuleArray` which is then returned. -pub(crate) unconstrained fn get_private_logs(contract_address: AztecAddress) -> CapsuleArray { +/// Searches for private logs emitted by `contract_address` that might contain messages for the given `scope`. +pub(crate) unconstrained fn get_private_logs( + contract_address: AztecAddress, + scope: AztecAddress, +) -> CapsuleArray { // We will eventually perform log discovery via tagging here, but for now we simply call the `fetchTaggedLogs` // oracle. This makes PXE synchronize tags, download logs and store the pending tagged logs in a capsule array. - oracle::message_processing::fetch_tagged_logs(PENDING_TAGGED_LOG_ARRAY_BASE_SLOT); + oracle::message_processing::fetch_tagged_logs(PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, scope); - CapsuleArray::at(contract_address, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT) + CapsuleArray::at(contract_address, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, scope) } /// Enqueues a note for validation and storage by PXE. @@ -82,7 +83,7 @@ pub(crate) unconstrained fn get_private_logs(contract_address: AztecAddress) -> /// `owner` is the address used in note hash and nullifier computation, often requiring knowledge of their nullifier /// secret key. /// -/// `recipient` is the account to which the note message was delivered (i.e. the address the message was encrypted to). +/// `scope` is the account to which the note message was delivered (i.e. the address the message was encrypted to). /// This determines which PXE account can see the note - other accounts will not be able to access it (e.g. other /// accounts will not be able to see one another's token balance notes, even in the same PXE) unless authorized. In /// most cases `recipient` equals `owner`, but they can differ in scenarios like delegated discovery. @@ -96,24 +97,28 @@ pub unconstrained fn enqueue_note_for_validation( note_hash: Field, nullifier: Field, tx_hash: Field, - recipient: AztecAddress, + scope: AztecAddress, ) { // We store requests in a `CapsuleArray`, which PXE will later read from and deserialize into its version of the // Noir `NoteValidationRequest` - CapsuleArray::at(contract_address, NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).push( - NoteValidationRequest { - contract_address, - owner, - storage_slot, - randomness, - note_nonce, - packed_note, - note_hash, - nullifier, - tx_hash, - recipient, - }, + CapsuleArray::at( + contract_address, + NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, + scope, ) + .push( + NoteValidationRequest { + contract_address, + owner, + storage_slot, + randomness, + note_nonce, + packed_note, + note_hash, + nullifier, + tx_hash, + }, + ) } /// Enqueues an event for validation and storage by PXE. @@ -135,21 +140,25 @@ pub unconstrained fn enqueue_event_for_validation( serialized_event: BoundedVec, event_commitment: Field, tx_hash: Field, - recipient: AztecAddress, + scope: AztecAddress, ) { // We store requests in a `CapsuleArray`, which PXE will later read from and deserialize into its version of the // Noir `EventValidationRequest` - CapsuleArray::at(contract_address, EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).push( - EventValidationRequest { - contract_address, - event_type_id, - randomness, - serialized_event, - event_commitment, - tx_hash, - recipient, - }, + CapsuleArray::at( + contract_address, + EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, + scope, ) + .push( + EventValidationRequest { + contract_address, + event_type_id, + randomness, + serialized_event, + event_commitment, + tx_hash, + }, + ) } /// Validates and stores all enqueued notes and events. @@ -159,13 +168,14 @@ pub unconstrained fn enqueue_event_for_validation( /// API (PXE::getPrivateEvents). /// /// This automatically clears both validation request queues, so no further work needs to be done by the caller. -pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_address: AztecAddress) { +pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_address: AztecAddress, scope: AztecAddress) { oracle::message_processing::validate_and_store_enqueued_notes_and_events( contract_address, NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, MAX_NOTE_PACKED_LEN as Field, MAX_EVENT_SERIALIZED_LEN as Field, + scope, ); } @@ -178,11 +188,13 @@ pub unconstrained fn resolve_message_contexts( contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, + scope: AztecAddress, ) { oracle::message_processing::resolve_message_contexts( contract_address, message_context_requests_array_base_slot, message_context_responses_array_base_slot, + scope, ); } @@ -196,8 +208,13 @@ pub unconstrained fn resolve_message_contexts( pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( contract_address: AztecAddress, pending_partial_notes: CapsuleArray, + scope: AztecAddress, ) -> CapsuleArray> { - let log_retrieval_requests = CapsuleArray::at(contract_address, LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT); + let log_retrieval_requests = CapsuleArray::at( + contract_address, + LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT, + scope, + ); // We create a LogRetrievalRequest for each PendingPartialNote in the CapsuleArray. Because we need the indices in // the request array to match the indices in the partial note array, we can't use CapsuleArray::for_each, as that @@ -217,7 +234,12 @@ pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( contract_address, LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT, LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, + scope, ); - CapsuleArray::at(contract_address, LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT) + CapsuleArray::at( + contract_address, + LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, + scope, + ) } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr index d845284f2f23..0d7c101eef38 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr @@ -15,7 +15,6 @@ pub(crate) struct NoteValidationRequest { pub note_hash: Field, pub nullifier: Field, pub tx_hash: Field, - pub recipient: AztecAddress, } mod test { @@ -34,7 +33,6 @@ mod test { note_hash: 6, nullifier: 7, tx_hash: 8, - recipient: AztecAddress::from_field(9), }; // We define the serialization in Noir and the deserialization in TS. If the deserialization changes from the @@ -59,7 +57,6 @@ mod test { 0x0000000000000000000000000000000000000000000000000000000000000006, 0x0000000000000000000000000000000000000000000000000000000000000007, 0x0000000000000000000000000000000000000000000000000000000000000008, - 0x0000000000000000000000000000000000000000000000000000000000000009, ]; assert_eq(request.serialize(), expected_serialization); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr index d33e2a8af2fc..987bf9590080 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr @@ -3,7 +3,7 @@ use crate::{ context::UtilityContext, messages::{ encoding::MESSAGE_CIPHERTEXT_LEN, - processing::{MessageContext, MessageTxContext, OffchainMessageWithContext, resolve_message_contexts}, + processing::{MessageContext, OffchainMessageWithContext, resolve_message_contexts}, }, oracle::contract_sync::invalidate_contract_sync_cache, protocol::{ @@ -54,7 +54,7 @@ global MAX_MSG_TTL: u64 = MAX_TX_LIFETIME + TX_EXPIRATION_TOLERANCE; /// The only current implementation of an `OffchainInboxSync` is [`sync_inbox`], which manages an inbox with expiration /// based eviction and automatic transaction context resolution. pub(crate) type OffchainInboxSync = unconstrained fn[Env]( -/* contract_address */AztecAddress) -> CapsuleArray; +/* contract_address */AztecAddress, /* scope */ AztecAddress) -> CapsuleArray; /// A message delivered via the `offchain_receive` utility function. pub struct OffchainMessage { @@ -93,6 +93,9 @@ struct PendingOffchainMsg { /// calls this function to hand the messages to the contract so they can be processed through the same mechanisms as /// onchain messages. /// +/// Each message is routed to the inbox scoped to its `recipient` field, so messages for different accounts are +/// automatically isolated. +/// /// Messages are processed when their originating transaction is found onchain (providing the context needed to /// validate resulting notes and events). /// @@ -104,7 +107,6 @@ pub unconstrained fn receive( contract_address: AztecAddress, messages: BoundedVec, ) { - let inbox: CapsuleArray = CapsuleArray::at(contract_address, OFFCHAIN_INBOX_SLOT); // May contain duplicates if multiple messages target the same recipient. This is harmless since // cache invalidation on the TS side is idempotent (deleting an already-deleted key is a no-op). let mut scopes: BoundedVec = BoundedVec::new(); @@ -117,6 +119,8 @@ pub unconstrained fn receive( } else { 0 }; + let inbox: CapsuleArray = + CapsuleArray::at(contract_address, OFFCHAIN_INBOX_SLOT, msg.recipient); inbox.push( PendingOffchainMsg { ciphertext: msg.ciphertext, @@ -136,13 +140,17 @@ pub unconstrained fn receive( /// /// Messages remain in the inbox and are reprocessed on each sync until their originating transaction is no longer at /// risk of being dropped by a reorg. -pub unconstrained fn sync_inbox(address: AztecAddress) -> CapsuleArray { - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); - let context_resolution_requests: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_CONTEXT_REQUESTS_SLOT); - let resolved_contexts: CapsuleArray> = - CapsuleArray::at(address, OFFCHAIN_CONTEXT_RESPONSES_SLOT); +pub unconstrained fn sync_inbox( + contract_address: AztecAddress, + scope: AztecAddress, +) -> CapsuleArray { + let inbox: CapsuleArray = CapsuleArray::at(contract_address, OFFCHAIN_INBOX_SLOT, scope); + let context_resolution_requests: CapsuleArray = + CapsuleArray::at(contract_address, OFFCHAIN_CONTEXT_REQUESTS_SLOT, scope); + let resolved_contexts: CapsuleArray> = + CapsuleArray::at(contract_address, OFFCHAIN_CONTEXT_RESPONSES_SLOT, scope); let ready_to_process: CapsuleArray = - CapsuleArray::at(address, OFFCHAIN_READY_MESSAGES_SLOT); + CapsuleArray::at(contract_address, OFFCHAIN_READY_MESSAGES_SLOT, scope); // Clear any stale ready messages from a previous run. ready_to_process.for_each(|i, _| { ready_to_process.remove(i); }); @@ -162,9 +170,10 @@ pub unconstrained fn sync_inbox(address: AztecAddress) -> CapsuleArray CapsuleArray CapsuleArray, anchor_block_timestamp: u64) -> OffchainMessage { - OffchainMessage { - ciphertext: BoundedVec::new(), - recipient: AztecAddress::from_field(42), - tx_hash, - anchor_block_timestamp, - } + OffchainMessage { ciphertext: BoundedVec::new(), recipient: SCOPE, tx_hash, anchor_block_timestamp } } /// Advances the TXE block timestamp by `offset` seconds and returns the resulting timestamp. @@ -263,9 +261,9 @@ mod test { unconstrained fn empty_inbox_returns_empty_result() { let env = TestEnvironment::new(); env.utility_context(|context| { - let result = sync_inbox(context.this_address()); + let result = sync_inbox(context.this_address(), SCOPE); let inbox: CapsuleArray = - CapsuleArray::at(context.this_address(), OFFCHAIN_INBOX_SLOT); + CapsuleArray::at(context.this_address(), OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); assert_eq(inbox.len(), 0); @@ -288,8 +286,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // context is None, not ready assert_eq(inbox.len(), 0); // expired, removed @@ -312,8 +310,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // context is None, not ready assert_eq(inbox.len(), 1); // not expired, stays @@ -336,8 +334,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // context is None, not ready assert_eq(inbox.len(), 0); // expired, removed @@ -359,8 +357,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // not resolved, not ready assert_eq(inbox.len(), 1); // not expired, stays @@ -392,8 +390,8 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); assert_eq(result.len(), 0); // all contexts are None // Message 0 expired (anchor=0), message 1 survived (anchor=anchor_ts), @@ -424,15 +422,14 @@ mod test { env.utility_context(|context| { let address = context.this_address(); - let result = sync_inbox(address); - let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT); + let result = sync_inbox(address, SCOPE); + let inbox: CapsuleArray = CapsuleArray::at(address, OFFCHAIN_INBOX_SLOT, SCOPE); // The message should be ready to process since its tx context was resolved. assert_eq(result.len(), 1); let ctx = result.get(0).message_context; assert_eq(ctx.tx_hash, known_tx_hash); - assert_eq(ctx.recipient, AztecAddress::from_field(42)); assert(ctx.first_nullifier_in_tx != 0, "resolved context must have a first nullifier"); // Message stays in inbox (not expired) for potential reorg reprocessing. diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr index 6739fb8d3253..7a85decfb504 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr @@ -12,7 +12,7 @@ pub(crate) struct PendingTaggedLog { mod test { use crate::messages::processing::MessageContext; - use crate::protocol::{address::AztecAddress, traits::{Deserialize, FromField}}; + use crate::protocol::traits::Deserialize; use super::PendingTaggedLog; #[test] @@ -21,14 +21,12 @@ mod test { let tx_hash = 123; let unique_note_hashes = BoundedVec::from_array([4, 5]); let first_nullifier = 6; - let recipient = AztecAddress::from_field(789); let pending_log = PendingTaggedLog { log, context: MessageContext { tx_hash, unique_note_hashes_in_tx: unique_note_hashes, first_nullifier_in_tx: first_nullifier, - recipient, }, }; @@ -119,7 +117,6 @@ mod test { 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000000000000000000000000000006, - 0x0000000000000000000000000000000000000000000000000000000000000315, ]; let deserialized = PendingTaggedLog::deserialize(serialized_pending_tagged_log_from_typescript); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr b/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr index 93b3b07d67b5..2d5f2c84148a 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr @@ -2,39 +2,50 @@ use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; /// Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `load`. If /// data was already stored at this slot, it is overwritten. -pub unconstrained fn store(contract_address: AztecAddress, slot: Field, value: T) +pub unconstrained fn store(contract_address: AztecAddress, slot: Field, value: T, scope: AztecAddress) where T: Serialize, { let serialized = value.serialize(); - store_oracle(contract_address, slot, serialized); + store_oracle(contract_address, slot, serialized, scope); } -/// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns Option::none() -/// if nothing was stored at the given slot. -pub unconstrained fn load(contract_address: AztecAddress, slot: Field) -> Option +/// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns +/// Option::none() if nothing was stored at the given slot. +pub unconstrained fn load(contract_address: AztecAddress, slot: Field, scope: AztecAddress) -> Option where T: Deserialize, { - let serialized_option = load_oracle(contract_address, slot, ::N); + let serialized_option = load_oracle(contract_address, slot, ::N, scope); serialized_option.map(|arr| Deserialize::deserialize(arr)) } /// Deletes data in the per-contract non-volatile database. Does nothing if no data was present. -pub unconstrained fn delete(contract_address: AztecAddress, slot: Field) { - delete_oracle(contract_address, slot); +pub unconstrained fn delete(contract_address: AztecAddress, slot: Field, scope: AztecAddress) { + delete_oracle(contract_address, slot, scope); } /// Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data /// structures by avoiding repeated calls to `loadCapsule` and `storeCapsule`. Supports overlapping source and /// destination regions (which will result in the overlapped source values being overwritten). All copied slots must /// exist in the database (i.e. have been stored and not deleted) -pub unconstrained fn copy(contract_address: AztecAddress, src_slot: Field, dst_slot: Field, num_entries: u32) { - copy_oracle(contract_address, src_slot, dst_slot, num_entries); +pub unconstrained fn copy( + contract_address: AztecAddress, + src_slot: Field, + dst_slot: Field, + num_entries: u32, + scope: AztecAddress, +) { + copy_oracle(contract_address, src_slot, dst_slot, num_entries, scope); } #[oracle(aztec_utl_storeCapsule)] -unconstrained fn store_oracle(contract_address: AztecAddress, slot: Field, values: [Field; N]) {} +unconstrained fn store_oracle( + contract_address: AztecAddress, + slot: Field, + values: [Field; N], + scope: AztecAddress, +) {} /// We need to pass in `array_len` (the value of N) as a parameter to tell the oracle how many fields the response must /// have. @@ -48,13 +59,20 @@ unconstrained fn load_oracle( contract_address: AztecAddress, slot: Field, array_len: u32, + scope: AztecAddress, ) -> Option<[Field; N]> {} #[oracle(aztec_utl_deleteCapsule)] -unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field) {} +unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field, scope: AztecAddress) {} #[oracle(aztec_utl_copyCapsule)] -unconstrained fn copy_oracle(contract_address: AztecAddress, src_slot: Field, dst_slot: Field, num_entries: u32) {} +unconstrained fn copy_oracle( + contract_address: AztecAddress, + src_slot: Field, + dst_slot: Field, + num_entries: u32, + scope: AztecAddress, +) {} mod test { // These tests are sort of redundant since we already test the oracle implementation directly in TypeScript, but @@ -68,6 +86,7 @@ mod test { use crate::protocol::{address::AztecAddress, traits::{FromField, ToField}}; global SLOT: Field = 1; + global SCOPE: AztecAddress = AztecAddress { inner: 0xcafe }; #[test] unconstrained fn stores_and_loads() { @@ -76,9 +95,9 @@ mod test { let contract_address = context.this_address(); let value = MockStruct::new(5, 6); - store(contract_address, SLOT, value); + store(contract_address, SLOT, value, SCOPE); - assert_eq(load(contract_address, SLOT).unwrap(), value); + assert_eq(load(contract_address, SLOT, SCOPE).unwrap(), value); }); } @@ -89,12 +108,12 @@ mod test { let contract_address = context.this_address(); let value = MockStruct::new(5, 6); - store(contract_address, SLOT, value); + store(contract_address, SLOT, value, SCOPE); let new_value = MockStruct::new(7, 8); - store(contract_address, SLOT, new_value); + store(contract_address, SLOT, new_value, SCOPE); - assert_eq(load(contract_address, SLOT).unwrap(), new_value); + assert_eq(load(contract_address, SLOT, SCOPE).unwrap(), new_value); }); } @@ -104,7 +123,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - let loaded_value: Option = load(contract_address, SLOT); + let loaded_value: Option = load(contract_address, SLOT, SCOPE); assert_eq(loaded_value, Option::none()); }); } @@ -116,10 +135,10 @@ mod test { let contract_address = context.this_address(); let value = MockStruct::new(5, 6); - store(contract_address, SLOT, value); - delete(contract_address, SLOT); + store(contract_address, SLOT, value, SCOPE); + delete(contract_address, SLOT, SCOPE); - let loaded_value: Option = load(contract_address, SLOT); + let loaded_value: Option = load(contract_address, SLOT, SCOPE); assert_eq(loaded_value, Option::none()); }); } @@ -130,8 +149,8 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - delete(contract_address, SLOT); - let loaded_value: Option = load(contract_address, SLOT); + delete(contract_address, SLOT, SCOPE); + let loaded_value: Option = load(contract_address, SLOT, SCOPE); assert_eq(loaded_value, Option::none()); }); } @@ -145,16 +164,16 @@ mod test { let src = 5; let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; - store(contract_address, src, values[0]); - store(contract_address, src + 1, values[1]); - store(contract_address, src + 2, values[2]); + store(contract_address, src, values[0], SCOPE); + store(contract_address, src + 1, values[1], SCOPE); + store(contract_address, src + 2, values[2], SCOPE); let dst = 10; - copy(contract_address, src, dst, 3); + copy(contract_address, src, dst, 3, SCOPE); - assert_eq(load(contract_address, dst).unwrap(), values[0]); - assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); - assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + assert_eq(load(contract_address, dst, SCOPE).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1, SCOPE).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2, SCOPE).unwrap(), values[2]); }); } @@ -167,21 +186,21 @@ mod test { let src = 1; let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; - store(contract_address, src, values[0]); - store(contract_address, src + 1, values[1]); - store(contract_address, src + 2, values[2]); + store(contract_address, src, values[0], SCOPE); + store(contract_address, src + 1, values[1], SCOPE); + store(contract_address, src + 2, values[2], SCOPE); let dst = 2; - copy(contract_address, src, dst, 3); + copy(contract_address, src, dst, 3, SCOPE); - assert_eq(load(contract_address, dst).unwrap(), values[0]); - assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); - assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + assert_eq(load(contract_address, dst, SCOPE).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1, SCOPE).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2, SCOPE).unwrap(), values[2]); // src[1] and src[2] should have been overwritten since they are also dst[0] and dst[1] - assert_eq(load(contract_address, src).unwrap(), values[0]); // src[0] (unchanged) - assert_eq(load(contract_address, src + 1).unwrap(), values[0]); // dst[0] - assert_eq(load(contract_address, src + 2).unwrap(), values[1]); // dst[1] + assert_eq(load(contract_address, src, SCOPE).unwrap(), values[0]); // src[0] (unchanged) + assert_eq(load(contract_address, src + 1, SCOPE).unwrap(), values[0]); // dst[0] + assert_eq(load(contract_address, src + 2, SCOPE).unwrap(), values[1]); // dst[1] }); } @@ -194,21 +213,21 @@ mod test { let src = 2; let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; - store(contract_address, src, values[0]); - store(contract_address, src + 1, values[1]); - store(contract_address, src + 2, values[2]); + store(contract_address, src, values[0], SCOPE); + store(contract_address, src + 1, values[1], SCOPE); + store(contract_address, src + 2, values[2], SCOPE); let dst = 1; - copy(contract_address, src, dst, 3); + copy(contract_address, src, dst, 3, SCOPE); - assert_eq(load(contract_address, dst).unwrap(), values[0]); - assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); - assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + assert_eq(load(contract_address, dst, SCOPE).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1, SCOPE).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2, SCOPE).unwrap(), values[2]); // src[0] and src[1] should have been overwritten since they are also dst[1] and dst[2] - assert_eq(load(contract_address, src).unwrap(), values[1]); // dst[1] - assert_eq(load(contract_address, src + 1).unwrap(), values[2]); // dst[2] - assert_eq(load(contract_address, src + 2).unwrap(), values[2]); // src[2] (unchanged) + assert_eq(load(contract_address, src, SCOPE).unwrap(), values[1]); // dst[1] + assert_eq(load(contract_address, src + 1, SCOPE).unwrap(), values[2]); // dst[2] + assert_eq(load(contract_address, src + 2, SCOPE).unwrap(), values[2]); // src[2] (unchanged) }); } @@ -218,7 +237,7 @@ mod test { env.private_context(|context| { let contract_address = context.this_address(); - copy(contract_address, SLOT, SLOT, 1); + copy(contract_address, SLOT, SLOT, 1, SCOPE); }); } @@ -230,7 +249,7 @@ mod test { let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); let value = MockStruct::new(5, 6); - store(other_contract_address, SLOT, value); + store(other_contract_address, SLOT, value, SCOPE); }); } @@ -241,7 +260,7 @@ mod test { let contract_address = context.this_address(); let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); - let _: Option = load(other_contract_address, SLOT); + let _: Option = load(other_contract_address, SLOT, SCOPE); }); } @@ -252,7 +271,7 @@ mod test { let contract_address = context.this_address(); let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); - delete(other_contract_address, SLOT); + delete(other_contract_address, SLOT, SCOPE); }); } @@ -263,7 +282,7 @@ mod test { let contract_address = context.this_address(); let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); - copy(other_contract_address, SLOT, SLOT, 0); + copy(other_contract_address, SLOT, SLOT, 0, SCOPE); }); } } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index 5adc067f5c4b..6081a12cce0a 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -2,12 +2,12 @@ use crate::protocol::address::AztecAddress; /// Finds new private logs that may have been sent to all registered accounts in PXE in the current contract and makes /// them available for later processing in Noir by storing them in a capsule array. -pub unconstrained fn fetch_tagged_logs(pending_tagged_log_array_base_slot: Field) { - fetch_tagged_logs_oracle(pending_tagged_log_array_base_slot); +pub unconstrained fn fetch_tagged_logs(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) { + fetch_tagged_logs_oracle(pending_tagged_log_array_base_slot, scope); } #[oracle(aztec_utl_fetchTaggedLogs)] -unconstrained fn fetch_tagged_logs_oracle(pending_tagged_log_array_base_slot: Field) {} +unconstrained fn fetch_tagged_logs_oracle(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) {} // This must be a single oracle and not one for notes and one for events because the entire point is to validate all // notes and events in one go, minimizing node round-trips. @@ -17,6 +17,7 @@ pub(crate) unconstrained fn validate_and_store_enqueued_notes_and_events( event_validation_requests_array_base_slot: Field, max_note_packed_len: Field, max_event_serialized_len: Field, + scope: AztecAddress, ) { validate_and_store_enqueued_notes_and_events_oracle( contract_address, @@ -24,6 +25,7 @@ pub(crate) unconstrained fn validate_and_store_enqueued_notes_and_events( event_validation_requests_array_base_slot, max_note_packed_len, max_event_serialized_len, + scope, ); } @@ -34,17 +36,20 @@ unconstrained fn validate_and_store_enqueued_notes_and_events_oracle( event_validation_requests_array_base_slot: Field, max_note_packed_len: Field, max_event_serialized_len: Field, + scope: AztecAddress, ) {} pub(crate) unconstrained fn bulk_retrieve_logs( contract_address: AztecAddress, log_retrieval_requests_array_base_slot: Field, log_retrieval_responses_array_base_slot: Field, + scope: AztecAddress, ) { bulk_retrieve_logs_oracle( contract_address, log_retrieval_requests_array_base_slot, log_retrieval_responses_array_base_slot, + scope, ); } @@ -53,17 +58,20 @@ unconstrained fn bulk_retrieve_logs_oracle( contract_address: AztecAddress, log_retrieval_requests_array_base_slot: Field, log_retrieval_responses_array_base_slot: Field, + scope: AztecAddress, ) {} pub(crate) unconstrained fn resolve_message_contexts( contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, + scope: AztecAddress, ) { resolve_message_contexts_oracle( contract_address, message_context_requests_array_base_slot, message_context_responses_array_base_slot, + scope, ); } @@ -72,4 +80,5 @@ unconstrained fn resolve_message_contexts_oracle( contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, + scope: AztecAddress, ) {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index b6b1a4e50469..bf312d89747c 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/version.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/version.nr @@ -4,7 +4,7 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is /// called and if the oracle version is incompatible an error is thrown. -pub global ORACLE_VERSION: Field = 19; +pub global ORACLE_VERSION: Field = 20; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr index 77985955ed71..b636a69ef6e0 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -944,14 +944,17 @@ impl TestEnvironment { // as events are properly scoped by recipient, but notes use a global scope instead. We therefore simply set // the zero address as the recipient if one is not supplied, which PXE accepts as a scope despite this not // being a registered account. - let message_context = MessageContext { - tx_hash, - unique_note_hashes_in_tx, - first_nullifier_in_tx: nullifiers_in_tx.get(0), - recipient: recipient.unwrap_or(AztecAddress::zero()), - }; + let message_context = + MessageContext { tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx: nullifiers_in_tx.get(0) }; + + // The scope controls which PXE account can see discovered notes/events. We use the zero address as the + // default scope if no recipient is supplied, which PXE accepts despite this not being a registered account. + // + // TODO(F-489, @mverzilli): we should revisit how we treat scope in test env, it seems we're treating it too + // implicitly. + let scope = recipient.unwrap_or(AztecAddress::zero()); - // Both private and utility functions perform message processing . We do it in an utility context here as that + // Both private and utility functions perform message processing. We do it in an utility context here as that // one is more lightweight and does not create new blocks, which also allows for `discover_note` and // `discover_event` to be called repeatedly with multiple messages from the same transaction. self.utility_context_opts(UtilityContextOptions { contract_address }, |context| { @@ -962,9 +965,10 @@ impl TestEnvironment { process_custom_message, message_plaintext, message_context, + scope, ); - validate_and_store_enqueued_notes_and_events(context.this_address()); + validate_and_store_enqueued_notes_and_events(context.this_address(), scope); }); } } diff --git a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr index 15d680e0909c..5fb0faa05d34 100644 --- a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr @@ -277,7 +277,7 @@ pub contract TokenBlacklist { tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - recipient: AztecAddress, + scope: AztecAddress, ) { let note = TransparentNote { amount, secret_hash }; let storage_slot = TokenBlacklist::storage_layout().pending_shields.slot; @@ -291,7 +291,6 @@ pub contract TokenBlacklist { tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, - recipient, _compute_note_hash, _compute_note_nullifier, AztecAddress::zero(), @@ -299,10 +298,11 @@ pub contract TokenBlacklist { TRANSPARENT_NOTE_RANDOMNESS, note_type_id, packed_note, + scope, ); // At this point, the note is pending validation and storage in the database. We must call // validate_and_store_enqueued_notes_and_events to complete that process. - validate_and_store_enqueued_notes_and_events(contract_address); + validate_and_store_enqueued_notes_and_events(contract_address, scope); } } diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr index 762b984311b2..ac57deab6903 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr @@ -2,27 +2,40 @@ use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; /// Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `load`. If /// data was already stored at this slot, it is overwritten. -pub unconstrained fn store(contract_address: AztecAddress, slot: Field, value: T) +pub unconstrained fn store( + contract_address: AztecAddress, + slot: Field, + value: T, + scope: AztecAddress, +) where T: Serialize, { let serialized = value.serialize(); - store_oracle(contract_address, slot, serialized); + store_oracle(contract_address, slot, serialized, scope); } -/// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns Option::none() -/// if nothing was stored at the given slot. -pub unconstrained fn load(contract_address: AztecAddress, slot: Field) -> Option +/// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns +/// Option::none() if nothing was stored at the given slot. +pub unconstrained fn load( + contract_address: AztecAddress, + slot: Field, + scope: AztecAddress, +) -> Option where T: Deserialize, { - let serialized_option = load_oracle(contract_address, slot, ::N); + let serialized_option = load_oracle(contract_address, slot, ::N, scope); serialized_option.map(|arr| Deserialize::deserialize(arr)) } /// Deletes data in the per-contract non-volatile database. Does nothing if no data was present. -pub unconstrained fn delete(contract_address: AztecAddress, slot: Field) { - delete_oracle(contract_address, slot); +pub unconstrained fn delete( + contract_address: AztecAddress, + slot: Field, + scope: AztecAddress, +) { + delete_oracle(contract_address, slot, scope); } /// Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data @@ -34,8 +47,9 @@ pub unconstrained fn copy( src_slot: Field, dst_slot: Field, num_entries: u32, + scope: AztecAddress, ) { - copy_oracle(contract_address, src_slot, dst_slot, num_entries); + copy_oracle(contract_address, src_slot, dst_slot, num_entries, scope); } #[oracle(aztec_utl_storeCapsule)] @@ -43,6 +57,7 @@ unconstrained fn store_oracle( contract_address: AztecAddress, slot: Field, values: [Field; N], + scope: AztecAddress, ) {} /// We need to pass in `array_len` (the value of N) as a parameter to tell the oracle how many fields the response must @@ -57,10 +72,11 @@ unconstrained fn load_oracle( contract_address: AztecAddress, slot: Field, array_len: u32, + scope: AztecAddress, ) -> Option<[Field; N]> {} #[oracle(aztec_utl_deleteCapsule)] -unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field) {} +unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field, scope: AztecAddress) {} #[oracle(aztec_utl_copyCapsule)] unconstrained fn copy_oracle( @@ -68,4 +84,5 @@ unconstrained fn copy_oracle( src_slot: Field, dst_slot: Field, num_entries: u32, + scope: AztecAddress, ) {} diff --git a/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr index 2a9286394448..d630d3330f92 100644 --- a/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr @@ -25,6 +25,7 @@ pub contract ContractClassRegistry { use aztec::{ oracle::capsules, protocol::{ + address::AztecAddress, constants::{ CONTRACT_CLASS_REGISTRY_BYTECODE_CAPSULE_SLOT, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS, @@ -77,6 +78,7 @@ pub contract ContractClassRegistry { capsules::load( context.this_address(), CONTRACT_CLASS_REGISTRY_BYTECODE_CAPSULE_SLOT, + AztecAddress::zero(), ) .unwrap() }; diff --git a/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr index 99abd3d0b473..6ec9821e5192 100644 --- a/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr @@ -32,6 +32,7 @@ unconstrained fn handle_multi_log_message( msg_metadata: u64, msg_content: BoundedVec, message_context: MessageContext, + scope: AztecAddress, ) { if msg_type_id == MULTI_LOG_MSG_TYPE_ID { let part_number = MultiLog::::unpack_part_number(msg_metadata); @@ -41,7 +42,11 @@ unconstrained fn handle_multi_log_message( let slot = poseidon2_hash([MULTI_LOG_PARTS_SEPARATOR, message_id]); // Load (or initialize) the pending event and place this part at its index. - let mut multi_log: MultiLog = capsules::load(contract_address, slot) + let mut multi_log: MultiLog = capsules::load( + contract_address, + slot, + scope, + ) .unwrap_or(MultiLog { parts: [BoundedVec::new(); NUM_MULTI_LOG_PARTS] }); multi_log.parts[part_number] = msg_content; @@ -70,7 +75,7 @@ unconstrained fn handle_multi_log_message( } } - capsules::delete(contract_address, slot); + capsules::delete(contract_address, slot, scope); let event_type_id = EventSelector::from_field(event_type_id_field); let event_commitment = compute_private_serialized_event_commitment( @@ -86,10 +91,10 @@ unconstrained fn handle_multi_log_message( serialized_event, event_commitment, message_context.tx_hash, - message_context.recipient, + scope, ); } else { - capsules::store(contract_address, slot, multi_log); + capsules::store(contract_address, slot, multi_log, scope); } } else { panic(f"Unknown message type id: {msg_type_id}"); diff --git a/noir-projects/noir-contracts/contracts/test/storage_proof_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/storage_proof_test_contract/src/main.nr index 9703c7303752..437294dc7a2d 100644 --- a/noir-projects/noir-contracts/contracts/test/storage_proof_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/storage_proof_test_contract/src/main.nr @@ -57,7 +57,12 @@ contract StorageProofTest { let address_capsule_key = compute_address_capsule_key(eth_storage_root, address); // Safety: We'll check that the address inside the hint matches. let hinted_account = unsafe { - Account::deserialize(capsules::load(self.address, address_capsule_key).unwrap()) + Account::deserialize(capsules::load( + self.address, + address_capsule_key, + AztecAddress::zero(), + ) + .unwrap()) }; assert_eq(hinted_account.address, address.to_field().to_be_bytes::<20>()); @@ -68,6 +73,7 @@ contract StorageProofTest { <(u32, [Node; MAX_ACCOUNT_PROOF_LENGTH]) as Deserialize>::deserialize(capsules::load( self.address, account_proof_capsule_key, + AztecAddress::zero(), ) .unwrap()) }; @@ -86,9 +92,12 @@ contract StorageProofTest { // Safety: This is a hint for path verification. let hinted_storage_proof_length = unsafe { - u32::deserialize( - capsules::load(self.address, storage_proof_capsule_key).unwrap(), + u32::deserialize(capsules::load( + self.address, + storage_proof_capsule_key, + AztecAddress::zero(), ) + .unwrap()) }; assert(hinted_storage_proof_length > 0, "Storage proof length must be greater than 0"); @@ -134,7 +143,12 @@ contract StorageProofTest { if i < num_nodes_to_process { // Safety: This is a hint for the path verification. hinted_nodes[i] = unsafe { - Node::deserialize(capsules::load(self.address, node_capsule_key).unwrap()) + Node::deserialize(capsules::load( + self.address, + node_capsule_key, + AztecAddress::zero(), + ) + .unwrap()) }; } } diff --git a/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts b/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts index 9968050c18b7..fe8db573792b 100644 --- a/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts +++ b/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts @@ -1,11 +1,7 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; import { Fr } from '@aztec/aztec.js/fields'; -import type { AztecNode } from '@aztec/aztec.js/node'; -import { PRIVATE_LOG_CIPHERTEXT_LEN } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { OffchainEffectContract, type TestEvent } from '@aztec/noir-test-contracts.js/OffchainEffect'; -import { MessageContext } from '@aztec/stdlib/logs'; -import { OFFCHAIN_MESSAGE_IDENTIFIER } from '@aztec/stdlib/tx'; import { jest } from '@jest/globals'; @@ -18,20 +14,17 @@ const TIMEOUT = 120_000; describe('e2e_offchain_effect', () => { let contract1: OffchainEffectContract; let contract2: OffchainEffectContract; - let aztecNode: AztecNode; jest.setTimeout(TIMEOUT); let wallet: TestWallet; let defaultAccountAddress: AztecAddress; let teardown: () => Promise; - beforeAll(async () => { ({ teardown, wallet, accounts: [defaultAccountAddress], - aztecNode, } = await setup(1)); ({ contract: contract1 } = await OffchainEffectContract.deploy(wallet).send({ from: defaultAccountAddress })); ({ contract: contract2 } = await OffchainEffectContract.deploy(wallet).send({ from: defaultAccountAddress })); @@ -95,44 +88,35 @@ describe('e2e_offchain_effect', () => { it('should emit event as offchain message and process it', async () => { const [a, b, c] = [1n, 2n, 3n]; - const provenTx = await proveInteraction( - wallet, - contract1.methods.emit_event_as_offchain_message_for_msg_sender(a, b, c), - { from: defaultAccountAddress }, - ); - const { txHash, blockNumber, blockHash } = await provenTx.send(); - - const offchainEffects = provenTx.offchainEffects; - expect(offchainEffects).toHaveLength(1); - const offchainEffect = offchainEffects[0]; - - // The data contains the ciphertext, an identifier and the recipient - expect(offchainEffect.data.length).toEqual(PRIVATE_LOG_CIPHERTEXT_LEN + 2); - - const identifier = offchainEffect.data[0]; - expect(identifier).toEqual(OFFCHAIN_MESSAGE_IDENTIFIER); - - const recipientAddressFr = offchainEffect.data[1]; - // Recipient was set to message sender inside the emit_event_as_offchain_message_for_msg_sender function const recipient = defaultAccountAddress; - expect(recipient.toField()).toEqual(recipientAddressFr); - - const ciphertext = offchainEffect.data.slice(2, PRIVATE_LOG_CIPHERTEXT_LEN); - const txEffect = (await aztecNode.getTxEffect(txHash))!.data; + const { receipt, offchainMessages } = await contract1.methods + .emit_event_as_offchain_message_for_msg_sender(a, b, c) + .send({ from: defaultAccountAddress }); - const messageContext = MessageContext.fromTxEffectAndRecipient(txEffect, recipient); + expect(offchainMessages).toHaveLength(1); + const msg = offchainMessages[0]; + expect(msg.recipient).toEqual(recipient); - // Process the message + // Deliver the offchain message via offchain_receive await contract1.methods - .process_message(ciphertext, messageContext.toNoirStruct()) - .simulate({ from: defaultAccountAddress }); + .offchain_receive([ + { + ciphertext: msg.payload, + recipient, + // eslint-disable-next-line camelcase + tx_hash: receipt.txHash.hash, + // eslint-disable-next-line camelcase + anchor_block_timestamp: msg.anchorBlockTimestamp, + }, + ]) + .simulate({ from: recipient }); // Get the event from PXE const events = await wallet.getPrivateEvents(OffchainEffectContract.events.TestEvent, { contractAddress: contract1.address, - fromBlock: BlockNumber(blockNumber!), - toBlock: BlockNumber(blockNumber! + 1), + fromBlock: BlockNumber(receipt.blockNumber!), + toBlock: BlockNumber(receipt.blockNumber! + 1), scopes: [recipient], }); @@ -144,9 +128,9 @@ describe('e2e_offchain_effect', () => { c, }, metadata: { - l2BlockNumber: blockNumber, - l2BlockHash: blockHash, - txHash, + l2BlockNumber: receipt.blockNumber, + l2BlockHash: receipt.blockHash, + txHash: receipt.txHash, }, }); }); @@ -154,36 +138,29 @@ describe('e2e_offchain_effect', () => { it('should emit note as offchain message and process it', async () => { const value = 123n; const owner = defaultAccountAddress; - const provenTx = await proveInteraction(wallet, contract1.methods.emit_note_as_offchain_message(value, owner), { - from: defaultAccountAddress, - }); - const { txHash } = await provenTx.send(); - - const offchainEffects = provenTx.offchainEffects; - expect(offchainEffects).toHaveLength(1); - const offchainEffect = offchainEffects[0]; - - // The data contains the ciphertext, an identifier, and the recipient - expect(offchainEffect.data.length).toEqual(PRIVATE_LOG_CIPHERTEXT_LEN + 2); - - const identifier = offchainEffect.data[0]; - expect(identifier).toEqual(OFFCHAIN_MESSAGE_IDENTIFIER); - - const recipientAddressFr = offchainEffect.data[1]; - // Recipient was set to message sender inside the emit_note_as_offchain_message function const recipient = defaultAccountAddress; - expect(recipient.toField()).toEqual(recipientAddressFr); - - const ciphertext = offchainEffect.data.slice(2, PRIVATE_LOG_CIPHERTEXT_LEN); - const txEffect = (await aztecNode.getTxEffect(txHash))!.data; + const { receipt, offchainMessages } = await contract1.methods + .emit_note_as_offchain_message(value, owner) + .send({ from: defaultAccountAddress }); - const messageContext = MessageContext.fromTxEffectAndRecipient(txEffect, recipient); + expect(offchainMessages).toHaveLength(1); + const msg = offchainMessages[0]; + expect(msg.recipient).toEqual(recipient); - // Process the message + // Deliver the offchain message via offchain_receive await contract1.methods - .process_message(ciphertext, messageContext.toNoirStruct()) - .simulate({ from: defaultAccountAddress }); + .offchain_receive([ + { + ciphertext: msg.payload, + recipient, + // eslint-disable-next-line camelcase + tx_hash: receipt.txHash.hash, + // eslint-disable-next-line camelcase + anchor_block_timestamp: msg.anchorBlockTimestamp, + }, + ]) + .simulate({ from: recipient }); // Get the note value const { result: noteValue } = await contract1.methods diff --git a/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts b/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts index 29785fe1d0df..8ca79a6238dc 100644 --- a/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts +++ b/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import { AztecAddress } from '@aztec/aztec.js/addresses'; -import { NO_WAIT, extractOffchainOutput } from '@aztec/aztec.js/contracts'; +import { extractOffchainOutput } from '@aztec/aztec.js/contracts'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { CheatCodes } from '@aztec/aztec/testing'; import type { BlockNumber } from '@aztec/foundation/branded-types'; @@ -76,8 +76,6 @@ describe('e2e_offchain_payment', () => { await cheatCodes.eth.reorg(1); await aztecNodeAdmin.rollbackTo(Number(block) - 1); expect(await aztecNode.getBlockNumber()).toBe(Number(block) - 1); - - await aztecNodeAdmin.resumeSync(); } it('processes an offchain-delivered private payment via QR-style handoff', async () => { @@ -98,6 +96,7 @@ describe('e2e_offchain_payment', () => { const messageForBob = offchainMessages.find(msg => msg.recipient.equals(bob)); expect(messageForBob).toBeTruthy(); + // Deliver Bob's offchain message (the payment note). await contract.methods .offchain_receive([ { @@ -109,11 +108,27 @@ describe('e2e_offchain_payment', () => { ]) .simulate({ from: bob }); - // TODO(F-413): we need to implement scopes on capsules so we can check Alice's balance too here. This is not - // possible right now because the offchain inbox is shared for all accounts using this contract in the same PXE, - // which is bad. + // TODO(F-324): until we implement F-324, we need Alice to self-deliver her own change note + const messageForAlice = offchainMessages.find(msg => msg.recipient.equals(alice)); + expect(messageForAlice).toBeTruthy(); + + // Deliver Alice's offchain message (the change note). + await contract.methods + .offchain_receive([ + { + ciphertext: messageForAlice!.payload, + recipient: alice, + tx_hash: receipt.txHash.hash, + anchor_block_timestamp: messageForAlice!.anchorBlockTimestamp, + }, + ]) + .simulate({ from: alice }); + const { result: bobBalance } = await contract.methods.get_balance(bob).simulate({ from: bob }); expect(bobBalance).toBe(paymentAmount); + + const { result: aliceBalance } = await contract.methods.get_balance(alice).simulate({ from: alice }); + expect(aliceBalance).toBe(mintAmount - paymentAmount); }); it('reprocesses an offchain-delivered payment after an L1 reorg', async () => { @@ -133,6 +148,9 @@ describe('e2e_offchain_payment', () => { const txBlockNumber = receipt.blockNumber!; const txHash = provenTx.getTxHash(); + const txEffectBeforeReorg = await aztecNode.getTxEffect(txHash); + expect(txEffectBeforeReorg).toBeTruthy(); + const { offchainMessages } = extractOffchainOutput( provenTx.offchainEffects, provenTx.data.constants.anchorBlockHeader.globalVariables.timestamp, @@ -140,7 +158,7 @@ describe('e2e_offchain_payment', () => { const messageForBob = offchainMessages.find(msg => msg.recipient.equals(bob)); expect(messageForBob).toBeTruthy(); - // Deliver the offchain message for eventual processing + // Deliver Bob's offchain message (the payment note). await contract.methods .offchain_receive([ { @@ -152,10 +170,28 @@ describe('e2e_offchain_payment', () => { ]) .simulate({ from: bob }); + // Deliver Alice's offchain message (the change note). + const messageForAlice = offchainMessages.find(msg => msg.recipient.equals(alice)); + expect(messageForAlice).toBeTruthy(); + + await contract.methods + .offchain_receive([ + { + ciphertext: messageForAlice!.payload, + recipient: alice, + tx_hash: txHash.hash, + anchor_block_timestamp: messageForAlice!.anchorBlockTimestamp, + }, + ]) + .simulate({ from: alice }); + // Check that Bob got the payment before a re-org const { result: bobBalance } = await contract.methods.get_balance(bob).simulate({ from: bob }); expect(bobBalance).toBe(paymentAmount); + const { result: aliceBalance } = await contract.methods.get_balance(alice).simulate({ from: alice }); + expect(aliceBalance).toBe(mintAmount - paymentAmount); + await forceReorg(txBlockNumber); // Verify that the payment TX is no longer present after the reorg @@ -166,8 +202,12 @@ describe('e2e_offchain_payment', () => { const { result: bobAfterRollback } = await contract.methods.get_balance(bob).simulate({ from: bob }); expect(bobAfterRollback).toBe(0n); - // Resend the tx after the reorg and force block production so the sequencer picks it up. - await provenTx.send({ wait: NO_WAIT }); + // Verify Alice's balance also rolled back to full mint amount (transfer was reverted) + const { result: aliceAfterRollback } = await contract.methods.get_balance(alice).simulate({ from: alice }); + expect(aliceAfterRollback).toBe(mintAmount); + + // The archiver re-syncs the same checkpoints from L1 after the reorg, so the tx gets re-mined automatically. + // Force an empty block so the PXE re-syncs and reprocesses the offchain-delivered notes. await forceEmptyBlock(); // Check that the message was reprocessed and Bob has his payment again. @@ -175,5 +215,8 @@ describe('e2e_offchain_payment', () => { // for the system to re-process it. const { result: bobBalanceAfterResentTx } = await contract.methods.get_balance(bob).simulate({ from: bob }); expect(bobBalanceAfterResentTx).toBe(paymentAmount); + + const { result: aliceBalanceAfterResentTx } = await contract.methods.get_balance(alice).simulate({ from: alice }); + expect(aliceBalanceAfterResentTx).toBe(mintAmount - paymentAmount); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts index 3b5d2ad06725..58248202981f 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.test.ts @@ -24,7 +24,6 @@ describe('EventValidationRequest', () => { 2, // bounded_vec_len 6, // event_commitment 7, // tx_hash - 8, // recipient ].map(n => new Fr(n)); const request = EventValidationRequest.fromFields(serialized, 10); @@ -35,7 +34,6 @@ describe('EventValidationRequest', () => { expect(request.serializedEvent).toEqual([new Fr(4), new Fr(5)]); expect(request.eventCommitment).toEqual(new Fr(6)); expect(request.txHash).toEqual(TxHash.fromBigInt(7n)); - expect(request.recipient).toEqual(AztecAddress.fromBigInt(8n)); }); it('throws if fed more fields than expected', () => { @@ -57,11 +55,10 @@ describe('EventValidationRequest', () => { 2, // bounded_vec_len 6, // event_commitment 7, // tx_hash - 8, // recipient ].map(n => new Fr(n)); expect(() => EventValidationRequest.fromFields(serialized, 10)).toThrow( - 'Error converting array of fields to EventValidationRequest: expected 17 fields but received 18 (maxEventSerializedLen=10).', + 'Error converting array of fields to EventValidationRequest: expected 16 fields but received 17 (maxEventSerializedLen=10).', ); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts index ccf4b7a944ae..992afadc74b5 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts @@ -16,7 +16,6 @@ export class EventValidationRequest { public serializedEvent: Fr[], public eventCommitment: Fr, public txHash: TxHash, - public recipient: AztecAddress, ) {} static fromFields(fields: Fr[], maxEventSerializedLen: number): EventValidationRequest { @@ -33,7 +32,6 @@ export class EventValidationRequest { const eventCommitment = reader.readField(); const txHash = TxHash.fromField(reader.readField()); - const recipient = AztecAddress.fromField(reader.readField()); if (reader.remainingFields() !== 0) { throw new Error( @@ -48,7 +46,6 @@ export class EventValidationRequest { serializedEvent, eventCommitment, txHash, - recipient, ); } } diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.test.ts deleted file mode 100644 index 72e8f5c01c9a..000000000000 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { Fr } from '@aztec/foundation/curves/bn254'; -import { TxHash } from '@aztec/stdlib/tx'; - -import { MessageTxContext } from './message_tx_context.js'; - -describe('MessageTxContext', () => { - it('serialization of some matches snapshot', () => { - const txHash = new TxHash(new Fr(123)); - const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; - const firstNullifier = new Fr(6n); - const ctx = new MessageTxContext(txHash, uniqueNoteHashes, firstNullifier); - const serialized = MessageTxContext.toSerializedOption(ctx); - expect(serialized.map(f => f.toString())).toMatchInlineSnapshot( - ` - [ - "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x000000000000000000000000000000000000000000000000000000000000007b", - "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000000000000000000000000000006", - ] - `, - ); - }); - it('serialization of none matches snapshot', () => { - const serialized = MessageTxContext.toSerializedOption(null); - expect(serialized.map(f => f.toString())).toMatchInlineSnapshot( - ` - [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - ] - `, - ); - }); - it('serialization length of empty matches', () => { - const txHash = new TxHash(new Fr(123)); - const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; - const firstNullifier = new Fr(6n); - const ctx = new MessageTxContext(txHash, uniqueNoteHashes, firstNullifier); - expect(ctx.toFields().length).toEqual(MessageTxContext.toEmptyFields().length); - }); -}); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.ts deleted file mode 100644 index c50b0e822c81..000000000000 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/message_tx_context.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MAX_NOTE_HASHES_PER_TX } from '@aztec/constants'; -import { range } from '@aztec/foundation/array'; -import { Fr } from '@aztec/foundation/curves/bn254'; -import type { TxHash } from '@aztec/stdlib/tx'; - -/** - * Intermediate struct used to return resolved message contexts from PXE. The - * `resolveMessageContexts` oracle stores values of this type in a CapsuleArray. - */ -export class MessageTxContext { - constructor( - public txHash: TxHash, - public uniqueNoteHashesInTx: Fr[], - public firstNullifierInTx: Fr, - ) {} - - toFields(): Fr[] { - return [ - this.txHash.hash, - ...serializeBoundedVec(this.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX), - this.firstNullifierInTx, - ]; - } - - static toEmptyFields(): Fr[] { - const serializationLen = - 1 /* txHash */ + MAX_NOTE_HASHES_PER_TX + 1 /* uniqueNoteHashesInTx BVec */ + 1; /* firstNullifierInTx */ - return range(serializationLen).map(_ => Fr.zero()); - } - - static toSerializedOption(response: MessageTxContext | null): Fr[] { - if (response) { - return [new Fr(1), ...response.toFields()]; - } else { - return [new Fr(0), ...MessageTxContext.toEmptyFields()]; - } - } -} - -/** - * Helper function to serialize a bounded vector according to Noir's BoundedVec format - * @param values - The values to serialize - * @param maxLength - The maximum length of the bounded vector - * @returns The serialized bounded vector as Fr[] - */ -function serializeBoundedVec(values: Fr[], maxLength: number): Fr[] { - if (values.length > maxLength) { - throw new Error(`Attempted to serialize ${values} values into a BoundedVec with max length ${maxLength}`); - } - - const lengthDiff = maxLength - values.length; - const zeroPaddingArray = Array(lengthDiff).fill(Fr.ZERO); - const storage = values.concat(zeroPaddingArray); - return [...storage, new Fr(values.length)]; -} diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts index 686d57c0e74a..aea6e8b6ebba 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.test.ts @@ -24,7 +24,6 @@ describe('NoteValidationRequest', () => { '0x0000000000000000000000000000000000000000000000000000000000000006', // note hash '0x0000000000000000000000000000000000000000000000000000000000000007', // nullifier '0x0000000000000000000000000000000000000000000000000000000000000008', // tx hash - '0x0000000000000000000000000000000000000000000000000000000000000009', // recipient ].map(Fr.fromHexString); const request = NoteValidationRequest.fromFields(serialized, 8); @@ -38,7 +37,6 @@ describe('NoteValidationRequest', () => { expect(request.noteHash).toEqual(new Fr(6)); expect(request.nullifier).toEqual(new Fr(7)); expect(request.txHash).toEqual(TxHash.fromBigInt(8n)); - expect(request.recipient).toEqual(AztecAddress.fromBigInt(9n)); }); it('throws if fed more fields than expected', () => { @@ -61,11 +59,10 @@ describe('NoteValidationRequest', () => { '0x0000000000000000000000000000000000000000000000000000000000000006', // note hash '0x0000000000000000000000000000000000000000000000000000000000000007', // nullifier '0x0000000000000000000000000000000000000000000000000000000000000008', // tx hash - '0x0000000000000000000000000000000000000000000000000000000000000009', // recipient ].map(Fr.fromHexString); expect(() => NoteValidationRequest.fromFields(serialized, 8)).toThrow( - 'Error converting array of fields to NoteValidationRequest: expected 18 fields but received 19 (maxNotePackedLen=8).', + 'Error converting array of fields to NoteValidationRequest: expected 17 fields but received 18 (maxNotePackedLen=8).', ); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts index 286ad25ef377..355a6a03a858 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts @@ -18,7 +18,6 @@ export class NoteValidationRequest { public noteHash: Fr, public nullifier: Fr, public txHash: TxHash, - public recipient: AztecAddress, ) {} static fromFields(fields: Fr[], maxNotePackedLen: number): NoteValidationRequest { @@ -37,7 +36,6 @@ export class NoteValidationRequest { const noteHash = reader.readField(); const nullifier = reader.readField(); const txHash = TxHash.fromField(reader.readField()); - const recipient = AztecAddress.fromField(reader.readField()); if (reader.remainingFields() !== 0) { throw new Error( @@ -55,7 +53,6 @@ export class NoteValidationRequest { noteHash, nullifier, txHash, - recipient, ); } } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index 9b1887b91239..cd33382e510b 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -119,28 +119,37 @@ export interface IUtilityExecutionOracle { startStorageSlot: Fr, numberOfElements: number, ): Promise; - fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr): Promise; + fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr, scope: AztecAddress): Promise; validateAndStoreEnqueuedNotesAndEvents( contractAddress: AztecAddress, noteValidationRequestsArrayBaseSlot: Fr, eventValidationRequestsArrayBaseSlot: Fr, maxNotePackedLen: number, maxEventSerializedLen: number, + scope: AztecAddress, ): Promise; bulkRetrieveLogs( contractAddress: AztecAddress, logRetrievalRequestsArrayBaseSlot: Fr, logRetrievalResponsesArrayBaseSlot: Fr, + scope: AztecAddress, ): Promise; resolveMessageContexts( contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, messageContextResponsesArrayBaseSlot: Fr, + scope: AztecAddress, + ): Promise; + storeCapsule(contractAddress: AztecAddress, key: Fr, capsule: Fr[], scope: AztecAddress): void; + loadCapsule(contractAddress: AztecAddress, key: Fr, scope: AztecAddress): Promise; + deleteCapsule(contractAddress: AztecAddress, key: Fr, scope: AztecAddress): void; + copyCapsule( + contractAddress: AztecAddress, + srcKey: Fr, + dstKey: Fr, + numEntries: number, + scope: AztecAddress, ): Promise; - storeCapsule(contractAddress: AztecAddress, key: Fr, capsule: Fr[]): Promise; - loadCapsule(contractAddress: AztecAddress, key: Fr): Promise; - deleteCapsule(contractAddress: AztecAddress, key: Fr): Promise; - copyCapsule(contractAddress: AztecAddress, srcKey: Fr, dstKey: Fr, numEntries: number): Promise; aes128Decrypt(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise; getSharedSecret(address: AztecAddress, ephPk: Point): Promise; invalidateContractSyncCache(contractAddress: AztecAddress, scopes: AztecAddress[]): void; diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index ccf8b110c5a6..a58597c66321 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -1,5 +1,6 @@ -import { Fr } from '@aztec/foundation/curves/bn254'; +import { toACVMField } from '@aztec/simulator/client'; import type { ACIRCallback, ACVMField } from '@aztec/simulator/client'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { Oracle } from './oracle.js'; @@ -23,7 +24,8 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { contractAddress: ACVMField[], slot: ACVMField[], tSize: ACVMField[], - ): Promise<(ACVMField | ACVMField[])[]> => oracle.aztec_utl_loadCapsule(contractAddress, slot, tSize), + ): Promise<(ACVMField | ACVMField[])[]> => + oracle.aztec_utl_loadCapsule(contractAddress, slot, tSize, [toACVMField(AztecAddress.ZERO)]), privateStoreInExecutionCache: (values: ACVMField[], hash: ACVMField[]): Promise => oracle.aztec_prv_storeInExecutionCache(values, hash), privateLoadFromExecutionCache: (returnsHash: ACVMField[]): Promise => @@ -65,33 +67,23 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { contractAddress: ACVMField[], slot: ACVMField[], capsule: ACVMField[], - ): Promise => oracle.aztec_utl_storeCapsule(contractAddress, slot, capsule), + ): Promise => + oracle.aztec_utl_storeCapsule(contractAddress, slot, capsule, [toACVMField(AztecAddress.ZERO)]), utilityCopyCapsule: ( contractAddress: ACVMField[], srcSlot: ACVMField[], dstSlot: ACVMField[], numEntries: ACVMField[], - ): Promise => oracle.aztec_utl_copyCapsule(contractAddress, srcSlot, dstSlot, numEntries), + ): Promise => + oracle.aztec_utl_copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, [toACVMField(AztecAddress.ZERO)]), utilityDeleteCapsule: (contractAddress: ACVMField[], slot: ACVMField[]): Promise => - oracle.aztec_utl_deleteCapsule(contractAddress, slot), + oracle.aztec_utl_deleteCapsule(contractAddress, slot, [toACVMField(AztecAddress.ZERO)]), utilityGetSharedSecret: ( address: ACVMField[], ephPKField0: ACVMField[], ephPKField1: ACVMField[], ephPKField2: ACVMField[], ): Promise => oracle.aztec_utl_getSharedSecret(address, ephPKField0, ephPKField1, ephPKField2), - utilityFetchTaggedLogs: (pendingTaggedLogArrayBaseSlot: ACVMField[]): Promise => - oracle.aztec_utl_fetchTaggedLogs(pendingTaggedLogArrayBaseSlot), - utilityBulkRetrieveLogs: ( - contractAddress: ACVMField[], - logRetrievalRequestsArrayBaseSlot: ACVMField[], - logRetrievalResponsesArrayBaseSlot: ACVMField[], - ): Promise => - oracle.aztec_utl_bulkRetrieveLogs( - contractAddress, - logRetrievalRequestsArrayBaseSlot, - logRetrievalResponsesArrayBaseSlot, - ), utilityGetL1ToL2MembershipWitness: ( contractAddress: ACVMField[], messageHash: ACVMField[], @@ -99,20 +91,6 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { ): Promise<(ACVMField | ACVMField[])[]> => oracle.aztec_utl_getL1ToL2MembershipWitness(contractAddress, messageHash, secret), utilityEmitOffchainEffect: (data: ACVMField[]): Promise => oracle.aztec_utl_emitOffchainEffect(data), - // Adapter: old 3-param signature → new 5-param with injected constants. - // Values derived from: MAX_MESSAGE_CONTENT_LEN(11) - RESERVED_FIELDS (3 for notes, 1 for events). - utilityValidateAndStoreEnqueuedNotesAndEvents: ( - contractAddress: ACVMField[], - noteValidationRequestsArrayBaseSlot: ACVMField[], - eventValidationRequestsArrayBaseSlot: ACVMField[], - ): Promise => - oracle.aztec_utl_validateAndStoreEnqueuedNotesAndEvents( - contractAddress, - noteValidationRequestsArrayBaseSlot, - eventValidationRequestsArrayBaseSlot, - [new Fr(8).toString()], - [new Fr(10).toString()], - ), // Renames (same signature, different oracle name) privateNotifySetMinRevertibleSideEffectCounter: (counter: ACVMField[]): Promise => oracle.aztec_prv_notifyRevertiblePhaseStart(counter), diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 5625f75b9835..247909ee59bd 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -491,8 +491,14 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_fetchTaggedLogs([pendingTaggedLogArrayBaseSlot]: ACVMField[]): Promise { - await this.handlerAsUtility().fetchTaggedLogs(Fr.fromString(pendingTaggedLogArrayBaseSlot)); + async aztec_utl_fetchTaggedLogs( + [pendingTaggedLogArrayBaseSlot]: ACVMField[], + [scope]: ACVMField[], + ): Promise { + await this.handlerAsUtility().fetchTaggedLogs( + Fr.fromString(pendingTaggedLogArrayBaseSlot), + AztecAddress.fromString(scope), + ); return []; } @@ -503,6 +509,7 @@ export class Oracle { [eventValidationRequestsArrayBaseSlot]: ACVMField[], [maxNotePackedLen]: ACVMField[], [maxEventSerializedLen]: ACVMField[], + [scope]: ACVMField[], ): Promise { await this.handlerAsUtility().validateAndStoreEnqueuedNotesAndEvents( AztecAddress.fromString(contractAddress), @@ -510,6 +517,7 @@ export class Oracle { Fr.fromString(eventValidationRequestsArrayBaseSlot), Fr.fromString(maxNotePackedLen).toNumber(), Fr.fromString(maxEventSerializedLen).toNumber(), + AztecAddress.fromString(scope), ); return []; @@ -520,11 +528,13 @@ export class Oracle { [contractAddress]: ACVMField[], [logRetrievalRequestsArrayBaseSlot]: ACVMField[], [logRetrievalResponsesArrayBaseSlot]: ACVMField[], + [scope]: ACVMField[], ): Promise { await this.handlerAsUtility().bulkRetrieveLogs( AztecAddress.fromString(contractAddress), Fr.fromString(logRetrievalRequestsArrayBaseSlot), Fr.fromString(logRetrievalResponsesArrayBaseSlot), + AztecAddress.fromString(scope), ); return []; } @@ -534,27 +544,31 @@ export class Oracle { [contractAddress]: ACVMField[], [messageContextRequestsArrayBaseSlot]: ACVMField[], [messageContextResponsesArrayBaseSlot]: ACVMField[], + [scope]: ACVMField[], ): Promise { await this.handlerAsUtility().resolveMessageContexts( AztecAddress.fromString(contractAddress), Fr.fromString(messageContextRequestsArrayBaseSlot), Fr.fromString(messageContextResponsesArrayBaseSlot), + AztecAddress.fromString(scope), ); return []; } // eslint-disable-next-line camelcase - async aztec_utl_storeCapsule( + aztec_utl_storeCapsule( [contractAddress]: ACVMField[], [slot]: ACVMField[], capsule: ACVMField[], + [scope]: ACVMField[], ): Promise { - await this.handlerAsUtility().storeCapsule( + this.handlerAsUtility().storeCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), capsule.map(Fr.fromString), + AztecAddress.fromField(Fr.fromString(scope)), ); - return []; + return Promise.resolve([]); } // eslint-disable-next-line camelcase @@ -562,10 +576,12 @@ export class Oracle { [contractAddress]: ACVMField[], [slot]: ACVMField[], [tSize]: ACVMField[], + [scope]: ACVMField[], ): Promise<(ACVMField | ACVMField[])[]> { const values = await this.handlerAsUtility().loadCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), + AztecAddress.fromField(Fr.fromString(scope)), ); // We are going to return a Noir Option struct to represent the possibility of null values. Options are a struct @@ -580,12 +596,17 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_deleteCapsule([contractAddress]: ACVMField[], [slot]: ACVMField[]): Promise { - await this.handlerAsUtility().deleteCapsule( + aztec_utl_deleteCapsule( + [contractAddress]: ACVMField[], + [slot]: ACVMField[], + [scope]: ACVMField[], + ): Promise { + this.handlerAsUtility().deleteCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), + AztecAddress.fromField(Fr.fromString(scope)), ); - return []; + return Promise.resolve([]); } // eslint-disable-next-line camelcase @@ -594,12 +615,14 @@ export class Oracle { [srcSlot]: ACVMField[], [dstSlot]: ACVMField[], [numEntries]: ACVMField[], + [scope]: ACVMField[], ): Promise { await this.handlerAsUtility().copyCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(srcSlot), Fr.fromString(dstSlot), Fr.fromString(numEntries).toNumber(), + AztecAddress.fromField(Fr.fromString(scope)), ); return []; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 5b55fc8ef5cb..96f947d177f5 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -323,18 +323,21 @@ describe('Private Execution test suite', () => { messageContextService.resolveMessageContexts.mockResolvedValue([]); // Configure mock to actually perform sync_state calls (needed for nested call tests) contractSyncService.ensureContractSynced.mockImplementation( - async (contractAddress, functionToInvokeAfterSync, utilityExecutor, anchorBlockHeader, jobId) => { - await syncState( - contractAddress, - contractStore, - functionToInvokeAfterSync, - utilityExecutor, - noteStore, - aztecNode, - anchorBlockHeader, - jobId, - 'ALL_SCOPES', - ); + async (contractAddress, functionToInvokeAfterSync, utilityExecutor, anchorBlockHeader, jobId, scopes) => { + const scopeAddresses = scopes === 'ALL_SCOPES' ? [owner] : scopes; + for (const scope of scopeAddresses) { + await syncState( + contractAddress, + contractStore, + functionToInvokeAfterSync, + utilityExecutor, + noteStore, + aztecNode, + anchorBlockHeader, + jobId, + scope, + ); + } }, ); contracts = {}; @@ -753,7 +756,7 @@ describe('Private Execution test suite', () => { contractAddress: parentAddress, }); - expect(contractStore.getFunctionCall).toHaveBeenCalledWith('sync_state', [], childAddress); + expect(contractStore.getFunctionCall).toHaveBeenCalledWith('sync_state', [owner], childAddress); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 3a2faea4860b..6da7fd7fe739 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -10,9 +10,10 @@ import { BlockHash } from '@aztec/stdlib/block'; import { CompleteAddress, type ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { deriveKeys } from '@aztec/stdlib/keys'; +import { MessageContext } from '@aztec/stdlib/logs'; import { Note, NoteDao } from '@aztec/stdlib/note'; import { makeL2Tips } from '@aztec/stdlib/testing'; -import { BlockHeader, GlobalVariables, TxHash } from '@aztec/stdlib/tx'; +import { BlockHeader, Capsule, GlobalVariables, TxHash } from '@aztec/stdlib/tx'; import { mock } from 'jest-mock-extended'; import type { _MockProxy } from 'jest-mock-extended/lib/Mock.js'; @@ -28,7 +29,6 @@ import type { RecipientTaggingStore } from '../../storage/tagging_store/recipien import type { SenderAddressBookStore } from '../../storage/tagging_store/sender_address_book_store.js'; import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js'; import { ContractFunctionSimulator } from '../contract_function_simulator.js'; -import { MessageTxContext } from '../noir-structs/message_tx_context.js'; import { UtilityExecutionOracle } from './utility_execution_oracle.js'; describe('Utility Execution test suite', () => { @@ -246,6 +246,76 @@ describe('Utility Execution test suite', () => { }); }); + describe('capsules', () => { + it('forwards scope to the capsule store', async () => { + const scope = await AztecAddress.random(); + const slot = Fr.random(); + const srcSlot = Fr.random(); + const dstSlot = Fr.random(); + const capsule = [Fr.random()]; + + capsuleStore.loadCapsule.mockResolvedValueOnce(capsule); + + utilityExecutionOracle.storeCapsule(contractAddress, slot, capsule, scope); + await utilityExecutionOracle.loadCapsule(contractAddress, slot, scope); + utilityExecutionOracle.deleteCapsule(contractAddress, slot, scope); + await utilityExecutionOracle.copyCapsule(contractAddress, srcSlot, dstSlot, 1, scope); + + expect(capsuleStore.storeCapsule).toHaveBeenCalledWith(contractAddress, slot, capsule, 'test-job-id', scope); + expect(capsuleStore.loadCapsule).toHaveBeenCalledWith(contractAddress, slot, 'test-job-id', scope); + expect(capsuleStore.deleteCapsule).toHaveBeenCalledWith(contractAddress, slot, 'test-job-id', scope); + expect(capsuleStore.copyCapsule).toHaveBeenCalledWith( + contractAddress, + srcSlot, + dstSlot, + 1, + 'test-job-id', + scope, + ); + }); + + it('loads transient capsules by scope', async () => { + const scope = await AztecAddress.random(); + const slot = Fr.random(); + const transientGlobal = [Fr.random()]; + const transientScoped = [Fr.random()]; + const persisted = [Fr.random()]; + + utilityExecutionOracle = new UtilityExecutionOracle({ + contractAddress, + authWitnesses: [], + capsules: [ + new Capsule(contractAddress, slot, transientGlobal), + new Capsule(contractAddress, slot, transientScoped, scope), + ], + anchorBlockHeader, + contractStore, + noteStore, + keyStore, + addressStore, + aztecNode, + recipientTaggingStore, + senderAddressBookStore, + capsuleStore, + privateEventStore, + messageContextService, + contractSyncService, + jobId: 'test-job-id', + scopes: 'ALL_SCOPES', + }); + + capsuleStore.loadCapsule.mockResolvedValueOnce(persisted); + + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot, AztecAddress.ZERO)).toEqual( + transientGlobal, + ); + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot, AztecAddress.ZERO)).toEqual( + transientGlobal, + ); + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot, scope)).toEqual(transientScoped); + }); + }); + describe('invalidateContractSyncCache', () => { it('throws when contract address does not match', async () => { const otherAddress = await AztecAddress.random(); @@ -267,29 +337,30 @@ describe('Utility Execution test suite', () => { describe('resolveMessageContexts', () => { const requestSlot = Fr.random(); const responseSlot = Fr.random(); + const scope = AztecAddress.fromBigInt(42n); it('throws when contractAddress does not match', async () => { const wrongAddress = await AztecAddress.random(); await expect( - utilityExecutionOracle.resolveMessageContexts(wrongAddress, requestSlot, responseSlot), + utilityExecutionOracle.resolveMessageContexts(wrongAddress, requestSlot, responseSlot, scope), ).rejects.toThrow(`Got a message context request from ${wrongAddress}, expected ${contractAddress}`); }); it('sets null in response capsule for zero tx hashes', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([[Fr.ZERO]]); - await utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot); + await utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot, scope); const response = capsuleStore.setCapsuleArray.mock.calls.find( call => call[0].equals(contractAddress) && call[1].equals(responseSlot), ); expect(response).toBeDefined(); const responseFields = response![2][0]; - expect(responseFields).toEqual(MessageTxContext.toSerializedOption(null)); + expect(responseFields).toEqual(MessageContext.toSerializedOption(null)); expect(aztecNode.getTxEffect).not.toHaveBeenCalled(); }); - it('resolves a valid tx hash into a MessageTxContext', async () => { + it('resolves a valid tx hash into a MessageContext', async () => { const txHash = TxHash.random(); const noteHash = Fr.random(); const firstNullifier = Fr.random(); @@ -302,14 +373,14 @@ describe('Utility Execution test suite', () => { data: { txHash, noteHashes: [noteHash], nullifiers: [firstNullifier] }, } as any); - await utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot); + await utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot, scope); const response = capsuleStore.setCapsuleArray.mock.calls.find( call => call[0].equals(contractAddress) && call[1].equals(responseSlot), ); expect(response).toBeDefined(); const responseFields = response![2][0]; - const expected = MessageTxContext.toSerializedOption(new MessageTxContext(txHash, [noteHash], firstNullifier)); + const expected = MessageContext.toSerializedOption(new MessageContext(txHash, [noteHash], firstNullifier)); expect(responseFields).toEqual(expected); }); @@ -324,42 +395,54 @@ describe('Utility Execution test suite', () => { data: { txHash, noteHashes: [], nullifiers: [Fr.random()] }, } as any); - await utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot); + await utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot, scope); const response = capsuleStore.setCapsuleArray.mock.calls.find( call => call[0].equals(contractAddress) && call[1].equals(responseSlot), ); expect(response).toBeDefined(); const responseFields = response![2][0]; - expect(responseFields).toEqual(MessageTxContext.toSerializedOption(null)); + expect(responseFields).toEqual(MessageContext.toSerializedOption(null)); }); it('throws on empty capsule entry', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([[]]); await expect( - utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot), + utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot, scope), ).rejects.toThrow('Malformed message context request at index 0: expected 1 field (tx hash), got 0'); }); it('throws on capsule entry with extra fields', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([[Fr.random(), Fr.random()]]); await expect( - utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot), + utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot, scope), ).rejects.toThrow('Malformed message context request at index 0: expected 1 field (tx hash), got 2'); }); it('clears the request capsule after processing', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([]); - await utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot); - expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith(contractAddress, requestSlot, [], 'test-job-id'); + await utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot, scope); + expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith( + contractAddress, + requestSlot, + [], + 'test-job-id', + scope, + ); }); it('clears the request capsule even on error', async () => { capsuleStore.readCapsuleArray.mockResolvedValueOnce([[]]); await expect( - utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot), + utilityExecutionOracle.resolveMessageContexts(contractAddress, requestSlot, responseSlot, scope), ).rejects.toThrow(); - expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith(contractAddress, requestSlot, [], 'test-job-id'); + expect(capsuleStore.setCapsuleArray).toHaveBeenCalledWith( + contractAddress, + requestSlot, + [], + 'test-job-id', + scope, + ); }); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index c8bcf7a94b3d..ee46c8c7c0d4 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -15,7 +15,7 @@ import { siloNullifier } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { type PublicKeys, computeAddressSecret } from '@aztec/stdlib/keys'; -import { deriveEcdhSharedSecret } from '@aztec/stdlib/logs'; +import { MessageContext, deriveEcdhSharedSecret } from '@aztec/stdlib/logs'; import { getNonNullifiedL1ToL2MessageWitness } from '@aztec/stdlib/messaging'; import type { NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; @@ -39,7 +39,6 @@ import type { SenderAddressBookStore } from '../../storage/tagging_store/sender_ import { EventValidationRequest } from '../noir-structs/event_validation_request.js'; import { LogRetrievalRequest } from '../noir-structs/log_retrieval_request.js'; import { LogRetrievalResponse } from '../noir-structs/log_retrieval_response.js'; -import { MessageTxContext } from '../noir-structs/message_tx_context.js'; import { NoteValidationRequest } from '../noir-structs/note_validation_request.js'; import { UtilityContext } from '../noir-structs/utility_context.js'; import { pickNotes } from '../pick_notes.js'; @@ -476,7 +475,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra logContractMessage(logger, LogLevels[level], message, fields); } - public async fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr) { + public async fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr, scope: AztecAddress) { const logService = new LogService( this.aztecNode, this.anchorBlockHeader, @@ -489,7 +488,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.logger.getBindings(), ); - await logService.fetchTaggedLogs(this.contractAddress, pendingTaggedLogArrayBaseSlot, this.scopes); + await logService.fetchTaggedLogs(this.contractAddress, pendingTaggedLogArrayBaseSlot, scope); } /** @@ -508,6 +507,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra eventValidationRequestsArrayBaseSlot: Fr, maxNotePackedLen: number, maxEventSerializedLen: number, + scope: AztecAddress, ) { // TODO(#10727): allow other contracts to store notes if (!this.contractAddress.equals(contractAddress)) { @@ -517,11 +517,11 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // We read all note and event validation requests and process them all concurrently. This makes the process much // faster as we don't need to wait for the network round-trip. const noteValidationRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, this.jobId) + await this.capsuleStore.readCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, this.jobId, scope) ).map(fields => NoteValidationRequest.fromFields(fields, maxNotePackedLen)); const eventValidationRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, this.jobId) + await this.capsuleStore.readCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, this.jobId, scope) ).map(fields => EventValidationRequest.fromFields(fields, maxEventSerializedLen)); const noteService = new NoteService(this.noteStore, this.aztecNode, this.anchorBlockHeader, this.jobId); @@ -536,7 +536,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra request.noteHash, request.nullifier, request.txHash, - request.recipient, + scope, ), ); @@ -549,21 +549,34 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra request.serializedEvent, request.eventCommitment, request.txHash, - request.recipient, + scope, ), ); await Promise.all([...noteStorePromises, ...eventStorePromises]); // Requests are cleared once we're done. - await this.capsuleStore.setCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, [], this.jobId); - await this.capsuleStore.setCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, [], this.jobId); + await this.capsuleStore.setCapsuleArray( + contractAddress, + noteValidationRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); + await this.capsuleStore.setCapsuleArray( + contractAddress, + eventValidationRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); } public async bulkRetrieveLogs( contractAddress: AztecAddress, logRetrievalRequestsArrayBaseSlot: Fr, logRetrievalResponsesArrayBaseSlot: Fr, + scope: AztecAddress, ) { // TODO(#10727): allow other contracts to process partial notes if (!this.contractAddress.equals(contractAddress)) { @@ -573,7 +586,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // We read all log retrieval requests and process them all concurrently. This makes the process much faster as we // don't need to wait for the network round-trip. const logRetrievalRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, this.jobId) + await this.capsuleStore.readCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, this.jobId, scope) ).map(LogRetrievalRequest.fromFields); const logService = new LogService( @@ -591,7 +604,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra const maybeLogRetrievalResponses = await logService.bulkRetrieveLogs(logRetrievalRequests); // Requests are cleared once we're done. - await this.capsuleStore.setCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, [], this.jobId); + await this.capsuleStore.setCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, [], this.jobId, scope); // The responses are stored as Option in a second CapsuleArray. await this.capsuleStore.setCapsuleArray( @@ -599,6 +612,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra logRetrievalResponsesArrayBaseSlot, maybeLogRetrievalResponses.map(LogRetrievalResponse.toSerializedOption), this.jobId, + scope, ); } @@ -606,15 +620,22 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, messageContextResponsesArrayBaseSlot: Fr, + scope: AztecAddress, ) { try { if (!this.contractAddress.equals(contractAddress)) { throw new Error(`Got a message context request from ${contractAddress}, expected ${this.contractAddress}`); } + + // TODO(@mverzilli): this is a prime example of where using a volatile array would make much more sense, we don't + // need scopes here, we just need a bit of shared memory to cross boundaries between Noir and TS. + // At the same time, we don't want to allow any global scope access other than where backwards compatibility + // forces us to. Hence we need the scope here to be artificial. const requestCapsules = await this.capsuleStore.readCapsuleArray( contractAddress, messageContextRequestsArrayBaseSlot, this.jobId, + scope, ); const txHashes = requestCapsules.map((fields, i) => { @@ -635,50 +656,65 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra await this.capsuleStore.setCapsuleArray( contractAddress, messageContextResponsesArrayBaseSlot, - maybeMessageContexts.map(MessageTxContext.toSerializedOption), + maybeMessageContexts.map(MessageContext.toSerializedOption), this.jobId, + scope, ); } finally { - await this.capsuleStore.setCapsuleArray(contractAddress, messageContextRequestsArrayBaseSlot, [], this.jobId); + await this.capsuleStore.setCapsuleArray( + contractAddress, + messageContextRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); } } - public storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[]): Promise { + public storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], scope: AztecAddress): void { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - this.capsuleStore.storeCapsule(this.contractAddress, slot, capsule, this.jobId); - return Promise.resolve(); + this.capsuleStore.storeCapsule(contractAddress, slot, capsule, this.jobId, scope); } - public async loadCapsule(contractAddress: AztecAddress, slot: Fr): Promise { + public async loadCapsule(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return ( - // TODO(#12425): On the following line, the pertinent capsule gets overshadowed by the transient one. Tackle this. - this.capsules.find(c => c.contractAddress.equals(contractAddress) && c.storageSlot.equals(slot))?.data ?? - (await this.capsuleStore.loadCapsule(this.contractAddress, slot, this.jobId)) - ); + const maybeTransientCapsule = this.capsules.find( + c => + c.contractAddress.equals(contractAddress) && + c.storageSlot.equals(slot) && + (c.scope ?? AztecAddress.ZERO).equals(scope), + )?.data; + + // TODO(#12425): On the following line, the pertinent capsule gets overshadowed by the transient one. Tackle this. + return maybeTransientCapsule ?? (await this.capsuleStore.loadCapsule(contractAddress, slot, this.jobId, scope)); } - public deleteCapsule(contractAddress: AztecAddress, slot: Fr): Promise { + public deleteCapsule(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): void { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - this.capsuleStore.deleteCapsule(this.contractAddress, slot, this.jobId); - return Promise.resolve(); + this.capsuleStore.deleteCapsule(contractAddress, slot, this.jobId, scope); } - public copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + public copyCapsule( + contractAddress: AztecAddress, + srcSlot: Fr, + dstSlot: Fr, + numEntries: number, + scope: AztecAddress, + ): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.capsuleStore.copyCapsule(this.contractAddress, srcSlot, dstSlot, numEntries, this.jobId); + return this.capsuleStore.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, this.jobId, scope); } /** diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts b/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts index 9e2d6b5f0eba..0ed395978b75 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts +++ b/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts @@ -28,9 +28,6 @@ describe('ContractSyncService', () => { const anchorBlockHeader = makeBlockHeader(0); const classId = Fr.fromHexString('0xdeadbeef'); - /** Sentinel for undefined scopes (sync all accounts). */ - const ALL_SCOPES = 'ALL_SCOPES' as const; - beforeEach(() => { utilityExecutor = jest .fn<(call: FunctionCall, scopes: AccessScopes) => Promise>() @@ -65,7 +62,13 @@ describe('ContractSyncService', () => { // syncNoteNullifiers returns early when no notes noteStore.getNotes.mockResolvedValue([]); - service = new ContractSyncService(aztecNode, contractStore, noteStore, createLogger('test:contract-sync')); + service = new ContractSyncService( + aztecNode, + contractStore, + noteStore, + () => Promise.resolve([scopeA, scopeB]), + createLogger('test:contract-sync'), + ); }); describe('ensureContractSynced', () => { @@ -90,12 +93,13 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes(ALL_SCOPES); + // ALL_SCOPES resolves to [scopeA, scopeB] via getRegisteredAccounts, so syncState is called once per account + expectSyncedScopes([scopeA], [scopeB]); // After syncing all scopes, scope-specific calls should be skipped await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeB]); - expectSyncedScopes(ALL_SCOPES); + expectSyncedScopes([scopeA], [scopeB]); }); it('still syncs all scopes even after scope-specific sync', async () => { @@ -108,7 +112,8 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes([scopeA], ALL_SCOPES); + // ALL_SCOPES resolves to [scopeA, scopeB]; both are re-synced since ALL_SCOPES bypasses per-scope cache + expectSyncedScopes([scopeA], [scopeA], [scopeB]); }); it('empty scopes array skips sync entirely', async () => { @@ -243,7 +248,7 @@ describe('ContractSyncService', () => { scopeA, scopeB, ]); - expectSyncedScopes([scopeA, scopeB]); + expectSyncedScopes([scopeA], [scopeB]); service.invalidateContractForScopes(contractAddress, [scopeA]); @@ -252,7 +257,7 @@ describe('ContractSyncService', () => { scopeB, ]); // Only scopeA should be re-synced, scopeB is still cached. - expectSyncedScopes([scopeA, scopeB], [scopeA]); + expectSyncedScopes([scopeA], [scopeB], [scopeA]); }); it('invalidates multiple scopes at once', async () => { @@ -260,7 +265,7 @@ describe('ContractSyncService', () => { scopeA, scopeB, ]); - expectSyncedScopes([scopeA, scopeB]); + expectSyncedScopes([scopeA], [scopeB]); service.invalidateContractForScopes(contractAddress, [scopeA, scopeB]); @@ -269,11 +274,11 @@ describe('ContractSyncService', () => { scopeB, ]); // Both scopes should be re-synced. - expectSyncedScopes([scopeA, scopeB], [scopeA, scopeB]); + expectSyncedScopes([scopeA], [scopeB], [scopeA], [scopeB]); }); it('also invalidates the ALL_SCOPES entry', async () => { - // Sync ALL_SCOPES -- covers every account. + // Sync ALL_SCOPES -- covers every account. Resolves to [scopeA, scopeB] via getRegisteredAccounts. await service.ensureContractSynced( contractAddress, null, @@ -282,18 +287,18 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes('ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB]); // Syncing scopeA is a no-op because ALL_SCOPES already covers it. await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectSyncedScopes('ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB]); // Invalidate scopeA -- this should also clear the ALL_SCOPES entry. service.invalidateContractForScopes(contractAddress, [scopeA]); // Now syncing scopeA triggers a re-sync. await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectSyncedScopes('ALL_SCOPES', [scopeA]); + expectSyncedScopes([scopeA], [scopeB], [scopeA]); // And syncing ALL_SCOPES also triggers a re-sync since it was invalidated too. await service.ensureContractSynced( @@ -304,7 +309,7 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes('ALL_SCOPES', [scopeA], 'ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB], [scopeA], [scopeA], [scopeB]); }); it('empty scopes is a no-op', async () => { @@ -316,7 +321,7 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes('ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB]); service.invalidateContractForScopes(contractAddress, []); @@ -329,7 +334,7 @@ describe('ContractSyncService', () => { jobId, 'ALL_SCOPES', ); - expectSyncedScopes('ALL_SCOPES'); + expectSyncedScopes([scopeA], [scopeB]); }); it('does not affect other contracts', async () => { diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts b/yarn-project/pxe/src/contract_sync/contract_sync_service.ts index 8726efada832..8708010c62e2 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts +++ b/yarn-project/pxe/src/contract_sync/contract_sync_service.ts @@ -31,6 +31,7 @@ export class ContractSyncService implements StagedStore { private aztecNode: AztecNode, private contractStore: ContractStore, private noteStore: NoteStore, + private getRegisteredAccounts: () => Promise, private log: Logger, ) {} @@ -92,18 +93,28 @@ export class ContractSyncService implements StagedStore { scopes: AccessScopes, ): Promise { this.log.debug(`Syncing contract ${contractAddress}`); + + // Resolve ALL_SCOPES to actual registered accounts, since sync_state must be called once per account. + const scopeAddresses = scopes === 'ALL_SCOPES' ? await this.getRegisteredAccounts() : scopes; + await Promise.all([ - syncState( - contractAddress, - this.contractStore, - functionToInvokeAfterSync, - utilityExecutor, - this.noteStore, - this.aztecNode, - anchorBlockHeader, - jobId, - scopes, - ), + // Call sync_state sequentially for each scope address — each invocation synchronizes one account's private + // state using scoped capsule arrays. + (async () => { + for (const scope of scopeAddresses) { + await syncState( + contractAddress, + this.contractStore, + functionToInvokeAfterSync, + utilityExecutor, + this.noteStore, + this.aztecNode, + anchorBlockHeader, + jobId, + scope, + ); + } + })(), verifyCurrentClassId(contractAddress, this.aztecNode, this.contractStore, anchorBlockHeader), ]); this.log.debug(`Contract ${contractAddress} synced`); diff --git a/yarn-project/pxe/src/contract_sync/helpers.ts b/yarn-project/pxe/src/contract_sync/helpers.ts index b2806d6151ab..8f437d10dd7f 100644 --- a/yarn-project/pxe/src/contract_sync/helpers.ts +++ b/yarn-project/pxe/src/contract_sync/helpers.ts @@ -48,11 +48,11 @@ export async function syncState( aztecNode: AztecNode, anchorBlockHeader: BlockHeader, jobId: string, - scopes: AccessScopes, + scope: AztecAddress, ) { // Protocol contracts don't have private state to sync if (!isProtocolContract(contractAddress)) { - const syncStateFunctionCall = await contractStore.getFunctionCall('sync_state', [], contractAddress); + const syncStateFunctionCall = await contractStore.getFunctionCall('sync_state', [scope], contractAddress); if (functionToInvokeAfterSync && functionToInvokeAfterSync.equals(syncStateFunctionCall.selector)) { throw new Error( 'Forbidden `sync_state` invocation. `sync_state` can only be invoked by PXE, manual execution can lead to inconsistencies.', @@ -60,6 +60,7 @@ export async function syncState( } const noteService = new NoteService(noteStore, aztecNode, anchorBlockHeader, jobId); + const scopes: AccessScopes = [scope]; // Both sync_state and syncNoteNullifiers interact with the note store, but running them in parallel is safe // because note store is designed to handle concurrent operations. diff --git a/yarn-project/pxe/src/logs/log_service.ts b/yarn-project/pxe/src/logs/log_service.ts index f267e5123b72..c0a46e64148c 100644 --- a/yarn-project/pxe/src/logs/log_service.ts +++ b/yarn-project/pxe/src/logs/log_service.ts @@ -12,7 +12,6 @@ import { } from '@aztec/stdlib/logs'; import type { BlockHeader } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../access_scopes.js'; import type { LogRetrievalRequest } from '../contract_function_simulator/noir-structs/log_retrieval_request.js'; import { LogRetrievalResponse } from '../contract_function_simulator/noir-structs/log_retrieval_response.js'; import { AddressStore } from '../storage/address_store/address_store.js'; @@ -114,60 +113,45 @@ export class LogService { ); } - public async fetchTaggedLogs(contractAddress: AztecAddress, pendingTaggedLogArrayBaseSlot: Fr, scopes: AccessScopes) { + public async fetchTaggedLogs( + contractAddress: AztecAddress, + pendingTaggedLogArrayBaseSlot: Fr, + recipient: AztecAddress, + ) { this.log.verbose(`Fetching tagged logs for ${contractAddress.toString()}`); // We only load logs from block up to and including the anchor block number const anchorBlockNumber = this.anchorBlockHeader.getBlockNumber(); const anchorBlockHash = await this.anchorBlockHeader.hash(); - // Determine recipients: use scopes if provided, otherwise get all accounts - const recipients = scopes !== 'ALL_SCOPES' && scopes.length > 0 ? scopes : await this.keyStore.getAccounts(); - - // We implicitly add all PXE accounts as senders, this helps us decrypt tags on notes that we send to ourselves - // (recipient = us, sender = us) - const allSenders = [...(await this.senderAddressBookStore.getSenders()), ...(await this.keyStore.getAccounts())]; - // We deduplicate the senders by adding them to a set and then converting the set back to an array - const deduplicatedSenders = Array.from(new Set(allSenders.map(sender => sender.toString()))).map(sender => - AztecAddress.fromString(sender), + // Get all secrets for this recipient (one per sender) + const secrets = await this.#getSecretsForSenders(contractAddress, recipient); + + // Load logs for all sender-recipient pairs in parallel + const logArrays = await Promise.all( + secrets.map(secret => + loadPrivateLogsForSenderRecipientPair( + secret, + this.aztecNode, + this.recipientTaggingStore, + anchorBlockNumber, + anchorBlockHash, + this.jobId, + ), + ), ); - // For each recipient, fetch secrets, load logs, and store them. - // We run these per-recipient tasks in parallel so that logs are loaded for all recipients concurrently. - await Promise.all( - recipients.map(async recipient => { - // Get all secrets for this recipient (one per sender) - const secrets = await this.#getSecretsForSenders(contractAddress, recipient, deduplicatedSenders); - - // Load logs for all sender-recipient pairs in parallel - const logArrays = await Promise.all( - secrets.map(secret => - loadPrivateLogsForSenderRecipientPair( - secret, - this.aztecNode, - this.recipientTaggingStore, - anchorBlockNumber, - anchorBlockHash, - this.jobId, - ), - ), - ); + // Flatten all logs from all secrets + const allLogs = logArrays.flat(); - // Flatten all logs from all secrets - const allLogs = logArrays.flat(); - - // Store the logs for this recipient - if (allLogs.length > 0) { - await this.#storePendingTaggedLogs(contractAddress, pendingTaggedLogArrayBaseSlot, recipient, allLogs); - } - }), - ); + if (allLogs.length > 0) { + await this.#storePendingTaggedLogs(contractAddress, pendingTaggedLogArrayBaseSlot, recipient, allLogs); + } } async #getSecretsForSenders( contractAddress: AztecAddress, recipient: AztecAddress, - senders: AztecAddress[], ): Promise { const recipientCompleteAddress = await this.addressStore.getCompleteAddress(recipient); if (!recipientCompleteAddress) { @@ -175,8 +159,17 @@ export class LogService { } const recipientIvsk = await this.keyStore.getMasterIncomingViewingSecretKey(recipient); + // We implicitly add all PXE accounts as senders, this helps us decrypt tags on notes that we send to ourselves + // (recipient = us, sender = us) + const allSenders = [...(await this.senderAddressBookStore.getSenders()), ...(await this.keyStore.getAccounts())]; + + // We deduplicate the senders by adding them to a set and then converting the set back to an array + const deduplicatedSenders = Array.from(new Set(allSenders.map(sender => sender.toString()))).map(sender => + AztecAddress.fromString(sender), + ); + return Promise.all( - senders.map(sender => { + deduplicatedSenders.map(sender => { return ExtendedDirectionalAppTaggingSecret.compute( recipientCompleteAddress, recipientIvsk, @@ -201,13 +194,18 @@ export class LogService { scopedLog.txHash, scopedLog.noteHashes, scopedLog.firstNullifier, - recipient, ); return pendingTaggedLog.toFields(); }); // TODO: This looks like it could belong more at the oracle interface level - return this.capsuleStore.appendToCapsuleArray(contractAddress, capsuleArrayBaseSlot, pendingTaggedLogs, this.jobId); + return this.capsuleStore.appendToCapsuleArray( + contractAddress, + capsuleArrayBaseSlot, + pendingTaggedLogs, + this.jobId, + recipient, + ); } } diff --git a/yarn-project/pxe/src/messages/message_context_service.test.ts b/yarn-project/pxe/src/messages/message_context_service.test.ts index 6449f1e1b6cf..d81cd8034fb3 100644 --- a/yarn-project/pxe/src/messages/message_context_service.test.ts +++ b/yarn-project/pxe/src/messages/message_context_service.test.ts @@ -2,11 +2,11 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { BlockHash } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; +import { MessageContext } from '@aztec/stdlib/logs'; import { TxHash } from '@aztec/stdlib/tx'; import { mock } from 'jest-mock-extended'; -import { MessageTxContext } from '../contract_function_simulator/noir-structs/message_tx_context.js'; import { MessageContextService } from './message_context_service.js'; describe('MessageContextService', () => { @@ -63,7 +63,7 @@ describe('MessageContextService', () => { ); }); - it('resolves a valid tx hash into a MessageTxContext', async () => { + it('resolves a valid tx hash into a MessageContext', async () => { const txHash = TxHash.random(); const noteHashes = [Fr.random(), Fr.random()]; const firstNullifier = Fr.random(); @@ -77,7 +77,7 @@ describe('MessageContextService', () => { const results = await service.resolveMessageContexts([txHash.hash], anchorBlockNumber); - expect(results).toEqual([new MessageTxContext(txHash, noteHashes, firstNullifier)]); + expect(results).toEqual([new MessageContext(txHash, noteHashes, firstNullifier)]); }); it('resolves tx hashes in different situations', async () => { @@ -111,14 +111,14 @@ describe('MessageContextService', () => { const results = await service.resolveMessageContexts( [ Fr.ZERO, // zero → null - validTxHash.hash, // valid → MessageTxContext + validTxHash.hash, // valid → MessageContext notFoundTxHash.hash, // not found → null futureTxHash.hash, // beyond anchor → null ], anchorBlockNumber, ); - expect(results).toEqual([null, new MessageTxContext(validTxHash, validNoteHashes, validNullifier), null, null]); + expect(results).toEqual([null, new MessageContext(validTxHash, validNoteHashes, validNullifier), null, null]); // Zero hash should not trigger getTxEffect expect(aztecNode.getTxEffect).toHaveBeenCalledTimes(3); diff --git a/yarn-project/pxe/src/messages/message_context_service.ts b/yarn-project/pxe/src/messages/message_context_service.ts index ae39812a7c3b..3139f1a4e3dd 100644 --- a/yarn-project/pxe/src/messages/message_context_service.ts +++ b/yarn-project/pxe/src/messages/message_context_service.ts @@ -1,9 +1,8 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; +import { MessageContext } from '@aztec/stdlib/logs'; import { TxHash } from '@aztec/stdlib/tx'; -import { MessageTxContext } from '../contract_function_simulator/noir-structs/message_tx_context.js'; - /** Resolves transaction hashes into the context needed to process messages. */ export class MessageContextService { constructor(private readonly aztecNode: AztecNode) {} @@ -15,7 +14,7 @@ export class MessageContextService { * process messages that originated from that transaction. Returns `null` for tx hashes that are zero, not yet * available, or in blocks beyond the anchor block. */ - resolveMessageContexts(txHashes: Fr[], anchorBlockNumber: number): Promise<(MessageTxContext | null)[]> { + resolveMessageContexts(txHashes: Fr[], anchorBlockNumber: number): Promise<(MessageContext | null)[]> { // TODO: optimize, we might be hitting the node to get the same txHash repeatedly return Promise.all( txHashes.map(async txHashField => { @@ -38,7 +37,7 @@ export class MessageContextService { throw new Error(`Tx effect for ${txHash} has no nullifiers`); } - return new MessageTxContext(data.txHash, data.noteHashes, data.nullifiers[0]); + return new MessageContext(data.txHash, data.noteHashes, data.nullifiers[0]); }), ); } diff --git a/yarn-project/pxe/src/notes/note_service.ts b/yarn-project/pxe/src/notes/note_service.ts index a269c97946f1..dd50499c3ffd 100644 --- a/yarn-project/pxe/src/notes/note_service.ts +++ b/yarn-project/pxe/src/notes/note_service.ts @@ -121,7 +121,7 @@ export class NoteService { noteHash: Fr, nullifier: Fr, txHash: TxHash, - recipient: AztecAddress, + scope: AztecAddress, ): Promise { // We are going to store the new note in the NoteStore, which will let us later return it via `getNotes`. // There's two things we need to check before we do this however: @@ -196,8 +196,7 @@ export class NoteService { noteIndexInTx, ); - // The note was found by `recipient`, so we use that as the scope when storing the note. - await this.noteStore.addNotes([noteDao], recipient, this.jobId); + await this.noteStore.addNotes([noteDao], scope, this.jobId); if (nullifierIndex !== undefined) { // We found nullifier index which implies that the note has already been nullified. diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 8d3e39230464..48265d378960 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -4,9 +4,9 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is called /// and if the oracle version is incompatible an error is thrown. -export const ORACLE_VERSION = 19; +export const ORACLE_VERSION = 20; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -export const ORACLE_INTERFACE_HASH = '038f85d2e84afb5f688ce868249a0c8c9f94d605690bd2f1b59b1eb978b9c670'; +export const ORACLE_INTERFACE_HASH = 'ab6bb5669bf20d3908eba5f3edaeec3bf1d828b902f5c2734054e372be6e5f22'; diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index a1000ecb7f8c..6d7b05ab88aa 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -214,6 +214,7 @@ export class PXE { node, contractStore, noteStore, + () => keyStore.getAccounts(), createLogger('pxe:contract_sync', bindings), ); const messageContextService = new MessageContextService(node); diff --git a/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts b/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts index 51217891caee..501276f802b0 100644 --- a/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts +++ b/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts @@ -7,14 +7,14 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { CapsuleStore } from './capsule_store.js'; describe('capsule data provider', () => { + let scope: AztecAddress; let contract: AztecAddress; let capsuleStore: CapsuleStore; let store: AztecLMDBStoreV2; beforeEach(async () => { - // Setup mock contract address contract = await AztecAddress.random(); - // Setup store + scope = await AztecAddress.random(); store = await openTmpStore('capsule_store_test'); capsuleStore = new CapsuleStore(store); }); @@ -24,8 +24,8 @@ describe('capsule data provider', () => { const slot = new Fr(1); const values = [new Fr(42)]; - capsuleStore.storeCapsule(contract, slot, values, 'test'); - const result = await capsuleStore.loadCapsule(contract, slot, 'test'); + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); + const result = await capsuleStore.loadCapsule(contract, slot, 'test', scope); expect(result).toEqual(values); }); @@ -33,8 +33,8 @@ describe('capsule data provider', () => { const slot = new Fr(1); const values = [new Fr(42), new Fr(43), new Fr(44)]; - capsuleStore.storeCapsule(contract, slot, values, 'test'); - const result = await capsuleStore.loadCapsule(contract, slot, 'test'); + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); + const result = await capsuleStore.loadCapsule(contract, slot, 'test', scope); expect(result).toEqual(values); }); @@ -43,10 +43,10 @@ describe('capsule data provider', () => { const initialValues = [new Fr(42)]; const newValues = [new Fr(100)]; - capsuleStore.storeCapsule(contract, slot, initialValues, 'test'); - capsuleStore.storeCapsule(contract, slot, newValues, 'test'); + capsuleStore.storeCapsule(contract, slot, initialValues, 'test', scope); + capsuleStore.storeCapsule(contract, slot, newValues, 'test', scope); - const result = await capsuleStore.loadCapsule(contract, slot, 'test'); + const result = await capsuleStore.loadCapsule(contract, slot, 'test', scope); expect(result).toEqual(newValues); }); @@ -56,19 +56,44 @@ describe('capsule data provider', () => { const values1 = [new Fr(42)]; const values2 = [new Fr(100)]; - capsuleStore.storeCapsule(contract, slot, values1, 'test'); - capsuleStore.storeCapsule(anotherContract, slot, values2, 'test'); + capsuleStore.storeCapsule(contract, slot, values1, 'test', scope); + capsuleStore.storeCapsule(anotherContract, slot, values2, 'test', scope); - const result1 = await capsuleStore.loadCapsule(contract, slot, 'test'); - const result2 = await capsuleStore.loadCapsule(anotherContract, slot, 'test'); + const result1 = await capsuleStore.loadCapsule(contract, slot, 'test', scope); + const result2 = await capsuleStore.loadCapsule(anotherContract, slot, 'test', scope); expect(result1).toEqual(values1); expect(result2).toEqual(values2); }); + it('stores values for different scopes independently', async () => { + const scopeA = await AztecAddress.random(); + const scopeB = await AztecAddress.random(); + const slot = new Fr(1); + const valuesA = [new Fr(42)]; + const valuesB = [new Fr(100)]; + + capsuleStore.storeCapsule(contract, slot, valuesA, 'test', scopeA); + capsuleStore.storeCapsule(contract, slot, valuesB, 'test', scopeB); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeA)).toEqual(valuesA); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeB)).toEqual(valuesB); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toBeNull(); + }); + + it('different scopes are isolated', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toEqual(values); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', AztecAddress.ZERO)).toBeNull(); + }); + it('returns null for non-existent slots', async () => { const slot = Fr.random(); - const result = await capsuleStore.loadCapsule(contract, slot, 'test'); + const result = await capsuleStore.loadCapsule(contract, slot, 'test', scope); expect(result).toBeNull(); }); }); @@ -78,17 +103,36 @@ describe('capsule data provider', () => { const slot = new Fr(1); const values = [new Fr(42)]; - capsuleStore.storeCapsule(contract, slot, values, 'test'); - capsuleStore.deleteCapsule(contract, slot, 'test'); + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); + capsuleStore.deleteCapsule(contract, slot, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, slot, 'test')).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toBeNull(); }); it('deletes an empty slot', async () => { const slot = new Fr(1); - capsuleStore.deleteCapsule(contract, slot, 'test'); + capsuleStore.deleteCapsule(contract, slot, 'test', scope); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toBeNull(); + }); + + it('deletes a scoped capsule without affecting other scopes', async () => { + const scopeA = await AztecAddress.random(); + const scopeB = await AztecAddress.random(); + const slot = Fr.random(); + const valuesA = [Fr.random(), Fr.random(), Fr.random()]; + const valuesB = [Fr.random(), Fr.random(), Fr.random()]; + const globalValues = [Fr.random(), Fr.random(), Fr.random()]; - expect(await capsuleStore.loadCapsule(contract, slot, 'test')).toBeNull(); + capsuleStore.storeCapsule(contract, slot, valuesA, 'test', scopeA); + capsuleStore.storeCapsule(contract, slot, valuesB, 'test', scopeB); + capsuleStore.storeCapsule(contract, slot, globalValues, 'test', scope); + + capsuleStore.deleteCapsule(contract, slot, 'test', scopeA); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeA)).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeB)).toEqual(valuesB); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scope)).toEqual(globalValues); }); }); @@ -97,85 +141,98 @@ describe('capsule data provider', () => { const slot = new Fr(1); const values = [new Fr(42)]; - capsuleStore.storeCapsule(contract, slot, values, 'test'); + capsuleStore.storeCapsule(contract, slot, values, 'test', scope); const dstSlot = new Fr(5); - await capsuleStore.copyCapsule(contract, slot, dstSlot, 1, 'test'); + await capsuleStore.copyCapsule(contract, slot, dstSlot, 1, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, dstSlot, 'test')).toEqual(values); + expect(await capsuleStore.loadCapsule(contract, dstSlot, 'test', scope)).toEqual(values); }); it('copies multiple non-overlapping values', async () => { const src = new Fr(1); const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test'); + capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test', scope); const dst = new Fr(5); - await capsuleStore.copyCapsule(contract, src, dst, 3, 'test'); + await capsuleStore.copyCapsule(contract, src, dst, 3, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, dst, 'test')).toEqual(valuesArray[0]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test')).toEqual(valuesArray[1]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test')).toEqual(valuesArray[2]); + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(valuesArray[0]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[1]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[2]); }); it('copies overlapping values with src ahead', async () => { const src = new Fr(1); const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test'); + capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test', scope); const dst = new Fr(2); - await capsuleStore.copyCapsule(contract, src, dst, 3, 'test'); + await capsuleStore.copyCapsule(contract, src, dst, 3, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, dst, 'test')).toEqual(valuesArray[0]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test')).toEqual(valuesArray[1]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test')).toEqual(valuesArray[2]); + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(valuesArray[0]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[1]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[2]); // Slots 2 and 3 (src[1] and src[2]) should have been overwritten since they are also dst[0] and dst[1] - expect(await capsuleStore.loadCapsule(contract, src, 'test')).toEqual(valuesArray[0]); // src[0] (unchanged) - expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(1)), 'test')).toEqual(valuesArray[0]); // dst[0] - expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(2)), 'test')).toEqual(valuesArray[1]); // dst[1] + expect(await capsuleStore.loadCapsule(contract, src, 'test', scope)).toEqual(valuesArray[0]); // src[0] (unchanged) + expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[0]); // dst[0] + expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[1]); // dst[1] }); it('copies overlapping values with dst ahead', async () => { const src = new Fr(5); const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test'); - capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test'); + capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(1)), valuesArray[1], 'test', scope); + capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test', scope); const dst = new Fr(4); - await capsuleStore.copyCapsule(contract, src, dst, 3, 'test'); + await capsuleStore.copyCapsule(contract, src, dst, 3, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, dst, 'test')).toEqual(valuesArray[0]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test')).toEqual(valuesArray[1]); - expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test')).toEqual(valuesArray[2]); + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(valuesArray[0]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[1]); + expect(await capsuleStore.loadCapsule(contract, dst.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[2]); // Slots 5 and 6 (src[0] and src[1]) should have been overwritten since they are also dst[1] and dst[2] - expect(await capsuleStore.loadCapsule(contract, src, 'test')).toEqual(valuesArray[1]); // dst[1] - expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(1)), 'test')).toEqual(valuesArray[2]); // dst[2] - expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(2)), 'test')).toEqual(valuesArray[2]); // src[2] (unchanged) + expect(await capsuleStore.loadCapsule(contract, src, 'test', scope)).toEqual(valuesArray[1]); // dst[1] + expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(1)), 'test', scope)).toEqual(valuesArray[2]); // dst[2] + expect(await capsuleStore.loadCapsule(contract, src.add(new Fr(2)), 'test', scope)).toEqual(valuesArray[2]); // src[2] (unchanged) }); it('copying fails if any value is empty', async () => { const src = new Fr(1); const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; - capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test'); + capsuleStore.storeCapsule(contract, src, valuesArray[0], 'test', scope); // We skip src[1] - capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test'); + capsuleStore.storeCapsule(contract, src.add(new Fr(2)), valuesArray[2], 'test', scope); const dst = new Fr(5); - await expect(capsuleStore.copyCapsule(contract, src, dst, 3, 'test')).rejects.toThrow( + await expect(capsuleStore.copyCapsule(contract, src, dst, 3, 'test', scope)).rejects.toThrow( 'Attempted to copy empty slot', ); }); + + it('copies values within a scope only', async () => { + const scope = await AztecAddress.random(); + const src = new Fr(1); + const dst = new Fr(5); + const values = [new Fr(42)]; + + capsuleStore.storeCapsule(contract, src, values, 'test', scope); + await capsuleStore.copyCapsule(contract, src, dst, 1, 'test', scope); + + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(values); + expect(await capsuleStore.loadCapsule(contract, dst, 'test', AztecAddress.ZERO)).toBeNull(); + }); }); describe('arrays', () => { @@ -184,11 +241,13 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const array = range(4).map(x => [new Fr(x)]); - await capsuleStore.appendToCapsuleArray(contract, baseSlot, array, 'test'); + await capsuleStore.appendToCapsuleArray(contract, baseSlot, array, 'test', scope); - expect(await capsuleStore.loadCapsule(contract, baseSlot, 'test')).toEqual([new Fr(array.length)]); + expect(await capsuleStore.loadCapsule(contract, baseSlot, 'test', scope)).toEqual([new Fr(array.length)]); for (const i of range(array.length)) { - expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test')).toEqual(array[i]); + expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test', scope)).toEqual( + array[i], + ); } }); @@ -196,16 +255,16 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const originalArray = range(4).map(x => [new Fr(x)]); - await capsuleStore.appendToCapsuleArray(contract, baseSlot, originalArray, 'test'); + await capsuleStore.appendToCapsuleArray(contract, baseSlot, originalArray, 'test', scope); const newElements = [[new Fr(13)], [new Fr(42)]]; - await capsuleStore.appendToCapsuleArray(contract, baseSlot, newElements, 'test'); + await capsuleStore.appendToCapsuleArray(contract, baseSlot, newElements, 'test', scope); const expectedLength = originalArray.length + newElements.length; - expect(await capsuleStore.loadCapsule(contract, baseSlot, 'test')).toEqual([new Fr(expectedLength)]); + expect(await capsuleStore.loadCapsule(contract, baseSlot, 'test', scope)).toEqual([new Fr(expectedLength)]); for (const i of range(expectedLength)) { - expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test')).toEqual( + expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test', scope)).toEqual( [...originalArray, ...newElements][i], ); } @@ -215,7 +274,7 @@ describe('capsule data provider', () => { describe('readCapsuleArray', () => { it('reads an empty array', async () => { const baseSlot = new Fr(3); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual([]); }); @@ -223,9 +282,9 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const storedArray = range(4).map(x => [new Fr(x)]); - await capsuleStore.appendToCapsuleArray(contract, baseSlot, storedArray, 'test'); + await capsuleStore.appendToCapsuleArray(contract, baseSlot, storedArray, 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual(storedArray); }); @@ -233,10 +292,10 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); // Store in the base slot a non-zero value, indicating a non-zero array length - capsuleStore.storeCapsule(contract, baseSlot, [new Fr(1)], 'test'); + capsuleStore.storeCapsule(contract, baseSlot, [new Fr(1)], 'test', scope); // Reading should now fail as some of the capsules in the array are empty - await expect(capsuleStore.readCapsuleArray(contract, baseSlot, 'test')).rejects.toThrow( + await expect(capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope)).rejects.toThrow( 'Expected non-empty value', ); }); @@ -247,9 +306,9 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const newArray = range(4).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual(newArray); }); @@ -257,12 +316,12 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const originalArray = range(4, 0).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test', scope); const newArray = range(10, 10).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual(newArray); }); @@ -270,18 +329,18 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const originalArray = range(10, 0).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test', scope); const newArray = range(4, 10).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, newArray, 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual(newArray); // Not only do we read the expected array, but also all capsules past the new array length have been cleared for (const i of range(originalArray.length - newArray.length)) { expect( - await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + newArray.length + i)), 'test'), + await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + newArray.length + i)), 'test', scope), ).toBeNull(); } }); @@ -290,16 +349,16 @@ describe('capsule data provider', () => { const baseSlot = new Fr(3); const originalArray = range(10, 0).map(x => [new Fr(x)]); - await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, originalArray, 'test', scope); - await capsuleStore.setCapsuleArray(contract, baseSlot, [], 'test'); + await capsuleStore.setCapsuleArray(contract, baseSlot, [], 'test', scope); - const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test'); + const retrievedArray = await capsuleStore.readCapsuleArray(contract, baseSlot, 'test', scope); expect(retrievedArray).toEqual([]); // All capsules from the original array have been cleared for (const i of range(originalArray.length)) { - expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test')).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, baseSlot.add(new Fr(1 + i)), 'test', scope)).toBeNull(); } }); }); @@ -327,6 +386,7 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { @@ -344,6 +404,7 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { @@ -361,6 +422,7 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { @@ -368,7 +430,13 @@ describe('capsule data provider', () => { }); // Append a single element - await capsuleStore.appendToCapsuleArray(contract, new Fr(0), [range(ARRAY_LENGTH).map(x => new Fr(x))], 'test'); + await capsuleStore.appendToCapsuleArray( + contract, + new Fr(0), + [range(ARRAY_LENGTH).map(x => new Fr(x))], + 'test', + scope, + ); await store.transactionAsync(async () => { await capsuleStore.commit('test'); @@ -385,6 +453,7 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { @@ -392,7 +461,7 @@ describe('capsule data provider', () => { }); // We just move the entire thing one slot. - await capsuleStore.copyCapsule(contract, new Fr(0), new Fr(1), NUMBER_OF_ITEMS, 'test'); + await capsuleStore.copyCapsule(contract, new Fr(0), new Fr(1), NUMBER_OF_ITEMS, 'test', scope); await store.transactionAsync(async () => { await capsuleStore.commit('test'); @@ -409,13 +478,14 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { await capsuleStore.commit('test'); }); - await capsuleStore.readCapsuleArray(contract, new Fr(0), 'test'); + await capsuleStore.readCapsuleArray(contract, new Fr(0), 'test', scope); await store.transactionAsync(async () => { await capsuleStore.commit('test'); @@ -432,13 +502,14 @@ describe('capsule data provider', () => { new Fr(0), times(NUMBER_OF_ITEMS, () => range(ARRAY_LENGTH).map(x => new Fr(x))), 'test', + scope, ); await store.transactionAsync(async () => { await capsuleStore.commit('test'); }); - await capsuleStore.setCapsuleArray(contract, new Fr(0), [], 'test'); + await capsuleStore.setCapsuleArray(contract, new Fr(0), [], 'test', scope); await store.transactionAsync(async () => { await capsuleStore.commit('test'); @@ -460,20 +531,20 @@ describe('capsule data provider', () => { const committedValues1 = [Fr.random()]; const committedValues2 = [Fr.random()]; - capsuleStore.storeCapsule(contract, slot, committedValues1, 'job-1'); + capsuleStore.storeCapsule(contract, slot, committedValues1, 'job-1', scope); // After this commit, 'job-1' should logically be reset // Any read of contract-slot after this should see committedValues1 await capsuleStore.commit('job-1'); // Any read of contract-slot should see job2committedValues - capsuleStore.storeCapsule(contract, slot, committedValues2, 'job-2'); + capsuleStore.storeCapsule(contract, slot, committedValues2, 'job-2', scope); await capsuleStore.commit('job-2'); // If we failed to properly dispose 'job-1's staged writes on commit, // Instead of reading committedValues2 (as we should), we would end // up reading committedValues1 (which would be wrong) - expect(await capsuleStore.loadCapsule(contract, slot, 'job-1')).toEqual(committedValues2); + expect(await capsuleStore.loadCapsule(contract, slot, 'job-1', scope)).toEqual(committedValues2); }); it('writes to job view are isolated from another job view', async () => { @@ -485,17 +556,17 @@ describe('capsule data provider', () => { const stagedJob2: string = 'staged-job-2'; // First set a committed capsule (using a different job that we commit) - capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId); + capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId, scope); await capsuleStore.commit(commitJobId); // Then set a staged capsule (not committed) - capsuleStore.storeCapsule(contract, slot, stagedValues, stagedJob1); + capsuleStore.storeCapsule(contract, slot, stagedValues, stagedJob1, scope); // With jobId=1, should get staged capsule - expect(await capsuleStore.loadCapsule(contract, slot, stagedJob1)).toEqual(stagedValues); + expect(await capsuleStore.loadCapsule(contract, slot, stagedJob1, scope)).toEqual(stagedValues); // With jobId=2, should get committed capsule - expect(await capsuleStore.loadCapsule(contract, slot, stagedJob2)).toEqual(committedValues); + expect(await capsuleStore.loadCapsule(contract, slot, stagedJob2, scope)).toEqual(committedValues); }); it('staged deletions hide committed data', async () => { @@ -506,17 +577,17 @@ describe('capsule data provider', () => { const stagedJob2: string = 'staged-job-2'; // First set a committed capsule - capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId); + capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId, scope); await capsuleStore.commit(commitJobId); // Delete in staging (not committed) - capsuleStore.deleteCapsule(contract, slot, stagedJob1); + capsuleStore.deleteCapsule(contract, slot, stagedJob1, scope); // Without jobId=2, should still see committed capsule - expect(await capsuleStore.loadCapsule(contract, slot, stagedJob2)).toEqual(committedValues); + expect(await capsuleStore.loadCapsule(contract, slot, stagedJob2, scope)).toEqual(committedValues); // With jobId=1, should see null (deleted in staging) - expect(await capsuleStore.loadCapsule(contract, slot, stagedJob1)).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, slot, stagedJob1, scope)).toBeNull(); }); it('commit applies staged deletions', async () => { @@ -525,14 +596,14 @@ describe('capsule data provider', () => { const commitJobId: string = 'commit-job'; const deleteJobId: string = 'delete-job'; - capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId); + capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId, scope); await capsuleStore.commit(commitJobId); - capsuleStore.deleteCapsule(contract, slot, deleteJobId); + capsuleStore.deleteCapsule(contract, slot, deleteJobId, scope); await capsuleStore.commit(deleteJobId); // Now any job should see this null (deleted) - expect(await capsuleStore.loadCapsule(contract, slot, 'any-job-sees-this')).toBeNull(); + expect(await capsuleStore.loadCapsule(contract, slot, 'any-job-sees-this', scope)).toBeNull(); }); it('discardStaged removes staged data without affecting main', async () => { @@ -542,17 +613,17 @@ describe('capsule data provider', () => { const commitJobId: string = 'commit-job'; const stagingJobId: string = 'staging-job'; - capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId); + capsuleStore.storeCapsule(contract, slot, committedValues, commitJobId, scope); await capsuleStore.commit(commitJobId); - capsuleStore.storeCapsule(contract, slot, stagedValues, stagingJobId); + capsuleStore.storeCapsule(contract, slot, stagedValues, stagingJobId, scope); await capsuleStore.discardStaged(stagingJobId); // Should still get committed capsule - expect(await capsuleStore.loadCapsule(contract, slot, 'any-job')).toEqual(committedValues); + expect(await capsuleStore.loadCapsule(contract, slot, 'any-job', scope)).toEqual(committedValues); // With stagingJobId should fall back to committed since staging was discarded - expect(await capsuleStore.loadCapsule(contract, slot, stagingJobId)).toEqual(committedValues); + expect(await capsuleStore.loadCapsule(contract, slot, stagingJobId, scope)).toEqual(committedValues); }); }); }); diff --git a/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts b/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts index 0b847c06df52..4986a6e94a30 100644 --- a/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts +++ b/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts @@ -1,7 +1,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { StagedStore } from '../../job_coordinator/job_coordinator.js'; @@ -10,11 +10,12 @@ export class CapsuleStore implements StagedStore { #store: AztecAsyncKVStore; - // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${key}` + // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${scope}:${key}`, using the zero + // address for the global scope. #capsules: AztecAsyncMap; - // jobId => `${contractAddress}:${key}` => capsule data - // when `#stagedCapsules.get('some-job-id').get('${some-contract-address:some-key') === null`, + // jobId => `${contractAddress}:${scope}:${key}` => capsule data + // when `#stagedCapsules.get('some-job-id').get('${some-contract-address}:${some-scope}:${some-key}') === null`, // it signals that the capsule was deleted during the job, so it needs to be deleted on commit #stagedCapsules: Map>; @@ -134,8 +135,8 @@ export class CapsuleStore implements StagedStore { * to public contract storage in that it's indexed by the contract address and storage slot but instead of the global * network state it's backed by local PXE db. */ - storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], jobId: string) { - const dbSlotKey = dbSlotToKey(contractAddress, slot); + storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], jobId: string, scope: AztecAddress) { + const dbSlotKey = dbSlotToKey(contractAddress, slot, scope); // A store overrides any pre-existing data on the slot this.#setOnStage(jobId, dbSlotKey, Buffer.concat(capsule.map(value => value.toBuffer()))); @@ -147,8 +148,8 @@ export class CapsuleStore implements StagedStore { * @param slot - The slot in the database to read. * @returns The stored data or `null` if no data is stored under the slot. */ - async loadCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string): Promise { - const dataBuffer = await this.#getFromStage(jobId, dbSlotToKey(contractAddress, slot)); + async loadCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string, scope: AztecAddress): Promise { + const dataBuffer = await this.#getFromStage(jobId, dbSlotToKey(contractAddress, slot, scope)); if (!dataBuffer) { this.logger.trace(`Data not found for contract ${contractAddress.toString()} and slot ${slot.toString()}`); return null; @@ -165,9 +166,9 @@ export class CapsuleStore implements StagedStore { * @param contractAddress - The contract address under which the data is scoped. * @param slot - The slot in the database to delete. */ - deleteCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string) { + deleteCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string, scope: AztecAddress) { // When we commit this, we will interpret null as a deletion, so we'll propagate the delete to the KV store - this.#deleteOnStage(jobId, dbSlotToKey(contractAddress, slot)); + this.#deleteOnStage(jobId, dbSlotToKey(contractAddress, slot, scope)); } /** @@ -187,6 +188,7 @@ export class CapsuleStore implements StagedStore { dstSlot: Fr, numEntries: number, jobId: string, + scope: AztecAddress, ): Promise { // This transactional context gives us "copy atomicity": // there shouldn't be concurrent writes to what's being copied here. @@ -203,8 +205,8 @@ export class CapsuleStore implements StagedStore { } for (const i of indexes) { - const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i))); - const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i))); + const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i)), scope); + const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i)), scope); const toCopy = await this.#getFromStage(jobId, currentSrcSlot); if (!toCopy) { @@ -224,7 +226,13 @@ export class CapsuleStore implements StagedStore { * @param baseSlot - The slot where the array length is stored * @param content - Array of capsule data to append */ - appendToCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, content: Fr[][], jobId: string): Promise { + appendToCapsuleArray( + contractAddress: AztecAddress, + baseSlot: Fr, + content: Fr[][], + jobId: string, + scope: AztecAddress, + ): Promise { // We wrap this in a transaction to serialize concurrent calls from Promise.all. // Without this, concurrent appends to the same array could race: both read length=0, // both write at the same slots, one overwrites the other. @@ -232,22 +240,22 @@ export class CapsuleStore implements StagedStore { // and not using a transaction here would heavily impact performance. return this.#store.transactionAsync(async () => { // Load current length, defaulting to 0 if not found - const lengthData = await this.loadCapsule(contractAddress, baseSlot, jobId); + const lengthData = await this.loadCapsule(contractAddress, baseSlot, jobId, scope); const currentLength = lengthData ? lengthData[0].toNumber() : 0; // Store each capsule at consecutive slots after baseSlot + 1 + currentLength for (let i = 0; i < content.length; i++) { const nextSlot = arraySlot(baseSlot, currentLength + i); - this.storeCapsule(contractAddress, nextSlot, content[i], jobId); + this.storeCapsule(contractAddress, nextSlot, content[i], jobId, scope); } // Update length to include all new capsules const newLength = currentLength + content.length; - this.storeCapsule(contractAddress, baseSlot, [new Fr(newLength)], jobId); + this.storeCapsule(contractAddress, baseSlot, [new Fr(newLength)], jobId, scope); }); } - readCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, jobId: string): Promise { + readCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, jobId: string, scope: AztecAddress): Promise { // I'm leaving this transactional context here though because I'm assuming this // gives us "read array atomicity": there shouldn't be concurrent writes to what's being copied // here. @@ -255,14 +263,14 @@ export class CapsuleStore implements StagedStore { // of jobs: different calls running concurrently on the same contract may cause trouble. return this.#store.transactionAsync(async () => { // Load length, defaulting to 0 if not found - const maybeLength = await this.loadCapsule(contractAddress, baseSlot, jobId); + const maybeLength = await this.loadCapsule(contractAddress, baseSlot, jobId, scope); const length = maybeLength ? maybeLength[0].toBigInt() : 0n; const values: Fr[][] = []; // Read each capsule at consecutive slots after baseSlot for (let i = 0; i < length; i++) { - const currentValue = await this.loadCapsule(contractAddress, arraySlot(baseSlot, i), jobId); + const currentValue = await this.loadCapsule(contractAddress, arraySlot(baseSlot, i), jobId, scope); if (currentValue == undefined) { throw new Error( `Expected non-empty value at capsule array in base slot ${baseSlot} at index ${i} for contract ${contractAddress}`, @@ -276,7 +284,7 @@ export class CapsuleStore implements StagedStore { }); } - setCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, content: Fr[][], jobId: string) { + setCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, content: Fr[][], jobId: string, scope: AztecAddress) { // This transactional context in theory isn't so critical now because we aren't // writing to DB so if there's exceptions midway and it blows up, no visible impact // to persistent storage will happen. @@ -287,27 +295,27 @@ export class CapsuleStore implements StagedStore { // of jobs: different calls running concurrently on the same contract may cause trouble. return this.#store.transactionAsync(async () => { // Load current length, defaulting to 0 if not found - const maybeLength = await this.loadCapsule(contractAddress, baseSlot, jobId); + const maybeLength = await this.loadCapsule(contractAddress, baseSlot, jobId, scope); const originalLength = maybeLength ? maybeLength[0].toNumber() : 0; // Set the new length - this.storeCapsule(contractAddress, baseSlot, [new Fr(content.length)], jobId); + this.storeCapsule(contractAddress, baseSlot, [new Fr(content.length)], jobId, scope); // Store the new content, possibly overwriting existing values for (let i = 0; i < content.length; i++) { - this.storeCapsule(contractAddress, arraySlot(baseSlot, i), content[i], jobId); + this.storeCapsule(contractAddress, arraySlot(baseSlot, i), content[i], jobId, scope); } // Clear any stragglers for (let i = content.length; i < originalLength; i++) { - this.deleteCapsule(contractAddress, arraySlot(baseSlot, i), jobId); + this.deleteCapsule(contractAddress, arraySlot(baseSlot, i), jobId, scope); } }); } } -function dbSlotToKey(contractAddress: AztecAddress, slot: Fr): string { - return `${contractAddress.toString()}:${slot.toString()}`; +function dbSlotToKey(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): string { + return [contractAddress.toString(), scope.toString(), slot.toString()].join(':'); } function arraySlot(baseSlot: Fr, index: number) { diff --git a/yarn-project/pxe/src/storage/metadata.ts b/yarn-project/pxe/src/storage/metadata.ts index 826f90735b91..aa63404894b5 100644 --- a/yarn-project/pxe/src/storage/metadata.ts +++ b/yarn-project/pxe/src/storage/metadata.ts @@ -1 +1 @@ -export const PXE_DATA_SCHEMA_VERSION = 4; +export const PXE_DATA_SCHEMA_VERSION = 5; diff --git a/yarn-project/stdlib/src/logs/message_context.test.ts b/yarn-project/stdlib/src/logs/message_context.test.ts index 2193985f2259..474342f6129b 100644 --- a/yarn-project/stdlib/src/logs/message_context.test.ts +++ b/yarn-project/stdlib/src/logs/message_context.test.ts @@ -1,7 +1,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { updateInlineTestData } from '@aztec/foundation/testing/files'; -import { AztecAddress } from '../aztec-address/index.js'; import { TxHash } from '../tx/tx_hash.js'; import { MessageContext } from './message_context.js'; @@ -11,10 +10,9 @@ describe('MessageContext', () => { const txHash = new TxHash(new Fr(123n)); const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; const firstNullifier = new Fr(6n); - const recipient = AztecAddress.fromField(new Fr(789n)); // Create a MessageContext instance - const messageContext = new MessageContext(txHash, uniqueNoteHashes, firstNullifier, recipient); + const messageContext = new MessageContext(txHash, uniqueNoteHashes, firstNullifier); // Serialize the message context const serialized = messageContext.toFields(); @@ -89,17 +87,41 @@ describe('MessageContext', () => { "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000315", ] `); - // Optionally update Noir test data - const fieldArrayStr = `[${serialized.map(f => f.toString()).join(',')}]`; // Run with AZTEC_GENERATE_TEST_DATA=1 to update noir test data + const fieldArrayStr = `[${serialized.map(f => f.toString()).join(',')}]`; updateInlineTestData( 'noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr', 'serialized_message_context_from_typescript', fieldArrayStr, ); }); + + it('serialization of some option matches snapshot', () => { + const txHash = new TxHash(new Fr(123)); + const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; + const firstNullifier = new Fr(6n); + const ctx = new MessageContext(txHash, uniqueNoteHashes, firstNullifier); + const serialized = MessageContext.toSerializedOption(ctx); + // is_some flag + fields + expect(serialized[0]).toEqual(new Fr(1)); + expect(serialized.length).toEqual(1 + ctx.toFields().length); + }); + + it('serialization of none option matches snapshot', () => { + const serialized = MessageContext.toSerializedOption(null); + expect(serialized[0]).toEqual(new Fr(0)); + // All fields should be zero + for (const f of serialized) { + expect(f).toEqual(Fr.zero()); + } + }); + + it('serialization length of empty matches some', () => { + const txHash = new TxHash(new Fr(123)); + const ctx = new MessageContext(txHash, [new Fr(4n), new Fr(5n)], new Fr(6n)); + expect(ctx.toFields().length).toEqual(MessageContext.toEmptyFields().length); + }); }); diff --git a/yarn-project/stdlib/src/logs/message_context.ts b/yarn-project/stdlib/src/logs/message_context.ts index e4e0c2286090..99570a49613f 100644 --- a/yarn-project/stdlib/src/logs/message_context.ts +++ b/yarn-project/stdlib/src/logs/message_context.ts @@ -1,8 +1,7 @@ import { MAX_NOTE_HASHES_PER_TX } from '@aztec/constants'; +import { range } from '@aztec/foundation/array'; import { Fr } from '@aztec/foundation/curves/bn254'; -import type { AztecAddress } from '../aztec-address/index.js'; -import type { TxEffect } from '../tx/tx_effect.js'; import type { TxHash } from '../tx/tx_hash.js'; /** @@ -19,7 +18,6 @@ export class MessageContext { public txHash: TxHash, public uniqueNoteHashesInTx: Fr[], public firstNullifierInTx: Fr, - public recipient: AztecAddress, ) {} toFields(): Fr[] { @@ -27,7 +25,6 @@ export class MessageContext { this.txHash.hash, ...serializeBoundedVec(this.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX), this.firstNullifierInTx, - this.recipient.toField(), ]; } @@ -37,13 +34,22 @@ export class MessageContext { tx_hash: this.txHash.hash, unique_note_hashes_in_tx: this.uniqueNoteHashesInTx, first_nullifier_in_tx: this.firstNullifierInTx, - recipient: this.recipient, }; /* eslint-enable camelcase */ } - static fromTxEffectAndRecipient(txEffect: TxEffect, recipient: AztecAddress): MessageContext { - return new MessageContext(txEffect.txHash, txEffect.noteHashes, txEffect.nullifiers[0], recipient); + static toEmptyFields(): Fr[] { + const serializationLen = + 1 /* txHash */ + MAX_NOTE_HASHES_PER_TX + 1 /* uniqueNoteHashesInTx BVec */ + 1; /* firstNullifierInTx */ + return range(serializationLen).map(_ => Fr.zero()); + } + + static toSerializedOption(response: MessageContext | null): Fr[] { + if (response) { + return [new Fr(1), ...response.toFields()]; + } else { + return [new Fr(0), ...MessageContext.toEmptyFields()]; + } } } @@ -55,6 +61,10 @@ export class MessageContext { * @dev Copied over from pending_tagged_log.ts. */ function serializeBoundedVec(values: Fr[], maxLength: number): Fr[] { + if (values.length > maxLength) { + throw new Error(`Attempted to serialize ${values} values into a BoundedVec with max length ${maxLength}`); + } + const lengthDiff = maxLength - values.length; const zeroPaddingArray = Array(lengthDiff).fill(Fr.ZERO); const storage = values.concat(zeroPaddingArray); diff --git a/yarn-project/stdlib/src/logs/pending_tagged_log.test.ts b/yarn-project/stdlib/src/logs/pending_tagged_log.test.ts index 30b023c9b0c4..b327fc5237fa 100644 --- a/yarn-project/stdlib/src/logs/pending_tagged_log.test.ts +++ b/yarn-project/stdlib/src/logs/pending_tagged_log.test.ts @@ -1,7 +1,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { updateInlineTestData } from '@aztec/foundation/testing/files'; -import { AztecAddress } from '../aztec-address/index.js'; import { TxHash } from '../tx/tx_hash.js'; import { PendingTaggedLog } from './pending_tagged_log.js'; @@ -11,9 +10,8 @@ describe('PendingTaggedLog', () => { const txHash = new TxHash(new Fr(123n)); const uniqueNoteHashes = [new Fr(4n), new Fr(5n)]; const firstNullifier = new Fr(6n); - const recipient = AztecAddress.fromField(new Fr(789n)); - const pendingLog = new PendingTaggedLog(log, txHash, uniqueNoteHashes, firstNullifier, recipient); + const pendingLog = new PendingTaggedLog(log, txHash, uniqueNoteHashes, firstNullifier); const serialized = pendingLog.toFields(); // Test against snapshot @@ -103,7 +101,6 @@ describe('PendingTaggedLog', () => { "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000315", ] `); diff --git a/yarn-project/stdlib/src/logs/pending_tagged_log.ts b/yarn-project/stdlib/src/logs/pending_tagged_log.ts index 1921736e0aa4..70b0f85ecac1 100644 --- a/yarn-project/stdlib/src/logs/pending_tagged_log.ts +++ b/yarn-project/stdlib/src/logs/pending_tagged_log.ts @@ -1,7 +1,6 @@ import { PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; import { Fr } from '@aztec/foundation/curves/bn254'; -import type { AztecAddress } from '../aztec-address/index.js'; import type { TxHash } from '../tx/tx_hash.js'; import { MessageContext } from './message_context.js'; @@ -17,9 +16,8 @@ export class PendingTaggedLog { txHash: TxHash, uniqueNoteHashesInTx: Fr[], firstNullifierInTx: Fr, - recipient: AztecAddress, ) { - this.context = new MessageContext(txHash, uniqueNoteHashesInTx, firstNullifierInTx, recipient); + this.context = new MessageContext(txHash, uniqueNoteHashesInTx, firstNullifierInTx); } toFields(): Fr[] { diff --git a/yarn-project/stdlib/src/tx/capsule.ts b/yarn-project/stdlib/src/tx/capsule.ts index 8efeee24983f..eaff72c8c56a 100644 --- a/yarn-project/stdlib/src/tx/capsule.ts +++ b/yarn-project/stdlib/src/tx/capsule.ts @@ -19,6 +19,8 @@ export class Capsule { public readonly storageSlot: Fr, /** Data passed to the contract */ public readonly data: Fr[], + /** Optional namespace for the capsule contents */ + public readonly scope?: AztecAddress, ) {} static get schema() { @@ -30,12 +32,18 @@ export class Capsule { } toBuffer() { - return serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data)); + return this.scope + ? serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data), true, this.scope) + : serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data), false); } static fromBuffer(buffer: Buffer | BufferReader): Capsule { const reader = BufferReader.asReader(buffer); - return new Capsule(AztecAddress.fromBuffer(reader), Fr.fromBuffer(reader), reader.readVector(Fr)); + const contractAddress = AztecAddress.fromBuffer(reader); + const storageSlot = Fr.fromBuffer(reader); + const data = reader.readVector(Fr); + const hasScope = reader.readBoolean(); + return new Capsule(contractAddress, storageSlot, data, hasScope ? AztecAddress.fromBuffer(reader) : undefined); } toString() { diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index ecaee7e8b3b7..1834c3afc6aa 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -738,10 +738,14 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_fetchTaggedLogs(foreignPendingTaggedLogArrayBaseSlot: ForeignCallSingle) { + async aztec_utl_fetchTaggedLogs( + foreignPendingTaggedLogArrayBaseSlot: ForeignCallSingle, + foreignScope: ForeignCallSingle, + ) { const pendingTaggedLogArrayBaseSlot = fromSingle(foreignPendingTaggedLogArrayBaseSlot); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - await this.handlerAsUtility().fetchTaggedLogs(pendingTaggedLogArrayBaseSlot); + await this.handlerAsUtility().fetchTaggedLogs(pendingTaggedLogArrayBaseSlot, scope); return toForeignCallResult([]); } @@ -753,12 +757,14 @@ export class RPCTranslator { foreignEventValidationRequestsArrayBaseSlot: ForeignCallSingle, foreignMaxNotePackedLen: ForeignCallSingle, foreignMaxEventSerializedLen: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const noteValidationRequestsArrayBaseSlot = fromSingle(foreignNoteValidationRequestsArrayBaseSlot); const eventValidationRequestsArrayBaseSlot = fromSingle(foreignEventValidationRequestsArrayBaseSlot); const maxNotePackedLen = fromSingle(foreignMaxNotePackedLen).toNumber(); const maxEventSerializedLen = fromSingle(foreignMaxEventSerializedLen).toNumber(); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); await this.handlerAsUtility().validateAndStoreEnqueuedNotesAndEvents( contractAddress, @@ -766,6 +772,7 @@ export class RPCTranslator { eventValidationRequestsArrayBaseSlot, maxNotePackedLen, maxEventSerializedLen, + scope, ); return toForeignCallResult([]); @@ -776,15 +783,18 @@ export class RPCTranslator { foreignContractAddress: ForeignCallSingle, foreignLogRetrievalRequestsArrayBaseSlot: ForeignCallSingle, foreignLogRetrievalResponsesArrayBaseSlot: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const logRetrievalRequestsArrayBaseSlot = fromSingle(foreignLogRetrievalRequestsArrayBaseSlot); const logRetrievalResponsesArrayBaseSlot = fromSingle(foreignLogRetrievalResponsesArrayBaseSlot); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); await this.handlerAsUtility().bulkRetrieveLogs( contractAddress, logRetrievalRequestsArrayBaseSlot, logRetrievalResponsesArrayBaseSlot, + scope, ); return toForeignCallResult([]); @@ -795,31 +805,36 @@ export class RPCTranslator { foreignContractAddress: ForeignCallSingle, foreignMessageContextRequestsArrayBaseSlot: ForeignCallSingle, foreignMessageContextResponsesArrayBaseSlot: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const messageContextRequestsArrayBaseSlot = fromSingle(foreignMessageContextRequestsArrayBaseSlot); const messageContextResponsesArrayBaseSlot = fromSingle(foreignMessageContextResponsesArrayBaseSlot); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); await this.handlerAsUtility().resolveMessageContexts( contractAddress, messageContextRequestsArrayBaseSlot, messageContextResponsesArrayBaseSlot, + scope, ); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - async aztec_utl_storeCapsule( + aztec_utl_storeCapsule( foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle, foreignCapsule: ForeignCallArray, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); const capsule = fromArray(foreignCapsule); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - await this.handlerAsUtility().storeCapsule(contractAddress, slot, capsule); + this.handlerAsUtility().storeCapsule(contractAddress, slot, capsule, scope); return toForeignCallResult([]); } @@ -829,12 +844,14 @@ export class RPCTranslator { foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle, foreignTSize: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); const tSize = fromSingle(foreignTSize).toNumber(); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - const values = await this.handlerAsUtility().loadCapsule(contractAddress, slot); + const values = await this.handlerAsUtility().loadCapsule(contractAddress, slot, scope); // We are going to return a Noir Option struct to represent the possibility of null values. Options are a struct // with two fields: `some` (a boolean) and `value` (a field array in this case). @@ -848,11 +865,16 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_deleteCapsule(foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle) { + aztec_utl_deleteCapsule( + foreignContractAddress: ForeignCallSingle, + foreignSlot: ForeignCallSingle, + foreignScope: ForeignCallSingle, + ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - await this.handlerAsUtility().deleteCapsule(contractAddress, slot); + this.handlerAsUtility().deleteCapsule(contractAddress, slot, scope); return toForeignCallResult([]); } @@ -863,13 +885,15 @@ export class RPCTranslator { foreignSrcSlot: ForeignCallSingle, foreignDstSlot: ForeignCallSingle, foreignNumEntries: ForeignCallSingle, + foreignScope: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const srcSlot = fromSingle(foreignSrcSlot); const dstSlot = fromSingle(foreignDstSlot); const numEntries = fromSingle(foreignNumEntries).toNumber(); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); - await this.handlerAsUtility().copyCapsule(contractAddress, srcSlot, dstSlot, numEntries); + await this.handlerAsUtility().copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, scope); return toForeignCallResult([]); } diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index 5976e9f346a6..db91399fb541 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -3,6 +3,7 @@ import { TestCircuitVerifier } from '@aztec/bb-prover/test'; import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; +import type { KeyStore } from '@aztec/key-store'; import { type AnchorBlockStore, type ContractStore, ContractSyncService, type NoteStore } from '@aztec/pxe/server'; import { MessageContextService } from '@aztec/pxe/simulator'; import { L2Block } from '@aztec/stdlib/block'; @@ -35,6 +36,7 @@ export class TXEStateMachine { anchorBlockStore: AnchorBlockStore, contractStore: ContractStore, noteStore: NoteStore, + keyStore: KeyStore, ) { const synchronizer = await TXESynchronizer.create(); const aztecNodeConfig = {} as AztecNodeConfig; @@ -67,6 +69,7 @@ export class TXEStateMachine { node, contractStore, noteStore, + () => keyStore.getAccounts(), createLogger('txe:contract_sync'), ); diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index 38e447b73153..71958ef8740b 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -179,7 +179,7 @@ export class TXESession implements TXESessionStateHandler { const archiver = new TXEArchiver(store); const anchorBlockStore = new AnchorBlockStore(store); - const stateMachine = await TXEStateMachine.create(archiver, anchorBlockStore, contractStore, noteStore); + const stateMachine = await TXEStateMachine.create(archiver, anchorBlockStore, contractStore, noteStore, keyStore); const nextBlockTimestamp = BigInt(Math.floor(new Date().getTime() / 1000)); const version = new Fr(await stateMachine.node.getVersion()); @@ -188,7 +188,13 @@ export class TXESession implements TXESessionStateHandler { const initialJobId = jobCoordinator.beginJob(); const logger = createLogger('txe:session'); - const contractSyncService = new ContractSyncService(stateMachine.node, contractStore, noteStore, logger); + const contractSyncService = new ContractSyncService( + stateMachine.node, + contractStore, + noteStore, + () => keyStore.getAccounts(), + logger, + ); const topLevelOracleHandler = new TXEOracleTopLevelContext( stateMachine, From 678c3a21da84d48deea15e083ae9c440394de5ad Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Wed, 25 Mar 2026 10:46:36 -0300 Subject: [PATCH 02/22] feat(aztec-nr): add initialization check to utility functions (#21751) --- .../macros/functions/initialization_utils.nr | 20 +++++++++++-- .../external/utility.nr | 16 +++++++++- .../test/init_test_contract/src/main.nr | 5 ++++ .../private_initialization.test.ts | 19 ++++++++++++ .../oracle/utility_execution.test.ts | 29 +++++++++++++++---- 5 files changed, 80 insertions(+), 9 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr index 2b1d6b32fa0e..3ef86dc96953 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr @@ -10,13 +10,16 @@ use crate::protocol::{ use std::meta::{ctstring::AsCtString, unquote}; use crate::{ - context::{PrivateContext, PublicContext}, + context::{PrivateContext, PublicContext, UtilityContext}, macros::{ internals_functions_generation::external_functions_registry::get_public_functions, utils::fn_has_noinitcheck, }, nullifier::utils::compute_nullifier_existence_request, - oracle::get_contract_instance::{ - get_contract_instance, get_contract_instance_deployer_avm, get_contract_instance_initialization_hash_avm, + oracle::{ + get_contract_instance::{ + get_contract_instance, get_contract_instance_deployer_avm, get_contract_instance_initialization_hash_avm, + }, + nullifiers::check_nullifier_exists, }, }; @@ -111,6 +114,17 @@ pub fn assert_is_initialized_private(context: &mut PrivateContext) { context.assert_nullifier_exists(nullifier_existence_request); } +/// Asserts that the contract has been initialized, from a utility function's perspective. +/// +/// Only checks the private initialization nullifier in the settled nullifier tree. Since both nullifiers are emitted in +/// the same transaction, the private nullifier's presence in settled state guarantees the public one is also settled. +pub unconstrained fn assert_is_initialized_utility(context: UtilityContext) { + let address = context.this_address(); + let instance = get_contract_instance(address); + let initialization_nullifier = compute_private_initialization_nullifier(address, instance.initialization_hash); + assert(check_nullifier_exists(initialization_nullifier), "Not initialized"); +} + /// Computes the private initialization nullifier for a contract. /// /// Including `init_hash` ensures that an observer who knows only the contract address cannot reconstruct this value diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr index f50bbc1249d7..55b7ec2ee515 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr @@ -1,6 +1,8 @@ -use crate::macros::utils::module_has_storage; +use crate::macros::utils::{fn_has_noinitcheck, module_has_initializer, module_has_storage}; pub(crate) comptime fn generate_utility_external(f: FunctionDefinition) -> Quoted { + let module_has_initializer = module_has_initializer(f.module()); + // Initialize Storage if module has storage let storage_init = if module_has_storage(f.module()) { quote { @@ -26,10 +28,22 @@ pub(crate) comptime fn generate_utility_external(f: FunctionDefinition) -> Quote }; }; + // Initialization checks are not included in contracts that don't have initializers. + let init_check = if module_has_initializer & !fn_has_noinitcheck(f) { + quote { + aztec::macros::functions::initialization_utils::assert_is_initialized_utility( + self.context, + ); + } + } else { + quote {} + }; + // A quote to be injected at the beginning of the function body. let to_prepend = quote { aztec::oracle::version::assert_compatible_oracle_version(); $contract_self_creation + $init_check }; let original_function_name = f.name(); diff --git a/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr index 2120f72e0fa5..b724e3ae574a 100644 --- a/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr @@ -74,4 +74,9 @@ pub contract InitTest { fn pub_no_init_check(owner: AztecAddress) -> pub Field { self.storage.public_values.at(owner).read() } + + #[external("utility")] + unconstrained fn utility_init_check(owner: AztecAddress) -> Field { + self.storage.public_values.at(owner).read() + } } diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts index ea5443226953..e90493db4b46 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts @@ -140,6 +140,25 @@ describe('e2e_deploy_contract private initialization', () => { ); }); + it('refuses to simulate a utility function that requires initialization', async () => { + const owner = (await wallet.createAccount()).address; + const initArgs: InitTestCtorArgs = [owner, 42]; + const contract = await t.registerContract(wallet, InitTestContract, { initArgs }); + await expect(contract.methods.utility_init_check(owner).simulate({ from: defaultAccountAddress })).rejects.toThrow( + /Not initialized/, + ); + }); + + it('allows calling a utility function after initialization', async () => { + const { contract, initArgs } = await deployUninitialized(); + const owner = defaultAccountAddress; + await contract.methods.constructor(...initArgs).send({ from: defaultAccountAddress }); + // Write a value via public function so the utility function has something to read. + await contract.methods.pub_init_check(owner, 84).send({ from: defaultAccountAddress }); + const result = await contract.methods.utility_init_check(owner).simulate({ from: defaultAccountAddress }); + expect(result.result).toEqual(84n); + }); + // A public call enqueued before the private constructor should fail the init check, even though the // private constructor emits the init nullifier in the same tx. it('refuses to call a public function enqueued before private initialization in same tx', async () => { diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 6da7fd7fe739..6affe5d2791f 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -7,9 +7,13 @@ import { WASMSimulator } from '@aztec/simulator/client'; import { FunctionCall, FunctionSelector, FunctionType, encodeArguments } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; -import { CompleteAddress, type ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import { + CompleteAddress, + type ContractInstanceWithAddress, + computeContractAddressFromInstance, +} from '@aztec/stdlib/contract'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; -import { deriveKeys } from '@aztec/stdlib/keys'; +import { PublicKeys, deriveKeys } from '@aztec/stdlib/keys'; import { MessageContext } from '@aztec/stdlib/logs'; import { Note, NoteDao } from '@aztec/stdlib/note'; import { makeL2Tips } from '@aztec/stdlib/testing'; @@ -138,7 +142,6 @@ describe('Utility Execution test suite', () => { }); it('should run the summed_values function on StatefulTestContractArtifact', async () => { - const contractAddress = await AztecAddress.random(); const artifact = { ...StatefulTestContractArtifact.functions.find(f => f.name === 'summed_values')!, contractName: StatefulTestContractArtifact.name, @@ -146,11 +149,27 @@ describe('Utility Execution test suite', () => { const notes: Note[] = [...Array(5).fill(buildNote(1n)), ...Array(2).fill(buildNote(2n))]; + // The initializer nullifier check requires the instance to be a valid preimage of the contract address, so we + // can't use a random contract address here. + const instanceFields = { + version: 1 as const, + salt: Fr.random(), + deployer: await AztecAddress.random(), + currentContractClassId: new Fr(42), + originalContractClassId: new Fr(42), + initializationHash: Fr.random(), + publicKeys: await PublicKeys.random(), + }; + const contractAddress = await computeContractAddressFromInstance(instanceFields); + aztecNode.getPublicStorageAt.mockResolvedValue(Fr.ZERO); + // The init check calls check_nullifier_exists, which queries findLeavesIndexes. + aztecNode.findLeavesIndexes.mockResolvedValue([ + { data: 1n, l2BlockNumber: BlockNumber(1), l2BlockHash: BlockHash.random() }, + ]); contractStore.getFunctionArtifact.mockResolvedValue(artifact); contractStore.getContractInstance.mockResolvedValue({ - currentContractClassId: new Fr(42), - originalContractClassId: new Fr(42), + ...instanceFields, address: contractAddress, } as ContractInstanceWithAddress); contractStore.getFunctionArtifactWithDebugMetadata.mockImplementation(async (address, selector) => { From a2cc6b01cab70bc19d56d1ba80aefe03bca07e4c Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Wed, 25 Mar 2026 11:27:19 -0300 Subject: [PATCH 03/22] refactor(aztec-nr): remove storage from init_test_contract (#21996) --- .../test/init_test_contract/src/main.nr | 19 ++++--------------- .../private_initialization.test.ts | 6 ++---- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr index b724e3ae574a..85558367aefe 100644 --- a/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr @@ -4,17 +4,8 @@ use aztec::macros::aztec; #[aztec] pub contract InitTest { - use aztec::macros::{ - functions::{external, initializer, noinitcheck, only_self, view}, - storage::storage, - }; + use aztec::macros::functions::{external, initializer, noinitcheck, only_self, view}; use aztec::protocol::address::AztecAddress; - use aztec::state_vars::{Map, PublicMutable}; - - #[storage] - struct Storage { - public_values: Map, Context>, - } #[external("private")] #[initializer] @@ -64,19 +55,17 @@ pub contract InitTest { fn priv_no_init_check(owner: AztecAddress, value: Field) {} #[external("public")] - fn pub_init_check(owner: AztecAddress, value: Field) { - self.storage.public_values.at(owner).write(value); - } + fn pub_init_check(owner: AztecAddress, value: Field) {} #[external("public")] #[noinitcheck] #[view] fn pub_no_init_check(owner: AztecAddress) -> pub Field { - self.storage.public_values.at(owner).read() + 1 } #[external("utility")] unconstrained fn utility_init_check(owner: AztecAddress) -> Field { - self.storage.public_values.at(owner).read() + 2 } } diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts index e90493db4b46..33d63164a9c1 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts @@ -153,10 +153,8 @@ describe('e2e_deploy_contract private initialization', () => { const { contract, initArgs } = await deployUninitialized(); const owner = defaultAccountAddress; await contract.methods.constructor(...initArgs).send({ from: defaultAccountAddress }); - // Write a value via public function so the utility function has something to read. - await contract.methods.pub_init_check(owner, 84).send({ from: defaultAccountAddress }); const result = await contract.methods.utility_init_check(owner).simulate({ from: defaultAccountAddress }); - expect(result.result).toEqual(84n); + expect(result.result).toEqual(2n); }); // A public call enqueued before the private constructor should fail the init check, even though the @@ -180,7 +178,7 @@ describe('e2e_deploy_contract private initialization', () => { ]); await batch.send({ from: defaultAccountAddress }); expect((await contract.methods.pub_no_init_check(owner).simulate({ from: defaultAccountAddress })).result).toEqual( - 84n, + 1n, ); }); From c90779c62ee8d9ccc0ab64aa721b6321549aeefd Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 25 Mar 2026 16:46:42 +0000 Subject: [PATCH 04/22] fix(p2p): check peer rate limit before global to prevent quota starvation (#21997) ## Summary In `SubProtocolRateLimiter.allow()`, the global rate limiter was checked first. `GCRARateLimiter.allow()` advances its virtual scheduling time (VST) as a side effect whenever it returns `true`, so a request that passed the global check but failed the per-peer check would silently consume a global quota token. A single spamming peer could therefore exhaust the global rate limit, starving all other peers on that sub-protocol. Fix: check the per-peer limit first. A peer that exceeds its individual quota is rejected immediately, without touching the shared global bucket. Fixes [A-758](https://linear.app/aztec-labs/issue/A-758) Made with [Cursor](https://cursor.com) --- .../reqresp/rate-limiter/rate_limiter.test.ts | 21 ++++++++++++++++++ .../reqresp/rate-limiter/rate_limiter.ts | 22 +++++++++++-------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.test.ts b/yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.test.ts index 80a6a6e423c9..6a11fbe9a017 100644 --- a/yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.test.ts +++ b/yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.test.ts @@ -156,6 +156,27 @@ describe('rate limiter', () => { multiProtocolRateLimiter.stop(); }); + it('Should not consume global quota when a peer is denied', () => { + // peer1 has a limit of 5/s; peer2 is a different peer + const spammingPeer = makePeer('spammer'); + const legitimatePeer = makePeer('legit'); + + // Exhaust spammingPeer's per-peer quota (5 requests) + for (let i = 0; i < 5; i++) { + expect(rateLimiter.allow(ReqRespSubProtocol.TX, spammingPeer)).toBe(RateLimitStatus.Allowed); + } + + // All further spammer requests are denied at the peer level + // With the bug, each of these would also consume a global token + for (let i = 0; i < 9; i++) { + expect(rateLimiter.allow(ReqRespSubProtocol.TX, spammingPeer)).toBe(RateLimitStatus.DeniedPeer); + } + + // The legitimate peer should still be allowed: the global quota (10/s) must not + // have been consumed by the spammer's peer-denied requests + expect(rateLimiter.allow(ReqRespSubProtocol.TX, legitimatePeer)).toBe(RateLimitStatus.Allowed); + }); + it('Should allow requests if no rate limiter is configured', () => { const rateLimiter = new RequestResponseRateLimiter(peerScoring, {} as ReqRespSubProtocolRateLimits); expect(rateLimiter.allow(ReqRespSubProtocol.TX, makePeer('peer1'))).toBe(RateLimitStatus.Allowed); diff --git a/yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.ts b/yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.ts index 782021be1daa..79e6e915827b 100644 --- a/yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.ts +++ b/yarn-project/p2p/src/services/reqresp/rate-limiter/rate_limiter.ts @@ -97,9 +97,10 @@ export function prettyPrintRateLimitStatus(status: RateLimitStatus) { * 2. Individual rate limits for each peer. * * How it works: - * - When a request comes in, it first checks against the global rate limit. - * - If the global limit allows, it then checks against the specific peer's rate limit. - * - The request is only allowed if both the global and peer-specific limits allow it. + * - When a request comes in, it first checks against the peer's individual rate limit. + * - If the peer limit allows, it then checks against the global rate limit. + * - The request is only allowed if both the peer-specific and global limits allow it. + * - Checking peer limit first ensures a rate-limited peer cannot exhaust the global quota. * - It automatically creates and manages rate limiters for new peers as they make requests. * - It periodically cleans up rate limiters for inactive peers to conserve memory. * @@ -119,10 +120,6 @@ export class SubProtocolRateLimiter { } allow(peerId: PeerId): RateLimitStatus { - if (!this.globalLimiter.allow()) { - return RateLimitStatus.DeniedGlobal; - } - const peerIdStr = peerId.toString(); let peerLimiter: PeerRateLimiter | undefined = this.peerLimiters.get(peerIdStr); if (!peerLimiter) { @@ -135,10 +132,17 @@ export class SubProtocolRateLimiter { } else { peerLimiter.lastAccess = Date.now(); } - const peerLimitAllowed = peerLimiter.limiter.allow(); - if (!peerLimitAllowed) { + + // Check peer limit first: a rate-limited peer must not consume global quota, + // otherwise one spamming peer can starve all others by exhausting the global bucket. + if (!peerLimiter.limiter.allow()) { return RateLimitStatus.DeniedPeer; } + + if (!this.globalLimiter.allow()) { + return RateLimitStatus.DeniedGlobal; + } + return RateLimitStatus.Allowed; } From a1e04d71b6b93c7059900d36b181fd7fd1e2e4ef Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Wed, 25 Mar 2026 14:58:28 -0300 Subject: [PATCH 05/22] chore: remove claude file (#22012) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dbcd78ff2261..8557c7d148e7 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ docs/docs/protocol-specs/public-vm/gen/ __pycache__ *.local.md +.claude/settings.local.json From 8414af4fc5ebf9176cb0322f3da0c049baec5c95 Mon Sep 17 00:00:00 2001 From: Martin Verzilli Date: Wed, 25 Mar 2026 20:36:04 +0100 Subject: [PATCH 06/22] cherry-pick: refactor!: more consistent oracle names (#22018) Cherry-pick of b0090ffc53 with conflict markers preserved for review. --- .../aztec/src/context/private_context.nr | 10 +++---- .../aztec/src/context/returns_hash.nr | 2 +- .../aztec-nr/aztec/src/keys/getters/mod.nr | 2 +- .../aztec/src/messages/encryption/aes128.nr | 4 +-- .../aztec/src/messages/processing/mod.nr | 6 ++-- .../aztec/src/messages/processing/offchain.nr | 8 ++--- .../aztec/src/oracle/aes128_decrypt.nr | 7 +++-- .../aztec-nr/aztec/src/oracle/capsules.nr | 14 +++++---- .../aztec/src/oracle/contract_sync.nr | 8 ++--- .../aztec/src/oracle/execution_cache.nr | 18 ++++++----- .../aztec-nr/aztec/src/oracle/keys.nr | 8 +++-- .../aztec/src/oracle/message_processing.nr | 24 +++++++++------ .../aztec-nr/aztec/src/oracle/nullifiers.nr | 7 +++-- .../aztec-nr/aztec/src/oracle/public_call.nr | 12 ++++---- .../aztec-nr/aztec/src/oracle/storage.nr | 8 +++-- .../aztec-nr/aztec/src/oracle/tx_phase.nr | 8 ++--- .../aztec-nr/aztec/src/utils/with_hash.nr | 2 +- .../aztec_sublib/src/oracle/capsules.nr | 6 ++-- .../src/oracle/execution_cache.nr | 6 ++-- .../aztec_sublib/src/oracle/nullifiers.nr | 3 +- .../aztec_sublib/src/oracle/public_call.nr | 3 +- .../aztec_sublib/src/oracle/storage.nr | 3 +- .../aztec_sublib/src/oracle/tx_phase.nr | 3 +- .../oracle/legacy_oracle_mappings.ts | 16 +++++----- .../oracle/oracle.ts | 30 +++++++++++-------- yarn-project/pxe/src/oracle_version.ts | 4 +++ yarn-project/txe/src/rpc_translator.ts | 30 +++++++++++-------- 27 files changed, 144 insertions(+), 108 deletions(-) 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 0232e747f925..54c62d76fa3c 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -11,8 +11,8 @@ use crate::{ logs::notify_created_contract_class_log, notes::notify_nullified_note, nullifiers::notify_created_nullifier, - public_call::validate_public_calldata, - tx_phase::{in_revertible_phase, notify_revertible_phase_start}, + public_call::assert_valid_public_call_data, + tx_phase::{is_execution_in_revertible_phase, notify_revertible_phase_start}, }, }; use crate::logging::aztecnr_trace_log_format; @@ -519,7 +519,7 @@ impl PrivateContext { let current_counter = self.side_effect_counter; // Safety: Kernel will validate that the claim is correct by validating the expected counters. - let is_revertible = unsafe { in_revertible_phase(current_counter) }; + let is_revertible = unsafe { is_execution_in_revertible_phase(current_counter) }; if is_revertible { if (self.expected_revertible_side_effect_counter == 0) @@ -1260,7 +1260,7 @@ impl PrivateContext { let mut is_static_call = is_static_call | self.inputs.call_context.is_static_call; - validate_public_calldata(calldata_hash); + assert_valid_public_call_data(calldata_hash); let msg_sender = if hide_msg_sender { NULL_MSG_SENDER_CONTRACT_ADDRESS @@ -1331,7 +1331,7 @@ impl PrivateContext { ) { let is_static_call = is_static_call | self.inputs.call_context.is_static_call; - validate_public_calldata(calldata_hash); + assert_valid_public_call_data(calldata_hash); let msg_sender = if hide_msg_sender { NULL_MSG_SENDER_CONTRACT_ADDRESS diff --git a/noir-projects/aztec-nr/aztec/src/context/returns_hash.nr b/noir-projects/aztec-nr/aztec/src/context/returns_hash.nr index 7226e9c4b092..0e073c69e0f4 100644 --- a/noir-projects/aztec-nr/aztec/src/context/returns_hash.nr +++ b/noir-projects/aztec-nr/aztec/src/context/returns_hash.nr @@ -82,7 +82,7 @@ mod test { let hash = hash_args(serialized); - let _ = OracleMock::mock("aztec_prv_loadFromExecutionCache").returns(bad_serialized); + let _ = OracleMock::mock("aztec_prv_getHashPreimage").returns(bad_serialized); assert_eq(ReturnsHash::new(hash).get_preimage(), value); } } diff --git a/noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr b/noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr index 6aa7f5bb52dc..d52b43d389e4 100644 --- a/noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr @@ -71,7 +71,7 @@ mod test { // partial address random_keys_and_partial_address[12] = 0x236703e2cb00a182e024e98e9f759231b556d25ff19f98896cebb69e9e678cc9; - let _ = OracleMock::mock("aztec_utl_tryGetPublicKeysAndPartialAddress").returns(Option::some( + let _ = OracleMock::mock("aztec_utl_getPublicKeysAndPartialAddress").returns(Option::some( random_keys_and_partial_address, )); let _ = get_public_keys(account); diff --git a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr index ba886e82d938..ee417690bac1 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr @@ -685,7 +685,7 @@ mod test { let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient)); let empty_header = BoundedVec::::new(); - let _ = OracleMock::mock("aztec_utl_tryAes128Decrypt").returns(Option::some(empty_header)).times(1); + let _ = OracleMock::mock("aztec_utl_decryptAes128").returns(Option::some(empty_header)).times(1); assert(AES128::decrypt(ciphertext, recipient).is_none()); }); @@ -705,7 +705,7 @@ mod test { let bad_header = BoundedVec::::from_array(encode_header( MESSAGE_PLAINTEXT_SIZE_IN_BYTES + 1, )); - let _ = OracleMock::mock("aztec_utl_tryAes128Decrypt").returns(Option::some(bad_header)).times(1); + let _ = OracleMock::mock("aztec_utl_decryptAes128").returns(Option::some(bad_header)).times(1); assert(AES128::decrypt(ciphertext, recipient).is_none()); }); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 9ab82066ffd9..7582906f2646 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -184,13 +184,13 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_addre /// The `message_context_requests_array_base_slot` must point to a CapsuleArray containing tx hashes. /// PXE will store `Option` values into the responses array at /// `message_context_responses_array_base_slot`. -pub unconstrained fn resolve_message_contexts( +pub unconstrained fn get_message_contexts_by_tx_hash( contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, scope: AztecAddress, ) { - oracle::message_processing::resolve_message_contexts( + oracle::message_processing::get_message_contexts_by_tx_hash( contract_address, message_context_requests_array_base_slot, message_context_responses_array_base_slot, @@ -230,7 +230,7 @@ pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( i += 1; } - oracle::message_processing::bulk_retrieve_logs( + oracle::message_processing::get_logs_by_tag( contract_address, LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT, LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr index 987bf9590080..8e62cbfc2b27 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr @@ -3,9 +3,9 @@ use crate::{ context::UtilityContext, messages::{ encoding::MESSAGE_CIPHERTEXT_LEN, - processing::{MessageContext, OffchainMessageWithContext, resolve_message_contexts}, + processing::{get_message_contexts_by_tx_hash, MessageContext, OffchainMessageWithContext}, }, - oracle::contract_sync::invalidate_contract_sync_cache, + oracle::contract_sync::set_contract_sync_cache_invalid, protocol::{ address::AztecAddress, constants::MAX_TX_LIFETIME, @@ -133,7 +133,7 @@ pub unconstrained fn receive( i += 1; } - invalidate_contract_sync_cache(contract_address, scopes); + set_contract_sync_cache_invalid(contract_address, scopes); } /// Returns offchain-delivered messages to process during sync. @@ -169,7 +169,7 @@ pub unconstrained fn sync_inbox( } // Ask PXE to resolve contexts for all requested tx hashes. - resolve_message_contexts( + get_message_contexts_by_tx_hash( contract_address, OFFCHAIN_CONTEXT_REQUESTS_SLOT, OFFCHAIN_CONTEXT_RESPONSES_SLOT, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr index 29044c297770..d48bc660564a 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr @@ -1,5 +1,5 @@ -#[oracle(aztec_utl_tryAes128Decrypt)] -unconstrained fn try_aes128_decrypt_oracle( +#[oracle(aztec_utl_decryptAes128)] +unconstrained fn aes128_decrypt_oracle( ciphertext: BoundedVec, iv: [u8; 16], sym_key: [u8; 16], @@ -14,12 +14,13 @@ unconstrained fn try_aes128_decrypt_oracle( /// Note that we accept ciphertext as a BoundedVec, not as an array. This is because this function is typically used /// when processing logs and at that point we don't have comptime information about the length of the ciphertext as /// the log is not specific to any individual note. +// TODO(F-498): review naming consistency pub unconstrained fn try_aes128_decrypt( ciphertext: BoundedVec, iv: [u8; 16], sym_key: [u8; 16], ) -> Option> { - try_aes128_decrypt_oracle(ciphertext, iv, sym_key) + aes128_decrypt_oracle(ciphertext, iv, sym_key) } mod test { diff --git a/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr b/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr index 2d5f2c84148a..d813f2ab8392 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr @@ -2,21 +2,23 @@ use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; /// Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `load`. If /// data was already stored at this slot, it is overwritten. +// TODO(F-498): review naming consistency pub unconstrained fn store(contract_address: AztecAddress, slot: Field, value: T, scope: AztecAddress) where T: Serialize, { let serialized = value.serialize(); - store_oracle(contract_address, slot, serialized, scope); + set_capsule_oracle(contract_address, slot, serialized, scope); } /// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns /// Option::none() if nothing was stored at the given slot. +// TODO(F-498): review naming consistency pub unconstrained fn load(contract_address: AztecAddress, slot: Field, scope: AztecAddress) -> Option where T: Deserialize, { - let serialized_option = load_oracle(contract_address, slot, ::N, scope); + let serialized_option = get_capsule_oracle(contract_address, slot, ::N, scope); serialized_option.map(|arr| Deserialize::deserialize(arr)) } @@ -39,8 +41,8 @@ pub unconstrained fn copy( copy_oracle(contract_address, src_slot, dst_slot, num_entries, scope); } -#[oracle(aztec_utl_storeCapsule)] -unconstrained fn store_oracle( +#[oracle(aztec_utl_setCapsule)] +unconstrained fn set_capsule_oracle( contract_address: AztecAddress, slot: Field, values: [Field; N], @@ -54,8 +56,8 @@ unconstrained fn store_oracle( /// require for the oracle resolver to know the shape of T (e.g. if T were a struct of 3 u32 values then the expected /// response shape would be 3 single items, whereas it were a struct containing `u32, [Field;10], u32` then the /// expected shape would be single, array, single.). Instead, we return the serialization and deserialize in Noir. -#[oracle(aztec_utl_loadCapsule)] -unconstrained fn load_oracle( +#[oracle(aztec_utl_getCapsule)] +unconstrained fn get_capsule_oracle( contract_address: AztecAddress, slot: Field, array_len: u32, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/contract_sync.nr b/noir-projects/aztec-nr/aztec/src/oracle/contract_sync.nr index 98017107ab0e..8c9198bf4b11 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/contract_sync.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/contract_sync.nr @@ -1,7 +1,7 @@ use crate::protocol::address::AztecAddress; -#[oracle(aztec_utl_invalidateContractSyncCache)] -unconstrained fn invalidate_contract_sync_cache_oracle( +#[oracle(aztec_utl_setContractSyncCacheInvalid)] +unconstrained fn set_contract_sync_cache_invalid_oracle( contract_address: AztecAddress, scopes: BoundedVec, ) {} @@ -10,9 +10,9 @@ unconstrained fn invalidate_contract_sync_cache_oracle( /// /// Call this after writing data (e.g. offchain messages) that the contract's `sync_state` function needs to discover. /// Without invalidation, the sync cache would skip re-running `sync_state` until the next block. -pub unconstrained fn invalidate_contract_sync_cache( +pub unconstrained fn set_contract_sync_cache_invalid( contract_address: AztecAddress, scopes: BoundedVec, ) { - invalidate_contract_sync_cache_oracle(contract_address, scopes); + set_contract_sync_cache_invalid_oracle(contract_address, scopes); } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/execution_cache.nr b/noir-projects/aztec-nr/aztec/src/oracle/execution_cache.nr index 1a56f7724fa5..aab0d16d555c 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/execution_cache.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/execution_cache.nr @@ -1,20 +1,22 @@ /// Stores values represented as slice in execution cache to be later obtained by its hash. +// TODO(F-498): review naming consistency pub fn store(values: [Field; N], hash: Field) { // Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to // call. When loading the values, however, the caller must check that the values are indeed the preimage. - unsafe { store_in_execution_cache_oracle_wrapper(values, hash) }; + unsafe { set_hash_preimage_oracle_wrapper(values, hash) }; } -unconstrained fn store_in_execution_cache_oracle_wrapper(values: [Field; N], hash: Field) { - store_in_execution_cache_oracle(values, hash); +unconstrained fn set_hash_preimage_oracle_wrapper(values: [Field; N], hash: Field) { + set_hash_preimage_oracle(values, hash); } +// TODO(F-498): review naming consistency pub unconstrained fn load(hash: Field) -> [Field; N] { - load_from_execution_cache_oracle(hash) + get_hash_preimage_oracle(hash) } -#[oracle(aztec_prv_storeInExecutionCache)] -unconstrained fn store_in_execution_cache_oracle(_values: [Field; N], _hash: Field) {} +#[oracle(aztec_prv_setHashPreimage)] +unconstrained fn set_hash_preimage_oracle(_values: [Field; N], _hash: Field) {} -#[oracle(aztec_prv_loadFromExecutionCache)] -unconstrained fn load_from_execution_cache_oracle(_hash: Field) -> [Field; N] {} +#[oracle(aztec_prv_getHashPreimage)] +unconstrained fn get_hash_preimage_oracle(_hash: Field) -> [Field; N] {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/keys.nr b/noir-projects/aztec-nr/aztec/src/oracle/keys.nr index 3c234ca34524..6834a04975ac 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/keys.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/keys.nr @@ -4,17 +4,19 @@ use crate::protocol::{ public_keys::{IvpkM, NpkM, OvpkM, PublicKeys, TpkM}, }; +// TODO(F-498): review naming consistency pub unconstrained fn get_public_keys_and_partial_address(address: AztecAddress) -> (PublicKeys, PartialAddress) { try_get_public_keys_and_partial_address(address).expect(f"Public keys not registered for account {address}") } -#[oracle(aztec_utl_tryGetPublicKeysAndPartialAddress)] -unconstrained fn try_get_public_keys_and_partial_address_oracle(_address: AztecAddress) -> Option<[Field; 13]> {} +#[oracle(aztec_utl_getPublicKeysAndPartialAddress)] +unconstrained fn get_public_keys_and_partial_address_oracle(_address: AztecAddress) -> Option<[Field; 13]> {} +// TODO(F-498): review naming consistency pub unconstrained fn try_get_public_keys_and_partial_address( address: AztecAddress, ) -> Option<(PublicKeys, PartialAddress)> { - try_get_public_keys_and_partial_address_oracle(address).map(|result: [Field; 13]| { + get_public_keys_and_partial_address_oracle(address).map(|result: [Field; 13]| { let keys = PublicKeys { npk_m: NpkM { inner: Point { x: result[0], y: result[1], is_infinite: result[2] != 0 } }, ivpk_m: IvpkM { inner: Point { x: result[3], y: result[4], is_infinite: result[5] != 0 } }, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index 6081a12cce0a..9ec62748bacd 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -2,12 +2,13 @@ use crate::protocol::address::AztecAddress; /// Finds new private logs that may have been sent to all registered accounts in PXE in the current contract and makes /// them available for later processing in Noir by storing them in a capsule array. +// TODO(F-498): review naming consistency pub unconstrained fn fetch_tagged_logs(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) { - fetch_tagged_logs_oracle(pending_tagged_log_array_base_slot, scope); + get_pending_tagged_logs_oracle(pending_tagged_log_array_base_slot, scope); } -#[oracle(aztec_utl_fetchTaggedLogs)] -unconstrained fn fetch_tagged_logs_oracle(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) {} +#[oracle(aztec_utl_getPendingTaggedLogs)] +unconstrained fn get_pending_tagged_logs_oracle(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) {} // This must be a single oracle and not one for notes and one for events because the entire point is to validate all // notes and events in one go, minimizing node round-trips. @@ -39,13 +40,13 @@ unconstrained fn validate_and_store_enqueued_notes_and_events_oracle( scope: AztecAddress, ) {} -pub(crate) unconstrained fn bulk_retrieve_logs( +pub(crate) unconstrained fn get_logs_by_tag( contract_address: AztecAddress, log_retrieval_requests_array_base_slot: Field, log_retrieval_responses_array_base_slot: Field, scope: AztecAddress, ) { - bulk_retrieve_logs_oracle( + get_logs_by_tag_oracle( contract_address, log_retrieval_requests_array_base_slot, log_retrieval_responses_array_base_slot, @@ -53,21 +54,21 @@ pub(crate) unconstrained fn bulk_retrieve_logs( ); } -#[oracle(aztec_utl_bulkRetrieveLogs)] -unconstrained fn bulk_retrieve_logs_oracle( +#[oracle(aztec_utl_getLogsByTag)] +unconstrained fn get_logs_by_tag_oracle( contract_address: AztecAddress, log_retrieval_requests_array_base_slot: Field, log_retrieval_responses_array_base_slot: Field, scope: AztecAddress, ) {} -pub(crate) unconstrained fn resolve_message_contexts( +pub(crate) unconstrained fn get_message_contexts_by_tx_hash( contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, scope: AztecAddress, ) { - resolve_message_contexts_oracle( + get_message_contexts_by_tx_hash_oracle( contract_address, message_context_requests_array_base_slot, message_context_responses_array_base_slot, @@ -75,8 +76,13 @@ pub(crate) unconstrained fn resolve_message_contexts( ); } +<<<<<<< HEAD #[oracle(aztec_utl_resolveMessageContexts)] unconstrained fn resolve_message_contexts_oracle( +======= +#[oracle(aztec_utl_getMessageContextsByTxHash)] +unconstrained fn get_message_contexts_by_tx_hash_oracle( +>>>>>>> b0090ffc53 (refactor!: more consistent oracle names (#22018)) contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/nullifiers.nr b/noir-projects/aztec-nr/aztec/src/oracle/nullifiers.nr index 7c8e64a85574..a083cf372ed1 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/nullifiers.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/nullifiers.nr @@ -33,9 +33,10 @@ unconstrained fn is_nullifier_pending_oracle(_inner_nullifier: Field, _contract_ /// nullifier, but a `false` value should not be relied upon since other transactions may emit this nullifier before /// the current transaction is included in a block. While this might seem of little use at first, certain design /// patterns benefit from this abstraction (see e.g. `PrivateMutable`). +// TODO(F-498): review naming consistency pub unconstrained fn check_nullifier_exists(inner_nullifier: Field) -> bool { - check_nullifier_exists_oracle(inner_nullifier) + does_nullifier_exist_oracle(inner_nullifier) } -#[oracle(aztec_utl_checkNullifierExists)] -unconstrained fn check_nullifier_exists_oracle(_inner_nullifier: Field) -> bool {} +#[oracle(aztec_utl_doesNullifierExist)] +unconstrained fn does_nullifier_exist_oracle(_inner_nullifier: Field) -> bool {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr b/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr index 055aa3048d69..396451d0559e 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr @@ -2,17 +2,17 @@ /// /// The check is unconstrained and the only purpose of it is to fail early in case of calldata overflow or a bug in /// calldata hashing. -pub(crate) fn validate_public_calldata(calldata_hash: Field) { +pub(crate) fn assert_valid_public_call_data(calldata_hash: Field) { // Safety: This oracle call returns nothing: we only call it for its side effects (validating the calldata). // It is therefore always safe to call. unsafe { - validate_public_calldata_wrapper(calldata_hash) + assert_valid_public_call_data_oracle_wrapper(calldata_hash) } } -unconstrained fn validate_public_calldata_wrapper(calldata_hash: Field) { - validate_public_calldata_oracle(calldata_hash) +unconstrained fn assert_valid_public_call_data_oracle_wrapper(calldata_hash: Field) { + assert_valid_public_call_data_oracle(calldata_hash) } -#[oracle(aztec_prv_validatePublicCalldata)] -unconstrained fn validate_public_calldata_oracle(_calldata_hash: Field) {} +#[oracle(aztec_prv_assertValidPublicCalldata)] +unconstrained fn assert_valid_public_call_data_oracle(_calldata_hash: Field) {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/storage.nr b/noir-projects/aztec-nr/aztec/src/oracle/storage.nr index fef8abe759c8..80b8297420c4 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/storage.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/storage.nr @@ -1,21 +1,23 @@ use crate::protocol::{abis::block_header::BlockHeader, address::AztecAddress, traits::{Hash, Packable, ToField}}; -#[oracle(aztec_utl_storageRead)] -unconstrained fn storage_read_oracle( +#[oracle(aztec_utl_getFromPublicStorage)] +unconstrained fn get_from_public_storage_oracle( block_hash: Field, address: Field, storage_slot: Field, length: u32, ) -> [Field; N] {} +// TODO(F-498): review naming consistency pub unconstrained fn raw_storage_read( block_hash_to_read_from: Field, address: AztecAddress, storage_slot: Field, ) -> [Field; N] { - storage_read_oracle(block_hash_to_read_from, address.to_field(), storage_slot, N) + get_from_public_storage_oracle(block_hash_to_read_from, address.to_field(), storage_slot, N) } +// TODO(F-498): review naming consistency pub unconstrained fn storage_read(header_to_read_from: BlockHeader, address: AztecAddress, storage_slot: Field) -> T where T: Packable, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/tx_phase.nr b/noir-projects/aztec-nr/aztec/src/oracle/tx_phase.nr index 207e2fb26030..baa45f2b21f0 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/tx_phase.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/tx_phase.nr @@ -9,8 +9,8 @@ pub(crate) fn notify_revertible_phase_start(counter: u32) { } /// Returns whether a side effect counter falls in the revertible phase of the transaction. -pub(crate) unconstrained fn in_revertible_phase(current_counter: u32) -> bool { - in_revertible_phase_oracle(current_counter) +pub(crate) unconstrained fn is_execution_in_revertible_phase(current_counter: u32) -> bool { + is_execution_in_revertible_phase_oracle(current_counter) } unconstrained fn notify_revertible_phase_start_oracle_wrapper(counter: u32) { @@ -20,5 +20,5 @@ unconstrained fn notify_revertible_phase_start_oracle_wrapper(counter: u32) { #[oracle(aztec_prv_notifyRevertiblePhaseStart)] unconstrained fn notify_revertible_phase_start_oracle(_counter: u32) {} -#[oracle(aztec_prv_inRevertiblePhase)] -unconstrained fn in_revertible_phase_oracle(current_counter: u32) -> bool {} +#[oracle(aztec_prv_isExecutionInRevertiblePhase)] +unconstrained fn is_execution_in_revertible_phase_oracle(current_counter: u32) -> bool {} diff --git a/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr b/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr index 14cc838857f1..e1651f527d76 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr @@ -173,7 +173,7 @@ mod test { // Mock the oracle to return a non-zero hint/packed value let value_packed = MockStruct { a: 1, b: 1 }.pack(); - let _ = OracleMock::mock("aztec_utl_storageRead") + let _ = OracleMock::mock("aztec_utl_getFromPublicStorage") .with_params((block_header.hash(), address.to_field(), STORAGE_SLOT, value_packed.len())) .returns(value_packed) .times(1); diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr index ac57deab6903..bd63e645f549 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr @@ -52,7 +52,8 @@ pub unconstrained fn copy( copy_oracle(contract_address, src_slot, dst_slot, num_entries, scope); } -#[oracle(aztec_utl_storeCapsule)] +#[oracle(aztec_utl_setCapsule)] +// TODO(F-498): review naming consistency unconstrained fn store_oracle( contract_address: AztecAddress, slot: Field, @@ -67,7 +68,8 @@ unconstrained fn store_oracle( /// require for the oracle resolver to know the shape of T (e.g. if T were a struct of 3 u32 values then the expected /// response shape would be 3 single items, whereas it were a struct containing `u32, [Field;10], u32` then the /// expected shape would be single, array, single.). Instead, we return the serialization and deserialize in Noir. -#[oracle(aztec_utl_loadCapsule)] +// TODO(F-498): review naming consistency +#[oracle(aztec_utl_getCapsule)] unconstrained fn load_oracle( contract_address: AztecAddress, slot: Field, diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/execution_cache.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/execution_cache.nr index 5ba553baa659..ccca9fdc1d31 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/execution_cache.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/execution_cache.nr @@ -16,8 +16,10 @@ pub unconstrained fn load(hash: Field) -> [Field; N] { load_from_execution_cache_oracle(hash) } -#[oracle(aztec_prv_storeInExecutionCache)] +// TODO(F-498): review naming consistency +#[oracle(aztec_prv_setHashPreimage)] unconstrained fn store_in_execution_cache_oracle(_values: [Field; N], _hash: Field) {} -#[oracle(aztec_prv_loadFromExecutionCache)] +// TODO(F-498): review naming consistency +#[oracle(aztec_prv_getHashPreimage)] unconstrained fn load_from_execution_cache_oracle(_hash: Field) -> [Field; N] {} diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/nullifiers.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/nullifiers.nr index 1f9f5de98a02..c950f029dcaa 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/nullifiers.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/nullifiers.nr @@ -42,5 +42,6 @@ pub unconstrained fn check_nullifier_exists(inner_nullifier: Field) -> bool { check_nullifier_exists_oracle(inner_nullifier) } -#[oracle(aztec_utl_checkNullifierExists)] +// TODO(F-498): review naming consistency +#[oracle(aztec_utl_doesNullifierExist)] unconstrained fn check_nullifier_exists_oracle(_inner_nullifier: Field) -> bool {} diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/public_call.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/public_call.nr index 055aa3048d69..8c1faf9a9379 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/public_call.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/public_call.nr @@ -14,5 +14,6 @@ unconstrained fn validate_public_calldata_wrapper(calldata_hash: Field) { validate_public_calldata_oracle(calldata_hash) } -#[oracle(aztec_prv_validatePublicCalldata)] +// TODO(F-498): review naming consistency +#[oracle(aztec_prv_assertValidPublicCalldata)] unconstrained fn validate_public_calldata_oracle(_calldata_hash: Field) {} diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/storage.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/storage.nr index b91bf4c6866a..145be1482f42 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/storage.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/storage.nr @@ -4,7 +4,8 @@ use crate::protocol::{ traits::{Hash, Packable, ToField}, }; -#[oracle(aztec_utl_storageRead)] +// TODO(F-498): review naming consistency +#[oracle(aztec_utl_getFromPublicStorage)] unconstrained fn storage_read_oracle( block_hash: Field, address: Field, diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/tx_phase.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/tx_phase.nr index 207e2fb26030..33a35efdef9a 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/tx_phase.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/tx_phase.nr @@ -20,5 +20,6 @@ unconstrained fn notify_revertible_phase_start_oracle_wrapper(counter: u32) { #[oracle(aztec_prv_notifyRevertiblePhaseStart)] unconstrained fn notify_revertible_phase_start_oracle(_counter: u32) {} -#[oracle(aztec_prv_inRevertiblePhase)] +// TODO(F-498): review naming consistency +#[oracle(aztec_prv_isExecutionInRevertiblePhase)] unconstrained fn in_revertible_phase_oracle(current_counter: u32) -> bool {} diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index a58597c66321..463c1bc807a6 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -25,11 +25,11 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { slot: ACVMField[], tSize: ACVMField[], ): Promise<(ACVMField | ACVMField[])[]> => - oracle.aztec_utl_loadCapsule(contractAddress, slot, tSize, [toACVMField(AztecAddress.ZERO)]), + oracle.aztec_utl_getCapsule(contractAddress, slot, tSize, [toACVMField(AztecAddress.ZERO)]), privateStoreInExecutionCache: (values: ACVMField[], hash: ACVMField[]): Promise => - oracle.aztec_prv_storeInExecutionCache(values, hash), + oracle.aztec_prv_setHashPreimage(values, hash), privateLoadFromExecutionCache: (returnsHash: ACVMField[]): Promise => - oracle.aztec_prv_loadFromExecutionCache(returnsHash), + oracle.aztec_prv_getHashPreimage(returnsHash), privateCallPrivateFunction: ( contractAddress: ACVMField[], functionSelector: ACVMField[], @@ -62,13 +62,13 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { startStorageSlot: ACVMField[], numberOfElements: ACVMField[], ): Promise => - oracle.aztec_utl_storageRead(blockHash, contractAddress, startStorageSlot, numberOfElements), + oracle.aztec_utl_getFromPublicStorage(blockHash, contractAddress, startStorageSlot, numberOfElements), utilityStoreCapsule: ( contractAddress: ACVMField[], slot: ACVMField[], capsule: ACVMField[], ): Promise => - oracle.aztec_utl_storeCapsule(contractAddress, slot, capsule, [toACVMField(AztecAddress.ZERO)]), + oracle.aztec_utl_setCapsule(contractAddress, slot, capsule, [toACVMField(AztecAddress.ZERO)]), utilityCopyCapsule: ( contractAddress: ACVMField[], srcSlot: ACVMField[], @@ -95,19 +95,19 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { privateNotifySetMinRevertibleSideEffectCounter: (counter: ACVMField[]): Promise => oracle.aztec_prv_notifyRevertiblePhaseStart(counter), privateIsSideEffectCounterRevertible: (sideEffectCounter: ACVMField[]): Promise => - oracle.aztec_prv_inRevertiblePhase(sideEffectCounter), + oracle.aztec_prv_isExecutionInRevertiblePhase(sideEffectCounter), // Signature changes: old 4-param oracles → new 1-param validatePublicCalldata privateNotifyEnqueuedPublicFunctionCall: ( _contractAddress: ACVMField[], calldataHash: ACVMField[], _sideEffectCounter: ACVMField[], _isStaticCall: ACVMField[], - ): Promise => oracle.aztec_prv_validatePublicCalldata(calldataHash), + ): Promise => oracle.aztec_prv_assertValidPublicCalldata(calldataHash), privateNotifySetPublicTeardownFunctionCall: ( _contractAddress: ACVMField[], calldataHash: ACVMField[], _sideEffectCounter: ACVMField[], _isStaticCall: ACVMField[], - ): Promise => oracle.aztec_prv_validatePublicCalldata(calldataHash), + ): Promise => oracle.aztec_prv_assertValidPublicCalldata(calldataHash), }; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 247909ee59bd..38e3b5a1a816 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -108,13 +108,13 @@ export class Oracle { } // eslint-disable-next-line camelcase - aztec_prv_storeInExecutionCache(values: ACVMField[], [hash]: ACVMField[]): Promise { + aztec_prv_setHashPreimage(values: ACVMField[], [hash]: ACVMField[]): Promise { this.handlerAsPrivate().storeInExecutionCache(values.map(Fr.fromString), Fr.fromString(hash)); return Promise.resolve([]); } // eslint-disable-next-line camelcase - async aztec_prv_loadFromExecutionCache([returnsHash]: ACVMField[]): Promise { + async aztec_prv_getHashPreimage([returnsHash]: ACVMField[]): Promise { const values = await this.handlerAsPrivate().loadFromExecutionCache(Fr.fromString(returnsHash)); return [values.map(toACVMField)]; } @@ -252,7 +252,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_tryGetPublicKeysAndPartialAddress([address]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { + async aztec_utl_getPublicKeysAndPartialAddress([address]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { const parsedAddress = AztecAddress.fromField(Fr.fromString(address)); const result = await this.handlerAsUtility().tryGetPublicKeysAndPartialAddress(parsedAddress); @@ -380,7 +380,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_checkNullifierExists([innerNullifier]: ACVMField[]): Promise { + async aztec_utl_doesNullifierExist([innerNullifier]: ACVMField[]): Promise { const exists = await this.handlerAsUtility().checkNullifierExists(Fr.fromString(innerNullifier)); return [toACVMField(exists)]; } @@ -400,7 +400,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_storageRead( + async aztec_utl_getFromPublicStorage( [blockHash]: ACVMField[], [contractAddress]: ACVMField[], [startStorageSlot]: ACVMField[], @@ -464,7 +464,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_prv_validatePublicCalldata([calldataHash]: ACVMField[]): Promise { + async aztec_prv_assertValidPublicCalldata([calldataHash]: ACVMField[]): Promise { await this.handlerAsPrivate().validatePublicCalldata(Fr.fromString(calldataHash)); return []; } @@ -476,7 +476,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_prv_inRevertiblePhase([sideEffectCounter]: ACVMField[]): Promise { + async aztec_prv_isExecutionInRevertiblePhase([sideEffectCounter]: ACVMField[]): Promise { const isRevertible = await this.handlerAsPrivate().inRevertiblePhase(Fr.fromString(sideEffectCounter).toNumber()); return Promise.resolve([toACVMField(isRevertible)]); } @@ -491,7 +491,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_fetchTaggedLogs( + async aztec_utl_getPendingTaggedLogs( [pendingTaggedLogArrayBaseSlot]: ACVMField[], [scope]: ACVMField[], ): Promise { @@ -524,7 +524,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_bulkRetrieveLogs( + async aztec_utl_getLogsByTag( [contractAddress]: ACVMField[], [logRetrievalRequestsArrayBaseSlot]: ACVMField[], [logRetrievalResponsesArrayBaseSlot]: ACVMField[], @@ -540,7 +540,11 @@ export class Oracle { } // eslint-disable-next-line camelcase +<<<<<<< HEAD async aztec_utl_resolveMessageContexts( +======= + async aztec_utl_getMessageContextsByTxHash( +>>>>>>> b0090ffc53 (refactor!: more consistent oracle names (#22018)) [contractAddress]: ACVMField[], [messageContextRequestsArrayBaseSlot]: ACVMField[], [messageContextResponsesArrayBaseSlot]: ACVMField[], @@ -556,7 +560,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - aztec_utl_storeCapsule( + aztec_utl_setCapsule( [contractAddress]: ACVMField[], [slot]: ACVMField[], capsule: ACVMField[], @@ -572,7 +576,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_loadCapsule( + async aztec_utl_getCapsule( [contractAddress]: ACVMField[], [slot]: ACVMField[], [tSize]: ACVMField[], @@ -628,7 +632,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_tryAes128Decrypt( + async aztec_utl_decryptAes128( ciphertextBVecStorage: ACVMField[], [ciphertextLength]: ACVMField[], iv: ACVMField[], @@ -664,7 +668,7 @@ export class Oracle { } // eslint-disable-next-line camelcase - aztec_utl_invalidateContractSyncCache( + aztec_utl_setContractSyncCacheInvalid( [contractAddress]: ACVMField[], scopes: ACVMField[], [scopeCount]: ACVMField[], diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 48265d378960..afc16c131812 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -9,4 +9,8 @@ export const ORACLE_VERSION = 20; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. +<<<<<<< HEAD export const ORACLE_INTERFACE_HASH = 'ab6bb5669bf20d3908eba5f3edaeec3bf1d828b902f5c2734054e372be6e5f22'; +======= +export const ORACLE_INTERFACE_HASH = '93b5352522338a33462efb253b67808c59a805c1c90ee0e394285b49f34d9691'; +>>>>>>> b0090ffc53 (refactor!: more consistent oracle names (#22018)) diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 1834c3afc6aa..5221a0391e83 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -339,7 +339,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - aztec_prv_storeInExecutionCache(foreignValues: ForeignCallArray, foreignHash: ForeignCallSingle) { + aztec_prv_setHashPreimage(foreignValues: ForeignCallArray, foreignHash: ForeignCallSingle) { const values = fromArray(foreignValues); const hash = fromSingle(foreignHash); @@ -349,7 +349,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_prv_loadFromExecutionCache(foreignHash: ForeignCallSingle) { + async aztec_prv_getHashPreimage(foreignHash: ForeignCallSingle) { const hash = fromSingle(foreignHash); const returns = await this.handlerAsPrivate().loadFromExecutionCache(hash); @@ -378,7 +378,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_storageRead( + async aztec_utl_getFromPublicStorage( foreignBlockHash: ForeignCallSingle, foreignContractAddress: ForeignCallSingle, foreignStartStorageSlot: ForeignCallSingle, @@ -556,7 +556,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_checkNullifierExists(foreignInnerNullifier: ForeignCallSingle) { + async aztec_utl_doesNullifierExist(foreignInnerNullifier: ForeignCallSingle) { const innerNullifier = fromSingle(foreignInnerNullifier); const exists = await this.handlerAsUtility().checkNullifierExists(innerNullifier); @@ -582,7 +582,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_tryGetPublicKeysAndPartialAddress(foreignAddress: ForeignCallSingle) { + async aztec_utl_getPublicKeysAndPartialAddress(foreignAddress: ForeignCallSingle) { const address = addressFromSingle(foreignAddress); const result = await this.handlerAsUtility().tryGetPublicKeysAndPartialAddress(address); @@ -652,7 +652,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - public aztec_prv_validatePublicCalldata(_foreignCalldataHash: ForeignCallSingle) { + public aztec_prv_assertValidPublicCalldata(_foreignCalldataHash: ForeignCallSingle) { throw new Error('Enqueueing public calls is not supported in TestEnvironment::private_context'); } @@ -662,7 +662,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - public async aztec_prv_inRevertiblePhase(foreignSideEffectCounter: ForeignCallSingle) { + public async aztec_prv_isExecutionInRevertiblePhase(foreignSideEffectCounter: ForeignCallSingle) { const sideEffectCounter = fromSingle(foreignSideEffectCounter).toNumber(); const isRevertible = await this.handlerAsPrivate().inRevertiblePhase(sideEffectCounter); return toForeignCallResult([toSingle(new Fr(isRevertible))]); @@ -738,7 +738,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_fetchTaggedLogs( + async aztec_utl_getPendingTaggedLogs( foreignPendingTaggedLogArrayBaseSlot: ForeignCallSingle, foreignScope: ForeignCallSingle, ) { @@ -779,7 +779,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - public async aztec_utl_bulkRetrieveLogs( + public async aztec_utl_getLogsByTag( foreignContractAddress: ForeignCallSingle, foreignLogRetrievalRequestsArrayBaseSlot: ForeignCallSingle, foreignLogRetrievalResponsesArrayBaseSlot: ForeignCallSingle, @@ -801,7 +801,11 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase +<<<<<<< HEAD public async aztec_utl_resolveMessageContexts( +======= + public async aztec_utl_getMessageContextsByTxHash( +>>>>>>> b0090ffc53 (refactor!: more consistent oracle names (#22018)) foreignContractAddress: ForeignCallSingle, foreignMessageContextRequestsArrayBaseSlot: ForeignCallSingle, foreignMessageContextResponsesArrayBaseSlot: ForeignCallSingle, @@ -823,7 +827,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - aztec_utl_storeCapsule( + aztec_utl_setCapsule( foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle, foreignCapsule: ForeignCallArray, @@ -840,7 +844,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_loadCapsule( + async aztec_utl_getCapsule( foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle, foreignTSize: ForeignCallSingle, @@ -903,7 +907,7 @@ export class RPCTranslator { // to implement this function here. Isn't there a way to programmatically identify that this is missing, given the // existence of a txe_oracle method? // eslint-disable-next-line camelcase - async aztec_utl_tryAes128Decrypt( + async aztec_utl_decryptAes128( foreignCiphertextBVecStorage: ForeignCallArray, foreignCiphertextLength: ForeignCallSingle, foreignIv: ForeignCallArray, @@ -947,7 +951,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - aztec_utl_invalidateContractSyncCache( + aztec_utl_setContractSyncCacheInvalid( foreignContractAddress: ForeignCallSingle, foreignScopes: ForeignCallArray, foreignScopeCount: ForeignCallSingle, From 33770af098d44688b44cbbc0f99873caadd4dad3 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 19:41:58 +0000 Subject: [PATCH 07/22] fix: resolve cherry-pick conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolved conflicts in oracle names: - aztec_utl_resolveMessageContexts → aztec_utl_getMessageContextsByTxHash - Bumped ORACLE_VERSION to 21 (both Noir and TS) - ORACLE_INTERFACE_HASH placeholder to be computed --- .../aztec-nr/aztec/src/oracle/message_processing.nr | 5 ----- noir-projects/aztec-nr/aztec/src/oracle/version.nr | 2 +- .../pxe/src/contract_function_simulator/oracle/oracle.ts | 4 ---- yarn-project/pxe/src/oracle_version.ts | 8 ++------ yarn-project/txe/src/rpc_translator.ts | 4 ---- 5 files changed, 3 insertions(+), 20 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index 9ec62748bacd..ac66b939dcaf 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -76,13 +76,8 @@ pub(crate) unconstrained fn get_message_contexts_by_tx_hash( ); } -<<<<<<< HEAD -#[oracle(aztec_utl_resolveMessageContexts)] -unconstrained fn resolve_message_contexts_oracle( -======= #[oracle(aztec_utl_getMessageContextsByTxHash)] unconstrained fn get_message_contexts_by_tx_hash_oracle( ->>>>>>> b0090ffc53 (refactor!: more consistent oracle names (#22018)) contract_address: AztecAddress, message_context_requests_array_base_slot: Field, message_context_responses_array_base_slot: Field, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index bf312d89747c..0e9c3ce1a270 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/version.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/version.nr @@ -4,7 +4,7 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is /// called and if the oracle version is incompatible an error is thrown. -pub global ORACLE_VERSION: Field = 20; +pub global ORACLE_VERSION: Field = 21; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 38e3b5a1a816..4b894568bce6 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -540,11 +540,7 @@ export class Oracle { } // eslint-disable-next-line camelcase -<<<<<<< HEAD - async aztec_utl_resolveMessageContexts( -======= async aztec_utl_getMessageContextsByTxHash( ->>>>>>> b0090ffc53 (refactor!: more consistent oracle names (#22018)) [contractAddress]: ACVMField[], [messageContextRequestsArrayBaseSlot]: ACVMField[], [messageContextResponsesArrayBaseSlot]: ACVMField[], diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index afc16c131812..a789519ffff0 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -4,13 +4,9 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is called /// and if the oracle version is incompatible an error is thrown. -export const ORACLE_VERSION = 20; +export const ORACLE_VERSION = 21; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -<<<<<<< HEAD -export const ORACLE_INTERFACE_HASH = 'ab6bb5669bf20d3908eba5f3edaeec3bf1d828b902f5c2734054e372be6e5f22'; -======= -export const ORACLE_INTERFACE_HASH = '93b5352522338a33462efb253b67808c59a805c1c90ee0e394285b49f34d9691'; ->>>>>>> b0090ffc53 (refactor!: more consistent oracle names (#22018)) +export const ORACLE_INTERFACE_HASH = 'PLACEHOLDER'; diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 5221a0391e83..0de2a095ecbd 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -801,11 +801,7 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase -<<<<<<< HEAD - public async aztec_utl_resolveMessageContexts( -======= public async aztec_utl_getMessageContextsByTxHash( ->>>>>>> b0090ffc53 (refactor!: more consistent oracle names (#22018)) foreignContractAddress: ForeignCallSingle, foreignMessageContextRequestsArrayBaseSlot: ForeignCallSingle, foreignMessageContextResponsesArrayBaseSlot: ForeignCallSingle, From 20e2358c2b2591943091f052c8909c596422bb0c Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 19:48:36 +0000 Subject: [PATCH 08/22] fix: update ORACLE_INTERFACE_HASH and bump ORACLE_VERSION to 21 Updated hash to match new oracle interface after rename. Bumped ORACLE_VERSION in both Noir and TypeScript. --- yarn-project/pxe/src/oracle_version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index a789519ffff0..0f386cf14917 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -9,4 +9,4 @@ export const ORACLE_VERSION = 21; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -export const ORACLE_INTERFACE_HASH = 'PLACEHOLDER'; +export const ORACLE_INTERFACE_HASH = '93b5352522338a33462efb253b67808c59a805c1c90ee0e394285b49f34d9691'; From c4475f32f34ae2bad1f9b22b213364520fa1aef9 Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Wed, 25 Mar 2026 17:36:37 -0300 Subject: [PATCH 09/22] cherry-pick: feat(aztec-nr)!: app-silo getSharedSecret oracle (#22020) Cherry-pick of 1c74bf659e with conflicts in yarn-project/pxe/src/oracle_version.ts --- .../aztec/src/keys/ecdh_shared_secret.nr | 53 ++++- .../src/messages/discovery/process_message.nr | 2 +- .../aztec/src/messages/encryption/aes128.nr | 225 +++++++++--------- .../messages/encryption/message_encryption.nr | 2 + .../aztec/src/messages/logs/partial_note.nr | 3 +- .../aztec/src/messages/message_delivery.nr | 3 +- .../aztec/src/oracle/aes128_decrypt.nr | 19 +- .../aztec/src/oracle/shared_secret.nr | 36 ++- .../aztec-nr/aztec/src/oracle/version.nr | 2 +- .../aztec-nr/uint-note/src/uint_note.nr | 1 + .../app/nft_contract/src/types/nft_note.nr | 1 + .../crates/types/src/constants.nr | 6 +- .../crates/types/src/constants_tests.nr | 41 ++-- yarn-project/constants/src/constants.gen.ts | 6 +- .../oracle/interfaces.ts | 2 +- .../oracle/legacy_oracle_mappings.ts | 6 - .../oracle/oracle.ts | 4 +- .../oracle/utility_execution.test.ts | 65 ++++- .../oracle/utility_execution_oracle.ts | 17 +- yarn-project/pxe/src/oracle_version.ts | 6 +- .../src/logs/shared_secret_derivation.ts | 31 ++- yarn-project/txe/src/rpc_translator.ts | 6 +- 22 files changed, 342 insertions(+), 195 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/keys/ecdh_shared_secret.nr b/noir-projects/aztec-nr/aztec/src/keys/ecdh_shared_secret.nr index 3a3d2fa87d5c..a1dbd44ed2c3 100644 --- a/noir-projects/aztec-nr/aztec/src/keys/ecdh_shared_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/keys/ecdh_shared_secret.nr @@ -1,4 +1,11 @@ -use crate::protocol::{address::aztec_address::AztecAddress, point::Point, scalar::Scalar, traits::FromField}; +use crate::protocol::{ + address::aztec_address::AztecAddress, + constants::{DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, DOM_SEP__ECDH_FIELD_MASK, DOM_SEP__ECDH_SUBKEY}, + hash::poseidon2_hash_with_separator, + point::Point, + scalar::Scalar, + traits::{FromField, ToField}, +}; use std::{embedded_curve_ops::multi_scalar_mul, ops::Neg}; /// Computes a standard ECDH shared secret: secret * public_key = shared_secret. @@ -6,13 +13,39 @@ use std::{embedded_curve_ops::multi_scalar_mul, ops::Neg}; /// The input secret is known only to one party. The output shared secret can be derived given knowledge of /// `public_key`'s key-pair and the public ephemeral secret, using this same function (with reversed inputs). /// -/// E.g.: Epk = esk * G // ephemeral key-pair Pk = sk * G // recipient key-pair Shared secret S = esk * Pk = sk * Epk +/// E.g.: Epk = esk * G // ephemeral key-pair +/// Pk = sk * G // recipient key-pair +/// Shared secret S = esk * Pk = sk * Epk /// /// See also: https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman pub fn derive_ecdh_shared_secret(secret: Scalar, public_key: Point) -> Point { multi_scalar_mul([public_key], [secret]) } +/// Computes an app-siloed shared secret from a raw ECDH shared secret point and a contract address. +/// +/// `s_app = h(DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, S.x, S.y, contract_address)` +pub(crate) fn compute_app_siloed_shared_secret(shared_secret: Point, contract_address: AztecAddress) -> Field { + poseidon2_hash_with_separator( + [shared_secret.x, shared_secret.y, contract_address.to_field()], + DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, + ) +} + +/// Derives an indexed subkey from an app-siloed shared secret, used for AES key/IV derivation. +/// +/// `s_i = h(DOM_SEP__ECDH_SUBKEY + i, s_app)` +pub(crate) fn derive_shared_secret_subkey(s_app: Field, index: u32) -> Field { + poseidon2_hash_with_separator([s_app], DOM_SEP__ECDH_SUBKEY + index) +} + +/// Derives an indexed field mask from an app-siloed shared secret, used for masking ciphertext fields. +/// +/// `m_i = h(DOM_SEP__ECDH_FIELD_MASK + i, s_app)` +pub(crate) fn derive_shared_secret_field_mask(s_app: Field, index: u32) -> Field { + poseidon2_hash_with_separator([s_app], DOM_SEP__ECDH_FIELD_MASK + index) +} + #[test] unconstrained fn test_consistency_with_typescript() { let secret = Scalar { @@ -81,3 +114,19 @@ unconstrained fn test_shared_secret_computation_from_address_in_both_directions( assert_eq(shared_secret, shared_secret_alt); } + +#[test] +unconstrained fn test_app_siloed_shared_secret_differs_per_contract() { + let secret_a = Scalar { lo: 0x1234, hi: 0x2345 }; + let pk_b = std::embedded_curve_ops::fixed_base_scalar_mul(Scalar { lo: 0x3456, hi: 0x4567 }); + + let shared_secret = derive_ecdh_shared_secret(secret_a, pk_b); + + let contract_a = AztecAddress::from_field(0xAAAA); + let contract_b = AztecAddress::from_field(0xBBBB); + + let s_app_a = compute_app_siloed_shared_secret(shared_secret, contract_a); + let s_app_b = compute_app_siloed_shared_secret(shared_secret, contract_b); + + assert(s_app_a != s_app_b, "app-siloed secrets must differ for different contracts"); +} diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr index 978f3557809d..026fae34ac15 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr @@ -35,7 +35,7 @@ pub unconstrained fn process_message_ciphertext( message_context: MessageContext, recipient: AztecAddress, ) { - let message_plaintext_option = AES128::decrypt(message_ciphertext, recipient); + let message_plaintext_option = AES128::decrypt(message_ciphertext, recipient, contract_address); if message_plaintext_option.is_some() { process_message_plaintext( diff --git a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr index ba886e82d938..3ba8011dd3e6 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr @@ -1,14 +1,13 @@ -use crate::protocol::{ - address::AztecAddress, - constants::{DOM_SEP__CIPHERTEXT_FIELD_MASK, DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2}, - hash::poseidon2_hash_with_separator, - point::Point, - public_keys::AddressPoint, - traits::ToField, -}; +use crate::protocol::{address::AztecAddress, public_keys::AddressPoint, traits::ToField}; use crate::{ - keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_positive_ephemeral_key_pair}, + keys::{ + ecdh_shared_secret::{ + compute_app_siloed_shared_secret, derive_ecdh_shared_secret, derive_shared_secret_field_mask, + derive_shared_secret_subkey, + }, + ephemeral::generate_positive_ephemeral_key_pair, + }, logging::aztecnr_warn_log_format, messages::{ encoding::{ @@ -33,75 +32,58 @@ use crate::{ use std::aes128::aes128_encrypt; -/// Computes N close-to-uniformly-random 256 bits from a given ECDH shared_secret. +/// Computes N close-to-uniformly-random 256 bits from a given app-siloed shared secret. /// -/// NEVER re-use the same iv and sym_key. DO NOT call this function more than once with the same shared_secret. +/// NEVER re-use the same iv and sym_key. DO NOT call this function more than once with the same s_app. /// -/// This function is only known to be safe if shared_secret is computed by combining a random ephemeral key with an -/// address point. See big comment within the body of the function. See big comment within the body of the function. -fn extract_many_close_to_uniformly_random_256_bits_from_ecdh_shared_secret_using_poseidon2_unsafe( - shared_secret: Point, -) -> [[u8; 32]; N] { +/// This function is only known to be safe if s_app is derived from combining a random ephemeral key with an +/// address point and a contract address. See big comment within the body of the function. +fn extract_many_close_to_uniformly_random_256_bits_using_poseidon2(s_app: Field) -> [[u8; 32]; N] { /* - * Unsafe because of https://eprint.iacr.org/2010/264.pdf Page 13, Lemma 2 (and the * two - paragraphs below it). + * Unsafe because of https://eprint.iacr.org/2010/264.pdf Page 13, Lemma 2 (and the two paragraphs below it). * - * If you call this function, you need to be careful and aware of how the arg - * `shared_secret` has been derived. + * If you call this function, you need to be careful and aware of how the arg `s_app` has been derived. * - * The paper says that the way you derive aes keys and IVs should be fine with poseidon2 - * (modelled as a RO), as long as you _don't_ use Poseidon2 as a PRG to generate the * two - exponents x & y which multiply to the shared secret S: + * The paper says that the way you derive aes keys and IVs should be fine with poseidon2 (modelled as a RO), + * as long as you _don't_ use Poseidon2 as a PRG to generate the two exponents x & y which multiply to the + * shared secret S: * * S = [x*y]*G. * - * (Otherwise, you would have to "key" poseidon2, i.e. generate a uniformly string K - * which can be public and compute Hash(x) as poseidon(K,x)). - * In that lemma, k would be 2*254=508, and m would be the number of points on the * grumpkin - curve (which is close to r according to the Hasse bound). + * (Otherwise, you would have to "key" poseidon2, i.e. generate a uniformly string K which can be public and + * compute Hash(x) as poseidon(K,x)). + * In that lemma, k would be 2*254=508, and m would be the number of points on the grumpkin curve (which is + * close to r according to the Hasse bound). * - * Our shared secret S is [esk * address_sk] * G, and the question is: * Can we compute hash(S) - using poseidon2 instead of sha256? + * Our shared secret S is [esk * address_sk] * G, and the question is: Can we compute hash(S) using poseidon2 + * instead of sha256? * * Well, esk is random and not generated with poseidon2, so that's good. * What about address_sk? - * Well, address_sk = poseidon2(stuff) + ivsk, so there was some - * discussion about whether address_sk is independent of poseidon2. - * Given that ivsk is random and independent of poseidon2, the address_sk is also + * Well, address_sk = poseidon2(stuff) + ivsk, so there was some discussion about whether address_sk is + * independent of poseidon2. Given that ivsk is random and independent of poseidon2, the address_sk is also * independent of poseidon2. * - * Tl;dr: we believe it's safe to hash S = [esk * address_sk] * G using poseidon2, - * in order to derive a symmetric key. - * - * If you're calling this function for a differently-derived `shared_secret`, be - * careful. + * Tl;dr: we believe it's safe to hash S = [esk * address_sk] * G using poseidon2, in order to derive a + * symmetric key. * + * If you're calling this function for a differently-derived `s_app`, be careful. */ /* The output of this function needs to be 32 random bytes. - * A single field won't give us 32 bytes of entropy. - * So we compute two "random" fields, by poseidon-hashing with two different - * generators. - * We then extract the last 16 (big endian) bytes of each "random" field. - * Note: we use to_be_bytes because it's slightly more efficient. But we have to - * be careful not to take bytes from the "big end", because the "big" byte is - * not uniformly random over the byte: it only has < 6 bits of randomness, because - * it's the big end of a 254-bit field element. + * A single field won't give us 32 bytes of entropy. So we compute two "random" fields, by poseidon-hashing + * with two different indices. We then extract the last 16 (big endian) bytes of each "random" field. + * Note: we use to_be_bytes because it's slightly more efficient. But we have to be careful not to take bytes + * from the "big end", because the "big" byte is not uniformly random over the byte: it only has < 6 bits of + * randomness, because it's the big end of a 254-bit field element. */ let mut all_bytes: [[u8; 32]; N] = std::mem::zeroed(); - // We restrict N to be < 2^8, because of how we compute the domain separator from k below (where k <= N must be 8 - // bits). In practice, it's extremely unlikely that an app will want to compute >= 256 ciphertexts. std::static_assert(N < 256, "N too large"); for k in 0..N { - // We augment the domain separator with the loop index, so that we can generate N lots of randomness. - let k_shift = (k << 8); - let separator_1 = k_shift + DOM_SEP__SYMMETRIC_KEY; - let separator_2 = k_shift + DOM_SEP__SYMMETRIC_KEY_2; - - let rand1: Field = poseidon2_hash_with_separator([shared_secret.x, shared_secret.y], separator_1); - let rand2: Field = poseidon2_hash_with_separator([shared_secret.x, shared_secret.y], separator_2); + let rand1: Field = derive_shared_secret_subkey(s_app, 2 * k); + let rand2: Field = derive_shared_secret_subkey(s_app, 2 * k + 1); let rand1_bytes: [u8; 32] = rand1.to_be_bytes(); let rand2_bytes: [u8; 32] = rand2.to_be_bytes(); @@ -139,11 +121,8 @@ fn derive_aes_symmetric_key_and_iv_from_uniformly_random_256_bits( many_pairs } -pub fn derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe( - shared_secret: Point, -) -> [([u8; 16], [u8; 16]); N] { - let many_random_256_bits: [[u8; 32]; N] = - extract_many_close_to_uniformly_random_256_bits_from_ecdh_shared_secret_using_poseidon2_unsafe(shared_secret); +pub fn derive_aes_symmetric_key_and_iv_from_shared_secret(s_app: Field) -> [([u8; 16], [u8; 16]); N] { + let many_random_256_bits: [[u8; 32]; N] = extract_many_close_to_uniformly_random_256_bits_using_poseidon2(s_app); derive_aes_symmetric_key_and_iv_from_uniformly_random_256_bits(many_random_256_bits) } @@ -218,9 +197,9 @@ impl MessageEncryption for AES128 { /// ``` /// /// **Step 2 -- Pack and mask.** The byte array is split into 31-byte chunks, each stored in one field. A - /// Poseidon2-derived mask (see `derive_field_mask`) is added to each so that the resulting fields appear as - /// uniformly random `Field` values to any observer without knowledge of the shared secret, hiding the fact - /// that the underlying ciphertext consists of 128-bit AES blocks. + /// Poseidon2-derived mask (see `derive_shared_secret_field_mask`) is added to each so that the resulting + /// fields appear as uniformly random `Field` values to any observer without knowledge of the shared secret, + /// hiding the fact that the underlying ciphertext consists of 128-bit AES blocks. /// /// **Step 3 -- Assemble ciphertext.** The ephemeral public key x-coordinate is prepended and random field padding /// is appended to fill to 15 fields: @@ -235,11 +214,15 @@ impl MessageEncryption for AES128 { /// /// ## Key Derivation /// - /// Two (key, IV) pairs are derived from the ECDH shared secret via Poseidon2 hashing with different domain - /// separators: one pair for the body ciphertext and one for the header ciphertext. + /// The raw ECDH shared secret point is first app-siloed into a scalar `s_app` by hashing with the contract + /// address (see + /// [`compute_app_siloed_shared_secret`](crate::keys::ecdh_shared_secret::compute_app_siloed_shared_secret)). + /// Two (key, IV) pairs are then derived from `s_app` via indexed Poseidon2 hashing: one pair for the body + /// ciphertext and one for the header ciphertext. fn encrypt( plaintext: [Field; PlaintextLen], recipient: AztecAddress, + contract_address: AztecAddress, ) -> [Field; MESSAGE_CIPHERTEXT_LEN] { std::static_assert( PlaintextLen <= MESSAGE_PLAINTEXT_LEN, @@ -253,7 +236,7 @@ impl MessageEncryption for AES128 { // Derive ECDH shared secret with recipient using a fresh ephemeral keypair. let (eph_sk, eph_pk) = generate_positive_ephemeral_key_pair(); - let ciphertext_shared_secret = derive_ecdh_shared_secret( + let raw_shared_secret = derive_ecdh_shared_secret( eph_sk, recipient .to_address_point() @@ -281,13 +264,12 @@ impl MessageEncryption for AES128 { .inner, ); - // AES128-CBC encrypt the plaintext bytes. - // It is safe to call the `unsafe` function here, because we know the `shared_secret` was derived using an - // AztecAddress (the recipient). See the block comment at the start of this unsafe target function for more - // info. - let pairs = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<2>( - ciphertext_shared_secret, - ); + let s_app = compute_app_siloed_shared_secret(raw_shared_secret, contract_address); + + // It is safe to derive AES keys from `s_app` using Poseidon2 because `s_app` was derived from an ECDH shared + // secret using an AztecAddress (the recipient). See the block comment in + // `extract_many_close_to_uniformly_random_256_bits_using_poseidon2` for more info. + let pairs = derive_aes_symmetric_key_and_iv_from_shared_secret::<2>(s_app); let (body_sym_key, body_iv) = pairs[0]; let (header_sym_key, header_iv) = pairs[1]; @@ -363,7 +345,7 @@ impl MessageEncryption for AES128 { // values let mut offset = 1; for i in 0..message_bytes_as_fields.len() { - let mask = derive_field_mask(ciphertext_shared_secret, i as u32); + let mask = derive_shared_secret_field_mask(s_app, i as u32); ciphertext[offset + i] = message_bytes_as_fields[i] + mask; } offset += message_bytes_as_fields.len(); @@ -381,6 +363,7 @@ impl MessageEncryption for AES128 { unconstrained fn decrypt( ciphertext: BoundedVec, recipient: AztecAddress, + contract_address: AztecAddress, ) -> Option> { // Extract the ephemeral public key x-coordinate and masked fields, returning None for empty ciphertext. if ciphertext.len() > 0 { @@ -395,11 +378,10 @@ impl MessageEncryption for AES128 { // y-coordinate must be positive. This may fail however, as not all x-coordinates are on the curve. In // that case, we simply return `Option::none`. point_from_x_coord_and_sign(eph_pk_x, true).and_then(|eph_pk| { - // Derive shared secret - let ciphertext_shared_secret = get_shared_secret(recipient, eph_pk); + let s_app = get_shared_secret(recipient, eph_pk, contract_address); let unmasked_fields = masked_fields.mapi(|i, field| { - let unmasked = unmask_field(ciphertext_shared_secret, i, field); + let unmasked = unmask_field(s_app, i, field); // If we failed to unmask the field, we are dealing with the random padding. We'll ignore it // later, so we can simply set it to 0 unmasked.unwrap_or(0) @@ -407,9 +389,7 @@ impl MessageEncryption for AES128 { let ciphertext_without_eph_pk_x = bytes_from_fields(unmasked_fields); // Derive symmetric keys: - let pairs = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<2>( - ciphertext_shared_secret, - ); + let pairs = derive_aes_symmetric_key_and_iv_from_shared_secret::<2>(s_app); let (body_sym_key, body_iv) = pairs[0]; let (header_sym_key, header_iv) = pairs[1]; @@ -467,8 +447,8 @@ global TWO_POW_248: Field = 2.pow_32(248); /// Removes the Poseidon2-derived mask from a ciphertext field. Returns the unmasked value if it fits in 31 bytes /// (a content field), or `None` if it doesn't (random padding). Unconstrained to prevent accidental use in /// constrained context. -unconstrained fn unmask_field(shared_secret: Point, index: u32, masked: Field) -> Option { - let unmasked = masked - derive_field_mask(shared_secret, index); +unconstrained fn unmask_field(s_app: Field, index: u32, masked: Field) -> Option { + let unmasked = masked - derive_shared_secret_field_mask(s_app, index); if unmasked.lt(TWO_POW_248) { Option::some(unmasked) } else { @@ -476,15 +456,6 @@ unconstrained fn unmask_field(shared_secret: Point, index: u32, masked: Field) - } } -/// Derives a field mask from an ECDH shared secret and field index. Applied only to data fields (those carrying -/// packed message bytes). Padding fields use `random()` instead. -fn derive_field_mask(shared_secret: Point, index: u32) -> Field { - poseidon2_hash_with_separator( - [shared_secret.x, shared_secret.y], - DOM_SEP__CIPHERTEXT_FIELD_MASK + index, - ) -} - /// Produces a random valid address point, i.e. one that is on the curve. This is equivalent to calling /// [`AztecAddress::to_address_point`] on a random valid address. unconstrained fn random_address_point() -> AddressPoint { @@ -506,7 +477,7 @@ unconstrained fn random_address_point() -> AddressPoint { mod test { use crate::{ - keys::ecdh_shared_secret::derive_ecdh_shared_secret, + keys::ecdh_shared_secret::{compute_app_siloed_shared_secret, derive_ecdh_shared_secret}, messages::{ encoding::{HEADER_CIPHERTEXT_SIZE_IN_BYTES, MESSAGE_PLAINTEXT_LEN, MESSAGE_PLAINTEXT_SIZE_IN_BYTES}, encryption::message_encryption::MessageEncryption, @@ -522,7 +493,8 @@ mod test { let env = TestEnvironment::new(); // Message decryption requires oracles that are only available during private execution - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3]; let recipient = AztecAddress::from_field( @@ -539,18 +511,19 @@ mod test { let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(42); // Encrypt the message - let encrypted_message = BoundedVec::from_array(AES128::encrypt(plaintext, recipient)); + let encrypted_message = BoundedVec::from_array(AES128::encrypt(plaintext, recipient, contract_address)); - // Mock shared secret for deterministic test - let shared_secret = derive_ecdh_shared_secret( + // Compute the same app-siloed shared secret that the oracle would return + let raw_shared_secret = derive_ecdh_shared_secret( EmbeddedCurveScalar::from_field(eph_sk), recipient.to_address_point().unwrap().inner, ); + let s_app = compute_app_siloed_shared_secret(raw_shared_secret, contract_address); - let _ = OracleMock::mock("aztec_utl_getSharedSecret").returns(shared_secret); + let _ = OracleMock::mock("aztec_utl_getSharedSecret").returns(s_app); // Decrypt the message - let decrypted = AES128::decrypt(encrypted_message, recipient).unwrap(); + let decrypted = AES128::decrypt(encrypted_message, recipient, contract_address).unwrap(); // The decryption function spits out a BoundedVec because it's designed to work with messages with unknown // length at compile time. For this reason we need to convert the original input to a BoundedVec. @@ -573,12 +546,18 @@ mod test { let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3]; - let ciphertext = AES128::encrypt(plaintext, recipient); + let ciphertext = AES128::encrypt(plaintext, recipient, contract_address); assert_eq( - AES128::decrypt(BoundedVec::from_array(ciphertext), recipient).unwrap(), + AES128::decrypt( + BoundedVec::from_array(ciphertext), + recipient, + contract_address, + ) + .unwrap(), BoundedVec::from_array(plaintext), ); }); @@ -588,10 +567,9 @@ mod test { unconstrained fn encrypt_to_invalid_address() { // x = 3 is a non-residue for this curve, resulting in an invalid address let invalid_address = AztecAddress { inner: 3 }; + let contract_address = AztecAddress { inner: 42 }; - // We just test that we produced some output and did not crash - the result is gibberish as it is encrypted - // using a public key for which we do not know the private key. - let _ = AES128::encrypt([1, 2, 3, 4], invalid_address); + let _ = AES128::encrypt([1, 2, 3, 4], invalid_address, contract_address); } // Documents the PKCS#7 padding behavior that `encrypt` relies on (see its static_assert). @@ -615,15 +593,21 @@ mod test { let mut env = TestEnvironment::new(); let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let mut plaintext = [0; MESSAGE_PLAINTEXT_LEN]; for i in 0..MESSAGE_PLAINTEXT_LEN { plaintext[i] = i as Field; } - let ciphertext = AES128::encrypt(plaintext, recipient); + let ciphertext = AES128::encrypt(plaintext, recipient, contract_address); assert_eq( - AES128::decrypt(BoundedVec::from_array(ciphertext), recipient).unwrap(), + AES128::decrypt( + BoundedVec::from_array(ciphertext), + recipient, + contract_address, + ) + .unwrap(), BoundedVec::from_array(plaintext), ); }); @@ -632,8 +616,9 @@ mod test { #[test(should_fail_with = "Plaintext length exceeds MESSAGE_PLAINTEXT_LEN")] unconstrained fn encrypt_oversized_plaintext() { let address = AztecAddress { inner: 3 }; + let contract_address = AztecAddress { inner: 42 }; let plaintext: [Field; MESSAGE_PLAINTEXT_LEN + 1] = [0; MESSAGE_PLAINTEXT_LEN + 1]; - let _ = AES128::encrypt(plaintext, address); + let _ = AES128::encrypt(plaintext, address, contract_address); } #[test] @@ -652,16 +637,17 @@ mod test { let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3, 4]; - let ciphertext = AES128::encrypt(plaintext, recipient); + let ciphertext = AES128::encrypt(plaintext, recipient, contract_address); // The first field of the ciphertext is the x-coordinate of the ephemeral public key. We set it to a known // non-residue (3), causing `decrypt` to fail to produce a decryption shared secret. let mut bad_ciphertext = BoundedVec::from_array(ciphertext); bad_ciphertext.set(0, 3); - assert(AES128::decrypt(bad_ciphertext, recipient).is_none()); + assert(AES128::decrypt(bad_ciphertext, recipient, contract_address).is_none()); }); } @@ -670,7 +656,10 @@ mod test { let mut env = TestEnvironment::new(); let recipient = env.create_light_account(); - env.private_context(|_| { assert(AES128::decrypt(BoundedVec::new(), recipient).is_none()); }); + env.private_context(|context| { + let contract_address = context.this_address(); + assert(AES128::decrypt(BoundedVec::new(), recipient, contract_address).is_none()); + }); } // Mocks the header AES decrypt oracle to return an empty result. The TS oracle never throws on invalid @@ -680,14 +669,15 @@ mod test { let mut env = TestEnvironment::new(); let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3]; - let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient)); + let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient, contract_address)); let empty_header = BoundedVec::::new(); let _ = OracleMock::mock("aztec_utl_tryAes128Decrypt").returns(Option::some(empty_header)).times(1); - assert(AES128::decrypt(ciphertext, recipient).is_none()); + assert(AES128::decrypt(ciphertext, recipient, contract_address).is_none()); }); } @@ -698,16 +688,17 @@ mod test { let mut env = TestEnvironment::new(); let recipient = env.create_light_account(); - env.private_context(|_| { + env.private_context(|context| { + let contract_address = context.this_address(); let plaintext = [1, 2, 3]; - let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient)); + let ciphertext = BoundedVec::from_array(AES128::encrypt(plaintext, recipient, contract_address)); let bad_header = BoundedVec::::from_array(encode_header( MESSAGE_PLAINTEXT_SIZE_IN_BYTES + 1, )); let _ = OracleMock::mock("aztec_utl_tryAes128Decrypt").returns(Option::some(bad_header)).times(1); - assert(AES128::decrypt(ciphertext, recipient).is_none()); + assert(AES128::decrypt(ciphertext, recipient, contract_address).is_none()); }); } diff --git a/noir-projects/aztec-nr/aztec/src/messages/encryption/message_encryption.nr b/noir-projects/aztec-nr/aztec/src/messages/encryption/message_encryption.nr index 6896a0b9e24e..c47e59a37fb9 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/encryption/message_encryption.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/encryption/message_encryption.nr @@ -34,6 +34,7 @@ pub trait MessageEncryption { fn encrypt( plaintext: [Field; PlaintextLen], recipient: AztecAddress, + contract_address: AztecAddress, ) -> [Field; MESSAGE_CIPHERTEXT_LEN]; /// Decrypts a message ciphertext into its original plaintext. @@ -49,5 +50,6 @@ pub trait MessageEncryption { unconstrained fn decrypt( ciphertext: BoundedVec, recipient: AztecAddress, + contract_address: AztecAddress, ) -> Option>; } diff --git a/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr b/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr index 4dd68c91a33f..a5b67dfefa69 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr @@ -32,6 +32,7 @@ pub fn compute_partial_note_private_content_log( randomness: Field, recipient: AztecAddress, note_completion_log_tag: Field, + contract_address: AztecAddress, ) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] where PartialNotePrivateContent: NoteType + Packable, @@ -42,7 +43,7 @@ where randomness, note_completion_log_tag, ); - let message_ciphertext = AES128::encrypt(message_plaintext, recipient); + let message_ciphertext = AES128::encrypt(message_plaintext, recipient, contract_address); prefix_with_tag(message_ciphertext, recipient) } diff --git a/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr b/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr index 20bae1d6059b..3a8221b7be35 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr @@ -210,9 +210,10 @@ pub fn do_private_message_delivery( // TODO(#14565): Add constrained tagging let _constrained_tagging = delivery_mode == MessageDelivery.ONCHAIN_CONSTRAINED; + let contract_address = context.this_address(); let ciphertext = remove_constraints_if( !constrained_encryption, - || AES128::encrypt(encode_into_message_plaintext(), recipient), + || AES128::encrypt(encode_into_message_plaintext(), recipient, contract_address), ); if deliver_as_offchain_message { diff --git a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr index 29044c297770..960202e3cedf 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr @@ -24,14 +24,17 @@ pub unconstrained fn try_aes128_decrypt( mod test { use crate::{ - messages::encryption::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe, + keys::ecdh_shared_secret::compute_app_siloed_shared_secret, + messages::encryption::aes128::derive_aes_symmetric_key_and_iv_from_shared_secret, utils::{array::subarray::subarray, point::point_from_x_coord}, }; + use crate::protocol::address::AztecAddress; use crate::test::helpers::test_environment::TestEnvironment; use super::try_aes128_decrypt; use poseidon::poseidon2::Poseidon2; use std::aes128::aes128_encrypt; + global CONTRACT_ADDRESS: AztecAddress = AztecAddress { inner: 42 }; global TEST_PLAINTEXT_LENGTH: u32 = 10; global TEST_CIPHERTEXT_LENGTH: u32 = 16; global TEST_PADDING_LENGTH: u32 = TEST_CIPHERTEXT_LENGTH - TEST_PLAINTEXT_LENGTH; @@ -41,11 +44,10 @@ mod test { let env = TestEnvironment::new(); env.utility_context(|_| { - let ciphertext_shared_secret = point_from_x_coord(1).unwrap(); + let shared_secret_point = point_from_x_coord(1).unwrap(); + let s_app = compute_app_siloed_shared_secret(shared_secret_point, CONTRACT_ADDRESS); - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<1>( - ciphertext_shared_secret, - )[0]; + let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_shared_secret::<1>(s_app)[0]; let plaintext: [u8; TEST_PLAINTEXT_LENGTH] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; @@ -82,11 +84,10 @@ mod test { // (https://en.wikipedia.org/wiki/Message_authentication_code). We demonstrate this approach in // this test: we compute a MAC, include it in the plaintext, encrypt, and then verify that // decryption with a bad key produces a MAC mismatch. - let ciphertext_shared_secret = point_from_x_coord(1).unwrap(); + let shared_secret_point = point_from_x_coord(1).unwrap(); + let s_app = compute_app_siloed_shared_secret(shared_secret_point, CONTRACT_ADDRESS); - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<1>( - ciphertext_shared_secret, - )[0]; + let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_shared_secret::<1>(s_app)[0]; let mac_preimage = 0x42; let mac = Poseidon2::hash([mac_preimage], 1); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr index a040afdb98da..10580939f5de 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr @@ -1,16 +1,32 @@ -use crate::protocol::{address::aztec_address::AztecAddress, point::Point}; +use crate::protocol::address::aztec_address::AztecAddress; +use crate::protocol::point::Point; -// TODO(#12656): return an app-siloed secret + document this #[oracle(aztec_utl_getSharedSecret)] -unconstrained fn get_shared_secret_oracle(address: AztecAddress, ephPk: Point) -> Point {} +unconstrained fn get_shared_secret_oracle( + address: AztecAddress, + ephPk: Point, + contract_address: AztecAddress, +) -> Field {} /// Returns an app-siloed shared secret between `address` and someone who knows the secret key behind an ephemeral -/// public key `ephPk`. The app-siloing means that contracts cannot retrieve secrets that belong to other contracts, -/// and therefore cannot e.g. decrypt their messages. This is an important security consideration given that both the -/// `address` and `ephPk` are public information. +/// public key `ephPk`. /// -/// The shared secret `S` is computed as: `let S = (ivsk + h) * ephPk` where `ivsk + h` is the 'preaddress' i.e. the -/// preimage of the address, also called the address secret. TODO(#12656): app-silo this secret -pub unconstrained fn get_shared_secret(address: AztecAddress, ephPk: Point) -> Point { - get_shared_secret_oracle(address, ephPk) +/// The returned value is a Field `s_app`, computed as: +/// +/// ```text +/// S = address_secret * ephPk (raw ECDH point) +/// s_app = h(DOM_SEP, S.x, S.y, contract) (app-siloed scalar) +/// ``` +/// +/// where `contract` is the address of the calling contract. The oracle host validates this matches its execution +/// context. +/// +/// Without app-siloing, a malicious contract could call this oracle with public information (address, ephPk) and +/// obtain the same raw secret as the legitimate contract, enabling cross-contract decryption. By including the +/// contract address in the hash, each contract receives a different `s_app`, preventing this attack. +/// +/// Callers derive indexed subkeys from `s_app` via +/// [`derive_shared_secret_subkey`](crate::keys::ecdh_shared_secret::derive_shared_secret_subkey). +pub unconstrained fn get_shared_secret(address: AztecAddress, ephPk: Point, contract_address: AztecAddress) -> Field { + get_shared_secret_oracle(address, ephPk, contract_address) } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index bf312d89747c..0e9c3ce1a270 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/version.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/version.nr @@ -4,7 +4,7 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is /// called and if the oracle version is incompatible an error is thrown. -pub global ORACLE_VERSION: Field = 20; +pub global ORACLE_VERSION: Field = 21; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 0c9ea6fcd8e8..0eaa2786b107 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -122,6 +122,7 @@ impl UintNote { randomness, recipient, commitment, + context.this_address(), ); // Regardless of the original content size, the log is padded with random bytes up to // `PRIVATE_LOG_SIZE_IN_FIELDS` to prevent leaking information about the actual size. diff --git a/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr b/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr index a2dad7d1e64d..421b81851dfd 100644 --- a/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr +++ b/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr @@ -123,6 +123,7 @@ impl NFTNote { randomness, recipient, commitment, + context.this_address(), ); // Regardless of the original content size, the log is padded with random bytes up to // `PRIVATE_LOG_SIZE_IN_FIELDS` to prevent leaking information about the actual size. diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 8fee36b4dc18..5fcb449b6088 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -736,9 +736,9 @@ pub global DOM_SEP__AUTHWIT_INNER: u32 = 221354163; pub global DOM_SEP__AUTHWIT_OUTER: u32 = 3283595782; pub global DOM_SEP__AUTHWIT_NULLIFIER: u32 = 1239150694; -pub global DOM_SEP__SYMMETRIC_KEY: u32 = 3882206064; -pub global DOM_SEP__SYMMETRIC_KEY_2: u32 = 4129434989; -pub global DOM_SEP__CIPHERTEXT_FIELD_MASK: u32 = 1870492847; +pub global DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET: u32 = 1707851664; +pub global DOM_SEP__ECDH_SUBKEY: u32 = 4277646631; +pub global DOM_SEP__ECDH_FIELD_MASK: u32 = 190532684; pub global DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT: u32 = 623934423; diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr index cd0b509ba3e1..a49fc51fdba8 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr @@ -6,22 +6,22 @@ use crate::{ CONTRACT_CLASS_REGISTRY_PRIVATE_FUNCTION_BROADCASTED_MAGIC_VALUE, CONTRACT_CLASS_REGISTRY_UTILITY_FUNCTION_BROADCASTED_MAGIC_VALUE, CONTRACT_INSTANCE_PUBLISHED_MAGIC_VALUE, CONTRACT_INSTANCE_UPDATED_MAGIC_VALUE, - DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, DOM_SEP__AUTHWIT_OUTER, - DOM_SEP__BLOCK_HEADER_HASH, DOM_SEP__CIPHERTEXT_FIELD_MASK, DOM_SEP__CONTRACT_ADDRESS_V1, - DOM_SEP__CONTRACT_CLASS_ID, DOM_SEP__EVENT_COMMITMENT, DOM_SEP__FUNCTION_ARGS, - DOM_SEP__INITIALIZATION_NULLIFIER, DOM_SEP__INITIALIZER, DOM_SEP__IVSK_M, - DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M, DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, - DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M, DOM_SEP__PARTIAL_ADDRESS, - DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, DOM_SEP__PRIVATE_FUNCTION_LEAF, - DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, DOM_SEP__PRIVATE_LOG_FIRST_FIELD, - DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS, DOM_SEP__PUBLIC_BYTECODE, - DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, - DOM_SEP__PUBLIC_KEYS_HASH, DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, - DOM_SEP__PUBLIC_TX_HASH, DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, - DOM_SEP__SILOED_NOTE_HASH, DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, - DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M, DOM_SEP__TX_NULLIFIER, - DOM_SEP__TX_REQUEST, DOM_SEP__UNIQUE_NOTE_HASH, NULL_MSG_SENDER_CONTRACT_ADDRESS, - SIDE_EFFECT_MASKING_ADDRESS, TX_START_PREFIX, + DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, + DOM_SEP__AUTHWIT_OUTER, DOM_SEP__BLOCK_HEADER_HASH, DOM_SEP__CONTRACT_ADDRESS_V1, + DOM_SEP__CONTRACT_CLASS_ID, DOM_SEP__ECDH_FIELD_MASK, DOM_SEP__ECDH_SUBKEY, + DOM_SEP__EVENT_COMMITMENT, DOM_SEP__FUNCTION_ARGS, DOM_SEP__INITIALIZATION_NULLIFIER, + DOM_SEP__INITIALIZER, DOM_SEP__IVSK_M, DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M, + DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M, + DOM_SEP__PARTIAL_ADDRESS, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, + DOM_SEP__PRIVATE_FUNCTION_LEAF, DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, + DOM_SEP__PRIVATE_LOG_FIRST_FIELD, DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS, + DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA, + DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, DOM_SEP__PUBLIC_KEYS_HASH, + DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH, + DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, DOM_SEP__SILOED_NOTE_HASH, + DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, DOM_SEP__TSK_M, + DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST, DOM_SEP__UNIQUE_NOTE_HASH, + NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS, TX_START_PREFIX, }, hash::poseidon2_hash_bytes, traits::{FromField, ToField}, @@ -178,9 +178,12 @@ fn hashed_values_match_derived() { tester.assert_dom_sep_matches_derived(DOM_SEP__AUTHWIT_INNER, "authwit_inner"); tester.assert_dom_sep_matches_derived(DOM_SEP__AUTHWIT_OUTER, "authwit_outer"); tester.assert_dom_sep_matches_derived(DOM_SEP__AUTHWIT_NULLIFIER, "authwit_nullifier"); - tester.assert_dom_sep_matches_derived(DOM_SEP__SYMMETRIC_KEY, "symmetric_key"); - tester.assert_dom_sep_matches_derived(DOM_SEP__SYMMETRIC_KEY_2, "symmetric_key_2"); - tester.assert_dom_sep_matches_derived(DOM_SEP__CIPHERTEXT_FIELD_MASK, "ciphertext_field_mask"); + tester.assert_dom_sep_matches_derived( + DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, + "app_siloed_ecdh_shared_secret", + ); + tester.assert_dom_sep_matches_derived(DOM_SEP__ECDH_SUBKEY, "ecdh_subkey"); + tester.assert_dom_sep_matches_derived(DOM_SEP__ECDH_FIELD_MASK, "ecdh_field_mask"); tester.assert_dom_sep_matches_derived( DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, "partial_note_validity_commitment", diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index 42e360bec1f1..f274465c1998 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -536,9 +536,9 @@ export enum DomainSeparator { AUTHWIT_INNER = 221354163, AUTHWIT_OUTER = 3283595782, AUTHWIT_NULLIFIER = 1239150694, - SYMMETRIC_KEY = 3882206064, - SYMMETRIC_KEY_2 = 4129434989, - CIPHERTEXT_FIELD_MASK = 1870492847, + APP_SILOED_ECDH_SHARED_SECRET = 1707851664, + ECDH_SUBKEY = 4277646631, + ECDH_FIELD_MASK = 190532684, PARTIAL_NOTE_VALIDITY_COMMITMENT = 623934423, INITIALIZATION_NULLIFIER = 1653084894, PUBLIC_INITIALIZATION_NULLIFIER = 3342006647, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index cd33382e510b..1dd70783bfbd 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -151,7 +151,7 @@ export interface IUtilityExecutionOracle { scope: AztecAddress, ): Promise; aes128Decrypt(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise; - getSharedSecret(address: AztecAddress, ephPk: Point): Promise; + getSharedSecret(address: AztecAddress, ephPk: Point, contractAddress: AztecAddress): Promise; invalidateContractSyncCache(contractAddress: AztecAddress, scopes: AztecAddress[]): void; emitOffchainEffect(data: Fr[]): Promise; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index a58597c66321..dd735eede694 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -78,12 +78,6 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { oracle.aztec_utl_copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, [toACVMField(AztecAddress.ZERO)]), utilityDeleteCapsule: (contractAddress: ACVMField[], slot: ACVMField[]): Promise => oracle.aztec_utl_deleteCapsule(contractAddress, slot, [toACVMField(AztecAddress.ZERO)]), - utilityGetSharedSecret: ( - address: ACVMField[], - ephPKField0: ACVMField[], - ephPKField1: ACVMField[], - ephPKField2: ACVMField[], - ): Promise => oracle.aztec_utl_getSharedSecret(address, ephPKField0, ephPKField1, ephPKField2), utilityGetL1ToL2MembershipWitness: ( contractAddress: ACVMField[], messageHash: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 247909ee59bd..bb9ca7853ff4 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -655,12 +655,14 @@ export class Oracle { [ephPKField0]: ACVMField[], [ephPKField1]: ACVMField[], [ephPKField2]: ACVMField[], + [contractAddress]: ACVMField[], ): Promise { const secret = await this.handlerAsUtility().getSharedSecret( AztecAddress.fromField(Fr.fromString(address)), Point.fromFields([ephPKField0, ephPKField1, ephPKField2].map(Fr.fromString)), + AztecAddress.fromField(Fr.fromString(contractAddress)), ); - return secret.toFields().map(toACVMField); + return [toACVMField(secret)]; } // eslint-disable-next-line camelcase diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 6affe5d2791f..e7b2420ac68b 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -1,6 +1,7 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; +import { Grumpkin } from '@aztec/foundation/crypto/grumpkin'; import { Fr } from '@aztec/foundation/curves/bn254'; -import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; +import { GrumpkinScalar, Point } from '@aztec/foundation/curves/grumpkin'; import type { KeyStore } from '@aztec/key-store'; import { StatefulTestContractArtifact } from '@aztec/noir-test-contracts.js/StatefulTest'; import { WASMSimulator } from '@aztec/simulator/client'; @@ -464,5 +465,67 @@ describe('Utility Execution test suite', () => { ); }); }); + + describe('getSharedSecret', () => { + it('returns different shared secrets for different contract addresses', async () => { + // Generate a deterministic ephemeral public key + const ephSk = GrumpkinScalar.random(); + const ephPk = await Grumpkin.mul(Grumpkin.generator, ephSk); + + // Derive keys so we can mock getMasterSecretKey (used by getSharedSecret) + const { masterIncomingViewingSecretKey: ownerIvskM } = await deriveKeys(ownerSecretKey); + keyStore.getMasterSecretKey.mockImplementation((publicKey: Point) => { + if (publicKey.equals(ownerCompleteAddress.publicKeys.masterIncomingViewingPublicKey)) { + return Promise.resolve(ownerIvskM); + } + throw new Error(`Unknown public key ${publicKey}`); + }); + + const contractAddressA = await AztecAddress.random(); + const contractAddressB = await AztecAddress.random(); + + const makeOracle = (addr: AztecAddress) => + new UtilityExecutionOracle({ + contractAddress: addr, + authWitnesses: [], + capsules: [], + anchorBlockHeader, + contractStore, + noteStore, + keyStore, + addressStore, + aztecNode, + recipientTaggingStore, + senderAddressBookStore, + capsuleStore, + privateEventStore, + messageContextService, + contractSyncService, + jobId: 'test-job-id', + scopes: 'ALL_SCOPES', + }); + + const oracleA = makeOracle(contractAddressA); + const oracleB = makeOracle(contractAddressB); + + const secretA = await oracleA.getSharedSecret(owner, ephPk, contractAddressA); + const secretB = await oracleB.getSharedSecret(owner, ephPk, contractAddressB); + + // After app-siloing, different contracts must get different shared secrets for the same + // (address, ephPk) pair. This prevents cross-contract decryption attacks. + expect(secretA).not.toEqual(secretB); + }); + + it('rejects when contract address does not match execution context', async () => { + const ephSk = GrumpkinScalar.random(); + const ephPk = await Grumpkin.mul(Grumpkin.generator, ephSk); + + const { masterIncomingViewingSecretKey: ownerIvskM } = await deriveKeys(ownerSecretKey); + keyStore.getMasterSecretKey.mockResolvedValue(ownerIvskM); + + const wrongAddress = await AztecAddress.random(); + await expect(utilityExecutionOracle.getSharedSecret(owner, ephPk, wrongAddress)).rejects.toThrow(/expected/); + }); + }); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index ee46c8c7c0d4..5a81c07ebd91 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -15,7 +15,7 @@ import { siloNullifier } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { type PublicKeys, computeAddressSecret } from '@aztec/stdlib/keys'; -import { MessageContext, deriveEcdhSharedSecret } from '@aztec/stdlib/logs'; +import { MessageContext, deriveAppSiloedSharedSecret } from '@aztec/stdlib/logs'; import { getNonNullifiedL1ToL2MessageWitness } from '@aztec/stdlib/messaging'; import type { NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; @@ -735,19 +735,24 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } /** - * Retrieves the shared secret for a given address and ephemeral public key. + * Retrieves the app-siloed shared secret for a given address and ephemeral public key. * @param address - The address to get the secret for. * @param ephPk - The ephemeral public key to get the secret for. - * @returns The secret for the given address. + * @param contractAddress - The contract address for app-siloing (validated against execution context). + * @returns The app-siloed shared secret as a Field. */ - public async getSharedSecret(address: AztecAddress, ephPk: Point): Promise { - // TODO(#12656): return an app-siloed secret + public async getSharedSecret(address: AztecAddress, ephPk: Point, contractAddress: AztecAddress): Promise { + if (!contractAddress.equals(this.contractAddress)) { + throw new Error( + `getSharedSecret called with contract address ${contractAddress}, expected ${this.contractAddress}`, + ); + } const recipientCompleteAddress = await this.getCompleteAddressOrFail(address); const ivskM = await this.keyStore.getMasterSecretKey( recipientCompleteAddress.publicKeys.masterIncomingViewingPublicKey, ); const addressSecret = await computeAddressSecret(await recipientCompleteAddress.getPreaddress(), ivskM); - return deriveEcdhSharedSecret(addressSecret, ephPk); + return deriveAppSiloedSharedSecret(addressSecret, ephPk, this.contractAddress); } public emitOffchainEffect(data: Fr[]): Promise { diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 48265d378960..55e0675bdbbc 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -4,9 +4,13 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is called /// and if the oracle version is incompatible an error is thrown. -export const ORACLE_VERSION = 20; +export const ORACLE_VERSION = 21; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. +<<<<<<< HEAD export const ORACLE_INTERFACE_HASH = 'ab6bb5669bf20d3908eba5f3edaeec3bf1d828b902f5c2734054e372be6e5f22'; +======= +export const ORACLE_INTERFACE_HASH = '83f1de1a9741a34916fd58cf12b857d0bac90f74bf00751b20304301a3f5c8eb'; +>>>>>>> 1c74bf659e (feat(aztec-nr)!: app-silo getSharedSecret oracle (#22020)) diff --git a/yarn-project/stdlib/src/logs/shared_secret_derivation.ts b/yarn-project/stdlib/src/logs/shared_secret_derivation.ts index 0ffcdd2b88ff..ff4e3bb8b1f2 100644 --- a/yarn-project/stdlib/src/logs/shared_secret_derivation.ts +++ b/yarn-project/stdlib/src/logs/shared_secret_derivation.ts @@ -1,26 +1,37 @@ +import { DomainSeparator } from '@aztec/constants'; import { Grumpkin } from '@aztec/foundation/crypto/grumpkin'; -import type { GrumpkinScalar, Point } from '@aztec/foundation/curves/grumpkin'; +import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto/poseidon'; +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; +import type { AztecAddress } from '../aztec-address/index.js'; import type { PublicKey } from '../keys/public_key.js'; /** - * Derive an Elliptic Curve Diffie-Hellman (ECDH) Shared Secret. - * The function takes in an ECDH public key, a private key, and a Grumpkin instance to compute - * the shared secret. + * Derives an app-siloed ECDH shared secret. + * + * Computes the raw ECDH shared secret `S = secretKey * publicKey`, then app-silos it: + * `s_app = h(DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, S.x, S.y, contractAddress)` * * @param secretKey - The secret key used to derive shared secret. * @param publicKey - The public key used to derive shared secret. - * @returns A derived shared secret. + * @param contractAddress - The address of the calling contract, used for app-siloing. + * @returns The app-siloed shared secret as a Field. * @throws If the publicKey is zero. - * - * TODO(#12656): This function is kept around because of the utilityGetSharedSecret oracle. Nuke this once returning - * the app-siloed secret. */ -export function deriveEcdhSharedSecret(secretKey: GrumpkinScalar, publicKey: PublicKey): Promise { +export async function deriveAppSiloedSharedSecret( + secretKey: GrumpkinScalar, + publicKey: PublicKey, + contractAddress: AztecAddress, +): Promise { if (publicKey.isZero()) { throw new Error( `Attempting to derive a shared secret with a zero public key. You have probably passed a zero public key in your Noir code somewhere thinking that the note won't be broadcast... but it was.`, ); } - return Grumpkin.mul(publicKey, secretKey); + const rawSharedSecret = await Grumpkin.mul(publicKey, secretKey); + return poseidon2HashWithSeparator( + [rawSharedSecret.x, rawSharedSecret.y, contractAddress], + DomainSeparator.APP_SILOED_ECDH_SHARED_SECRET, + ); } diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 1834c3afc6aa..3a0f8fc9bdf8 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -933,6 +933,7 @@ export class RPCTranslator { foreignEphPKField0: ForeignCallSingle, foreignEphPKField1: ForeignCallSingle, foreignEphPKField2: ForeignCallSingle, + foreignContractAddress: ForeignCallSingle, ) { const address = AztecAddress.fromField(fromSingle(foreignAddress)); const ephPK = Point.fromFields([ @@ -940,10 +941,11 @@ export class RPCTranslator { fromSingle(foreignEphPKField1), fromSingle(foreignEphPKField2), ]); + const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); - const secret = await this.handlerAsUtility().getSharedSecret(address, ephPK); + const secret = await this.handlerAsUtility().getSharedSecret(address, ephPK, contractAddress); - return toForeignCallResult(secret.toFields().map(toSingle)); + return toForeignCallResult([toSingle(secret)]); } // eslint-disable-next-line camelcase From 3c924bae5a683a48f1723a21cf907c4107881990 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 20:41:03 +0000 Subject: [PATCH 10/22] fix: resolve cherry-pick conflicts in oracle_version.ts --- yarn-project/pxe/src/oracle_version.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 55e0675bdbbc..30b0dc7d9929 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -9,8 +9,4 @@ export const ORACLE_VERSION = 21; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -<<<<<<< HEAD -export const ORACLE_INTERFACE_HASH = 'ab6bb5669bf20d3908eba5f3edaeec3bf1d828b902f5c2734054e372be6e5f22'; -======= -export const ORACLE_INTERFACE_HASH = '83f1de1a9741a34916fd58cf12b857d0bac90f74bf00751b20304301a3f5c8eb'; ->>>>>>> 1c74bf659e (feat(aztec-nr)!: app-silo getSharedSecret oracle (#22020)) +export const ORACLE_INTERFACE_HASH = 'PLACEHOLDER_NEEDS_REGENERATION'; From 2274da90ed6262e0f8701262c23340ad645f21fd Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 20:47:19 +0000 Subject: [PATCH 11/22] fix: regenerate oracle interface hash for v4-next The oracle interface hash differs between next and v4-next due to different Oracle class signatures. Regenerated the hash for v4-next after cherry-pick. --- yarn-project/pxe/src/oracle_version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 30b0dc7d9929..d9d1fce3c463 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -9,4 +9,4 @@ export const ORACLE_VERSION = 21; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -export const ORACLE_INTERFACE_HASH = 'PLACEHOLDER_NEEDS_REGENERATION'; +export const ORACLE_INTERFACE_HASH = '09ca7d52029fc37eea33d7b2346329708cfbab52a953962dd621a8a36a4820a4'; From e7f94df49278dc786664ea31254abb59e82735de Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 21:11:05 +0000 Subject: [PATCH 12/22] fix: regenerate constants after cherry-pick The cherry-pick of PR #22020 modified domain separators in constants.nr but the generated files (constants.gen.ts, aztec_constants.hpp, constants_gen.pil) were not regenerated. Run remake-constants to sync. --- barretenberg/cpp/pil/vm2/constants_gen.pil | 4 ++-- .../cpp/src/barretenberg/vm2/common/aztec_constants.hpp | 7 +++---- yarn-project/constants/src/constants.gen.ts | 7 ++++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/barretenberg/cpp/pil/vm2/constants_gen.pil b/barretenberg/cpp/pil/vm2/constants_gen.pil index 2029daa6bb0a..235af2d2fa64 100644 --- a/barretenberg/cpp/pil/vm2/constants_gen.pil +++ b/barretenberg/cpp/pil/vm2/constants_gen.pil @@ -165,9 +165,9 @@ namespace constants; pol UPDATES_DELAYED_PUBLIC_MUTABLE_METADATA_BIT_SIZE = 144; pol GRUMPKIN_ONE_X = 1; pol GRUMPKIN_ONE_Y = 17631683881184975370165255887551781615748388533673675138860; - pol DOM_SEP__NOTE_HASH_NONCE = 1721808740; - pol DOM_SEP__UNIQUE_NOTE_HASH = 226850429; pol DOM_SEP__SILOED_NOTE_HASH = 3361878420; + pol DOM_SEP__UNIQUE_NOTE_HASH = 226850429; + pol DOM_SEP__NOTE_HASH_NONCE = 1721808740; pol DOM_SEP__SILOED_NULLIFIER = 57496191; pol DOM_SEP__PUBLIC_LEAF_SLOT = 1247650290; pol DOM_SEP__PUBLIC_STORAGE_MAP_SLOT = 4015149901; diff --git a/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp b/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp index 1939294f3582..6801cd163b49 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp @@ -247,8 +247,7 @@ #define AVM_EMITPUBLICLOG_DYN_DA_GAS 32 #define AVM_SSTORE_DYN_DA_GAS 64 #define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_HEIGHT 6 -#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_ROOT \ - "0x2870b93163d4fd6ada360fe48ee1e8e8e69308af34cdfaeffacbbe5929e2466d" +#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_ROOT "0x2870b93163d4fd6ada360fe48ee1e8e8e69308af34cdfaeffacbbe5929e2466d" #define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_SIZE 1 #define AVM_RETRIEVED_BYTECODES_TREE_HEIGHT 5 #define AVM_RETRIEVED_BYTECODES_TREE_INITIAL_ROOT "0x100ba46aea628d39c08788f05fcad4ab19adf5b8bba866a1f5c5baa6e297891d" @@ -257,9 +256,9 @@ #define UPDATES_DELAYED_PUBLIC_MUTABLE_VALUES_LEN 3 #define UPDATES_DELAYED_PUBLIC_MUTABLE_METADATA_BIT_SIZE 144 #define DEFAULT_MAX_DEBUG_LOG_MEMORY_READS 125000 -#define DOM_SEP__NOTE_HASH_NONCE 1721808740UL -#define DOM_SEP__UNIQUE_NOTE_HASH 226850429UL #define DOM_SEP__SILOED_NOTE_HASH 3361878420UL +#define DOM_SEP__UNIQUE_NOTE_HASH 226850429UL +#define DOM_SEP__NOTE_HASH_NONCE 1721808740UL #define DOM_SEP__SILOED_NULLIFIER 57496191UL #define DOM_SEP__PUBLIC_LEAF_SLOT 1247650290UL #define DOM_SEP__PUBLIC_STORAGE_MAP_SLOT 4015149901UL diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index f274465c1998..4475757c64b6 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -505,12 +505,13 @@ export const GRUMPKIN_ONE_Y = 17631683881184975370165255887551781615748388533673 export const DEFAULT_MAX_DEBUG_LOG_MEMORY_READS = 125000; export enum DomainSeparator { NOTE_HASH = 116501019, - NOTE_HASH_NONCE = 1721808740, - UNIQUE_NOTE_HASH = 226850429, SILOED_NOTE_HASH = 3361878420, + UNIQUE_NOTE_HASH = 226850429, + NOTE_HASH_NONCE = 1721808740, + SINGLE_USE_CLAIM_NULLIFIER = 1465998995, NOTE_NULLIFIER = 50789342, - MESSAGE_NULLIFIER = 3754509616, SILOED_NULLIFIER = 57496191, + MESSAGE_NULLIFIER = 3754509616, PRIVATE_LOG_FIRST_FIELD = 2769976252, PUBLIC_LEAF_SLOT = 1247650290, PUBLIC_STORAGE_MAP_SLOT = 4015149901, From d650d2cc17ecb2c20333114edaa53d1fedcb5578 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 21:20:46 +0000 Subject: [PATCH 13/22] Revert "fix: regenerate constants after cherry-pick" This reverts commit e7f94df49278dc786664ea31254abb59e82735de. --- barretenberg/cpp/pil/vm2/constants_gen.pil | 4 ++-- .../cpp/src/barretenberg/vm2/common/aztec_constants.hpp | 7 ++++--- yarn-project/constants/src/constants.gen.ts | 7 +++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/barretenberg/cpp/pil/vm2/constants_gen.pil b/barretenberg/cpp/pil/vm2/constants_gen.pil index 235af2d2fa64..2029daa6bb0a 100644 --- a/barretenberg/cpp/pil/vm2/constants_gen.pil +++ b/barretenberg/cpp/pil/vm2/constants_gen.pil @@ -165,9 +165,9 @@ namespace constants; pol UPDATES_DELAYED_PUBLIC_MUTABLE_METADATA_BIT_SIZE = 144; pol GRUMPKIN_ONE_X = 1; pol GRUMPKIN_ONE_Y = 17631683881184975370165255887551781615748388533673675138860; - pol DOM_SEP__SILOED_NOTE_HASH = 3361878420; - pol DOM_SEP__UNIQUE_NOTE_HASH = 226850429; pol DOM_SEP__NOTE_HASH_NONCE = 1721808740; + pol DOM_SEP__UNIQUE_NOTE_HASH = 226850429; + pol DOM_SEP__SILOED_NOTE_HASH = 3361878420; pol DOM_SEP__SILOED_NULLIFIER = 57496191; pol DOM_SEP__PUBLIC_LEAF_SLOT = 1247650290; pol DOM_SEP__PUBLIC_STORAGE_MAP_SLOT = 4015149901; diff --git a/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp b/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp index 6801cd163b49..1939294f3582 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/common/aztec_constants.hpp @@ -247,7 +247,8 @@ #define AVM_EMITPUBLICLOG_DYN_DA_GAS 32 #define AVM_SSTORE_DYN_DA_GAS 64 #define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_HEIGHT 6 -#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_ROOT "0x2870b93163d4fd6ada360fe48ee1e8e8e69308af34cdfaeffacbbe5929e2466d" +#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_ROOT \ + "0x2870b93163d4fd6ada360fe48ee1e8e8e69308af34cdfaeffacbbe5929e2466d" #define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_SIZE 1 #define AVM_RETRIEVED_BYTECODES_TREE_HEIGHT 5 #define AVM_RETRIEVED_BYTECODES_TREE_INITIAL_ROOT "0x100ba46aea628d39c08788f05fcad4ab19adf5b8bba866a1f5c5baa6e297891d" @@ -256,9 +257,9 @@ #define UPDATES_DELAYED_PUBLIC_MUTABLE_VALUES_LEN 3 #define UPDATES_DELAYED_PUBLIC_MUTABLE_METADATA_BIT_SIZE 144 #define DEFAULT_MAX_DEBUG_LOG_MEMORY_READS 125000 -#define DOM_SEP__SILOED_NOTE_HASH 3361878420UL -#define DOM_SEP__UNIQUE_NOTE_HASH 226850429UL #define DOM_SEP__NOTE_HASH_NONCE 1721808740UL +#define DOM_SEP__UNIQUE_NOTE_HASH 226850429UL +#define DOM_SEP__SILOED_NOTE_HASH 3361878420UL #define DOM_SEP__SILOED_NULLIFIER 57496191UL #define DOM_SEP__PUBLIC_LEAF_SLOT 1247650290UL #define DOM_SEP__PUBLIC_STORAGE_MAP_SLOT 4015149901UL diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index 4475757c64b6..f274465c1998 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -505,13 +505,12 @@ export const GRUMPKIN_ONE_Y = 17631683881184975370165255887551781615748388533673 export const DEFAULT_MAX_DEBUG_LOG_MEMORY_READS = 125000; export enum DomainSeparator { NOTE_HASH = 116501019, - SILOED_NOTE_HASH = 3361878420, - UNIQUE_NOTE_HASH = 226850429, NOTE_HASH_NONCE = 1721808740, - SINGLE_USE_CLAIM_NULLIFIER = 1465998995, + UNIQUE_NOTE_HASH = 226850429, + SILOED_NOTE_HASH = 3361878420, NOTE_NULLIFIER = 50789342, - SILOED_NULLIFIER = 57496191, MESSAGE_NULLIFIER = 3754509616, + SILOED_NULLIFIER = 57496191, PRIVATE_LOG_FIRST_FIELD = 2769976252, PUBLIC_LEAF_SLOT = 1247650290, PUBLIC_STORAGE_MAP_SLOT = 4015149901, From 40a729b6821796c930c0ed7930ba7c2cf5125eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 25 Mar 2026 18:19:16 -0300 Subject: [PATCH 14/22] fix: disallow infinite pubkeys (#22026) These are not allowed elsewhere, so we just add this early catch to prevent getting weirder errors down the stack. --- noir-projects/aztec-nr/aztec/src/context/private_context.nr | 1 + 1 file changed, 1 insertion(+) 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 0232e747f925..e4873da422e1 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -775,6 +775,7 @@ impl PrivateContext { // Safety: Kernels verify that the key validation request is valid and below we verify that a request for // the correct public key has been received. let request = unsafe { get_key_validation_request(pk_m_hash, key_index) }; + assert(!request.pk_m.is_infinite, "Infinite public key points are not allowed"); assert_eq(request.pk_m.hash(), pk_m_hash, "Obtained invalid key validation request"); self.key_validation_requests_and_separators.push( From 72202a0e8f08c55f26ee16659fea591a56d81392 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 21:21:35 +0000 Subject: [PATCH 15/22] fix: noir formatter line length in initialization_utils.nr doc comment --- .../aztec/src/macros/functions/initialization_utils.nr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr index 3ef86dc96953..c8c7780191d2 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr @@ -116,7 +116,8 @@ pub fn assert_is_initialized_private(context: &mut PrivateContext) { /// Asserts that the contract has been initialized, from a utility function's perspective. /// -/// Only checks the private initialization nullifier in the settled nullifier tree. Since both nullifiers are emitted in +/// Only checks the private initialization nullifier in the settled nullifier tree. Since both nullifiers are emitted +/// in /// the same transaction, the private nullifier's presence in settled state guarantees the public one is also settled. pub unconstrained fn assert_is_initialized_utility(context: UtilityContext) { let address = context.this_address(); From 5ae7599f7cbd58867a399ec635396f43c3204824 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 21:51:55 +0000 Subject: [PATCH 16/22] fix: run nargo fmt on aztec-nr (pre-existing formatting issue) --- .../aztec/src/macros/functions/initialization_utils.nr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr index 3ef86dc96953..c8c7780191d2 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr @@ -116,7 +116,8 @@ pub fn assert_is_initialized_private(context: &mut PrivateContext) { /// Asserts that the contract has been initialized, from a utility function's perspective. /// -/// Only checks the private initialization nullifier in the settled nullifier tree. Since both nullifiers are emitted in +/// Only checks the private initialization nullifier in the settled nullifier tree. Since both nullifiers are emitted +/// in /// the same transaction, the private nullifier's presence in settled state guarantees the public one is also settled. pub unconstrained fn assert_is_initialized_utility(context: UtilityContext) { let address = context.this_address(); From f779e1a608deacffe599234f4144f27df98ed151 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 22:21:44 +0000 Subject: [PATCH 17/22] chore: mark e2e_offchain_payment reorg test as flaky The reorg simulation test is intermittently failing. Added to flake patterns with Martin as owner. --- .test_patterns.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.test_patterns.yml b/.test_patterns.yml index 32666e514a5a..2012a92e1348 100644 --- a/.test_patterns.yml +++ b/.test_patterns.yml @@ -293,6 +293,11 @@ tests: owners: - *palla + - regex: "src/e2e_offchain_payment\\.test\\.ts" + error_regex: "reprocesses an offchain-delivered payment after an L1 reorg" + owners: + - *martin + - regex: "bb-micro-bench/wasm/chonk build-wasm-threads/bin/chonk_bench" error_regex: "core dumped" owners: From 89fda7dd088ac98bd6d937ed601916246534d977 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 22:28:18 +0000 Subject: [PATCH 18/22] cherry-pick: feat: aztecnr log prefixing (PR #22027) Cherry-pick of 52a0af801754b3648fa8b6b27acdf4ea4dc8e25e with conflicts in: - yarn-project/aztec/scripts/aztec.sh --- .../crates/types/src/logging.nr | 3 ++ yarn-project/aztec/scripts/aztec.sh | 7 +++++ .../src/utils/client/foreign_call_handler.ts | 6 ++-- .../src/utils/server/foreign_call_handler.ts | 6 ++-- .../oracle/legacy_oracle_mappings.ts | 4 +-- .../oracle/oracle.ts | 4 ++- .../oracle/utility_execution_oracle.ts | 30 ++++++++++++++++--- yarn-project/pxe/src/contract_logging.ts | 23 ++++++++++---- yarn-project/txe/src/rpc_translator.ts | 6 ++-- 9 files changed, 70 insertions(+), 19 deletions(-) diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/logging.nr b/noir-projects/noir-protocol-circuits/crates/types/src/logging.nr index 30afa0713bb2..19d717947907 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/logging.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/logging.nr @@ -83,6 +83,9 @@ unconstrained fn log_oracle_wrapper( log_oracle(log_level, msg, N, args); } +// While the length parameter might seem unnecessary given that we have N, we keep it around because at the AVM +// bytecode level we want to support non-comptime-known lengths for such opcodes, even if Noir code will not generally +// take that route. The AVM transpiler maps this oracle to the DEBUGLOG opcode, which reads the fields size from memory. #[oracle(aztec_utl_log)] unconstrained fn log_oracle( log_level: u8, diff --git a/yarn-project/aztec/scripts/aztec.sh b/yarn-project/aztec/scripts/aztec.sh index 0a7e9003882e..2628c9a13bae 100755 --- a/yarn-project/aztec/scripts/aztec.sh +++ b/yarn-project/aztec/scripts/aztec.sh @@ -21,7 +21,14 @@ function aztec { case $cmd in test) +<<<<<<< HEAD export LOG_LEVEL="${LOG_LEVEL:-"error;trace:contract_log"}" +======= + # Attempt to compile, no-op if there are no changes + node --no-warnings "$script_dir/../dest/bin/index.js" compile + + export LOG_LEVEL="${LOG_LEVEL:-"error;trace:contract"}" +>>>>>>> 52a0af8017 (feat: aztecnr log prefixing) aztec start --txe --port 8081 & server_pid=$! trap 'kill $server_pid &>/dev/null || true' EXIT diff --git a/yarn-project/noir-protocol-circuits-types/src/utils/client/foreign_call_handler.ts b/yarn-project/noir-protocol-circuits-types/src/utils/client/foreign_call_handler.ts index f7e2240a102f..06fd965065e8 100644 --- a/yarn-project/noir-protocol-circuits-types/src/utils/client/foreign_call_handler.ts +++ b/yarn-project/noir-protocol-circuits-types/src/utils/client/foreign_call_handler.ts @@ -9,8 +9,10 @@ export function foreignCallHandler(name: string, args: ForeignCallInput[]): Prom const log = createLogger('noir-protocol-circuits:oracle'); if (name === 'utilityLog') { - assert(args.length === 4, 'expected 4 arguments for debugLog: level, msg, fields_length, fields'); - const [levelInput, msgRaw, _ignoredFieldsSize, fields] = args; + // The fieldsSize parameter is not used here, but it exists because the AVM transpiler maps this oracle to the + // DEBUGLOG opcode, which reads the fields size from memory at runtime. + assert(args.length === 4, 'expected 4 arguments for debugLog: level, msg, fields_size, fields'); + const [levelInput, msgRaw, _fieldsSize, fields] = args; const levelNumber = Fr.fromString(levelInput[0]).toNumber(); if (!LogLevels[levelNumber]) { throw new Error(`Invalid debug log level: ${levelNumber}`); diff --git a/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts b/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts index 33e40d554ee6..5a0df71ab7e4 100644 --- a/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts +++ b/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts @@ -25,8 +25,10 @@ export async function foreignCallHandler(name: string, args: ForeignCallInput[]) const log = createLogger('noir-protocol-circuits:oracle'); if (name === 'utilityLog') { - assert(args.length === 4, 'expected 4 arguments for debugLog: level, msg, fields_length, fields'); - const [levelInput, msgRaw, _ignoredFieldsSize, fields] = args; + // The fieldsSize parameter is not used here, but it exists because the AVM transpiler maps this oracle to the + // DEBUGLOG opcode, which reads the fields size from memory at runtime. + assert(args.length === 4, 'expected 4 arguments for debugLog: level, msg, fields_size, fields'); + const [levelInput, msgRaw, _fieldsSize, fields] = args; const levelNumber = Fr.fromString(levelInput[0]).toNumber(); if (!LogLevels[levelNumber]) { throw new Error(`Invalid debug log level: ${levelNumber}`); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index dd735eede694..8aedb79a9e7c 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -15,9 +15,9 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { utilityLog: ( level: ACVMField[], message: ACVMField[], - _ignoredFieldsSize: ACVMField[], + _fieldsSize: ACVMField[], fields: ACVMField[], - ): Promise => oracle.aztec_utl_log(level, message, _ignoredFieldsSize, fields), + ): Promise => oracle.aztec_utl_log(level, message, _fieldsSize, fields), utilityAssertCompatibleOracleVersion: (version: ACVMField[]): Promise => oracle.aztec_utl_assertCompatibleOracleVersion(version), utilityLoadCapsule: ( diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index bb9ca7853ff4..592ecce77ca2 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -429,11 +429,13 @@ export class Oracle { return Promise.resolve([]); } + // The fieldsSize parameter is not used here (the ACVM passes the full array), but it exists because the AVM + // transpiler maps this oracle to the DEBUGLOG opcode, which reads the fields size from memory at runtime. // eslint-disable-next-line camelcase async aztec_utl_log( level: ACVMField[], message: ACVMField[], - _ignoredFieldsSize: ACVMField[], + _fieldsSize: ACVMField[], fields: ACVMField[], ): Promise { const levelFr = Fr.fromString(level[0]); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 5a81c07ebd91..e28745da78f5 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -22,7 +22,7 @@ import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from import type { BlockHeader, Capsule, OffchainEffect } from '@aztec/stdlib/tx'; import type { AccessScopes } from '../../access_scopes.js'; -import { createContractLogger, logContractMessage } from '../../contract_logging.js'; +import { createContractLogger, logContractMessage, stripAztecnrLogPrefix } from '../../contract_logging.js'; import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; import { EventService } from '../../events/event_service.js'; import { LogService } from '../../logs/log_service.js'; @@ -76,6 +76,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra isUtility = true as const; private contractLogger: Logger | undefined; + private aztecnrLogger: Logger | undefined; private offchainEffects: OffchainEffect[] = []; protected readonly contractAddress: AztecAddress; @@ -452,7 +453,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } /** - * Returns a per-contract logger whose output is prefixed with `contract_log::()`. + * Returns a per-contract logger whose output is prefixed with `contract:()`. */ async #getContractLogger(): Promise { if (!this.contractLogger) { @@ -461,18 +462,39 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.contractLogger = await createContractLogger( this.contractAddress, addr => this.contractStore.getDebugContractName(addr), + 'user', { instanceId: this.jobId }, ); } return this.contractLogger; } + /** + * Returns a per-contract logger whose output is prefixed with `aztecnr:()`. + */ + async #getAztecnrLogger(): Promise { + if (!this.aztecnrLogger) { + // Purpose of instanceId is to distinguish logs from different instances of the same component. It makes sense + // to re-use jobId as instanceId here as executions of different PXE jobs are isolated. + this.aztecnrLogger = await createContractLogger( + this.contractAddress, + addr => this.contractStore.getDebugContractName(addr), + 'aztecnr', + { instanceId: this.jobId }, + ); + } + return this.aztecnrLogger; + } + public async log(level: number, message: string, fields: Fr[]): Promise { if (!LogLevels[level]) { throw new Error(`Invalid log level: ${level}`); } - const logger = await this.#getContractLogger(); - logContractMessage(logger, LogLevels[level], message, fields); + + const { kind, message: strippedMessage } = stripAztecnrLogPrefix(message); + + const logger = kind == 'aztecnr' ? await this.#getAztecnrLogger() : await this.#getContractLogger(); + logContractMessage(logger, LogLevels[level], strippedMessage, fields); } public async fetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr, scope: AztecAddress) { diff --git a/yarn-project/pxe/src/contract_logging.ts b/yarn-project/pxe/src/contract_logging.ts index cb32e2026fa1..e6030d469400 100644 --- a/yarn-project/pxe/src/contract_logging.ts +++ b/yarn-project/pxe/src/contract_logging.ts @@ -5,18 +5,22 @@ import type { DebugLog } from '@aztec/stdlib/logs'; /** Resolves a contract address to a human-readable name, if available. */ export type ContractNameResolver = (address: AztecAddress) => Promise; +export type CONTRACT_LOG_KIND = 'aztecnr' | 'user'; /** - * Creates a logger whose output is prefixed with `contract_log::()`. + * Creates a logger whose output is prefixed with `contract:()`. */ export async function createContractLogger( contractAddress: AztecAddress, getContractName: ContractNameResolver, + kind: CONTRACT_LOG_KIND, options?: { instanceId?: string }, ): Promise { const addrAbbrev = contractAddress.toString().slice(0, 10); const name = await getContractName(contractAddress); - const module = name ? `contract_log::${name}(${addrAbbrev})` : `contract_log::Unknown(${addrAbbrev})`; + + const prefix = kind == 'aztecnr' ? 'aztecnr' : 'contract'; + const module = name ? `${prefix}:${name}(${addrAbbrev})` : `${prefix}:Unknown(${addrAbbrev})`; return createLogger(module, options); } @@ -29,11 +33,20 @@ export function logContractMessage(logger: Logger, level: LogLevel, message: str /** * Displays debug logs collected during public function simulation, - * using the `contract_log::` prefixed logger format. + * using the `contract:` prefixed logger format. */ export async function displayDebugLogs(debugLogs: DebugLog[], getContractName: ContractNameResolver): Promise { for (const log of debugLogs) { - const logger = await createContractLogger(log.contractAddress, getContractName); - logContractMessage(logger, log.level, log.message, log.fields); + const { kind, message } = stripAztecnrLogPrefix(log.message); + const logger = await createContractLogger(log.contractAddress, getContractName, kind); + logContractMessage(logger, log.level, message, log.fields); + } +} + +export function stripAztecnrLogPrefix(message: string): { kind: CONTRACT_LOG_KIND; message: string } { + if (message.startsWith('[aztec-nr] ')) { + return { kind: 'aztecnr', message: message.slice('[aztec-nr] '.length) }; + } else { + return { kind: 'user', message }; } } diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 3a0f8fc9bdf8..9b801243314b 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -357,13 +357,13 @@ export class RPCTranslator { return toForeignCallResult([toArray(returns)]); } - // When the argument is a slice, noir automatically adds a length field to oracle call. - // When the argument is an array, we add the field length manually to the signature. + // The fieldsSize parameter is not used here (the TXE passes the full array), but it exists because the AVM + // transpiler maps this oracle to the DEBUGLOG opcode, which reads the fields size from memory at runtime. // eslint-disable-next-line camelcase async aztec_utl_log( foreignLevel: ForeignCallSingle, foreignMessage: ForeignCallArray, - _foreignLength: ForeignCallSingle, + _foreignFieldsSize: ForeignCallSingle, foreignFields: ForeignCallArray, ) { const level = fromSingle(foreignLevel).toNumber(); From 916fc4e2a7f66a28184138459273766f7d322470 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 22:28:41 +0000 Subject: [PATCH 19/22] fix: resolve cherry-pick conflicts in aztec.sh Kept v4-next base (no compile step) while applying the log filter rename from contract_log to contract. --- yarn-project/aztec/scripts/aztec.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/yarn-project/aztec/scripts/aztec.sh b/yarn-project/aztec/scripts/aztec.sh index 2628c9a13bae..3b905090d772 100755 --- a/yarn-project/aztec/scripts/aztec.sh +++ b/yarn-project/aztec/scripts/aztec.sh @@ -21,14 +21,7 @@ function aztec { case $cmd in test) -<<<<<<< HEAD - export LOG_LEVEL="${LOG_LEVEL:-"error;trace:contract_log"}" -======= - # Attempt to compile, no-op if there are no changes - node --no-warnings "$script_dir/../dest/bin/index.js" compile - export LOG_LEVEL="${LOG_LEVEL:-"error;trace:contract"}" ->>>>>>> 52a0af8017 (feat: aztecnr log prefixing) aztec start --txe --port 8081 & server_pid=$! trap 'kill $server_pid &>/dev/null || true' EXIT From 36ebdaac3953ed806e9812f94371f24b4bfdb96c Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 25 Mar 2026 21:34:06 +0000 Subject: [PATCH 20/22] chore: revert parameter renames to preserve oracle interface hash --- .../src/utils/client/foreign_call_handler.ts | 6 ++---- .../src/utils/server/foreign_call_handler.ts | 6 ++---- .../oracle/legacy_oracle_mappings.ts | 4 ++-- .../pxe/src/contract_function_simulator/oracle/oracle.ts | 4 +--- yarn-project/txe/src/rpc_translator.ts | 6 +++--- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/yarn-project/noir-protocol-circuits-types/src/utils/client/foreign_call_handler.ts b/yarn-project/noir-protocol-circuits-types/src/utils/client/foreign_call_handler.ts index 06fd965065e8..f7e2240a102f 100644 --- a/yarn-project/noir-protocol-circuits-types/src/utils/client/foreign_call_handler.ts +++ b/yarn-project/noir-protocol-circuits-types/src/utils/client/foreign_call_handler.ts @@ -9,10 +9,8 @@ export function foreignCallHandler(name: string, args: ForeignCallInput[]): Prom const log = createLogger('noir-protocol-circuits:oracle'); if (name === 'utilityLog') { - // The fieldsSize parameter is not used here, but it exists because the AVM transpiler maps this oracle to the - // DEBUGLOG opcode, which reads the fields size from memory at runtime. - assert(args.length === 4, 'expected 4 arguments for debugLog: level, msg, fields_size, fields'); - const [levelInput, msgRaw, _fieldsSize, fields] = args; + assert(args.length === 4, 'expected 4 arguments for debugLog: level, msg, fields_length, fields'); + const [levelInput, msgRaw, _ignoredFieldsSize, fields] = args; const levelNumber = Fr.fromString(levelInput[0]).toNumber(); if (!LogLevels[levelNumber]) { throw new Error(`Invalid debug log level: ${levelNumber}`); diff --git a/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts b/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts index 5a0df71ab7e4..33e40d554ee6 100644 --- a/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts +++ b/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts @@ -25,10 +25,8 @@ export async function foreignCallHandler(name: string, args: ForeignCallInput[]) const log = createLogger('noir-protocol-circuits:oracle'); if (name === 'utilityLog') { - // The fieldsSize parameter is not used here, but it exists because the AVM transpiler maps this oracle to the - // DEBUGLOG opcode, which reads the fields size from memory at runtime. - assert(args.length === 4, 'expected 4 arguments for debugLog: level, msg, fields_size, fields'); - const [levelInput, msgRaw, _fieldsSize, fields] = args; + assert(args.length === 4, 'expected 4 arguments for debugLog: level, msg, fields_length, fields'); + const [levelInput, msgRaw, _ignoredFieldsSize, fields] = args; const levelNumber = Fr.fromString(levelInput[0]).toNumber(); if (!LogLevels[levelNumber]) { throw new Error(`Invalid debug log level: ${levelNumber}`); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index 8aedb79a9e7c..dd735eede694 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -15,9 +15,9 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { utilityLog: ( level: ACVMField[], message: ACVMField[], - _fieldsSize: ACVMField[], + _ignoredFieldsSize: ACVMField[], fields: ACVMField[], - ): Promise => oracle.aztec_utl_log(level, message, _fieldsSize, fields), + ): Promise => oracle.aztec_utl_log(level, message, _ignoredFieldsSize, fields), utilityAssertCompatibleOracleVersion: (version: ACVMField[]): Promise => oracle.aztec_utl_assertCompatibleOracleVersion(version), utilityLoadCapsule: ( diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 592ecce77ca2..bb9ca7853ff4 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -429,13 +429,11 @@ export class Oracle { return Promise.resolve([]); } - // The fieldsSize parameter is not used here (the ACVM passes the full array), but it exists because the AVM - // transpiler maps this oracle to the DEBUGLOG opcode, which reads the fields size from memory at runtime. // eslint-disable-next-line camelcase async aztec_utl_log( level: ACVMField[], message: ACVMField[], - _fieldsSize: ACVMField[], + _ignoredFieldsSize: ACVMField[], fields: ACVMField[], ): Promise { const levelFr = Fr.fromString(level[0]); diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 9b801243314b..3a0f8fc9bdf8 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -357,13 +357,13 @@ export class RPCTranslator { return toForeignCallResult([toArray(returns)]); } - // The fieldsSize parameter is not used here (the TXE passes the full array), but it exists because the AVM - // transpiler maps this oracle to the DEBUGLOG opcode, which reads the fields size from memory at runtime. + // When the argument is a slice, noir automatically adds a length field to oracle call. + // When the argument is an array, we add the field length manually to the signature. // eslint-disable-next-line camelcase async aztec_utl_log( foreignLevel: ForeignCallSingle, foreignMessage: ForeignCallArray, - _foreignFieldsSize: ForeignCallSingle, + _foreignLength: ForeignCallSingle, foreignFields: ForeignCallArray, ) { const level = fromSingle(foreignLevel).toNumber(); From 9782c0ab216467470beb1a0c23ea6287387c0938 Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Wed, 25 Mar 2026 21:16:46 -0300 Subject: [PATCH 21/22] cherry-pick: feat(aztec-nr)!: domain-separated tags on log emission (#21910) Cherry-pick of 8c88e11c0d with conflicts for reviewer visibility. --- .../docs/resources/migration_notes.md | 37 ++++++++- .../aztec/src/context/private_context.nr | 56 +++++++++---- .../aztec/src/context/public_context.nr | 20 ++++- .../aztec/src/event/event_emission.nr | 22 ++--- .../aztec/src/messages/logs/partial_note.nr | 31 +------ .../aztec-nr/aztec/src/messages/logs/utils.nr | 47 ++++------- .../aztec/src/messages/message_delivery.nr | 16 ++-- .../aztec/src/messages/processing/mod.nr | 15 +++- .../aztec-nr/uint-note/src/uint_note.nr | 61 +++++++------- .../app/nft_contract/src/types/nft_note.nr | 46 ++++++----- .../test/avm_test_contract/src/main.nr | 19 +++-- .../test/benchmarking_contract/src/main.nr | 81 ++++++++++++++++++- .../contracts/test/child_contract/src/main.nr | 10 +-- .../test/no_constructor_contract/src/main.nr | 4 +- .../test/static_child_contract/src/main.nr | 6 +- .../contracts/test/test_contract/src/main.nr | 34 ++++++-- .../crates/types/src/constants.nr | 6 ++ .../crates/types/src/constants_tests.nr | 21 +++-- .../crates/types/src/hash.nr | 4 + yarn-project/aztec.js/src/api/events.ts | 19 ++--- yarn-project/constants/src/constants.gen.ts | 6 ++ .../end-to-end/src/e2e_block_building.test.ts | 17 ++-- .../e2e_deploy_contract/deploy_method.test.ts | 7 +- .../end-to-end/src/e2e_multiple_blobs.test.ts | 1 + .../oracle/private_execution.test.ts | 2 +- .../src/public/avm/avm_simulator.test.ts | 10 ++- yarn-project/stdlib/src/hash/hash.ts | 5 ++ yarn-project/stdlib/src/logs/siloed_tag.ts | 9 ++- 28 files changed, 390 insertions(+), 222 deletions(-) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index ddbc36293e6e..40df1255046e 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,42 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.nr] Domain-separated tags on log emission + +All logs emitted through the Aztec.nr framework now include a domain-separated tag at `fields[0]`. Each log category uses its own domain separator via `compute_log_tag(raw_tag, dom_sep)`: + +- **Events** (`DOM_SEP__EVENT_LOG_TAG`): the event type ID is the raw tag. +- **Message delivery** (`DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG`): the discovery tag is the raw tag. +- **Partial note completion logs** (`DOM_SEP__NOTE_COMPLETION_LOG_TAG`): the partial note's `commitment` field is the raw tag. + +The low-level emit methods now take `tag` as an explicit first parameter and have been renamed with an `_unsafe` suffix. Previously the tag was included as `log[0]` — it has now been extracted into its own parameter, and `log` no longer contains it: + +```diff +- context.emit_private_log(log, length); ++ context.emit_private_log_unsafe(tag, log, length); +- context.emit_raw_note_log(log, length, note_hash_counter); ++ context.emit_raw_note_log_unsafe(tag, log, length, note_hash_counter); +- context.emit_public_log(log); ++ context.emit_public_log_unsafe(tag, log); +``` + +Prefer the higher-level APIs (`emit` for events, `MessageDelivery` for messages) which handle tagging automatically. + +### [Aztec.nr] Public events no longer include the event type selector at the end of the payload + +`emit_event_in_public` previously appended the event type selector as the last field. It now prepends a domain-separated tag at `fields[0]` instead. The payload after the tag contains only the serialized event fields. + +If you were reading public event directly from node logs (i.e. via `node.getPublicLogs` and not via `wallet.getPublicEvents`), update your parsing: + +```diff +- // Old: fields = [serialized_event..., event_type_selector] +- const selector = EventSelector.fromField(fields[fields.length - 1]); +- const event = decodeFromAbi([abiType], fields); ++ // New: fields = [domain_separated_tag, serialized_event...] ++ const eventFields = log.getEmittedFieldsWithoutTag(); ++ const event = decodeFromAbi([abiType], eventFields); +``` + ### [Aztec.nr] Capsule operations are now addressed by scope All capsule operations (`store`, `load`, `delete`, `copy`) and `CapsuleArray` now require a `scope: AztecAddress` parameter. This scopes capsule storage by address, providing isolation between different accounts within the same PXE. @@ -62,7 +98,6 @@ The `CustomMessageHandler` function type now receives an additional `scope: Azte ``` **Impact**: Contracts that implement a custom message handler must update the function signature. - ### [aztec.js] `DeployMethod.send()` always returns `{ contract, receipt, instance }` The `returnReceipt` option in deploy wait options has been removed. `DeployMethod.send()` now always returns an object with `contract`, `receipt`, and `instance` at the top level, provided the user waits for the transaction to be included. 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 769af4b3c551..9d5e32df68b1 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -37,7 +37,7 @@ use crate::protocol::{ MAX_KEY_VALIDATION_REQUESTS_PER_CALL, MAX_L2_TO_L1_MSGS_PER_CALL, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, MAX_NOTE_HASHES_PER_CALL, MAX_NULLIFIER_READ_REQUESTS_PER_CALL, MAX_NULLIFIERS_PER_CALL, MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL, MAX_PRIVATE_LOGS_PER_CALL, MAX_TX_LIFETIME, - NULL_MSG_SENDER_CONTRACT_ADDRESS, PRIVATE_LOG_SIZE_IN_FIELDS, + NULL_MSG_SENDER_CONTRACT_ADDRESS, PRIVATE_LOG_CIPHERTEXT_LEN, }, hash::poseidon2_hash, messaging::l2_to_l1_message::L2ToL1Message, @@ -878,22 +878,33 @@ impl PrivateContext { /// about _which_ function has been executed. A tx which leaks such information does not contribute to the privacy /// set of the network. /// - /// * Unlike `emit_raw_note_log`, this log is not tied to any specific note + /// * Unlike `emit_raw_note_log_unsafe`, this log is not tied to any specific note /// /// # Arguments + /// * `tag` - A tag placed at `fields[0]` of the emitted log. Used by recipients and nodes to identify and + /// filter for relevant logs without scanning all of them. /// * `log` - The log data that will be publicly broadcast (so make sure it's already been encrypted before you - /// call this function). Private logs are bounded in size (PRIVATE_LOG_SIZE_IN_FIELDS), to encourage all logs from - /// all smart contracts look identical. - /// * `length` - The actual length of the `log` (measured in number of Fields). Although the input log has a max - /// size of PRIVATE_LOG_SIZE_IN_FIELDS, the latter values of the array might all be 0's for small logs. This - /// `length` should reflect the trimmed length of the array. The protocol's kernel circuits can then append random - /// fields as "padding" after the `length`, so that the logs of this smart contract look indistinguishable from - /// (the same length as) the logs of all other applications. It's up to wallets how much padding to apply, so - /// ideally all wallets should agree on standards for this. - pub fn emit_private_log(&mut self, log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS], length: u32) { + /// call this function). Private logs are bounded in size (`PRIVATE_LOG_CIPHERTEXT_LEN`), to encourage all logs + /// from all smart contracts look identical. + /// * `length` - The actual length of `log` (measured in number of Fields). Although the input log has a max + /// size of `PRIVATE_LOG_CIPHERTEXT_LEN`, the latter values of the array might all be 0's for small logs. This + /// `length` should reflect the trimmed length of the array. The protocol's kernel circuits can then append + /// random fields as "padding" after the `length`, so that the logs of this smart contract look + /// indistinguishable from (the same length as) the logs of all other applications. It's up to wallets how much + /// padding to apply, so ideally all wallets should agree on standards for this. + /// + /// ## Safety + /// + /// The `tag` should be domain-separated (e.g. via [`crate::protocol::hash::compute_log_tag`]) to prevent + /// collisions between logs from different sources. Without domain separation, two unrelated log types that + /// happen to share a raw tag value become indistinguishable. Prefer the higher-level APIs + /// ([`crate::messages::message_delivery::MessageDelivery`] for messages, `self.emit(event)` for events) which + /// handle tagging automatically. + pub fn emit_private_log_unsafe(&mut self, tag: Field, log: [Field; PRIVATE_LOG_CIPHERTEXT_LEN], length: u32) { let counter = self.next_counter(); - let private_log = PrivateLogData { log: PrivateLog::new(log, length), note_hash_counter: 0 }.count(counter); - self.private_logs.push(private_log); + let full_log = [tag].concat(log); + self.private_logs.push(PrivateLogData { log: PrivateLog::new(full_log, length + 1), note_hash_counter: 0 } + .count(counter)); } // TODO: rename. @@ -903,20 +914,31 @@ impl PrivateContext { /// This linkage is important in case the note gets squashed (due to being read later in this same tx), since we /// can then squash the log as well. /// - /// See `emit_private_log` for more info about private log emission. + /// See `emit_private_log_unsafe` for more info about private log emission. /// /// # Arguments + /// * `tag` - A tag placed at `fields[0]`. See `emit_private_log_unsafe`. /// * `log` - The log data as an array of Field elements /// * `length` - The actual length of the `log` (measured in number of Fields). /// * `note_hash_counter` - The side-effect counter that was assigned to the new note_hash when it was pushed to /// this `PrivateContext`. /// /// Important: If your application logic requires the log to always be emitted regardless of note squashing, - /// consider using `emit_private_log` instead, or emitting additional events. + /// consider using `emit_private_log_unsafe` instead, or emitting additional events. /// - pub fn emit_raw_note_log(&mut self, log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS], length: u32, note_hash_counter: u32) { + /// ## Safety + /// + /// Same as [`PrivateContext::emit_private_log_unsafe`]: the `tag` should be domain-separated. + pub fn emit_raw_note_log_unsafe( + &mut self, + tag: Field, + log: [Field; PRIVATE_LOG_CIPHERTEXT_LEN], + length: u32, + note_hash_counter: u32, + ) { let counter = self.next_counter(); - let private_log = PrivateLogData { log: PrivateLog::new(log, length), note_hash_counter }; + let full_log = [tag].concat(log); + let private_log = PrivateLogData { log: PrivateLog::new(full_log, length + 1), note_hash_counter }; self.private_logs.push(private_log.count(counter)); } diff --git a/noir-projects/aztec-nr/aztec/src/context/public_context.nr b/noir-projects/aztec-nr/aztec/src/context/public_context.nr index ad9e91c0ada4..8a70842b0bb1 100644 --- a/noir-projects/aztec-nr/aztec/src/context/public_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/public_context.nr @@ -11,6 +11,7 @@ use crate::protocol::{ address::{AztecAddress, EthAddress}, constants::{MAX_U32_VALUE, NULL_MSG_SENDER_CONTRACT_ADDRESS}, traits::{Empty, FromField, Packable, Serialize, ToField}, + utils::writer::Writer, }; /// # PublicContext @@ -96,14 +97,27 @@ impl PublicContext { /// Emits a _public_ log that will be visible onchain to everyone. /// /// # Arguments - /// * `log` - The data to log, must implement Serialize trait + /// * `tag` - A tag placed at `fields[0]` of the emitted log. Nodes index logs by this value, allowing + /// clients to efficiently query for matching logs without scanning all of them. + /// * `log` - The data to log, must implement Serialize trait. /// - pub fn emit_public_log(_self: Self, log: T) + /// ## Safety + /// + /// The `tag` should be domain-separated (e.g. via [`crate::protocol::hash::compute_log_tag`]) to prevent + /// collisions between logs from different sources. Without domain separation, two unrelated log types that + /// happen to share a raw tag value become indistinguishable. Prefer `self.emit(event)` for events, which + /// handles tagging automatically. + pub fn emit_public_log_unsafe(_self: Self, tag: Field, log: T) where T: Serialize, { + // We use a Writer to serialize the log directly after the tag, avoiding an extra O(n) copy that would + // result from serializing first and then prepending the tag. + let mut writer: Writer<1 + ::N> = Writer::new(); + writer.write(tag); + Serialize::stream_serialize(log, &mut writer); // Safety: AVM opcodes are constrained by the AVM itself - unsafe { avm::emit_public_log(Serialize::serialize(log).as_vector()) }; + unsafe { avm::emit_public_log(writer.finish().as_vector()) }; } /// Checks if a given note hash exists in the note hash tree at a particular leaf_index. diff --git a/noir-projects/aztec-nr/aztec/src/event/event_emission.nr b/noir-projects/aztec-nr/aztec/src/event/event_emission.nr index 845c2f366f6c..38824169d3ac 100644 --- a/noir-projects/aztec-nr/aztec/src/event/event_emission.nr +++ b/noir-projects/aztec-nr/aztec/src/event/event_emission.nr @@ -3,7 +3,7 @@ use crate::{ event::{event_interface::{compute_private_event_commitment, EventInterface}, EventMessage}, oracle::random::random, }; -use crate::protocol::traits::{Serialize, ToField}; +use crate::protocol::{constants::DOM_SEP__EVENT_LOG_TAG, hash::compute_log_tag, traits::{Serialize, ToField}}; /// An event that was emitted in the current contract call. pub struct NewEvent { @@ -39,17 +39,11 @@ pub fn emit_event_in_public(context: PublicContext, event: Event) where Event: EventInterface + Serialize, { - let mut log_content = [0; ::N + 1]; - - let serialized_event = event.serialize(); - for i in 0..serialized_event.len() { - log_content[i] = serialized_event[i]; - } - - // We put the selector in the "last" place, to avoid reading or assigning to an expression in an index - // - // TODO(F-224): change this order. - log_content[serialized_event.len()] = Event::get_event_type_id().to_field(); - - context.emit_public_log(log_content); + // We prepend a domain-separated tag derived from the event type ID so that clients can filter for specific + // events without scanning all public logs. + let log_tag = compute_log_tag( + Event::get_event_type_id().to_field(), + DOM_SEP__EVENT_LOG_TAG, + ); + context.emit_public_log_unsafe(log_tag, event); } diff --git a/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr b/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr index a5b67dfefa69..0c2711c5f829 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr @@ -1,18 +1,12 @@ use crate::{ messages::{ encoding::{encode_message, MAX_MESSAGE_CONTENT_LEN, MESSAGE_EXPANDED_METADATA_LEN}, - encryption::{aes128::AES128, message_encryption::MessageEncryption}, - logs::utils::prefix_with_tag, msg_type::PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID, }, note::note_interface::NoteType, utils::array, }; -use crate::protocol::{ - address::AztecAddress, - constants::PRIVATE_LOG_SIZE_IN_FIELDS, - traits::{FromField, Packable, ToField}, -}; +use crate::protocol::{address::AztecAddress, traits::{FromField, Packable, ToField}}; /// The number of fields in a private note message content that are not the note's packed representation. pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN: u32 = 3; @@ -25,29 +19,6 @@ pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NOTE_COMPLETION_LOG_TAG_IND pub global MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN: u32 = MAX_MESSAGE_CONTENT_LEN - PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN; -// TODO(#16881): once partial notes support delivery via an offchain message we will most likely want to remove this. -pub fn compute_partial_note_private_content_log( - partial_note_private_content: PartialNotePrivateContent, - owner: AztecAddress, - randomness: Field, - recipient: AztecAddress, - note_completion_log_tag: Field, - contract_address: AztecAddress, -) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] -where - PartialNotePrivateContent: NoteType + Packable, -{ - let message_plaintext = encode_partial_note_private_message( - partial_note_private_content, - owner, - randomness, - note_completion_log_tag, - ); - let message_ciphertext = AES128::encrypt(message_plaintext, recipient, contract_address); - - prefix_with_tag(message_ciphertext, recipient) -} - /// Creates the plaintext for a partial note private message (i.e. one of type [`PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID`]). /// /// This plaintext is meant to be decoded via [`decode_partial_note_private_message`]. diff --git a/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr b/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr index afd9ab550f49..526acc55b4d3 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr @@ -2,63 +2,44 @@ use crate::oracle::notes::{get_next_app_tag_as_sender, get_sender_for_tags}; use crate::protocol::address::AztecAddress; // TODO(#14565): Add constrained tagging -pub(crate) fn prefix_with_tag(log_without_tag: [Field; L], recipient: AztecAddress) -> [Field; L + 1] { +/// Returns the next discovery tag for a private log sent to `recipient`. +/// +/// Private logs are encrypted, so the recipient cannot tell which logs are meant for it just by looking at them. +/// To solve this, sender and recipient derive a shared secret from their keys, and from that secret they produce a +/// sequence of one-time tags (tag_0, tag_1, ...). The recipient scans for these tags because it can compute the same +/// sequence. This function returns the next raw (not domain-separated) tag in the sequence. +pub(crate) fn compute_discovery_tag(recipient: AztecAddress) -> Field { // Safety: we assume that the sender wants for the recipient to find the tagged note, and therefore that they will // cooperate and use the correct tag. Usage of a bad tag will result in the recipient not being able to find the // note automatically. - let tag = unsafe { + unsafe { let sender = get_sender_for_tags().expect( f"Sender for tags is not set when emitting a private log. Set it by calling `set_sender_for_tags(...)`.", ); get_next_app_tag_as_sender(sender, recipient) - }; - - let mut log_with_tag = [0; L + 1]; - - log_with_tag[0] = tag; - for i in 0..log_without_tag.len() { - log_with_tag[i + 1] = log_without_tag[i]; } - - log_with_tag } mod test { use crate::protocol::{address::AztecAddress, traits::FromField}; - use super::prefix_with_tag; + use super::compute_discovery_tag; use std::test::OracleMock; - #[test(should_fail)] + #[test(should_fail_with = "Sender for tags is not set")] unconstrained fn no_tag_sender() { let recipient = AztecAddress::from_field(2); - - let expected_tag = 42; - - // Mock the tagging oracles - note aztec_prv_getSenderForTags returns none let _ = OracleMock::mock("aztec_prv_getSenderForTags").returns(Option::::none()); - let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(expected_tag); - - let log_without_tag = [1, 2, 3]; - let _ = prefix_with_tag(log_without_tag, recipient); + let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(42); + let _ = compute_discovery_tag(recipient); } #[test] - unconstrained fn prefixing_with_tag() { + unconstrained fn returns_oracle_tag() { let sender = AztecAddress::from_field(1); let recipient = AztecAddress::from_field(2); - let expected_tag = 42; - - // Mock the tagging oracles let _ = OracleMock::mock("aztec_prv_getSenderForTags").returns(Option::some(sender)); let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(expected_tag); - - let log_without_tag = [1, 2, 3]; - let log_with_tag = prefix_with_tag(log_without_tag, recipient); - - let expected_result = [expected_tag, 1, 2, 3]; - - // Check tag was prefixed correctly - assert_eq(log_with_tag, expected_result, "Tag was not prefixed correctly"); + assert_eq(compute_discovery_tag(recipient), expected_tag); } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr b/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr index 3a8221b7be35..8b4a489ffe50 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/message_delivery.nr @@ -2,12 +2,12 @@ use crate::{ context::PrivateContext, messages::{ encryption::{aes128::AES128, message_encryption::MessageEncryption}, - logs::utils::prefix_with_tag, + logs::utils::compute_discovery_tag, offchain_messages::deliver_offchain_message, }, utils::remove_constraints::remove_constraints_if, }; -use crate::protocol::address::AztecAddress; +use crate::protocol::{address::AztecAddress, constants::DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, hash::compute_log_tag}; /// Placeholder struct until Noir adds `enum` support. /// @@ -219,9 +219,11 @@ pub fn do_private_message_delivery( if deliver_as_offchain_message { deliver_offchain_message(ciphertext, recipient); } else { - // Safety: Currently unsafe. See description of ONCHAIN_CONSTRAINED in MessageDeliveryEnum. TODO(#14565): - // Implement proper constrained tag prefixing to make this truly ONCHAIN_CONSTRAINED - let log_content = prefix_with_tag(ciphertext, recipient); + // TODO(#14565): constrained tagging is not yet implemented. Both modes currently use the unconstrained + // domain separator because the discovery tag always comes from an oracle. Once constrained tagging lands, + // this should branch on `constrained_tagging` to select the appropriate separator. + let discovery_tag = compute_discovery_tag(recipient); + let log_tag = compute_log_tag(discovery_tag, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG); // We forbid this value not being constant to avoid predicating the context calls below, which might result in // the context's arrays having unknown compile time write indices and hence dramatically increasing constraints @@ -235,9 +237,9 @@ pub fn do_private_message_delivery( // // Note that the log always has the same length regardless of `MESSAGE_PLAINTEXT_LEN`, because all message // ciphertexts also have the same length. This prevents accidental privacy leakage via the log length. - context.emit_raw_note_log(log_content, log_content.len(), maybe_note_hash_counter.unwrap()); + context.emit_raw_note_log_unsafe(log_tag, ciphertext, ciphertext.len(), maybe_note_hash_counter.unwrap()); } else { - context.emit_private_log(log_content, log_content.len()); + context.emit_private_log_unsafe(log_tag, ciphertext, ciphertext.len()); } } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 7582906f2646..486436587642 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -23,7 +23,12 @@ use crate::{ }, oracle, }; -use crate::protocol::{address::AztecAddress, hash::sha256_to_field, traits::{Deserialize, Serialize}}; +use crate::protocol::{ + address::AztecAddress, + constants::DOM_SEP__NOTE_COMPLETION_LOG_TAG, + hash::{compute_log_tag, sha256_to_field}, + traits::{Deserialize, Serialize}, +}; use event_validation_request::EventValidationRequest; // Base slot for the pending tagged log array to which the fetch_tagged_logs oracle inserts found private logs. @@ -224,9 +229,13 @@ pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( let pending_partial_notes_count = pending_partial_notes.len(); while i < pending_partial_notes_count { let pending_partial_note = pending_partial_notes.get(i); - log_retrieval_requests.push( - LogRetrievalRequest { contract_address, unsiloed_tag: pending_partial_note.note_completion_log_tag }, + // Partial note completion logs are emitted with a domain-separated tag. To find matching logs, we apply the + // same domain separation to the stored raw tag. + let log_tag = compute_log_tag( + pending_partial_note.note_completion_log_tag, + DOM_SEP__NOTE_COMPLETION_LOG_TAG, ); + log_retrieval_requests.push(LogRetrievalRequest { contract_address, unsiloed_tag: log_tag }); i += 1; } diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 0eaa2786b107..d075a6e5491d 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -3,13 +3,19 @@ use aztec::{ history::nullifier::assert_nullifier_existed_by, keys::getters::{get_nhk_app, get_public_keys, try_get_public_keys}, macros::notes::custom_note, - messages::logs::partial_note::compute_partial_note_private_content_log, + messages::{ + logs::partial_note::encode_partial_note_private_message, + message_delivery::{do_private_message_delivery, MessageDelivery}, + }, note::{note_interface::{NoteHash, NoteType}, utils::compute_note_nullifier}, oracle::random::random, protocol::{ address::AztecAddress, - constants::{DOM_SEP__NOTE_HASH, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, PRIVATE_LOG_SIZE_IN_FIELDS}, - hash::{compute_siloed_nullifier, poseidon2_hash_with_separator}, + constants::{ + DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__NOTE_HASH, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, + PRIVATE_LOG_CIPHERTEXT_LEN, + }, + hash::{compute_log_tag, compute_siloed_nullifier, poseidon2_hash_with_separator}, traits::{Deserialize, FromField, Hash, Packable, Serialize, ToField}, }, }; @@ -116,18 +122,13 @@ impl UintNote { // - other contracts cannot impersonate us and emit logs with the same tag due to public log siloing let private_log_content = UintPartialNotePrivateLogContent {}; - let encrypted_log = compute_partial_note_private_content_log( - private_log_content, - owner, - randomness, + do_private_message_delivery( + context, + || encode_partial_note_private_message(private_log_content, owner, randomness, commitment), + Option::none(), recipient, - commitment, - context.this_address(), + MessageDelivery.ONCHAIN_UNCONSTRAINED, ); - // Regardless of the original content size, the log is padded with random bytes up to - // `PRIVATE_LOG_SIZE_IN_FIELDS` to prevent leaking information about the actual size. - let length = encrypted_log.len(); - context.emit_private_log(encrypted_log, length); let partial_note = PartialUintNote { commitment }; @@ -171,7 +172,7 @@ pub struct PartialUintNote { } // docs:end:partial_uint_note_def -global NOTE_COMPLETION_LOG_LENGTH: u32 = 3; +global NOTE_COMPLETION_PAYLOAD_LENGTH: u32 = 2; impl PartialUintNote { /// Completes the partial note, creating a new note that can be used like any other UintNote. @@ -194,11 +195,12 @@ impl PartialUintNote { // We need to do two things: // - emit a public log containing the public fields (the storage slot and value). The contract will later find - // it by searching for the expected tag (which is simply the partial note commitment). + // it by searching for the domain-separated commitment as the tag. // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. - context.emit_public_log(self.compute_note_completion_log(storage_slot, value)); + let log_tag = compute_log_tag(self.commitment, DOM_SEP__NOTE_COMPLETION_LOG_TAG); + context.emit_public_log_unsafe(log_tag, [storage_slot, value.to_field()]); context.push_note_hash(self.compute_complete_note_hash(storage_slot, value)); } @@ -225,15 +227,13 @@ impl PartialUintNote { // We need to do two things: // - emit an unencrypted log containing the public fields (the storage slot and value) via the private log - // channel. The contract will later find it by searching for the expected tag (which is simply the partial note - // commitment). + // channel. The contract will later find it by searching for the domain-separated commitment as the tag. // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. - context.emit_private_log( - self.compute_note_completion_log_padded_for_private_log(storage_slot, value), - NOTE_COMPLETION_LOG_LENGTH, - ); + let log_tag = compute_log_tag(self.commitment, DOM_SEP__NOTE_COMPLETION_LOG_TAG); + let padded_payload = self.compute_note_completion_payload_padded_for_private_log(storage_slot, value); + context.emit_private_log_unsafe(log_tag, padded_payload, NOTE_COMPLETION_PAYLOAD_LENGTH); context.push_note_hash(self.compute_complete_note_hash(storage_slot, value)); } @@ -249,20 +249,13 @@ impl PartialUintNote { ) } - fn compute_note_completion_log(self, storage_slot: Field, value: u128) -> [Field; NOTE_COMPLETION_LOG_LENGTH] { - // The first field of this log must be the tag that the recipient of the partial note private field logs - // expects, which is equal to the partial note commitment. - [self.commitment, storage_slot, value.to_field()] - } - - fn compute_note_completion_log_padded_for_private_log( - self, + fn compute_note_completion_payload_padded_for_private_log( + _self: Self, storage_slot: Field, value: u128, - ) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] { - let note_completion_log = self.compute_note_completion_log(storage_slot, value); - let padding = [0; PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_COMPLETION_LOG_LENGTH]; - note_completion_log.concat(padding) + ) -> [Field; PRIVATE_LOG_CIPHERTEXT_LEN] { + let payload = [storage_slot, value.to_field()]; + payload.concat([0; PRIVATE_LOG_CIPHERTEXT_LEN - NOTE_COMPLETION_PAYLOAD_LENGTH]) } // docs:start:compute_complete_note_hash diff --git a/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr b/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr index 421b81851dfd..0542cdf24f09 100644 --- a/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr +++ b/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr @@ -2,13 +2,19 @@ use aztec::{ context::{PrivateContext, PublicContext}, keys::getters::{get_nhk_app, get_public_keys, try_get_public_keys}, macros::notes::custom_note, - messages::logs::partial_note::compute_partial_note_private_content_log, + messages::{ + logs::partial_note::encode_partial_note_private_message, + message_delivery::{do_private_message_delivery, MessageDelivery}, + }, note::{note_interface::{NoteHash, NoteType}, utils::compute_note_nullifier}, oracle::random::random, protocol::{ address::AztecAddress, - constants::{DOM_SEP__NOTE_HASH, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT}, - hash::poseidon2_hash_with_separator, + constants::{ + DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__NOTE_HASH, + DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, + }, + hash::{compute_log_tag, poseidon2_hash_with_separator}, traits::{Deserialize, Hash, Packable, Serialize, ToField}, }, }; @@ -117,18 +123,20 @@ impl NFTNote { // - other contracts cannot impersonate us and emit logs with the same tag due to public log siloing let private_log_content = NFTPartialNotePrivateLogContent {}; - let encrypted_log = compute_partial_note_private_content_log( - private_log_content, - owner, - randomness, + do_private_message_delivery( + context, + || { + encode_partial_note_private_message( + private_log_content, + owner, + randomness, + commitment, + ) + }, + Option::none(), recipient, - commitment, - context.this_address(), + MessageDelivery.ONCHAIN_UNCONSTRAINED, ); - // Regardless of the original content size, the log is padded with random bytes up to - // `PRIVATE_LOG_SIZE_IN_FIELDS` to prevent leaking information about the actual size. - let length = encrypted_log.len(); - context.emit_private_log(encrypted_log, length); let partial_note = PartialNFTNote { commitment }; @@ -196,11 +204,12 @@ impl PartialNFTNote { // We need to do two things: // - emit a public log containing the public fields (the storage slot and token id). The contract will later - // find it by searching for the expected tag (which is simply the partial note commitment). + // find it by searching for the domain-separated commitment as the tag. // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. - context.emit_public_log(self.compute_note_completion_log(storage_slot, token_id)); + let log_tag = compute_log_tag(self.commitment, DOM_SEP__NOTE_COMPLETION_LOG_TAG); + context.emit_public_log_unsafe(log_tag, [storage_slot, token_id]); context.push_note_hash(self.compute_complete_note_hash(storage_slot, token_id)); } @@ -216,13 +225,6 @@ impl PartialNFTNote { ) } - fn compute_note_completion_log(self, storage_slot: Field, token_id: Field) -> [Field; 3] { - // The first field of this log must be the tag that the recipient of the partial note private field logs - // expects, which is equal to the partial note commitment. The storage slot is included as the first public - // value so that note discovery can extract it. - [self.commitment, storage_slot, token_id] - } - fn compute_complete_note_hash(self, storage_slot: Field, token_id: Field) -> Field { // Here we finalize the note hash by including the (public) storage slot and token id into the partial note // commitment. Note that we use the same separator as we used for the first round of poseidon - this is not diff --git a/noir-projects/noir-contracts/contracts/test/avm_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/avm_test_contract/src/main.nr index 841584bdd2ac..820e24452a80 100644 --- a/noir-projects/noir-contracts/contracts/test/avm_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/avm_test_contract/src/main.nr @@ -605,15 +605,18 @@ pub contract AvmTest { #[contract_library_method] fn _emit_public_log(context: PublicContext) { - context.emit_public_log(/*message=*/ [10, 20, 30]); - context.emit_public_log(/*message=*/ "Hello, world!"); + context.emit_public_log_unsafe(0, /*message=*/ [10, 20, 30]); + context.emit_public_log_unsafe(0, /*message=*/ "Hello, world!"); let s: CompressedString<2, 44> = CompressedString::from_string("A long time ago, in a galaxy far far away..."); - context.emit_public_log(/*message=*/ s); - context.emit_public_log(/*message=*/ [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, - ]); // Large log + context.emit_public_log_unsafe(0, /*message=*/ s); + context.emit_public_log_unsafe( + 0, + /*message=*/ [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + ], + ); // Large log } #[external("public")] @@ -684,7 +687,7 @@ pub contract AvmTest { #[external("public")] fn n_new_public_logs(num: u32) { for i in 0..num { - self.context.emit_public_log(/*message=*/ [i as Field]); + self.context.emit_public_log_unsafe(0, /*message=*/ [i as Field]); } } diff --git a/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr index 24528824843c..a2544bf9907b 100644 --- a/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr @@ -11,7 +11,19 @@ pub contract Benchmarking { macros::{functions::external, storage::storage}, messages::message_delivery::MessageDelivery, note::note_getter_options::NoteGetterOptions, +<<<<<<< HEAD protocol::address::AztecAddress, +======= + oracle::random::random, + protocol::{ + address::{AztecAddress, EthAddress}, + constants::{ + CONTRACT_CLASS_LOG_SIZE_IN_FIELDS, MAX_L2_TO_L1_MSGS_PER_CALL, + MAX_NOTE_HASHES_PER_CALL, MAX_NULLIFIERS_PER_CALL, MAX_PRIVATE_LOGS_PER_CALL, + PRIVATE_LOG_CIPHERTEXT_LEN, + }, + }, +>>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) state_vars::{Map, Owned, PrivateSet, PublicMutable}, }; use field_note::FieldNote; @@ -53,7 +65,7 @@ pub contract Benchmarking { // Emits a public log. #[external("public")] fn broadcast(owner: AztecAddress) { - self.context.emit_public_log(self.storage.balances.at(owner).read()); + self.context.emit_public_log_unsafe(0, self.storage.balances.at(owner).read()); } // Does a bunch of heavy compute @@ -61,4 +73,71 @@ pub contract Benchmarking { fn sha256_hash_1024(data: [u8; 1024]) -> [u8; 32] { sha256::sha256_var(data, data.len()) } +<<<<<<< HEAD +======= + + // Lightest possible private transaction: empty app circuit, no state changes, no public calls. + #[external("private")] + fn noop() {} + + #[external("private")] + fn emit_nullifiers() { + // Safety: Benchmarking code + let random_seed = unsafe { random() }; + for i in 0..MAX_NULLIFIERS_PER_CALL { + self.context.push_nullifier(random_seed + (i as Field)); + } + } + + #[external("private")] + fn emit_note_hashes() { + // Safety: Benchmarking code + let random_seed = unsafe { random() }; + + for i in 0..MAX_NOTE_HASHES_PER_CALL { + self.context.push_note_hash(random_seed + (i as Field)); + } + } + + #[external("private")] + fn emit_l2_to_l1_msgs() { + // Safety: Benchmarking code + let random_seed = unsafe { random() }; + + for i in 0..MAX_L2_TO_L1_MSGS_PER_CALL { + let recipient = EthAddress::from_field((random_seed as u128) as Field + (i as Field)); + self.context.message_portal(recipient, random_seed + (i + 1) as Field); + } + } + + #[external("private")] + fn emit_private_logs() { + // Safety: Benchmarking code + let random_seed = unsafe { random() }; + + for i in 0..MAX_PRIVATE_LOGS_PER_CALL { + let mut log = [0; PRIVATE_LOG_CIPHERTEXT_LEN]; + for j in 0..PRIVATE_LOG_CIPHERTEXT_LEN { + log[j] = random_seed + (i * MAX_PRIVATE_LOGS_PER_CALL + j) as Field; + } + self.context.emit_private_log_unsafe(0, log, PRIVATE_LOG_CIPHERTEXT_LEN); + } + } + + #[external("private")] + fn emit_contract_class_log() { + // Safety: Benchmarking code + let random_seed = unsafe { random() }; + + let mut log = [0; CONTRACT_CLASS_LOG_SIZE_IN_FIELDS]; + for i in 0..log.len() { + log[i] = random_seed + (i as Field); + } + self.context.emit_contract_class_log(log); + } + + // Lightest possible private transaction: empty app circuit, no state changes, no public calls. + #[external("public")] + fn noop_pub() {} +>>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) } diff --git a/noir-projects/noir-contracts/contracts/test/child_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/child_contract/src/main.nr index 1a5ae2c167e3..d8ed6010ae59 100644 --- a/noir-projects/noir-contracts/contracts/test/child_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/child_contract/src/main.nr @@ -42,7 +42,7 @@ pub contract Child { #[external("public")] fn pub_set_value(new_value: Field) -> Field { self.storage.current_value.write(new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } @@ -71,7 +71,7 @@ pub contract Child { fn pub_inc_value(new_value: Field) -> Field { let old_value = self.storage.current_value.read(); self.storage.current_value.write(old_value + new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } @@ -80,13 +80,13 @@ pub contract Child { fn set_value_twice_with_nested_first() { let _result = self.call_self.pub_set_value(10); self.storage.current_value.write(20); - self.context.emit_public_log(20); + self.context.emit_public_log_unsafe(0, 20); } #[external("public")] fn set_value_twice_with_nested_last() { self.storage.current_value.write(20); - self.context.emit_public_log(20); + self.context.emit_public_log_unsafe(0, 20); let _result = self.call_self.pub_set_value(10); } @@ -95,6 +95,6 @@ pub contract Child { self.call_self.set_value_twice_with_nested_first(); self.call_self.set_value_twice_with_nested_last(); self.storage.current_value.write(20); - self.context.emit_public_log(20); + self.context.emit_public_log_unsafe(0, 20); } } diff --git a/noir-projects/noir-contracts/contracts/test/no_constructor_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/no_constructor_contract/src/main.nr index 9395ae288ea1..b62c2c8ab851 100644 --- a/noir-projects/noir-contracts/contracts/test/no_constructor_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/no_constructor_contract/src/main.nr @@ -20,8 +20,8 @@ pub contract NoConstructor { /// Arbitrary public method used to test that publishing for public execution works for a contract with no constructor. #[external("public")] - fn emit_public(value: Field) { - self.context.emit_public_log(/*message=*/ value); + fn emit_public(tag: Field, value: Field) { + self.context.emit_public_log_unsafe(tag, /*message=*/ value); } /// Arbitrary function used to test that we can call private functions on a contract with diff --git a/noir-projects/noir-contracts/contracts/test/static_child_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/static_child_contract/src/main.nr index 2ec353639904..aea7f59909d1 100644 --- a/noir-projects/noir-contracts/contracts/test/static_child_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/static_child_contract/src/main.nr @@ -37,7 +37,7 @@ pub contract StaticChild { #[external("public")] fn pub_set_value(new_value: Field) -> Field { self.storage.current_value.write(new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } @@ -79,7 +79,7 @@ pub contract StaticChild { fn pub_inc_value(new_value: Field) -> Field { let old_value = self.storage.current_value.read(); self.storage.current_value.write(old_value + new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } @@ -89,7 +89,7 @@ pub contract StaticChild { fn pub_illegal_inc_value(new_value: Field) -> Field { let old_value = self.storage.current_value.read(); self.storage.current_value.write(old_value + new_value); - self.context.emit_public_log(new_value); + self.context.emit_public_log_unsafe(0, new_value); new_value } } diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr index cbd0b9eba3c5..a03874828045 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr @@ -31,7 +31,11 @@ pub contract Test { protocol::{ abis::function_selector::FunctionSelector, address::{AztecAddress, EthAddress}, +<<<<<<< HEAD constants::PRIVATE_LOG_SIZE_IN_FIELDS, +======= + constants::{PRIVATE_LOG_CIPHERTEXT_LEN, MAX_PUBLIC_LOG_SIZE_IN_FIELDS}, +>>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) traits::{Hash, Packable, Serialize}, }, // Event related @@ -325,7 +329,12 @@ pub contract Test { // For testing non-note encrypted logs #[external("private")] - fn emit_array_as_encrypted_log(fields: [Field; 5], owner: AztecAddress, nest: bool) { + fn emit_array_as_encrypted_log( + tag: Field, + fields: [Field; 5], + owner: AztecAddress, + nest: bool, + ) { let event = ExampleEvent { value0: fields[0], value1: fields[1], @@ -339,21 +348,32 @@ pub contract Test { // this contract has reached max number of functions, so using this one fn // to test nested and non nested encrypted logs if nest { - self.call_self.emit_array_as_encrypted_log([0, 0, 0, 0, 0], owner, false); + self.call_self.emit_array_as_encrypted_log(tag, [0, 0, 0, 0, 0], owner, false); // Emit a log with non-encrypted content for testing purpose. - let leaky_log = event.serialize().concat([0; PRIVATE_LOG_SIZE_IN_FIELDS - 5]); - self.context.emit_private_log(leaky_log, 5); + let leaky_log = event.serialize().concat([0; PRIVATE_LOG_CIPHERTEXT_LEN - 5]); + self.context.emit_private_log_unsafe(tag, leaky_log, 5); } } #[external("public")] fn emit_public(value: Field) { - self.context.emit_public_log(/*message=*/ value); - self.context.emit_public_log(/*message=*/ [10, 20, 30]); - self.context.emit_public_log(/*message=*/ "Hello, world!"); + self.context.emit_public_log_unsafe(0, /*message=*/ value); + self.context.emit_public_log_unsafe(0, /*message=*/ [10, 20, 30]); + self.context.emit_public_log_unsafe(0, /*message=*/ "Hello, world!"); + } + +<<<<<<< HEAD +======= + #[external("public")] + fn emit_full_size_public_log(value_offset: Field) { + // -1 because emit_public_log_unsafe prepends the tag, so the emitted log is N + 1 fields. + let log_fields = + [0; MAX_PUBLIC_LOG_SIZE_IN_FIELDS - 1].mapi(|i, _| value_offset + i as Field); + self.context.emit_public_log_unsafe(0, log_fields); } +>>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) #[external("private")] fn consume_mint_to_private_message( amount: u128, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 5fcb449b6088..9c0ddc78d254 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -674,6 +674,12 @@ pub global DOM_SEP__SILOED_NULLIFIER: u32 = 57496191; /// This is not technically a protocol constant as message nullifiers are computed by each contract. pub global DOM_SEP__MESSAGE_NULLIFIER: u32 = 3754509616; +/// Domain separator for event log tags. Used by [`crate::hash::compute_log_tag`]. +pub global DOM_SEP__EVENT_LOG_TAG: u32 = 926040838; +/// Domain separator for partial note completion log tags. Used by [`crate::hash::compute_log_tag`]. +pub global DOM_SEP__NOTE_COMPLETION_LOG_TAG: u32 = 3372669888; +/// Domain separator for unconstrained message delivery log tags. Used by [`crate::hash::compute_log_tag`]. +pub global DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG: u32 = 1485357192; /// Domain separator for private log tags. /// /// Used by [`crate::hash::compute_siloed_private_log_first_field`]. diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr index a49fc51fdba8..8ade1f4e9cbb 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr @@ -9,8 +9,9 @@ use crate::{ DOM_SEP__APP_SILOED_ECDH_SHARED_SECRET, DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, DOM_SEP__AUTHWIT_OUTER, DOM_SEP__BLOCK_HEADER_HASH, DOM_SEP__CONTRACT_ADDRESS_V1, DOM_SEP__CONTRACT_CLASS_ID, DOM_SEP__ECDH_FIELD_MASK, DOM_SEP__ECDH_SUBKEY, - DOM_SEP__EVENT_COMMITMENT, DOM_SEP__FUNCTION_ARGS, DOM_SEP__INITIALIZATION_NULLIFIER, - DOM_SEP__INITIALIZER, DOM_SEP__IVSK_M, DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M, + DOM_SEP__EVENT_COMMITMENT, DOM_SEP__EVENT_LOG_TAG, DOM_SEP__FUNCTION_ARGS, + DOM_SEP__INITIALIZATION_NULLIFIER, DOM_SEP__INITIALIZER, DOM_SEP__IVSK_M, + DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M, DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M, DOM_SEP__PARTIAL_ADDRESS, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, DOM_SEP__PRIVATE_FUNCTION_LEAF, DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, @@ -20,8 +21,9 @@ use crate::{ DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH, DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, DOM_SEP__SILOED_NOTE_HASH, DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, DOM_SEP__TSK_M, - DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST, DOM_SEP__UNIQUE_NOTE_HASH, - NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS, TX_START_PREFIX, + DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, + DOM_SEP__UNIQUE_NOTE_HASH, NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS, + TX_START_PREFIX, }, hash::poseidon2_hash_bytes, traits::{FromField, ToField}, @@ -131,7 +133,7 @@ impl HashedValueTester::new(); + let mut tester = HashedValueTester::<56, 49>::new(); // ----------------- // Domain separators @@ -155,6 +157,15 @@ fn hashed_values_match_derived() { DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, "public_storage_map_slot", ); + tester.assert_dom_sep_matches_derived(DOM_SEP__EVENT_LOG_TAG, "event_log_tag"); + tester.assert_dom_sep_matches_derived( + DOM_SEP__NOTE_COMPLETION_LOG_TAG, + "note_completion_log_tag", + ); + tester.assert_dom_sep_matches_derived( + DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, + "unconstrained_msg_log_tag", + ); tester.assert_dom_sep_matches_derived(DOM_SEP__MESSAGE_NULLIFIER, "message_nullifier"); tester.assert_dom_sep_matches_derived(DOM_SEP__PRIVATE_FUNCTION_LEAF, "private_function_leaf"); tester.assert_dom_sep_matches_derived(DOM_SEP__PUBLIC_BYTECODE, "public_bytecode"); diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr index bdaca9ae4dd9..85cfcb3c3fb5 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr @@ -133,6 +133,10 @@ pub fn create_protocol_nullifier(tx_request: TxRequest) -> Scoped Field { + poseidon2_hash_with_separator([raw_tag], dom_sep) +} + pub fn compute_siloed_private_log_first_field( contract_address: AztecAddress, field: Field, diff --git a/yarn-project/aztec.js/src/api/events.ts b/yarn-project/aztec.js/src/api/events.ts index a683718a8ee8..9e616c0822f9 100644 --- a/yarn-project/aztec.js/src/api/events.ts +++ b/yarn-project/aztec.js/src/api/events.ts @@ -1,4 +1,6 @@ -import { type EventMetadataDefinition, EventSelector, decodeFromAbi } from '@aztec/stdlib/abi'; +import { DomainSeparator } from '@aztec/constants'; +import { type EventMetadataDefinition, decodeFromAbi } from '@aztec/stdlib/abi'; +import { computeLogTag } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { PublicEvent, PublicEventFilter } from '../wallet/wallet.js'; @@ -28,27 +30,26 @@ export async function getPublicEvents( eventMetadataDef: EventMetadataDefinition, filter: PublicEventFilter, ): Promise> { + // Public events are tagged with a domain-separated hash of their event type ID, so we compute + // the same hash here to filter for logs of the requested event type. + const logTag = await computeLogTag(eventMetadataDef.eventSelector.toField(), DomainSeparator.EVENT_LOG_TAG); + const { logs, maxLogsHit } = await node.getPublicLogs({ fromBlock: filter.fromBlock ? Number(filter.fromBlock) : undefined, toBlock: filter.toBlock ? Number(filter.toBlock) : undefined, txHash: filter.txHash, contractAddress: filter.contractAddress, afterLog: filter.afterLog, + tag: logTag, }); const events: PublicEvent[] = []; for (const log of logs) { - const logFields = log.log.getEmittedFields(); - // Event selector is at the last position of the emitted fields - const logEventSelector = EventSelector.fromField(logFields[logFields.length - 1]); - - if (!logEventSelector.equals(eventMetadataDef.eventSelector)) { - continue; - } + const logFieldsWithoutTag = log.log.getEmittedFieldsWithoutTag(); events.push({ - event: decodeFromAbi([eventMetadataDef.abiType], log.log.fields) as T, + event: decodeFromAbi([eventMetadataDef.abiType], logFieldsWithoutTag) as T, metadata: { l2BlockNumber: log.id.blockNumber, l2BlockHash: log.id.blockHash, diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index f274465c1998..ac64c9ae2693 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -510,7 +510,13 @@ export enum DomainSeparator { SILOED_NOTE_HASH = 3361878420, NOTE_NULLIFIER = 50789342, MESSAGE_NULLIFIER = 3754509616, +<<<<<<< HEAD SILOED_NULLIFIER = 57496191, +======= + EVENT_LOG_TAG = 926040838, + NOTE_COMPLETION_LOG_TAG = 3372669888, + UNCONSTRAINED_MSG_LOG_TAG = 1485357192, +>>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) PRIVATE_LOG_FIRST_FIELD = 2769976252, PUBLIC_LEAF_SLOT = 1247650290, PUBLIC_STORAGE_MAP_SLOT = 4015149901, 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 40b2e4ea307f..f5edd5eeaadc 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 @@ -427,7 +427,8 @@ describe('e2e_block_building', () => { // call test contract const valuesAsArray = Object.values(values); - const action = testContract.methods.emit_array_as_encrypted_log(valuesAsArray, ownerAddress, true); + const tag = 42n; + const action = testContract.methods.emit_array_as_encrypted_log(tag, valuesAsArray, ownerAddress, true); const tx = await proveInteraction(wallet, action, { from: ownerAddress }); const rct = await tx.send(); @@ -448,14 +449,12 @@ describe('e2e_block_building', () => { expect(events[1].event).toEqual(nestedValues); // The last log is not encrypted. - // The first field is the first value and is siloed with contract address by the kernel circuit. - const expectedFirstField = await computeSiloedPrivateLogFirstField( - testContract.address, - new Fr(valuesAsArray[0]), - ); - expect(privateLogs[2].fields.slice(0, 5).map((f: Fr) => f.toBigInt())).toEqual([ - expectedFirstField.toBigInt(), - ...valuesAsArray.slice(1), + // fields[0] is the tag, siloed with the contract address by the kernel circuit. + // The payload starts at fields[1]. + const expectedSiloedTag = await computeSiloedPrivateLogFirstField(testContract.address, new Fr(tag)); + expect(privateLogs[2].fields.slice(0, 6).map((f: Fr) => f.toBigInt())).toEqual([ + expectedSiloedTag.toBigInt(), + ...valuesAsArray, ]); }, 60_000); }); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts index 601be528a2ca..181302b58961 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts @@ -139,11 +139,14 @@ describe('e2e_deploy_contract deploy method', () => { it('publicly deploys a contract with no constructor', async () => { logger.debug(`Deploying contract with no constructor`); const { contract } = await NoConstructorContract.deploy(wallet).send({ from: defaultAccountAddress }); + const arbitraryTag = 99; const arbitraryValue = 42; logger.debug(`Call a public function to check that it was publicly deployed`); - const { receipt } = await contract.methods.emit_public(arbitraryValue).send({ from: defaultAccountAddress }); + const { receipt } = await contract.methods + .emit_public(arbitraryTag, arbitraryValue) + .send({ from: defaultAccountAddress }); const logs = await aztecNode.getPublicLogs({ txHash: receipt.txHash }); - expect(logs.logs[0].log.getEmittedFields()).toEqual([new Fr(arbitraryValue)]); + expect(logs.logs[0].log.getEmittedFields()).toEqual([new Fr(arbitraryTag), new Fr(arbitraryValue)]); }); it('refuses to deploy a contract with no constructor and no public deployment', async () => { diff --git a/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts b/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts index 38d8ce4ee3f4..c46a88a5954d 100644 --- a/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts +++ b/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts @@ -60,6 +60,7 @@ describe('e2e_multiple_blobs', () => { contract.methods.emit_nullifier(Fr.random()), contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(Fr.random(), EthAddress.random()), contract.methods.emit_array_as_encrypted_log( + 0n, Array.from({ length: 5 }).map(() => Fr.random()), defaultAccountAddress, true, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 96f947d177f5..ceb572d541a8 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -497,7 +497,7 @@ describe('Private Execution test suite', () => { describe('no constructor', () => { it('emits a field array as an encrypted log', async () => { - const args = [times(5, () => Fr.random()), owner, false]; + const args = [Fr.ZERO, times(5, () => Fr.random()), owner, false]; const result = await runSimulator({ artifact: TestContractArtifact, functionName: 'emit_array_as_encrypted_log', diff --git a/yarn-project/simulator/src/public/avm/avm_simulator.test.ts b/yarn-project/simulator/src/public/avm/avm_simulator.test.ts index eaeb53080746..a9354bfe30bf 100644 --- a/yarn-project/simulator/src/public/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/public/avm/avm_simulator.test.ts @@ -766,6 +766,8 @@ describe('AVM simulator: transpiled Noir contracts', () => { const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); + // emit_public_log_unsafe prepends a tag at fields[0]. The test contract passes 0 as the tag. + const withTag = (fields: Fr[]) => [Fr.ZERO, ...fields]; const expectedFields = [new Fr(10), new Fr(20), new Fr(30)]; const expectedString = 'Hello, world!'.split('').map(c => new Fr(c.charCodeAt(0))); const expectedCompressedString = [ @@ -775,10 +777,10 @@ describe('AVM simulator: transpiled Noir contracts', () => { const expectedLargeLog = Array.from({ length: 42 }, (_, i) => new Fr(i + 1)); expect(trace.tracePublicLog).toHaveBeenCalledTimes(4); - expect(trace.tracePublicLog).toHaveBeenCalledWith(address, expectedFields); - expect(trace.tracePublicLog).toHaveBeenCalledWith(address, expectedString); - expect(trace.tracePublicLog).toHaveBeenCalledWith(address, expectedCompressedString); - expect(trace.tracePublicLog).toHaveBeenCalledWith(address, expectedLargeLog); + expect(trace.tracePublicLog).toHaveBeenCalledWith(address, withTag(expectedFields)); + expect(trace.tracePublicLog).toHaveBeenCalledWith(address, withTag(expectedString)); + expect(trace.tracePublicLog).toHaveBeenCalledWith(address, withTag(expectedCompressedString)); + expect(trace.tracePublicLog).toHaveBeenCalledWith(address, withTag(expectedLargeLog)); }); }); diff --git a/yarn-project/stdlib/src/hash/hash.ts b/yarn-project/stdlib/src/hash/hash.ts index afc80031b1a4..5b5b882df57c 100644 --- a/yarn-project/stdlib/src/hash/hash.ts +++ b/yarn-project/stdlib/src/hash/hash.ts @@ -98,6 +98,11 @@ export function computeProtocolNullifier(txRequestHash: Fr): Promise { return siloNullifier(AztecAddress.fromBigInt(NULL_MSG_SENDER_CONTRACT_ADDRESS), txRequestHash); } +/** Domain-separates a raw log tag with the given domain separator. */ +export function computeLogTag(rawTag: number | bigint | boolean | Fr | Buffer, domSep: DomainSeparator): Promise { + return poseidon2HashWithSeparator([new Fr(rawTag)], domSep); +} + export function computeSiloedPrivateLogFirstField(contract: AztecAddress, field: Fr): Promise { return poseidon2HashWithSeparator([contract, field], DomainSeparator.PRIVATE_LOG_FIRST_FIELD); } diff --git a/yarn-project/stdlib/src/logs/siloed_tag.ts b/yarn-project/stdlib/src/logs/siloed_tag.ts index 0710d3e91fe7..a51b6c643989 100644 --- a/yarn-project/stdlib/src/logs/siloed_tag.ts +++ b/yarn-project/stdlib/src/logs/siloed_tag.ts @@ -1,8 +1,9 @@ +import { DomainSeparator } from '@aztec/constants'; import type { Fr } from '@aztec/foundation/curves/bn254'; import type { ZodFor } from '@aztec/foundation/schemas'; import type { AztecAddress } from '../aztec-address/index.js'; -import { computeSiloedPrivateLogFirstField } from '../hash/hash.js'; +import { computeLogTag, computeSiloedPrivateLogFirstField } from '../hash/hash.js'; import { schemas } from '../schemas/schemas.js'; import type { PreTag } from './pre_tag.js'; import { Tag } from './tag.js'; @@ -24,9 +25,13 @@ export class SiloedTag { static async compute(preTag: PreTag): Promise { const tag = await Tag.compute(preTag); - return SiloedTag.computeFromTagAndApp(tag, preTag.extendedSecret.app); + const logTag = await computeLogTag(tag.value, DomainSeparator.UNCONSTRAINED_MSG_LOG_TAG); + return SiloedTag.computeFromTagAndApp(new Tag(logTag), preTag.extendedSecret.app); } + /** + * Unlike `compute`, this expects a tag whose value is already domain-separated. + */ static async computeFromTagAndApp(tag: Tag, app: AztecAddress): Promise { const siloedTag = await computeSiloedPrivateLogFirstField(app, tag.value); return new SiloedTag(siloedTag); From df9eda1328ed52c86d99d4abf2bf0d5e1d8e846f Mon Sep 17 00:00:00 2001 From: AztecBot Date: Thu, 26 Mar 2026 00:21:36 +0000 Subject: [PATCH 22/22] fix: resolve cherry-pick conflicts Resolved conflicts in 3 files: - benchmarking_contract: took incoming imports and new benchmark functions - test_contract: took incoming constants (PRIVATE_LOG_CIPHERTEXT_LEN, MAX_PUBLIC_LOG_SIZE_IN_FIELDS) and new emit_full_size_public_log function - constants.gen.ts: kept both SILOED_NULLIFIER (HEAD) and new log tag domain separators (incoming) --- .../contracts/test/benchmarking_contract/src/main.nr | 7 ------- .../contracts/test/test_contract/src/main.nr | 7 ------- yarn-project/constants/src/constants.gen.ts | 3 --- 3 files changed, 17 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr index a2544bf9907b..42ad349a3889 100644 --- a/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/benchmarking_contract/src/main.nr @@ -11,9 +11,6 @@ pub contract Benchmarking { macros::{functions::external, storage::storage}, messages::message_delivery::MessageDelivery, note::note_getter_options::NoteGetterOptions, -<<<<<<< HEAD - protocol::address::AztecAddress, -======= oracle::random::random, protocol::{ address::{AztecAddress, EthAddress}, @@ -23,7 +20,6 @@ pub contract Benchmarking { PRIVATE_LOG_CIPHERTEXT_LEN, }, }, ->>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) state_vars::{Map, Owned, PrivateSet, PublicMutable}, }; use field_note::FieldNote; @@ -73,8 +69,6 @@ pub contract Benchmarking { fn sha256_hash_1024(data: [u8; 1024]) -> [u8; 32] { sha256::sha256_var(data, data.len()) } -<<<<<<< HEAD -======= // Lightest possible private transaction: empty app circuit, no state changes, no public calls. #[external("private")] @@ -139,5 +133,4 @@ pub contract Benchmarking { // Lightest possible private transaction: empty app circuit, no state changes, no public calls. #[external("public")] fn noop_pub() {} ->>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) } diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr index a03874828045..bc952c1943b0 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr @@ -31,11 +31,7 @@ pub contract Test { protocol::{ abis::function_selector::FunctionSelector, address::{AztecAddress, EthAddress}, -<<<<<<< HEAD - constants::PRIVATE_LOG_SIZE_IN_FIELDS, -======= constants::{PRIVATE_LOG_CIPHERTEXT_LEN, MAX_PUBLIC_LOG_SIZE_IN_FIELDS}, ->>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) traits::{Hash, Packable, Serialize}, }, // Event related @@ -363,8 +359,6 @@ pub contract Test { self.context.emit_public_log_unsafe(0, /*message=*/ "Hello, world!"); } -<<<<<<< HEAD -======= #[external("public")] fn emit_full_size_public_log(value_offset: Field) { // -1 because emit_public_log_unsafe prepends the tag, so the emitted log is N + 1 fields. @@ -373,7 +367,6 @@ pub contract Test { self.context.emit_public_log_unsafe(0, log_fields); } ->>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) #[external("private")] fn consume_mint_to_private_message( amount: u128, diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index ac64c9ae2693..3225586816bc 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -510,13 +510,10 @@ export enum DomainSeparator { SILOED_NOTE_HASH = 3361878420, NOTE_NULLIFIER = 50789342, MESSAGE_NULLIFIER = 3754509616, -<<<<<<< HEAD SILOED_NULLIFIER = 57496191, -======= EVENT_LOG_TAG = 926040838, NOTE_COMPLETION_LOG_TAG = 3372669888, UNCONSTRAINED_MSG_LOG_TAG = 1485357192, ->>>>>>> 8c88e11c0d (feat(aztec-nr)!: domain-separated tags on log emission (#21910)) PRIVATE_LOG_FIRST_FIELD = 2769976252, PUBLIC_LEAF_SLOT = 1247650290, PUBLIC_STORAGE_MAP_SLOT = 4015149901,