Skip to content

Commit

Permalink
chore: merging TokenWithRefunds with Token
Browse files Browse the repository at this point in the history
  • Loading branch information
benesjan committed Aug 16, 2024
1 parent e3be8e6 commit 2501ff6
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 1,195 deletions.
1 change: 0 additions & 1 deletion noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ members = [
"contracts/pending_note_hashes_contract",
"contracts/price_feed_contract",
"contracts/private_fpc_contract",
"contracts/token_with_refunds_contract",
"contracts/schnorr_account_contract",
"contracts/schnorr_hardcoded_account_contract",
"contracts/schnorr_single_key_account_contract",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ type = "contract"
[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
authwit = { path = "../../../aztec-nr/authwit" }
token_with_refunds = { path = "../token_with_refunds_contract" }
token = { path = "../token_contract" }
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod settings;

contract PrivateFPC {
use dep::aztec::{protocol_types::{address::AztecAddress, hash::compute_siloed_nullifier}, state_vars::SharedImmutable};
use dep::token_with_refunds::TokenWithRefunds;
use dep::token::Token;
use crate::settings::Settings;

#[aztec(storage)]
Expand All @@ -25,14 +25,14 @@ contract PrivateFPC {
assert(asset == settings.other_asset);

// We use different randomness for fee payer to prevent a potential privacy leak (see description
// of `setup_refund(...)` function in TokenWithRefunds for details.
// of `setup_refund(...)` function in Token for details.
let fee_payer_randomness = compute_siloed_nullifier(context.this_address(), user_randomness);
// We emit fee payer randomness as nullifier to ensure FPC admin can reconstruct their fee note - note that
// protocol circuits will perform the siloing as was done above and hence the final nullifier will be correct
// fee payer randomness.
context.push_nullifier(user_randomness);

TokenWithRefunds::at(asset).setup_refund(
Token::at(asset).setup_refund(
settings.admin,
context.msg_sender(),
amount,
Expand Down
139 changes: 137 additions & 2 deletions noir-projects/noir-contracts/contracts/token_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ contract Token {

use dep::aztec::{
context::{PrivateContext, PrivateCallInterface}, hash::compute_secret_hash,
prelude::{NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress},
prelude::{
NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress,
FunctionSelector, NoteHeader, Point
},
encrypted_logs::{
encrypted_note_emission::{
encode_and_encrypt_note, encode_and_encrypt_note_with_keys,
Expand All @@ -32,7 +35,10 @@ contract Token {
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};
use crate::types::{
transparent_note::TransparentNote,
token_note::{TokenNote, TOKEN_NOTE_LEN, TokenNoteHidingPoint}, balances_map::BalancesMap
};
// docs:end::imports

// In the first transfer iteration we are computing a lot of additional information (validating inputs, retrieving
Expand Down Expand Up @@ -478,6 +484,135 @@ contract Token {
}
// docs:end:burn

/// 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.
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.
fee_payer_randomness: Field // A randomness to mix in with the generated fee note for the fee payer.
) {
// 1. This function is called by fee paying contract (fee_payer) when setting up a refund so we need to support
// 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 keys
let header = context.get_header();

let fee_payer_npk_m_hash = get_current_public_keys(&mut context, fee_payer).npk_m.hash();
let user_keys = get_current_public_keys(&mut context, user);
let user_npk_m_hash = user_keys.npk_m.hash();

// 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.
let change = subtract_balance(
&mut context,
storage,
user,
U128::from_integer(funded_amount),
INITIAL_TRANSFER_CALL_MAX_NOTES
);
storage.balances.add(user, change).emit(
encode_and_encrypt_note_with_keys_unconstrained(&mut context, user_keys.ovpk_m, user_keys.ivpk_m, user)
);

// 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 {
contract_address: AztecAddress::zero(),
nonce: 0,
storage_slot: storage.balances.map.at(fee_payer).storage_slot,
note_hash_counter: 0
},
amount: U128::zero(),
npk_m_hash: fee_payer_npk_m_hash,
randomness: fee_payer_randomness
};
let user_partial_note = TokenNote {
header: NoteHeader {
contract_address: AztecAddress::zero(),
nonce: 0,
storage_slot: storage.balances.map.at(user).storage_slot,
note_hash_counter: 0
},
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. 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)"),
[
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(
// TODO(#7771): 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(#7771): 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.
// TODO(#7796): we should try to prevent reverts here
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();

// 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.
}

/// Internal ///

// docs:start:increase_public_balance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod burn;
mod utils;
mod transfer_public;
mod transfer_private;
mod refunds;
mod unshielding;
mod minting;
mod reading_constants;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{test::utils, TokenWithRefunds, types::token_note::TokenNote};
use crate::{test::utils, Token, types::token_note::TokenNote};

use dep::aztec::{
test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand, hash::compute_secret_hash,
Expand All @@ -11,7 +11,7 @@ use dep::authwit::cheatcodes as authwit_cheatcodes;
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
// Renaming owner and recipient to match naming in Token
let user = owner;
let fee_payer = recipient;

Expand All @@ -20,7 +20,7 @@ unconstrained fn setup_refund_success() {
let fee_payer_randomness = 123;
let mut context = env.private();

let setup_refund_from_call_interface = TokenWithRefunds::at(token_contract_address).setup_refund(
let setup_refund_from_call_interface = Token::at(token_contract_address).setup_refund(
fee_payer,
user,
funded_amount,
Expand All @@ -37,8 +37,8 @@ unconstrained fn setup_refund_success() {
let user_npk_m_hash = get_current_public_keys(&mut context, user).npk_m.hash();
let fee_payer_npk_m_hash = get_current_public_keys(&mut context, fee_payer).npk_m.hash();

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 fee_payer_balances_slot = derive_storage_slot_in_map(Token::storage().balances.slot, fee_payer);
let user_balances_slot = derive_storage_slot_in_map(Token::storage().balances.slot, user);

// 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
Expand Down Expand Up @@ -76,7 +76,7 @@ unconstrained fn setup_refund_success() {
unconstrained fn setup_refund_insufficient_funded_amount() {
let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(true);

// Renaming owner and recipient to match naming in TokenWithRefunds
// Renaming owner and recipient to match naming in Token
let user = owner;
let fee_payer = recipient;

Expand All @@ -86,7 +86,7 @@ unconstrained fn setup_refund_insufficient_funded_amount() {
let fee_payer_randomness = 123;
let mut context = env.private();

let setup_refund_from_call_interface = TokenWithRefunds::at(token_contract_address).setup_refund(
let setup_refund_from_call_interface = Token::at(token_contract_address).setup_refund(
fee_payer,
user,
funded_amount,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use dep::aztec::{
prelude::{AztecAddress, NoteHeader, NoteInterface, PrivateContext},
protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, hash::poseidon2_hash_with_separator},
generators::{Ga1 as G_amt, Ga2 as G_npk, Ga3 as G_rnd, G_slot},
prelude::{NoteHeader, NoteInterface, PrivateContext},
protocol_types::{
constants::GENERATOR_INDEX__NOTE_NULLIFIER, point::{Point, POINT_LENGTH}, scalar::Scalar,
hash::poseidon2_hash_with_separator, traits::Serialize
},
note::utils::compute_note_hash_for_nullify, oracle::unsafe_rand::unsafe_rand,
keys::getters::get_nsk_app
};

// TODO(#7738): Nuke the following imports
use dep::aztec::{
generators::{Ga1 as G_amt, Ga2 as G_npk, Ga3 as G_rnd, G_slot},
protocol_types::{point::Point, scalar::Scalar}
};
use dep::std::{embedded_curve_ops::multi_scalar_mul, hash::from_field_unsafe};

trait OwnedNote {
Expand All @@ -18,7 +16,6 @@ trait OwnedNote {
}

global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header.
// TOKEN_NOTE_LEN * 32 + 32(storage_slot as bytes) + 32(note_type_id as bytes)
global TOKEN_NOTE_BYTES_LEN: Field = 3 * 32 + 64;

#[aztec(note)]
Expand Down Expand Up @@ -47,15 +44,9 @@ impl NoteInterface<TOKEN_NOTE_LEN, TOKEN_NOTE_BYTES_LEN> for TokenNote {
fn compute_nullifier_without_context(self) -> Field {
let note_hash_for_nullify = compute_note_hash_for_nullify(self);
let secret = get_nsk_app(self.npk_m_hash);
poseidon2_hash_with_separator([
note_hash_for_nullify,
secret,
],
GENERATOR_INDEX__NOTE_NULLIFIER as Field,
)
poseidon2_hash_with_separator([note_hash_for_nullify, secret],GENERATOR_INDEX__NOTE_NULLIFIER)
}

// TODO(#7738): Nuke this function and have it auto-generated by macros
fn compute_note_hiding_point(self) -> Point {
assert(self.header.storage_slot != 0, "Storage slot must be set before computing note hiding point");

Expand All @@ -68,13 +59,71 @@ impl NoteInterface<TOKEN_NOTE_LEN, TOKEN_NOTE_BYTES_LEN> for TokenNote {
let npk_m_hash_scalar = from_field_unsafe(self.npk_m_hash);
let randomness_scalar = from_field_unsafe(self.randomness);
let slot_scalar = from_field_unsafe(self.header.storage_slot);
// We compute the note hiding point as:
// `G_amt * amount + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot`
// instead of using pedersen or poseidon2 because it allows us to privately add and subtract from amount
// in public by leveraging homomorphism.
multi_scalar_mul(
[G_amt, G_npk, G_rnd, G_slot],
[amount_scalar, npk_m_hash_scalar, randomness_scalar, slot_scalar]
)
}
}

impl TokenNote {
// TODO: Merge this func with `compute_note_hiding_point`. I (benesjan) didn't do it in the initial PR to not have
// to modify macros and all the related funcs in it.
fn to_note_hiding_point(self) -> TokenNoteHidingPoint {
TokenNoteHidingPoint::new(self.compute_note_hiding_point())
}
}

struct TokenNoteHidingPoint {
inner: Point
}

impl TokenNoteHidingPoint {
fn new(point: Point) -> Self {
Self { inner: point }
}

fn add_amount(&mut self, amount: U128) {
// TODO(#7772): decompose amount with from_field_unsafe or constrain it fits into 1 limb
let amount_scalar = Scalar { lo: amount.to_integer(), hi: 0 };
self.inner = multi_scalar_mul([G_amt], [amount_scalar]) + self.inner;
}

fn add_npk_m_hash(&mut self, npk_m_hash: Field) {
self.inner = multi_scalar_mul([G_npk], [from_field_unsafe(npk_m_hash)]) + self.inner;
}

fn add_randomness(&mut self, randomness: Field) {
self.inner = multi_scalar_mul([G_rnd], [from_field_unsafe(randomness)]) + self.inner;
}

fn add_slot(&mut self, slot: Field) {
self.inner = multi_scalar_mul([G_slot], [from_field_unsafe(slot)]) + self.inner;
}

fn finalize(self) -> Field {
self.inner.x
}
}

impl Serialize<POINT_LENGTH> for TokenNoteHidingPoint {
fn serialize(self) -> [Field; POINT_LENGTH] {
self.inner.serialize()
}
}

impl Eq for TokenNote {
fn eq(self, other: Self) -> bool {
(self.amount == other.amount) &
(self.npk_m_hash == other.npk_m_hash) &
(self.randomness == other.randomness)
}
}

impl OwnedNote for TokenNote {
fn new(amount: U128, owner_npk_m_hash: Field) -> Self {
Self {
Expand All @@ -89,11 +138,3 @@ impl OwnedNote for TokenNote {
self.amount
}
}

impl Eq for TokenNote {
fn eq(self, other: Self) -> bool {
(self.amount == other.amount) &
(self.npk_m_hash == other.npk_m_hash) &
(self.randomness == other.randomness)
}
}
Loading

0 comments on commit 2501ff6

Please sign in to comment.