You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue tracks the work needed in mostro-cli to support the new Cashu ecash escrow mode, the non-custodial alternative to Lightning hold invoices. It is the client counterpart of the daemon effort described in mostro/docs/CASHU_ESCROW_ARCHITECTURE.md.
The protocol types already landed in mostro-core 0.12.0 (feat(cashu): protocol types for 2-of-3 multisig escrow (F1)), and the daemon foundation is in review:
mostro#761 — F5 DB helpers (escrow lock CAS + active-locked query)
⚠️ This is a guide to scope the work, not a finished design. Confirm exact cdk APIs against the version the daemon pins (see mostro#759) before implementing.
Core idea
Lightning hold invoices are replaced by Cashu ecash locked to a NUT-11 P2PK 2-of-3 spending condition over three keys:
P_B — buyer's per-order trade pubkey
P_S — seller's per-order trade pubkey
P_M — Mostro's arbitrator pubkey (provided by the daemon)
Mostro holds 1 of 3 keys and never takes custody: it only validates the lock and coordinates. All wallet operations (token swap, signing, mint swap submission) happen in the client — i.e. in mostro-cli. Two of {P_B, P_M} must sign to spend (primary key P_S).
🔑 Critical constraint:P_B and P_SMUST be the per-order trade keys, never the identity (master) keys — for unlinkability at the mint and for consistency with every other signature in an order. mostro-cli already derives a trade key per order (Order.trade_keys, User::get_next_trade_keys); reuse those exact keys for the Cashu condition and signatures.
What mostro-core 0.12.0 already gives us
Bump mostro-core = "0.11.3" → "0.12.0" in Cargo.toml. New surface to wire up:
Actions (Action):
AddCashuEscrow — seller → Mostro: submit the locked token (replaces seller paying a hold invoice)
Payload::CashuLockProof(CashuLockProof)// on AddCashuEscrowPayload::CashuSignatures(Vec<CashuProofSignature>)// on CashuPmSignature
pubstructCashuLockProof{pubtoken:String,// serialized locked Cashu tokenpubmint_url:String,// mint hosting the proofs (must match node config)pubbuyer_pubkey:String,// P_B (hex) — buyer trade pubkeypubseller_pubkey:String,// P_S (hex) — seller trade pubkeypubmostro_pubkey:String,// P_M (hex) — Mostro arbitrator pubkey}pubstructCashuProofSignature{pubsecret:String,// NUT-11 secret of the proof this sig applies topubsignature:String,// P_M signature (hex) to insert into that proof's witness}
Under NUT-11 SIG_INPUTS, each input proof carries its own witness signed over that proof's own secret. A token split across denominations has multiple proofs → one signature per proof, matched by secret. Any signature-exchange handling must be a Vec, not a single signature.
Order fields (Order in mostro-core; mirror in our src/db.rs Order):
cashu_mint_url: Option<String>
cashu_escrow_token: Option<String>
cashu_escrow_locked_at: Option<i64>
Errors (CantDoReason): InvalidCashuToken, CashuMintUnavailable, CashuEscrowNotLocked, CashuSignatureMissing — surface these to the user in print_dm_events / response handling.
New dependency: a Cashu wallet
The client must embed a real ecash wallet. Add cdk (Cashu Dev Kit) — the same crate the daemon's CashuClient (mostro#759) wraps. We need wallet-side capabilities the daemon doesn't:
New --mint-url flag / MINT_URL env, alongside the existing MOSTRO_PUBKEY/RELAYS/POW pattern in get_env_var (src/cli.rs).
One mint per node for now — no per-order mint negotiation. Prompt the user to confirm the mint at trade start.
Cashu and LN bonds are mutually exclusive (design decision Add release command #5); the daemon enforces this — the CLI should not offer bond flows in Cashu mode.
Local wallet storage (DB migration)
src/db.rs is SQLite via sqlx. We need new tables/columns for:
the wallet's unencumbered proofs + mint keysets (or whatever cdk's store layer needs)
escrow state per order: add cashu_mint_url, cashu_escrow_token, cashu_escrow_locked_at to the Order struct + schema, plus locally-held release/cancel signatures received via DM.
Follow the existing migration approach used for the orders/users tables.
Message flow & client work
Seller — lock escrow (replaces AddInvoice for the seller)
Mirror src/cli/add_invoice.rs / take_order.rs. After the order is matched, Mostro provides P_B and P_M. The seller wallet must:
Serialize the locked token and send Action::AddCashuEscrow with Payload::CashuLockProof { token, mint_url, P_B, P_S, P_M } (signed with the order's trade key via the existing send_dm path).
Wait for CashuEscrowLocked from Mostro (wait_for_dm / print_dm_events).
Buyer — release (happy path)
Signature exchange bypasses Mostro — it goes party-to-party over NIP-59 DM using trade keys. The CLI already has this infra (send_dm gift-wrap, wait_for_dm, get_dm_user).
Seller, on Release, signs each escrowed proof and DMs the signatures to the buyer (Vec of per-proof signatures keyed by secret), then notifies Mostro (informational).
Buyer receives the seller's signatures via NIP-59 DM, adds their own P_B signature per proof.
Buyer builds a SwapRequest with both witnesses and submits to the mint (/v1/swap, NUT-11).
Mint issues unencumbered ecash to the buyer; store it in the local wallet.
Buyer notifies Mostro: trade complete.
Cooperative cancel (buyer → seller P2P)
Buyer generates their P_B signature(s), DMs them to the seller via NIP-59.
Seller combines with their own P_S signature → P_S + P_B, submits to the mint to reclaim the escrow.
Dispute resolution
Mostro supplies its P_M signatures via Action::CashuPmSignature / Payload::CashuSignatures(Vec<CashuProofSignature>):
admin_settle → winner is the buyer: combine P_M (matched per proof by secret) with P_B, swap, redeem.
admin_cancel → winner is the seller: combine P_M with P_S, reclaim.
Suggested new CLI subcommands (src/cli.rs, src/cli/)
Config: [cashu] block, --mint-url/MINT_URL, mode selection, mutual-exclusion with bonds.
DB: wallet proof/keyset storage + cashu_* order fields + received-signature storage (mirror mostro#761 helpers where useful).
Track A — seller AddCashuEscrow lock flow + handle CashuEscrowLocked.
Track B — buyer release: receive seller sigs over NIP-59, add P_B sigs, mint swap, store unencumbered ecash.
Track C — cooperative cancel: buyer→seller P2P signatures, seller reclaim.
Track D — dispute: consume CashuPmSignature, combine with own key, redeem/reclaim.
Tests against a containerized mint (mirror daemon's F6 harness).
Docs / README usage for Cashu mode.
Notes & open questions
SIG_INPUTS vs SIG_ALL: baseline is SIG_INPUTS (simplest UX; assumes a non-adversarial agreed mint). SIG_ALL (buyer pre-builds outputs, seller signs the bundle) protects against a malicious front-running mint but needs tighter coordination — future work.
Fees: deferred in the initial design. Today's Mostro fee is taken from LN amounts; fee collection in Cashu mode needs its own design. No fee mechanism in the first rollout.
Offline resilience: signatures move async over Nostr — parties don't need to be online simultaneously; no CLTV timeout pressure.
Reuse the existing trade-key derivation and NIP-59 DM infra — no new key machinery is needed, just point the Cashu condition/signatures at the order's trade key.
Depends on: mostro#758, mostro#759, mostro#760, mostro#761 landing and a tagged daemon release exposing the mint_url config.
Implement Cashu 2-of-3 multisig escrow (client-side)
This issue tracks the work needed in mostro-cli to support the new Cashu ecash escrow mode, the non-custodial alternative to Lightning hold invoices. It is the client counterpart of the daemon effort described in
mostro/docs/CASHU_ESCROW_ARCHITECTURE.md.The protocol types already landed in
mostro-core0.12.0 (feat(cashu): protocol types for 2-of-3 multisig escrow (F1)), and the daemon foundation is in review:EscrowBackendtrait + Lightning impl + Cashu stubCashuClientwrapper aroundcdkCore idea
Lightning hold invoices are replaced by Cashu ecash locked to a NUT-11 P2PK 2-of-3 spending condition over three keys:
P_B— buyer's per-order trade pubkeyP_S— seller's per-order trade pubkeyP_M— Mostro's arbitrator pubkey (provided by the daemon)Mostro holds 1 of 3 keys and never takes custody: it only validates the lock and coordinates. All wallet operations (token swap, signing, mint swap submission) happen in the client — i.e. in mostro-cli. Two of
{P_B, P_M}must sign to spend (primary keyP_S).What mostro-core 0.12.0 already gives us
Bump
mostro-core = "0.11.3"→"0.12.0"inCargo.toml. New surface to wire up:Actions (
Action):AddCashuEscrow— seller → Mostro: submit the locked token (replaces seller paying a hold invoice)CashuEscrowLocked— Mostro → buyer: token validated & unspent, "send fiat"CashuPmSignature— Mostro → dispute winner: hands over Mostro'sP_MsignaturesPayloads (
Payload):Order fields (
Orderin mostro-core; mirror in oursrc/db.rs Order):cashu_mint_url: Option<String>cashu_escrow_token: Option<String>cashu_escrow_locked_at: Option<i64>Errors (
CantDoReason):InvalidCashuToken,CashuMintUnavailable,CashuEscrowNotLocked,CashuSignatureMissing— surface these to the user inprint_dm_events/ response handling.New dependency: a Cashu wallet
The client must embed a real ecash wallet. Add
cdk(Cashu Dev Kit) — the same crate the daemon'sCashuClient(mostro#759) wraps. We need wallet-side capabilities the daemon doesn't:SIG_INPUTS)Build the 2-of-3 condition with
cdk::nuts::nut10(from the architecture doc):Configuration
Add a Cashu config block + CLI flag, mirroring the daemon (
[cashu] enabled,mint_url):--mint-urlflag /MINT_URLenv, alongside the existingMOSTRO_PUBKEY/RELAYS/POWpattern inget_env_var(src/cli.rs).Local wallet storage (DB migration)
src/db.rsis SQLite viasqlx. We need new tables/columns for:cdk's store layer needs)cashu_mint_url,cashu_escrow_token,cashu_escrow_locked_atto theOrderstruct + schema, plus locally-held release/cancel signatures received via DM.Follow the existing migration approach used for the
orders/userstables.Message flow & client work
Seller — lock escrow (replaces
AddInvoicefor the seller)Mirror
src/cli/add_invoice.rs/take_order.rs. After the order is matched, Mostro providesP_BandP_M. The seller wallet must:P_Sprimary,{P_B, P_M}secondary,n_sigs = 2,SIG_INPUTS).Action::AddCashuEscrowwithPayload::CashuLockProof { token, mint_url, P_B, P_S, P_M }(signed with the order's trade key via the existingsend_dmpath).CashuEscrowLockedfrom Mostro (wait_for_dm/print_dm_events).Buyer — release (happy path)
Signature exchange bypasses Mostro — it goes party-to-party over NIP-59 DM using trade keys. The CLI already has this infra (
send_dmgift-wrap,wait_for_dm,get_dm_user).Release, signs each escrowed proof and DMs the signatures to the buyer (Vecof per-proof signatures keyed bysecret), then notifies Mostro (informational).P_Bsignature per proof.SwapRequestwith both witnesses and submits to the mint (/v1/swap, NUT-11).Cooperative cancel (buyer → seller P2P)
P_Bsignature(s), DMs them to the seller via NIP-59.P_Ssignature →P_S + P_B, submits to the mint to reclaim the escrow.Dispute resolution
Mostro supplies its
P_Msignatures viaAction::CashuPmSignature/Payload::CashuSignatures(Vec<CashuProofSignature>):admin_settle→ winner is the buyer: combineP_M(matched per proof bysecret) withP_B, swap, redeem.admin_cancel→ winner is the seller: combineP_MwithP_S, reclaim.Suggested new CLI subcommands (
src/cli.rs,src/cli/)(Names indicative — align with daemon semantics.)
add-cashu-escrow --order-id <id>— seller lock flow.release/cancel— extend existing commands to, in Cashu mode, emit the P2P signature DM + mint swap instead of LN settle/cancel.cashu-balance,cashu-mint <token>(deposit),cashu-send/cashu-receivefor funding the local wallet.adm-settle/adm-cancel) to deliverP_Msignatures (daemon side) — on the client, consumeCashuPmSignature.Implementation checklist
mostro-coreto0.12.0; wire up the newAction/Payload/error variants in response handling (src/parser,src/util/messaging.rs).cdkdependency; build asrc/cashu/module (wallet wrapper: connect, balance, swap-to-locked, sign proof, swap-to-redeem,verify_2of3_condition).[cashu]block,--mint-url/MINT_URL, mode selection, mutual-exclusion with bonds.cashu_*order fields + received-signature storage (mirror mostro#761 helpers where useful).AddCashuEscrowlock flow + handleCashuEscrowLocked.P_Bsigs, mint swap, store unencumbered ecash.CashuPmSignature, combine with own key, redeem/reclaim.Notes & open questions
SIG_INPUTSvsSIG_ALL: baseline isSIG_INPUTS(simplest UX; assumes a non-adversarial agreed mint).SIG_ALL(buyer pre-builds outputs, seller signs the bundle) protects against a malicious front-running mint but needs tighter coordination — future work.Depends on: mostro#758, mostro#759, mostro#760, mostro#761 landing and a tagged daemon release exposing the
mint_urlconfig.