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 @@ -46,7 +46,7 @@ reqwest = { version = "0.12.23", default-features = false, features = [
"json",
"rustls-tls",
] }
mostro-core = "0.11.3"
mostro-core = "0.13.0"
lnurl-rs = { version = "0.9.0", default-features = false, features = ["ureq"] }
pretty_env_logger = "0.5.0"
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio-rustls"] }
Expand Down
138 changes: 138 additions & 0 deletions docs/TRANSPORT_V2_SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# mostro-cli — Transport v2 (NIP-44 Direct) client support

**Status:** Phase 1 implemented · Phases 2–3 pending
**Daemon spec:** `MostroP2P/mostro` → `docs/TRANSPORT_V2_SPEC.md`
**Issue:** [#626 — Messaging Transport Abstraction Layer](https://github.com/MostroP2P/mostro/issues/626)
**Core:** `transport` module shipped in **mostro-core 0.13.0**

This document is the client-side counterpart to the daemon's transport-v2
spec. It drives the work of teaching `mostro-cli` to speak protocol **v2**
(signed kind-`14` events with NIP-44 encrypted content) in addition to
protocol **v1** (NIP-59 gift wraps, kind `1059`), so the CLI can trade
against a node running either wire transport — and in particular so we can
exercise the daemon's Phase 2 anti-spam gates (which only engage on the v2
transport).

## 1. Why

The daemon now speaks one of two wire transports per node, selected by its
`[mostro] transport` setting and advertised on the kind-`38385` instance-info
event via a `protocol_versions` tag (`"1"` or `"2"`). A v1-only client cannot
talk to a `transport = "nip44"` node at all (it never sees kind-14 traffic and
its gift wraps are ignored). To test and use v2 nodes, the CLI must:

- send protocol messages through the node's transport, and
- subscribe to / unwrap the matching event kind.

mostro-core 0.13.0 provides everything needed; the CLI work is wiring.

## 2. Wire format recap

| | v1 (`gift-wrap`) | v2 (`nip44`) |
|---|---|---|
| event kind | `1059` (GiftWrap) | `14` (signed, NIP-44 content) |
| outer author | throwaway ephemeral key | **the trade key** (signature is load-bearing) |
| inner payload | 2-tuple `(Message, Option<sig>)` | 3-tuple `(Message, Option<sig>, identity-proof?)` |
| `Message.version` | 2 (since core 0.13) | 2 |
| expiration | none | NIP-40 `expiration` tag |

The v2 identity proof lives **inside** the NIP-44 ciphertext (never at the
event level), bound to the authoring trade key — exactly as private as v1's
seal-carried identity. mostro-core handles the tuple, the proof, and its
verification; the CLI only chooses which wrap/unwrap entry point to call.

> **Note — kind 14 is overloaded.** The CLI already uses kind 14 for NIP-17
> peer-to-peer chat (`SendDm` / `dm-to-user`). Protocol-v2 Mostro messages are
> *also* kind 14 but use mostro-core's `wrap_message_nip44` layout and are
> authored by / addressed to Mostro. The two are disambiguated on receive by
> author + `p` tag and by which conversation key decrypts (a non-matching
> event yields `Ok(None)` from `unwrap_incoming`). Peer chat is out of scope
> for this effort and stays as-is.
Comment on lines +44 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

mostro-core 0.13.0 public API wrap_message_nip44

💡 Result:

In mostro-core version 0.13.0, there is no public API function named wrap_message_nip44. The project handles messaging primarily through its nip59 transport module, which implements dual-key gift wrapping [1]. Historical documentation and protocol references indicate that NIP-44 encryption is used within the Mostro architecture for private direct messaging and as a component of NIP-59 gift wrapping, rather than being exposed as a top-level wrap_message_nip44 API function [2][3]. For managing wrapped messages in mostro-core, developers typically interact with the mostro_core::nip59 module, which provides functions such as wrap_message and unwrap_message [1]. These utilities handle the necessary protocol-specific encryption (including NIP-44) and identity/trade key signing [1][2]. If you are looking for specific NIP-44 encryption implementation, it is generally handled by the underlying nostr-sdk crate, which mostro-core depends on and exposes via its internal message-handling pipelines [4][5].

