Conversation
added 3 commits
September 2, 2026 19:05
Long-lived `ledger-v9` branch. `main` stays the 1.x/preview line and is what https://shielded-night.pages.dev runs; this branch targets node 2.0.0-rc.4 / indexer-standalone 4.4.0-rc.1 / proof-server 9.0.0-rc.5 and is merged into `main` when the network moves to 2.x. Contract Only the pragma changed: `language_version 0.23` -> `0.26` (toolchain 0.34.0 = compactc 0.34.0, language 0.26.0, compact-runtime 0.19.0, ledger 9). None of the 0.25/0.26 tripwires bit: no `as Field` casts, no point/scalar-type import moves, no stdlib renames — and the two ATOMIC circuits, which net a shielded and an unshielded move inside one segment, compile unchanged. All 44 files under src/managed/keys and src/managed/zkir are BYTE-IDENTICAL to the 0.31.1 build, so the deployed preview contract's verifier keys stay valid. Only the generated TypeScript moved (circuits and `initialState` are async on runtime 0.19, plus a new `expectedVk` export), and compactc 0.33+ emits compiler/contract-manifest.json, which midnight-js 5's fetch provider now verifies artifacts against. SDK set @midnightntwrk/ledger-v9 1.0.0-rc.3 (new scope, no hyphen), compact-runtime 0.19.0, compact-js 2.5.5-rc.8, midnight-js 5.0.0-beta.7, @midnightntwrk/ wallet-sdk 2.0.0-beta.2, wallet-sdk-address-format 4.0.0-beta.2; dapp-connector-api stays 4.0.1. midnight-js must be beta.7, not beta.6: midnight-js-protocol beta.6 pins compact-runtime 0.18.0-rc.1, and the generated contract opens with `checkRuntimeVersion('0.19.0')`, whose guard rejects a minor mismatch while the major is 0. compact-runtime 0.19.0 and the 0.19.0-rc.0 that beta.7 pins differ only in the version string, so the overrides block collapses them. Harness ledger-v8 -> ledger-v9 is a pure specifier rename at all 11 import sites. The real work was runtime 0.19's async flip (the simulator gains a static `create()` and async accessors; the unit tests await, and negative cases become `rejects.toThrow`) and three wallet-sdk 2 changes: `createKeystore` now takes `{ kind: 'schnorr', secret }` rather than raw bytes, TransactionHistoryCommonSchema was renamed, and `SignSegment` is async (use the keystore's own `signDataAsync`). Frontend Same SDK set, `resolve.dedupe` updated to the new package names (the build still emits exactly one ledger wasm and one onchain-runtime wasm). FetchZkConfigProvider takes an options bag now, and `ZswapChainState.postBlockUpdate` gained a required retentionDuration. ledger-v9 widened `ProvingProvider` with `lookupKey`, which midnight-js 5 requires — but dapp-connector-api declares only `check`/`prove` in 4.0.1 AND in 4.1.0-beta.1. `withLookupKey()` uses the wallet's implementation when it has one and otherwise serves the same public artifacts the dApp already handed the wallet, logging which lane it took. Stack and CI envs/docker-compose-dynamic.yml moves to the 2.x triple, mirroring midnight-2-offers' compose/core.yml: the node volume must be at /node (the 2.x image sets BASE_PATH=/node/chain), the indexer is linux/amd64-only and its 4.3.x password env keys are gone, and both volumes are wiped together because a v8->v9 chain cannot be upgraded in place. CI pins COMPACT_VERSION 0.34.0 and runs on pushes to `ledger-v9` as well as `main`. README and TESTING describe the two lines.
The first cut of `withLookupKey` derived a circuit id with
`keyLocation.split('/').pop()` and let any failure propagate. Both halves were
wrong at the edges that matter, and midnight-js already exports the pieces to
do it properly (`ZKConfigRegistry`, `zkConfigToProvingKeyMaterial`):
- a canonical contract key location resolves through the registry's
verifier-key join, not by slicing the string;
- a protocol builtin (`midnight/...`) must resolve to `undefined` so the prover
supplies it, while a `ZkArtifactIntegrityError` must still THROW — "the
artifact is present but stale or tampered with" cannot be masked as "no key
material".
`makeKeyMaterialResolver` here is transcribed from midnight-js 5's own resolver
in midnight-js-http-client-proof-provider so both behave identically.
Also delegate `check`/`prove` explicitly instead of object-spreading the
wallet's provider, which would drop prototype methods if a connector ever
returns a class instance rather than a plain object.
The first CI run on this branch was killed by `timeout-minutes: 60` partway through transient.test.ts. It was not stuck — it had passed 15 of the 17 tests and was still going. 60 was already thin on `main`: its last green run spent 50.3 of its 60 minutes inside vitest. Measured per-file, 1.x vs this branch: security.test.ts (5) 1078.7 s -> 1373.4 s 1.27x shielded-night.test.ts (8) 885.7 s -> 1101.2 s 1.24x multiparty.test.ts (1) 471.8 s -> 753.9 s 1.60x maintenance-authority (1) 153.8 s -> 153.8 s 1.00x (fixed waits, no proving) Only the proving-bound files stretched; the fixed-wait one did not move at all, which is what says this is proving/settlement cost on the 2.x triple rather than something hanging. Projected total is ~66 minutes of test time, so 150 covers it plus the `retry: 2` tail while still being short enough to catch a genuine hang.
Brings in from main: PR #11 (frontend/.env PREPROD_ADDRESS=e354e672...) and PR #12 (scripts/verify-deployment.ts --allow-unlocked + scripts/verify-args.ts + tests + docs, plus a bun-1.4 lockfile regeneration with new root overrides compact-js 2.5.1 / onchain-runtime-v3 3.0.0 and a frontend onchain-runtime-v3 3.0.0 override). Conflict resolution: package.json / frontend/package.json kept this branch's own v9 overrides (ledger-v9 1.0.0-rc.3, compact-runtime 0.19.0, onchain-runtime-v4 4.0.0-rc.3, plus frontend's compact-js 2.5.5-rc.8) and documented in the //overrides comment why main's v8-line overrides (compact-js 2.5.1, ledger-v8 8.1.0, onchain-runtime-v3 3.0.0) do not apply to this dependency graph: ledger-v8 and onchain-runtime-v3 are not in this tree at all, and compact-js is already pinned exact here so needs no defensive override. bun.lock and frontend/bun.lock were not hand-merged: both were deleted and regenerated with bun 1.4.0 over a git-archive-style export (no root .env mounted or read), then verified frozen-installable, exactly one copy each of ledger-v9 / compact-runtime / onchain-runtime-v4 / compact-js in both trees, and zero copies of ledger-v8 / onchain-runtime-v3. Everything else (README, TESTING, frontend/.env, scripts/verify-deployment.ts, scripts/verify-args.ts, its unit test) auto-merged cleanly with no conflicts.
…llo/client + the indexer provider
Every integration test file crashed at import time under MN_EXTERNAL_STACK=1
(vitest's SSR loader, i.e. anyone proving this branch against a real deployed
stack instead of vitest's own testcontainers): "require() async module
'.../graphql/__dev__/index.mjs' is unsupported". graphql@17's package.json
exports map lists Bun's own "bun" condition before "require" in every
branch, both pointing at an ESM-only .mjs; Bun always activates its "bun"
condition, so any resolution of the bare specifier "graphql" -- including
graphql-tag's plain CJS require('graphql') -- lands on the async file, and
Bun refuses to require() it synchronously. Plain `bun run` of the same
dependency chain (e.g. scripts/verify-deployment.ts) is unaffected because
it never goes through vitest's SSR module runner.
The fix is not just inlining the leaf packages. Under `pool: 'forks'`,
vitest loads an externalized (non-inlined) module wholesale via a plain
runtime import()/require(), and from that point on every one of that
module's own internal requires is a native Bun require with zero further
Vite involvement -- so inlining only graphql/graphql-tag/@apollo/client did
nothing while @midnight-ntwrk/midnight-js-indexer-public-data-provider
itself stayed externalized (its own require('graphql-tag') still resolved
natively and still crashed). Inlining the indexer provider too routes its
whole module graph through Vite's SSR transform, which resolves the nested
graphql/graphql-tag specifiers itself instead of leaving a raw externalized
require() for Bun to resolve on its own.
vitest.unit.config.ts is untouched: no unit test imports this chain.
Measured (bun 1.4.0, oven/bun:1.4 containers, git-archive export, no root
.env read):
- bun run test:unit: 95/95 passing (--no-file-parallelism; the default
parallel mode has a pre-existing flaky tail on this host's bind mount,
unrelated to this change and already noted in the F2.1 log)
- MN_EXTERNAL_STACK=1 against literally dead URLs (http://127.0.0.1:1):
fails at the global-setup preflight naming the unreachable URLs, same as
before this change -- the preflight runs before file collection either way
- MN_EXTERNAL_STACK=1 against reachable-but-fake stub endpoints (so the
preflight passes and collection is actually reached), unfiltered across
all six integration files, matching CI's own invocation shape: all 6
files collect cleanly, 17/17 tests discovered, zero crashes. The same
stub setup against the pre-fix config crashed on 5 of 6 files with the
exact require() error above.
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.
DO NOT MERGE UNTIL LEDGER v9
This PR is meant to stay OPEN
ledger-v9is a long-lived branch, not a change queued for merge.mainremains the Midnight 1.x / preview line — it is what https://shielded-night.pages.dev runs against the locked contract80b89b9a…bee1396. This branch is the 2.x port, and it is merged intomainonly when the Midnight network itself moves to 2.x. The PR exists so the branch is reviewable and its head is a citable, fetchable SHA.Not a breaking change to
main: nothing onmainis touched, and no published deployment changes behaviour.The two lines are kept apart deliberately: one dependency tree cannot hold both ledger wasm modules — two copies give two class identities and every cross-copy
instanceoffails. That is the same hazard bothpackage.jsonoverridesblocks already exist to prevent.mainledger-v9(this branch)@midnight-ntwrk/ledger-v88.1.0@midnightntwrk/ledger-v91.0.0-rc.3 (new scope, no hyphen)@midnightntwrk/wallet-sdk^1.2.0@midnight-ntwrk/…3.1.0@midnightntwrk/…4.0.0-beta.2The contract needed one line changed
pragma language_version 0.23;→0.26;and nothing else. None of the language 0.25/0.26 tripwires bit: noas Fieldcasts (Fieldis no longer a supertype ofUint, but this contract's arithmetic was already annotated), no point/scalar-type import moves (JubjubPoint/Secp256k1*are unused), no stdlib renames —receiveUnshielded,sendUnshielded,receiveShielded,sendImmediateShielded,shieldedBurnAddress,mintShieldedToken,tokenType,kernel.self()andnativeToken()all kept their signatures.That includes the part most at risk: the two ATOMIC circuits (
convertToShielded,convertToUnshielded), which net a shielded and an unshielded move inside a single segment.All 44 files under
src/managed/keys/andsrc/managed/zkir/are byte-identical to the 0.31.1 build. The verifier keys of the deployed preview contract stay valid across the toolchain bump; only the generated TypeScript moved. Two independent full compiles of this source reproducesrc/managed/byte-for-byte.No
--feature-zkir-v3: the emitted keys are taggedmidnight:verifier-key[v6]and the zkir is version 2, so the plainproof-server:9.0.0-rc.5proves them — the_experimentalbuild is not needed.New in compactc 0.33+:
compiler/contract-manifest.json. It is now load-bearing rather than decorative — midnight-js 5'sFetchZkConfigProviderverifies every artifact against it withverifydefaulting to'require'(fail-closed), so any deployment must serve the wholesrc/managed/tree, manifest included.Why midnight-js 5.0.0-beta.7 and not beta.6
The generated contract opens with
__compactRuntime.checkRuntimeVersion('0.19.0'), and that guard rejects a minor mismatch outright while the major is 0.@midnight-ntwrk/midnight-js-protocol@5.0.0-beta.6pinscompact-runtime 0.18.0-rc.1; beta.7 pins0.19.0-rc.0. So beta.6 — the version the sibling ledger-v9 ports in this org use — cannot run compactc 0.34.0 output.compact-runtimeis pinned to the released0.19.0rather than beta.7's0.19.0-rc.0: the two published tarballs differ in three lines (the version string, its source map, andpackage.json) and are otherwise identical, and the runtime guard strips the prerelease suffix. Theoverridesblock collapses them to one copy.@midnightntwrk/ledger-v9is pinned at1.0.0-rc.3because midnight-js-protocol beta.7 and everywallet-sdk@2.0.0-beta.2subpackage pin exactly that.1.0.0-rc.4exists but nothing in the stack references it, so adopting it would fork the ledger into two wasm copies.What the port actually cost
@midnight-ntwrk/ledger-v8→@midnightntwrk/ledger-v9is a pure specifier rename at all 11 import sites; no ledger call had to change. The real work was elsewhere:Contract.initialStateasync, and collapsedcurrentQueryContext/currentPrivateState/currentZswapLocalState/gasCostunder a singlecallContext;createCircuitContextalso gained the executing circuit's id as its first argument. The simulator now has a staticcreate()factory and async accessors, the unit testsawait, and negative cases becameawait expect(...).rejects.toThrow(...). 78/78 unit tests still pass — the same count asmain, so nothing was dropped in the conversion.createKeystoretakes{ kind, secret }instead of raw bytes (ledger-v9 turnedSigningKey/SignatureVerifyingKey/Signatureinto{ tag, value }records).kind: 'schnorr'is what reproduces the same addresses the v8 line derived — anything else compiles fine and silently derives a different, unfunded wallet.TransactionHistoryCommonSchemawas renamed toTransactionHistoryEntryCommonSchema, andSignSegmentis now async, so the keystore's ownsignDataAsyncreplaces the hand-rolled wrappers.FetchZkConfigProvider's second argument is an options bag;ZswapChainState.postBlockUpdategained a requiredretentionDuration(3600n, the value midnight-js 5 uses itself — noteLedgerState.postBlockUpdateis a different method that did not change this way).resolve.dedupefollows the new package names, and the build still emits exactly one ledger wasm and one onchain-runtime wasm.One thing worth a reviewer's eye:
ProvingProvider.lookupKeyledger-v9 widened
ProvingProviderfrom{ check, prove }to{ check, prove, lookupKey }, and midnight-js 5'screateProofProviderrequires the wider shape.@midnight-ntwrk/dapp-connector-apihas not followed — neither 4.0.1 nor 4.1.0-beta.1 declareslookupKey(both tarballs were read to confirm). So a v9 wallet that does implement it still returns a value the connector types describe without it.frontend/src/lib/providers.tshandles this inwithLookupKey(): the wallet'slookupKeyis used when present, and otherwise the dApp serves it from its ownzkConfigProvider— the identical public artifacts it already serves at/contract/compiled/shielded-night/and already handed the wallet throughasKeyMaterialProvider()one line earlier.checkandproveare untouched, so proving stays wallet-owned and the page still never names a proof server. Which lane was taken is logged rather than silent.Devnet stack
envs/docker-compose-dynamic.ymlmoves to the 2.x triple. Three things there are easy to get wrong and are commented in place:/node, not/data— the 2.x image setsBASE_PATH=/node/chain, so a/datavolume loses the chain db without an error;indexer-standalone:4.4.0-rc.1is published for linux/amd64 only, so it runs emulated on Apple Silicon (hence the longer healthcheck budget), and its 4.3.xAPP__INFRA__{STORAGE,PUB_SUB,LEDGER_STATE_STORAGE}__PASSWORDkeys are gone;node-dataandindexer-dataare wiped together: a v8→v9 chain cannot be upgraded in place, so a fresh genesis against a stale indexer db is worse than no db.CI
COMPACT_VERSION: '0.34.0'for all four jobs, andon.push.branchesnow includesledger-v9so the branch gets full CI (unit, frontend, byte-exact rebuild, integration on the 2.x triple) on every push, not only inside this PR.Measured: the plain proof-server 9.0.0-rc.5 proves this, and the atomic circuits settle on ledger 9
The first CI run on this branch was killed by
timeout-minutes: 60partway throughtransient.test.ts. It was not stuck — it had passed 15 of the 17 integration tests against node 2.0.0-rc.4 / indexer 4.4.0-rc.1 / proof-server 9.0.0-rc.5 and was still going:Every one of those proves and submits a real transaction, so compactc 0.34.0's zkir-v2 artifacts are proven by the plain proof-server image — the
_experimentalbuild is not needed.It also settles the risk this port was expected to hit. The passing security test "combined circuits: forged or double-spent coins cannot drain the reserve; nonce reuse cannot re-mint" is an on-chain test that calls
convertToShieldedand thenconvertToUnshieldedthree times and asserts the caller's NIGHT comes back — i.e. a shielded and an unshielded move netted inside one segment proves, applies and balances on ledger 9. No two-step-only fallback is needed.Two other things it proves that typechecking could not:
kind: 'schnorr'really is the keystore envelope that reproduces the funded v8 addresses (every test runs on a genesis-funded wallet derived through it), and theSigningKey→{ tag, value }change is threaded correctly —maintenance-authority.test.tssigned aMaintenanceUpdatethe chain accepted.Why the timeout moved to 150
The suite is 1.2–1.6× slower per file on the 2.x triple, and 60 was already thin on
main(its last green run spent 50.3 of its 60 minutes inside vitest):main(1.x)ledger-v9security.test.ts(5)shielded-night.test.ts(8)multiparty.test.ts(1)maintenance-authority.test.ts(1)The
1.00×row is the diagnostic one: that file is dominated by fixed waits rather than proving and did not move at all. Only the proving-bound files stretched, which is what says this is per-transaction cost on the 2.x stack rather than a hang. No test, assertion or gate was weakened, skipped or removed to fit the budget.Update 2026-09-03: merged
main@f7fcefaMerged
main@f7fcefa7921bf2c3f634871f9ad3aa3a32251af0into this branch (merge commit36caf59), bringing in PR #11 (frontend/.envPREPROD_ADDRESS=e354e672…) and PR #12 (scripts/verify-deployment.ts --allow-unlocked+scripts/verify-args.ts+ tests + docs, plusmain's own bun-1.4 lockfile regeneration).Conflicts were in
package.json/frontend/package.json(overrides) and bothbun.lockfiles. This branch's own v9 pins win (@midnightntwrk/ledger-v91.0.0-rc.3,@midnight-ntwrk/compact-runtime0.19.0,@midnightntwrk/onchain-runtime-v44.0.0-rc.3) —main's v8-line overrides (compact-js2.5.1,ledger-v88.1.0,onchain-runtime-v33.0.0) do not apply to this dependency graph and were not copied over; the//overridescomment now says so explicitly. Both lockfiles were not hand-merged: deleted and regenerated with bun 1.4.0, then verified to hold exactly one copy each of the v9 packages and zero v8/onchain-runtime-v3copies in both trees.Locally verified: byte-exact
bun run compactrebuild,bun install --frozen-lockfileclean on both trees, 95/95 unit tests (incl. the 17 newverify-argstests), root + frontendtypecheckclean, frontendbuildclean (1469 modules, one ledger wasm, one onchain-runtime wasm, PreProd + Local(undeployed) networks configured in the built bundle). CI dispatched on the push to36caf59.