-
Notifications
You must be signed in to change notification settings - Fork 310
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
244ef7e
commit 6fafff6
Showing
29 changed files
with
1,369 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
noir-projects/noir-contracts/contracts/private_fpc_contract/Nargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } |
16 changes: 16 additions & 0 deletions
16
noir-projects/noir-contracts/contracts/private_fpc_contract/src/lib.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
39 changes: 39 additions & 0 deletions
39
noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
noir-projects/noir-contracts/contracts/private_token_contract/Nargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } |
239 changes: 239 additions & 0 deletions
239
noir-projects/noir-contracts/contracts/private_token_contract/src/main.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
2 changes: 2 additions & 0 deletions
2
noir-projects/noir-contracts/contracts/private_token_contract/src/test.nr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
mod basic; | ||
mod utils; |
Oops, something went wrong.