Skip to content

Commit c1c65ac

Browse files
authored
Merge pull request #46 from MostroP2P/docs/anti-abuse-bond-full-coverage
docs: document anti-abuse bond implementation (Phases 1–7)
2 parents 60123c6 + 60f4956 commit c1c65ac

12 files changed

Lines changed: 284 additions & 21 deletions

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- [Take range buy order](./take_buy_range_order.md)
1919
- [Pay bond invoice](./pay_bond_invoice.md)
2020
- [Bond payout invoice](./add_bond_invoice.md)
21+
- [Bond slashed notification](./bond_slashed.md)
2122
- [Seller pays hold invoice](./seller_pay_hold_invoice.md)
2223
- [Fiat sent](./fiatsent.md)
2324
- [Release](./release.md)

src/add_bond_invoice.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,78 @@ The recipient of the request message is the non-slashed counterparty of the trad
9999
- **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.
100100
- **Sign the reply with the non-slashed side's trade key.** See [Keys management](./key_management.md).
101101

102+
## Payout confirmations (Phase 3.5)
103+
104+
After the counterparty submits their payout invoice, Mostro acknowledges receipt and later confirms payment with two dedicated actions.
105+
106+
### `bond-invoice-accepted`
107+
108+
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.
109+
110+
```json
111+
[
112+
{
113+
"order": {
114+
"version": 1,
115+
"id": "<Order Id>",
116+
"action": "bond-invoice-accepted",
117+
"payload": {
118+
"order": {
119+
"id": "<Order Id>",
120+
"kind": "sell",
121+
"amount": 500,
122+
"fiat_code": "VES",
123+
"fiat_amount": 100,
124+
"payment_method": "face to face",
125+
"premium": 1
126+
}
127+
}
128+
}
129+
},
130+
null
131+
]
132+
```
133+
134+
Clients should surface this as a reassurance: *"Your bond payout invoice has been received and the payment is being processed."*
135+
136+
### `bond-payout-completed`
137+
138+
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.
139+
140+
```json
141+
[
142+
{
143+
"order": {
144+
"version": 1,
145+
"id": "<Order Id>",
146+
"action": "bond-payout-completed",
147+
"payload": {
148+
"order": {
149+
"id": "<Order Id>",
150+
"kind": "sell",
151+
"amount": 500,
152+
"fiat_code": "VES",
153+
"fiat_amount": 100,
154+
"payment_method": "face to face",
155+
"premium": 1
156+
}
157+
}
158+
}
159+
},
160+
null
161+
]
162+
```
163+
164+
Clients should surface this as a terminal success: *"Your bond payout of `amount` Sats has been completed!"*
165+
166+
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.
167+
102168
## Failure modes
103169

104170
- The counterparty never replies before the deadline → no Lightning payment arrives and the share is forfeited; the slashed funds remain with the node.
105171
- 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`.
106172
- 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`.
107173
- 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.
174+
- 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.
108175

109176
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.

