Skip to content

fix: correlate create_order replies by request_id and stop stale relay replay#172

Merged
Catrya merged 6 commits into
mainfrom
create-order-reply-correlation
Jul 11, 2026
Merged

fix: correlate create_order replies by request_id and stop stale relay replay#172
Catrya merged 6 commits into
mainfrom
create-order-reply-correlation

Conversation

@Catrya

@Catrya Catrya commented Jul 9, 2026

Copy link
Copy Markdown
Member

Closes #168

Problem

create_order waits for the daemon's reply, but resolved its waiter by
trade pubkey alone. When the per-trade kind-14 subscription opens,
relays replay the key's stored history, and any old reply addressed to
that key could consume the waiter before the genuine reply arrived:

  • a stale CantDo (about an unrelated old order) rejected a create the
    daemon actually accepted — the order existed on the daemon while the
    app reported an error, so the user retried and created duplicates
  • a stale NewOrder confirmation (replayed from a previous session)
    confirmed a create the daemon actually rejected — the app navigated
    to My Order showing an old, unrelated order as "just created", while
    the genuine CantDo was discarded as stale

Key reuse (fixed in #171 ) was the fuel; this PR fixes the correlation
itself, which still matters for re-imported mnemonics (no
last-trade-index resync yet) and any future counter regression.

Fix — two independent layers

fix(orders): correlate create_order confirmations by request_id

  • the outgoing new-order now carries a random request_id nonce;
    mostrod echoes it in both the NewOrder confirmation
    (publish_order) and every CantDo rejection (enqueue_cant_do_msg)
  • the pending waiter records its nonce and is only resolved by a reply
    that echoes it (take_matching_confirmation); stale events — no or
    foreign request_id — leave the waiter in place for the genuine
    reply; late-reconciliation / no-waiter paths are unchanged
  • note: validate_response still gets None because it short-circuits
    on CantDo before comparing request_ids; correlation lives at the
    waiter arms (documented in code). This closes the gap flagged by the
    existing "does not yet track outstanding request_ids" comment.

fix(orders): stop replaying stored history into per-trade subscriptions

  • the ephemeral per-trade reply filter gets since(now - 60s); freshly
    derived keys have no history, so this guards the key-reuse cases
    (mnemonic re-import, counter regression) where the key's full reply
    history sits on the relays
  • since is a lower bound on created_at, not a delivery deadline:
    live and late-arriving replies pass, and reconnects replay anything
    newer than the cutoff
  • no events are lost: offline catch-up is the global feed's job
    (subscribe_node_filters), which deliberately has no cutoff and
    keeps replaying full history — documented on both filters

Testing

  • cargo test (98 passed): wrap/unwrap roundtrip proving the nonce
    travels in the message; request_id_matches cases; waiter left
    intact by stale/foreign ids and consumed exactly once by the echoed
    nonce; 60s-window helper. cargo clippy clean on touched files.
    • genuine CantDo (OutOfRangeSatsAmount) resolves the waiter → real
      error shown
    • daemon down → NoDaemonResponse after 10s, nothing persisted
    • genuine NewOrder resolves the waiter → correct order confirmed
    • replayed historical events arrive only on the global feed and land
      harmlessly ("no waiting caller"); the per-trade feed receives only
      the fresh reply

No FRB regeneration needed: no rust/src/api/ signatures changed.

Summary by CodeRabbit

  • New Features
    • Outgoing order creation now includes a unique per-request identifier to improve end-to-end correlation.
  • Bug Fixes
    • Confirmation replies are matched strictly to the expected request identifier, preventing stale or unrelated events from completing a request.
    • Live reply handling is now resilient to replayed history, reducing incorrect waiter resolution and improving stored-order reconciliation safety.
  • Tests
    • Added/expanded tests covering request-id matching, stale-event handling, timeout behavior, concurrent creates, and active-waiter skipping.

Catrya added 2 commits July 9, 2026 12:36
- send a random request_id nonce in the outgoing new-order message;
  the daemon echoes it in both the NewOrder confirmation and any
  CantDo rejection (mostrod publish_order / enqueue_cant_do_msg)
- the pending create_order waiter now records its nonce and is only
  resolved by a reply that echoes it: stale relay-replayed events
  (no or foreign request_id) leave the waiter in place for the
  genuine reply instead of falsely rejecting or falsely confirming
  a live create
- late-reconciliation and no-waiter paths are unchanged
- add a since(now - 60s) cutoff to the per-trade kind-14 reply filter:
  freshly derived keys have no stored history, so this guards the key
  reuse cases — a mnemonic re-imported without last-trade-index resync
  re-derives keys whose full reply history sits on the relays, and any
  future counter regression does the same; replayed replies are what
  used to falsely resolve waiting create_order calls
- since is a lower bound on created_at, not a delivery deadline: live
  and late-arriving replies pass; offline catch-up stays on the global
  feed, which deliberately has no cutoff (documented on both filters)
- defense in depth with the request_id correlation from the previous
  commit, for relays that ignore since
@coderabbitai

coderabbitai Bot commented Jul 9, 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: e5744d0b-79c0-411d-b66e-d1991c455938

📥 Commits

Reviewing files that changed from the base of the PR and between df40fe9 and 2ee1daf.

📒 Files selected for processing (2)
  • rust/src/api/orders.rs
  • rust/src/frb_generated.rs

Walkthrough

Adds per-attempt request_id correlation to order creation. Pending creates retain trade metadata, waiter state, and reconciliation data; daemon replies, timeouts, replay handling, and Kind 38383 reconciliation now use nonce and ownership checks. Generated Flutter Rust bridge dispatch metadata is also updated.

Changes

Order confirmation correlation

Layer / File(s) Summary
Thread request IDs through new-order messages
rust/src/mostro/actions.rs
new_order accepts request_id, writes it into the outgoing message, and tests the decrypted metadata.
Register and manage pending creates
rust/src/api/orders.rs
PendingCreate tracks request IDs, trade metadata, local UUIDs, content keys, and optional waiters; create, publish failure, timeout, subscription, and cleanup paths update this state.
Match replies and reconcile order state
rust/src/api/orders.rs
NewOrder and CantDo require matching nonces, stored-ID rebinding is ownership-gated, and Kind 38383 lookup skips active waiters; tests cover stale replies, concurrent creates, timeouts, and reconciliation.
Update generated bridge dispatch
rust/src/frb_generated.rs
The bridge content hash changes, resolve_maker_order wiring is removed, and subsequent dispatcher IDs are renumbered.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
    participant App as create_order
    participant Pending as PendingCreate
    participant Relay
    participant Daemon

    App->>Pending: register trade key and request_id
    App->>Relay: subscribe live kind-14 replies
    App->>Daemon: publish NewOrder(request_id)
    Relay->>App: NewOrder or CantDo(request_id)
    App->>Pending: take_matching_create(trade_key, request_id)
    alt matching request_id
        Pending-->>App: resolve waiter or retain detached record
    else stale request_id
        Pending-->>App: preserve pending record
    end
Loading

Possibly related issues

  • #168: Directly tracks stale relay-replayed confirmations and rejections addressed by nonce matching and live-only subscriptions.

Possibly related PRs

  • MostroP2P/app#80: Overlaps the new_order and order-creation message construction paths.
  • MostroP2P/app#81: Overlaps pending-create content-key reconciliation behavior.
  • MostroP2P/app#159: Overlaps timeout handling and pending confirmation reconciliation.

Poem

A rabbit sends a nonce so bright,
Old relay ghosts fade from sight.
Waiters wake for matching news,
Stale replies lose what they’d abuse.
Orders reconcile, clean and true—
Hop hooray for request_id too!

🚥 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 clearly captures the main fix: correlating create_order replies by request_id and blocking stale relay replays.
Linked Issues check ✅ Passed The PR satisfies #168 by adding request_id correlation, ignoring stale replayed replies, making per-trade subscriptions live-only, and preserving global catch-up.
Out of Scope Changes check ✅ Passed The changes stay focused on create_order correlation and replay handling; the bridge update follows the new request_id parameter.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch create-order-reply-correlation

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.

@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 `@rust/src/api/orders.rs`:
- Around line 90-92: The fixed 60-second lookback in reply_subscription_since
can miss valid replies when the client clock is ahead of the daemon. Update the
since calculation to use daemon-aligned time or widen the window, and make sure
create_order consumes that adjusted timestamp so the reply subscription does not
time out on skewed hosts.
🪄 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: b29023a5-2ac6-4f9e-8e06-422bded05b5f

📥 Commits

Reviewing files that changed from the base of the PR and between 129bd5a and df40fe9.

📒 Files selected for processing (2)
  • rust/src/api/orders.rs
  • rust/src/mostro/actions.rs

Comment thread rust/src/api/orders.rs Outdated
…skew

- 60s could silently drop the daemon's live reply on devices whose
  clock runs ahead of the daemon's: since is a delivery-level filter
  (relays match live events against it too) and the reply's created_at
  is stamped by the daemon's clock, so every create would time out on
  a skewed device with no diagnostic trail
- 600s comfortably covers realistic mobile clock skew; widening is
  nearly free because request_id correlation is what protects the
  waiter — the cutoff only trims stored-history replay on reused keys

@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

The request-id correlation is good for the live waiter, but the no-waiter fallback in Action::NewOrder still accepts any replayed confirmation and can rebind the local order to the wrong daemon UUID after the 10s timeout. That leaves a stale replay path that bypasses the new guard, so the fix is incomplete.

Catrya added 3 commits July 10, 2026 14:30
- limit(0) makes the per-trade subscription live-only: no stored history,
  and unlike since it never drops the genuine reply under client clock skew
- removes reply_subscription_since and its test; request_id correlation
  remains the primary protection (also resolves the CodeRabbit note)
- same pattern as mostro-cli and MostriX waiter subscriptions; offline
  catch-up stays on the global feed, which replays history with no cutoff
- collapse PENDING_CONFIRMATIONS, PENDING_MAKER_KEYS and PENDING_LOCAL_IDS
  into one PendingCreate record per attempt, keyed by its fresh trade key
- NewOrder/CantDo side effects (trade-key binding, waiter, local→daemon id
  bridge) now require the echoed request_id, closing the paths where a stale
  replay could consume state belonging to a live or concurrent create
- the 10s timeout detaches only the waiter channel: a genuine late reply
  still reconciles, and the record dies with the per-trade subscription
- the Kind 38383 path matches by content fingerprint and skips records with
  a live waiter; the arbitrary any-entry fallback is gone
- absorb resolve_maker_order (unused from Dart) and regenerate the bridge;
  widen gift-wrap dedup to 512 entries; add correlation tests
- gate the pre-dispatch local→daemon id reconciliation on ownership: the
  stored id must be the local UUID of this trade key's own pending create
- without the gate, a stale replay carrying an old order id for a reused
  trade index could rebind a confirmed order — daemon→daemon — corrupting
  the order book, trade row and trade-key mapping
- cold start is unaffected: the pending map is empty after a restart and
  the Kind 38383 fingerprint path owns maker recovery there
- extract the decision into may_reconcile_stored_id with a unit test

@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.

Reviewed the current head. The request_id correlation is wired through create_order, stale relay replays no longer consume the pending create, and the per-trade subscription avoids replaying stored history. I also ran targeted Rust tests for the new correlation helpers and they passed. No blocking issues found.

@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.

I re-reviewed the current head strictly, with the failure mode in mind where stale relay-replayed events could consume pending create-order state.

What I checked:

  • The outgoing new_order payload now carries request_id and trade_index on the wire; the correlation nonce is not reconstructed later.
  • take_matching_create(...) only consumes the pending record when the echoed request_id matches exactly. Missing or mismatched ids leave the record in place.
  • detach_create_waiter(...) only clears the waiter channel on timeout. It does not delete the record, so a late genuine reply can still reconcile.
  • take_pending_create_by_content_key(...) ignores live waiters (tx.is_some()), which prevents the content-key path from stealing an in-flight create.
  • may_reconcile_stored_id(...) only allows rebinding when the stored id is the pending local UUID. That prevents stale replayed identifiers from rewriting a confirmed order.

The targeted tests cover the important edges:

  • late genuine reply after timeout
  • stale / mismatched request_id ignored
  • concurrent creates do not cross-consume
  • content-key lookup skips live waiters

I do not see a blocking correctness issue in the current head. The key invariant is consistent: only a reply that echoes the exact request_id may resolve or consume the pending create.

@Catrya Catrya merged commit b6f78f8 into main Jul 11, 2026
1 check passed
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.

create_order can be falsely rejected or falsely confirmed by stale relay-replayed daemon messages

1 participant