Skip to content

Commit

Permalink
feat: add nullifying key to Token Note (#6130)
Browse files Browse the repository at this point in the history
  • Loading branch information
sklppy88 committed May 15, 2024
1 parent 8f6fa34 commit 95c6b4a
Show file tree
Hide file tree
Showing 23 changed files with 276 additions and 120 deletions.
15 changes: 15 additions & 0 deletions docs/docs/misc/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ Aztec is in full-speed development. Literally every version breaks compatibility

## TBD

### [Aztec.nr] Keys: Token note now stores an owner master nullifying public key hash instead of an owner address.

i.e.

struct TokenNote {
amount: U128,
```diff
- owner: AztecAddress,
+ npk_m_hash: Field,
```
randomness: Field,
}

Computing the nullifier similarly changes to use this master nullifying public key hash.

### [Aztec.nr] Debug logging

The function `debug_log_array_with_prefix` has been removed. Use `debug_log_format` with `{}` instead. The special sequence `{}` will be replaced with the whole array. You can also use `{0}`, `{1}`, ... as usual with `debug_log_format`.
Expand Down
38 changes: 31 additions & 7 deletions noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ use crate::{
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,
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
},
header::get_header_at, logs::emit_encrypted_log,
logs_traits::{LensForEncryptedLog, ToBytesForUnencryptedLog},
nullifier_key::{get_nullifier_keys, NullifierKeys}
}
}
};
use dep::protocol_types::{
Expand All @@ -29,8 +28,10 @@ use dep::protocol_types::{
MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL, MAX_ENCRYPTED_LOGS_PER_CALL,
MAX_UNENCRYPTED_LOGS_PER_CALL
},
grumpkin_point::GrumpkinPoint, header::Header, messaging::l2_to_l1_message::L2ToL1Message,
traits::{is_empty, Deserialize, Empty}
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
};

// When finished, one can call .finish() to convert back to the abi
Expand Down Expand Up @@ -227,6 +228,29 @@ impl PrivateContext {
keys.app_nullifier_secret_key
}

// 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 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);
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
}

// docs:start:context_message_portal
pub fn message_portal(&mut self, recipient: EthAddress, content: Field) {
// docs:end:context_message_portal
Expand Down
17 changes: 15 additions & 2 deletions noir-projects/aztec-nr/aztec/src/keys/getters.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use dep::protocol_types::{address::AztecAddress, constants::CANONICAL_KEY_REGISTRY_ADDRESS, grumpkin_point::GrumpkinPoint};
use dep::protocol_types::{
address::AztecAddress, constants::CANONICAL_KEY_REGISTRY_ADDRESS, grumpkin_point::GrumpkinPoint,
hash::poseidon2_hash
};
use crate::{
context::PrivateContext, oracle::keys::get_public_keys_and_partial_address,
context::PrivateContext,
oracle::keys::{get_public_keys_and_partial_address, get_public_keys_and_partial_address_with_npk_m_hash},
keys::public_keys::{PublicKeys, NULLIFIER_INDEX, INCOMING_INDEX},
state_vars::{
map::derive_storage_slot_in_map,
Expand All @@ -14,6 +18,10 @@ pub fn get_npk_m(context: &mut PrivateContext, address: AztecAddress) -> Grumpki
get_master_key(context, address, NULLIFIER_INDEX)
}

pub fn get_npk_m_hash(context: &mut PrivateContext, address: AztecAddress) -> Field {
poseidon2_hash(get_master_key(context, address, NULLIFIER_INDEX).serialize())
}

pub fn get_ivpk_m(context: &mut PrivateContext, address: AztecAddress) -> GrumpkinPoint {
get_master_key(context, address, INCOMING_INDEX)
}
Expand Down Expand Up @@ -80,3 +88,8 @@ 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
}
22 changes: 22 additions & 0 deletions noir-projects/aztec-nr/aztec/src/oracle/keys.nr
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,25 @@ 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)
}
24 changes: 24 additions & 0 deletions noir-projects/aztec-nr/aztec/src/oracle/nullifier_key.nr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use dep::protocol_types::{address::AztecAddress, grumpkin_point::GrumpkinPoint,

// 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,
Expand All @@ -26,3 +27,26 @@ pub fn get_nullifier_keys(account: AztecAddress) -> NullifierKeys {
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use dep::aztec::prelude::{
};
use dep::aztec::{
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}
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
};
use crate::types::token_note::{TokenNote, OwnedNote};

Expand Down Expand Up @@ -60,7 +60,10 @@ impl<T> BalancesMap<T> {
owner: AztecAddress,
addend: U128
) where T: NoteInterface<T_SERIALIZED_LEN> + OwnedNote {
let mut addend_note = T::new(addend, owner);
// 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 mut addend_note = T::new(addend, owner_npk_m_hash);

// docs:start:insert
self.map.at(owner).insert(&mut addend_note, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
use dep::aztec::{
keys::getters::get_ivpk_m, prelude::{AztecAddress, NoteHeader, NoteInterface, PrivateContext},
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_app_nullifier_secret_key}
oracle::{unsafe_rand::unsafe_rand, nullifier_key::get_nsk_app_with_npk_m_hash}
};

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 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.
npk_m_hash: Field,
// Randomness of the note to hide its contents
randomness: Field,
}

impl NoteInterface<TOKEN_NOTE_LEN> 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_with_npk_m_hash(self.npk_m_hash);
poseidon2_hash([
note_hash_for_nullify,
secret,
Expand All @@ -40,7 +40,7 @@ impl NoteInterface<TOKEN_NOTE_LEN> 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_with_npk_m_hash(self.npk_m_hash);
poseidon2_hash([
note_hash_for_nullify,
secret,
Expand All @@ -51,8 +51,9 @@ impl NoteInterface<TOKEN_NOTE_LEN> for TokenNote {
// Broadcasts the note as an encrypted log on L1.
fn broadcast(self, context: &mut PrivateContext, slot: Field) {
// 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);
let ivpk_m = get_ivpk_m_with_npk_m_hash(self.npk_m_hash);
context.emit_encrypted_log(
(*context).this_address(),
slot,
Expand All @@ -65,10 +66,10 @@ impl NoteInterface<TOKEN_NOTE_LEN> 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(),
}
Expand All @@ -78,7 +79,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
}
}
12 changes: 6 additions & 6 deletions yarn-project/circuit-types/src/keys/key_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ export interface KeyStore {
getAccounts(): Promise<AztecAddress[]>;

/**
* Gets the master nullifier public key for a given 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 account - The account address for which to retrieve the master nullifier public key.
* @param accountOrNpkMHash - account address or master nullifier public key hash.
* @returns The master nullifier public key for the account.
*/
getMasterNullifierPublicKey(account: AztecAddress): Promise<PublicKey>;
getMasterNullifierPublicKey(accountOrNpkMHash: AztecAddress | Fr): Promise<PublicKey>;

/**
* Gets the master incoming viewing public key for a given account.
Expand All @@ -64,13 +64,13 @@ export interface KeyStore {
getMasterTaggingPublicKey(account: AztecAddress): Promise<PublicKey>;

/**
* Retrieves application nullifier secret key.
* 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 account - The account to retrieve the application nullifier secret key for.
* @param accountOrNpkMHash - account address or 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(account: AztecAddress, app: AztecAddress): Promise<Fr>;
getAppNullifierSecretKey(accountOrNpkMHash: AztecAddress | Fr, app: AztecAddress): Promise<Fr>;

/**
* Retrieves application incoming viewing secret key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,5 @@ describe('benchmarks/proving', () => {

const receipts = await Promise.all(txs.map(tx => tx.wait({ timeout: txTimeoutSec })));
expect(receipts.every(r => r.status === TxStatus.MINED)).toBe(true);
});
}, 1_200_000);
});
1 change: 1 addition & 0 deletions yarn-project/end-to-end/src/e2e_2_pxes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ describe('e2e_2_pxes', () => {
await sharedAccountOnB.register();
const sharedWalletOnB = await sharedAccountOnB.getWallet();

// Register wallet B in the pxe of wallet A
await pxeA.registerRecipient(walletB.getCompleteAddress());

// deploy the contract on PXE A
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('guides/writing_an_account_contract', () => {
logger.info(`Deployed account contract at ${address}`);

// docs:start:account-contract-works
const token = await TokenContract.deploy(wallet, { address }, 'TokenName', 'TokenSymbol', 18).send().deployed();
const token = await TokenContract.deploy(wallet, address, 'TokenName', 'TokenSymbol', 18).send().deployed();
logger.info(`Deployed token contract at ${token.address}`);

const secret = Fr.random();
Expand All @@ -84,9 +84,9 @@ describe('guides/writing_an_account_contract', () => {
);
await pxe.addNote(extendedNote);

await token.methods.redeem_shield({ address }, mintAmount, secret).send().wait();
await token.methods.redeem_shield(address, mintAmount, secret).send().wait();

const balance = await token.methods.balance_of_private({ address }).simulate();
const balance = await token.methods.balance_of_private(address).simulate();
logger.info(`Balance of wallet is now ${balance}`);
// docs:end:account-contract-works
expect(balance).toEqual(50n);
Expand Down
Loading

0 comments on commit 95c6b4a

Please sign in to comment.