diff --git a/noir-projects/aztec-nr/aztec/src/prelude.nr b/noir-projects/aztec-nr/aztec/src/prelude.nr index f3ba3ffbb9c..bfa103d66f8 100644 --- a/noir-projects/aztec-nr/aztec/src/prelude.nr +++ b/noir-projects/aztec-nr/aztec/src/prelude.nr @@ -8,7 +8,7 @@ pub use crate::{ map::Map, private_immutable::PrivateImmutable, private_mutable::PrivateMutable, 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/aztec-nr/aztec/src/utils/bytes.nr b/noir-projects/aztec-nr/aztec/src/utils/bytes.nr index 58bbd233065..355ddad2b57 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/bytes.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/bytes.nr @@ -75,7 +75,8 @@ mod test { #[test] fn test_bytes_to_1_field() { let input = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, ]; let output = bytes_to_fields::<31, 1>(input); @@ -88,9 +89,11 @@ mod test { let output = fields_to_bytes::<31, 1>(input); assert_eq( - output, [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 - ] + output, + [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + ], ); } @@ -101,9 +104,13 @@ mod test { // Each field should occupy 31 bytes with the non-zero value being placed in the last one. assert_eq( - output, [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3 - ] + output, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 3, + ], ); } @@ -116,16 +123,21 @@ mod test { // field should occupy 1 byte. There is not information destruction here because the last field fits into // 1 byte. assert_eq( - output, [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3 - ] + output, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 3, + ], ); } #[test] fn test_bytes_to_2_fields() { let input = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, ]; let output = bytes_to_fields::<59, 2>(input); @@ -136,14 +148,18 @@ mod test { #[test] fn test_2_fields_to_bytes() { let input = [ - 0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f, 0x202122232425262728292a2b2c2d2e2f303132333435363738393a3b + 0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f, + 0x202122232425262728292a2b2c2d2e2f303132333435363738393a3b, ]; let output = fields_to_bytes::<62, 2>(input); assert_eq( - output, [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 0, 0, 0, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59 - ] + output, + [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 0, 0, 0, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + ], ); } @@ -165,11 +181,16 @@ mod test { input3: [u64; 5], input4: [u32; 5], input5: [u16; 5], - input6: [u8; 5] + input6: [u8; 5], ) { let mut input = [0; 5]; for i in 0..5 { - input[i] = (input1[i] as Field * 2.pow_32(184)) + (input2[i] as Field * 2.pow_32(120)) + (input3[i] as Field * 2.pow_32(56)) + (input4[i] as Field * 2.pow_32(24)) + (input5[i] as Field * 2.pow_32(8)) + input6[i] as Field; + input[i] = (input1[i] as Field * 2.pow_32(184)) + + (input2[i] as Field * 2.pow_32(120)) + + (input3[i] as Field * 2.pow_32(56)) + + (input4[i] as Field * 2.pow_32(24)) + + (input5[i] as Field * 2.pow_32(8)) + + input6[i] as Field; } let output = fields_to_bytes::<155, 5>(input); 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 3d142c4fb95..bdab2dac015 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -12,7 +12,7 @@ contract NFT { oracle::random::random, prelude::{ NoteGetterOptions, NoteViewerOptions, Map, PublicMutable, SharedImmutable, PrivateSet, - AztecAddress, + AztecAddress, PrivateContext, PublicContext, }, encrypted_logs::encrypted_note_emission::{ encode_and_encrypt_note, encrypt_and_emit_partial_log, @@ -31,8 +31,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] @@ -144,21 +142,46 @@ contract NFT { public_owners_storage.write(to); } - /// 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 to the followup call to `finalize_transfer_to_private`. + // Transfers token with `token_id` from public balance of message sender to a private balance of `to`. #[private] - fn prepare_transfer_to_private( - from: AztecAddress, + 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 = _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 NFTs. + nft._finalize_transfer_to_private_unsafe(from, token_id, 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, - transient_storage_slot_randomness: Field, - ) { + context: &mut PrivateContext, + storage: Storage<&mut PrivateContext>, + ) -> 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 { random() }; @@ -166,56 +189,70 @@ contract NFT { NFTNote::setup_payload().new(to_npk_m_hash, note_randomness, to_note_slot); // 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 transfer_preparer_storage_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], - TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX, - ); - + encrypt_and_emit_partial_log(context, note_setup_payload.log_plaintext, to_keys, 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. NFT::at(context.this_address()) - ._store_point_in_transient_storage(note_setup_payload.hiding_point, slot) - .enqueue(&mut context); + ._store_point_in_transient_storage_unsafe( + hiding_point_slot, + note_setup_payload.hiding_point, + ) + .enqueue(context); + + 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_unsafe(slot: Field, point: Point) { 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. - /// 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 + /// `hiding_point_slot` must be passed as an argument to this function. #[public] - fn finalize_transfer_to_private( + fn finalize_transfer_to_private(token_id: Field, hiding_point_slot: Field) { + 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_unsafe( + from: AztecAddress, token_id: Field, - transfer_preparer_storage_slot_commitment: 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>, ) { - // 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. - 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(), transfer_preparer_storage_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"); @@ -329,3 +366,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 c9a0bc9e460..9d48bdb4977 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,13 +1,17 @@ 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; +/// Internal orchestration means that the calls to `prepare_transfer_to_private` +/// and `finalize_transfer_to_private` are done by the NFT contract itself. +/// In this test's case this is done by the `NFT::transfer_to_private(...)` function called +/// in `utils::setup_mint_and_transfer_to_private`. #[test] -unconstrained fn transfer_to_private_to_self() { - // The transfer to private to self is done in `utils::setup_mint_and_transfer_to_private` and for this reason +unconstrained fn transfer_to_private_internal_orchestration() { + // The transfer to private is done in `utils::setup_mint_and_transfer_to_private` and for this reason // in this test we just call it and check the outcome. // Setup without account contracts. We are not using authwits here, so dummy accounts are enough let (env, nft_contract_address, user, _, token_id) = @@ -20,32 +24,28 @@ 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) = + let (env, nft_contract_address, _, 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 - NFT::at(nft_contract_address) - .prepare_transfer_to_private(sender, recipient, transient_storage_slot_randomness) + let hiding_point_slot: Field = NFT::at(nft_contract_address) + .prepare_transfer_to_private(recipient) .call(&mut env.private()); - // Finalize the transfer of the NFT - NFT::at(nft_contract_address) - .finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) - .call(&mut env.public()); + // Finalize the transfer of the NFT (message sender owns the NFT in public) + NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot).call( + &mut env.public(), + ); // 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. @@ -72,48 +72,18 @@ unconstrained fn transfer_to_private_to_a_different_account() { } #[test(should_fail_with = "transfer not prepared")] -unconstrained fn transfer_to_private_to_self_transfer_not_prepared() { +unconstrained fn transfer_to_private_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 transfer_preparer_storage_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 - NFT::at(nft_contract_address) - .finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) - .call(&mut env.public()) -} - -#[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); - - 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, + NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot).call( + &mut env.public(), ); - - // We prepare the transfer - NFT::at(nft_contract_address) - .prepare_transfer_to_private(correct_sender, recipient, transient_storage_slot_randomness) - .call(&mut env.private()); - - // 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); - - NFT::at(nft_contract_address) - .finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) - .call(&mut env.public()); } #[test(should_fail_with = "invalid NFT owner")] @@ -122,13 +92,16 @@ 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(); + // (For this specific test we could set a random value for the commitment and not do the call to `prepare...` + // as the NFT owner check is before we use the value but that would made the test less robust against changes + // in the contract.) + let hiding_point_slot: Field = NFT::at(nft_contract_address) + .prepare_transfer_to_private(not_owner) + .call(&mut env.private()); // Try transferring someone else's public NFT env.impersonate(not_owner); - - NFT::at(nft_contract_address) - .finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) - .call(&mut env.public()); + NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, hiding_point_slot).call( + &mut env.public(), + ); } 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 fdf076c1021..bd4829003eb 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 @@ -59,24 +59,14 @@ pub unconstrained fn setup_mint_and_transfer_to_private( 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) - NFT::at(nft_contract_address) - .prepare_transfer_to_private(owner, owner, transient_storage_slot_randomness) - .call(&mut env.private()); - - // Finalize the transfer of the NFT - NFT::at(nft_contract_address) - .finalize_transfer_to_private(minted_token_id, transfer_preparer_storage_slot_commitment) - .call(&mut env.public()); + // We transfer the public NFT to private. + NFT::at(nft_contract_address).transfer_to_private(owner, minted_token_id).call( + &mut env.private(), + ); // 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. 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 a63049dbf0a..1f7c5eb3008 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[]; @@ -60,24 +58,12 @@ describe('NFT', () => { it('transfers to private', async () => { 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. - 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(), - ]) + // In a simple "shield" flow the sender and recipient are the same. In the "AMM swap to private" flow + // the sender would be the AMM contract. + const recipient = user2Wallet.getAddress(); + + const { debugInfo } = await nftContractAsUser1.methods + .transfer_to_private(recipient, TOKEN_ID) .send() .wait({ debug: true }); @@ -94,29 +80,29 @@ describe('NFT', () => { }); it('transfers in private', async () => { - const nftContractAsUser1 = await NFTContract.at(nftContractAddress, user1Wallet); + const nftContractAsUser2 = await NFTContract.at(nftContractAddress, user2Wallet); - await nftContractAsUser1.methods - .transfer_in_private(user1Wallet.getAddress(), user2Wallet.getAddress(), TOKEN_ID, 0) + await nftContractAsUser2.methods + .transfer_in_private(user2Wallet.getAddress(), user1Wallet.getAddress(), TOKEN_ID, 0) .send() .wait(); const user1Nfts = await getPrivateNfts(user1Wallet.getAddress()); - expect(user1Nfts).toEqual([]); + expect(user1Nfts).toEqual([TOKEN_ID]); const user2Nfts = await getPrivateNfts(user2Wallet.getAddress()); - expect(user2Nfts).toEqual([TOKEN_ID]); + expect(user2Nfts).toEqual([]); }); it('transfers to public', async () => { - const nftContractAsUser2 = await NFTContract.at(nftContractAddress, user2Wallet); + const nftContractAsUser1 = await NFTContract.at(nftContractAddress, user1Wallet); - await nftContractAsUser2.methods - .transfer_to_public(user2Wallet.getAddress(), user2Wallet.getAddress(), TOKEN_ID, 0) + await nftContractAsUser1.methods + .transfer_to_public(user1Wallet.getAddress(), user2Wallet.getAddress(), TOKEN_ID, 0) .send() .wait(); - const publicOwnerAfter = await nftContractAsUser2.methods.owner_of(TOKEN_ID).simulate(); + const publicOwnerAfter = await nftContractAsUser1.methods.owner_of(TOKEN_ID).simulate(); expect(publicOwnerAfter).toEqual(user2Wallet.getAddress()); });