Skip to content

feat(paseo): substitute relay genesis preset + launch/handover tooling#389

Merged
al3mart merged 12 commits into
mainfrom
feat/substitute-relay
Jul 2, 2026
Merged

feat(paseo): substitute relay genesis preset + launch/handover tooling#389
al3mart merged 12 commits into
mainfrom
feat/substitute-relay

Conversation

@al3mart

@al3mart al3mart commented Jun 30, 2026

Copy link
Copy Markdown
Member

What & why

Tooling to stand up a fresh Paseo relay from block 0 — the substitute relay that takes over once the current relay is wound down — and to hand validator election to Asset Hub afterwards.

The plan hinges on the post-AHM architecture: Session::SessionManager = NoteHistoricalRoot<Self, StakingAhClient>, so the relay's validator set comes from pallet_staking_async_ah_client, with local pallet_staking only as a fallback. Everything keys off ah_client's operating mode:

Mode new_session source session reports → AH accepts AH set use
Passive (default) local pallet_staking (Fallback) no no (Blocked) bootstrap
Buffered none → current set frozen no no (Blocked) brief staging step
Active AH-provided ValidatorSet yes yes (≥ MinimumValidatorSetSize) steady state

Transitions are forward-only: Passive → Buffered → Active.

The substitute relay boots in Passive, self-electing a small bootstrap set from local staking — fully independent of Asset Hub. Once AH is onboarded and its staking populated, the operator drives ah_client.set_mode(Buffered)set_mode(Active) and the relay starts consuming AH's elected set.

What's in this PR

