Skip to content

Commit

Permalink
feat: Private refunds (#7226)
Browse files Browse the repository at this point in the history
This PR creates a new token contract and fee payment contract that
support private refunds.

I.e. Alice pays Bob in private notes, and receives refunds in private
notes within the same transaction.

This is a massive improvement over the existing PrivateFeePaymentMethod
which uses an un/shield flow, which puts Alice in a never-ending loop of
refunding refunds.

*Note* I suspect we will want to:
1. consolidate this token/fpc, and the other token/fpc
2. and/or create a more general pattern for this type of homomorphic
operation

but the exact way forward there is not clear to me yet.

This PR also shows off some of the ugly things we need to do to get this
working, like:
- storing notes as a set to avoid mixing in the owner's address when
computing the note hash
- storing raw npk hashes in the contracts
- needing to use to_unconstrained to get private balances

This PR also fixes two bugs:
- supporting transactions that only have public teardown
- validation of complete addresses that did not have public keys
associated with their deployment

This PR also has the TXE charge nominal TX fees, and basic support for a
teardown function.

Side note, see https://hackmd.io/NUfIc2LJRlqL0-myhij3KQ for a cost
analysis (in terms of TXEffects byte size) for different fee payment
methods.

### In conclusion

I vote to merge the PR roughly as is and start the discussion on how to
clean the stuff up that we hate, but if someone has strong negative
reactions, I'm definitely open to hearing which parts we want to tease
out into individual PRs.
  • Loading branch information
just-mitch authored Jul 2, 2024
1 parent 244ef7e commit 6fafff6
Show file tree
Hide file tree
Showing 29 changed files with 1,369 additions and 56 deletions.
46 changes: 35 additions & 11 deletions noir-projects/aztec-nr/aztec/src/keys/public_keys.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use dep::protocol_types::{
address::PublicKeysHash, constants::GENERATOR_INDEX__PUBLIC_KEYS_HASH, hash::poseidon2_hash,
grumpkin_point::GrumpkinPoint, traits::{Deserialize, Serialize}
grumpkin_point::GrumpkinPoint, traits::{Deserialize, Serialize, Empty, is_empty}
};
use crate::keys::constants::{NUM_KEY_TYPES, NULLIFIER_INDEX, INCOMING_INDEX, OUTGOING_INDEX};

Expand All @@ -13,22 +13,46 @@ struct PublicKeys {
tpk_m: GrumpkinPoint,
}

impl Empty for PublicKeys {
fn empty() -> Self {
PublicKeys {
npk_m : GrumpkinPoint::empty(),
ivpk_m : GrumpkinPoint::empty(),
ovpk_m : GrumpkinPoint::empty(),
tpk_m : GrumpkinPoint::empty()
}
}
}

impl Eq for PublicKeys {
fn eq(self, other: PublicKeys) -> bool {
( self.npk_m == other.npk_m ) &
( self.ivpk_m == other.ivpk_m ) &
( self.ovpk_m == other.ovpk_m ) &
( self.tpk_m == other.tpk_m )
}
}

impl PublicKeys {
pub fn hash(self) -> PublicKeysHash {
PublicKeysHash::from_field(
if is_empty(self) {
0
} else {
poseidon2_hash(
[
self.npk_m.x,
self.npk_m.y,
self.ivpk_m.x,
self.ivpk_m.y,
self.ovpk_m.x,
self.ovpk_m.y,
self.tpk_m.x,
self.tpk_m.y,
GENERATOR_INDEX__PUBLIC_KEYS_HASH
]
self.npk_m.x,
self.npk_m.y,
self.ivpk_m.x,
self.ivpk_m.y,
self.ovpk_m.x,
self.ovpk_m.y,
self.tpk_m.x,
self.tpk_m.y,
GENERATOR_INDEX__PUBLIC_KEYS_HASH
]
)
}
)
}

Expand Down
2 changes: 2 additions & 0 deletions noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ members = [
"contracts/parent_contract",
"contracts/pending_note_hashes_contract",
"contracts/price_feed_contract",
"contracts/private_fpc_contract",
"contracts/private_token_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
@@ -0,0 +1,10 @@
[package]
name = "private_fpc_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
authwit = { path = "../../../aztec-nr/authwit" }
private_token = { path = "../private_token_contract" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use dep::aztec::protocol_types::abis::log_hash::LogHash;
use dep::aztec::oracle::logs::emit_unencrypted_log_private_internal;
use dep::aztec::hash::compute_unencrypted_log_hash;
use dep::aztec::context::PrivateContext;

fn emit_nonce_as_unencrypted_log(context: &mut PrivateContext, nonce: Field) {
let counter = context.next_counter();
let event_type_id = 0;
let log_slice = nonce.to_be_bytes_arr();
let log_hash = compute_unencrypted_log_hash(context.this_address(), event_type_id, nonce);
// 44 = addr (32) + selector (4) + raw log len (4) + processed log len (4)
let len = 44 + log_slice.len().to_field();
let side_effect = LogHash { value: log_hash, counter, length: len };
context.unencrypted_logs_hashes.push(side_effect);
let _void = emit_unencrypted_log_private_internal(context.this_address(), event_type_id, nonce, counter);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
mod lib;

contract PrivateFPC {
use dep::aztec::protocol_types::{abis::log_hash::LogHash, address::AztecAddress};
use dep::aztec::state_vars::SharedImmutable;
use dep::private_token::PrivateToken;
use crate::lib::emit_nonce_as_unencrypted_log;

#[aztec(storage)]
struct Storage {
other_asset: SharedImmutable<AztecAddress>,
admin_npk_m_hash: SharedImmutable<Field>
}

#[aztec(public)]
#[aztec(initializer)]
fn constructor(other_asset: AztecAddress, admin_npk_m_hash: Field) {
storage.other_asset.initialize(other_asset);
storage.admin_npk_m_hash.initialize(admin_npk_m_hash);
}

#[aztec(private)]
fn fund_transaction_privately(amount: Field, asset: AztecAddress, nonce: Field) {
assert(asset == storage.other_asset.read_private());
// convince the FPC we are not cheating
context.push_new_nullifier(nonce, 0);

// allow the FPC to reconstruct their fee note
emit_nonce_as_unencrypted_log(&mut context, nonce);

PrivateToken::at(asset).setup_refund(
storage.admin_npk_m_hash.read_private(),
context.msg_sender(),
amount,
nonce
).call(&mut context);
context.set_as_fee_payer();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "private_token_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
compressed_string = { path = "../../../aztec-nr/compressed-string" }
authwit = { path = "../../../aztec-nr/authwit" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
mod types;
mod test;

// Minimal token implementation that supports `AuthWit` accounts and private refunds

contract PrivateToken {
use dep::compressed_string::FieldCompressedString;
use dep::aztec::{
hash::compute_secret_hash,
prelude::{NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress},
protocol_types::{
abis::function_selector::FunctionSelector, hash::pedersen_hash,
constants::GENERATOR_INDEX__INNER_NOTE_HASH
},
oracle::unsafe_rand::unsafe_rand,
encrypted_logs::encrypted_note_emission::{encode_and_encrypt_note, encode_and_encrypt_note_with_keys}
};
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};
use dep::std::embedded_curve_ops::EmbeddedCurvePoint;
use dep::std::ec::tecurve::affine::Point;

#[aztec(storage)]
struct Storage {
admin: PublicMutable<AztecAddress>,
minters: Map<AztecAddress, PublicMutable<bool>>,
balances: BalancesMap<TokenNote>,
total_supply: PublicMutable<U128>,
symbol: SharedImmutable<FieldCompressedString>,
name: SharedImmutable<FieldCompressedString>,
decimals: SharedImmutable<u8>,
}

#[aztec(public)]
#[aztec(initializer)]
fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) {
assert(!admin.is_zero(), "invalid admin");
storage.admin.write(admin);
storage.minters.at(admin).write(true);
storage.name.initialize(FieldCompressedString::from_string(name));
storage.symbol.initialize(FieldCompressedString::from_string(symbol));
storage.decimals.initialize(decimals);
}

#[aztec(public)]
fn set_admin(new_admin: AztecAddress) {
assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin");
storage.admin.write(new_admin);
}

#[aztec(public)]
fn public_get_name() -> pub FieldCompressedString {
storage.name.read_public()
}

#[aztec(private)]
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)]
fn public_get_symbol() -> pub FieldCompressedString {
storage.symbol.read_public()
}

#[aztec(private)]
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)]
fn public_get_decimals() -> pub u8 {
storage.decimals.read_public()
}

#[aztec(private)]
fn private_get_decimals() -> pub u8 {
storage.decimals.read_private()
}

unconstrained fn un_get_decimals() -> pub u8 {
storage.decimals.read_public()
}

#[aztec(public)]
fn set_minter(minter: AztecAddress, approve: bool) {
assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin");
storage.minters.at(minter).write(approve);
}

#[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));
PrivateToken::at(context.this_address()).assert_minter_and_mint(context.msg_sender(), amount).enqueue(&mut context);
}

#[aztec(public)]
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);
}