src/bond_slashed.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Bond slashed notification
2+
3+
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.
4+
5+
> **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.
6+
7+
## Direction and trigger
8+
9+
- **Direction:** Mostro → the party whose bond was slashed.
10+
- **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.
11+
- The `amount` in the payload is the **slashed bond amount in satoshis** — not the trade amount.
12+
13+
## Wire format
14+
15+
```json
16+
[
17+
{
18+
"order": {
19+
"version": 1,
20+
"id": "<Order Id>",
21+
"action": "bond-slashed",
22+
"payload": {
23+
"order": {
24+
"id": "<Order Id>",
25+
"kind": "sell",
26+
"status": null,
27+
"amount": 785,
28+
"fiat_code": "VES",
29+
"fiat_amount": 100,
30+
"payment_method": "face to face",
31+
"premium": 1,
32+
"created_at": null
33+
}
34+
}
35+
}
36+
},
37+
null
38+
]
39+
```
40+
41+
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).
42+
43+
## What happens next
44+
45+
After `bond-slashed` is sent to the responsible party, Mostro also:
46+
47+
1. **Cancels or republishes the order** depending on which party was responsible:
48+
- **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.
49+
- **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`.
50+
51+
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).
52+
53+
## Expected client behaviour
54+
55+
- 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."*
56+
- This action is distinct from `canceled` — clients should not suppress it or merge it into the cancel flow.
57+
- A `canceled` message will typically follow for the order itself; clients should handle both.
58+
59+
## Note on cancels vs. slashes
60+
61+
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.

src/cancel.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,26 @@ Mostro updates the addressable event with `d` tag `<Order Id>` to change the sta
6868

6969
## Bond race during take
7070

71-
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.
71+
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`.
72+
73+
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.
74+
75+
## Cancel during `waiting-taker-bond`
76+
77+
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.
78+
79+
### Taker self-cancel
80+
81+
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.
82+
83+
- If the cancelling taker was the only prospective taker, the order drops back to `pending` and is visible on the order book again.
84+
- If other takers' bonds are still `Requested`, the order remains in `waiting-taker-bond` with those bonds still racing.
85+
86+
The maker is not notified; the NIP-33 order event stays `pending` throughout.
87+
88+
### Maker cancel
89+
90+
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.
7291

7392
## Cancel cooperatively
7493

src/message_suggestions_for_actions.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ Below are suggestions for messages that clients can show to users when receiving
1919
- **add-bond-invoice:**
2020
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.
2121

22+
- **bond-invoice-accepted:**
23+
Your bond payout invoice has been received. The payment is being processed — you'll be notified once it completes.
24+
25+
- **bond-payout-completed:**
26+
Your bond payout of `amount` Sats has been sent successfully. The funds should arrive in your Lightning wallet shortly.
27+
28+
- **bond-slashed:**
29+
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.
30+
2231
- **add-invoice:**
2332
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.
2433

src/new_buy_order.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ The nostr event will look like this:
3838
}
3939
```
4040

41+
## Optional: anti-abuse maker bond
42+
43+
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.
44+
45+
Only after the maker pays the bond does Mostro:
46+
1. Publish the order to Nostr with status `pending`.
47+
2. Send the `new-order` confirmation (shown below).
48+
49+
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.
50+
4151
## Confirmation message
4252

4353
Mostro will send back a nip59 event as a confirmation message, the message in the rumor looks like the following:

src/new_sell_order.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ The event to send to Mostro would look like this:
4747
}
4848
```
4949

50+
## Optional: anti-abuse maker bond
51+
52+
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.
53+
54+
Only after the maker pays the bond does Mostro:
55+
1. Publish the order to Nostr with status `pending`.
56+
2. Send the `new-order` confirmation (shown below).
57+
58+
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.
59+
5060
## Confirmation message
5161

5262
Mostro will send back a nip59 event as a confirmation message to the user like the following (unencrypted rumor's content example):

src/new_sell_range_order.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ Here we have two new fields, `min_amount` and `max_amount`, to define the range
3030

3131
When a taker takes the order, the amount will be set on the message.
3232

33+
## Optional: anti-abuse maker bond
34+
35+
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.
36+
37+
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.
38+
39+
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.
40+
3341
## Confirmation message
3442

3543
Mostro will send back a nip59 event as a confirmation message to the user like the following:

src/order_event.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Events are [addressable events](https://github.com/nostr-protocol/nips/blob/mast
6565
- `f` < Currency >: The fiat asset being traded, using the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) standard.
6666
- `s` < Status >: `pending`, `canceled`, `in-progress`, `success`, `expired`.
6767

68-
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.
68+
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.
6969
- `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.
7070
- `fa` < Fiat amount >: The fiat amount being traded, for range orders two values are expected, the minimum and maximum amount.
7171
- `pm` < Payment method >: The payment method used for the trade, if the order has multiple payment methods, they should be separated by a comma.

0 commit comments

Comments
 (0)