Runtimerelay/paseo/src/genesis_config_presets.rs:

  • New preset id substitute (paseo_substitute_genesis()), registered in preset_names() + get_preset().
  • 4 bootstrap authorities — Alice/Bob/Charlie/Dave dev-key placeholders, to be replaced with real operator keys before any real launch.
  • sudo.key reused from the current on-chain relay sudo (13uYxsEfJL5FYbJ1E7cW85ihp5LckYTyZT6Bqpc7tS4NAArK, embedded as raw bytes).
  • forceEra: ForceNone (freezes the bootstrap set — no ElectionProviderMultiPhase churn during the Passive window).
  • scheduler_params.num_cores = 2 (the default leaves it at 0, which can't back any parachain; raise toward 20 post-launch via coretime.request_core_count as validators scale).
  • ah_client omitted → Mode defaults to Passive. No paras/HRMP at genesis.

cargo check -p paseo-runtime passes.

Toolingsubstitute-relay/:

  • README.md — launch + staking-handover runbook (genesis spec, chain-spec-builder steps, Phases 1–5, event-watch table, failure modes).
  • zombienet-launch.toml — boot the 4-validator relay and confirm it self-elects in Passive.
  • chopsticks/ — relay-only, sudo-driven dry-run of Passive → Buffered → Active and the MinimumValidatorSetSize gate (no Asset Hub, no XCM — exploits that validator_set accepts Root, so synthetic sets can be fed via sudo).
  • tools/derive-session-keys.mjs — derive real bootstrap keys + keystore-insert commands to replace the dev placeholders.

Verified

The chopsticks dry-run was run against a fork of the live relay (spec 2003001, 152 live validators):

Step Observed
force Mode → Passive Passive
validator_set @ Passive sudo.Sudid: Err = StakingAhClient::Blocked ✅ refused
set_mode(Buffered) Buffered
set_mode(Active) Active
validator_set(4) @ Active SetTooSmallAndDropped, not stored ✅ min-size gate (4 < 100)
validator_set(152) @ Active ValidatorSetReceived, stored ✅ accepted (≥ 100)

Notes captured during the run: the live extrinsic is set_mode (not buffer()/activate(), which are internal helpers); chopsticks 1.5.0 needs the ESM cli + a yargs@17 pin under Node 20; the driver funds //Alice on the fork (else 1010 invalid payment). All documented in chopsticks/README.md.

Before a real launch (not in this PR)

  • Replace the 4 dev validators with real, team-generated bootstrap session keys (tools/derive-session-keys.mjs).
  • Lower MinimumValidatorSetSize (default 100) to ≤ the first real cohort before the handover; raise it as you scale.
  • Validators must bond → validate → set_keys on AH so the new relay's Session keys exist (a fork-off of AH ledgers does not carry relay session keys).

Out of scope / follow-ups

  • Asset Hub state migration onto the fresh relay (era/session reconciliation, fork-off vs clean reset).
  • Full AH→relay handover dry-run with a real election + XCM export (chopsticks XCM mode or zombienet).
  • Bridge / BEEFY / GRANDPA continuity decision.

Testing

  • cargo check -p paseo-runtime
  • chopsticks dry-run ✅ (output above)
  • zombienet launch test — not run here (needs binaries)

al3mart added 12 commits June 30, 2026 21:48
Add tooling to launch a fresh Paseo relay from block 0 (the "substitute"
relay that will take over once the current relay is wound down) and hand
validator election to Asset Hub.

Paseo is post-AHM: Session::SessionManager = NoteHistoricalRoot<_, StakingAhClient>,
so the relay's validator set comes from pallet_staking_async_ah_client, with local
pallet_staking only as a Passive-mode fallback. The substitute relay boots in
Passive (self-elects a small bootstrap set from local staking), then transitions
ah_client Passive -> Buffered -> Active to consume Asset Hub's elected set.

Changes:
- relay/paseo/src/genesis_config_presets.rs: new `substitute` preset
  (paseo_substitute_genesis) — 4 bootstrap authorities (dev-key placeholders),
  sudo reused from the current on-chain key, forceEra=ForceNone, num_cores=2,
  ah_client defaults to Passive. Registered in preset_names + get_preset.
- substitute-relay/README.md: launch + staking-handover runbook.
- substitute-relay/zombienet-launch.toml: boot the 4-validator relay.
- substitute-relay/chopsticks/: relay-only, sudo-driven dry-run of
  Passive->Buffered->Active and the MinimumValidatorSetSize gate (no AH/XCM).
- substitute-relay/tools/derive-session-keys.mjs: real-key substitution helper.
Wrap a doc comment and collapse an iterator chain in the substitute genesis
preset (nightly rustfmt), and reformat zombienet-launch.toml (taplo) to satisfy
the fmt CI checks.
substitute_host_configuration previously derived from
default_parachains_host_configuration, which is the runtime's original genesis
default and has drifted from what governance has set on-chain (validation
upgrade delays, HRMP channel limits, scheduler params, node_features, ...).

Replace it with a faithful snapshot of the live Paseo relay
configuration.activeConfig (spec 2_003_001, surveyed 2026-06-30), overriding
only num_cores (56 -> bootstrap value; raised post-launch via
coretime.request_core_count). max_validators_per_core carries over the live
value of 3, already the target ratio.

Also fix a stale doc reference: the handover extrinsic is set_mode, not
buffer()/activate() (those are internal helpers).
Make the substitute relay ready to take real community-operator keys without
putting secrets in source:

- add substitute_authority(stash, babe, grandpa, para_validator,
  para_assignment, authority_discovery, beefy), building the bootstrap authority
  tuple from PUBLIC keys via hex! + unchecked_into;
- express the 4 bootstrap slots through it (still dev keys, clearly marked
  PLACEHOLDER, so zombienet/chopsticks keep working);
- add substitute-relay/tools/format-operator-keys.mjs: validates an operator's
  stash + author_rotateKeys blob (or the 6 keys individually) and emits the
  ready-to-paste initial_authorities block;
- document the intake process + submission format in substitute-relay/README.md.

All six session keys are required (babe/grandpa/para_validator/para_assignment/
authority_discovery/beefy) — authoring-only would give no finality and no
parachain backing.
Replace the operator-1 dev placeholder in the substitute genesis with real keys
submitted by turboflakes (turboflakes.io): stash + 6 session keys (babe,
grandpa, para_validator, para_assignment, authority_discovery, beefy), all
public. Operators 2-4 remain DEV placeholders pending their submissions.
Slot turboflakes' second validator (turboflakes.io/01) into bootstrap position 2
of the substitute genesis: stash + 6 session keys, all public. Operators 3-4
remain DEV placeholders (Charlie/Dave) pending their submissions.
Slot CoinStudio into bootstrap position 3 of the substitute genesis: stash + 6
session keys, all public. Operator 4 remains a DEV placeholder (Dave) pending
the final provider's submission.
Slot ParaNodes into bootstrap position 4 of the substitute genesis (stash + 6
session keys, all public; their submission included an im_online key from an
older runtime, dropped since our SessionKeys has no im_online). All four
bootstrap authorities are now real community providers — no dev placeholders.
Generate the launch chain spec for the substitute relay and the supporting bits:

- lib.rs: SS58Prefix 0 -> 42. NOTE: this is a relay/paseo runtime-wide constant,
  so any chain built from this runtime becomes prefix 42 — intended, as the
  substitute IS the new Paseo; the old relay is being retired.
- genesis preset: substitute_host_configuration uses the MAX_CODE_SIZE/MAX_POV_SIZE
  hard limits. The live max_code_size=3_500_000 was set post-genesis by governance
  and exceeds MAX_CODE_SIZE (3 MiB), so it can't be used at genesis; raise it after
  launch via configuration.set_max_code_size if a system chain's wasm needs >3 MiB.
- generate_chain_specs.sh: paseo-substitute case (name "Paseo", id "paseo",
  protocol-id "pad", type live, is-relay, per-package ss58Format=42, chain=substitute).
- chain-specs/paseo-substitute.json: generated raw spec — 4 real provider bootstrap
  validators, sudo = current on-chain key, num_cores=2, ah_client Passive.
- substitute-relay/PROVIDER-KEY-INSTRUCTIONS.md: provider key-intake guide.
Add the two initial bootstrap nodes (193.39.195.54, tcp + ws each) to
chain-specs/paseo-substitute.json, matching the published paseo.raw.json.
Match the published paseo.raw.json: single turboflakes bootnode
(/dns/n8r2.bn.turboflakes.io/tcp/30433/wss/p2p/12D3KooWC3no…nPXbjG).
…rypto check

CoinStudio's submission had babe/grandpa reversed (babe was their ed25519 key,
grandpa their sr25519 key) — the runtime's SessionKeys is grandpa-first, so a
rotateKeys blob split babe-first swaps them. Swap them back in the genesis preset
and regenerate paseo-substitute.json (verified: grandpa=ed25519, babe=sr25519 for
all four operators, correctly placed).

Also harden format-operator-keys.mjs: LAYOUT is now grandpa-first (matches the
runtime), and it verifies each key's crypto (babe/para/authority=sr25519,
grandpa=ed25519, beefy=secp256k1), hard-failing a wrong-type/swapped submission.
@al3mart al3mart merged commit e99e83a into main Jul 2, 2026
1 check passed
@al3mart al3mart deleted the feat/substitute-relay branch July 2, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant