From 5320436b146b33908d79f9adcc44b9d61becffef Mon Sep 17 00:00:00 2001 From: grunch Date: Tue, 16 Jun 2026 09:28:34 -0300 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20document=20anti-abuse=20bond=20impl?= =?UTF-8?q?ementation=20(Phases=201=E2=80=937)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover all protocol changes introduced by the anti-abuse bond PRs (#712–#777) that were not yet reflected in the protocol docs: ### New page - `bond_slashed.md`: documents the `bond-slashed` action (Phase 4/7). Sent to the bonded party when a waiting-state timeout elapses and `slash_on_waiting_timeout = true`. Distinguishes timeout-slash from dispute-slash path; explains order cancel vs. republish depending on which party was responsible; notes that cancel-before-timeout never triggers this. ### Extended pages - `pay_bond_invoice.md`: restructured to cover both taker bond (Phase 1.5, existing) and maker bond (Phase 5/6, new). Maker bond: trigger is `new-order` response; order not published until bond HTLC accepts; `waiting-maker-bond` DM status; range orders sized against `max_amount`. - `add_bond_invoice.md`: adds Phase 3.5 payout confirmations — `bond-invoice-accepted` (Mostro acknowledges receipt of payout bolt11) and `bond-payout-completed` (payment settled). Notes re-prompt on `send_payment` exhaustion and claim window. - `cancel.md`: fixes incorrect "supersede mechanism" wording (replaced by concurrent bonds / first-to-lock-wins model since Phase 1.5). Adds two new sections — taker self-cancel during `waiting-taker-bond` (releases only sender's bond; order stays in `waiting-taker-bond` if others remain) and maker cancel during `waiting-taker-bond` (releases all concurrent bonds). ### Order creation pages - `new_sell_order.md`, `new_buy_order.md`: "Optional: anti-abuse maker bond" section explaining that `pay-bond-invoice` precedes `new-order` confirmation when `apply_to ∈ {make, both}`. - `new_sell_range_order.md`: same section, noting bond sized against `max_amount` and proportional per-slice accounting. ### Message suggestions - `message_suggestions_for_actions.md`: adds `bond-invoice-accepted`, `bond-payout-completed`, and `bond-slashed` suggested texts. ### Navigation - `SUMMARY.md`: adds `bond_slashed.md` link after `add_bond_invoice.md`. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/SUMMARY.md | 1 + src/add_bond_invoice.md | 67 +++++++++++++++++ src/bond_slashed.md | 61 +++++++++++++++ src/cancel.md | 21 +++++- src/message_suggestions_for_actions.md | 9 +++ src/new_buy_order.md | 10 +++ src/new_sell_order.md | 10 +++ src/new_sell_range_order.md | 8 ++ src/pay_bond_invoice.md | 100 ++++++++++++++++++++++--- 9 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 src/bond_slashed.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index fd2041b..9c882bc 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -18,6 +18,7 @@ - [Take range buy order](./take_buy_range_order.md) - [Pay bond invoice](./pay_bond_invoice.md) - [Bond payout invoice](./add_bond_invoice.md) +- [Bond slashed notification](./bond_slashed.md) - [Seller pays hold invoice](./seller_pay_hold_invoice.md) - [Fiat sent](./fiatsent.md) - [Release](./release.md) diff --git a/src/add_bond_invoice.md b/src/add_bond_invoice.md index 07eb696..c0f25f5 100644 --- a/src/add_bond_invoice.md +++ b/src/add_bond_invoice.md @@ -99,11 +99,78 @@ The recipient of the request message is the non-slashed counterparty of the trad - **Treat re-deliveries as idempotent reminders.** The same outstanding request is being repeated; do not re-prompt the user for a fresh invoice on every retry. `slashed_at` is re-emitted unchanged, so the deadline the client shows must not shift across retries. - **Sign the reply with the non-slashed side's trade key.** See [Keys management](./key_management.md). +## Payout confirmations (Phase 3.5) + +After the counterparty submits their payout invoice, Mostro acknowledges receipt and later confirms payment with two dedicated actions. + +### `bond-invoice-accepted` + +Sent by Mostro to the counterparty immediately after successfully receiving and validating their payout bolt11. This confirms Mostro has the invoice on file and is attempting the Lightning payment. + +```json +[ + { + "order": { + "version": 1, + "id": "", + "action": "bond-invoice-accepted", + "payload": { + "order": { + "id": "", + "kind": "sell", + "amount": 500, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1 + } + } + } + }, + null +] +``` + +Clients should surface this as a reassurance: *"Your bond payout invoice has been received and the payment is being processed."* + +### `bond-payout-completed` + +Sent by Mostro to the counterparty when the Lightning payment to their invoice has been confirmed as settled. The `amount` in the payload reflects the amount paid. + +```json +[ + { + "order": { + "version": 1, + "id": "", + "action": "bond-payout-completed", + "payload": { + "order": { + "id": "", + "kind": "sell", + "amount": 500, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1 + } + } + } + }, + null +] +``` + +Clients should surface this as a terminal success: *"Your bond payout of `amount` Sats has been completed!"* + +Both actions carry `Payload::Order` (the `SmallOrder` context) and are serde-additive: a client that does not recognise them ignores the message and falls back to the `add-bond-invoice` reply it already handles — no funds at risk. + ## Failure modes - The counterparty never replies before the deadline → no Lightning payment arrives and the share is forfeited; the slashed funds remain with the node. - A reply arrives after the deadline, or from a sender other than the resolved recipient, or after another reply already won the race → Mostro responds with a `cant-do` action carrying reason `not-allowed-by-status`. - The bolt11 principal does not match the requested counterparty share, or the invoice is otherwise undecodable / expired → Mostro responds with `cant-do` reason `invalid-invoice`. - On a node where the operator retains 100% of slashed bonds, **no `add-bond-invoice` message is emitted at all**. Clients should not surface a phantom payout request. +- If `send_payment` retries are exhausted (see `payout_max_retries` in the info event) the counterparty is re-prompted with a fresh `add-bond-invoice` request, provided the claim window has not yet elapsed. If the window has passed, the share is forfeited and no further messages are sent. On success, the counterparty receives their share as a Lightning payment from the node's wallet. The routing fee is paid separately from that wallet and is not deducted from the principal. diff --git a/src/bond_slashed.md b/src/bond_slashed.md new file mode 100644 index 0000000..b9f21f0 --- /dev/null +++ b/src/bond_slashed.md @@ -0,0 +1,61 @@ +# Bond slashed notification + +The `bond-slashed` action is a notification Mostro sends to a bonded party when their anti-abuse bond has been **settled due to a waiting-state timeout**. It is a forfeiture notice: the bond HTLC has already been claimed into Mostro's wallet by the time this message is sent. + +> **Scope.** This action is only emitted on the **timeout slash** path (scheduler-driven, gated by `slash_on_waiting_timeout = true` in the Mostro info event). It is **not** sent on the dispute-slash path — when a solver slashes a bond via [`admin-settle`](./admin_settle_order.md) or [`admin-cancel`](./admin_cancel_order.md), the slashed party receives the `admin-settled` / `admin-canceled` confirmation instead. + +## Direction and trigger + +- **Direction:** Mostro → the party whose bond was slashed. +- **Trigger:** The waiting-state timeout elapsed while the responsible party had not performed their expected trade action (e.g. the seller never paid the hold invoice while in `waiting-payment`, or the buyer never submitted an invoice in `waiting-buyer-invoice`), and the operator has configured `slash_on_waiting_timeout = true`. +- The `amount` in the payload is the **slashed bond amount in satoshis** — not the trade amount. + +## Wire format + +```json +[ + { + "order": { + "version": 1, + "id": "", + "action": "bond-slashed", + "payload": { + "order": { + "id": "", + "kind": "sell", + "status": "canceled", + "amount": 785, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": 1698937797 + } + } + } + }, + null +] +``` + +The `amount` field in the embedded `SmallOrder` is the **slashed bond amount** (not the original trade amount). For a range-order maker bond, this is the proportional slice amount for the taken sub-order. + +## What happens next + +After `bond-slashed` is sent to the responsible party, Mostro also: + +1. **Cancels or republishes the order** depending on which party was responsible: + - **Maker responsible** (e.g. maker-as-seller never paid the hold invoice): the order is **cancelled** (since the maker cannot be trusted to fulfil it). The maker receives the `bond-slashed` notice followed by a `canceled` confirmation. + - **Taker responsible** (e.g. taker-as-buyer never submitted their invoice): the order is **republished** to the book as `pending` so the maker can be matched again. The taker receives `bond-slashed` followed by `canceled`. The maker's bond (if any) remains `Locked`. + +2. **Asks the winning counterparty for a payout invoice** by sending them an [`add-bond-invoice`](./add_bond_invoice.md) message for their share of the slashed bond (governed by `bond_slash_node_share_pct` — the node retains its share, the rest goes to the counterparty). + +## Expected client behaviour + +- Surface `bond-slashed` to the user as an explicit forfeiture notice, not as a generic cancellation. Suggested wording: *"Your bond of `amount` Sats has been forfeited due to a waiting-state timeout."* +- This action is distinct from `canceled` — clients should not suppress it or merge it into the cancel flow. +- A `canceled` message will typically follow for the order itself; clients should handle both. + +## Note on cancels vs. slashes + +A cancel sent by either party **before** the timeout elapses never triggers `bond-slashed`. Bonds are always released (never slashed) on user-initiated cancels. Only the automated scheduler-driven timeout slash path emits this action, and only when `slash_on_waiting_timeout = true` is set by the operator. diff --git a/src/cancel.md b/src/cancel.md index d303836..c039915 100644 --- a/src/cancel.md +++ b/src/cancel.md @@ -68,7 +68,26 @@ Mostro updates the addressable event with `d` tag `` to change the sta ## Bond race during take -A client that sent `take-buy` / `take-sell` and is waiting for [`pay-bond-invoice`](./pay_bond_invoice.md) may receive `Action::Canceled` instead — meaning another user paid their bond first on the same order (whichever bond locks first wins; see [pay bond invoice](./pay_bond_invoice.md)). Surface this clearly to the user, e.g. *"Order was taken by another user before you locked the bond."* Do not silently retry the take — the order may not be available anymore, and the supersede mechanism on the daemon side has already discarded the prior bond request. +Multiple takers may simultaneously attempt to take the same order: each creates their own bond hold invoice (a `Requested` bond row) and whichever HTLC is accepted first by LND wins. When a bond locks, all other concurrent `Requested` bonds on the same order are released and their takers each receive `Action::Canceled`. + +A client that sent `take-buy` / `take-sell` and is waiting for [`pay-bond-invoice`](./pay_bond_invoice.md) may therefore receive `canceled` instead — meaning another taker's bond locked first. Surface this clearly to the user, e.g. *"Order was taken by another user before you locked the bond."* Do not silently retry the take — the order may no longer be available. + +## Cancel during `waiting-taker-bond` + +An order remains in the daemon-internal `waiting-taker-bond` state (NIP-33 `s` tag still `pending`) while one or more taker bonds are outstanding. Cancels during this window are routed differently from cancels on `pending` orders. + +### Taker self-cancel + +A taker who has a `Requested` (unpaid) bond on an order may cancel it by sending `cancel`. Mostro releases **only that taker's bond** (LND invoice cancelled, funds returned). Other concurrent takers' bonds are unaffected and continue racing. + +- If the cancelling taker was the only prospective taker, the order drops back to `pending` and is visible on the order book again. +- If other takers' bonds are still `Requested`, the order remains in `waiting-taker-bond` with those bonds still racing. + +The maker is not notified; the NIP-33 order event stays `pending` throughout. + +### Maker cancel + +The maker can cancel the order at any point before trade flow starts (i.e. while in `pending` or `waiting-taker-bond`). Sending `cancel` releases **all** concurrent taker bonds on the order, notifies each prospective taker with `canceled`, transitions the order to `canceled`, and publishes the updated NIP-33 event. ## Cancel cooperatively diff --git a/src/message_suggestions_for_actions.md b/src/message_suggestions_for_actions.md index 8a601ac..7bbeab9 100644 --- a/src/message_suggestions_for_actions.md +++ b/src/message_suggestions_for_actions.md @@ -19,6 +19,15 @@ Below are suggestions for messages that clients can show to users when receiving - **add-bond-invoice:** Please send me a Lightning invoice for `amount` Sats — this is your share of a slashed bond on order `id`. You have until `deadline` to submit it, or your share will be forfeited and the entire bond will be retained by the node. +- **bond-invoice-accepted:** + Your bond payout invoice has been received. The payment is being processed — you'll be notified once it completes. + +- **bond-payout-completed:** + Your bond payout of `amount` Sats has been sent successfully. The funds should arrive in your Lightning wallet shortly. + +- **bond-slashed:** + Your anti-abuse bond of `amount` Sats has been forfeited. This happened because the waiting-state timeout elapsed before you completed your required action on order `id`. The bond has been settled into the node's wallet. + - **add-invoice:** Please send me an invoice for `amount` satoshis equivalent to `fiat_code` `fiat_amount`. This is where I will send the funds upon trade completion. If you don’t provide the invoice within `expiration_seconds`, the trade will be canceled. diff --git a/src/new_buy_order.md b/src/new_buy_order.md index 9b7bd4d..0633c78 100644 --- a/src/new_buy_order.md +++ b/src/new_buy_order.md @@ -38,6 +38,16 @@ The nostr event will look like this: } ``` +## Optional: anti-abuse maker bond + +When the Mostro node has bonds enabled and `apply_to` is `"make"` or `"both"`, the maker must lock a bond **before** the order is published. Instead of a `new-order` confirmation, Mostro first responds with a [`pay-bond-invoice`](./pay_bond_invoice.md#maker-bond) message asking the maker to pay a small hold invoice (typically ~1% of the trade amount). The order is **not visible on Nostr** until the bond HTLC is accepted. + +Only after the maker pays the bond does Mostro: +1. Publish the order to Nostr with status `pending`. +2. Send the `new-order` confirmation (shown below). + +If the maker never pays the bond invoice it expires and no order is created. See [Pay bond invoice — Maker bond](./pay_bond_invoice.md#maker-bond) for details. + ## Confirmation message Mostro will send back a nip59 event as a confirmation message, the message in the rumor looks like the following: diff --git a/src/new_sell_order.md b/src/new_sell_order.md index 07e7db9..cf89341 100644 --- a/src/new_sell_order.md +++ b/src/new_sell_order.md @@ -47,6 +47,16 @@ The event to send to Mostro would look like this: } ``` +## Optional: anti-abuse maker bond + +When the Mostro node has bonds enabled and `apply_to` is `"make"` or `"both"`, the maker must lock a bond **before** the order is published. Instead of a `new-order` confirmation, Mostro first responds with a [`pay-bond-invoice`](./pay_bond_invoice.md#maker-bond) message asking the maker to pay a small hold invoice (typically ~1% of the trade amount). The order is **not visible on Nostr** until the bond HTLC is accepted. + +Only after the maker pays the bond does Mostro: +1. Publish the order to Nostr with status `pending`. +2. Send the `new-order` confirmation (shown below). + +If the maker never pays the bond invoice it expires and no order is created. See [Pay bond invoice — Maker bond](./pay_bond_invoice.md#maker-bond) for details. + ## Confirmation message Mostro will send back a nip59 event as a confirmation message to the user like the following (unencrypted rumor's content example): diff --git a/src/new_sell_range_order.md b/src/new_sell_range_order.md index 979931c..c221718 100644 --- a/src/new_sell_range_order.md +++ b/src/new_sell_range_order.md @@ -30,6 +30,14 @@ Here we have two new fields, `min_amount` and `max_amount`, to define the range When a taker takes the order, the amount will be set on the message. +## Optional: anti-abuse maker bond + +When the Mostro node has bonds enabled and `apply_to` is `"make"` or `"both"`, the maker of a range order must lock a bond **before** the order is published — same as non-range orders, with one key difference: **the bond is sized against `max_amount`**, not the minimum. + +Mostro responds to the `new-order` with a [`pay-bond-invoice`](./pay_bond_invoice.md#maker-bond) instead of the confirmation below. The order is not published to Nostr until the bond HTLC is accepted. + +When a taker takes a slice of the range order, the bond obligation is reduced proportionally (the slice's fiat amount relative to `max_amount`). The bond hold invoice remains `Locked` for the lifetime of the range order and is settled once in full when the range closes; partial slashes are tracked as child bond rows rather than early partial settlements. See [Pay bond invoice — Maker bond](./pay_bond_invoice.md#maker-bond) for details. + ## Confirmation message Mostro will send back a nip59 event as a confirmation message to the user like the following: diff --git a/src/pay_bond_invoice.md b/src/pay_bond_invoice.md index 303d355..1faf2a1 100644 --- a/src/pay_bond_invoice.md +++ b/src/pay_bond_invoice.md @@ -1,16 +1,20 @@ # Pay bond invoice -When the receiving Mostro node has the anti-abuse bond feature enabled, takers must lock a small Lightning hold invoice as a security deposit before the trade flow proceeds. Mostro asks for this payment with the `pay-bond-invoice` action. +When the receiving Mostro node has the anti-abuse bond feature enabled, makers and/or takers must lock a small Lightning hold invoice as a security deposit before the trade flow proceeds. Mostro asks for this payment with the `pay-bond-invoice` action. The bond is **independent from the trade escrow**: it is a second hold invoice with its own payment hash, and it is **released on every normal exit path** — completion, cancel before timeout, or dispute resolution where the solver does not direct otherwise. Bonds are typically around **1% of the trade amount** (with an operator-configured floor), much smaller than the trade hold invoice that may follow. -## Direction and trigger +The `bond_apply_to` tag in the Mostro info event (kind 38385) tells clients which sides must post a bond: `"take"`, `"make"`, or `"both"` — see [Other events published by Mostro](./other_events.md#anti-abuse-bond-policy-tags). + +## Taker bond + +### Direction and trigger - **Direction:** Mostro → user (the taker). -- **Trigger:** Sent immediately after a successful `take-buy` / `take-sell` when the operator has bonds enabled. +- **Trigger:** Sent immediately after a successful `take-buy` / `take-sell` when the operator has bonds enabled for takers. - **Order status:** The published NIP-33 order event keeps the `s` tag at `pending` while the bond is outstanding (per NIP-69's four-bucket model — see [Peer-to-peer Order events. NIP-69](./order_event.md)). The DM payload's embedded `SmallOrder` echo carries the daemon-internal status `waiting-taker-bond` so the recipient client can render the bond-payment phase distinctly, but external observers (other potential takers, order-book aggregators) continue to see the order as `pending` and may still attempt to take it. This is deliberate: a taker who never pays the bond cannot park the order off the book. -## Mostro message to the taker +### Mostro message to the taker The rumor's content has the same shape as `pay-invoice`; only the action discriminator differs: @@ -45,13 +49,13 @@ The rumor's content has the same shape as `pay-invoice`; only the action discrim > Note: the `status` value `"waiting-taker-bond"` here is the daemon-internal state echoed in the DM payload. The corresponding NIP-33 addressable order event's `s` tag is still `pending`. -## Expected client behaviour +### Expected client behaviour - Decode the bolt11 and surface it to the user **explicitly as a bond**, not as the trade escrow. Do not reuse the same wording or UI step that you use for `pay-invoice`. - Pay it. The HTLC enters `Accepted` state — the sats are locked in the taker's wallet, not captured. - Do not collapse this into the same UI step as any subsequent `pay-invoice` on the same order. They are independent HTLCs (different payment hashes), and the user must explicitly approve each. -## Follow-up flow +### Follow-up flow Once the bond HTLC is `Accepted`, Mostro proceeds with the normal trade flow: @@ -60,7 +64,7 @@ Once the bond HTLC is `Accepted`, Mostro proceeds with the normal trade flow: > **Important — buy order taken (seller-as-taker):** this is the only flow on which a single user pays **two hold invoices in sequence on the same order** — first the bond (`pay-bond-invoice`), then the trade escrow (`pay-invoice`). They arrive as distinct actions and must be presented to the user as separate steps. Do not auto-pay either, do not coalesce them, and make the distinction obvious in the UI; this is the most error-prone path for client developers. -## Daemon status `waiting-taker-bond` (DM payloads only) +### Daemon status `waiting-taker-bond` (DM payloads only) In addition to the NIP-69 wire status (`pending` while the bond is outstanding), Mostro tags the order's internal state as `waiting-taker-bond` so it can route subsequent messages correctly. Clients see this value in the `SmallOrder` echo embedded in `pay-bond-invoice` payloads and may use it to drive UI ("Waiting for bond payment"). It does **not** appear on the addressable NIP-33 order event — that one continues to advertise the order as `pending`. @@ -70,17 +74,89 @@ Internal transitions (visible only in DM payload echoes): - From `waiting-taker-bond` → `waiting-payment` (buy order taken) or → `waiting-buyer-invoice` (sell order taken), once the bond HTLC is `Accepted`. - From `waiting-taker-bond` → `pending`, if the bond bolt11 is never paid and expires, or the taker cancels before locking. The published `s` tag was `pending` throughout — observers see only that the take attempt left no trace. -## Failure modes +### Failure modes - The user never pays the bond bolt11 → the invoice expires; the order's NIP-33 status was `pending` throughout, so the rollback only undoes the daemon-internal take state. The order remains takeable. - The user pays the bond and then cancels before trade completion → the bond HTLC is cancelled and the funds return to the taker. -- Slashing conditions (solver-directed dispute resolution, or timeout while in a waiting state) can settle the bond rather than release it. The solver directs slashing via the `bond_resolution` payload documented under [Admin Settle order](./admin_settle_order.md) and [Admin Cancel order](./admin_cancel_order.md), and the non-slashed counterparty is then asked for their share of the bond via [Bond payout invoice](./add_bond_invoice.md). +- Slashing conditions (solver-directed dispute resolution, or timeout while in a waiting state) can settle the bond rather than release it. The solver directs slashing via the `bond_resolution` payload documented under [Admin Settle order](./admin_settle_order.md) and [Admin Cancel order](./admin_cancel_order.md), and the non-slashed counterparty is then asked for their share of the bond via [Bond payout invoice](./add_bond_invoice.md). When a timeout slash fires, the slashed party first receives a [`bond-slashed`](./bond_slashed.md) notification. + +--- + +## Maker bond + +When `apply_to` is `"make"` or `"both"`, the **maker** must lock a bond before the order is published to Nostr. The same `pay-bond-invoice` action is used, but the trigger and the order lifecycle differ from the taker case. + +### Direction and trigger + +- **Direction:** Mostro → user (the maker). +- **Trigger:** Sent in response to a `new-order` message when the operator has bonds enabled for makers, **before** the order is published. +- **Order visibility:** The order is **not published** to Nostr while the maker bond is outstanding. External observers see nothing — no `pending` order event, no order in the book. This differs from the taker bond, where the order remains visible and re-takeable throughout. The daemon tracks this internally as `waiting-maker-bond`, a status visible only in DM payload echoes. + +### Mostro message to the maker + +The action and wire shape are identical to the taker case; only the embedded `status` field in the `SmallOrder` differs: + +```json +[ + { + "order": { + "version": 1, + "id": "", + "action": "pay-bond-invoice", + "payload": { + "payment_request": [ + { + "id": "", + "kind": "sell", + "status": "waiting-maker-bond", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": 1698937797 + }, + "lnbcrt780n1pj59wmepp5..." + ] + } + } + }, + null +] +``` + +> Note: `"waiting-maker-bond"` is the daemon-internal status echoed in the DM payload. No NIP-33 order event has been emitted yet at this point. + +For **range sell orders**, the bond is sized against `max_amount` (not the individual fiat_amount). The `SmallOrder` echo will reflect the range order fields (`min_amount`, `max_amount`). + +### Follow-up after bond locks + +Once the maker's bond HTLC is `Accepted`: + +1. Mostro publishes the order to Nostr with status `pending` for the first time. +2. Mostro sends the maker the `new-order` confirmation message (same as in the no-bond flow — see [Creating a new sell order](./new_sell_order.md) and [Creating a new buy order](./new_buy_order.md)). + +### Daemon status `waiting-maker-bond` (DM payloads only) + +Internal transitions (visible only in DM payload echoes): + +- On `new-order` receipt → `waiting-maker-bond`, before any NIP-33 event is emitted. +- `waiting-maker-bond` → `pending` (and order published), once the bond HTLC is `Accepted`. +- `waiting-maker-bond` discarded (no NIP-33 ever emitted), if the bond invoice expires without payment. + +### Failure modes + +- Maker never pays → invoice expires, order discarded silently. No NIP-33 event was ever emitted so the order book is unaffected. +- Maker pays the bond, order publishes as `pending`, then maker cancels → bond HTLC released, funds return to maker. +- A waiting-state timeout slash (when `slash_on_waiting_timeout = true`) settles the maker's bond HTLC instead of releasing it. The maker first receives a [`bond-slashed`](./bond_slashed.md) notification, then the order is canceled. The winning counterparty (the taker) then receives [`add-bond-invoice`](./add_bond_invoice.md) for their share. + +--- ## Backwards compatibility -Clients running an older `mostro-core` version that does not yet know `pay-bond-invoice` will fail to deserialize the message and silently drop it; from the user's perspective the take stalls and eventually times out without surfacing a useful error. Operators are responsible for not enabling bonds in production until clients in the wild have adopted the `mostro-core` release that ships `Action::PayBondInvoice`. Clients should: +Clients running an older `mostro-core` version that does not yet know `pay-bond-invoice` will fail to deserialize the message and silently drop it; from the user's perspective the operation stalls and eventually times out without surfacing a useful error. Operators are responsible for not enabling bonds in production until clients in the wild have adopted the `mostro-core` release that ships `Action::PayBondInvoice`. Clients should: - Recognise the action explicitly and surface it to the user. -- If unable to handle it (e.g. an older build talking to a bond-enabled node), present a clear error rather than silently retrying the take. +- If unable to handle it (e.g. an older build talking to a bond-enabled node), present a clear error rather than silently retrying. -The Mostro info event will gain bond-related tags so clients can detect bond-enabled nodes ahead of a take — see [Other events published by Mostro](./other_events.md#mostro-instance-status). +The Mostro info event carries bond-related tags so clients can detect bond-enabled nodes ahead of a take or create — see [Other events published by Mostro](./other_events.md#anti-abuse-bond-policy-tags). From 2ec4c38a5ae6ba31ecf95e9b6ecd5e12c010cb74 Mon Sep 17 00:00:00 2001 From: grunch Date: Tue, 16 Jun 2026 09:41:01 -0300 Subject: [PATCH 2/3] docs: fix bond doc review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bond_slashed.md: 'cancelled' → 'canceled' (American spelling throughout) - bond_slashed.md: slash_on_waiting_timeout → bond_slash_on_waiting_timeout (correct wire tag name per other_events.md kind-38385 spec) - pay_bond_invoice.md: 'rumor's content' → 'message's content' - pay_bond_invoice.md: maker-bond SmallOrder status 'waiting-maker-bond' → 'pending' (matches flow.rs:380 which uses Some(Status::Pending) as the wire-facing NIP-69 bucket; waiting-maker-bond is daemon-internal only and never emitted in DM payloads) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/bond_slashed.md | 6 +++--- src/pay_bond_invoice.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/bond_slashed.md b/src/bond_slashed.md index b9f21f0..bac96df 100644 --- a/src/bond_slashed.md +++ b/src/bond_slashed.md @@ -2,12 +2,12 @@ The `bond-slashed` action is a notification Mostro sends to a bonded party when their anti-abuse bond has been **settled due to a waiting-state timeout**. It is a forfeiture notice: the bond HTLC has already been claimed into Mostro's wallet by the time this message is sent. -> **Scope.** This action is only emitted on the **timeout slash** path (scheduler-driven, gated by `slash_on_waiting_timeout = true` in the Mostro info event). It is **not** sent on the dispute-slash path — when a solver slashes a bond via [`admin-settle`](./admin_settle_order.md) or [`admin-cancel`](./admin_cancel_order.md), the slashed party receives the `admin-settled` / `admin-canceled` confirmation instead. +> **Scope.** This action is only emitted on the **timeout slash** path (scheduler-driven, gated by `bond_slash_on_waiting_timeout = "true"` in the Mostro info event — see [Other events published by Mostro](./other_events.md#anti-abuse-bond-policy-tags)). It is **not** sent on the dispute-slash path — when a solver slashes a bond via [`admin-settle`](./admin_settle_order.md) or [`admin-cancel`](./admin_cancel_order.md), the slashed party receives the `admin-settled` / `admin-canceled` confirmation instead. ## Direction and trigger - **Direction:** Mostro → the party whose bond was slashed. -- **Trigger:** The waiting-state timeout elapsed while the responsible party had not performed their expected trade action (e.g. the seller never paid the hold invoice while in `waiting-payment`, or the buyer never submitted an invoice in `waiting-buyer-invoice`), and the operator has configured `slash_on_waiting_timeout = true`. +- **Trigger:** The waiting-state timeout elapsed while the responsible party had not performed their expected trade action (e.g. the seller never paid the hold invoice while in `waiting-payment`, or the buyer never submitted an invoice in `waiting-buyer-invoice`), and the operator has configured `bond_slash_on_waiting_timeout = "true"` in the info event. - The `amount` in the payload is the **slashed bond amount in satoshis** — not the trade amount. ## Wire format @@ -45,7 +45,7 @@ The `amount` field in the embedded `SmallOrder` is the **slashed bond amount** ( After `bond-slashed` is sent to the responsible party, Mostro also: 1. **Cancels or republishes the order** depending on which party was responsible: - - **Maker responsible** (e.g. maker-as-seller never paid the hold invoice): the order is **cancelled** (since the maker cannot be trusted to fulfil it). The maker receives the `bond-slashed` notice followed by a `canceled` confirmation. + - **Maker responsible** (e.g. maker-as-seller never paid the hold invoice): the order is **canceled** (since the maker cannot be trusted to fulfil it). The maker receives the `bond-slashed` notice followed by a `canceled` confirmation. - **Taker responsible** (e.g. taker-as-buyer never submitted their invoice): the order is **republished** to the book as `pending` so the maker can be matched again. The taker receives `bond-slashed` followed by `canceled`. The maker's bond (if any) remains `Locked`. 2. **Asks the winning counterparty for a payout invoice** by sending them an [`add-bond-invoice`](./add_bond_invoice.md) message for their share of the slashed bond (governed by `bond_slash_node_share_pct` — the node retains its share, the rest goes to the counterparty). diff --git a/src/pay_bond_invoice.md b/src/pay_bond_invoice.md index 1faf2a1..8a203fe 100644 --- a/src/pay_bond_invoice.md +++ b/src/pay_bond_invoice.md @@ -16,7 +16,7 @@ The `bond_apply_to` tag in the Mostro info event (kind 38385) tells clients whic ### Mostro message to the taker -The rumor's content has the same shape as `pay-invoice`; only the action discriminator differs: +The message's content has the same shape as `pay-invoice`; only the action discriminator differs: ```json [ @@ -94,7 +94,7 @@ When `apply_to` is `"make"` or `"both"`, the **maker** must lock a bond before t ### Mostro message to the maker -The action and wire shape are identical to the taker case; only the embedded `status` field in the `SmallOrder` differs: +The action and wire shape are identical to the taker case: ```json [ @@ -108,7 +108,7 @@ The action and wire shape are identical to the taker case; only the embedded `st { "id": "", "kind": "sell", - "status": "waiting-maker-bond", + "status": "pending", "amount": 7851, "fiat_code": "VES", "fiat_amount": 100, @@ -125,7 +125,7 @@ The action and wire shape are identical to the taker case; only the embedded `st ] ``` -> Note: `"waiting-maker-bond"` is the daemon-internal status echoed in the DM payload. No NIP-33 order event has been emitted yet at this point. +> Note: the `SmallOrder` echo always carries `"status": "pending"` — this is the NIP-69 wire bucket the order will appear in once the bond locks and the order is published. The daemon-internal state `waiting-maker-bond` is tracked only in the daemon's database and is never emitted in DM payloads. No NIP-33 order event has been emitted yet at this point. For **range sell orders**, the bond is sized against `max_amount` (not the individual fiat_amount). The `SmallOrder` echo will reflect the range order fields (`min_amount`, `max_amount`). From 60f4956468648d4a73e94b5bccd477165abf52ba Mon Sep 17 00:00:00 2001 From: grunch Date: Tue, 16 Jun 2026 09:55:04 -0300 Subject: [PATCH 3/3] docs: correct waiting-*-bond status echo to match implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review against the daemon (src/app/bond/flow.rs) found the DM payload echo was documented incorrectly: - request_taker_bond (flow.rs:203) and request_maker_bond (flow.rs:380) both build the pay-bond-invoice SmallOrder with `Some(Status::Pending)`. The internal `waiting-taker-bond` / `waiting-maker-bond` states are kept in the daemon DB only and are NEVER emitted on the wire. Docs previously claimed clients "see" / "echo" these internal statuses in DM payloads. Fixes: - pay_bond_invoice.md: taker JSON example status waiting-taker-bond → pending; rewrote both "Daemon status" notes (taker + maker) to state the status is internal-only, never echoed. (Maker example was already pending from the prior review.) - take_buy.md / take_sell.md: corrected the "DM payload echoes waiting-taker-bond" phrasing to "SmallOrder carries status: pending". - order_event.md: corrected "visible only in DM payload echoes" → "kept in the daemon's database only and never emitted on the wire". - bond_slashed.md: notify_bond_slashed (slash.rs:696) builds the SmallOrder with status=None and created_at=None; corrected the wire example from status "canceled" / created_at to null, with an explanatory note. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/bond_slashed.md | 6 +++--- src/order_event.md | 2 +- src/pay_bond_invoice.md | 18 ++++++++++-------- src/take_buy.md | 2 +- src/take_sell.md | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/bond_slashed.md b/src/bond_slashed.md index bac96df..875cce3 100644 --- a/src/bond_slashed.md +++ b/src/bond_slashed.md @@ -23,13 +23,13 @@ The `bond-slashed` action is a notification Mostro sends to a bonded party when "order": { "id": "", "kind": "sell", - "status": "canceled", + "status": null, "amount": 785, "fiat_code": "VES", "fiat_amount": 100, "payment_method": "face to face", "premium": 1, - "created_at": 1698937797 + "created_at": null } } } @@ -38,7 +38,7 @@ The `bond-slashed` action is a notification Mostro sends to a bonded party when ] ``` -The `amount` field in the embedded `SmallOrder` is the **slashed bond amount** (not the original trade amount). For a range-order maker bond, this is the proportional slice amount for the taken sub-order. +The `amount` field in the embedded `SmallOrder` is the **slashed bond amount** (not the original trade amount). For a range-order maker bond, this is the proportional slice amount for the taken sub-order. The `status` field is `null` on this message — the `SmallOrder` carries only the bond context (id, kind, amounts), not an order status; clients should rely on the `bond-slashed` action itself, and on the separate order-status messages that follow (`canceled` for the order, plus a republished NIP-33 event when the order returns to the book). ## What happens next diff --git a/src/order_event.md b/src/order_event.md index 15e363f..a2da8de 100644 --- a/src/order_event.md +++ b/src/order_event.md @@ -65,7 +65,7 @@ Events are [addressable events](https://github.com/nostr-protocol/nips/blob/mast - `f` < Currency >: The fiat asset being traded, using the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) standard. - `s` < Status >: `pending`, `canceled`, `in-progress`, `success`, `expired`. - An order with `s = pending` may already be matched to a taker who is in the middle of paying their anti-abuse bond. It remains takeable in this window — another user may attempt the take, and whichever bond locks first wins (the prior taker is notified with `Action::Canceled`, see [Cancel](./cancel.md)). The internal daemon state that tracks this (`waiting-taker-bond`, visible only in DM payload echoes) is not part of NIP-69's four-bucket wire model. Clients **must not** gray out or hide a `pending` order from the local order-book view just because their user has initiated a take. + An order with `s = pending` may already be matched to a taker who is in the middle of paying their anti-abuse bond. It remains takeable in this window — another user may attempt the take, and whichever bond locks first wins (the prior taker is notified with `Action::Canceled`, see [Cancel](./cancel.md)). The internal daemon state that tracks this (`waiting-taker-bond`, kept in the daemon's database only and never emitted on the wire) is not part of NIP-69's four-bucket wire model. Clients **must not** gray out or hide a `pending` order from the local order-book view just because their user has initiated a take. - `amt` < Amount >: The amount of Bitcoin to be traded, the amount is defined in satoshis, if `0` means that the amount of satoshis will be obtained from a public API after the taker accepts the order. - `fa` < Fiat amount >: The fiat amount being traded, for range orders two values are expected, the minimum and maximum amount. - `pm` < Payment method >: The payment method used for the trade, if the order has multiple payment methods, they should be separated by a comma. diff --git a/src/pay_bond_invoice.md b/src/pay_bond_invoice.md index 8a203fe..233bb65 100644 --- a/src/pay_bond_invoice.md +++ b/src/pay_bond_invoice.md @@ -30,7 +30,7 @@ The message's content has the same shape as `pay-invoice`; only the action discr { "id": "", "kind": "sell", - "status": "waiting-taker-bond", + "status": "pending", "amount": 7851, "fiat_code": "VES", "fiat_amount": 100, @@ -47,7 +47,7 @@ The message's content has the same shape as `pay-invoice`; only the action discr ] ``` -> Note: the `status` value `"waiting-taker-bond"` here is the daemon-internal state echoed in the DM payload. The corresponding NIP-33 addressable order event's `s` tag is still `pending`. +> Note: the `SmallOrder` echo carries `"status": "pending"` — the NIP-69 wire bucket the order is advertised in while the bond is outstanding. The daemon-internal state `waiting-taker-bond` (which Mostro uses to route subsequent messages) is tracked only in the daemon's database and is **never** emitted in the DM payload. Clients should dispatch on the `pay-bond-invoice` action, not on a status field. ### Expected client behaviour @@ -64,11 +64,11 @@ Once the bond HTLC is `Accepted`, Mostro proceeds with the normal trade flow: > **Important — buy order taken (seller-as-taker):** this is the only flow on which a single user pays **two hold invoices in sequence on the same order** — first the bond (`pay-bond-invoice`), then the trade escrow (`pay-invoice`). They arrive as distinct actions and must be presented to the user as separate steps. Do not auto-pay either, do not coalesce them, and make the distinction obvious in the UI; this is the most error-prone path for client developers. -### Daemon status `waiting-taker-bond` (DM payloads only) +### Daemon status `waiting-taker-bond` (internal only) -In addition to the NIP-69 wire status (`pending` while the bond is outstanding), Mostro tags the order's internal state as `waiting-taker-bond` so it can route subsequent messages correctly. Clients see this value in the `SmallOrder` echo embedded in `pay-bond-invoice` payloads and may use it to drive UI ("Waiting for bond payment"). It does **not** appear on the addressable NIP-33 order event — that one continues to advertise the order as `pending`. +Mostro tags the order's internal state as `waiting-taker-bond` so it can route subsequent messages correctly while one or more taker bonds are outstanding. This status lives **only in the daemon's database** — it is neither published on the NIP-33 order event (whose `s` tag stays `pending`) nor echoed in any DM payload (the `pay-bond-invoice` `SmallOrder` carries `pending`, as shown above). It is documented here only to describe the daemon's lifecycle; clients never observe the literal `waiting-taker-bond` value on the wire. -Internal transitions (visible only in DM payload echoes): +Internal transitions (not visible to clients): - From `pending` → `waiting-taker-bond`, after a successful `take-buy` / `take-sell` when bonds are enabled. - From `waiting-taker-bond` → `waiting-payment` (buy order taken) or → `waiting-buyer-invoice` (sell order taken), once the bond HTLC is `Accepted`. @@ -90,7 +90,7 @@ When `apply_to` is `"make"` or `"both"`, the **maker** must lock a bond before t - **Direction:** Mostro → user (the maker). - **Trigger:** Sent in response to a `new-order` message when the operator has bonds enabled for makers, **before** the order is published. -- **Order visibility:** The order is **not published** to Nostr while the maker bond is outstanding. External observers see nothing — no `pending` order event, no order in the book. This differs from the taker bond, where the order remains visible and re-takeable throughout. The daemon tracks this internally as `waiting-maker-bond`, a status visible only in DM payload echoes. +- **Order visibility:** The order is **not published** to Nostr while the maker bond is outstanding. External observers see nothing — no `pending` order event, no order in the book. This differs from the taker bond, where the order remains visible and re-takeable throughout. The daemon tracks this internally as `waiting-maker-bond`, a status kept in its database only — it is never emitted on the wire (the `SmallOrder` echo carries `pending`, as shown below). ### Mostro message to the maker @@ -136,9 +136,11 @@ Once the maker's bond HTLC is `Accepted`: 1. Mostro publishes the order to Nostr with status `pending` for the first time. 2. Mostro sends the maker the `new-order` confirmation message (same as in the no-bond flow — see [Creating a new sell order](./new_sell_order.md) and [Creating a new buy order](./new_buy_order.md)). -### Daemon status `waiting-maker-bond` (DM payloads only) +### Daemon status `waiting-maker-bond` (internal only) + +Like `waiting-taker-bond`, this status lives **only in the daemon's database** — it is never published on a NIP-33 event (none exists yet) nor echoed in any DM payload (the `pay-bond-invoice` `SmallOrder` carries `pending`). It is documented here only to describe the daemon's lifecycle. -Internal transitions (visible only in DM payload echoes): +Internal transitions (not visible to clients): - On `new-order` receipt → `waiting-maker-bond`, before any NIP-33 event is emitted. - `waiting-maker-bond` → `pending` (and order published), once the bond HTLC is `Accepted`. diff --git a/src/take_buy.md b/src/take_buy.md index e301082..ac829fc 100644 --- a/src/take_buy.md +++ b/src/take_buy.md @@ -33,7 +33,7 @@ The event to send to Mostro would look like this: ## Optional: anti-abuse bond step -When the receiving Mostro node has bonds enabled, the seller (taker) first receives a [`pay-bond-invoice`](./pay_bond_invoice.md) message; the published NIP-33 order event remains `pending` (re-takeability is preserved — see [order event](./order_event.md)) and the DM payload echoes the internal status `waiting-taker-bond`. Only after the bond HTLC is `Accepted` does Mostro send the `pay-invoice` message described below. +When the receiving Mostro node has bonds enabled, the seller (taker) first receives a [`pay-bond-invoice`](./pay_bond_invoice.md) message; the published NIP-33 order event remains `pending` (re-takeability is preserved — see [order event](./order_event.md)) and the bond message's embedded `SmallOrder` carries `status: pending` (the daemon tracks the internal `waiting-taker-bond` state in its database only — it is never emitted on the wire). Only after the bond HTLC is `Accepted` does Mostro send the `pay-invoice` message described below. This is the **only flow on which a single user pays two hold invoices in sequence on the same order**: the bond first, then the trade hold invoice. They arrive as distinct actions (`pay-bond-invoice` then `pay-invoice`) and clients **must** present them as separate steps. Clients that do not recognise `pay-bond-invoice` should expect the take to time out and surface a clear error to the user — do not silently retry the take. diff --git a/src/take_sell.md b/src/take_sell.md index 7cb432d..9fb5050 100644 --- a/src/take_sell.md +++ b/src/take_sell.md @@ -19,7 +19,7 @@ If the order amount is `0` the buyer doesn't know the exact amount to create the ## Optional: anti-abuse bond step -When the receiving Mostro node has bonds enabled, the buyer (taker) first receives a [`pay-bond-invoice`](./pay_bond_invoice.md) message; the published NIP-33 order event remains `pending` (re-takeability is preserved — see [order event](./order_event.md)) and the DM payload echoes the internal status `waiting-taker-bond`. Only after the bond HTLC is `Accepted` does Mostro send the `add-invoice` message described below. Clients that do not recognise `pay-bond-invoice` should expect the take to time out and surface a clear error to the user — do not silently retry the take. +When the receiving Mostro node has bonds enabled, the buyer (taker) first receives a [`pay-bond-invoice`](./pay_bond_invoice.md) message; the published NIP-33 order event remains `pending` (re-takeability is preserved — see [order event](./order_event.md)) and the bond message's embedded `SmallOrder` carries `status: pending` (the daemon tracks the internal `waiting-taker-bond` state in its database only — it is never emitted on the wire). Only after the bond HTLC is `Accepted` does Mostro send the `add-invoice` message described below. Clients that do not recognise `pay-bond-invoice` should expect the take to time out and surface a clear error to the user — do not silently retry the take. ## Mostro response