Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ You can also prove a contract was initialized (constructor was called):

```rust
use dep::aztec::history::deployment::assert_contract_was_initialized_by;
use dep::aztec::oracle::get_contract_instance::get_contract_instance;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just updating some docs


let header = self.context.get_anchor_block_header();
assert_contract_was_initialized_by(header, contract_address);
let instance = get_contract_instance(contract_address);
assert_contract_was_initialized_by(header, contract_address, instance.initialization_hash);
```

## Available proof functions
Expand Down
17 changes: 17 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,23 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### Private initialization nullifier now includes `init_hash`

The private initialization nullifier is no longer derived from just the contract address. It is now computed as a Poseidon2 hash of `[address, init_hash]` using a dedicated domain separator. This prevents observers from determining whether a fully private contract has been initialized by simply knowing its address.

Note that `Wallet.getContractMetadata` now returns `isContractInitialized: false` when the wallet does not have the contract instance registered, since `init_hash` is needed to compute the nullifier. Previously, this check worked for any address.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it instead fail, as it can't know?

@nchamo nchamo Mar 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we shouldn't fail, getContractMetadata can already be called when the walled doesn't have the contract instance registered, and it says so in the return type (by returning instance: undefined).

I think we should change it so that now we have this too isContractInitialized : boolean | undefined, and explain correctly on the js docs. Or maybe we could use something different like isContractInitialized: boolean | 'unknown' to be clearer. I'm don't like any of my suggestions, but I don't think we should fail


If you use `assert_contract_was_initialized_by` or `assert_contract_was_not_initialized_by` from `aztec::history::deployment`, these now require an additional `init_hash: Field` parameter:

```diff
+ let instance = get_contract_instance(contract_address);
assert_contract_was_initialized_by(
block_header,
contract_address,
+ instance.initialization_hash,
);
```

### Two separate init nullifiers for private and public

Contract initialization now emits two separate nullifiers instead of one: a **private init nullifier** and a **public init nullifier**. Each nullifier gates its respective execution domain:
Expand Down
21 changes: 17 additions & 4 deletions noir-projects/aztec-nr/aztec/src/history/deployment.nr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::protocol::{
};

use crate::history::nullifier::{assert_nullifier_did_not_exist_by, assert_nullifier_existed_by};
use crate::macros::functions::initialization_utils::compute_private_init_nullifier;

// This is tested in `noir-projects/noir-contracts/test_contract/src/test.nr because we cannot define a contract from
// within aztec.nr (due to the contract macro).
Expand All @@ -28,14 +29,26 @@ pub fn assert_contract_bytecode_was_not_published_by(block_header: BlockHeader,
assert_nullifier_did_not_exist_by(block_header, bytecode_publishing_nullifier);
}

