Skip to content
Closed
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
35 changes: 34 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,40 @@ 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.

### [Aztec.nr] Changes to standard note hash computation

Note hashes used to be computed with the storage slot being the last value of the preimage, it is now the first. This is to make it easier to ensure all note hashes have proper domain separation.

This change requires no input from your side unless you were testing or relying on hardcoded note hashes.
### [Aztec.js] `getPublicEvents` now returns an object instead of an array

`getPublicEvents` now returns a `GetPublicEventsResult<T>` object with `events` and `maxLogsHit` fields instead of a plain array. This enables pagination through large result sets using the new `afterLog` filter option.
Expand All @@ -23,7 +57,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
31 changes: 18 additions & 13 deletions noir-projects/aztec-nr/aztec/src/macros/aztec.nr
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use crate::macros::{
calls_generation::{
external_functions::{generate_external_function_calls, generate_external_function_self_calls_structs},
internal_functions::generate_call_internal_struct,
},
dispatch::generate_public_dispatch,
internals_functions_generation::{create_fn_abi_exports, process_functions},
notes::NOTES,
storage::STORAGE_LAYOUT_NAME,
utils::{
get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test,
module_has_storage,
use crate::{
macros::{
calls_generation::{
external_functions::{generate_external_function_calls, generate_external_function_self_calls_structs},
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,
utils::{
get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test,
module_has_storage,
},
},
};

Expand Down Expand Up @@ -53,7 +56,8 @@ pub comptime fn aztec(m: Module) -> 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 @@ -65,6 +69,7 @@ pub comptime fn aztec(m: Module) -> Quoted {
$public_dispatch
$sync_state_fn_and_abi_export
$process_message_fn_and_abi_export
$emit_public_init_nullifier_fn_body
}
}

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 {
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.
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()
}

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