Skip to content
Open
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
76 changes: 43 additions & 33 deletions docs/architecture/TRANSPORT_V2_MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ during the migration window.
> - **Reference client (CLI)**: `MostroP2P/mostro-cli` PRs #176, #177, #178 and
> its `docs/TRANSPORT_V2_SPEC.md`.
>
> **Status.** Living design specification. **Phase A (dual receive)** —
> including `protocol_version` auto-detection and per-node transport resolution
> on the receive path — is merged to `main`. **Phase B (dual send)** is
> implemented in this branch. Phases C–D (§5) are pending.
> **Status.** **Migration complete.** All phases (§5) are implemented and
> merged: dual receive (A), dual send (B), per-node `protocol_version`
> auto-detection and transport wiring (folded into A/B), the `version`-field
> cleanup (C), and the test suite (D). The app speaks both transports and
> selects per node; the v1 gift-wrap path is unchanged and the v2 NIP-44 path
> engages automatically against a node advertising `protocol_version=2`.

---

Expand Down Expand Up @@ -231,16 +233,14 @@ this is the version-skew guard. The downgrade must be **logged explicitly** (at
`warn`) so a misconfigured or unreachable node cannot silently leave the app in
a degraded transport without anyone noticing.

### 4.2 `version` field stops being a global constant
### 4.2 `version` field is derived from the transport

