Cross-mint smoke: a script that runs, and states what it cannot prove - #204
Open
orveth wants to merge 2 commits into
Open
Cross-mint smoke: a script that runs, and states what it cannot prove#204orveth wants to merge 2 commits into
orveth wants to merge 2 commits into
Conversation
The smoke script's command surface had never been executed. Against the
real binary every invocation returns rc=1: `budget`, `config` and `job`
are not commands at all; `--home` is a post-subcommand flag on `wallet`
alone; `wallet balances` is `balance` and has no `--json`; `buyer restart`
does not exist; and the binary is not at ./target (CARGO_TARGET_DIR is
redirected). A script in that state does not fail at step 0 — it fails
partway, and on the real-money path that means after the melt.
The script now targets the surface the binary has, and is staged so the
external unknowns are measured before our own code is involved:
--self-test parsers against fixtures. The `status=needs_payment` branch
only ever appears on a non-test mint, so fixtures are the
only coverage available before real sats.
--dry-run testnut: gates, home discipline, env config, balance math,
and that a melt which cannot route returns non-zero — the
property every `|| die` guarding a real melt depends on.
--stage-a the hop by hand in three wallet commands. Answers what no
hermetic test can: does LN route source -> target, and what
does it cost. Ends in a fee-fit verdict against the cap.
--stage-b refuses, naming what it still needs, rather than pretending.
Safety is structural rather than procedural. Config is supplied entirely
by environment, and env overrides are never persisted: a home booted with
MOBEE_ALLOW_REAL_MINTS=true still reads `allow_real_mints = false` on
disk, so the real-mint fence is process-scoped and cannot be left open.
Bootstrap does write into MOBEE_HOME, and an unset MOBEE_HOME resolves to
~/.mobee, so every invocation is funnelled through one wrapper that
refuses any directory this run did not itself create and mark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage A created a fresh empty home and then asserted a source balance, so the first real run would have died at "buyer holds 0 sats" — the funding step was missing entirely. Paying the invoice happens out of band and by a human, so funding is two invocations (`--fund` prints the bolt11, `--fund-complete` issues once it is paid) sharing one throwaway wallet. That means a home has to be reusable across invocations: `ensure_home` reuses one carrying our marker and still refuses any directory this tooling did not create. The authorization token now names the TOTAL FUNDED EXPOSURE rather than each step's amount — one human decision about how many real sats are at risk, which is the shape the spend was approved in. A mint that auto-funds prints `minted_sats=...` where a real mint prints a bolt11. Both are non-empty, so an emptiness check would feed that line straight into `wallet melt`; `is_bolt11` checks the shape instead, and the self-test pins both directions. Verified: self-test 11/11, dry-run rc=0, and --fund / --fund-complete / --stage-a / --stage-b each rc=1 without matching auth, creating no home before the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
What
The cross-mint smoke script, rewritten against the CLI the binary actually has, and staged.
Why
The script shipped in #196 had never been run. Against the real binary, every invocation in it returns
rc=1:mobee budget status --jsonbudgetcommandmobee config set …configcommandmobee job post/settle/showjobcommand — the trade path is daemon RPC / MCP (post_job,award,collect,get_job)mobee --home X …--homeis a post-subcommand flag, onwalletonly; everything else readsMOBEE_HOMEmobee wallet balances --jsonbalance, and there is no--jsonmobee buyer restartbuyer/buyer serve/buyer statusMOBEE=./target/release/mobeeCARGO_TARGET_DIRis redirected; nothing is at./targetconfig set default_mintaccepted_mintsentryA script in that state does not fail at step 0. It fails partway — and on the real-money path, partway means after the melt, with sats gone from the source and nothing issued at the target. That is the worst failure geometry the slice has.
Staging
The external unknowns are measured before our own code is involved:
--self-test— parsers against fixtures. Thestatus=needs_paymentbranch appears only on a non-test mint, so fixtures are the only coverage available before real sats. Runs on every mode, including both real ones.--dry-run— testnut. Proves gates, home discipline, env config, funding, balance parsing, bolt11 issue, and that a melt which cannot route returns non-zero — the property every|| dieguarding a real melt depends on.--stage-a— the hop by hand in three wallet commands (wallet mintat target →wallet meltat source →wallet mint-completeat target). No daemon, no relay, no seller, no hop code. Answers the one question no hermetic test can: does Lightning route source → target, and what does it cost? Ends in a fee-fit verdict against the exposure cap.--stage-b— refuses, naming exactly what it still needs, rather than pretending.Stage A before Stage B is the point: A's unknowns are external (two mints, LN routing, fees), B's are ours. Run together, a failure tells you neither. Run in order, an A-failure means the mint pair and a B-failure means our code.
Safety is structural, not procedural
MOBEE_ALLOW_REAL_MINTS=truestill readsallow_real_mints = falsein itsconfig.toml, and re-reading without the env reverts. The real-mint flip is process-scoped and evaporates on exit. The previous design restored the fence in anEXITtrap with|| trueon both restores — a restore that can fail silently.~/.mobeefallback is unreachable. Bootstrap does write intoMOBEE_HOME, and an unsetMOBEE_HOMEresolves to~/.mobee, which on this box holds a real key and wallet. Every invocation is funnelled through one wrapper that refuses any directory this run did not itself create and mark, refuses an empty home, and refuses anything resolving into~/.mobee. Both refusals are asserted in the dry run.MOBEE_TOTAL_BUDGET_SATSbounds the spend in the budget gate, which charges the hop's full cost (delivery + fee reserve + input fee) — independent of anything the script asserts afterwards.<amount>:<source>:<target>. The expected value is never echoed on refusal.Evidence
Built and run on the merged tree.
dev@2ab64b7is #196 squashed with nothing else on top, and its tree hash is identical to the branch it was built from (d3f58c4f…), so the binary under test is the merged-tree binary.Refusal paths, each verified:
What this still does not prove
A successful melt,
mint-completeafter real payment, and cross-mint LN routing. Those are structurally impossible against a test mint — measured, not assumed: on testnut a melt of a freshly issued invoice returnswallet error: Payment failed(rc=2). They are exactly what Stage A exists to measure, and it is gudnuf-gated.Follow-up in this PR: funding, and shape-checking what gets melted
Stage A created a fresh empty home and then asserted a source balance — so the first real run would have died at
buyer holds 0 sats. The funding step was missing entirely.Paying the invoice happens out of band and by a human, so funding is two invocations sharing one throwaway wallet:
--fundprints the bolt11,--fund-complete <quote_id>issues once it is paid. That makes a home reusable across invocations, somake_homebecameensure_home— it reuses one carrying our marker, and still refuses any directory this tooling did not create.The authorization token now names the total funded exposure rather than each step's amount: one human decision about how many real sats are at risk, which is the shape the spend was approved in.
A mint that auto-funds prints
minted_sats=…where a real mint prints a bolt11. Both are non-empty, so an emptiness check would feed that line straight intowallet melt.is_bolt11checks the shape instead, and the self-test pins both directions.Re-verified after the change rather than carrying the earlier green forward:
Two operational notes for whoever runs it:
RUN_DIRmust be set to a persistent path — it defaults to/tmp, and real ecash lives in that home, so a/tmpclean burns it. And the mint pair pre-flighted read-only: both HTTP 200, NUT-4 and NUT-5 present,"disabled":false, no advertised min/max.🤖 Generated with Claude Code