Skip to content

feat!: include init_hash in private initialization nullifier to prevent privacy leak#21427

Merged
nventuro merged 12 commits intomerge-train/fairiesfrom
feat/private-init-nullifier-privacy-fix
Mar 17, 2026
Merged

feat!: include init_hash in private initialization nullifier to prevent privacy leak#21427
nventuro merged 12 commits intomerge-train/fairiesfrom
feat/private-init-nullifier-privacy-fix

Conversation

@nchamo
Copy link
Contributor

@nchamo nchamo commented Mar 12, 2026

Problem

The private initialization nullifier was computed as just address.to_field(). Anyone who knows a contract's address can compute this nullifier and check for its existence in the nullifier tree, revealing whether the contract has been initialized. This is a privacy leak for fully private contracts.

Fix

The private initialization nullifier is now computed as poseidon2_hash(address, init_hash) with a dedicated domain separator (DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER). Since init_hash is not publicly available for fully private contracts, address knowledge alone is no longer sufficient to determine initialization status.

Fixes F-194
Fixes #17128

@nchamo nchamo self-assigned this Mar 12, 2026
@nchamo nchamo added the ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure label Mar 12, 2026
@nchamo nchamo marked this pull request as draft March 12, 2026 15:11
@nchamo nchamo added the ci-draft Run CI on draft PRs. label Mar 12, 2026

const mockContractInstance = async (artifact: ContractArtifact, address: AztecAddress) => {
contracts[address.toString()] = artifact;
const mockContractInstance = async (artifact: ContractArtifact) => {
Copy link
Contributor Author

@nchamo nchamo Mar 12, 2026

Choose a reason for hiding this comment

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

Now assert_is_initialized_private (in Noir) calls get_contract_instance(address) to fetch the contract's initialization_hash. That oracle validates that instance.to_address() == address, meaning the instance fields must actually hash to the expected address. Previously, the tests used AztecAddress.random() with incomplete/fake contract instances, which broke under this new validation.

So we made a few changes to fix this inconsistency and now, instead of using a random address, we use the actual instance's address

contractAddress: parentAddress,
anchorBlockHeader,
artifact: ParentContractArtifact,
artifact: parentContractArtifact,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a bug, we were supposed to use the clone here


```rust
use dep::aztec::history::deployment::assert_contract_was_initialized_by;
use dep::aztec::oracle::get_contract_instance::get_contract_instance;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just updating some docs

@nchamo nchamo marked this pull request as ready for review March 12, 2026 19:11
@nchamo nchamo requested a review from benesjan March 12, 2026 19:11

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

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

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

@nchamo nchamo Mar 16, 2026

Choose a reason for hiding this comment

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

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

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

Comment on lines +710 to +711
const parentAddress = await mockContractInstance(ParentContractArtifact);
const childAddress = await mockContractInstance(ChildContractArtifact);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this test, the parent's entry point does a nested call to Child. Both contracts have initializers, so they will both call assert_is_initialized_private during private execution. And the thing is that that the oracle validates instance.to_address() == address, which fails with random addresses

So we create mocks, and use their addresses instead

@benesjan
Copy link
Contributor

Feel free to request my review again once you address Nico's feedback 👍

@benesjan benesjan removed their request for review March 16, 2026 06:23
nchamo added 6 commits March 16, 2026 14:22
…rivate-init-nullifier-privacy-fix

# Conflicts:
#	docs/docs-developers/docs/resources/migration_notes.md
#	noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr
…rivate-init-nullifier-privacy-fix

# Conflicts:
#	yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts
@nchamo nchamo requested a review from nventuro March 16, 2026 19:45
@nchamo nchamo requested a review from nventuro March 17, 2026 19:27
@nventuro nventuro merged commit 1c9652a into merge-train/fairies Mar 17, 2026
11 checks passed
@nventuro nventuro deleted the feat/private-init-nullifier-privacy-fix branch March 17, 2026 21:11
@AztecBot
Copy link
Collaborator

❌ Failed to cherry-pick to v4-next due to conflicts. (🤖) View backport run.

github-merge-queue bot pushed a commit that referenced this pull request Mar 18, 2026
BEGIN_COMMIT_OVERRIDE
fix(stdlib): accept null return_type for void Noir functions (#21647)
feat!: make AES128 decrypt oracle return Option (#21696)
fix(aztec-nr): fix OOB index with nonzero offset (#21613)
feat!: include init_hash in private initialization nullifier to prevent
privacy leak (#21427)
END_COMMIT_OVERRIDE
AztecBot pushed a commit that referenced this pull request Mar 18, 2026
…fier (PR #21427)

Cherry-pick of merge commit 1c9652a with conflicts.
AztecBot added a commit that referenced this pull request Mar 18, 2026
Resolved conflicts in:
- migration_notes.md: kept only init_hash migration note (dropped unrelated next-only notes)
- deployment_proofs.nr: included new wrong-init-hash test and updated error message
- constants_tests.nr: added DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER import, updated tester size
- private_execution.test.ts: merged imports from both sides
AztecBot pushed a commit that referenced this pull request Mar 18, 2026
…fier (PR #21427)

Cherry-pick of merge commit 1c9652a with conflicts.
AztecBot added a commit that referenced this pull request Mar 18, 2026
Resolved conflicts in:
- migration_notes.md: kept only init_hash migration note
- constants_tests.nr: added DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER import, tester size <51, 45>
- private_execution.test.ts: merged imports from both sides
AztecBot pushed a commit that referenced this pull request Mar 18, 2026
…fier (PR #21427)

Cherry-pick of merge commit 1c9652a with conflicts.
AztecBot added a commit that referenced this pull request Mar 18, 2026
Resolved conflicts in:
- migration_notes.md: kept only init_hash migration note
- constants_tests.nr: added DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER import, tester size <51, 45>
- private_execution.test.ts: merged imports from both sides
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-to-v4-next ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants