Implement Cashu 2-of-3 multisig escrow (client-side)
@AndreaDiazCorreia がすでに取り組んでいます。
2026年5月30日 から。
評価
この issue はまだ評価されていません。
説明
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
EscrowBackendtrait + Lightning impl + Cashu stub - mostro#759 — F4
CashuClientwrapper aroundcdk - 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
cdkAPIs 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 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 key P_S).
🔑 Critical constraint:
P_BandP_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)CashuEscrowLocked— Mostro → buyer: token validated & unspent, "send fiat"CashuPmSignature— Mostro → dispute winner: hands over Mostro'sP_Msignatures
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 bysecret. Any signature-exchange handling must be aVec, 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-urlflag /MINT_URLenv, alongside the existingMOSTRO_PUBKEY/RELAYS/POWpattern inget_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_atto theOrderstruct + 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:
- Take unencumbered proofs from the local balance.
- Build the 2-of-3 condition (
P_Sprimary,{P_B, P_M}secondary,n_sigs = 2,SIG_INPUTS). - Swap unencumbered → locked proofs at the mint.
- Serialize the locked token and send
Action::AddCashuEscrowwithPayload::CashuLockProof { token, mint_url, P_B, P_S, P_M }(signed with the order's trade key via the existingsend_dmpath). - Wait for
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_dm gift-wrap, wait_for_dm, get_dm_user).
- Seller, on
Release, signs each escrowed proof and DMs the signatures to the buyer (Vecof per-proof signatures keyed bysecret), then notifies Mostro (informational). - Buyer receives the seller's signatures via NIP-59 DM, adds their own
P_Bsignature per proof. - Buyer builds a
SwapRequestwith 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_Bsignature(s), DMs them to the seller via NIP-59. - Seller combines with their own
P_Ssignature →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: 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.- wallet helpers:
cashu-balance,cashu-mint <token>(deposit),cashu-send/cashu-receivefor funding the local wallet. - Cashu paths inside dispute admin commands (
adm-settle/adm-cancel) to deliverP_Msignatures (daemon side) — on the client, consumeCashuPmSignature.
Implementation checklist
- Bump
mostro-coreto0.12.0; wire up the newAction/Payload/error variants in response handling (src/parser,src/util/messaging.rs). - Add
cdkdependency; build asrc/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
AddCashuEscrowlock flow + handleCashuEscrowLocked. - Track B — buyer release: receive seller sigs over NIP-59, add
P_Bsigs, 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_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.- 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.
- 主要言語
- Rust
- スター
- 39
- フォーク
- 16
- 平均マージ
- 11時間 7分
- マージ済み PR(30日)
- 7
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
MostroP2P/mostro-cli のほかの issue
-
難易度 3/5 半日 初心者へのやさしさ 74/100
MostroP2P/mostro-cli#200 ·
-
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
MostroP2P/mostro-cli#188 ·
-
bug
難易度 3/5 1〜2日 初心者へのやさしさ 56/100
MostroP2P/mostro-cli#187 ·
-
bug enhancement good first issue help wanted
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
MostroP2P/mostro-cli#107 · コメント 1 件 ·
-
enhancement good first issue help wanted
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
MostroP2P/mostro-cli#67 · コメント 10 件 · リアクション 1 件 ·
MostroP2P/mostro-cli の issue をすべて見る
似ている issue
-
難易度 1/5 1〜3時間 初心者へのやさしさ 72/100
MattA-Official/vwmcp#20 ·
-
documentation good first issue
難易度 1/5 1〜3時間 初心者へのやさしさ 88/100
undergroundrap/hatchling#15 ·
-
Progress difficulty filter lists Hard before Medium対応中かも @Pandamachi が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
sysprog21/codetrial#281 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
CLI: --verdict silently ignores extra program/file arguments対応中かも @oxura が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
HigherOrderCO/Bend#1503 ·
-
Add MySQL test coverage for numeric_precision/numeric_scale and seq_in_fk (follow-up to #939)対応中かも @tosinxt が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
TabularisDB/tabularis#977 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信