Skip to content

Commit

Permalink
Merge 0b264e0 into 4e5ae95
Browse files Browse the repository at this point in the history
  • Loading branch information
benesjan authored Oct 30, 2024
2 parents 4e5ae95 + 0b264e0 commit e88ebc4
Show file tree
Hide file tree
Showing 31 changed files with 495 additions and 951 deletions.
2 changes: 1 addition & 1 deletion docs/docs/guides/developer_guides/js_apps/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ You can use the `debug` option in the `wait` method to get more information abou

This debug information will be populated in the transaction receipt. You can log it to the console or use it to make assertions about the transaction.

#include_code debug /yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts typescript
#include_code debug /yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts typescript

You can also log directly from Aztec contracts. Read [this guide](../../../reference/developer_references/debugging.md#logging-in-aztecnr) for some more information.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ When you send someone a note, the note hash gets added to the note hash tree. To
1. When sending someone a note, use `encrypt_and_emit_note` (the function encrypts the log in such a way that only a recipient can decrypt it). PXE then tries to decrypt all the encrypted logs, and stores the successfully decrypted one. [More info here](../how_to_emit_event.md)
2. Manually using `pxe.addNote()` - If you choose to not emit logs to save gas or when creating a note in the public domain and want to consume it in private domain (`encrypt_and_emit_note` shouldn't be called in the public domain because everything is public), like in the previous section where we created a TransparentNote in public.

#include_code pxe_add_note yarn-project/end-to-end/src/e2e_cheat_codes.test.ts typescript
#include_code pxe_add_note yarn-project/end-to-end/src/composed/e2e_persistence.test.ts typescript

In the token contract, TransparentNotes are stored in a set called "pending_shields" which is in storage slot 5tutorials/tutorials/codealong/contract_tutorials/token_contract.md#contract-storage)

Expand Down
120 changes: 118 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 @@ -35,8 +35,8 @@ contract Token {
},
oracle::random::random,
prelude::{
AztecAddress, FunctionSelector, Map, NoteGetterOptions, PrivateSet, PublicMutable,
SharedImmutable,
AztecAddress, FunctionSelector, Map, NoteGetterOptions, PrivateSet, PublicContext,
PublicMutable, SharedImmutable,
},
protocol_types::{point::Point, traits::Serialize},
utils::comparison::Comparator,
Expand Down Expand Up @@ -481,6 +481,122 @@ contract Token {
Token::at(context.this_address())._reduce_total_supply(amount).enqueue(&mut context);
}
// docs:end:burn

// Transfers token `amount` from public balance of message sender to a private balance of `to`.
#[private]
fn transfer_to_private(to: AztecAddress, amount: Field) {
let from = context.msg_sender();
let token = Token::at(context.this_address());

// We prepare the transfer.
let hiding_point_slot = _prepare_transfer_to_private(to, &mut context, storage);

// At last we finalize the transfer. Usafe of the `unsafe` method here is safe because we set the `from`
// function argument to a message sender, guaranteeing that he can transfer only his own tokens.
token._finalize_transfer_to_private_unsafe(from, amount, hiding_point_slot).enqueue(
&mut context,
);
}

/// Prepares a transfer to a private balance of `to`. The transfer then needs to be
/// finalized by calling `finalize_transfer_to_private`. Returns a hiding point slot.
#[private]
fn prepare_transfer_to_private(to: AztecAddress) -> Field {
_prepare_transfer_to_private(to, &mut context, storage)
}

/// This function exists separately from `prepare_transfer_to_private` solely as an optimization as it allows
/// us to have it inlined in the `transfer_to_private` function which results in one less kernel iteration.
///
/// TODO(#9180): Consider adding macro support for functions callable both as an entrypoint and as an internal
/// function.
#[contract_library_method]
fn _prepare_transfer_to_private(
to: AztecAddress,
context: &mut PrivateContext,
storage: Storage<&mut PrivateContext>,
) -> Field {
let to_note_slot = storage.balances.at(to).set.storage_slot;

// We create a setup payload with unpopulated/zero `amount` for 'to'
// TODO(#7775): Manually fetching the randomness here is not great. If we decide to include randomness in all
// notes we could just inject it in macros.
let note_randomness = unsafe { random() };
let note_setup_payload = UintNote::setup_payload().new(to, note_randomness, to_note_slot);

// We set the ovpk to the message sender's ovpk and we encrypt the log.
let from_ovpk = get_public_keys(context.msg_sender()).ovpk_m;
let setup_log = note_setup_payload.encrypt_log(context, from_ovpk, to);

// Using the x-coordinate as a hiding point slot is safe against someone else interfering with it because
// we have a guarantee that the public functions of the transaction are executed right after the private ones
// and for this reason the protocol guarantees that nobody can front-run us in consuming the hiding point.
// This guarantee would break if `finalize_transfer_to_private` was not called in the same transaction. This
// however is not the flow we are currently concerned with. To support the multi-transaction flow we could
// introduce a `from` function argument, hash the x-coordinate with it and then repeat the hashing in
// `finalize_transfer_to_private`.
//
// We can also be sure that the `hiding_point_slot` will not overwrite any other value in the storage because
// in our state variables we derive slots using a different hash function from multi scalar multiplication
// (MSM).
let hiding_point_slot = note_setup_payload.hiding_point.x;

// We don't need to perform a check that the value overwritten by `_store_point_in_transient_storage_unsafe`
// is zero because the slot is the x-coordinate of the hiding point and hence we could only overwrite
// the value in the slot with the same value. This makes usage of the `unsafe` method safe.
Token::at(context.this_address())
._store_payload_in_transient_storage_unsafe(
hiding_point_slot,
note_setup_payload.hiding_point,
setup_log,
)
.enqueue(context);

hiding_point_slot
}

/// Finalizes a transfer of token `amount` from public balance of `from` to a private balance of `to`.
/// The transfer must be prepared by calling `prepare_transfer_to_private` first and the resulting
/// `hiding_point_slot` must be passed as an argument to this function.
#[public]
fn finalize_transfer_to_private(amount: Field, hiding_point_slot: Field) {
let from = context.msg_sender();
_finalize_transfer_to_private(from, amount, hiding_point_slot, &mut context, storage);
}

#[public]
#[internal]
fn _finalize_transfer_to_private_unsafe(
from: AztecAddress,
amount: Field,
hiding_point_slot: Field,
) {
_finalize_transfer_to_private(from, amount, hiding_point_slot, &mut context, storage);
}

#[contract_library_method]
fn _finalize_transfer_to_private(
from: AztecAddress,
amount: Field,
note_transient_storage_slot: Field,
context: &mut PublicContext,
storage: Storage<&mut PublicContext>,
) {
// TODO(#8271): Type the amount as U128 and nuke the ugly cast
let amount = U128::from_integer(amount);

// First we subtract the `amount` from the public balance of `from`
let from_balance = storage.public_balances.at(from).read().sub(amount);
storage.public_balances.at(from).write(from_balance);

// Then we finalize the partial note with the `amount`
let finalization_payload =
UintNote::finalization_payload().new(context, note_transient_storage_slot, amount);

// At last we emit the note hash and the final log
finalization_payload.emit();
}

/// 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 =
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 transfer_to_private;
mod refunds;
mod unshielding;
mod minting;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::test::utils;
use crate::{Token, types::transparent_note::TransparentNote};
use dep::aztec::{hash::compute_secret_hash, oracle::random::random, test::helpers::cheatcodes};
use dep::aztec::{oracle::random::random, test::helpers::cheatcodes};

#[test]
unconstrained fn mint_public_success() {
Expand Down Expand Up @@ -56,185 +56,3 @@ unconstrained fn mint_public_failures() {
utils::check_public_balance(token_contract_address, recipient, mint_for_recipient_amount);
utils::check_public_balance(token_contract_address, owner, 0);
}

#[test]
unconstrained fn mint_private_success() {
// Setup without account contracts. We are not using authwits here, so dummy accounts are enough
let (env, token_contract_address, owner, _) = utils::setup(/* with_account_contracts */ false);
let mint_amount = 10000;
// Mint some tokens
let secret = random();
let secret_hash = compute_secret_hash(secret);
Token::at(token_contract_address).mint_private(mint_amount, secret_hash).call(&mut env.public());

Token::at(token_contract_address).mint_public(owner, mint_amount).call(&mut env.public());

// Time travel so we can read keys from the registry
env.advance_block_by(6);

// We need to manually add the note to TXE because `TransparentNote` does not support automatic note log delivery.
env.add_note(
&mut TransparentNote::new(mint_amount, secret_hash),
Token::storage_layout().pending_shields.slot,
token_contract_address,
);

// Redeem our shielded tokens
Token::at(token_contract_address).redeem_shield(owner, mint_amount, secret).call(
&mut env.private(),
);

utils::check_private_balance(token_contract_address, owner, mint_amount);
}

#[test(should_fail_with = "note not popped")]
unconstrained fn mint_private_failure_double_spend() {
// Setup without account contracts. We are not using authwits here, so dummy accounts are enough
let (env, token_contract_address, owner, recipient) =
utils::setup(/* with_account_contracts */ false);
let mint_amount = 10000;
// Mint some tokens
let secret = random();
let secret_hash = compute_secret_hash(secret);
Token::at(token_contract_address).mint_private(mint_amount, secret_hash).call(&mut env.public());

Token::at(token_contract_address).mint_public(owner, mint_amount).call(&mut env.public());

// Time travel so we can read keys from the registry
env.advance_block_by(6);

// We need to manually add the note to TXE because `TransparentNote` does not support automatic note log delivery.
env.add_note(
&mut TransparentNote::new(mint_amount, secret_hash),
Token::storage_layout().pending_shields.slot,
token_contract_address,
);

// Redeem our shielded tokens
Token::at(token_contract_address).redeem_shield(owner, mint_amount, secret).call(
&mut env.private(),
);

utils::check_private_balance(token_contract_address, owner, mint_amount);

// Attempt to double spend
Token::at(token_contract_address).redeem_shield(recipient, mint_amount, secret).call(
&mut env.private(),
);
}

#[test(should_fail_with = "caller is not minter")]
unconstrained fn mint_private_failure_non_minter() {
// Setup without account contracts. We are not using authwits here, so dummy accounts are enough
let (env, token_contract_address, _, recipient) =
utils::setup(/* with_account_contracts */ false);
let mint_amount = 10000;
// Try to mint some tokens impersonating recipient
env.impersonate(recipient);

let secret = random();
let secret_hash = compute_secret_hash(secret);
Token::at(token_contract_address).mint_private(mint_amount, secret_hash).call(&mut env.public());
}

#[test(should_fail_with = "call to assert_max_bit_size")]
unconstrained fn mint_private_failure_overflow() {
// Setup without account contracts. We are not using authwits here, so dummy accounts are enough
let (env, token_contract_address, _, _) = utils::setup(/* with_account_contracts */ false);

// Overflow recipient
let mint_amount = 2.pow_32(128);
let secret = random();
let secret_hash = compute_secret_hash(secret);
Token::at(token_contract_address).mint_private(mint_amount, secret_hash).call(&mut env.public());
}

#[test(should_fail_with = "attempt to add with overflow")]
unconstrained fn mint_private_failure_overflow_recipient() {
// Setup without account contracts. We are not using authwits here, so dummy accounts are enough
let (env, token_contract_address, owner, _) = utils::setup(/* with_account_contracts */ false);
let mint_amount = 10000;
// Mint some tokens
let secret = random();
let secret_hash = compute_secret_hash(secret);
Token::at(token_contract_address).mint_private(mint_amount, secret_hash).call(&mut env.public());
// Time travel so we can read keys from the registry
env.advance_block_by(6);

// We need to manually add the note to TXE because `TransparentNote` does not support automatic note log delivery.
env.add_note(
&mut TransparentNote::new(mint_amount, secret_hash),
Token::storage_layout().pending_shields.slot,
token_contract_address,
);

// Redeem our shielded tokens
Token::at(token_contract_address).redeem_shield(owner, mint_amount, secret).call(
&mut env.private(),
);

utils::check_private_balance(token_contract_address, owner, mint_amount);

let mint_amount = 2.pow_32(128) - mint_amount;
// Mint some tokens
let secret = random();
let secret_hash = compute_secret_hash(secret);
Token::at(token_contract_address).mint_private(mint_amount, secret_hash).call(&mut env.public());
}

#[test(should_fail_with = "attempt to add with overflow")]
unconstrained fn mint_private_failure_overflow_total_supply() {
// Setup without account contracts. We are not using authwits here, so dummy accounts are enough
let (env, token_contract_address, owner, recipient) =
utils::setup(/* with_account_contracts */ false);
let mint_amount = 10000;
// Mint some tokens
let secret_owner = random();
let secret_recipient = random();
let secret_hash_owner = compute_secret_hash(secret_owner);
let secret_hash_recipient = compute_secret_hash(secret_recipient);

Token::at(token_contract_address).mint_private(mint_amount, secret_hash_owner).call(
&mut env.public(),
);
Token::at(token_contract_address).mint_private(mint_amount, secret_hash_recipient).call(
&mut env.public(),
);

// Time travel so we can read keys from the registry
env.advance_block_by(6);

// Store 2 notes in the cache so we can redeem it for owner and recipient
env.add_note(
&mut TransparentNote::new(mint_amount, secret_hash_owner),
Token::storage_layout().pending_shields.slot,
token_contract_address,
);
env.add_note(
&mut TransparentNote::new(mint_amount, secret_hash_recipient),
Token::storage_layout().pending_shields.slot,
token_contract_address,
);

// Redeem owner's shielded tokens
env.impersonate(owner);
Token::at(token_contract_address).redeem_shield(owner, mint_amount, secret_owner).call(
&mut env.private(),
);

// Redeem recipient's shielded tokens
env.impersonate(recipient);
Token::at(token_contract_address).redeem_shield(recipient, mint_amount, secret_recipient).call(
&mut env.private(),
);

utils::check_private_balance(token_contract_address, owner, mint_amount);
utils::check_private_balance(token_contract_address, recipient, mint_amount);

env.impersonate(owner);
let mint_amount = 2.pow_32(128) - 2 * mint_amount;
// Try to mint some tokens
let secret = random();
let secret_hash = compute_secret_hash(secret);
Token::at(token_contract_address).mint_private(mint_amount, secret_hash).call(&mut env.public());
}
Loading

0 comments on commit e88ebc4

Please sign in to comment.