Skip to content

undeployed: protocol-selectable (midnight-1.x default | midnight-2.x) — UNDEPLOYED_PROTOCOL switch, v2 undeployed() profile, 2.x external-stack suite - #16

Merged
acedward merged 7 commits into
mainfrom
00008-shielded-night-undeployed-2x-lane
Sep 9, 2026
Merged

undeployed: protocol-selectable (midnight-1.x default | midnight-2.x) — UNDEPLOYED_PROTOCOL switch, v2 undeployed() profile, 2.x external-stack suite#16
acedward merged 7 commits into
mainfrom
00008-shielded-night-undeployed-2x-lane

Conversation

@acedward

@acedward acedward commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

What

undeployed becomes protocol-selectable instead of hard-wired to the Midnight 1.x (ledger-v8) lane, in the three places that decide it. Nothing changes by default.

Seam Before After
Page (frontend/) networks.ts declares undeployed as midnight-1.x; the hook loads the v1 adapter for it new setting UNDEPLOYED_PROTOCOLmidnight-1.x (default) | midnight-2.x, read like the addresses: build-time import.meta.env.UNDEPLOYED_PROTOCOL (envPrefix UNDEPLOYED_ already covered it), overridable at runtime by window.SHIELDED_NIGHT.UNDEPLOYED_PROTOCOL from /config.js (runtime wins). Label shows the generation (Local (undeployed · 2.x)). An invalid value is a visible error that disables Connect — never a silent fallback. A protocol-mismatch hint is logged when a connect fails after the adapter is reached.
v2 scripts (contracts/v2/scripts/) deploy.ts / verify-deployment.ts refuse every MN_ENV but stagenet; one stagenet() profile; maintenance-key record typed network: 'stagenet' on durable storage MN_ENVstagenet (default) | undeployed; undeployed() profile (MN_NODE_URL / MN_NODE_WS_URL / MN_INDEXER_URL / MN_INDEXER_WS_URL / MN_PROOF_SERVER_URL with the 1.x lane's loopback defaults, genesis-seed fallback with a warning); maintenance key optional and ephemeral on undeployed (default .local/private-state/v2-undeployed/maintenance-key.json, still 0600 + read-back, record network: 'undeployed', a key from another network is refused in both directions); per-env private-state store (shielded-night-v2-<env>) and record (v2-<env>-<addr>.json); prints <ENV>_ADDRESS=.
v2 tests (contracts/v2/test/) two unit files, no integration suite test/external/: a 2.x counterpart of the 1.x MN_EXTERNAL_STACK=1 round-trip suite — 11-circuit verifier-key check + metadata + authority + offline wrapper-colour derivation, two-step and atomic NIGHT ⇄ sNight with exact balance asserts, wrong-secret negative. Opt-in (bun run test:v2:external, refuses without MN_EXTERNAL_STACK, never starts or stops a stack). bun run test:v2 now runs through vitest.config.ts and collects zero external tests.

Docs: README gains a "Which lane runs where" table, the local 2.x recipe and the switch; TESTING.md the external suite; every bun --cwd frontend … is now bun --cwd=frontend … (the space form is a silent no-op on bun ≥ 1.4, which the committed lockfiles require).

Why

midnight-2-offers runs a local undeployed ledger-9 devnet. Since #13, main is multi-network but its 2.x half is stagenet-only, so the demo had to stay pinned to the ledger-v9 branch (infra 00017, Q15). This PR is option A of that question: the missing work upstream, so one branch and one page build serve every network and the demo can re-pin to main.

Not breaking

No default changes: undeployed is still midnight-1.x on the page, MN_ENV still defaults to stagenet in the v2 scripts, the durable maintenance key is still mandatory on stagenet and its error text is unchanged. The Stagenet deployment 052051d3…, its keys, frontend/.env, both managed/ trees and the public site are untouched (git diff 6c4b03d..HEAD -- src/managed contracts/v2/managed frontend/.env is empty).

Evidence (live, 2026-09-09, midnight-2-offers core stack — node 2.0.0-rc.4 / indexer 4.4.0-rc.3 / proof-server 9.0.0-rc.5)

  • US1 MN_ENV=undeployed … bun run deploy:v2UNDEPLOYED_ADDRESS=0f334f634b4ddf8bbeb6abefc03f19f87f98b9c1add6f69cb2cb55dbb135faf6, verified 11/11, locked=false, key at the default path (0600, network: "undeployed"); verify:deployment:v2code and metadata match. Negatives fail before any network call: MN_ENV=stagenet without a key file → unchanged Set MN_MAINTENANCE_KEY_FILE…; MN_ENV=previewInvalid MN_ENV "preview". The v2 commands support stagenet | undeployed.
  • US3 MN_EXTERNAL_STACK=1 MN_ENV=undeployed … bun run test:v2:external4/4 passed joining that contract (598 s) and 4/4 deploying fresh inside the suite (548 s). The on-chain tokenColor() equals rawTokenType(pad32("shielded-night:wrapper"), address).
  • US2 (page level) a UNDEPLOYED_PROTOCOL=midnight-2.x build and a stock build served with a rewritten /config.js: midnight-2.xLocal (undeployed · 2.x); ledger9UNDEPLOYED_PROTOCOL must be "midnight-1.x" or "midnight-2.x"; got "ledger9". with Connect disabled; no key → build-time value → 1.x default on a stock build; runtime midnight-1.x beats a 2.x build. The stock build + runtime key path is what the demo needs. The wallet-driven round trip in the browser needs a ledger-v9 wallet (Moth, feat: ledger v9 support via a dual ledger/SDK seam shieldedtech/moth-wallet#30) and was not performed here — the Node round trips above ran against the same contract.
  • Offline: root unit 160/160, v2 unit 27/27, root + frontend + v2 typecheck, frontend build (SHIELDED_NIGHT marker present).

Note for reviewers: a re-run of deploy:v2 deploys a second contract and reuses the key file — there is no resume path, same as the 1.x lane and the stagenet lane today; resumability stays with the caller (the demo image's JOIN on contract.json).

Downstream (midnight-2-offers, infra 00017 PR-D follow-up)

After merge: re-pin SHIELDED_NIGHT_REF from 30af63f (branch ledger-v9) to this merge commit; set UNDEPLOYED_PROTOCOL: "midnight-2.x" in the injected /config.js next to UNDEPLOYED_ADDRESS; run the v2 one-shots with MN_ENV=undeployed (npm --prefix contracts/v2 run deploy / verify:deployment, DEPLOY_OUT honoured). Config changes only, no patch.

MN_ENV now selects between stagenet (the default, unchanged) and a local
ledger-v9 devnet. requestedEnv() is the single validator and names both
accepted values; every env-dependent seam takes the env as an optional
trailing argument so an unparameterized call keeps today's behaviour.

- profile.ts: V2_ENV_NAMES / V2EnvName / requestedEnv / profileFor;
  undeployed() with the 1.x lane's loopback defaults and MN_INDEXER_URL,
  MN_INDEXER_WS_URL, MN_NODE_URL, MN_NODE_WS_URL, MN_PROOF_SERVER_URL
  overrides; WALLET_NETWORK_IDS (pinned to the SDK constants from
  deploy.ts at typecheck time so the unit tier need not load the wallet
  barrel); GENESIS_MINT_SEED; privateStateStoreName(env).
- Maintenance key: the record's network is the env and is checked on
  read, so a stagenet key can never be used on undeployed or the
  reverse. On stagenet the durable absolute path stays mandatory; on
  undeployed the file is optional and defaults to the gitignored
  .local/private-state/v2-undeployed/maintenance-key.json, with a
  relative override accepted. Mode 0600 and the write/read-back check
  apply on both.
- Records and output: v2-<env>-<address>.json, `[deploy] confirmed <env>
  contract`, UNDEPLOYED_ADDRESS= on undeployed, network.name = env.
- deploy.ts: on undeployed only, a missing seed falls back to genesis-1
  with a printed warning that it is shared; stagenet still refuses.
- verify-deployment.ts: same env selection; the CV_ADDRESS message names
  the selected env.
- Tests: new profile-env.unit.test.ts covers env selection, the profile
  defaults and overrides, key custody per env in both directions, record
  paths and the printed lines. The existing file now pins MN_ENV unset so
  it keeps asserting the stagenet defaults.
Preview, Preprod and Stagenet are pinned to the ledger generation their
chain runs, but "Local (undeployed)" is whichever devnet is on the other
end, and it was hard-wired to midnight-1.x. A local Midnight 2.x devnet
therefore got the ledger-v8 adapter and could not be served at all.

Make that row a setting read exactly like the contract addresses:
build-time `UNDEPLOYED_PROTOCOL` (the vite envPrefix already exposes
`UNDEPLOYED_`), overridable at runtime by
`window.SHIELDED_NIGHT.UNDEPLOYED_PROTOCOL` from an injected /config.js,
default `midnight-1.x` — so every existing build and deployment behaves
exactly as before.

- runtime-config.ts: `resolveUndeployedProtocol()` next to
  `resolveContractAddress()`; runtime wins, blank falls through, values
  are trimmed and case-sensitive, and anything else is an ERROR naming
  the variable and both accepted values rather than a silent fallback —
  a guess here picks the wrong ledger adapter against a real chain. The
  module stays dependency-free and free of `import.meta.env`.
- networks.ts: `resolveNetwork()` returns the row with the setting
  applied (label `Local (undeployed · 2.x)` on 2.x); the exported
  NETWORKS const is never mutated, because other consumers import it.
  `contractConfigurationError()` reports a bad protocol value FIRST, so
  it reaches the page through the existing plumbing that disables
  Connect. `protocolMismatchHint()` explains the failure a wallet of the
  other generation produces — the wallet never announces its own.
- useShieldedNight: resolve the row instead of looking it up statically,
  and log that hint when a connection fails at or after adapter
  creation.

Unit tests cover default / build-time / runtime-wins / blank / invalid
for both seams; the docs get the switch next to the address override.
The v2 tree gets the local-chain integration the 1.x tree already has, as
the 2.x counterpart of MN_EXTERNAL_STACK=1. There is no self-booting
mode: this tree carries no 2.x compose file, so the suite only ever
joins a stack somebody else started and never tears anything down.

- test/external/global-setup.ts refuses to run unless MN_EXTERNAL_STACK
  is truthy, naming the variable; MN_ENV defaults to undeployed here
  (stagenet is accepted, and then MN_SEED is mandatory); the indexer,
  node and proof server are probed with a 10 s fetch and a failure names
  the URL(s) that are wrong; the resolved env and profile are handed to
  the forked workers.
- test/external/shielded-night.external.test.ts reuses the deploy path's
  building blocks (MidnightWalletProvider.build, syncWallet, the funding
  assertion and sync-timeout policy from profile.ts,
  initializeMidnightProviders, verifyAddress) rather than porting a
  second wallet builder. CV_ADDRESS joins an existing deployment via
  findDeployedContract; without it the suite deploys with a maintenance
  key sampled for the run. Cases: the 11-circuit set with matching
  verifier keys, release metadata and (when it deployed) the maintenance
  authority; the two-step round trip with exact balances; the atomic
  convertToShielded/convertToUnshielded pair with exact balances; and a
  wrong-secret withdrawal refused with "no balance for this secret".
- Config split: vitest.config.ts is the unit tier and keeps
  test/external/** out through both the include pattern and the exclude
  list, so `bun run test:v2` and CI's unit-v2 job can never collect a
  test that needs a chain. vitest.external.config.ts runs the suite
  serially in forked workers with 10-minute test timeouts and retries
  off by default, because every case asserts exact balances.
- Scripts: contracts/v2 `test:external`, root `test:v2:external`.
- TESTING.md documents the suite, its env block, what it asserts, and
  that it needs a throwaway stack.
…-cwd= form

README gains a "Which lane runs where" table (preview/preprod 1.x, stagenet 2.x,
undeployed 1.x by default or 2.x opt-in) and a local 2.x recipe: MN_ENV=undeployed
deploy:v2, verify:deployment:v2 and the MN_EXTERNAL_STACK=1 round-trip suite, with
the printed <ENV>_ADDRESS, the per-env deployment record and the optional
undeployed maintenance key. It also states that deploy:v2 always deploys a NEW
contract, so resuming is the caller's job, as on the 1.x lane.

UNDEPLOYED_PROTOCOL joins the runtime knob table (build-time env or
window.SHIELDED_NIGHT, midnight-1.x default, invalid values reported on the page
with Connect disabled), and the frontend README reconciles with it; the stale
runtime-override link is repointed at its renamed heading.

Every documented `bun --cwd frontend ...` becomes `bun --cwd=frontend ...`: bun
1.4 prints its usage and exits 0 for the space form, so those commands silently
did nothing. Both READMEs and TESTING.md now say the lockfiles need bun >= 1.4.

.env.example gains a commented block for the v2 undeployed lane.
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