Skip to content

Commit 9d8c26c

Browse files
nventuroclaudemverzilli
authored
feat: add salt and secret params to env.deploy (#21183)
Originally requested by @xorsal and @zkfrov. This simply adds a new `deploy_opts` function which takes `DeployOptions`, via which custom salt and secret can be speficied. In the future we might be able to allow specifying more things. The bigger change is that the deployment oraacle now supports salt, secret (and deployer, currently hardcoded), so we should not need to break this API in the near future. Closes #16656 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Martin Verzilli <martin@aztec-labs.com>
1 parent d0b57f2 commit 9d8c26c

7 files changed

Lines changed: 134 additions & 16 deletions

File tree

noir-projects/aztec-nr/aztec/src/oracle/version.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is
66
/// called
77
/// and if the oracle version is incompatible an error is thrown.
8-
pub global ORACLE_VERSION: Field = 13;
8+
pub global ORACLE_VERSION: Field = 14;
99

1010
/// Asserts that the version of the oracle is compatible with the version expected by the contract.
1111
pub fn assert_compatible_oracle_version() {

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

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,17 @@ impl Counter {
7676
/// }
7777
/// ```
7878
pub struct TestEnvironment {
79-
// The secrets to be used for light and contract account creation, as well as contract deployments. By keeping
80-
// track of the last used secret we can issue new ones automatically without requiring the user to provide
79+
// The secrets and salt to be used for light and contract account creation, as well as contract deployments. By
80+
// keeping track of the last used secret we can issue new ones automatically without requiring the user to provide
8181
// different ones.
8282
//
83-
// Additionally, having the secrets be deterministic for each set of accounts and all concurrent tests results in
84-
// TXE being able to maximize cache usage and not have to recompute account addresses and contract artifacts, which
85-
// are relatively expensive operations.
83+
// Additionally, having the secrets and salt be deterministic for each set of accounts and all concurrent tests
84+
// results in TXE being able to maximize cache usage and not have to recompute account addresses and contract
85+
// artifacts, which are relatively expensive operations.
8686
light_account_secret: Counter,
8787
contract_account_secret: Counter,
8888
contract_deployment_secret: Counter,
89+
contract_deployment_salt: Counter,
8990
}
9091

9192
/// Configuration values for [`TestEnvironment::private_context_opts`]. Meant to be used by calling `new` and then
@@ -138,6 +139,38 @@ struct EventDiscoveryOptions {
138139
contract_address: Option<AztecAddress>,
139140
}
140141

142+
/// Configuration values for [`TestEnvironment::deploy_opts`]. Meant to be used by calling `new` and then chaining
143+
/// methods setting each value, e.g.:
144+
/// ```noir
145+
/// env.deploy_opts(DeployOptions::new().with_salt(42).with_secret(100), "MyContract").without_initializer();
146+
/// ```
147+
pub struct DeployOptions {
148+
salt: Option<Field>,
149+
secret: Option<Field>,
150+
}
151+
152+
impl DeployOptions {
153+
/// Creates a new `DeployOptions` with default values, i.e. the same as if using the `deploy` method instead of
154+
/// `deploy_opts`. Use the `with_salt` and `with_secret` methods to set the desired configuration values.
155+
pub fn new() -> Self {
156+
Self { salt: Option::none(), secret: Option::none() }
157+
}
158+
159+
/// Sets the deployment salt. The salt affects the resulting contract address: the same contract deployed with
160+
/// different salts will have different addresses.
161+
pub fn with_salt(&mut self, salt: Field) -> Self {
162+
self.salt = Option::some(salt);
163+
*self
164+
}
165+
166+
/// Sets the secret used for key derivation. The secret affects the contract's public keys and therefore its
167+
/// address: the same contract deployed with different secrets will have different addresses.
168+
pub fn with_secret(&mut self, secret: Field) -> Self {
169+
self.secret = Option::some(secret);
170+
*self
171+
}
172+
}
173+
141174
impl TestEnvironment {
142175
/// Creates a new `TestEnvironment`. This function should only be called once per test.
143176
pub unconstrained fn new() -> Self {
@@ -150,6 +183,9 @@ impl TestEnvironment {
150183
light_account_secret: Counter::new(),
151184
contract_account_secret: Counter::new_with_offset(1_000),
152185
contract_deployment_secret: Counter::new_with_offset(2_000),
186+
// The salt counter does not need an offset because keys (derived from secret) and salt are unrelated: a
187+
// collision between a salt and a secret value has no effect on the resulting contract address.
188+
contract_deployment_salt: Counter::new(),
153189
}
154190
}
155191

@@ -477,7 +513,19 @@ impl TestEnvironment {
477513
/// );
478514
/// ```
479515
pub unconstrained fn deploy<let N: u32>(&mut self, path: str<N>) -> ContractDeployment<N> {
480-
ContractDeployment { env: *self, path, secret: self.contract_deployment_secret.next() }
516+
self.deploy_opts(DeployOptions::new(), path)
517+
}
518+
519+
/// Variant of `deploy` which allows specifying configuration values via `DeployOptions`, such as a custom salt
520+
/// or secret. If not specified, the salt and secret default to values from internal counters.
521+
pub unconstrained fn deploy_opts<let N: u32>(
522+
&mut self,
523+
opts: DeployOptions,
524+
path: str<N>,
525+
) -> ContractDeployment<N> {
526+
let secret = opts.secret.unwrap_or_else(|| self.contract_deployment_secret.next());
527+
let salt = opts.salt.unwrap_or_else(|| self.contract_deployment_salt.next());
528+
ContractDeployment { env: *self, path, secret, salt }
481529
}
482530

483531
/// Performs a private contract function call, including the processing of any nested private calls and enqueued

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

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::test::helpers::test_environment::TestEnvironment;
1+
use crate::test::helpers::test_environment::{DeployOptions, TestEnvironment};
22

33
#[test]
44
unconstrained fn deploy_does_not_repeat_addresses() {
@@ -15,3 +15,43 @@ unconstrained fn deploy_does_not_repeat_addresses() {
1515
// This at least does test that the basics of the deployment mechanism work.
1616
assert(first_contract != second_contract);
1717
}
18+
19+
#[test(should_fail_with = "NullifierTree")]
20+
unconstrained fn deploy_with_same_salt_and_secret_fails() {
21+
let mut env = TestEnvironment::new();
22+
23+
let opts = DeployOptions::new().with_salt(42).with_secret(100);
24+
25+
let _ = env.deploy_opts(opts, "../noir-contracts/@test_contract/Test").without_initializer();
26+
// This will result in the exact same address preimage and hence same address, and so the initialization
27+
// nullifiers will be duplicated.
28+
let _ = env.deploy_opts(opts, "../noir-contracts/@test_contract/Test").without_initializer();
29+
}
30+
31+
#[test]
32+
unconstrained fn deploy_with_same_salt_does_not_repeat_addresses() {
33+
let mut env = TestEnvironment::new();
34+
35+
let first_contract = env
36+
.deploy_opts(DeployOptions::new().with_salt(42).with_secret(100), "../noir-contracts/@test_contract/Test")
37+
.without_initializer();
38+
let second_contract = env
39+
.deploy_opts(DeployOptions::new().with_salt(42).with_secret(200), "../noir-contracts/@test_contract/Test")
40+
.without_initializer();
41+
42+
assert(first_contract != second_contract);
43+
}
44+
45+
#[test]
46+
unconstrained fn deploy_with_same_secret_does_not_repeat_addresses() {
47+
let mut env = TestEnvironment::new();
48+
49+
let first_contract = env
50+
.deploy_opts(DeployOptions::new().with_salt(42).with_secret(100), "../noir-contracts/@test_contract/Test")
51+
.without_initializer();
52+
let second_contract = env
53+
.deploy_opts(DeployOptions::new().with_salt(43).with_secret(100), "../noir-contracts/@test_contract/Test")
54+
.without_initializer();
55+
56+
assert(first_contract != second_contract);
57+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ pub unconstrained fn deploy<let M: u32, let N: u32, let P: u32>(
1818
initializer: str<P>,
1919
args: [Field; M],
2020
secret: Field,
21+
salt: Field,
22+
deployer: AztecAddress,
2123
) -> ContractInstance {
22-
let instance_fields = deploy_oracle(path, initializer, args, secret);
24+
let instance_fields = deploy_oracle(path, initializer, args, secret, salt, deployer);
2325
ContractInstance::deserialize(instance_fields)
2426
}
2527

@@ -116,6 +118,8 @@ pub unconstrained fn deploy_oracle<let N: u32, let P: u32>(
116118
initializer: str<P>,
117119
args: [Field],
118120
secret: Field,
121+
salt: Field,
122+
deployer: AztecAddress,
119123
) -> [Field; CONTRACT_INSTANCE_LENGTH] {}
120124

121125
#[oracle(aztec_txe_createAccount)]

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub struct ContractDeployment<let O: u32> {
66
pub env: TestEnvironment,
77
pub path: str<O>,
88
pub secret: Field,
9+
pub salt: Field,
910
}
1011

1112
impl<let O: u32> ContractDeployment<O> {
@@ -38,7 +39,14 @@ impl<let O: u32> ContractDeployment<O> {
3839
let name = initializer_call.name;
3940
let selector = initializer_call.selector;
4041
let args = initializer_call.args;
41-
let instance = txe_oracles::deploy(self.path, name, args, self.secret);
42+
let instance = txe_oracles::deploy(
43+
self.path,
44+
name,
45+
args,
46+
self.secret,
47+
self.salt,
48+
AztecAddress::zero(),
49+
);
4250

4351
// initializer_call does not actually have the target_contract value set - it is created with the helper
4452
// `interface` function created by `generate_contract_interface` in the aztec macros - it represents a call to
@@ -77,7 +85,14 @@ impl<let O: u32> ContractDeployment<O> {
7785
let name = initializer_call.name;
7886
let selector = initializer_call.selector;
7987
let args = initializer_call.args;
80-
let instance = txe_oracles::deploy(self.path, name, args, self.secret);
88+
let instance = txe_oracles::deploy(
89+
self.path,
90+
name,
91+
args,
92+
self.secret,
93+
self.salt,
94+
AztecAddress::zero(),
95+
);
8196

8297
// initializer_call does not actually have the target_contract value set - it is created with the helper
8398
// `interface` function created by `generate_contract_interface` in the aztec macros - it represents a call to
@@ -96,7 +111,15 @@ impl<let O: u32> ContractDeployment<O> {
96111
/// contains a commitment to the lack of initialization arguments as per the protocol rules. Initializers can only
97112
/// be invoked by using the `with_private_initializer` or `with_public_initializer` functions.
98113
pub unconstrained fn without_initializer(self) -> AztecAddress {
99-
txe_oracles::deploy(self.path, "", [], self.secret).to_address()
114+
txe_oracles::deploy(
115+
self.path,
116+
"",
117+
[],
118+
self.secret,
119+
self.salt,
120+
AztecAddress::zero(),
121+
)
122+
.to_address()
100123
}
101124
}
102125

yarn-project/pxe/src/oracle_version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
///
55
/// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is called
66
/// and if the oracle version is incompatible an error is thrown.
7-
export const ORACLE_VERSION = 13;
7+
export const ORACLE_VERSION = 14;
88

99
/// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes,
1010
/// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in

yarn-project/txe/src/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
type ForeignCallResult,
3333
ForeignCallResultSchema,
3434
type ForeignCallSingle,
35+
addressFromSingle,
3536
fromArray,
3637
fromSingle,
3738
toSingle,
@@ -105,6 +106,8 @@ class TXEDispatcher {
105106

106107
const decodedArgs = fromArray(inputs[3] as ForeignCallArray);
107108
const secret = fromSingle(inputs[4] as ForeignCallSingle);
109+
const salt = fromSingle(inputs[5] as ForeignCallSingle);
110+
const deployer = addressFromSingle(inputs[6] as ForeignCallSingle);
108111
const publicKeys = secret.equals(Fr.ZERO) ? PublicKeys.default() : (await deriveKeys(secret)).publicKeys;
109112
const publicKeysHash = await publicKeys.hash();
110113

@@ -135,7 +138,7 @@ class TXEDispatcher {
135138

136139
const cacheKey = `${contractDirectory ?? ''}-${contractFilename}-${initializer}-${decodedArgs
137140
.map(arg => arg.toString())
138-
.join('-')}-${publicKeysHash}-${fileHash}`;
141+
.join('-')}-${publicKeysHash}-${salt}-${deployer}-${fileHash}`;
139142

140143
let instance;
141144
let artifact: ContractArtifactWithHash;
@@ -161,10 +164,10 @@ class TXEDispatcher {
161164
const computedInstance = await getContractInstanceFromInstantiationParams(computedArtifact, {
162165
constructorArgs: decodedArgs,
163166
skipArgsDecoding: true,
164-
salt: Fr.ONE,
167+
salt,
165168
publicKeys,
166169
constructorArtifact: initializer ? initializer : undefined,
167-
deployer: AztecAddress.ZERO,
170+
deployer,
168171
});
169172
const result = { artifact: computedArtifact, instance: computedInstance };
170173
TXEArtifactsCache.set(cacheKey, result);

0 commit comments

Comments
 (0)