Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f28eeb5
scoped capsule arrays
mverzilli Mar 16, 2026
8c82f1e
migration notes
mverzilli Mar 16, 2026
952a340
port implementations to scoped capsule arrays
mverzilli Mar 16, 2026
9fa7584
Merge branch 'martin/scoped-capsules' into martin/default-to-scoped-c…
mverzilli Mar 16, 2026
1e3b341
Merge branch 'martin/default-to-scoped-capsules' into martin/capsule-…
mverzilli Mar 16, 2026
1fed051
pass new capsule params through for txe
mverzilli Mar 16, 2026
15acc46
Merge branch 'martin/default-to-scoped-capsules' into martin/capsule-…
mverzilli Mar 16, 2026
d7979cf
fix offchain tests
mverzilli Mar 16, 2026
6d9044c
remove unused import
mverzilli Mar 16, 2026
207eb78
update oracle hash
mverzilli Mar 16, 2026
089a31f
use scoped capsule arrays in custom message handler
mverzilli Mar 16, 2026
9a8304a
fix compile error
mverzilli Mar 16, 2026
000a07d
Alice and Bob sides both coherent
mverzilli Mar 16, 2026
078189c
fix reorg test
mverzilli Mar 16, 2026
4d15105
Merge branch 'martin/scoped-capsules' into martin/default-to-scoped-c…
mverzilli Mar 16, 2026
87af2e4
Merge branch 'martin/default-to-scoped-capsules' into martin/capsule-…
mverzilli Mar 16, 2026
a9fdc1a
fix tests
mverzilli Mar 16, 2026
79b9dea
fix lint
mverzilli Mar 17, 2026
a8cc169
fix utility execution tests
mverzilli Mar 17, 2026
9806851
use keystore to provide scopes from txe
mverzilli Mar 17, 2026
0d2b04b
Merge remote-tracking branch 'origin/martin/scoped-capsules' into mar…
mverzilli Mar 18, 2026
83e7a7a
fix bad migration notes merge
mverzilli Mar 18, 2026
3e69b34
typo in migration notes
mverzilli Mar 18, 2026
b1a28d4
fix reorg flake
mverzilli Mar 18, 2026
59a5240
Merge remote-tracking branch 'origin/martin/default-to-scoped-capsule…
mverzilli Mar 18, 2026
99021e4
torch at_global_scope
mverzilli Mar 19, 2026
bbf8d88
typo
mverzilli Mar 19, 2026
e377325
remove scope optionality in message context resolution oracle
mverzilli Mar 19, 2026
b6841d9
update oracle version hash
mverzilli Mar 19, 2026
f055902
update oracle hash
mverzilli Mar 19, 2026
a190ad6
fix oracle unit tests
mverzilli Mar 19, 2026
87ed982
remove at_global_scope
mverzilli Mar 19, 2026
2080f7c
fix tests
mverzilli Mar 19, 2026
469a3d2
make scope non-optional for capsulearray
mverzilli Mar 19, 2026
942fc88
Merge remote-tracking branch 'origin/martin/scoped-capsules' into mar…
mverzilli Mar 23, 2026
3874e28
remove bad comment
mverzilli Mar 23, 2026
3ce1ad2
fix comment
mverzilli Mar 23, 2026
b40472c
remove unnecessary legacy oracle mappings
mverzilli Mar 23, 2026
c201638
remove recipient from msg context
mverzilli Mar 23, 2026
8ff53ae
make capsules always scoped
mverzilli Mar 23, 2026
92e5449
fix txe
mverzilli Mar 23, 2026
3b4df4f
fix sublib capsules
mverzilli Mar 23, 2026
fac1f2c
update oracle hash
mverzilli Mar 23, 2026
2339aff
remove redundant tests
mverzilli Mar 23, 2026
85a059e
merge from upstream
mverzilli Mar 24, 2026
37f28b3
fix test
mverzilli Mar 24, 2026
24ac5e7
fix test
mverzilli Mar 24, 2026
3b72bf2
revert vocabulary from scope to recipient in message processing contexts
mverzilli Mar 24, 2026
fc21e07
scope => recipient on LogService
mverzilli Mar 24, 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
35 changes: 35 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,41 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [Aztec.nr] Capsule operations now require a scope param

All capsule oracle functions (`store`, `load`, `delete`, `copy`) now take an additional `scope: Option<AztecAddress>`
parameter. This enables multiple independent namespaces within the same contract's capsule storage. If you need to keep
the previous (global) behavior, call said methods with `Option::none()` as scope.

`CapsuleArray::at` now requires a `scope: AztecAddress` argument that isolates the array's data to that scope. For the previous (global) behavior, use the new `CapsuleArray::at_global_scope` constructor.

**Migration:**

```diff
// Oracle functions now require a scope parameter
- capsules::store(contract_address, slot, value);
+ capsules::store(contract_address, slot, value, Option::none());

- capsules::load(contract_address, slot);
+ capsules::load(contract_address, slot, Option::none());

- capsules::delete(contract_address, slot);
+ capsules::delete(contract_address, slot, Option::none());

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

```diff
// CapsuleArray: use at_global_scope for the old behavior
- let array = CapsuleArray::at(contract_address, base_slot);
+ let array = CapsuleArray::at_global_scope(contract_address, base_slot);

// Or scope to a specific address for isolated namespaces
+ let array = CapsuleArray::at(contract_address, base_slot, scope_address);
```

**Impact**: All code using capsule oracles directly or `CapsuleArray::at` must be updated. Pass `Option::none()` to oracle functions for the previous global behavior, or replace `CapsuleArray::at` with `CapsuleArray::at_global_scope`.

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

Expand Down
159 changes: 137 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,30 @@ 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,
/// Optional scope for capsule isolation. When set, capsule operations are scoped to the given address, allowing
/// multiple independent namespaces within the same contract. `Option::none()` means global (unscoped).
scope: Option<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: Option::some(scope) }
}

/// Returns a CapsuleArray with global (unscoped) capsule access.
pub unconstrained fn at_global_scope(contract_address: AztecAddress, base_slot: Field) -> Self {
Self { contract_address, base_slot, scope: Option::none() }
}

/// 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 +45,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 +69,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 +87,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,6 +154,7 @@ impl<T> CapsuleArray<T> {
}

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

Expand All @@ -135,7 +166,7 @@ mod test {
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_global_scope(contract_address, SLOT);
assert_eq(array.len(), 0);
});
}
Expand All @@ -146,7 +177,7 @@ mod test {
env.private_context(|context| {
let contract_address = context.this_address();

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

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

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

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

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

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

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

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

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

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

array.push(7);
array.push(8);
Expand All @@ -243,7 +274,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_global_scope(contract_address, SLOT);

array.push(4);
array.push(5);
Expand All @@ -266,7 +297,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_global_scope(contract_address, SLOT);

array.push(4);
array.push(5);
Expand All @@ -289,7 +320,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_global_scope(contract_address, SLOT);

array.push(4);
array.push(5);
Expand All @@ -306,7 +337,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_global_scope(contract_address, SLOT);

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

#[test]
unconstrained fn scoped_push_and_get() {
Comment thread
mverzilli marked this conversation as resolved.
Outdated
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();
let scope = AztecAddress { inner: 0xcafe };
let array = CapsuleArray::at(contract_address, SLOT, scope);

array.push(42);
array.push(43);

assert_eq(array.len(), 2);
assert_eq(array.get(0), 42);
assert_eq(array.get(1), 43);
});
}

#[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);
});
}

#[test]
unconstrained fn scoped_and_global_are_isolated() {
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();
let scope = AztecAddress { inner: 0xddd };

let global_array = CapsuleArray::at_global_scope(contract_address, SLOT);
let scoped_array = CapsuleArray::at(contract_address, SLOT, scope);

global_array.push(1);
global_array.push(2);
scoped_array.push(100);

assert_eq(global_array.len(), 2);
assert_eq(global_array.get(0), 1);
assert_eq(global_array.get(1), 2);

assert_eq(scoped_array.len(), 1);
assert_eq(scoped_array.get(0), 100);
});
}

#[test]
unconstrained fn scoped_remove() {
let env = TestEnvironment::new();
env.private_context(|context| {
let contract_address = context.this_address();
let scope = AztecAddress { inner: 0xeee };
let array = CapsuleArray::at(contract_address, SLOT, scope);
Comment thread
mverzilli marked this conversation as resolved.
Outdated

array.push(7);
array.push(8);
array.push(9);

array.remove(1);

assert_eq(array.len(), 2);
assert_eq(array.get(0), 7);
assert_eq(array.get(1), 9);
});
}
}
12 changes: 10 additions & 2 deletions noir-projects/aztec-nr/aztec/src/macros/aztec.nr
Original file line number Diff line number Diff line change
Expand Up @@ -374,21 +374,24 @@ comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() -
/// 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 {
pub struct sync_state_parameters {}
pub struct sync_state_parameters {
pub scope: aztec::protocol::address::AztecAddress,
}

#[abi(functions)]
pub struct sync_state_abi {
parameters: sync_state_parameters,
}

#[aztec::macros::internals_functions_generation::abi_attributes::abi_utility]
unconstrained fn sync_state() {
unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) {
let address = aztec::context::UtilityContext::new().this_address();
aztec::messages::discovery::do_sync_state(
address,
_compute_note_hash_and_nullifier,
$process_custom_message_option,
$offchain_inbox_sync_option,
scope,
);
}
}
Expand All @@ -400,6 +403,7 @@ comptime fn generate_process_message(process_custom_message_option: Quoted) -> Q
pub struct process_message_parameters {
pub message_ciphertext: BoundedVec<Field, aztec::messages::encoding::MESSAGE_CIPHERTEXT_LEN>,
pub message_context: aztec::messages::processing::MessageContext,
pub scope: aztec::protocol::address::AztecAddress,
}

#[abi(functions)]
Expand All @@ -411,6 +415,7 @@ comptime fn generate_process_message(process_custom_message_option: Quoted) -> Q
unconstrained fn process_message(
message_ciphertext: BoundedVec<Field, aztec::messages::encoding::MESSAGE_CIPHERTEXT_LEN>,
message_context: aztec::messages::processing::MessageContext,
scope: aztec::protocol::address::AztecAddress,
) {
let address = aztec::context::UtilityContext::new().this_address();

Expand All @@ -420,6 +425,7 @@ comptime fn generate_process_message(process_custom_message_option: Quoted) -> Q
$process_custom_message_option,
message_ciphertext,
message_context,
scope,
);

// At this point, the note is pending validation and storage in the database. We must call
Expand Down Expand Up @@ -448,6 +454,8 @@ comptime fn generate_offchain_receive() -> Quoted {

/// Receives offchain messages into this contract's offchain inbox for subsequent processing.
///
/// Each message is routed to the inbox scoped to its `recipient` field.
Comment thread
mverzilli marked this conversation as resolved.
///
/// For more details, see `aztec::messages::processing::offchain::receive`.
///
/// This function is automatically injected by the `#[aztec]` macro.
Expand Down
Loading
Loading