feat(world-state): support prefilled nullifiers in genesis state#24567
Merged
PhilWindle merged 1 commit intoJul 8, 2026
Conversation
Add opt-in plumbing to seed nullifier leaves into the genesis nullifier tree, without changing production genesis. The field defaults to empty, so every existing genesis root stays bit-identical to today. - C++ WorldState constructors, the napi binding, create_canonical_fork and the standalone wsdb server gain a prefilled_nullifiers input, inserted as the nullifier tree's initial leaves. The indexed tree requires those leaves to be unique and strictly increasing. - GenesisData.prefilledNullifiers (optional, defaults to []) threads through the native world state; the TS side enforces the ordering invariant before the native call, and getGenesisValues gains an optional prefilledNullifiers param (sorted ascending) plus a fast-path guard so the GENESIS_ARCHIVE_ROOT short-circuit only fires for the canonical empty genesis. This is a subset port of #24254 kept shape-identical to it: it deliberately omits DEFAULT_GENESIS_DATA, the protocol-contract nullifier generation and all genesis-constant recomputation. When #24254 reaches the v5 line the mechanism will already exist. Coverage: a new C++ WorldStateTest.GetInitialTreeInfoWithPrefilledNullifiers plus world-state testing.test.ts cases proving empty-field equivalence, deterministic non-empty roots, and rejection of unsorted/duplicate leaves.
PhilWindle
approved these changes
Jul 8, 2026
PhilWindle
pushed a commit
that referenced
this pull request
Jul 8, 2026
Round-4 e2e speedup, PR 1b (adoption half of the AuthRegistry prize). **Stacked on #24567** (`spl/genesis-prefilled-nullifiers`, the genesis-nullifier mechanism). This PR targets that branch; retarget to `merge-train/spartan-v5` once #24567 merges. ## What Make e2e environments start with the standard contracts (AuthRegistry, PublicChecks, HandshakeRegistry) already "published", so `ensureAuthRegistryPublished` and its two siblings skip their two publish txs each. At production cadence `setup:auth-registry` alone was ~16 min/run (2 sequential txs per process, paid per-process across the suite). This collapses that span to sub-second. ## Mechanism (three parts, all default-off outside e2e) - **Genesis nullifiers.** Per standard contract, seed `siloNullifier(ContractClassRegistry, classId)` and `siloNullifier(ContractInstanceRegistry, instanceAddress)` (the real derived address — standard contracts are not magic-address protocol contracts) into the fixtures' `getGenesisValues` call, via PR 1a's new 5th param. These are exactly the nullifiers the publish txs would emit, so the AVM's deployment-nullifier check passes when the contracts are called publicly. A single helper (`getStandardContractGenesisNullifiers`) feeds all four e2e genesis builders (`fixtures/setup.ts`, `e2e_prover_test.ts`, `p2p/p2p_network.ts`, `multi-node/governance/add_rollup.test.ts`) — the latter three recompute a genesis that must reproduce the L1-deployed archive root, so they must seed the identical set. Non-e2e callers (cli, sandbox/local-network) are untouched. - **Archiver preload.** `registerStandardContracts` mirrors `registerProtocolContracts`: it seeds each contract's class (with recomputed `publicBytecodeCommitment`), instance, and public-function signatures into the contract store at block 0, reading the bundled `@aztec/standard-contracts` artifacts. It is idempotent (skips already-registered classes on restart) and gated behind a new archiver config flag `testPreloadStandardContracts` (env `TEST_PRELOAD_STANDARD_CONTRACTS`, default **false**). The flag is set from the e2e node config so every spawned node (validators, prover nodes) picks it up through the normal config path. This adds `@aztec/standard-contracts` as an archiver dependency. - **Guard short-circuit.** The `ensure*Published` helpers are unchanged. Their guards read `wallet.getContractClassMetadata(id).isContractClassPubliclyRegistered` (to `aztecNode.getContractClass`) and `wallet.getContractMetadata(addr).isContractPublished` (to `aztecNode.getContract`) — both are archiver-store reads, not nullifier-tree reads. The store preload alone makes both short-circuit; the genesis nullifiers are what make the contract actually callable in the AVM afterwards. Keeping the helpers doubles as a regression check: if seeding ever breaks, the tx path re-engages and tests still pass, just slow. ## Why the flag is test-only (A-1257 rationale) Preloading unconditionally in production would recreate the A-1257 collision #24254 fixed: a real on-chain publish of a preloaded class would collide with the block-0 preload, because production genesis will **not** carry the matching nullifiers. The flag is only set by the e2e fixtures, which also seed the nullifiers, keeping the store and the nullifier tree consistent. A single source of truth (`getPublishableStandardContracts`) drives both the preloaded set and the seeded-nullifier set so they cannot drift. ## State-write verification Inspected the two publish txs on this base: - `ContractClassRegistry.publish` pushes one nullifier (`classId`, siloed to the class-registry address) and broadcasts a `ContractClassPublished` contract-class log carrying the packed bytecode. No public-data writes. - `ContractInstanceRegistry.publish_for_public_execution` asserts the class-registration nullifier exists, then pushes one nullifier (`address`, siloed to the instance-registry address) and broadcasts a `ContractInstancePublished` private log. No public-data writes. So the full state-write set is two nullifiers + two broadcast logs. The nullifiers are replicated via genesis `prefilledNullifiers`; the archiver normally learns the class/instance from those two logs (`data_store_updater.ts`), and the block-0 preload replaces that path exactly. No `genesisPublicData` is required. PXE-side registration (`wallet.registerContract`) is unaffected and still runs in the helpers. ## Local verification - `automine/accounts/authwit.test.ts` (public-authwit path against the standard AuthRegistry), timing on: passes; `setup:auth-registry` span = **10 ms** (was ~33 s at production cadence). No on-chain AuthRegistry publish (the only `ContractClassRegistry.publish` executions are the test's own AuthWitTest/GenericProxy deploys); the public AuthRegistry contract executes fine against the seeded nullifier. `testPreloadStandardContracts: true` confirmed in the node config dump. - `single-node/fees/account_init.test.ts` (production sequencer + simulated prover node), timing on: 5/5 pass; `setup:auth-registry` span = **10 ms**. Prover node syncs against the seeded genesis (no root divergence), and the flag propagates to it. No duplicate-nullifier errors. - New unit test in `archiver/src/modules/data_store_updater.test.ts`: `registerStandardContracts` preloads each publishable standard contract's class + instance into the store and is idempotent on a second call. ## Expected impact ~14–16 min/run from auth-registry alone, plus more from the two untagged siblings (PublicChecks / HandshakeRegistry) wherever used. ## Measured impact `setup:auth-registry` collapses from 702.2s of setup time run-wide (39 occurrences, 5–35s each) to 482ms total (max 87ms per occurrence) — sub-second run-wide, every shard clean, no seeding or flag-propagation slow path. - The measured beforeHooks reduction is larger than the auth-registry span alone, because the two untagged sibling publishes (PublicChecks, HandshakeRegistry) are removed too. Example: account_init beforeHooks −49.6s vs a 30.9s tagged auth-registry span; the extra ~19s is the two siblings. - Per-suite beforeHooks deltas: account_init −49.6s, fee_settings −49.5s, failures −48.7s, private_payments.parallel −41.8s/shard (×8), gas_estimation.parallel −41.5s/shard (×3), l1_to_l2 −38.1s, l2_to_l1 −37.5s, l1_to_l2_inbox_drift −38.2s, token_bridge −37.7s, fee_juice_payments −37.4s, bot −34.3s; automine/parallel suites shed 5–15s each (their fast-cadence publishes). - Total beforeHooks reduction over the suites present in both runs: −1,649.7s summed across shards (wall-clock benefit is smaller, since shards run in parallel across workers). Baseline CI run 1783389062016888 (#24567 `ci/x-fast`; mechanism-only, so timing-neutral vs merge-base). PR CI run 1783391194852197 (#24568 `ci/x-fast`). ## Notes for downstream - **PR 6 (fees harness):** `applyEnsureAuthRegistryPublished` now short-circuits; the fees setup chain loses the ~32 s/shard auth-registry step, so rebase the overlap/batch shape onto what remains (token deploy, FPC, mints). - **Fix phase / timing capture:** expected span assertion is `setup:auth-registry` around sub-second across the run (10 ms observed locally per process). Any shard still showing tens of seconds means the guards took the slow path (a seeding or flag-propagation bug), not noise. Fixes A-1404
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.
What
Adds opt-in plumbing to seed nullifier leaves into the genesis nullifier tree, threaded through the C++ world state and the TypeScript world-state layer. The field is optional and defaults to empty, so production genesis is unchanged — every existing genesis root stays bit-identical to today.
WorldStateconstructors, the napi binding,create_canonical_fork, and the standaloneaztec-wsdbIPC server gain aprefilled_nullifiersinput, inserted as the nullifier tree's initial leaves. The indexed nullifier tree requires those leaves to be unique and strictly increasing (and distinct from the padding leaves), which the tree already enforces; the TS side additionally checks it before the native call for a friendlier error.GenesisData.prefilledNullifiers(optional, defaults to[]) threads throughnative_world_state_instance.ts.getGenesisValuesgains an optionalprefilledNullifiersparam (sorted ascending on the way in), and theGENESIS_ARCHIVE_ROOTfast-path ingenerateGenesisValuesnow also checks the nullifier list is empty, so the short-circuit only fires for the canonical empty genesis.Why
Mechanism for round-4 e2e speedup PR 1b, which will seed the standard-contract (AuthRegistry / PublicChecks / HandshakeRegistry) registration nullifiers at genesis in the e2e fixtures so their publish txs short-circuit. This PR carries no consumers and is intentionally behavior- and timing-neutral: with the field empty (the only value ever passed in production), the native tree, all genesis roots, and the archive root are identical to before.
Relationship to #24254
This is a deliberate subset port of #24254 ("seed protocol contract registration nullifiers at genesis"), kept shape- and signature-identical wherever it does not diverge, so that when #24254 itself reaches the v5 line the mechanism merges as "already applied". The intentional divergences:
prefilledNullifiersis optional and defaults to empty here (feat(protocol)!: seed protocol contract registration nullifiers at genesis #24254 made it required and non-empty by default viaDEFAULT_GENESIS_DATA).DEFAULT_GENESIS_DATA, the protocol-contract nullifier generation (protocol-contracts/src/genesis_data.ts,scripts/generate_data.ts), or any constant recomputation (GENESIS_ARCHIVE_ROOT,constants.nr,constants.gen.ts,ConstantsGen.sol,aztec_constants.hpp, checkpoint / AVM golden fixtures). With the field empty, every genesis root is unchanged, which is the invariant CI must prove.Testing (local)
world_state_tests:GetInitialTreeInfoForAllTrees(unchanged — still asserts the current empty-nullifier root0x18935581…andGENESIS_ARCHIVE_ROOT, proving empty-field behavior is bit-identical),GetInitialTreeInfoWithPrefilledPublicData(updated to pass an empty nullifier vector), and a newGetInitialTreeInfoWithPrefilledNullifiers(seeding changes the root, tree size stays 128, seeded leaves are present) — 3/3 pass.@aztec/world-statetesting.test.ts(7/7): empty genesis returnsGENESIS_ARCHIVE_ROOTvia the fast path; the empty-genesis computed root matches that constant; an explicit emptyprefilledNullifiersyields the same root as omitting the field; a non-empty sorted list yields a deterministic root that differs from empty;getGenesisValuessorts and seeds; and non-strictly-increasing (descending / duplicate) lists are rejected by the defensive check.@aztec/world-statenative_world_state.test.ts(53/53),@aztec/stdlibl2_block.test.ts(3/3, confirms the genesis nullifier root is unchanged),@aztec/prover-clientlightweight_checkpoint_builder.test.ts(7/7).yarn build, plusyarn format/yarn linton the touched packages, are clean.Notes
This PR is intentionally behavior- and timing-neutral; no e2e behavior changes until PR 1b adopts the mechanism.
Measured impact
None expected, and none observed.
prefilledNullifiersdefaults to[], so the production genesispayload and roots are bit-identical (asserted by the unchanged C++ empty-nullifier root and
GENESIS_ARCHIVE_ROOTtests); no fixture opts into a non-empty list in this PR.CI timings confirm neutrality: per-suite beforeHooks deltas scatter in both directions within the
per-suite noise floor — largest were runtime_config −12.8s, token_bridge −12.7s, account_init +11.5s,
failures +11.0s — with the fees suites essentially flat (private_payments −3.4s, gas_estimation
−0.2s) and no systematic direction. The summed beforeHooks delta over all 276 suites present in both
runs is −122.8s (~−2%), well inside accumulated run-to-run variance.
Baseline CI run 1783374468714213 (merge-base e1711d3, full). PR CI run 1783389062016888 (x-fast).
Part of A-1404