feat: add dual receive for transport v2 (Phase A)#621
Conversation
WalkthroughImplements NIP-44 direct (kind-14) wire transport alongside existing gift-wrap (kind-1059). Adds protocol version parsing to MostroInstance, Transport enum with resolver, NIP-44 crypto utilities with event-signature validation, repository streaming for instance updates, transport-aware subscription filters, dual-path decryption in main and background services, and a comprehensive migration specification documenting phases and implementation status. ChangesTransport V2 Migration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~28 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 808bd1744f
ℹ️ 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".
There was a problem hiding this comment.
Code Review Summary
Verdict: Changes Requested
Critical
- The foreground v2 branch is wired, but the background receive path is still incomplete.
background_notification_service.dart::_decryptAndProcessEventonly accepts kinds 4 and 1059, and_handleTradeKeyEventstill callsevent.unWrap(...). Once the resolver flips to v2, backgrounded Mostro replies will be dropped instead of notifying the user or updating the session.
Looks Good
- The transport abstraction is cleanly isolated.
- The foreground decrypt path is structured sensibly for a staged rollout.
There was a problem hiding this comment.
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`:
- Around line 21-22: The status line in the TRANSPORT_V2_MIGRATION.md document
at line 21 incorrectly states that no `lib/` code has been changed yet, but this
branch has already implemented Phase A receive-path code. Update the status text
to accurately reflect the current implementation state of the branch, replacing
the outdated "No `lib/` code has been changed yet" statement with text that
indicates Phase A receive-path code has been implemented to prevent confusion
during rollout and debugging.
🪄 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: 8eab4295-22b2-4dd1-a098-5ac4802907e1
📒 Files selected for processing (6)
docs/architecture/TRANSPORT_V2_MIGRATION.mdlib/features/mostro/transport.dartlib/features/notifications/services/background_notification_service.dartlib/features/subscriptions/subscription_manager.dartlib/services/mostro_service.dartlib/shared/utils/nostr_utils.dart
There was a problem hiding this comment.
Code Review Summary
Verdict: Changes Requested
Blocking issue
resolveTransport(null)insubscription_manager.dartis still hardcoded toTransport.giftWrap, so the orders subscription never switches to kind14for v2 nodes. That means the new NIP-44 receive path is not actually reachable through the normal subscription flow, which defeats Phase A as described in the migration plan.
Positive
- The background isolate now handles kind
14, which fixes the earlier gap. - The NIP-44 direct decrypt helper is cleanly isolated.
The merge-base changed after approval.
The merge-base changed after approval.
There was a problem hiding this comment.
I reviewed the current head and I do see one real blocking issue before merge.
Blocking issue:
- In
MostroService._onData, the storage key for persisted history is now transport-dependent: v1 uses the inner rumor id when available, but v2 falls back to the outer kind-14event.idbecause there is no inner rumor. - At the same time,
MostroMessage.fromJson(result[0])still gives you the logical Mostro message whose ownidis the durable order/message identity used throughout the app. - That means the same logical history is no longer keyed consistently across transports. Under v1 you persist under the inner id; under v2 you persist under the transport-level outer event id.
Why this matters:
- This PR is specifically about dual receive across two envelopes while keeping the logical message layer unchanged.
- If persisted history keys diverge by transport, replay/dedup/history queries can silently stop talking about the same thing even when the logical Mostro message is the same.
- In other words, the transport envelope starts leaking into storage identity, which is exactly what this migration should avoid.
What I would want:
- Persist the received message under the same logical identity across both transports, i.e. the Mostro message/order identity parsed from
tuple[0], not a transport-specific outer event id. - The outer Nostr event id can still be logged/inspected if useful, but it should not become the durable storage identity for v2 messages.
I think this is a real blocker for a dual-receive phase because otherwise the history model is no longer transport-invariant.
grunch
left a comment
There was a problem hiding this comment.
Review — Phase A dual receive
Reviewed the full diff at head 954a7bd1 plus every review thread. Walked the foreground receive path, the background isolate, the subscription/transport resolution, the NIP‑44 decrypt helper and the new tests. flutter analyze is clean on all 7 changed lib/ files and the two new test files pass (27/27).
Earlier review threads — all addressed
- Background kind‑14 path (codex / ermeme): fixed.
_decryptAndProcessEventnow allows kind 14,_handleTradeKeyEventbranches todecryptNIP44DirectEvent, and_loadMostroPubkeyreads the node pubkey from persistedAppSettingsinside the isolate (no Riverpod there). The orders filter is transferred to the background service vialifecycle_manager, so a v2 node yields a kind‑14 filter there too. resolveTransport(null)hardcoded (ermeme): fixed._resolveOrdersTransport()now reads the realprotocol_versionfrom the kind‑38385 info event, and_initMostroInstanceListenerre‑subscribes when the info event arrives after the initial subscription. Covered bytransport_test.dartand theprotocol_versioncases inmostro_instance_test.dart.- Doc status line (CodeRabbit): fixed — now states Phase A is implemented.
On the remaining "storage key is transport‑dependent" blocker
I dug into this one and I don't think it holds up — and the suggested remedy would actually break history.
The persisted record key (decryptedId ?? event.id) must be unique per message, because a single order has many messages. Every history query keys on the logical id field of the stored MostroMessage (Filter.equals('id', orderId) in mostro_storage.dart), which is the order id and is fully transport‑invariant. The record key is only a per‑message uniqueness/dedup handle.
So persisting "under the Mostro message/order identity from tuple[0]" would set the record key to the order id, and addMessage's if (await hasItem(id)) return; would then store only the first message per order and silently drop the rest. The current scheme (inner rumor id for v1, outer kind‑14 event id for v2) is the correct design: distinct per‑message keys, with logical grouping done by the id field at query time.
Cross‑transport divergence also can't occur in practice: a node speaks exactly one transport, and within a transport the identifier is stable across relays, so dedup is sound. History identity is already transport‑invariant where it matters (the id field).
Other things I checked and found fine
- v2 filter pins
authors:[mostroPublicKey]+p:[tradeKeys], disambiguating from NIP‑17 peer chat (also kind 14). decryptNIP44DirectEventverifies kind, expected author, and recomputes the event id + Schnorr signature before decrypting — author signature is load‑bearing for v2, good.event.recipientreads theptag, so session matching works identically for kind 14.
LGTM. Approving.
The merge-base changed after approval.
…in background isolate (Phase A)
…eceive for NIP-44 direct messages (Phase A)
954a7bd to
d8d09fc
Compare
There was a problem hiding this comment.
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/open_orders_repository.dart (1)
138-144:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReset cached node info when
mostroPublicKeychanges to avoid stale transport selection.On a node switch,
_mostroInstancekeeps the previous node’s info. Downstream transport resolution can therefore use an oldprotocolVersionuntil a new info event arrives, causing a wrong orders filter during that window.Proposed fix
void updateSettings(Settings settings) { if (_settings.mostroPublicKey != settings.mostroPublicKey) { logger.i('Mostro instance changed, updating...'); _settings = settings.copyWith(); + _mostroInstance = null; _events.clear(); _subscribeToOrders(); } else { _settings = settings.copyWith(); } }🤖 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/open_orders_repository.dart` around lines 138 - 144, In the updateSettings method within the OpenOrdersRepository class, when the mostroPublicKey changes and you clear _events and call _subscribeToOrders(), also reset the _mostroInstance field to null or its default state. This ensures that stale node information (particularly the old protocolVersion) from the previous node is not used downstream during transport resolution, preventing incorrect order filtering until a new node info event arrives.
🧹 Nitpick comments (1)
test/features/mostro/mostro_instance_test.dart (1)
329-357: ⚡ Quick winAdd a regression test for parseable unsupported versions (e.g.,
protocol_version="3").This group covers absent/valid/unparseable values, but not the explicit-version-skew path. A single case asserting
event.protocolVersion == 3andMostroInstance.fromEvent(event).protocolVersion == 3will lock the downgrade contract inresolveTransport(...).🤖 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/mostro/mostro_instance_test.dart` around lines 329 - 357, Add a new test case in the protocol version test group that covers a parseable but unsupported version number. Create a test that builds an event with protocol_version set to a value like "3" (a number that can be parsed but is not explicitly handled), then verify that event.protocolVersion correctly parses to the integer value 3 and that MostroInstance.fromEvent(event).protocolVersion also equals 3. This test ensures the downgrade contract behavior in resolveTransport(...) is properly locked in for version skew scenarios.
🤖 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.
Outside diff comments:
In `@lib/data/repositories/open_orders_repository.dart`:
- Around line 138-144: In the updateSettings method within the
OpenOrdersRepository class, when the mostroPublicKey changes and you clear
_events and call _subscribeToOrders(), also reset the _mostroInstance field to
null or its default state. This ensures that stale node information
(particularly the old protocolVersion) from the previous node is not used
downstream during transport resolution, preventing incorrect order filtering
until a new node info event arrives.
---
Nitpick comments:
In `@test/features/mostro/mostro_instance_test.dart`:
- Around line 329-357: Add a new test case in the protocol version test group
that covers a parseable but unsupported version number. Create a test that
builds an event with protocol_version set to a value like "3" (a number that can
be parsed but is not explicitly handled), then verify that event.protocolVersion
correctly parses to the integer value 3 and that
MostroInstance.fromEvent(event).protocolVersion also equals 3. This test ensures
the downgrade contract behavior in resolveTransport(...) is properly locked in
for version skew scenarios.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 08a843b7-6725-4778-ac8f-ee9962be9ecf
📒 Files selected for processing (10)
docs/architecture/TRANSPORT_V2_MIGRATION.mdlib/data/repositories/open_orders_repository.dartlib/features/mostro/mostro_instance.dartlib/features/mostro/transport.dartlib/features/notifications/services/background_notification_service.dartlib/features/subscriptions/subscription_manager.dartlib/services/mostro_service.dartlib/shared/utils/nostr_utils.darttest/features/mostro/mostro_instance_test.darttest/features/mostro/transport_test.dart
✅ Files skipped from review due to trivial changes (2)
- test/features/mostro/transport_test.dart
- docs/architecture/TRANSPORT_V2_MIGRATION.md
🚧 Files skipped from review as they are similar to previous changes (4)
- lib/shared/utils/nostr_utils.dart
- lib/features/notifications/services/background_notification_service.dart
- lib/services/mostro_service.dart
- lib/features/subscriptions/subscription_manager.dart
Important
Depends on #620 (transport v2 migration spec). That PR must be merged first.
Implements Phase A — Dual receive of the transport v1 → v2 migration described in
docs/architecture/TRANSPORT_V2_MIGRATION.md.Adds the machinery to receive and decrypt protocol-v2 NIP-44 direct messages (kind 14) alongside the existing v1 gift-wrap path (kind 1059), selecting per node.
Changes
Transportenum + resolver (lib/features/mostro/transport.dart): single source of truth for the per-node transport. Phase A always resolves togiftWrap; Phase C wires realprotocol_versionresolution.subscription_manager.dart): v2 nodes subscribe to kind 14 pinned toauthors=[mostroPubkey]andp=[tradeKeys]; v1 keeps kind 1059.nostr_utils.dart):decryptNIP44DirectEventverifies the kind-14 author signature (author = node), NIP-44 decrypts the content, and returns the message tuple.mostro_service.dart):_onDatadispatches onevent.kind; v1 and v2 converge onMostroMessage.fromJson(tuple[0]).Scope
Dormant by design: the resolver returns
giftWrap, so the emitted filters and runtime behaviour are identical to today. The v1 gift-wrap path is byte-for-byte unchanged. Phase C activates dual-receive via auto-detection.Out of scope: dual send (Phase B),
protocol_versiondetection/wiring (Phase C), formal tests (Phase D), NIP-17 peer/dispute chat.Verification
flutter analyze: no issues.flutter test: matches baseline (pre-existing unrelated failures only); v1 path unchanged.Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests