This document describes the alternative escrow mechanism utilizing Cashu (NUT-11 P2PK) rather than Lightning Network hold invoices. In this model, Mostro acts strictly as a coordinator and arbitrator, and never takes custody of funds.
The Lightning hold-invoice escrow works well for users who run their own node and have reliable connectivity, but it does not serve everyone in the Mostro ecosystem. This Cashu-based model is not a replacement for the Lightning flow — it is an additional option aimed at communities and conditions where hold invoices are impractical. It deliberately trades self-custody and trustlessness for offline resilience and simplicity, which is an acceptable bargain in trust-based communities.
-
Offline resilience for users with unreliable infrastructure. In places like Cuba, recurring electricity outages and intermittent connectivity make it impractical to keep a node — or even a phone — online for the full duration of a trade. The Lightning hold-invoice flow requires the payer, the payee, and Mostro's routing node to be online and able to route an HTLC at the same moment. The Cashu flow does not: funds are locked in an ecash token, and the release signature can be exchanged out-of-band over Nostr whenever each party happens to come online. A trade can progress across separate, non-overlapping connectivity windows.
-
Non-technical users who don't want to run a node. Operating a Lightning node means managing channels, inbound/outbound liquidity, rebalancing, and the custody risk of the funds those channels hold. Many users — especially newcomers — neither want nor are equipped to do this. The Cashu model lets them rely on an external mint instead. This means delegating custody and trust to the mint operator, which is a reasonable tradeoff for trust-based communities that already share a mint they trust.
-
Reduced legal/custody surface for the operator. This is the key structural improvement over Lightning. With a hold invoice, Mostro takes actual custody of the funds for the few seconds between accepting the inbound HTLC and settling the outbound payment — brief, but real custody. In the Cashu 2-of-3 model the operator never takes possession of user funds at any point: Mostro only ever holds 1 of the 3 keys and can never unilaterally move the ecash. This removes Mostro from the custody path entirely, which materially shrinks the legal and regulatory burden of operating a coordinator.
-
Long-lived escrow for marketplace-style trades. A Lightning hold invoice cannot stay pending indefinitely — it is bounded by its CLTV delta (measured in blocks), so the funds must be released or refunded within hours. This makes Lightning escrow unsuitable for any trade that does not settle almost immediately. Cashu ecash, by contrast, does not expire: a 2-of-3 locked token can sit in escrow for days or weeks without any on-chain timeout forcing resolution. This unlocks Mostro as a marketplace for physical goods and other slow-to-deliver items — a buyer can lock funds, wait for the seller to ship and the package to arrive, and only then release, with the same non-custodial safety throughout the entire delivery window.
flowchart LR
subgraph Runtime[src/]
MAIN[src/main.rs]
APP[src/app.rs]
A_APP[src/app/*]
CASHU[src/cashu/mod.rs]
RPC[src/rpc/server.rs]
DB[src/db.rs]
end
MAIN --> APP
APP --> A_APP
APP --> DB
APP --> CASHU
RPC --> DB
RPC --> CASHU
- Entry:
src/main.rsinitializes standard subsystems but bypassesfedimint-tonic-lndinitialization if operating in pure-Cashu mode. - Cashu:
src/cashu/mod.rsinterfaces with thecdkcrate to verify token conditions (NUT-10/NUT-11) and communicate with the Mint's/v1/checkstateendpoint.
Instead of routing HTLCs through Mostro's node, the seller locks the funds in a Cashu token governed by a 2-of-3 signature requirement:
-
$P_B$ (Buyer Pubkey) -
$P_S$ (Seller Pubkey) -
$P_M$ (Mostro/Arbitrator Pubkey)
The buyer and seller pubkeys MUST be the per-order trade keys, never the parties' identity (master) keys. Mostro already derives a fresh, ephemeral trade key for each side of every order (see the existing trade-key flow used by
add-invoice/take-*). The Cashu escrow reuses exactly those keys:$P_B$ is the buyer's trade pubkey for this order and$P_S$ is the seller's trade pubkey for this order. This is not an arbitrary choice — it is required for both privacy and protocol consistency:
- Privacy / unlinkability. Using identity keys would publish a long-lived pubkey into the mint's spending condition, letting the mint (or anyone who later inspects the token) link every escrow a user ever participates in back to one identity and to each other. Per-order trade keys keep each escrow cryptographically independent.
- Consistency with the rest of the protocol. Every other signature a party produces in an order (order messages,
release,cancel, NIP-59 DMs carrying the release signature) is already made with the trade key. The Cashu signature that satisfies the 2-of-3 condition is produced by the same key, so the party can sign the swap with the key they already hold for this order, and the daemon verifies against the trade pubkey it already has on file.
$P_M$ is Mostro's arbitrator key. It MAY be a Mostro key dedicated to Cashu arbitration rather than a per-order key, since Mostro only ever signs during dispute resolution; the daemon's identity is not something the trade is trying to hide.
sequenceDiagram
participant S as Seller (Maker)
participant M as Mostro
participant B as Buyer (Taker)
participant Mint as Cashu Mint
Note over S,B: 1. ORDER MATCHING
B->>M: Take Sell (Take Order)
M->>S: Notify: Order Taken, Provide P_B and P_M
Note over S,Mint: 2. ESCROW SETUP (No Mostro Custody)
S->>Mint: Swap unencumbered ecash for 2-of-3 Locked ecash
S->>M: Submit locked tokens & condition details
M->>Mint: Checkstate (Are tokens unspent?)
M->>B: Notify: Escrow Locked. Send Fiat!
Note over B,S: 3. FIAT TRANSFER
B->>M: Fiat Sent
M->>S: Notify: Check your bank
Note over S,B: 4. RELEASE (Happy Path)
S->>B: Direct Nostr DM: Seller's Signature (Sig 1)
S->>M: Notify: "I have released the funds" (State Update)
B->>Mint: Submit SwapRequest (Sig 1 + Sig 2)
Mint-->>B: Unencumbered ecash issued to Buyer
B->>M: Notify: "Trade complete" (State Update)
The introduction of Cashu Escrow modifies the responsibility of core action handlers.
| Action | Proposed Handler Mod | Responsibility |
|---|---|---|
add-invoice |
src/app/add_invoice.rs |
Instead of creating a hold invoice, validates the submitted Cashu token using cdk, verifies the 2-of-3 spending condition, and calls the Mint API to ensure funds exist. |
release |
src/app/release.rs |
Instead of acting as the middleman for signatures, Mostro simply receives the state update notification from the Seller. The cryptographic signature is sent directly to the Buyer via a P2P Nostr Direct Message (NIP-59) using the trade's ephemeral keys. |
cancel |
src/app/cancel.rs |
If a trade is canceled cooperatively, the Buyer provides their signature directly to the Seller (via NIP-59 DM) so the Seller can reclaim the locked ecash, bypassing Mostro's servers. |
admin-settle |
src/app/admin_settle.rs |
(Dispute Resolution) Mostro generates its signature ( |
admin-cancel |
src/app/admin_cancel.rs |
(Dispute Resolution) Mostro generates its signature ( |
Sellers construct the 2-of-3 spending condition using cdk::nuts::nut10. We recommend the SIG_INPUTS flag. This allows the seller to sign the authorization once and pass it to the buyer, allowing the buyer to specify their own target outputs independently.
use cdk::nuts::nut10::{Conditions, SpendingConditions, SigFlag};
use cdk::nuts::PublicKey;
// 1. Gather pubkeys — P_B and P_S MUST be the per-order *trade* keys
// for this order, NOT the parties' identity/master keys.
let p_s: PublicKey = /* Seller's trade pubkey for this order */;
let p_b: PublicKey = /* Buyer's trade pubkey for this order */;
let p_m: PublicKey = /* Mostro's arbitrator pubkey */;
// 2. Define 2-of-3 constraints
let conditions = Conditions::new(
None,
Some(vec![p_b, p_m]), // Secondary keys
None,
Some(2), // Requires 2 signatures
None,
Some(SigFlag::SigInputs), // SigInputs for flexible output assignment
).unwrap();
// 3. Generate Secret for blinding
let secret = SpendingConditions::new_p2pk(p_s, Some(conditions));SIG_INPUTS: The easiest UX. The Seller only signs the intent to release. The Buyer receives the signature via Nostr DM, crafts their own unblinded outputs, signs the request, and asks the Mint to swap.SIG_ALL: The safest UX against malicious Mints. The Buyer must pre-construct their outputs, send the hash to the Seller, and the Seller signs the entire bundle.- Decision: Mostro relies on
SIG_INPUTSas the baseline. Because both parties must mutually agree on the Mint provider prior to the trade, we assume the Mint will not maliciously front-run transaction outputs.
- Non-Custodial: Mostro drops all legal and technical burdens of custody. A compromised Mostro server only leaks 1 of 3 keys, meaning attacker cannot steal active escrows.
- Offline Resilience: If Mostro's daemon crashes or vanishes permanently, the Buyer and Seller can still cooperate out-of-band to settle the trade (Seller + Buyer = 2 keys).
- No Routing Failures: Bypasses Lightning Network topology, channel liquidity constraints, and unpredictable routing fees.
- Zero Capital Lockup: Mostro does not require inbound/outbound channel liquidity to facilitate trades.
This section turns the architecture above into a concrete, incremental engineering plan. It follows the same phased-PR convention as ANTI_ABUSE_BOND.md: each phase is a self-contained, reviewable pull request that leaves main shippable. The feature is opt-in and defaults to off — until an operator enables it, the daemon behaves exactly as it does today.
These decisions scope the plan and should not be re-litigated per phase:
- Global mode switch, not per-order. A node runs in one escrow mode at a time:
lightning(today's default) orcashu. The mode is fixed insettings.toml. When a node runs incashumode, thefedimint-tonic-lndconnector is not initialized at startup — the node needs no LND. There is no mixed mode where a single node offers both escrow types simultaneously. - Node-configured, fixed mint. The operator sets a single
mint_urlinsettings.toml. All Cashu trades on that node use that mint. Per-order mint negotiation is explicitly out of scope for this rollout (possible future work). - The daemon is a coordinator, not a wallet. All wallet-side ecash operations — the seller swapping unencumbered ecash into a 2-of-3 locked token, and the buyer redeeming the locked token with two signatures — happen in the client. The daemon's responsibilities are narrow: validate a submitted locked token against the mint (
/v1/checkstate), verify the 2-of-3 spending condition embeds the right three pubkeys, hold its own keyP_M, and produce aP_Msignature only during dispute resolution. Client work is documented here as an interface contract but implemented in the client repos, not here. mostro-corechanges ship first. NewAction/Payload/CantDoReasonvariants and any newStatuslive in the sharedmostro-corecrate. They are additive and must be released (and the daemon's dependency bumped) before the daemon can use them. During development the daemon points at the localmostro-corevia apathdependency.- Bonds and Cashu mode are mutually exclusive (for now). The anti-abuse bond is built on LN hold invoices and cannot function without LND. In
cashumode the bond feature is rejected at config-validation time. A Cashu-native bond is future work.
flowchart LR
subgraph mostro-core
MSG[message.rs<br/>Actions + Payloads]
ORD[order.rs<br/>Status + fields]
end
subgraph mostro/src
MAIN[main.rs<br/>escrow-mode boot]
CFG[config/*<br/>CashuSettings]
CASHU[cashu/mod.rs<br/>CashuClient]
ESCROW[escrow backend<br/>trait/enum]
APP[app/*<br/>handlers branch on mode]
DB[db.rs<br/>cashu columns]
end
MSG --> APP
ORD --> APP
CFG --> MAIN
MAIN --> CASHU
APP --> ESCROW
ESCROW --> CASHU
APP --> DB
This feature is large enough that several developers (each AI-assisted) should be able to work simultaneously without stepping on each other. Sequential phasing would serialize that team; instead we split the work into a small Foundation milestone that everyone depends on, followed by independent feature tracks that can be built in parallel.
The whole strategy rests on three ideas:
- Freeze the contracts first. Three interfaces are the seams between workstreams. Once their signatures are agreed and merged (even as stubs), every track can code and unit-test against them in isolation, mocking the other side:
- Protocol contract — the new
Action/Payload/Status/CantDoReasonshapes inmostro-core. EscrowBackendtrait — the abstraction the action handlers call instead of touching LND orcdkdirectly (lock,release,cooperative_cancel,dispute_settle,dispute_cancel). ALightningBackendwraps today's code unchanged; aCashuBackendis filled in by the feature tracks.CashuClientAPI — thecdkwrapper insrc/cashu/(connect,check_state,verify_2of3_condition,sign_with_pm). A thin, self-contained library the tracks call.
- Protocol contract — the new
- Stub every integration point in Foundation. All new enum variants, all dispatch
matcharms (pointing at stub handlers that return "not implemented"), all trait-method signatures (Cashu impl =unimplemented!()), and the full DB schema land together in Foundation. This is what avoids merge hell: after Foundation, the conflict-prone shared files (app.rsdispatch,message.rsenums, the migration) are frozen, and each feature track only fills in handler/trait bodies in its own files. - One DB migration up front. Parallel devs each writing migrations against the same
orderstable causes ordering/merge conflicts. Foundation adds all Cashu columns in a single migration so no feature track touches the schema.
flowchart TD
subgraph FOUNDATION["Milestone 0 — Foundation (merge before tracks)"]
F1["F1 · mostro-core protocol<br/>Actions/Payloads/Status/CantDo + verify()"]
F2["F2 · config + escrow-mode<br/>CashuSettings, LND-optional boot"]
F3["F3 · EscrowBackend trait<br/>+ LightningBackend (no-op refactor)"]
F4["F4 · CashuClient lib<br/>cdk wrapper: connect/checkstate/verify/sign"]
F5["F5 · DB migration<br/>all cashu columns + helpers"]
F6["F6 · test harness<br/>containerized mint in CI"]
end
subgraph TRACKS["Parallel feature tracks (independent)"]
A["Track A · Lock / escrow setup"]
B["Track B · Release (happy path)"]
C["Track C · Cooperative cancel"]
D["Track D · Dispute resolution (P_M signs)"]
end
INT["Integration & hardening (continuous → final)"]
F1 --> A & B & C & D
F3 --> A & B & C & D
F4 --> A & D
F5 --> A
F2 --> INT
F6 --> INT
A --> INT
B --> INT
C --> INT
D --> INT
Six PRs. F1 and F3 are the critical contracts and should land first; F2/F4/F5/F6 can themselves be built in parallel once the contract shapes are agreed (a short written interface spec on day one lets F4 proceed before F3 merges, etc.). Zero behavior change when the feature is off.
- F1 ·
mostro-coreprotocol.src/message.rs: addActionvariants (AddCashuEscrow,CashuEscrowLocked, …) andPayloadvariants (CashuToken(String),CashuMintUrl(String),CashuLockProof(CashuLockProofData)where the struct carries{ token, mint_url, buyer_pubkey, seller_pubkey, mostro_pubkey, signatures }—buyer_pubkeyandseller_pubkeyare the per-order trade pubkeys, never identity keys); extendMessageKind::verify(); add round-trip tests.src/error.rs: addCantDoReasonvariants (InvalidCashuToken,CashuMintUnavailable,InvalidMintUrl,CashuEscrowNotLocked,CashuSignatureMissing).src/order.rs: add optionalOrderfields (cashu_mint_url,cashu_escrow_token,cashu_escrow_locked_at); keepSmallOrderlean. Bump crate version; daemon pins the localpathduring dev. - F2 · config + escrow-mode + boot.
Cargo.toml: addcdk.config/types.rs+settings.rs:CashuSettings { enabled, mint_url, .. }as#[serde(default)] pub cashu: Option<CashuSettings>mirroringanti_abuse_bond;get_cashu()/is_cashu_enabled(). Addenum EscrowMode { Lightning, Cashu }resolved from config; validation (parseablemint_url; Cashu +anti_abuse_bond.enabledis a hard error; Lightning default when[cashu]absent).main.rs: in Cashu mode skipLndConnector::new()and the LN_STATUS probe.wizard.rs+settings.tpl.toml: prompts and a commented[cashu]block. - F3 ·
EscrowBackendtrait + Lightning impl. Define the trait (lock,release,cooperative_cancel,dispute_settle,dispute_cancel, plus whatever startup/status hooks are needed). Refactor today's LND calls intake_*/add_invoice/release/cancel/admin_*to go through aLightningBackendimplementation — behavior-preserving, fully covered by existing tests. Add aCashuBackendwhose methods areunimplemented!().AppContextcarries the active backend. This is the seam that lets Tracks A–D edit disjoint method bodies. - F4 ·
CashuClientlibrary.src/cashu/mod.rs: a self-containedcdkwrapper —connect(mint_url),check_state(...)(NUT-07/v1/checkstate),verify_2of3_condition(token, p_b, p_s, p_m)(NUT-10/11), wherep_b/p_sare the caller-supplied per-order trade pubkeys the condition must embed,sign_with_pm(proofs)(NUT-11 P2PK). Startup mint-connectivity check storing aCASHU_STATUSOnceLock. Unit-tested against the F6 mint; no daemon wiring, so it's fully parallelizable. - F5 · DB migration + helpers. One migration adding all Cashu columns to
orders;find_order_by_*and update helpers. Frozen after this PR so no feature track writes a migration. - F6 · test harness. Containerized test mint (e.g.
nutshell) wired into CI alongside the existing LN regtest, plus fixtures/helpers the tracks reuse for integration tests.
Deliverable: node boots in either mode; in Cashu mode it connects to the mint and starts; all Cashu handlers/backends are stubbed (unimplemented!()), gated off by default. The shared files are now frozen.
After Foundation merges, these four tracks are mutually independent — each fills in stubbed handler/backend bodies in its own files and adds its own tests against the F4 client and F6 mint. They can be assigned to four developers and merged in any order.
-
Track A · Lock / escrow setup (box 2 of the sequence diagram). Implement
CashuBackend::lock: onAddCashuEscrow, parse the seller's token, callCashuClient::verify_2of3_conditionover{P_B, P_S, P_M}— asserting that$P_B$ and$P_S$ match the trade pubkeys Mostro already holds for this order (reject the token otherwise) and that$P_M$ is Mostro's own arbitrator key —check_stateto confirm unspent, persist the F5 columns, advance the order (WaitingPayment→Active), notify the buyer to send fiat. Touches the lock branch oftake_*/add_invoiceonly. Depends on F1, F3, F4, F5. -
Track B · Release happy path (box 4). Implement
CashuBackend::release: the seller's release signature goes seller→buyer P2P over NIP-59 DM (reusemostro-core'schat/SendDm); the daemon only validates theFiatSent → releasedtransition and advances to a terminal success state — it never touches funds. Document the exact client↔client payload as an interface contract. Touchesrelease.rs's Cashu branch only. Depends on F1, F3. -
Track C · Cooperative cancel. Implement
CashuBackend::cooperative_cancel: record the cancel and transition state with no hold-invoice cancellation; the buyer hands their signature to the seller P2P so the seller reconstructs aP_S + P_B2-of-3 swap to reclaim. Touchescancel.rs's Cashu branch only. Depends on F1, F3. -
Track D · Dispute resolution — the only place the daemon signs with
P_M. ImplementCashuBackend::dispute_settle/dispute_cancelusingCashuClient::sign_with_pm:admin_settle→ deliverP_Msignature to the buyer;admin_cancel→ deliverP_Msignature to the seller. Reuse existing solver/permission checks; only the settlement primitive changes. Touchesadmin_settle.rs/admin_cancel.rsCashu branches only. Depends on F1, F3, F4.
Runs alongside the tracks and closes the milestone:
- End-to-end happy-path and dispute trades on the F6 mint; cross-track wiring once A+B (and then C, D) land.
- Mint-unavailable handling and retries; idempotency on resubmitted tokens;
restore_sessionfor in-flight Cashu orders; expiry/timeout for un-locked escrows. - Enforce and test the bonds-vs-Cashu mutual exclusion end to end.
- Operator docs: enabling Cashu mode, choosing a mint, the trust model, and migration/runbook notes.
| Unit | Depends on | Conflict-prone shared files it touches | Parallel with |
|---|---|---|---|
| F1 protocol | — | mostro-core enums (frozen after) |
F2, F4, F5, F6 (shapes agreed first) |
| F3 trait | F1 shapes | app.rs dispatch, handler signatures (frozen after) |
F2, F4, F5, F6 |
| F2 config/boot | F1 shapes | main.rs, config/* |
F3, F4, F5, F6 |
| F4 CashuClient | — (cdk only) | none (new module) | everything |
| F5 migration | — | one migration (frozen after) | everything |
| F6 test harness | — | CI config | everything |
| Track A lock | F1, F3, F4, F5 | own files only | B, C, D |
| Track B release | F1, F3 | own files only | A, C, D |
| Track C coop-cancel | F1, F3 | own files only | A, B, D |
| Track D dispute | F1, F3, F4 | own files only | A, B, C |
SIG_INPUTSvsSIG_ALLis currently decided asSIG_INPUTS(see above). Revisit if malicious-mint resistance becomes a requirement.- Per-order mint negotiation and multi-mint allow-lists are deferred.
- Cashu-native anti-abuse bond to replace the LN bond in Cashu mode.
- Fee collection in Cashu mode (today the Mostro fee is taken from the LN amounts) needs its own design — how/whether the operator collects a fee on a non-custodial ecash trade.