pub fn assert_contract_was_initialized_by(block_header: BlockHeader, contract_address: AztecAddress) {
let initialization_nullifier = compute_siloed_nullifier(contract_address, contract_address.to_field());
/// Asserts that a contract was initialized by the given block.
///
/// `init_hash` is the contract's initialization hash, obtainable via `get_contract_instance`.
Comment thread
nchamo marked this conversation as resolved.
Outdated
pub fn assert_contract_was_initialized_by(block_header: BlockHeader, contract_address: AztecAddress, init_hash: Field) {
let inner_init_nullifier = compute_private_init_nullifier(contract_address, init_hash);
let initialization_nullifier = compute_siloed_nullifier(contract_address, inner_init_nullifier);

assert_nullifier_existed_by(block_header, initialization_nullifier);
Comment thread
nchamo marked this conversation as resolved.
Outdated
}

pub fn assert_contract_was_not_initialized_by(block_header: BlockHeader, contract_address: AztecAddress) {
let initialization_nullifier = compute_siloed_nullifier(contract_address, contract_address.to_field());
/// Asserts that a contract was not initialized by the given block.
///
/// `init_hash` is the contract's initialization hash, obtainable via `get_contract_instance`.
pub fn assert_contract_was_not_initialized_by(
block_header: BlockHeader,
contract_address: AztecAddress,
init_hash: Field,
) {
let inner_init_nullifier = compute_private_init_nullifier(contract_address, init_hash);
let initialization_nullifier = compute_siloed_nullifier(contract_address, inner_init_nullifier);

assert_nullifier_did_not_exist_by(block_header, initialization_nullifier);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::protocol::{
abis::function_selector::FunctionSelector,
address::AztecAddress,
constants::{DOM_SEP__INITIALIZER, DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER},
constants::{
DOM_SEP__INITIALIZER, DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER,
},
hash::poseidon2_hash_with_separator,
traits::ToField,
};
Expand Down Expand Up @@ -53,7 +55,9 @@ pub fn mark_as_initialized_public(context: PublicContext) {
}

fn mark_as_initialized_private(context: &mut PrivateContext) {
let init_nullifier = compute_private_init_nullifier((*context).this_address());
let address = (*context).this_address();
let instance = get_contract_instance(address);
let init_nullifier = compute_private_init_nullifier(address, instance.initialization_hash);
context.push_nullifier(init_nullifier);
}

Expand All @@ -74,7 +78,12 @@ pub fn mark_as_initialized_from_private_initializer(context: &mut PrivateContext
/// Called by public [`initializer`](crate::macros::functions::initializer) macros, since public initializers must set
/// both so that both private and public functions see the contract as initialized.
pub fn mark_as_initialized_from_public_initializer(context: PublicContext) {
let private_nullifier = compute_private_init_nullifier(context.this_address());
let address = context.this_address();
// `get_contract_instance_initialization_hash_avm` returns None when there is no deployed contract instance at the
// given address. This cannot happen here because we're querying `this_address()`, i.e. the contract that is
// currently executing, which by definition must have been deployed.
let init_hash = get_contract_instance_initialization_hash_avm(address).unwrap();
let private_nullifier = compute_private_init_nullifier(address, init_hash);
context.push_nullifier(private_nullifier);
mark_as_initialized_public(context);
}
Expand All @@ -94,15 +103,22 @@ pub fn assert_is_initialized_public(context: PublicContext) {
///
/// Checks that the private initialization nullifier exists.
pub fn assert_is_initialized_private(context: &mut PrivateContext) {
let init_nullifier = compute_private_init_nullifier(context.this_address());
let nullifier_existence_request = compute_nullifier_existence_request(init_nullifier, context.this_address());
let address = context.this_address();
let instance = get_contract_instance(address);
let init_nullifier = compute_private_init_nullifier(address, instance.initialization_hash);
let nullifier_existence_request = compute_nullifier_existence_request(init_nullifier, address);
context.assert_nullifier_exists(nullifier_existence_request);
}

// TODO(F-194): This leaks whether a contract has been initialized, since anyone who knows the address can compute this
// nullifier and check for its existence. It is also not domain separated.
fn compute_private_init_nullifier(address: AztecAddress) -> Field {
address.to_field()
/// Computes the private initialization nullifier for a contract.
///
/// Includes `init_hash` so that observers cannot determine whether a contract has been initialized from the address
/// alone.
pub(crate) fn compute_private_init_nullifier(address: AztecAddress, init_hash: Field) -> Field {
poseidon2_hash_with_separator(
[address.to_field(), init_hash],
DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER,
)
Comment thread
nchamo marked this conversation as resolved.
Outdated
}

fn compute_public_init_nullifier(address: AztecAddress) -> Field {
Expand Down
4 changes: 4 additions & 0 deletions noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ use initialization_utils::EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME;
/// begins: if a single nullifier were used, public functions enqueued by the initializer would see it as
/// existing *before* the public initialization code had a chance to run.
///
/// The private init nullifier is computed from the contract address and the contract's `init_hash`. This means that
Comment thread
nchamo marked this conversation as resolved.
Outdated
/// address knowledge alone is insufficient to check whether a contract has been initialized, preventing a privacy
/// leak for fully private contracts.
///
/// - **Private initializers** emit the private init nullifier. For contracts that also have external public functions,
/// they auto-enqueue a call to an auto-generated public function that emits the public init nullifier during public
/// execution. This function name is reserved and cannot be used by contract developers.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::Test;
use aztec::{
hash::hash_args,
history::deployment::{
assert_contract_bytecode_was_not_published_by, assert_contract_bytecode_was_published_by,
assert_contract_was_initialized_by, assert_contract_was_not_initialized_by,
},
macros::functions::initialization_utils::compute_initialization_hash,
protocol::address::AztecAddress,
test::helpers::test_environment::{PrivateContextOptions, TestEnvironment},
};
Expand All @@ -18,7 +20,7 @@ global CONTRACT_DEPLOYED_AT: u32 = 2;
// following block.
global CONTRACT_INITIALIZED_AT: u32 = CONTRACT_DEPLOYED_AT + 1;

pub unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress) {
pub unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress, Field) {
let mut env = TestEnvironment::new();
let owner = env.create_light_account();

Expand All @@ -27,17 +29,18 @@ pub unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress) {

// Deploy contract and initialize
let initializer = Test::interface().initialize();
let init_hash = compute_initialization_hash(initializer.selector, hash_args(initializer.args));
let contract_address = env.deploy("Test").with_private_initializer(owner, initializer);

// We sanity check that the initialization block was the last one
assert_eq(env.last_block_number(), CONTRACT_INITIALIZED_AT);

(env, contract_address, owner)
(env, contract_address, owner, init_hash)
}

#[test]
unconstrained fn contract_historical_proofs_happy_path() {
let (env, contract_address, _owner) = setup();
let (env, contract_address, _owner, init_hash) = setup();

env.private_context_opts(
PrivateContextOptions::new().at_anchor_block_number(CONTRACT_DEPLOYED_AT - 1),
Expand All @@ -62,21 +65,29 @@ unconstrained fn contract_historical_proofs_happy_path() {
env.private_context_opts(
PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT - 1),
|context| {
assert_contract_was_not_initialized_by(context.anchor_block_header, contract_address);
assert_contract_was_not_initialized_by(
context.anchor_block_header,
contract_address,
init_hash,
);
},
);

env.private_context_opts(
PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT),
|context| {
assert_contract_was_initialized_by(context.anchor_block_header, contract_address);
assert_contract_was_initialized_by(
context.anchor_block_header,
contract_address,
init_hash,
);
},
);
}

#[test(should_fail_with = "Nullifier membership witness not found at block")]
unconstrained fn assert_contract_bytecode_was_published_by_before_deployment_fails() {
let (env, contract_address, _owner) = setup();
let (env, contract_address, _owner, _init_hash) = setup();

// Note that we're only testing that the function fails, but not that it would correct reject bad hints from an oracle
env.private_context_opts(
Expand All @@ -92,20 +103,24 @@ unconstrained fn assert_contract_bytecode_was_published_by_before_deployment_fai

#[test(should_fail_with = "Nullifier membership witness not found at block")]
unconstrained fn assert_contract_was_initialized_by_before_initialization_fails() {
let (env, contract_address, _owner) = setup();
let (env, contract_address, _owner, init_hash) = setup();

// Note that we're only testing that the function fails, but not that it would correct reject bad hints from an oracle
env.private_context_opts(
PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT - 1),
|context| {
assert_contract_was_initialized_by(context.anchor_block_header, contract_address);
assert_contract_was_initialized_by(
context.anchor_block_header,
contract_address,
init_hash,
);
},
);
}

#[test(should_fail_with = "Proving nullifier non-inclusion failed")]
unconstrained fn assert_contract_bytecode_was_not_published_by_of_deployed_fails() {
let (env, contract_address, _owner) = setup();
let (env, contract_address, _owner, _init_hash) = setup();

// Note that we're only testing that the function fails, but not that it would correct reject bad hints from an oracle
env.private_context_opts(
Expand All @@ -119,15 +134,35 @@ unconstrained fn assert_contract_bytecode_was_not_published_by_of_deployed_fails
);
}

#[test(should_fail_with = "Nullifier membership witness not found at block")]
unconstrained fn assert_contract_was_initialized_by_with_wrong_init_hash_fails() {
let (env, contract_address, _owner, _init_hash) = setup();

env.private_context_opts(
PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT),
|context| {
assert_contract_was_initialized_by(
context.anchor_block_header,
contract_address,
0xdeadbeef,
);
},
);
}

#[test(should_fail_with = "Proving nullifier non-inclusion failed")]
unconstrained fn assert_contract_was_not_initialized_by_of_initialized_fails() {
let (env, contract_address, _owner) = setup();
let (env, contract_address, _owner, init_hash) = setup();

// Note that we're only testing that the function fails, but not that it would correct reject bad hints from an oracle
env.private_context_opts(
PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT),
|context| {
assert_contract_was_not_initialized_by(context.anchor_block_header, contract_address);
assert_contract_was_not_initialized_by(
context.anchor_block_header,
contract_address,
init_hash,
);
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ pub global DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT: u32 = 623934423;
/// Should not be reused for a given storage slot.
pub global DOM_SEP__INITIALIZATION_NULLIFIER: u32 = 1653084894;
pub global DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER: u32 = 3342006647;
pub global DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER: u32 = 3990889078;

/// Domain separator for L1 to L2 message secret hashes.
pub global DOM_SEP__SECRET_HASH: u32 = 4199652938;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ use crate::{
DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M, DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE,
DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M, DOM_SEP__PARTIAL_ADDRESS,
DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, DOM_SEP__PRIVATE_FUNCTION_LEAF,
DOM_SEP__PRIVATE_LOG_FIRST_FIELD, DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS,
DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA,
DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, DOM_SEP__PUBLIC_KEYS_HASH,
DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH,
DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, DOM_SEP__SILOED_NOTE_HASH,
DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, DOM_SEP__SYMMETRIC_KEY,
DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M, DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST,
DOM_SEP__UNIQUE_NOTE_HASH, NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS,
TX_START_PREFIX,
DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, DOM_SEP__PRIVATE_LOG_FIRST_FIELD,
DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS, DOM_SEP__PUBLIC_BYTECODE,
DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER,
DOM_SEP__PUBLIC_KEYS_HASH, DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT,
DOM_SEP__PUBLIC_TX_HASH, DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD,
DOM_SEP__SILOED_NOTE_HASH, DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER,
DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M, DOM_SEP__TX_NULLIFIER,
DOM_SEP__TX_REQUEST, DOM_SEP__UNIQUE_NOTE_HASH, NULL_MSG_SENDER_CONTRACT_ADDRESS,
SIDE_EFFECT_MASKING_ADDRESS, TX_START_PREFIX,
},
hash::poseidon2_hash_bytes,
traits::{FromField, ToField},
Expand Down Expand Up @@ -131,7 +131,7 @@ impl<let NUM_VALUES: u32, let NUM_U32_VALUES: u32> HashedValueTester<NUM_VALUES,

#[test]
fn hashed_values_match_derived() {
let mut tester = HashedValueTester::<52, 45>::new();
let mut tester = HashedValueTester::<53, 46>::new();

// -----------------
// Domain separators
Expand Down Expand Up @@ -193,6 +193,10 @@ fn hashed_values_match_derived() {
DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER,
"public_initialization_nullifier",
);
tester.assert_dom_sep_matches_derived(
DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER,
"private_initialization_nullifier",
);
tester.assert_dom_sep_matches_derived(DOM_SEP__SECRET_HASH, "secret_hash");
tester.assert_dom_sep_matches_derived(DOM_SEP__TX_NULLIFIER, "tx_nullifier");
tester.assert_dom_sep_matches_derived(DOM_SEP__SIGNATURE_PAYLOAD, "signature_payload");
Expand Down
Loading
Loading