#[aztec(private)]
fn transfer_from(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");
}

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));
storage.balances.add(to_npk_m_hash, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, to_ivpk));
}

#[aztec(private)]
fn transfer(to: AztecAddress, amount: Field) {
let from = context.msg_sender();
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));
storage.balances.add(to_npk_m_hash, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, to_ivpk));
}

#[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()
}

unconstrained fn balance_of_unconstrained(owner_npk_m_hash: Field) -> pub Field {
storage.balances.balance_of(owner_npk_m_hash).to_integer()
}

#[aztec(private)]
fn setup_refund(
fee_payer_npk_m_hash: Field,
sponsored_user: AztecAddress,
funded_amount: Field,
refund_nonce: Field
) {
assert_current_call_valid_authwit(&mut context, sponsored_user);
let header = context.get_header();
let sponsored_user_npk_m_hash = header.get_npk_m_hash(&mut context, sponsored_user);
let sponsored_user_ovpk = header.get_ovpk_m(&mut context, sponsored_user);
let sponsored_user_ivpk = header.get_ivpk_m(&mut context, sponsored_user);
storage.balances.sub(sponsored_user_npk_m_hash, U128::from_integer(funded_amount)).emit(encode_and_encrypt_note_with_keys(&mut context, sponsored_user_ovpk, sponsored_user_ivpk));
let points = TokenNote::generate_refund_points(
fee_payer_npk_m_hash,
sponsored_user_npk_m_hash,
funded_amount,
refund_nonce
);
context.set_public_teardown_function(
context.this_address(),
FunctionSelector::from_signature("complete_refund(Field,Field,Field,Field)"),
[points[0].x, points[0].y, points[1].x, points[1].y]
);
}

#[aztec(public)]
#[aztec(internal)]
fn complete_refund(
fpc_point_x: Field,
fpc_point_y: Field,
user_point_x: Field,
user_point_y: Field
) {
let fpc_point = EmbeddedCurvePoint { x: fpc_point_x, y: fpc_point_y, is_infinite: false };
let user_point = EmbeddedCurvePoint { x: user_point_x, y: user_point_y, is_infinite: false };
let tx_fee = context.transaction_fee();
let note_hashes = TokenNote::complete_refund(fpc_point, user_point, tx_fee);

// `compute_inner_note_hash` manually, without constructing the note
// `3` is the storage slot of the balances
context.push_new_note_hash(
pedersen_hash(
[PrivateToken::storage().balances.slot, note_hashes[0]],
GENERATOR_INDEX__INNER_NOTE_HASH
)
);
context.push_new_note_hash(
pedersen_hash(
[PrivateToken::storage().balances.slot, note_hashes[1]],
GENERATOR_INDEX__INNER_NOTE_HASH
)
);
}

/// 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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod basic;
mod utils;
Loading

0 comments on commit 6fafff6

Please sign in to comment.