Skip to content
61 changes: 52 additions & 9 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [Aztec.nr] `attempt_note_discovery` now takes two separate functions instead of one

The `attempt_note_discovery` function (and related discovery functions like `do_sync_state`, `process_message_ciphertext`) now takes separate `compute_note_hash` and `compute_note_nullifier` arguments instead of a single combined `compute_note_hash_and_nullifier`. The corresponding type aliases are now `ComputeNoteHash` and `ComputeNoteNullifier` (instead of `ComputeNoteHashAndNullifier`).

This split improves performance during nonce discovery: the note hash only needs to be computed once, while the old combined function recomputed it for every candidate nonce.

Most contracts are not affected, as the macro-generated `sync_state` and `process_message` functions handle this automatically. Only contracts that call `attempt_note_discovery` directly need to update.

**Migration:**

```diff
attempt_note_discovery(
contract_address,
tx_hash,
unique_note_hashes_in_tx,
first_nullifier_in_tx,
recipient,
- _compute_note_hash_and_nullifier,
+ _compute_note_hash,
+ _compute_note_nullifier,
owner,
storage_slot,
randomness,
note_type_id,
packed_note,
);
```

**Impact**: Contracts that call `attempt_note_discovery` or related discovery functions directly with a custom `_compute_note_hash_and_nullifier` argument. The old combined function is still generated (deprecated) but is no longer used by the framework. Additionally, if you had a custom `_compute_note_hash_and_nullifier` function then compilation will now fail as you'll need to also produce the corresponding `_compute_note_hash` and `_compute_note_nullifier` functions.

### Private initialization nullifier now includes `init_hash`

The private initialization nullifier is no longer derived from just the contract address. It is now computed as a Poseidon2 hash of `[address, init_hash]` using a dedicated domain separator. This prevents observers from determining whether a fully private contract has been initialized by simply knowing its address.
Expand Down Expand Up @@ -47,6 +77,7 @@ The function signature has changed to resolve the epoch internally from a transa
The return type `L2ToL1MembershipWitness` now includes `epochNumber`. An optional `messageIndexInTx` parameter can be passed as the fourth argument to disambiguate when a transaction emits multiple identical L2-to-L1 messages.

**Impact**: All call sites that compute L2-to-L1 membership witnesses must update to the new argument order and extract `epochNumber` from the result instead of passing it in.

### Two separate init nullifiers for private and public

Contract initialization now emits two separate nullifiers instead of one: a **private init nullifier** and a **public init nullifier**. Each nullifier gates its respective execution domain:
Expand Down Expand Up @@ -149,9 +180,14 @@ When using `NO_WAIT`, returns `{ txHash, offchainEffects, offchainMessages }` in
Offchain messages emitted by the transaction are available on the result:

```typescript
const { receipt, offchainMessages } = await contract.methods.foo(args).send({ from: sender });
const { receipt, offchainMessages } = await contract.methods
.foo(args)
.send({ from: sender });
for (const msg of offchainMessages) {
console.log(`Message for ${msg.recipient} from contract ${msg.contractAddress}:`, msg.payload);
console.log(
`Message for ${msg.recipient} from contract ${msg.contractAddress}:`,
msg.payload,
);
}
```

Expand Down Expand Up @@ -206,6 +242,7 @@ counter/
This enables adding multiple contracts to a single workspace. Running `aztec new <name>` inside an existing workspace (a directory with a `Nargo.toml` containing `[workspace]`) now adds a new `<name>_contract` and `<name>_test` crate pair to the workspace instead of creating a new directory.

**What changed:**

- Crate directories are now `<name>_contract/` and `<name>_test/` instead of `contract/` and `test/`.
- Contract code is now at `<name>_contract/src/main.nr` instead of `contract/src/main.nr`.
- Contract dependencies go in `<name>_contract/Nargo.toml` instead of `contract/Nargo.toml`.
Expand Down Expand Up @@ -265,6 +302,7 @@ my_project/
```

**What changed:**

- The `--contract` and `--lib` flags have been removed from `aztec new` and `aztec init`. These commands now always create a contract workspace.
- Contract code is now at `contract/src/main.nr` instead of `src/main.nr`.
- The `Nargo.toml` in the project root is now a workspace file. Contract dependencies go in `contract/Nargo.toml`.
Expand All @@ -290,7 +328,7 @@ The wallet now passes scopes to PXE, and only the `from` address is in scope by

2. **Operations that access another contract's private state** (e.g., withdrawing from an escrow contract that nullifies the contract's own token notes).

```
````

