Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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;

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
16 changes: 16 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,22 @@ 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: undefined` when the wallet does not have the contract instance registered, since `init_hash` is needed to compute the nullifier and initialization status cannot be determined. Previously, this check worked for any address. Callers should check for `undefined` before branching on the boolean value.

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
33 changes: 25 additions & 8 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_initialization_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,30 @@ 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());

assert_nullifier_existed_by(block_header, initialization_nullifier);
/// Asserts that a contract was initialized by the given block.
///
/// `init_hash` is the contract's initialization hash, obtainable via
/// [`get_contract_instance`](crate::oracle::get_contract_instance::get_contract_instance).
pub fn assert_contract_was_initialized_by(block_header: BlockHeader, contract_address: AztecAddress, init_hash: Field) {
let initialization_nullifier = compute_private_initialization_nullifier(contract_address, init_hash);
assert_nullifier_existed_by(
block_header,
compute_siloed_nullifier(contract_address, initialization_nullifier),
);
}

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());

assert_nullifier_did_not_exist_by(block_header, initialization_nullifier);
/// Asserts that a contract was not initialized by the given block.
///
/// `init_hash` is the contract's initialization hash, obtainable via
/// [`get_contract_instance`](crate::oracle::get_contract_instance::get_contract_instance).
pub fn assert_contract_was_not_initialized_by(
block_header: BlockHeader,
contract_address: AztecAddress,
init_hash: Field,
) {
let initialization_nullifier = compute_private_initialization_nullifier(contract_address, init_hash);
assert_nullifier_did_not_exist_by(
block_header,
compute_siloed_nullifier(contract_address, 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 All @@ -18,14 +20,14 @@ use crate::{
},
};

/// The name of the auto-generated function that emits the public init nullifier.
/// The name of the auto-generated function that emits the public initialization nullifier.
///
/// This function is injected into the public dispatch table for contracts with initializers.
pub(crate) comptime global EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME: Quoted = quote { __emit_public_init_nullifier };

/// Returns `true` if the module has any public functions that require initialization checks (i.e. that don't have
/// `#[noinitcheck]`). If all public functions skip init checks, there's no point emitting the public init nullifier
/// since nothing will check it.
/// `#[noinitcheck]`). If all public functions skip initialization checks, there's no point emitting the public
/// initialization nullifier since nothing will check it.
pub(crate) comptime fn has_public_init_checked_functions(m: Module) -> bool {
get_public_functions(m).any(|f: FunctionDefinition| !fn_has_noinitcheck(f))
}
Expand All @@ -48,20 +50,23 @@ global EMIT_PUBLIC_INIT_NULLIFIER_SELECTOR: FunctionSelector = comptime {
/// This should not be called manually. Incorrect use can leave the contract in a broken initialization state (e.g.
/// emitting the public nullifier without the private one). The macro-generated code handles this automatically.
pub fn mark_as_initialized_public(context: PublicContext) {
let init_nullifier = compute_public_init_nullifier(context.this_address());
let init_nullifier = compute_public_initialization_nullifier(context.this_address());
context.push_nullifier(init_nullifier);
}

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_initialization_nullifier(address, instance.initialization_hash);
context.push_nullifier(init_nullifier);
}

