Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
"[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