Skip to content

test: pin what clarinet deployments apply --mainnet would publish - #76

Merged
mattglory merged 1 commit into
mainfrom
security-lead/mainnet-plan-guard
Sep 28, 2026
Merged

mattglory merged 1 commit into
mainfrom
security-lead/mainnet-plan-guard

Conversation

@unixwhisperer

Copy link
Copy Markdown
Collaborator

Summary

Adds tests/mainnet-plan-guard.test.ts, the regression guard proposed in the #75 review.

On any machine with a settings/Mainnet.toml, clarinet deployments apply --mainnet publishes the plan clarinet computes from Clarinet.toml: after two Enters by default, or with no prompt under -d. Today that plan has 57 publishes, 26 from contracts/test/. These are the localized copies of every funds-bearing contract, whose sBTC calls resolve to the in-plan mock contracts/sbtc-token.clar, plus fixtures such as malicious-token, mock-usdcx and test-receiver-bad. The full evidence (isolated container, throwaway key, mock node) is in the #75 review.

Design: why this doesn't simply go red

The real invariant is no contracts/test/ path in the mainnet plan. That can't hold until D6's structural fix (CONTRACT_INVENTORY §7.3), and a red test on main would block every PR. So:

  • The target is recorded with it.fails. It shows up as "expected fail" and flips the moment D6 lands. At that point, turn it into it.
  • The known 26 are pinned exactly. Registering one more contracts/test/ file in Clarinet.toml, which puts a test build into what a mainnet deploy run publishes, fails CI. Removing one also fails, so the list is only ever changed deliberately.
  • A vacuity guard runs first: it asserts that the parsed plan contains a known canonical source.

If you'd rather have a plain failing test and fix D6 in the same change, that's a one-line swap. Your call.

Safety

deployments generate only writes a plan file, and it writes it inside a temp copy of Clarinet.toml, contracts/, .cache and settings/Devnet.toml. Nothing is written to the working tree, and no deployments/default.mainnet-plan.yaml reappears.

  • Key: the dummy Mainnet.toml uses the public Clarinet devnet deployer mnemonic, read at runtime from settings/Devnet.toml. No new secret material is introduced.
  • No broadcast possible: it points at http://127.0.0.1:1, and --manual-cost skips fee estimation.
  • Offline: requirements resolve from the vendored .cache, so no network is needed (verified under --network none).

Clarinet version

The test reads the version from .github/workflows/test.yml (currently 3.23.2), so the two can't drift. In CI (CI set) a missing or mismatched clarinet throws. Locally it skips, since several of us have older binaries (mine is 3.13). CLARINET_BIN points it at a matching binary. No workflow change is needed, because CI already puts clarinet on PATH before npm test.

Verification (clarinet 3.23.2, CLARINET_BIN)

Case Result
unchanged tree 2 passed, 1 expected fail
+ register contracts/test/zz-mutation.clar pin fails, diff shows + zz-mutation.clar
− unregister malicious-token pin fails, diff shows - malicious-token.clar
plan parser broken all 3 fail, including the vacuity guard
flashstack-stx-core pointed at its canonical source generation errors (epoch 3.0 vs trait at 3.4), so beforeAll throws and the file fails closed
local clarinet 3.13, no CLARINET_BIN 3 skipped
full suite 259/259 across 25 files

Noted, not touched

README/ROADMAP/AUDIT_SCOPE say 254 tests; main is already at 256/24, and this makes it 259/25. That drift predates this PR, so I've left the docs alone. Happy to reconcile the counts once the open PRs settle.

🤖 Generated with Claude Code

