Skip to content

feat!: azip 8 public key hashes#23159

Open
jeanmon wants to merge 20 commits into
merge-train/avmfrom
jm/azip-8-public-key-hashes
Open

feat!: azip 8 public key hashes#23159
jeanmon wants to merge 20 commits into
merge-train/avmfrom
jm/azip-8-public-key-hashes

Conversation

@jeanmon
Copy link
Copy Markdown
Contributor

@jeanmon jeanmon commented May 11, 2026

Implementation of AZIP-8

@jeanmon jeanmon force-pushed the jm/azip-8-public-key-hashes branch from e55bfae to 8464e00 Compare May 11, 2026 17:25
@jeanmon jeanmon marked this pull request as ready for review May 11, 2026 17:25
@jeanmon jeanmon changed the title feat: azip 8 public key hashes feat!: azip 8 public key hashes May 11, 2026
@jeanmon jeanmon force-pushed the jm/azip-8-public-key-hashes branch from 8464e00 to 608173e Compare May 12, 2026 08:34
@jeanmon jeanmon requested a review from a team as a code owner May 12, 2026 10:06
@jeanmon jeanmon force-pushed the jm/azip-8-public-key-hashes branch 9 times, most recently from cd809b8 to d145a8a Compare May 15, 2026 17:33
Domain separator for hashing individual public keys per AZIP-8.
Value 3452068255 derived from poseidon2("az_dom_sep__single_public_key_hash").

Wired through the constants codegen (constants.in.ts) to TypeScript,
C++, and PIL outputs so the AVM and downstream consumers see it.

Part of AZIP-8 public key hashing migration.
@jeanmon jeanmon force-pushed the jm/azip-8-public-key-hashes branch from d145a8a to dd69116 Compare May 18, 2026 09:47
jeanmon added 6 commits May 18, 2026 11:39
Replaces the four-point PublicKeys struct with one Point (ivpk_m) plus three
field-element hashes (npk_m_hash, ovpk_m_hash, tpk_m_hash).

Adds the canonical primitive 'hash_public_key(p) = poseidon2(DOM_SEP__SINGLE_PUBLIC_KEY_HASH, [p.x, p.y])'
and rewrites PublicKeys::hash to combine the four hashes under DOM_SEP__PUBLIC_KEYS_HASH.

Drops NpkM, OvpkM, TpkM wrapper structs (only IvpkM survives because
ivpk_m must remain a point for encrypt-to-address). validate_on_curve and
validate_non_infinity now apply only to ivpk_m -- the other three keys
become PXE-side trust assumptions per AZIP-8 Security Considerations.

CONTRACT_INSTANCE_LENGTH drops from 16 to 10 fields, propagated via
remake-constants. Test golden values regenerated.

Downstream consumers (aztec-nr, kernel circuits, contracts) will not
compile until subsequent commits migrate them; types crate test suite
passes (367 tests).
Migrate aztec-nr (`state_vars`, `macros/notes.nr`, `oracle/keys.nr`) and noir-contracts
(`nft_note.nr`, `cards.nr`, scope test) to read `.npk_m_hash` directly from the AZIP-8
reshape. Rewrite `ContractInstancePublished.serialize_non_standard` to 12 fields and bump
`version: 2` in the Noir contract.

Also updates the `publish_contract_instance_for_public_execution` helper for the new
layout: arg buffer 16 → 10 fields, public-keys loop `0..12 → 0..6`, `universal_deploy`
index 15 → 9, and refresh the function selector signature to the new `PublicKeys` shape.
Without that helper update, `test_contract::publish_contract_instance` constant-folds an
out-of-bounds read into an unsatisfiable AssertZero at compile time.
Replace `KeyValidationRequest.pk_m: Point` with `pk_m_hash: Field`. The kernel reset
circuit's `validate_key_validation_request.nr` now derives `pk_m` from `sk_m`, hashes it
via `hash_public_key`, and asserts equality with `request.pk_m_hash`.

