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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ reqwest = { version = "0.12.1", default-features = false, features = [
"json",
"rustls-tls",
] }
mostro-core = { version = "0.14.3", features = ["sqlx"] }
mostro-core = { version = "0.14.5", features = ["sqlx"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
async-trait = "0.1.83"
Expand Down
49 changes: 37 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,20 @@ While @lnp2pBot works excellently, it relies on Telegram—a platform potentiall
### Developer Tools
- **Comprehensive Docs** - Architecture guides, event routing, Lightning operations, RPC reference
- **SQLite Backend** - Lightweight, embedded database with full migration support
- **Nostr Protocol** - NIP-59 (GiftWrap), NIP-33 (replaceable events) compliance
- **Nostr Protocol** - Protocol v2 (NIP-44 encrypted kind-14 direct messages, default) with a deprecated protocol-v1 (NIP-59 GiftWrap) opt-in, NIP-33 replaceable events
- **Observability** - Structured logging with `tracing`, configurable log levels

### Nostr Event Kinds Used by the Daemon

Mostro uses a small set of Nostr event kinds. Some are part of the public protocol, and some are transport-only details that live inside NIP-59 GiftWrap envelopes.
Mostro uses a small set of Nostr event kinds. Some are part of the public protocol, and some depend on the wire transport the node is configured with (see [Protocol Transport](#protocol-transport-v1-vs-v2) below). A node speaks exactly one transport: `nip44` (protocol v2, kind `14`, the default) or `gift-wrap` (protocol v1, kind `1059`, deprecated opt-in).

| Kind | Name / Constant | Used for | Notes |
| --- | --- | --- | --- |
| `0` | `Metadata` | Mostro profile metadata | Standard Nostr profile event published at startup when metadata is configured. |
| `1` | `TextNote` | Inner rumor in NIP-59 | Transport-only. Mostro creates and reads it *inside* GiftWrap; it is not published as a standalone public event. |
| `13` | `Seal` | Inner sealed envelope in NIP-59 | Transport-only. Mostro creates and reads it *inside* GiftWrap; it is not published as a standalone public event. |
| `1059` | `GiftWrap` | NIP-59 outer envelope | This is the relay-visible event kind that Mostro subscribes to and publishes for wrapped messages. |
| `14` | `DM_EVENT_KIND` | Protocol v2 direct message | **Default transport.** Signed kind-14 event with NIP-44 encrypted content, sent directly between the client's trade key and Mostro's pubkey. Carries a NIP-40 `expiration` tag (`[expiration] dm_days`). This is the relay-visible kind Mostro subscribes to when `transport = "nip44"`. |
| `1` | `TextNote` | Inner rumor in NIP-59 | Protocol v1 only, transport-only. Mostro creates and reads it *inside* GiftWrap; it is not published as a standalone public event. |
| `13` | `Seal` | Inner sealed envelope in NIP-59 | Protocol v1 only, transport-only. Mostro creates and reads it *inside* GiftWrap; it is not published as a standalone public event. |
| `1059` | `GiftWrap` | NIP-59 outer envelope | Protocol v1 (**deprecated**, removed in v0.19.0). The relay-visible kind Mostro subscribes to only when the operator explicitly sets `transport = "gift-wrap"`. |
| `10002` | `RelayList` | Relay metadata | Standard Nostr relay list event published periodically by the scheduler. |
| `8383` | `DEV_FEE_AUDIT_EVENT_KIND` | Dev fee audit event | Public audit event used for transparent fee accounting. |
| `30078` | `NOSTR_EXCHANGE_RATES_EVENT_KIND` | Exchange rates | NIP-33 replaceable event for BTC/fiat rate publishing. |
Expand All @@ -86,7 +87,18 @@ Mostro uses a small set of Nostr event kinds. Some are part of the public protoc
| `38385` | `NOSTR_INFO_EVENT_KIND` | Mostro info | NIP-33 replaceable event for operator / node metadata. |
| `38386` | `NOSTR_DISPUTE_EVENT_KIND` | Disputes | NIP-33 replaceable event for dispute publications. |

> Note: `kind 1` and `kind 13` are *inside* the `kind 1059` GiftWrap transport. They are created and verified by the wrapping/unwrapping code, but they are not emitted or consumed as standalone public relay events by the daemon.
> Note: `kind 1` and `kind 13` are *inside* the `kind 1059` GiftWrap transport (protocol v1). They are created and verified by the wrapping/unwrapping code, but they are not emitted or consumed as standalone public relay events by the daemon. On the default `nip44` transport none of `1`/`13`/`1059` are used at all.

### Protocol Transport (v1 vs v2)

Mostro speaks **one** wire protocol per node, selected with `[mostro] transport` in `settings.toml`:

| `transport` | Protocol | Relay-visible kind | Status |
| --- | --- | --- | --- |
| `"nip44"` | v2 — signed kind-14 events with NIP-44 encrypted content, sent directly between the client's trade key and Mostro | `14` | **Default.** A node with no `transport` line starts here. Rate-limitable by relays; first-contact senders can be required to grind extra PoW (`pow_first_contact`). |
| `"gift-wrap"` | v1 — NIP-59 GiftWrap (rumor → seal → wrap) | `1059` | **Deprecated, explicit opt-in only.** Never selected automatically; write `transport = "gift-wrap"` to keep serving v1-only clients during the transition. Removed in v0.19.0 ([#786](https://github.com/MostroP2P/mostro/issues/786)). |

The node advertises its protocol in the kind `38385` info event via the `protocol_version` tag (`"1"` or `"2"`) so v2-capable clients pick the matching wire format. Full details in [docs/TRANSPORT_V2_SPEC.md](docs/TRANSPORT_V2_SPEC.md).

---

Expand Down Expand Up @@ -305,7 +317,7 @@ Mostro is a Rust-based daemon with a modular architecture:
```mermaid
flowchart TB
subgraph "External Systems"
Nostr[Nostr Relays<br/>NIP-59 GiftWrap]
Nostr[Nostr Relays<br/>kind 14 NIP-44 DMs<br/>or NIP-59 GiftWrap]
LND[LND Node<br/>Hold Invoices]
API[Price API<br/>Yadio]
end
Expand Down Expand Up @@ -333,7 +345,7 @@ flowchart TB
Config[settings.toml]
end

Nostr <-->|GiftWrap Events| App
Nostr <-->|Protocol messages| App
LND <-->|gRPC| Lightning
API -->|Price Feed| Order

Expand Down Expand Up @@ -364,7 +376,7 @@ flowchart TB
```

**Key Components**:
- **Event Router** (`app.rs`): Unwraps NIP-59 GiftWraps, validates PoW/signatures, routes to action handlers
- **Event Router** (`app.rs`): Unwraps incoming protocol messages (NIP-44 kind-14 by default, NIP-59 GiftWrap on the deprecated v1 transport), validates PoW/signatures, routes to action handlers
- **Action Handlers** (`app/*`): 17 modules handling order lifecycle, disputes, admin operations
- **Lightning Client** (`lightning/*`): Creates hold invoices, settles/cancels, manages payments
- **RPC Server** (`rpc/*`): gRPC interface for direct admin communication (optional)
Expand All @@ -373,7 +385,7 @@ flowchart TB

**Technology Stack**:
- **Runtime**: Tokio async executor
- **Nostr**: `nostr-sdk` v0.43 (NIP-59 GiftWrap, NIP-33 replaceable events)
- **Nostr**: `nostr-sdk` v0.43 (NIP-44 kind-14 direct messages, NIP-59 GiftWrap for the deprecated v1 transport, NIP-33 replaceable events)
- **Lightning**: `fedimint-tonic-lnd` v0.3 (LND gRPC client)
- **Database**: SQLite via `sqlx` v0.9 (runtime queries; migrations embedded with `sqlx::migrate!`)
- **RPC**: `tonic` + `prost` (Protocol Buffers)
Expand Down Expand Up @@ -631,6 +643,13 @@ expiration_seconds = 900 # 15 minutes for taker to complete payment
# Proof-of-Work requirement (0 = disabled)
pow = 0 # Set to 10-20 to prevent spam

# Wire transport: "nip44" (protocol v2, default) or "gift-wrap" (protocol v1,
# DEPRECATED — explicit opt-in only, removed in v0.19.0). Omit for nip44.
transport = "nip44"
# Extra PoW demanded from first-contact senders on the nip44 transport
# (unset = same as `pow`). No effect on gift-wrap.
# pow_first_contact = 16

# Development sustainability fee (0.30 = 30% of Mostro fee goes to dev fund)
dev_fee_percentage = 0.30 # Minimum: 0.10 (10%), Maximum: 1.0 (100%)

Expand All @@ -645,6 +664,8 @@ bitcoin_price_api_url = "https://api.yadio.io"
- `fee`: Your Mostro's fee percentage (default 0.6% split between buyer/seller)
- `dev_fee_percentage`: Portion of your fee donated to Mostro development (default 30%)
- `pow`: Increase to 10-20 if experiencing spam (higher = slower client messages)
- `transport`: Defaults to `"nip44"` (protocol v2). Set `"gift-wrap"` **explicitly** only if you must keep serving protocol-v1 clients — it is deprecated and goes away in v0.19.0
- `pow_first_contact`: On `nip44`, a stiffer PoW for senders not yet part of an active trade (anti-spam, checked before decryption)
- `fiat_currencies_accepted`: Restrict supported currencies (empty array = accept all)

---
Expand Down Expand Up @@ -889,7 +910,10 @@ client.add_relay("wss://relay.mostro.network").await?;
let order = Order { /* ... */ };
let message = Message::new(Action::NewOrder, order);

// Send via NIP-59 GiftWrap to Mostro pubkey
// Send as a protocol-v2 kind-14 NIP-44 direct message to Mostro's pubkey
// (default). Check the node's `protocol_version` tag in its kind-38385
// info event: "2" → NIP-44 kind 14, "1" → legacy NIP-59 GiftWrap.
// mostro-core ships helpers for both wire formats.
// ...
```

Expand Down Expand Up @@ -1189,7 +1213,8 @@ Join the operator community:

### Technical Documentation (This Repository)
- **[Architecture Overview](docs/ARCHITECTURE.md)** - System design, module map, startup sequence
- **[Event Routing](docs/EVENT_ROUTING.md)** - Message handling, NIP-59 GiftWrap processing
- **[Event Routing](docs/EVENT_ROUTING.md)** - Message handling, unwrapping of protocol messages
- **[Transport v2 Spec](docs/TRANSPORT_V2_SPEC.md)** - Protocol v2 (NIP-44 kind 14) vs deprecated v1 (NIP-59 GiftWrap), migration timeline
- **[Lightning Operations](docs/LIGHTNING_OPS.md)** - Hold invoice lifecycle, payment flows
- **[Orders & Actions](docs/ORDERS_AND_ACTIONS.md)** - Order state machine, action handlers
- **[Admin RPC & Disputes](docs/ADMIN_RPC_AND_DISPUTES.md)** - Dispute resolution, admin interface
Expand Down
23 changes: 17 additions & 6 deletions docs/TRANSPORT_V2_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ There is **no dual mode**: a node speaks exactly one protocol version.

```toml
[mostro]
# "gift-wrap" (protocol v1, DEPRECATED) | "nip44" (protocol v2)
transport = "gift-wrap"
# "nip44" (protocol v2, default) | "gift-wrap" (protocol v1, DEPRECATED,
# explicit opt-in only)
transport = "nip44"

[expiration]
# kind-14 direct messages
Expand All @@ -134,8 +135,12 @@ dm_days = 30

| `transport` | event kind | who can trade on this node |
|---|---|---|
| `gift-wrap` *(default in 0.18.x)* | 1059 (v1) | every current client — wire behavior identical to pre-v2 daemons |
| `nip44` | 14 (v2) | v2-capable clients only — the only mode from v0.19.0 |
| `nip44` *(default)* | 14 (v2) | v2-capable clients only — the only mode from v0.19.0 |
| `gift-wrap` *(deprecated, explicit opt-in only)* | 1059 (v1) | every current client — wire behavior identical to pre-v2 daemons |

A node with no `transport` line starts in `nip44`. Operators who still need
to serve protocol-v1 clients must write `transport = "gift-wrap"` explicitly
in `settings.toml`; it is never selected automatically.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +141 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show nip44 in the configuration example

The only TOML example immediately above this paragraph still sets transport = "gift-wrap". An operator copying the canonical example will therefore explicitly select deprecated protocol v1 and defeat the new fresh-install default described here; change the example to nip44 or clearly label it as a legacy opt-in example.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 05b77c8 — the §4 example now uses transport = "nip44".


**Capability discovery:** the node advertises its protocol in the kind
`38385` instance-info event with a `protocol_version` tag (`"1"` or
Expand All @@ -153,6 +158,11 @@ with the clients that community uses.
(nothing changes for existing clients). **Protocol v1 is DEPRECATED**:
announced in release notes, protocol docs and the `protocol_version`
tag. Client developers have the 0.18.x cycle to ship v2.
- **v0.18.5** — the daemon default flips to `transport = "nip44"`.
`gift-wrap` stays fully functional but only as an explicit opt-in in
`settings.toml` (the code path is untouched; removal is deferred to
v0.19.0). The mostro-core `Transport::default()` remains `gift-wrap`
for clients; mostrod overrides it with its own `default_transport()`.
- **v0.19.0** — protocol v2 becomes the default and only protocol.
Everything v1-related is removed from mostrod (gift-wrap path,
`"gift-wrap"` setting value, v1 acceptance). mostro-core keeps its
Expand Down Expand Up @@ -180,8 +190,9 @@ The bulk of the work, all additive, in mostro-core's `transport` module:
Minimal daemon integration; **zero handler changes** by design:

- `mostro-core` 0.12.1 → **0.13.0**.
- `[mostro] transport` setting (`Transport`, serde default = `gift-wrap`)
in `src/config/types.rs` + `settings.tpl.toml`.
- `[mostro] transport` setting (`Transport`, serde default = `gift-wrap`
at the time; `nip44` since v0.18.5) in `src/config/types.rs` +
`settings.tpl.toml`.
- `[expiration] dm_days` knob (default 30) in `ExpirationSettings` and the
`get_expiration_timestamp_for_kind` fallback (`DM_EVENT_KIND = 14` in
`src/config/constants.rs`).
Expand Down
18 changes: 11 additions & 7 deletions settings.tpl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,19 @@ publish_relays_interval = 60
# Requested POW
pow = 0
# Wire transport for protocol messages. A node speaks exactly one:
# "gift-wrap" - protocol v1, NIP-59 gift wraps (kind 1059). DEPRECATED,
# will be removed in v0.19.0 — mostrod will then run protocol
# v2 only and this setting disappears. See
# https://github.com/MostroP2P/mostro/issues/786
# "nip44" - protocol v2, signed kind-14 events with NIP-44 encrypted
# content. Rate-limitable by relays; switch once the clients
# your community uses support protocol v2.
# content. Rate-limitable by relays. THIS IS THE DEFAULT:
# a node with no `transport` line (or a fresh settings.toml)
# starts in nip44.
# "gift-wrap" - protocol v1, NIP-59 gift wraps (kind 1059). DEPRECATED and
# OPT-IN ONLY: it is never selected automatically — you must
# explicitly write `transport = "gift-wrap"` here to keep
# serving protocol-v1 clients during the transition. It will
# be removed in v0.19.0 — mostrod will then run protocol v2
# only and this setting disappears. See
# https://github.com/MostroP2P/mostro/issues/786
# See docs/TRANSPORT_V2_SPEC.md
transport = "gift-wrap"
transport = "nip44"
# Anti-spam gate for the "nip44" transport (docs/TRANSPORT_V2_SPEC.md §6
# Phase 2). Proof-of-work (leading-zero bits) demanded of a *first-contact*
# event — one whose visible sender (trade key) is not part of an active
Expand Down
58 changes: 48 additions & 10 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,12 @@ impl Settings {
MOSTRO_CONFIG.get()?.anti_abuse_bond.as_ref()
}

/// Wire transport for protocol messages. Falls back to the default
/// (`gift-wrap`, protocol v1) when the global settings haven't been
/// initialized yet — `send_dm()` sits on every reply path and must
/// degrade to v1 behavior rather than panic in unit tests that don't
/// bring up the full configuration, mirroring [`Settings::get_bond`].
/// Wire transport for protocol messages. Falls back to the daemon
/// default (`nip44`, protocol v2 — see `default_transport`) when the
/// global settings haven't been initialized yet — `send_dm()` sits on
/// every reply path and must degrade gracefully rather than panic in
/// unit tests that don't bring up the full configuration, mirroring
/// [`Settings::get_bond`].
///
/// DEPRECATED(v0.19.0, #786): goes away with the `transport` setting —
/// v0.19.0 hardcodes the protocol-v2 (`nip44`) wire format.
Expand All @@ -137,10 +138,23 @@ impl Settings {
)]
pub fn get_transport() -> Transport {
#[allow(deprecated)]
MOSTRO_CONFIG
.get()
Self::transport_or_default(MOSTRO_CONFIG.get())
}

/// Selection logic behind [`Settings::get_transport`], split out so the
/// uninitialized (`None`) fallback is unit-testable without touching the
/// process-wide `MOSTRO_CONFIG`.
///
/// DEPRECATED(v0.19.0, #786): goes away with the `transport` setting.
#[deprecated(
since = "0.18.0",
note = "transitional v1/v2 transport selection; removed in v0.19.0 (protocol v2 only) — see issue #786"
)]
fn transport_or_default(settings: Option<&Settings>) -> Transport {
#[allow(deprecated)]
settings
.map(|s| s.mostro.transport)
.unwrap_or_default()
.unwrap_or_else(crate::config::types::default_transport)
}

/// Retrieve the multi-source price configuration from the global
Expand Down Expand Up @@ -236,10 +250,34 @@ mod tests {
}

#[test]
fn transport_falls_back_to_default() {
#[allow(deprecated)]
fn transport_falls_back_to_nip44_when_uninitialized() {
// No global config ⇒ daemon default is protocol v2, regardless of
// mostro-core's `Transport::default()` (gift-wrap).
assert_eq!(Settings::transport_or_default(None), Transport::Nip44Direct);
}

#[test]
#[allow(deprecated)]
fn transport_uses_configured_value_when_initialized() {
let mut settings = test_settings();
settings.mostro.transport = Transport::GiftWrap;
assert_eq!(
Settings::transport_or_default(Some(&settings)),
Transport::GiftWrap
);
settings.mostro.transport = Transport::Nip44Direct;
assert_eq!(
Settings::transport_or_default(Some(&settings)),
Transport::Nip44Direct
);
}

#[test]
fn get_transport_reads_global_config() {
init_test_settings();
#[allow(deprecated)]
let transport = Settings::get_transport();
assert_eq!(transport, Transport::default());
assert_eq!(transport, Transport::Nip44Direct);
}
}
Loading