feat(protocol)!: seed protocol contract registration nullifiers at genesis - #25497
Conversation
…nesis A production genesis world state now starts with the protocol contracts already registered: for each of the three protocol contracts, the class registration nullifier siloed by ContractClassRegistry and the instance publication nullifier siloed by ContractInstanceRegistry over the magic protocol address. The six leaves replace padding inside the nullifier tree's 128-leaf prefill region, so the tree size is unchanged while the root moves. That shifts the canonical genesis constants: GENESIS_BLOCK_HEADER_HASH 0x22c20c79... -> 0x075fee31... GENESIS_ARCHIVE_ROOT 0x0a0877e7... -> 0x1e8df7d0... and the genesis nullifier-tree root 0x18935581... -> 0x21a19fe6... Because the genesis archive root is what a rollup stores in archives[0], this only applies to a network initialized from the new genesis; already deployed networks keep their original roots. The TypeScript side is carried as labs patch 0009: protocol-contracts generates ProtocolContractGenesisNullifiers from the contract artifacts and exposes DEFAULT_GENESIS_DATA, GenesisData.prefilledNullifiers becomes required, and world-state defaults resolve to the protocol baseline. getGenesisValues' fifth argument is now *additional* nullifiers composed on top of that baseline, with duplicates rejected. The remaining patches are renumbered by `labs-patches/bootstrap.sh export`; their contents are unchanged.
…undled artifacts The membership assertions in world-state read back the same generated list they seed, so they cannot catch a protocol class id rotating out from under the constants. Derive the expected set independently from the artifacts.
…ive root The first checkpoint of each family builds on the genesis archive, and the second builds on the first, so all six move.
|
Validation update — everything below was run on a clean clone of this branch (all nine patches applied cleanly on a fresh checkout), fully built from source. Checkpoint fixtures. Regenerated all six through
New e2e case. Two pre-existing failures, confirmed unrelated.
|
|
Smoke-tested the removed fast path from the packaged CLI, since Zero-prefund genesis with no explicit |
…ects The generated protocol contract data is rebuilt from the same artifacts the test recomputes from, so a rotated class id moves both sides and passes. The pinned-root assertion in world-state is what catches that.
|
Re: the regeneration flow — opened as #25503, stacked on this PR. It reduces the pins before automating them: a new Also corrected here in |
Stacked on #25497 — review that one first; this PR's diff is only the two commits on top. Answers the review comment on #25497: *"Is there a signposted automated regeneration flow that a person/ai can do to easily update them? I wonder if a git commit hook might be useful to warn people about this."* ## The problem The genesis nullifier tree is seeded with the protocol contracts' registration nullifiers, which derive from their **class ids**. Anything that rotates a class id — a protocol contract change, a compiler bump, a transpiler change — moves the genesis roots, and several tracked files record them. Before this PR that was five hand-pinned locations, one of them inside the labs patch series, discoverable only via a late CI failure. ## Reduce the pins first, then automate Rather than automate the rewriting of five duplicated literals, most of them are removed: - New `GENESIS_NULLIFIER_TREE_ROOT` in `constants.nr`. The existing codegen already projects it into `aztec_constants.hpp`, `ConstantsGen.sol` and `constants.gen.ts`, so the Noir, C++ and TypeScript genesis tests now all read one constant instead of repeating the literal. - The six seed nullifiers move out of `world_state.test.cpp` into a generated `genesis_protocol_nullifiers.hpp`. Two hand-pinned locations remain (`constants.nr` and that header), and **none inside `labs/`** — which matters, because a file inside the patch series cannot be rewritten by a script that "never commits": persisting a labs edit requires a commit plus `bootstrap.sh export`. ## The regeneration flow ```bash noir-projects/fnd/scripts/regenerate_genesis_constants.sh --fixtures ``` Single source of truth, modelled on `noir/scripts/bump_noir_compiler.sh`. Phases: preflight → measure → validate → update → regenerate consumers → verify → stage. It does not commit. | flag | effect | | --- | --- | | *(none)* | rewrites the constants and the seed header; reports the L1 fixtures as outstanding | | `--fixtures` | also regenerates the six L1 checkpoint fixtures (needs anvil) | | `--check` | measures and compares only, writes nothing, non-zero exit if stale | Three deliberate design points, all from review: - **Fail-fast, not best-effort.** The Noir bump script degrades gracefully because a partial bump still leaves a useful diff. Here a half-applied regeneration leaves constants that disagree with each other, so a missing dependency, a failed measurement or a replacement that did not match is a hard error. A replacement must match its named declaration exactly once. - **It rebuilds `constants-codegen` before the remake scripts.** Those read the inputs embedded when the package was built, so running them against an edited `constants.nr` without rebuilding will cheerfully reproduce the *previous* values and report success. This bit me during development. - **It refuses to run against a build older than the protocol contract sources**, rather than measuring stale artifacts. `aztec compute-genesis-values` now also reports the canonical measurement, through the same shared `measureCanonicalGenesis` helper the script reads, so there is one computation rather than two that can drift. ## Precommit reminder Fires when staged changes touch the protocol contract sources, the Noir submodule pin or the transpiler. It names the regeneration command and never blocks. It is explicitly a **reminder, not evidence of freshness** — deciding whether a class id actually moved means compiling the contracts and computing the ids, far too slow for a hook, so the path predicate is a cheap and deliberately incomplete proxy (a shared library the contracts pull in can move an id without tripping it). It also does not go quiet merely because `constants.nr` is staged: only a staged edit to the `GENESIS_` declarations themselves counts as handled. ## What it deliberately does not touch `mainnet_compatibility.test.ts` and `testnet_compatibility.test.ts` pin what a **live network was actually deployed with**. A class id rotation fails those too, and that is correct — they change only at a governance upgrade. The script never rewrites them, and both the skill and the `CLAUDE.md` block say so explicitly, because the tempting "fix" is to paste in the new values, which silently claims the deployed network moved. ## Signposting `.claude/skills/genesis-constants/` plus a `<genesis_constants>` block in the root `CLAUDE.md`, mirroring how `bump_noir_compiler.sh` is surfaced through `noir-sync-update`. ## Validation - `--check` against the current tree: measures independently and confirms all three constants, the seed header and the fixtures agree. - Red/green on the write path: corrupted `GENESIS_ARCHIVE_ROOT` → `--check` exits 1 naming the stale constant → a plain run restores it and regenerates the derived outputs. The verify phase caught a real bug in its own Solidity check (grouped digits) during that run. - Precommit reminder: fires on a staged protocol contract edit, silent once the `GENESIS_` constants are staged too. - `nargo test --package types hash_of_genesis` — 1 passed. - `world_state_tests --gtest_filter='*GetInitialTreeInfo*'` — 3 passed. - `stdlib/src/block/l2_block.test.ts` (3), `world-state/src/testing.test.ts` (7), `protocol-contracts/src/genesis_data.test.ts` (2) — passed. - `labs-patches/bootstrap.sh check` — clean. Patch `0009` is unchanged; the labs changes here are a new `0010`. Relates to A-1264.
Moves the world-state DB service (world_state engine, persistent content-addressed merkle storage, lmdb tree store, IPC server) out of barretenberg into native-packages/wsdb, compiling with zero barretenberg headers: bb is linked only as a prebuilt archive for the poseidon2 c_bind and for the new bb_wsref_* C ABI over the in-memory reference world state (world_state_reference), against which this package's conformance test drives its WorldState and asserts agreement on roots, sibling paths, low-leaf lookups, preimages and checkpointing. Constants stay in lockstep via a wsdb-local remake-constants hook on the protocol constants-codegen (generated header, no longer checked in). Rebased onto next after the @aztec-foundation scope rename (#25328). wsdb keeps the name next gave it; the new kvdb package follows the same scope rather than introducing an @aztec-scoped foundation package. The labs series is re-exported against the current pin: the wsdb/kvdb consumption patch is rewritten for the new scope, and the indexed/nullifier tree reference update is dropped because upstream removed that references frontmatter, leaving only its cspell additions. Rebased onto next: the labs patch series is re-derived against the current pin (42d7d24b) and renumbered -- next's own scope patch has been absorbed upstream and dropped out. scripts/labs_fnd_hashes.sh keeps next's acvm -> noir-execute rename alongside the native-packages/{wsdb,kvdb} component entries. A test pins the on-disk lmdb key bytes, which nothing covered: the bb-linked parity target compiles only field_element.test.cpp, so it checks fr's hash, msgpack and ordering but not how a key reaches disk -- and lmdblib's concrete serialise_key(const uint256_t&) became a generic template that memcpys from &key rather than uint256_t::data, which agree only while data[4] is the sole member at offset 0. Rebased onto next: the labs series is re-derived against the current pin and our three patches renumbered to 0016-0018, since next's own series has grown into 0002-0015 and the old numbering collided. The barretenberg-side wsdb bootstrap and CMakeLists carry #25403's flag removal into their native-packages homes, which next applied to the paths this moves. The NAPI scaffolding moves rather than being copied. lmdb_store_wrapper was its only consumer -- msgpack_client includes just ipc_client.hpp and napi.h, and nothing outside the module referenced barretenberg/messaging -- so extracting the store orphaned util/{promise,async_op,message_processor} and messaging/{dispatcher, header} on the bb side. Deleting them there leaves nodejs_module as init_module.cpp plus msgpack_client (still needed until #25362 removes it), and lets the diff read as renames instead of ~450 lines of apparently new code. stream_parser.hpp goes with them: it was already unreferenced and only header.hpp kept it compiling. The five bin32 wire aliases (Fr, Nullifier, PublicDataSlot, PublicDataValue, BlockHeaderHash) are the same 32 canonical bytes as FieldElement under a nominal tag, so FieldElement now converts to and from any of them implicitly, detected by the marker typedef codegen stamps on ipc::Bin32Alias (no generated header reaches field_element.hpp). That removes the ten per-alias converter functions and the fr-vector helpers from the wsdb converter; the composite converters reduce to aggregate init. bb's copy gets the equivalent for bb::fr without touching field.hpp: a to_wire proxy convertible to whichever alias the call site expects, and one from_wire template. Rebased onto next: the genesis seeding (#25497, #25503) landed in bb's world_state, so its generated seed header moves with the rest of world_state into native-packages/wsdb in wsdb's own types, regenerate_genesis_constants.sh and the genesis-constants skill point at the new path, GENESIS_NULLIFIER_TREE_ROOT joins wsdb's constants selection, and the three labs patches renumber to 0021-0023 behind next's grown series. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EUKia6wteDk9kZZuT2Gju
Revives #24254 (closed in favour of this branch, which is rebuilt on current
next; the old one still carried the deleted N-API backend, obsolete generated files and stale roots), and builds on #24567.A production genesis world state now starts with the protocol contracts already registered. For each of the three protocol contracts we seed two nullifiers:
siloNullifier(ContractClassRegistry, classId)— the class registrationsiloNullifier(ContractInstanceRegistry, magicAddress)— the instance publication, over the magic protocol address (1/2/3), which is the address the canonical instance actually hasThe six leaves replace padding inside the nullifier tree's 128-leaf prefill region, so the tree size stays 128 while the root moves.
New canonical constants
GENESIS_BLOCK_HEADER_HASH0x22c20c79…0x075fee3188fcffcdcdfaf61295ed5ff6e12f4a2316274c1fca25481e09709ac7GENESIS_ARCHIVE_ROOT0x0a0877e7…0x1e8df7d033f879c44fbdddbacf525017e2f35a5ea1f3c33d4d1f531735b9a1100x18935581…0x21a19fe6f636fb24d9f63edb7b807613492cc0001c91e531917a2539f57e2ba8The genesis archive root is what a rollup stores in
archives[0], so this only applies to a network initialized from the new genesis. Already deployed networks keep their original roots; no legacy-genesis configuration, chain detection or migration is in scope. The mainnet/testnet compatibility tests now reconstruct each network's historical genesis (empty nullifier list, its own prefunding) and still assert its deployed root.Foundation
constants.nr/block_header.nr: new constants and the genesis nullifier root inhash_of_genesis_block_header.world_state.test.cpp:GetInitialTreeInfoForAllTreesconstructs the world state with the six canonical nullifiers and asserts the new root.GetInitialTreeInfoWithPrefilledNullifiersis untouched.aztec_constants.hpp,ConstantsGen.sol, labsconstants.gen.ts) are untracked and regenerate fromconstants.nr.WorldStatealready supportedprefilled_nullifiers(feat(world-state): support prefilled nullifiers in genesis state #24567) — no new plumbing.Labs (patch
0009)protocol-contracts:generate_data.tsemitsProtocolContractGenesisNullifiersderived from the contract artifacts (sorted ascending, duplicates rejected, since the indexed nullifier tree requires unique strictly-increasing prefilled leaves); newDEFAULT_GENESIS_DATA.stdlib:GenesisData.prefilledNullifiersbecomes required, forcing an audit of every explicit literal.EMPTY_GENESIS_DATAstays as a deliberately empty state for low-level tests and is documented as not matching production roots.world-state: native-service and synchronizer-factory defaults resolve toDEFAULT_GENESIS_DATA.getGenesisValues' fifth argument is redefined as additional nullifiers, composed on top of the protocol baseline, sorted on a copy, with duplicates rejected rather than discarded. The empty-genesis fast path is gone — the root is always computed on an ephemeral world state.FeeJuiceclass is rejected as a duplicate nullifier.getL1FixturePath(AZTEC_L1_FIXTURES_DIR) so the checkpoint-fixture writer resolves from inside the labs checkout.Patches
0001–0008are only renumbered bylabs-patches/bootstrap.sh export; their patch IDs are unchanged.Validation
NativeWorldStateService; the same measurement withEMPTY_GENESIS_DATAreproduces today's pinned constants exactly.nargo test --package types hash_of_genesis— 1 passed.world_state_tests --gtest_filter='*GetInitialTreeInfo*'— 3 passed.world-state/src/testing.test.ts(7),world-state/src/native/native_world_state.test.ts(56),stdlib/src/block/l2_block.test.ts(3) — passed.labs-patches/bootstrap.sh check— clean.protocol-contracts/src/genesis_data.test.ts— 2 passed; the artifact-derived recomputation matches the generated constants exactly.l1-contracts/test/fixtures/*_checkpoint_{1,2}.jsonregenerated (checkpoint 1 builds on the genesis archive, checkpoint 2 on checkpoint 1).forge test --match-contract 'CheckpointPreflight|Rollup'— 99 passed, 0 failed, against the regenerated fixtures.Known outstanding
Protocol class IDs are now genesis inputs, so an artifact or compiler change that rotates a class ID rotates the genesis roots.
protocol-contracts/src/genesis_data.test.tsrecomputes the expected set from the bundled artifacts, so that drift fails a test rather than silently moving the roots.