Skip to content

Commit

Permalink
feat: typing unfinalized partial notes (#7742)
Browse files Browse the repository at this point in the history
  • Loading branch information
benesjan authored Aug 6, 2024
1 parent 1ce6f31 commit 795b832
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 213 deletions.
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
/// 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

0 comments on commit 795b832

Please sign in to comment.