Skip to content

Commit 5db7d51

Browse files
authored
feat: merge-train/fairies-v5 (#24891)
BEGIN_COMMIT_OVERRIDE docs(aztec-nr): document partial note completion trust model (#24816) refactor(aztec-nr): shared no-op sync handler for stateless contracts (#24844) fix: dont panic on note msgs on contracts with no notes (#24852) docs(noir-contracts): document standard-contract re-pin consequences (#24890) fix: change init and single claim nullif to incl owner address, add testing utilities (#24892) END_COMMIT_OVERRIDE
2 parents f245aed + c3a2a85 commit 5db7d51

40 files changed

Lines changed: 521 additions & 111 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ When staging files, prefer `git add -u` or name specific files rather than `git
5757
Never bulk-update lockfiles (`Cargo.lock`, `yarn.lock`). Use targeted updates only: `cargo update --precise <version> --package <name>` for Rust, and `yarn up <package>@<version>` in the relevant workspace for TypeScript. Bulk updates drag in unrelated transitive changes that make review impossible and frequently break reproducibility.
5858
</lockfile_discipline>
5959

60+
<standard_contract_repin>
61+
Never run `noir-projects/noir-contracts/bootstrap.sh pin-standard-build` on your own initiative. The pin exists so ordinary source or bytecode changes do NOT move the standard contracts' canonical addresses, and CI does not fail when the bytecode drifts. A re-pin is a deliberate redeploy decision for a human to make: if a change seems to need one, leave the pin, rebuild against it, and ask. See the comment on `pin-standard-build` for why re-pinning is breaking.
62+
</standard_contract_repin>
63+
6064
</git_workflow>
6165

6266
<code_formatting>

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12+
### [Aztec.nr] Canonical HandshakeRegistry re-pinned at a new address
13+
14+
The canonical `HandshakeRegistry` has been re-pinned so that it includes the owner's address in its `PrivateMutable` initialization nullifiers, keeping the handshake state of accounts that share keys independent. The registry moves to a new address. Handshakes established with the previous registry instance are not visible to the new one and must be re-established. The other standard contracts keep their addresses.
15+
1216
### [Aztec.nr] Note property selectors are typed and use packed-layout indices
1317

1418
The selectors in the generated `properties()` used the field's position in the note struct declaration, which pointed at the wrong packed field for any note with an earlier field packing to more than one `Field` (a `Point`, an array, a nested struct). Selector indices are now the field's offset in the note's packed representation, so `select`/`sort` criteria constrain the field they name.

noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ pub(crate) comptime fn generate_contract_library_methods_compute_note_hash_and_n
4444

4545
comptime fn generate_contract_library_method_compute_note_hash() -> Quoted {
4646
if NOTES.len() == 0 {
47-
// Contracts with no notes still implement this function to avoid having special-casing, the implementation
48-
// simply throws immediately.
47+
// Contracts with no notes still implement this function to avoid having special-casing. Since the contract
48+
// declares no note types there is never a note to hash, so we return `Option::none()`. This will cause any
49+
// messages related to (non-existing) notes to be simply skipped, mirroring the unknown-note-type branch of the
50+
// with-notes implementation.
4951
quote {
50-
/// This contract does not use private notes, so this function should never be called as it will unconditionally fail.
52+
/// This contract does not use private notes, so this function always returns `Option::none()`.
5153
///
5254
/// This function is automatically injected by the `#[aztec]` macro.
5355
#[contract_library_method]
@@ -59,7 +61,7 @@ comptime fn generate_contract_library_method_compute_note_hash() -> Quoted {
5961
_contract_address: aztec::protocol::address::AztecAddress,
6062
_randomness: Field,
6163
) -> Option<Field> {
62-
panic(f"This contract does not use private notes")
64+
Option::none()
6365
}
6466
}
6567
} else {
@@ -155,10 +157,12 @@ comptime fn generate_contract_library_method_compute_note_hash() -> Quoted {
155157

156158
comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted {
157159
if NOTES.len() == 0 {
158-
// Contracts with no notes still implement this function to avoid having special-casing, the implementation
159-
// simply throws immediately.
160+
// Contracts with no notes still implement this function to avoid having special-casing. Since the contract
161+
// declares no note types there is never a note to nullify, so we return `Option::none()`. This will cause any
162+
// messages related to (non-existing) notes to be simply skipped, mirroring the unknown-note-type branch of the
163+
// with-notes implementation.
160164
quote {
161-
/// This contract does not use private notes, so this function should never be called as it will unconditionally fail.
165+
/// This contract does not use private notes, so this function always returns `Option::none()`.
162166
///
163167
/// This function is automatically injected by the `#[aztec]` macro.
164168
#[contract_library_method]
@@ -171,7 +175,7 @@ comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted
171175
_contract_address: aztec::protocol::address::AztecAddress,
172176
_randomness: Field,
173177
) -> Option<Field> {
174-
panic(f"This contract does not use private notes")
178+
Option::none()
175179
}
176180
}
177181
} else {

noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,19 @@ pub unconstrained fn do_sync_state_without_handshake_discovery(
174174
);
175175
}
176176

177+
/// A [`CustomSyncHandler`] that skips state synchronization entirely.
178+
///
179+
/// For contracts that hold no private state and emit no messages, there is nothing to discover: syncing would only
180+
/// perform unnecessary RPC calls.
181+
pub unconstrained fn do_sync_state_no_op(
182+
_contract_address: AztecAddress,
183+
_compute_note_hash: ComputeNoteHash,
184+
_compute_note_nullifier: ComputeNoteNullifier,
185+
_process_custom_message: Option<CustomMessageHandler>,
186+
_offchain_inbox_sync: Option<OffchainInboxSync>,
187+
_scope: AztecAddress,
188+
) {}
189+
177190
unconstrained fn sync_state_with_secrets(
178191
contract_address: AztecAddress,
179192
compute_note_hash: ComputeNoteHash,

noir-projects/aztec-nr/aztec/src/standard_addresses.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie
1414
);
1515

1616
pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
17-
0x086c3c67589e1141c70ed0ed8ae324c51d3bf7c5637043fd84c424ffb625831d,
17+
0x06127814dca78709650de6629637194f7381d2e05b35eb9d53a4746636c9aa9d,
1818
);

noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ use crate::{
1212
};
1313

1414
use crate::protocol::{
15-
address::AztecAddress, constants::DOM_SEP__INITIALIZATION_NULLIFIER, hash::poseidon2_hash_with_separator,
16-
traits::Packable,
15+
address::AztecAddress,
16+
constants::DOM_SEP__INITIALIZATION_NULLIFIER,
17+
hash::poseidon2_hash_with_separator,
18+
traits::{Packable, ToField},
1719
};
1820

1921
mod test;
@@ -67,7 +69,7 @@ impl<Note, Context> PrivateImmutable<Note, Context> {
6769
/// Computes the initialization nullifier using the provided secret.
6870
fn compute_initialization_nullifier(self, secret: Field) -> Field {
6971
poseidon2_hash_with_separator(
70-
[self.storage_slot, secret],
72+
[self.storage_slot, self.owner.to_field(), secret],
7173
DOM_SEP__INITIALIZATION_NULLIFIER,
7274
)
7375
}

noir-projects/aztec-nr/aztec/src/state_vars/private_immutable/test.nr

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::{
22
context::{PrivateContext, UtilityContext},
3+
keys::getters::get_public_keys,
34
state_vars::{OwnedStateVariable, private_immutable::PrivateImmutable},
4-
test::{helpers::test_environment::TestEnvironment, mocks::mock_note::MockNote},
5+
test::{helpers::test_environment::{CreateAccountOptions, TestEnvironment}, mocks::mock_note::MockNote},
56
};
67
use crate::protocol::address::AztecAddress;
78

@@ -197,6 +198,22 @@ unconstrained fn initialize_twice_same_tx() {
197198
});
198199
}
199200

