diff --git a/docs/cashu/03-track-b-release.md b/docs/cashu/03-track-b-release.md new file mode 100644 index 00000000..8baa3cc2 --- /dev/null +++ b/docs/cashu/03-track-b-release.md @@ -0,0 +1,191 @@ +# Cashu Escrow — Track B: Release (Happy Path) + +**Status:** Draft for review · **Target:** `main` (**requires `mostro-core ≥ 0.14.0`**) · +**Depends on:** Fundamentals **CF-1, CF-2, CF-5** + **Track A** (the escrow must be +locked before it can be released) · **Feature flag:** `[cashu].enabled` + +Track B is the **happy-path settlement** of a Cashu trade: the buyer confirms +fiat sent, the seller releases, and the buyer redeems the locked 2-of-3 token +with **two signatures** (its own + the seller's). It is "box 3" of the sequence +diagram in [`../CASHU_ESCROW_ARCHITECTURE.md`](../CASHU_ESCROW_ARCHITECTURE.md). + +This document assumes Fundamentals and Track A are merged. It only adds behaviour +*inside the Cashu branch*; the Lightning path is never changed. + +--- + +## 1. Goal and scope + +### Goal +Complete a locked Cashu trade without Mostro ever touching the funds: +1. The **buyer** signals `FiatSent` (gated by the remaining-locktime guard, §4B). +2. The **seller** signals `Release`, and delivers its **Cashu release signature** + directly to the buyer (P2P NIP-59 DM) so the buyer can build a valid 2-of-3 + `SwapRequest` and redeem the ecash **itself**. +3. Mostro only **advances the order state** (`Active → FiatSent → Success`), + makes the trade **rateable**, and — because the fee was collected at lock + (Track A §4A) — takes **no** settlement action on the funds. + +### In scope +- The Cashu branch of `fiat_sent_action` carrying the **§4B remaining-locktime + guard** (`escrow_settlement_margin_days`, default 3). +- The Cashu branch of `release_action`: the seller marks release, the daemon + advances state and relays/acknowledges the seller's release signature to the + buyer, and the order becomes rateable. +- Unblocking `FiatSent`, `Release`, and `RateUser` in `dispatch_cashu`. + +### Out of scope (other tracks) +- **Cooperative cancel** → Track C. **Dispute resolution** → Track D. +- The **live fee redeem** (Track A TA-1f follow-up) — orthogonal to release. +- Any change to the Lightning release/settle path. + +--- + +## 2. Where Track B sits — flow and state transitions + +```mermaid +sequenceDiagram + participant B as Buyer + participant M as Mostro (cashu mode) + participant S as Seller + participant Mint as Cashu Mint + + Note over B,S: Escrow already locked (Track A) — order is Active + B->>M: FiatSent + M->>M: guard: remaining locktime >= escrow_settlement_margin_days + M->>S: FiatSentOk (buyer paid — release when ready) + S->>B: Release signature (NIP-59 DM, seller signs SIG_INPUTS) + S->>M: Release + M->>M: advance Active/FiatSent -> Success; make rateable + B->>Mint: SwapRequest {2-of-3 proofs, buyer_sig + seller_sig, buyer outputs} + Mint-->>B: fresh ecash (buyer holds the funds) +``` + +**State transitions Track B performs:** +`Active → FiatSent` (buyer), then `FiatSent → Success` (seller release). Both are +Cashu analogues of the Lightning flow, but **no hold invoice is settled** — the +buyer redeems the token itself, off Mostro's servers. + +> **Why the seller's signature goes P2P, not through Mostro.** On the happy path +> Mostro is neither a signer nor a required relay: the seller signs the release +> and hands it to the buyer over a NIP-59 DM using the trade keys, exactly as the +> `release`/`cancel` messages already travel. Mostro only needs the **state** +> update. (Mostro MAY still relay a copy for reliability, but the trust model +> does not depend on it.) + +--- + +## 3. What Track B consumes from Fundamentals + Track A + +| Needs | From | Exact item | +|-------|------|------------| +| Mode gate | CF-1 | `Settings::is_cashu_enabled()`, `escrow_mode()` | +| Locktime floor / margin | CF-1 | `get_cashu().escrow_locktime_days`; **new** `escrow_settlement_margin_days` (default 3, §4B) | +| Locked escrow row | Track A | `Order.{cashu_escrow_token, cashu_escrow_locked_at}` populated, status `Active` | +| Token locktime read | CF-2 | parse the stored token's `locktime` (the guard compares it to now) | +| Dispatch seam | CF-5 | `FiatSent`/`Release`/`RateUser` arms in `dispatch_cashu` | +| Notifications | existing | `enqueue_order_msg`, `update_order_event` | + +Protocol (already on `main`, `mostro-core ≥ 0.14.0`): `Action::{FiatSent, +FiatSentOk, Release, Released, RateUser}` and the existing rating payloads — +**no new protocol variant is required**. The seller's Cashu release signature is +carried in the existing P2P release message shape (trade-key-signed NIP-59 DM); +Mostro validates the *state transition*, not the swap. + +--- + +## 4. The `escrow_settlement_margin_days` guard (§4B — Track B executes it) + +Track A §4B defines the attack: a seller stalls the fiat phase until little +locktime remains, lets the buyer send fiat on day 13 of 15, goes silent, and +reclaims via the refund path on day 15 — keeping both fiat and sats without +failing a single protocol check. **Track B closes it at the `FiatSent` gate.** + +- New `#[serde(default)]` key on `CashuSettings`: **`escrow_settlement_margin_days`, + default 3**. Added by Track B (not needed during foundation). +- In the Cashu branch of `fiat_sent_action`: read the stored escrow token's + `locktime`; reject `FiatSent` with a clear `CantDo` (e.g. + `CashuEscrowNotLocked` if the token/locktime is missing, else a + settlement-window reason) when + `locktime - now < escrow_settlement_margin_days`. Fiat can never be sent inside + the danger window, so the seller cannot weaponise the locktime. + +The margin must be comfortably below the locktime floor (`escrow_locktime_days`, +default 15) so a normal trade has ample settlement room; the difference (≈12 +days) is the usable fiat-settlement window. + +--- + +## 5. Handlers — the Cashu branches + +### 5A · `fiat_sent_action` (Cashu branch) +Same identity/status checks as today (only the **buyer** may send fiat; order +must be `Active`). Then, in Cashu mode only: apply the §4B guard, advance +`Active → FiatSent`, publish the order event, and notify both parties +(`FiatSentOk`) — no LND, no hold-invoice interaction. + +### 5B · `release_action` (Cashu branch) +The submitter must be the **seller** (same identity check as the Lightning +release). In Cashu mode: **do not** settle a hold invoice (there is none); +instead advance `FiatSent → Success`, publish the order event, acknowledge the +seller's release signature to the buyer (relay/ack the P2P DM), and make the +trade rateable (`RateUser` becomes valid). The fee was already collected at lock +(Track A §4A), so release moves **no** funds. + +### 5C · `dispatch_cashu` unblocks +Replace the `InvalidAction` arms for `FiatSent`, `Release`, and `RateUser` +(route through the existing `handle_message_action_no_ln`, whose branches now +carry the Cashu logic). No other action changes. + +--- + +## 6. PR breakdown (atomic, backwards-compatible) + +### TB-1 · `escrow_settlement_margin_days` + `FiatSent` guard +Add the `CashuSettings` key and the Cashu branch of `fiat_sent_action` with the +§4B remaining-locktime guard. Unblock `FiatSent` in `dispatch_cashu`. +*Depends on CF-1, Track A. Conflict surface: `config/*`, `fiat_sent.rs`, +`app.rs` (one dispatch arm).* + +### TB-2 · `release_action` Cashu branch + rating +Add the Cashu branch of `release_action` (advance state, ack the seller +signature, make rateable) and unblock `Release` + `RateUser`. Completes the +happy path end-to-end with TB-1 and Track A. +*Depends on CF-5, Track A, TB-1 (for a full e2e test). Conflict surface: +`release.rs`, `rate_user.rs` (if touched), `app.rs` (two dispatch arms).* + +--- + +## 7. Issues table — sequential vs parallel + +| ID | Title | Depends on | Parallel with | Conflict surface | Risk | +|----|-------|-----------|---------------|------------------|------| +| **TB-1** | `escrow_settlement_margin_days` + `FiatSent` §4B guard | CF-1, Track A | Tracks C/D | `config/*`, `fiat_sent.rs` | Low-Medium | +| **TB-2** | `release_action` Cashu branch + unblock `Release`/`RateUser` | CF-5, Track A, TB-1 | Tracks C/D | `release.rs`, `app.rs` | Medium | + +All of Track B is parallel with Tracks C/D (disjoint handler files); the shared +touch point is the `dispatch_cashu` allow-list, edited one arm at a time. + +--- + +## 8. Definition of Done + +1. A locked Cashu order can be driven `Active → FiatSent → Success` end-to-end + against the CF-3 mint, with the buyer redeeming the 2-of-3 token itself. +2. `FiatSent` is rejected inside the settlement-margin window (§4B), and accepted + outside it; the exact `CantDoReason` is asserted. +3. Every identity/status rejection path returns the correct reason and leaves the + order unchanged (wrong sender, wrong status, guard tripped). +4. The trade becomes rateable only after release; `RateUser` works. +5. With Cashu disabled, behaviour is identical to `main`; existing tests pass + unmodified. `fmt`/`clippy -D warnings`/`test` green. + +--- + +## 9. Cross-track obligations satisfied / raised + +| Obligation | Defined in | Track B does | +|------------|-----------|--------------| +| `FiatSent` rejected when remaining locktime < `escrow_settlement_margin_days` | Track A §4B | **Executed** (TB-1) | +| `RateUser` unblocked once terminal state reachable | CF-5 §6 | **Executed** (TB-2) | +| Buyer locktime warnings as expiry approaches | Track A §4B | Surfaced by TA-3 monitor; TB may add a nudge on `FiatSent` | diff --git a/docs/cashu/04-track-c-coop-cancel.md b/docs/cashu/04-track-c-coop-cancel.md new file mode 100644 index 00000000..90160b7d --- /dev/null +++ b/docs/cashu/04-track-c-coop-cancel.md @@ -0,0 +1,186 @@ +# Cashu Escrow — Track C: Cooperative Cancel + +**Status:** Draft for review · **Target:** `main` (**requires `mostro-core ≥ 0.14.0`**) · +**Depends on:** Fundamentals **CF-1, CF-2, CF-5** + **Track A** (the escrow must be +locked before it can be cancelled) · **Feature flag:** `[cashu].enabled` + +Track C is the **cooperative unwind**: both parties agree to abandon a locked +trade, the buyer hands the seller the signature needed to reclaim the escrow, and +Mostro records the cancellation and **refunds the seller the fee** it collected at +lock. No dispute, no arbitrator signature. + +This document assumes Fundamentals and Track A are merged. It only adds behaviour +*inside the Cashu branch*; the Lightning path is never changed. + +--- + +## 1. Goal and scope + +### Goal +Let a locked Cashu trade be cancelled by mutual consent without Mostro moving the +escrow: +1. Either party requests `Cancel`; when **both** have requested it (the existing + cooperative-cancel handshake), the trade is cancelled. +2. The **buyer** delivers its **Cashu signature** directly to the **seller** (P2P + NIP-59 DM) so the seller can build a 2-of-3 `SwapRequest` + (`buyer_sig + seller_sig`) and **reclaim** the locked ecash itself. +3. Mostro advances the order to `Canceled` and — because the fee was collected at + lock (Track A §4A) — **refunds the seller** the whole Mostro fee + (`2 * order.fee`). + +### In scope +- The Cashu branch of `cancel_action` (cooperative handshake → `Canceled`). +- The **fee-refund obligation** (Track A §4A): mint/send a fresh token of + `2 * order.fee` to the seller's **trade** pubkey on a cancelled-after-lock + order. (This is the first track to execute the refund obligation.) +- Unblocking `Cancel` in `dispatch_cashu`. + +### Out of scope (other tracks) +- **Unilateral / dispute-driven** cancellation → Track D (`admin_cancel`). +- **Release** happy path → Track B. +- The **live fee redeem** mechanics (Track A TA-1f follow-up); the refund shares + the same "Mostro sends ecash" capability and should land with it. + +--- + +## 2. Where Track C sits — flow and state transitions + +```mermaid +sequenceDiagram + participant B as Buyer + participant M as Mostro (cashu mode) + participant S as Seller + participant Mint as Cashu Mint + + Note over B,S: Escrow already locked (Track A) + B->>M: Cancel + S->>M: Cancel + M->>M: both requested -> cooperative cancel + B->>S: Buyer signature (NIP-59 DM) + M->>M: advance -> Canceled + M->>S: Fee refund (fresh token 2*order.fee to P_S) + S->>Mint: SwapRequest {2-of-3 proofs, buyer_sig + seller_sig, seller outputs} + Mint-->>S: reclaimed ecash +``` + +**State transition Track C performs:** the existing cooperative-cancel handshake +drives the order to `Canceled`. No hold invoice is cancelled — the seller +reclaims the token itself with the buyer's signature. + +> **Why the buyer signs for the seller here.** On the happy path the *seller* +> signs so the *buyer* redeems; on a cancel the roles invert — the funds return to +> the seller, so the *buyer* provides the second signature that lets the *seller* +> reclaim. Same 2-of-3 token, opposite redeemer. + +--- + +## 3. What Track C consumes from Fundamentals + Track A + +| Needs | From | Exact item | +|-------|------|------------| +| Mode gate | CF-1 | `Settings::is_cashu_enabled()`, `escrow_mode()` | +| Locked escrow row | Track A | `Order.{cashu_escrow_token, cashu_escrow_locked_at}` populated | +| Fee to refund | Track A §4A | `order.fee` → refund value `2 * order.fee`; `cashu_fee_token`/`cashu_fee_redeemed_at` state | +| Seller trade pubkey | existing | `order.get_seller_pubkey()` (refund recipient) | +| Mostro-sends-ecash | CF-2 (TA-1f follow-up) | mint/send a fresh token to `P_S` | +| Dispatch seam | CF-5 | `Cancel` arm in `dispatch_cashu` | + +Protocol (already on `main`, `mostro-core ≥ 0.14.0`): the existing +cooperative-cancel messages (`Action::Cancel` and its +`CooperativeCancel*`/`Canceled` acknowledgements). The buyer's Cashu signature +travels in the existing P2P cancel message shape; **no new protocol variant is +required**. + +--- + +## 4. The fee refund (Track A §4A obligation — Track C executes it) + +Because the fee is realised at **lock**, not at success, the daemon **owes the +seller a refund** on every non-success path. A cooperative cancel is one such +path. + +- Refund value = **`2 * order.fee`** (the whole Mostro fee), sent as a fresh + token to the seller's **trade** pubkey. +- Idempotency: gate the refund on the fee state so a replayed/duplicate cancel + never double-refunds — e.g. only refund when `cashu_fee_token IS NOT NULL` and + the order has not already been refunded (stamp a `cashu_fee_refunded_at`, or + reuse `cashu_fee_redeemed_at` semantics: a redeemed fee is refunded by minting + fresh ecash back, a not-yet-redeemed fee is simply not collected). The exact + bookkeeping is a TC decision, but it **must** be single-shot. +- If the fee was **never charged** (`mostro.fee == 0`), there is nothing to + refund — the branch is a no-op. + +> **Interaction with the self-service-refund refinement (Track A §4A).** If a +> future revision locks the fee token with `locktime` + `refund = [P_S]`, the +> seller can reclaim the fee unilaterally after locktime and Track C's proactive +> refund becomes a fast-path optimisation rather than the sole recovery route. +> Track C ships the proactive refund against the simple 1-of-1 fee token TA-1f +> defines. + +--- + +## 5. Handler — the Cashu branch of `cancel_action` + +Keep the existing cooperative-cancel handshake (both parties must request +`Cancel`). In Cashu mode, at the point the Lightning path would cancel the hold +invoice: +- **Do not** touch LND (there is no hold invoice). +- Advance the order to `Canceled`, publish the order event. +- Acknowledge/relay the buyer's Cashu signature to the seller so the seller can + reclaim. +- Execute the **fee refund** (§4) once, if a fee was collected. + +`dispatch_cashu` replaces the `InvalidAction` arm for `Cancel` (routing through +the cashu-aware `cancel` handler). A *unilateral* cancel of a locked escrow (no +peer consent) is **not** a Track C concern — that path is a dispute (Track D). + +--- + +## 6. PR breakdown (atomic, backwards-compatible) + +### TC-1 · `cancel_action` Cashu branch + fee refund +Add the Cashu branch to `cancel_action` (cooperative handshake → `Canceled`, no +LND), the single-shot fee refund to `P_S`, and unblock `Cancel` in +`dispatch_cashu`. Unit-tested against the CF-3 mint (both-parties cancel → +`Canceled` + seller can reclaim + fee refunded once; single-party cancel does not +finalise; replay does not double-refund). +*Depends on CF-5, Track A (and TA-1f for the fee state + the Mostro-sends-ecash +capability). Conflict surface: `cancel.rs`, possibly `db.rs` (refund bookkeeping, +additive) + `migrations/` if a `cashu_fee_refunded_at` column is chosen, +`app.rs` (one dispatch arm).* + +--- + +## 7. Issues table — sequential vs parallel + +| ID | Title | Depends on | Parallel with | Conflict surface | Risk | +|----|-------|-----------|---------------|------------------|------| +| **TC-1** | `cancel_action` Cashu branch + fee refund + unblock `Cancel` | CF-5, Track A, TA-1f | Tracks B/D | `cancel.rs`, `app.rs`, (opt.) `migrations/` | Medium (funds return + refund) | + +Track C is parallel with Tracks B/D; the only shared touch point is the +`dispatch_cashu` `Cancel` arm. + +--- + +## 8. Definition of Done + +1. A locked Cashu order, cancelled cooperatively by both parties, reaches + `Canceled`, the seller can reclaim the escrow with the buyer's signature, and + the seller is refunded `2 * order.fee` — verified end-to-end against the CF-3 + mint. +2. A single-party `Cancel` does **not** finalise the cancellation (the handshake + still requires both). +3. The fee refund is **single-shot**: a replayed/duplicate cancel never + double-refunds; a fee-free order refunds nothing. +4. With Cashu disabled, behaviour is identical to `main`; existing tests pass + unmodified. `fmt`/`clippy -D warnings`/`test` green. + +--- + +## 9. Cross-track obligations satisfied / raised + +| Obligation | Defined in | Track C does | +|------------|-----------|--------------| +| Fee refund on non-success (coop cancel after lock), `2 * order.fee` to `P_S` | Track A §4A / §10 | **Executed** (TC-1) | +| Single-shot refund bookkeeping | Track A §4A | **Executed** (idempotent refund) | +| Mostro-sends-ecash capability | Track A §4A (TA-1f) | **Shared** with the fee redeem; lands together | diff --git a/docs/cashu/05-track-d-dispute.md b/docs/cashu/05-track-d-dispute.md new file mode 100644 index 00000000..68642da5 --- /dev/null +++ b/docs/cashu/05-track-d-dispute.md @@ -0,0 +1,225 @@ +# Cashu Escrow — Track D: Dispute Resolution (`P_M` signs) + +**Status:** Draft for review · **Target:** `main` (**requires `mostro-core ≥ 0.14.0`**) · +**Depends on:** Fundamentals **CF-1, CF-2, CF-5** + **Track A** (the escrow must be +locked before it can be disputed) · **Feature flag:** `[cashu].enabled` + +Track D is the **only** track where Mostro's arbitrator key `P_M` produces a +signature. When a locked Cashu trade cannot be resolved cooperatively, a solver +decides the outcome and Mostro hands the **winner** its `P_M` signature so the +winner can complete a 2-of-3 swap and take the funds. This closes the trust model: +the 2-of-3 exists precisely so that a single honest arbitrator can break a +buyer↔seller deadlock without ever holding the funds. + +This document assumes Fundamentals and Track A are merged. It only adds behaviour +*inside the Cashu branch*; the Lightning path is never changed. + +--- + +## 1. Goal and scope + +### Goal +Resolve a disputed locked Cashu trade by arbitrator signature: +1. A party opens a `Dispute`; a solver takes it (`AdminTakeDispute`). +2. The solver rules for one side: + - **`AdminSettle`** (buyer wins) → Mostro signs with `P_M` and delivers the + signature to the **buyer**, who redeems with `P_M + P_B`. + - **`AdminCancel`** (seller wins) → Mostro signs with `P_M` and delivers the + signature to the **seller**, who reclaims with `P_M + P_S`, and Mostro + **refunds the seller the fee** (Track A §4A). +3. Mostro advances the order to its terminal dispute state and makes the outcome + auditable. + +### In scope +- `sign_with_pm` — the `P_M` signing primitive (NUT-11 P2PK) in `CashuClient`. +- The Cashu branches of `dispute_action`, `admin_take_dispute_action`, + `admin_settle_action`, `admin_cancel_action`, delivering `CashuPmSignature`. +- Unblocking `Dispute`, `AdminTakeDispute`, `AdminSettle`, `AdminCancel`, + `AdminAddSolver` in `dispatch_cashu`. +- The **dispute-near-locktime solver alert** (Track A §4B). +- The **fee refund** on a seller-wins resolution (Track A §4A). + +### Out of scope (other tracks) +- Happy-path **release** → Track B. **Cooperative cancel** → Track C. +- Bond-related admin flows — bonds are mutually exclusive with Cashu (CF-1 §4.5), + so `AddBondInvoice` stays permanently `InvalidAction`. + +--- + +## 2. Where Track D sits — flow and state transitions + +```mermaid +sequenceDiagram + participant P as Party (buyer or seller) + participant M as Mostro (cashu mode) + participant Solver as Solver + participant W as Winner + participant Mint as Cashu Mint + + Note over P,M: Escrow locked (Track A); trade stalled + P->>M: Dispute + Solver->>M: AdminTakeDispute + Note over M,Solver: solver alerted if remaining locktime < resolution SLA (§4B) + Solver->>M: AdminSettle (buyer wins) | AdminCancel (seller wins) + M->>M: sign_with_pm(escrow proofs) + M->>W: CashuPmSignature { per-proof {secret, signature} } + W->>Mint: SwapRequest {2-of-3 proofs, P_M sig + own sig, own outputs} + Mint-->>W: funds to the winner + Note over M: if seller wins, refund 2*order.fee to P_S (§4A) +``` + +**State transitions Track D performs:** `Dispute` opens the dispute +(`Active/FiatSent → Dispute`); `AdminSettle`/`AdminCancel` drive it to the +terminal `SettledByAdmin`/`CanceledByAdmin` (or the existing dispute-terminal +statuses), mirroring the Lightning admin flow but with **no hold-invoice +settle/cancel** — Mostro emits a signature instead. + +--- + +## 3. What Track D consumes from Fundamentals + Track A + +| Needs | From | Exact item | +|-------|------|------------| +| Mode gate | CF-1 | `Settings::is_cashu_enabled()` | +| Locked escrow row | Track A | `Order.{cashu_escrow_token}` populated | +| `P_M` signing | CF-2 (this track adds it) | `CashuClient::sign_with_pm(token \| proofs) -> Vec` | +| Winner pubkey | existing | `order.get_buyer_pubkey()` / `get_seller_pubkey()` | +| Fee refund (seller wins) | Track A §4A | `2 * order.fee` to `P_S`; shared with Track C's refund | +| Solver management | existing | `admin_add_solver`, `admin_take_dispute` | +| Dispatch seam | CF-5 | `Dispute`/`AdminTakeDispute`/`AdminSettle`/`AdminCancel`/`AdminAddSolver` arms | + +Protocol (already on `main`, `mostro-core ≥ 0.14.0`, frozen): +- `Action::CashuPmSignature` (Mostro → winner) carrying + `Payload::CashuSignatures(Vec)`, where + `CashuProofSignature = { secret, signature }` — **one entry per escrow proof**. +- `Action::{Dispute, AdminTakeDispute, AdminSettle, AdminCancel, AdminAddSolver}` + and the dispute payloads. +- `CantDoReason::CashuEscrowNotLocked` (settle/cancel a never-locked escrow), + plus the dispute-status reasons. + +**No new protocol variant is required** — the `CashuPmSignature` / +`CashuSignatures` surface was landed in the 0.13.0 baseline exactly for this +track. The only new **daemon-side** capability is `sign_with_pm` in `CashuClient`. + +--- + +## 4. `sign_with_pm` — the arbitrator signing primitive (CF-2 surface) + +The one place the daemon uses its `P_M` key on funds-bearing material: + +```rust +/// Produce Mostro's NUT-11 P2PK signature over every proof of the escrow +/// token, so the dispute winner can assemble a 2-of-3 SwapRequest +/// (P_M + winner). Returns one {secret, signature} per proof. +fn sign_with_pm(token: &Token, p_m_secret: &SecretKey) + -> Result, Error>; +``` + +- It signs **only** the escrow token's proofs, and only when a solver has ruled — + never on the happy path, never unilaterally. The non-custodial guarantee holds: + Mostro's one signature is worthless without the winner's second signature. +- The signature is delivered to the winner in `CashuPmSignature`; the winner (not + Mostro) chooses the swap outputs and submits to the mint. + +--- + +## 5. Handlers — the Cashu branches + +### 5A · `dispute_action` (Cashu branch) +Same identity/status rules as today (a party to a locked/active order opens the +dispute). In Cashu mode: advance to `Dispute`, no LND. + +### 5B · `admin_take_dispute_action` (Cashu branch) +Assign the solver as today; additionally, **alert the solver** when the escrow's +remaining locktime is below the resolution SLA (§4B) — a late `P_M` signature is +worthless once the seller can reclaim via the refund path. Log + priority-flag. + +### 5C · `admin_settle_action` (buyer wins) / `admin_cancel_action` (seller wins) +In Cashu mode, replace the hold-invoice settle/cancel with: +- `sign_with_pm(escrow_token)` → `CashuPmSignature` to the winner (buyer for + settle, seller for cancel). +- Advance to the terminal dispute status, publish the order event. +- **`admin_cancel` (seller wins) additionally refunds the fee** (`2 * order.fee` + to `P_S`, §4A) — single-shot, shared with Track C's refund bookkeeping. +- Map a settle/cancel against a never-locked escrow to + `CantDo(CashuEscrowNotLocked)`. + +### 5D · `dispatch_cashu` unblocks +Replace the `InvalidAction` arms for `Dispute`, `AdminTakeDispute`, +`AdminSettle`, `AdminCancel`, and route `AdminAddSolver` to +`handle_message_action_no_ln` (solver management touches no escrow/LND). + +--- + +## 6. PR breakdown (atomic, backwards-compatible) + +### TD-1 · `sign_with_pm` + `CashuClient` surface +Add `sign_with_pm` (NUT-11 P2PK) to `CashuClient`, unit-tested against the CF-3 +mint (a `P_M`-signed proof + a winner signature satisfies the 2-of-3; a wrong key +does not). Pure library; no daemon wiring. +*Depends on CF-2. Conflict surface: `cashu/mod.rs` (additive). Parallel with all.* + +### TD-2 · `dispute` + `admin_take_dispute` Cashu branches + solver alert +Cashu branches for opening and taking a dispute, plus the §4B near-locktime +solver alert. Unblock `Dispute`, `AdminTakeDispute`, `AdminAddSolver`. +*Depends on CF-5, Track A. Conflict surface: `dispute.rs`, +`admin_take_dispute.rs`, `admin_add_solver.rs` (if touched), `app.rs`.* + +### TD-3 · `admin_settle` / `admin_cancel` Cashu branches + `P_M` delivery + fee refund +Deliver `CashuPmSignature` to the winner; seller-wins path refunds the fee. +Unblock `AdminSettle`, `AdminCancel`. Completes dispute resolution end-to-end. +*Depends on TD-1, TD-2, Track A (+ TA-1f/Track C for the shared refund). +Conflict surface: `admin_settle.rs`, `admin_cancel.rs`, `app.rs`.* + +--- + +## 7. Issues table — sequential vs parallel + +| ID | Title | Depends on | Parallel with | Conflict surface | Risk | +|----|-------|-----------|---------------|------------------|------| +| **TD-1** | `sign_with_pm` + CF-2 surface | CF-2 | everything | `cashu/mod.rs` | Medium (crypto) | +| **TD-2** | `dispute`/`admin_take_dispute` Cashu + solver alert | CF-5, Track A | Tracks B/C | `dispute.rs`, `admin_take_dispute.rs`, `app.rs` | Medium | +| **TD-3** | `admin_settle`/`admin_cancel` + `P_M` delivery + fee refund | TD-1, TD-2, Track A | Tracks B/C | `admin_settle.rs`, `admin_cancel.rs`, `app.rs` | Medium-High (funds + revenue) | + +**Sequencing:** TD-1 (library) can land first and in parallel with everything; +TD-2 and TD-3 are the daemon wiring, TD-3 last (it needs the signing primitive and +the dispute-open path). All of Track D is parallel with Tracks B/C. + +--- + +## 8. Definition of Done + +1. A disputed locked Cashu order can be resolved either way against the CF-3 mint: + `AdminSettle` delivers a `P_M` signature the **buyer** uses to redeem; + `AdminCancel` delivers a `P_M` signature the **seller** uses to reclaim. +2. Mostro's `P_M` signature is produced **only** during dispute resolution, is + worthless alone (the winner must add its own signature), and is delivered via + `CashuPmSignature`. +3. A seller-wins resolution refunds `2 * order.fee` to `P_S`, single-shot. +4. The solver is alerted when remaining locktime is below the resolution SLA + (§4B). A settle/cancel against a never-locked escrow returns + `CashuEscrowNotLocked`. +5. With Cashu disabled, behaviour is identical to `main`; existing tests pass + unmodified. `fmt`/`clippy -D warnings`/`test` green. + +--- + +## 9. Cross-track obligations satisfied / raised + +| Obligation | Defined in | Track D does | +|------------|-----------|--------------| +| Dispute-near-locktime solver alert | Track A §4B | **Executed** (TD-2) | +| Fee refund on dispute-resolved-for-seller | Track A §4A / §10 | **Executed** (TD-3, shared with Track C) | +| Every blocked admin/dispute action has an owner | CF-5 §6 matrix | **Executed** (TD-2/TD-3 unblock all dispute actions; `AddBondInvoice` stays permanently blocked) | + +--- + +## 10. After Track D — the feature is complete + +With Tracks A–D merged, a `[cashu] enabled = true` node can run a full trade +lifecycle — create, take, lock, release, cooperatively cancel, and resolve +disputes — entirely on ecash, with Mostro as a non-custodial coordinator that +signs only to arbitrate. The remaining open items are the two scoped follow-ups +Track A raised (the live fee-token redeem / ecash revenue store, and the +self-service-refund locktime refinement), neither of which blocks a functioning +Cashu marketplace. diff --git a/docs/cashu/README.md b/docs/cashu/README.md index 2e6af828..0927f50d 100644 --- a/docs/cashu/README.md +++ b/docs/cashu/README.md @@ -30,9 +30,9 @@ untouched while the feature is off, and lets several developers work in parallel | 00 | [`../CASHU_ESCROW_ARCHITECTURE.md`](../CASHU_ESCROW_ARCHITECTURE.md) | Architecture, motivation, crypto model, trust model | Reference | | 01 | [`01-fundamentals.md`](./01-fundamentals.md) | **Foundation milestone** — config, mint client, DB helpers, test harness, boot wiring | **Draft (this PR)** | | 02 | [`02-track-a-lock.md`](./02-track-a-lock.md) | Escrow lock / setup (`AddCashuEscrow`) | **Draft** | -| 03 | `03-track-b-release.md` | Release happy path | Planned | -| 04 | `04-track-c-coop-cancel.md` | Cooperative cancel | Planned | -| 05 | `05-track-d-dispute.md` | Dispute resolution (`P_M` signs) | Planned | +| 03 | [`03-track-b-release.md`](./03-track-b-release.md) | Release happy path (`FiatSent` + `Release`) | **Draft** | +| 04 | [`04-track-c-coop-cancel.md`](./04-track-c-coop-cancel.md) | Cooperative cancel + fee refund | **Draft** | +| 05 | [`05-track-d-dispute.md`](./05-track-d-dispute.md) | Dispute resolution (`P_M` signs) | **Draft** | The **fundamentals** document (01) is the only one that touches shared, conflict-prone files. Once it has merged, the feature tracks (02–05) edit