diff --git a/boxes/boxes/react/src/config.ts b/boxes/boxes/react/src/config.ts index faf312be0b74..d2aac474f2ca 100644 --- a/boxes/boxes/react/src/config.ts +++ b/boxes/boxes/react/src/config.ts @@ -23,5 +23,5 @@ export class PrivateEnv { export const deployerEnv = await PrivateEnv.create(process.env.PXE_URL || 'http://localhost:8080'); -const IGNORE_FUNCTIONS = ['constructor', 'compute_note_hash_and_optionally_a_nullifier']; +const IGNORE_FUNCTIONS = ['constructor']; export const filteredInterface = BoxReactContractArtifact.functions.filter(f => !IGNORE_FUNCTIONS.includes(f.name)); diff --git a/boxes/boxes/vite/src/config.ts b/boxes/boxes/vite/src/config.ts index ac254682eeba..f2b3c0e9804e 100644 --- a/boxes/boxes/vite/src/config.ts +++ b/boxes/boxes/vite/src/config.ts @@ -77,12 +77,7 @@ export class PrivateEnv { export const deployerEnv = new PrivateEnv(); -const IGNORE_FUNCTIONS = [ - "constructor", - "compute_note_hash_and_optionally_a_nullifier", - "process_log", - "sync_notes", -]; +const IGNORE_FUNCTIONS = ["constructor", "process_log", "sync_notes"]; export const filteredInterface = BoxReactContractArtifact.functions.filter( (f) => !IGNORE_FUNCTIONS.includes(f.name), ); diff --git a/docs/docs/aztec/concepts/accounts/index.md b/docs/docs/aztec/concepts/accounts/index.md index bfd8d6a2971f..de5aa8e03e69 100644 --- a/docs/docs/aztec/concepts/accounts/index.md +++ b/docs/docs/aztec/concepts/accounts/index.md @@ -102,7 +102,7 @@ A side-effect of not having nonces at the protocol level is that it is not possi Since the `entrypoint` interface is not enshrined, there is nothing that differentiates an account contract from an application one in the protocol. This means that a transaction can be initiated in any contract. This allows implementing functions that do not need to be called by any particular user and are just intended to advance the state of a contract. -As an example, we can think of a lottery contract, where at some point a prize needs to be paid out to its winners. This `pay` action does not require authentication and does not need to be executed by any user in particular, so anyone could submit a transaction that defines the lottery contract itself as `origin` and `pay` as entrypoint function. For an example of this behavior see our [non_contract_account test](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/end-to-end/src/e2e_non_contract_account.test.ts) and the [SignerLess wallet](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/aztec.js/src/wallet/signerless_wallet.ts) implementation. +As an example, we can think of a lottery contract, where at some point a prize needs to be paid out to its winners. This `pay` action does not require authentication and does not need to be executed by any user in particular, so anyone could submit a transaction that defines the lottery contract itself as `origin` and `pay` as entrypoint function. Notice that the Signerless wallet doesn't invoke an entrypoint function of an account contract but instead invokes the target contract function directly. :::info diff --git a/docs/docs/aztec/smart_contracts/functions/function_transforms.md b/docs/docs/aztec/smart_contracts/functions/function_transforms.md index cc82dcee1733..8ceedea172f8 100644 --- a/docs/docs/aztec/smart_contracts/functions/function_transforms.md +++ b/docs/docs/aztec/smart_contracts/functions/function_transforms.md @@ -129,43 +129,6 @@ This process allows the return values to be included in the function's computati In public functions, the return value is directly used, and the function's return type remains as specified by the developer. -## Computing note hash and nullifier - -A function called `compute_note_hash_and_optionally_a_nullifier` is automatically generated and injected into all contracts that use notes. This function tells Aztec how to compute hashes and nullifiers for notes used in the contract. You can optionally write this function yourself if you want notes to be handled a specific way. - -The function is automatically generated based on the note types defined in the contract. Here's how it works: - -- The function takes several parameters: - ```rust - fn compute_note_hash_and_optionally_a_nullifier( - contract_address: AztecAddress, - nonce: Field, - storage_slot: Field, - note_type_id: Field, - compute_nullifier: bool, - serialized_note: [Field; MAX_NOTE_FIELDS_LENGTH], - ) -> [Field; 4] - ``` - -- It creates a `NoteHeader` using the provided args: - ```rust - let note_header = NoteHeader::new(contract_address, nonce, storage_slot); - ``` - -- The function then checks the `note_type_id` against all note types defined in the contract. For each note type, it includes a condition like this: - ```rust - if (note_type_id == NoteType::get_note_type_id()) { - aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( - NoteType::unpack, - note_header, - compute_nullifier, - packed_note - ) - } - ``` - -- The function returns an array of 4 Field elements, which represent the note hash and, if computed, the nullifier. - ## Function signature generation Unique function signatures are generated for each contract function. @@ -274,4 +237,4 @@ Contract artifacts are important because: - They help decode function return values in the simulator ## Further reading -- [Function attributes and macros](./attributes.md) \ No newline at end of file +- [Function attributes and macros](./attributes.md) diff --git a/docs/docs/developers/guides/smart_contracts/how_to_compile_contract.md b/docs/docs/developers/guides/smart_contracts/how_to_compile_contract.md index ceec6fcbd5a2..f29b645a0beb 100644 --- a/docs/docs/developers/guides/smart_contracts/how_to_compile_contract.md +++ b/docs/docs/developers/guides/smart_contracts/how_to_compile_contract.md @@ -226,17 +226,6 @@ export class TokenContract extends ContractBase { cancel_authwit: ((inner_hash: FieldLike) => ContractFunctionInteraction) & Pick; - /** compute_note_hash_and_optionally_a_nullifier(contract_address: struct, nonce: field, storage_slot: field, note_type_id: field, compute_nullifier: boolean, serialized_note: array) */ - compute_note_hash_and_optionally_a_nullifier: (( - contract_address: AztecAddressLike, - nonce: FieldLike, - storage_slot: FieldLike, - note_type_id: FieldLike, - compute_nullifier: boolean, - serialized_note: FieldLike[] - ) => ContractFunctionInteraction) & - Pick; - /** constructor(admin: struct, name: string, symbol: string, decimals: integer) */ constructor: (( admin: AztecAddressLike, diff --git a/docs/docs/developers/guides/smart_contracts/writing_contracts/common_patterns/index.md b/docs/docs/developers/guides/smart_contracts/writing_contracts/common_patterns/index.md index cd0c387db20b..367875a89084 100644 --- a/docs/docs/developers/guides/smart_contracts/writing_contracts/common_patterns/index.md +++ b/docs/docs/developers/guides/smart_contracts/writing_contracts/common_patterns/index.md @@ -82,9 +82,13 @@ See [partial notes](../../../../../aztec/concepts/advanced/storage/partial_notes When you send someone a note, the note hash gets added to the note hash tree. To spend the note, the receiver needs to get the note itself (the note hash preimage). There are two ways you can get a hold of your notes: 1. When sending someone a note, emit the note log to the recipient (the function encrypts the log in such a way that only a recipient can decrypt it). PXE then tries to decrypt all the encrypted logs, and stores the successfully decrypted one. [More info here](../how_to_emit_event.md) -2. Manually using `pxe.addNote()` - If you choose to not emit logs to save gas or when creating a note in the public domain and want to consume it in private domain (`encrypt_and_emit_note` shouldn't be called in the public domain because everything is public), like in the previous section where we created a note in public that doesn't have a designated owner. +2. Manually delivering it via a custom contract method, if you choose to not emit logs to save gas or when creating a note in the public domain and want to consume it in private domain (`encrypt_and_emit_note` shouldn't be called in the public domain because everything is public), like in the previous section where we created a note in public that doesn't have a designated owner. -#include_code pxe_add_note yarn-project/end-to-end/src/composed/e2e_persistence.test.ts typescript +#include_code offchain_delivery yarn-project/end-to-end/src/composed/e2e_persistence.test.ts typescript + +Note that this requires your contract to have an unconstrained function that processes these notes and adds them to PXE. + +#include_code deliver_note_contract_method noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr rust ### Revealing encrypted logs conditionally diff --git a/docs/docs/developers/guides/smart_contracts/writing_contracts/how_to_emit_event.md b/docs/docs/developers/guides/smart_contracts/writing_contracts/how_to_emit_event.md index 72173cc04aee..4a38d5ebc2b5 100644 --- a/docs/docs/developers/guides/smart_contracts/writing_contracts/how_to_emit_event.md +++ b/docs/docs/developers/guides/smart_contracts/writing_contracts/how_to_emit_event.md @@ -26,13 +26,7 @@ Furthermore, if not emitting the note, one should explicitly `discard` the value ### Successfully process the encrypted event -One of the functions of the PXE is constantly loading encrypted logs from the `AztecNode` and decrypting them. -When new encrypted logs are obtained, the PXE will try to decrypt them using the private encryption key of all the accounts registered inside PXE. -If the decryption is successful, the PXE will store the decrypted note inside a database. -If the decryption fails, the specific log will be discarded. - -For the PXE to successfully process the decrypted note we need to compute the note's 'note hash' and 'nullifier'. -Aztec.nr enables smart contract developers to design custom notes, meaning developers can also customize how a note's note hash and nullifier should be computed. Because of this customizability, and because there will be a potentially-unlimited number of smart contracts deployed to Aztec, an PXE needs to be 'taught' how to compute the custom note hashes and nullifiers for a particular contract. This is done by a function called `compute_note_hash_and_optionally_a_nullifier`, which is automatically injected into every contract when compiled. +Contracts created using aztec-nr will try to discover newly created notes by searching for logs emitted for any of the accounts registered inside PXE, decrypting their contents and notifying PXE of any notes found. This process is automatic and occurs whenever a contract function is invoked. ## Unencrypted Events diff --git a/docs/docs/developers/reference/environment_reference/cli_reference.md b/docs/docs/developers/reference/environment_reference/cli_reference.md index 7e922854dc14..a2c31796ae33 100644 --- a/docs/docs/developers/reference/environment_reference/cli_reference.md +++ b/docs/docs/developers/reference/environment_reference/cli_reference.md @@ -226,16 +226,6 @@ Options: - `-ca, --contract-address
`: Contract address to filter logs by. - `--follow`: Keep polling for new logs until interrupted. -### add-note -Adds a note to the database in the PXE. - -``` -aztec add-note
[options] -``` - -Required option: -- `-n, --note [note...]`: The members of a Note serialized as hex strings. - ## Development and Debugging Tools ### flamegraph @@ -382,4 +372,4 @@ Commands: list, add, remove, who-next Required option: - `--l1-rpc-url `: URL of the Ethereum host. -Note: Most commands accept a `--rpc-url` option to specify the Aztec node URL, and many accept fee-related options for gas limit and price configuration. \ No newline at end of file +Note: Most commands accept a `--rpc-url` option to specify the Aztec node URL, and many accept fee-related options for gas limit and price configuration. diff --git a/docs/docs/developers/reference/environment_reference/cli_wallet_reference.md b/docs/docs/developers/reference/environment_reference/cli_wallet_reference.md index cc6a0c9658b1..2387ced4b8bc 100644 --- a/docs/docs/developers/reference/environment_reference/cli_wallet_reference.md +++ b/docs/docs/developers/reference/environment_reference/cli_wallet_reference.md @@ -156,15 +156,5 @@ This example mints and bridges 1000 units of fee juice and bridges it to the `ma aztec-wallet bridge-fee-juice --mint 1000 master_yoda ``` -### Add Note - -The Add Note method makes it easy to store notes on your local PXE if they haven't been broadcasted yet. For example, if a JediMember note was sent to you, and you want to spend it on another transaction, you can use this method with the `--transaction-hash` flag to pass the transaction hash that contains the note. - -It expects `name` and `storageFieldName`. For example, if the `#[storage]` struct had a `available_members: PrivateMutable` property: - -```bash -aztec-wallet add-note JediMember available_members -a master_yoda -ca jedi_order -h 0x00000 -``` - ## Proving You can prove a transaction using the aztec-wallet with a running sandbox. Follow the guide [here](../../guides/local_env/sandbox_proving.md#proving-with-aztec-wallet) diff --git a/docs/docs/developers/tutorials/codealong/contract_tutorials/counter_contract.md b/docs/docs/developers/tutorials/codealong/contract_tutorials/counter_contract.md index ad07e67aca27..bdea3a326e1c 100644 --- a/docs/docs/developers/tutorials/codealong/contract_tutorials/counter_contract.md +++ b/docs/docs/developers/tutorials/codealong/contract_tutorials/counter_contract.md @@ -117,7 +117,7 @@ The `increment` function works very similarly to the `constructor`, but instead ## Prevent double spending -Because our counters are private, the network can't directly verify if a note was spent or not, which could lead to double-spending. To solve this, we use a nullifier - a unique identifier generated from each spent note and its nullifier key. Although this isn't really an issue in this simple smart contract, Aztec injects a special function called `compute_note_hash_and_optionally_a_nullifier` to determine these values for any given note produced by this contract. +Because our counters are private, the network can't directly verify if a note was spent or not, which could lead to double-spending. To solve this, we use a nullifier - a unique identifier generated from each spent note and its nullifier key. ## Getting a counter diff --git a/docs/docs/migration_notes.md b/docs/docs/migration_notes.md index d054fff98109..2df484f4128c 100644 --- a/docs/docs/migration_notes.md +++ b/docs/docs/migration_notes.md @@ -8,6 +8,66 @@ Aztec is in full-speed development. Literally every version breaks compatibility ## TBD +### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifer` + +This function is no longer mandatory for contracts, and the `#[aztec]` macro no longer injects it. + +### [PXE] Removed `addNote` and `addNullifiedNote` + +These functions have been removed from PXE and the base `Wallet` interface. If you need to deliver a note manually because its creation is not being broadcast in an encrypted log, then create an unconstrained contract function to process it and simulate execution of it. The `aztec::discovery::private_logs::do_process_log` function can be used to perform note discovery and add to it to PXE. + +See an example of how to handle a `TransparentNote`: + +```rust + unconstrained fn deliver_transparent_note( + contract_address: AztecAddress, + amount: Field, + secret_hash: Field, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + ) { + // do_process_log expects a standard aztec-nr encoded note, which has the following shape: + // [ storage_slot, note_type_id, ...packed_note ] + let note = TransparentNote::new(amount, secret_hash); + let log_plaintext = BoundedVec::from_array(array_concat( + [ + MyContract::storage_layout().my_state_variable.slot, + TransparentNote::get_note_type_id(), + ], + note.pack(), + )); + + do_process_log( + contract_address, + log_plaintext, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, + _compute_note_hash_and_nullifier, + ); + } +``` + +The note is then processed by calling this function: + +```typescript +const txEffects = await wallet.getTxEffect(txHash); +await contract.methods + .deliver_transparent_note( + contract.address, + new Fr(amount), + secretHash, + txHash.hash, + toBoundedVec(txEffects!.data.noteHashes, MAX_NOTE_HASHES_PER_TX), + txEffects!.data.nullifiers[0], + wallet.getAddress(), + ) + .simulate(); +``` + ### Fee is mandatory All transactions must now pay fees. Previously, the default payment method was `NoFeePaymentMethod`; It has been changed to `FeeJuicePaymentMethod`, with the wallet owner as the fee payer. diff --git a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr index 0988d95ead0e..08a06c0e9b3c 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr @@ -28,23 +28,24 @@ pub struct NoteHashAndNullifier { /// address). /// /// This function must be user-provided as its implementation requires knowledge of how note type IDs are allocated in a -/// contract. A typical implementation would look like this: +/// contract. The `#[aztec]` macro automatically creates such a contract library method called +/// `_compute_note_hash_and_nullifier`, which looks something like this: /// /// ``` /// |packed_note_content, contract_address, nonce, storage_slot, note_type_id| { /// if note_type_id == MyNoteType::get_note_type_id() { /// assert(packed_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); -/// let hashes = dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( -/// MyNoteType::unpack_content, -/// note_header, -/// true, -/// packed_note_content.storage(), -/// ) /// -/// Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { -/// note_hash: hashes[0], -/// inner_nullifier: hashes[3], -/// }) +/// let note = MyNoteType::unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); +/// +/// let note_hash = note.compute_note_hash(storage_slot); +/// let inner_nullifier = note.compute_nullifier_without_context(storage_slot, contract_address, nonce); +/// +/// Option::some( +/// aztec::discovery::NoteHashAndNullifier { +/// note_hash, inner_nullifier +/// } +/// ) /// } else if note_type_id == MyOtherNoteType::get_note_type_id() { /// ... // Similar to above but calling MyOtherNoteType::unpack_content /// } else { @@ -52,7 +53,7 @@ pub struct NoteHashAndNullifier { /// }; /// } /// ``` -type ComputeNoteHashAndNullifier = fn[Env](/* packed_note_content */BoundedVec, /* contract_address */ AztecAddress, /* nonce */ Field, /* storage_slot */ Field, /* note_type_id */ Field) -> Option; +type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note_content */BoundedVec, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* nonce */ Field) -> Option; /// Performs the note discovery process, in which private and public logs are downloaded and inspected to find private /// notes, partial notes, and their completion. This is the mechanism via which PXE learns of new notes. diff --git a/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr index a96985796bc9..54c28f831e96 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr @@ -1,4 +1,4 @@ -use crate::{discovery::{MAX_NOTE_PACKED_LEN, NoteHashAndNullifier}, utils::array}; +use crate::{discovery::{ComputeNoteHashAndNullifier, MAX_NOTE_PACKED_LEN}, utils::array}; use dep::protocol_types::{ address::AztecAddress, @@ -26,7 +26,7 @@ pub struct DiscoveredNoteInfo { pub unconstrained fn attempt_note_nonce_discovery( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, contract_address: AztecAddress, storage_slot: Field, note_type_id: Field, @@ -53,10 +53,10 @@ pub unconstrained fn attempt_note_nonce_discovery( // TODO(#11157): handle failed note_hash_and_nullifier computation let hashes = compute_note_hash_and_nullifier( packed_note_content, - contract_address, - candidate_nonce, storage_slot, note_type_id, + contract_address, + candidate_nonce, ) .expect(f"Failed to compute a note hash for note type {note_type_id}"); diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/utils.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/utils.nr index 0cf94a054b7e..cdd01e992767 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/utils.nr @@ -348,17 +348,7 @@ comptime fn create_note_discovery_call() -> Quoted { unsafe { dep::aztec::discovery::discover_new_notes( context.this_address(), - |packed_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { - // _compute_note_hash_and_optionally_a_nullifier is a contract library method injected by `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` - let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, packed_note_content); - - Option::some( - aztec::discovery::NoteHashAndNullifier { - note_hash: hashes[0], - inner_nullifier: hashes[3], - }, - ) - }, + _compute_note_hash_and_nullifier, ) }; } diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 0497fbda4c0c..97877ad8567c 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -15,7 +15,7 @@ use utils::{get_trait_impl_method, module_has_storage}; use crate::discovery::MAX_NOTE_PACKED_LEN; /// Marks a contract as an Aztec contract, generating the interfaces for its functions and notes, as well as injecting -/// the `compute_note_hash_and_optionally_a_nullifier` function PXE requires in order to validate notes. +/// the `process_log` and `sync_notes` functions PXE requires in order to discover notes. /// Note: This is a module annotation, so the returned quote gets injected inside the module (contract) itself. pub comptime fn aztec(m: Module) -> Quoted { let interface = generate_contract_interface(m); @@ -26,10 +26,8 @@ pub comptime fn aztec(m: Module) -> Quoted { transform_unconstrained(f); } - let contract_library_method_compute_note_hash_and_optionally_a_nullifier = - generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier(); - let compute_note_hash_and_optionally_a_nullifier = - generate_compute_note_hash_and_optionally_a_nullifier(); + let contract_library_method_compute_note_hash_and_nullifier = + generate_contract_library_method_compute_note_hash_and_nullifier(); let process_log = generate_process_log(); let note_exports = generate_note_exports(); let public_dispatch = generate_public_dispatch(m); @@ -38,8 +36,7 @@ pub comptime fn aztec(m: Module) -> Quoted { quote { $note_exports $interface - $contract_library_method_compute_note_hash_and_optionally_a_nullifier - $compute_note_hash_and_optionally_a_nullifier + $contract_library_method_compute_note_hash_and_nullifier $process_log $public_dispatch $sync_notes @@ -114,10 +111,10 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } } -/// Generates a contract library method called `_compute_note_hash_and_optionally_a_nullifier` which is used for note +/// Generates a contract library method called `_compute_note_hash_and_nullifier` which is used for note /// discovery (to create the `aztec::discovery::ComputeNoteHashAndNullifier` function) and to implement the /// `compute_note_hash_and_nullifier` unconstrained contract function. -comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier() -> Quoted { +comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() -> Quoted { let notes = NOTES.entries(); let mut max_note_packed_len: u32 = 0; @@ -141,8 +138,8 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we - // know it we call `aztec::note::utils::compute_note_hash_and_optionally_a_nullifier` (which is the one that - // actually does the work) with the correct `unpack()` function. + // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and + // compute the note hash (non-siloed) and inner nullifier (also non-siloed). let mut if_note_type_id_match_statements_list = &[]; for i in 0..notes.len() { @@ -168,9 +165,9 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( quote { $if_or_else_if note_type_id == $get_note_type_id() { - // As an extra safety check we make sure that the packed_note bounded vec has the - // expected length, to avoid scenarios in which compute_note_hash_and_optionally_a_nullifier - // silently trims the end if the log were to be longer. + // As an extra safety check we make sure that the packed_note BoundedVec has the expected + // length, since we're about to interpret it's raw storage as a fixed-size array by calling the + // unpack function on it. let expected_len = $packed_note_length; let actual_len = packed_note.len(); assert( @@ -178,7 +175,16 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ f"Expected packed note of length {expected_len} but got {actual_len} for note type id {note_type_id}" ); - aztec::note::utils::compute_note_hash_and_optionally_a_nullifier($unpack, contract_address, nonce, storage_slot, compute_nullifier, packed_note.storage()) + let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); + + let note_hash = note.compute_note_hash(storage_slot); + let inner_nullifier = note.compute_nullifier_without_context(storage_slot, contract_address, nonce); + + Option::some( + aztec::discovery::NoteHashAndNullifier { + note_hash, inner_nullifier + } + ) } }, ); @@ -187,18 +193,26 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); quote { + /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash + /// (non-siloed) and inner nullifier (non-siloed) assuming the note has been inserted into the note hash + /// tree with `nonce`. + /// + /// The signature of this function notably matches the `aztec::discovery::ComputeNoteHashAndNullifier` type, + /// and so it can be used to call functions from that module such as `discover_new_notes`, `do_process_log` + /// and `process_private_note_log`. + /// + /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] - unconstrained fn _compute_note_hash_and_optionally_a_nullifier( - contract_address: aztec::protocol_types::address::AztecAddress, - nonce: Field, + unconstrained fn _compute_note_hash_and_nullifier( + packed_note: BoundedVec, storage_slot: Field, note_type_id: Field, - compute_nullifier: bool, - packed_note: BoundedVec, - ) -> pub [Field; 4] { + contract_address: aztec::protocol_types::address::AztecAddress, + nonce: Field, + ) -> pub Option { $if_note_type_id_match_statements else { - panic(f"Unknown note type ID") + Option::none() } } } @@ -206,54 +220,18 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ // Contracts with no notes still implement this function to avoid having special-casing, the implementation // simply throws immediately. quote { + /// This contract does not use private notes, so this function should never be called as it will + /// unconditionally fail. + /// + /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] - unconstrained fn _compute_note_hash_and_optionally_a_nullifier( - _contract_address: aztec::protocol_types::address::AztecAddress, - _nonce: Field, - _storage_slot: Field, - _note_type_id: Field, - _compute_nullifier: bool, + unconstrained fn _compute_note_hash_and_nullifier( _packed_note: BoundedVec, - ) -> pub [Field; 4] { - panic(f"This contract does not use private notes") - } - } - } -} - -comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { - // For historical reasons we keep this function returning four fields (even though the caller should likely perform - // note hash siloing on their own and not trust this). The contract library method `_compute_note_hash...` is - // affected by this. - // TODO(#11638): In the future we might remove these things as we rely less and less on this function, and then - // change the `_compute_note_hash...` contract library method to be of type - // `aztec::discovery::ComputeNoteHashAndNullifier`, simplifying other macros by removing the need to create - // intermediate lambdas that adapt their interfaces. - - if NOTES.entries().len() > 0 { - quote { - unconstrained fn compute_note_hash_and_optionally_a_nullifier( - contract_address: aztec::protocol_types::address::AztecAddress, - nonce: Field, - storage_slot: Field, - note_type_id: Field, - compute_nullifier: bool, - packed_note: BoundedVec, - ) -> pub [Field; 4] { - _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, compute_nullifier, packed_note) - } - } - } else { - quote { - unconstrained fn compute_note_hash_and_optionally_a_nullifier( - _contract_address: aztec::protocol_types::address::AztecAddress, - _nonce: Field, _storage_slot: Field, _note_type_id: Field, - _compute_nullifier: bool, - _packed_note: BoundedVec, - ) -> pub [Field; 4] { - panic(f"No notes defined") + _contract_address: aztec::protocol_types::address::AztecAddress, + ) -> pub Option { + panic(f"This contract does not use private notes") } } } @@ -263,9 +241,7 @@ comptime fn generate_process_log() -> Quoted { // This mandatory function processes a log emitted by the contract. This is currently used to process private logs // and perform note discovery of either private notes or partial notes. // The bulk of the work of this function is done by aztec::discovery::do_process_log, so all we need to do is call - // that function. We use the contract library method injected by - // `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` in order to create the required - // `aztec::discovery::ComputeNoteHashAndNullifier` function. + // that function. // We'll produce the entire body of the function in one go and then insert it into the function. let notes = NOTES.entries(); @@ -294,16 +270,7 @@ comptime fn generate_process_log() -> Quoted { unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, - |packed_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { - let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, packed_note_content); - - Option::some( - aztec::discovery::NoteHashAndNullifier { - note_hash: hashes[0], - inner_nullifier: hashes[3], - }, - ) - } + _compute_note_hash_and_nullifier, ); } } @@ -327,7 +294,7 @@ comptime fn generate_process_log() -> Quoted { comptime fn generate_note_exports() -> Quoted { let notes = NOTES.values(); // Second value in each tuple is `note_packed_len` and that is ignored here because it's only used when - // generating the `compute_note_hash_and_optionally_a_nullifier` function. + // generating partial note helper functions. notes .map(|(s, _, note_type_id, fields): (StructDefinition, u32, Field, [(Quoted, u32, bool)])| { generate_note_export(s, note_type_id, fields) diff --git a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr index 87eaea1d58af..4ede75c5d58f 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr @@ -30,7 +30,8 @@ pub trait NullifiableNote { fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field; /// Same as compute_nullifier, but unconstrained. This version does not take a note hash because it'll only be - /// invoked in unconstrained contexts, where there is no gate count. + /// invoked in unconstrained contexts, where there is no gate count. Note that it also takes a contract address: + /// this is because nullifier computation typically involves contract siloed nullifying keys. unconstrained fn compute_nullifier_without_context( self, storage_slot: Field, diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 2f3e6143abdd..ac9d2b39bcc0 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -1,16 +1,11 @@ use crate::{ context::PrivateContext, note::{note_interface::{NoteInterface, NullifiableNote}, retrieved_note::RetrievedNote}, - utils::array, }; -use dep::protocol_types::{ - address::AztecAddress, - hash::{ - compute_siloed_note_hash, - compute_siloed_nullifier as compute_siloed_nullifier_from_preimage, - compute_unique_note_hash, - }, +use dep::protocol_types::hash::{ + compute_siloed_note_hash, compute_siloed_nullifier as compute_siloed_nullifier_from_preimage, + compute_unique_note_hash, }; pub fn compute_siloed_nullifier( @@ -118,39 +113,3 @@ where compute_note_hash_for_read_request(retrieved_note, storage_slot); compute_note_hash_for_nullify_internal(retrieved_note, note_hash_for_read_request) } - -/// Computes the note hash and optionally a nullifier for a given note. `N` is the length of the packed note, -/// `S` is the length of the packed note with its padding array. -/// -/// Note: `packed_note_with_padding` is typically constructed by calling the `storage()` method on a `BoundedVec`. This -/// function will then extract the relevant fields from the array using the `subarray` method and the actual packed -/// note length `N`. -// TODO (#11638): simplify or remove this function by inlining it in the `_compute_note_hash_and_nullifier` contract -// library method that is autogenerated by macros. -pub unconstrained fn compute_note_hash_and_optionally_a_nullifier( - unpack_note: fn([Field; N]) -> Note, - contract_address: AztecAddress, - nonce: Field, - storage_slot: Field, - compute_nullifier: bool, - packed_note_with_padding: [Field; S], -) -> [Field; 4] -where - Note: NoteInterface + NullifiableNote, -{ - let note = unpack_note(array::subarray(packed_note_with_padding, 0)); - - let note_hash = note.compute_note_hash(storage_slot); - - let siloed_note_hash = compute_siloed_note_hash(contract_address, note_hash); - let unique_note_hash = compute_unique_note_hash(nonce, siloed_note_hash); - - let inner_nullifier = if compute_nullifier { - note.compute_nullifier_without_context(storage_slot, contract_address, nonce) - } else { - 0 - }; - // docs:start:compute_note_hash_and_optionally_a_nullifier_returns - [note_hash, unique_note_hash, siloed_note_hash, inner_nullifier] - // docs:end:compute_note_hash_and_optionally_a_nullifier_returns -} diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr index 941a2243bfae..712ecd912269 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -150,7 +150,7 @@ impl TestEnvironment { } /// Manually adds a note to TXE. This needs to be called if you want to work with a note in your test with the note - /// not having an encrypted log emitted. TXE alternative to `PXE.addNote(...)`. + /// not having an encrypted log emitted. pub unconstrained fn add_note( _self: Self, note: Note, diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index c51cfae29847..1f4c8de3fa37 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -18,7 +18,10 @@ pub contract Test { }; use dep::aztec::protocol_types::{ - constants::{MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, PRIVATE_LOG_SIZE_IN_FIELDS}, + constants::{ + MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, MAX_NOTE_HASHES_PER_TX, + PRIVATE_LOG_SIZE_IN_FIELDS, + }, point::Point, traits::{Hash, Packable, Serialize}, utils::arrays::array_concat, @@ -29,12 +32,14 @@ pub contract Test { use dep::aztec::{ deploy::deploy_contract as aztec_deploy_contract, + discovery::private_logs::do_process_log, hash::{ArgsHasher, compute_secret_hash, pedersen_hash}, macros::{events::event, functions::{internal, private, public}, storage::storage}, note::{ lifecycle::{create_note, destroy_note_unsafe}, note_getter::{get_notes, view_notes}, note_getter_options::NoteStatus, + note_interface::NoteInterface, retrieved_note::RetrievedNote, }, test::mocks::mock_struct::MockStruct, @@ -426,9 +431,40 @@ pub contract Test { #[private] fn set_constant(value: Field) { let note = TestNote::new(value); + // We don't broadcast the fact that we created a note, it must be manually delivered by calling the deliver_note + // function. Note that we can only do this because the note contains no contract-generated values (e.g. + // randomness), if it did then we'd need to extract those out of the contract so that the app can get them (e.g. + // via capsules). storage.example_constant.initialize(note).discard(); } + unconstrained fn deliver_note( + contract_address: AztecAddress, + value: Field, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + ) { + // do_process_log expects a standard aztec-nr encoded note, which has the following shape: + // [ storage_slot, note_type_id, ...packed_note ] + let note = TestNote::new(value); + let log_plaintext = BoundedVec::from_array(array_concat( + [Test::storage_layout().example_constant.slot, TestNote::get_note_type_id()], + note.pack(), + )); + + do_process_log( + contract_address, + log_plaintext, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, + _compute_note_hash_and_nullifier, + ); + } + #[private] fn assert_private_global_vars(chain_id: Field, version: Field) { assert(context.chain_id() == chain_id, "Invalid chain id"); diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr index 3552aa9d19b0..400031c011c1 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr @@ -18,11 +18,13 @@ use dep::aztec::macros::aztec; pub contract TokenBlacklist { // Libs use dep::aztec::{ + discovery::private_logs::do_process_log, encrypted_logs::log_assembly_strategies::default_aes128::note::{ encode_and_encrypt_note, encode_and_encrypt_note_unconstrained, }, hash::compute_secret_hash, macros::{functions::{initializer, internal, private, public, view}, storage::storage}, + note::note_interface::NoteInterface, prelude::{AztecAddress, Map, NoteGetterOptions, PrivateSet, PublicMutable, SharedMutable}, utils::comparison::Comparator, }; @@ -35,6 +37,9 @@ pub contract TokenBlacklist { balances_map::BalancesMap, roles::UserFlags, token_note::TokenNote, transparent_note::TransparentNote, }; + use aztec::protocol_types::{ + constants::MAX_NOTE_HASHES_PER_TX, traits::Packable, utils::arrays::array_concat, + }; // Changing an address' roles has a certain block delay before it goes into effect. global CHANGE_ROLES_DELAY_BLOCKS: u32 = 2; @@ -300,4 +305,38 @@ pub contract TokenBlacklist { unconstrained fn balance_of_private(owner: AztecAddress) -> pub Field { storage.balances.balance_of(owner).to_field() } + + // docs:start:deliver_note_contract_method + unconstrained fn deliver_transparent_note( + contract_address: AztecAddress, + amount: Field, + secret_hash: Field, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + ) { + // docs:end:deliver_note_contract_method + + // do_process_log expects a standard aztec-nr encoded note, which has the following shape: + // [ storage_slot, note_type_id, ...packed_note ] + let note = TransparentNote::new(amount, secret_hash); + let log_plaintext = BoundedVec::from_array(array_concat( + [ + TokenBlacklist::storage_layout().pending_shields.slot, + TransparentNote::get_note_type_id(), + ], + note.pack(), + )); + + do_process_log( + contract_address, + log_plaintext, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, + _compute_note_hash_and_nullifier, + ); + } } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr b/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr index c64ca823eebc..eaf7cd438131 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr @@ -457,8 +457,7 @@ pub(crate) comptime fn derive_deserialize(s: StructDefinition) -> Quoted { /// /// Note: We are having this function separate from `derive_packable` because we use this in the note macros to get /// the packed length of a note as well as the `Packable` implementation. We need the length to be able to register -/// the note in the global `NOTES` map. There the length is used to generate -/// `compute_note_hash_and_optionally_a_nullifier` function on contracts. +/// the note in the global `NOTES` map. There the length is used to generate partial note helper functions. pub comptime fn derive_packable_and_get_packed_len(s: StructDefinition) -> (Quoted, u32) { let packing_enabled = true; diff --git a/playground/src/components/contract/contract.tsx b/playground/src/components/contract/contract.tsx index 3daacc1dc5f1..772408a8704f 100644 --- a/playground/src/components/contract/contract.tsx +++ b/playground/src/components/contract/contract.tsx @@ -124,7 +124,6 @@ const loadingArtifactContainer = css({ const FORBIDDEN_FUNCTIONS = [ "process_log", - "compute_note_hash_and_optionally_a_nullifier", "sync_notes", ]; diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index c7e05f205bc5..f3be12c7083d 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -1,6 +1,5 @@ import { type AuthWitness, - type ExtendedNote, type GetContractClassLogsResponse, type GetPublicLogsResponse, type L2Block, @@ -138,9 +137,6 @@ export abstract class BaseWallet implements Wallet { getPublicStorageAt(contract: AztecAddress, storageSlot: Fr): Promise { return this.pxe.getPublicStorageAt(contract, storageSlot); } - addNote(note: ExtendedNote): Promise { - return this.pxe.addNote(note, this.getAddress()); - } getBlock(number: number): Promise { return this.pxe.getBlock(number); } diff --git a/yarn-project/circuit-types/src/interfaces/pxe.test.ts b/yarn-project/circuit-types/src/interfaces/pxe.test.ts index c873154c6e01..38fbb62a222f 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.test.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.test.ts @@ -39,7 +39,7 @@ import { type GetPublicLogsResponse, type LogFilter, } from '../logs/index.js'; -import { ExtendedNote, UniqueNote } from '../notes/index.js'; +import { UniqueNote } from '../notes/index.js'; import { type NotesFilter } from '../notes/notes_filter.js'; import { PrivateExecutionResult } from '../private_execution_result.js'; import { SiblingPath } from '../sibling_path/sibling_path.js'; @@ -232,10 +232,6 @@ describe('PXESchema', () => { expect(result).toEqual([expect.any(BigInt), expect.any(SiblingPath)]); }); - it('addNote', async () => { - await context.client.addNote(await ExtendedNote.random(), address); - }); - it('getBlock', async () => { const result = await context.client.getBlock(1); expect(result).toBeInstanceOf(L2Block); @@ -454,11 +450,6 @@ class MockPXE implements PXE { expect(l2Tol1Message).toBeInstanceOf(Fr); return Promise.resolve([1n, SiblingPath.random(4)]); } - addNote(note: ExtendedNote, scope?: AztecAddress | undefined): Promise { - expect(note).toBeInstanceOf(ExtendedNote); - expect(scope).toEqual(this.address); - return Promise.resolve(); - } getBlock(number: number): Promise { return Promise.resolve(L2Block.random(number)); } diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index 50c9864fbb6b..800d027b3f88 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -39,7 +39,7 @@ import { type LogFilter, LogFilterSchema, } from '../logs/index.js'; -import { ExtendedNote, UniqueNote } from '../notes/index.js'; +import { UniqueNote } from '../notes/index.js'; import { type NotesFilter, NotesFilterSchema } from '../notes/notes_filter.js'; import { PrivateExecutionResult } from '../private_execution_result.js'; import { SiblingPath } from '../sibling_path/sibling_path.js'; @@ -276,14 +276,6 @@ export interface PXE { */ getL2ToL1MembershipWitness(blockNumber: number, l2Tol1Message: Fr): Promise<[bigint, SiblingPath]>; - /** - * Adds a note to the database. - * @throws If the note hash of the note doesn't exist in the tree. - * @param note - The note to add. - * @param scope - The scope to add the note under. Currently optional. - */ - addNote(note: ExtendedNote, scope?: AztecAddress): Promise; - /** * Get the given block. * @param number - The block number being requested. @@ -510,7 +502,6 @@ export const PXESchema: ApiSchemaFor = { .function() .args(z.number(), schemas.Fr) .returns(z.tuple([schemas.BigInt, SiblingPath.schema])), - addNote: z.function().args(ExtendedNote.schema, optional(schemas.AztecAddress)).returns(z.void()), getBlock: z .function() .args(z.number()) diff --git a/yarn-project/circuits.js/src/hash/hash.ts b/yarn-project/circuits.js/src/hash/hash.ts index b3f445a78a82..0f70cd0bb0de 100644 --- a/yarn-project/circuits.js/src/hash/hash.ts +++ b/yarn-project/circuits.js/src/hash/hash.ts @@ -28,18 +28,18 @@ export function computeNoteHashNonce(nullifierZero: Fr, noteHashIndex: number): * Computes a siloed note hash, given the contract address and the note hash itself. * A siloed note hash effectively namespaces a note hash to a specific contract. * @param contract - The contract address - * @param uniqueNoteHash - The unique note hash to silo. + * @param noteHash - The note hash to silo. * @returns A siloed note hash. */ -export function siloNoteHash(contract: AztecAddress, uniqueNoteHash: Fr): Promise { - return poseidon2HashWithSeparator([contract, uniqueNoteHash], GeneratorIndex.SILOED_NOTE_HASH); +export function siloNoteHash(contract: AztecAddress, noteHash: Fr): Promise { + return poseidon2HashWithSeparator([contract, noteHash], GeneratorIndex.SILOED_NOTE_HASH); } /** * Computes a unique note hash. * @dev Includes a nonce which contains data that guarantees the resulting note hash will be unique. * @param nonce - A nonce (typically derived from tx hash and note hash index in the tx). - * @param siloedNoteHash - A note hash. + * @param siloedNoteHash - A siloed note hash. * @returns A unique note hash. */ export function computeUniqueNoteHash(nonce: Fr, siloedNoteHash: Fr): Promise { diff --git a/yarn-project/cli-wallet/src/cmds/add_note.ts b/yarn-project/cli-wallet/src/cmds/add_note.ts deleted file mode 100644 index 94f7a80e1a5b..000000000000 --- a/yarn-project/cli-wallet/src/cmds/add_note.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { type AccountWalletWithSecretKey, type AztecAddress } from '@aztec/aztec.js'; -import { ExtendedNote, Note, type TxHash } from '@aztec/circuit-types'; -import { getContractArtifact, parseFields } from '@aztec/cli/utils'; -import { type LogFn } from '@aztec/foundation/log'; - -export async function addNote( - wallet: AccountWalletWithSecretKey, - address: AztecAddress, - contractAddress: AztecAddress, - noteName: string, - storageFieldName: string, - artifactPath: string, - txHash: TxHash, - noteBody: string[], - log: LogFn, -) { - const fields = parseFields(noteBody); - const note = new Note(fields); - const contractArtifact = await getContractArtifact(artifactPath, log); - - const contractNote = contractArtifact.notes[noteName]; - const storageField = contractArtifact.storageLayout[storageFieldName]; - - if (!contractNote) { - throw new Error(`Note ${noteName} not found in contract ${contractArtifact.name}`); - } - - const extendedNote = new ExtendedNote(note, address, contractAddress, storageField.slot, contractNote.id, txHash); - await wallet.addNote(extendedNote); -} diff --git a/yarn-project/cli-wallet/src/cmds/index.ts b/yarn-project/cli-wallet/src/cmds/index.ts index c2bceccb6ab4..d6548732770d 100644 --- a/yarn-project/cli-wallet/src/cmds/index.ts +++ b/yarn-project/cli-wallet/src/cmds/index.ts @@ -385,63 +385,6 @@ export function injectCommands( } }); - program - .command('add-note') - .description('Adds a note to the database in the PXE.') - .argument('[name]', 'The Note name') - .argument( - '[storageFieldName]', - 'The name of the variable in the storage field that contains the note. WARNING: Maps are not supported', - ) - .requiredOption('-a, --address ', 'The Aztec address of the note owner.', address => - aliasedAddressParser('accounts', address, db), - ) - .addOption(createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db))) - .requiredOption('-t, --transaction-hash ', 'The hash of the tx containing the note.', txHash => - aliasedTxHashParser(txHash, db), - ) - .addOption(createContractAddressOption(db)) - .addOption(createArtifactOption(db)) - .addOption( - new Option('-b, --body [noteFields...]', 'The members of a Note') - .argParser((arg, prev: string[]) => { - const next = db?.tryRetrieveAlias(arg) || arg; - prev.push(next); - return prev; - }) - .default([]), - ) - .addOption(pxeOption) - .action(async (noteName, storageFieldName, _options, command) => { - const { addNote } = await import('./add_note.js'); - const options = command.optsWithGlobals(); - const { - contractArtifact: artifactPathPromise, - contractAddress, - address, - secretKey, - rpcUrl, - body, - transactionHash, - } = options; - const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db); - const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); - const account = await createOrRetrieveAccount(client, address, db, undefined, secretKey); - const wallet = await getWalletWithScopes(account, db); - - await addNote( - wallet, - address, - contractAddress, - noteName, - storageFieldName, - artifactPath, - transactionHash, - body, - log, - ); - }); - program .command('create-authwit') .description( diff --git a/yarn-project/cli/src/cmds/contracts/inspect_contract.ts b/yarn-project/cli/src/cmds/contracts/inspect_contract.ts index d2434962d8c0..d63931b14075 100644 --- a/yarn-project/cli/src/cmds/contracts/inspect_contract.ts +++ b/yarn-project/cli/src/cmds/contracts/inspect_contract.ts @@ -12,7 +12,7 @@ import { getContractArtifact } from '../../utils/aztec.js'; export async function inspectContract(contractArtifactFile: string, debugLogger: Logger, log: LogFn) { const contractArtifact = await getContractArtifact(contractArtifactFile, log); - const contractFns = contractArtifact.functions.filter(f => f.name !== 'compute_note_hash_and_optionally_a_nullifier'); + const contractFns = contractArtifact.functions; if (contractFns.length === 0) { log(`No functions found for contract ${contractArtifact.name}`); } diff --git a/yarn-project/end-to-end/bootstrap.sh b/yarn-project/end-to-end/bootstrap.sh index 14df1e4d2b35..167ab3be9011 100755 --- a/yarn-project/end-to-end/bootstrap.sh +++ b/yarn-project/end-to-end/bootstrap.sh @@ -74,7 +74,7 @@ function test_cmds { echo "$prefix simple e2e_nested_contract/manual_public" echo "$prefix simple e2e_nft" - echo "$prefix simple e2e_non_contract_account" + echo "$prefix simple e2e_offchain_note_delivery" echo "$prefix simple e2e_note_getter" echo "$prefix simple e2e_ordering" echo "$prefix simple e2e_outbox" diff --git a/yarn-project/end-to-end/scripts/e2e_test_config.yml b/yarn-project/end-to-end/scripts/e2e_test_config.yml index ff72785e708f..11afa8fe7d6d 100644 --- a/yarn-project/end-to-end/scripts/e2e_test_config.yml +++ b/yarn-project/end-to-end/scripts/e2e_test_config.yml @@ -45,8 +45,8 @@ tests: e2e_multiple_accounts_1_enc_key: {} e2e_nested_contract: {} e2e_nft: {} - e2e_non_contract_account: {} e2e_note_getter: {} + e2e_offchain_note_delivery: {} e2e_ordering: {} e2e_outbox: {} # TODO reenable in https://github.com/AztecProtocol/aztec-packages/pull/9727 diff --git a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts index 45e351c6797b..b599ff95bc96 100644 --- a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts @@ -1,14 +1,8 @@ import { getSchnorrAccount, getSchnorrWallet } from '@aztec/accounts/schnorr'; import { type InitialAccountData, deployFundedSchnorrAccount } from '@aztec/accounts/testing'; -import { - type AccountWallet, - type ContractInstanceWithAddress, - ExtendedNote, - Note, - type TxHash, - computeSecretHash, -} from '@aztec/aztec.js'; +import { type AccountWallet, type ContractInstanceWithAddress, type TxHash, computeSecretHash } from '@aztec/aztec.js'; import { type AztecAddress, Fr } from '@aztec/circuits.js'; +import { MAX_NOTE_HASHES_PER_TX } from '@aztec/constants'; import { type DeployL1Contracts } from '@aztec/ethereum'; // We use TokenBlacklist because we want to test the persistence of manually added notes and standard token no longer // implements TransparentNote shield flow. @@ -86,8 +80,8 @@ describe('Aztec persistence', () => { .wait(); await addPendingShieldNoteToPXE( + contract, ownerWallet, - contractAddress, 1000n, await computeSecretHash(secret), mintTxReceipt.txHash, @@ -153,8 +147,8 @@ describe('Aztec persistence', () => { .send() .wait(); await addPendingShieldNoteToPXE( + contract, ownerWallet, - contractAddress, 1000n, await computeSecretHash(secret), mintTxReceipt.txHash, @@ -324,13 +318,7 @@ describe('Aztec persistence', () => { it('allows consuming transparent note created on another PXE', async () => { // this was created in the temporary PXE in `beforeAll` - await addPendingShieldNoteToPXE( - ownerWallet, - contractAddress, - mintAmount, - await computeSecretHash(secret), - mintTxHash, - ); + await addPendingShieldNoteToPXE(contract, ownerWallet, mintAmount, await computeSecretHash(secret), mintTxHash); const balanceBeforeRedeem = await contract.methods.balance_of_private(ownerWallet.getAddress()).simulate(); @@ -342,23 +330,29 @@ describe('Aztec persistence', () => { }); }); +function toBoundedVec(arr: Fr[], maxLen: number) { + return { len: arr.length, storage: arr.concat(new Array(maxLen - arr.length).fill(new Fr(0))) }; +} + async function addPendingShieldNoteToPXE( + contract: TokenBlacklistContract, wallet: AccountWallet, - asset: AztecAddress, amount: bigint, secretHash: Fr, txHash: TxHash, ) { - // docs:start:pxe_add_note - const note = new Note([new Fr(amount), secretHash]); - const extendedNote = new ExtendedNote( - note, - wallet.getAddress(), - asset, - TokenBlacklistContract.storage.pending_shields.slot, - TokenBlacklistContract.notes.TransparentNote.id, - txHash, - ); - await wallet.addNote(extendedNote); - // docs:end:pxe_add_note + // docs:start:offchain_delivery + const txEffects = await wallet.getTxEffect(txHash); + await contract.methods + .deliver_transparent_note( + contract.address, + new Fr(amount), + secretHash, + txHash.hash, + toBoundedVec(txEffects!.data.noteHashes, MAX_NOTE_HASHES_PER_TX), + txEffects!.data.nullifiers[0], + wallet.getAddress(), + ) + .simulate(); + // docs:end:offchain_delivery } diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts index 8df43cbf288d..f9a006b9b04c 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts @@ -2,14 +2,13 @@ import { getSchnorrWallet } from '@aztec/accounts/schnorr'; import { type AccountWallet, type CompleteAddress, - ExtendedNote, Fr, type Logger, - Note, type TxHash, computeSecretHash, createLogger, } from '@aztec/aztec.js'; +import { MAX_NOTE_HASHES_PER_TX } from '@aztec/constants'; import { DocsExampleContract } from '@aztec/noir-contracts.js/DocsExample'; import { type TokenContract } from '@aztec/noir-contracts.js/Token'; import { TokenBlacklistContract } from '@aztec/noir-contracts.js/TokenBlacklist'; @@ -159,24 +158,36 @@ export class BlacklistTokenContractTest { await this.snapshotManager.teardown(); } - async addPendingShieldNoteToPXE(accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) { - const note = new Note([new Fr(amount), secretHash]); - const extendedNote = new ExtendedNote( - note, - this.accounts[accountIndex].address, - this.asset.address, - TokenBlacklistContract.storage.pending_shields.slot, - TokenBlacklistContract.notes.TransparentNote.id, - txHash, - ); - await this.wallets[accountIndex].addNote(extendedNote); + #toBoundedVec(arr: Fr[], maxLen: number) { + return { len: arr.length, storage: arr.concat(new Array(maxLen - arr.length).fill(new Fr(0))) }; + } + + async addPendingShieldNoteToPXE( + contract: TokenBlacklistContract, + wallet: AccountWallet, + amount: bigint, + secretHash: Fr, + txHash: TxHash, + ) { + const txEffects = await wallet.getTxEffect(txHash); + await contract.methods + .deliver_transparent_note( + contract.address, + new Fr(amount), + secretHash, + txHash.hash, + this.#toBoundedVec(txEffects!.data.noteHashes, MAX_NOTE_HASHES_PER_TX), + txEffects!.data.nullifiers[0], + wallet.getAddress(), + ) + .simulate(); } async applyMintSnapshot() { await this.snapshotManager.snapshot( 'mint', async () => { - const { asset, accounts } = this; + const { asset, accounts, wallets } = this; const amount = 10000n; const adminMinterRole = new Role().withAdmin().withMinter(); @@ -207,7 +218,7 @@ export class BlacklistTokenContractTest { const secretHash = await computeSecretHash(secret); const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); - await this.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await this.addPendingShieldNoteToPXE(asset, wallets[0], amount, secretHash, receipt.txHash); const txClaim = asset.methods.redeem_shield(accounts[0].address, amount, secret).send(); await txClaim.wait({ debug: true }); this.logger.verbose(`Minting complete.`); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts index 8775f4c493af..c3742500d0ad 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts @@ -87,7 +87,7 @@ describe('e2e_blacklist_token_contract mint', () => { const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); txHash = receipt.txHash; - await t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash); + await t.addPendingShieldNoteToPXE(asset, wallets[0], amount, secretHash, txHash); const receiptClaim = await asset.methods .redeem_shield(wallets[0].getAddress(), amount, secret) @@ -105,13 +105,16 @@ describe('e2e_blacklist_token_contract mint', () => { }); describe('failure cases', () => { - it('try to redeem as recipient (double-spend) [REVERTS]', async () => { - await expect(t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash)).rejects.toThrow( - 'The note has been destroyed.', - ); - await expect(asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).prove()).rejects.toThrow( - `Assertion failed: note not popped 'notes.len() == 1'`, - ); + it('try to redeem as recipient again (double-spend) [REVERTS]', async () => { + // We have another wallet add the note to their PXE and then try to spend it. They will be able to successfully + // add it, but PXE will realize that the note has been nullified already and not inject it into the circuit + // during execution of redeem_shield, resulting in a simulaton failure. + + await t.addPendingShieldNoteToPXE(asset, wallets[1], amount, secretHash, txHash); + + await expect( + asset.withWallet(wallets[1]).methods.redeem_shield(wallets[1].getAddress(), amount, secret).prove(), + ).rejects.toThrow(`Assertion failed: note not popped 'notes.len() == 1'`); }); it('mint_private as non-minter', async () => { diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts index 810d74255b2f..8aea08f540dd 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts @@ -38,7 +38,7 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { const receipt = await asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 0).send().wait(); // Redeem it - await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await t.addPendingShieldNoteToPXE(asset, wallets[0], amount, secretHash, receipt.txHash); await asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send().wait(); // Check that the result matches token sim @@ -68,7 +68,7 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { ).rejects.toThrow(/unauthorized/); // Redeem it - await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await t.addPendingShieldNoteToPXE(asset, wallets[0], amount, secretHash, receipt.txHash); await asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send().wait(); // Check that the result matches token sim diff --git a/yarn-project/end-to-end/src/e2e_non_contract_account.test.ts b/yarn-project/end-to-end/src/e2e_non_contract_account.test.ts deleted file mode 100644 index cf27c0249c42..000000000000 --- a/yarn-project/end-to-end/src/e2e_non_contract_account.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ExtendedNote, Fr, type Logger, Note, type Wallet } from '@aztec/aztec.js'; -import { TestContract } from '@aztec/noir-contracts.js/Test'; - -import { setup } from './fixtures/utils.js'; - -describe('e2e_non_contract_account', () => { - let teardown: () => Promise; - - let logger: Logger; - - let contract: TestContract; - let wallet: Wallet; - - beforeEach(async () => { - ({ teardown, wallet, logger } = await setup(1)); - - logger.debug(`Deploying L2 contract...`); - contract = await TestContract.deploy(wallet).send().deployed(); - logger.info(`L2 contract deployed at ${contract.address}`); - }); - - afterEach(() => teardown()); - - // Note: This test doesn't really belong here as it doesn't have anything to do with non-contract accounts. I needed - // to test the TestNote functionality and it doesn't really fit anywhere else. Creating a separate e2e test for this - // seems wasteful. Move this test if a better place is found. - it('can set and get a constant', async () => { - const value = 123n; - - const { txHash, debugInfo } = await contract.methods - .set_constant(value) - .send() - .wait({ interval: 0.1, debug: true }); - - // check that 1 note hash was created - expect(debugInfo!.noteHashes.length).toBe(1); - - // Add the note - const note = new Note([new Fr(value)]); - - // We have to manually add the note because the note was not broadcasted. - const extendedNote = new ExtendedNote( - note, - wallet.getCompleteAddress().address, - contract.address, - TestContract.storage.example_constant.slot, - TestContract.notes.TestNote.id, - txHash, - ); - await wallet.addNote(extendedNote); - - expect(await contract.methods.get_constant().simulate()).toEqual(value); - }); -}); diff --git a/yarn-project/end-to-end/src/e2e_offchain_note_delivery.test.ts b/yarn-project/end-to-end/src/e2e_offchain_note_delivery.test.ts new file mode 100644 index 000000000000..f3498a61797a --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_offchain_note_delivery.test.ts @@ -0,0 +1,51 @@ +import { Fr, type Wallet } from '@aztec/aztec.js'; +import { MAX_NOTE_HASHES_PER_TX } from '@aztec/constants'; +import { TestContract } from '@aztec/noir-contracts.js/Test'; + +import { setup } from './fixtures/utils.js'; + +describe('e2e_offchain_note_delivery', () => { + let teardown: () => Promise; + + let contract: TestContract; + let wallet: Wallet; + + beforeEach(async () => { + ({ teardown, wallet } = await setup(1)); + + contract = await TestContract.deploy(wallet).send().deployed(); + }); + + afterEach(() => teardown()); + + function toBoundedVec(arr: Fr[], maxLen: number) { + return { len: arr.length, storage: arr.concat(new Array(maxLen - arr.length).fill(new Fr(0))) }; + } + + it('can create a note that is not broadcast, deliver it offchain and read it', async () => { + const value = 123n; + + const { txHash, debugInfo } = await contract.methods + .set_constant(value) + .send() + .wait({ interval: 0.1, debug: true }); + + // check that 1 note hash was created + expect(debugInfo!.noteHashes.length).toBe(1); + + // The note was not broadcast, so we must manually deliver it to the contract via the custom mechanism to do so. + const txEffect = await wallet.getTxEffect(txHash); + await contract.methods + .deliver_note( + contract.address, + value, + txHash.hash, + toBoundedVec(txEffect!.data.noteHashes, MAX_NOTE_HASHES_PER_TX), + txEffect!.data.nullifiers[0], + wallet.getAddress(), + ) + .simulate(); + + expect(await contract.methods.get_constant().simulate()).toEqual(value); + }); +}); diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index 67d714d551c5..dd239c881389 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -1,7 +1,6 @@ import { type AuthWitness, EventMetadata, - type ExtendedNote, type FunctionCall, type GetContractClassLogsResponse, type GetPublicLogsResponse, @@ -9,7 +8,6 @@ import { L1EventPayload, type L2Block, type LogFilter, - MerkleTreeId, type NotesFilter, PrivateSimulationResult, type PublicSimulationOutput, @@ -53,7 +51,7 @@ import { } from '@aztec/circuits.js/abi'; import { type AztecAddress } from '@aztec/circuits.js/aztec-address'; import { computeContractAddressFromInstance, getContractClassFromArtifact } from '@aztec/circuits.js/contract'; -import { computeNoteHashNonce, siloNullifier } from '@aztec/circuits.js/hash'; +import { siloNullifier } from '@aztec/circuits.js/hash'; import { PrivateKernelTailCircuitPublicInputs } from '@aztec/circuits.js/kernel'; import { computeAddressSecret } from '@aztec/circuits.js/keys'; import { L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/constants'; @@ -72,7 +70,6 @@ import { type PXEServiceConfig } from '../config/index.js'; import { getPackageInfo } from '../config/package_info.js'; import { ContractDataOracle } from '../contract_data_oracle/index.js'; import { type PxeDatabase } from '../database/index.js'; -import { NoteDao } from '../database/note_dao.js'; import { KernelOracle } from '../kernel_oracle/index.js'; import { KernelProver, type ProvingConfig } from '../kernel_prover/kernel_prover.js'; import { getAcirSimulator } from '../simulator/index.js'; @@ -372,96 +369,6 @@ export class PXEService implements PXE { return this.node.getL2ToL1MessageMembershipWitness(blockNumber, l2Tol1Message); } - public async addNote(note: ExtendedNote, scope?: AztecAddress) { - const owner = await this.db.getCompleteAddress(note.owner); - if (!owner) { - throw new Error(`Unknown account: ${note.owner.toString()}`); - } - - const { data: nonces, l2BlockNumber, l2BlockHash } = await this.#getNoteNonces(note); - if (nonces.length === 0) { - throw new Error(`Cannot find the note in tx: ${note.txHash}.`); - } - - for (const nonce of nonces) { - const { noteHash, uniqueNoteHash, innerNullifier } = await this.simulator.computeNoteHashAndNullifier( - note.contractAddress, - nonce, - note.storageSlot, - note.noteTypeId, - note.note, - ); - - const [index] = await this.node.findLeavesIndexes('latest', MerkleTreeId.NOTE_HASH_TREE, [uniqueNoteHash]); - if (index === undefined) { - throw new Error('Note does not exist.'); - } - - const siloedNullifier = await siloNullifier(note.contractAddress, innerNullifier!); - const [nullifierIndex] = await this.node.findLeavesIndexes('latest', MerkleTreeId.NULLIFIER_TREE, [ - siloedNullifier, - ]); - if (nullifierIndex !== undefined) { - throw new Error('The note has been destroyed.'); - } - - await this.db.addNote( - new NoteDao( - note.note, - note.contractAddress, - note.storageSlot, - nonce, - noteHash, - siloedNullifier, - note.txHash, - l2BlockNumber, - l2BlockHash, - index, - await owner.address.toAddressPoint(), - note.noteTypeId, - ), - scope, - ); - } - } - - /** - * Finds the nonce(s) for a given note. - * @param note - The note to find the nonces for. - * @returns The nonces of the note. - * @remarks More than a single nonce may be returned since there might be more than one nonce for a given note. - */ - async #getNoteNonces(note: ExtendedNote): Promise> { - const tx = await this.node.getTxEffect(note.txHash); - if (!tx) { - throw new Error(`Unknown tx: ${note.txHash}`); - } - - const nonces: Fr[] = []; - const firstNullifier = tx.data.nullifiers[0]; - const hashes = tx.data.noteHashes; - for (let i = 0; i < hashes.length; ++i) { - const hash = hashes[i]; - if (hash.equals(Fr.ZERO)) { - break; - } - - const nonce = await computeNoteHashNonce(firstNullifier, i); - const { uniqueNoteHash } = await this.simulator.computeNoteHashAndNullifier( - note.contractAddress, - nonce, - note.storageSlot, - note.noteTypeId, - note.note, - ); - if (hash.equals(uniqueNoteHash)) { - nonces.push(nonce); - } - } - - return { l2BlockHash: tx.l2BlockHash, l2BlockNumber: tx.l2BlockNumber, data: nonces }; - } - public async getBlock(blockNumber: number): Promise { // If a negative block number is provided the current block number is fetched. if (blockNumber < 0) { diff --git a/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts b/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts index d43adba1b995..a3bc30924472 100644 --- a/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts +++ b/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts @@ -568,7 +568,6 @@ describe('Simulator oracle', () => { addNotesSpy.mockReset(); getNotesSpy.mockReset(); removeNullifiedNotesSpy.mockReset(); - simulator.computeNoteHashAndNullifier.mockReset(); aztecNode.getTxEffect.mockReset(); }); diff --git a/yarn-project/simulator/src/client/private_execution.test.ts b/yarn-project/simulator/src/client/private_execution.test.ts index 0033ca91ece9..bbec066e8269 100644 --- a/yarn-project/simulator/src/client/private_execution.test.ts +++ b/yarn-project/simulator/src/client/private_execution.test.ts @@ -43,9 +43,10 @@ import { AztecAddress } from '@aztec/circuits.js/aztec-address'; import { computeNoteHashNonce, computeSecretHash, + computeUniqueNoteHash, computeVarArgsHash, deriveStorageSlotInMap, - siloNullifier, + siloNoteHash, } from '@aztec/circuits.js/hash'; import { makeHeader } from '@aztec/circuits.js/testing'; import { AppendOnlyTreeSnapshot } from '@aztec/circuits.js/trees'; @@ -216,6 +217,11 @@ describe('Private Execution test suite', () => { return trees[name]; }; + const computeNoteHash = (note: Note, storageSlot: Fr) => { + // We're assuming here that the note hash function is the default one injected by the #[note] macro. + return poseidon2HashWithSeparator([...note.items, storageSlot], GeneratorIndex.NOTE_HASH); + }; + beforeAll(async () => { logger = createLogger('simulator:test:private_execution'); @@ -395,17 +401,7 @@ describe('Private Execution test suite', () => { const noteHashes = getNonEmptyItems(result.publicInputs.noteHashes); expect(noteHashes).toHaveLength(1); - expect(noteHashes[0].value).toEqual( - ( - await acirSimulator.computeNoteHashAndNullifier( - contractAddress, - Fr.ZERO, - newNote.storageSlot, - newNote.noteTypeId, - newNote.note, - ) - ).noteHash, - ); + expect(noteHashes[0].value).toEqual(await computeNoteHash(newNote.note, newNote.storageSlot)); const privateLogs = getNonEmptyItems(result.publicInputs.privateLogs); expect(privateLogs).toHaveLength(1); @@ -425,17 +421,7 @@ describe('Private Execution test suite', () => { const noteHashes = getNonEmptyItems(result.publicInputs.noteHashes); expect(noteHashes).toHaveLength(1); - expect(noteHashes[0].value).toEqual( - ( - await acirSimulator.computeNoteHashAndNullifier( - contractAddress, - Fr.ZERO, - newNote.storageSlot, - newNote.noteTypeId, - newNote.note, - ) - ).noteHash, - ); + expect(noteHashes[0].value).toEqual(await computeNoteHash(newNote.note, newNote.storageSlot)); const privateLogs = getNonEmptyItems(result.publicInputs.privateLogs); expect(privateLogs).toHaveLength(1); @@ -458,13 +444,17 @@ describe('Private Execution test suite', () => { oracle.processTaggedLogs.mockResolvedValue(); oracle.getNotes.mockResolvedValue(notes); - const consumedNotes = await asyncMap(notes, ({ nonce, note }) => - acirSimulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, valueNoteTypeId, note), - ); - await insertLeaves(consumedNotes.map(n => n.uniqueNoteHash)); + const consumedNotes = await asyncMap(notes, async ({ note, nonce }) => { + const noteHash = await computeNoteHash(note, storageSlot); + const siloedNoteHash = await siloNoteHash(contractAddress, noteHash); + const uniqueNoteHash = await computeUniqueNoteHash(nonce, siloedNoteHash); + return uniqueNoteHash; + }); + + await insertLeaves(consumedNotes); const args = [recipient, amountToTransfer]; - const { entrypoint: result, firstNullifier } = await runSimulator({ + const { entrypoint: result } = await runSimulator({ args, artifact: StatefulTestContractArtifact, functionName: 'destroy_and_create_no_init_check', @@ -472,15 +462,10 @@ describe('Private Execution test suite', () => { contractAddress, }); - // The two notes were nullified + // The two notes were nullified. Uses one of the notes as first nullifier, not requiring a protocol injected + // nullifier, so the total number of nullifiers is still two. const nullifiers = getNonEmptyItems(result.publicInputs.nullifiers).map(n => n.value); expect(nullifiers).toHaveLength(consumedNotes.length); - expect(nullifiers).toEqual(expect.arrayContaining(consumedNotes.map(n => n.innerNullifier))); - // Uses one of the notes as first nullifier, not requiring a protocol injected nullifier. - const consumedNotesNullifiers = await Promise.all( - consumedNotes.map(n => siloNullifier(contractAddress, n.innerNullifier)), - ); - expect(consumedNotesNullifiers).toContainEqual(firstNullifier); expect(result.newNotes).toHaveLength(2); const [changeNote, recipientNote] = result.newNotes; @@ -489,29 +474,6 @@ describe('Private Execution test suite', () => { const noteHashes = getNonEmptyItems(result.publicInputs.noteHashes); expect(noteHashes).toHaveLength(2); - const [changeNoteHash, recipientNoteHash] = noteHashes; - const [siloedChangeNoteHash, siloedRecipientNoteHash] = [ - ( - await acirSimulator.computeNoteHashAndNullifier( - contractAddress, - Fr.ZERO, - storageSlot, - valueNoteTypeId, - changeNote.note, - ) - ).noteHash, - ( - await acirSimulator.computeNoteHashAndNullifier( - contractAddress, - Fr.ZERO, - recipientStorageSlot, - valueNoteTypeId, - recipientNote.note, - ) - ).noteHash, - ]; - expect(changeNoteHash.value).toEqual(siloedChangeNoteHash); - expect(recipientNoteHash.value).toEqual(siloedRecipientNoteHash); expect(recipientNote.note.items[0]).toEqual(new Fr(amountToTransfer)); expect(changeNote.note.items[0]).toEqual(new Fr(40n)); @@ -521,7 +483,6 @@ describe('Private Execution test suite', () => { const readRequests = getNonEmptyItems(result.publicInputs.noteHashReadRequests).map(r => r.value); expect(readRequests).toHaveLength(consumedNotes.length); - expect(readRequests).toEqual(expect.arrayContaining(consumedNotes.map(n => n.uniqueNoteHash))); }); it('should be able to destroy_and_create with dummy notes', async () => { @@ -535,10 +496,14 @@ describe('Private Execution test suite', () => { oracle.processTaggedLogs.mockResolvedValue(); oracle.getNotes.mockResolvedValue(notes); - const consumedNotes = await asyncMap(notes, ({ nonce, note }) => - acirSimulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, valueNoteTypeId, note), - ); - await insertLeaves(consumedNotes.map(n => n.uniqueNoteHash)); + const consumedNotes = await asyncMap(notes, async ({ note, nonce }) => { + const noteHash = await computeNoteHash(note, storageSlot); + const siloedNoteHash = await siloNoteHash(contractAddress, noteHash); + const uniqueNoteHash = await computeUniqueNoteHash(nonce, siloedNoteHash); + return uniqueNoteHash; + }); + + await insertLeaves(consumedNotes); const args = [recipient, amountToTransfer]; const { entrypoint: result } = await runSimulator({ @@ -550,7 +515,7 @@ describe('Private Execution test suite', () => { }); const nullifiers = getNonEmptyItems(result.publicInputs.nullifiers).map(n => n.value); - expect(nullifiers).toEqual(consumedNotes.map(n => n.innerNullifier)); + expect(nullifiers).toHaveLength(consumedNotes.length); expect(result.newNotes).toHaveLength(2); const [changeNote, recipientNote] = result.newNotes; @@ -1028,13 +993,7 @@ describe('Private Execution test suite', () => { owner, ); - const { noteHash: derivedNoteHash } = await acirSimulator.computeNoteHashAndNullifier( - contractAddress, - Fr.ZERO, - storageSlot, - valueNoteTypeId, - noteAndSlot.note, - ); + const derivedNoteHash = await computeNoteHash(noteAndSlot.note, storageSlot); expect(noteHashFromCall).toEqual(derivedNoteHash); const privateLogs = getNonEmptyItems(result.publicInputs.privateLogs); @@ -1106,13 +1065,7 @@ describe('Private Execution test suite', () => { const noteHashes = getNonEmptyItems(execInsert.publicInputs.noteHashes); expect(noteHashes).toHaveLength(1); - const { noteHash: derivedNoteHash } = await acirSimulator.computeNoteHashAndNullifier( - contractAddress, - Fr.ZERO, - noteAndSlot.storageSlot, - noteAndSlot.noteTypeId, - noteAndSlot.note, - ); + const derivedNoteHash = await computeNoteHash(noteAndSlot.note, storageSlot); expect(noteHashes[0].value).toEqual(derivedNoteHash); const privateLogs = getNonEmptyItems(execInsert.publicInputs.privateLogs); diff --git a/yarn-project/simulator/src/client/simulator.test.ts b/yarn-project/simulator/src/client/simulator.test.ts deleted file mode 100644 index 009cd33aabfd..000000000000 --- a/yarn-project/simulator/src/client/simulator.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { CompleteAddress, Note } from '@aztec/circuit-types'; -import { type AztecNode } from '@aztec/circuit-types/interfaces/client'; -import { KeyValidationRequest, computeAppNullifierSecretKey, deriveKeys } from '@aztec/circuits.js'; -import { type FunctionArtifact, getFunctionArtifactByName } from '@aztec/circuits.js/abi'; -import { AztecAddress } from '@aztec/circuits.js/aztec-address'; -import { Fr, type Point } from '@aztec/foundation/fields'; -import { TokenBlacklistContractArtifact } from '@aztec/noir-contracts.js/TokenBlacklist'; - -import { type MockProxy, mock } from 'jest-mock-extended'; - -import { WASMSimulator } from '../providers/acvm_wasm.js'; -import { type DBOracle } from './db_oracle.js'; -import { AcirSimulator } from './simulator.js'; - -describe('Simulator', () => { - const simulationProvider = new WASMSimulator(); - - let oracle: MockProxy; - let node: MockProxy; - - let simulator: AcirSimulator; - let ownerMasterNullifierPublicKey: Point; - let contractAddress: AztecAddress; - let appNullifierSecretKey: Fr; - - beforeEach(async () => { - const ownerSk = Fr.fromHexString('2dcc5485a58316776299be08c78fa3788a1a7961ae30dc747fb1be17692a8d32'); - const allOwnerKeys = await deriveKeys(ownerSk); - - ownerMasterNullifierPublicKey = allOwnerKeys.publicKeys.masterNullifierPublicKey; - const ownerMasterNullifierSecretKey = allOwnerKeys.masterNullifierSecretKey; - - contractAddress = await AztecAddress.random(); - - const ownerPartialAddress = Fr.random(); - const ownerCompleteAddress = CompleteAddress.fromSecretKeyAndPartialAddress(ownerSk, ownerPartialAddress); - - appNullifierSecretKey = await computeAppNullifierSecretKey(ownerMasterNullifierSecretKey, contractAddress); - - oracle = mock(); - node = mock(); - oracle.getKeyValidationRequest.mockResolvedValue( - new KeyValidationRequest(ownerMasterNullifierPublicKey, appNullifierSecretKey), - ); - oracle.getCompleteAddress.mockResolvedValue(ownerCompleteAddress); - - simulator = new AcirSimulator(oracle, node, simulationProvider); - }); - - describe('compute_note_hash_and_optionally_a_nullifier', () => { - const artifact = getFunctionArtifactByName( - TokenBlacklistContractArtifact, - 'compute_note_hash_and_optionally_a_nullifier', - ); - const nonce = Fr.random(); - const storageSlot = TokenBlacklistContractArtifact.storageLayout['balances'].slot; - const noteTypeId = TokenBlacklistContractArtifact.notes['TokenNote'].id; - - // Amount is a U128, with a lo and hi limbs - const createNote = async (amount = 123n) => - new Note([new Fr(amount), new Fr(0), await ownerMasterNullifierPublicKey.hash(), Fr.random()]); - - it('throw if the contract does not implement "compute_note_hash_and_optionally_a_nullifier"', async () => { - oracle.getFunctionArtifactByName.mockResolvedValue(undefined); - - const note = await createNote(); - await expect( - simulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, noteTypeId, note), - ).rejects.toThrow(/Mandatory implementation of "compute_note_hash_and_optionally_a_nullifier" missing/); - }); - - it('throw if "compute_note_hash_and_optionally_a_nullifier" has the wrong number of parameters', async () => { - const note = await createNote(); - - const modifiedArtifact: FunctionArtifact = { - ...artifact, - parameters: artifact.parameters.slice(1), - }; - oracle.getFunctionArtifactByName.mockResolvedValue(modifiedArtifact); - - await expect( - simulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, noteTypeId, note), - ).rejects.toThrow( - new RegExp( - `Expected 6 parameters in mandatory implementation of "compute_note_hash_and_optionally_a_nullifier", but found 5 in noir contract ${contractAddress}.`, - ), - ); - }); - }); -}); diff --git a/yarn-project/simulator/src/client/simulator.ts b/yarn-project/simulator/src/client/simulator.ts index 854fcd01501b..548b9146408a 100644 --- a/yarn-project/simulator/src/client/simulator.ts +++ b/yarn-project/simulator/src/client/simulator.ts @@ -1,13 +1,7 @@ -import { type FunctionCall, type Note, type TxExecutionRequest } from '@aztec/circuit-types'; +import { type FunctionCall, type TxExecutionRequest } from '@aztec/circuit-types'; import { type AztecNode, PrivateExecutionResult } from '@aztec/circuit-types/interfaces/client'; import { CallContext } from '@aztec/circuits.js'; -import { - type FunctionArtifact, - FunctionSelector, - FunctionType, - type NoteSelector, - encodeArguments, -} from '@aztec/circuits.js/abi'; +import { FunctionSelector, FunctionType } from '@aztec/circuits.js/abi'; import { AztecAddress } from '@aztec/circuits.js/aztec-address'; import { Fr } from '@aztec/foundation/fields'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -154,79 +148,4 @@ export class AcirSimulator { throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during private execution')); } } - - /** - * Computes the note hash and inner nullifier of a note. - * @param contractAddress - The address of the contract. - * @param nonce - The nonce of the note hash, is not used when calculating the base note hash. - * @param storageSlot - The storage slot. - * @param noteTypeId - The note type identifier. - * @param note - The note. - * @returns The note hash (and intermediary forms) and inner nullifier. - */ - public async computeNoteHashAndNullifier( - contractAddress: AztecAddress, - nonce: Fr, - storageSlot: Fr, - noteTypeId: NoteSelector, - note: Note, - ) { - const artifact: FunctionArtifact | undefined = await this.db.getFunctionArtifactByName( - contractAddress, - 'compute_note_hash_and_optionally_a_nullifier', - ); - if (!artifact) { - throw new Error( - `Mandatory implementation of "compute_note_hash_and_optionally_a_nullifier" missing in noir contract ${contractAddress.toString()}.`, - ); - } - - if (artifact.parameters.length != 6) { - throw new Error( - `Expected 6 parameters in mandatory implementation of "compute_note_hash_and_optionally_a_nullifier", but found ${ - artifact.parameters.length - } in noir contract ${contractAddress.toString()}.`, - ); - } - - // This constant is not exposed anywhere (because it doesn't have to - it's internal to aztec-nr). It's only here as - // a temporary stopgap until we delete this function fully. - const MAX_NOTE_PACKED_LEN = 16; - const maxNoteFields = MAX_NOTE_PACKED_LEN; - - if (maxNoteFields < note.items.length) { - throw new Error( - `The note being processed has ${note.items.length} fields, while "compute_note_hash_and_optionally_a_nullifier" can only handle a maximum of ${maxNoteFields} fields. Please reduce the number of fields in your note.`, - ); - } - - const noteItemsBoundedVec = { - len: note.items.length, - storage: note.items.concat(Array(maxNoteFields - note.items.length).fill(Fr.ZERO)), - }; - const selector = await FunctionSelector.fromNameAndParameters(artifact); - const execRequest: FunctionCall = { - name: artifact.name, - to: contractAddress, - selector, - type: FunctionType.UNCONSTRAINED, - isStatic: artifact.isStatic, - args: encodeArguments(artifact, [contractAddress, nonce, storageSlot, noteTypeId, true, noteItemsBoundedVec]), - returnTypes: artifact.returnTypes, - }; - - const [noteHash, uniqueNoteHash, siloedNoteHash, innerNullifier] = (await this.runUnconstrained( - execRequest, - contractAddress, - selector, - // We can omit scopes here, because "compute_note_hash_and_optionally_a_nullifier" does not need access to any notes. - )) as bigint[]; - - return { - noteHash: new Fr(noteHash), - uniqueNoteHash: new Fr(uniqueNoteHash), - siloedNoteHash: new Fr(siloedNoteHash), - innerNullifier: new Fr(innerNullifier), - }; - } }