Skip to content

feat(nip59): migrate gift-wrap transport to mostro-core 0.9 - #102

Merged
grunch merged 4 commits into
mainfrom
feat/migrate-nip59-mostro-core-0.9
Apr 23, 2026
Merged

feat(nip59): migrate gift-wrap transport to mostro-core 0.9#102
grunch merged 4 commits into
mainfrom
feat/migrate-nip59-mostro-core-0.9

Conversation

@grunch

@grunch grunch commented Apr 22, 2026

Copy link
Copy Markdown
Member

Replace the hand-rolled NIP-59 wrap/unwrap in rust/src/nostr/gift_wrap.rs with thin shims over mostro_core::nip59::{wrap_message, unwrap_message, validate_response}, so every Mostro client shares one implementation of seal construction, ephemeral keys, timestamp tweak, PoW, and inner-tuple signing/verification.

Scope is limited to typed Message traffic with the Mostro daemon. The Kind 14 text DM paths in messages.rs (P2P chat) and disputes.rs (admin escalation) wrap raw {"text": …} JSON and stay on the legacy local helper until mostro-core grows a DM variant.

Notable behavior changes on the inbound path:

  • Gift-wraps now authenticate the sender: responses whose sender is not the configured active Mostro pubkey are rejected and logged. Previously the only check was "it decrypted under one of our trade keys".
  • validate_response(&msg, None) runs on every unwrapped message, short-circuiting CantDo responses centrally. request_id tracking is a follow-up (see issue Migrate NIP-59 gift-wrap transport to mostro-core 0.9 (wrap_message / unwrap_message / validate_response) #101 §5).
  • The per-trade and global subscriptions can now distinguish "wrap was not addressed to this key" (Ok(None)) from "corrupted wrap" (Err).

Outbound actions.rs builders keep their Result<String> signature via event.as_json() so call sites in orders.rs / disputes.rs are unchanged; the (Message, Option<Peer>) tuple (de)serialization that used to straddle the wrap and unwrap paths is gone.

Adds unit tests covering round-trip, Ok(None) on wrong recipient, and PoW difficulty propagation to the outer 1059 event.

Refs: #101

Summary by CodeRabbit

  • Chores

    • Updated mostro-core dependency from 0.8.0 to 0.9.0.
  • Bug Fixes

    • Refactored gift-wrap message processing for improved validation and error handling.
    • Enhanced message authentication and centralized validation routing.

Replace the hand-rolled NIP-59 wrap/unwrap in `rust/src/nostr/gift_wrap.rs`
with thin shims over `mostro_core::nip59::{wrap_message, unwrap_message,
validate_response}`, so every Mostro client shares one implementation of
seal construction, ephemeral keys, timestamp tweak, PoW, and inner-tuple
signing/verification.

Scope is limited to typed `Message` traffic with the Mostro daemon. The
Kind 14 text DM paths in `messages.rs` (P2P chat) and `disputes.rs` (admin
escalation) wrap raw `{"text": …}` JSON and stay on the legacy local
helper until `mostro-core` grows a DM variant.

Notable behavior changes on the inbound path:

- Gift-wraps now authenticate the sender: responses whose `sender` is not
  the configured active Mostro pubkey are rejected and logged. Previously
  the only check was "it decrypted under one of our trade keys".
- `validate_response(&msg, None)` runs on every unwrapped message,
  short-circuiting `CantDo` responses centrally. `request_id` tracking is
  a follow-up (see issue #101 §5).
- The per-trade and global subscriptions can now distinguish "wrap was
  not addressed to this key" (`Ok(None)`) from "corrupted wrap" (`Err`).

Outbound `actions.rs` builders keep their `Result<String>` signature via
`event.as_json()` so call sites in `orders.rs` / `disputes.rs` are
unchanged; the `(Message, Option<Peer>)` tuple (de)serialization that
used to straddle the wrap and unwrap paths is gone.

Adds unit tests covering round-trip, `Ok(None)` on wrong recipient, and
PoW difficulty propagation to the outer 1059 event.

Refs: #101
@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@grunch has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 21 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 44 minutes and 21 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e151caad-68df-421d-a32f-a7ec6460e20b

📥 Commits

Reviewing files that changed from the base of the PR and between 208294c and 8d7b219.

📒 Files selected for processing (5)
  • rust/Cargo.toml
  • rust/src/api/orders.rs
  • rust/src/config.rs
  • rust/src/mostro/actions.rs
  • rust/src/nostr/gift_wrap.rs

Walkthrough

This PR migrates from mostro-core 0.8.0 to 0.9.0 and refactors NIP-59 gift-wrap message handling across multiple Rust modules. Changes introduce new typed Mostro message wrapping/unwrapping APIs, add daemon sender authentication checks, centralize message validation, and replace the old serialization-based flow with strongly-typed Message objects.

Changes

Cohort / File(s) Summary
Dependency Version Bump
rust/Cargo.toml
Updates mostro-core dependency from 0.8.0 to 0.9.0.
NIP-59 Gift Wrap APIs
rust/src/nostr/gift_wrap.rs
Adds two new exported functions: wrap_mostro_message (constructs NIP-59 Gift Wrap events with PoW and WrapOptions) and unwrap_mostro_message (delegates to mostro_core::nip59::unwrap_message). Includes unit tests for roundtrip correctness, wrong-recipient handling, and PoW effect validation.
Action Builder Refactoring
rust/src/mostro/actions.rs
Replaces wrap_message helper with wrap_for_mostro that accepts a mostro_core::Message reference, fetches daemon PoW difficulty via get_pow(), and calls gift_wrap::wrap_mostro_message instead of manual serialization.
Gift-Wrap Dispatcher Refactoring
rust/src/api/orders.rs
Replaces process_gift_wrap_rumor with dispatch_mostro_message flow: both per-trade and global subscription handlers now call unwrap_mostro_message, validate responses via centralized validate_response(&msg, None) gate, authenticate daemon sender against active_mostro_pubkey, and dispatch validated Message objects through existing action routing logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰✨ Wrapped gifts with validation true,
Daemon keys to authenticate you,
NIP-59 in types so strong,
Messages journeying safe along,
From 0.8 to 0.9—pure delight! 🎁

🚥 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(nip59): migrate gift-wrap transport to mostro-core 0.9' clearly and specifically summarizes the main change: migrating NIP-59 gift-wrap functionality to use mostro-core version 0.9, which aligns with all file changes across Cargo.toml, gift_wrap.rs, actions.rs, and orders.rs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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/migrate-nip59-mostro-core-0.9

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: 208294c80c

ℹ️ 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 rust/src/api/orders.rs Outdated
@grunch

grunch commented Apr 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@grunch

grunch commented Apr 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Returning early on any `validate_response` error meant `MostroCantDo`
responses never reached the `Action::CantDo` arm in
`dispatch_mostro_message`, so `create_order` callers waiting on a
`pending_confirmations` oneshot were never unblocked — rejected orders
timed out and fell back to the optimistic local-ID path, leaving
phantom pending orders in the book.

Only `MostroInternalErr` (malformed `request_id`, etc.) warrants a
drop. `MostroCantDo` falls through so the existing CantDo handler can
notify the waiting caller with the daemon's reason.

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

🧹 Nitpick comments (6)
rust/Cargo.toml (1)

14-14: Consider "0.9" for caret-compatible patch updates.

mostro-core = "0.9.0" pins to exactly >=0.9.0, <0.10.0 under Cargo's default caret semantics, which is fine — but if you want to automatically pick up 0.9.x bug-fix releases without editing this file, drop the patch and use "0.9". Purely a dependency-hygiene preference; no functional impact.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/Cargo.toml` at line 14, Update the dependency spec for mostro-core in
Cargo.toml: replace the exact version string mostro-core = "0.9.0" with the
caret-compatible shorthand mostro-core = "0.9" so Cargo will accept 0.9.x patch
updates automatically.
rust/src/nostr/gift_wrap.rs (2)

43-49: Doc overstates what unwrap_mostro_message guarantees.

The comment claims that "sender mismatch" surfaces as Err, but this shim just delegates to mostro_core::nip59::unwrap_message — which verifies the inner-tuple signature against the seal's sender, not against the configured Mostro pubkey. The daemon-authentication check ("sender == active mostro") is actually performed upstream in orders.rs::dispatch_mostro_message, so a wrap authored by a random key that correctly signs its own seal will still return Ok(Some(_)) here. Consider tightening the wording so readers don't assume this layer enforces daemon identity.

📝 Proposed doc tweak
 /// Try to open an incoming Kind 1059 event using `trade_keys`.
 ///
 /// Returns `Ok(None)` only when the outer NIP-44 layer cannot be decrypted
 /// with the given key — the canonical "not addressed to me" signal, used by
 /// the global subscription to trial-decrypt across all derived trade keys.
-/// Every other failure (corrupted seal, malformed rumor, bad signature,
-/// sender mismatch) surfaces as `Err`.
+/// Every other failure (corrupted seal, malformed rumor, bad inner-tuple
+/// signature) surfaces as `Err`. Daemon-identity authentication (rejecting
+/// wraps whose decrypted sender is not the active Mostro pubkey) is the
+/// caller's responsibility — see `orders::dispatch_mostro_message`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/nostr/gift_wrap.rs` around lines 43 - 49, Docstring for
unwrap_mostro_message overclaims sender verification: it does not enforce that
the seal's sender matches the configured/active Mostro key but simply delegates
to mostro_core::nip59::unwrap_message which only verifies the inner signature
against the seal's sender. Update the comment for unwrap_mostro_message to
remove or soften the claim that "sender mismatch surfaces as Err" and instead
state that only the inner signature vs. seal sender is checked here and that
daemon-authentication (ensuring sender == active Mostro) is performed upstream
in orders.rs::dispatch_mostro_message.

185-213: PoW test is mildly flaky and over-asserts leading zeros.

With difficulty = 4 this is usually fast, but PoW mining time is probabilistic and the test has no timeout — on a slow CI runner a bad random seed could occasionally stretch wall time. Also, the leading-zero counter short-circuits the continue logic correctly, but you don't need to hand-roll it: event.id.as_bytes() (or the existing to_bytes()) fed through a standard leading_zeros helper is sufficient. Not blocking; just something to revisit if CI ever goes flaky here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/nostr/gift_wrap.rs` around lines 185 - 213, The test
pow_is_applied_to_outer_event can hang on slow CI and uses a hand-rolled
leading-zero byte loop; wrap the async mining call (wrap_mostro_message) in a
tokio::time::timeout to bound wall time and fail fast, and replace the manual
byte loop over event.id.to_bytes() with a standard aggregation of
byte.leading_zeros() (e.g., sum the leading_zeros for the byte slice until a
non-zero byte) to compute leading_zero_bits before asserting it >= difficulty;
update the test function pow_is_applied_to_outer_event to use timeout and the
new leading-zero computation using event.id.to_bytes() or event.id.as_bytes().
rust/src/api/orders.rs (3)

1063-1066: Unused binding id in PaymentRequest destructure.

id is bound but never read in this debug string — silences a clippy unused_variables warning. Consider _id (or just use pr without destructuring).

📝 Proposed tweak
-        Some(mostro_core::message::Payload::PaymentRequest(id, pr, amt)) => format!(
-            "PaymentRequest(id={id:?}, invoice_len={}, amount={amt:?})",
+        Some(mostro_core::message::Payload::PaymentRequest(_id, pr, amt)) => format!(
+            "PaymentRequest(invoice_len={}, amount={amt:?})",
             pr.len()
         ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/orders.rs` around lines 1063 - 1066, The match arm for
mostro_core::message::Payload::PaymentRequest binds `id` but never uses it,
causing an unused binding warning; change the pattern to ignore the id (e.g.,
use `_id` or `_`) in the destructure for PaymentRequest(id, pr, amt) and keep
the existing format string that uses pr.len() and amt, or alternatively stop
destructuring `id` entirely (PaymentRequest(_, pr, amt)) so the compiler warning
is silenced while leaving `pr` and `amt` intact.

975-986: Ok(None) on the per-trade path warrants debug, not warn.

The per-trade relay filter already matches on this p-tag, so Ok(None) here means the outer NIP-44 layer decrypted "not for us" despite the tag match — most commonly a stale relay echo for a recycled key, or a p-tag collision from another client. Logging every occurrence at warn will produce noisy client logs during normal reconnects/replays. Suggest dropping to debug and keeping warn/error for the decrypt-error branch below.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/orders.rs` around lines 975 - 986, Change the OK(None) branch to
log at debug rather than warn: replace the crate::api::logging::blog_warn call
in the Ok(None) arm with crate::api::logging::blog_debug (keeping the same
message and trade_pubkey_hex formatting), while leaving the Err(e) branch as a
warn; this reduces noisy warnings when the per-trade filter already filtered
"not for us" cases.

1020-1040: Sender-auth gate: good addition; consider logging sender hex at debug instead of warn for UX.

Rejecting wraps whose decrypted seal author is not the active Mostro pubkey is the right call and closes a real attack surface (previously anything decrypting under the trade key was trusted). Two small notes:

  • If the user switches the active Mostro pubkey override (set_active_mostro_pubkey) mid-session, in-flight responses from the previous daemon will now be dropped with a warn! — expected, but worth documenting near the config's override setter so operators don't chase it as a bug.
  • The Err(e) branch at 1034-1039 covers a misconfigured active pubkey — this should probably be louder than blog_warn (an error! or surfaced through the UI), since every inbound response will silently fail until the user fixes their settings.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/orders.rs` around lines 1020 - 1040, Change the sender-mismatch
log from warn to a debug-level message and include the sender hex there
(reference: the match on
nostr_sdk::PublicKey::from_hex(&crate::config::active_mostro_pubkey()) comparing
expected == sender and the blog_warn call that logs
sender/expected/trade_pubkey_hex); also make the Err(e) branch louder by
replacing blog_warn with a blog_error (or equivalent error-level reporting) and
include the error detail e in the message so a misconfigured
active_mostro_pubkey() is prominently reported; additionally add a short comment
near the set_active_mostro_pubkey override setter documenting that in-flight
responses from the prior daemon will be dropped when the override changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@rust/Cargo.toml`:
- Line 14: Update the dependency spec for mostro-core in Cargo.toml: replace the
exact version string mostro-core = "0.9.0" with the caret-compatible shorthand
mostro-core = "0.9" so Cargo will accept 0.9.x patch updates automatically.

In `@rust/src/api/orders.rs`:
- Around line 1063-1066: The match arm for
mostro_core::message::Payload::PaymentRequest binds `id` but never uses it,
causing an unused binding warning; change the pattern to ignore the id (e.g.,
use `_id` or `_`) in the destructure for PaymentRequest(id, pr, amt) and keep
the existing format string that uses pr.len() and amt, or alternatively stop
destructuring `id` entirely (PaymentRequest(_, pr, amt)) so the compiler warning
is silenced while leaving `pr` and `amt` intact.
- Around line 975-986: Change the OK(None) branch to log at debug rather than
warn: replace the crate::api::logging::blog_warn call in the Ok(None) arm with
crate::api::logging::blog_debug (keeping the same message and trade_pubkey_hex
formatting), while leaving the Err(e) branch as a warn; this reduces noisy
warnings when the per-trade filter already filtered "not for us" cases.
- Around line 1020-1040: Change the sender-mismatch log from warn to a
debug-level message and include the sender hex there (reference: the match on
nostr_sdk::PublicKey::from_hex(&crate::config::active_mostro_pubkey()) comparing
expected == sender and the blog_warn call that logs
sender/expected/trade_pubkey_hex); also make the Err(e) branch louder by
replacing blog_warn with a blog_error (or equivalent error-level reporting) and
include the error detail e in the message so a misconfigured
active_mostro_pubkey() is prominently reported; additionally add a short comment
near the set_active_mostro_pubkey override setter documenting that in-flight
responses from the prior daemon will be dropped when the override changes.

In `@rust/src/nostr/gift_wrap.rs`:
- Around line 43-49: Docstring for unwrap_mostro_message overclaims sender
verification: it does not enforce that the seal's sender matches the
configured/active Mostro key but simply delegates to
mostro_core::nip59::unwrap_message which only verifies the inner signature
against the seal's sender. Update the comment for unwrap_mostro_message to
remove or soften the claim that "sender mismatch surfaces as Err" and instead
state that only the inner signature vs. seal sender is checked here and that
daemon-authentication (ensuring sender == active Mostro) is performed upstream
in orders.rs::dispatch_mostro_message.
- Around line 185-213: The test pow_is_applied_to_outer_event can hang on slow
CI and uses a hand-rolled leading-zero byte loop; wrap the async mining call
(wrap_mostro_message) in a tokio::time::timeout to bound wall time and fail
fast, and replace the manual byte loop over event.id.to_bytes() with a standard
aggregation of byte.leading_zeros() (e.g., sum the leading_zeros for the byte
slice until a non-zero byte) to compute leading_zero_bits before asserting it >=
difficulty; update the test function pow_is_applied_to_outer_event to use
timeout and the new leading-zero computation using event.id.to_bytes() or
event.id.as_bytes().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9a360266-59c0-46dd-a248-caaf000620b5

📥 Commits

Reviewing files that changed from the base of the PR and between 648adb7 and 208294c.

⛔ Files ignored due to path filters (1)
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • rust/Cargo.toml
  • rust/src/api/orders.rs
  • rust/src/mostro/actions.rs
  • rust/src/nostr/gift_wrap.rs

grunch added 2 commits April 22, 2026 21:09
- Cargo.toml: use short `mostro-core = "0.9"` for consistency with the
  rest of the deps (`serde = "1"`, `bip32 = "0.5"`, etc.); semantics
  unchanged (`^0.9` == `^0.9.0`).
- orders.rs: demote per-trade `Ok(None)` from `warn` to `debug` — the
  p-tag filter has already narrowed to our key, so this only fires on
  unsolicited wraps that don't decrypt, which a hostile relay could
  cheaply spam.
- config.rs: document that `set_active_mostro_pubkey` override drops
  in-flight responses from the previous daemon (they fail the sender
  check in `dispatch_mostro_message`).
- gift_wrap.rs: tighten the `unwrap_mostro_message` docstring — the
  helper verifies inner-tuple signature against the seal author, not
  against the active Mostro pubkey; daemon authentication is upstream.
- gift_wrap.rs: wrap the PoW mining test in `tokio::time::timeout(30s)`
  and use `u32` for the leading-zero count so a regression can't hang
  CI or silently overflow.

Intentionally skipped:
- "unused `id` binding in `PaymentRequest(id, pr, amt)` match" — `id`
  is consumed via named-capture in the `{id:?}` format string; compiler
  emits no warning.
- "sender-mismatch warn → debug" — any peer can wrap a message to our
  trade key, so the check catches spoofed daemon responses, not just
  override races. Keeping `warn` as a security signal.
The local helper was renamed to wrap_for_mostro during the migration to
avoid an imagined collision with mostro_core::nip59::wrap_message. Since
that symbol is not imported into this module (only mostro_core::message
types and the crate::nostr::gift_wrap module are), there is no collision
— restore the shorter name.
@grunch
grunch merged commit 725c7a7 into main Apr 23, 2026
1 check passed
@grunch
grunch deleted the feat/migrate-nip59-mostro-core-0.9 branch April 23, 2026 00:16
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.

1 participant