diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 9c882bc..571cd37 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -5,6 +5,7 @@ --- - [Keys management](./key_management.md) +- [Transport migration (v1 → v2)](./transport_migration.md) - [Creating a Sell order](./new_sell_order.md) - [Creating a Sell range order](./new_sell_range_order.md) - [Creating a Buy order](./new_buy_order.md) diff --git a/src/key_management.md b/src/key_management.md index 3abe5ef..d902da1 100644 --- a/src/key_management.md +++ b/src/key_management.md @@ -162,3 +162,142 @@ Clients must offer a more private version where the client never send the identi "sig": "" } ``` + +## Protocol v2 — NIP-44 direct messages + +Everything above describes **protocol v1** (NIP-59 gift wrap, kind `1059`), +which is **DEPRECATED**. Nodes that advertise `protocol_version = "2"` in +their [instance-info event](./other_events.md#mostro-instance-status) speak +**protocol v2** instead: a single signed [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) +direct message of kind `14`, with no gift-wrap or seal layer. The key +derivation, indexing and rotation rules are **unchanged** — the same +identity key (index `0`) and per-trade keys (index `1`, `2`, …); only the +envelope and how the identity key is proven differ. + +What changes on the wire: + +- **The trade key authors the event.** The visible `pubkey`/`sig` are the + trade key's (not a throwaway ephemeral key as in v1). This is deliberate: + it lets relays rate-limit by sender and lets the node cheaply pre-filter + spam before decrypting. The exposure is bounded because trade keys are + single-trade and rotated. +- **`content` is NIP-44 ciphertext** of the message array. The conversation + key is derived from the trade key ↔ Mostro pair, so only those two can + decrypt. +- **The array gains a third element**, the identity proof — because there is + no seal to carry the identity key authenticated. See below. +- **An `expiration` tag** ([NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md)) + is always present so trade messages do not linger on relays forever. The + concrete expiration window is chosen by the node and is not part of the + protocol. +- **`version` is `2`** in the message. + +> **Note (deliberate NIP-17 deviation):** [NIP-17](https://github.com/nostr-protocol/nips/blob/master/17.md) +> defines kind `14` as an *unsigned* rumor that only ever travels inside a +> gift wrap. Mostro publishes it *signed*, because the author is an +> ephemeral single-trade key — the association NIP-17 protects against is +> here intentional and bounded. These are not standard NIP-17 chat messages. + +### Reputation mode (`take-sell` example) + +Reusing Alice's `take-sell` from above (trade key index `1`, identity key +index `0`), the v2 event looks like this: + +```json +{ + "id": "", + "kind": 14, + "pubkey": "", + "content": "", + "tags": [ + ["p", ""], + ["expiration", ""] + ], + "created_at": 1691518405, + "sig": "" +} +``` + +The decrypted `content` is the message array — the same first two elements +as v1, plus the identity proof as a third element: + +```json +[ + { + "order": { + "version": 2, + "id": "", + "trade_index": 1, + "action": "take-sell", + "payload": null + } + }, + "", + ["", ""] +] +``` + +### Identity proof + +The identity signature (third element) is **not** taken over the message +alone. The identity key signs a domain-tagged payload that binds the proof +to the *specific trade key* authoring the event: + +```text +mostro-transport-v2-identity:: +``` + +where `` is the serialized first element and `` is this event's `pubkey`. The receiver recomputes the payload from +`event.pubkey`, so a proof grafted onto an event authored by a different +trade key fails verification. This binding is what the gift-wrap seal +signature provided implicitly in v1. The signing scheme is the existing +Schnorr-over-sha256 `Message::sign` / `verify_signature`, and the identity +key signs once per message — the same custody model as v1, where it signs +every seal. + +### Full privacy mode + +As in v1, a client that does not want to maintain reputation never sends +the identity key. In v2 that means **both** the trade signature and the +identity proof are `null`, and the identity is taken to be the trade key +itself: + +```json +{ + "id": "", + "kind": 14, + "pubkey": "", + "content": "", + "tags": [ + ["p", ""], + ["expiration", ""] + ], + "created_at": 1691518405, + "sig": "" +} +``` + +Decrypted `content`: + +```json +[ + { + "order": { + "version": 2, + "id": "", + "action": "take-sell", + "payload": null + } + }, + null, + null +] +``` + +### Messages from Mostro + +Mostro authors its replies with its own well-known key and NIP-44 encrypts +them to the user's trade key (`p`-tagged). As in v1, Mostro's own messages +are unsigned: the trade signature and identity proof are both `null`. +Clients can subscribe with `authors=[mostro] AND #p=[their trade keys]`. diff --git a/src/other_events.md b/src/other_events.md index 6e7f2ee..5f33bf9 100644 --- a/src/other_events.md +++ b/src/other_events.md @@ -117,6 +117,10 @@ This event contains specific data about a Mostro instance. The instance is ident "pow", "0" ], + [ + "protocol_version", + "1" + ], [ "hold_invoice_expiration_window", "120" @@ -214,6 +218,7 @@ Below is an explanation of the meaning of some of the labels in this event, all - `max_orders_per_response`: Maximum complete orders data per response in orders action. - `fee`: The fee percentage charged by the instance. For example, "0.006" means a 0.6% fee. - `pow`: The Proof of Work required of incoming events. +- `protocol_version`: The Mostro protocol (wire transport) this node speaks — `"1"` for NIP-59 gift wrap (kind `1059`, DEPRECATED) or `"2"` for NIP-44 direct messages (kind `14`). A node speaks exactly one; clients read this tag to pick the matching wire format. See the [client migration guide](./transport_migration.md). - `hold_invoice_expiration_window`: The maximum time, in seconds, for the hold invoice issued by Mostro to be paid by the seller. - `hold_invoice_cltv_delta`: The number of blocks in which the Mostro hold invoice will expire. - `invoice_expiration_window`: The maximum time, in seconds, for a buyer to submit an invoice to Mostro. diff --git a/src/overview.md b/src/overview.md index 4825f47..225bc80 100644 --- a/src/overview.md +++ b/src/overview.md @@ -15,10 +15,43 @@ You can find more details about the order event [here](./order_event.md) ## The Message -All **_messages_** from/to Mostro should be [Gift wrap Nostr events](https://github.com/nostr-protocol/nips/blob/master/59.md), the `content` of the `rumor` event is a JSON-serialized `array` as a string (with no white space or line breaks), the first element is the message, the second element is the `signature` of the sha256 hash of the serialized first element, here the structure of the first element: +### Transports + +Mostro messages travel over one of two interchangeable wire transports. A +given node speaks **exactly one** of them — there is no dual mode — and +advertises which in its [instance-info event](./other_events.md#mostro-instance-status) +(kind `38385`) through the `protocol_version` tag (`"1"` or `"2"`): + +| Protocol | Transport | Event kind | Status | +|----------|-----------|------------|--------| +| **v1** | [NIP-59 Gift Wrap](https://github.com/nostr-protocol/nips/blob/master/59.md) | `1059` | **DEPRECATED** (default through v0.18.x) | +| **v2** | NIP-44 direct message | `14` | current (default from v0.19.0) | + +Both transports carry the **same logical message** and, once unwrapped, +yield the same structure to the daemon's handlers — only the envelope and +how the identity key is proven differ. Client developers should support +both during the transition and pick per node from the `protocol_version` +tag; see the [client migration guide](./transport_migration.md). + +The logical message itself (the first tuple element described below) is +identical across transports, except for the `version` field: `1` on the +gift-wrap transport, `2` on the NIP-44 direct transport. + +### The logical message + +In **protocol v1** all **_messages_** from/to Mostro are +[Gift wrap Nostr events](https://github.com/nostr-protocol/nips/blob/master/59.md); +the `content` of the `rumor` event is a JSON-serialized `array` as a string +(with no white space or line breaks), the first element is the message, the +second element is the `signature` of the sha256 hash of the serialized +first element. In **protocol v2** the same array travels NIP-44 encrypted +inside a signed kind-`14` event and gains a third element (the identity +proof) — see [Keys management](./key_management.md#protocol-v2--nip-44-direct-messages) +for the v2 wire format. Here the structure of the first element (the +logical message, shared by both transports): - [Wrapper](https://docs.rs/mostro-core/latest/mostro_core/message/enum.Message.html): Wrapper of the **_Message_** - - <`version` integer>: Version of the protocol, currently `1` + - <`version` integer>: Version of the protocol — `1` on the gift-wrap transport, `2` on the NIP-44 direct transport - [`id` integer]: (optional) Wrapper Id - [`request_id` integer]: (optional) Mostro daemon should send back this same id in the response - [`trade_index` integer]: (optional) This field is used by users who wants to maintain reputation, it should be the index of the trade in the user's trade history @@ -52,6 +85,43 @@ Here an example of a `new-order` order **_message_**: } ``` +### The content array (v1 vs v2) + +The first element above is the logical message. The array that actually +travels in the `content` differs by transport: + +**Protocol v1** (gift wrap) — a **2-element** array: + +```json +[ + { "order": { "version": 1, "...": "..." } }, + "" +] +``` + +**Protocol v2** (NIP-44 direct) — a **3-element** array, the v1 pair plus an +identity proof, NIP-44 encrypted inside a signed kind-`14` event: + +```json +[ + { "order": { "version": 2, "...": "..." } }, + "", + ["", ""] +] +``` + +| element | meaning | +|---|---| +| 1 | the logical message (the `version: 2` wrapper shown above) | +| 2 | the trade key's signature over the serialized first element, or `null` (Mostro replies and full-privacy mode set this element to `null`; the outer kind-14 event is still signed, as in v1) | +| 3 | the identity proof `["", ""]`, or `null` for full-privacy mode (where the identity is the trade key itself) | + +In v1 the identity key is carried, authenticated, by the gift-wrap *seal*. +v2 has no seal, so the identity instead travels **inside the ciphertext** +as element 3 — never visible at the event level, exactly as private as +before. The full v2 envelope and identity-proof signing rule are documented +in [Keys management](./key_management.md#protocol-v2--nip-44-direct-messages). + ## Payment Request Array Structure The `payment_request` field in the payload can have different structures depending on the use case: diff --git a/src/transport_migration.md b/src/transport_migration.md new file mode 100644 index 0000000..1f60135 --- /dev/null +++ b/src/transport_migration.md @@ -0,0 +1,77 @@ +# Transport migration (v1 → v2) + +Mostro is moving its wire transport from **protocol v1** (NIP-59 gift wrap, +kind `1059`) to **protocol v2** (NIP-44 direct message, kind `14`). This +page is the practical guide for **client developers**: what changes, how to +detect which transport a node speaks, and how to support both during the +transition. + +The logical messages, key derivation, indexing and rotation rules are +unchanged — only the envelope differs. The two formats are documented +side by side in [Keys management](./key_management.md) (the v2 wire format +is under *Protocol v2 — NIP-44 direct messages*) and the message tuples in +[Overview](./overview.md#the-content-array-v1-vs-v2). + +## Why the change + +Gift wraps give strong metadata privacy, but their outer event is signed by +a random throwaway key, so neither relays nor the daemon can tell legitimate +traffic from garbage without paying the full NIP-44 decrypt cost — a spam +flood ("Gift Wrap Apocalypse") cannot be rate-limited by sender. Protocol v2 +makes the **trade key** the visible author of the event. Because trade keys +are already single-trade and rotated, exposing one leaks little, while +enabling relay-side rate limiting by sender and cheap daemon-side +pre-validation before decryption. See the threat model in +[issue #626](https://github.com/MostroP2P/mostro/issues/626). + +## Capability discovery + +A node speaks **exactly one** transport — there is no dual mode. It +advertises which in its [instance-info event](./other_events.md#mostro-instance-status) +(kind `38385`) via the `protocol_version` tag: + +- `["protocol_version", "1"]` → gift wrap (kind `1059`) +- `["protocol_version", "2"]` → NIP-44 direct (kind `14`) + +A client should read this tag **before** sending anything and use the +matching wire format. Old daemons that predate the tag emit nothing; treat +their absence as v1. + +## What a client must change + +1. **Read `protocol_version`** from the node's kind-`38385` event and + branch on it. +2. **Subscribe to the right kind**: `1059` for v1, `14` for v2 (authored by + the node, `#p`-tagged to your trade keys for node replies). +3. **Wrap/unwrap with the matching path.** `mostro-core` **0.13.0** ships + both — `wrap_message_with(transport, …)` / `unwrap_incoming(event, …)` + dispatch on the transport (or event kind), so a client holding both + paths needs only to pass the node's transport. +4. **Set `version: 2`** in the message on the v2 transport (`1` on v1). +5. **On v2, build the 3-element content tuple** — message, trade signature + (or `null`), identity proof `["", ""]` (or + `null` for full-privacy mode). The identity proof is a signature over the + domain-tagged payload `mostro-transport-v2-identity::`; + see [Keys management → Identity proof](./key_management.md#identity-proof). +6. **On v2, add a NIP-40 `expiration` tag** to outgoing events. Mostro fills + a default (the node's `dm_days`, 30 days) on its own messages when none + is supplied. + +Full-privacy mode and reputation mode work the same way as in v1: omit the +identity key (proof and trade signature both `null`) for full privacy, or +include them to maintain reputation. + +## Release timeline + +- **v0.18.0** — protocol v2 ships. Default `transport = "gift-wrap"`, so + nothing changes for existing clients. **Protocol v1 is DEPRECATED.** + Client developers have the 0.18.x cycle to ship v2 support. +- **v0.19.0** — protocol v2 becomes the **default and only** protocol. + mostrod removes the v1 path entirely. `mostro-core` keeps its gift-wrap + helpers so clients can still migrate at their own pace, but nodes will no + longer accept kind-`1059` traffic. + +The recommendation is therefore: **keep both wrap paths now** and select per +node from `protocol_version`. A client that supports both will work against +every node throughout the transition, and against v2-only nodes after the +v0.19.0 cutover with no further change.