201+
#[test]
202+
unconstrained fn shared_keys_distinct_addresses_have_distinct_initialization_nullifiers() {
203+
let mut env = TestEnvironment::new();
204+
205+
let owner_a = env.create_light_account_opts(CreateAccountOptions::new().with_secret(42));
206+
let owner_b = env.create_light_account_opts(CreateAccountOptions::new().with_secret(42));
207+
208+
assert(owner_a != owner_b);
209+
env.utility_context(|_| { assert_eq(get_public_keys(owner_a).npk_m_hash, get_public_keys(owner_b).npk_m_hash); });
210+
211+
let nullifier_a = env.private_context(|context| in_private(context, owner_a).get_initialization_nullifier());
212+
let nullifier_b = env.private_context(|context| in_private(context, owner_b).get_initialization_nullifier());
213+
214+
assert(nullifier_a != nullifier_b);
215+
}
216+
200217
#[test(should_fail_with = "already present")]
201218
unconstrained fn initialize_twice_other_tx() {
202219
let mut env = TestEnvironment::new();

noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ use crate::{
1212
};
1313

1414
use crate::protocol::{
15-
address::AztecAddress, constants::DOM_SEP__INITIALIZATION_NULLIFIER, hash::poseidon2_hash_with_separator,
16-
traits::Packable,
15+
address::AztecAddress,
16+
constants::DOM_SEP__INITIALIZATION_NULLIFIER,
17+
hash::poseidon2_hash_with_separator,
18+
traits::{Packable, ToField},
1719
};
1820

1921
mod test;
@@ -86,7 +88,7 @@ impl<Note, Context> PrivateMutable<Note, Context> {
8688
/// Computes the initialization nullifier using the provided secret.
8789
fn compute_initialization_nullifier(self, secret: Field) -> Field {
8890
poseidon2_hash_with_separator(
89-
[self.storage_slot, secret],
91+
[self.storage_slot, self.owner.to_field(), secret],
9092
DOM_SEP__INITIALIZATION_NULLIFIER,
9193
)
9294
}

noir-projects/aztec-nr/aztec/src/state_vars/private_mutable/test.nr

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::{
22
context::{PrivateContext, UtilityContext},
3+
keys::getters::get_public_keys,
34
state_vars::{OwnedStateVariable, private_mutable::PrivateMutable},
4-
test::{helpers::test_environment::TestEnvironment, mocks::mock_note::MockNote},
5+
test::{helpers::test_environment::{CreateAccountOptions, TestEnvironment}, mocks::mock_note::MockNote},
56
};
67
use crate::protocol::address::AztecAddress;
78

@@ -339,6 +340,22 @@ unconstrained fn initialize_or_replace_initialized_settled() {
339340
});
340341
}
341342