`KEY_VALIDATION_REQUEST_LENGTH` 4 -> 2 (and the `_AND_GENERATOR` and `SCOPED_*`
derivatives follow). Test fixture builders + golden addresses updated. 816
private-kernel-lib tests pass.

Includes two related guards / fixes that belong with this reshape rather than as
follow-ups:

- The reset circuit rejects `sk_m = 0` (which derives the point at infinity, encoding
  to `(0, 0)`). Without this guard, an attacker who publishes a contract instance with
  `npk_m_hash = hash_public_key((0, 0))` could validate KVRs by supplying `sk_m = 0`,
  yielding a deterministic / publicly-computable `sk_app`.

- The `hash_public_key` doc link in `key_validation_request.nr` is qualified with its
  full crate path (`crate::public_keys::hash_public_key`) so nargo's doc-link checker
  accepts it.
TS `PublicKeys` reshape (`{ npkMHash, ivpkM, ovpkMHash, tpkMHash }`), new `hashPublicKey`
free function, `KeyValidationRequest.pkMHash: Fr`, KeyStore `getKeyValidationRequest`
returns the new shape; `getMasterSecretKey(pkMHash)` takes a hash. PXE + TXE oracle
handlers updated for the new wire formats. All TS factories, mocks, conversion mappings,
and the auto-generated noir-protocol-circuits-types mirror updated.

`PublicKeys.hash()` mirrors Noir's `PublicKeys::hash`: hash the four single-key digests
under `DOM_SEP__PUBLIC_KEYS_HASH`. `ivpk_m` is reduced to its single-key digest first
(`hash_public_key([x, y])`); passing the Point object directly would inadvertently
include `is_infinite` and produce a different value. `hash()` is async as a result.

`PublicKeys.toFields()` / `fromFields()` emit the 6-field shape that matches Noir's
auto-derived `Serialize::N` and the ABI's struct flattening:
  [npkMHash, ivpkM.x, ivpkM.y, ivpkM.isInfinite, ovpkMHash, tpkMHash]
`is_infinite` is always `0` for `ivpkM` produced by `deriveKeys` (fixed-base scalar mul
on Grumpkin cannot reach infinity for a non-zero scalar). The slot stays on the wire
because the Noir struct's `Point` still carries the flag. TODO(F-553) once `is_infinite`
is removed from `Point`, struct flattening becomes 5 fields naturally and the slot can be
dropped.

`CompleteAddress.SIZE_IN_BYTES` updates 320 -> 224 (PublicKeys.toBuffer() shrinks 256 ->
160 bytes), and the `contract_address.test.ts` inline snapshot is regenerated for the new
derived address.

`KeyStore.addAccount` reuses the already-derived `publicKeys` via the new
`CompleteAddress.fromPublicKeysAndPartialAddress` factory (avoids a redundant
`deriveKeys` call). On-curve / non-infinity for the four master keys is by construction
of `deriveKeys`, so no explicit runtime check is needed — external account-creation
flows that bypass `deriveKeys` must validate themselves (see the AZIP-8 migration note).

`deriveKeys` no longer returns `masterIncomingViewingPublicKey` separately, since
`publicKeys.ivpkM` already carries it. The three non-ivpk points remain in the return
because only their hashes live in `publicKeys`.

Carve-out for `aztec_utl_getPublicKeysAndPartialAddress` (PXE + TXE handlers): the Noir
side declares its return type as a hand-rolled `Option<[Field; 6]>` and decodes the array
manually (see `aztec-nr/aztec/src/oracle/keys.nr`), expecting the AZIP-8 5-field
PublicKeys shape (`is_infinite` excluded) + `partial_address`. Both handlers bypass
`toFields()` and emit the explicit layout inline.