**Example: deploying a contract with private storage (e.g., `PrivateToken`)**

Expand All @@ -304,7 +342,7 @@ The wallet now passes scopes to PXE, and only the `from` address is in scope by
from: sender,
+ additionalScopes: [tokenInstance.address],
});
```
````

**Example: withdrawing from an escrow contract**

Expand Down Expand Up @@ -353,22 +391,26 @@ The `include_by_timestamp` field has been renamed to `expiration_timestamp` acro
The Aztec CLI is now installed without Docker. The installation command has changed:

**Old installation (deprecated):**

```bash
bash -i <(curl -sL https://install.aztec.network)
aztec-up <version>
```

**New installation:**

```bash
VERSION=<version> bash -i <(curl -sL https://install.aztec.network/<version>)
```

For example, to install version `#include_version_without_prefix`:

```bash
VERSION=#include_version_without_prefix bash -i <(curl -sL https://install.aztec.network/#include_version_without_prefix)
```

**Key changes:**

- Docker is no longer required to run the Aztec CLI tools
- The `VERSION` environment variable must be set in the installation command
- The version must also be included in the URL path
Expand All @@ -377,12 +419,13 @@ VERSION=#include_version_without_prefix bash -i <(curl -sL https://install.aztec

After installation, `aztec-up` functions as a version manager with the following commands:

| Command | Description |
|---------|-------------|
| Command | Description |
| ---------------------------- | ------------------------------------------- |
| `aztec-up install <version>` | Install a specific version and switch to it |
| `aztec-up use <version>` | Switch to an already installed version |
| `aztec-up list` | List all installed versions |
| `aztec-up self-update` | Update aztec-up itself |
| `aztec-up use <version>` | Switch to an already installed version |
| `aztec-up list` | List all installed versions |
| `aztec-up self-update` | Update aztec-up itself |

### `@aztec/test-wallet` replaced by `@aztec/wallets`

The `@aztec/test-wallet` package has been removed. Use `@aztec/wallets` instead, which provides `EmbeddedWallet` with a `static create()` factory:
Expand Down
177 changes: 16 additions & 161 deletions noir-projects/aztec-nr/aztec/src/macros/aztec.nr
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod compute_note_hash_and_nullifier;

use crate::{
macros::{
calls_generation::{
Expand All @@ -7,16 +9,14 @@ use crate::{
dispatch::generate_public_dispatch,
emit_public_init_nullifier::generate_emit_public_init_nullifier,
internals_functions_generation::{create_fn_abi_exports, process_functions},
notes::NOTES,
storage::STORAGE_LAYOUT_NAME,
utils::{
get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test,
module_has_storage,
},
utils::{is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, module_has_storage},
},
messages::discovery::CustomMessageHandler,
};

use compute_note_hash_and_nullifier::generate_contract_library_methods_compute_note_hash_and_nullifier;

/// Configuration for the [`aztec`] macro.
///
/// This type lets users override different parts of the default aztec-nr contract behavior, such
Expand Down Expand Up @@ -100,12 +100,16 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted {
// We generate ABI exports for all the external functions in the contract.
let fn_abi_exports = create_fn_abi_exports(m);

// We generate `_compute_note_hash_and_nullifier`, `sync_state` and `process_message` functions only if they are
// not already implemented. If they are implemented we just insert empty quotes.
// We generate `_compute_note_hash`, `_compute_note_nullifier` (and the deprecated
// `_compute_note_hash_and_nullifier` wrapper), `sync_state` and `process_message` functions only if they are not
// already implemented. If they are implemented we just insert empty quotes.
let contract_library_method_compute_note_hash_and_nullifier = if !m.functions().any(|f| {
// Note that we don't test for `_compute_note_hash` or `_compute_note_nullifier` in order to make this simpler
// - users must either implement all three or none.
// Down the line we'll remove this check and use `AztecConfig`.
f.name() == quote { _compute_note_hash_and_nullifier }
}) {
generate_contract_library_method_compute_note_hash_and_nullifier()
generate_contract_library_methods_compute_note_hash_and_nullifier()
} else {
quote {}
};
Expand Down Expand Up @@ -220,157 +224,6 @@ comptime fn generate_contract_interface(m: Module) -> Quoted {
}
}

/// Generates a contract library method called `_compute_note_hash_and_nullifier` which is used for note discovery (to
/// create the `aztec::messages::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_nullifier() -> Quoted {
if NOTES.len() > 0 {
// 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 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() {
let typ = NOTES.get(i);

let get_note_type_id = get_trait_impl_method(
typ,
quote { crate::note::note_interface::NoteType },
quote { get_id },
);
let unpack = get_trait_impl_method(
typ,
quote { crate::protocol::traits::Packable },
quote { unpack },
);

let compute_note_hash = get_trait_impl_method(
typ,
quote { crate::note::note_interface::NoteHash },
quote { compute_note_hash },
);

let compute_nullifier_unconstrained = get_trait_impl_method(
typ,
quote { crate::note::note_interface::NoteHash },
quote { compute_nullifier_unconstrained },
);

let if_or_else_if = if i == 0 {
quote { if }
} else {
quote { else if }
};

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 BoundedVec has the expected
// length, since we're about to interpret its raw storage as a fixed-size array by calling the
// unpack function on it.
let expected_len = <$typ as $crate::protocol::traits::Packable>::N;
let actual_len = packed_note.len();
if actual_len != expected_len {
aztec::protocol::logging::warn_log_format(
"[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.",
[expected_len as Field, actual_len as Field, note_type_id],
);
Option::none()
} else {
let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0));

let note_hash = $compute_note_hash(note, owner, storage_slot, randomness);

// The message discovery process finds settled notes, that is, notes that were created in
// prior transactions and are therefore already part of the note hash tree. We therefore
// compute the nullification note hash by treating the note as a settled note with the
// provided note nonce.
let note_hash_for_nullification =
aztec::note::utils::compute_note_hash_for_nullification(
aztec::note::HintedNote {
note,
contract_address,
owner,
randomness,
storage_slot,
metadata:
aztec::note::note_metadata::SettledNoteMetadata::new(
note_nonce,
)
.into(),
},
);

let inner_nullifier = $compute_nullifier_unconstrained(
note,
owner,
note_hash_for_nullification,
);

Option::some(
aztec::messages::discovery::NoteHashAndNullifier {
note_hash,
inner_nullifier,
},
)
}
}
},
);
}

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 `note_nonce`.
///
/// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHashAndNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`.
///
/// This function is automatically injected by the `#[aztec]` macro.
#[contract_library_method]
unconstrained fn _compute_note_hash_and_nullifier(
packed_note: BoundedVec<Field, aztec::messages::logs::note::MAX_NOTE_PACKED_LEN>,
owner: aztec::protocol::address::AztecAddress,
storage_slot: Field,
note_type_id: Field,
contract_address: aztec::protocol::address::AztecAddress,
randomness: Field,
note_nonce: Field,
) -> Option<aztec::messages::discovery::NoteHashAndNullifier> {
$if_note_type_id_match_statements
else {
aztec::protocol::logging::warn_log_format(
Comment thread
nventuro marked this conversation as resolved.
"[aztec-nr] Unknown note type id {0}. Skipping note.",
[note_type_id],
);
Option::none()
}
}
}
} else {
// 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_nullifier(
_packed_note: BoundedVec<Field, aztec::messages::logs::note::MAX_NOTE_PACKED_LEN>,
_owner: aztec::protocol::address::AztecAddress,
_storage_slot: Field,
_note_type_id: Field,
_contract_address: aztec::protocol::address::AztecAddress,
_randomness: Field,
_nonce: Field,
) -> Option<aztec::messages::discovery::NoteHashAndNullifier> {
panic(f"This contract does not use private notes")
}
}
}
}

/// Generates the `sync_state` utility function that performs message discovery.
comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_inbox_sync_option: Quoted) -> Quoted {
quote {
Expand All @@ -386,7 +239,8 @@ comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_
let address = aztec::context::UtilityContext::new().this_address();
aztec::messages::discovery::do_sync_state(
address,
_compute_note_hash_and_nullifier,
_compute_note_hash,
_compute_note_nullifier,
$process_custom_message_option,
$offchain_inbox_sync_option,
);
Expand Down Expand Up @@ -416,7 +270,8 @@ comptime fn generate_process_message(process_custom_message_option: Quoted) -> Q

aztec::messages::discovery::process_message::process_message_ciphertext(
address,
_compute_note_hash_and_nullifier,
_compute_note_hash,
_compute_note_nullifier,
$process_custom_message_option,
message_ciphertext,
message_context,
Expand Down
Loading
Loading