Skip to content

Commit

Permalink
feat!: autogenerate compute_note_hash_and_nullifier (#4610)
Browse files Browse the repository at this point in the history
Closes #2918.

This adds a new macro function that processes the unresolved trait impls
and injects a new function before name resolution takes place. This lets
us leverage the parser and write the function in Noir instead of having
to deal with more complicated processed objects.

In order to find all of the note types we look for impls of the
`NoteInterface` trait. This is a bit more involved than it seems, since
other crates may also have structs that impl this trait, and those will
have already been processed. We rely on the fact that the contract crate
is processed last, and search for external crate impls via the
NodeInterner and internal ones in the unresolved functions. This method
is robust as long as we do the contract crate after all of its imports,
which holds because the imports are recursively collected from the root
crate.

I also added a small escape hatch mechanism by skipping any code
generation if the function is already defined by the user. This could
use some polishing since it is possible for the user to e.g. provide the
function but get the arity or parameter types wrong, in which case
they'd get a duplicate definition error. Diagnosis and error messages
can be improved here
(#4647), but I
think a simple mechanism is sufficient for now.

---

### Minor issues

- One of the function arguments is a fixed-size array, which should be
as big as the largest note length. We are not yet pulling the note
length (#4649), so
I'm passing a hardcoded value for now. 12 Fields ought to be enough for
anyone.

- Doing this introduces an implicit dependency on `AztecAddress` on all
contracts. This won't be an issue once
#4496 is in, but I
did have to manually add it to some of the account contracts for now.

- I created a new macro function that is called on each crate after
collection but before resolution. Due to some internal compilers
shenanigans (mostly who holds mut references to what) I chose to
specialize this function so that for now it only passes the collected
unresolved functions, making it a bit niche for the current use case.
@vezenovm and I are planning to generalize this down the road
(#4653).

- I'm now importing in the macro from some places that were not
previously used, notably the HIR and Noir errors. I am not sure if we
might want to pull those differently - I simply imported what I needed.

- I also introduced some getters to provide mutable access to private
fields of the HIR Context and CrateDefMap. This is because we need to
modify the contract module in the def map by declaring the new function
(which is how we get things like duplicate definition detection). We're
already mutating the HIR Context in the macros, so also mutating its
members doesn't feel like a stretch.

- Finally, I don't know enough about how Noir errors work to know how to
produce a useful `Location` value for the new function, if indeed that
can be done. Providing an empty span seems to at least not cause weird
errors on the LSP plugin, so that's how I left it for now.
  • Loading branch information
nventuro committed Feb 19, 2024
1 parent 5a2c534 commit 286e708
Show file tree
Hide file tree
Showing 56 changed files with 586 additions and 851 deletions.
1 change: 1 addition & 0 deletions avm-transpiler/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 0 additions & 13 deletions boxes/blank/src/contracts/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,4 @@ contract Blank {

[pub_key.x, pub_key.y]
}

// A function which needs to be implemented by every contract working with storage. Replace it's content with your
// own logic once you start working with private storage.
// TODO: Remove this placeholder once https://github.com/AztecProtocol/aztec-packages/issues/2918 is implemented.
unconstrained fn compute_note_hash_and_nullifier(
contract_address: AztecAddress,
nonce: Field,
storage_slot: Field,
note_type_id: Field,
serialized_note: [Field; 0]
) -> pub [Field; 4] {
[0, 0, 0, 0]
}
}
27 changes: 1 addition & 26 deletions boxes/token/src/contracts/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ contract Token {

use crate::types::{
transparent_note::TransparentNote,
token_note::{TokenNote, TOKEN_NOTE_LEN},
token_note::TokenNote,
balances_map::BalancesMap
};
// docs:end::imports
Expand Down Expand Up @@ -379,30 +379,5 @@ contract Token {
storage.public_balances.at(owner).read().value
}
// docs:end:balance_of_public

// docs:start:compute_note_hash_and_nullifier
// Computes note hash and nullifier.
// Note 1: Needs to be defined by every contract producing logs.
// Note 2: Having it in all the contracts gives us the ability to compute the note hash and nullifier differently for different kind of notes.
unconstrained fn compute_note_hash_and_nullifier(
contract_address: AztecAddress,
nonce: Field,
storage_slot: Field,
note_type_id: Field,
serialized_note: [Field; TOKEN_NOTE_LEN]
) -> pub [Field; 4] {
let note_header = NoteHeader::new(contract_address, nonce, storage_slot);

if (note_type_id == TransparentNote::get_note_type_id()) {
note_utils::compute_note_hash_and_nullifier(
TransparentNote::deserialize_content,
note_header,
serialized_note
)
} else {
note_utils::compute_note_hash_and_nullifier(TokenNote::deserialize_content, note_header, serialized_note)
}
}
// docs:end:compute_note_hash_and_nullifier
}
// docs:end:token_all
22 changes: 4 additions & 18 deletions boxes/token/src/contracts/src/types/token_note.nr
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
use dep::aztec::{
protocol_types::{
address::AztecAddress,
constants::MAX_READ_REQUESTS_PER_CALL
},
note::{
note_header::NoteHeader,
note_interface::NoteInterface,
utils::compute_note_hash_for_consumption,
},
context::PrivateContext,
state_vars::set::Set,
log::emit_encrypted_log,
hash::pedersen_hash,
};
use dep::aztec::oracle::{
rand::rand,
nullifier_key::get_nullifier_secret_key,
get_public_key::get_public_key,
protocol_types::{address::AztecAddress, constants::MAX_READ_REQUESTS_PER_CALL},
note::{note_header::NoteHeader, note_interface::NoteInterface, utils::compute_note_hash_for_consumption},
context::PrivateContext, state_vars::set::Set, log::emit_encrypted_log, hash::pedersen_hash
};
use dep::aztec::oracle::{rand::rand, nullifier_key::get_nullifier_secret_key, get_public_key::get_public_key};
use dep::safe_math::SafeU120;
use dep::std::option::Option;

Expand Down
19 changes: 0 additions & 19 deletions docs/docs/developers/contracts/references/storage/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,6 @@ contract Token {
unconstrained fn balance_of_private(owner: AztecAddress) -> Field {}

unconstrained fn balance_of_public(owner: AztecAddress) -> Field {}

unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, serialized_note: [Field; VALUE_NOTE_LEN]) -> [Field; 4] {}
}
```

Expand Down Expand Up @@ -193,8 +191,6 @@ Aztec transactions can pass data to Ethereum contracts through the rollup via th

Unconstrained functions can be thought of as view functions from Solidity--they only return information from the contract storage or compute and return data without modifying contract storage.

The `compute_note_hash_and_nullifier` function allows contract devs to specify how to compute notes and nullifiers. This must be included in every contract because it depends on the storage slots, which are defined when we set up storage.

## Contract dependencies

Before we can implement the functions, we need set up the contract storage, and before we do that we need to import the appropriate dependencies.
Expand Down Expand Up @@ -435,21 +431,6 @@ A getter function for checking the public balance of the provided Aztec account.

#include_code balance_of_public /noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust

#### `compute_note_hash_and_nullifier`

A getter function to compute the note hash and nullifier for notes in the contract's storage.

This must be included in every contract because it depends on the storage slots, which are defined when we set up storage.

#include_code compute_note_hash_and_nullifier /noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust

:::danger
If your contract works with storage (has Storage struct defined), you **MUST** include a `compute_note_hash_and_nullifier` function.
If you don't yet have any private state variables defined put there a placeholder function:

#include_code compute_note_hash_and_nullifier_placeholder /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust
:::

## Compiling

Now that the contract is complete, you can compile it with `aztec-nargo`. See the [Sandbox reference page](../../../sandbox/references/sandbox-reference.md) for instructions on setting it up.
Expand Down
11 changes: 0 additions & 11 deletions docs/docs/developers/contracts/resources/common_patterns/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,6 @@ Hence, it's necessary to add a "randomness" field to your note to prevent such a

#include_code address_note_def noir-projects/aztec-nr/address-note/src/address_note.nr rust

### Working with `compute_note_hash_and_nullifier()`

Currently, if you have storage defined, the compiler will error if you don't have a `compute_note_hash_and_nullifier()` defined. Without this, the PXE can't process encrypted events and discover your notes.

If your contract doesn't have anything to do with notes (e.g. operates solely in the public domain), you can do the following:
#include_code compute_note_hash_and_nullifier_placeholder /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust

Otherwise, you need this method to help the PXE with processing your notes. In our [demo token contract](../../../tutorials/writing_token_contract.md#compute_note_hash_and_nullifier), we work with 2 kinds of notes: `ValueNote` and `TransparentNote`. Hence this method must define how to work with both:

#include_code compute_note_hash_and_nullifier /noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust

### L1 -- L2 interactions

Refer to [Token Portal tutorial on bridging tokens between L1 and L2](../../../tutorials/token_portal/main.md) and/or [Uniswap tutorial that shows how to swap on L1 using funds on L2](../../../tutorials/uniswap/main.md). Both examples show how to:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,7 @@ If the decryption is successful, the PXE will store the decrypted note inside a
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. Therefore, developers will need to implement a `compute_note_hash_and_nullifier` function inside their contracts.

:::danger
If your contract works with storage (has Storage struct defined), you **MUST** include a `compute_note_hash_and_nullifier` function to allow PXE to process encrypted events.
:::

Every time a new note is successfully decrypted, the PXE will expect the existence of a `compute_note_hash_and_nullifier` function, which must teach it how to correctly process the new note.

#include_code compute_note_hash_and_nullifier /noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust
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_nullifier`, which is automatically injected into every contract when compiled.

## Unencrypted Events

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ struct Storage {
}
```

:::danger
If your contract uses storage (has Storage struct defined), you **MUST** include a `compute_note_hash_and_nullifier` function to allow PXE to process encrypted events. See [encrypted events](../events/emit_event.md#successfully-process-the-encrypted-event) for more.

If you don't yet have any private state variables defined you can use this placeholder function:

#include_code compute_note_hash_and_nullifier_placeholder /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust
:::

Since Aztec.nr is written in Noir, which is state-less, we need to specify how the storage struct should be initialized to read and write data correctly. This is done by specifying an `init` function that is run in functions that rely on reading or altering the state variables. This `init` function must declare the Storage struct with an instantiation defining how variables are accessed and manipulated. The function MUST be called `init` for the Aztec.nr library to properly handle it (this will be relaxed in the future).

```rust
Expand Down
10 changes: 0 additions & 10 deletions docs/docs/developers/debugging/aztecnr-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_

You can learn more about dependencies and their paths [here](../contracts/resources/dependencies.md).

#### `compute_note_hash_and_nullifier function not found. Define it in your contract`

Any smart contract that works with storage must include a [`compute_note_hash_and_nullifier`](https://github.com/AztecProtocol/aztec-packages/blob/6c20b45993ee9cbd319ab8351e2722e0c912f427/noir-projects/aztec-nr/aztec/src/note/utils.nr#L69) function to allow the PXE to process encrypted events.

This is an example of this function in the token contract:

#include_code compute_note_hash_and_nullifier noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust

This error may also show if the `compute_note_hash_and_nullifier` function is not correct or sits outside of the contract.

#### `backend has encountered an error`

This is likely due to a version mismatch or bad install of barretenberg. Try [reinstalling nargo](../updating.md) or uninstalling barretenberg:
Expand Down
10 changes: 2 additions & 8 deletions docs/docs/developers/getting_started/aztecnr-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,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 owner. Although this isn't really an issue in this simple smart contract, Aztec requires a contract that has any private functions to include this function.

Add a new function into your contract as shown below:

#include_code nullifier /noir-projects/noir-contracts/contracts/counter_contract/src/main.nr rust

Here, we're computing both the note hash and the nullifier. The nullifier computation uses Aztec’s `compute_note_hash_and_nullifier` function, which takes details about the note's attributes eg contract address, nonce, storage slot, type id, and preimage.
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 owner. Although this isn't really an issue in this simple smart contract, Aztec injects a special function called `compute_note_hash_and_nullifier` to determine these values for any given note produced by this contract.

## Getting a counter

Expand Down Expand Up @@ -172,7 +166,7 @@ This will return something like this:

```bash
➜ counter aztec-cli get-accounts
Accounts found:
Accounts found:

Address: 0x2fd4503a9b855a852272945df53d7173297c1469cceda31048b85118364b09a3
Public Key: 0x27c20118733174347b8082f578a7d8fb84b3ad38be293715eee8119ee5cd8a6d0d6b7d8124b37359663e75bcd2756f544a93b821a06f8e33fba68cc8029794d9
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,6 @@ We need a function that lets us read the token value. Paste this into `main.nr`:

#include_code read_token /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust

And the `compute_note_hash_and_nullifier` required on every contract:

```rust
#include_code compute_note_hash_and_nullifier_placeholder /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr raw
}
```

## Compile code

Congratulations, you have written all the contracts we need for this tutorial! Now let's compile them.
Expand Down
10 changes: 0 additions & 10 deletions docs/docs/developers/tutorials/writing_private_voting_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,6 @@ Paste this function in your contract:

Here, we are asserting that the `msg_sender()` is equal to the admin stored in public state. We have to create an `AztecAddress` type from the `msg_sender()` in order to do a direct comparison.

## compute_note_hash_and_nullifier

Every Aztec contract that has storage must have a `compute_note_hash_and_nullifier()` function. If you try to compile without this function, you will get an error. This is explained in more detail [here](../contracts/resources/common_patterns/main.md#working-with-compute_note_hash_and_nullifier).

At the end of the contract, paste this:

#include_code compute_note_hash_and_nullifier noir-projects/noir-contracts/contracts/easy_private_voting_contract/src/main.nr rust

We can simply return `[0,0,0,0]` because we are not creating any notes in our contract.

## Compiling and deploying

The easiest way to compile the contract is with `aztec-nargo`. Run the following command in the directory with your Nargo.toml file:
Expand Down
19 changes: 0 additions & 19 deletions docs/docs/developers/tutorials/writing_token_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,6 @@ contract Token {
unconstrained fn balance_of_private(owner: AztecAddress) -> Field {}

unconstrained fn balance_of_public(owner: AztecAddress) -> Field {}

unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, note_type_id: Field, serialized_note: [Field; VALUE_NOTE_LEN]) -> [Field; 4] {}
}
```

Expand Down Expand Up @@ -193,8 +191,6 @@ Aztec transactions can pass data to Ethereum contracts through the rollup via th

Unconstrained functions can be thought of as view functions from Solidity--they only return information from the contract storage or compute and return data without modifying contract storage.

The `compute_note_hash_and_nullifier` function allows contract devs to specify how to compute notes and nullifiers. This must be included in every contract because it depends on the storage slots, which are defined when we set up storage.

## Contract dependencies

Before we can implement the functions, we need set up the contract storage, and before we do that we need to import the appropriate dependencies.
Expand Down Expand Up @@ -435,21 +431,6 @@ A getter function for checking the public balance of the provided Aztec account.

#include_code balance_of_public /noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust

#### `compute_note_hash_and_nullifier`

A getter function to compute the note hash and nullifier for notes in the contract's storage.

This must be included in every contract because it depends on the storage slots, which are defined when we set up storage.

#include_code compute_note_hash_and_nullifier /noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust

:::danger
If your contract works with storage (has Storage struct defined), you **MUST** include a `compute_note_hash_and_nullifier` function.
If you don't yet have any private state variables defined put there a placeholder function:

#include_code compute_note_hash_and_nullifier_placeholder /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust
:::

## Compiling

Now that the contract is complete, you can compile it with `aztec-nargo`. See the [Sandbox reference page](../sandbox/references/sandbox-reference.md) for instructions on setting it up.
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/misc/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ keywords: [sandbox, cli, aztec, notes, migration, updating, upgrading]

Aztec is in full-speed development. Literally every version breaks compatibility with the previous ones. This page attempts to target errors and difficulties you might encounter when upgrading, and how to resolve them.

## TBD

### Autogenerate `compute_note_hash_and_nullifier`

Historically developers have been required to include a `compute_note_hash_and_nullifier` function in each of their contracts. This function is now automatically generated, and all instances of it in contract code can be safely removed.

It is possible to provide a user-defined implementation, in which case auto-generation will be skipped (though there are no known use cases for this).

## 0.24.0

### Introduce Note Type IDs
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/value-note/src/balance_utils.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use dep::aztec::note::{note_getter::view_notes, note_viewer_options::NoteViewerOptions};
use dep::aztec::state_vars::set::Set;
use crate::value_note::{VALUE_NOTE_LEN, ValueNote};
use crate::value_note::ValueNote;

unconstrained pub fn get_balance(set: Set<ValueNote>) -> Field {
get_balance_with_offset(set, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,8 @@ contract AvmTest {
fn getTimestamp() -> pub Field {
context.timestamp()
}

// #[aztec(public-vm)]
// fn getContractCallDepth() -> pub Field {
// context.contract_call_depth()
// }

// Function required for all contracts
unconstrained fn compute_note_hash_and_nullifier(
_contract_address: AztecAddress,
_nonce: Field,
_storage_slot: Field,
_note_type_id: Field,
_serialized_note: [Field; 1]
) -> pub [Field; 4] {
[0, 0, 0, 0]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// arise from code changes.

contract Benchmarking {
use dep::value_note::{utils::{increment, decrement}, value_note::{VALUE_NOTE_LEN, ValueNote}};
use dep::value_note::{utils::{increment, decrement}, value_note::ValueNote};

use dep::aztec::{
protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress},
Expand Down Expand Up @@ -60,15 +60,4 @@ contract Benchmarking {
fn broadcast(owner: AztecAddress) {
emit_unencrypted_log(&mut context, storage.balances.at(owner).read());
}

unconstrained fn compute_note_hash_and_nullifier(
contract_address: AztecAddress,
nonce: Field,
storage_slot: Field,
note_type_id: Field,
serialized_note: [Field; VALUE_NOTE_LEN]
) -> pub [Field; 4] {
let note_header = NoteHeader::new(contract_address, nonce, storage_slot);
note_utils::compute_note_hash_and_nullifier(ValueNote::deserialize_content, note_header, serialized_note)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use dep::std::{
option::Option,
};
use dep::value_note::{
value_note::{ValueNote, VALUE_NOTE_LEN},
value_note::ValueNote,
};

struct Card {
Expand Down
Loading

0 comments on commit 286e708

Please sign in to comment.