Skip to content

verify-deployment --allow-unlocked, and both lockfiles regenerated on bun 1.4.0 - #12

Merged
acedward merged 2 commits into
mainfrom
00007-lockfile-and-allow-unlocked
Sep 3, 2026
Merged

verify-deployment --allow-unlocked, and both lockfiles regenerated on bun 1.4.0#12
acedward merged 2 commits into
mainfrom
00007-lockfile-and-allow-unlocked

Conversation

@acedward

@acedward acedward commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

No behaviour change, and nothing to migrate in the dApp. bun run verify:deployment with no arguments does exactly what it did before; the contract, src/managed/ and every served artifact are untouched (a full compact compile +0.31.1 still reproduces src/managed byte for byte); and every version this repo pins is on the same version it was on before. What lands is an opt-in CLI flag with 17 unit tests and docs, plus three defensive overrides and a regeneration of both lockfiles.

⚠️ One consumer-visible requirement: the minimum bun for this repository is now 1.4.x. bun 1.4 writes "lockfileVersion": 2 and has no flag to write the old format, and bun 1.3.x cannot read it (error: Unknown lockfile version). This workflow's oven-sh/setup-bun@v2 is unpinned and already installs 1.4.0, so CI is unaffected — but anything that installs this tree with an older bun must move to a 1.4.x base at the same time it pins a commit from this branch. Concretely: both demo stacks' images/shielded-night/Dockerfile pin oven/bun:1.3.11 by digest and run bun install --frozen-lockfile in both trees; their SHIELDED_NIGHT_REF re-pin commit has to bump BUN_BASE too. Measured below.

This PR carries both halves of the follow-up: the --allow-unlocked flag, and the lockfile regeneration that was previously held pending a decision on whether package.json could be touched (it could — see §2).


1. --allow-unlocked for scripts/verify-deployment.ts