On any machine with settings/Mainnet.toml, `apply --mainnet` publishes the
plan clarinet computes from Clarinet.toml (two Enters by default, no prompt
under -d). Today that plan has 57 publishes, 26 of them from contracts/test/:
the localized copies of every funds-bearing contract, whose sBTC calls
resolve to the in-plan mock sbtc-token, plus fixtures like malicious-token.
Verified 2026-09-27 against clarinet 3.23.2 in a network-isolated container
(see the #75 review).

The target, no contracts/test/ path in the mainnet plan, needs D6's
structural fix, so it is recorded with it.fails. Until then the known set
of 26 is pinned, so registering one more contracts/test/ file fails CI.

Generates the plan in a temp copy with the public devnet deployer mnemonic,
an unreachable RPC address and --manual-cost: nothing is signed or sent,
and no network is needed. Requires CI's clarinet version in CI; skips
locally when the binary is missing or mismatched (CLARINET_BIN overrides).

Red/green: registering an extra contracts/test/ file, or unregistering
malicious-token, fails the pin; breaking the plan parser fails all three.
@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
web Ready Ready Preview Sep 28, 2026 8:02am UTC

Request Review

@mattglory mattglory left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Verified independently, not just re-run:

  • Ran the test itself: 2 passed, 1 expected fail — matches your table exactly.
  • Then reproduced it completely from scratch, outside the harness: copied Clarinet.toml/contracts/.cache to a temp dir, generated a Mainnet.toml from the same public devnet mnemonic, ran clarinet deployments generate --mainnet --manual-cost directly. 57 total publishes, 26 from contracts/test/ — exact match. The sbtc-token dot-reference in contracts/test/flashstack-sbtc-pool-v3.clar is at lines 91 and 102, exactly where you cited it.
  • Re-checked all 5 audit-track successor names live on-chain right now: all still 404 at SPR9PQAN…, so the squatting risk is current, not stale.
  • it.fails framing is right — a hard-red test here would block every unrelated PR, and the pinned list turning strict (fails on add OR remove) is exactly the guard this needs.

Good work isolating this properly (network-none container, throwaway key, mock node) rather than trusting source-reading a second time. Merging.

@mattglory
mattglory merged commit 64ec031 into main Sep 28, 2026
7 checks passed
@mattglory
mattglory deleted the security-lead/mainnet-plan-guard branch September 28, 2026 17:54
mattglory added a commit that referenced this pull request Sep 28, 2026
…orrection)

Requested changes from Hillary's #75 review. Two prior passes at this
section relied on reading clarinet's source -- mine and hers, independently
-- and both were wrong. She then actually ran clarinet 3.23.2 (the CI
binary) in a network-isolated container against the real repo, before and
after #74, with a mock node logging every broadcast attempt. I re-ran her
test (#76, merged) and independently reproduced the core claim from
scratch outside the harness before writing any of this down: 57 publishes,
26 from contracts/test/, matching exactly; the sbtc-token references she
cited at lines 91/102 of the test sbtc-pool-v3 copy matched exactly; all
five undeployed audit-track successor names re-confirmed 404 today.

What actually changes:
- D5: a clean checkout never broadcasts, before or after #74 (recompute
  fails, falls back, prompts, exits with zero requests either way). The
  gen-1 plan could only ever be signed by SP3TGRVG..., whose 13 names
  already exist on mainnet, so a real broadcast would be refused as
  duplicates regardless. #74 is hygiene -- it removed a route only that
  key could use, one the chain would have refused anyway -- not the risk
  reduction either earlier version of this row claimed.
- D6: the real, still-live exposure, unaffected by #74. Any machine with
  settings/Mainnet.toml, any key, reaches the SAME plan clarinet computes
  fresh from Clarinet.toml (byte-identical before/after #74) -- 57
  publishes, 26 from contracts/test/, sBTC resolving to a flash-mintable
  mock, 54 of 57 names free including all five successors. Now pinned by
  tests/mainnet-plan-guard.test.ts (#76).
- Archive file header: same correction, so the historical record doesn't
  repeat either wrong prior explanation.
- Dates: 2026-09-23 -> 2026-09-26 in all three places, per review.

Verified: suite 258 passed / 1 expected fail (259) across 25 files
(unchanged from post-#76 main), clarinet check 211/0 (unchanged), archive
YAML still parses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 5a020e0d Deployed Sep 28, 2026 by vercel[bot]
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.

2 participants