Skip to content

feat(sdk): fail strict mainnet readiness on deployment drift - #463

Merged
karagozemin merged 2 commits into
Sub-Rosa-Issue:mainfrom
Hamda-gbade:feat/strict-mainnet-readiness
Oct 1, 2026
Merged

karagozemin merged 2 commits into
Sub-Rosa-Issue:mainfrom
Hamda-gbade:feat/strict-mainnet-readiness

Conversation

@Hamda-gbade

Copy link
Copy Markdown

Summary

Closes #383.

mainnet:ready --strict and mainnet:verify used expectations supplied by the caller, so a green run did not mean the deployment matched what this repo believes is mainnet. A different contract, a testnet passphrase, a rebuilt contract, or a swapped escrow SAC all passed. The expected values are now committed data, and readiness compares the live network against them.

Committed manifest

packages/sdk/mainnet-artifacts.json is the source of truth: contract id, network passphrase, rpc url, wasm hash, the mainnet XLM SAC used as escrow token, plus the settled-round 1 expectations. parseMainnetManifest enforces a closed schema, so a typo or an unexpected key is an error rather than a silently ignored field, and a test pins the JSON to the existing MAINNET_* constants.

The four compared fields

manifest field live value
contractId the contract the read-only client is bound to
networkPassphrase the passphrase the RPC reports via getNetwork
wasmHash the executable hash in the contract ledger entry
tokenContract usdc in the deployed GlobalConfig (the escrow SAC)

Each is a blocking check with its own id (contract-id, network-passphrase, wasm-hash, token-contract). Mismatch blocks. A field that cannot be read also blocks: an unverifiable field must never read as a pass, which is the gap that let this drift hide. Config drift is also caught before any RPC call, so pointing NETWORK_PASSPHRASE/ROUND_CONTRACT_ID at testnet fails instead of reporting a healthy check against the wrong network.

Output stays shareable

mainnet-ready and mainnet-verify exit non-zero and name the disagreeing field. The passphrase is printed as a short sha256 fingerprint and anything shaped like an S... secret key is redacted, so a failing readiness report can be pasted into an issue. Neither command needs a secret key; --with-balances now takes OPERATOR_PUBLIC_KEY / KEEPER_PUBLIC_KEY / BIDDER_PUBLIC_KEY.

CI-safe fixture mode

--fixture replays a recorded deployment through the same comparison with no mainnet RPC:

pnpm mainnet:ready --strict --fixture     # bare flag replays the committed recording
pnpm --filter @sub-rosa/sdk exec tsx scripts/mainnet-verify.ts --fixture

The recording is committed at packages/sdk/fixtures/mainnet-readiness.json and a new step in the TypeScript coverage workflow runs both scripts. Tests assert every mismatch field fails, a matching recording passes, and the source of both scripts contains no _SECRET reference.

One footgun found while testing this: --fixture with no value used to fall through to the live path, so a CI run would have quietly hit mainnet. A bare --fixture now replays the committed recording, and both flags ignore a following value that is another flag.

Tests

  • pnpm --filter @sub-rosa/sdk test — 250 pass (49 in mainnet-readiness.test.ts)
  • pnpm --filter @sub-rosa/sdk typecheck, plus typecheck for keeper, receipt-cli, agent, web
  • keeper 73, receipt-cli 19, agent 38, tlock 67, time 17, drand-tools 5, round-bindings 17
  • logging:check, errors:normalize:check, errors:check, snapshot:check, threat-model:check, time:guard, docs:check, docs:check-links
  • CLI: matching fixture exit 0; wrong passphrase, wrong wasm, wrong token each exit 1 naming the field; mismatched verify fixture exit 1

Notes for review

  • The manifest is the one place to change on redeploy: bump wasmHash/contractId in mainnet-artifacts.json and the parity test forces the constant in mainnet-artifacts.ts to move with it.
  • src/mainnet-manifest.ts is Node-only and deliberately not re-exported from the barrel, so browser consumers do not pull node:fs into the web bundle.
  • MainnetReadinessInput no longer takes expectedWasmHash; the manifest owns it. mainnet-settle and mainnet-micro were updated to the new shape.
  • No contract, binding, or Rust changes.

Readiness checked operator-supplied expectations, so `mainnet:ready --strict`
and `mainnet:verify` could pass against a drifted deployment: wrong contract,
testnet passphrase, old WASM, or a swapped escrow SAC all still went green.

- Commit the mainnet deployment as data: packages/sdk/mainnet-artifacts.json
  is the source of truth, parsed by parseMainnetManifest with a closed schema.
- Compare the live deployment against the manifest through the read-only
  client: contractId, networkPassphrase, wasmHash, tokenContract. Mismatch or
  unreadable blocks; the report names the field, fingerprints the passphrase,
  and redacts anything shaped like a secret key.
- Replay a recorded snapshot with no RPC (`--fixture`), committed under
  packages/sdk/fixtures and exercised in CI, so each mismatch field is proven
  to fail without touching mainnet.
- mainnet-ready/mainnet-verify exit non-zero on drift and need no secret keys;
  balance checks take public keys.

Refs Sub-Rosa-Issue#383
@drips-wave

drips-wave Bot commented Sep 30, 2026

Copy link
Copy Markdown

@Hamda-gbade Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@karagozemin
karagozemin merged commit 7c56b89 into Sub-Rosa-Issue:main Oct 1, 2026
1 of 4 checks passed
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.

Fail strict mainnet readiness when deployed artifacts disagree

3 participants