From 74e98d413b4135a1c84da9cef08ddb547a9c0bd5 Mon Sep 17 00:00:00 2001 From: esau <152162806+sklppy88@users.noreply.github.com> Date: Thu, 16 May 2024 11:42:40 +0200 Subject: [PATCH] feat: `npk_m_hash` in all notes + key rotation test (#6405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #6313 Resolves #6296 Resolves #5630 Have created #6417 to refactor the rotate call --------- Co-authored-by: Jan Beneš --- .../src/core/libraries/ConstantsGen.sol | 4 +- .../aztec-nr/address-note/src/address_note.nr | 23 +- .../aztec/src/context/private_context.nr | 56 ++--- .../aztec/src/encrypted_logs/incoming_body.nr | 5 +- noir-projects/aztec-nr/aztec/src/hash.nr | 2 +- .../aztec-nr/aztec/src/keys/getters.nr | 26 +- .../aztec-nr/aztec/src/note/lifecycle.nr | 6 +- .../aztec-nr/aztec/src/note/note_interface.nr | 3 +- noir-projects/aztec-nr/aztec/src/oracle.nr | 2 +- .../aztec-nr/aztec/src/oracle/keys.nr | 22 -- .../aztec/src/oracle/nullifier_key.nr | 52 ---- .../aztec/src/oracle/nullifier_keys.nr | 35 +++ .../aztec/src/state_vars/private_immutable.nr | 14 +- .../aztec/src/state_vars/private_mutable.nr | 31 ++- .../aztec/src/state_vars/private_set.nr | 15 +- .../src/easy_private_uint.nr | 23 +- .../aztec-nr/tests/src/mock/test_note.nr | 7 +- .../aztec-nr/value-note/src/utils.nr | 42 +++- .../aztec-nr/value-note/src/value_note.nr | 24 +- .../app_subscription_contract/src/main.nr | 17 +- .../src/subscription_note.nr | 20 +- .../benchmarking_contract/src/main.nr | 4 +- .../contracts/card_game_contract/src/cards.nr | 35 ++- .../contracts/child_contract/src/main.nr | 18 +- .../contracts/claim_contract/src/main.nr | 8 +- .../crowdfunding_contract/src/main.nr | 14 +- .../delegated_on_contract/src/main.nr | 22 +- .../contracts/delegator_contract/src/main.nr | 17 +- .../docs_example_contract/src/main.nr | 50 ++-- .../docs_example_contract/src/options.nr | 24 +- .../src/types/card_note.nr | 21 +- .../easy_private_voting_contract/src/main.nr | 6 +- .../src/ecdsa_public_key_note.nr | 22 +- .../ecdsa_account_contract/src/main.nr | 8 +- .../contracts/escrow_contract/src/main.nr | 9 +- .../inclusion_proofs_contract/src/main.nr | 40 ++- .../pending_note_hashes_contract/src/main.nr | 25 +- .../schnorr_account_contract/src/main.nr | 8 +- .../src/public_key_note.nr | 19 +- .../stateful_test_contract/src/main.nr | 13 +- .../contracts/test_contract/src/main.nr | 19 +- .../contracts/test_contract/src/test_note.nr | 4 +- .../src/types/balances_map.nr | 20 +- .../src/types/token_note.nr | 38 ++- .../src/types/transparent_note.nr | 6 +- .../token_contract/src/types/balances_map.nr | 13 +- .../token_contract/src/types/token_note.nr | 17 +- .../src/types/transparent_note.nr | 6 +- .../types/src/abis/private_call_stack_item.nr | 2 +- .../src/abis/private_circuit_public_inputs.nr | 2 +- .../crates/types/src/constants.nr | 4 +- .../crates/types/src/grumpkin_point.nr | 8 +- .../aztec.js/src/wallet/base_wallet.ts | 5 +- .../circuit-types/src/interfaces/pxe.ts | 4 +- .../circuit-types/src/keys/key_store.ts | 19 +- yarn-project/circuits.js/src/constants.gen.ts | 4 +- yarn-project/circuits.js/src/keys/index.ts | 22 +- .../private_call_stack_item.test.ts.snap | 4 +- ...private_circuit_public_inputs.test.ts.snap | 4 +- .../src/e2e_crowdfunding_and_claim.test.ts | 3 +- .../src/e2e_delegate_calls/delegate.test.ts | 9 +- .../end-to-end/src/e2e_key_rotation.test.ts | 233 ++++++++++++++++++ yarn-project/foundation/src/fields/point.ts | 5 + .../key-store/src/test_key_store.test.ts | 93 ++++++- yarn-project/key-store/src/test_key_store.ts | 212 ++++++++++++---- .../pxe/src/database/kv_pxe_database.ts | 22 +- yarn-project/pxe/src/database/pxe_database.ts | 8 +- .../pxe/src/pxe_service/pxe_service.ts | 6 +- .../pxe/src/simulator_oracle/index.ts | 12 +- .../simulator/src/acvm/oracle/oracle.ts | 22 +- .../simulator/src/acvm/oracle/typed_oracle.ts | 4 +- .../simulator/src/client/db_oracle.ts | 14 +- .../src/client/private_execution.test.ts | 55 +++-- .../simulator/src/client/simulator.test.ts | 9 +- .../client/unconstrained_execution.test.ts | 6 +- .../simulator/src/client/view_data_oracle.ts | 18 +- 76 files changed, 1154 insertions(+), 570 deletions(-) delete mode 100644 noir-projects/aztec-nr/aztec/src/oracle/nullifier_key.nr create mode 100644 noir-projects/aztec-nr/aztec/src/oracle/nullifier_keys.nr create mode 100644 yarn-project/end-to-end/src/e2e_key_rotation.test.ts diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 6d7217fa8e6..3f5440263ac 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -25,7 +25,7 @@ library Constants { uint256 internal constant MAX_NOTE_HASH_READ_REQUESTS_PER_CALL = 32; uint256 internal constant MAX_NULLIFIER_READ_REQUESTS_PER_CALL = 2; uint256 internal constant MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_CALL = 2; - uint256 internal constant MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL = 1; + uint256 internal constant MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL = 16; uint256 internal constant MAX_ENCRYPTED_LOGS_PER_CALL = 4; uint256 internal constant MAX_UNENCRYPTED_LOGS_PER_CALL = 4; uint256 internal constant MAX_NEW_NOTE_HASHES_PER_TX = 64; @@ -38,7 +38,7 @@ library Constants { uint256 internal constant MAX_NOTE_HASH_READ_REQUESTS_PER_TX = 128; uint256 internal constant MAX_NULLIFIER_READ_REQUESTS_PER_TX = 8; uint256 internal constant MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX = 8; - uint256 internal constant MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX = 4; + uint256 internal constant MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX = 64; uint256 internal constant MAX_ENCRYPTED_LOGS_PER_TX = 8; uint256 internal constant MAX_UNENCRYPTED_LOGS_PER_TX = 8; uint256 internal constant NUM_ENCRYPTED_LOGS_HASHES_PER_TX = 1; diff --git a/noir-projects/aztec-nr/address-note/src/address_note.nr b/noir-projects/aztec-nr/address-note/src/address_note.nr index 002c56de934..8480c0cce7a 100644 --- a/noir-projects/aztec-nr/address-note/src/address_note.nr +++ b/noir-projects/aztec-nr/address-note/src/address_note.nr @@ -1,9 +1,10 @@ use dep::aztec::{ - keys::getters::get_ivpk_m, - protocol_types::{address::AztecAddress, traits::Empty, constants::GENERATOR_INDEX__NOTE_NULLIFIER}, + protocol_types::{ + address::AztecAddress, traits::Empty, constants::GENERATOR_INDEX__NOTE_NULLIFIER, + grumpkin_point::GrumpkinPoint, hash::poseidon2_hash +}, note::{note_header::NoteHeader, note_interface::NoteInterface, utils::compute_note_hash_for_consumption}, - oracle::{unsafe_rand::unsafe_rand, nullifier_key::get_app_nullifier_secret_key}, - context::PrivateContext, hash::poseidon2_hash + oracle::unsafe_rand::unsafe_rand, keys::getters::get_nsk_app, context::PrivateContext }; global ADDRESS_NOTE_LEN: Field = 3; @@ -13,7 +14,8 @@ global ADDRESS_NOTE_LEN: Field = 3; #[aztec(note)] struct AddressNote { address: AztecAddress, - owner: AztecAddress, + // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. + npk_m_hash: Field, randomness: Field, } @@ -21,7 +23,7 @@ impl NoteInterface for AddressNote { fn compute_nullifier(self, context: &mut PrivateContext) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = context.request_app_nullifier_secret_key(self.owner); + let secret = context.request_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -31,7 +33,7 @@ impl NoteInterface for AddressNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = get_app_nullifier_secret_key(self.owner); + let secret = get_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -40,8 +42,7 @@ impl NoteInterface for AddressNote { } // Broadcasts the note as an encrypted log on L1. - fn broadcast(self, context: &mut PrivateContext, slot: Field) { - let ivpk_m = get_ivpk_m(context, self.owner); + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { // docs:start:encrypted context.emit_encrypted_log( (*context).this_address(), @@ -55,9 +56,9 @@ impl NoteInterface for AddressNote { } impl AddressNote { - pub fn new(address: AztecAddress, owner: AztecAddress) -> Self { + pub fn new(address: AztecAddress, npk_m_hash: Field) -> Self { let randomness = unsafe_rand(); - AddressNote { address, owner, randomness, header: NoteHeader::empty() } + AddressNote { address, npk_m_hash, randomness, header: NoteHeader::empty() } } // docs:end:address_note_def } 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 9768e71fcf0..ce7987cf015 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -1,11 +1,11 @@ use crate::{ context::{inputs::PrivateContextInputs, interface::ContextInterface}, - messaging::process_l1_to_l2_message, + keys::getters::get_nullifier_keys, messaging::process_l1_to_l2_message, hash::{hash_args_array, ArgsHasher, compute_encrypted_log_hash, compute_unencrypted_log_hash}, oracle::{ - arguments, returns, call_private_function::call_private_function_internal, header::get_header_at, + nullifier_keys::NullifierKeys, arguments, returns, + call_private_function::call_private_function_internal, header::get_header_at, logs::emit_encrypted_log, logs_traits::{LensForEncryptedLog, ToBytesForUnencryptedLog}, - nullifier_key::{get_nullifier_keys, get_nullifier_keys_with_npk_m_hash, NullifierKeys}, enqueue_public_function_call::{ enqueue_public_function_call_internal, set_public_teardown_function_call_internal, parse_public_call_stack_item_from_oracle @@ -31,7 +31,7 @@ use dep::protocol_types::{ contrakt::{storage_read::StorageRead, storage_update_request::StorageUpdateRequest}, grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint, header::Header, messaging::l2_to_l1_message::L2ToL1Message, utils::reader::Reader, - traits::{is_empty, Deserialize, Empty}, hash::poseidon2_hash + traits::{is_empty, Deserialize, Empty} }; // When finished, one can call .finish() to convert back to the abi @@ -203,47 +203,25 @@ impl PrivateContext { self.nullifier_read_requests.push(request); } - pub fn request_app_nullifier_secret_key(&mut self, account: AztecAddress) -> Field { - let keys = if self.nullifier_key.is_none() { - let keys = get_nullifier_keys(account); - let request = NullifierKeyValidationRequest { - master_nullifier_public_key: keys.master_nullifier_public_key, - app_nullifier_secret_key: keys.app_nullifier_secret_key - }; - self.nullifier_key_validation_requests.push(request); - self.nullifier_key = Option::some(keys); - keys - } else { - let keys = self.nullifier_key.unwrap_unchecked(); - // If MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL is larger than 1, need to update the way the key pair is cached. - assert(MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL == 1); - assert(keys.account == account, "Cannot query nullifier key for more than one account per call"); - keys - }; - keys.app_nullifier_secret_key - } + pub fn request_nsk_app(&mut self, npk_m_hash: Field) -> Field { + // A value of empty nullifier keys will fail the key validation request. + let cached_nullifier_keys = self.nullifier_key.unwrap_or(NullifierKeys::empty()); - // TODO(#5630) Replace request_app_nullifier_secret_key above with this once we no longer get app nullifier secret key with address - pub fn request_nsk_app_with_npk_m_hash(&mut self, npk_m_hash: Field) -> Field { - let keys = if self.nullifier_key.is_none() { - let keys = get_nullifier_keys_with_npk_m_hash(npk_m_hash); + let nullifier_keys = if cached_nullifier_keys.master_nullifier_public_key.hash() == npk_m_hash { + cached_nullifier_keys + } else { + let fetched_nullifier_keys = get_nullifier_keys(npk_m_hash); let request = NullifierKeyValidationRequest { - master_nullifier_public_key: keys.master_nullifier_public_key, - app_nullifier_secret_key: keys.app_nullifier_secret_key + master_nullifier_public_key: fetched_nullifier_keys.master_nullifier_public_key, + app_nullifier_secret_key: fetched_nullifier_keys.app_nullifier_secret_key }; self.nullifier_key_validation_requests.push(request); - self.nullifier_key = Option::some(keys); - keys - } else { - let keys = self.nullifier_key.unwrap_unchecked(); - // If MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL is larger than 1, need to update the way the key pair is cached. - assert(MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL == 1); - keys + self.nullifier_key = Option::some(fetched_nullifier_keys); + fetched_nullifier_keys }; - // We have to check if the key that was requested or cached corresponds to the one we request for - assert_eq(poseidon2_hash(keys.master_nullifier_public_key.serialize()), npk_m_hash); - keys.app_nullifier_secret_key + assert_eq(nullifier_keys.master_nullifier_public_key.hash(), npk_m_hash); + nullifier_keys.app_nullifier_secret_key } // docs:start:context_message_portal diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr index 1016e7f4d09..ee99daf8943 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/incoming_body.nr @@ -67,8 +67,7 @@ mod test { use crate::{ note::{note_header::NoteHeader, note_interface::NoteInterface, utils::compute_note_hash_for_consumption}, - oracle::{unsafe_rand::unsafe_rand, nullifier_key::get_app_nullifier_secret_key}, - context::PrivateContext, hash::poseidon2_hash + oracle::unsafe_rand::unsafe_rand, context::PrivateContext }; struct AddressNote { @@ -93,7 +92,7 @@ mod test { fn compute_nullifier_without_context(self) -> Field {1} - fn broadcast(self, context: &mut PrivateContext, slot: Field) {} + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) {} fn serialize_content(self) -> [Field; ADDRESS_NOTE_LEN] { [self.address.to_field(), self.owner.to_field(), self.randomness]} diff --git a/noir-projects/aztec-nr/aztec/src/hash.nr b/noir-projects/aztec-nr/aztec/src/hash.nr index 989ecfb0f31..f0abd4912e1 100644 --- a/noir-projects/aztec-nr/aztec/src/hash.nr +++ b/noir-projects/aztec-nr/aztec/src/hash.nr @@ -4,7 +4,7 @@ use dep::protocol_types::{ GENERATOR_INDEX__SECRET_HASH, GENERATOR_INDEX__MESSAGE_NULLIFIER, ARGS_HASH_CHUNK_COUNT, GENERATOR_INDEX__FUNCTION_ARGS, ARGS_HASH_CHUNK_LENGTH }, - traits::Hash, hash::{pedersen_hash, poseidon2_hash, silo_nullifier, sha256_to_field} + traits::Hash, hash::{pedersen_hash, silo_nullifier, sha256_to_field} }; use crate::oracle::logs_traits::{LensForEncryptedLog, ToBytesForUnencryptedLog}; diff --git a/noir-projects/aztec-nr/aztec/src/keys/getters.nr b/noir-projects/aztec-nr/aztec/src/keys/getters.nr index eda6fc0b6bd..1759d3a9e57 100644 --- a/noir-projects/aztec-nr/aztec/src/keys/getters.nr +++ b/noir-projects/aztec-nr/aztec/src/keys/getters.nr @@ -1,10 +1,10 @@ -use dep::protocol_types::{ - address::AztecAddress, constants::CANONICAL_KEY_REGISTRY_ADDRESS, grumpkin_point::GrumpkinPoint, - hash::poseidon2_hash -}; +use dep::protocol_types::{address::AztecAddress, constants::CANONICAL_KEY_REGISTRY_ADDRESS, grumpkin_point::GrumpkinPoint}; use crate::{ context::PrivateContext, - oracle::keys::{get_public_keys_and_partial_address, get_public_keys_and_partial_address_with_npk_m_hash}, + oracle::{ + keys::get_public_keys_and_partial_address, + nullifier_keys::{NullifierKeys, get_nullifier_keys as get_nullifier_keys_oracle} +}, keys::public_keys::{PublicKeys, NULLIFIER_INDEX, INCOMING_INDEX}, state_vars::{ map::derive_storage_slot_in_map, @@ -20,7 +20,7 @@ pub fn get_npk_m(context: &mut PrivateContext, address: AztecAddress) -> Grumpki } pub fn get_npk_m_hash(context: &mut PrivateContext, address: AztecAddress) -> Field { - poseidon2_hash(get_master_key(context, address, NULLIFIER_INDEX).serialize()) + get_master_key(context, address, NULLIFIER_INDEX).hash() } pub fn get_ivpk_m(context: &mut PrivateContext, address: AztecAddress) -> GrumpkinPoint { @@ -91,7 +91,15 @@ fn fetch_and_constrain_keys(address: AztecAddress) -> PublicKeys { public_keys } -pub fn get_ivpk_m_with_npk_m_hash(npk_m_hash: Field) -> GrumpkinPoint { - let result = get_public_keys_and_partial_address_with_npk_m_hash(npk_m_hash); - result.0.ivpk_m +// We get the full struct Nullifier Keys here +pub fn get_nullifier_keys(npk_m_hash: Field) -> NullifierKeys { + let nullifier_keys = get_nullifier_keys_oracle(npk_m_hash); + assert_eq(nullifier_keys.master_nullifier_public_key.hash(), npk_m_hash); + + nullifier_keys +} + +// We are only getting the app_nullifier_secret_key here +pub fn get_nsk_app(npk_m_hash: Field) -> Field { + get_nullifier_keys(npk_m_hash).app_nullifier_secret_key } diff --git a/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr b/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr index 537416c1068..af461ff44cb 100644 --- a/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr +++ b/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr @@ -1,3 +1,4 @@ +use dep::protocol_types::grumpkin_point::GrumpkinPoint; use crate::context::{PrivateContext, PublicContext}; use crate::note::{ note_header::NoteHeader, note_interface::NoteInterface, @@ -9,7 +10,8 @@ pub fn create_note( context: &mut PrivateContext, storage_slot: Field, note: &mut Note, - broadcast: bool + broadcast: bool, + ivpk_m: GrumpkinPoint ) where Note: NoteInterface { let contract_address = (*context).this_address(); @@ -36,7 +38,7 @@ pub fn create_note( context.push_new_note_hash(inner_note_hash); if broadcast { - Note::broadcast(*note, context, storage_slot); + Note::broadcast(*note, context, storage_slot, ivpk_m); } } diff --git a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr index 3eaf10c0b77..54b6783769a 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr @@ -1,5 +1,6 @@ use crate::context::PrivateContext; use crate::note::note_header::NoteHeader; +use dep::protocol_types::grumpkin_point::GrumpkinPoint; // docs:start:note_interface trait NoteInterface { @@ -7,7 +8,7 @@ trait NoteInterface { fn compute_nullifier_without_context(self) -> Field; - fn broadcast(self, context: &mut PrivateContext, slot: Field) -> (); + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) -> (); // Autogenerated by the #[aztec(note)] macro unless it is overridden by a custom implementation fn serialize_content(self) -> [Field; N]; diff --git a/noir-projects/aztec-nr/aztec/src/oracle.nr b/noir-projects/aztec-nr/aztec/src/oracle.nr index 59031fcdb9f..2d3cc6dd1f9 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle.nr @@ -11,7 +11,7 @@ mod get_nullifier_membership_witness; mod get_public_data_witness; mod get_membership_witness; mod keys; -mod nullifier_key; +mod nullifier_keys; mod get_sibling_path; mod unsafe_rand; mod enqueue_public_function_call; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/keys.nr b/noir-projects/aztec-nr/aztec/src/oracle/keys.nr index 451d9bbff63..c3c191b6ba5 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/keys.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/keys.nr @@ -22,25 +22,3 @@ fn get_public_keys_and_partial_address(address: AztecAddress) -> (PublicKeys, Pa (keys, partial_address) } - -#[oracle(getPublicKeysAndPartialAddressWithNpkMHash)] -fn get_public_keys_and_partial_address_with_npk_m_hash_oracle(_npk_m_hash: Field) -> [Field; 9] {} - -unconstrained fn get_public_keys_and_partial_address_with_npk_m_hash_oracle_wrapper(npk_m_hash: Field) -> [Field; 9] { - get_public_keys_and_partial_address_with_npk_m_hash_oracle(npk_m_hash) -} - -fn get_public_keys_and_partial_address_with_npk_m_hash(npk_m_hash: Field) -> (PublicKeys, PartialAddress) { - let result = get_public_keys_and_partial_address_with_npk_m_hash_oracle_wrapper(npk_m_hash); - - let keys = PublicKeys { - npk_m: GrumpkinPoint::new(result[0], result[1]), - ivpk_m: GrumpkinPoint::new(result[2], result[3]), - ovpk_m: GrumpkinPoint::new(result[4], result[5]), - tpk_m: GrumpkinPoint::new(result[6], result[7]) - }; - - let partial_address = PartialAddress::from_field(result[8]); - - (keys, partial_address) -} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/nullifier_key.nr b/noir-projects/aztec-nr/aztec/src/oracle/nullifier_key.nr deleted file mode 100644 index c15e36bc6db..00000000000 --- a/noir-projects/aztec-nr/aztec/src/oracle/nullifier_key.nr +++ /dev/null @@ -1,52 +0,0 @@ -use dep::protocol_types::{address::AztecAddress, grumpkin_point::GrumpkinPoint, grumpkin_private_key::GrumpkinPrivateKey}; - -// Nullifier keys pertaining to a specific account -struct NullifierKeys { - // TODO(#5630): Replace get_nullifier_keys above with this once we no longer get nullifier keys with address - account: AztecAddress, - master_nullifier_public_key: GrumpkinPoint, - app_nullifier_secret_key: Field, -} - -#[oracle(getNullifierKeys)] -fn get_nullifier_keys_oracle(_account: AztecAddress) -> [Field; 3] {} - -unconstrained fn get_nullifier_keys_internal(account: AztecAddress) -> NullifierKeys { - let result = get_nullifier_keys_oracle(account); - NullifierKeys { - account, - master_nullifier_public_key: GrumpkinPoint { x: result[0], y: result[1] }, - app_nullifier_secret_key: result[2] - } -} - -pub fn get_nullifier_keys(account: AztecAddress) -> NullifierKeys { - get_nullifier_keys_internal(account) -} - -pub fn get_app_nullifier_secret_key(account: AztecAddress) -> Field { - get_nullifier_keys_internal(account).app_nullifier_secret_key -} - -// TODO(#5630): Replace get_nullifier_keys above with this once we no longer get nullifier keys with address -#[oracle(getNullifierKeysWithNpkMHash)] -fn get_nullifier_keys_with_npk_m_hash_oracle(_npk_m_hash: Field) -> [Field; 3] {} - -unconstrained fn get_nullifier_keys_with_npk_m_hash_internal(npk_m_hash: Field) -> NullifierKeys { - let result = get_nullifier_keys_with_npk_m_hash_oracle(npk_m_hash); - NullifierKeys { - account: AztecAddress::zero(), - master_nullifier_public_key: GrumpkinPoint { x: result[0], y: result[1] }, - app_nullifier_secret_key: result[2] - } -} - -// We get the full struct Nullifier Keys here -pub fn get_nullifier_keys_with_npk_m_hash(npk_m_hash: Field) -> NullifierKeys { - get_nullifier_keys_with_npk_m_hash_internal(npk_m_hash) -} - -// We are only getting the app_nullifier_secret_key here -pub fn get_nsk_app_with_npk_m_hash(npk_m_hash: Field) -> Field { - get_nullifier_keys_with_npk_m_hash_internal(npk_m_hash).app_nullifier_secret_key -} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/nullifier_keys.nr b/noir-projects/aztec-nr/aztec/src/oracle/nullifier_keys.nr new file mode 100644 index 00000000000..1d8fd94052f --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/nullifier_keys.nr @@ -0,0 +1,35 @@ +use dep::protocol_types::{ + address::AztecAddress, traits::Empty, grumpkin_point::GrumpkinPoint, + grumpkin_private_key::GrumpkinPrivateKey +}; + +// Nullifier keys pertaining to a specific account +struct NullifierKeys { + master_nullifier_public_key: GrumpkinPoint, + app_nullifier_secret_key: Field, +} + +impl Empty for NullifierKeys { + fn empty() -> Self { + NullifierKeys { + master_nullifier_public_key: GrumpkinPoint::zero(), + app_nullifier_secret_key: 0 + } + } +} + +#[oracle(getNullifierKeys)] +fn get_nullifier_keys_oracle(_npk_m_hash: Field) -> [Field; 3] {} + +unconstrained fn get_nullifier_keys_internal(npk_m_hash: Field) -> NullifierKeys { + let result = get_nullifier_keys_oracle(npk_m_hash); + NullifierKeys { + master_nullifier_public_key: GrumpkinPoint { x: result[0], y: result[1] }, + app_nullifier_secret_key: result[2] + } +} + +// We get the full struct Nullifier Keys here +pub fn get_nullifier_keys(npk_m_hash: Field) -> NullifierKeys { + get_nullifier_keys_internal(npk_m_hash) +} diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr index 67a0268f58b..56c55a1384c 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr @@ -1,4 +1,7 @@ -use dep::protocol_types::{address::AztecAddress, constants::GENERATOR_INDEX__INITIALIZATION_NULLIFIER, hash::pedersen_hash}; +use dep::protocol_types::{ + address::AztecAddress, grumpkin_point::GrumpkinPoint, + constants::GENERATOR_INDEX__INITIALIZATION_NULLIFIER, hash::pedersen_hash +}; use crate::context::{PrivateContext, Context}; use crate::note::{ @@ -46,14 +49,19 @@ impl PrivateImmutable { // docs:end:is_initialized // docs:start:initialize - pub fn initialize(self, note: &mut Note, broadcast: bool) where Note: NoteInterface { + pub fn initialize( + self, + note: &mut Note, + broadcast: bool, + ivpk_m: GrumpkinPoint + ) where Note: NoteInterface { let context = self.context.unwrap(); // Nullify the storage slot. let nullifier = self.compute_initialization_nullifier(); context.push_new_nullifier(nullifier, 0); - create_note(context, self.storage_slot, note, broadcast); + create_note(context, self.storage_slot, note, broadcast, ivpk_m); } // docs:end:initialize diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr index 06942a2cbd0..a4908c37553 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr @@ -1,4 +1,7 @@ -use dep::protocol_types::{address::AztecAddress, constants::GENERATOR_INDEX__INITIALIZATION_NULLIFIER, hash::pedersen_hash}; +use dep::protocol_types::{ + address::AztecAddress, constants::GENERATOR_INDEX__INITIALIZATION_NULLIFIER, + grumpkin_point::GrumpkinPoint, hash::pedersen_hash +}; use crate::context::{PrivateContext, PublicContext, Context}; use crate::note::{ @@ -48,19 +51,29 @@ impl PrivateMutable { // docs:end:is_initialized // docs:start:initialize - pub fn initialize(self, note: &mut Note, broadcast: bool) where Note: NoteInterface { + pub fn initialize( + self, + note: &mut Note, + broadcast: bool, + ivpk_m: GrumpkinPoint + ) where Note: NoteInterface { let context = self.context.unwrap(); // Nullify the storage slot. let nullifier = self.compute_initialization_nullifier(); context.push_new_nullifier(nullifier, 0); - create_note(context, self.storage_slot, note, broadcast); + create_note(context, self.storage_slot, note, broadcast, ivpk_m); } // docs:end:initialize // docs:start:replace - pub fn replace(self, new_note: &mut Note, broadcast: bool) where Note: NoteInterface { + pub fn replace( + self, + new_note: &mut Note, + broadcast: bool, + ivpk_m: GrumpkinPoint + ) where Note: NoteInterface { let context = self.context.unwrap(); let prev_note: Note = get_note(context, self.storage_slot); @@ -68,12 +81,16 @@ impl PrivateMutable { destroy_note(context, prev_note); // Add replacement note. - create_note(context, self.storage_slot, new_note, broadcast); + create_note(context, self.storage_slot, new_note, broadcast, ivpk_m); } // docs:end:replace // docs:start:get_note - pub fn get_note(self, broadcast: bool) -> Note where Note: NoteInterface { + pub fn get_note( + self, + broadcast: bool, + ivpk_m: GrumpkinPoint + ) -> Note where Note: NoteInterface { let context = self.context.unwrap(); let mut note = get_note(context, self.storage_slot); @@ -82,7 +99,7 @@ impl PrivateMutable { // Add the same note again. // Because a nonce is added to every note in the kernel, its nullifier will be different. - create_note(context, self.storage_slot, &mut note, broadcast); + create_note(context, self.storage_slot, &mut note, broadcast, ivpk_m); note } diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr b/noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr index 716a6588355..012facf8591 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr @@ -1,4 +1,7 @@ -use dep::protocol_types::{constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, abis::read_request::ReadRequest}; +use dep::protocol_types::{ + constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, abis::read_request::ReadRequest, + grumpkin_point::GrumpkinPoint +}; use crate::context::{PrivateContext, PublicContext, Context}; use crate::note::{ constants::MAX_NOTES_PER_PAGE, lifecycle::{create_note, create_note_hash_from_public, destroy_note}, @@ -25,12 +28,18 @@ impl PrivateSet { } // docs:end:new // docs:start:insert - pub fn insert(self, note: &mut Note, broadcast: bool) where Note: NoteInterface { + pub fn insert( + self, + note: &mut Note, + broadcast: bool, + ivpk_m: GrumpkinPoint + ) where Note: NoteInterface { create_note( self.context.private.unwrap(), self.storage_slot, note, - broadcast + broadcast, + ivpk_m ); } // docs:end:insert diff --git a/noir-projects/aztec-nr/easy-private-state/src/easy_private_uint.nr b/noir-projects/aztec-nr/easy-private-state/src/easy_private_uint.nr index f6a4c9ebce2..af6a3609ea6 100644 --- a/noir-projects/aztec-nr/easy-private-state/src/easy_private_uint.nr +++ b/noir-projects/aztec-nr/easy-private-state/src/easy_private_uint.nr @@ -1,6 +1,7 @@ use dep::aztec::{ - protocol_types::address::AztecAddress, context::Context, - note::note_getter_options::NoteGetterOptions, state_vars::PrivateSet + protocol_types::{address::AztecAddress, grumpkin_point::GrumpkinPoint}, context::Context, + note::note_getter_options::NoteGetterOptions, state_vars::PrivateSet, + keys::getters::{get_npk_m_hash, get_ivpk_m} }; use dep::value_note::{filter::filter_notes_min_sum, value_note::ValueNote}; @@ -21,19 +22,26 @@ impl EasyPrivateUint { // Very similar to `value_note::utils::increment`. pub fn add(self, addend: u64, owner: AztecAddress) { assert(self.context.public.is_none(), "EasyPrivateUint::add can be called from private only."); + let context = self.context.private.unwrap(); + let owner_npk_m_hash = get_npk_m_hash(context, owner); + let owner_ivpk_m = get_ivpk_m(context, owner); // Creates new note for the owner. - let mut addend_note = ValueNote::new(addend as Field, owner); + let mut addend_note = ValueNote::new(addend as Field, owner_npk_m_hash); // Insert the new note to the owner's set of notes. // docs:start:insert - self.set.insert(&mut addend_note, true); + self.set.insert(&mut addend_note, true, owner_ivpk_m); // docs:end:insert } // Very similar to `value_note::utils::decrement`. pub fn sub(self, subtrahend: u64, owner: AztecAddress) { assert(self.context.public.is_none(), "EasyPrivateUint::sub can be called from private only."); + let context = self.context.private.unwrap(); + + let owner_npk_m_hash = get_npk_m_hash(context, owner); + let owner_ivpk_m = get_ivpk_m(context, owner); // docs:start:get_notes let options = NoteGetterOptions::with_filter(filter_notes_min_sum, subtrahend as Field); @@ -47,7 +55,8 @@ impl EasyPrivateUint { // Ensure the notes are actually owned by the owner (to prevent user from generating a valid proof while // spending someone else's notes). - assert(note.owner.eq(owner)); + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to pass this assert after rotating key + assert(note.npk_m_hash.eq(owner_npk_m_hash)); // Removes the note from the owner's set of notes. // docs:start:remove @@ -62,7 +71,7 @@ impl EasyPrivateUint { // Creates change note for the owner. let result_value = minuend - subtrahend; - let mut result_note = ValueNote::new(result_value as Field, owner); - self.set.insert(&mut result_note, result_value != 0); + let mut result_note = ValueNote::new(result_value as Field, owner_npk_m_hash); + self.set.insert(&mut result_note, result_value != 0, owner_ivpk_m); } } diff --git a/noir-projects/aztec-nr/tests/src/mock/test_note.nr b/noir-projects/aztec-nr/tests/src/mock/test_note.nr index 17f6d968d36..3aadf5fdd94 100644 --- a/noir-projects/aztec-nr/tests/src/mock/test_note.nr +++ b/noir-projects/aztec-nr/tests/src/mock/test_note.nr @@ -1,5 +1,8 @@ use dep::aztec::context::PrivateContext; -use dep::aztec::note::{note_header::NoteHeader, note_interface::NoteInterface}; +use dep::aztec::{ + note::{note_header::NoteHeader, note_interface::NoteInterface}, + protocol_types::grumpkin_point::GrumpkinPoint +}; global TEST_NOTE_LENGTH = 1; @@ -44,7 +47,7 @@ impl NoteInterface for TestNote { 0 } - fn broadcast(self, context: &mut PrivateContext, slot: Field) { + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { assert( false, "TestNote does not support broadcast." ); diff --git a/noir-projects/aztec-nr/value-note/src/utils.nr b/noir-projects/aztec-nr/value-note/src/utils.nr index 8d88fc6ef0a..80392c24b2f 100644 --- a/noir-projects/aztec-nr/value-note/src/utils.nr +++ b/noir-projects/aztec-nr/value-note/src/utils.nr @@ -1,5 +1,7 @@ use dep::aztec::prelude::{AztecAddress, PrivateContext, PrivateSet, NoteGetterOptions}; use dep::aztec::note::note_getter_options::SortOrder; +use dep::aztec::protocol_types::grumpkin_point::GrumpkinPoint; +use dep::aztec::keys::getters::{get_npk_m_hash, get_ivpk_m}; use crate::{filter::filter_notes_min_sum, value_note::{ValueNote, VALUE_NOTE_LEN}}; // Sort the note values (0th field) in descending order. @@ -10,18 +12,31 @@ pub fn create_note_getter_options_for_decreasing_balance(amount: Field) -> NoteG // Creates a new note for the recipient. // Inserts it to the recipient's set of notes. -pub fn increment(balance: PrivateSet, amount: Field, recipient: AztecAddress) { - let mut note = ValueNote::new(amount, recipient); +pub fn increment( + mut context: PrivateContext, + balance: PrivateSet, + amount: Field, + recipient: AztecAddress +) { + let recipient_npk_m_hash = get_npk_m_hash(&mut context, recipient); + let recipient_ivpk_m = get_ivpk_m(&mut context, recipient); + + let mut note = ValueNote::new(amount, recipient_npk_m_hash); // Insert the new note to the owner's set of notes and emit the log if value is non-zero. - balance.insert(&mut note, amount != 0); + balance.insert(&mut note, amount != 0, recipient_ivpk_m); } // Find some of the `owner`'s notes whose values add up to the `amount`. // Remove those notes. // If the value of the removed notes exceeds the requested `amount`, create a new note containing the excess value, so that exactly `amount` is removed. // Fail if the sum of the selected notes is less than the amount. -pub fn decrement(balance: PrivateSet, amount: Field, owner: AztecAddress) { - let sum = decrement_by_at_most(balance, amount, owner); +pub fn decrement( + mut context: PrivateContext, + balance: PrivateSet, + amount: Field, + owner: AztecAddress +) { + let sum = decrement_by_at_most(context, balance, amount, owner); assert(sum == amount, "Balance too low"); } @@ -34,6 +49,7 @@ pub fn decrement(balance: PrivateSet, amount: Field, owner: AztecAddr // // It returns the decremented amount, which should be less than or equal to max_amount. pub fn decrement_by_at_most( + mut context: PrivateContext, balance: PrivateSet, max_amount: Field, owner: AztecAddress @@ -44,7 +60,7 @@ pub fn decrement_by_at_most( let mut decremented = 0; for i in 0..opt_notes.len() { if opt_notes[i].is_some() { - decremented += destroy_note(balance, owner, opt_notes[i].unwrap_unchecked()); + decremented += destroy_note(context, balance, owner, opt_notes[i].unwrap_unchecked()); } } @@ -54,17 +70,25 @@ pub fn decrement_by_at_most( change_value = decremented - max_amount; decremented -= change_value; } - increment(balance, change_value, owner); + increment(context, balance, change_value, owner); decremented } // Removes the note from the owner's set of notes. // Returns the value of the destroyed note. -pub fn destroy_note(balance: PrivateSet, owner: AztecAddress, note: ValueNote) -> Field { +pub fn destroy_note( + mut context: PrivateContext, + balance: PrivateSet, + owner: AztecAddress, + note: ValueNote +) -> Field { // Ensure the note is actually owned by the owner (to prevent user from generating a valid proof while // spending someone else's notes). - assert(note.owner.eq(owner)); + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to pass this after rotating keys. + assert(note.npk_m_hash.eq(owner_npk_m_hash)); balance.remove(note); diff --git a/noir-projects/aztec-nr/value-note/src/value_note.nr b/noir-projects/aztec-nr/value-note/src/value_note.nr index ac790864aa8..0d98dec28a7 100644 --- a/noir-projects/aztec-nr/value-note/src/value_note.nr +++ b/noir-projects/aztec-nr/value-note/src/value_note.nr @@ -1,9 +1,11 @@ use dep::aztec::{ keys::getters::get_ivpk_m, - protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}, constants::GENERATOR_INDEX__NOTE_NULLIFIER}, + protocol_types::{ + address::AztecAddress, grumpkin_point::GrumpkinPoint, traits::{Deserialize, Serialize}, + constants::GENERATOR_INDEX__NOTE_NULLIFIER, hash::poseidon2_hash +}, note::{note_header::NoteHeader, note_interface::NoteInterface, utils::compute_note_hash_for_consumption}, - oracle::{unsafe_rand::unsafe_rand, nullifier_key::get_app_nullifier_secret_key}, - hash::poseidon2_hash, context::PrivateContext + oracle::unsafe_rand::unsafe_rand, keys::getters::get_nsk_app, context::PrivateContext }; global VALUE_NOTE_LEN: Field = 3; // 3 plus a header. @@ -12,7 +14,8 @@ global VALUE_NOTE_LEN: Field = 3; // 3 plus a header. #[aztec(note)] struct ValueNote { value: Field, - owner: AztecAddress, + // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. + npk_m_hash: Field, randomness: Field, } // docs:end:value-note-def @@ -22,7 +25,7 @@ impl NoteInterface for ValueNote { fn compute_nullifier(self, context: &mut PrivateContext) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = context.request_app_nullifier_secret_key(self.owner); + let secret = context.request_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -34,7 +37,7 @@ impl NoteInterface for ValueNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = get_app_nullifier_secret_key(self.owner); + let secret = get_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -43,8 +46,7 @@ impl NoteInterface for ValueNote { } // Broadcasts the note as an encrypted log on L1. - fn broadcast(self, context: &mut PrivateContext, slot: Field) { - let ivpk_m = get_ivpk_m(context, self.owner); + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { context.emit_encrypted_log( (*context).this_address(), slot, @@ -56,10 +58,10 @@ impl NoteInterface for ValueNote { } impl ValueNote { - pub fn new(value: Field, owner: AztecAddress) -> Self { + pub fn new(value: Field, npk_m_hash: Field) -> Self { let randomness = unsafe_rand(); let header = NoteHeader::empty(); - ValueNote { value, owner, randomness, header } + ValueNote { value, npk_m_hash, randomness, header } } } @@ -67,6 +69,6 @@ impl Serialize<7> for ValueNote { fn serialize(self) -> [Field; 7] { let header = self.header.serialize(); - [self.value, self.owner.to_field(), self.randomness, header[0], header[1], header[2], header[3]] + [self.value, self.npk_m_hash, self.randomness, header[0], header[1], header[2], header[3]] } } diff --git a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr index f51d506af83..25bcda79fc0 100644 --- a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr @@ -9,7 +9,8 @@ contract AppSubscription { AztecAddress, FunctionSelector, PrivateContext, NoteHeader, Map, PrivateMutable, PublicMutable, SharedImmutable }, - protocol_types::traits::is_empty + protocol_types::{traits::is_empty, grumpkin_point::GrumpkinPoint}, + keys::getters::{get_npk_m_hash, get_ivpk_m} }, authwit::{account::AccountActions, auth_witness::get_auth_witness, auth::assert_current_call_valid_authwit}, gas_token::GasToken, token::Token @@ -37,11 +38,13 @@ contract AppSubscription { assert(context.msg_sender().to_field() == 0); assert_current_call_valid_authwit(&mut context, user_address); - let mut note = storage.subscriptions.at(user_address).get_note(false); + let mut note = storage.subscriptions.at(user_address).get_note(false, GrumpkinPoint::zero()); assert(note.remaining_txs as u64 > 0, "you're out of txs"); note.remaining_txs -= 1; - storage.subscriptions.at(user_address).replace(&mut note, true); + + let subscriber_ivpk_m = get_ivpk_m(&mut context, user_address); + storage.subscriptions.at(user_address).replace(&mut note, true, subscriber_ivpk_m); context.set_as_fee_payer(); let gas_limit = storage.gas_token_limit_per_tx.read_private(); @@ -109,12 +112,14 @@ contract AppSubscription { // Assert that the given expiry_block_number < current_block_number + SUBSCRIPTION_DURATION_IN_BLOCKS. AppSubscription::at(context.this_address()).assert_block_number(expiry_block_number).static_enqueue(&mut context); + let subscriber_npk_m_hash = get_npk_m_hash(&mut context, subscriber_address); + let subscriber_ivpk_m = get_ivpk_m(&mut context, subscriber_address); - let mut subscription_note = SubscriptionNote::new(subscriber_address, expiry_block_number, tx_count); + let mut subscription_note = SubscriptionNote::new(subscriber_npk_m_hash, expiry_block_number, tx_count); if (!is_initialized(subscriber_address)) { - storage.subscriptions.at(subscriber_address).initialize(&mut subscription_note, true); + storage.subscriptions.at(subscriber_address).initialize(&mut subscription_note, true, subscriber_ivpk_m); } else { - storage.subscriptions.at(subscriber_address).replace(&mut subscription_note, true) + storage.subscriptions.at(subscriber_address).replace(&mut subscription_note, true, subscriber_ivpk_m) } } diff --git a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/subscription_note.nr b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/subscription_note.nr index 665393b166f..49e8bc43652 100644 --- a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/subscription_note.nr +++ b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/subscription_note.nr @@ -1,8 +1,8 @@ use dep::aztec::prelude::{AztecAddress, PrivateContext, NoteHeader, NoteInterface}; use dep::aztec::{ - keys::getters::get_ivpk_m, protocol_types::constants::GENERATOR_INDEX__NOTE_NULLIFIER, - note::utils::compute_note_hash_for_consumption, hash::poseidon2_hash, - oracle::{nullifier_key::get_app_nullifier_secret_key} + keys::getters::get_ivpk_m, + protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, grumpkin_point::GrumpkinPoint, hash::poseidon2_hash}, + note::utils::compute_note_hash_for_consumption, keys::getters::get_nsk_app }; global SUBSCRIPTION_NOTE_LEN: Field = 3; @@ -11,7 +11,8 @@ global SUBSCRIPTION_NOTE_LEN: Field = 3; // TODO: Do we need to include a nonce, in case we want to read/nullify/recreate with the same pubkey value? #[aztec(note)] struct SubscriptionNote { - owner: AztecAddress, + // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. + npk_m_hash: Field, expiry_block_number: Field, remaining_txs: Field, } @@ -19,7 +20,7 @@ struct SubscriptionNote { impl NoteInterface for SubscriptionNote { fn compute_nullifier(self, context: &mut PrivateContext) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = context.request_app_nullifier_secret_key(self.owner); + let secret = context.request_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -29,7 +30,7 @@ impl NoteInterface for SubscriptionNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = get_app_nullifier_secret_key(self.owner); + let secret = get_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -38,8 +39,7 @@ impl NoteInterface for SubscriptionNote { } // Broadcasts the note as an encrypted log on L1. - fn broadcast(self, context: &mut PrivateContext, slot: Field) { - let ivpk_m = get_ivpk_m(context, self.owner); + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { context.emit_encrypted_log( (*context).this_address(), slot, @@ -51,7 +51,7 @@ impl NoteInterface for SubscriptionNote { } impl SubscriptionNote { - pub fn new(owner: AztecAddress, expiry_block_number: Field, remaining_txs: Field) -> Self { - SubscriptionNote { owner, expiry_block_number, remaining_txs, header: NoteHeader::empty() } + pub fn new(npk_m_hash: Field, expiry_block_number: Field, remaining_txs: Field) -> Self { + SubscriptionNote { npk_m_hash, expiry_block_number, remaining_txs, header: NoteHeader::empty() } } } diff --git a/noir-projects/noir-contracts/contracts/benchmarking_contract/src/main.nr b/noir-projects/noir-contracts/contracts/benchmarking_contract/src/main.nr index a8b9f34d36b..1a81cef4ff6 100644 --- a/noir-projects/noir-contracts/contracts/benchmarking_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/benchmarking_contract/src/main.nr @@ -18,7 +18,7 @@ contract Benchmarking { // Creates a new value note for the target owner. Use this method to seed an initial set of notes. #[aztec(private)] fn create_note(owner: AztecAddress, value: Field) { - increment(storage.notes.at(owner), value, owner); + increment(context, storage.notes.at(owner), value, owner); } // Deletes a note at a specific index in the set and creates a new one with the same value. @@ -33,7 +33,7 @@ contract Benchmarking { let notes = owner_notes.get_notes(getter_options.set_limit(1).set_offset(index)); let note = notes[0].unwrap_unchecked(); owner_notes.remove(note); - increment(owner_notes, note.value, owner); + increment(context, owner_notes, note.value, owner); } // Reads and writes to public storage and enqueues a call to another public function. diff --git a/noir-projects/noir-contracts/contracts/card_game_contract/src/cards.nr b/noir-projects/noir-contracts/contracts/card_game_contract/src/cards.nr index c43ba634b10..72f51f7fd57 100644 --- a/noir-projects/noir-contracts/contracts/card_game_contract/src/cards.nr +++ b/noir-projects/noir-contracts/contracts/card_game_contract/src/cards.nr @@ -1,9 +1,12 @@ use dep::aztec::prelude::{AztecAddress, FunctionSelector, PrivateContext, NoteHeader, NoteGetterOptions, NoteViewerOptions}; use dep::aztec::{ - protocol_types::{traits::{ToField, Serialize, FromField}, constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL}, - context::{PublicContext, Context}, note::note_getter::view_notes, state_vars::PrivateSet, - note::constants::MAX_NOTES_PER_PAGE + protocol_types::{ + traits::{ToField, Serialize, FromField}, grumpkin_point::GrumpkinPoint, + constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL +}, + keys::getters::{get_npk_m_hash, get_ivpk_m}, context::{PublicContext, Context}, + note::note_getter::view_notes, state_vars::PrivateSet, note::constants::MAX_NOTES_PER_PAGE }; use dep::std; use dep::std::{option::Option}; @@ -49,13 +52,13 @@ struct CardNote { } impl CardNote { - fn new(strength: u32, points: u32, owner: AztecAddress) -> Self { + fn new(strength: u32, points: u32, npk_m_hash: Field) -> Self { let card = Card { strength, points }; - CardNote::from_card(card, owner) + CardNote::from_card(card, npk_m_hash) } - pub fn from_card(card: Card, owner: AztecAddress) -> CardNote { - CardNote { card, note: ValueNote::new(card.to_field(), owner) } + pub fn from_card(card: Card, npk_m_hash: Field) -> CardNote { + CardNote { card, note: ValueNote::new(card.to_field(), npk_m_hash) } } pub fn from_note(note: ValueNote) -> CardNote { @@ -100,12 +103,15 @@ impl Deck { } pub fn add_cards(&mut self, cards: [Card; N], owner: AztecAddress) -> [CardNote] { - let _context = self.set.context.private.unwrap(); + let context = self.set.context.private.unwrap(); + + let owner_npk_m_hash = get_npk_m_hash(context, owner); + let owner_ivpk_m = get_ivpk_m(context, owner); let mut inserted_cards = &[]; for card in cards { - let mut card_note = CardNote::from_card(card, owner); - self.set.insert(&mut card_note.note, true); + let mut card_note = CardNote::from_card(card, owner_npk_m_hash); + self.set.insert(&mut card_note.note, true, owner_ivpk_m); inserted_cards = inserted_cards.push_back(card_note); } @@ -113,6 +119,8 @@ impl Deck { } pub fn get_cards(&mut self, cards: [Card; N], owner: AztecAddress) -> [CardNote; N] { + let owner_npk_m_hash = get_npk_m_hash(self.set.context.private.unwrap(), owner); + let options = NoteGetterOptions::with_filter(filter_cards, cards); let maybe_notes = self.set.get_notes(options); let mut found_cards = [Option::none(); N]; @@ -121,7 +129,8 @@ impl Deck { let card_note = CardNote::from_note(maybe_notes[i].unwrap_unchecked()); // Ensure the notes are actually owned by the owner (to prevent user from generating a valid proof while // spending someone else's notes). - assert(card_note.note.owner.eq(owner)); + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to pass this assert after rotating keys. + assert(card_note.note.npk_m_hash.eq(owner_npk_m_hash)); for j in 0..cards.len() { if found_cards[j].is_none() @@ -168,8 +177,10 @@ pub fn get_pack_cards( owner: AztecAddress, context: &mut PrivateContext ) -> [Card; PACK_CARDS] { + let owner_npk_m_hash = get_npk_m_hash(context, owner); + // generate pseudo randomness deterministically from 'seed' and user secret - let secret = context.request_app_nullifier_secret_key(owner); + let secret = context.request_nsk_app(owner_npk_m_hash); let mix = secret + seed; let mix_bytes: [u8; 32] = mix.to_le_bytes(32).as_array(); let random_bytes = std::hash::sha256(mix_bytes); diff --git a/noir-projects/noir-contracts/contracts/child_contract/src/main.nr b/noir-projects/noir-contracts/contracts/child_contract/src/main.nr index 75cfe1a1673..e05623a233e 100644 --- a/noir-projects/noir-contracts/contracts/child_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/child_contract/src/main.nr @@ -4,8 +4,9 @@ contract Child { use dep::aztec::{ context::{PublicContext, Context, gas::GasOpts}, - protocol_types::{abis::{call_context::CallContext}}, - note::{note_getter_options::NoteGetterOptions, note_header::NoteHeader} + protocol_types::{abis::call_context::CallContext, grumpkin_point::GrumpkinPoint}, + note::{note_getter_options::NoteGetterOptions, note_header::NoteHeader}, + keys::getters::{get_npk_m_hash, get_ivpk_m} }; use dep::value_note::value_note::ValueNote; @@ -51,17 +52,22 @@ contract Child { #[aztec(private)] fn private_set_value(new_value: Field, owner: AztecAddress) -> Field { - let mut note = ValueNote::new(new_value, owner); - storage.a_private_value.insert(&mut note, true); + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + + let mut note = ValueNote::new(new_value, owner_npk_m_hash); + storage.a_private_value.insert(&mut note, true, owner_ivpk_m); new_value } #[aztec(private)] fn private_get_value(amount: Field, owner: AztecAddress) -> Field { + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let mut options = NoteGetterOptions::new(); options = options.select(ValueNote::properties().value, amount, Option::none()).select( - ValueNote::properties().owner, - owner.to_field(), + ValueNote::properties().npk_m_hash, + owner_npk_m_hash, Option::none() ).set_limit(1); let notes = storage.a_private_value.get_notes(options); diff --git a/noir-projects/noir-contracts/contracts/claim_contract/src/main.nr b/noir-projects/noir-contracts/contracts/claim_contract/src/main.nr index 090ea87a0a0..72f80b872a4 100644 --- a/noir-projects/noir-contracts/contracts/claim_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/claim_contract/src/main.nr @@ -2,7 +2,7 @@ contract Claim { use dep::aztec::{ history::note_inclusion::prove_note_inclusion, protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress}, - state_vars::SharedImmutable + state_vars::SharedImmutable, keys::getters::get_npk_m_hash }; use dep::value_note::value_note::ValueNote; use dep::token::Token; @@ -29,7 +29,11 @@ contract Claim { assert( target_address == proof_note.header.contract_address, "Note does not correspond to the target contract" ); - assert_eq(proof_note.owner, context.msg_sender(), "Note does not belong to the sender"); + + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to pass this assert after key rotation. + assert_eq(proof_note.npk_m_hash, msg_sender_npk_m_hash, "Note does not belong to the sender"); // 2) Prove that the note hash exists in the note hash tree prove_note_inclusion(proof_note, context); diff --git a/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr b/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr index 38fb7984a38..9eef23a50bd 100644 --- a/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr @@ -4,8 +4,12 @@ contract Crowdfunding { // docs:start:all-deps use dep::aztec::{ - protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress, traits::Serialize}, - state_vars::{PrivateSet, PublicImmutable, SharedImmutable} + protocol_types::{ + abis::function_selector::FunctionSelector, address::AztecAddress, traits::Serialize, + grumpkin_point::GrumpkinPoint + }, + state_vars::{PrivateSet, PublicImmutable, SharedImmutable}, + keys::getters::{get_npk_m_hash, get_ivpk_m} }; use dep::value_note::value_note::ValueNote; use dep::token::Token; @@ -83,8 +87,10 @@ contract Crowdfunding { // 3) Create a value note for the donor so that he can later on claim a rewards token in the Claim // contract by proving that the hash of this note exists in the note hash tree. - let mut note = ValueNote::new(amount as Field, context.msg_sender()); - storage.donation_receipts.insert(&mut note, true); + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + let msg_sender_ivpk_m = get_ivpk_m(&mut context, context.msg_sender()); + let mut note = ValueNote::new(amount as Field, msg_sender_npk_m_hash); + storage.donation_receipts.insert(&mut note, true, msg_sender_ivpk_m); } // docs:end:donate diff --git a/noir-projects/noir-contracts/contracts/delegated_on_contract/src/main.nr b/noir-projects/noir-contracts/contracts/delegated_on_contract/src/main.nr index e6f093e569d..f38d4bff58c 100644 --- a/noir-projects/noir-contracts/contracts/delegated_on_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/delegated_on_contract/src/main.nr @@ -4,6 +4,7 @@ contract DelegatedOn { AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, NoteViewerOptions, PublicMutable, PrivateSet, PrivateContext }; + use dep::aztec::{protocol_types::grumpkin_point::GrumpkinPoint, keys::getters::{get_npk_m_hash, get_ivpk_m}}; use dep::value_note::value_note::ValueNote; #[aztec(storage)] @@ -14,8 +15,11 @@ contract DelegatedOn { #[aztec(private)] fn private_set_value(new_value: Field, owner: AztecAddress) -> Field { - let mut note = ValueNote::new(new_value, owner); - storage.a_private_value.insert(&mut note, true); + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + + let mut note = ValueNote::new(new_value, owner_npk_m_hash); + storage.a_private_value.insert(&mut note, true, owner_ivpk_m); new_value } @@ -25,14 +29,18 @@ contract DelegatedOn { new_value } - unconstrained fn view_private_value(amount: Field, owner: AztecAddress) -> pub Field { - let mut options = NoteViewerOptions::new(); + #[aztec(private)] + fn get_private_value(amount: Field, owner: AztecAddress) -> pub Field { + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key. + let mut options = NoteGetterOptions::new(); options = options.select(ValueNote::properties().value, amount, Option::none()).select( - ValueNote::properties().owner, - owner.to_field(), + ValueNote::properties().npk_m_hash, + owner_npk_m_hash, Option::none() ).set_limit(1); - let notes = storage.a_private_value.view_notes(options); + let notes = storage.a_private_value.get_notes(options); notes[0].unwrap_unchecked().value } diff --git a/noir-projects/noir-contracts/contracts/delegator_contract/src/main.nr b/noir-projects/noir-contracts/contracts/delegator_contract/src/main.nr index b7f7592350a..023974cd81f 100644 --- a/noir-projects/noir-contracts/contracts/delegator_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/delegator_contract/src/main.nr @@ -1,6 +1,7 @@ // A contract used along with `Parent` contract to test nested calls. contract Delegator { - use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteViewerOptions, PublicMutable, PrivateSet, Deserialize}; + use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, PublicMutable, PrivateSet, Deserialize}; + use dep::aztec::keys::getters::get_npk_m_hash; use dep::value_note::value_note::ValueNote; use dep::delegated_on::DelegatedOn; @@ -30,14 +31,18 @@ contract Delegator { DelegatedOn::at(target_contract).public_set_value(value).delegate_call(&mut context) } - unconstrained fn view_private_value(amount: Field, owner: AztecAddress) -> pub Field { - let mut options = NoteViewerOptions::new(); + #[aztec(private)] + fn get_private_value(amount: Field, owner: AztecAddress) -> pub Field { + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key. + let mut options = NoteGetterOptions::new(); options = options.select(ValueNote::properties().value, amount, Option::none()).select( - ValueNote::properties().owner, - owner.to_field(), + ValueNote::properties().npk_m_hash, + owner_npk_m_hash, Option::none() ).set_limit(1); - let notes = storage.a_private_value.view_notes(options); + let notes = storage.a_private_value.get_notes(options); notes[0].unwrap_unchecked().value } diff --git a/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr b/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr index e36efb875bc..d2ed49d84ec 100644 --- a/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr @@ -18,7 +18,11 @@ contract DocsExample { PrivateContext, Map, PublicMutable, PublicImmutable, PrivateMutable, PrivateImmutable, PrivateSet, SharedImmutable, Deserialize }; - use dep::aztec::{note::note_getter_options::Comparator, context::{PublicContext, Context}}; + use dep::aztec::{ + note::note_getter_options::Comparator, context::{PublicContext, Context}, + keys::getters::{get_npk_m_hash, get_ivpk_m} + }; + use dep::aztec::protocol_types::grumpkin_point::GrumpkinPoint; // how to import methods from other files/folders within your workspace use crate::options::create_account_card_getter_options; use crate::types::{card_note::{CardNote, CARD_NOTE_LEN}, leader::Leader}; @@ -166,31 +170,43 @@ contract DocsExample { // docs:start:initialize-private-mutable #[aztec(private)] fn initialize_private_immutable(randomness: Field, points: u8) { - let mut new_card = CardNote::new(points, randomness, context.msg_sender()); - storage.private_immutable.initialize(&mut new_card, true); + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + let msg_sender_ivpk_m = get_ivpk_m(&mut context, context.msg_sender()); + + let mut new_card = CardNote::new(points, randomness, msg_sender_npk_m_hash); + storage.private_immutable.initialize(&mut new_card, true, msg_sender_ivpk_m); } // docs:end:initialize-private-mutable #[aztec(private)] // msg_sender() is 0 at deploy time. So created another function fn initialize_private(randomness: Field, points: u8) { - let mut legendary_card = CardNote::new(points, randomness, context.msg_sender()); + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + let msg_sender_ivpk_m = get_ivpk_m(&mut context, context.msg_sender()); + + let mut legendary_card = CardNote::new(points, randomness, msg_sender_npk_m_hash); // create and broadcast note - storage.legendary_card.initialize(&mut legendary_card, true); + storage.legendary_card.initialize(&mut legendary_card, true, msg_sender_ivpk_m); } #[aztec(private)] fn insert_notes(amounts: [u8; 3]) { + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + let msg_sender_ivpk_m = get_ivpk_m(&mut context, context.msg_sender()); + for i in 0..amounts.len() { - let mut note = CardNote::new(amounts[i], 1, context.msg_sender()); - storage.set.insert(&mut note, true); + let mut note = CardNote::new(amounts[i], 1, msg_sender_npk_m_hash); + storage.set.insert(&mut note, true, msg_sender_ivpk_m); } } #[aztec(private)] fn insert_note(amount: u8, randomness: Field) { - let mut note = CardNote::new(amount, randomness, context.msg_sender()); - storage.set.insert(&mut note, true); + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + let msg_sender_ivpk_m = get_ivpk_m(&mut context, context.msg_sender()); + + let mut note = CardNote::new(amount, randomness, msg_sender_npk_m_hash); + storage.set.insert(&mut note, true, msg_sender_ivpk_m); } // docs:start:state_vars-NoteGetterOptionsComparatorExampleNoir @@ -210,8 +226,11 @@ contract DocsExample { #[aztec(private)] fn update_legendary_card(randomness: Field, points: u8) { - let mut new_card = CardNote::new(points, randomness, context.msg_sender()); - storage.legendary_card.replace(&mut new_card, true); + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + let msg_sender_ivpk_m = get_ivpk_m(&mut context, context.msg_sender()); + + let mut new_card = CardNote::new(points, randomness, msg_sender_npk_m_hash); + storage.legendary_card.replace(&mut new_card, true, msg_sender_ivpk_m); DocsExample::at(context.this_address()).update_leader(context.msg_sender(), points).enqueue(&mut context); } @@ -221,14 +240,17 @@ contract DocsExample { // Also serves as a e2e test that you can `get_note()` and then `replace()` // docs:start:state_vars-PrivateMutableGet - let card = storage.legendary_card.get_note(false); + let card = storage.legendary_card.get_note(false, GrumpkinPoint::zero()); // docs:end:state_vars-PrivateMutableGet let points = card.points + 1; - let mut new_card = CardNote::new(points, card.randomness, context.msg_sender()); + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + let msg_sender_ivpk_m = get_ivpk_m(&mut context, context.msg_sender()); + + let mut new_card = CardNote::new(points, card.randomness, msg_sender_npk_m_hash); // docs:start:state_vars-PrivateMutableReplace - storage.legendary_card.replace(&mut new_card, true); + storage.legendary_card.replace(&mut new_card, true, msg_sender_ivpk_m); // docs:end:state_vars-PrivateMutableReplace DocsExample::at(context.this_address()).update_leader(context.msg_sender(), points).enqueue(&mut context); diff --git a/noir-projects/noir-contracts/contracts/docs_example_contract/src/options.nr b/noir-projects/noir-contracts/contracts/docs_example_contract/src/options.nr index 7421112c715..ca5c22d9afc 100644 --- a/noir-projects/noir-contracts/contracts/docs_example_contract/src/options.nr +++ b/noir-projects/noir-contracts/contracts/docs_example_contract/src/options.nr @@ -8,13 +8,13 @@ use dep::aztec::note::note_getter_options::{Sort, SortOrder}; // docs:start:state_vars-NoteGetterOptionsSelectSortOffset pub fn create_account_card_getter_options( - account: AztecAddress, + account_npk_m_hash: Field, offset: u32 ) -> NoteGetterOptions { let mut options = NoteGetterOptions::new(); options.select( - CardNote::properties().owner, - account.to_field(), + CardNote::properties().npk_m_hash, + account_npk_m_hash, Option::none() ).sort(CardNote::properties().points, SortOrder.DESC).set_offset(offset) } @@ -24,12 +24,12 @@ pub fn create_account_card_getter_options( pub fn create_exact_card_getter_options( points: u8, secret: Field, - account: AztecAddress + account_npk_m_hash: Field ) -> NoteGetterOptions { let mut options = NoteGetterOptions::new(); options.select(CardNote::properties().points, points as Field, Option::none()).select(CardNote::properties().randomness, secret, Option::none()).select( - CardNote::properties().owner, - account.to_field(), + CardNote::properties().npk_m_hash, + account_npk_m_hash, Option::none() ) } @@ -53,21 +53,21 @@ pub fn filter_min_points( // docs:end:state_vars-OptionFilter // docs:start:state_vars-NoteGetterOptionsFilter -pub fn create_account_cards_with_min_points_getter_options(account: AztecAddress, min_points: u8) -> NoteGetterOptions { +pub fn create_account_cards_with_min_points_getter_options(account_npk_m_hash: Field, min_points: u8) -> NoteGetterOptions { NoteGetterOptions::with_filter(filter_min_points, min_points).select( - CardNote::properties().owner, - account.to_field(), + CardNote::properties().npk_m_hash, + account_npk_m_hash, Option::none() ).sort(CardNote::properties().points, SortOrder.ASC) } // docs:end:state_vars-NoteGetterOptionsFilter // docs:start:state_vars-NoteGetterOptionsPickOne -pub fn create_largest_account_card_getter_options(account: AztecAddress) -> NoteGetterOptions { +pub fn create_largest_account_card_getter_options(account_npk_m_hash: Field) -> NoteGetterOptions { let mut options = NoteGetterOptions::new(); options.select( - CardNote::properties().owner, - account.to_field(), + CardNote::properties().npk_m_hash, + account_npk_m_hash, Option::none() ).sort(CardNote::properties().points, SortOrder.DESC).set_limit(1) } diff --git a/noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr b/noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr index aa2fea463d4..5f57d70fa5a 100644 --- a/noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr +++ b/noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr @@ -1,8 +1,11 @@ use dep::aztec::prelude::{AztecAddress, NoteInterface, NoteHeader, PrivateContext}; use dep::aztec::{ keys::getters::get_ivpk_m, note::{utils::compute_note_hash_for_consumption}, - oracle::nullifier_key::get_app_nullifier_secret_key, hash::poseidon2_hash, - protocol_types::{traits::Empty, constants::GENERATOR_INDEX__NOTE_NULLIFIER} + keys::getters::get_nsk_app, + protocol_types::{ + traits::Empty, grumpkin_point::GrumpkinPoint, constants::GENERATOR_INDEX__NOTE_NULLIFIER, + hash::poseidon2_hash +} }; // Shows how to create a custom note @@ -14,20 +17,21 @@ global CARD_NOTE_LEN: Field = 3; struct CardNote { points: u8, randomness: Field, - owner: AztecAddress, + // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. + npk_m_hash: Field, } // docs:end:state_vars-CardNote impl CardNote { - pub fn new(points: u8, randomness: Field, owner: AztecAddress) -> Self { - CardNote { points, randomness, owner, header: NoteHeader::empty() } + pub fn new(points: u8, randomness: Field, npk_m_hash: Field) -> Self { + CardNote { points, randomness, npk_m_hash, header: NoteHeader::empty() } } } impl NoteInterface for CardNote { fn compute_nullifier(self, context: &mut PrivateContext) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = context.request_app_nullifier_secret_key(self.owner); + let secret = context.request_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -37,7 +41,7 @@ impl NoteInterface for CardNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = get_app_nullifier_secret_key(self.owner); + let secret = get_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -46,8 +50,7 @@ impl NoteInterface for CardNote { } // Broadcasts the note as an encrypted log on L1. - fn broadcast(self, context: &mut PrivateContext, slot: Field) { - let ivpk_m = get_ivpk_m(context, self.owner); + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { context.emit_encrypted_log( (*context).this_address(), slot, diff --git a/noir-projects/noir-contracts/contracts/easy_private_voting_contract/src/main.nr b/noir-projects/noir-contracts/contracts/easy_private_voting_contract/src/main.nr index 6924ca2b328..ba4656e8fbb 100644 --- a/noir-projects/noir-contracts/contracts/easy_private_voting_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/easy_private_voting_contract/src/main.nr @@ -4,7 +4,7 @@ contract EasyPrivateVoting { AztecAddress, FunctionSelector, NoteHeader, NoteInterface, NoteGetterOptions, PrivateContext, Map, PublicMutable }; - use dep::aztec::context::Context; + use dep::aztec::{context::Context, keys::getters::get_npk_m_hash}; // docs:end:imports // docs:start:storage_struct #[aztec(storage)] @@ -27,7 +27,9 @@ contract EasyPrivateVoting { // docs:start:cast_vote #[aztec(private)] // annotation to mark function as private and expose private context fn cast_vote(candidate: Field) { - let secret = context.request_app_nullifier_secret_key(context.msg_sender()); // get secret key of caller of function + let msg_sender_npk_m_hash = get_npk_m_hash(&mut context, context.msg_sender()); + // TODO (#6312): This will break with key rotation. Fix this. Can vote multiple times by rotating keys. + let secret = context.request_nsk_app(msg_sender_npk_m_hash); // get secret key of caller of function let nullifier = dep::std::hash::pedersen_hash([context.msg_sender().to_field(), secret]); // derive nullifier from sender and secret context.push_new_nullifier(nullifier, 0); // push nullifier EasyPrivateVoting::at(context.this_address()).add_to_tally_public(candidate).enqueue(&mut context); diff --git a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/ecdsa_public_key_note.nr b/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/ecdsa_public_key_note.nr index eaadbbc60ac..0a9d4aa2497 100644 --- a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/ecdsa_public_key_note.nr +++ b/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/ecdsa_public_key_note.nr @@ -2,8 +2,8 @@ use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteInterf use dep::aztec::{ keys::getters::get_ivpk_m, note::utils::compute_note_hash_for_consumption, - oracle::nullifier_key::get_app_nullifier_secret_key, hash::poseidon2_hash, - protocol_types::constants::GENERATOR_INDEX__NOTE_NULLIFIER + keys::getters::get_nsk_app, + protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, grumpkin_point::GrumpkinPoint, hash::poseidon2_hash} }; global ECDSA_PUBLIC_KEY_NOTE_LEN: Field = 5; @@ -14,7 +14,8 @@ global ECDSA_PUBLIC_KEY_NOTE_LEN: Field = 5; struct EcdsaPublicKeyNote { x: [u8; 32], y: [u8; 32], - owner: AztecAddress, // We store the owner address only to get the secret key to compute the nullifier + // We store the npk_m_hash only to get the secret key to compute the nullifier + npk_m_hash: Field, } impl NoteInterface for EcdsaPublicKeyNote { @@ -40,7 +41,7 @@ impl NoteInterface for EcdsaPublicKeyNote { let last_x = self.x[31] as Field; let last_y = self.y[31] as Field; - [x, last_x, y, last_y, self.owner.to_field()] + [x, last_x, y, last_y, self.npk_m_hash] } // Cannot use the automatic deserialization for the aforementioned reasons @@ -60,12 +61,12 @@ impl NoteInterface for EcdsaPublicKeyNote { } y[31] = serialized_note[3].to_be_bytes(32)[31]; - EcdsaPublicKeyNote { x, y, owner: AztecAddress::from_field(serialized_note[4]), header: NoteHeader::empty() } + EcdsaPublicKeyNote { x, y, npk_m_hash: serialized_note[4], header: NoteHeader::empty() } } fn compute_nullifier(self, context: &mut PrivateContext) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = context.request_app_nullifier_secret_key(self.owner); + let secret = context.request_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -75,7 +76,7 @@ impl NoteInterface for EcdsaPublicKeyNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = get_app_nullifier_secret_key(self.owner); + let secret = get_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -84,8 +85,7 @@ impl NoteInterface for EcdsaPublicKeyNote { } // Broadcasts the note as an encrypted log on L1. - fn broadcast(self, context: &mut PrivateContext, slot: Field) { - let ivpk_m = get_ivpk_m(context, self.owner); + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { context.emit_encrypted_log( (*context).this_address(), slot, @@ -97,7 +97,7 @@ impl NoteInterface for EcdsaPublicKeyNote { } impl EcdsaPublicKeyNote { - pub fn new(x: [u8; 32], y: [u8; 32], owner: AztecAddress) -> Self { - EcdsaPublicKeyNote { x, y, owner, header: NoteHeader::empty() } + pub fn new(x: [u8; 32], y: [u8; 32], npk_m_hash: Field) -> Self { + EcdsaPublicKeyNote { x, y, npk_m_hash, header: NoteHeader::empty() } } } diff --git a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr index 89da4adddb8..0a967637e31 100644 --- a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr @@ -6,6 +6,7 @@ contract EcdsaAccount { use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, PrivateContext, PrivateImmutable}; use dep::aztec::protocol_types::abis::call_context::CallContext; + use dep::aztec::keys::getters::{get_npk_m_hash, get_ivpk_m}; use dep::std; use dep::aztec::context::Context; @@ -28,8 +29,11 @@ contract EcdsaAccount { #[aztec(initializer)] fn constructor(signing_pub_key_x: [u8; 32], signing_pub_key_y: [u8; 32]) { let this = context.this_address(); - let mut pub_key_note = EcdsaPublicKeyNote::new(signing_pub_key_x, signing_pub_key_y, this); - storage.public_key.initialize(&mut pub_key_note, true); + let this_npk_m_hash = get_npk_m_hash(&mut context, this); + let this_ivpk_m = get_ivpk_m(&mut context, this); + + let mut pub_key_note = EcdsaPublicKeyNote::new(signing_pub_key_x, signing_pub_key_y, this_npk_m_hash); + storage.public_key.initialize(&mut pub_key_note, true, this_ivpk_m); } // Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts diff --git a/noir-projects/noir-contracts/contracts/escrow_contract/src/main.nr b/noir-projects/noir-contracts/contracts/escrow_contract/src/main.nr index ae45b80b6e5..7f04b26caed 100644 --- a/noir-projects/noir-contracts/contracts/escrow_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/escrow_contract/src/main.nr @@ -4,6 +4,8 @@ contract Escrow { use dep::aztec::context::{PublicContext, Context}; + use dep::aztec::keys::getters::{get_npk_m_hash, get_ivpk_m}; + use dep::address_note::address_note::AddressNote; use dep::token::Token; @@ -17,8 +19,11 @@ contract Escrow { #[aztec(private)] #[aztec(initializer)] fn constructor(owner: AztecAddress) { - let mut note = AddressNote::new(owner, owner); - storage.owner.initialize(&mut note, true); + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + + let mut note = AddressNote::new(owner, owner_npk_m_hash); + storage.owner.initialize(&mut note, true, owner_ivpk_m); } // Withdraws balance. Requires that msg.sender is the owner. diff --git a/noir-projects/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr b/noir-projects/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr index 3e1112ff07b..47cd407a002 100644 --- a/noir-projects/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr @@ -6,7 +6,7 @@ contract InclusionProofs { }; use dep::aztec::protocol_types::{grumpkin_point::GrumpkinPoint, contract_class_id::ContractClassId}; - use dep::aztec::{context::Context, note::note_getter_options::NoteStatus}; + use dep::aztec::{context::Context, note::note_getter_options::NoteStatus, keys::getters::{get_npk_m_hash, get_ivpk_m}}; // docs:start:imports use dep::aztec::history::{ contract_inclusion::{ @@ -44,8 +44,11 @@ contract InclusionProofs { #[aztec(private)] fn create_note(owner: AztecAddress, value: Field) { let owner_private_values = storage.private_values.at(owner); - let mut note = ValueNote::new(value, owner); - owner_private_values.insert(&mut note, true); + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + + let mut note = ValueNote::new(value, owner_npk_m_hash); + owner_private_values.insert(&mut note, true, owner_ivpk_m); } // docs:end:create_note @@ -56,13 +59,16 @@ contract InclusionProofs { block_number: u32, // The block at which we'll prove that the note exists nullified: bool ) { + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key. // docs:start:get_note_from_pxe // 1) Get the note from PXE. let private_values = storage.private_values.at(owner); let mut options = NoteGetterOptions::new(); options = options.select( - ValueNote::properties().owner, - owner.to_field(), + ValueNote::properties().npk_m_hash, + owner_npk_m_hash, Option::none() ).set_limit(1); if (nullified) { @@ -88,7 +94,8 @@ contract InclusionProofs { use_block_number: bool, block_number: u32 // The block at which we'll prove that the note exists ) { - let mut note = ValueNote::new(1, owner); + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let mut note = ValueNote::new(1, owner_npk_m_hash); if (use_block_number) { prove_note_inclusion_at(note, block_number, context); @@ -108,12 +115,15 @@ contract InclusionProofs { // because PXE performs note commitment inclusion check when you add a new note). fail_case: bool ) { + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key. // 2) Get the note from PXE let private_values = storage.private_values.at(owner); let mut options = NoteGetterOptions::new(); options = options.select( - ValueNote::properties().owner, - owner.to_field(), + ValueNote::properties().npk_m_hash, + owner_npk_m_hash, Option::none() ).set_limit(1); if (fail_case) { @@ -139,12 +149,15 @@ contract InclusionProofs { block_number: u32, // The block at which we'll prove that the note exists and is not nullified nullified: bool ) { + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key. // 1) Get the note from PXE. let private_values = storage.private_values.at(owner); let mut options = NoteGetterOptions::new(); options = options.select( - ValueNote::properties().owner, - owner.to_field(), + ValueNote::properties().npk_m_hash, + owner_npk_m_hash, Option::none() ).set_limit(1); if (nullified) { @@ -166,11 +179,14 @@ contract InclusionProofs { // docs:start:nullify_note #[aztec(private)] fn nullify_note(owner: AztecAddress) { + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key. let private_values = storage.private_values.at(owner); let mut options = NoteGetterOptions::new(); options = options.select( - ValueNote::properties().owner, - owner.to_field(), + ValueNote::properties().npk_m_hash, + owner_npk_m_hash, Option::none() ).set_limit(1); let notes = private_values.get_notes(options); diff --git a/noir-projects/noir-contracts/contracts/pending_note_hashes_contract/src/main.nr b/noir-projects/noir-contracts/contracts/pending_note_hashes_contract/src/main.nr index 9cce1d75274..213dbc91661 100644 --- a/noir-projects/noir-contracts/contracts/pending_note_hashes_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/pending_note_hashes_contract/src/main.nr @@ -7,6 +7,8 @@ contract PendingNoteHashes { use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, PrivateContext, Map, PrivateSet}; use dep::value_note::{balance_utils, filter::filter_notes_min_sum, value_note::{VALUE_NOTE_LEN, ValueNote}}; use dep::aztec::context::{PublicContext, Context}; + use dep::aztec::keys::getters::{get_npk_m_hash, get_ivpk_m}; + use dep::aztec::protocol_types::grumpkin_point::GrumpkinPoint; #[aztec(storage)] struct Storage { @@ -22,10 +24,14 @@ contract PendingNoteHashes { #[aztec(private)] fn test_insert_then_get_then_nullify_flat(amount: Field, owner: AztecAddress) -> Field { let owner_balance = storage.balances.at(owner); - let mut note = ValueNote::new(amount, owner); + + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + + let mut note = ValueNote::new(amount, owner_npk_m_hash); // Insert note - owner_balance.insert(&mut note, true); + owner_balance.insert(&mut note, true, owner_ivpk_m); let options = NoteGetterOptions::with_filter(filter_notes_min_sum, amount); // get note inserted above @@ -52,9 +58,12 @@ contract PendingNoteHashes { assert(maybe_notes[0].is_none()); assert(maybe_notes[1].is_none()); + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + // Insert note - let mut note = ValueNote::new(amount, owner); - owner_balance.insert(&mut note, true); + let mut note = ValueNote::new(amount, owner_npk_m_hash); + owner_balance.insert(&mut note, true, owner_ivpk_m); 0 } @@ -67,10 +76,14 @@ contract PendingNoteHashes { #[aztec(private)] fn insert_note(amount: Field, owner: AztecAddress) { let owner_balance = storage.balances.at(owner); - let mut note = ValueNote::new(amount, owner); + + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + + let mut note = ValueNote::new(amount, owner_npk_m_hash); // Insert note - owner_balance.insert(&mut note, true); + owner_balance.insert(&mut note, true, owner_ivpk_m); } // Nested/inner function to get a note and confirm it matches the expected value diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr index b7e0f890eb0..6bff06003ce 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr @@ -13,6 +13,7 @@ contract SchnorrAccount { auth_witness::get_auth_witness }; use dep::aztec::hash::compute_siloed_nullifier; + use dep::aztec::keys::getters::{get_ivpk_m, get_npk_m_hash}; use dep::aztec::oracle::get_nullifier_membership_witness::get_low_nullifier_membership_witness; use crate::public_key_note::{PublicKeyNote, PUBLIC_KEY_NOTE_LEN}; @@ -30,9 +31,12 @@ contract SchnorrAccount { #[aztec(initializer)] fn constructor(signing_pub_key_x: Field, signing_pub_key_y: Field) { let this = context.this_address(); + let this_npk_m_hash = get_npk_m_hash(&mut context, this); + let this_ivpk_m = get_ivpk_m(&mut context, this); + // docs:start:initialize - let mut pub_key_note = PublicKeyNote::new(signing_pub_key_x, signing_pub_key_y, this); - storage.signing_public_key.initialize(&mut pub_key_note, true); + let mut pub_key_note = PublicKeyNote::new(signing_pub_key_x, signing_pub_key_y, this_npk_m_hash); + storage.signing_public_key.initialize(&mut pub_key_note, true, this_ivpk_m); // docs:end:initialize } diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/public_key_note.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/public_key_note.nr index 74812ec7465..71601730d87 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/public_key_note.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/public_key_note.nr @@ -1,8 +1,7 @@ use dep::aztec::prelude::{AztecAddress, NoteHeader, NoteInterface, PrivateContext}; use dep::aztec::{ - keys::getters::get_ivpk_m, note::utils::compute_note_hash_for_consumption, hash::poseidon2_hash, - oracle::{nullifier_key::get_app_nullifier_secret_key}, - protocol_types::constants::GENERATOR_INDEX__NOTE_NULLIFIER + note::utils::compute_note_hash_for_consumption, keys::getters::get_nsk_app, + protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, grumpkin_point::GrumpkinPoint, hash::poseidon2_hash} }; global PUBLIC_KEY_NOTE_LEN: Field = 3; @@ -13,13 +12,14 @@ global PUBLIC_KEY_NOTE_LEN: Field = 3; struct PublicKeyNote { x: Field, y: Field, - owner: AztecAddress, // We store the owner address only to get the secret key to compute the nullifier and to broadcast + // We store the npk_m_hash only to get the secret key to compute the nullifier + npk_m_hash: Field, } impl NoteInterface for PublicKeyNote { fn compute_nullifier(self, context: &mut PrivateContext) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = context.request_app_nullifier_secret_key(self.owner); + let secret = context.request_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -29,7 +29,7 @@ impl NoteInterface for PublicKeyNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = get_app_nullifier_secret_key(self.owner); + let secret = get_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -38,8 +38,7 @@ impl NoteInterface for PublicKeyNote { } // Broadcasts the note as an encrypted log on L1. - fn broadcast(self, context: &mut PrivateContext, slot: Field) { - let ivpk_m = get_ivpk_m(context, self.owner); + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { context.emit_encrypted_log( (*context).this_address(), slot, @@ -51,7 +50,7 @@ impl NoteInterface for PublicKeyNote { } impl PublicKeyNote { - pub fn new(x: Field, y: Field, owner: AztecAddress) -> Self { - PublicKeyNote { x, y, owner, header: NoteHeader::empty() } + pub fn new(x: Field, y: Field, npk_m_hash: Field) -> Self { + PublicKeyNote { x, y, npk_m_hash, header: NoteHeader::empty() } } } diff --git a/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr index db8efde4a25..db8c9965eb2 100644 --- a/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr @@ -37,7 +37,7 @@ contract StatefulTest { fn create_note(owner: AztecAddress, value: Field) { if (value != 0) { let loc = storage.notes.at(owner); - increment(loc, value, owner); + increment(context, loc, value, owner); } } @@ -46,7 +46,8 @@ contract StatefulTest { fn create_note_no_init_check(owner: AztecAddress, value: Field) { if (value != 0) { let loc = storage.notes.at(owner); - increment(loc, value, owner); + // TODO (#6312): This will break with key rotation. Fix this. Will not be able to spend / increment any notes after rotating key. + increment(context, loc, value, owner); } } @@ -56,10 +57,10 @@ contract StatefulTest { let sender = context.msg_sender(); let sender_notes = storage.notes.at(sender); - decrement(sender_notes, amount, sender); + decrement(context, sender_notes, amount, sender); let recipient_notes = storage.notes.at(recipient); - increment(recipient_notes, amount, recipient); + increment(context, recipient_notes, amount, recipient); } #[aztec(private)] @@ -68,10 +69,10 @@ contract StatefulTest { let sender = context.msg_sender(); let sender_notes = storage.notes.at(sender); - decrement(sender_notes, amount, sender); + decrement(context, sender_notes, amount, sender); let recipient_notes = storage.notes.at(recipient); - increment(recipient_notes, amount, recipient); + increment(context, recipient_notes, amount, recipient); } #[aztec(public)] diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index baa3a451c44..fde26e00d04 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -23,7 +23,7 @@ contract Test { use dep::aztec::state_vars::{shared_mutable::SharedMutablePrivateGetter, map::derive_storage_slot_in_map}; use dep::aztec::{ - keys::getters::{get_npk_m, get_ivpk_m}, + keys::getters::{get_npk_m, get_ivpk_m, get_npk_m_hash}, context::{Context, inputs::private_context_inputs::PrivateContextInputs}, hash::{pedersen_hash, compute_secret_hash, ArgsHasher}, note::{ @@ -81,8 +81,11 @@ contract Test { storage_slot != storage.example_constant.get_storage_slot(), "this storage slot is reserved for example_constant" ); - let mut note = ValueNote::new(value, owner); - create_note(&mut context, storage_slot, &mut note, true); + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + + let mut note = ValueNote::new(value, owner_npk_m_hash); + create_note(&mut context, storage_slot, &mut note, true, owner_ivpk_m); } #[aztec(private)] @@ -259,8 +262,12 @@ contract Test { let mut storage_slot = storage.example_constant.get_storage_slot() + 1; Test::at(context.this_address()).call_create_note(value, owner, storage_slot).call(&mut context); storage_slot += 1; - let mut note = ValueNote::new(value + 1, owner); - create_note(&mut context, storage_slot, &mut note, true); + + let owner_npk_m_hash = get_npk_m_hash(&mut context, owner); + let owner_ivpk_m = get_ivpk_m(&mut context, owner); + + let mut note = ValueNote::new(value + 1, owner_npk_m_hash); + create_note(&mut context, storage_slot, &mut note, true, owner_ivpk_m); storage_slot += 1; Test::at(context.this_address()).call_create_note(value + 2, owner, storage_slot).call(&mut context); } @@ -334,7 +341,7 @@ contract Test { #[aztec(private)] fn set_constant(value: Field) { let mut note = TestNote::new(value); - storage.example_constant.initialize(&mut note, false); + storage.example_constant.initialize(&mut note, false, GrumpkinPoint::zero()); } #[aztec(private)] diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/test_note.nr b/noir-projects/noir-contracts/contracts/test_contract/src/test_note.nr index 17f2d2d244d..228f406c63e 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/test_note.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/test_note.nr @@ -1,6 +1,6 @@ use dep::aztec::{ note::{note_header::NoteHeader, note_interface::NoteInterface}, hash::pedersen_hash, - context::PrivateContext + context::PrivateContext, protocol_types::grumpkin_point::GrumpkinPoint }; global TEST_NOTE_LEN: Field = 1; @@ -26,7 +26,7 @@ impl NoteInterface for TestNote { 0 } - fn broadcast(self, context: &mut PrivateContext, slot: Field) { + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { assert( false, "TestNote does not support broadcast. Add it to PXE directly using the `.addNote` function." ); diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/balances_map.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/balances_map.nr index db69c2a8c1b..e405431cd3b 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/balances_map.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/balances_map.nr @@ -1,7 +1,12 @@ -use dep::aztec::prelude::{AztecAddress, NoteHeader, NoteInterface, NoteGetterOptions, NoteViewerOptions, PrivateSet, Map}; +use dep::aztec::prelude::{ + AztecAddress, NoteGetterOptions, NoteViewerOptions, NoteHeader, NoteInterface, PrivateContext, + PrivateSet, Map +}; use dep::aztec::{ - context::Context, protocol_types::{constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL}, - note::{note_getter::view_notes, note_getter_options::SortOrder} + context::{PublicContext, Context}, hash::pedersen_hash, + protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, + note::{note_getter::view_notes, note_getter_options::SortOrder}, + keys::getters::{get_npk_m_hash, get_ivpk_m} }; use crate::types::token_note::{TokenNote, OwnedNote}; @@ -56,10 +61,15 @@ impl BalancesMap { owner: AztecAddress, addend: U128 ) where T: NoteInterface + OwnedNote { - let mut addend_note = T::new(addend, owner); + let context = self.map.context.private.unwrap(); + let owner_ivpk_m = get_ivpk_m(context, owner); + + // We fetch the nullifier public key hash from the registry / from our PXE + let owner_npk_m_hash = get_npk_m_hash(context, owner); + let mut addend_note = T::new(addend, owner_npk_m_hash); // docs:start:insert - self.map.at(owner).insert(&mut addend_note, true); + self.map.at(owner).insert(&mut addend_note, true, owner_ivpk_m); // docs:end:insert } diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/token_note.nr index 8492b92a1fc..b5df8c9ba14 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/token_note.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/token_note.nr @@ -1,27 +1,25 @@ use dep::aztec::{ - keys::getters::get_ivpk_m, prelude::{AztecAddress, NoteHeader, NoteInterface, PrivateContext}, - protocol_types::constants::GENERATOR_INDEX__NOTE_NULLIFIER, - note::utils::compute_note_hash_for_consumption, hash::poseidon2_hash, - oracle::{unsafe_rand::unsafe_rand, nullifier_key::get_app_nullifier_secret_key} + prelude::{AztecAddress, NoteHeader, NoteInterface, PrivateContext}, + protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, grumpkin_point::GrumpkinPoint, hash::poseidon2_hash}, + note::utils::compute_note_hash_for_consumption, oracle::unsafe_rand::unsafe_rand, + keys::getters::get_nsk_app }; trait OwnedNote { - fn new(amount: U128, owner: AztecAddress) -> Self; + fn new(amount: U128, owner_npk_m_hash: Field) -> Self; fn get_amount(self) -> U128; - fn get_owner(self) -> AztecAddress; + fn get_owner_npk_m_hash(self) -> Field; } global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header. #[aztec(note)] struct TokenNote { - // the amount of tokens in the note + // The amount of tokens in the note amount: U128, - // the provider of secrets for the nullifier. The owner (recipient) to ensure that the note - // can be privately spent. When nullifier secret and encryption private key is same - // we can simply use the owner for this one. - owner: AztecAddress, - // randomness of the note to hide contents. + // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. + npk_m_hash: Field, + // Randomness of the note to hide its contents randomness: Field, } @@ -29,7 +27,7 @@ impl NoteInterface for TokenNote { // docs:start:nullifier fn compute_nullifier(self, context: &mut PrivateContext) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = context.request_app_nullifier_secret_key(self.owner); + let secret = context.request_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -40,7 +38,7 @@ impl NoteInterface for TokenNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = get_app_nullifier_secret_key(self.owner); + let secret = get_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -49,10 +47,10 @@ impl NoteInterface for TokenNote { } // Broadcasts the note as an encrypted log on L1. - fn broadcast(self, context: &mut PrivateContext, slot: Field) { + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { // We only bother inserting the note if non-empty to save funds on gas. + // TODO: (#5901) This will be changed a lot, as it should use the updated encrypted log format if !(self.amount == U128::from_integer(0)) { - let ivpk_m = get_ivpk_m(context, self.owner); context.emit_encrypted_log( (*context).this_address(), slot, @@ -65,10 +63,10 @@ impl NoteInterface for TokenNote { } impl OwnedNote for TokenNote { - fn new(amount: U128, owner: AztecAddress) -> Self { + fn new(amount: U128, owner_npk_m_hash: Field) -> Self { Self { amount, - owner, + npk_m_hash: owner_npk_m_hash, randomness: unsafe_rand(), header: NoteHeader::empty(), } @@ -78,7 +76,7 @@ impl OwnedNote for TokenNote { self.amount } - fn get_owner(self) -> AztecAddress { - self.owner + fn get_owner_npk_m_hash(self) -> Field { + self.npk_m_hash } } diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/transparent_note.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/transparent_note.nr index d5cf7197cef..061ca460642 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/transparent_note.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/transparent_note.nr @@ -1,8 +1,8 @@ // docs:start:token_types_all use dep::aztec::{ note::{note_getter_options::PropertySelector, utils::compute_note_hash_for_consumption}, - hash::poseidon2_hash, prelude::{NoteHeader, NoteInterface, PrivateContext}, - protocol_types::constants::GENERATOR_INDEX__NOTE_NULLIFIER + prelude::{NoteHeader, NoteInterface, PrivateContext}, + protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, grumpkin_point::GrumpkinPoint, hash::poseidon2_hash} }; global TRANSPARENT_NOTE_LEN: Field = 2; @@ -59,7 +59,7 @@ impl NoteInterface for TransparentNote { ]) } - fn broadcast(self, context: &mut PrivateContext, slot: Field) { + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { assert(false, "TransparentNote does not support broadcast"); } } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/types/balances_map.nr b/noir-projects/noir-contracts/contracts/token_contract/src/types/balances_map.nr index 6ae314da25c..e405431cd3b 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/types/balances_map.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/types/balances_map.nr @@ -4,8 +4,9 @@ use dep::aztec::prelude::{ }; use dep::aztec::{ context::{PublicContext, Context}, hash::pedersen_hash, - protocol_types::{constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, hash::poseidon2_hash}, - note::{note_getter::view_notes, note_getter_options::SortOrder}, keys::getters::get_npk_m_hash + protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, + note::{note_getter::view_notes, note_getter_options::SortOrder}, + keys::getters::{get_npk_m_hash, get_ivpk_m} }; use crate::types::token_note::{TokenNote, OwnedNote}; @@ -60,13 +61,15 @@ impl BalancesMap { owner: AztecAddress, addend: U128 ) where T: NoteInterface + OwnedNote { - // We fetch the nullifier public key hash in the registry / from our PXE - let owner_npk_m_hash = get_npk_m_hash(self.map.context.private.unwrap(), owner); + let context = self.map.context.private.unwrap(); + let owner_ivpk_m = get_ivpk_m(context, owner); + // We fetch the nullifier public key hash from the registry / from our PXE + let owner_npk_m_hash = get_npk_m_hash(context, owner); let mut addend_note = T::new(addend, owner_npk_m_hash); // docs:start:insert - self.map.at(owner).insert(&mut addend_note, true); + self.map.at(owner).insert(&mut addend_note, true, owner_ivpk_m); // docs:end:insert } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr index fa13976f9d5..b5df8c9ba14 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr @@ -1,9 +1,8 @@ use dep::aztec::{ - keys::getters::get_ivpk_m_with_npk_m_hash, prelude::{AztecAddress, NoteHeader, NoteInterface, PrivateContext}, - protocol_types::constants::GENERATOR_INDEX__NOTE_NULLIFIER, - note::utils::compute_note_hash_for_consumption, hash::poseidon2_hash, - oracle::{unsafe_rand::unsafe_rand, nullifier_key::get_nsk_app_with_npk_m_hash} + protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, grumpkin_point::GrumpkinPoint, hash::poseidon2_hash}, + note::utils::compute_note_hash_for_consumption, oracle::unsafe_rand::unsafe_rand, + keys::getters::get_nsk_app }; trait OwnedNote { @@ -18,8 +17,7 @@ global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header. struct TokenNote { // The amount of tokens in the note amount: U128, - // The nullifying public key hash of the person who owns the note. - // This is used with the app_nullifier_secret_key to ensure that the note can be privately spent. + // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. npk_m_hash: Field, // Randomness of the note to hide its contents randomness: Field, @@ -29,7 +27,7 @@ impl NoteInterface for TokenNote { // docs:start:nullifier fn compute_nullifier(self, context: &mut PrivateContext) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = context.request_nsk_app_with_npk_m_hash(self.npk_m_hash); + let secret = context.request_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -40,7 +38,7 @@ impl NoteInterface for TokenNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_consumption(self); - let secret = get_nsk_app_with_npk_m_hash(self.npk_m_hash); + let secret = get_nsk_app(self.npk_m_hash); poseidon2_hash([ note_hash_for_nullify, secret, @@ -49,11 +47,10 @@ impl NoteInterface for TokenNote { } // Broadcasts the note as an encrypted log on L1. - fn broadcast(self, context: &mut PrivateContext, slot: Field) { + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { // We only bother inserting the note if non-empty to save funds on gas. // TODO: (#5901) This will be changed a lot, as it should use the updated encrypted log format if !(self.amount == U128::from_integer(0)) { - let ivpk_m = get_ivpk_m_with_npk_m_hash(self.npk_m_hash); context.emit_encrypted_log( (*context).this_address(), slot, diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/types/transparent_note.nr b/noir-projects/noir-contracts/contracts/token_contract/src/types/transparent_note.nr index d5cf7197cef..061ca460642 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/types/transparent_note.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/types/transparent_note.nr @@ -1,8 +1,8 @@ // docs:start:token_types_all use dep::aztec::{ note::{note_getter_options::PropertySelector, utils::compute_note_hash_for_consumption}, - hash::poseidon2_hash, prelude::{NoteHeader, NoteInterface, PrivateContext}, - protocol_types::constants::GENERATOR_INDEX__NOTE_NULLIFIER + prelude::{NoteHeader, NoteInterface, PrivateContext}, + protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, grumpkin_point::GrumpkinPoint, hash::poseidon2_hash} }; global TRANSPARENT_NOTE_LEN: Field = 2; @@ -59,7 +59,7 @@ impl NoteInterface for TransparentNote { ]) } - fn broadcast(self, context: &mut PrivateContext, slot: Field) { + fn broadcast(self, context: &mut PrivateContext, slot: Field, ivpk_m: GrumpkinPoint) { assert(false, "TransparentNote does not support broadcast"); } } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/private_call_stack_item.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/private_call_stack_item.nr index e3d17abef66..6c9f5e003d7 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/private_call_stack_item.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/private_call_stack_item.nr @@ -85,6 +85,6 @@ fn empty_hash() { let hash = item.hash(); // Value from private_call_stack_item.test.ts "computes empty item hash" test - let test_data_empty_hash = 0x138c6ad441864ce43487e99d5e1e122c38b4b55d893edec04a32f5aacecc856c; + let test_data_empty_hash = 0x092493dbe24afc80afb32c5ba43dde1eb447a66f88a36676a63cdd04f21636ad; assert_eq(hash, test_data_empty_hash); } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/private_circuit_public_inputs.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/private_circuit_public_inputs.nr index fd62b5efee9..c9b97cbcc0a 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/private_circuit_public_inputs.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/private_circuit_public_inputs.nr @@ -221,6 +221,6 @@ fn empty_hash() { let inputs = PrivateCircuitPublicInputs::empty(); let hash = inputs.hash(); // Value from private_circuit_public_inputs.test.ts "computes empty item hash" test - let test_data_empty_hash = 0x2517b9a84487bde68e18647e59530c6ffe4a7a88c5c556f013d09fd22b84ba35; + let test_data_empty_hash = 0x2a6e17b55aa91be7eb562d5fcf1bd67a0abed9098ec2d1a9c7e68541e7518326; assert_eq(hash, test_data_empty_hash); } 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 1799f1e176a..d6e99a66310 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -32,7 +32,7 @@ global MAX_PUBLIC_DATA_READS_PER_CALL: u64 = 16; global MAX_NOTE_HASH_READ_REQUESTS_PER_CALL: u64 = 32; global MAX_NULLIFIER_READ_REQUESTS_PER_CALL: u64 = 2; // Change it to a larger value when there's a seperate reset circuit. global MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_CALL: u64 = 2; -global MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL: u64 = 1; +global MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL: u64 = 16; // THIS SHOULD EQUAL MAX_NEW_NULLIFIERS_PER_CALL global MAX_ENCRYPTED_LOGS_PER_CALL: u64 = 4; // If modifying, update DEPLOYER_CONTRACT_ADDRESS. global MAX_UNENCRYPTED_LOGS_PER_CALL: u64 = 4; // If modifying, update DEPLOYER_CONTRACT_ADDRESS. @@ -47,7 +47,7 @@ global MAX_NEW_L2_TO_L1_MSGS_PER_TX: u64 = 2; global MAX_NOTE_HASH_READ_REQUESTS_PER_TX: u64 = 128; global MAX_NULLIFIER_READ_REQUESTS_PER_TX: u64 = 8; // Change it to a larger value when there's a seperate reset circuit. global MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX: u64 = 8; -global MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX: u64 = 4; +global MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX: u64 = 64; // THIS SHOULD EQUAL MAX_NEW_NULLIFIERS_PER_TX global MAX_ENCRYPTED_LOGS_PER_TX: u64 = 8; global MAX_UNENCRYPTED_LOGS_PER_TX: u64 = 8; global NUM_ENCRYPTED_LOGS_HASHES_PER_TX: u64 = 1; diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/grumpkin_point.nr b/noir-projects/noir-protocol-circuits/crates/types/src/grumpkin_point.nr index 467a022947b..a7caaa39751 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/grumpkin_point.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/grumpkin_point.nr @@ -1,4 +1,4 @@ -use crate::traits::{Serialize, Deserialize}; +use crate::{traits::{Serialize, Deserialize, Hash}, hash::poseidon2_hash}; use dep::std::cmp::Eq; global GRUMPKIN_POINT_SERIALIZED_LEN: Field = 2; @@ -30,6 +30,12 @@ impl Eq for GrumpkinPoint { } } +impl Hash for GrumpkinPoint { + fn hash(self) -> Field { + poseidon2_hash(self.serialize()) + } +} + impl GrumpkinPoint { pub fn new(x: Field, y: Field) -> Self { Self { x, y } diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index 200ad930dee..e94c9920359 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -15,7 +15,7 @@ import { type TxHash, type TxReceipt, } from '@aztec/circuit-types'; -import { type AztecAddress, type CompleteAddress, type Fr, type PartialAddress } from '@aztec/circuits.js'; +import { type AztecAddress, type CompleteAddress, type Fq, type Fr, type PartialAddress } from '@aztec/circuits.js'; import { type ContractArtifact } from '@aztec/foundation/abi'; import { type ContractClassWithId, type ContractInstanceWithAddress } from '@aztec/types/contracts'; import { type NodeInfo } from '@aztec/types/interfaces'; @@ -69,6 +69,9 @@ export abstract class BaseWallet implements Wallet { registerAccount(secretKey: Fr, partialAddress: PartialAddress): Promise { return this.pxe.registerAccount(secretKey, partialAddress); } + rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq): Promise { + return this.pxe.rotateMasterNullifierKey(account, secretKey); + } registerRecipient(account: CompleteAddress): Promise { return this.pxe.registerRecipient(account); } diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index 9e01820e4f7..0ad069a0ceb 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -1,4 +1,4 @@ -import { type AztecAddress, type CompleteAddress, type Fr, type PartialAddress } from '@aztec/circuits.js'; +import { type AztecAddress, type CompleteAddress, type Fq, type Fr, type PartialAddress } from '@aztec/circuits.js'; import { type ContractArtifact } from '@aztec/foundation/abi'; import { type ContractClassWithId, type ContractInstanceWithAddress } from '@aztec/types/contracts'; import { type NodeInfo } from '@aztec/types/interfaces'; @@ -61,6 +61,8 @@ export interface PXE { */ registerAccount(secretKey: Fr, partialAddress: PartialAddress): Promise; + rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq): Promise; + /** * Registers a recipient in PXE. This is required when sending encrypted notes to * a user who hasn't deployed their account contract yet. Since their account is not deployed, their diff --git a/yarn-project/circuit-types/src/keys/key_store.ts b/yarn-project/circuit-types/src/keys/key_store.ts index 256db6e36a9..1dbeeb7831c 100644 --- a/yarn-project/circuit-types/src/keys/key_store.ts +++ b/yarn-project/circuit-types/src/keys/key_store.ts @@ -1,6 +1,7 @@ import { type AztecAddress, type CompleteAddress, + type Fq, type Fr, type GrumpkinPrivateKey, type PartialAddress, @@ -32,12 +33,12 @@ export interface KeyStore { getAccounts(): Promise; /** - * Gets the master nullifier public key for a given account or master nullifier public key hash. - * @throws If the account does not exist in the key store. - * @param accountOrNpkMHash - account address or master nullifier public key hash. + * Gets the master nullifier public key for a given master nullifier public key hash. + * @throws If the account corresponding to the master nullifier public key hash does not exist in the key store. + * @param npkMHash - The master nullifier public key hash. * @returns The master nullifier public key for the account. */ - getMasterNullifierPublicKey(accountOrNpkMHash: AztecAddress | Fr): Promise; + getMasterNullifierPublicKey(npkMHash: Fr): Promise; /** * Gets the master incoming viewing public key for a given account. @@ -64,13 +65,13 @@ export interface KeyStore { getMasterTaggingPublicKey(account: AztecAddress): Promise; /** - * Derives and returns the application nullifier secret key for a given account or master nullifier public key hash. - * @throws If the account does not exist in the key store. - * @param accountOrNpkMHash - account address or master nullifier public key hash. + * Derives and returns the application nullifier secret key for a given master nullifier public key hash. + * @throws If the account corresponding to the master nullifier public key hash does not exist in the key store. + * @param npkMHash - The master nullifier public key hash. * @param app - The application address to retrieve the nullifier secret key for. * @returns A Promise that resolves to the application nullifier secret key. */ - getAppNullifierSecretKey(accountOrNpkMHash: AztecAddress | Fr, app: AztecAddress): Promise; + getAppNullifierSecretKey(npkMHash: Fr, app: AztecAddress): Promise; /** * Retrieves application incoming viewing secret key. @@ -117,4 +118,6 @@ export interface KeyStore { * @returns A Promise that resolves to the public keys hash. */ getPublicKeysHash(account: AztecAddress): Promise; + + rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq): Promise; } diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index ecf31fe3334..5b4153088f7 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -11,7 +11,7 @@ export const MAX_PUBLIC_DATA_READS_PER_CALL = 16; export const MAX_NOTE_HASH_READ_REQUESTS_PER_CALL = 32; export const MAX_NULLIFIER_READ_REQUESTS_PER_CALL = 2; export const MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_CALL = 2; -export const MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL = 1; +export const MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL = 16; export const MAX_ENCRYPTED_LOGS_PER_CALL = 4; export const MAX_UNENCRYPTED_LOGS_PER_CALL = 4; export const MAX_NEW_NOTE_HASHES_PER_TX = 64; @@ -24,7 +24,7 @@ export const MAX_NEW_L2_TO_L1_MSGS_PER_TX = 2; export const MAX_NOTE_HASH_READ_REQUESTS_PER_TX = 128; export const MAX_NULLIFIER_READ_REQUESTS_PER_TX = 8; export const MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX = 8; -export const MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX = 4; +export const MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX = 64; export const MAX_ENCRYPTED_LOGS_PER_TX = 8; export const MAX_UNENCRYPTED_LOGS_PER_TX = 8; export const NUM_ENCRYPTED_LOGS_HASHES_PER_TX = 1; diff --git a/yarn-project/circuits.js/src/keys/index.ts b/yarn-project/circuits.js/src/keys/index.ts index 3de9716a203..a994d96e5ae 100644 --- a/yarn-project/circuits.js/src/keys/index.ts +++ b/yarn-project/circuits.js/src/keys/index.ts @@ -1,6 +1,6 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { poseidon2Hash, sha512ToGrumpkinScalar } from '@aztec/foundation/crypto'; -import { type Fr, type GrumpkinScalar } from '@aztec/foundation/fields'; +import { type Fq, type Fr, type GrumpkinScalar } from '@aztec/foundation/fields'; import { Grumpkin } from '../barretenberg/crypto/grumpkin/index.js'; import { GeneratorIndex } from '../constants.gen.js'; @@ -29,13 +29,17 @@ export function computeAddress(publicKeysHash: Fr, partialAddress: Fr) { return AztecAddress.fromField(addressFr); } +export function derivePublicKeyFromSecretKey(secretKey: Fq) { + const curve = new Grumpkin(); + return curve.mul(curve.generator(), secretKey); +} + /** * Computes secret and public keys and public keys hash from a secret key. * @param secretKey - The secret key to derive keys from. * @returns The derived keys. */ export function deriveKeys(secretKey: Fr) { - const curve = new Grumpkin(); // First we derive master secret keys - we use sha512 here because this derivation will never take place // in a circuit const masterNullifierSecretKey = deriveMasterNullifierSecretKey(secretKey); @@ -44,11 +48,17 @@ export function deriveKeys(secretKey: Fr) { const masterTaggingSecretKey = sha512ToGrumpkinScalar([secretKey, GeneratorIndex.TSK_M]); // Then we derive master public keys + const masterNullifierPublicKey = derivePublicKeyFromSecretKey(masterNullifierSecretKey); + const masterIncomingViewingPublicKey = derivePublicKeyFromSecretKey(masterIncomingViewingSecretKey); + const masterOutgoingViewingPublicKey = derivePublicKeyFromSecretKey(masterOutgoingViewingSecretKey); + const masterTaggingPublicKey = derivePublicKeyFromSecretKey(masterTaggingSecretKey); + + // We hash the public keys to get the public keys hash const publicKeys = new PublicKeys( - curve.mul(curve.generator(), masterNullifierSecretKey), - curve.mul(curve.generator(), masterIncomingViewingSecretKey), - curve.mul(curve.generator(), masterOutgoingViewingSecretKey), - curve.mul(curve.generator(), masterTaggingSecretKey), + masterNullifierPublicKey, + masterIncomingViewingPublicKey, + masterOutgoingViewingPublicKey, + masterTaggingPublicKey, ); return { diff --git a/yarn-project/circuits.js/src/structs/__snapshots__/private_call_stack_item.test.ts.snap b/yarn-project/circuits.js/src/structs/__snapshots__/private_call_stack_item.test.ts.snap index 2de7a0cede8..161ec6eecc5 100644 --- a/yarn-project/circuits.js/src/structs/__snapshots__/private_call_stack_item.test.ts.snap +++ b/yarn-project/circuits.js/src/structs/__snapshots__/private_call_stack_item.test.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PrivateCallStackItem computes empty item hash 1`] = `Fr<0x138c6ad441864ce43487e99d5e1e122c38b4b55d893edec04a32f5aacecc856c>`; +exports[`PrivateCallStackItem computes empty item hash 1`] = `Fr<0x092493dbe24afc80afb32c5ba43dde1eb447a66f88a36676a63cdd04f21636ad>`; -exports[`PrivateCallStackItem computes hash 1`] = `Fr<0x2078c0fe8fa7dc6d0c4623ec068d3297e027e60131ff4b0e333a99f72503aa32>`; +exports[`PrivateCallStackItem computes hash 1`] = `Fr<0x0a3f795ce85d43fddedcde7711982cf50382e3da1c0ad7afbbc2cb7a219a46c3>`; diff --git a/yarn-project/circuits.js/src/structs/__snapshots__/private_circuit_public_inputs.test.ts.snap b/yarn-project/circuits.js/src/structs/__snapshots__/private_circuit_public_inputs.test.ts.snap index 4ace7377315..b741933be5b 100644 --- a/yarn-project/circuits.js/src/structs/__snapshots__/private_circuit_public_inputs.test.ts.snap +++ b/yarn-project/circuits.js/src/structs/__snapshots__/private_circuit_public_inputs.test.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PrivateCircuitPublicInputs computes empty inputs hash 1`] = `Fr<0x2517b9a84487bde68e18647e59530c6ffe4a7a88c5c556f013d09fd22b84ba35>`; +exports[`PrivateCircuitPublicInputs computes empty inputs hash 1`] = `Fr<0x2a6e17b55aa91be7eb562d5fcf1bd67a0abed9098ec2d1a9c7e68541e7518326>`; -exports[`PrivateCircuitPublicInputs hash matches snapshot 1`] = `Fr<0x0e570673c6fee73b2c55d8acff12bdd9084820e6448c32cfb2600847f493bec1>`; +exports[`PrivateCircuitPublicInputs hash matches snapshot 1`] = `Fr<0x112e706493a7ca33bb253550f8c1e4822c8193e4e2367aefda0f40fba4063455>`; \ No newline at end of file diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts index 97a91be1e12..2dedcbade81 100644 --- a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -188,7 +188,8 @@ describe('e2e_crowdfunding_and_claim', () => { nonce: noteNonces[0], }, value: extendedNote.note.items[0], - owner: extendedNote.note.items[1], + // eslint-disable-next-line camelcase + npk_m_hash: extendedNote.note.items[1], randomness: extendedNote.note.items[2], }; }; diff --git a/yarn-project/end-to-end/src/e2e_delegate_calls/delegate.test.ts b/yarn-project/end-to-end/src/e2e_delegate_calls/delegate.test.ts index 9f6890efb00..60117019d07 100644 --- a/yarn-project/end-to-end/src/e2e_delegate_calls/delegate.test.ts +++ b/yarn-project/end-to-end/src/e2e_delegate_calls/delegate.test.ts @@ -24,14 +24,13 @@ describe('e2e_delegate_calls', () => { .wait(); const delegatorValue = await delegatorContract.methods - .view_private_value(sentValue, wallet.getCompleteAddress().address) + .get_private_value(sentValue, wallet.getCompleteAddress().address) .simulate(); - const delegatedOnValue = await delegatedOnContract.methods - .view_private_value(sentValue, wallet.getCompleteAddress().address) - .simulate(); + await expect( + delegatedOnContract.methods.get_private_value(sentValue, wallet.getCompleteAddress().address).simulate(), + ).rejects.toThrow(`Assertion failed: Cannot return zero notes 'num_notes != 0'`); - expect(delegatedOnValue).toEqual(0n); expect(delegatorValue).toEqual(sentValue); }); diff --git a/yarn-project/end-to-end/src/e2e_key_rotation.test.ts b/yarn-project/end-to-end/src/e2e_key_rotation.test.ts new file mode 100644 index 00000000000..3f2a3dc22e7 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_key_rotation.test.ts @@ -0,0 +1,233 @@ +import { createAccounts } from '@aztec/accounts/testing'; +import { + type AztecAddress, + type AztecNode, + type DebugLogger, + ExtendedNote, + Fq, + Fr, + Note, + type PXE, + type TxHash, + type Wallet, + computeSecretHash, + retryUntil, +} from '@aztec/aztec.js'; +import { type PublicKey, derivePublicKeyFromSecretKey } from '@aztec/circuits.js'; +import { KeyRegistryContract, TestContract, TokenContract } from '@aztec/noir-contracts.js'; +import { getCanonicalKeyRegistryAddress } from '@aztec/protocol-contracts/key-registry'; + +import { jest } from '@jest/globals'; + +import { expectsNumOfEncryptedLogsInTheLastBlockToBe, setup, setupPXEService } from './fixtures/utils.js'; + +const TIMEOUT = 120_000; + +const SHARED_MUTABLE_DELAY = 5; + +describe('e2e_key_rotation', () => { + jest.setTimeout(TIMEOUT); + + let aztecNode: AztecNode; + let pxeA: PXE; + let pxeB: PXE; + let walletA: Wallet; + let walletB: Wallet; + let logger: DebugLogger; + let teardownA: () => Promise; + let teardownB: () => Promise; + + let keyRegistryWithB: KeyRegistryContract; + let testContract: TestContract; + let contractWithWalletA: TokenContract; + let contractWithWalletB: TokenContract; + + let tokenAddress: AztecAddress; + + const initialBalance = 987n; + + beforeAll(async () => { + ({ + aztecNode, + pxe: pxeA, + wallets: [walletA], + logger, + teardown: teardownA, + } = await setup(1)); + + ({ pxe: pxeB, teardown: teardownB } = await setupPXEService(aztecNode, {}, undefined, true)); + + [walletB] = await createAccounts(pxeB, 1); + keyRegistryWithB = await KeyRegistryContract.at(getCanonicalKeyRegistryAddress(), walletB); + + // We deploy test and token contracts + testContract = await TestContract.deploy(walletA).send().deployed(); + const tokenInstance = await deployTokenContract(initialBalance, walletA.getAddress(), pxeA); + tokenAddress = tokenInstance.address; + + // Add account B to wallet A + await pxeA.registerRecipient(walletB.getCompleteAddress()); + // Add account A to wallet B + await pxeB.registerRecipient(walletA.getCompleteAddress()); + + // Add token to PXE B (PXE A already has it because it was deployed through it) + await pxeB.registerContract({ + artifact: TokenContract.artifact, + instance: tokenInstance, + }); + + contractWithWalletA = await TokenContract.at(tokenAddress, walletA); + contractWithWalletB = await TokenContract.at(tokenAddress, walletB); + }); + + afterEach(async () => { + await teardownB(); + await teardownA(); + }); + + const awaitUserSynchronized = async (wallet: Wallet, owner: AztecAddress) => { + const isUserSynchronized = async () => { + return await wallet.isAccountStateSynchronized(owner); + }; + await retryUntil(isUserSynchronized, `synch of user ${owner.toString()}`, 10); + }; + + const crossDelay = async () => { + for (let i = 0; i < SHARED_MUTABLE_DELAY; i++) { + // We send arbitrary tx to mine a block + await testContract.methods.emit_unencrypted(0).send().wait(); + } + }; + + const expectTokenBalance = async ( + wallet: Wallet, + tokenAddress: AztecAddress, + owner: AztecAddress, + expectedBalance: bigint, + checkIfSynchronized = true, + ) => { + if (checkIfSynchronized) { + // First wait until the corresponding PXE has synchronized the account + await awaitUserSynchronized(wallet, owner); + } + + // Then check the balance + const contractWithWallet = await TokenContract.at(tokenAddress, wallet); + const balance = await contractWithWallet.methods.balance_of_private(owner).simulate({ from: owner }); + logger.info(`Account ${owner} balance: ${balance}`); + expect(balance).toBe(expectedBalance); + }; + + const deployTokenContract = async (initialAdminBalance: bigint, admin: AztecAddress, pxe: PXE) => { + logger.info(`Deploying Token contract...`); + const contract = await TokenContract.deploy(walletA, admin, 'TokenName', 'TokenSymbol', 18).send().deployed(); + + if (initialAdminBalance > 0n) { + await mintTokens(contract, admin, initialAdminBalance, pxe); + } + + logger.info('L2 contract deployed'); + + return contract.instance; + }; + + const mintTokens = async (contract: TokenContract, recipient: AztecAddress, balance: bigint, pxe: PXE) => { + const secret = Fr.random(); + const secretHash = computeSecretHash(secret); + + const receipt = await contract.methods.mint_private(balance, secretHash).send().wait(); + + const note = new Note([new Fr(balance), secretHash]); + const extendedNote = new ExtendedNote( + note, + recipient, + contract.address, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, + receipt.txHash, + ); + await pxe.addNote(extendedNote); + + await contract.methods.redeem_shield(recipient, balance, secret).send().wait(); + }; + + it(`Rotates keys and uses them`, async () => { + // 1. We check that setup set initial balances as expected + await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance); + await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), 0n); + + // 2. Transfer funds from A to B via PXE A + let txHashTransfer1: TxHash; + const transfer1Amount = 654n; + { + ({ txHash: txHashTransfer1 } = await contractWithWalletA.methods + .transfer(walletA.getAddress(), walletB.getAddress(), transfer1Amount, 0) + .send() + .wait()); + + // Check balances and logs are as expected + await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance - transfer1Amount); + await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), transfer1Amount); + await expectsNumOfEncryptedLogsInTheLastBlockToBe(aztecNode, 2); + } + + // 3. Rotates B key + let newNpkM: PublicKey; + { + const newNskM = Fq.random(); + newNpkM = derivePublicKeyFromSecretKey(newNskM); + await pxeB.rotateMasterNullifierKey(walletB.getAddress(), newNskM); + + await keyRegistryWithB.methods.rotate_npk_m(walletB.getAddress(), newNpkM, 0).send().wait(); + await crossDelay(); + } + + // 4. Transfer funds from A to B via PXE A + let txHashTransfer2: TxHash; + const transfer2Amount = 321n; + { + ({ txHash: txHashTransfer2 } = await contractWithWalletA.methods + .transfer(walletA.getAddress(), walletB.getAddress(), transfer2Amount, 0) + .send() + .wait()); + + await expectTokenBalance( + walletA, + tokenAddress, + walletA.getAddress(), + initialBalance - transfer1Amount - transfer2Amount, + ); + await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), transfer1Amount + transfer2Amount); + } + + // 5. Now we check that a correct nullifier keys were used in both transfers + { + await awaitUserSynchronized(walletB, walletB.getAddress()); + const transfer1Notes = await walletB.getNotes({ txHash: txHashTransfer1 }); + const transfer2Notes = await walletB.getNotes({ txHash: txHashTransfer2 }); + expect(transfer1Notes.length).toBe(1); + expect(transfer2Notes.length).toBe(1); + // Second field in the token note is the npk_m_hash + const noteNpkMHashTransfer1 = transfer1Notes[0].note.items[1]; + const noteNpkMHashTransfer2 = transfer2Notes[0].note.items[1]; + + // Now we check the note created in transfer 2 used the new npk_m_hash + expect(noteNpkMHashTransfer2.equals(newNpkM.hash())).toBe(true); + // We sanity check that the note created in transfer 1 had old npk_m_hash by checking it's different from the new + // one + expect(noteNpkMHashTransfer2.equals(noteNpkMHashTransfer1)).toBe(false); + } + + // 6. Finally we check that all the B notes are spendable by transferring full B balance to A + // --> this way we verify that it's possible to obtain both keys via oracles + { + await contractWithWalletB.methods + .transfer(walletB.getAddress(), walletA.getAddress(), transfer1Amount + transfer2Amount, 0) + .send() + .wait(); + + await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance); + await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), 0n); + } + }, 600_000); +}); diff --git a/yarn-project/foundation/src/fields/point.ts b/yarn-project/foundation/src/fields/point.ts index dfb5065c03d..9fd8e8fdf03 100644 --- a/yarn-project/foundation/src/fields/point.ts +++ b/yarn-project/foundation/src/fields/point.ts @@ -1,3 +1,4 @@ +import { poseidon2Hash } from '../crypto/index.js'; import { BufferReader, FieldReader, serializeToBuffer } from '../serialize/index.js'; import { Fr } from './fields.js'; @@ -129,6 +130,10 @@ export class Point { isZero() { return this.x.isZero() && this.y.isZero(); } + + hash() { + return poseidon2Hash(this.toFields()); + } } /** diff --git a/yarn-project/key-store/src/test_key_store.test.ts b/yarn-project/key-store/src/test_key_store.test.ts index 2395dbf1472..1dde6a3f02b 100644 --- a/yarn-project/key-store/src/test_key_store.test.ts +++ b/yarn-project/key-store/src/test_key_store.test.ts @@ -1,4 +1,11 @@ -import { AztecAddress, Fr } from '@aztec/circuits.js'; +import { + AztecAddress, + Fq, + Fr, + computeAppNullifierSecretKey, + deriveKeys, + derivePublicKeyFromSecretKey, +} from '@aztec/circuits.js'; import { openTmpStore } from '@aztec/kv-store/utils'; import { TestKeyStore } from './test_key_store.js'; @@ -9,6 +16,10 @@ describe('TestKeyStore', () => { // Arbitrary fixed values const sk = new Fr(8923n); + const keys = deriveKeys(sk); + const derivedMasterNullifierPublicKey = derivePublicKeyFromSecretKey(keys.masterNullifierSecretKey); + const computedMasterNullifierPublicKeyHash = derivedMasterNullifierPublicKey.hash(); + const partialAddress = new Fr(243523n); const { address: accountAddress } = await keyStore.addAccount(sk, partialAddress); @@ -16,7 +27,7 @@ describe('TestKeyStore', () => { `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`, ); - const masterNullifierPublicKey = await keyStore.getMasterNullifierPublicKey(accountAddress); + const masterNullifierPublicKey = await keyStore.getMasterNullifierPublicKey(computedMasterNullifierPublicKeyHash); expect(masterNullifierPublicKey.toString()).toMatchInlineSnapshot( `"0x2ef5d15dd65d29546680ab72846fb071f41cb9f2a0212215e6c560e29df4ff650ce764818364b376be92dc2f49577fe440e64a16012584f7c4ee94f7edbc323a"`, ); @@ -44,7 +55,10 @@ describe('TestKeyStore', () => { // Arbitrary app contract address const appAddress = AztecAddress.fromBigInt(624n); - const appNullifierSecretKey = await keyStore.getAppNullifierSecretKey(accountAddress, appAddress); + const appNullifierSecretKey = await keyStore.getAppNullifierSecretKey( + computedMasterNullifierPublicKeyHash, + appAddress, + ); expect(appNullifierSecretKey.toString()).toMatchInlineSnapshot( `"0x230a44dfe7cfec7a735c89f7289c5cb5d2c3dc0bf5d3505917fd2476f67873a8"`, ); @@ -71,4 +85,77 @@ describe('TestKeyStore', () => { `"0x0fde74d5e504c73b58aad420dd72590fc6004571411e7f77c45378714195a52b"`, ); }); + + it('nullifier key rotation tests', async () => { + const keyStore = new TestKeyStore(openTmpStore()); + + // Arbitrary fixed values + const sk = new Fr(8923n); + const partialAddress = new Fr(243523n); + + const { address: accountAddress } = await keyStore.addAccount(sk, partialAddress); + expect(accountAddress.toString()).toMatchInlineSnapshot( + `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`, + ); + + // Arbitrary fixed values + const newMasterNullifierSecretKeys = [new Fq(420n), new Fq(69n), new Fq(42069n)]; + const newDerivedMasterNullifierPublicKeys = [ + derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[0]), + derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[1]), + derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[2]), + ]; + + const newComputedMasterNullifierPublicKeyHashes = [ + newDerivedMasterNullifierPublicKeys[0].hash(), + newDerivedMasterNullifierPublicKeys[1].hash(), + newDerivedMasterNullifierPublicKeys[2].hash(), + ]; + + // We rotate our nullifier key + await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[0]); + await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[1]); + await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[2]); + + // We make sure we can get master nullifier public keys with master nullifier public key hashes + expect(await keyStore.getMasterNullifierPublicKey(newComputedMasterNullifierPublicKeyHashes[2])).toEqual( + newDerivedMasterNullifierPublicKeys[2], + ); + expect(await keyStore.getMasterNullifierPublicKey(newComputedMasterNullifierPublicKeyHashes[1])).toEqual( + newDerivedMasterNullifierPublicKeys[1], + ); + expect(await keyStore.getMasterNullifierPublicKey(newComputedMasterNullifierPublicKeyHashes[0])).toEqual( + newDerivedMasterNullifierPublicKeys[0], + ); + + // Arbitrary app contract address + const appAddress = AztecAddress.fromBigInt(624n); + + // We make sure we can get app nullifier secret keys with master nullifier public key hashes + const appNullifierSecretKey0 = await keyStore.getAppNullifierSecretKey( + newComputedMasterNullifierPublicKeyHashes[0], + appAddress, + ); + expect(appNullifierSecretKey0.toString()).toMatchInlineSnapshot( + `"0x296e42f1039b62290372d608fcab55b00a3f96c1c8aa347b2a830639c5a12757"`, + ); + const appNullifierSecretKey1 = await keyStore.getAppNullifierSecretKey( + newComputedMasterNullifierPublicKeyHashes[1], + appAddress, + ); + expect(appNullifierSecretKey1.toString()).toMatchInlineSnapshot( + `"0x019f2a705b68683f1d86da639a543411fa779af41896c3920d0c2d5226c686dd"`, + ); + const appNullifierSecretKey2 = await keyStore.getAppNullifierSecretKey( + newComputedMasterNullifierPublicKeyHashes[2], + appAddress, + ); + expect(appNullifierSecretKey2.toString()).toMatchInlineSnapshot( + `"0x117445c8819c06b9a0889e5cce1f550e32ec6993c23f57bc9fc5cda05df520ae"`, + ); + + expect(appNullifierSecretKey0).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[0], appAddress)); + expect(appNullifierSecretKey1).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[1], appAddress)); + expect(appNullifierSecretKey2).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[2], appAddress)); + }); }); diff --git a/yarn-project/key-store/src/test_key_store.ts b/yarn-project/key-store/src/test_key_store.ts index 2d0e0672104..6e5416be404 100644 --- a/yarn-project/key-store/src/test_key_store.ts +++ b/yarn-project/key-store/src/test_key_store.ts @@ -2,6 +2,7 @@ import { type KeyStore, type PublicKey } from '@aztec/circuit-types'; import { AztecAddress, CompleteAddress, + Fq, Fr, GeneratorIndex, type GrumpkinPrivateKey, @@ -11,6 +12,7 @@ import { computeAddress, computeAppNullifierSecretKey, deriveKeys, + derivePublicKeyFromSecretKey, } from '@aztec/circuits.js'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { type AztecKVStore, type AztecMap } from '@aztec/kv-store'; @@ -58,16 +60,23 @@ export class TestKeyStore implements KeyStore { await this.#keys.set(`${accountAddress.toString()}-public_keys_hash`, publicKeysHash.toBuffer()); // Naming of keys is as follows ${from}-${to}_m - await this.#keys.set(`${accountAddress.toString()}-nsk_m`, masterNullifierSecretKey.toBuffer()); await this.#keys.set(`${accountAddress.toString()}-ivsk_m`, masterIncomingViewingSecretKey.toBuffer()); await this.#keys.set(`${accountAddress.toString()}-ovsk_m`, masterOutgoingViewingSecretKey.toBuffer()); await this.#keys.set(`${accountAddress.toString()}-tsk_m`, masterTaggingSecretKey.toBuffer()); + // The key of the following is different from the others because the buffer can store multiple keys + await this.#keys.set(`${accountAddress.toString()}-ns_keys_m`, masterNullifierSecretKey.toBuffer()); - await this.#keys.set(`${accountAddress.toString()}-npk_m`, publicKeys.masterNullifierPublicKey.toBuffer()); + await this.#keys.set(`${accountAddress.toString()}-np_keys_m`, publicKeys.masterNullifierPublicKey.toBuffer()); await this.#keys.set(`${accountAddress.toString()}-ivpk_m`, publicKeys.masterIncomingViewingPublicKey.toBuffer()); await this.#keys.set(`${accountAddress.toString()}-ovpk_m`, publicKeys.masterOutgoingViewingPublicKey.toBuffer()); await this.#keys.set(`${accountAddress.toString()}-tpk_m`, publicKeys.masterTaggingPublicKey.toBuffer()); + // We store a npk_m_hash-account_address map to make address easy to obtain with the hash later on + await this.#keys.set( + `${publicKeys.masterNullifierPublicKey.hash().toString()}-npk_m_hash`, + accountAddress.toBuffer(), + ); + // At last, we return the newly derived account address return Promise.resolve(new CompleteAddress(accountAddress, publicKeys, partialAddress)); } @@ -78,28 +87,49 @@ export class TestKeyStore implements KeyStore { */ public getAccounts(): Promise { const allMapKeys = Array.from(this.#keys.keys()); - // We return account addresses based on the map keys that end with '-nsk_m' - const accounts = allMapKeys.filter(key => key.endsWith('-nsk_m')).map(key => key.split('-')[0]); + // We return account addresses based on the map keys that end with '-ivsk_m' + const accounts = allMapKeys.filter(key => key.endsWith('-ivsk_m')).map(key => key.split('-')[0]); return Promise.resolve(accounts.map(account => AztecAddress.fromString(account))); } /** - * Gets the master nullifier public key for a given account or master nullifier public key hash. - * @throws If the account does not exist in the key store. - * @param accountOrNpkMHash - account address or master nullifier public key hash. + * Gets the master nullifier public key for a given master nullifier public key hash. + * @throws If the account corresponding to the master nullifier public key hash does not exist in the key store. + * @param npkMHash - The master nullifier public key hash. * @returns The master nullifier public key for the account. */ - public async getMasterNullifierPublicKey(accountOrNpkMHash: AztecAddress | Fr): Promise { - const masterNullifierPublicKeyBuffer = - this.#keys.get(`${accountOrNpkMHash.toString()}-npk_m`) ?? - this.#keys.get(`${this.#getAccountAddressForMasterNullifierPublicKeyHash(accountOrNpkMHash)?.toString()}-npk_m`); + public getMasterNullifierPublicKey(npkMHash: Fr): Promise { + // Get the address for npk_m_hash + const accountAddressBuffer = this.#keys.get(`${npkMHash.toString()}-npk_m_hash`); + if (!accountAddressBuffer) { + throw new Error(`Could no find address for master nullifier public key hash ${npkMHash}.`); + } + const accountAddress = AztecAddress.fromBuffer(accountAddressBuffer); - if (!masterNullifierPublicKeyBuffer) { + // Get the master nullifier public keys buffer for the account + const masterNullifierPublicKeysBuffer = this.#keys.get(`${accountAddress.toString()}-np_keys_m`); + if (!masterNullifierPublicKeysBuffer) { throw new Error( - `Account or master nullifier public key hash ${accountOrNpkMHash} does not exist. Registered accounts: ${await this.getAccounts()}.`, + `Could not find master nullifier public key for account ${accountAddress.toString()} whose address was successfully obtained with npk_m_hash ${npkMHash.toString()}.`, ); } - return Promise.resolve(Point.fromBuffer(masterNullifierPublicKeyBuffer)); + + // We check that the buffer's length is a multiple of Point.SIZE_IN_BYTES + if (masterNullifierPublicKeysBuffer.byteLength % Point.SIZE_IN_BYTES !== 0) { + throw new Error("Master nullifier public key buffer's length is not a multiple of Point.SIZE_IN_BYTES."); + } + + // Now we iterate over the public keys in the buffer to find the one that matches the hash + const numKeys = masterNullifierPublicKeysBuffer.byteLength / Point.SIZE_IN_BYTES; + for (let i = 0; i < numKeys; i++) { + const masterNullifierPublicKey = Point.fromBuffer( + masterNullifierPublicKeysBuffer.subarray(i * Point.SIZE_IN_BYTES, (i + 1) * Point.SIZE_IN_BYTES), + ); + if (masterNullifierPublicKey.hash().equals(npkMHash)) { + return Promise.resolve(masterNullifierPublicKey); + } + } + throw new Error(`Could not find master nullifier public key for npk_m_hash ${npkMHash.toString()}.`); } /** @@ -151,25 +181,47 @@ export class TestKeyStore implements KeyStore { } /** - * Derives and returns the application nullifier secret key for a given account or master nullifier public key hash. - * @throws If the account does not exist in the key store. - * @param accountOrNpkMHash - account address or master nullifier public key hash. + * Derives and returns the application nullifier secret key for a given master nullifier public key hash. + * @throws If the account corresponding to the master nullifier public key hash does not exist in the key store. + * @param npkMHash - The master nullifier public key hash. * @param app - The application address to retrieve the nullifier secret key for. * @returns A Promise that resolves to the application nullifier secret key. */ - public async getAppNullifierSecretKey(accountOrNpkMHash: AztecAddress | Fr, app: AztecAddress): Promise { - const masterNullifierSecretKeyBuffer = - this.#keys.get(`${accountOrNpkMHash.toString()}-nsk_m`) ?? - this.#keys.get(`${this.#getAccountAddressForMasterNullifierPublicKeyHash(accountOrNpkMHash)?.toString()}-nsk_m`); + public getAppNullifierSecretKey(npkMHash: Fr, app: AztecAddress): Promise { + // First we get the account address for npk_m_hash + const accountAddressBuffer = this.#keys.get(`${npkMHash.toString()}-npk_m_hash`); + if (!accountAddressBuffer) { + throw new Error(`Could no find address for master nullifier public key hash ${npkMHash}.`); + } - if (!masterNullifierSecretKeyBuffer) { + // Now we get the master nullifier secret keys for the account + const masterNullifierSecretKeysBuffer = this.#keys.get( + `${AztecAddress.fromBuffer(accountAddressBuffer).toString()}-ns_keys_m`, + ); + if (!masterNullifierSecretKeysBuffer) { throw new Error( - `Account or master nullifier public key hash ${accountOrNpkMHash} does not exist. Registered accounts: ${await this.getAccounts()}.`, + `Could not find master nullifier secret keys for account ${AztecAddress.fromBuffer( + accountAddressBuffer, + ).toString()}`, ); } - const masterNullifierSecretKey = GrumpkinScalar.fromBuffer(masterNullifierSecretKeyBuffer); - const appNullifierSecretKey = computeAppNullifierSecretKey(masterNullifierSecretKey, app); - return Promise.resolve(appNullifierSecretKey); + + // Now we iterate over all the secret keys to find the one that matches the hash + const numKeys = masterNullifierSecretKeysBuffer.byteLength / GrumpkinScalar.SIZE_IN_BYTES; + for (let i = 0; i < numKeys; i++) { + const secretKey = GrumpkinScalar.fromBuffer( + masterNullifierSecretKeysBuffer.subarray( + i * GrumpkinScalar.SIZE_IN_BYTES, + (i + 1) * GrumpkinScalar.SIZE_IN_BYTES, + ), + ); + const publicKey = derivePublicKeyFromSecretKey(secretKey); + if (publicKey.hash().equals(npkMHash)) { + return Promise.resolve(computeAppNullifierSecretKey(secretKey, app)); + } + } + + throw new Error(`Could not find master nullifier secret key for npk_m_hash ${npkMHash.toString()}.`); } /** @@ -233,21 +285,60 @@ export class TestKeyStore implements KeyStore { * @dev Used when feeding the master nullifier secret key to the kernel circuit for nullifier keys verification. */ public getMasterNullifierSecretKeyForPublicKey(masterNullifierPublicKey: PublicKey): Promise { - // We iterate over the map keys to find the account address that corresponds to the provided public key - for (const [key, value] of this.#keys.entries()) { - if (value.equals(masterNullifierPublicKey.toBuffer()) && key.endsWith('-npk_m')) { - // We extract the account address from the map key - const accountAddress = key.split('-')[0]; - // We fetch the secret key and return it - const masterNullifierSecretKeyBuffer = this.#keys.get(`${accountAddress.toString()}-nsk_m`); - if (!masterNullifierSecretKeyBuffer) { - throw new Error(`Could not find master nullifier secret key for account ${accountAddress.toString()}`); - } - return Promise.resolve(GrumpkinScalar.fromBuffer(masterNullifierSecretKeyBuffer)); + // We get the account address associated with the master nullifier public key hash + const accountAddressBuffer = this.#keys.get(`${masterNullifierPublicKey.hash().toString()}-npk_m_hash`); + if (!accountAddressBuffer) { + throw new Error( + `Could not find account address for master nullifier public key ${masterNullifierPublicKey.toString()}`, + ); + } + const accountAddress = AztecAddress.fromBuffer(accountAddressBuffer); + + // We fetch the public keys and find this specific public key's position in the buffer + const masterNullifierPublicKeysBuffer = this.#keys.get(`${accountAddress.toString()}-np_keys_m`); + if (!masterNullifierPublicKeysBuffer) { + throw new Error(`Could not find master nullifier public keys for account ${accountAddress.toString()}`); + } + + // We check that the buffer's length is a multiple of Point.SIZE_IN_BYTES + if (masterNullifierPublicKeysBuffer.byteLength % Point.SIZE_IN_BYTES !== 0) { + throw new Error("Master nullifier public key buffer's length is not a multiple of Point.SIZE_IN_BYTES."); + } + + // Now we iterate over the public keys in the buffer to find the one that matches the hash + const numKeys = masterNullifierPublicKeysBuffer.byteLength / Point.SIZE_IN_BYTES; + let keyIndex = -1; + for (let i = 0; i < numKeys; i++) { + const publicKey = Point.fromBuffer( + masterNullifierPublicKeysBuffer.subarray(i * Point.SIZE_IN_BYTES, (i + 1) * Point.SIZE_IN_BYTES), + ); + if (publicKey.equals(masterNullifierPublicKey)) { + keyIndex = i; + break; } } - throw new Error(`Could not find master nullifier secret key for public key ${masterNullifierPublicKey.toString()}`); + // Now we fetch the secret keys buffer and extract the secret key at the same index + const masterNullifierSecretKeysBuffer = this.#keys.get(`${accountAddress.toString()}-ns_keys_m`); + if (!masterNullifierSecretKeysBuffer) { + throw new Error(`Could not find master nullifier secret keys for account ${accountAddress.toString()}`); + } + + // We extract the secret key from the buffer + const secretKeyBuffer = masterNullifierSecretKeysBuffer.subarray( + keyIndex * GrumpkinScalar.SIZE_IN_BYTES, + (keyIndex + 1) * GrumpkinScalar.SIZE_IN_BYTES, + ); + const secretKey = GrumpkinScalar.fromBuffer(secretKeyBuffer); + + // We sanity check that it's possible to derive the public key from the secret key + if (!derivePublicKeyFromSecretKey(secretKey).equals(masterNullifierPublicKey)) { + throw new Error( + `Could not find master nullifier secret key for public key ${masterNullifierPublicKey.toString()}`, + ); + } + + return Promise.resolve(secretKey); } /** @@ -296,18 +387,41 @@ export class TestKeyStore implements KeyStore { return Promise.resolve(Fr.fromBuffer(publicKeysHashBuffer)); } - #getAccountAddressForMasterNullifierPublicKeyHash(masterNullifierPublicKeyHash: Fr): AztecAddress | undefined { - for (const [key, value] of this.#keys.entries()) { - if (key.endsWith('-npk_m')) { - const computedMasterNullifierPublicKeyHash = poseidon2Hash(Point.fromBuffer(value).toFields()); - if (computedMasterNullifierPublicKeyHash.equals(masterNullifierPublicKeyHash)) { - // We extract the account address from the map key - const accountAddress = key.split('-')[0]; - return AztecAddress.fromString(accountAddress); - } - } + /** + * Rotates the master nullifier key for the specified account. + * + * @dev This function updates the secret and public keys associated with the account. + * It appends a new secret key to the existing secret keys, derives the + * corresponding public key, and updates the stored keys accordingly. + * + * @param account - The account address for which the master nullifier key is being rotated. + * @param newSecretKey - (Optional) A new secret key of type Fq. If not provided, a random key is generated. + * @throws If the account does not have existing nullifier secret keys or public keys. + * @returns A Promise that resolves when the key rotation is complete. + */ + public async rotateMasterNullifierKey(account: AztecAddress, newSecretKey: Fq = Fq.random()) { + // We append the secret key to the original secret key + const secretKeysBuffer = this.#keys.get(`${account.toString()}-ns_keys_m`); + if (!secretKeysBuffer) { + throw new Error(`Could not find nullifier secret keys for account ${account.toString()}`); + } + + // We append the new secret key to the buffer of secret keys + const newSecretKeysBuffer = Buffer.concat([secretKeysBuffer, newSecretKey.toBuffer()]); + await this.#keys.set(`${account.toString()}-ns_keys_m`, newSecretKeysBuffer); + + // Now we derive the public key from the new secret key and append it to the buffer of original public keys + const newPublicKey = derivePublicKeyFromSecretKey(newSecretKey); + const publicKeysBuffer = this.#keys.get(`${account.toString()}-np_keys_m`); + if (!publicKeysBuffer) { + throw new Error(`Could not find nullifier public keys for account ${account.toString()}`); } - return undefined; + // We append the new public key to the buffer of public keys + const newPublicKeysBuffer = Buffer.concat([publicKeysBuffer, newPublicKey.toBuffer()]); + await this.#keys.set(`${account.toString()}-np_keys_m`, newPublicKeysBuffer); + + // We store a npk_m_hash-account_address map to make address easy to obtain with the hash later on + await this.#keys.set(`${newPublicKey.hash().toString()}-npk_m_hash`, account.toBuffer()); } } diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts index 2de3eaff5b2..f17bc651823 100644 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ b/yarn-project/pxe/src/database/kv_pxe_database.ts @@ -2,7 +2,6 @@ import { MerkleTreeId, type NoteFilter, NoteStatus, type PublicKey } from '@azte import { AztecAddress, CompleteAddress, Header } from '@aztec/circuits.js'; import { type ContractArtifact } from '@aztec/foundation/abi'; import { toBufferBE } from '@aztec/foundation/bigint-buffer'; -import { poseidon2Hash } from '@aztec/foundation/crypto'; import { Fr, type Point } from '@aztec/foundation/fields'; import { type AztecArray, @@ -386,27 +385,12 @@ export class KVPxeDatabase implements PxeDatabase { return value ? CompleteAddress.fromBuffer(value) : undefined; } - getCompleteAddress(accountOrNpkMHash: AztecAddress | Fr): Promise { - return Promise.resolve( - this.#getCompleteAddress(accountOrNpkMHash) ?? this.#getCompleteAddressWithNpkMHash(accountOrNpkMHash), - ); - } - - #getCompleteAddressWithNpkMHash(npkMHash: Fr): Promise { - const completeAddresses = this.#getCompleteAddresses(); - - const completeAddress = completeAddresses.find(completeAddress => - poseidon2Hash(completeAddress.publicKeys.masterNullifierPublicKey.toFields()).equals(npkMHash), - ); - return Promise.resolve(completeAddress); - } - - #getCompleteAddresses(): CompleteAddress[] { - return Array.from(this.#addresses).map(v => CompleteAddress.fromBuffer(v)); + getCompleteAddress(account: AztecAddress): Promise { + return Promise.resolve(this.#getCompleteAddress(account)); } getCompleteAddresses(): Promise { - return Promise.resolve(this.#getCompleteAddresses()); + return Promise.resolve(Array.from(this.#addresses).map(v => CompleteAddress.fromBuffer(v))); } getSynchedBlockNumberForPublicKey(publicKey: Point): number | undefined { diff --git a/yarn-project/pxe/src/database/pxe_database.ts b/yarn-project/pxe/src/database/pxe_database.ts index 13c930b2b92..e37d8fc7069 100644 --- a/yarn-project/pxe/src/database/pxe_database.ts +++ b/yarn-project/pxe/src/database/pxe_database.ts @@ -133,14 +133,14 @@ export interface PxeDatabase extends ContractArtifactDatabase, ContractInstanceD addCompleteAddress(address: CompleteAddress): Promise; /** - * Retrieve the complete address associated to a given address or master nullifier public key hash. - * @param accountOrNpkMHash - account address or master nullifier public key hash. + * Retrieve the complete address associated to a given address. + * @param account - The account address. * @returns A promise that resolves to a CompleteAddress instance if found, or undefined if not found. */ - getCompleteAddress(accountOrNpkMHash: AztecAddress | Fr): Promise; + getCompleteAddress(account: AztecAddress): Promise; /** - * Retrieves the list of complete address added to this database + * Retrieves the list of complete addresses added to this database * @returns A promise that resolves to an array of AztecAddress instances. */ getCompleteAddresses(): Promise; diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index cababdd6772..68f011b7f92 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -38,7 +38,7 @@ import { import { computeNoteHashNonce, siloNullifier } from '@aztec/circuits.js/hash'; import { type ContractArtifact, type DecodedReturn, FunctionSelector, encodeArguments } from '@aztec/foundation/abi'; import { arrayNonEmptyLength, padArrayEnd } from '@aztec/foundation/collection'; -import { Fr } from '@aztec/foundation/fields'; +import { Fq, Fr } from '@aztec/foundation/fields'; import { SerialQueue } from '@aztec/foundation/fifo'; import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; @@ -191,6 +191,10 @@ export class PXEService implements PXE { return accountCompleteAddress; } + public async rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq = Fq.random()): Promise { + await this.keyStore.rotateMasterNullifierKey(account, secretKey); + } + public async getRegisteredAccounts(): Promise { // Get complete addresses of both the recipients and the accounts const completeAddresses = await this.db.getCompleteAddresses(); diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index e598a1c49e9..0aafc050919 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -37,17 +37,17 @@ export class SimulatorOracle implements DBOracle { private log = createDebugLogger('aztec:pxe:simulator_oracle'), ) {} - async getNullifierKeys(accountOrNpkMHash: AztecAddress | Fr, contractAddress: AztecAddress): Promise { - const masterNullifierPublicKey = await this.keyStore.getMasterNullifierPublicKey(accountOrNpkMHash); - const appNullifierSecretKey = await this.keyStore.getAppNullifierSecretKey(accountOrNpkMHash, contractAddress); + async getNullifierKeys(npkMHash: Fr, contractAddress: AztecAddress): Promise { + const masterNullifierPublicKey = await this.keyStore.getMasterNullifierPublicKey(npkMHash); + const appNullifierSecretKey = await this.keyStore.getAppNullifierSecretKey(npkMHash, contractAddress); return { masterNullifierPublicKey, appNullifierSecretKey }; } - async getCompleteAddress(accountOrNpkMHash: AztecAddress | Fr): Promise { - const completeAddress = await this.db.getCompleteAddress(accountOrNpkMHash); + async getCompleteAddress(account: AztecAddress): Promise { + const completeAddress = await this.db.getCompleteAddress(account); if (!completeAddress) { throw new Error( - `No public key registered for address or master nullifier public key hash ${accountOrNpkMHash}. + `No public key registered for address ${account}. Register it by calling pxe.registerRecipient(...) or pxe.registerAccount(...).\nSee docs for context: https://docs.aztec.network/developers/debugging/aztecnr-errors#simulation-error-No-public-key-registered-for-address-0x0-Register-it-by-calling-pxeregisterRecipient-or-pxeregisterAccount`, ); } diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 400203a085b..9def84c9f12 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -40,19 +40,7 @@ export class Oracle { return unpacked.map(toACVMField); } - async getNullifierKeys([accountAddress]: ACVMField[]): Promise { - const { masterNullifierPublicKey, appNullifierSecretKey } = await this.typedOracle.getNullifierKeys( - fromACVMField(accountAddress), - ); - return [ - toACVMField(masterNullifierPublicKey.x), - toACVMField(masterNullifierPublicKey.y), - toACVMField(appNullifierSecretKey), - ]; - } - - // Keeping this oracle separate from above because I don't want an implicit overload in noir code - async getNullifierKeysWithNpkMHash([masterNullifierPublicKeyHash]: ACVMField[]): Promise { + async getNullifierKeys([masterNullifierPublicKeyHash]: ACVMField[]): Promise { const { masterNullifierPublicKey, appNullifierSecretKey } = await this.typedOracle.getNullifierKeys( fromACVMField(masterNullifierPublicKeyHash), ); @@ -182,14 +170,6 @@ export class Oracle { return [...publicKeys.toFields(), partialAddress].map(toACVMField); } - // Keeping this oracle separate from above because I don't want an implicit overload in noir code - async getPublicKeysAndPartialAddressWithNpkMHash([masterNullifierPublicKeyHash]: ACVMField[]) { - const parsedNpkMHash = fromACVMField(masterNullifierPublicKeyHash); - const { publicKeys, partialAddress } = await this.typedOracle.getCompleteAddress(parsedNpkMHash); - - return [...publicKeys.toFields(), partialAddress].map(toACVMField); - } - async getNotes( [storageSlot]: ACVMField[], [numSelects]: ACVMField[], diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 7da764e01ff..4a910040384 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -89,7 +89,7 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('unpackReturns'); } - getNullifierKeys(_accountOrNpkMHash: AztecAddress | Fr): Promise { + getNullifierKeys(_npkMHash: Fr): Promise { throw new OracleMethodNotAvailableError('getNullifierKeys'); } @@ -124,7 +124,7 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('getHeader'); } - getCompleteAddress(_accountOrNpkMHash: AztecAddress | Fr): Promise { + getCompleteAddress(_account: AztecAddress): Promise { throw new OracleMethodNotAvailableError('getCompleteAddress'); } diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 13bc8a9c7d4..f36616c2303 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -44,12 +44,12 @@ export interface DBOracle extends CommitmentsDB { getContractInstance(address: AztecAddress): Promise; /** - * Retrieve the complete address associated to a given address or master nullifier public key hash. - * @param accountOrNpkMHash - account address or master nullifier public key hash. - * @returns A complete address associated with the input address or master nullifier public key hash + * Retrieve the complete address associated to a given address. + * @param account - The account address. + * @returns A complete address associated with the input address. * @throws An error if the account is not registered in the database. */ - getCompleteAddress(accountOrNpkMHash: AztecAddress | Fr): Promise; + getCompleteAddress(account: AztecAddress): Promise; /** * Retrieve the auth witness for a given message hash. @@ -66,12 +66,12 @@ export interface DBOracle extends CommitmentsDB { popCapsule(): Promise; /** - * Retrieve nullifier keys associated with a specific account or master nullifier public key and app address. - * @param accountOrNpkMHash - account address or master nullifier public key hash. + * Retrieve nullifier keys associated with a specific master nullifier public key and app address. + * @param npkMHash - The master nullifier public key hash. * @returns A Promise that resolves to nullifier keys. * @throws If the nullifier keys are not registered in the key store. */ - getNullifierKeys(accountOrNpkMHash: AztecAddress | Fr, contractAddress: AztecAddress): Promise; + getNullifierKeys(npkMHash: Fr, contractAddress: AztecAddress): Promise; /** * Retrieves a set of notes stored in the database for a given contract address and storage slot. diff --git a/yarn-project/simulator/src/client/private_execution.test.ts b/yarn-project/simulator/src/client/private_execution.test.ts index a09bfdbe780..e3f6bd825f6 100644 --- a/yarn-project/simulator/src/client/private_execution.test.ts +++ b/yarn-project/simulator/src/client/private_execution.test.ts @@ -141,7 +141,7 @@ describe('Private Execution test suite', () => { // Create a new snapshot. const newSnap = new AppendOnlyTreeSnapshot(Fr.fromBuffer(tree.getRoot(true)), Number(tree.getNumLeaves(true))); - if (name === 'noteHash' || name === 'l1ToL2Messages' || 'publicData') { + if (name === 'noteHash' || name === 'l1ToL2Messages' || name === 'publicData') { header = new Header( header.lastArchive, header.contentCommitment, @@ -190,37 +190,35 @@ describe('Private Execution test suite', () => { beforeEach(async () => { trees = {}; oracle = mock(); - oracle.getNullifierKeys.mockImplementation( - (accountOrNpkMHash: AztecAddress | Fr, contractAddress: AztecAddress) => { - if (accountOrNpkMHash.equals(ownerCompleteAddress.address)) { - return Promise.resolve({ - masterNullifierPublicKey: ownerCompleteAddress.publicKeys.masterNullifierPublicKey, - appNullifierSecretKey: computeAppNullifierSecretKey(ownerMasterNullifierSecretKey, contractAddress), - }); - } - if (accountOrNpkMHash.equals(recipientCompleteAddress.address)) { - return Promise.resolve({ - masterNullifierPublicKey: recipientCompleteAddress.publicKeys.masterNullifierPublicKey, - appNullifierSecretKey: computeAppNullifierSecretKey(recipientMasterNullifierSecretKey, contractAddress), - }); - } - throw new Error(`Unknown address ${accountOrNpkMHash}`); - }, - ); + oracle.getNullifierKeys.mockImplementation((masterNullifierPublicKeyHash: Fr, contractAddress: AztecAddress) => { + if (masterNullifierPublicKeyHash.equals(ownerCompleteAddress.publicKeys.masterNullifierPublicKey.hash())) { + return Promise.resolve({ + masterNullifierPublicKey: ownerCompleteAddress.publicKeys.masterNullifierPublicKey, + appNullifierSecretKey: computeAppNullifierSecretKey(ownerMasterNullifierSecretKey, contractAddress), + }); + } + if (masterNullifierPublicKeyHash.equals(recipientCompleteAddress.publicKeys.masterNullifierPublicKey.hash())) { + return Promise.resolve({ + masterNullifierPublicKey: recipientCompleteAddress.publicKeys.masterNullifierPublicKey, + appNullifierSecretKey: computeAppNullifierSecretKey(recipientMasterNullifierSecretKey, contractAddress), + }); + } + throw new Error(`Unknown master nullifier public key hash: ${masterNullifierPublicKeyHash}`); + }); // We call insertLeaves here with no leaves to populate empty public data tree root --> this is necessary to be // able to get ivpk_m during execution await insertLeaves([], 'publicData'); oracle.getHeader.mockResolvedValue(header); - oracle.getCompleteAddress.mockImplementation((accountOrNpkMHash: AztecAddress | Fr) => { - if (accountOrNpkMHash.equals(owner)) { + oracle.getCompleteAddress.mockImplementation((address: AztecAddress) => { + if (address.equals(owner)) { return Promise.resolve(ownerCompleteAddress); } - if (accountOrNpkMHash.equals(recipient)) { + if (address.equals(recipient)) { return Promise.resolve(recipientCompleteAddress); } - throw new Error(`Unknown address ${accountOrNpkMHash}`); + throw new Error(`Unknown address: ${address}`); }); // This oracle gets called when reading ivpk_m from key registry --> we return zero witness indicating that // the keys were not registered. This triggers non-registered keys flow in which getCompleteAddress oracle @@ -282,7 +280,7 @@ describe('Private Execution test suite', () => { const mockFirstNullifier = new Fr(1111); let currentNoteIndex = 0n; - const buildNote = (amount: bigint, owner: AztecAddress, storageSlot: Fr, noteTypeId: Fr) => { + const buildNote = (amount: bigint, ownerNpkMHash: Fr, storageSlot: Fr, noteTypeId: Fr) => { // WARNING: this is not actually how nonces are computed! // For the purpose of this test we use a mocked firstNullifier and and a random number // to compute the nonce. Proper nonces are only enforced later by the kernel/later circuits @@ -293,7 +291,7 @@ describe('Private Execution test suite', () => { // `hash(firstNullifier, noteHashIndex)` const noteHashIndex = randomInt(1); // mock index in TX's final newNoteHashes array const nonce = computeNoteHashNonce(mockFirstNullifier, noteHashIndex); - const note = new Note([new Fr(amount), owner.toField(), Fr.random()]); + const note = new Note([new Fr(amount), ownerNpkMHash, Fr.random()]); const innerNoteHash = pedersenHash(note.items); return { contractAddress, @@ -394,7 +392,10 @@ describe('Private Execution test suite', () => { const noteTypeId = StatefulTestContractArtifact.notes['ValueNote'].id; - const notes = [buildNote(60n, owner, storageSlot, noteTypeId), buildNote(80n, owner, storageSlot, noteTypeId)]; + const notes = [ + buildNote(60n, ownerCompleteAddress.publicKeys.masterNullifierPublicKey.hash(), storageSlot, noteTypeId), + buildNote(80n, ownerCompleteAddress.publicKeys.masterNullifierPublicKey.hash(), storageSlot, noteTypeId), + ]; oracle.getNotes.mockResolvedValue(notes); const consumedNotes = await asyncMap(notes, ({ nonce, note }) => @@ -451,7 +452,9 @@ describe('Private Execution test suite', () => { const storageSlot = computeSlotForMapping(new Fr(1n), owner); const noteTypeId = StatefulTestContractArtifact.notes['ValueNote'].id; - const notes = [buildNote(balance, owner, storageSlot, noteTypeId)]; + const notes = [ + buildNote(balance, ownerCompleteAddress.publicKeys.masterNullifierPublicKey.hash(), storageSlot, noteTypeId), + ]; oracle.getNotes.mockResolvedValue(notes); const consumedNotes = await asyncMap(notes, ({ nonce, note }) => diff --git a/yarn-project/simulator/src/client/simulator.test.ts b/yarn-project/simulator/src/client/simulator.test.ts index 5eb1ecde8d4..f4cdbd7c97b 100644 --- a/yarn-project/simulator/src/client/simulator.test.ts +++ b/yarn-project/simulator/src/client/simulator.test.ts @@ -9,7 +9,7 @@ import { import { ABIParameterVisibility, type FunctionArtifact, getFunctionArtifact } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { poseidon2Hash } from '@aztec/foundation/crypto'; -import { Fr } from '@aztec/foundation/fields'; +import { Fr, type Point } from '@aztec/foundation/fields'; import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -22,7 +22,7 @@ describe('Simulator', () => { let node: MockProxy; let simulator: AcirSimulator; - let owner: AztecAddress; + let ownerMasterNullifierPublicKey: Point; let contractAddress: AztecAddress; let appNullifierSecretKey: Fr; @@ -30,14 +30,13 @@ describe('Simulator', () => { const ownerSk = Fr.fromString('2dcc5485a58316776299be08c78fa3788a1a7961ae30dc747fb1be17692a8d32'); const allOwnerKeys = deriveKeys(ownerSk); - const ownerMasterNullifierPublicKey = allOwnerKeys.publicKeys.masterNullifierPublicKey; + ownerMasterNullifierPublicKey = allOwnerKeys.publicKeys.masterNullifierPublicKey; const ownerMasterNullifierSecretKey = allOwnerKeys.masterNullifierSecretKey; contractAddress = AztecAddress.random(); const ownerPartialAddress = Fr.random(); const ownerCompleteAddress = CompleteAddress.fromSecretKeyAndPartialAddress(ownerSk, ownerPartialAddress); - owner = ownerCompleteAddress.address; appNullifierSecretKey = computeAppNullifierSecretKey(ownerMasterNullifierSecretKey, contractAddress); @@ -58,7 +57,7 @@ describe('Simulator', () => { const storageSlot = TokenContractArtifact.storageLayout['balances'].slot; const noteTypeId = TokenContractArtifact.notes['TokenNote'].id; - const createNote = (amount = 123n) => new Note([new Fr(amount), owner.toField(), Fr.random()]); + const createNote = (amount = 123n) => new Note([new Fr(amount), ownerMasterNullifierPublicKey.hash(), Fr.random()]); it('should compute note hashes and nullifier', async () => { oracle.getFunctionArtifactByName.mockResolvedValue(artifact); diff --git a/yarn-project/simulator/src/client/unconstrained_execution.test.ts b/yarn-project/simulator/src/client/unconstrained_execution.test.ts index 3d1b69cb0ca..9077aa0489e 100644 --- a/yarn-project/simulator/src/client/unconstrained_execution.test.ts +++ b/yarn-project/simulator/src/client/unconstrained_execution.test.ts @@ -33,11 +33,11 @@ describe('Unconstrained Execution test suite', () => { const ownerCompleteAddress = CompleteAddress.fromSecretKeyAndPartialAddress(ownerSecretKey, Fr.random()); owner = ownerCompleteAddress.address; - oracle.getCompleteAddress.mockImplementation((accountOrNpkMHash: AztecAddress | Fr) => { - if (accountOrNpkMHash.equals(owner)) { + oracle.getCompleteAddress.mockImplementation((account: AztecAddress) => { + if (account.equals(owner)) { return Promise.resolve(ownerCompleteAddress); } - throw new Error(`Unknown address ${accountOrNpkMHash}`); + throw new Error(`Unknown address ${account}`); }); }); diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index bca1a2b0697..68479c6266b 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -35,13 +35,13 @@ export class ViewDataOracle extends TypedOracle { } /** - * Retrieve nullifier keys associated with a specific account or master nullifier public key and app address. - * @param accountOrNpkMHash - account address or master nullifier public key hash. + * Retrieve nullifier keys associated with a specific master nullifier public key and app address. + * @param npkMHash - The master nullifier public key hash. * @returns A Promise that resolves to nullifier keys. * @throws If the nullifier keys are not registered in the key store. */ - public override getNullifierKeys(accountOrNpkMHash: AztecAddress | Fr): Promise { - return this.db.getNullifierKeys(accountOrNpkMHash, this.contractAddress); + public override getNullifierKeys(npkMHash: Fr): Promise { + return this.db.getNullifierKeys(npkMHash, this.contractAddress); } /** @@ -127,13 +127,13 @@ export class ViewDataOracle extends TypedOracle { } /** - * Retrieve the complete address associated to a given address or master nullifier public key hash. - * @param accountOrNpkMHash - account address or master nullifier public key hash. - * @returns A complete address associated with the input address or master nullifier public key hash + * Retrieve the complete address associated to a given address. + * @param account - The account address. + * @returns A complete address associated with the input address. * @throws An error if the account is not registered in the database. */ - public override getCompleteAddress(accountOrNpkMHash: AztecAddress | Fr): Promise { - return this.db.getCompleteAddress(accountOrNpkMHash); + public override getCompleteAddress(account: AztecAddress): Promise { + return this.db.getCompleteAddress(account); } /**