Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
139 changes: 139 additions & 0 deletions src/key_management.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,142 @@ Clients must offer a more private version where the client never send the identi
"sig": "<Buyer's ephemeral pubkey signature>"
}
```

## 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": "<id>",
"kind": 14,
"pubkey": "<index 1 pubkey (trade key)>",
"content": "<NIP-44 ciphertext of the array below>",
"tags": [
["p", "<Mostro's pubkey>"],
["expiration", "<unix timestamp>"]
],
"created_at": 1691518405,
"sig": "<index 1 (trade key) signature>"
}
```

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": "<Order Id>",
"trade_index": 1,
"action": "take-sell",
"payload": null
}
},
"<index 1 signature of the sha256 hash of the serialized first element>",
["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]
```

### 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:<trade pubkey hex>:<message JSON>
```

where `<message JSON>` is the serialized first element and `<trade pubkey
hex>` 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": "<id>",
"kind": 14,
"pubkey": "<index N pubkey (trade key)>",
"content": "<NIP-44 ciphertext of the array below>",
"tags": [
["p", "<Mostro's pubkey>"],
["expiration", "<unix timestamp>"]
],
"created_at": 1691518405,
"sig": "<index N (trade key) signature>"
}
```

Decrypted `content`:

```json
[
{
"order": {
"version": 2,
"id": "<Order 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]`.
5 changes: 5 additions & 0 deletions src/other_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
74 changes: 72 additions & 2 deletions src/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, "...": "..." } },
"<trade-key signature, or null>"
]
```

**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, "...": "..." } },
"<trade-key signature, or null>",
["<identity pubkey>", "<identity signature>"]
]
```

| 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 `["<identity pubkey>", "<identity signature>"]`, or `null` for full-privacy mode (where the identity is the trade key itself) |
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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:
Expand Down
77 changes: 77 additions & 0 deletions src/transport_migration.md
Original file line number Diff line number Diff line change
@@ -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 `["<identity pubkey>", "<identity sig>"]` (or
`null` for full-privacy mode). The identity proof is a signature over the
domain-tagged payload `mostro-transport-v2-identity:<trade pubkey hex>:<message JSON>`;
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.