/// Emits the private initialization nullifier and, if relevant, enqueues the emission of the public one.
///
/// If the contract has public functions that perform initialization checks (i.e. that don't have `#[noinitcheck]`),
/// this also enqueues a call to the auto-generated `__emit_public_init_nullifier` function so the public nullifier is
/// emitted in public. Called by private [`initializer`](crate::macros::functions::initializer) macros.
/// this also enqueues a call to the auto-generated `__emit_public_init_nullifier` function so the public
/// initialization nullifier is emitted in public. Called by private
/// [`initializer`](crate::macros::functions::initializer) macros.
pub fn mark_as_initialized_from_private_initializer(context: &mut PrivateContext, emit_public_init_nullifier: bool) {
mark_as_initialized_private(context);
if emit_public_init_nullifier {
Expand All @@ -74,7 +79,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_initialization_nullifier(address, init_hash);
context.push_nullifier(private_nullifier);
mark_as_initialized_public(context);
}
Expand All @@ -83,29 +93,37 @@ pub fn mark_as_initialized_from_public_initializer(context: PublicContext) {
///
/// Checks that the public initialization nullifier exists.
pub fn assert_is_initialized_public(context: PublicContext) {
let init_nullifier = compute_public_init_nullifier(context.this_address());
// Safety: the public init nullifier is only ever emitted by public functions, and so the timing concerns from
// nullifier_exists_unsafe do not apply. Additionally, it is emitted after all initializer functions have run,
// so initialization is guaranteed to be complete by the time it exists.
let init_nullifier = compute_public_initialization_nullifier(context.this_address());
// Safety: the public initialization nullifier is only ever emitted by public functions, and so the timing
// concerns from nullifier_exists_unsafe do not apply. Additionally, it is emitted after all initializer
// functions have run, so initialization is guaranteed to be complete by the time it exists.
assert(context.nullifier_exists_unsafe(init_nullifier, context.this_address()), "Not initialized");
}

/// Asserts that the contract has been initialized, from private's perspective.
///
/// 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_initialization_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.
///
/// Including `init_hash` ensures that an observer who knows only the contract address cannot reconstruct this value
/// and scan the nullifier tree to determine initialization status. `init_hash` is only known to parties that hold
/// the contract instance.
pub fn compute_private_initialization_nullifier(address: AztecAddress, init_hash: Field) -> Field {
poseidon2_hash_with_separator(
[address.to_field(), init_hash],
DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER,
)
}

fn compute_public_init_nullifier(address: AztecAddress) -> Field {
fn compute_public_initialization_nullifier(address: AztecAddress) -> Field {
poseidon2_hash_with_separator(
[address.to_field()],
DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER,
Expand Down
46 changes: 26 additions & 20 deletions noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,22 @@ use initialization_utils::EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME;
///
/// ## How It Works
///
/// Initializers emit nullifiers to mark the contract as initialized. Two separate nullifiers are used (a private init
/// nullifier and a public init nullifier) because private nullifiers are committed before public execution
/// 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.
///
/// - **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.
/// Initializers emit nullifiers to mark the contract as initialized. Two separate nullifiers are used (a private
/// initialization nullifier and a public initialization nullifier) because private nullifiers are committed before
/// public execution 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 initialization nullifier is computed from the contract address and the contract's `init_hash`. This
/// means that 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 initialization nullifier. For contracts that also have external
/// public functions, they auto-enqueue a call to an auto-generated public function that emits the public
/// initialization nullifier during public execution. This function name is reserved and cannot be used by
/// contract developers.
/// - **Public initializers** emit both nullifiers directly.
/// - **Private external functions** check the private init nullifier.
/// - **Public external functions** check the public init nullifier.
/// - **Private external functions** check the private initialization nullifier.
/// - **Public external functions** check the public initialization nullifier.
///
/// For private non-initializer functions, the cost of this check is equivalent to a call to
/// [`PrivateContext::assert_nullifier_exists`](crate::context::PrivateContext::assert_nullifier_exists). For public
Expand Down Expand Up @@ -199,20 +204,21 @@ pub comptime fn allow_phase_change(f: FunctionDefinition) {
/// ## Initialization Checks
///
/// `only_self` functions implicitly skip initialization checks (as if they had [`noinitcheck`]). We want
/// `only_self` functions to be callable during initialization, so we can't have them check the init nullifier
/// since it would fail.
/// `only_self` functions to be callable during initialization, so we can't have them check the initialization
/// nullifier since it would fail.
///
/// This is safe because `only_self` functions can be called only by the contract the function is in, meaning
/// execution must start with another external function in the same contract. Eventually the call stack reaches the
/// `only_self` function, but let's focus on that external entry point:
/// - If it already performed an init check, then we are safe.
/// - If it skipped the init check (via [`noinitcheck`]), then the contract developer is explicitly choosing to not
/// check for initialization, and so will our `only_self` function. That's a design choice by the developer. If
/// we didn't skip the init check on `only_self`, the developer would just add `noinitcheck` to it anyway.
/// - If it was the initializer, note that init nullifiers are emitted at the end of initialization: the private
/// init nullifier after all private execution, and the public one after all public execution. So in terms of
/// init checking, everything behaves as if the contract hasn't been initialized yet, and the same two points
/// above still apply.
/// - If it already performed an initialization check, then we are safe.
/// - If it skipped the initialization check (via [`noinitcheck`]), then the contract developer is explicitly
/// choosing to not check for initialization, and so will our `only_self` function. That's a design choice by
/// the developer. If we didn't skip the initialization check on `only_self`, the developer would just add
/// `noinitcheck` to it anyway.
/// - If it was the initializer, note that initialization nullifiers are emitted at the end of initialization:
/// the private initialization nullifier after all private execution, and the public one after all public
/// execution. So in terms of initialization checking, everything behaves as if the contract hasn't been
/// initialized yet, and the same two points above still apply.
pub comptime fn only_self(f: FunctionDefinition) {
// Marker attribute - see the comment at the top of this file

Expand Down
Loading
Loading