Citations:


🏁 Script executed:

# Find and examine TRANSPORT_V2_SPEC.md
find . -name "TRANSPORT_V2_SPEC.md" -type f

Repository: MostroP2P/mostro-cli

Length of output: 92


🏁 Script executed:

# Search for wrap_message_nip44 references in the codebase
rg "wrap_message_nip44" --type markdown --type rust

Repository: MostroP2P/mostro-cli

Length of output: 165


🏁 Script executed:

# Search for wrap_message references to understand the API
rg "wrap_message" --type rust -B 2 -A 5 | head -100

Repository: MostroP2P/mostro-cli

Length of output: 5583


🏁 Script executed:

cat -n docs/TRANSPORT_V2_SPEC.md | sed -n '40,70p'

Repository: MostroP2P/mostro-cli

Length of output: 1922


🏁 Script executed:

# Check if wrap_message_nip44 is mentioned anywhere in the mostro-core codebase
rg "wrap_message_nip44" -r . --type rust --type toml --type md

Repository: MostroP2P/mostro-cli

Length of output: 148


🏁 Script executed:

# Look for the actual API/function definitions in mostro-core references
rg "pub.*fn.*wrap_message" -A 3 --type rust

Repository: MostroP2P/mostro-cli

Length of output: 46


Clarify wrap_message_nip44 reference.