`publishInstance.ts` calls `publicKeys.toNoirStruct()` to satisfy the auto-generated
binding's snake_case parameter type (`{ npk_m_hash, ivpk_m: { inner: { x, y,
is_infinite } }, ovpk_m_hash, tpk_m_hash }`) directly — no `as unknown as
Parameters<...>` cast needed.

`e2e_keys.test.ts` "gets ovsk_app" uses `hashPublicKey` from `@aztec/stdlib/keys` (the
AZIP-8 domain-separated hash over `[x, y]`) rather than `Point.hash()` (`Poseidon2([x, y,
isInfinite])` with no separator) so the lookup hash matches what `KeyStore.addAccount`
stores.
`ContractInstance.VERSION` 1 -> 2 in stdlib (`contract_instance.ts`,
`interfaces/contract_instance.ts`). `ContractInstancePublishedEvent.toContractInstance`
now rejects anything other than `version === 2`.

Test fixtures that emitted the old version are updated:

- `p2p/contract_instance_validator.test.ts`: `buildContractInstanceLog` was constructing
  the underlying `instance` with `version: 2 as const` but emitting the log fields with a
  hardcoded `new Fr(1)`. After the VERSION bump the validator parses the log, calls
  `event.toContractInstance()`, which rejects anything other than v2; both the
  happy-path and wrong-address cases hit the validator's
  `Failed to parse contract instance deployment log` catch-all. Bumped emitted `version`
  to `2` and refreshed the stale "PublicKeys serializes as 4 Points" comment.

- `stdlib/interfaces/archiver.test.ts`: `version: 1` -> `version: 2` in the expected
  `ContractInstance` shape.
PIL changes (address_derivation.pil, contract_instance_retrieval.pil):
- Drop nullifier_key_x/y, outgoing_viewing_key_x/y, tagging_key_x/y
  columns; replace with the corresponding single-key hash columns.
- Keep incoming_viewing_key_x/y as the only point columns.
- Replace 5-round PUBLIC_KEYS_HASH_POSEIDON2_0..4 (over 13 inputs) with:
  - IVPK_M_HASH_POSEIDON2 (1 round, 3 inputs) computing ivpk_m_hash in-circuit
  - PUBLIC_KEYS_HASH_POSEIDON2_0..1 (2 rounds, 5 inputs) combining the four hashes
  Net: 9 -> 7 Poseidon2 lookups in address_derivation.pil per AZIP-8 §Performance.
- Lookup signature into address_derivation switches accordingly.

C++ changes:
- struct PublicKeys: 4 AffinePoints -> {1 AffinePoint + 3 FF hashes}.
- struct PublicKeysHint: same. Msgpack field names match TS PublicKeys
  (npkMHash, ivpkM, ovpkMHash, tpkMHash).
- hash_public_keys() rewritten to compute ivpk_m_hash in-circuit and
  combine with the three hint-supplied hashes under DOM_SEP__PUBLIC_KEYS_HASH.
- AddressDerivation gadget + simulation events carry ivpk_m_hash.
- All AVM gadget unit tests migrated.
- Generated headers regenerated via avm2_gen.sh.

22 tests across ContractInstanceRetrieval/AddressDerivation/TraceGen pass.
The end-to-end AvmVerifierTests.GoodPublicInputs requires the
minimal_tx.testdata.bin fixture to be regenerated (Phase 9).
jeanmon added 7 commits May 18, 2026 11:41
Updates the rust example to read .npk_m_hash directly (instead of
.npk_m.hash()) and documents that npk/ovpk/tpk are now exposed only as
hashes.
- keys.md: rewrite the public_keys_hash pseudocode to show npk/ovpk/tpk
  as their hashes plus an in-circuit ivpk_m_hash; add an admonition
  noting the SVG diagram is pending refresh.
- custom_notes.md: switch the rust code example to read .npk_m_hash
  directly (drop the .npk_m.hash() pattern).
Add a `## TBD` migration entry to `docs/.../migration_notes.md` covering contract-author,
TS/wallet, indexer, and PXE-security migration steps.

