Skip to content
Closed
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
2 changes: 1 addition & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@
"noirup",
"allow_phase_change",
"nophasecheck",
"nullifer",
"Nullifiable",
"offchain",
"onchain",
Expand Down Expand Up @@ -367,6 +366,7 @@
"unfinalized",
"uniquified",
"uniquify",
"unlinkability",
"unkonstrained",
"unnullify",
"unpadded",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1987,7 +1987,7 @@ Doing the changes is as straightforward as:

`UintNote` has also been updated to use the native `u128` type.

### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifer`
### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifier`

This function is no longer mandatory for contracts, and the `#[aztec]` macro no longer injects it.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2684,7 +2684,7 @@ Doing the changes is as straightforward as:

`UintNote` has also been updated to use the native `u128` type.

### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifer`
### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifier`

This function is no longer mandatory for contracts, and the `#[aztec]` macro no longer injects it.

Expand Down
111 changes: 101 additions & 10 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,75 @@ 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.

Note that `Wallet.getContractMetadata` now returns `isContractInitialized: undefined` when the wallet does not have the contract instance registered, since `init_hash` is needed to compute the nullifier and initialization status cannot be determined. Previously, this check worked for any address. Callers should check for `undefined` before branching on the boolean value.

If you use `assert_contract_was_initialized_by` or `assert_contract_was_not_initialized_by` from `aztec::history::deployment`, these now require an additional `init_hash: Field` parameter:

```diff
+ let instance = get_contract_instance(contract_address);
assert_contract_was_initialized_by(
block_header,
contract_address,
+ instance.initialization_hash,
);
```

### [Aztec.js] `TxReceipt` now includes `epochNumber`

`TxReceipt` now includes an `epochNumber` field that indicates which epoch the transaction was included in.

### [Aztec.js] `computeL2ToL1MembershipWitness` signature changed

The function signature has changed to resolve the epoch internally from a transaction hash, rather than requiring the caller to pass the epoch number.

**Migration:**

```diff
- const witness = await computeL2ToL1MembershipWitness(aztecNode, epochNumber, messageHash);
- // epoch was passed in by the caller
+ const witness = await computeL2ToL1MembershipWitness(aztecNode, messageHash, txHash);
+ // epoch is now available on the returned witness
+ const epoch = witness.epochNumber;
```

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 @@ -56,6 +125,16 @@ The return type `L2ToL1MembershipWitness` now includes `epochNumber`. An optiona

**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.

### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained

This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ConfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe.

### [Aztec.nr] Changes to standard note hash computation

Note hashes used to be computed with the storage slot being the last value of the preimage, it is now the first. This is to make it easier to ensure all note hashes have proper domain separation.

This change requires no input from your side unless you were testing or relying on hardcoded note hashes.

### [Aztec.js] `getPublicEvents` now returns an object instead of an array

`getPublicEvents` now returns a `GetPublicEventsResult<T>` object with `events` and `maxLogsHit` fields instead of a plain array. This enables pagination through large result sets using the new `afterLog` filter option.
Expand Down Expand Up @@ -122,9 +201,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 @@ -179,6 +263,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 @@ -238,6 +323,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 @@ -263,7 +349,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 @@ -277,7 +363,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 @@ -324,22 +410,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 @@ -348,12 +438,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 Expand Up @@ -2984,7 +3075,7 @@ Doing the changes is as straightforward as:

`UintNote` has also been updated to use the native `u128` type.

### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifer`
### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifier`

This function is no longer mandatory for contracts, and the `#[aztec]` macro no longer injects it.

Expand Down
8 changes: 6 additions & 2 deletions noir-projects/aztec-nr/aztec/src/capsules/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ impl<T> CapsuleArray<T> {
capsules::store(self.contract_address, self.base_slot, current_length - 1);
}

/// Iterates over the entire array, calling the callback with all values and their array index. The order in which
/// values are processed is arbitrary.
/// Calls a function on each element of the array.
///
/// The function `f` is called once with each array value and its corresponding index. The order in which values
/// are processed is arbitrary.
///
/// ## Array Mutation
///
/// It is safe to delete the current element (and only the current element) from inside the callback via `remove`:
/// ```noir
Expand Down
87 changes: 36 additions & 51 deletions noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -349,71 +349,56 @@ impl PrivateContext {
self.note_hashes.push(Counted::new(note_hash, self.next_counter()));
}