Line 46 mentions wrap_message_nip44 layout, but this term does not correspond to a public API in mostro-core 0.13.0. The actual public dispatcher is wrap_message_with(transport, …) (listed in the API section). Replace the informal wrap_message_nip44 reference on line 46 with the correct public API name to avoid reader confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/TRANSPORT_V2_SPEC.md` around lines 44 - 50, In the documentation section
discussing how Protocol-v2 Mostro messages use kind 14, replace the informal
reference to `wrap_message_nip44` layout on line 46 with the correct public API
name `wrap_message_with(transport, …)` to accurately reflect the actual public
dispatcher available in mostro-core 0.13.0 and avoid confusing readers about the
proper API to use.


## 3. mostro-core 0.13.0 APIs the client uses

All re-exported from `mostro_core::prelude`:

- `Transport` — `enum { GiftWrap, Nip44Direct }`; `event_kind() -> Kind`
(`1059` / `14`), `protocol_version() -> u8` (`1` / `2`), `FromStr`/`Display`
(`"gift-wrap"` / `"nip44"`), `Default = GiftWrap`.
- `wrap_message_with(transport, message, identity_keys, trade_keys, receiver, opts) -> Event`
— send-side dispatcher; routes to gift-wrap or kind-14 wrap.
- `unwrap_incoming(event, receiver_keys) -> Option<UnwrappedMessage>`
— receive-side dispatcher; routes on `event.kind`, returns `Ok(None)` for
"not addressed to me" (decrypt miss), same as the existing `unwrap_message`.
- `wrap_message` / `unwrap_message` (the v1 pair) keep their 0.11 signatures —
no change.

`WrapOptions`, `UnwrappedMessage`, `validate_response`, `Message`, and
`nip59::RANGE_RANDOM_TIMESTAMP_TWEAK` are unchanged from 0.11.3.

## 4. Phases

### Phase 1 — Adopt mostro-core 0.13.0 (foundation) — IMPLEMENTED

Pure dependency bump; **no transport behaviour change** (the CLI still speaks
gift-wrap). De-risks the major-version jump on its own.

- `Cargo.toml`: `mostro-core` `0.11.3` → `0.13.0`.
- The only source break: a non-exhaustive `match` on `order::Status` in
`src/parser/dms.rs` gains the new `Status::WaitingMakerBond` arm (the bond
feature's maker-side status; rendered like `WaitingTakerBond`). The new
`Action`/`Payload` variants (bond + cashu) and `Order`'s new optional Cashu
fields do not break any call site (existing matches already carry catch-alls;
`Order` is only deserialized, and the new fields are `Option`).

Effect: messages now carry `Message.version = 2` (core `PROTOCOL_VER`), still
inside gift wraps. The daemon dispatches on event **kind**, not the version
field, and `verify()` validates action↔payload shape (not version), so a 0.13
CLI interoperates with a 0.13 daemon over gift-wrap unchanged. (Talking to a
pre-0.13, version-1 daemon is out of scope — that is the v1-deprecation
timeline's concern.)

Acceptance: `cargo build`, `cargo test`, `cargo clippy --all-targets
--all-features`, `cargo fmt --check` all clean; behaviour identical to before
against a gift-wrap node.

### Phase 2 — Transport selection (v2 capability) — PENDING

Teach the CLI to send and receive on either transport, selected explicitly.

- **Config:** a `TRANSPORT` env var / `--transport <gift-wrap|nip44>` flag,
parsed into `Transport` (default `gift-wrap` — wire-identical to today).
Mirrors the daemon's `[mostro] transport` knob. Store it on `Context`.
- **Send:** route the Mostro-protocol path of `send_dm` through
`wrap_message_with(ctx.transport, …)` instead of the hard-wired
`wrap_message`. The NIP-17 peer-chat path (`to_user`) is untouched.
- **Receive:** replace the hard-coded `Kind::GiftWrap` filter in `wait_for_dm`
(and the notification-loop kind check) with `ctx.transport.event_kind()`.
For v2, additionally constrain the filter to `author = mostro_pubkey` so the
Mostro reply is not confused with NIP-17 peer chat on the same kind.
- **Unwrap:** `parse_dm_events` calls `unwrap_incoming` instead of
`unwrap_message`, so it transparently handles whichever kind arrived.
- **Blast radius:** the ~12 command call sites of `send_dm` thread
`ctx.transport` through; no per-command logic changes.

Acceptance: against a `transport = "nip44"` daemon, a full
`new-order → take → add-invoice → fiat-sent → release` round-trips; against a
gift-wrap daemon, behaviour is unchanged. This is the phase that lets us test
the daemon's Phase 2 anti-spam gates.
Comment on lines +115 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

mostro daemon Phase 2 anti-spam gates mostro-core 0.13.0 protocol version

💡 Result:

The Mostro daemon implements anti-spam and message validation through a multi-stage pipeline designed to mitigate denial-of-service (DoS) attacks and ensure protocol integrity [1][2]. While there is no specific architectural component labeled as "Phase 2 anti-spam gates," anti-abuse measures are foundational to the daemon's message processing flow [1]. Anti-spam and validation gates include: 1. Proof-of-Work (PoW): The daemon enforces a PoW difficulty threshold configured in settings.toml [1][3]. Events failing this check are discarded immediately before any expensive processing [1][2]. 2. Event Freshness: Nostr events older than 10 seconds are silently discarded to prevent replay attacks [1]. 3. Trade Index Validation: The check_trade_index function enforces sequential trade index validation for actions, preventing replay attacks and ensuring proper user tracking [1][2]. 4. Silent Rejections: To further mitigate DoS, the daemon uses silent discards for invalid timestamp and non-GiftWrap events, avoiding unnecessary logging or responses [1]. Regarding protocol versions, Mostro v0.13.0, released in January 2025, was a significant milestone that introduced key management [4][5]. This version implemented: - Key Rotation: A mechanism for clients to rotate keys for every trade, adding a privacy layer beyond previous NIP-59 GiftWrap implementations [4][5]. - Core Updates: The release bumped mostro-core to v0.6.25, reflecting these architectural changes [6]. The "Phase 2" nomenclature is often associated with specific feature rollouts in the Mostro development roadmap, such as recent enhancements to the anti-abuse bond system (e.g., solver-directed dispute slashing) [7], rather than a formal anti-spam gate versioning. The anti-abuse bond system itself provides node operators with granular control over trade requirements (e.g., bond_enabled, bond_apply_to, bond_amount_pct) to further gate participation and deter malicious activity [8].

Citations:


🏁 Script executed:

# Find and inspect the TRANSPORT_V2_SPEC.md file
find . -name "TRANSPORT_V2_SPEC.md" -type f

Repository: MostroP2P/mostro-cli

Length of output: 92


🏁 Script executed:

# Search for PROTOCOL_VER and related protocol version configuration
rg -i "PROTOCOL_VER|protocol.*version|phase.*2.*anti.spam" --max-count=20

Repository: MostroP2P/mostro-cli

Length of output: 643


🏁 Script executed:

# Search for anti-spam gate implementation in the codebase
rg -i "anti.spam|anti-spam|phase.*2" --type rust --max-count=30

Repository: MostroP2P/mostro-cli

Length of output: 354


🏁 Script executed:

# Read the TRANSPORT_V2_SPEC.md file to understand full context
cat -n ./docs/TRANSPORT_V2_SPEC.md | head -200

Repository: MostroP2P/mostro-cli

Length of output: 8156


🏁 Script executed:

# Get the specific lines mentioned in the review (115-118) and surrounding context
sed -n '100,140p' ./docs/TRANSPORT_V2_SPEC.md

Repository: MostroP2P/mostro-cli

Length of output: 2245


Clarify Phase 2 anti-spam gate trigger mechanism in acceptance criteria.

The document already states (line 13) that Phase 2 anti-spam gates "only engage on the v2 transport," but lines 115–118 could be clearer about this distinction. Since Phase 1 still uses gift-wrap despite Message.version = 2, and Phase 2 switches to NIP-44 transport, the gates are keyed on event kind (14 for NIP-44), not protocol version. Add a note to the Phase 2 acceptance criteria clarifying that these gates only activate when the daemon detects the NIP-44 transport (kind 14), ensuring Phase 1 backward compatibility is unaffected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/TRANSPORT_V2_SPEC.md` around lines 115 - 118, The Phase 2 acceptance
criteria in the transport specification lacks clarity about when the anti-spam
gates activate. Enhance the acceptance criteria section (starting around the
description of the full round-trip: new-order → take → add-invoice → fiat-sent →
release against a transport = "nip44" daemon) by adding an explicit note
clarifying that the Phase 2 anti-spam gates only activate when the daemon
detects the NIP-44 transport based on event kind 14, not based on the
Message.version field. This distinction is critical because Phase 1 continues to
use gift-wrap transport despite Message.version = 2, so explicitly stating that
the gates trigger on transport kind detection (not protocol version) makes it
clear that Phase 1 backward compatibility remains unaffected.


