Skip to content

Commit f66808c

Browse files
authored
fix: change init and single claim nullif to incl owner address, add testing utilities (#24892)
Reimplementation of #24837 - same nullifier fix, this time extending `TestEnvironment` so that we can test the fix works as intended.
1 parent 7e29a2d commit f66808c

15 files changed

Lines changed: 187 additions & 27 deletions

File tree

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
}

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::{
22
context::{PrivateContext, UtilityContext},
3+
keys::getters::get_public_keys,
34
oracle::random::random,
45
state_vars::{OwnedStateVariable, SingleUseClaim},
56
};
67
use crate::protocol::{address::AztecAddress, traits::FromField};
7-
use crate::test::helpers::test_environment::TestEnvironment;
8+
use crate::test::helpers::test_environment::{CreateAccountOptions, TestEnvironment};
89

910
global STORAGE_SLOT: Field = 17;
1011

@@ -134,6 +135,24 @@ unconstrained fn claim_two_claim_ids_different_state_vars() {
134135
});
135136
}
136137

138+
#[test]
139+
unconstrained fn claim_by_owner_does_not_claim_for_shared_key_distinct_address_owner() {
140+
let mut env = TestEnvironment::new();
141+
142+
let owner_a = env.create_light_account_opts(CreateAccountOptions::new().with_secret(42));
143+
let owner_b = env.create_light_account_opts(CreateAccountOptions::new().with_secret(42));
144+
145+
assert(owner_a != owner_b);
146+
env.utility_context(|_| { assert_eq(get_public_keys(owner_a).npk_m_hash, get_public_keys(owner_b).npk_m_hash); });
147+
148+
env.private_context(|context| { in_private(context, owner_a).claim(); });
149+
150+
env.utility_context(|context| {
151+
assert(in_utility(context, STORAGE_SLOT, owner_a).has_claimed());
152+
assert(!in_utility(context, STORAGE_SLOT, owner_b).has_claimed());
153+
});
154+
}
155+
137156
#[test(should_fail_with = "Public keys not registered for account")]
138157
unconstrained fn claim_owner_unknown_public_keys() {
139158
let env = TestEnvironment::new();

noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ pub struct TestEnvironment {
8080
// results in TXE being able to maximize cache usage and not have to recompute account addresses and contract
8181
// artifacts, which are relatively expensive operations.
8282
light_account_secret: Counter,
83+
// The partial address used for light account creation. Kept separate from the secret (which derives the keys) so
84+
// that light accounts with the same keys but different addresses can be created via `create_light_account_opts`.
85+
light_account_partial_address: Counter,
8386
contract_account_secret: Counter,
8487
contract_deployment_secret: Counter,
8588
contract_deployment_salt: Counter,
@@ -522,6 +525,44 @@ impl DeployOptions {
522525
}
523526
}
524527

528+
/// Configuration values for [`TestEnvironment::create_light_account_opts`]. Meant to be used by calling `new` and then
529+
/// chaining methods setting each value, e.g.:
530+
/// ```noir
531+
/// env.create_light_account_opts(CreateAccountOptions::new().with_secret(42).with_partial_address(1));
532+
/// ```
533+
///
534+
/// The account address is derived from both the public keys (which come from the secret) and the partial address.
535+
/// Setting the same secret with different partial addresses produces accounts that share master keys but have distinct
536+
/// addresses.
537+
pub struct CreateAccountOptions {
538+
secret: Option<Field>,
539+
partial_address: Option<Field>,
540+
}
541+
542+
impl CreateAccountOptions {
543+
/// Creates a new `CreateAccountOptions` with default values, i.e. the same as if using the `create_light_account`
544+
/// method instead of `create_light_account_opts`. Use the `with_secret` and `with_partial_address` methods to set
545+
/// the desired configuration values.
546+
pub fn new() -> Self {
547+
Self { secret: Option::none(), partial_address: Option::none() }
548+
}
549+
550+
/// Sets the secret used for key derivation. The secret affects the account's public keys and therefore its address:
551+
/// accounts created with different secrets will have different keys and addresses.
552+
pub fn with_secret(&mut self, secret: Field) -> Self {
553+
self.secret = Option::some(secret);
554+
*self
555+
}
556+
557+
/// Sets the partial address used for address derivation. The partial address affects the resulting account address
558+
/// but not its keys: accounts created with the same secret but different partial addresses share master keys while
559+
/// having different addresses.
560+
pub fn with_partial_address(&mut self, partial_address: Field) -> Self {
561+
self.partial_address = Option::some(partial_address);
562+
*self
563+
}
564+
}
565+
525566
/// Configuration values for [`TestEnvironment::call_public_opts`]. Meant to be used by calling `new` and then chaining
526567
/// methods setting each value, e.g.:
527568
/// ```noir
@@ -669,6 +710,10 @@ impl TestEnvironment {
669710
// secrets (e.g., Token1 with deployment secret=1 and liquidity_provider with account
670711
// secret=1), causing key collisions since keys are derived from the secret alone.
671712
light_account_secret: Counter::new(),
713+
// The partial address counter does not need an offset for the same reason the salt counter doesn't: keys
714+
// are derived from the secret alone, so a collision between a partial address and a secret value has no
715+
// effect on key derivation.
716+
light_account_partial_address: Counter::new(),
672717
contract_account_secret: Counter::new_with_offset(1_000),
673718
contract_deployment_secret: Counter::new_with_offset(2_000),
674719
// The salt counter does not need an offset because keys (derived from secret) and salt are unrelated: a
@@ -988,8 +1033,24 @@ impl TestEnvironment {
9881033
/// Each call to `create_light_account` will return a different address, and so it can be called repeatedly to
9891034
/// generate multiple addresses. These addresses are also different from the ones that `create_contract_account`
9901035
/// returns, and so these two functions can be mixed and match to create a set of unique accounts.
1036+
///
1037+
/// See [`create_light_account_opts`](TestEnvironment::create_light_account_opts) for a variant that allows
1038+
/// controlling the account's secret and partial address, e.g. to create accounts that share keys but have
1039+
/// different addresses.
9911040
pub unconstrained fn create_light_account(&mut self) -> AztecAddress {
992-
let test_account = txe_oracles::create_account(self.light_account_secret.next());
1041+
self.create_light_account_opts(CreateAccountOptions::new())
1042+
}
1043+
1044+
/// Variant of `create_light_account` which allows specifying configuration values via `CreateAccountOptions`, such
1045+
/// as a custom secret or partial address. If not specified, the secret and partial address default to values from
1046+
/// internal counters.
1047+
///
1048+
/// Setting the same secret with different partial addresses produces accounts that share master keys but have
1049+
/// distinct addresses.
1050+
pub unconstrained fn create_light_account_opts(&mut self, opts: CreateAccountOptions) -> AztecAddress {
1051+
let secret = opts.secret.unwrap_or_else(|| self.light_account_secret.next());
1052+
let partial_address = opts.partial_address.unwrap_or_else(|| self.light_account_partial_address.next());
1053+
let test_account = txe_oracles::create_account(secret, partial_address);
9931054
test_account.address
9941055
}
9951056

noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/accounts.nr

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::{
22
authwit::auth::{compute_authwit_message_hash, IS_VALID_SELECTOR},
33
context::calls::PrivateStaticCall,
44
keys::getters::get_public_keys,
5-
test::helpers::{test_environment::TestEnvironment, txe_oracles},
5+
test::helpers::{test_environment::{CreateAccountOptions, TestEnvironment}, txe_oracles},
66
};
77
use crate::protocol::abis::function_selector::FunctionSelector;
88

@@ -16,6 +16,45 @@ unconstrained fn create_light_account_does_not_repeat_accounts() {
1616
assert(first_account != second_account);
1717
}
1818

19+
#[test]
20+
unconstrained fn create_light_account_opts_same_secret_and_partial_address_repeats_address() {
21+
let mut env = TestEnvironment::new();
22+
23+
let first_account =
24+
env.create_light_account_opts(CreateAccountOptions::new().with_secret(42).with_partial_address(1));
25+
let second_account =
26+
env.create_light_account_opts(CreateAccountOptions::new().with_secret(42).with_partial_address(1));
27+
28+
assert_eq(first_account, second_account);
29+
}
30+
31+
#[test]
32+
unconstrained fn create_light_account_opts_shared_secret_distinct_partial_address_shares_keys_not_address() {
33+
let mut env = TestEnvironment::new();
34+
35+
let first_account =
36+
env.create_light_account_opts(CreateAccountOptions::new().with_secret(42).with_partial_address(1));
37+
let second_account =
38+
env.create_light_account_opts(CreateAccountOptions::new().with_secret(42).with_partial_address(2));
39+
40+
assert(first_account != second_account);
41+
env.utility_context(|_| {
42+
assert_eq(get_public_keys(first_account).npk_m_hash, get_public_keys(second_account).npk_m_hash);
43+
});
44+
}
45+
46+
#[test]
47+
unconstrained fn create_light_account_opts_distinct_secret_shared_partial_address_differ() {
48+
let mut env = TestEnvironment::new();
49+
50+
let first_account =
51+
env.create_light_account_opts(CreateAccountOptions::new().with_secret(42).with_partial_address(1));
52+
let second_account =
53+
env.create_light_account_opts(CreateAccountOptions::new().with_secret(43).with_partial_address(1));
54+
55+
assert(first_account != second_account);
56+
}
57+
1958
#[test]
2059
unconstrained fn create_contract_account_does_not_repeat_accounts() {
2160
let mut env = TestEnvironment::new();

noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ use crate::protocol::{
2121
/// [`oracle::version`](crate::oracle::version), which covers oracles used during contract execution by PXE.
2222
///
2323
/// The TypeScript counterparts are in `yarn-project/txe/src/txe_oracle_version.ts`.
24-
pub global TXE_ORACLE_VERSION_MAJOR: Field = 3;
25-
pub global TXE_ORACLE_VERSION_MINOR: Field = 1;
24+
pub global TXE_ORACLE_VERSION_MAJOR: Field = 4;
25+
pub global TXE_ORACLE_VERSION_MINOR: Field = 0;
2626

2727
/// Asserts that the TXE oracle interface version is compatible.
2828
pub unconstrained fn assert_compatible_txe_oracle_version() {
@@ -249,7 +249,7 @@ pub unconstrained fn deploy_oracle<let N: u32, let P: u32>(
249249
) -> [Field; CONTRACT_INSTANCE_LENGTH] {}
250250

251251
#[oracle(aztec_txe_createAccount)]
252-
pub unconstrained fn create_account(secret: Field) -> TestAccount {}
252+
pub unconstrained fn create_account(secret: Field, partial_address: Field) -> TestAccount {}
253253

254254
#[oracle(aztec_txe_addAccount)]
255255
pub unconstrained fn add_account(secret: Field) -> TestAccount {}

noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ use crate::{
1313
DOM_SEP__CONSTRAINED_MSG_NULLIFIER, DOM_SEP__CONSTRAINED_MSG_SENDER_SECRET,
1414
DOM_SEP__CONTRACT_ADDRESS_V2, DOM_SEP__CONTRACT_CLASS_ID, DOM_SEP__ECDH_FIELD_MASK,
1515
DOM_SEP__ECDH_SUBKEY, DOM_SEP__EVENT_COMMITMENT, DOM_SEP__EVENT_LOG_TAG, DOM_SEP__FBSK_M,
16-
DOM_SEP__FUNCTION_ARGS, DOM_SEP__INITIALIZATION_NULLIFIER, DOM_SEP__INITIALIZER,
17-
DOM_SEP__HANDSHAKE_FORGERY_PROTECTION, DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE,
18-
DOM_SEP__IVSK_M, DOM_SEP__MERKLE_HASH,
16+
DOM_SEP__FUNCTION_ARGS, DOM_SEP__HANDSHAKE_FORGERY_PROTECTION,
17+
DOM_SEP__INITIALIZATION_NULLIFIER, DOM_SEP__INITIALIZER,
18+
DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE, DOM_SEP__IVSK_M, DOM_SEP__MERKLE_HASH,
1919
DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__MSSK_M, DOM_SEP__NHK_M,
2020
DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG, DOM_SEP__NOTE_COMPLETION_LOG_TAG,
2121
DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, DOM_SEP__NOTE_NULLIFIER,

0 commit comments

Comments
 (0)