From 0118d914e2b77918be7c5d055cb13f9b30ccf35b Mon Sep 17 00:00:00 2001 From: IlyasRidhuan Date: Mon, 27 Apr 2026 09:33:14 +0000 Subject: [PATCH 1/5] AZIP-9: Private Immutables --- AZIPs/azip-9.md | 220 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 AZIPs/azip-9.md diff --git a/AZIPs/azip-9.md b/AZIPs/azip-9.md new file mode 100644 index 0000000..f6ed410 --- /dev/null +++ b/AZIPs/azip-9.md @@ -0,0 +1,220 @@ +# AZIP-9: Private Immutables + +## Preamble + + +| `azip` | `title` | `description` | `author` | `discussions-to` | `status` | `category` | `created` | +| --- | --- | --- | --- | --- | --- | --- | --- | +|9| Private Immutables | Adds private immutables | Ilyas Ridhuan (@IlyasRidhuan) | - | Draft | Core | 2026-04-28 | + + +## Abstract + +This AZIP enables deploy-time private immutables as a first-class primitive of the protocol. These immutables are committed to via an `immutables_hash` as part of the contract address. + + +## Impacted Stakeholders + +This AZIP changes contract address derivation, so every stakeholder that consumes, displays, or persists an Aztec address is affected on the next version of Aztec that adopts it. See [Backwards Compatibility](#backwards-compatibility) for the underlying change. + +### End Users +A user's contracts (e.g. account contracts, deployed app contracts) will resolve to different addresses on the version of Aztec that adopts this AZIP than on prior versions. Users wishing to interact with both old and new versions will need to track both addresses for the same logical contract. + +### App Developers +Contract authors can use the `#[immutable]` attribute (parallel to `#[storage]`) to bind a struct of deploy-time-immutable values to the contract address with protocol-level enforcement. + + +### Wallets / PXE Implementers +The PXE's contract-registration API has an `immutables` parameter. The PXE verifies the supplied preimage against `instance.immutables_hash` and stores it in a reserved capsule slot keyed by `(contractAddress, IMMUTABLES_CAPSULE_SLOT, contractAddress)`. + +Wallets and PXE implementations that intend to operate across rollup versions MUST use versions of `aztec.js` / `aztec-nr` / PXE / node software capable of computing both the pre- and post-AZIP-9 address-derivation schemes. + +### Infrastructure Providers (Indexers, P2P Nodes, Block Explorers) +Off-chain decoders of the `ContractInstancePublished` event must be updated to handle the new payload shape (one additional `Field`). `SerializableContractInstance.VERSION` is bumped. + +Tooling that indexes or displays addresses across rollup versions MUST be able to compute both the pre- and post-AZIP-9 address-derivation schemes. + +## Motivation + +Adding a private immutable value to an Aztec contract today involves writing an initialiser function that writes a note to private storage. + +There are contract designs which wouldn't otherwise require an initialiser function to be called, or which might not even need a transaction to instantiate the contract. Where applicable, avoiding such notes, function calls, and transactions would reduce deployment complexity, reduce proving times, and reduce dollar-costs. + +Alternatively, third-party libraries exist that "hack" immutable values into contracts by overloading existing fields (e.g. salt) in the instance see (Wonderland's [Initializer-less Contracts: The Immutables Macro](https://forum.aztec.network/t/initializerless-contracts-the-immutables-macro/8512) demonstrating this pattern. + +## Specification + + +### Change to the Contract Instance and Event payload +The `ContractInstance` struct SHALL have an additional `Field` member: + +```noir +pub struct ContractInstance { + salt: Field, + deployer: AztecAddress, + class_id: ContractClassId, + initialization_hash: Field, + public_keys: PublicKeys, + immutables_hash: Field, +} +``` + +### Change to Address Derivation +The `immutables_hash` MUST be included as part of the `salted_initialization_hash` when computing the pre-address of an Aztec address. + +```noir +salted_initialization_hash = Poseidon2::hash([DOM_SEP__SALTED_INITIALIZATION_HASH, salt, initialization_hash, deployer, immutables_hash]) +``` +For contracts with no immutable values, `immutables_hash` is `0`, matching the default-zero convention already used for `salt`, `initialization_hash`, and `deployer`. The protocol cannot enforce this convention — the deployer chooses the `immutables_hash` they commit to in `salted_initialization_hash` — but a deployer who deviates from it produces an address that off-the-shelf tooling will not derive, so users will not interact with the contract at that address (see [Security Considerations](#security-considerations)). + +This does require an additional Poseidon2 permutation round. + +The diagram below shows the address-derivation pipeline as modified by this AZIP. Inputs and intermediate values introduced by AZIP-9 are highlighted; everything else is the existing pipeline, unchanged. + +```mermaid +flowchart TD + %% --- NEW: immutables hash --- + DSI["DOM_SEP__IMMUTABLES_HASH"]:::new + IV["immutable values
(list of Field)"]:::new + H1(("Poseidon2")):::new + IH["immutables_hash"]:::new + DSI --> H1 + IV --> H1 + H1 --> IH + + %% --- salted initialisation hash --- + DSS["DOM_SEP__INIT_HASH"] + SALT["salt"] + INIT["initialisation_hash"] + DEP["deployer_address"] + H2(("Poseidon2")) + SIH["salted_initialisation_hash"] + DSS --> H2 + SALT --> H2 + INIT --> H2 + DEP --> H2 + IH --> H2 + H2 --> SIH + + %% --- partial address --- + DSPA["DOM_SEP__PARTIAL_ADDR"] + CID["contract_class_id"] + H3(("Poseidon2")) + PA["partial_address"] + DSPA --> H3 + CID --> H3 + SIH --> H3 + H3 --> PA + + %% --- public keys hash --- + DSPK["DOM_SEP__PUBLIC_KEYS_HASH"] + NPK["npk_m"] + IVPK["ivpk_m"] + OVPK["ovpk_m"] + TPK["tpk_m"] + H4(("Poseidon2")) + PKH["pk_hash
(public_keys_hash)"] + DSPK --> H4 + NPK --> H4 + IVPK --> H4 + OVPK --> H4 + TPK --> H4 + H4 --> PKH + + %% --- pre address --- + DSCA["DOM_SEP__CONTRACT_ADDR_V1"] + H5(("Poseidon2")) + PRE["pre_addr"] + DSCA --> H5 + PKH --> H5 + PA --> H5 + H5 --> PRE + + %% --- address point and address --- + EC["pre_addr · G + ivpk_m"] + AP["address_point"] + ADDR["address
(x-coordinate)"] + PRE --> EC + IVPK -. reused .-> EC + EC --> AP + AP --> ADDR + + classDef new fill:#fff4cd,stroke:#d4a017,stroke-width:2px,color:#000 +``` + +### Change to the Oracle Interface +The oracle interface between the PXE and a private function — which feeds a `ContractInstance` into a private execution — forms part of the protocol, since alternative smart-contract frameworks must be able to execute one another's contracts. The oracle response that returns a `ContractInstance` MUST be updated to include the new `immutables_hash` field. Any framework consuming this oracle output MUST handle the additional field when reconstructing or hashing the instance. + +### Change to AVM Opcodes +The AVM opcode that loads a `ContractInstance` (e.g. `GETCONTRACTINSTANCE`) MUST return the new payload shape including `immutables_hash`. The AVM's serialization layout for `ContractInstance` is bumped accordingly. + +### Change to Kernel Circuits +Kernel circuits that validate the contract address of the function call being processed MUST use the updated `salted_initialization_hash` (which now includes `immutables_hash`) when re-deriving and checking that address. This applies to any kernel-level address-derivation or address-membership check tied to a `ContractInstance`. + +## Rationale + +By committing to the hash as part of the contract address, the protocol is flexible in that it does not enforce any "shape" to the preimage of the `immutables_hash`, nor any limit to the set of immutable values. The `immutables_hash` is not validated on deployment, instead it is validated on-use and only by functions that require access to the set of immutables. + +### Suggested Immutables Hashing Scheme +A new domain separator `DOM_SEP__IMMUTABLES_HASH` could be defined as: + +```noir +DOM_SEP__IMMUTABLES_HASH: u32 = poseidon2_hash_bytes(b"az_dom_sep__immutables_hash"); +``` + +The `immutables_hash` itself is defined as the flat hash of the list of immutable values in the contract. +```noir +immutables_hash = Poseidon2::hash([DOM_SEP__IMMUTABLES_HASH, ]); +``` + +### Suggested Contract API and Verification Flow +The protocol does not mandate a specific smart-contract syntax for declaring or accessing immutables; the choice belongs to smart-contract framework maintainers. The following is offered as a suggestion that aztec-nr could adopt. + +A new `#[immutable]` attribute could be provided for contract authors who need to declare a struct as immutable, following the approach taken by `#[storage]`. + +```noir +#[immutable] +struct Immutables { + some_immutable_val: AztecAddress, + another_immutable_val: u8, +} +``` + +The `#[external]`, `#[internal]`, and `#[private]` function macros could inspect the function body and inject initialisation code only for functions that reference `self.immutables`. + +The injected runtime verification would be: + +1) Read `this_address` from the context. +2) Constrained retrieval of the `ContractInstance` using `this_address`. +3) Load the list of immutable values from the capsule store, using the defined reserved slot `IMMUTABLES_CAPSULE_SLOT`. +4) Compute the `immutables_hash` of the capsule-returned values and compare it to the value stored within the `ContractInstance`. +5) Deserialise and return the `Immutables` value to the function. + +The estimated cost for this verification is ~2k gates. + +A separate AZIP — likely from smart-contract framework maintainers — could standardise a preimage layout for the `immutables_hash` (e.g. a leading "shape" identifier distinguishing flat hashing from a Merkle tree, followed by the data). + +### Eager Verification +Unlike `#[storage]`, where work is only done per-access to `.read()` / `.write()` calls, the immutables are eagerly verified when `self.immutables` is referenced. Subsequent accesses via `self.immutables.` are free, and so the cost of verification is amortised over multiple reads. + + +### Why no Public Immutables +Public immutables are excluded from this proposal for now. Their inclusion would require discussions around: +1) Setting limits on the number of immutable values available to public functions. +2) Efficient broadcasting of these values to sequencers. +3) Additional opcodes to allow public functions to access these immutable values. + +## Backwards Compatibility: + +This AZIP is NOT backwards compatible and represents a breaking change to the protocol. The AZIP MUST therefore be shipped as part of a new Aztec rollup. + +1) **Address invalidation**. Every existing contract address is derived from a four-input `salted_initialization_hash`. The additional input of `immutables_hash` will produce a different hash. +2) **`ContractInstance` and `ContractInstancePublished` event consumers** MUST parse the new payload that contains the additional field. + +## Security Considerations +As `immutables_hash` is not validated on deployment, a deployer could grief a user by deploying with a different `immutables_hash` than the one yielded by the user-specified immutable values. However, a user is able to pre-compute the address that the instance should be deployed at, and so will never interact with the contract with an invalid `immutables_hash`. + + +## Copyright Waiver: +Copyright and related rights waived via [CC0](https://github.com/AztecProtocol/governance/blob/main/LICENSE). + From 2cc7d38bb700b50ab9cb7ba9f1e24cc3a5c15417 Mon Sep 17 00:00:00 2001 From: Ilyas Ridhuan Date: Tue, 28 Apr 2026 13:41:01 +0100 Subject: [PATCH 2/5] Update AZIPs/azip-9.md Co-authored-by: Michael Connor --- AZIPs/azip-9.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AZIPs/azip-9.md b/AZIPs/azip-9.md index f6ed410..6629fdb 100644 --- a/AZIPs/azip-9.md +++ b/AZIPs/azip-9.md @@ -5,7 +5,7 @@ | `azip` | `title` | `description` | `author` | `discussions-to` | `status` | `category` | `created` | | --- | --- | --- | --- | --- | --- | --- | --- | -|9| Private Immutables | Adds private immutables | Ilyas Ridhuan (@IlyasRidhuan) | - | Draft | Core | 2026-04-28 | +|9| Private Immutables | Adds private immutables | Ilyas Ridhuan (@IlyasRidhuan), Mike Connor (iAmMichaelConnor) | - | Draft | Core | 2026-04-28 | ## Abstract From caa5917a8c6f9cbf526b2e5eefdeb1c2a43c641c Mon Sep 17 00:00:00 2001 From: IlyasRidhuan Date: Mon, 27 Apr 2026 09:33:14 +0000 Subject: [PATCH 3/5] AZIP-9: Private Immutables --- AZIPs/azip-9.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/AZIPs/azip-9.md b/AZIPs/azip-9.md index 6629fdb..6fcce70 100644 --- a/AZIPs/azip-9.md +++ b/AZIPs/azip-9.md @@ -40,7 +40,7 @@ Adding a private immutable value to an Aztec contract today involves writing an There are contract designs which wouldn't otherwise require an initialiser function to be called, or which might not even need a transaction to instantiate the contract. Where applicable, avoiding such notes, function calls, and transactions would reduce deployment complexity, reduce proving times, and reduce dollar-costs. -Alternatively, third-party libraries exist that "hack" immutable values into contracts by overloading existing fields (e.g. salt) in the instance see (Wonderland's [Initializer-less Contracts: The Immutables Macro](https://forum.aztec.network/t/initializerless-contracts-the-immutables-macro/8512) demonstrating this pattern. +Alternatively, third-party libraries exist that "hack" immutable values into contracts by overloading existing fields (e.g. salt) in the instance (see Wonderland's [Initializer-less Contracts: The Immutables Macro](https://forum.aztec.network/t/initializerless-contracts-the-immutables-macro/8512) demonstrating this pattern). ## Specification @@ -65,11 +65,11 @@ The `immutables_hash` MUST be included as part of the `salted_initialization_has ```noir salted_initialization_hash = Poseidon2::hash([DOM_SEP__SALTED_INITIALIZATION_HASH, salt, initialization_hash, deployer, immutables_hash]) ``` -For contracts with no immutable values, `immutables_hash` is `0`, matching the default-zero convention already used for `salt`, `initialization_hash`, and `deployer`. The protocol cannot enforce this convention — the deployer chooses the `immutables_hash` they commit to in `salted_initialization_hash` — but a deployer who deviates from it produces an address that off-the-shelf tooling will not derive, so users will not interact with the contract at that address (see [Security Considerations](#security-considerations)). +For contracts with no immutable values, `immutables_hash` is `0`, matching the default-zero convention already used for `salt`, `initialization_hash`, and `deployer`. -This does require an additional Poseidon2 permutation round. +The protocol cannot enforce this convention — the deployer chooses the `immutables_hash` they commit to in `salted_initialization_hash` — but a deployer who deviates from it produces an address that off-the-shelf tooling will not derive, so users will not interact with the contract at that address (see [Security Considerations](#security-considerations)). -The diagram below shows the address-derivation pipeline as modified by this AZIP. Inputs and intermediate values introduced by AZIP-9 are highlighted; everything else is the existing pipeline, unchanged. +This does require an additional Poseidon2 permutation round. The diagram below shows the address-derivation pipeline as modified by this AZIP. Inputs and intermediate values introduced by AZIP-9 are highlighted; everything else is the existing pipeline, unchanged. ```mermaid flowchart TD @@ -143,7 +143,7 @@ flowchart TD ``` ### Change to the Oracle Interface -The oracle interface between the PXE and a private function — which feeds a `ContractInstance` into a private execution — forms part of the protocol, since alternative smart-contract frameworks must be able to execute one another's contracts. The oracle response that returns a `ContractInstance` MUST be updated to include the new `immutables_hash` field. Any framework consuming this oracle output MUST handle the additional field when reconstructing or hashing the instance. +The oracle interface between the PXE and a private function, which feeds a `ContractInstance` into a private execution, forms part of the protocol since alternative smart-contract frameworks must be able to execute one another's contracts. The oracle response that returns a `ContractInstance` MUST be updated to include the new `immutables_hash` field. Any framework consuming this oracle output MUST handle the additional field when reconstructing or hashing the instance. ### Change to AVM Opcodes The AVM opcode that loads a `ContractInstance` (e.g. `GETCONTRACTINSTANCE`) MUST return the new payload shape including `immutables_hash`. The AVM's serialization layout for `ContractInstance` is bumped accordingly. @@ -192,7 +192,7 @@ The injected runtime verification would be: The estimated cost for this verification is ~2k gates. -A separate AZIP — likely from smart-contract framework maintainers — could standardise a preimage layout for the `immutables_hash` (e.g. a leading "shape" identifier distinguishing flat hashing from a Merkle tree, followed by the data). +A separate AZIP (perhaps from smart-contract framework maintainers) could standardise a preimage layout for the `immutables_hash` (e.g. Merkle tree-style hashing for larger immutable sets that charge per access via a merkle sibling path). ### Eager Verification Unlike `#[storage]`, where work is only done per-access to `.read()` / `.write()` calls, the immutables are eagerly verified when `self.immutables` is referenced. Subsequent accesses via `self.immutables.` are free, and so the cost of verification is amortised over multiple reads. From d1c4b0b8e5d229a361bf6946e688c481cf45e171 Mon Sep 17 00:00:00 2001 From: Ilyas Ridhuan Date: Thu, 30 Apr 2026 14:15:09 +0100 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Michael Connor --- AZIPs/azip-9.md | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/AZIPs/azip-9.md b/AZIPs/azip-9.md index 6fcce70..eb40205 100644 --- a/AZIPs/azip-9.md +++ b/AZIPs/azip-9.md @@ -40,7 +40,7 @@ Adding a private immutable value to an Aztec contract today involves writing an There are contract designs which wouldn't otherwise require an initialiser function to be called, or which might not even need a transaction to instantiate the contract. Where applicable, avoiding such notes, function calls, and transactions would reduce deployment complexity, reduce proving times, and reduce dollar-costs. -Alternatively, third-party libraries exist that "hack" immutable values into contracts by overloading existing fields (e.g. salt) in the instance (see Wonderland's [Initializer-less Contracts: The Immutables Macro](https://forum.aztec.network/t/initializerless-contracts-the-immutables-macro/8512) demonstrating this pattern). +The way immutables are currently achieved is through third-party libraries which "hack" immutable values into contracts by overloading existing fields (e.g. salt) in the instance (see Wonderland's [Initializer-less Contracts: The Immutables Macro](https://forum.aztec.network/t/initializerless-contracts-the-immutables-macro/8512) demonstrating this pattern). ## Specification @@ -162,37 +162,17 @@ A new domain separator `DOM_SEP__IMMUTABLES_HASH` could be defined as: DOM_SEP__IMMUTABLES_HASH: u32 = poseidon2_hash_bytes(b"az_dom_sep__immutables_hash"); ``` -The `immutables_hash` itself is defined as the flat hash of the list of immutable values in the contract. +The `immutables_hash` itself could be defined as the flat hash of the list of immutable values in the contract. ```noir immutables_hash = Poseidon2::hash([DOM_SEP__IMMUTABLES_HASH, ]); ``` +A separate AZIP (perhaps from smart-contract framework maintainers) could standardise a preimage layout for the `immutables_hash` (e.g. Merkle tree-style hashing for larger immutable sets that charge per access via a merkle sibling path). ### Suggested Contract API and Verification Flow The protocol does not mandate a specific smart-contract syntax for declaring or accessing immutables; the choice belongs to smart-contract framework maintainers. The following is offered as a suggestion that aztec-nr could adopt. A new `#[immutable]` attribute could be provided for contract authors who need to declare a struct as immutable, following the approach taken by `#[storage]`. -```noir -#[immutable] -struct Immutables { - some_immutable_val: AztecAddress, - another_immutable_val: u8, -} -``` - -The `#[external]`, `#[internal]`, and `#[private]` function macros could inspect the function body and inject initialisation code only for functions that reference `self.immutables`. - -The injected runtime verification would be: - -1) Read `this_address` from the context. -2) Constrained retrieval of the `ContractInstance` using `this_address`. -3) Load the list of immutable values from the capsule store, using the defined reserved slot `IMMUTABLES_CAPSULE_SLOT`. -4) Compute the `immutables_hash` of the capsule-returned values and compare it to the value stored within the `ContractInstance`. -5) Deserialise and return the `Immutables` value to the function. - -The estimated cost for this verification is ~2k gates. - -A separate AZIP (perhaps from smart-contract framework maintainers) could standardise a preimage layout for the `immutables_hash` (e.g. Merkle tree-style hashing for larger immutable sets that charge per access via a merkle sibling path). ### Eager Verification Unlike `#[storage]`, where work is only done per-access to `.read()` / `.write()` calls, the immutables are eagerly verified when `self.immutables` is referenced. Subsequent accesses via `self.immutables.` are free, and so the cost of verification is amortised over multiple reads. From 966ac97f8a85f2ac269c2dc8cf9982491edeef62 Mon Sep 17 00:00:00 2001 From: Koen Date: Thu, 14 May 2026 11:40:57 +0400 Subject: [PATCH 5/5] chore: move status to accepted --- AZIPs/azip-9.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AZIPs/azip-9.md b/AZIPs/azip-9.md index eb40205..cf48e0a 100644 --- a/AZIPs/azip-9.md +++ b/AZIPs/azip-9.md @@ -5,7 +5,7 @@ | `azip` | `title` | `description` | `author` | `discussions-to` | `status` | `category` | `created` | | --- | --- | --- | --- | --- | --- | --- | --- | -|9| Private Immutables | Adds private immutables | Ilyas Ridhuan (@IlyasRidhuan), Mike Connor (iAmMichaelConnor) | - | Draft | Core | 2026-04-28 | +|9| Private Immutables | Adds private immutables | Ilyas Ridhuan (@IlyasRidhuan), Mike Connor (iAmMichaelConnor) | - | Accepted | Core | 2026-04-28 | ## Abstract