### Phase 3 — Capability auto-detection + docs/UX — PENDING

- Read the node's `protocol_versions` tag from its kind-`38385` info event
(same fetch path as the existing `pow` probe) and, when `--transport` is not
given, auto-select the matching transport — warning on a mismatch
("this node speaks v2; re-run with --transport nip44") instead of silently
timing out.
- Surface the active transport in verbose output.
- Update `docs/architecture.md`, `docs/commands.md`, and the README.

## 5. Testing notes

- The daemon under test (`MostroP2P/mostro` PR #780) defaults to
`transport = "gift-wrap"`; set `transport = "nip44"` in its `settings.toml`
to exercise v2 + the anti-spam gate.
- After Phase 2, run the CLI with `--transport nip44` (or `TRANSPORT=nip44`)
against that node.
- The daemon's first-contact PoW lane (`pow_first_contact`) is testable by
combining `--transport nip44` with `--pow <bits>` on the CLI.
1 change: 1 addition & 0 deletions src/parser/dms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ fn format_payload_details(
Status::SettledHoldInvoice => "💰",
Status::InProgress => "🔄",
Status::WaitingTakerBond => "🪙",
Status::WaitingMakerBond => "🪙",
};

let kind_emoji = match o.kind.as_ref().unwrap_or(&mostro_core::order::Kind::Sell) {
Expand Down
Loading