From f64f2b29c30ad9b00d75119a5dbbca2a075592a1 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 12:16:10 +0000 Subject: [PATCH 1/8] refactor: updating NFT flows --- .../contracts/nft_contract/src/main.nr | 48 ++++++++++++------- .../src/test/transfer_to_private.nr | 29 ++--------- .../contracts/nft_contract/src/test/utils.nr | 10 +--- 3 files changed, 36 insertions(+), 51 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr index 6ec923ba8c2f..4087433b178e 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -134,21 +134,31 @@ contract NFT { public_owners_storage.write(to); } + #[private] + fn transfer_to_private(from: AztecAddress, to: AztecAddress, token_id: Field) { + let nft = NFT::at(context.this_address()); + + let slot_commitment = nft.prepare_transfer_to_private(from, to).call(&mut context); + + // We now transfer the token to this contract so that the `msg_sender` in `finalize_transfer_to_private` call + // below is the token owner. + nft.transfer_in_public(from, context.this_address(), token_id, 0).enqueue(&mut context); + + // At last we finalize the transfer. + nft.finalize_transfer_to_private(slot_commitment, token_id).enqueue(&mut context); + } + /// Prepares a transfer from public balance of `from` to a private balance of `to`. The transfer then needs to be /// finalized by calling `finalize_transfer_to_private`. `transient_storage_slot_randomness` is passed - /// as an argument so that we can derive `transfer_preparer_storage_slot_commitment` off-chain and then pass it + /// as an argument so that we can derive `slot_commitment` off-chain and then pass it /// as an argument to the followup call to `finalize_transfer_to_private`. #[private] - fn prepare_transfer_to_private( - from: AztecAddress, - to: AztecAddress, - transient_storage_slot_randomness: Field - ) { + fn prepare_transfer_to_private(from: AztecAddress, to: AztecAddress) -> Field { let to_keys = get_public_keys(to); let to_npk_m_hash = to_keys.npk_m.hash(); let to_note_slot = storage.private_nfts.at(to).storage_slot; - // We create a partial NFT note hiding point with unpopulated/zero token id for 'to' + // We create a setup payload with unpopulated/zero token id 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 { @@ -161,18 +171,23 @@ contract NFT { // We make the msg_sender/transfer_preparer part of the slot preimage to ensure he cannot interfere with // non-sender's slots - let transfer_preparer_storage_slot_commitment: Field = pedersen_hash( + let transient_storage_slot_randomness = unsafe { + random() + }; + let slot_commitment: Field = pedersen_hash( [context.msg_sender().to_field(), transient_storage_slot_randomness], TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX ); // Then we hash the transfer preparer storage slot commitment with `from` and use that as the final slot // --> by hashing it with a `from` we ensure that `from` cannot interfere with slots not assigned to him. let slot: Field = pedersen_hash( - [from.to_field(), transfer_preparer_storage_slot_commitment], + [from.to_field(), slot_commitment], TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX ); NFT::at(context.this_address())._store_point_in_transient_storage(note_setup_payload.hiding_point, slot).enqueue(&mut context); + + slot_commitment } #[public] @@ -184,16 +199,12 @@ contract NFT { } /// Finalizes a transfer of NFT with `token_id` from public balance of `from` to a private balance of `to`. - /// The transfer must be prepared by calling `prepare_transfer_to_private` first. - /// The `transfer_preparer_storage_slot_commitment` has to be computed off-chain the same way as was done - /// in the preparation call. + /// The transfer must be prepared by calling `prepare_transfer_to_private` first and the resulting + /// `slot_commitment` must be passed as an argument to this function. #[public] - fn finalize_transfer_to_private( - token_id: Field, - transfer_preparer_storage_slot_commitment: Field - ) { + fn finalize_transfer_to_private(slot_commitment: Field, token_id: Field) { // We don't need to support authwit here because `prepare_transfer_to_private` allows us to set arbitrary - // `from` and `from` will always be the msg sender here. + // `from` and hence the call is authenticated by the loaded hiding point being non-zero. let from = context.msg_sender(); let public_owners_storage = storage.public_owners.at(token_id); assert(public_owners_storage.read().eq(from), "invalid NFT owner"); @@ -201,7 +212,7 @@ contract NFT { // Derive the slot from the transfer preparer storage slot commitment and the `from` address (declared // as `from` in this function) let hiding_point_slot = pedersen_hash( - [from.to_field(), transfer_preparer_storage_slot_commitment], + [from.to_field(), slot_commitment], TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX ); @@ -308,3 +319,4 @@ contract NFT { (owned_nft_ids, page_limit_reached) } } + diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr index 49bde9031d59..5f10bed336e3 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr @@ -26,25 +26,15 @@ unconstrained fn transfer_to_private_to_a_different_account() { let (env, nft_contract_address, sender, recipient, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); let note_randomness = random(); - let transient_storage_slot_randomness = random(); - // Sender will be the msg_sender/transfer_preparer in prepare_transfer_to_private - let transfer_preparer_storage_slot_commitment = pedersen_hash( - [sender.to_field(), transient_storage_slot_randomness], - NFT::TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX - ); // We mock the Oracle to return the note randomness such that later on we can manually add the note let _ = OracleMock::mock("getRandomField").returns(note_randomness); // We prepare the transfer - env.call_private_void( - NFT::at(nft_contract_address).prepare_transfer_to_private(sender, recipient, transient_storage_slot_randomness) - ); + let slot_commitment: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(sender, recipient)); // Finalize the transfer of the NFT - env.call_public( - NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) - ); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, slot_commitment)); // TODO(#8771): We need to manually add the note because in the partial notes flow `notify_created_note_oracle` // is not called and we don't have a `NoteProcessor` in TXE. @@ -85,25 +75,14 @@ unconstrained fn transfer_to_private_finalizing_from_incorrect_sender() { let correct_sender = AztecAddress::from_field(9); - let transient_storage_slot_randomness = random(); - // Sender will be the msg_sender/transfer_preparer in prepare_transfer_to_private - let transfer_preparer_storage_slot_commitment = pedersen_hash( - [correct_sender.to_field(), transient_storage_slot_randomness], - NFT::TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX - ); - // We prepare the transfer - env.call_private_void( - NFT::at(nft_contract_address).prepare_transfer_to_private(correct_sender, recipient, transient_storage_slot_randomness) - ); + let slot_commitment: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(correct_sender, recipient)); // We impersonate incorrect sender and try to finalize the transfer of the NFT. The incorrect sender owns the NFT // but tries to consume a prepared transfer not belonging to him. For this reason the test should fail with // "transfer not prepared". env.impersonate(incorrect_sender); - env.call_public( - NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) - ); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, slot_commitment)); } #[test(should_fail_with="invalid NFT owner")] diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr index c1cfc8ed520a..46376b342f7e 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr @@ -50,21 +50,15 @@ unconstrained pub fn setup_mint_and_transfer_to_private(with_account_contracts: let (env, nft_contract_address, owner, recipient, minted_token_id) = setup_and_mint(with_account_contracts); let note_randomness = random(); - let transient_storage_slot_randomness = random(); - let transfer_preparer_storage_slot_commitment = pedersen_hash( - [owner.to_field(), transient_storage_slot_randomness], - NFT::TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX - ); // We mock the Oracle to return the note randomness such that later on we can manually add the note let _ = OracleMock::mock("getRandomField").returns(note_randomness); // We prepare the transfer with user being both the sender and the recipient (classical "shield" flow) - let prepare_transfer_to_private_call_interface = NFT::at(nft_contract_address).prepare_transfer_to_private(owner, owner, transient_storage_slot_randomness); - env.call_private_void(prepare_transfer_to_private_call_interface); + let slot_commitment: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(owner, owner)); // Finalize the transfer of the NFT - let finalize_transfer_to_private_call_interface = NFT::at(nft_contract_address).finalize_transfer_to_private(minted_token_id, transfer_preparer_storage_slot_commitment); + let finalize_transfer_to_private_call_interface = NFT::at(nft_contract_address).finalize_transfer_to_private(minted_token_id, slot_commitment); env.call_public(finalize_transfer_to_private_call_interface); // TODO(#8771): We need to manually add the note because in the partial notes flow `notify_created_note_oracle` From 9d0a6c8976eda7b5e533b675a40081f1245e7109 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 13:39:42 +0000 Subject: [PATCH 2/8] WIP --- .../contracts/nft_contract/src/main.nr | 4 ++-- .../nft_contract/src/test/transfer_to_private.nr | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr index 4087433b178e..9e139881ae9b 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -145,7 +145,7 @@ contract NFT { nft.transfer_in_public(from, context.this_address(), token_id, 0).enqueue(&mut context); // At last we finalize the transfer. - nft.finalize_transfer_to_private(slot_commitment, token_id).enqueue(&mut context); + nft.finalize_transfer_to_private(token_id, slot_commitment).enqueue(&mut context); } /// Prepares a transfer from public balance of `from` to a private balance of `to`. The transfer then needs to be @@ -202,7 +202,7 @@ contract NFT { /// The transfer must be prepared by calling `prepare_transfer_to_private` first and the resulting /// `slot_commitment` must be passed as an argument to this function. #[public] - fn finalize_transfer_to_private(slot_commitment: Field, token_id: Field) { + fn finalize_transfer_to_private(token_id: Field, slot_commitment: Field) { // We don't need to support authwit here because `prepare_transfer_to_private` allows us to set arbitrary // `from` and hence the call is authenticated by the loaded hiding point being non-zero. let from = context.msg_sender(); diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr index 5f10bed336e3..5afc9ad16b98 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr @@ -1,7 +1,7 @@ use crate::{test::utils, types::nft_note::NFTNote, NFT}; use dep::aztec::{ - hash::pedersen_hash, keys::getters::get_public_keys, prelude::{AztecAddress, NoteHeader}, - oracle::random::random, protocol_types::storage::map::derive_storage_slot_in_map + keys::getters::get_public_keys, prelude::{AztecAddress, NoteHeader}, oracle::random::random, + protocol_types::storage::map::derive_storage_slot_in_map }; use std::test::OracleMock; @@ -60,12 +60,10 @@ unconstrained fn transfer_to_private_to_self_transfer_not_prepared() { let (env, nft_contract_address, _, _, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); // Transfer was not prepared so we can use random value for the commitment - let transfer_preparer_storage_slot_commitment = random(); + let slot_commitment = random(); // Try finalizing the transfer without preparing it - env.call_public( - NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) - ); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, slot_commitment)); } #[test(should_fail_with="transfer not prepared")] @@ -91,11 +89,9 @@ unconstrained fn transfer_to_private_failure_not_an_owner() { let (env, nft_contract_address, _, not_owner, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); // We set random value for the commitment as the NFT owner check is before we use the value - let transfer_preparer_storage_slot_commitment = random(); + let slot_commitment = random(); // Try transferring someone else's public NFT env.impersonate(not_owner); - env.call_public( - NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) - ); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, slot_commitment)); } From 9f5d2dbddbb9fe3ab3d5b01ef1419ba4d5a8ff9c Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 15:03:24 +0000 Subject: [PATCH 3/8] x coord as hiding point slot --- .../contracts/nft_contract/src/main.nr | 55 +++++++------------ .../src/test/transfer_to_private.nr | 31 +++-------- 2 files changed, 26 insertions(+), 60 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr index 9e139881ae9b..6899db2c133e 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -21,8 +21,6 @@ contract NFT { use std::{embedded_curve_ops::EmbeddedCurvePoint, meta::derive}; use crate::types::nft_note::NFTNote; - global TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX = 3; - // TODO(#8467): Rename this to Transfer - calling this NFTTransfer to avoid export conflict with the Transfer event // in the Token contract. #[event] @@ -138,20 +136,18 @@ contract NFT { fn transfer_to_private(from: AztecAddress, to: AztecAddress, token_id: Field) { let nft = NFT::at(context.this_address()); - let slot_commitment = nft.prepare_transfer_to_private(from, to).call(&mut context); + let hiding_point_slot = nft.prepare_transfer_to_private(from, to).call(&mut context); // We now transfer the token to this contract so that the `msg_sender` in `finalize_transfer_to_private` call // below is the token owner. nft.transfer_in_public(from, context.this_address(), token_id, 0).enqueue(&mut context); // At last we finalize the transfer. - nft.finalize_transfer_to_private(token_id, slot_commitment).enqueue(&mut context); + nft.finalize_transfer_to_private(token_id, hiding_point_slot).enqueue(&mut context); } /// Prepares a transfer from public balance of `from` to a private balance of `to`. The transfer then needs to be - /// finalized by calling `finalize_transfer_to_private`. `transient_storage_slot_randomness` is passed - /// as an argument so that we can derive `slot_commitment` off-chain and then pass it - /// as an argument to the followup call to `finalize_transfer_to_private`. + /// finalized by calling `finalize_transfer_to_private`. Returns a hiding point slot. #[private] fn prepare_transfer_to_private(from: AztecAddress, to: AztecAddress) -> Field { let to_keys = get_public_keys(to); @@ -169,53 +165,40 @@ contract NFT { // We encrypt and emit the partial note log encrypt_and_emit_partial_log(&mut context, note_setup_payload.log_plaintext, to_keys, to); - // We make the msg_sender/transfer_preparer part of the slot preimage to ensure he cannot interfere with - // non-sender's slots - let transient_storage_slot_randomness = unsafe { - random() - }; - let slot_commitment: Field = pedersen_hash( - [context.msg_sender().to_field(), transient_storage_slot_randomness], - TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX - ); - // Then we hash the transfer preparer storage slot commitment with `from` and use that as the final slot - // --> by hashing it with a `from` we ensure that `from` cannot interfere with slots not assigned to him. - let slot: Field = pedersen_hash( - [from.to_field(), slot_commitment], - TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX - ); + NFT::at(context.this_address())._store_point_in_transient_storage(note_setup_payload.hiding_point).enqueue(&mut context); - NFT::at(context.this_address())._store_point_in_transient_storage(note_setup_payload.hiding_point, slot).enqueue(&mut context); + // Using the x-coordinate as a hiding point slot is safe 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 hash the x-coordinate with + // `from` and then repeat the hashing in `finalize_transfer_to_private`. + let hiding_point_slot = note_setup_payload.hiding_point.x; - slot_commitment + hiding_point_slot } #[public] #[internal] - fn _store_point_in_transient_storage(point: Point, slot: Field) { - // We don't perform check for the overwritten value to be non-zero because the slots are siloed to `to` - // and hence `to` can interfere only with his own execution. + fn _store_point_in_transient_storage(point: Point) { + let slot = point.x; + // We don't perform check for the overwritten value to be non-zero because the slot is implictly siloed to `to` + // (`to_note_slot` is part of the hiding point preimage) and hence `to` can interfere only with his own + // execution. context.storage_write(slot, point); } /// Finalizes a transfer of NFT with `token_id` 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 - /// `slot_commitment` must be passed as an argument to this function. + /// `hiding_point_slot` must be passed as an argument to this function. #[public] - fn finalize_transfer_to_private(token_id: Field, slot_commitment: Field) { + fn finalize_transfer_to_private(token_id: Field, hiding_point_slot: Field) { // We don't need to support authwit here because `prepare_transfer_to_private` allows us to set arbitrary // `from` and hence the call is authenticated by the loaded hiding point being non-zero. let from = context.msg_sender(); let public_owners_storage = storage.public_owners.at(token_id); assert(public_owners_storage.read().eq(from), "invalid NFT owner"); - // Derive the slot from the transfer preparer storage slot commitment and the `from` address (declared - // as `from` in this function) - let hiding_point_slot = pedersen_hash( - [from.to_field(), slot_commitment], - TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX - ); - // Read the hiding point from "transient" storage and check it's not empty to ensure the transfer was prepared let hiding_point: Point = context.storage_read(hiding_point_slot); assert(!is_empty(hiding_point), "transfer not prepared"); diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr index 5afc9ad16b98..def5494917fd 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr @@ -31,10 +31,10 @@ unconstrained fn transfer_to_private_to_a_different_account() { let _ = OracleMock::mock("getRandomField").returns(note_randomness); // We prepare the transfer - let slot_commitment: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(sender, recipient)); + let hiding_point_slot: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(sender, recipient)); // Finalize the transfer of the NFT - env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, slot_commitment)); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot)); // TODO(#8771): We need to manually add the note because in the partial notes flow `notify_created_note_oracle` // is not called and we don't have a `NoteProcessor` in TXE. @@ -59,28 +59,11 @@ unconstrained fn transfer_to_private_to_self_transfer_not_prepared() { // Setup without account contracts. We are not using authwits here, so dummy accounts are enough let (env, nft_contract_address, _, _, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); - // Transfer was not prepared so we can use random value for the commitment - let slot_commitment = random(); + // Transfer was not prepared so we can use random value for the hiding point slot + let hiding_point_slot = random(); // Try finalizing the transfer without preparing it - env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, slot_commitment)); -} - -#[test(should_fail_with="transfer not prepared")] -unconstrained fn transfer_to_private_finalizing_from_incorrect_sender() { - // Setup without account contracts. We are not using authwits here, so dummy accounts are enough - let (env, nft_contract_address, incorrect_sender, recipient, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); - - let correct_sender = AztecAddress::from_field(9); - - // We prepare the transfer - let slot_commitment: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(correct_sender, recipient)); - - // We impersonate incorrect sender and try to finalize the transfer of the NFT. The incorrect sender owns the NFT - // but tries to consume a prepared transfer not belonging to him. For this reason the test should fail with - // "transfer not prepared". - env.impersonate(incorrect_sender); - env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, slot_commitment)); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot)); } #[test(should_fail_with="invalid NFT owner")] @@ -89,9 +72,9 @@ unconstrained fn transfer_to_private_failure_not_an_owner() { let (env, nft_contract_address, _, not_owner, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); // We set random value for the commitment as the NFT owner check is before we use the value - let slot_commitment = random(); + let hiding_point_slot = random(); // Try transferring someone else's public NFT env.impersonate(not_owner); - env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, slot_commitment)); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot)); } From 911141fe3d39a3cd94f2394d8a87017af155889b Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 15:27:09 +0000 Subject: [PATCH 4/8] WIP --- noir-projects/aztec-nr/aztec/src/prelude.nr | 2 +- .../contracts/nft_contract/src/main.nr | 38 ++++++++++++++----- yarn-project/end-to-end/src/e2e_nft.test.ts | 22 ++--------- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/prelude.nr b/noir-projects/aztec-nr/aztec/src/prelude.nr index 793550c5668a..5d4d32954d71 100644 --- a/noir-projects/aztec-nr/aztec/src/prelude.nr +++ b/noir-projects/aztec-nr/aztec/src/prelude.nr @@ -9,7 +9,7 @@ pub use crate::{ public_immutable::PublicImmutable, public_mutable::PublicMutable, private_set::PrivateSet, shared_immutable::SharedImmutable, shared_mutable::SharedMutable, storage::Storable }, - context::{PrivateContext, PackedReturns, FunctionReturns}, + context::{PrivateContext, PackedReturns, FunctionReturns, PublicContext}, note::{ note_header::NoteHeader, note_interface::{NoteInterface, NullifiableNote}, note_getter_options::NoteGetterOptions, note_viewer_options::NoteViewerOptions, diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr index 6899db2c133e..4357d06f6a1b 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -10,9 +10,12 @@ contract NFT { use dep::compressed_string::FieldCompressedString; use dep::aztec::{ oracle::random::random, - prelude::{NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress}, + prelude::{ + NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, SharedImmutable, PrivateSet, + AztecAddress, PublicContext + }, encrypted_logs::{encrypted_note_emission::{encode_and_encrypt_note, encrypt_and_emit_partial_log}}, - hash::pedersen_hash, keys::getters::get_public_keys, note::constants::MAX_NOTES_PER_PAGE, + keys::getters::get_public_keys, note::constants::MAX_NOTES_PER_PAGE, protocol_types::traits::is_empty, utils::comparison::Comparator, protocol_types::{point::Point, traits::Serialize}, macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}} @@ -132,18 +135,18 @@ contract NFT { public_owners_storage.write(to); } + // Transfers token with `token_id` from public balance of message sender to a private balance of `to`. #[private] - fn transfer_to_private(from: AztecAddress, to: AztecAddress, token_id: Field) { + fn transfer_to_private(to: AztecAddress, token_id: Field) { + let from = context.msg_sender(); + let nft = NFT::at(context.this_address()); + // We prepare the transfer. let hiding_point_slot = nft.prepare_transfer_to_private(from, to).call(&mut context); - // We now transfer the token to this contract so that the `msg_sender` in `finalize_transfer_to_private` call - // below is the token owner. - nft.transfer_in_public(from, context.this_address(), token_id, 0).enqueue(&mut context); - // At last we finalize the transfer. - nft.finalize_transfer_to_private(token_id, hiding_point_slot).enqueue(&mut context); + nft._finalize_transfer_to_private_trusted(from, token_id, hiding_point_slot).enqueue(&mut context); } /// Prepares a transfer from public balance of `from` to a private balance of `to`. The transfer then needs to be @@ -193,9 +196,24 @@ contract NFT { /// `hiding_point_slot` must be passed as an argument to this function. #[public] fn finalize_transfer_to_private(token_id: Field, hiding_point_slot: Field) { - // We don't need to support authwit here because `prepare_transfer_to_private` allows us to set arbitrary - // `from` and hence the call is authenticated by the loaded hiding point being non-zero. let from = context.msg_sender(); + _finalize_transfer_to_private(from, token_id, hiding_point_slot, &mut context, storage); + } + + #[public] + #[internal] + fn _finalize_transfer_to_private_trusted(from: AztecAddress, token_id: Field, hiding_point_slot: Field) { + _finalize_transfer_to_private(from, token_id, hiding_point_slot, &mut context, storage); + } + + #[contract_library_method] + fn _finalize_transfer_to_private( + from: AztecAddress, + token_id: Field, + hiding_point_slot: Field, + context: &mut PublicContext, + storage: Storage<&mut PublicContext> + ) { let public_owners_storage = storage.public_owners.at(token_id); assert(public_owners_storage.read().eq(from), "invalid NFT owner"); diff --git a/yarn-project/end-to-end/src/e2e_nft.test.ts b/yarn-project/end-to-end/src/e2e_nft.test.ts index a63049dbf0a8..7efbc9aef711 100644 --- a/yarn-project/end-to-end/src/e2e_nft.test.ts +++ b/yarn-project/end-to-end/src/e2e_nft.test.ts @@ -1,5 +1,4 @@ -import { type AccountWallet, AztecAddress, BatchCall, Fr } from '@aztec/aztec.js'; -import { pedersenHash } from '@aztec/foundation/crypto'; +import { type AccountWallet, AztecAddress, Fr } from '@aztec/aztec.js'; import { NFTContract } from '@aztec/noir-contracts.js'; import { jest } from '@jest/globals'; @@ -24,7 +23,6 @@ describe('NFT', () => { // Arbitrary token id const TOKEN_ID = Fr.random().toBigInt(); - const TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX = 3; beforeAll(async () => { let wallets: AccountWallet[]; @@ -63,21 +61,9 @@ describe('NFT', () => { // In a simple "shield" flow the sender and recipient are the same. In the "uniswap swap to private" flow // it would be the uniswap contract. const recipient = user1Wallet.getAddress(); - const sender = recipient; - const transientStorageSlotRandomness = Fr.random(); - const transferPreparerStorageSlotCommitment = pedersenHash( - [user1Wallet.getAddress(), transientStorageSlotRandomness], - TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX, - ); - - const { debugInfo } = await new BatchCall(user1Wallet, [ - nftContractAsUser1.methods - .prepare_transfer_to_private(sender, recipient, transientStorageSlotRandomness) - .request(), - nftContractAsUser1.methods - .finalize_transfer_to_private(TOKEN_ID, transferPreparerStorageSlotCommitment) - .request(), - ]) + + const { debugInfo } = await nftContractAsUser1.methods + .transfer_to_private(recipient, TOKEN_ID) .send() .wait({ debug: true }); From 7137fd991cdd6ee9e9ce82df6edb45cd88cd74c2 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 15:29:35 +0000 Subject: [PATCH 5/8] WIP --- noir-projects/noir-contracts/contracts/nft_contract/src/main.nr | 2 +- yarn-project/end-to-end/src/e2e_nft.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr index 4357d06f6a1b..3d93127c76b0 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -21,7 +21,7 @@ contract NFT { macros::{storage::storage, events::event, functions::{private, public, view, internal, initializer}} }; use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; - use std::{embedded_curve_ops::EmbeddedCurvePoint, meta::derive}; + use std::meta::derive; use crate::types::nft_note::NFTNote; // TODO(#8467): Rename this to Transfer - calling this NFTTransfer to avoid export conflict with the Transfer event diff --git a/yarn-project/end-to-end/src/e2e_nft.test.ts b/yarn-project/end-to-end/src/e2e_nft.test.ts index 7efbc9aef711..afa41541f15a 100644 --- a/yarn-project/end-to-end/src/e2e_nft.test.ts +++ b/yarn-project/end-to-end/src/e2e_nft.test.ts @@ -59,7 +59,7 @@ describe('NFT', () => { const nftContractAsUser1 = await NFTContract.at(nftContractAddress, user1Wallet); // In a simple "shield" flow the sender and recipient are the same. In the "uniswap swap to private" flow - // it would be the uniswap contract. + // the sender would be the uniswap contract. const recipient = user1Wallet.getAddress(); const { debugInfo } = await nftContractAsUser1.methods From 5521592d45d60fc2521d88d69d26a20050127902 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 15:40:06 +0000 Subject: [PATCH 6/8] last touches --- .../nft_contract/src/test/transfer_to_private.nr | 4 +++- .../contracts/nft_contract/src/test/utils.nr | 8 ++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr index def5494917fd..6b4c8b7b4e03 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr @@ -20,8 +20,10 @@ unconstrained fn transfer_to_private_to_self() { utils::assert_owns_public_nft(env, nft_contract_address, AztecAddress::zero(), token_id); } +/// External orchestration means that the calls to prepare and finalize are not done by the NFT contract. This flow +/// will typically be used by a DEX. #[test] -unconstrained fn transfer_to_private_to_a_different_account() { +unconstrained fn transfer_to_private_external_orchestration() { // Setup without account contracts. We are not using authwits here, so dummy accounts are enough let (env, nft_contract_address, sender, recipient, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr index 46376b342f7e..f55c70888f92 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr @@ -54,12 +54,8 @@ unconstrained pub fn setup_mint_and_transfer_to_private(with_account_contracts: // We mock the Oracle to return the note randomness such that later on we can manually add the note let _ = OracleMock::mock("getRandomField").returns(note_randomness); - // We prepare the transfer with user being both the sender and the recipient (classical "shield" flow) - let slot_commitment: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(owner, owner)); - - // Finalize the transfer of the NFT - let finalize_transfer_to_private_call_interface = NFT::at(nft_contract_address).finalize_transfer_to_private(minted_token_id, slot_commitment); - env.call_public(finalize_transfer_to_private_call_interface); + // We transfer the public NFT to private. + env.call_private_void(NFT::at(nft_contract_address).transfer_to_private(owner, minted_token_id)); // TODO(#8771): We need to manually add the note because in the partial notes flow `notify_created_note_oracle` // is not called and we don't have a `NoteProcessor` in TXE. From 6ef3490a45ee6bf00ce4b8a0032b07d3ed3409c4 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 15:44:35 +0000 Subject: [PATCH 7/8] comment fix --- .../noir-contracts/contracts/nft_contract/src/main.nr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr index 3d93127c76b0..e34c596e5ef9 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -185,9 +185,8 @@ contract NFT { #[internal] fn _store_point_in_transient_storage(point: Point) { let slot = point.x; - // We don't perform check for the overwritten value to be non-zero because the slot is implictly siloed to `to` - // (`to_note_slot` is part of the hiding point preimage) and hence `to` can interfere only with his own - // execution. + // We don't perform a check for the overwritten value to be 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. context.storage_write(slot, point); } From 07acfa955aef1122b288af5bd69fb5add4185e3d Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 10 Oct 2024 17:18:31 +0000 Subject: [PATCH 8/8] refactor: note hiding point --> partial commitment --- .../aztec/concepts/storage/partial_notes.md | 14 ++--- .../functions/inner_workings.md | 55 +++++++++---------- .../storage/storage_slots.md | 3 +- .../aztec-nr/aztec/src/macros/notes/mod.nr | 34 ++++++------ .../aztec-nr/aztec/src/note/utils.nr | 2 +- .../contracts/nft_contract/src/main.nr | 40 +++++++------- .../src/test/transfer_to_private.nr | 14 ++--- .../contracts/token_contract/src/main.nr | 8 +-- .../crates/types/src/constants.nr | 3 +- yarn-project/circuits.js/src/constants.gen.ts | 3 +- yarn-project/end-to-end/src/e2e_nft.test.ts | 2 +- .../simulator/src/client/test_utils.ts | 2 +- 12 files changed, 87 insertions(+), 93 deletions(-) diff --git a/docs/docs/aztec/concepts/storage/partial_notes.md b/docs/docs/aztec/concepts/storage/partial_notes.md index 06d3e2ff0a44..5415136271e7 100644 --- a/docs/docs/aztec/concepts/storage/partial_notes.md +++ b/docs/docs/aztec/concepts/storage/partial_notes.md @@ -69,11 +69,11 @@ Now let's do the same for partial notes. ## Partial notes life cycle 1. Create a partial/unfinished note in a private function of your contract --> partial here means that the values within the note are not yet considered finalized (e.g. `amount` in a `TokenNote`), -2. compute a note hiding point of the partial note using a multi scalar multiplication on an elliptic curve. For `TokenNote` this would be done as `G_amt * amount0 + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot`, where each `G_` is a generator point for a specific field in the note, +2. compute a partial commitment of the partial note using a multi scalar multiplication on an elliptic curve. For `TokenNote` this would be done as `G_amt * amount0 + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot`, where each `G_` is a generator point for a specific field in the note, 3. emit partial note log, -4. pass the note hiding point to a public function, -5. in a public function determine the value you want to add to the note (e.g. adding a value to an amount) and add it to the note hiding point (e.g. `NOTE_HIDING_POINT + G_amt * amount`), -6. get the note hash by finalizing the note hiding point (the note hash is the x coordinate of the point), +4. pass the partial commitment to a public function, +5. in a public function determine the value you want to add to the note (e.g. adding a value to an amount) and add it to the partial commitment (e.g. `PARTIAL_COMMITMENT + G_amt * amount`), +6. get the note hash by taking the x-coordinate of the resulting point, 7. emit the note hash, 8. emit the value added to the note in public as an unencrypted log (PXE then matches it with encrypted partial note log emitted from private), 9. from this point on the flow of partial notes is the same as for normal notes. @@ -88,10 +88,10 @@ The trouble is that the FPC doesn't know if Alice is going to run public functio And we can't use the normal flow to create a transaction fee refund note for Alice, since that demands we have Alice's address in public. -So we define a new type of note with its `compute_note_hiding_point` defined as: +So we define a new type of note with its `compute_note_hash` defined as: $$ -\text{amount}*G_{amount} + \text{address}*G_{address} + \text{randomness}*G_{randomness} + \text{slot}*G_{slot} +(\text{amount}*G_{amount} + \text{address}*G_{address} + \text{randomness}*G_{randomness} + \text{slot}*G_{slot}).x $$ Suppose Alice is willing to pay up to a set amount in stablecoins for her transaction. (Note, this amount gets passed into public so that when `transaction_fee` is known the FPC can verify that it isn't losing money. Wallets are expected to choose common values here, e.g. powers of 10). @@ -132,7 +132,7 @@ Then we just emit `P_a.x` and `P_b.x` as a note hashes, and we're done! ### Private Fee Payment Implementation -[`NoteInterface.nr`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/note/note_interface.nr) implements `compute_note_hiding_point`, which takes a note and computes the point "hides" it. +[`NoteInterface.nr`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/note/note_interface.nr) implements `compute_note_hash`, which takes a note and computes the point "hides" it. This is implemented by applying the `partial_note` attribute: diff --git a/docs/docs/aztec/smart_contracts/functions/inner_workings.md b/docs/docs/aztec/smart_contracts/functions/inner_workings.md index dc72805ea2c9..a0f755f9ae86 100644 --- a/docs/docs/aztec/smart_contracts/functions/inner_workings.md +++ b/docs/docs/aztec/smart_contracts/functions/inner_workings.md @@ -8,11 +8,11 @@ Below, we go more into depth of what is happening under the hood when you create If you are looking for a reference of function macros, go [here](../../../reference/developer_references/smart_contract_reference/macros.md). -## Private functions #[aztec(private)] +## Private functions #[private] -A private function operates on private information, and is executed by the user on their device. Annotate the function with the `#[aztec(private)]` attribute to tell the compiler it's a private function. This will make the [private context](./context.md#the-private-context) available within the function's execution scope. The compiler will create a circuit to define this function. +A private function operates on private information, and is executed by the user on their device. Annotate the function with the `#[private]` attribute to tell the compiler it's a private function. This will make the [private context](./context.md#the-private-context) available within the function's execution scope. The compiler will create a circuit to define this function. -`#aztec(private)` is just syntactic sugar. At compile time, the Aztec.nr framework inserts code that allows the function to interact with the [kernel](../../../aztec/concepts/circuits/kernels/private_kernel.md). +`#private` is just syntactic sugar. At compile time, the Aztec.nr framework inserts code that allows the function to interact with the [kernel](../../../aztec/concepts/circuits/kernels/private_kernel.md). To help illustrate how this interacts with the internals of Aztec and its kernel circuits, we can take an example private function, and explore what it looks like after Aztec.nr's macro expansion. @@ -107,7 +107,7 @@ Beyond using them inside your other functions, they are convenient for providing Note, that unconstrained functions can have access to both public and private data when executed on the user's device. This is possible since it is not actually part of the circuits that are executed in contract execution. ::: -## `Public` Functions #[aztec(public)] +## `Public` Functions #[public] A public function is executed by the sequencer and has access to a state model that is very similar to that of the EVM and Ethereum. Even though they work in an EVM-like model for public transactions, they are able to write data into private storage that can be consumed later by a private function. @@ -115,7 +115,7 @@ A public function is executed by the sequencer and has access to a state model t All data inserted into private storage from a public function will be publicly viewable (not private). ::: -To create a public function you can annotate it with the `#[aztec(public)]` attribute. This will make the public context available within the function's execution scope. +To create a public function you can annotate it with the `#[public]` attribute. This will make the public context available within the function's execution scope. #include_code set_minter /noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust @@ -139,17 +139,17 @@ let storage = Storage::init(&mut context); - Visibility Control: The function is marked as pub, making it accessible from outside the contract. - Unconstrained Execution: Public functions are marked as unconstrained, meaning they don't generate proofs and are executed directly by the sequencer. -## Constrained `view` Functions #[aztec(view)] +## Constrained `view` Functions #[view] -The `#[aztec(view)]` attribute is used to define constrained view functions in Aztec contracts. These functions are similar to view functions in Solidity, in that they are read-only and do not modify the contract's state. They are similar to the [`unconstrained`](#unconstrained-functions-aztecunconstrained) keyword but are executed in a constrained environment. It is not possible to update state within an `#[aztec(view)]` function. +The `#[view]` attribute is used to define constrained view functions in Aztec contracts. These functions are similar to view functions in Solidity, in that they are read-only and do not modify the contract's state. They are similar to the [`unconstrained`](#unconstrained-functions-aztecunconstrained) keyword but are executed in a constrained environment. It is not possible to update state within an `#[view]` function. This means the results of these functions are verifiable and can be trusted, as they are part of the proof generation and verification process. This is unlike unconstrained functions, where results are provided by the PXE and are not verified. -This makes `#[aztec(view)]` functions suitable for critical read-only operations where the integrity of the result is crucial. Unconstrained functions, on the other hand, are executed entirely client-side without generating any proofs. It is better to use `#[aztec(view)]` if the result of the function will be used in another function that will affect state, and they can be used for cross-contract calls. +This makes `#[view]` functions suitable for critical read-only operations where the integrity of the result is crucial. Unconstrained functions, on the other hand, are executed entirely client-side without generating any proofs. It is better to use `#[view]` if the result of the function will be used in another function that will affect state, and they can be used for cross-contract calls. -`#[aztec(view)]` functions can be combined with other Aztec attributes like `#[aztec(private)]` or `#[aztec(public)]`. +`#[view]` functions can be combined with other Aztec attributes like `#[private]` or `#[public]`. -## `Initializer` Functions #[aztec(initializer)] +## `Initializer` Functions #[initializer] This is used to designate functions as initializers (or constructors) for an Aztec contract. These functions are responsible for setting up the initial state of the contract when it is first deployed. The macro does two important things: @@ -159,18 +159,18 @@ This is used to designate functions as initializers (or constructors) for an Azt Key things to keep in mind: - A contract can have multiple initializer functions defined, but only one initializer function should be called for the lifetime of a contract instance -- Other functions in the contract will have an initialization check inserted, ie they cannot be called until the contract is initialized, unless they are marked with [`#[aztec(noinitcheck)]`](#aztecnoinitcheck) +- Other functions in the contract will have an initialization check inserted, ie they cannot be called until the contract is initialized, unless they are marked with [`#[noinitcheck]`](#aztecnoinitcheck) -## #[aztec(noinitcheck)] +## #[noinitcheck] -In normal circumstances, all functions in an Aztec contract (except initializers) have an initialization check inserted at the beginning of the function body. This check ensures that the contract has been initialized before any other function can be called. However, there may be scenarios where you want a function to be callable regardless of the contract's initialization state. This is when you would use `#[aztec(noinitcheck)]`. +In normal circumstances, all functions in an Aztec contract (except initializers) have an initialization check inserted at the beginning of the function body. This check ensures that the contract has been initialized before any other function can be called. However, there may be scenarios where you want a function to be callable regardless of the contract's initialization state. This is when you would use `#[noinitcheck]`. -When a function is annotated with `#[aztec(noinitcheck)]`: +When a function is annotated with `#[noinitcheck]`: - The Aztec macro processor skips the [insertion of the initialization check](#initializer-functions-aztecinitializer) for this specific function - The function can be called at any time, even if the contract hasn't been initialized yet -## `Internal` functions #[aztec(internal)] +## `Internal` functions #[internal] This macro inserts a check at the beginning of the function to ensure that the caller is the contract itself. This is done by adding the following assertion: @@ -178,18 +178,18 @@ This macro inserts a check at the beginning of the function to ensure that the c assert(context.msg_sender() == context.this_address(), "Function can only be called internally"); ``` -## Custom notes #[aztec(note)] +## Custom notes #[note] -The `#[aztec(note)]` attribute is used to define custom note types in Aztec contracts. Learn more about notes [here](../../concepts/storage/index.md). +The `#[note]` attribute is used to define custom note types in Aztec contracts. Learn more about notes [here](../../concepts/storage/index.md). -When a struct is annotated with `#[aztec(note)]`, the Aztec macro applies a series of transformations and generates implementations to turn it into a note that can be used in contracts to store private data. +When a struct is annotated with `#[note]`, the Aztec macro applies a series of transformations and generates implementations to turn it into a note that can be used in contracts to store private data. 1. **NoteInterface Implementation**: The macro automatically implements most methods of the `NoteInterface` trait for the annotated struct. This includes: - `serialize_content` and `deserialize_content` - `get_header` and `set_header` - `get_note_type_id` - - `compute_note_hiding_point` + - `compute_note_hash` - `to_be_bytes` - A `properties` method in the note's implementation @@ -208,7 +208,7 @@ When a struct is annotated with `#[aztec(note)]`, the Aztec macro applies a seri Here is how you could define a custom note: ```rust -#[aztec(note)] +#[note] struct CustomNote { data: Field, owner: Address, @@ -244,11 +244,8 @@ impl CustomNote { self.header = header; } - fn compute_note_hiding_point(self: CustomNote) -> Point { - aztec::hash::pedersen_commitment( - self.serialize_content(), - aztec::protocol_types::constants::GENERATOR_INDEX__NOTE_HIDING_POINT - ) + fn compute_note_hash(self: CustomNote) -> Field { + aztec::hash::poseidon_hash(self.serialize_content()) } fn to_be_bytes(self, storage_slot: Field) -> [u8; 128] { @@ -293,11 +290,11 @@ Key things to keep in mind: - Developers can override any of the auto-generated methods by specifying a note interface - The note's fields are automatically serialized and deserialized in the order they are defined in the struct -## Storage struct #[aztec(storage)] +## Storage struct #[storage] -The `#[aztec(storage)]` attribute is used to define the storage structure for an Aztec contract. +The `#[storage]` attribute is used to define the storage structure for an Aztec contract. -When a struct is annotated with `#[aztec(storage)]`, the macro does this under the hood: +When a struct is annotated with `#[storage]`, the macro does this under the hood: 1. **Context Injection**: injects a `Context` generic parameter into the storage struct and all its fields. This allows the storage to interact with the Aztec context, eg when using `context.msg_sender()` @@ -310,7 +307,7 @@ When a struct is annotated with `#[aztec(storage)]`, the macro does this under t ### Before expansion ```rust -#[aztec(storage)] +#[storage] struct Storage { balance: PublicMutable, owner: PublicMutable
, diff --git a/docs/docs/guides/developer_guides/smart_contracts/writing_contracts/storage/storage_slots.md b/docs/docs/guides/developer_guides/smart_contracts/writing_contracts/storage/storage_slots.md index ba3c45fe4c69..02295280af3b 100644 --- a/docs/docs/guides/developer_guides/smart_contracts/writing_contracts/storage/storage_slots.md +++ b/docs/docs/guides/developer_guides/smart_contracts/writing_contracts/storage/storage_slots.md @@ -31,8 +31,7 @@ sequenceDiagram BalanceSet->>Set: insert(note) Set->>LifeCycle: create_note(derived_slot, note) LifeCycle->>LifeCycle: note.header = NoteHeader { contract_address,
storage_slot: derived_slot, nonce: 0, note_hash_counter } - Utils->>TokenNote: note_hiding_point = note.compute_note_hiding_point() - TokenNote->>Utils: note_hash = note_hiding_point.x + Utils->>TokenNote: note_hash = note.compute_note_hash() LifeCycle->>Context: push_note_hash(note_hash) end Context->>Kernel: unique_note_hash = H(nonce, note_hash) diff --git a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr index ec702f4f7b11..8c9960100320 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr @@ -318,18 +318,18 @@ comptime fn generate_multi_scalar_mul(indexed_fields: [(Quoted, Type, u32)]) -> (generators_list, scalars_list, args_list, aux_vars) } -/// Generates setup payload for a given note struct `s`. The setup payload contains log plaintext and hiding point. +/// Generates setup payload for a given note struct `s`. The setup payload contains log plaintext and partial commitment. /// /// Example: /// ``` /// struct TokenNoteSetupPayload { /// log_plaintext: [u8; 160], -/// hiding_point: aztec::protocol_types::point::Point +/// partial_commitment: aztec::protocol_types::point::Point /// } /// /// impl TokenNoteSetupPayload { /// fn new(mut self, npk_m_hash: Field, randomness: Field, storage_slot: Field) -> TokenNoteSetupPayload { -/// let hiding_point = std::embedded_curve_ops::multi_scalar_mul( +/// let partial_commitment = std::embedded_curve_ops::multi_scalar_mul( /// [aztec::generators::Ga1, aztec::generators::Ga2, aztec::generators::G_slot], /// [ /// std::hash::from_field_unsafe(npk_m_hash), @@ -357,14 +357,14 @@ comptime fn generate_multi_scalar_mul(indexed_fields: [(Quoted, Type, u32)]) -> /// /// TokenNoteSetupPayload { /// log_plaintext, -/// hiding_point +/// partial_commitment /// } /// } /// } /// /// impl aztec::protocol_types::traits::Empty for TokenNoteSetupPayload { /// fn empty() -> Self { -/// Self { log_plaintext: [0; 160], hiding_point: aztec::protocol_types::point::Point::empty() } +/// Self { log_plaintext: [0; 160], partial_commitment: aztec::protocol_types::point::Point::empty() } /// } /// } /// ``` @@ -389,13 +389,13 @@ comptime fn generate_setup_payload( (quote { struct $setup_payload_name { log_plaintext: [u8; $log_plaintext_length], - hiding_point: aztec::protocol_types::point::Point + partial_commitment: aztec::protocol_types::point::Point } impl $setup_payload_name { fn new($new_args) -> $setup_payload_name { $new_aux_vars - let hiding_point = std::embedded_curve_ops::multi_scalar_mul( + let partial_commitment = std::embedded_curve_ops::multi_scalar_mul( [$new_generators], [$new_scalars] ); @@ -403,14 +403,14 @@ comptime fn generate_setup_payload( $setup_payload_name { log_plaintext, - hiding_point + partial_commitment } } } impl aztec::protocol_types::traits::Empty for $setup_payload_name { fn empty() -> Self { - Self { log_plaintext: [0; $log_plaintext_length], hiding_point: aztec::protocol_types::point::Point::empty() } + Self { log_plaintext: [0; $log_plaintext_length], partial_commitment: aztec::protocol_types::point::Point::empty() } } } }, setup_payload_name) @@ -474,18 +474,18 @@ comptime fn get_setup_log_plaintext_body( /// } /// /// impl TokenNoteFinalizationPayload { -/// fn new(mut self, hiding_point: aztec::protocol_types::point::Point, amount: U128) -> TokenNoteFinalizationPayload { +/// fn new(mut self, partial_commitment: aztec::protocol_types::point::Point, amount: U128) -> TokenNoteFinalizationPayload { /// self.log = [amount.lo as Field, amount.hi as Field]; /// -/// let finalization_hiding_point = std::embedded_curve_ops::multi_scalar_mul( +/// let final_commitment = std::embedded_curve_ops::multi_scalar_mul( /// [aztec::generators::Ga3, aztec::generators::Ga4], /// [ /// std::hash::from_field_unsafe(amount.lo), /// std::hash::from_field_unsafe(amount.hi) /// ] -/// ) + hiding_point; +/// ) + partial_commitment; /// -/// self.note_hash = finalization_hiding_point.x; +/// self.note_hash = final_commitment.x; /// self /// } /// } @@ -536,17 +536,17 @@ comptime fn generate_finalization_payload( } impl $finalization_payload_name { - fn new(mut self, hiding_point: aztec::protocol_types::point::Point, $args) -> $finalization_payload_name { + fn new(mut self, partial_commitment: aztec::protocol_types::point::Point, $args) -> $finalization_payload_name { $aux_vars_for_serialization self.log = [$fields]; $msm_aux_vars - let finalization_hiding_point = std::embedded_curve_ops::multi_scalar_mul( + let final_commitment = std::embedded_curve_ops::multi_scalar_mul( [$generators], [$scalars] - ) + hiding_point; + ) + partial_commitment; - self.note_hash = finalization_hiding_point.x; + self.note_hash = final_commitment.x; self } } diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 47f58177c04e..52cf9e0bebc4 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -68,7 +68,7 @@ pub fn compute_note_hash_for_nullify_internal( // // the same transaction: (note_hash_counter != 0) & (nonce != 0) // // 3. The note was inserted in a previous transaction: (note_hash_counter == 0) & (nonce != 0) -// let note_hash = note.compute_note_hiding_point().x; +// let note_hash = note.compute_note_hash(); // if header.nonce == 0 { // // Case 1. diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr index e34c596e5ef9..325d09391dfc 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -143,14 +143,14 @@ contract NFT { let nft = NFT::at(context.this_address()); // We prepare the transfer. - let hiding_point_slot = nft.prepare_transfer_to_private(from, to).call(&mut context); + let partial_commitment_slot = nft.prepare_transfer_to_private(from, to).call(&mut context); // At last we finalize the transfer. - nft._finalize_transfer_to_private_trusted(from, token_id, hiding_point_slot).enqueue(&mut context); + nft._finalize_transfer_to_private_trusted(from, token_id, partial_commitment_slot).enqueue(&mut context); } /// Prepares a transfer from public balance of `from` to a private balance of `to`. The transfer then needs to be - /// finalized by calling `finalize_transfer_to_private`. Returns a hiding point slot. + /// finalized by calling `finalize_transfer_to_private`. Returns a partial commitment slot. #[private] fn prepare_transfer_to_private(from: AztecAddress, to: AztecAddress) -> Field { let to_keys = get_public_keys(to); @@ -168,17 +168,17 @@ contract NFT { // We encrypt and emit the partial note log encrypt_and_emit_partial_log(&mut context, note_setup_payload.log_plaintext, to_keys, to); - NFT::at(context.this_address())._store_point_in_transient_storage(note_setup_payload.hiding_point).enqueue(&mut context); + NFT::at(context.this_address())._store_point_in_transient_storage(note_setup_payload.partial_commitment).enqueue(&mut context); - // Using the x-coordinate as a hiding point slot is safe because we have a guarantee that the public functions + // Using the x-coordinate as a partial commitment slot is safe 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 + // that nobody can front-run us in consuming the partial commitment. 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 hash the x-coordinate with // `from` and then repeat the hashing in `finalize_transfer_to_private`. - let hiding_point_slot = note_setup_payload.hiding_point.x; + let partial_commitment_slot = note_setup_payload.partial_commitment.x; - hiding_point_slot + partial_commitment_slot } #[public] @@ -186,45 +186,45 @@ contract NFT { fn _store_point_in_transient_storage(point: Point) { let slot = point.x; // We don't perform a check for the overwritten value to be 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. + // of the partial commitment and hence we could only overwrite the value in the slot with the same value. context.storage_write(slot, point); } /// Finalizes a transfer of NFT with `token_id` 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. + /// `partial_commitment_slot` must be passed as an argument to this function. #[public] - fn finalize_transfer_to_private(token_id: Field, hiding_point_slot: Field) { + fn finalize_transfer_to_private(token_id: Field, partial_commitment_slot: Field) { let from = context.msg_sender(); - _finalize_transfer_to_private(from, token_id, hiding_point_slot, &mut context, storage); + _finalize_transfer_to_private(from, token_id, partial_commitment_slot, &mut context, storage); } #[public] #[internal] - fn _finalize_transfer_to_private_trusted(from: AztecAddress, token_id: Field, hiding_point_slot: Field) { - _finalize_transfer_to_private(from, token_id, hiding_point_slot, &mut context, storage); + fn _finalize_transfer_to_private_trusted(from: AztecAddress, token_id: Field, partial_commitment_slot: Field) { + _finalize_transfer_to_private(from, token_id, partial_commitment_slot, &mut context, storage); } #[contract_library_method] fn _finalize_transfer_to_private( from: AztecAddress, token_id: Field, - hiding_point_slot: Field, + partial_commitment_slot: Field, context: &mut PublicContext, storage: Storage<&mut PublicContext> ) { let public_owners_storage = storage.public_owners.at(token_id); assert(public_owners_storage.read().eq(from), "invalid NFT owner"); - // Read the hiding point from "transient" storage and check it's not empty to ensure the transfer was prepared - let hiding_point: Point = context.storage_read(hiding_point_slot); - assert(!is_empty(hiding_point), "transfer not prepared"); + // Read the partial commitment from "transient" storage and check it's not empty to ensure the transfer was prepared + let partial_commitment: Point = context.storage_read(partial_commitment_slot); + assert(!is_empty(partial_commitment), "transfer not prepared"); // Set the public NFT owner to zero public_owners_storage.write(AztecAddress::zero()); // Finalize the partial note with the `token_id` - let finalization_payload = NFTNote::finalization_payload().new(hiding_point, token_id); + let finalization_payload = NFTNote::finalization_payload().new(partial_commitment, token_id); // We insert the finalization note hash context.push_note_hash(finalization_payload.note_hash); @@ -234,7 +234,7 @@ contract NFT { // At last we reset public storage to zero to achieve the effect of transient storage - kernels will squash // the writes - context.storage_write(hiding_point_slot, Point::empty()); + context.storage_write(partial_commitment_slot, Point::empty()); } /** diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr index 6b4c8b7b4e03..85e1ab1cc3ad 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr @@ -33,10 +33,10 @@ unconstrained fn transfer_to_private_external_orchestration() { let _ = OracleMock::mock("getRandomField").returns(note_randomness); // We prepare the transfer - let hiding_point_slot: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(sender, recipient)); + let partial_commitment_slot: Field = env.call_private(NFT::at(nft_contract_address).prepare_transfer_to_private(sender, recipient)); // Finalize the transfer of the NFT - env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot)); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, partial_commitment_slot)); // TODO(#8771): We need to manually add the note because in the partial notes flow `notify_created_note_oracle` // is not called and we don't have a `NoteProcessor` in TXE. @@ -61,11 +61,11 @@ unconstrained fn transfer_to_private_to_self_transfer_not_prepared() { // Setup without account contracts. We are not using authwits here, so dummy accounts are enough let (env, nft_contract_address, _, _, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); - // Transfer was not prepared so we can use random value for the hiding point slot - let hiding_point_slot = random(); + // Transfer was not prepared so we can use random value for the partial commitment slot + let partial_commitment_slot = random(); // Try finalizing the transfer without preparing it - env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot)); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, partial_commitment_slot)); } #[test(should_fail_with="invalid NFT owner")] @@ -74,9 +74,9 @@ unconstrained fn transfer_to_private_failure_not_an_owner() { let (env, nft_contract_address, _, not_owner, token_id) = utils::setup_and_mint(/* with_account_contracts */ false); // We set random value for the commitment as the NFT owner check is before we use the value - let hiding_point_slot = random(); + let partial_commitment_slot = random(); // Try transferring someone else's public NFT env.impersonate(not_owner); - env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot)); + env.call_public(NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, partial_commitment_slot)); } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 1525d3fce63f..c625a359d77e 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -516,11 +516,11 @@ contract Token { ); encrypt_and_emit_partial_log(&mut context, user_setup_payload.log_plaintext, user_keys, user); - // 6. We convert the hiding points to standard `Point` type as we cannot pass `TokenNoteHidingPoint` type + // 6. We convert the partial commitments to standard `Point` type as we cannot pass `TokenNoteHidingPoint` type // as an argument to a function due to macro limitations (the `TokenNoteHidingPoint` type is macro generated // and hence is not resolved soon enough by the compiler). - let fee_payer_point = fee_payer_setup_payload.hiding_point; - let user_point = user_setup_payload.hiding_point; + let fee_payer_point = fee_payer_setup_payload.partial_commitment; + let user_point = user_setup_payload.partial_commitment; // 7. 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. @@ -553,7 +553,7 @@ contract Token { // 2. We compute the refund amount as the difference between funded amount and tx fee. let refund_amount = funded_amount - tx_fee; - // 3. We construct the note finalization payloads with the correct amounts and hiding points to get the note + // 3. We construct the note finalization payloads with the correct amounts and partial commitments to get the note // hashes and unencrypted logs. let fee_payer_finalization_payload = TokenNote::finalization_payload().new(fee_payer_point, tx_fee); let user_finalization_payload = TokenNote::finalization_payload().new(user_point, refund_amount); diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index b4871c87ac13..06e7dd2af704 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -398,8 +398,7 @@ global GENERATOR_INDEX__OVSK_M: u32 = 50; global GENERATOR_INDEX__TSK_M: u32 = 51; global GENERATOR_INDEX__PUBLIC_KEYS_HASH: u32 = 52; global GENERATOR_INDEX__NOTE_NULLIFIER: u32 = 53; -global GENERATOR_INDEX__NOTE_HIDING_POINT: u32 = 54; -global GENERATOR_INDEX__SYMMETRIC_KEY: u8 = 55; +global GENERATOR_INDEX__SYMMETRIC_KEY: u8 = 54; // AVM memory tags global MEM_TAG_U1 = 1; diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index d0825bf30ce4..ec869a98ae40 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -371,6 +371,5 @@ export enum GeneratorIndex { TSK_M = 51, PUBLIC_KEYS_HASH = 52, NOTE_NULLIFIER = 53, - NOTE_HIDING_POINT = 54, - SYMMETRIC_KEY = 55, + SYMMETRIC_KEY = 54, } diff --git a/yarn-project/end-to-end/src/e2e_nft.test.ts b/yarn-project/end-to-end/src/e2e_nft.test.ts index afa41541f15a..ef376fdca741 100644 --- a/yarn-project/end-to-end/src/e2e_nft.test.ts +++ b/yarn-project/end-to-end/src/e2e_nft.test.ts @@ -70,7 +70,7 @@ describe('NFT', () => { const publicOwnerAfter = await nftContractAsUser1.methods.owner_of(TOKEN_ID).simulate(); expect(publicOwnerAfter).toEqual(AztecAddress.ZERO); - // We should get 4 data writes setting values to 0 - 3 for note hiding point and 1 for public owner (we transfer + // We should get 4 data writes setting values to 0 - 3 for partial commitment and 1 for public owner (we transfer // to private so public owner is set to 0). Ideally we would have here only 1 data write as the 4 values change // from zero to non-zero to zero in the tx and hence no write could be committed. This makes public writes // squashing too expensive for transient storage. This however probably does not matter as I assume we will want diff --git a/yarn-project/simulator/src/client/test_utils.ts b/yarn-project/simulator/src/client/test_utils.ts index af7fa0345d92..8819bd337d14 100644 --- a/yarn-project/simulator/src/client/test_utils.ts +++ b/yarn-project/simulator/src/client/test_utils.ts @@ -37,7 +37,7 @@ const G_SLOT = new Point( ); /** - * Computes a note hiding point as is done by the default implementation injected by macros. + * Computes a note hash as is done by the default implementation injected by macros. * @param storageSlot - The slot to which the note was inserted. * @param noteContent - The note content (e.g. note.items). * @returns A note hash.