Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Implement Cashu 2-of-3 multisig escrow (client-side)

Đang mở
#175 0 bình luận 0 reaction 1 người được giao Xem trên GitHub

@AndreaDiazCorreia đang làm issue này rồi.

Từ ngày 30/5/2026.

Đánh giá

Issue này chưa được đánh giá.

Mô tả

enhancement

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-core 0.12.0 (feat(cashu): protocol types for 2-of-3 multisig escrow (F1)), and the daemon foundation is in review:

  • mostro#758 — F2 config, escrow mode, conditional boot
  • mostro#760 — F3 EscrowBackend trait + Lightning impl + Cashu stub
  • mostro#759 — F4 CashuClient wrapper around cdk
  • 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_S MUST 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)
  • CashuEscrowLocked — Mostro → buyer: token validated & unspent, "send fiat"
  • CashuPmSignature — Mostro → dispute winner: hands over Mostro's P_M signatures

Payloads (Payload):

Payload::CashuLockProof(CashuLockProof)        // on AddCashuEscrow
Payload::CashuSignatures(Vec<CashuProofSignature>) // on CashuPmSignature
pub struct CashuLockProof {
    pub token: String,         // serialized locked Cashu token
    pub mint_url: String,      // mint hosting the proofs (must match node config)
    pub buyer_pubkey: String,  // P_B (hex) — buyer trade pubkey
    pub seller_pubkey: String, // P_S (hex) — seller trade pubkey
    pub mostro_pubkey: String, // P_M (hex) — Mostro arbitrator pubkey
}

pub struct CashuProofSignature {
    pub secret: String,    // NUT-11 secret of the proof this sig applies to
    pub signature: 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:

  • mint connection + keyset fetch
  • holding unencumbered proofs (a local balance)
  • swapping unencumbered → P2PK-locked proofs (SIG_INPUTS)
  • producing NUT-11 witness signatures with a trade key
  • submitting the final swap to the mint to redeem

Build the 2-of-3 condition with cdk::nuts::nut10 (from the architecture doc):

use cdk::nuts::nut10::{Conditions, SpendingConditions, SigFlag};
use cdk::nuts::PublicKey;

let conditions = Conditions::new(
    None,
    Some(vec![p_b, p_m]),       // secondary keys (2 of 3)
    None,
    Some(2),                    // require exactly 2 signatures
    None,
    Some(SigFlag::SigInputs),   // baseline choice for first rollout
).unwrap();

let secret = SpendingConditions::new_p2pk(p_s, Some(conditions)); // primary = seller

Configuration

Add a Cashu config block + CLI flag, mirroring the daemon ([cashu] enabled, mint_url):

[cashu]
enabled = true
mint_url = "https://mint.example.com"
  • 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 #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:

  1. Take unencumbered proofs from the local balance.
  2. Build the 2-of-3 condition (P_S primary, {P_B, P_M} secondary, n_sigs = 2, SIG_INPUTS).
  3. Swap unencumbered → locked proofs at the mint.
  4. 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).
  5. 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).

  1. 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).
  2. Buyer receives the seller's signatures via NIP-59 DM, adds their own P_B signature per proof.
  3. Buyer builds a SwapRequest with both witnesses and submits to the mint (/v1/swap, NUT-11).
  4. Mint issues unencumbered ecash to the buyer; store it in the local wallet.
  5. Buyer notifies Mostro: trade complete.
Cooperative cancel (buyer → seller P2P)
  1. Buyer generates their P_B signature(s), DMs them to the seller via NIP-59.
  2. 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/)

(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.
  • wallet helpers: cashu-balance, cashu-mint <token> (deposit), cashu-send/cashu-receive for funding the local wallet.
  • Cashu paths inside dispute admin commands (adm-settle/adm-cancel) to deliver P_M signatures (daemon side) — on the client, consume CashuPmSignature.

Implementation checklist
  • Bump mostro-core to 0.12.0; wire up the new Action/Payload/error variants in response handling (src/parser, src/util/messaging.rs).
  • Add cdk dependency; build a src/cashu/ module (wallet wrapper: connect, balance, swap-to-locked, sign proof, swap-to-redeem, verify_2of3_condition).
  • 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.

Ngôn ngữ chính
Rust
Star
39
Fork
16
Merge trung bình
11 giờ 7 phút
Pull request đã merge (30 ngày)
7

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của MostroP2P/mostro-cli

Tất cả issue của MostroP2P/mostro-cli

Issue tương tự

Thêm issue về Rust

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.