mint class: the issuer class is declared, never sniffed (stage 2) - #966
Open
maxy-player wants to merge 6 commits into
Open
mint class: the issuer class is declared, never sniffed (stage 2)#966maxy-player wants to merge 6 commits into
maxy-player wants to merge 6 commits into
Conversation
Stage 1 of the ecash mutual-credit build. One new OPTIONAL tag on the seat announcement, `["issuer_mint", url, cap, outstanding, retired, last_seen]`, carrying the seat's own mint URL and its issuance counters. Emitted from `HeartbeatDraft` (`with_issuer_mint`), parsed into `ParsedHeartbeat.issuer_mint`. Absent or malformed reads as unstated, never a rejection. Announcement only, never on the kind-3402 claim (protocol-v1 §4.2 "Issuer mint"). No version bump: a reader MUST ignore unrecognised tags (§2.1). No production publish path sets it yet; stage 3 wires live counters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner decision (Bob, 2 Sep, "lets keep it simple for now - no wrapper, no limit"): nothing enforces a ceiling on an issuer's outstanding tokens, so the tag no longer declares one. The tag is positional and shrinks to four values: `["issuer_mint", url, outstanding, retired, last_seen]`. Both index tables (the doc's position table and the malformed-shape test's field table) are renumbered: outstanding 3->2, retired 4->3, last_seen 5->4. `IssuerMintAd` loses `cap_sats`; serializer, parser destructure, the wire-shape fixture and the arity labels follow. The counters stay: they are the only trust signal an operator reads before extending credit.
…the tag The tag shrank to four values when `cap` was removed; one module-doc sentence at the top of the section still said five. Doc only.
…stage 2) The buyer wallet learns a mint CLASS (docs/protocol-v1.md §4.2 "Issuer mint"): Lightning, or ISSUER — a seat's own Cashu mint whose tokens are an IOU for that seat's work and have no Lightning route in or out. Markers, each recorded with WHO said it (`mint_class::IssuerMarker`): - Own: this seat's own mint, from a new optional `issuer_mint` config key. - Info: a mint whose NUT-06 info lists no bolt11 method under NUT-04/05, learned by a bounded GET /v1/info per accepted mint at accept. - Declared: the seller's own kind-30340 `issuer_mint` tag, read off its announcement at accept by (author, kind, d). Rules: - The real-mint fence admits an Own/Info issuer mint whatever `allow_real_mints` says (it carries no sats). A Declared mint does NOT widen the fence: a seller's signed word must not open the buyer's fence to any mint it names. Every marker refuses the hop. - `plan_payment` refuses a Lightning hop INTO or OUT OF any issuer mint with the plain reason "you hold none of this seller's currency"; an issuer entry is never a hop target. Direct payment at one is unchanged. - `select_source_mint` prefers the buyer's OWN mint when the seller lists it. - The hop executor asks both mints their class before any leg (run_hop gate + plan_quotes guard); `wallet fund` / `wallet melt` refuse at a mint whose info lists no bolt11. An unreachable info read is UNKNOWN, i.e. Lightning, so reachability errors keep their existing labels. - The class knowledge is SEALED into the accept-bind (`issuer_mints`, with markers) and re-derived at pay; legacy binds read as none known. Unit stays `sat`; the three sat gates (gateway.rs parse_offer, payment_wallet.rs terms_for_offer + seller receive) are untouched.
`complete_mint_async` opened the wallet and went straight to `poll_and_mint` (`check_mint_quote`, then `wallet.mint`) with no issuer-class check — the guard sat only on fund-begin and melt. Insert the same `refuse_lightning_op_at_issuer(&wallet, "wallet fund", &mint_url)` after the wallet opens, before any quote call; nothing else in the function changes. NEGATIVE test, offline: a same-process mint stub that answers `/v1/info` (and the keysets refresh cdk makes alongside it) and RECORDS every request path. At a stub whose NUT-04/NUT-05 list no bolt11 (Issuer), completion returns `WalletOpsError::IssuerMint` and the mint sees only the info read — no `check_mint_quote`, no `wallet.mint`. Control: the identical call at a bolt11-listing stub passes the guard and fails later inside `poll_and_mint` as an ordinary Wallet error, so the gate is the class, not the stub.
…ape)
A mint is an issuer mint because an operator said so — this seat's own
config (`IssuerMarker::Own`) or the counterparty's kind-30340 ad
(`IssuerMarker::Declared`) — and never because its /v1/info document
listed no bolt11 method. Owner's ruling, 3 Sep 2026 ("lets do 2").
Dies: `class_from_info`, `IssuerMarker::Info`, `IssuerMints::from_urls`,
`probe_issuer_mints`, and the three production network reads that
existed only to obtain a class — the probe in mint_class.rs,
`CdkHopEffects::class_of` in crossmint_hop.rs (`load_mint_info`), and
`refuse_lightning_op_at_issuer` in wallet_ops.rs (`load_mint_info`).
Replaces: `IssuerMarker::admits` is `matches!(self, Self::Own)`; nothing
else about admission changes. `CdkHopEffects::open` takes the caller's
`IssuerMints` and fixes both legs' classes there — the bind's seal on
the pay path, this seat's own config on the recovery sweep — and
`HopEffects::mint_classes` returns them. `wallet_ops`'s guard reads own
config and runs before the wallet opens. Accept builds
`IssuerMints::none().with_own(..).with_declared(..)`.
Kept: `IssuerMintSeal` and accept-time sealing. A seal written under the
retired `info` marker still loads: `#[serde(alias = "info")]` reads it
as `Declared` — the hop it refused it still refuses, it admits nothing,
and it re-serializes as `declared`.
Tests: 3 removed (a_mint_listing_no_bolt11_method_is_an_issuer_mint,
bolt11_on_either_table_reads_as_lightning,
every_marker_refuses_but_only_own_and_info_admit), 4 added
(every_marker_refuses_but_only_own_admits,
a_legacy_info_seal_still_reads_as_a_declaration_and_admits_nothing,
an_undeclared_mint_is_fenced_exactly_as_before_whatever_it_is,
crossmint_hop_plan_quotes_refuses_a_declared_issuer_leg_without_asking_the_mint);
the wallet_ops check-H test now asserts the declared mint receives NO
request at all, not even /v1/info, while its stub would answer as a
Lightning mint if asked.
`grep -rnw -E 'class_from_info|load_mint_info|get_mint_info'` under
crates/: 17 hits in 4 files at 3f13a1c, 1 at this commit (doctor.rs:93,
the reachability probe, which obtains no class).
|
Someone is attempting to deploy a commit to the MakePrisms Team on Vercel. A member of the Team first needs to authorize it. |
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.
The issuer class of a mint is DECLARED by an operator — this seat's own config, or the counterparty's kind‑30340 advertisement — and is never inferred from the mint's
/v1/infodocument. Owner's ruling, 3 Sep 2026 ("lets do 2"). Stage 2 of the ecash / mutual‑credit lane.Stacking — read this first
feat/ecash-mutual-credit, heade78b177). The diff below therefore shows stage‑1's three commits as well as stage‑2's three; only the three on top are this PR's.feat/one-mint-concept, head14e149b), which is unmerged and touches 5 of the 6 files this head changes (authorize_pay.rs,crossmint.rs,crossmint_hop.rs,job_lifecycle.rs,wallet_ops.rs;mint_class.rsis this branch's alone). The intended merge order is heartbeat: issuer_mint seat-ad tag + reader (agent-issued ecash, stage 1) #962 → one mint concept: retire the real-mint fence and the play-money class, leaving the seat's accepted list as the only gate #964 → this; the rebase is a separate, mechanical step after one mint concept: retire the real-mint fence and the play-money class, leaving the seat's accepted list as the only gate #964 lands and has deliberately not been done here.main=8c3bc9b834fefaf1fc8985ef5eee172a10f4684c, which is alsogit merge-base upstream/main HEAD. Headfc67661069461e1ee7728e9921aab3312433011b.git diff --shortstat 8c3bc9b..fc67661: 17 files, +1916/−61;Cargo.*files touched: 0. Stage‑2 only (e78b177..fc67661): 15 files, +1577/−58. This reshape commit alone (3f13a1c..fc67661): 6 files, +285/−310.What this commit does (
fc67661)Dies.
class_from_info,IssuerMarker::Info,IssuerMints::from_urls,probe_issuer_mints, and the three production network reads that existed only to obtain a class — each named with the header of the function it lived in at3f13a1c:mint_class.rs:303inpub async fn probe_issuer_mints(mint_urls: &[String], timeout: Duration) -> IssuerMints— the whole function is gone.crossmint_hop.rs:1089inasync fn class_of(wallet: &Wallet) -> MintClass(impl CdkHopEffects,:1079), consumed byHopEffects::mint_classes(:1096) — the function is gone; see below for whatmint_classesreturns now.wallet_ops.rs:459–466inasync fn refuse_lightning_op_at_issuer(wallet: &Wallet, op: &str, mint_url: &str)— nowfn refuse_lightning_op_at_issuer(home: &MaxplayerHome, op: &str, mint_url: &str), synchronous, and it runs before the wallet is opened.Replaces.
IssuerMarker::admits(mint_class.rs:79at head) ismatches!(self, Self::Own). Nothing else about admission changes:Declaredrefuses the hop and does not admit, exactly as its tests already asserted at3f13a1c.CdkHopEffects::open(home, source, target, issuers: &IssuerMints)fixes both legs' classes at open from the caller's declared knowledge;HopEffects::mint_classesreturns those two values and asks nothing of either mint. On the pay path the caller passes the accept‑bind's seal (authorize_pay.rs, the sameissuersthe plan was derived from). On the recovery sweep, which holds no bind, it passes this seat's own config (IssuerMints::none().with_own(home.config.issuer_mint())).job_lifecycle.rs:1363) buildsIssuerMints::none().with_own(home.config.issuer_mint()).with_declared(declared_issuer_mint.as_deref()). The one bounded relay read that fetches the seller's advertisement is unchanged; it reads a declaration, not a mint.IssuerMintSealand accept‑time sealing are unchanged;from_sealis still the pay path's only constructor.Legacy seals — one compatibility decision, disclosed. A bind sealed before this change may carry
"marker":"info".IssuerMarker::Declarednow carries#[serde(alias = "info")], so such a bind still loads: the entry refuses the hop as it did, admits nothing (no operator ever stated it), and re‑serializes asdeclared. The alternative — dropping the entry — would have silently forgotten a hop refusal. Noinfovalue is ever written again.The properties, each with the test that fails if it breaks
All lines below are from ci.yml:146 (
cargo test -p maxplayer-core --release --features acp,gateway,git-delivery,wallet --locked) atfc67661:running 1388 tests,0 filtered out./v1/infoto obtain a classOwn) admitsmint_class::tests::every_marker_refuses_but_only_own_admitsokDeclared) refuses and does not admitmint_class::tests::a_sellers_declaration_does_not_widen_the_fence,crossmint::tests::a_sellers_declared_issuer_mint_refuses_the_hop_but_widens_nothingok,okhome::mint_allowedfences it — unreachable, unroutable, unresolvable, malformed includedmint_class::tests::an_undeclared_mint_is_fenced_exactly_as_before_whatever_it_is,mint_class::tests::the_fence_admits_a_known_issuer_mint_and_nothing_else_newok,ok/v1/info— while its stub would answer as a Lightning mint if askedwallet_ops::tests::completing_a_mint_quote_at_an_issuer_mint_is_refused_before_any_quote_callokplan_quotesat a declared issuer leg refuses on the declaration alone; the same unreachable address undeclared isMintUnreachablefrom the quotecrossmint_hop::tests::crossmint_hop_plan_quotes_refuses_a_declared_issuer_leg_without_asking_the_mint,crossmint_hop::tests::an_issuer_mint_on_either_leg_refuses_before_any_leg_is_touchedok,okmint_class::tests::the_seal_round_trips_with_its_markers,job_lifecycle::tests::accept_bind_round_trips_on_disk,authorize_pay::tests::sealed_realized_mint_stabilizes_attempt_id_across_config_default_changeok,ok,okmint_class::tests::a_legacy_info_seal_still_reads_as_a_declaration_and_admits_nothing,job_lifecycle::tests::accept_bind_deserializes_legacy_json_without_realized_mintok,oksatgates are untouchedgit diff 3f13a1c..fc67661 -- crates | grep -E '^[-+].*\b[Ss]at\b': 5 lines, every one aunit: CurrencyUnit::Satfield inside aMintMethodSettings/MeltMethodSettingsliteral in#[cfg(test)]code (4 removed with the deleted info‑classification test helpers, 1 added in the rewritten wallet_ops test). No relative unit check was written.§4.1 gate —
git grep -nw -E 'class_from_info|load_mint_info|get_mint_info' <rev> -- 'crates/*'8c3bc9b(basemain)doctor.rs1e78b177(#962 head)doctor.rs13f13a1c(this branch before the reshape)mint_class.rs9 ·wallet_ops.rs4 ·crossmint_hop.rs3 ·doctor.rs1fc67661(head)doctor.rs1The one survivor, read:
doctor.rs:93inpub async fn probe_mint(mint_url: &str, timeout: Duration) -> Result<(), String>— the doctor's reachability probe. It returnsOk(())iff the mint answers a well‑formed info document and obtains no class from it; it is unchanged from base.Test inventory delta (
cargo test … -- --list, full feature set,3f13a1c→fc67661)Removed 3:
mint_class::tests::a_mint_listing_no_bolt11_method_is_an_issuer_mint,mint_class::tests::bolt11_on_either_table_reads_as_lightning(both tested the deleted classifier),mint_class::tests::every_marker_refuses_but_only_own_and_info_admit(renamed). Added 4:mint_class::tests::every_marker_refuses_but_only_own_admits,mint_class::tests::a_legacy_info_seal_still_reads_as_a_declaration_and_admits_nothing,mint_class::tests::an_undeclared_mint_is_fenced_exactly_as_before_whatever_it_is,crossmint_hop::tests::crossmint_hop_plan_quotes_refuses_a_declared_issuer_leg_without_asking_the_mint.CI ladder at
fc67661(exact ci.yml commands,--locked, run just now)cargo build --workspace --lockedcargo test -p maxplayer-core --lockedcargo test -p maxplayer --lockedcargo test -p maxplayer-relay-write-policy --lockedcargo build -p maxplayer --release --features acp,wallet --lockedcargo build -p maxplayer-core --features acp,gateway,git-delivery,wallet --lockedcargo test -p maxplayer-core --features acp --lockedcargo test -p maxplayer --features acp,wallet --lockedcargo test -p maxplayer-core --release --features acp,gateway,git-delivery,wallet --lockedcredential_proxy::tests::a_declared_over_cap_body_is_refused_before_the_upstream_sees_it(credential_proxy.rs:3520), a known pre‑existing flake in a file this branch does not touch (git diff --name-only 3f13a1c fc67661 | grep -c credential_proxy= 0). Not re‑run, not investigated here.cargo build -p maxplayer --no-default-features --lockedrustfmt --check --edition 2024per changed file, hunks at3f13a1c→fc67661:mint_class.rs0→0,crossmint_hop.rs2→2,wallet_ops.rs25→24,job_lifecycle.rs96→96,crossmint.rs30→30,authorize_pay.rs38→38 (the pre‑existing hunks are not this PR's to fix).Out of scope, named
Network reachability of an issuer mint for third parties (the second question in the owner's message) is not in this PR.
docs/protocol-v1.md§"Issuer mint" never described the info sniff, so no doc change is needed there. The narrowing this reshape implies — no marker by which a counterparty's mint can widen this seat's real‑mint fence — is the minimal reading of the ruling and is implemented as such; if the owner rules the other way it is a one‑clause change toIssuerMarker::admits.No merge, no tag, no release: the owner merges.