Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d588e8c
feat!: auto-enqueue public init nullifier for contracts with public f…
nchamo Feb 23, 2026
0e5db75
fix: remove skip flags and fix updatable contract for public init nul…
nchamo Feb 23, 2026
ad2b242
fix: remove skip flags from e2e_ha_full and pre-publish class in web3…
nchamo Feb 23, 2026
6a622ce
fix: actually send publishContractClass in web3signer e2e test
nchamo Feb 23, 2026
f748e4b
fix: pr review changes
nchamo Feb 27, 2026
c2184a6
fix: update comments on test contracts and initialization_utils
nchamo Feb 27, 2026
e2d3423
Merge remote-tracking branch 'origin/merge-train/fairies' into feat/p…
nchamo Feb 27, 2026
bcc1d63
fix: use onchain instead of on-chain in migration notes
nchamo Feb 27, 2026
7cb5443
refactor: pass InjectedPublicFunction struct to generate_public_dispatch
nchamo Mar 2, 2026
1488a52
fix: address PR comments
nchamo Mar 3, 2026
c3bcee1
fix: remove unnecessary mut in dispatch.nr
nchamo Mar 3, 2026
e5894af
Apply suggestions from code review
nchamo Mar 4, 2026
528e38f
docs: improve only_self noinitcheck doc comment
nchamo Mar 4, 2026
772302b
test: update private initialization e2e test
nchamo Mar 4, 2026
7e8877d
Merge remote-tracking branch 'origin/merge-train/fairies' into feat/p…
nchamo Mar 4, 2026
5b84fbe
docs: reflow only_self doc comment to 120 char width
nchamo Mar 4, 2026
39f5d57
refactor: inline dispatch logic, extract emit_public_init_nullifier m…
nchamo Mar 6, 2026
c389405
docs: fix dispatch comment grammar
nchamo Mar 6, 2026
3535ec3
Apply suggestions from code review
nchamo Mar 9, 2026
20249d5
Merge remote-tracking branch 'origin/merge-train/fairies' into feat/p…
nchamo Mar 9, 2026
8aec344
fix(e2e): use DEFAULT_DA_GAS_LIMIT in e2e_sequencer_config test
nchamo Mar 9, 2026
91cf5c7
fix: address self-review items on PR #20775
nchamo Mar 10, 2026
dc54c88
fix: address review comments on PR #20775
nchamo Mar 12, 2026
a122c76
fix: reflow doc comment to avoid awkward nargo fmt line break
nchamo Mar 12, 2026
760a963
Merge remote-tracking branch 'origin/merge-train/fairies' into feat/p…
nchamo Mar 12, 2026
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
26 changes: 25 additions & 1 deletion docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### 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:

- Private external functions check the private init nullifier.
- Public external functions check the public init nullifier.

**How initializers work:**

- **Private initializers** emit the private init nullifier. If the contract has any external public functions, the protocol auto-enqueues a public call to emit the public init nullifier.
- **Public initializers** emit both nullifiers directly.
- Contracts with no public functions only emit the private init nullifier.

**`only_self` functions no longer have init checks.** They behave as if marked `noinitcheck`.

**External functions called during private initialization must be `#[only_self]`.** Init nullifiers are emitted at the end of the initializer, so any external functions called on the initializing contract (e.g. via `enqueue_self` or `call_self`) during initialization will fail the init check unless they skip it.

**Breaking change for deployment:** If your contract has external public functions and a private initializer, the class must be registered onchain before initialization. You can no longer pass `skipClassPublication: true`, because the auto-enqueued public call requires the class to be available.

```diff
const deployed = await MyContract.deploy(wallet, ...args).send({
- skipClassPublication: true,
}).deployed();
```

### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained

This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ConfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe.
Expand All @@ -33,7 +58,6 @@ The `maxLogsHit` flag indicates whether the log limit was reached, meaning more
### [Aztec.nr] Removed `get_random_bytes`

The `get_random_bytes` unconstrained function has been removed from `aztec::utils::random`. If you were using it, you can replace it with direct calls to the `random` oracle from `aztec::oracle::random` and convert to bytes yourself.

### [Aztec.js] `simulate()`, `send()`, and deploy return types changed to always return objects

All SDK interaction methods now return structured objects that include offchain output alongside the primary result. This affects `.simulate()`, `.send()`, deploy `.send()`, and `Wallet.sendTx()`.
Expand Down
5 changes: 4 additions & 1 deletion noir-projects/aztec-nr/aztec/src/macros/aztec.nr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
internal_functions::generate_call_internal_struct,
},
dispatch::generate_public_dispatch,
emit_public_init_nullifier::generate_emit_public_init_nullifier,
internals_functions_generation::{create_fn_abi_exports, process_functions},
notes::NOTES,
storage::STORAGE_LAYOUT_NAME,
Expand Down Expand Up @@ -137,7 +138,8 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted {
} else {
quote {}
};
let public_dispatch = generate_public_dispatch(m);
let (has_public_init_nullifier_fn, emit_public_init_nullifier_fn_body) = generate_emit_public_init_nullifier(m);
let public_dispatch = generate_public_dispatch(m, has_public_init_nullifier_fn);

quote {
$interface
Expand All @@ -149,6 +151,7 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted {
$public_dispatch
$sync_state_fn_and_abi_export
$process_message_fn_and_abi_export
$emit_public_init_nullifier_fn_body
$offchain_receive_fn_and_abi_export
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ comptime fn create_stub_base(f: FunctionDefinition) -> (Quoted, Quoted, Quoted,

let fn_name_str = f"\"{fn_name}\"".quoted_contents();
let fn_name_len: u32 = unquote!(quote { $fn_name_str.as_bytes().len()});
let fn_selector: Field = compute_fn_selector(f);
let fn_selector: Field = compute_fn_selector(f.name(), f.parameters());

(
fn_name, fn_parameters_list, serialized_args_array_construction, serialized_args_array_name,
Expand Down
29 changes: 25 additions & 4 deletions noir-projects/aztec-nr/aztec/src/macros/dispatch.nr
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
use crate::macros::internals_functions_generation::external_functions_registry::get_public_functions;
use crate::protocol::meta::utils::get_params_len_quote;
use crate::utils::cmap::CHashMap;
use super::functions::initialization_utils::EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME;
use super::utils::compute_fn_selector;
use std::panic;

/// Returns an `fn public_dispatch(...)` function for the given module that's assumed to be an Aztec contract.
pub comptime fn generate_public_dispatch(m: Module) -> Quoted {
/// Generates a `public_dispatch` function for an Aztec contract module `m`.
///
/// The generated function dispatches public calls based on selector to the appropriate contract function. If
/// `generate_emit_public_init_nullifier` is true, it also handles dispatch to the macro-generated
/// `__emit_public_init_nullifier` function.
pub comptime fn generate_public_dispatch(m: Module, generate_emit_public_init_nullifier: bool) -> Quoted {
let functions = get_public_functions(m);

let unit = get_type::<()>();

let seen_selectors = &mut CHashMap::<Field, Quoted>::new();

let ifs = functions.map(|function: FunctionDefinition| {
let mut ifs = functions.map(|function: FunctionDefinition| {
let parameters = function.parameters();
let return_type = function.return_type();

let selector: Field = compute_fn_selector(function);
let fn_name = function.name();
let selector: Field = compute_fn_selector(fn_name, parameters);

// Since function selectors are computed as the first 4 bytes of the hash of the function signature, it's
// possible to have collisions. With the following check, we ensure it doesn't happen within the same contract.
Expand Down Expand Up @@ -96,6 +101,22 @@ pub comptime fn generate_public_dispatch(m: Module) -> Quoted {
if_
});

// If we injected the auto-generated public function to emit the public initialization nullifier, then
// we'll also need to handle its dispatch.
if generate_emit_public_init_nullifier {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is much better!

let name = EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME;
let init_nullifier_selector: Field = compute_fn_selector(name, @[]);

ifs = ifs.push_back(
quote {
if selector == $init_nullifier_selector {
$name();
aztec::oracle::avm::avm_return([]);
}
},
);
}

if ifs.len() == 0 {
// No dispatch function if there are no public functions
quote {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::macros::{
functions::initialization_utils::{EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME, has_public_init_checked_functions},
internals_functions_generation::external_functions_registry::get_private_functions,
utils::is_fn_initializer,
};

/// Returns `(has_public_init_nullifier_fn, fn_body)` for the auto-generated public init nullifier function.
///
/// Contracts with a private initializer and public functions that check initialization need an auto-generated public
/// function to emit the public init nullifier. If these conditions are met, returns `(true, fn_body_quoted)`;
/// otherwise returns `(false, quote {})`.
pub(crate) comptime fn generate_emit_public_init_nullifier(m: Module) -> (bool, Quoted) {
let has_private_initializer = get_private_functions(m).any(|f: FunctionDefinition| is_fn_initializer(f));
let has_public_fns_with_init_check = has_public_init_checked_functions(m);

if has_private_initializer & has_public_fns_with_init_check {
let name = EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME;
let assertion_message = f"Function {name} can only be called by the same contract".as_quoted_str();
let body = quote {
#[aztec::macros::internals_functions_generation::abi_attributes::abi_public]
unconstrained fn $name() {
let context = aztec::context::PublicContext::new(|| { 0 });
assert(
context.maybe_msg_sender().unwrap() == context.this_address(),
$assertion_message,
);
aztec::macros::functions::initialization_utils::mark_as_initialized_public(context);
}
};
(true, body)
} else {
(false, quote {})
}
}
Original file line number Diff line number Diff line change
@@ -1,47 +1,117 @@
use crate::protocol::{
abis::function_selector::FunctionSelector, address::AztecAddress, constants::DOM_SEP__INITIALIZER,
hash::poseidon2_hash_with_separator, traits::ToField,
abis::function_selector::FunctionSelector,
address::AztecAddress,
constants::{DOM_SEP__INITIALIZER, DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER},
hash::poseidon2_hash_with_separator,
traits::ToField,
};
use std::meta::{ctstring::AsCtString, unquote};

use crate::{
context::{PrivateContext, PublicContext},
macros::{
internals_functions_generation::external_functions_registry::get_public_functions, utils::fn_has_noinitcheck,
},
nullifier::utils::compute_nullifier_existence_request,
oracle::get_contract_instance::{
get_contract_instance, get_contract_instance_deployer_avm, get_contract_instance_initialization_hash_avm,
},
};

// Used by `create_mark_as_initialized` (you won't find it through searching)
/// The name of the auto-generated function that emits the public init 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.
Comment on lines +26 to +28
Copy link
Contributor

Choose a reason for hiding this comment

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

If we do this, it then means that mark_as_initialized_public is a dangerous functiont hat should never called manually. All of the functions in that mod are pub because technically they are in user code (used by macros). We should make it clear in their docs that they should not be used unless you know very well what you're doing.

pub(crate) comptime fn has_public_init_checked_functions(m: Module) -> bool {
get_public_functions(m).any(|f: FunctionDefinition| !fn_has_noinitcheck(f))
}

/// Selector for `EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME`, derived at comptime so it stays in sync.
global EMIT_PUBLIC_INIT_NULLIFIER_SELECTOR: FunctionSelector = comptime {
let name = EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME;
let sig = f"{name}()".as_ctstring();
unquote!(quote { FunctionSelector::from_signature($sig) })
};

/// Emits (only) the public initialization nullifier.
///
/// This function is called by the aztec-nr auto-generated external public contract function (enqueued by private
/// [`initializer`](crate::macros::functions::initializer) functions), and also by
/// [`mark_as_initialized_from_public_initializer`] for public initializers.
///
/// # Warning
///
/// 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_unsiloed_contract_initialization_nullifier((context).this_address());
let init_nullifier = compute_public_init_nullifier(context.this_address());
context.push_nullifier(init_nullifier);
}

// Used by `create_mark_as_initialized` (you won't find it through searching)
pub fn mark_as_initialized_private(context: &mut PrivateContext) {
let init_nullifier = compute_unsiloed_contract_initialization_nullifier((*context).this_address());
fn mark_as_initialized_private(context: &mut PrivateContext) {
let init_nullifier = compute_private_init_nullifier((*context).this_address());
context.push_nullifier(init_nullifier);
}

// Used by `create_init_check` (you won't find it through searching)
/// 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.
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 {
context.call_public_function((*context).this_address(), EMIT_PUBLIC_INIT_NULLIFIER_SELECTOR, [], false);
}
}

/// Emits both initialization nullifiers (private and public).
///
/// 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());
context.push_nullifier(private_nullifier);
mark_as_initialized_public(context);
}

/// Asserts that the contract has been initialized, from public's perspective.
///
/// Checks that the public initialization nullifier exists.
pub fn assert_is_initialized_public(context: PublicContext) {
let init_nullifier = compute_unsiloed_contract_initialization_nullifier(context.this_address());
// Safety: TODO(F-239) - this is currently unsafe, we cannot rely on the nullifier existing to determine that any
// public component of contract initialization has been complete.
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.
assert(context.nullifier_exists_unsafe(init_nullifier, context.this_address()), "Not initialized");
}

// Used by `create_init_check` (you won't find it through searching)
/// 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_unsiloed_contract_initialization_nullifier(context.this_address());
let init_nullifier = compute_private_init_nullifier(context.this_address());
let nullifier_existence_request = compute_nullifier_existence_request(init_nullifier, context.this_address());
context.assert_nullifier_exists(nullifier_existence_request);
}

fn compute_unsiloed_contract_initialization_nullifier(address: AztecAddress) -> Field {
// 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()
Copy link
Contributor

Choose a reason for hiding this comment

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

(not just that, this is also not domain separated)

}

fn compute_public_init_nullifier(address: AztecAddress) -> Field {
poseidon2_hash_with_separator(
[address.to_field()],
DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER,
)
}

// Used by `create_assert_correct_initializer_args` (you won't find it through searching)
pub fn assert_initialization_matches_address_preimage_public(context: PublicContext) {
let address = context.this_address();
Expand Down
Loading
Loading