feat!: azip 8 public key hashes#23159
Open
jeanmon wants to merge 20 commits into
Open
Conversation
e55bfae to
8464e00
Compare
8464e00 to
608173e
Compare
cd809b8 to
d145a8a
Compare
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.
d145a8a to
dd69116
Compare
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).
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.
dd69116 to
64e06ed
Compare
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.
80bd613 to
06fe0f9
Compare
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.
06fe0f9 to
d49756b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implementation of AZIP-8