Adds `unspendable` to the cspell dictionary -- the word is used in the security note that
describes the PXE-side risk if `KeyStore.addAccount`'s on-curve / non-infinity check is
bypassed (notes encrypted to a malformed `ivpk_m` can never be decrypted). Common
UTXO-domain term, just not in the project's word list yet.
The TS PublicKeys.default() previously hardcoded the three default
hashes as Fr literals. If anyone changed DEFAULT_NPK_M_X/Y (or the
hashing primitive) in constants.nr without updating those literals,
TS and Noir would silently drift on the default-hash values, breaking
address derivation symmetry for default-key accounts.

Promote them to first-class constants:
- DEFAULT_NPK_M_HASH, DEFAULT_OVPK_M_HASH, DEFAULT_TPK_M_HASH in
  constants.nr; flow through remake-constants to constants.gen.ts and
  aztec_constants.hpp.
- PublicKeys::default() (Noir) reads them directly.
- PublicKeys.default() (TS) reads them from @aztec/constants.
- New types-crate test 'default_hashes_match_default_points' asserts
  hash_public_key(DEFAULT_*_M_X/Y) == DEFAULT_*_M_HASH; this catches
  drift between the points and the precomputed hashes.

Also bumps the trailing version: 1 -> 2 ContractInstance literals in
p2p/pxe tests that the typechecker caught after fix #1.
After replacing point.hash() with direct field reads on PublicKeys,
several files still imported the Hash trait or Point type that they
no longer use:

Hash trait (no longer needed since the .hash() resolution moved to a
field access):
  - aztec-nr state_vars: private_immutable, private_mutable,
    single_private_immutable, single_private_mutable, single_use_claim
  - aztec-nr uint-note
  - noir-contracts: card_game_contract/cards, nft_contract/nft_note,
    test/scope_test_contract

Point type (only ever appeared as the Point literals I replaced with
plain Field values during the KVR reshape):
  - private-kernel-lib tests: private_kernel_inner/output_composition_tests,
    private_kernel_tail/previous_kernel_validation_tests,
    private_kernel_tail_to_public/previous_kernel_validation_tests
  - protocol-test-utils/src/fixture_builder

