Skip to content

perf(e2e): seed standard contracts at genesis - #24568

Merged
PhilWindle merged 2 commits into
merge-train/spartan-v5from
spl/e2e-genesis-standard-contracts
Jul 8, 2026
Merged

perf(e2e): seed standard contracts at genesis#24568
PhilWindle merged 2 commits into
merge-train/spartan-v5from
spl/e2e-genesis-standard-contracts

Conversation

@spalladino

@spalladino spalladino commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

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

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.
Make e2e environments start with the standard contracts (AuthRegistry,
PublicChecks, HandshakeRegistry) already "published" so the
ensure*Published setup helpers skip their two publish txs each. At
production cadence setup:auth-registry alone was ~16 min/run
(2 sequential txs per process); this collapses it to sub-second.

Three parts, all default-off outside e2e:

- Genesis nullifiers: per standard contract, seed
  siloNullifier(ContractClassRegistry, classId) and
  siloNullifier(ContractInstanceRegistry, instanceAddress) into the
  fixtures' getGenesisValues call (new 5th param from PR 1a). These are
  exactly the nullifiers the publish txs would emit, so the AVM
  deployment-nullifier check passes when the contracts are called.
  Seeded consistently across all four e2e genesis builders (setup,
  prover fixture, p2p network, add_rollup) since the latter three
  recompute a genesis that must match the L1-deployed archive root.

- Archiver preload: registerStandardContracts mirrors
  registerProtocolContracts, seeding class/instance/public-function
  signatures into the contract store at block 0. Gated behind a new
  test-only archiver config flag (testPreloadStandardContracts, default
  false), set from the e2e node config so every spawned node (validators,
  prover nodes) picks it up. This is what makes the wallet guards
  (getContractClass / getContract, both archiver-store reads)
  short-circuit both publish txs. Gating avoids recreating the A-1257
  publish-collision bug: production genesis has no matching nullifiers.

The two publish txs write only their respective nullifiers plus a
class/instance broadcast log (no public data), so no genesisPublicData
is needed; the store preload replaces the log-consumption path. The
ensure*Published helpers are unchanged, so if seeding ever regresses the
tx path re-engages and tests still pass (just slow).
@spalladino spalladino added the ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure label Jul 7, 2026
@spalladino spalladino added the wip Work in progress label Jul 7, 2026
@spalladino spalladino removed the wip Work in progress label Jul 7, 2026
Base automatically changed from spl/genesis-prefilled-nullifiers to merge-train/spartan-v5 July 8, 2026 10:37
@PhilWindle
PhilWindle merged commit f320049 into merge-train/spartan-v5 Jul 8, 2026
42 of 46 checks passed
@PhilWindle
PhilWindle deleted the spl/e2e-genesis-standard-contracts branch July 8, 2026 11:40
PhilWindle pushed a commit that referenced this pull request Jul 21, 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

(cherry picked from commit f320049)
PhilWindle pushed a commit that referenced this pull request Jul 21, 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

(cherry picked from commit f320049)
rangozd pushed a commit to rangozd/aztec-packages that referenced this pull request Aug 5, 2026
Round-4 e2e speedup, PR 1b (adoption half of the AuthRegistry prize).

**Stacked on AztecProtocol#24567** (`spl/genesis-prefilled-nullifiers`, the
genesis-nullifier mechanism). This PR targets that branch; retarget to
`merge-train/spartan-v5` once AztecProtocol#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 AztecProtocol#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 (AztecProtocol#24567 `ci/x-fast`; mechanism-only, so
timing-neutral vs
merge-base). PR CI run 1783391194852197 (AztecProtocol#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

(cherry picked from commit f320049)
rangozd pushed a commit to rangozd/aztec-packages that referenced this pull request Aug 5, 2026
…rotocol#24930)

Forward-ports the **end-to-end / testing** slice of the v5-next → next
backlog (work merged to `v5-next` after the ~2026-07-08 cut that
reshaped `next`).

## Applied (clean cherry-picks, chronological)
- AztecProtocol#24518 docs(e2e): update READMEs for the consolidated suite layout
- AztecProtocol#24534 test(e2e): instrument and diagnose bot suite setup cost
- AztecProtocol#24566 perf(e2e): warp dead waits in multi-node recovery and proving
tests
- AztecProtocol#24570 perf(e2e): shrink e2e slot times
- AztecProtocol#24564 perf(e2e): seed BananaFPC fee juice at genesis instead of
bridging
- AztecProtocol#24568 perf(e2e): seed standard contracts at genesis
- AztecProtocol#24597 chore: add writing-e2e-tests skill
- AztecProtocol#24590 feat(e2e): interactive handshake e2e
- AztecProtocol#24671 test: fix proof_boundary startup race

## ⚠️ Needs owner conflict-resolution (conflict against reshaped `next`;
not included here)
Cherry-pick onto this branch and resolve:
- [ ] AztecProtocol#24569 `git cherry-pick -x 1d280af` — perf(e2e): overlap and
batch setup txs in e2e harnesses
- [ ] AztecProtocol#24479 `git cherry-pick -x da9ac1c` — feat: assert non
revertible phase when setting fee payer

Part of the manual v5-next backlog sweep. Draft until owners resolve the
conflicts above and CI is green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants