Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions noir-projects/aztec-nr/aztec/src/note/utils.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,20 @@ use dep::protocol_types::{
};
use dep::std::{embedded_curve_ops::multi_scalar_mul, hash::from_field_unsafe};

pub fn compute_slotted_note_hiding_point_raw(storage_slot: Field, note_hiding_point: Point) -> Point {
pub fn compute_slotted_note_hash<Note, let N: u32, let M: u32>(note: Note) -> Field where Note: NoteInterface<N, M> {
let storage_slot = note.get_header().storage_slot;
let note_hiding_point = note.compute_note_hiding_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]);

// 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, let N: u32, let M: u32>(note: Note) -> Field where Note: NoteInterface<N, M> {
let header = note.get_header();
let note_hiding_point = note.compute_note_hiding_point();
let slotted_note_hiding_point = storage_slot_point + note_hiding_point;

compute_slotted_note_hiding_point_raw(header.storage_slot, note_hiding_point).x
// 3. Finally, we return the slotted note hash which is the x-coordinate of the slotted note hiding point.
slotted_note_hiding_point.x
}

pub fn compute_siloed_nullifier<Note, let N: u32, let M: u32>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ contract PrivateFPC {
// convince the FPC we are not cheating
context.push_nullifier(user_randomness);

// We use different randomness for fee payer to prevent a potential privay leak (see impl
// of PrivatelyRefundable for TokenNote for details).
// We use different randomness for fee payer to prevent a potential privacy leak (see description
// of `setup_refund(...)` function in TokenWithRefunds for details.
let fee_payer_randomness = poseidon2_hash([user_randomness]);
// We emit fee payer randomness to ensure FPC admin can reconstruct their fee note
emit_randomness_as_unencrypted_log(&mut context, fee_payer_randomness);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,26 @@ contract TokenWithRefunds {

// REFUNDS SPECIFIC FUNCTIONALITY FOLLOWS
use dep::aztec::{
note::utils::compute_slotted_note_hiding_point_raw, prelude::FunctionSelector,
prelude::{FunctionSelector, NoteHeader},
protocol_types::{storage::map::derive_storage_slot_in_map, point::Point}
};

use crate::types::token_note::TokenNoteHidingPoint;

/// We need to use different randomness for the user and for the fee payer notes because if the randomness values
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this over from the original description in TokeNote since it no longer made sense to have it there.

/// were the same we could fingerprint the user by doing the following:
/// 1) randomness_influence = fee_payer_point - G_npk * fee_payer_npk =
/// = (G_npk * fee_payer_npk + G_rnd * randomness) - G_npk * fee_payer_npk =
/// = G_rnd * randomness
/// 2) user_fingerprint = user_point - randomness_influence =
/// = (G_npk * user_npk + G_rnd * randomness) - G_rnd * randomness =
/// = G_npk * user_npk
/// 3) Then the second time the user would use this fee paying contract we would recover the same fingerprint
/// and link that the 2 transactions were made by the same user. Given that it's expected that only
/// a limited set of fee paying contracts will be used and they will be known, searching for fingerprints
/// by trying different fee payer npk values of these known contracts is a feasible attack.
///
/// `fee_payer_point` and `user_point` above are public information because they are passed as args to the public
/// `complete_refund(...)` function.
#[aztec(private)]
fn setup_refund(
fee_payer: AztecAddress, // Address of the entity which will receive the fee note.
Expand All @@ -453,46 +469,80 @@ contract TokenWithRefunds {
// to the user in the `complete_refund(...)` function.
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(
fee_payer_npk_m_hash,
user_npk_m_hash,
user_randomness,
fee_payer_randomness
);

// 5. Now we "manually" compute the slots and the slotted note hiding points
// 4. We create the partial notes for the fee payer and the user.
// --> Called "partial" because they don't have the amount set yet (that will be done in `complete_refund(...)`).
let fee_payer_partial_note = TokenNote {
header: NoteHeader::empty(),
amount: U128::zero(),
npk_m_hash: fee_payer_npk_m_hash,
randomness: fee_payer_randomness
};
let user_partial_note = TokenNote {
header: NoteHeader::empty(),
amount: U128::zero(),
npk_m_hash: user_npk_m_hash,
randomness: user_randomness
};

// 5. Now we get the note hiding points.
let mut fee_payer_point = fee_payer_partial_note.to_note_hiding_point();
let mut user_point = user_partial_note.to_note_hiding_point();

// 6. Now we "manually" compute the slot points and add them to 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);
// 7. We add the slot to the points --> this way we insert the notes into the balances Map under the respective key.
// TODO(#7753): Consider making slots part of the initital note hiding point creation.
fee_payer_point.add_slot(fee_payer_balances_slot);
user_point.add_slot(user_balances_slot);

// 6. Set the public teardown function to `complete_refund(...)`. Public teardown is the only time when a public
// 8. 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),Field)"),
FunctionSelector::from_signature("complete_refund(((Field,Field,bool)),((Field,Field,bool)),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, funded_amount
fee_payer_point.inner.x, fee_payer_point.inner.y, fee_payer_point.inner.is_infinite as Field, user_point.inner.x, user_point.inner.y, user_point.inner.is_infinite as Field, funded_amount
]
);
}

// TODO(#7728): even though the funded_amount should be a U128, we can't have that type in a contract interface due
// to serialization issues.
#[aztec(public)]
#[aztec(internal)]
fn complete_refund(fee_payer_point: Point, user_point: Point, funded_amount: Field) {
// 1. We get the final note hashes by calling a `complete_refund` function on the note.
// We use 1:1 exchange rate between fee juice and token so just passing transaction fee and funded amount
// to `complete_refund(...)` function is enough.
let (fee_payer_note_hash, user_note_hash) = TokenNote::complete_refund(
fee_payer_point,
user_point,
funded_amount,
context.transaction_fee()
);
fn complete_refund(
// TODO: the following makes macros crash --> try getting it work once we migrate to metaprogramming
// mut fee_payer_point: TokenNoteHidingPoint,
// mut user_point: TokenNoteHidingPoint,
fee_payer_point_immutable: TokenNoteHidingPoint,
user_point_immutable: TokenNoteHidingPoint,
funded_amount: Field
) {
// TODO: nuke the following 2 lines once we have mutable args
let mut fee_payer_point = fee_payer_point_immutable;
let mut user_point = user_point_immutable;

// TODO(#7728): Remove the next line
let funded_amount = U128::from_integer(funded_amount);
let tx_fee = U128::from_integer(context.transaction_fee());

// 1. We check that user funded the fee payer contract with at least the transaction fee.
assert(funded_amount >= tx_fee, "funded amount not enough to cover tx fee");

// 2. We compute the refund amount as the difference between funded amount and tx fee.
let refund_amount = funded_amount - tx_fee;

// 3. We add fee to the fee payer point and refund amount to the user point.
fee_payer_point.add_amount(tx_fee);
user_point.add_amount(refund_amount);

// 4. We finalize the hiding points to get the note hashes.
let fee_payer_note_hash = fee_payer_point.finalize();
let user_note_hash = user_point.finalize();

// 2. At last we emit the note hashes.
// 5. At last we emit the note hashes.
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ unconstrained fn setup_refund_success() {

// TODO(#7694): Ideally we would check the error message here but it's currently not supported by TXE. Once this
// is supported, check the message here and delete try deleting the corresponding e2e test.
// #[test(should_fail_with = "tx fee is higher than funded amount")]
// #[test(should_fail_with = "funded amount not enough to cover tx fee")]
#[test(should_fail)]
unconstrained fn setup_refund_insufficient_funded_amount() {
let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(true);
Expand All @@ -97,6 +97,6 @@ unconstrained fn setup_refund_insufficient_funded_amount() {

env.impersonate(fee_payer);

// The following should fail with "tx fee is higher than funded amount" because funded amount is 0
// The following should fail with "funded amount not enough to cover tx fee" because funded amount is 0
env.call_private_void(setup_refund_from_call_interface);
}
Loading