verify-deployment.ts asserts two things and exits 0 only if both hold:

  1. CODE — every on-chain verifier key is byte-identical to src/managed/keys/*.verifier, and the circuit sets match exactly.
  2. LOCK — the maintenance authority is dissolved, so no rule can ever change.

That is right for a hosted release. It is wrong for a devnet: a demo stack deliberately leaves the contract unlocked (SHIELDED_NIGHT_LOCK=false), because locking is a one-way door and a throwaway contract gains nothing from it.

The consequence, measured against the live preprod contract e354e6725893397e6a2dfa44522a017fabb5d9c92efed50288711f5f865c8950: the script prints eleven lines and then exits 1. So the strongest check a demo stack can run is unreadable from its exit code — which is exactly how both demo stacks consume it, as the exit status of a compose one-shot rather than as parsed stdout.

--allow-unlocked keeps measuring and printing the lock state, but lets only the code check decide the exit code.

# strict (hosted release) — unchanged
MN_ENV=preprod CV_ADDRESS=<addr> bun run verify:deployment

# devnet gate — lock state reported, exit code = the verifier-key check only
MN_ENV=undeployed CV_ADDRESS=<addr> bun run verify:deployment -- --allow-unlocked
keys match key mismatch / missing / extra circuit
locked, no flag exit 0 exit 1
unlocked, no flag exit 1 exit 1
locked, --allow-unlocked exit 0 exit 1
unlocked, --allow-unlocked exit 0 exit 1

The flag only ever changes what an unlocked contract does to the exit code. It never weakens the code check. Unknown arguments are rejected rather than ignored, so a typo (--allow-unlock) fails loudly instead of silently reverting to strict behaviour and reading as "the flag does not work".

Shape

The policy is extracted into a new dependency-free scripts/verify-args.ts (parseVerifyArgs, verifyOutcome, lockVerdictLine) — the same seam pattern as scripts/deploy-record.ts. verify-deployment.ts keeps all the chain work and just calls it. That makes the exit-code table unit-testable with no chain, no docker and no new dependency.

How it was verified

bun run test:unit95 passed / 95 (7 files; was 78/78 in 6 — 17 new). bun run typecheck exit 0.

Measured against the live preprod contract above (unlocked: committee=1 threshold=1 counter=0; keys match), read-only, no wallet or seed:

# invocation result exit
1 bun run verify:deployment 11/11 ✓ on-chain == local, ✗ NOT locked, ❌ verification FAILED 1 — today's behaviour, unchanged
2 … -- --allow-unlocked 11/11 , ℹ NOT locked: … Reported only, not failed, ✅ verified: … Lock state REPORTED ONLY 0
3 negative — temp copy with the last byte of src/managed/keys/getBalance.verifier flipped, -- --allow-unlocked 10 / 1 ✗ getBalance: on-chain 7981e854…a08416bf != local 66a1c38f…a7e6287d 1
4 negativeCV_ADDRESS=0000…0000, -- --allow-unlocked no contract state on chain 1
5 negative--allow-unlock (typo) Unknown argument … 1

Row 3 is the one that matters: the flag does not weaken the key check.

Docs

  • README.md → new subsection under "Verifying the deployment": what it relaxes, what it never relaxes, sample output, why not to use it for a hosted release.
  • TESTING.md → "Verifying a deployment as a gate", with the exit-code table and a pointer to the unit test.

2. Lockfile regeneration on bun 1.4.0, with three pinning overrides

CI installs with an unpinned oven-sh/setup-bun@v2, so it now runs bun 1.4.0, while both lockfiles here were written by an older bun. The plan was to delete them and regenerate wholesale on 1.4.x with package.json untouched. Measured with oven/bun:1.4 = bun 1.4.0+34cbb9a40 (the newest release, i.e. exactly what CI installs) against a git archive of main @ 6d87db4, that is not possible — two independent defects surface, both in the family this repo pins on purpose.

Defect 1 — the root tree does not install at all

error: No version matching "^0.1.0-alpha.1" found for specifier "@midnight-ntwrk/ledger-v9" (but package exists)
error: @midnight-ntwrk/ledger-v9@^0.1.0-alpha.1 failed to resolve

The root declares "@midnight-ntwrk/compact-js": "^2.5.1" — the only loose range in the midnight family. compact-js has since published 2.5.3 (now dist-tags.latest), and 2.5.3 replaced its ledger-v8: ^8.0.3 dependency with ledger-v9: ^0.1.0-alpha.1, a range that is no longer published (the v9 line moved to 1.0.0-rc.x). The committed lockfile hid this by pinning 2.5.1; deleting it exposed it.

This is a pre-existing landmine on main, not something this branch introduces: a plain bun install at the repo root fails today, so only --frozen-lockfile is safe. And even if 2.5.3 did resolve, taking it would put two copies of compact-js in the tree (2.5.3 direct, plus the 2.5.1 that midnight-js-protocol@4.1.1 pins exactly) — the exact class-identity hazard the //overrides note was written about.

Defect 2 — both trees fork the onchain-runtime WASM

With package.json untouched, frontend/ does regenerate (271 packages) with every pinned version intact — but @midnight-ntwrk/onchain-runtime-v3 goes from one copy to two, in both trees: 3.1.0 hoisted plus 3.0.0 nested under midnight-js-protocol@4.1.1, which pins it exactly while compact-runtime@0.16.0 asks for ^3.0.0 (now satisfied by the newly published 3.1.0).

onchain-runtime-v3 is named in frontend/vite.config.ts's resolve.dedupe for precisely this reason — its comment reads that two WASM instances give classes that "fail each other's instanceof checks ("expected instance of ChargedState" when reading ledger state)". On the frontend dedupe would paper over it, but only by silently substituting 3.1.0 where midnight-js-protocol pinned 3.0.0. On the root/Node side — the integration suite, deploy.ts, verify-deployment.ts — there is no dedupe, so both copies genuinely load.

The fix: three overrides

// package.json
"overrides": {
  "@midnight-ntwrk/compact-js": "2.5.1",          // NEW
  "@midnight-ntwrk/ledger-v8": "8.1.0",
  "@midnight-ntwrk/onchain-runtime-v3": "3.0.0"   // NEW
}
// frontend/package.json
"overrides": {
  "@midnight-ntwrk/compact-js": "2.5.1",
  "@midnight-ntwrk/compact-runtime": "0.16.0",
  "@midnight-ntwrk/ledger-v8": "8.1.0",
  "@midnight-ntwrk/onchain-runtime-v3": "3.0.0"   // NEW
}

Neither override forces a version below anybody's floor. In both regenerated lockfiles every requirement for compact-js is either the exact 2.5.1 that midnight-js-protocol@4.1.1 pins or the root's own ^2.5.1, and every requirement for onchain-runtime-v3 is either the exact 3.0.0 that midnight-js-protocol@4.1.1 pins or compact-runtime@0.16.0's ^3.0.0 — the pins satisfy all of them, and simply pick one of several admissible resolutions.

They are the same defensive pin the file already applies to ledger-v8, and the top-level //overrides note is rewritten to say, per entry, which class identity it protects and why the pin is needed — so the next person does not have to re-derive it from an npm graph.

Nothing that must hold, moved

package committed after regeneration
@midnight-ntwrk/ledger-v8 8.1.0 8.1.0 both trees
@midnight-ntwrk/compact-runtime 0.16.0 0.16.0 both trees
@midnight-ntwrk/compact-js 2.5.1 2.5.1 both trees; now held by an override instead of by luck
@midnight-ntwrk/midnight-js + all nine midnight-js-* 4.1.1 4.1.1 both trees
@midnight-ntwrk/dapp-connector-api 4.0.1 4.0.1 frontend (the root does not depend on it)
@midnight-ntwrk/onchain-runtime-v3 3.0.0 ×1 3.0.0 ×1 the point of the new override
@midnight-ntwrk/platform-js / zkir-v2 2.2.4 / 2.1.0 unchanged root
@midnightntwrk/wallet-sdk + 11 subpackages unchanged root

Every other @midnight-ntwrk/* / @midnightntwrk/* change, in full

package old new why
@midnightntwrk/wallet-sdk-utilities (root) 1.2.0 1.2.1 patch bump inside the declared ^1.2.0; the only midnight-family version change in either tree
@effect/platform (root, nested under it) 0.96.2 0.96.3 follows the line above
@midnight-ntwrk/wallet-sdk-address-format (frontend) 3.1.0 + 3.1.2 (two copies) 3.1.0 (one copy) de-duplication, not a version move: the 3.1.2 nested under midnight-js-utils@4.1.1 (range ^3.1.0) now collapses onto the pinned top-level 3.1.0. One fewer ledger-v8-bearing copy in the SPA — it is why the build transforms 1403 modules instead of 1448

No @effectstream/* package exists in either tree.

Ordinary tooling drift

Root: 76 names changed, 1 added, 8 removed (376 → 369 distinct names). Frontend: 54 changed, 1 added, 1 removed (315 → 315).

package old new
vitest / @vitest/* 4.1.10 4.1.11
vite 8.1.3 8.2.2
rolldown (+ bindings) 1.1.4 1.2.7
rollup (+ bindings, frontend) 4.62.2 4.63.1
effect 3.21.4 3.22.1
@apollo/client 4.2.5 / 4.2.6 4.2.12
@noble/hashes, @noble/ciphers, @noble/curves 2.2.0 2.4.0
@scure/base, @scure/bip32, @scure/bip39 2.2.0 2.4.0
graphql-ws / graphql-http 6.0.8 / 1.22.4 6.2.1 / 1.23.0
ws 8.21.0 8.21.3
undici 7.28.0 7.29.0
testcontainers 11.12.x 11.14.0
lightningcss (+ bindings) 1.32.0 1.33.0
postcss 8.5.16 8.5.27
@babel/* (frontend) 7.29.7 7.29.8
caniuse-lite / electron-to-chromium / browserslist 1.0.30001802 / 1.5.387 / 4.28.5 1.0.30001810 / 1.5.420 / 4.28.8

The root removals are all wasm-fallback binding packages that rolldown 1.2.7 no longer pulls (@rolldown/binding-wasm32-wasi, @emnapi/*, @napi-rs/wasm-runtime, @tybys/wasm-util, bare-os, is-what), replaced by @rolldown/binding-android-arm-eabi. @types/node at the root goes from one copy to three (24.13.3 plus 18.19.130 and 26.4.1 nested under @types/ssh2 / undici-types); bun run typecheck is green with it.

How it was verified

Every run used bun 1.4.0+34cbb9a40 in a short-lived oven/bun:1.4 container over a git archive export of this branch.

# check result
1 bun install --frozen-lockfile, root exit 0, 368 packages; bun.lock byte-identical afterwards
2 bun install --frozen-lockfile, frontend/ exit 0, 270 packages; frontend/bun.lock byte-identical afterwards
3 physical node_modules audit after both installs exactly ONE directory per tree for onchain-runtime-v3 (3.0.0), ledger-v8 (8.1.0), compact-js (2.5.1)
4 full bun run compact (compactc 0.31.1) then git diff --exit-code -- src/managed exit 0 — the byte-exact rebuild still holds
5 bun run typecheck (root) exit 0
6 bun run test:unit 95 passed / 95 (7 files)
7 frontend: bun run typecheck exit 0
8 frontend: bun run build exit 0, 1403 modules; dist/assets carries exactly two wasm files (one ledger, one onchain-runtime), the SHIELDED_NIGHT runtime-config marker survives minification, dist/config.js still emitted

The bun floor, measured

lockfiles oven/bun:1.4 (1.4.0+34cbb9a40) oven/bun:1.3.11 (1.3.11+af24e281e)
main @ 6d87db4 (lockfileVersion: 1) exit 0 exit 0, 365 packages
this branch (lockfileVersion: 2), root exit 0, 368 packages exit 1Unknown lockfile versionlockfile had changes, but lockfile is frozen
this branch (lockfileVersion: 2), frontend/ exit 0, 270 packages exit 1 — same

bun install --help on 1.4.0 offers --save-text-lockfile and --lockfile-only and nothing that selects a lockfile version, so the bump is unavoidable once the lockfiles are regenerated on 1.4. Hence the warning at the top of this PR.


Follow-ups this unblocks

  • Both demo stacks' verify sections can drop their stdout parse and read the exit code instead (midnight-1-offers scripts/verify-shielded-night.sh via docker compose run --rm shielded-night-verify, and its midnight-2-offers counterpart).
  • A plain bun install at the root works again for the first time since compact-js 2.5.3 was published, so adding a dependency no longer requires reverse-engineering an npm graph.
  • Both of those re-pins happen in the demo repos, not here — and each must bump BUN_BASE from oven/bun:1.3.11 to a 1.4.x digest in the same commit.

…nt to be unlocked

verify-deployment.ts asserts two things and exits 0 only if BOTH hold: every
on-chain verifier key is byte-identical to the committed build, and the
maintenance authority is dissolved so the contract can never change. That is
right for a hosted release. It is wrong for a devnet: a demo stack deliberately
leaves the contract unlocked (SHIELDED_NIGHT_LOCK=false), because locking is a
one-way door and a throwaway contract gains nothing from it.

The consequence, measured against the live preprod contract e354e672… (11/11
keys matching, committee=1 threshold=1): the script prints eleven ✓ lines and
then exits 1. The strongest check a demo stack can run is therefore unreadable
from its exit code, which is exactly how both demo stacks consume it — as the
exit status of a compose one-shot, not as parsed stdout.

--allow-unlocked keeps measuring and printing the lock state, but lets only the
code check decide the exit code. It never weakens that check: a key mismatch, a
missing circuit or an extra circuit still exits 1 with the flag set. Unknown
arguments are rejected rather than ignored, so a typo fails loudly instead of
silently reverting to the strict behaviour.

The policy lives in a new dependency-free scripts/verify-args.ts (the same seam
pattern as scripts/deploy-record.ts) so the exit-code table is unit-testable
without a chain: 17 new tests in the existing bun run test:unit tier, no new
dependency, both lockfiles untouched.

Default behaviour is unchanged.
…un 1.4.0

CI installs with an unpinned setup-bun@v2, so it now runs bun 1.4.0, and both
lockfiles here were written by an older bun. Regenerating them wholesale on
1.4.0 turned out to be impossible with package.json untouched, for two separate
reasons that were measured against oven/bun:1.4 (bun 1.4.0+34cbb9a40):

  1. The root tree did not install at all. `bun install` exited non-zero with
     "No version matching ^0.1.0-alpha.1 found for @midnight-ntwrk/ledger-v9".
     The root declares compact-js ^2.5.1 — the only loose range in the midnight
     family — and compact-js has since published 2.5.3, which replaced its
     ledger-v8 dependency with ledger-v9 ^0.1.0-alpha.1, a range the registry no
     longer serves. The committed lockfile hid this by pinning 2.5.1; deleting
     it exposed it. Anyone adding a dependency at the root would have hit the
     same wall.

  2. Both trees forked the onchain-runtime WASM. compact-runtime 0.16.0 asks for
     ^3.0.0, now satisfied by the newly published 3.1.0, while
     midnight-js-protocol 4.1.1 pins 3.0.0 exactly, so an unpinned resolve
     installs both. Two copies of a WASM package give two class identities and
     break each other's instanceof checks ("expected instance of ChargedState"
     when reading ledger state) — which is precisely why onchain-runtime-v3 is
     already named in frontend/vite.config.ts resolve.dedupe. On the Node side
     there is no dedupe at all, so both copies would genuinely be loaded by the
     integration suite, deploy.ts and verify-deployment.ts.

Three overrides fix both: compact-js 2.5.1 and onchain-runtime-v3 3.0.0 at the
root, onchain-runtime-v3 3.0.0 in frontend/. They are the same defensive pin the
file already applies to ledger-v8, and the //overrides note is expanded to say
what each entry is for. With them, both trees regenerate cleanly on 1.4.0: root
368 packages, frontend 270, exactly one onchain-runtime-v3@3.0.0 in each, and
the built SPA still emits exactly one ledger wasm and one onchain-runtime wasm.

The five versions that must not move did not move: ledger-v8 8.1.0,
compact-runtime 0.16.0, compact-js 2.5.1, midnight-js 4.1.1 and
dapp-connector-api 4.0.1. The only other midnight-family drift is
@midnightntwrk/wallet-sdk-utilities 1.2.0 -> 1.2.1 (inside the declared ^1.2.0)
with its nested @effect/platform 0.96.2 -> 0.96.3 at the root, and a
de-duplication in frontend/: wallet-sdk-address-format went from two copies
(3.1.0 plus a 3.1.2 nested under midnight-js-utils) to the single pinned 3.1.0.
Everything else is ordinary tooling: vitest 4.1.10 -> 4.1.11, vite 8.1.3 ->
8.2.2, rolldown 1.1.4 -> 1.2.7, rollup 4.62.2 -> 4.63.1, effect 3.21.4 ->
3.22.1, @noble/* 2.2.0 -> 2.4.0, @apollo/client -> 4.2.12, ws 8.21.0 -> 8.21.3.

Contract sources and src/managed are untouched: a full `bun run compact` with
compactc 0.31.1 reproduces src/managed byte for byte after the change.

NOTE FOR CONSUMERS: bun 1.4 writes lockfileVersion 2, which bun 1.3.x cannot
parse ("Unknown lockfile version"), and there is no flag to write the old
format. So the minimum bun for this repository is now 1.4.x. CI is already
there; anything that installs this tree with an older bun — the demo stacks'
images pin oven/bun:1.3.11 by digest — has to move to a 1.4.x base at the same
time it pins a commit from this branch.
@acedward acedward changed the title verify-deployment: --allow-unlocked for contracts that are meant to be unlocked verify-deployment --allow-unlocked, and both lockfiles regenerated on bun 1.4.0 Sep 3, 2026
@acedward
acedward merged commit f7fcefa into main Sep 3, 2026
4 checks passed
acedward added a commit that referenced this pull request Sep 3, 2026
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.
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