343+
#[test]
344+
unconstrained fn shared_keys_distinct_addresses_have_distinct_initialization_nullifiers() {
345+
let mut env = TestEnvironment::new();
346+
347+
let owner_a = env.create_light_account_opts(CreateAccountOptions::new().with_secret(42));
348+
let owner_b = env.create_light_account_opts(CreateAccountOptions::new().with_secret(42));
349+
350+
assert(owner_a != owner_b);
351+
env.utility_context(|_| { assert_eq(get_public_keys(owner_a).npk_m_hash, get_public_keys(owner_b).npk_m_hash); });
352+
353+
let nullifier_a = env.private_context(|context| in_private(context, owner_a).get_initialization_nullifier());
354+
let nullifier_b = env.private_context(|context| in_private(context, owner_b).get_initialization_nullifier());
355+
356+
assert(nullifier_a != nullifier_b);
357+
}
358+
342359
#[test]
343360
unconstrained fn get_replacement_note_has_unique_randomness() {
344361
let mut env = TestEnvironment::new();

noir-projects/aztec-nr/aztec/src/state_vars/single_use_claim.nr

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::protocol::{
22
address::AztecAddress, constants::DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, hash::poseidon2_hash_with_separator,
3+
traits::ToField,
34
};
45

56
use crate::{
@@ -82,7 +83,7 @@ impl<Context> SingleUseClaim<Context> {
8283
/// [`SingleUseClaim::assert_claimed`] and [`SingleUseClaim::has_claimed`] to coherently write and read state.
8384
fn compute_nullifier(self, owner_nhk_app: Field) -> Field {
8485
poseidon2_hash_with_separator(
85-
[owner_nhk_app, self.storage_slot],
86+
[owner_nhk_app, self.storage_slot, self.owner.to_field()],
8687
DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER,
8788
)
8889
}

0 commit comments

Comments
 (0)