Skip to content

fix(cluster-tool): verify the Solana .so at deploy - #80

Open
valthon wants to merge 1 commit into
masterfrom
fix/verify-deployed-solana-program-binary
Open

fix(cluster-tool): verify the Solana .so at deploy#80
valthon wants to merge 1 commit into
masterfrom
fix/verify-deployed-solana-program-binary

Conversation

@valthon

@valthon valthon commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

The harness never builds the Solana program and target/ is gitignored, so nothing tied the liqsol_core.so on disk to the sources beside it. This makes that link explicit and checks it at the point of deploy.

  • anchor-build-strict.mjs (wire-solana#476, merged) stamps target/deploy/wire-build-manifest.json beside each emitted .so — arch, byte length, sha256, and the build checkout's git describe
  • SolanaOutpostProgramTool.assertProgramSoFile checks the binary against that manifest and fails at startup naming the mismatch
  • SolanaValidatorProcessSteps.runStart calls it immediately before launching the validator
  • BuildRemediationHint moves from anchor build to npm run build:programs — only the wrapper writes the manifest
  • 8 new tests (14 across the two touched suites)

Cause

SolanaValidatorProcess launches with --upgradeable-program <id> <target/deploy/liqsol_core.so>, so whatever sits at that path is what executes on chain. The tool asserted only that the file existed. Nothing rebuilds the program, target/ is gitignored, and the //wire-solana:build genrule declares only a .stamp — so the .so is an untracked side effect a git checkout/rebase leaves behind while the sources move.

Observed 2026-08-21: a host carrying a day-old .so built on a different branch failed as

Program 5nBtmutQ… consumed 427 of 200000 compute units
Program 5nBtmutQ… failed: Access violation writing 1 bytes at address 0x32 (in unallocated region)

at the SolanaOutpost: init PDAs step. An instruction-entry abort reads as a program bug and invites blaming whichever wire-solana PR landed most recently — in that case #409, which CI had already passed 14/14 flows on. Making the mismatch a named startup error removes that whole misattribution path.

Why both halves of the check are load-bearing

The sha alone proves only that the .so and the manifest agree — and the two are written together into the same gitignored directory. A branch switch leaves the pair behind intact and mutually consistent, so a sha-only check passes on exactly the incident that motivated it. sourceDescribe closes that gap by comparing the build's checkout against the current one.

A dirty build is reported, not rejected: two different dirty trees at one commit describe identically, so the stamp can only mark the binary unverifiable. Refusing to run would block ordinary local iteration for a guarantee it cannot give either way.

Why the check lives at runStart, not in resolvePrograms

resolvePrograms is shared by two callers, and only one of them deploys:

Caller What it does Verify?
runStart launches the validator — loads the binary on this host yes
StartScriptSteps.resolveSolanaValidatorConfig renders a path into start.sh no

The renderer emits a script that runs later and often elsewhere. create-external-config clones a cluster into a deployable tree whose wire-solana was never built here, so checking against this checkout would reject a valid deployment payload while proving nothing about the machine that will run it. Both callers still produce the identical argv string.

Verification

  • pnpm build / pnpm run lint: clean
  • pnpm test: 225 suites, 1964 tests, 0 failures (rebased onto current master)
  • flow-swap-non-native-tokens end to end against a verified binary: SUCCEEDED, 195 steps, fatal=0

The flow ran before the call site moved from resolvePrograms up into runStart — the same assertion on the same deploy path, one frame earlier. Happy to re-run it if you'd rather have the evidence post-move.

Dependency — satisfied

The manifest this checks against is written by wire-solana#476, merged to next on 2026-08-21 (9e64dbf1). Since next is the manifest revision the e2e gate floats wire-solana to, the wire-build-manifest.json this PR reads is already present on the branch CI builds — no merge-order constraint remains.

🤖 Generated with Claude Code

The harness never builds the Solana program and `target/` is
gitignored, so nothing tied the binary on disk to the sources beside
it. A `git checkout` or rebase moved the sources while the `.so`
stayed put, and the stale binary deployed silently.

It did not fail as a build-provenance problem. The validator loaded
it, the program mis-executed at instruction entry, and the run showed
`consumed 427 of 200000 compute units` / `Access violation writing 1
bytes at address 0x32` during the outpost's init-PDAs step -- which
reads as a program bug, and was diagnosed as one.

`anchor-build-strict.mjs` now stamps a manifest beside each emitted
`.so` (arch, byte length, sha256, and the build checkout's `git
describe`). `assertProgramSoFile` checks the binary against it and
fails at startup naming the mismatch.

Both halves of that check are load-bearing. The sha alone proves only
that the `.so` and the manifest agree, and the two are written
together into the same gitignored directory -- a branch switch leaves
the pair intact and mutually consistent, so a sha-only check passes on
exactly the incident that motivated it. `sourceDescribe` closes that
gap by comparing the build's checkout against the current one.

A DIRTY build is reported, not rejected: two different dirty trees at
one commit describe identically, so the stamp can only mark the binary
unverifiable. Refusing to run would block ordinary local iteration for
a guarantee it cannot give either way.

Verification lives at `runStart` -- the one path that actually loads
the binary on this host -- and NOT in the shared `resolvePrograms`,
which also feeds the `start.sh` renderer. That renderer emits a script
which runs later and often elsewhere: `create-external-config` clones
a tree whose wire-solana was never built here, so checking against
this checkout would reject a valid deployment payload while proving
nothing about the machine that will run it.

The remediation hint moves from `anchor build` to
`npm run build:programs`, since only the wrapper writes the manifest.
@valthon
valthon force-pushed the fix/verify-deployed-solana-program-binary branch from b2e1396 to 5e0e8be Compare August 21, 2026 23:38
@valthon valthon changed the title fix(harness): verify the Solana program binary before deploying it fix(cluster-tool): verify the Solana .so at deploy Aug 22, 2026
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