Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cc10c2a
feat!: scoped capsules (backport #21533) (#21986)
AztecBot Mar 25, 2026
678c3a2
feat(aztec-nr): add initialization check to utility functions (#21751)
nchamo Mar 25, 2026
a2cc6b0
refactor(aztec-nr): remove storage from init_test_contract (#21996)
nchamo Mar 25, 2026
c90779c
fix(p2p): check peer rate limit before global to prevent quota starva…
spypsy Mar 25, 2026
a1e04d7
chore: remove claude file (#22012)
nchamo Mar 25, 2026
8414af4
cherry-pick: refactor!: more consistent oracle names (#22018)
mverzilli Mar 25, 2026
33770af
fix: resolve cherry-pick conflicts
AztecBot Mar 25, 2026
20e2358
fix: update ORACLE_INTERFACE_HASH and bump ORACLE_VERSION to 21
AztecBot Mar 25, 2026
c4475f3
cherry-pick: feat(aztec-nr)!: app-silo getSharedSecret oracle (#22020)
nchamo Mar 25, 2026
3c924ba
fix: resolve cherry-pick conflicts in oracle_version.ts
AztecBot Mar 25, 2026
2274da9
fix: regenerate oracle interface hash for v4-next
AztecBot Mar 25, 2026
e7f94df
fix: regenerate constants after cherry-pick
AztecBot Mar 25, 2026
d650d2c
Revert "fix: regenerate constants after cherry-pick"
AztecBot Mar 25, 2026
40a729b
fix: disallow infinite pubkeys (#22026)
nventuro Mar 25, 2026
72202a0
fix: noir formatter line length in initialization_utils.nr doc comment
AztecBot Mar 25, 2026
5ae7599
fix: run nargo fmt on aztec-nr (pre-existing formatting issue)
AztecBot Mar 25, 2026
f779e1a
chore: mark e2e_offchain_payment reorg test as flaky
AztecBot Mar 25, 2026
13b53c2
feat(aztec-nr)!: app-silo getSharedSecret oracle (#22020) [v4-next ba…
nventuro Mar 25, 2026
89fda7d
cherry-pick: feat: aztecnr log prefixing (PR #22027)
AztecBot Mar 25, 2026
916fc4e
fix: resolve cherry-pick conflicts in aztec.sh
AztecBot Mar 25, 2026
36ebdaa
chore: revert parameter renames to preserve oracle interface hash
AztecBot Mar 25, 2026
a0cbbb6
fix: merge backport-to-v4-next-staging, resolve oracle hash conflict
AztecBot Mar 25, 2026
df868e6
refactor!: more consistent oracle names (#22018) [v4-next backport] (…
mverzilli Mar 25, 2026
5a5b915
feat: aztecnr log prefixing (#22027) [v4-next backport] (#22029)
nchamo Mar 25, 2026
9782c0a
cherry-pick: feat(aztec-nr)!: domain-separated tags on log emission (…
nchamo Mar 26, 2026
df9eda1
fix: resolve cherry-pick conflicts
AztecBot Mar 26, 2026
3785717
feat(aztec-nr)!: domain-separated tags on log emission (#21910) [v4-n…
nchamo Mar 26, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ docs/docs/protocol-specs/public-vm/gen/
__pycache__

*.local.md
.claude/settings.local.json
5 changes: 5 additions & 0 deletions .test_patterns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ tests:
owners:
- *palla

- regex: "src/e2e_offchain_payment\\.test\\.ts"
error_regex: "reprocesses an offchain-delivered payment after an L1 reorg"
owners:
- *martin

- regex: "bb-micro-bench/wasm/chonk build-wasm-threads/bin/chonk_bench"
error_regex: "core dumped"
owners:
Expand Down
111 changes: 110 additions & 1 deletion docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,115 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [Aztec.nr] Domain-separated tags on log emission

All logs emitted through the Aztec.nr framework now include a domain-separated tag at `fields[0]`. Each log category uses its own domain separator via `compute_log_tag(raw_tag, dom_sep)`:

- **Events** (`DOM_SEP__EVENT_LOG_TAG`): the event type ID is the raw tag.
- **Message delivery** (`DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG`): the discovery tag is the raw tag.
- **Partial note completion logs** (`DOM_SEP__NOTE_COMPLETION_LOG_TAG`): the partial note's `commitment` field is the raw tag.

The low-level emit methods now take `tag` as an explicit first parameter and have been renamed with an `_unsafe` suffix. Previously the tag was included as `log[0]` — it has now been extracted into its own parameter, and `log` no longer contains it:

```diff
- context.emit_private_log(log, length);
+ context.emit_private_log_unsafe(tag, log, length);
- context.emit_raw_note_log(log, length, note_hash_counter);
+ context.emit_raw_note_log_unsafe(tag, log, length, note_hash_counter);
- context.emit_public_log(log);
+ context.emit_public_log_unsafe(tag, log);
```

Prefer the higher-level APIs (`emit` for events, `MessageDelivery` for messages) which handle tagging automatically.

### [Aztec.nr] Public events no longer include the event type selector at the end of the payload

`emit_event_in_public` previously appended the event type selector as the last field. It now prepends a domain-separated tag at `fields[0]` instead. The payload after the tag contains only the serialized event fields.

If you were reading public event directly from node logs (i.e. via `node.getPublicLogs` and not via `wallet.getPublicEvents`), update your parsing:

```diff
- // Old: fields = [serialized_event..., event_type_selector]
- const selector = EventSelector.fromField(fields[fields.length - 1]);
- const event = decodeFromAbi([abiType], fields);
+ // New: fields = [domain_separated_tag, serialized_event...]
+ const eventFields = log.getEmittedFieldsWithoutTag();
+ const event = decodeFromAbi([abiType], eventFields);
```

### [Aztec.nr] Capsule operations are now addressed by scope

All capsule operations (`store`, `load`, `delete`, `copy`) and `CapsuleArray` now require a `scope: AztecAddress` parameter. This scopes capsule storage by address, providing isolation between different accounts within the same PXE.

Contracts that use `CapsuleArray` directly also need to update.

**Migration:**

```diff
- let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, slot);
+ let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, slot, scope);
```

The low-level capsule functions are similarly affected:

```diff
- capsules::store(contract_address, slot, value);
+ capsules::store(contract_address, slot, value, scope);

- capsules::load(contract_address, slot);
+ capsules::load(contract_address, slot, scope);

- capsules::delete(contract_address, slot);
+ capsules::delete(contract_address, slot, scope);

- capsules::copy(contract_address, src_slot, dst_slot, num_entries);
+ capsules::copy(contract_address, src_slot, dst_slot, num_entries, scope);
```

If you need to stick the old, scope-less behavior, and you are really sure that that's what you need to use, you can use `scope = AztecAddress::zero()`.

### [Aztec.nr] `process_message` utility function removed

The auto-generated `process_message` utility function has been removed. If you need to deliver offchain messages (messages not broadcast via onchain logs), use the `offchain_receive` utility function instead. This function is automatically injected by the `#[aztec]` macro and accepts messages into a persistent inbox scoped by recipient. These messages are then picked up and processed during `sync_state`.

**Impact**: Contracts that explicitly called `process_message` must switch to delivering messages via `offchain_receive` and letting `sync_state` handle processing.

### [Aztec.nr] `CustomMessageHandler` type signature changed

The `CustomMessageHandler` function type now receives an additional `scope: AztecAddress` parameter:

```diff
type CustomMessageHandler = unconstrained fn(
AztecAddress, // contract_address
u64, // msg_type_id
u64, // msg_metadata
BoundedVec<Field, MAX_MESSAGE_CONTENT_LEN>, // msg_content
MessageContext, // message_context
+ AztecAddress, // scope
);
```

**Impact**: Contracts that implement a custom message handler must update the function signature.
### [aztec.js] `DeployMethod.send()` always returns `{ contract, receipt, instance }`

The `returnReceipt` option in deploy wait options has been removed. `DeployMethod.send()` now always returns an object with `contract`, `receipt`, and `instance` at the top level, provided the user waits for the transaction to be included.

The `DeployTxReceipt` and `DeployWaitOptions` types have been removed.

**Migration:**

```diff
- const {
- receipt: { contract, instance },
- } = await MyContract.deploy(wallet, ...args).send({
- from: address,
- wait: { returnReceipt: true },
- });

+ const { contract, instance } = await MyContract.deploy(wallet, ...args).send({
+ from: address,
+ });
```
### [aztec.js] `isContractInitialized` is now `initializationStatus` tri-state enum

`ContractMetadata.isContractInitialized` has been renamed to `ContractMetadata.initializationStatus` and changed from `boolean | undefined` to a `ContractInitializationStatus` enum with values `INITIALIZED`, `UNINITIALIZED`, and `UNKNOWN`.
Expand Down Expand Up @@ -97,7 +206,7 @@ Most contracts are not affected, as the macro-generated `sync_state` and `proces
**Migration:**

```diff
attempt_note_discovery(
attempt_note_discovery(
contract_address,
tx_hash,
unique_note_hashes_in_tx,
Expand Down
95 changes: 73 additions & 22 deletions noir-projects/aztec-nr/aztec/src/capsules/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,25 @@ pub struct CapsuleArray<T> {
/// after the base slot. For example, with base slot 5: the length is at slot 5, the first element (index 0) is at
/// slot 6, the second element (index 1) is at slot 7, and so on.
base_slot: Field,
/// Scope for capsule isolation. Capsule operations are scoped to the given address, allowing multiple independent
/// namespaces within the same contract.
scope: AztecAddress,
}

impl<T> CapsuleArray<T> {
/// Returns a CapsuleArray connected to a contract's capsules at a base slot. Array elements are stored in
/// contiguous slots following the base slot, so there should be sufficient space between array base slots to
/// accommodate elements. A reasonable strategy is to make the base slot a hash of a unique value.
pub unconstrained fn at(contract_address: AztecAddress, base_slot: Field) -> Self {
Self { contract_address, base_slot }
/// Returns a CapsuleArray scoped to a specific address.
///
/// Array elements are stored in contiguous slots
/// following the base slot, so there should be sufficient space between array base slots to accommodate elements.
/// A reasonable strategy is to make the base slot a hash of a unique value.
pub unconstrained fn at(contract_address: AztecAddress, base_slot: Field, scope: AztecAddress) -> Self {
Self { contract_address, base_slot, scope }
}

/// Returns the number of elements stored in the array.
pub unconstrained fn len(self) -> u32 {
// An uninitialized array defaults to a length of 0.
capsules::load(self.contract_address, self.base_slot).unwrap_or(0) as u32
capsules::load(self.contract_address, self.base_slot, self.scope).unwrap_or(0) as u32
}

/// Stores a value at the end of the array.
Expand All @@ -35,11 +40,21 @@ impl<T> CapsuleArray<T> {

// The slot corresponding to the index `current_length` is the first slot immediately after the end of the
// array, which is where we want to place the new value.
capsules::store(self.contract_address, self.slot_at(current_length), value);
capsules::store(
self.contract_address,
self.slot_at(current_length),
value,
self.scope,
);

// Then we simply update the length.
let new_length = current_length + 1;
capsules::store(self.contract_address, self.base_slot, new_length);
capsules::store(
self.contract_address,
self.base_slot,
new_length,
self.scope,
);
}

/// Retrieves the value stored in the array at `index`. Throws if the index is out of bounds.
Expand All @@ -49,7 +64,7 @@ impl<T> CapsuleArray<T> {
{
assert(index < self.len(), "Attempted to read past the length of a CapsuleArray");

capsules::load(self.contract_address, self.slot_at(index)).unwrap()
capsules::load(self.contract_address, self.slot_at(index), self.scope).unwrap()
}

/// Deletes the value stored in the array at `index`. Throws if the index is out of bounds.
Expand All @@ -67,13 +82,23 @@ impl<T> CapsuleArray<T> {
self.slot_at(index + 1),
self.slot_at(index),
current_length - index - 1,
self.scope,
);
}

// We can now delete the last element (which has either been copied to the slot immediately before it, or was
// the element we meant to delete in the first place) and update the length.
capsules::delete(self.contract_address, self.slot_at(current_length - 1));
capsules::store(self.contract_address, self.base_slot, current_length - 1);
capsules::delete(
self.contract_address,
self.slot_at(current_length - 1),
self.scope,
);
capsules::store(
self.contract_address,
self.base_slot,
current_length - 1,
self.scope,
);
}

/// Calls a function on each element of the array.
Expand Down Expand Up @@ -124,18 +149,20 @@ impl<T> CapsuleArray<T> {
}

mod test {
use crate::protocol::address::AztecAddress;
use crate::test::helpers::test_environment::TestEnvironment;
use super::CapsuleArray;

global SLOT: Field = 1230;
global SCOPE: AztecAddress = AztecAddress { inner: 0xface };

#[test]
unconstrained fn empty_array() {
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();

let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, SLOT);
let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, SLOT, SCOPE);
assert_eq(array.len(), 0);
});
}
Expand All @@ -146,7 +173,7 @@ mod test {
env.private_context(|context| {
let contract_address = context.this_address();

let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);
let _: Field = array.get(0);
});
}
Expand All @@ -157,7 +184,7 @@ mod test {
env.private_context(|context| {
let contract_address = context.this_address();

let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);
array.push(5);

assert_eq(array.len(), 1);
Expand All @@ -171,7 +198,7 @@ mod test {
env.private_context(|context| {
let contract_address = context.this_address();

let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);
array.push(5);

let _ = array.get(1);
Expand All @@ -184,7 +211,7 @@ mod test {
env.private_context(|context| {
let contract_address = context.this_address();

let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);

array.push(5);
array.remove(0);
Expand All @@ -199,7 +226,7 @@ mod test {
env.private_context(|context| {
let contract_address = context.this_address();

let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);

array.push(7);
array.push(8);
Expand All @@ -224,7 +251,7 @@ mod test {
env.private_context(|context| {
let contract_address = context.this_address();

let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);

array.push(7);
array.push(8);
Expand All @@ -243,7 +270,7 @@ mod test {
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();
let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);

array.push(4);
array.push(5);
Expand All @@ -266,7 +293,7 @@ mod test {
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();
let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);

array.push(4);
array.push(5);
Expand All @@ -289,7 +316,7 @@ mod test {
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();
let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);

array.push(4);
array.push(5);
Expand All @@ -306,7 +333,7 @@ mod test {
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();
let array = CapsuleArray::at(contract_address, SLOT);
let array = CapsuleArray::at(contract_address, SLOT, SCOPE);

array.push(4);
array.push(5);
Expand All @@ -321,4 +348,28 @@ mod test {
assert_eq(mock.times_called(), 0);
});
}

#[test]
unconstrained fn different_scopes_are_isolated() {
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();
let scope_a = AztecAddress { inner: 0xaaa };
let scope_b = AztecAddress { inner: 0xbbb };

let array_a = CapsuleArray::at(contract_address, SLOT, scope_a);
let array_b = CapsuleArray::at(contract_address, SLOT, scope_b);

array_a.push(10);
array_a.push(20);
array_b.push(99);

assert_eq(array_a.len(), 2);
assert_eq(array_a.get(0), 10);
assert_eq(array_a.get(1), 20);

assert_eq(array_b.len(), 1);
assert_eq(array_b.get(0), 99);
});
}
}
Loading
Loading