Skip to content

feat(protocol)!: seed protocol contract registration nullifiers at genesis - #25497

Open
spalladino wants to merge 4 commits into
nextfrom
spl/a-1264-genesis-protocol-nullifiers
Open

spalladino wants to merge 4 commits into
nextfrom
spl/a-1264-genesis-protocol-nullifiers

Conversation

@spalladino

@spalladino spalladino commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

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 registration
  • siloNullifier(ContractInstanceRegistry, magicAddress) — the instance publication, over the magic protocol address (1/2/3), which is the address the canonical instance actually has

The 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

before after
GENESIS_BLOCK_HEADER_HASH 0x22c20c79… 0x075fee3188fcffcdcdfaf61295ed5ff6e12f4a2316274c1fca25481e09709ac7
GENESIS_ARCHIVE_ROOT 0x0a0877e7… 0x1e8df7d033f879c44fbdddbacf525017e2f35a5ea1f3c33d4d1f531735b9a110
genesis nullifier-tree root 0x18935581… 0x21a19fe6f636fb24d9f63edb7b807613492cc0001c91e531917a2539f57e2ba8

The 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 in hash_of_genesis_block_header.
  • world_state.test.cpp: GetInitialTreeInfoForAllTrees constructs the world state with the six canonical nullifiers and asserts the new root. GetInitialTreeInfoWithPrefilledNullifiers is untouched.
  • The generated outputs (aztec_constants.hpp, ConstantsGen.sol, labs constants.gen.ts) are untracked and regenerate from constants.nr.
  • C++ WorldState already supported prefilled_nullifiers (feat(world-state): support prefilled nullifiers in genesis state #24567) — no new plumbing.

Labs (patch 0009)

  • protocol-contracts: generate_data.ts emits ProtocolContractGenesisNullifiers derived from the contract artifacts (sorted ascending, duplicates rejected, since the indexed nullifier tree requires unique strictly-increasing prefilled leaves); new DEFAULT_GENESIS_DATA.
  • stdlib: GenesisData.prefilledNullifiers becomes required, forcing an audit of every explicit literal. EMPTY_GENESIS_DATA stays 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 to DEFAULT_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.
  • New e2e case: re-publishing the bundled FeeJuice class is rejected as a duplicate nullifier.
  • getL1FixturePath (AZTEC_L1_FIXTURES_DIR) so the checkpoint-fixture writer resolves from inside the labs checkout.

Patches 00010008 are only renumbered by labs-patches/bootstrap.sh export; their patch IDs are unchanged.

Validation

  • Roots derived by constructing a real ephemeral NativeWorldStateService; the same measurement with EMPTY_GENESIS_DATA reproduces today's pinned constants exactly.
  • nargo test --package types hash_of_genesis — 1 passed.
  • world_state_tests --gtest_filter='*GetInitialTreeInfo*' — 3 passed.
  • labs 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.
  • All six l1-contracts/test/fixtures/*_checkpoint_{1,2}.json regenerated (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.ts recomputes the expected set from the bundled artifacts, so that drift fails a test rather than silently moving the roots.

…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.
@spalladino

Copy link
Copy Markdown
Contributor Author

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 l1_publisher.integration.test.ts with AZTEC_GENERATE_TEST_DATA=1. Checkpoint 1 of each family now starts from 0x1e8df7d0… and checkpoint 2 chains off checkpoint 1's archive:

family cp1 archive = cp2 lastArchiveRoot
empty 0x18725cd4cd7c2e4d9562f781628e2a6f1421ab16f74879647fe6b795f75873ba
mixed 0x15c64df622e91018e5d9e1a95ee95790de66a9eebecb139bc701812d4e0b2675
single_tx 0x0520ee82863b0fe5df440ab2c56de195a0a3801e8a6caba56fd766bf3dc5225d

forge test --match-contract 'CheckpointPreflight|Rollup' — 99 passed, 0 failed. The Rollup__InvalidArchive failure is gone.

New e2e case. automine/contracts/deploy/contract_class_registrationrejects re-publishing a bundled protocol contract class as a duplicate nullifier passes.

Two pre-existing failures, confirmed unrelated.

  • mainnet/testnet_compatibility still fail their has expected VK tree root assertion on this clean build (getVKTreeRoot() = 0x0a5d5f0a… vs the pinned 0x258bc0b9…). That is the ongoing fast-inbox circuit churn on next — the InboxParity public-input changes and the new inbox-parity-4/-16 crates — not this change; the genesis constants are only referenced from a Noir #[test], so circuit bytecode and VKs cannot move. Both has expected Genesis tree roots cases pass.
  • An earlier local build aborted in inbox-parity-1024 VK generation with a bb arena assertion. That step builds clean here in 71s, so it was local memory pressure rather than anything in this branch.

@spalladino

Copy link
Copy Markdown
Contributor Author

Smoke-tested the removed fast path from the packaged CLI, since getGenesisValues now always spawns aztec-wsdb:

$ node dest/bin/index.js compute-genesis-values
world-state:ipc-database Created IPC-backed world state instance
{
  "vkTreeRoot": "0x0a5d5f0a58c5362112bb04c013f194bf5a5cc22042cd6ed9e0fb2f9a3cba0c33",
  "protocolContractsHash": "0x04b0ccfafec7ff9a4a31c67c21377f73a8c01dcadc8710ee2a8e7751565db7f4",
  "genesisArchiveRoot": "0x1e8df7d033f879c44fbdddbacf525017e2f35a5ea1f3c33d4d1f531735b9a110"
}

Zero-prefund genesis with no explicit GenesisData resolves to the canonical baseline and returns GENESIS_ARCHIVE_ROOT, over IPC, in the packaged build. Same code path the prover broker and deploy-l1-contracts take.

…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.
@spalladino

Copy link
Copy Markdown
Contributor Author

Re: the regeneration flow — opened as #25503, stacked on this PR.

It reduces the pins before automating them: a new GENESIS_NULLIFIER_TREE_ROOT in constants.nr replaces the literal repeated across the Noir, C++ and TypeScript genesis tests, and the six seed nullifiers move into a generated C++ header. That leaves two hand-pinned locations instead of five, and none inside the labs patch series. Then noir-projects/fnd/scripts/regenerate_genesis_constants.sh --fixtures (plus a genesis-constants skill and a CLAUDE.md block, mirroring bump_noir_compiler.sh / noir-sync-update) rewrites what remains, with --check for a no-write staleness report and a precommit reminder on changes that can rotate a class id.

Also corrected here in 1f01c8a: my claim that genesis_data.test.ts catches class-id drift was wrong. protocol_contract_data.ts is gitignored and regenerated from the same artifacts that test recomputes from, so both sides move together and it passes. The real drift detector is the pinned-root assertion in world-state/src/testing.test.ts. Thanks for pushing on this — the question is what surfaced it.

@spalladino
spalladino added this pull request to stack #25505 September 17, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant