Skip to content

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

Open
spalladino wants to merge 3 commits into
merge-train/spartan-v5from
spl/a-1257-genesis-protocol-nullifiers
Open

feat(protocol)!: seed protocol contract registration nullifiers at genesis#24254
spalladino wants to merge 3 commits into
merge-train/spartan-v5from
spl/a-1257-genesis-protocol-nullifiers

Conversation

@spalladino

@spalladino spalladino commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Motivation

Root-cause fix for A-1257, complementing the archiver-resilience fix in #24227. Nodes preload every bundled protocol contract class/instance at synthetic block 0, but world-state genesis seeded no matching registration nullifiers. That gap made a first on-chain ContractClassRegistry.publish of a bundled protocol class id protocol-valid, which then collided with the archiver's block-0 preload and stalled L1 sync. Seeding those nullifiers at genesis rejects the re-publish at the protocol level (duplicate nullifier), so it never reaches the archiver at all.

Fixes A-1264

Approach

Pre-insert the protocol contracts' registration nullifiers into the genesis nullifier tree. For each of the three protocol contracts we seed two siloed nullifiers:

  • the class nullifier siloNullifier(ContractClassRegistry, classId) — matches what ContractClassRegistry.publish emits;

  • the instance nullifier siloNullifier(ContractInstanceRegistry, magicAddress) — uses the magic protocol address (1/2/3), consistent with the node's block-0 instance preload.

  • C++ world-state gains a prefilled_nullifiers input, threaded through the WorldState constructors, the napi binding, and create_canonical_fork, which inserts them as the nullifier tree's initial leaves. The indexed tree requires its initial leaves to be unique and strictly increasing, so the seeded list is sorted ascending (and a defensive check enforces this before the C++ call).

  • GenesisData.prefilledNullifiers is a required field. EMPTY_GENESIS_DATA (truly empty) is retained for low-level tree tests; a new canonical DEFAULT_GENESIS_DATA (in @aztec/protocol-contracts, which can see both the type and the generated nullifier list) carries the protocol nullifiers and is now the default in all production world-state constructors. GenesisData stays in @aztec/stdlib, which cannot depend on protocol-contracts (circular), so the default is applied one layer up.

  • The seeded set changes the genesis nullifier-tree root, hence the genesis block header hash and archive root. Both constants were recomputed authoritatively by seeding the same nullifiers in the C++ WorldStateTest.GetInitialTreeInfoForAllTrees and reading the result, then propagated to every location that pins them: constants.nr, constants.gen.ts, ConstantsGen.sol, aztec_constants.hpp, the V5 deploy script, the mainnet_compatibility test, and the regenerated L1 checkpoint fixtures.

Breaking change

This changes the genesis state root. It applies to new networks only — an already-deployed chain has the old genesis archive root committed to L1 archives[0] and cannot adopt the new one. Coordinated genesis migration is required for any redeploy.

Validation

  • C++ WorldStateTest genesis tests pass with the recomputed roots; @aztec/world-state native tests 50/50.
  • New e2e protocol_class_publish.test.ts: re-publishing a bundled protocol class is rejected as a duplicate nullifier. The protocol instance path is documented as unreachable on-chain (publish emits the derived address; no tx can have a magic address as msg_sender), so it is not separately testable.
  • L1 checkpoint fixtures regenerated; forge test on their consumers passes (69/69).
  • Fixed an napi argument-index collision uncovered once the module was loadable (the map_size/thread_pool/ephemeral indices had to shift to 7/8/9 after inserting prefilled_nullifiers at index 4), which otherwise broke every NativeWorldState constructor.

Changes

  • barretenberg (world-state, napi, wsdb, constants): prefilled_nullifiers plumbing + recomputed genesis constants; standalone wsdb IPC documented as non-production (seeds empty nullifiers).
  • noir-protocol-circuits / constants / l1-contracts: regenerated GENESIS_ARCHIVE_ROOT / GENESIS_BLOCK_HEADER_HASH; V5 deploy script and 6 checkpoint fixtures updated.
  • stdlib: required prefilledNullifiers on GenesisData.
  • protocol-contracts: generated ProtocolContractGenesisNullifiers + DEFAULT_GENESIS_DATA.
  • world-state: prod defaults switched to DEFAULT_GENESIS_DATA; seeded-nullifier wiring + ordering guard.
  • tests: new e2e + updated genesis literals.

@spalladino spalladino added ci-full Run all master checks. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure S-do-not-merge Status: Do not merge this PR labels Jun 23, 2026
…nesis

The world-state genesis only seeded prefilled public data, never the
registration nullifiers for the bundled protocol contract classes. The
archiver preloads those classes at a synthetic block 0, so a permissionless
on-chain re-publish of a bundled protocol class id was protocol-valid (the
class-id nullifier was absent at genesis), and when the archiver replayed that
block it hit a duplicate-key throw on the block-0 preload and stalled L1 sync.

Seed the canonical protocol contract registration nullifiers into the genesis
nullifier tree so such a re-publish pushes an already-existing nullifier and is
rejected as a duplicate nullifier before it ever reaches the archiver.

- Generate ProtocolContractGenesisNullifiers (siloed class-id and
  magic-address instance nullifiers, sorted ascending as the indexed tree
  requires) in protocol-contracts.
- Add DEFAULT_GENESIS_DATA (the canonical genesis: empty public data + the
  protocol nullifiers) and make GenesisData.prefilledNullifiers required.
  Production world-state defaults switch from EMPTY_GENESIS_DATA to
  DEFAULT_GENESIS_DATA; EMPTY_GENESIS_DATA now seeds an empty nullifier tree
  for low-level tree tests only.
- Plumb prefilled_nullifiers through the native WorldState and the napi
  wrapper, enforcing uniqueness/strict-increase before handing leaves to C++.

BREAKING CHANGE: seeding the nullifiers changes the genesis nullifier-tree
root, hence the genesis block header hash and genesis archive root. Recomputed
via WorldStateTest.GetInitialTreeInfoForAllTrees and propagated through
constants.nr to constants.gen.ts, ConstantsGen.sol and aztec_constants.hpp:
GENESIS_ARCHIVE_ROOT = 0x063786f95f1ae8ebd17b22cb07d7ba122cefae9b0ecb975f5dc3e6e576a5bddc,
GENESIS_BLOCK_HEADER_HASH = 0x1302a9e6f643ec596522764c20e5d08d60d33988f11c1559ee13bcd2e2bd8e5d.

Adds an e2e regression test that re-publishing a bundled protocol class fails
with a duplicate nullifier. The instance nullifier cannot be triggered
on-chain (a publish emits the derived address, never a magic protocol
address), which is documented in the test.

Fixes A-1257
…ests (A-1257)

The genesis nullifier-tree root changed to 0x1bcda34f33b87d40db8bb8ee1378ef7123c16c197da1ded6ea47659230559f42
after seeding protocol-contract registration nullifiers, so update the
two test sites that hardcode the old stale root.
@spalladino spalladino force-pushed the spl/a-1257-genesis-protocol-nullifiers branch from 41ad236 to 35c2820 Compare June 23, 2026 21:37
…ullifiers (A-1257)

The genesis defaults in NativeWorldStateService now use DEFAULT_GENESIS_DATA, which seeds
6 protocol-contract registration nullifiers into the nullifier tree at genesis. This changes
the nullifier-tree root embedded in the serialized AvmCircuitInputs, so the golden binary
used by avm_minimal.test.ts and the C++ hinting_dbs tests must be regenerated.

Regenerated with:
  AZTEC_GENERATE_TEST_DATA=1 yarn workspace @aztec/simulator test \
    src/public/public_tx_simulator/apps_tests/avm_minimal.test.ts

Verification: TS test passes without env var; C++ vm2_tests HintingDBs suite passes (15/15).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-full Run all master checks. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure S-do-not-merge Status: Do not merge this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant