Skip to content

feat: add dual send for transport v2 (Phase B)#624

Merged
grunch merged 5 commits into
mainfrom
feat/transport-v2-phase-b-dual-send
Jun 19, 2026
Merged

feat: add dual send for transport v2 (Phase B)#624
grunch merged 5 commits into
mainfrom
feat/transport-v2-phase-b-dual-send

Conversation

@AndreaDiazCorreia

@AndreaDiazCorreia AndreaDiazCorreia commented Jun 19, 2026

Copy link
Copy Markdown
Member

Implements Phase B — Dual send of the transport v1 → v2 migration (docs/architecture/TRANSPORT_V2_MIGRATION.md). Builds on Phase A (#621, merged), routing outbound Mostro messages through the per-node resolved transport.

Changes

  • v2 wrap (MostroMessage.wrapNip44): builds the [message, tradeSig, identityProof] 3-tuple with version: 2, NIP-44 encrypts it toward the node, and emits a kind-14 event signed by the trade key with a p tag. PoW is preserved (mined on the kind-14 id).
  • Identity proof mirrors mostro-core's transport.rs: trade-key signature = existing MostroMessage.sign (SHA-256 hex digest, Schnorr); identity proof = master key signing mostro-transport-v2-identity:<tradePubkey>:<messageJSON>. Both are null in full-privacy mode.
  • version threading: derived from the resolved transport instead of the global constant (1 for gift wrap, 2 for NIP-44).
  • Routing (MostroService.publishOrder): selects wrapNip44 (v2) or wrap (v1) from the node's protocol_version.

Scope

  • The v1 gift-wrap send path is byte-for-byte unchanged; v2 send only engages for nodes advertising protocol_version=2 (none in production yet).
  • The NIP-40 expiration tag is omitted (optional/caller-supplied per the daemon), avoiding any risk of a message expiring before processing.
  • Out of scope: send-side wiring polish (Phase C), additional tests (Phase D).

Verification

  • flutter analyze: no issues.
  • flutter test: all pass, including a new v2 wrap → unwrap round-trip (reputation + full-privacy) that cryptographically verifies the trade signature and identity proof, plus v1 regression.

Summary by CodeRabbit

  • New Features
    • Added NIP-44 encrypted direct messaging for protocol v2 with protocol-versioned signing/serialization.
    • Outbound message wrapping now routes per the connected Mostro node’s protocol version, covering restore and dispute flows.
    • Restore receiving now supports both v1 gift-wrap and v2 direct responses.
  • Documentation
    • Updated transport migration and session recovery docs to reflect dual receive/send behavior.
  • Tests
    • Added regression tests for NIP-44 wrapping and restore decoding across both transports, including signature validation cases.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0c77917-b953-4e1b-a1a5-0186f6d3ecab

📥 Commits

Reviewing files that changed from the base of the PR and between 9a8da5a and e7bbcfd.

📒 Files selected for processing (2)
  • lib/data/repositories/dispute_repository.dart
  • lib/features/restore/restore_manager.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/data/repositories/dispute_repository.dart
  • lib/features/restore/restore_manager.dart

Walkthrough

Phase B of Transport V2 migration extends MostroMessage with versioned signing and NIP-44 kind-14 wrapping via wrapNip44, adds a wrapForTransport dispatcher that selects between NIP-44 and gift-wrap based on the connected node's advertised protocol version, and routes publishOrder, disputes, and restore sends through it. Restore flow now subscribes to both transport kinds concurrently and decodes via a shared decodeRestoreMessage utility that branches by event kind. Tests cover reputation and full-privacy wrapping modes and dual-transport restore decode.

Changes

Transport V2 Dual Send and Restore Dual-Transport (Phase B)

Layer / File(s) Summary
Versioned message signing pipeline
lib/data/models/mostro_message.dart
toJson, sign, and serialize each gain an optional version parameter to emit protocol-v1 or v2 messages; shared _mostroSign helper extracts signature logic.
NIP-44 kind-14 wrapping and wrapForTransport dispatcher
lib/data/models/mostro_message.dart
wrapNip44 builds v2 message JSON, computes tradeSig via _mostroSign, constructs identityProof (master-key signature or null), NIP-44-encrypts the three-element tuple, and returns kind-14 event signed by trade key with optional PoW. wrapForTransport dispatcher selects wrapNip44 (v2) or wrap (v1) based on advertised transport.
NIP-44 wrapping reputation and full-privacy mode tests
test/data/mostro_message_nip44_test.dart
Verifies kind-14 event shape, NIP-44 decryption, three-element tuple structure, message version field, trade-key signature and master-key identity proof correctness in reputation mode; asserts null tradeSig/identityProof and correct message decoding in full-privacy mode.
publishOrder and dispute repository transport integration
lib/services/mostro_service.dart, lib/data/repositories/dispute_repository.dart
Both publishOrder and DisputeRepository.createDispute resolve the node's advertised transport and route through wrapForTransport instead of unconditional gift-wrap, passing protocolVersion and logging the selected event kind.
Restore dual-transport receive and shared decode utilities
lib/features/restore/restore_manager.dart
RestoreService subscribes to both v1 gift-wrap (kind 1059) and v2 NIP-44 (kind 14) simultaneously. New decodeRestoreMessage function branches by event kind, decrypts kind-14 via NIP-44 or unwraps kind-1059, and returns decoded tuple[0] as Map.
Restore dual-transport outbound requests
lib/features/restore/restore_manager.dart
Restore data, orders-details, and last-trade-index requests use wrapForTransport with protocolVersion instead of unconditional wrap. Message extraction steps use _decodeRestoreMessage for consistent dual-transport decoding.
Restore decode dual-transport test coverage
test/features/restore/restore_decode_test.dart
Tests verify correct decoding for v1 and v2 transports, validate equivalence between decoded outputs, and assert authorization behavior rejecting messages from unexpected authors.
Migration and architecture documentation updates
docs/architecture/TRANSPORT_V2_MIGRATION.md, docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md
Phase B status updated (Phase A merged, Phase B implemented); Phase B notes document routing all sends through wrapForTransport, v2 identity proof composition, and NIP-40 expiration tag omission. SESSION_RECOVERY_ARCHITECTURE.md documents dual-transport subscription and event-kind branching in decodeRestoreMessage.

Sequence Diagram

sequenceDiagram
  participant MostroService
  participant wrapForTransport
  participant wrapNip44
  participant _mostroSign
  participant NIP44
  participant NostrRelay
  
  MostroService->>MostroService: resolve transport from node
  alt transport == NIP-44 (v2)
    MostroService->>wrapForTransport: wrapForTransport(v2, tradeKey, recipientPubKey, masterKey?, difficulty)
    wrapForTransport->>wrapNip44: wrapNip44(tradeKey, recipientPubKey, masterKey?, difficulty)
    wrapNip44->>wrapNip44: toJson(version: 2)
    wrapNip44->>_mostroSign: sign message JSON with tradeKey
    _mostroSign-->>wrapNip44: tradeSig
    alt masterKey != null
      wrapNip44->>_mostroSign: sign identity domain with masterKey
      _mostroSign-->>wrapNip44: identityProof
    else
      wrapNip44->>wrapNip44: identityProof = null
    end
    wrapNip44->>NIP44: encrypt [messageMap, tradeSig, identityProof]
    NIP44-->>wrapNip44: ciphertext
    wrapNip44-->>wrapForTransport: kind-14 NostrEvent
  else transport == Gift-wrap (v1)
    wrapForTransport->>wrapForTransport: wrap (gift-wrap existing path)
  end
  wrapForTransport-->>MostroService: NostrEvent
  MostroService->>NostrRelay: publish event
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • MostroP2P/mobile#620: Adds initial Transport V2 (NIP-44 kind-14) migration documentation; this PR updates that spec with Phase B dual-send routing, identity proof composition, and restore dual-transport details.

Suggested reviewers

  • ermeme
  • grunch
  • arkanoider
  • BraCR10

Poem

🐇 Hop, hop through the Nostr lanes,
Kind-14 wrapped, no gift-box chains,
Trade keys sign, the master proves,
Restore both ways in privacy grooves.
Phase B lands — the dual transports align,
The rabbit cheers: interop design! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add dual send for transport v2 (Phase B)' accurately summarizes the main change: implementing Phase B of the transport migration with dual send capabilities for v2. It is concise, specific, and clearly conveys the primary objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/transport-v2-phase-b-dual-send

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d32a8624c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/services/mostro_service.dart Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@docs/architecture/TRANSPORT_V2_MIGRATION.md`:
- Line 307: The documentation contains a contradiction where line 307 states the
kind-14 event includes "`p` and NIP-40 `expiration` tags," but lines 317-318
clarify that the expiration tag is omitted, and the actual implementation
confirms only the `p` tag is emitted. Update line 307 in the Phase B scope
description to remove the reference to "and NIP-40 `expiration` tags" and state
only that the kind-14 event includes the `p` tag, making it consistent with the
implementation notes below and the actual wrapNip44 implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 171a4da6-2b62-4cd0-b6e7-a53da85f8809

📥 Commits

Reviewing files that changed from the base of the PR and between fe3a897 and d32a862.

📒 Files selected for processing (4)
  • docs/architecture/TRANSPORT_V2_MIGRATION.md
  • lib/data/models/mostro_message.dart
  • lib/services/mostro_service.dart
  • test/data/mostro_message_nip44_test.dart

Comment thread docs/architecture/TRANSPORT_V2_MIGRATION.md Outdated

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Verdict: Request Changes

Critical

  • Restore requests now route through wrapForTransport() for protocol_version=2, but RestoreService still subscribes/extracts only kind 1059 gift-wrap events. On a v2-only node the restore flow will wait for a response it never listens for.

Looks Good

  • The previous direct-send gap in publishOrder() / dispute creation is now covered by the shared transport resolver.

Please update the restore receive path to handle kind 14 before merging.

Comment thread lib/features/restore/restore_manager.dart
ermeme[bot]
ermeme Bot previously approved these changes Jun 19, 2026

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Verdict: Approve

The restore flow now handles both transports, including v2 NIP-44 direct replies, and the orders subscription still resolves transport from the node protocol version. This closes the blocking restore regression from the previous head.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/data/repositories/dispute_repository.dart (1)

55-60: ⚠️ Potential issue | 🟠 Major

Pass masterKey and keyIndex consistent with publishOrder.

The wrapForTransport() call is missing masterKey and keyIndex. Unlike publishOrder in mostro_service.dart, disputes will omit identity proof (tradeSig and identityProof remain null) when v2 transport is used, breaking the binding between trade key and master identity in reputation mode.

Apply the same pattern as publishOrder:

🐛 Proposed fix to add reputation mode parameters
       final event = await disputeMessage.wrapForTransport(
         protocolVersion: mostroInstance?.protocolVersion,
         tradeKey: session.tradeKey,
         recipientPubKey: _mostroPubkey,
+        masterKey: session.fullPrivacy ? null : session.masterKey,
+        keyIndex: session.fullPrivacy ? null : session.keyIndex,
         difficulty: mostroPow,
       );
🤖 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 `@lib/data/repositories/dispute_repository.dart` around lines 55 - 60, The
wrapForTransport() call in the dispute message handling is missing the masterKey
and keyIndex parameters, which are required to maintain identity proof for
reputation mode. Add the masterKey and keyIndex parameters to the
wrapForTransport() call, following the same pattern used in the publishOrder
method from mostro_service.dart. This ensures that tradeSig and identityProof
are properly populated instead of remaining null, preserving the binding between
the trade key and the master identity in reputation mode.
🧹 Nitpick comments (2)
test/features/restore/restore_decode_test.dart (1)

17-28: ⚡ Quick win

Cover the v2-native 3-tuple payload shape in restore decode tests.

Line 17 currently reuses a 2-item tuple for both transports, but v2 send-side contract is a 3-item tuple (message, tradeSig, identityProof). Add a v2 fixture/assertion with 3 elements so restore decode is regression-tested against the actual transport-v2 envelope.

Suggested test adjustment
-    final restoreTuple = jsonEncode([
+    final restoreTupleV1 = jsonEncode([
       {
         'restore': {
           'version': 1,
           'action': 'restore',
           'payload': {
             'restore_data': {'orders': [], 'disputes': []},
           },
         },
       },
       null,
     ]);
+
+    final restoreTupleV2 = jsonEncode([
+      {
+        'restore': {
+          'version': 2,
+          'action': 'restore',
+          'payload': {
+            'restore_data': {'orders': [], 'disputes': []},
+          },
+        },
+      },
+      'tradeSig-placeholder',
+      'identityProof-placeholder',
+    ]);
-      final event = await NostrUtils.createNIP59Event(
-        restoreTuple,
+      final event = await NostrUtils.createNIP59Event(
+        restoreTupleV1,
         tempTradeKey.public,
         mostroKey.private,
       );
-      final event = await buildV2Reply(restoreTuple);
+      final event = await buildV2Reply(restoreTupleV2);
-      final v1 = await NostrUtils.createNIP59Event(
-        restoreTuple,
+      final v1 = await NostrUtils.createNIP59Event(
+        restoreTupleV1,
         tempTradeKey.public,
         mostroKey.private,
       );
-      final v2 = await buildV2Reply(restoreTuple);
+      final v2 = await buildV2Reply(restoreTupleV2);

Also applies to: 69-94

🤖 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 `@test/features/restore/restore_decode_test.dart` around lines 17 - 28, The
restore decode tests are currently using a 2-item tuple for both transports, but
the v2 transport contract specifies a 3-item tuple with the shape (message,
tradeSig, identityProof). Add a separate v2-specific test fixture and assertion
using jsonEncode with a 3-element array instead of the 2-element tuple shown at
lines 17-28 and the related test assertions at lines 69-94, ensuring the restore
decode logic is properly tested against the actual v2 transport envelope format
with all three required elements.
lib/data/repositories/dispute_repository.dart (1)

43-46: 💤 Low value

Stale comments reference only NIP-59 gift wrap.

Lines 43 and 46 still describe "Gift Wrap protocol (NIP-59)" but the code now uses wrapForTransport which may produce NIP-44 (kind 14) for v2 nodes. Consider updating these comments to reflect the dual-transport behavior.

🤖 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 `@lib/data/repositories/dispute_repository.dart` around lines 43 - 46, Update
the comments on lines 43 and 46 in the dispute creation flow to accurately
reflect the dual-transport capability. Instead of stating only "Gift Wrap
protocol (NIP-59)", clarify that the `wrapForTransport` method dynamically
selects between NIP-59 gift wrap for v1 nodes and NIP-44 (kind 14) encryption
for v2 nodes. This ensures the comments accurately describe the actual behavior
of the code being executed.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@lib/features/restore/restore_manager.dart`:
- Around line 1093-1098: The code checks if the string content is empty but does
not validate that the decoded JSON array has elements before accessing
contentList[0], which will cause a RangeError if the array is empty. After
decoding content with jsonDecode and casting to List<dynamic> as contentList,
add a bounds check to verify that contentList is not empty before attempting to
access contentList[0], throwing an appropriate exception if the list is empty.

---

Outside diff comments:
In `@lib/data/repositories/dispute_repository.dart`:
- Around line 55-60: The wrapForTransport() call in the dispute message handling
is missing the masterKey and keyIndex parameters, which are required to maintain
identity proof for reputation mode. Add the masterKey and keyIndex parameters to
the wrapForTransport() call, following the same pattern used in the publishOrder
method from mostro_service.dart. This ensures that tradeSig and identityProof
are properly populated instead of remaining null, preserving the binding between
the trade key and the master identity in reputation mode.

---

Nitpick comments:
In `@lib/data/repositories/dispute_repository.dart`:
- Around line 43-46: Update the comments on lines 43 and 46 in the dispute
creation flow to accurately reflect the dual-transport capability. Instead of
stating only "Gift Wrap protocol (NIP-59)", clarify that the `wrapForTransport`
method dynamically selects between NIP-59 gift wrap for v1 nodes and NIP-44
(kind 14) encryption for v2 nodes. This ensures the comments accurately describe
the actual behavior of the code being executed.

In `@test/features/restore/restore_decode_test.dart`:
- Around line 17-28: The restore decode tests are currently using a 2-item tuple
for both transports, but the v2 transport contract specifies a 3-item tuple with
the shape (message, tradeSig, identityProof). Add a separate v2-specific test
fixture and assertion using jsonEncode with a 3-element array instead of the
2-element tuple shown at lines 17-28 and the related test assertions at lines
69-94, ensuring the restore decode logic is properly tested against the actual
v2 transport envelope format with all three required elements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bab75273-8a14-4c26-9b40-65ab2bc9b174

📥 Commits

Reviewing files that changed from the base of the PR and between d32a862 and 9a8da5a.

📒 Files selected for processing (7)
  • docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md
  • docs/architecture/TRANSPORT_V2_MIGRATION.md
  • lib/data/models/mostro_message.dart
  • lib/data/repositories/dispute_repository.dart
  • lib/features/restore/restore_manager.dart
  • lib/services/mostro_service.dart
  • test/features/restore/restore_decode_test.dart
✅ Files skipped from review due to trivial changes (2)
  • docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md
  • docs/architecture/TRANSPORT_V2_MIGRATION.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/data/models/mostro_message.dart

Comment thread lib/features/restore/restore_manager.dart
ermeme[bot]
ermeme Bot previously approved these changes Jun 19, 2026

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Verdict: Approve

Looks Good

  • Restore now has regression coverage for both transport v1 (kind 1059) and transport v2 (kind 14).
  • The restore decode path is factored cleanly into a shared helper, which keeps the transport branch testable.
  • The docs now match the current dual-receive behavior.

Notes

  • I did not find any blocking issues in the current head.

Reviewed by Hermes Agent

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Strict Review — Phase B (Transport v2, Dual send)

Verdict: Approvable with minor changes. The architecture is solid, well-documented, and well-tested (the tests cryptographically verify the trade signature and identity proof, not just the tuple shape). I confirmed locally that flutter analyze reports 0 issues and all 6 new tests pass. ermeme's blocking concern (restore only received kind 1059) is resolved by the dual-receive commits, and the doc contradiction at line 307 (NIP-40 expiration tag) is already fixed.

Status of prior feedback

  • Codex P2 (unconverted direct sends): resolved — there are no remaining direct .wrap() sends in lib/; everything routes through wrapForTransport.
  • ermeme (restore receive path): resolved — _createTempSubscription now subscribes to both kind 1059 and kind 14 (the latter scoped with authors:[mostroPubkey], good).
  • ⚠️ CodeRabbit Major (dispute missing masterKey/keyIndex): still present and valid — see below.

Findings

1. (Medium) DisputeRepository.createDispute omits masterKey/keyIndex → in v2 reputation mode the dispute is sent without identity proof
dispute_repository.dart:55-60 calls wrapForTransport without masterKey/keyIndex. This is the actively used path (trade_detail_screen.dart:774). Consequence: in wrapNip44, when masterKey == null, both tradeSig and identityProof are null → a v2 node receives a dispute with no identity binding in reputation mode. This is inconsistent with publishOrder (mostro_service.dart:372-373), which does pass them. In v1 this is pre-existing behavior (the seal is signed by the trade key), but the PR's own docs state this path is a first-class v2 citizen, so it's worth closing. session already exposes masterKey/keyIndex/fullPrivacy, so the fix is trivial:

masterKey: session.fullPrivacy ? null : session.masterKey,
keyIndex: session.fullPrivacy ? null : session.keyIndex,

2. (Low) decodeRestoreMessage — missing bounds check before contentList[0] (restore_manager.dart:~1098)
It validates content.isEmpty but not contentList.isEmpty. An empty JSON array [] would throw RangeError instead of a controlled exception. Defensive fix:

if (contentList.isEmpty) throw Exception('Restore message tuple is empty');

3. (Nit) Stale comments in dispute_repository.dart:43,46 still say "Gift Wrap protocol (NIP-59)" even though the path can now emit kind 14.

4. (Verify before enabling v2 — non-blocking) Interop with mostro-core
tradeSig is computed over jsonEncode(messageMap) and the identity proof over mostro-transport-v2-identity:<tradePubkey>:<messageJSON>. This relies on the JSON key ordering matching transport.rs's re-serialization on the daemon byte-for-byte. Since it reuses the same toJson() as the v1 sign() (which already works in production), it should match, but there is no v2 node in production yet, so this is not validated against a real daemon. Recommend an end-to-end interop test before activating v2.

5. (Minor note) wrapNip44 uses createdAt: DateTime.now() with no timestamp randomization. Acceptable since the kind-14 event is signed by the trade key (already linkable), but worth being aware of.

Conclusion

None of these are blocking for the phase (no v2 nodes in production). I recommend addressing #1 before merge (identity consistency + it's the active path), with #2/#3 as quick cleanup. #4 is a pre-launch to-do for v2.

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Follow-up: wire-format verification against mostro-core/src/transport.rs

Re-framing my earlier review per a fair point: there are no v2 nodes in production because there is no v2 client yet — this PR is the prerequisite. So the wire format isn't a "verify later" item; it's the contract that future v2 daemons will be built against. I verified wrapNip44 byte-for-byte against the reference impl (MostroP2P/mostro-core:src/transport.rs::wrap_message_nip44 + Message::sign). It matches — this implementation is spec-compliant. Details, so reviewers don't have to re-derive it:

Aspect mostro-core reference This PR (wrapNip44) Match
Event kind Kind::PrivateDirectMessage (14) kind: 14
Tuple (Message, Option<trade_sig>, Option<(id_pub, id_sig)>) → JSON array [messageMap, tradeSig, identityProof]
Tuple element 0 message.as_json() = {"order"/"restore": {...}} {wrapperKey: toJson(version:2)}
Sign scheme schnorr(sha256_digest_raw_32B) sign(hex(sha256(msg))) → dart_nostr hex-decodes to the same 32B
Identity domain mostro-transport-v2-identity:<trade_pubkey_hex>:<message_json> mostro-transport-v2-identity:${tradeKey.public}:$messageJson
identityProof shape [identity_pubkey_hex, sig] or null [masterKey.public, sig] or null
NIP-44 keys encrypt(trade_secret, receiver) encryptNIP44(tuple, tradeKey.private, recipient)
Author / event sig signed by trade key keyPairs: tradeKey
p tag Tag::public_key(receiver) ['p', recipientPubKey]
PoW EventBuilder.pow(opts.pow), re-signed mineProofOfWork(event, difficulty, tradeKey)
created_at builder default (real time, no randomization) DateTime.now() ✅ (retracting my earlier note — the reference doesn't randomize either)
NIP-40 expiration optional (opts.expiration) omitted ✅ (valid: daemon makes it optional)

The one cross-impl risk — that Dart's jsonEncode key ordering must equal the daemon's serde message.as_json() for tradeSig/identity-proof to verify — is already proven by the v1 path in production, which signs the exact same {wrapperKey: toJson()} and is verified the same way (Message::verify_signature over message.as_json()). v2 reuses both sides verbatim. So no new canonicalization risk.

One real consequence to fix before merge (sharpening finding #1)

Given the daemon's unwrap_message_nip44, a null identity_proof is accepted and sets identity = event.pubkey (the trade key) — i.e. it is treated as full-privacy mode. Because DisputeRepository.createDispute omits masterKey/keyIndex, a dispute raised by a reputation-mode user on a v2 node would be silently received as full-privacy: the daemon binds it to the trade key, not the user's master identity. Same omission exists in the v1 path here (pre-existing), but since we're establishing the v2 contract strictly, this is the moment to fix it so reputation disputes carry the identity proof. Fix is the 2 lines I noted (mirror publishOrder: masterKey/keyIndex gated on session.fullPrivacy).

Net

Crypto/serialization interop: verified correct. Remaining actionable items unchanged: dispute identity proof (#1, now with a concrete consequence), contentList empty-array guard (#2), stale NIP-59 comments (#3). Recommend a real end-to-end roundtrip test against a mostro-core v2 node (or a vector generated by wrap_message_nip44) added in Phase D before any v2 node ships.

Reference: mostro-core/src/transport.rs, TRANSPORT_V2_SPEC.md

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Request Changes

The wire format is verified spec-compliant against mostro-core/src/transport.rs (see my detailed comparison comment), and the dual-receive regression is resolved. Approving is blocked only by the following, which should be addressed before merge:

Must fix

  1. DisputeRepository.createDispute omits masterKey/keyIndex, so a reputation-mode dispute on a v2 node is silently downgraded to full-privacy identity (daemon accepts null identity_proof and binds the dispute to the trade key, not the master identity). Mirror publishOrder:
    masterKey: session.fullPrivacy ? null : session.masterKey,
    keyIndex: session.fullPrivacy ? null : session.keyIndex,

Should fix
2. decodeRestoreMessage: add an empty-array guard before contentList[0] to avoid RangeError on a [] payload.
3. Stale comments in dispute_repository.dart:43,46 still say "Gift Wrap protocol (NIP-59)" though the path can now emit kind 14.

Recommended (Phase D)

  • Add an end-to-end roundtrip test against a mostro-core v2 wrap vector before any v2 node ships.

Once #1#3 land I'm happy to approve.

- DisputeRepository.createDispute now passes masterKey/keyIndex to
  wrapForTransport so reputation-mode disputes carry the v2 identity
  proof instead of being silently downgraded to full-privacy, matching
  publishOrder.
- decodeRestoreMessage guards against an empty JSON tuple before
  indexing [0] to avoid a RangeError.
- Refresh stale NIP-59-only comments in the dispute send path.

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve

Must/should-fix items from the previous review are addressed in e7bbcfdf:

  1. DisputeRepository.createDispute now passes masterKey/keyIndex (gated on session.fullPrivacy), so reputation-mode v2 disputes carry the identity proof instead of being downgraded to full-privacy.
  2. decodeRestoreMessage guards against an empty tuple before indexing [0].
  3. ✅ Stale NIP-59-only comments in the dispute send path refreshed.

Verified locally: flutter analyze clean, nip44 + restore-decode tests pass. Wire format remains spec-compliant with mostro-core/src/transport.rs.

Non-blocking follow-up for Phase D: add an end-to-end roundtrip test against a mostro-core v2 wrap vector before any v2 node ships.

@grunch grunch merged commit 5153994 into main Jun 19, 2026
2 checks passed
@grunch grunch deleted the feat/transport-v2-phase-b-dual-send branch June 19, 2026 21:18
AndreaDiazCorreia added a commit that referenced this pull request Jun 24, 2026
revert: roll back Riverpod 3.x migration (#613) and dependent PRs (#624, #625)
AndreaDiazCorreia added a commit that referenced this pull request Jun 24, 2026
* feat(transport): implement protocol v2 (NIP-44 direct) send path with identity proof (Phase B)

* feat(transport): route all outbound Mostro sends through wrapForTransport (Phase B)

* feat(restore): add dual-receive support for transport v2 (NIP-44 direct) in restore flow

* test(restore): add dual-receive transport v2 regression tests for restore flow

* fix(transport): bind dispute identity proof and guard restore tuple

- DisputeRepository.createDispute now passes masterKey/keyIndex to
  wrapForTransport so reputation-mode disputes carry the v2 identity
  proof instead of being silently downgraded to full-privacy, matching
  publishOrder.
- decodeRestoreMessage guards against an empty JSON tuple before
  indexing [0] to avoid a RangeError.
- Refresh stale NIP-59-only comments in the dispute send path.

---------

Co-authored-by: grunch <[email protected]>
grunch added a commit that referenced this pull request Jul 1, 2026
…3.x (#632)

* feat: add dual send for transport v2 (Phase B) (#624)

* feat(transport): implement protocol v2 (NIP-44 direct) send path with identity proof (Phase B)

* feat(transport): route all outbound Mostro sends through wrapForTransport (Phase B)

* feat(restore): add dual-receive support for transport v2 (NIP-44 direct) in restore flow

* test(restore): add dual-receive transport v2 regression tests for restore flow

* fix(transport): bind dispute identity proof and guard restore tuple

- DisputeRepository.createDispute now passes masterKey/keyIndex to
  wrapForTransport so reputation-mode disputes carry the v2 identity
  proof instead of being silently downgraded to full-privacy, matching
  publishOrder.
- decodeRestoreMessage guards against an empty JSON tuple before
  indexing [0] to avoid a RangeError.
- Refresh stale NIP-59-only comments in the dispute send path.

---------

Co-authored-by: grunch <[email protected]>

* fix(order): prevent order creation race with node-switch restore session reset

- Add RestoreService.isOperationInProgress and awaitOperationCompletion() to expose restore/sync state
- AddOrderNotifier.submitOrder now waits for any in-flight restore to finish before creating an order
- Prevents session orphaning when node-switch restore's _clearAll wipes sessions mid-order-creation

* fix(restore): loop awaitOperationCompletion to handle overlapping operations

- Change awaitOperationCompletion from `if` to `while` to wait for any operation that starts while we're already waiting
- Prevents race where a second restore begins after the first completes but before the caller proceeds
- Update doc comment to clarify looping behavior and note that restore triggers are mutually exclusive with order submission

* fix(restore): wait for node info before sending restore request to prevent v1/v2 protocol mismatch

- Add _waitForNodeConnectivity call before _createTempSubscription to ensure mostroInstance is populated
- Prevents wrapForTransport from defaulting to v1 gift wrap when mostroInstance is null at app init
- Avoids protocol v2 nodes receiving kind 1059 requests they won't answer, which would leave local key index stale

* fix(order): replace restore wait with session lock to close TOCTOU race in order creation

- Replace awaitOperationCompletion with acquireSessionLock in AddOrderNotifier.submitOrder
- Wrap session creation in try/finally with lock release to serialize with restore's _clearAll
- Add _sessionLockTail future chain to RestoreService to implement critical-section lock
- Hold session lock for entire restore process in initRestoreProcess
- Prevents race where restore's session reset interleaves with order creation even after wait compl

* fix(order): move submitOrder inside session lock to prevent session orphaning

- Move mostroService.submitOrder and state update inside try/finally lock block
- Prevents race where restore's _clearAll can wipe session after creation but before publish
- Ensures entire create critical section (session + publish) is atomic with respect to restore

* refactor(session): extract session lifecycle lock to shared provider for reuse across order flows

* fix(restore): hold session lock during syncTradeIndex to prevent stale index reads

- Acquire sessionLifecycleLock before updating trade index in syncTradeIndex
- Prevents order/take flows from creating sessions with stale keyIndex while sync is in progress
- Release lock in finally block after index update completes

* fix(order): serialize range-order child session creation with restore

* refactor(message): extract wrapper-key selection into a single getter

- Deduplicate the restore/order wrapper-key logic repeated in sign(),
  serialize() and wrapNip44() into one _wrapperKey getter
- The key is load-bearing for the message signature; a single source of
  truth prevents the three copies from drifting into mismatched signatures

* docs(transport): document session lock, dispute wire change, optional expiration

---------

Co-authored-by: grunch <[email protected]>
Co-authored-by: Catrya <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants