diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 5e8ff069687d..094fdc814765 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -12,21 +12,21 @@ use dep::protocol_types::{ }; use dep::std::{embedded_curve_ops::multi_scalar_mul, hash::from_field_unsafe}; -pub fn compute_slotted_note_hash_raw(storage_slot: Field, note_hiding_point: Point) -> Field { +pub fn compute_slotted_note_hiding_point_raw(storage_slot: Field, note_hiding_point: Point) -> Point { + // 1. We derive the storage slot point by multiplying the storage slot with the generator G_slot. // We use the unsafe version because the multi_scalar_mul will constrain the scalars. let storage_slot_scalar = from_field_unsafe(storage_slot); let storage_slot_point = multi_scalar_mul([G_slot], [storage_slot_scalar]); - let slotted_note_hiding_point = storage_slot_point + note_hiding_point; - let slotted_note_hash = slotted_note_hiding_point.x; - slotted_note_hash + // 2. Then we compute the slotted note hiding point by adding the storage slot point to the note hiding point. + storage_slot_point + note_hiding_point } pub fn compute_slotted_note_hash(note: Note) -> Field where Note: NoteInterface { let header = note.get_header(); - let note_hash = note.compute_note_hiding_point(); + let note_hiding_point = note.compute_note_hiding_point(); - compute_slotted_note_hash_raw(header.storage_slot, note_hash) + compute_slotted_note_hiding_point_raw(header.storage_slot, note_hiding_point).x } pub fn compute_siloed_nullifier( diff --git a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr index 238ae7f4fcab..60abc87e0ba3 100644 --- a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr @@ -9,14 +9,14 @@ contract PrivateFPC { #[aztec(storage)] struct Storage { other_asset: SharedImmutable, - admin_npk_m_hash: SharedImmutable + admin: SharedImmutable, } #[aztec(public)] #[aztec(initializer)] - fn constructor(other_asset: AztecAddress, admin_npk_m_hash: Field) { + fn constructor(other_asset: AztecAddress, admin: AztecAddress) { storage.other_asset.initialize(other_asset); - storage.admin_npk_m_hash.initialize(admin_npk_m_hash); + storage.admin.initialize(admin); } #[aztec(private)] @@ -32,7 +32,7 @@ contract PrivateFPC { emit_randomness_as_unencrypted_log(&mut context, fee_payer_randomness); TokenWithRefunds::at(asset).setup_refund( - storage.admin_npk_m_hash.read_private(), + storage.admin.read_private(), context.msg_sender(), amount, user_randomness, diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr index 9a943af45e79..5cde52d8ef1f 100644 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr @@ -1,31 +1,68 @@ +// docs:start:token_all +// docs:start:imports mod types; mod test; -// Minimal token implementation that supports `AuthWit` accounts and private refunds +// Copy of standard token contract enhanced with refund functionality. contract TokenWithRefunds { + // Libs + use dep::compressed_string::FieldCompressedString; + use dep::aztec::{ - note::utils::compute_slotted_note_hash_raw, hash::compute_secret_hash, + hash::compute_secret_hash, prelude::{NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress}, - protocol_types::{abis::function_selector::FunctionSelector, point::Point, hash::pedersen_hash}, - oracle::unsafe_rand::unsafe_rand, - encrypted_logs::encrypted_note_emission::{encode_and_encrypt_note, encode_and_encrypt_note_with_keys} + encrypted_logs::{ + encrypted_note_emission::{ + encode_and_encrypt_note, encode_and_encrypt_note_with_keys, + encode_and_encrypt_note_with_keys_unconstrained + }, + encrypted_event_emission::{encode_and_encrypt_event, encode_and_encrypt_event_with_keys_unconstrained} + } }; - use dep::authwit::{auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public}}; - use crate::types::{token_note::{TokenNote, TOKEN_NOTE_LEN}, balances_map::BalancesMap}; + // docs:start:import_authwit + use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; + // docs:end:import_authwit + + use crate::types::{transparent_note::TransparentNote, token_note::{TokenNote, TOKEN_NOTE_LEN}, balances_map::BalancesMap}; + // docs:end::imports + + // TODO(#7425): Rename back to `Transfer` + #[aztec(event)] + struct Transfer2 { + from: AztecAddress, + to: AztecAddress, + amount: Field, + } + + // docs:start:storage_struct #[aztec(storage)] struct Storage { + // docs:start:storage_admin admin: PublicMutable, + // docs:end:storage_admin + // docs:start:storage_minters minters: Map>, + // docs:end:storage_minters + // docs:start:storage_balances balances: BalancesMap, + // docs:end:storage_balances total_supply: PublicMutable, + // docs:start:storage_pending_shields + pending_shields: PrivateSet, + // docs:end:storage_pending_shields + public_balances: Map>, symbol: SharedImmutable, name: SharedImmutable, + // docs:start:storage_decimals decimals: SharedImmutable, + // docs:end:storage_decimals } + // docs:end:storage_struct + // docs:start:constructor #[aztec(public)] #[aztec(initializer)] fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) { @@ -34,128 +71,367 @@ contract TokenWithRefunds { storage.minters.at(admin).write(true); storage.name.initialize(FieldCompressedString::from_string(name)); storage.symbol.initialize(FieldCompressedString::from_string(symbol)); + // docs:start:initialize_decimals storage.decimals.initialize(decimals); + // docs:end:initialize_decimals } + // docs:end:constructor + // docs:start:set_admin #[aztec(public)] fn set_admin(new_admin: AztecAddress) { assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin"); + // docs:start:write_admin storage.admin.write(new_admin); + // docs:end:write_admin } + // docs:end:set_admin #[aztec(public)] + #[aztec(view)] fn public_get_name() -> pub FieldCompressedString { storage.name.read_public() } #[aztec(private)] + #[aztec(view)] fn private_get_name() -> pub FieldCompressedString { storage.name.read_private() } - unconstrained fn un_get_name() -> pub [u8; 31] { - storage.name.read_public().to_bytes() - } - #[aztec(public)] + #[aztec(view)] fn public_get_symbol() -> pub FieldCompressedString { storage.symbol.read_public() } #[aztec(private)] + #[aztec(view)] fn private_get_symbol() -> pub FieldCompressedString { storage.symbol.read_private() } - unconstrained fn un_get_symbol() -> pub [u8; 31] { - storage.symbol.read_public().to_bytes() - } - #[aztec(public)] + #[aztec(view)] fn public_get_decimals() -> pub u8 { + // docs:start:read_decimals_public storage.decimals.read_public() + // docs:end:read_decimals_public } #[aztec(private)] + #[aztec(view)] fn private_get_decimals() -> pub u8 { + // docs:start:read_decimals_private storage.decimals.read_private() + // docs:end:read_decimals_private } - unconstrained fn un_get_decimals() -> pub u8 { - storage.decimals.read_public() + // docs:start:admin + #[aztec(public)] + #[aztec(view)] + fn admin() -> Field { + storage.admin.read().to_field() } + // docs:end:admin + // docs:start:is_minter + #[aztec(public)] + #[aztec(view)] + fn is_minter(minter: AztecAddress) -> bool { + storage.minters.at(minter).read() + } + // docs:end:is_minter + + // docs:start:total_supply + #[aztec(public)] + #[aztec(view)] + fn total_supply() -> Field { + storage.total_supply.read().to_integer() + } + // docs:end:total_supply + + // docs:start:balance_of_public + #[aztec(public)] + #[aztec(view)] + fn balance_of_public(owner: AztecAddress) -> Field { + storage.public_balances.at(owner).read().to_integer() + } + // docs:end:balance_of_public + + // docs:start:set_minter #[aztec(public)] fn set_minter(minter: AztecAddress, approve: bool) { + // docs:start:read_admin assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin"); + // docs:end:read_admin + // docs:start:write_minter storage.minters.at(minter).write(approve); + // docs:end:write_minter + } + // docs:end:set_minter + + // docs:start:mint_public + #[aztec(public)] + fn mint_public(to: AztecAddress, amount: Field) { + // docs:start:read_minter + assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter"); + // docs:end:read_minter + let amount = U128::from_integer(amount); + let new_balance = storage.public_balances.at(to).read().add(amount); + let supply = storage.total_supply.read().add(amount); + + storage.public_balances.at(to).write(new_balance); + storage.total_supply.write(supply); } + // docs:end:mint_public + + // docs:start:mint_private + #[aztec(public)] + fn mint_private(amount: Field, secret_hash: Field) { + assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter"); + let pending_shields = storage.pending_shields; + let mut note = TransparentNote::new(amount, secret_hash); + let supply = storage.total_supply.read().add(U128::from_integer(amount)); + storage.total_supply.write(supply); + // docs:start:insert_from_public + pending_shields.insert_from_public(&mut note); + // docs:end:insert_from_public + } + // docs:end:mint_private + + // TODO: Nuke this - test functions do not belong to token contract! #[aztec(private)] fn privately_mint_private_note(amount: Field) { let caller = context.msg_sender(); - let header = context.get_header(); - let caller_npk_m_hash = header.get_npk_m_hash(&mut context, caller); - storage.balances.add(caller_npk_m_hash, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, caller, caller)); + storage.balances.add(caller, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, caller, caller)); + TokenWithRefunds::at(context.this_address()).assert_minter_and_mint(context.msg_sender(), amount).enqueue(&mut context); } #[aztec(public)] + #[aztec(internal)] fn assert_minter_and_mint(minter: AztecAddress, amount: Field) { assert(storage.minters.at(minter).read(), "caller is not minter"); let supply = storage.total_supply.read() + U128::from_integer(amount); storage.total_supply.write(supply); } + // docs:start:shield + #[aztec(public)] + fn shield(from: AztecAddress, amount: Field, secret_hash: Field, nonce: Field) { + if (!from.eq(context.msg_sender())) { + // The redeem is only spendable once, so we need to ensure that you cannot insert multiple shields from the same message. + assert_current_call_valid_authwit_public(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let amount = U128::from_integer(amount); + let from_balance = storage.public_balances.at(from).read().sub(amount); + + let pending_shields = storage.pending_shields; + let mut note = TransparentNote::new(amount.to_field(), secret_hash); + + storage.public_balances.at(from).write(from_balance); + pending_shields.insert_from_public(&mut note); + } + // docs:end:shield + + // docs:start:transfer_public + #[aztec(public)] + fn transfer_public(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit_public(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + + let amount = U128::from_integer(amount); + let from_balance = storage.public_balances.at(from).read().sub(amount); + storage.public_balances.at(from).write(from_balance); + + let to_balance = storage.public_balances.at(to).read().add(amount); + storage.public_balances.at(to).write(to_balance); + } + // docs:end:transfer_public + + // docs:start:burn_public + #[aztec(public)] + fn burn_public(from: AztecAddress, amount: Field, nonce: Field) { + // docs:start:assert_current_call_valid_authwit_public + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit_public(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + // docs:end:assert_current_call_valid_authwit_public + + let amount = U128::from_integer(amount); + let from_balance = storage.public_balances.at(from).read().sub(amount); + storage.public_balances.at(from).write(from_balance); + + let new_supply = storage.total_supply.read().sub(amount); + storage.total_supply.write(new_supply); + } + // docs:end:burn_public + + // docs:start:redeem_shield #[aztec(private)] - fn transfer_from(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) { + fn redeem_shield(to: AztecAddress, amount: Field, secret: Field) { + let pending_shields = storage.pending_shields; + let secret_hash = compute_secret_hash(secret); + // Get 1 note (set_limit(1)) which has amount stored in field with index 0 (select(0, amount)) and secret_hash + // stored in field with index 1 (select(1, secret_hash)). + let mut options = NoteGetterOptions::new(); + options = options.select(TransparentNote::properties().amount, amount, Option::none()).select( + TransparentNote::properties().secret_hash, + secret_hash, + Option::none() + ).set_limit(1); + let notes = pending_shields.get_notes(options); + let note = notes.get_unchecked(0); + // Remove the note from the pending shields set + pending_shields.remove(note); + + // Add the token note to user's balances set + // Note: Using context.msg_sender() as a sender below makes this incompatible with escrows because we send + // outgoing logs to that address and to send outgoing logs you need to get a hold of ovsk_m. + let from = context.msg_sender(); + storage.balances.add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, from, to)); + } + // docs:end:redeem_shield + + // docs:start:unshield + #[aztec(private)] + fn unshield(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) { if (!from.eq(context.msg_sender())) { assert_current_call_valid_authwit(&mut context, from); } else { assert(nonce == 0, "invalid nonce"); } + storage.balances.sub(from, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, from, from)); + + TokenWithRefunds::at(context.this_address())._increase_public_balance(to, amount).enqueue(&mut context); + } + // docs:end:unshield + + // docs:start:transfer + #[aztec(private)] + fn transfer(to: AztecAddress, amount: Field) { + let from = context.msg_sender(); + + // By fetching the keys here, we can avoid doing an extra read from the storage, since from_ovpk would + // be needed twice. let header = context.get_header(); let from_ovpk = header.get_ovpk_m(&mut context, from); let from_ivpk = header.get_ivpk_m(&mut context, from); - let from_npk_m_hash = header.get_npk_m_hash(&mut context, from); let to_ivpk = header.get_ivpk_m(&mut context, to); - let to_npk_m_hash = header.get_npk_m_hash(&mut context, to); let amount = U128::from_integer(amount); - storage.balances.sub(from_npk_m_hash, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, from_ivpk, from)); - storage.balances.add(to_npk_m_hash, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, to_ivpk, to)); + storage.balances.sub(from, amount).emit(encode_and_encrypt_note_with_keys_unconstrained(&mut context, from_ovpk, from_ivpk, from)); + storage.balances.add(to, amount).emit(encode_and_encrypt_note_with_keys_unconstrained(&mut context, from_ovpk, to_ivpk, to)); + + Transfer2 { from, to, amount: amount.to_field() }.emit(encode_and_encrypt_event_with_keys_unconstrained(&mut context, from_ovpk, to_ivpk, to)); } + // docs:end:transfer + /** + * Cancel a private authentication witness. + * @param inner_hash The inner hash of the authwit to cancel. + */ + // docs:start:cancel_authwit #[aztec(private)] - fn transfer(to: AztecAddress, amount: Field) { - let from = context.msg_sender(); + fn cancel_authwit(inner_hash: Field) { + let on_behalf_of = context.msg_sender(); + let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); + context.push_nullifier(nullifier, 0); + } + // docs:end:cancel_authwit + + // docs:start:transfer_from + #[aztec(private)] + fn transfer_from(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) { + // docs:start:assert_current_call_valid_authwit + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + // docs:end:assert_current_call_valid_authwit + + // By fetching the keys here, we can avoid doing an extra read from the storage, since from_ovpk would + // be needed twice. let header = context.get_header(); let from_ovpk = header.get_ovpk_m(&mut context, from); let from_ivpk = header.get_ivpk_m(&mut context, from); - let from_npk_m_hash = header.get_npk_m_hash(&mut context, from); let to_ivpk = header.get_ivpk_m(&mut context, to); - let to_npk_m_hash = header.get_npk_m_hash(&mut context, to); let amount = U128::from_integer(amount); - storage.balances.sub(from_npk_m_hash, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, from_ivpk, from)); - storage.balances.add(to_npk_m_hash, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, to_ivpk, to)); + // docs:start:increase_private_balance + // docs:start:encrypted + storage.balances.sub(from, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, from_ivpk, from)); + // docs:end:encrypted + // docs:end:increase_private_balance + storage.balances.add(to, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, to_ivpk, to)); } + // docs:end:transfer_from + // docs:start:burn #[aztec(private)] - fn balance_of_private(owner: AztecAddress) -> pub Field { - let header = context.get_header(); - let owner_npk_m_hash = header.get_npk_m_hash(&mut context, owner); - storage.balances.to_unconstrained().balance_of(owner_npk_m_hash).to_integer() + fn burn(from: AztecAddress, amount: Field, nonce: Field) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + + storage.balances.sub(from, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, from, from)); + + TokenWithRefunds::at(context.this_address())._reduce_total_supply(amount).enqueue(&mut context); } + // docs:end:burn + + /// Internal /// - unconstrained fn balance_of_unconstrained(owner_npk_m_hash: Field) -> pub Field { - storage.balances.balance_of(owner_npk_m_hash).to_integer() + // docs:start:increase_public_balance + #[aztec(public)] + #[aztec(internal)] + fn _increase_public_balance(to: AztecAddress, amount: Field) { + let new_balance = storage.public_balances.at(to).read().add(U128::from_integer(amount)); + storage.public_balances.at(to).write(new_balance); } + // docs:end:increase_public_balance + + // docs:start:reduce_total_supply + #[aztec(public)] + #[aztec(internal)] + fn _reduce_total_supply(amount: Field) { + // Only to be called from burn. + let new_supply = storage.total_supply.read().sub(U128::from_integer(amount)); + storage.total_supply.write(new_supply); + } + // docs:end:reduce_total_supply + + /// Unconstrained /// + + // docs:start:balance_of_private + unconstrained fn balance_of_private(owner: AztecAddress) -> pub Field { + storage.balances.balance_of(owner).to_field() + } + // docs:end:balance_of_private + + // REFUNDS SPECIFIC FUNCTIONALITY FOLLOWS + use dep::aztec::{ + note::utils::compute_slotted_note_hiding_point_raw, prelude::FunctionSelector, + protocol_types::{storage::map::derive_storage_slot_in_map, point::Point} + }; #[aztec(private)] fn setup_refund( - fee_payer_npk_m_hash: Field, // NpkMHash of the entity which will receive the fee note. + fee_payer: AztecAddress, // Address of the entity which will receive the fee note. user: AztecAddress, // A user for which we are setting up the fee refund. funded_amount: Field, // The amount the user funded the fee payer with (represents fee limit). user_randomness: Field, // A randomness to mix in with the generated refund note for the sponsored user. @@ -165,8 +441,9 @@ contract TokenWithRefunds { // the authwit flow here and check that the user really permitted fee_payer to set up a refund on their behalf. assert_current_call_valid_authwit(&mut context, user); - // 2. Get all the relevant user keys + // 2. Get all the relevant keys let header = context.get_header(); + let fee_payer_npk_m_hash = header.get_npk_m_hash(&mut context, fee_payer); let user_npk_m_hash = header.get_npk_m_hash(&mut context, user); let user_ovpk = header.get_ovpk_m(&mut context, user); let user_ivpk = header.get_ivpk_m(&mut context, user); @@ -174,8 +451,7 @@ contract TokenWithRefunds { // 3. Deduct the funded amount from the user's balance - this is a maximum fee a user is willing to pay // (called fee limit in aztec spec). The difference between fee limit and the actual tx fee will be refunded // to the user in the `complete_refund(...)` function. - // TODO(#7324), TODO(#7323): using npk_m_hash here is vulnerable in 2 ways described in the linked issues. - storage.balances.sub(user_npk_m_hash, U128::from_integer(funded_amount)).emit(encode_and_encrypt_note_with_keys(&mut context, user_ovpk, user_ivpk, user)); + storage.balances.sub(user, U128::from_integer(funded_amount)).emit(encode_and_encrypt_note_with_keys(&mut context, user_ovpk, user_ivpk, user)); // 4. We generate the refund points. let (fee_payer_point, user_point) = TokenNote::generate_refund_points( @@ -186,13 +462,20 @@ contract TokenWithRefunds { fee_payer_randomness ); - // 5. Set the public teardown function to `complete_refund(...)`. Public teardown is the only time when a public + // 5. Now we "manually" compute the slots and the slotted note hiding points + let fee_payer_balances_slot = derive_storage_slot_in_map(TokenWithRefunds::storage().balances.slot, fee_payer); + let user_balances_slot = derive_storage_slot_in_map(TokenWithRefunds::storage().balances.slot, user); + + let slotted_fee_payer_point = compute_slotted_note_hiding_point_raw(fee_payer_balances_slot, fee_payer_point); + let slotted_user_point = compute_slotted_note_hiding_point_raw(user_balances_slot, user_point); + + // 6. Set the public teardown function to `complete_refund(...)`. Public teardown is the only time when a public // function has access to the final transaction fee, which is needed to compute the actual refund amount. context.set_public_teardown_function( context.this_address(), FunctionSelector::from_signature("complete_refund((Field,Field,bool),(Field,Field,bool))"), [ - fee_payer_point.x, fee_payer_point.y, fee_payer_point.is_infinite as Field, user_point.x, user_point.y, user_point.is_infinite as Field + slotted_fee_payer_point.x, slotted_fee_payer_point.y, slotted_fee_payer_point.is_infinite as Field, slotted_user_point.x, slotted_user_point.y, slotted_user_point.is_infinite as Field ] ); } @@ -203,35 +486,13 @@ contract TokenWithRefunds { // 1. We get the final note hiding points by calling a `complete_refund` function on the note. // We use 1:1 exchange rate between fee juice and token. So using `tx_fee` is enough let tx_fee = context.transaction_fee(); - let (fee_payer_note_hiding_point, user_note_hiding_point) = TokenNote::complete_refund(fee_payer_point, user_point, tx_fee); + let (fee_payer_note_hash, user_note_hash) = TokenNote::complete_refund(fee_payer_point, user_point, tx_fee); // 2. At last we emit the note hashes. - context.push_note_hash(fee_payer_note_hiding_point.x); - context.push_note_hash(user_note_hiding_point.x); + context.push_note_hash(fee_payer_note_hash); + context.push_note_hash(user_note_hash); // --> Once the tx is settled user and fee recipient can add the notes to their pixies. } - - /// Internal /// - - #[aztec(public)] - #[aztec(internal)] - fn _reduce_total_supply(amount: Field) { - // Only to be called from burn. - let new_supply = storage.total_supply.read().sub(U128::from_integer(amount)); - storage.total_supply.write(new_supply); - } - - /// Unconstrained /// - - unconstrained fn admin() -> pub Field { - storage.admin.read().to_field() - } - - unconstrained fn is_minter(minter: AztecAddress) -> pub bool { - storage.minters.at(minter).read() - } - - unconstrained fn total_supply() -> pub Field { - storage.total_supply.read().to_integer() - } + // END OF REFUNDS SPECIFIC FUNCTIONALITY } +// docs:end:token_all \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/basic.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/basic.nr index 98510f8a443e..af5837f07115 100644 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/basic.nr +++ b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/basic.nr @@ -1,100 +1,70 @@ -use crate::test::utils; +use crate::{test::utils, TokenWithRefunds, types::token_note::TokenNote}; + use dep::aztec::{ test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand, hash::compute_secret_hash, - prelude::NoteHeader + prelude::NoteHeader, protocol_types::storage::map::derive_storage_slot_in_map }; -use crate::TokenWithRefunds; -use crate::types::token_note::TokenNote; use dep::authwit::cheatcodes as authwit_cheatcodes; -#[test] -unconstrained fn transfer_success() { - let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(true); - - let transfer_amount = 1_000; - - let transfer_private_from_call_interface = TokenWithRefunds::at(token_contract_address).transfer_from(owner, recipient, transfer_amount, 1); - - authwit_cheatcodes::add_private_authwit_from_call_interface(owner, recipient, transfer_private_from_call_interface); - - env.impersonate(recipient); - // Transfer tokens - env.call_private_void(transfer_private_from_call_interface); - // Check balances - utils::check_private_balance( - &mut env.private(), - token_contract_address, - owner, - mint_amount - transfer_amount - ); - utils::check_private_balance( - &mut env.private(), - token_contract_address, - recipient, - transfer_amount - ); -} - #[test] unconstrained fn setup_refund_success() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(true); + // Renaming owner and recipient to match naming in TokenWithRefunds + let user = owner; + let fee_payer = recipient; + let funded_amount = 1_000; let user_randomness = 42; let fee_payer_randomness = 123; let mut context = env.private(); - let recipient_npk_m_hash = context.get_header().get_npk_m_hash(&mut context, recipient); let setup_refund_from_call_interface = TokenWithRefunds::at(token_contract_address).setup_refund( - recipient_npk_m_hash, // fee payer - owner, // sponsored user + fee_payer, + user, funded_amount, user_randomness, fee_payer_randomness ); - authwit_cheatcodes::add_private_authwit_from_call_interface(owner, recipient, setup_refund_from_call_interface); + authwit_cheatcodes::add_private_authwit_from_call_interface(user, fee_payer, setup_refund_from_call_interface); - env.impersonate(recipient); + env.impersonate(fee_payer); env.call_private_void(setup_refund_from_call_interface); let mut context = env.private(); - let owner_npk_m_hash = context.get_header().get_npk_m_hash(&mut context, owner); - let recipient_npk_m_hash = context.get_header().get_npk_m_hash(&mut context, recipient); + let user_npk_m_hash = context.get_header().get_npk_m_hash(&mut context, user); + let fee_payer_npk_m_hash = context.get_header().get_npk_m_hash(&mut context, fee_payer); + + let fee_payer_balances_slot = derive_storage_slot_in_map(TokenWithRefunds::storage().balances.slot, fee_payer); + let user_balances_slot = derive_storage_slot_in_map(TokenWithRefunds::storage().balances.slot, user); - // when the refund was set up, we would've broken the note worth mint_amount, and added back a note worth - // mint_amount - funded_amount - // then when completing the refund, we would've constructed a hash corresponding to a note worth - // funded_amount - transaction_fee - // we "know" the transaction fee was 1 (it is hardcoded in TXE oracle) - // but we need to notify TXE of the note (preimage) + // When the refund was set up, we would've spent the note worth mint_amount, and inserted a note worth + //`mint_amount - funded_amount`. When completing the refund, we would've constructed a hash corresponding to a note + // worth `funded_amount - transaction_fee`. We "know" the transaction fee was 1 (it is hardcoded in TXE oracle) + // but we need to notify TXE of the note (preimage). env.store_note_in_cache( &mut TokenNote { amount: U128::from_integer(funded_amount - 1), - npk_m_hash: owner_npk_m_hash, + npk_m_hash: user_npk_m_hash, randomness: user_randomness, header: NoteHeader::empty() }, - TokenWithRefunds::storage().balances.slot, + user_balances_slot, token_contract_address ); env.store_note_in_cache( &mut TokenNote { amount: U128::from_integer(1), - npk_m_hash: recipient_npk_m_hash, + npk_m_hash: fee_payer_npk_m_hash, randomness: fee_payer_randomness, header: NoteHeader::empty() }, - TokenWithRefunds::storage().balances.slot, + fee_payer_balances_slot, token_contract_address ); - utils::check_private_balance( - &mut env.private(), - token_contract_address, - owner, - mint_amount - 1 - ); - utils::check_private_balance(&mut env.private(), token_contract_address, recipient, 1) + utils::check_private_balance(token_contract_address, user, mint_amount - 1); + utils::check_private_balance(token_contract_address, fee_payer, 1) } diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/utils.nr index b2dfd1779215..770c87dba8dc 100644 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/utils.nr @@ -1,12 +1,10 @@ use dep::aztec::{ hash::compute_secret_hash, prelude::AztecAddress, test::helpers::{cheatcodes, test_environment::TestEnvironment}, - note::{note_getter::{MAX_NOTES_PER_PAGE, view_notes}, note_viewer_options::NoteViewerOptions}, - oracle::{execution::get_contract_address, unsafe_rand::unsafe_rand, storage::storage_read}, - context::PrivateContext + oracle::{execution::get_contract_address, unsafe_rand::unsafe_rand} }; -use crate::{types::{token_note::TokenNote}, TokenWithRefunds}; +use crate::{types::{token_note::TokenNote, transparent_note::TransparentNote}, TokenWithRefunds}; pub fn setup(with_account_contracts: bool) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress) { // Setup env, generate keys @@ -35,36 +33,47 @@ pub fn setup(with_account_contracts: bool) -> (&mut TestEnvironment, AztecAddres ); let token_contract = env.deploy_self("TokenWithRefunds").with_public_initializer(initializer_call_interface); let token_contract_address = token_contract.to_address(); - env.advance_block_by(6); + env.advance_block_by(1); (&mut env, token_contract_address, owner, recipient) } pub fn setup_and_mint(with_account_contracts: bool) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, Field) { // Setup let (env, token_contract_address, owner, recipient) = setup(with_account_contracts); - let mint_amount = 10_000; - let mint_private_call_interface = TokenWithRefunds::at(token_contract_address).privately_mint_private_note(mint_amount); - env.call_private_void(mint_private_call_interface); + let mint_amount = 10000; + // Mint some tokens + let secret = unsafe_rand(); + let secret_hash = compute_secret_hash(secret); + let mint_private_call_interface = TokenWithRefunds::at(token_contract_address).mint_private(mint_amount, secret_hash); + env.call_public(mint_private_call_interface); + + let mint_public_call_interface = TokenWithRefunds::at(token_contract_address).mint_public(owner, mint_amount); + env.call_public(mint_public_call_interface); + + // Time travel so we can read keys from the registry env.advance_block_by(6); - check_private_balance(&mut env.private(), token_contract_address, owner, mint_amount); + // docs:start:txe_test_store_note + // Store a note in the cache so we can redeem it + env.store_note_in_cache( + &mut TransparentNote::new(mint_amount, secret_hash), + TokenWithRefunds::storage().pending_shields.slot, + token_contract_address + ); + // docs:end:txe_test_store_note + + // Redeem our shielded tokens + let redeem_shield_call_interface = TokenWithRefunds::at(token_contract_address).redeem_shield(owner, mint_amount, secret); + env.call_private_void(redeem_shield_call_interface); (env, token_contract_address, owner, recipient, mint_amount) } -pub fn check_private_balance( - context: &mut PrivateContext, - token_contract_address: AztecAddress, - address: AztecAddress, - address_amount: Field -) { +pub fn check_private_balance(token_contract_address: AztecAddress, address: AztecAddress, address_amount: Field) { let current_contract_address = get_contract_address(); cheatcodes::set_contract_address(token_contract_address); - - let header = context.get_header(); - let owner_npk_m_hash = header.get_npk_m_hash(context, address); - - let balance_of_private = TokenWithRefunds::balance_of_unconstrained(owner_npk_m_hash); + // Direct call to unconstrained + let balance_of_private = TokenWithRefunds::balance_of_private(address); assert(balance_of_private == address_amount, "Private balance is not correct"); cheatcodes::set_contract_address(current_contract_address); } diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types.nr index ac754901ded1..e162c8cae130 100644 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types.nr +++ b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types.nr @@ -1,2 +1,3 @@ mod balances_map; mod token_note; +mod transparent_note; diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/balances_map.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/balances_map.nr index d70e62fa91c2..63a0092ea5c9 100644 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/balances_map.nr +++ b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/balances_map.nr @@ -1,59 +1,56 @@ -use dep::aztec::prelude::{ - AztecAddress, NoteGetterOptions, NoteViewerOptions, NoteHeader, NoteInterface, PrivateContext, - PrivateSet, Map -}; +use dep::aztec::prelude::{AztecAddress, NoteGetterOptions, NoteViewerOptions, NoteHeader, NoteInterface, PrivateSet, Map}; use dep::aztec::{ - hash::pedersen_hash, protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, + context::{PrivateContext, UnconstrainedContext}, hash::pedersen_hash, + protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, note::{ - note_getter::view_notes, note_getter_options::{SortOrder, Comparator}, - note_emission::OuterNoteEmission -}, - context::UnconstrainedContext + note_getter::view_notes, note_getter_options::SortOrder, + note_emission::{NoteEmission, OuterNoteEmission} +} }; -use crate::types::token_note::{TokenNote, OwnedNote}; +use crate::types::{token_note::{TokenNote, OwnedNote}}; struct BalancesMap { - map: PrivateSet + map: Map, Context> } impl BalancesMap { pub fn new(context: Context, storage_slot: Field) -> Self { assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1."); - Self { map: PrivateSet::new(context, storage_slot) } + Self { + map: Map::new( + context, + storage_slot, + |context, slot| PrivateSet::new(context, slot) + ) + } } } impl BalancesMap { - unconstrained fn balance_of( + unconstrained pub fn balance_of( self: Self, - owner_npk_m_hash: Field + owner: AztecAddress ) -> U128 where T: NoteInterface + OwnedNote { - self.balance_of_with_offset(owner_npk_m_hash, 0) + self.balance_of_with_offset(owner, 0) } - unconstrained fn balance_of_with_offset( + unconstrained pub fn balance_of_with_offset( self: Self, - owner_npk_m_hash: Field, + owner: AztecAddress, offset: u32 ) -> U128 where T: NoteInterface + OwnedNote { let mut balance = U128::from_integer(0); + // docs:start:view_notes let mut options = NoteViewerOptions::new(); - - let notes = self.map.view_notes( - options.select( - T::get_owner_selector(), - owner_npk_m_hash, - Option::some(Comparator.EQ) - ) - ); - + let notes = self.map.at(owner).view_notes(options.set_offset(offset)); + // docs:end:view_notes for i in 0..options.limit { if i < notes.len() { balance = balance + notes.get_unchecked(i).get_amount(); } } if (notes.len() == options.limit) { - balance = balance + self.balance_of_with_offset(owner_npk_m_hash, offset + options.limit); + balance = balance + self.balance_of_with_offset(owner, offset + options.limit); } balance @@ -61,33 +58,36 @@ impl BalancesMap { } impl BalancesMap { - - pub fn to_unconstrained(self: Self) -> BalancesMap { - BalancesMap { map: PrivateSet::new(UnconstrainedContext::new(), self.map.storage_slot) } - } - pub fn add( self: Self, - owner_npk_m_hash: Field, + owner: AztecAddress, addend: U128 ) -> OuterNoteEmission where T: NoteInterface + OwnedNote + Eq { - let mut addend_note = T::new(addend, owner_npk_m_hash); - OuterNoteEmission::new(Option::some(self.map.insert(&mut addend_note))) + if addend == U128::from_integer(0) { + OuterNoteEmission::new(Option::none()) + } else { + let context = self.map.context; + let header = context.get_header(); + + // We fetch the nullifier public key hash from the registry / from our PXE + let owner_npk_m_hash = header.get_npk_m_hash(context, owner); + let mut addend_note = T::new(addend, owner_npk_m_hash); + + // docs:start:insert + OuterNoteEmission::new(Option::some(self.map.at(owner).insert(&mut addend_note))) + // docs:end:insert + } } pub fn sub( self: Self, - owner_npk_m_hash: Field, + owner: AztecAddress, subtrahend: U128 ) -> OuterNoteEmission where T: NoteInterface + OwnedNote + Eq { - let mut options = NoteGetterOptions::with_filter(filter_notes_min_sum, subtrahend); - let notes = self.map.get_notes( - options.select( - T::get_owner_selector(), - owner_npk_m_hash, - Option::some(Comparator.EQ) - ) - ); + // docs:start:get_notes + let options = NoteGetterOptions::with_filter(filter_notes_min_sum, subtrahend); + let notes = self.map.at(owner).get_notes(options); + // docs:end:get_notes let mut minuend: U128 = U128::from_integer(0); for i in 0..options.limit { @@ -99,7 +99,7 @@ impl BalancesMap { // which require knowledge of the secret key (currently the users encryption key). // The contract logic must ensure that the spending key is used as well. // docs:start:remove - self.map.remove(note); + self.map.at(owner).remove(note); // docs:end:remove minuend = minuend + note.get_amount(); @@ -111,7 +111,7 @@ impl BalancesMap { // without the == true, it includes 'minuend.ge(subtrahend)' as part of the error. assert(minuend >= subtrahend, "Balance too low"); - self.add(owner_npk_m_hash, minuend - subtrahend) + self.add(owner, minuend - subtrahend) } } diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/token_note.nr index c5251008c0f3..5ba2b46b4ed2 100644 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/token_note.nr +++ b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/token_note.nr @@ -27,7 +27,7 @@ trait PrivatelyRefundable { incomplete_fee_payer_point: Point, incomplete_user_point: Point, transaction_fee: Field - ) -> (Point, Point); + ) -> (Field, Field); } global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header. @@ -216,7 +216,6 @@ impl PrivatelyRefundable for TokenNote { // 3. We do the necessary conversion for values relevant for the sponsored user point. // We use the unsafe version because the multi_scalar_mul will constrain the scalars. let funded_amount_scalar = from_field_unsafe(funded_amount); - // TODO(#7324), TODO(#7323): using npk_m_hash here is vulnerable in 2 ways described in the linked issues. let user_npk_m_hash_scalar = from_field_unsafe(user_npk_m_hash); let user_randomness_scalar = from_field_unsafe(user_randomness); @@ -230,7 +229,7 @@ impl PrivatelyRefundable for TokenNote { (incomplete_fee_payer_point, incomplete_user_point) } - fn complete_refund(incomplete_fee_payer_point: Point, incomplete_user_point: Point, transaction_fee: Field) -> (Point, Point) { + fn complete_refund(incomplete_fee_payer_point: Point, incomplete_user_point: Point, transaction_fee: Field) -> (Field, Field) { // 1. We convert the transaction fee to high and low limbs to be able to use BB API. // We use the unsafe version because the multi_scalar_mul will constrain the scalars. let transaction_fee_scalar = from_field_unsafe(transaction_fee); @@ -248,7 +247,12 @@ impl PrivatelyRefundable for TokenNote { assert_eq(user_point.is_infinite, false); - // Finally we return the points which represent the note hiding points. - (fee_payer_point, user_point) + // 4. We no longer need to do any elliptic curve operations with the points so we collapse them to the final + // note hashes. + let fee_payer_note_hash = fee_payer_point.x; + let user_note_hash = user_point.x; + + // 5. Finally we return the hashes. + (fee_payer_note_hash, user_note_hash) } } diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/transparent_note.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/transparent_note.nr new file mode 100644 index 000000000000..e3b028c722ad --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/transparent_note.nr @@ -0,0 +1,88 @@ +// docs:start:token_types_all +use dep::aztec::{ + note::{note_getter_options::PropertySelector, utils::compute_note_hash_for_consumption}, + prelude::{NoteHeader, NoteInterface, PrivateContext}, + protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, hash::poseidon2_hash} +}; + +global TRANSPARENT_NOTE_LEN: Field = 2; +// TRANSPARENT_NOTE_LEN * 32 + 32(storage_slot as bytes) + 32(note_type_id as bytes) +global TRANSPARENT_NOTE_BYTES_LEN: Field = 2 * 32 + 64; + +// Transparent note represents a note that is created in the clear (public execution), but can only be spent by those +// that know the preimage of the "secret_hash" (the secret). This is typically used when shielding a token balance. +// Owner of the tokens provides a "secret_hash" as an argument to the public "shield" function and then the tokens +// can be redeemed in private by presenting the preimage of the "secret_hash" (the secret). +#[aztec(note)] +struct TransparentNote { + amount: Field, + secret_hash: Field, +} + +struct TransparentNoteProperties { + amount: PropertySelector, + secret_hash: PropertySelector, +} + +impl NoteInterface for TransparentNote { + + // Custom serialization to avoid disclosing the secret field + fn serialize_content(self) -> [Field; TRANSPARENT_NOTE_LEN] { + [self.amount, self.secret_hash] + } + + // Custom deserialization since we don't have access to the secret plaintext + fn deserialize_content(serialized_note: [Field; TRANSPARENT_NOTE_LEN]) -> Self { + TransparentNote { + amount: serialized_note[0], + secret_hash: serialized_note[1], + header: NoteHeader::empty(), + } + } + + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386): Ensure nullifier collisions are prevented + fn compute_note_hash_and_nullifier(self, _context: &mut PrivateContext) -> (Field, Field) { + self.compute_note_hash_and_nullifier_without_context() + } + + // Computing a nullifier in a transparent note is not guarded by making secret a part of the nullifier preimage (as + // is common in other cases) and instead is guarded by the functionality of "redeem_shield" function. There we do + // the following: + // 1) We pass the secret as an argument to the function and use it to compute a secret hash, + // 2) we fetch a note via the "get_notes" oracle which accepts the secret hash as an argument, + // 3) the "get_notes" oracle constrains that the secret hash in the returned note matches the one computed in + // circuit. + // This achieves that the note can only be spent by the party that knows the secret. + fn compute_note_hash_and_nullifier_without_context(self) -> (Field, Field) { + let note_hash_for_nullify = compute_note_hash_for_consumption(self); + let nullifier = poseidon2_hash([ + note_hash_for_nullify, + GENERATOR_INDEX__NOTE_NULLIFIER as Field, + ]); + (note_hash_for_nullify, nullifier) + } +} + +impl TransparentNote { + // CONSTRUCTORS + pub fn new(amount: Field, secret_hash: Field) -> Self { + TransparentNote { amount, secret_hash, header: NoteHeader::empty() } + } + + // CUSTOM FUNCTIONS FOR THIS NOTE TYPE + // Custom serialization forces us to manually create the metadata struct and its getter + pub fn properties() -> TransparentNoteProperties { + TransparentNoteProperties { + amount: PropertySelector { index: 0, offset: 0, length: 32 }, + secret_hash: PropertySelector { index: 1, offset: 0, length: 32 } + } + } +} + +impl Eq for TransparentNote { + fn eq(self, other: Self) -> bool { + (self.amount == other.amount) & (self.secret_hash == other.secret_hash) + } +} + +// docs:end:token_types_all \ No newline at end of file diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index a3e0792c4355..daa55f0dc1dd 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -261,9 +261,12 @@ export class FeesTest { .deployed(); this.logger.info(`TokenWithRefunds deployed at ${tokenWithRefunds.address}`); - const adminKeyHash = this.bobWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); - const privateFPCSent = PrivateFPCContract.deploy(this.bobWallet, tokenWithRefunds.address, adminKeyHash).send(); + const privateFPCSent = PrivateFPCContract.deploy( + this.bobWallet, + tokenWithRefunds.address, + this.bobWallet.getAddress(), + ).send(); const privateFPC = await privateFPCSent.deployed(); this.logger.info(`PrivateFPC deployed at ${privateFPC.address}`); diff --git a/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts b/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts index bfee50babff3..8549dcee1c27 100644 --- a/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts @@ -7,6 +7,7 @@ import { type Wallet, } from '@aztec/aztec.js'; import { Fr, type GasSettings } from '@aztec/circuits.js'; +import { deriveStorageSlotInMap } from '@aztec/circuits.js/hash'; import { FunctionSelector, FunctionType } from '@aztec/foundation/abi'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { type PrivateFPCContract, TokenWithRefundsContract } from '@aztec/noir-contracts.js'; @@ -48,13 +49,8 @@ describe('e2e_fees/private_refunds', () => { ]); }); - // This will get re-enabled in a PR up the stack. - it.skip('can do private payments and refunds', async () => { - // 1. We get the hash of Bob's master nullifier public key. The corresponding nullifier secret key can later on - // be used to nullify/spend the note that contains the npk_m_hash. - // TODO(#7324): The values in complete address are currently not updated after the keys are rotated so this does - // not work with key rotation as the key might be the old one and then we would fetch a new one in the contract. - const bobNpkMHash = t.bobWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); + it('can do private payments and refunds', async () => { + // 1. We generate randomness for Alice and derive randomness for Bob. const aliceRandomness = Fr.random(); // Called user_randomness in contracts const bobRandomness = poseidon2Hash([aliceRandomness]); // Called fee_payer_randomness in contracts @@ -70,7 +66,7 @@ describe('e2e_fees/private_refunds', () => { aliceWallet, aliceRandomness, bobRandomness, - bobNpkMHash, // We use Bob's npk_m_hash in the notes that contain the transaction fee. + t.bobWallet.getAddress(), // Bob is the recipient of the fee notes. ), }, }) @@ -87,8 +83,6 @@ describe('e2e_fees/private_refunds', () => { // the fee limit minus the final transaction fee. The other 2 fields in the note are Alice's npk_m_hash and // the randomness. const refundNoteValue = t.gasSettings.getFeeLimit().sub(new Fr(tx.transactionFee!)); - // TODO(#7324): The values in complete address are currently not updated after the keys are rotated so this does - // not work with key rotation as the key might be the old one and then we would fetch a new one in the contract. const aliceNpkMHash = t.aliceWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); const aliceRefundNote = new Note([refundNoteValue, aliceNpkMHash, aliceRandomness]); @@ -101,16 +95,17 @@ describe('e2e_fees/private_refunds', () => { aliceRefundNote, t.aliceAddress, tokenWithRefunds.address, - TokenWithRefundsContract.storage.balances.slot, + deriveStorageSlotInMap(TokenWithRefundsContract.storage.balances.slot, t.aliceAddress), TokenWithRefundsContract.notes.TokenNote.id, tx.txHash, ), ); // 6. Now we reconstruct the note for the final fee payment. It should contain the transaction fee, Bob's - // npk_m_hash (set in the paymentMethod above) and the randomness. + // npk_m_hash and the randomness. // Note that FPC emits randomness as unencrypted log and the tx fee is publicly know so Bob is able to reconstruct // his note just from on-chain data. + const bobNpkMHash = t.bobWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); const bobFeeNote = new Note([new Fr(tx.transactionFee!), bobNpkMHash, bobRandomness]); // 7. Once again we add the note to PXE which computes the note hash and checks that it is in the note hash tree. @@ -119,7 +114,7 @@ describe('e2e_fees/private_refunds', () => { bobFeeNote, t.bobAddress, tokenWithRefunds.address, - TokenWithRefundsContract.storage.balances.slot, + deriveStorageSlotInMap(TokenWithRefundsContract.storage.balances.slot, t.bobAddress), TokenWithRefundsContract.notes.TokenNote.id, tx.txHash, ), @@ -165,9 +160,9 @@ class PrivateRefundPaymentMethod implements FeePaymentMethod { private feePayerRandomness: Fr, /** - * The hash of the master nullifier public key that the FPC sends notes it receives to. + * Address that the FPC sends notes it receives to. */ - private feeRecipientNpkMHash: Fr, + private feeRecipient: AztecAddress, ) {} /** @@ -196,13 +191,13 @@ class PrivateRefundPaymentMethod implements FeePaymentMethod { action: { name: 'setup_refund', args: [ - this.feeRecipientNpkMHash, + this.feeRecipient, this.wallet.getCompleteAddress().address, maxFee, this.userRandomness, this.feePayerRandomness, ], - selector: FunctionSelector.fromSignature('setup_refund(Field,(Field),Field,Field,Field)'), + selector: FunctionSelector.fromSignature('setup_refund((Field),(Field),Field,Field,Field)'), type: FunctionType.PRIVATE, isStatic: false, to: this.asset,