TS:
  - pxe/private_kernel/private_kernel_oracle.ts: drop Point (signature
    changed to take pkMHash: Fr in fix #4)
  - noir-protocol-circuits-types/conversion/client.ts: drop
    mapPointFromNoir (only used by the old KVR fromNoir mapping)

All 816 private-kernel-lib tests still pass; both Noir workspaces and
the full TS build are clean.
The `PXE_DATA_SCHEMA_VERSION` bump from 5 to 6 itself is delivered upstream by AZIP-9.
This commit adapts the PXE schema-test fixtures and snapshots so the v6 baseline
captures the AZIP-8 on-disk shape:

  - The `ContractStore` schema-test fixture uses the new `PublicKeys` constructor
    (`Fr, Point, Fr, Fr`) and `version: 2`.
  - `AddressStore.complete_addresses` / `complete_address_index` keys change because
    the address derived from a given secret changes (AZIP-8 hashes `public_keys_hash`
    over four single-key digests rather than four raw points; AZIP-9 also inserts
    `immutables_hash` into `salted_initialization_hash`).
  - `KeyStore.key_store` adds `npk_m_hash` / `ovpk_m_hash` / `tpk_m_hash` entries and
    re-keys all per-account material under the new address.
  - `ContractStore.contracts_instances` carries the new PublicKeys layout.

The three affected per-store snapshots (`AddressStore.json`, `KeyStore.json`,
`ContractStore.json`) are regenerated to the v6 baseline.

This remains BREAKING for end users: the same secret produces a different address, so
existing on-device state cannot be migrated forward at the storage layer.
`DatabaseVersionManager` (using the upstream v6 bump) wipes pre-AZIP-8 PXE DBs on first
open; users re-sync from L1 and operate under the new address.
…a fixture for AZIP-8

The committed `ContractInstancePublishedEventData.hex` fixture was generated by an e2e deploy
under the pre-AZIP-8 protocol (4-Point `PublicKeys`, `version: 1`). After AZIP-8 it parses as a
malformed v1 event, and `ContractInstancePublishedEvent.toContractInstance` rejects it with
"Unexpected contract instance version 1", failing:

  - archiver `data_store_updater.test.ts` (3 tests)
  - protocol-contracts `contract_instance_published_event.test.ts` snapshot

Regenerated synthetically with `version: 2`, the new `PublicKeys` layout
(`npkMHash, ivpkM, ovpkMHash, tpkMHash`), and an `address` actually derived from the
instance fields via `computeContractAddressFromInstance` so the archiver's
`updateDeployedContractInstances` address-consistency check passes. Snapshot regenerated
accordingly. Field values are deterministic so the fixture is reproducible.

Strictly an offline regeneration -- the proper end-to-end regenerator
(`e2e_deploy_contract/contract_class_registration.test.ts` with `AZTEC_GENERATE_TEST_DATA=1`)
is still the canonical producer for "real on-chain payloads" and remains the path to use once
that test suite is rerun.
@jeanmon jeanmon force-pushed the jm/azip-8-public-key-hashes branch from dd69116 to 64e06ed Compare May 18, 2026 11:49
jeanmon added 3 commits May 18, 2026 12:28
…for AZIP-8

The `Computes contract info for {default,parent,updated}Contract` snapshot tests pin the
derived `address` and `public_keys` values for the three default fixture contracts. Both fields
shift under AZIP-8: `public_keys` shrinks from 8 fields (4 raw Points) to 5 fields
(`npkMHash`, `ivpkM`, `ovpkMHash`, `tpkMHash`), and `address = (preaddress * G + ivpkM).x` with
`preaddress = H(public_keys_hash, partial_address)` -- where `public_keys_hash` is now hashed
over the four single-key digests rather than the four raw points.

The new addresses match the golden values documented in `AZIP_8_IMPLEMENTATION_SUMMARY.md`
("Test golden values that moved"). All other snapshot fields (`artifact_hash`,
`contract_address_salt`, `contract_class_id`, `deployer`, `partial_address`,
`private_functions_root`, `public_bytecode_commitment`, `salted_initialization_hash`)
are unchanged.
…eys layout

The AVM testdata fixture used by `avm_minimal.test.ts` (and downstream C++ AVM tests) embeds
msgpack-serialized `AvmCircuitInputs`, including hints that carry the old 4-Point
`PublicKeys` layout. After AZIP-8 the serialized PublicKeys shape changes
(npkMHash + ivpkM + ovpkMHash + tpkMHash), and the buffer fails to match the committed binary.

Regenerated with `AZTEC_GENERATE_TEST_DATA=1` on the test itself (the canonical regenerator,
per the comment at line 45). File shrinks 189007 -> 188808 bytes, consistent with the
PublicKeys layout change.
… layout

`avm.test.ts` ("serialization sample for avm2") compares the
msgpack-serialized `AvmCircuitInputs` against the committed
`avm_inputs.testdata.bin`. The fixture embeds `PublicKeys` in the
encoded inputs, so the AZIP-8 layout change (4 Points -> 1 hash + 1 Point +
2 hashes) shifts the buffer.

Regenerated via `AZTEC_GENERATE_TEST_DATA=1` on the test itself (the
documented canonical regenerator). File shrinks 2084088 -> 2080108
bytes, consistent with the PublicKeys layout change applied across the
serialized inputs.
@jeanmon jeanmon force-pushed the jm/azip-8-public-key-hashes branch 2 times, most recently from 80bd613 to 06fe0f9 Compare May 18, 2026 14:47
jeanmon added 3 commits May 18, 2026 15:15
…l test data

The `'generates sample Prover.toml files'` test in `e2e_prover/full.test.ts` lists
`'private-kernel-inner'` in the regen array but the test scenario only submits Token
`transfer` calls, which the PXE planner packs into `private_kernel_init_2` — plain
`private_kernel_inner` never fires, `pushTestData('private-kernel-inner', ...)` is never
called, and `getTestData('private-kernel-inner')` returns `[]`. Result:
`private-kernel-inner/Prover.toml` is left untouched by regen and silently goes stale.

After the AZIP-8 / AZIP-9 derivation changes invalidated the formerly-stale fixture's
address hints, this gap surfaced as a `nargo execute` failure on the inner kernel circuit
("computed contract address does not match expected one"). The fix is to seed the test
with a 4-call private chain that the planner splits as `init_3 + inner`:

  entrypoint (init_3)
    parent.private_nested_static_call (init_3)
      parent.private_call (init_3)
        child.private_get_value (inner)

Deploys `ParentContract` + `ChildContract`, seeds a note via `child.private_set_value`
(needed because `private_get_value` reads notes and a static call can't write them), then
`proveInteraction`s the nested-call interaction. The existing regen loop now finds data
in `getTestData('private-kernel-inner')` and writes the file.

`proveInteraction` alone is enough to capture the witness — the tx doesn't need to land.
After the AZIP-8 wire-format and address-derivation changes, the committed sample inputs
under `noir-projects/noir-protocol-circuits/crates/*/Prover.toml` were stale on two axes:

1. Schema: the `public_keys` sub-table used the old wrapped-Point layout
   (`public_keys.npk_m.inner.{x,y,is_infinite}`, same for ovpk_m / tpk_m), and the
   key-validation requests carried `request.pk_m.{x,y,is_infinite}`. The new layout collapses
   these to single-Field hashes (`public_keys.npk_m_hash` / `.ovpk_m_hash` / `.tpk_m_hash`,
   and `request.pk_m_hash`); `ivpk_m` stays as a Point (it remains a curve point in-circuit
   for address derivation).

2. Values: every derived field that depended on `public_keys_hash` had to be recomputed --
   addresses, salted_initialization_hash, pre_address, etc. -- because the hash now ingests
   the four single-key digests instead of the four raw points.

Regenerated end-to-end against the live prover stack: run
`e2e_prover/full.test.ts` with `AZTEC_GENERATE_TEST_DATA=1 REAL_PROOFS=true`. That test
pushes data via `pushTestData` from the orchestrator/private-kernel code paths and writes
the Prover.tomls at `full.test.ts:250-278`. This is the canonical regenerator -- the values
in the committed files are now self-consistent against the post-AZIP-8 derivation flow.

Closes "Outstanding manual step #5" (Prover.toml regen) -- five private-kernel and seven
rollup circuits in scope.
…nt-disable)

Four lint issues surfaced by `yarn lint`:

- `private_execution.test.ts:350` -- `accountHasKey` mock used an `async` arrow with no
  `await`. Drop the `async` and wrap returns in `Promise.resolve(...)` so the mock still
  satisfies the `Promise<boolean>` signature.
- `utility_execution.test.ts:4` and `private_kernel/hints/test_utils.ts:17` -- `Point`
  imports are no longer referenced after the AZIP-8 reshape (point-typed master keys are
  now hashes); drop the imports.
- `aztec-node/server.ts:406` -- the `eslint-disable-next-line
  aztec-custom/no-non-primitive-in-collections` directive no longer matches a fired rule
  ("Unused eslint-disable directive"). The lint rule now accepts `CheckpointNumber` in
  `Map<...>` keys, so drop the disable and the `TODO(palla)` that marked it.

The `aztec-node` change is pre-existing drift unrelated to AZIP-8; folded in here since it
was reported together with the other findings and is a small fix.
@jeanmon jeanmon force-pushed the jm/azip-8-public-key-hashes branch from 06fe0f9 to d49756b Compare May 18, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant