diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md index 2e54b20ab5f4..b3c3cc6105c4 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md @@ -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 diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index b7d1acf80b68..93ad52afb2ac 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -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: diff --git a/noir-projects/aztec-nr/aztec/src/history/deployment.nr b/noir-projects/aztec-nr/aztec/src/history/deployment.nr index e78362647818..708351e0cfe8 100644 --- a/noir-projects/aztec-nr/aztec/src/history/deployment.nr +++ b/noir-projects/aztec-nr/aztec/src/history/deployment.nr @@ -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). @@ -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), + ); } diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr index 7c816adfda32..2b1d6b32fa0e 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr @@ -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, }; @@ -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)) } @@ -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 { @@ -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); } @@ -83,10 +93,10 @@ 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"); } @@ -94,18 +104,26 @@ 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_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, diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index b9db8529f058..3965e62210fe 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -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 @@ -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 diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr index c285062e440a..926ccdc1129a 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr @@ -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}, }; @@ -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(); @@ -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), @@ -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( @@ -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 = "Cannot prove nullifier non-inclusion")] 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( @@ -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 = "Cannot prove nullifier non-inclusion")] 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, + ); }, ); } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 116fb2002d64..697ca3d47b5c 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -714,6 +714,7 @@ pub global DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT: u32 = 623934423; 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; pub global DOM_SEP__SECRET_HASH: u32 = 4199652938; pub global DOM_SEP__TX_NULLIFIER: u32 = 1025801951; diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr index 6a5048b6926f..54770daf5330 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr @@ -13,14 +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__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__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}, @@ -130,7 +131,7 @@ impl HashedValueTester::new(); + let mut tester = HashedValueTester::<52, 45>::new(); // ----------------- // Domain separators @@ -188,6 +189,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"); diff --git a/yarn-project/aztec.js/src/wallet/wallet.test.ts b/yarn-project/aztec.js/src/wallet/wallet.test.ts index 44e11dc8f9a9..221c2e9e35ac 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.test.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.test.ts @@ -107,7 +107,7 @@ describe('WalletSchema', () => { const result = await context.client.getContractMetadata(await AztecAddress.random()); expect(result).toEqual({ instance: undefined, - isContractInitialized: expect.any(Boolean), + isContractInitialized: undefined, isContractPublished: expect.any(Boolean), isContractUpdated: expect.any(Boolean), updatedContractClassId: undefined, @@ -354,7 +354,7 @@ describe('WalletSchema', () => { expect(results[0]).toEqual({ name: 'getChainInfo', result: { chainId: expect.any(Fr), version: expect.any(Fr) } }); expect(results[1]).toEqual({ name: 'getContractMetadata', - result: expect.objectContaining({ isContractInitialized: expect.any(Boolean) }), + result: expect.objectContaining({ isContractPublished: expect.any(Boolean) }), }); expect(results[2]).toEqual({ name: 'getContractClassMetadata', @@ -408,7 +408,7 @@ class MockWallet implements Wallet { getContractMetadata(_address: AztecAddress): Promise { return Promise.resolve({ instance: undefined, - isContractInitialized: false, + isContractInitialized: undefined, isContractPublished: false, isContractUpdated: false, updatedContractClassId: undefined, diff --git a/yarn-project/aztec.js/src/wallet/wallet.ts b/yarn-project/aztec.js/src/wallet/wallet.ts index 7f7b61befdcc..2c5ebd7e585a 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.ts @@ -208,8 +208,11 @@ export type PublicEvent = Event< export type ContractMetadata = { /** The contract instance */ instance?: ContractInstanceWithAddress; - /** Whether the contract has been initialized (init nullifier exists) */ - isContractInitialized: boolean; + /** + * Whether the contract has been initialized (initialization nullifier exists). + * Undefined when instance is not registered. + */ + isContractInitialized: boolean | undefined; /** Whether the contract instance is publicly deployed on-chain */ isContractPublished: boolean; /** Whether the contract has been updated to a different class */ @@ -371,7 +374,7 @@ export const PublicEventSchema = zodFor>()( export const ContractMetadataSchema = z.object({ instance: optional(ContractInstanceWithAddressSchema), - isContractInitialized: z.boolean(), + isContractInitialized: optional(z.boolean()), isContractPublished: z.boolean(), isContractUpdated: z.boolean(), updatedContractClassId: optional(schemas.Fr), diff --git a/yarn-project/cli-wallet/src/cmds/check_tx.ts b/yarn-project/cli-wallet/src/cmds/check_tx.ts index 907bc9cda800..1ff5ef54dbfe 100644 --- a/yarn-project/cli-wallet/src/cmds/check_tx.ts +++ b/yarn-project/cli-wallet/src/cmds/check_tx.ts @@ -5,7 +5,11 @@ import type { AztecNode } from '@aztec/aztec.js/node'; import { ProtocolContractAddress } from '@aztec/aztec.js/protocol'; import type { TxHash } from '@aztec/aztec.js/tx'; import type { LogFn } from '@aztec/foundation/log'; -import { siloNullifier } from '@aztec/stdlib/hash'; +import { + computeSiloedPrivateInitializationNullifier, + computeSiloedPublicInitializationNullifier, + siloNullifier, +} from '@aztec/stdlib/hash'; import { NoteDao } from '@aztec/stdlib/note'; import type { CLIWallet } from '../utils/wallet.js'; @@ -144,22 +148,49 @@ function toFriendlyAddress(address: AztecAddress, artifactMap: ArtifactMap) { async function getKnownNullifiers(wallet: CLIWallet, artifactMap: ArtifactMap) { const knownContracts = await wallet.getContracts(); - const deployerAddress = ProtocolContractAddress.ContractInstanceRegistry; - const classRegistryAddress = ProtocolContractAddress.ContractClassRegistry; + + const [contractResults, classResults] = await Promise.all([ + Promise.all(knownContracts.map(contract => getContractNullifiers(wallet, contract))), + Promise.all(Object.values(artifactMap).map(artifact => getClassNullifier(artifact))), + ]); + const initNullifiers: Record = {}; const deployNullifiers: Record = {}; const classNullifiers: Record = {}; - for (const contract of knownContracts) { - initNullifiers[(await siloNullifier(contract, contract.toField())).toString()] = contract; - deployNullifiers[(await siloNullifier(deployerAddress, contract.toField())).toString()] = contract; + + for (const { contract, deployNullifier, privateInitNullifier, publicInitNullifier } of contractResults) { + deployNullifiers[deployNullifier.toString()] = contract; + if (privateInitNullifier) { + initNullifiers[privateInitNullifier.toString()] = contract; + } + initNullifiers[publicInitNullifier.toString()] = contract; } - for (const artifact of Object.values(artifactMap)) { - classNullifiers[(await siloNullifier(classRegistryAddress, artifact.classId)).toString()] = - `${artifact.name}Class<${artifact.classId}>`; + for (const { nullifier, label } of classResults) { + classNullifiers[nullifier.toString()] = label; } + return { initNullifiers, deployNullifiers, classNullifiers }; } +async function getContractNullifiers(wallet: CLIWallet, contract: AztecAddress) { + const deployerAddress = ProtocolContractAddress.ContractInstanceRegistry; + const deployNullifier = await siloNullifier(deployerAddress, contract.toField()); + + const metadata = await wallet.getContractMetadata(contract); + const privateInitNullifier = metadata.instance + ? await computeSiloedPrivateInitializationNullifier(contract, metadata.instance.initializationHash) + : undefined; + const publicInitNullifier = await computeSiloedPublicInitializationNullifier(contract); + + return { contract, deployNullifier, privateInitNullifier, publicInitNullifier }; +} + +async function getClassNullifier(artifact: ContractArtifactWithClassId) { + const classRegistryAddress = ProtocolContractAddress.ContractClassRegistry; + const nullifier = await siloNullifier(classRegistryAddress, artifact.classId); + return { nullifier, label: `${artifact.name}Class<${artifact.classId}>` }; +} + type ArtifactMap = Record; type ContractArtifactWithClassId = ContractArtifact & { classId: Fr }; diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index c192fb14654e..42e360bec1f1 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -542,6 +542,7 @@ export enum DomainSeparator { PARTIAL_NOTE_VALIDITY_COMMITMENT = 623934423, INITIALIZATION_NULLIFIER = 1653084894, PUBLIC_INITIALIZATION_NULLIFIER = 3342006647, + PRIVATE_INITIALIZATION_NULLIFIER = 3990889078, SECRET_HASH = 4199652938, TX_NULLIFIER = 1025801951, SIGNATURE_PAYLOAD = 463525807, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index edf5e87ea101..4a0d68e4b49a 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -37,7 +37,6 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash, type BlockParameter } from '@aztec/stdlib/block'; import { CompleteAddress, - type ContractInstanceWithAddress, getContractClassFromArtifact, getContractInstanceFromInstantiationParams, } from '@aztec/stdlib/contract'; @@ -49,7 +48,7 @@ import { computeAppNullifierHidingKey, deriveKeys } from '@aztec/stdlib/keys'; import type { SiloedTag } from '@aztec/stdlib/logs'; import { L1Actor, L1ToL2Message, L2Actor } from '@aztec/stdlib/messaging'; import { Note, NoteDao } from '@aztec/stdlib/note'; -import { makeBlockHeader, makeL2Tips } from '@aztec/stdlib/testing'; +import { makeBlockHeader, makeL2Tips, randomContractInstanceWithAddress } from '@aztec/stdlib/testing'; import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; import { BlockHeader, @@ -130,7 +129,6 @@ describe('Private Execution test suite', () => { let anchorBlockHeader = BlockHeader.empty(); let logger: Logger; - let defaultContractAddress: AztecAddress; const ownerSk = Fr.fromHexString('2dcc5485a58316776299be08c78fa3788a1a7961ae30dc747fb1be17692a8d32'); const recipientSk = Fr.fromHexString('0c9ed344548e8f9ba8aa3c9f8651eaa2853130f6c1e9c050ccf198f7ea18a7ec'); const senderForTagsSk = Fr.fromHexString('2f0e5a8f3ba9c0738d6f3a9e0c2e13f7b2d4207f36efda729a2c6e2a5a9f8b1d'); @@ -171,15 +169,12 @@ describe('Private Execution test suite', () => { return expectedValue?.toString() === actualValue.toString(); }, 'Matches aztec addresses'); - const mockContractInstance = async (artifact: ContractArtifact, address: AztecAddress) => { - contracts[address.toString()] = artifact; + const mockContractInstance = async (artifact: ContractArtifact) => { const contractClass = await getContractClassFromArtifact(artifact); - - contractStore.getContractInstance.calledWith(aztecAddressMatcher(address)).mockResolvedValue({ - currentContractClassId: contractClass.id, - originalContractClassId: contractClass.id, - address, - } as ContractInstanceWithAddress); + const instance = await randomContractInstanceWithAddress({ contractClassId: contractClass.id }); + contracts[instance.address.toString()] = artifact; + contractStore.getContractInstance.calledWith(aztecAddressMatcher(instance.address)).mockResolvedValue(instance); + return instance.address; }; const runSimulator = async ({ @@ -201,9 +196,9 @@ describe('Private Execution test suite', () => { txContext?: Partial>; }) => { const functionArtifact = getFunctionArtifactByName(artifact, functionName); - contractAddress = contractAddress ?? defaultContractAddress; + contractAddress = contractAddress ?? (await mockContractInstance(artifact)); + contracts[contractAddress.toString()] = artifact; const selector = await FunctionSelector.fromNameAndParameters(functionName, functionArtifact.parameters); - await mockContractInstance(artifact, contractAddress); const hashedArguments = await HashedValues.fromArgs(encodeArguments(functionArtifact, args)); const txRequest = TxExecutionRequest.from({ @@ -308,8 +303,6 @@ describe('Private Execution test suite', () => { owner = ownerCompleteAddress.address; recipient = recipientCompleteAddress.address; senderForTags = senderForTagsCompleteAddress.address; - - defaultContractAddress = await AztecAddress.random(); }); beforeEach(async () => { @@ -553,9 +546,7 @@ describe('Private Execution test suite', () => { }; beforeEach(async () => { - contractAddress = await AztecAddress.random(); - - await mockContractInstance(StatefulTestContractArtifact, contractAddress); + contractAddress = await mockContractInstance(StatefulTestContractArtifact); }); it('should have a constructor with arguments that inserts notes', async () => { @@ -716,11 +707,10 @@ describe('Private Execution test suite', () => { it('parent should call child', async () => { const childArtifact = getFunctionArtifactByName(ChildContractArtifact, 'value'); - const parentAddress = await AztecAddress.random(); - const childAddress = await AztecAddress.random(); + const parentAddress = await mockContractInstance(ParentContractArtifact); + const childAddress = await mockContractInstance(ChildContractArtifact); const childSelector = await FunctionSelector.fromNameAndParameters(childArtifact.name, childArtifact.parameters); - await mockContractInstance(ChildContractArtifact, childAddress); logger.info(`Parent deployed at ${parentAddress.toString()}`); logger.info(`Calling child function ${childSelector.toString()} at ${childAddress.toString()}`); @@ -748,12 +738,10 @@ describe('Private Execution test suite', () => { it('syncs private state for child in nested calls', async () => { const childArtifact = getFunctionArtifactByName(ChildContractArtifact, 'value'); - const parentAddress = await AztecAddress.random(); - const childAddress = await AztecAddress.random(); + const parentAddress = await mockContractInstance(ParentContractArtifact); + const childAddress = await mockContractInstance(ChildContractArtifact); const childSelector = await FunctionSelector.fromNameAndParameters(childArtifact.name, childArtifact.parameters); - await mockContractInstance(ChildContractArtifact, childAddress); - contractStore.getFunctionCall.mockClear(); const args = [childAddress, childSelector]; @@ -773,7 +761,7 @@ describe('Private Execution test suite', () => { let contractAddress: AztecAddress; beforeEach(async () => { - contractAddress = await AztecAddress.random(); + contractAddress = await mockContractInstance(TestContractArtifact); }); describe('L1 to L2', () => { let bridgedAmount = 100n; @@ -991,10 +979,9 @@ describe('Private Execution test suite', () => { expect(childFunctionArtifact).toBeDefined(); childFunctionArtifact.isOnlySelf = isOnlySelf; - const childAddress = await AztecAddress.random(); - await mockContractInstance(childContractArtifact, childAddress); + const childAddress = await mockContractInstance(childContractArtifact); const childSelector = await FunctionSelector.fromSignature('pub_set_value(Field)'); - const parentAddress = await AztecAddress.random(); + const parentAddress = await mockContractInstance(ParentContractArtifact); const args = [childAddress, childSelector, 42n]; const result = await runSimulator({ @@ -1017,8 +1004,7 @@ describe('Private Execution test suite', () => { const parentFunctionArtifact = parentContractArtifact.functions.find(fn => fn.name === 'public_dispatch')!; expect(parentFunctionArtifact).toBeDefined(); - const parentAddress = await AztecAddress.random(); - await mockContractInstance(parentContractArtifact, parentAddress); + const parentAddress = await mockContractInstance(parentContractArtifact); // Only recurse once, so that we only enqueue 2 calls. #total-args should be low. const args = [/*remainingRecursions=*/ 1]; @@ -1026,7 +1012,7 @@ describe('Private Execution test suite', () => { msgSender: parentAddress, contractAddress: parentAddress, anchorBlockHeader, - artifact: ParentContractArtifact, + artifact: parentContractArtifact, functionName: 'enqueue_call_to_child_with_many_args_and_recurse', args, }); @@ -1038,8 +1024,7 @@ describe('Private Execution test suite', () => { const parentFunctionArtifact = parentContractArtifact.functions.find(fn => fn.name === 'public_dispatch')!; expect(parentFunctionArtifact).toBeDefined(); - const parentAddress = await AztecAddress.random(); - await mockContractInstance(parentContractArtifact, parentAddress); + const parentAddress = await mockContractInstance(parentContractArtifact); // 10 recursions (11 enqueued public calls) should overflow the total args limit // since each call enqueues a call with max / 10 args (plus 1 each time for function selector) @@ -1049,7 +1034,7 @@ describe('Private Execution test suite', () => { msgSender: parentAddress, contractAddress: parentAddress, anchorBlockHeader, - artifact: ParentContractArtifact, + artifact: parentContractArtifact, functionName: 'enqueue_call_to_child_with_many_args_and_recurse', args, }), @@ -1074,24 +1059,19 @@ describe('Private Execution test suite', () => { describe('setting fee payer', () => { it('should default to not being a fee payer', async () => { - // arbitrary random function that doesn't set a fee payer - const contractAddress = await AztecAddress.random(); const { entrypoint: result } = await runSimulator({ artifact: TestContractArtifact, anchorBlockHeader, functionName: 'get_this_address', - contractAddress, }); expect(result.publicInputs.isFeePayer).toBe(false); }); it('should be able to set a fee payer', async () => { - const contractAddress = await AztecAddress.random(); const { entrypoint: result } = await runSimulator({ artifact: TestContractArtifact, anchorBlockHeader, functionName: 'test_setting_fee_payer', - contractAddress, }); expect(result.publicInputs.isFeePayer).toBe(true); }); @@ -1099,13 +1079,10 @@ describe('Private Execution test suite', () => { describe('phase checking', () => { it('should be able to end setup checking phases', async () => { - // arbitrary random function that doesn't set a fee payer - const contractAddress = await AztecAddress.random(); const { entrypoint: result } = await runSimulator({ artifact: TestContractArtifact, anchorBlockHeader, functionName: 'end_setup_checking_phases', - contractAddress, }); const minRevertibleSideEffectCounter = result.publicInputs.minRevertibleSideEffectCounter.toNumber(); const expectedNonRevertibleSideEffectCounter = @@ -1119,17 +1096,12 @@ describe('Private Execution test suite', () => { }); describe('pending note hashes contract', () => { - beforeEach(async () => { - await mockContractInstance(PendingNoteHashesContractArtifact, defaultContractAddress); - }); - it('should be able to insert, read, and nullify pending note hashes in one call', async () => { noteStore.getNotes.mockResolvedValue([]); const amountToTransfer = 100n; - const contractAddress = await AztecAddress.random(); - + const contractAddress = await mockContractInstance(PendingNoteHashesContractArtifact); const sender = owner; const args = [amountToTransfer, owner, sender]; const { entrypoint: result } = await runSimulator({ @@ -1290,7 +1262,7 @@ describe('Private Execution test suite', () => { describe('Context oracles', () => { it('this_address should return the current context address', async () => { - const contractAddress = await AztecAddress.random(); + const contractAddress = await mockContractInstance(TestContractArtifact); const { entrypoint: result } = await runSimulator({ artifact: TestContractArtifact, diff --git a/yarn-project/stdlib/src/hash/hash.ts b/yarn-project/stdlib/src/hash/hash.ts index fe966082b1ef..afc80031b1a4 100644 --- a/yarn-project/stdlib/src/hash/hash.ts +++ b/yarn-project/stdlib/src/hash/hash.ts @@ -58,6 +58,35 @@ export function siloNullifier(contract: AztecAddress, innerNullifier: Fr): Promi return poseidon2HashWithSeparator([contract, innerNullifier], DomainSeparator.SILOED_NULLIFIER); } +/** + * Computes the siloed private initialization nullifier for a contract, given its address and initialization hash. + * @param contract - The contract address. + * @param initializationHash - The contract's initialization hash. + * @returns The siloed private initialization nullifier. + */ +export async function computeSiloedPrivateInitializationNullifier( + contract: AztecAddress, + initializationHash: Fr, +): Promise { + const innerNullifier = await poseidon2HashWithSeparator( + [contract, initializationHash], + DomainSeparator.PRIVATE_INITIALIZATION_NULLIFIER, + ); + return siloNullifier(contract, innerNullifier); +} + +/** + * Computes the siloed public initialization nullifier for a contract. Not all contracts emit this nullifier: it is only + * emitted when the contract has public functions that perform initialization checks (i.e. external public functions that + * are not `#[noinitcheck]` or `#[only_self]`). + * @param contract - The contract address. + * @returns The siloed public initialization nullifier. + */ +export async function computeSiloedPublicInitializationNullifier(contract: AztecAddress): Promise { + const innerNullifier = await poseidon2HashWithSeparator([contract], DomainSeparator.PUBLIC_INITIALIZATION_NULLIFIER); + return siloNullifier(contract, innerNullifier); +} + /** * Computes the protocol nullifier, which is the hash of the initial tx request siloed with the null msg sender address. * @param txRequestHash - The hash of the initial tx request. diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index 37ac0238cf25..762d520c156e 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -50,7 +50,7 @@ import { } from '@aztec/stdlib/contract'; import { SimulationError } from '@aztec/stdlib/errors'; import { Gas, GasSettings } from '@aztec/stdlib/gas'; -import { siloNullifier } from '@aztec/stdlib/hash'; +import { computeSiloedPrivateInitializationNullifier } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { BlockHeader, @@ -478,17 +478,36 @@ export abstract class BaseWallet implements Wallet { return decodedEvents; } + /** + * Returns metadata about a contract, including whether it has been initialized, published, and updated. + * + * `isContractInitialized` requires the contract instance to be registered in the PXE (for `init_hash`). When the + * instance is not available, `isContractInitialized` is `undefined` since it cannot be determined. + * @param address - The contract address to query. + */ async getContractMetadata(address: AztecAddress) { const instance = await this.pxe.getContractInstance(address); - const initNullifier = await siloNullifier(address, address.toField()); - const publiclyRegisteredContract = await this.aztecNode.getContract(address); - const initNullifierMembershipWitness = await this.aztecNode.getNullifierMembershipWitness('latest', initNullifier); + const publiclyRegisteredContractPromise = this.aztecNode.getContract(address); + // We check only the private initialization nullifier. It is emitted by both private and public initializers and + // includes init_hash, preventing observers from determining initialization status from the address alone. Without + // the instance (and thus init_hash), we can't compute it, so we return undefined. + // + // We skip the public initialization nullifier because it's not always emitted (contracts without public external + // functions that require initialization checks won't emit it). If the private one exists, the public one was + // created in the same tx and will also be present. + let isContractInitialized: boolean | undefined = undefined; + if (instance) { + const initNullifier = await computeSiloedPrivateInitializationNullifier(address, instance.initializationHash); + const witness = await this.aztecNode.getNullifierMembershipWitness('latest', initNullifier); + isContractInitialized = !!witness; + } + const publiclyRegisteredContract = await publiclyRegisteredContractPromise; const isContractUpdated = publiclyRegisteredContract && !publiclyRegisteredContract.currentContractClassId.equals(publiclyRegisteredContract.originalContractClassId); return { instance: instance ?? undefined, - isContractInitialized: !!initNullifierMembershipWitness, + isContractInitialized, isContractPublished: !!publiclyRegisteredContract, isContractUpdated: !!isContractUpdated, updatedContractClassId: isContractUpdated ? publiclyRegisteredContract.currentContractClassId : undefined,