`Config.mostroVersion` is currently a hardcoded `1`
(`lib/core/config.dart:56`) read by `MostroMessage.toJson()`
(`lib/data/models/mostro_message.dart:30`). The message `version` must instead
be **derived from the resolved `Transport`** (1 for `giftWrap`, 2 for `nip44` —
decision #2). The cleanest seam is to pass the `Transport` (not a raw int) into
the serialize/wrap path rather than reading a global, so the version number is
always a function of the transport and a single `MostroMessage` can be wrapped
for either transport without the two drifting apart.
The global `Config.mostroVersion` constant has been **removed**. The message
`version` is now a function of the wire transport: `MostroMessage.toJson({int?
version})` defaults to `1` (gift wrap — used by storage, logging and the v1 send
path), and `wrapNip44` passes `version: 2` explicitly. The same serialized
message JSON is reused for the tuple, the trade signature and the identity-proof
payload, so element 0, element 1 and element 2 can never drift apart.

### 4.3 Touch points (current v1 code → where v2 plugs in)

Expand All @@ -256,7 +256,7 @@ for either transport without the two drifting apart.
| Background receive decrypt (unWrap → `result[0]`) | `lib/features/notifications/services/background_notification_service.dart:199-322` |
| NIP-59 unwrap | `lib/shared/utils/nostr_utils.dart:383-443` |
| Node info / `protocol_version` parse | `lib/features/mostro/mostro_instance.dart:119-251` |
| Global `version` constant | `lib/core/config.dart:56` |
| `version` derived from transport (no global constant) | `lib/data/models/mostro_message.dart` (`toJson`) |

---

Expand Down Expand Up @@ -321,25 +321,35 @@ unchanged.
- The NIP-40 `expiration` tag is **omitted** (the daemon treats it as optional /
caller-supplied), avoiding any risk of a message expiring before processing.

### Phase C — Send-side wiring

> `protocol_version` parsing and per-node transport resolution already landed in
> Phase A (receive). This phase only threads that same resolved transport into
> the **send** path.

- Thread the resolved `Transport` (§4.1) into `MostroService.publishOrder` so
outbound messages use the v2 `wrap` from Phase B against a v2 node, while v1
nodes keep the gift-wrap path.
- Reuse the version-skew guard from Phase A: degrade to v1 on an unsupported /
unreachable node, keeping the existing explicit downgrade logging.

### Phase D — Tests

- v2 `wrap` → `unwrap` round-trip: produces a kind-14 event authored by the
trade key; decodes back to the same `MostroMessage`.
- `protocol_version` tag parse → transport mapping (including absent → v1).
- Identity proof: present in normal mode, `null` in full-privacy; domain string
is exactly `mostro-transport-v2-identity:<tradePubkey>:<messageJSON>`.
### Phase C — Wiring & finalization ✅

> Send-side transport wiring landed with Phase B: every outbound Mostro send
> already routes through `MostroMessage.wrapForTransport(protocolVersion: …)`,
> and receive-side detection landed with Phase A. This phase finalized the
> remaining loose ends.

- Removed the global `Config.mostroVersion`; the message `version` is derived
from the transport (§4.2).
- Extracted the transport → orders-filter mapping into the testable top-level
`buildOrdersFilter` (`subscription_manager.dart`).
- The version-skew guard (`resolveTransport`) degrades to v1 on an unsupported
protocol version and logs it (§4.1).

### Phase D — Tests ✅

- v2 `wrap` → `unwrap` round-trip, reputation and full-privacy
(`test/data/mostro_message_nip44_test.dart`): produces a kind-14 event
authored by the trade key and decodes back to the same message; cryptographically
verifies the trade signature and the identity proof, including the exact domain
string `mostro-transport-v2-identity:<tradePubkey>:<messageJSON>` (`null` in
full-privacy).
- `protocol_version` tag parse → transport mapping, including absent → v1
(`test/features/mostro/transport_test.dart`,
`test/features/mostro/mostro_instance_test.dart`).
- Orders subscription filter is transport-aware
(`test/features/subscriptions/orders_filter_test.dart`).
- Restore decode handles both transports and yields identical results
(`test/features/restore/restore_decode_test.dart`).
- Regression: the v1 gift-wrap path is byte-for-byte unchanged (existing
serialization tests stay green).

Expand Down
3 changes: 0 additions & 3 deletions lib/core/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ class Config {
// Debug mode
static bool get isDebug => !kReleaseMode;

// Mostro version
static int mostroVersion = 1;

// Key derivation configuration
static const String keyDerivationPath = "m/44'/1237'/38383'/0";

Expand Down
7 changes: 5 additions & 2 deletions lib/data/models/mostro_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:dart_nostr/nostr/core/key_pairs.dart';
import 'package:dart_nostr/nostr/model/event/event.dart';
import 'package:mostro_mobile/core/config.dart';
import 'package:mostro_mobile/data/models/enums/action.dart';
import 'package:mostro_mobile/data/models/payload.dart';
import 'package:mostro_mobile/features/mostro/transport.dart';
Expand All @@ -28,7 +27,11 @@ class MostroMessage<T extends Payload> {

Map<String, dynamic> toJson({int? version}) {
Map<String, dynamic> json = {
'version': version ?? Config.mostroVersion,
// The message version is derived from the wire transport: 1 for gift wrap
// (the default here, used by storage/logging and the v1 send path) and 2
// for NIP-44 direct (passed explicitly by `wrapNip44`). See §4.2 of
// docs/architecture/TRANSPORT_V2_MIGRATION.md.
'version': version ?? 1,
'request_id': requestId,
'trade_index': tradeIndex,
};
Expand Down
8 changes: 8 additions & 0 deletions lib/features/settings/about_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,14 @@ class AboutScreen extends ConsumerWidget {
),
const SizedBox(height: 16),

_buildInfoRowWithDialog(
context,
S.of(context)!.protocolVersion,
instance.protocolVersion.toString(),
S.of(context)!.protocolVersionExplanation,
),
const SizedBox(height: 16),

_buildInfoRowWithDialog(
context,
S.of(context)!.mostroCommitId,
Expand Down
52 changes: 36 additions & 16 deletions lib/features/subscriptions/subscription_manager.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mostro_mobile/services/logger_service.dart';
Expand Down Expand Up @@ -186,22 +187,11 @@ class SubscriptionManager {
// and re-subscribe when the node info arrives after this subscription.
final transport = _resolveOrdersTransport();
_appliedOrdersTransport = transport;
switch (transport) {
case Transport.giftWrap:
return NostrFilter(
kinds: [1059],
p: tradeKeys,
);
case Transport.nip44:
// v2 Mostro replies are kind 14 authored by the node and addressed
// (p) to the trade key; the authors pin disambiguates them from
// NIP-17 peer chat, which is also kind 14 (§3.4).
return NostrFilter(
kinds: [14],
authors: [ref.read(settingsProvider).mostroPublicKey],
p: tradeKeys,
);
}
return buildOrdersFilter(
transport,
tradeKeys,
ref.read(settingsProvider).mostroPublicKey,
);
case SubscriptionType.chat:
if (sessions.isEmpty) {
return null;
Expand Down Expand Up @@ -423,3 +413,33 @@ class SubscriptionManager {
_relayListController.close();
}
}

/// Builds the orders subscription filter for the resolved [transport] (§4.1).
///
/// - v1 ([Transport.giftWrap]): kind 1059 addressed (`p`) to the trade keys.
/// - v2 ([Transport.nip44]): kind 14 authored by the node ([mostroPubkey]) and
/// addressed to the trade keys — the `authors` pin disambiguates Mostro v2
/// replies from NIP-17 peer chat, which is also kind 14 (§3.4).
///
/// Top-level so the transport → filter mapping can be unit-tested without the
/// full [SubscriptionManager] / Riverpod orchestration.
@visibleForTesting
NostrFilter buildOrdersFilter(
Transport transport,
List<String> tradeKeys,
String mostroPubkey,
) {
switch (transport) {
case Transport.giftWrap:
return NostrFilter(
kinds: [1059],
p: tradeKeys,
);
case Transport.nip44:
return NostrFilter(
kinds: [14],
authors: [mostroPubkey],
p: tradeKeys,
);
}
}
2 changes: 2 additions & 0 deletions lib/l10n/intl_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,8 @@
"read": "Lesen",
"technicalDetails": "Technische Details",
"mostroDaemonVersion": "Mostro-Version",
"protocolVersion": "Protokollversion",
"protocolVersionExplanation": "Das Transportprotokoll dieses Knotens: 1 = NIP-59 Gift Wrap, 2 = NIP-44 Direktnachrichten. Die App erkennt dies automatisch und verwendet den passenden Transport.",
"mostroCommitId": "Mostro Commit-ID",
"orderExpiration": "Order-Ablaufzeit",
"holdInvoiceExpiration": "Hold-Invoice Ablaufzeit",
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,8 @@
"read": "Read",
"technicalDetails": "Technical Details",
"mostroDaemonVersion": "Mostro Version",
"protocolVersion": "Protocol Version",
"protocolVersionExplanation": "The wire transport protocol this node speaks: 1 = NIP-59 gift wrap, 2 = NIP-44 direct messages. The app detects this automatically and uses the matching transport.",
"mostroCommitId": "Mostro Commit ID",
"orderExpiration": "Order Expiration",
"holdInvoiceExpiration": "Hold Invoice Expiration",
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/intl_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,8 @@
"read": "Leer",
"technicalDetails": "Detalles Técnicos",
"mostroDaemonVersion": "Versión de Mostro",
"protocolVersion": "Versión del Protocolo",
"protocolVersionExplanation": "El protocolo de transporte que habla este nodo: 1 = gift wrap NIP-59, 2 = mensajes directos NIP-44. La app lo detecta automáticamente y usa el transporte correspondiente.",
"mostroCommitId": "ID de Commit Mostro",
"orderExpiration": "Expiración de Orden",
"holdInvoiceExpiration": "Expiración de Factura de Retención",
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,8 @@
"read": "Lire",
"technicalDetails": "Détails techniques",
"mostroDaemonVersion": "Version de Mostro",
"protocolVersion": "Version du Protocole",
"protocolVersionExplanation": "Le protocole de transport utilisé par ce nœud : 1 = gift wrap NIP-59, 2 = messages directs NIP-44. L'application le détecte automatiquement et utilise le transport correspondant.",
"mostroCommitId": "ID de commit Mostro",
"orderExpiration": "Expiration de commande",
"holdInvoiceExpiration": "Expiration hold invoice",
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/intl_it.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,8 @@
"read": "Leggi",
"technicalDetails": "Dettagli Tecnici",
"mostroDaemonVersion": "Versione di Mostro",
"protocolVersion": "Versione del Protocollo",
"protocolVersionExplanation": "Il protocollo di trasporto utilizzato da questo nodo: 1 = gift wrap NIP-59, 2 = messaggi diretti NIP-44. L'app lo rileva automaticamente e usa il trasporto corrispondente.",
"mostroCommitId": "ID Commit Mostro",
"orderExpiration": "Scadenza Ordine",
"holdInvoiceExpiration": "Scadenza Fattura di Blocco",
Expand Down
35 changes: 35 additions & 0 deletions test/features/subscriptions/orders_filter_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mostro_mobile/features/mostro/transport.dart';
import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart';

void main() {
group('buildOrdersFilter (transport-aware orders subscription)', () {
final mostroPubkey = 'a' * 64;
final tradeKeys = ['b' * 64, 'c' * 64];

test('v1 (giftWrap) → kind 1059, no authors pin', () {
final filter = buildOrdersFilter(
Transport.giftWrap,
tradeKeys,
mostroPubkey,
);

expect(filter.kinds, [1059]);
expect(filter.p, tradeKeys);
expect(filter.authors, isNull);
});

test('v2 (nip44) → kind 14 authored by the node, addressed to trade keys',
() {
final filter = buildOrdersFilter(
Transport.nip44,
tradeKeys,
mostroPubkey,
);

expect(filter.kinds, [14]);
expect(filter.authors, [mostroPubkey]);
expect(filter.p, tradeKeys);
});
});
}
8 changes: 4 additions & 4 deletions test/services/mostro_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ void main() {

final messageContent = {
'order': {
'version': Config.mostroVersion,
'version': 1,
'id': orderId,
'action': 'take-sell',
'payload': {
Expand Down Expand Up @@ -289,7 +289,7 @@ void main() {
userPubKey: userPubKey,
messageContent: {
'order': {
'version': Config.mostroVersion,
'version': 1,
'id': orderId,
'action': 'take-sell',
'payload': {
Expand Down Expand Up @@ -349,7 +349,7 @@ void main() {
userPubKey: userPubKey,
messageContent: {
'order': {
'version': Config.mostroVersion,
'version': 1,
'id': orderId,
'action': 'take-sell',
'payload': {
Expand Down Expand Up @@ -407,7 +407,7 @@ void main() {
// Simulate server-side verification
final messageContent = {
'order': {
'version': Config.mostroVersion,
'version': 1,
'id': orderId,
'action': 'take-sell',
'payload': {
Expand Down
Loading