/// Pushes a new nullifier to the Aztec blockchain's global Nullifier Tree (a state tree).
/// Creates a new [nullifier](crate::nullifier).
///
/// See also: `push_nullifier_for_note_hash`.
/// ## Safety
///
/// Low-level function: Ordinarily, smart contract developers will not need to manually call this. Aztec-nr's state
/// variables (see `../state_vars/`) are designed to understand when to create and push new nullifiers.
///
/// A nullifier can only be emitted once. Duplicate nullifier insertions are rejected by the protocol.
///
/// Generally, a nullifier is emitted to prevent an action from happening more than once, in such a way that the
/// action cannot be linked (by an observer of the blockchain) to any earlier transactions.
///
/// I.e. a nullifier is a random-looking, but deterministic record of a private, one-time action, which does not
/// leak what action has been taken, and which preserves the property of "tx unlinkability".
/// This is a low-level function that must be used with great care to avoid subtle corruption of contract state.
/// Instead of calling this function, consider using the higher-level [`crate::state_vars::SingleUseClaim`].
///
/// Usually, a nullifier will be emitted to "spend" a note (a piece of private state), without revealing which
/// specific note is being spent.
/// In particular, callers must ensure all nullifiers created by a contract are properly domain-separated, so that
/// unrelated components don't interfere with one another (e.g. a transaction nullifier accidentally marking a
/// variable as initialized). Only [`PrivateContext::push_nullifier_for_note_hash`] should be used for note
/// nullifiers, never this one.
///
/// (Important: in such cases, use the below `push_nullifier_for_note_hash`).
///
/// Sometimes, a nullifier might be emitted completely unrelated to any notes. Examples include initialization of a
/// new contract; initialization of a PrivateMutable, or signalling in Semaphore-like applications. This
/// `push_nullifier` function serves such use cases.
///
/// # Arguments
/// * `nullifier`
///
/// # Advanced
/// From here, the protocol's kernel circuits will take over and insert the nullifier into the protocol's
/// "nullifier tree" (in the Base Rollup circuit). Before insertion, the protocol will:
/// - "Silo" the `nullifier` with the contract address of this function, to yield a `siloed_nullifier`. This
/// prevents state collisions between different smart contracts.
/// - Ensure the `siloed_nullifier` is unique (the nullifier tree is an indexed merkle tree which supports
/// efficient non-membership proofs).
/// ## Advanced
///
/// The raw `nullifier` is not what is inserted into the Aztec state tree: it will be first siloed by contract
/// address via [`crate::protocol::hash::compute_siloed_nullifier`] in order to prevent accidental or malicious
/// interference of nullifiers from different contracts.
pub fn push_nullifier(&mut self, nullifier: Field) {
notify_created_nullifier(nullifier);
self.nullifiers.push(Nullifier { value: nullifier, note_hash: 0 }.count(self.next_counter()));
}

/// Pushes a nullifier that corresponds to a specific note hash.
/// Creates a new [nullifier](crate::nullifier) associated with a note.
///
/// Low-level function: Ordinarily, smart contract developers will not need to manually call this. Aztec-nr's state
/// variables (see `../state_vars/`) are designed to understand when to create and push new nullifiers.
/// This is a variant of [`PrivateContext::push_nullifier`] that is used for note nullifiers, i.e. nullifiers that
/// correspond to a note. If a note and its nullifier are created in the same transaction, then the private kernels
/// will 'squash' these values, deleting them both as if they never existed and reducing transaction fees.
///
/// This is a specialized version of `push_nullifier` that links a nullifier to the specific note hash it's
/// nullifying. This is the most common usage pattern for nullifiers. See `push_nullifier` for more explanation on
/// nullifiers.
/// The `nullification_note_hash` must be the result of calling
/// [`crate::note::utils::compute_confirmed_note_hash_for_nullification`] for pending notes, and `0` for settled
/// notes (which cannot be squashed).
///
/// # Arguments
/// * `nullifier`
/// * `nullified_note_hash` - The note hash of the note being nullified
/// ## Safety
///
/// # Advanced
///Important: usage of this function doesn't mean that the world will _see_ that this nullifier relates to the
/// given nullified_note_hash (as that would violate "tx unlinkability"); it simply informs the user's PXE about
/// the relationship (via `notify_nullified_note`). The PXE can then use this information to feed hints to the
/// kernel circuits for "squashing" purposes: If a note is nullified during the same tx which created it, we can
/// "squash" (delete) the note and nullifier (and any private logs associated with the note), to save on data
/// emission costs.
///
pub fn push_nullifier_for_note_hash(&mut self, nullifier: Field, nullified_note_hash: Field) {
/// This is a low-level function that must be used with great care to avoid subtle corruption of contract state.
/// Instead of calling this function, consider using the higher-level [`crate::note::lifecycle::destroy_note`].
///
/// The precautions listed for [`PrivateContext::push_nullifier`] apply here as well, and callers should
/// additionally ensure `nullification_note_hash` corresponds to a note emitted by this contract, with its hash
/// computed in the same transaction execution phase as the call to this function. Finally, only this function
/// should be used for note nullifiers, never [`PrivateContext::push_nullifier`].
///
/// Failure to do these things can result in unprovable contexts, accidental deletion of notes, or double-spend
/// attacks.
pub fn push_nullifier_for_note_hash(&mut self, nullifier: Field, nullification_note_hash: Field) {
let nullifier_counter = self.next_counter();
notify_nullified_note(nullifier, nullified_note_hash, nullifier_counter);
self.nullifiers.push(Nullifier { value: nullifier, note_hash: nullified_note_hash }.count(nullifier_counter));
notify_nullified_note(nullifier, nullification_note_hash, nullifier_counter);
self.nullifiers.push(Nullifier { value: nullifier, note_hash: nullification_note_hash }.count(
nullifier_counter,
));
}

/// Returns the anchor block header - the historical block header that this private function is reading from.
Expand Down Expand Up @@ -607,7 +592,7 @@ impl PrivateContext {
/// network as a whole. For example, if a contract interaction sets include-by to some publicly-known value (e.g.
/// the time when a contract upgrades), then the wallet might wish to set an even lower one to avoid revealing that
/// this tx is interacting with said contract. Ideally, all wallets should standardize on an approach in order to
/// provide users with a large anonymity set -- although the exact approach
/// provide users with a large privacy set -- although the exact approach
/// will need to be discussed. Wallets that deviate from a standard might accidentally reveal which wallet each
/// transaction originates from.
///
Expand Down
Loading
Loading