Skip to main content

mostro_core/
nip59.rs

1//! NIP-59 GiftWrap transport for Mostro messages.
2//!
3//! Every message exchanged with a Mostro node travels through the same
4//! pipeline:
5//!
6//! ```text
7//! Message -> JSON((Message, Option<Signature>)) -> Rumor -> Seal -> GiftWrap
8//! ```
9//!
10//! This module centralizes wrap/unwrap so clients do not need to reimplement
11//! NIP-59 glue themselves. It does not manage relays, subscriptions, waiters
12//! or persistence — the returned `Event` is ready to publish, and the caller
13//! decides how to do so.
14
15use std::str::FromStr;
16
17use crate::message::{Action, Message, Payload};
18use crate::prelude::{CantDoReason, MostroError, ServiceError};
19use nostr_sdk::nips::{nip44, nip59};
20use nostr_sdk::prelude::*;
21
22/// Options controlling how a Mostro message is wrapped.
23#[derive(Debug, Clone)]
24pub struct WrapOptions {
25    /// NIP-13 proof-of-work difficulty applied to the outer GiftWrap event.
26    pub pow: u8,
27    /// Optional expiration tag for the outer GiftWrap event.
28    pub expiration: Option<Timestamp>,
29    /// When true the inner rumor content is `(Message, Some(Signature))`,
30    /// with the signature produced from the JSON of `Message` using
31    /// `trade_keys`. When false the content is `(Message, None)`. Traffic
32    /// to a Mostro node always uses `true`.
33    pub signed: bool,
34}
35
36impl Default for WrapOptions {
37    fn default() -> Self {
38        Self {
39            pow: 0,
40            expiration: None,
41            signed: true,
42        }
43    }
44}
45
46/// A Mostro message recovered from an incoming GiftWrap, plus metadata from
47/// the outer envelopes.
48#[derive(Debug, Clone)]
49pub struct UnwrappedMessage {
50    /// The logical Mostro message carried inside the rumor.
51    pub message: Message,
52    /// Signature of the JSON-serialized `Message`, produced with the sender's
53    /// trade keys. Present only when the sender set `signed = true`.
54    pub signature: Option<Signature>,
55    /// Rumor author (the sender's trade public key).
56    pub sender: PublicKey,
57    /// Rumor `created_at` timestamp.
58    pub created_at: Timestamp,
59}
60
61/// Build a GiftWrap event (`kind: 1059`) ready to be published to a relay.
62///
63/// * `message` — the Mostro message to send.
64/// * `trade_keys` — per-trade keys. Author of the rumor, signer of the Seal,
65///   and signer of the inner tuple signature when `opts.signed == true`.
66/// * `receiver` — the Mostro node public key.
67/// * `opts` — wrap options (PoW, expiration, signed).
68///
69/// `nostr-sdk` 0.44 enforces that the rumor author equals the seal signer
70/// (NIP-59 `SenderMismatch`), so both layers are signed with `trade_keys`.
71/// The inner tuple signature (produced with `trade_keys`) is what allows
72/// Mostro to cryptographically bind the message to the trade identity.
73pub async fn wrap_message(
74    message: &Message,
75    trade_keys: &Keys,
76    receiver: PublicKey,
77    opts: WrapOptions,
78) -> Result<Event, MostroError> {
79    let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
80
81    let content = if opts.signed {
82        let sig = Message::sign(message_json, trade_keys);
83        serde_json::to_string(&(message, Some(sig.to_string())))
84            .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
85    } else {
86        serde_json::to_string(&(message, Option::<String>::None))
87            .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
88    };
89
90    // PoW only applies to the outer GiftWrap (per WrapOptions docs); the
91    // rumor is encrypted inside the seal and never published on its own,
92    // so mining its event id would burn CPU for nothing.
93    let rumor = EventBuilder::text_note(content).build(trade_keys.public_key());
94
95    let seal: Event = EventBuilder::seal(trade_keys, &receiver, rumor)
96        .await
97        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?
98        .sign(trade_keys)
99        .await
100        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
101
102    gift_wrap_from_seal_with_pow(&seal, receiver, opts.pow, opts.expiration)
103}
104
105/// Wrap an already built Seal into a NIP-59 GiftWrap with optional PoW and
106/// expiration. The outer event is signed with a freshly generated ephemeral
107/// key and carries a mandatory `p` tag pointing at `receiver`.
108fn gift_wrap_from_seal_with_pow(
109    seal: &Event,
110    receiver: PublicKey,
111    pow: u8,
112    expiration: Option<Timestamp>,
113) -> Result<Event, MostroError> {
114    if seal.kind != Kind::Seal {
115        return Err(MostroError::MostroInternalErr(
116            ServiceError::UnexpectedError("expected Seal kind".to_string()),
117        ));
118    }
119
120    let ephemeral = Keys::generate();
121    let encrypted = nip44::encrypt(
122        ephemeral.secret_key(),
123        &receiver,
124        seal.as_json(),
125        nip44::Version::default(),
126    )
127    .map_err(|e| MostroError::MostroInternalErr(ServiceError::EncryptionError(e.to_string())))?;
128
129    let mut tags: Vec<Tag> = Vec::new();
130    if let Some(exp) = expiration {
131        tags.push(Tag::expiration(exp));
132    }
133    tags.push(Tag::public_key(receiver));
134
135    EventBuilder::new(Kind::GiftWrap, encrypted)
136        .tags(tags)
137        .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK))
138        .pow(pow)
139        .sign_with_keys(&ephemeral)
140        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))
141}
142
143/// Try to open an incoming GiftWrap with the given `trade_keys`.
144///
145/// Returns `Ok(None)` only when the outer NIP-44 layer could not be
146/// decrypted with `trade_keys` — the canonical "not addressed to me"
147/// signal, so callers can try multiple trade keys without treating each
148/// miss as fatal. Every other failure (corrupted seal, malformed rumor
149/// JSON, seal/rumor pubkey mismatch, invalid signatures, etc.) yields
150/// `Err` so callers can tell "not mine" apart from "broken".
151pub async fn unwrap_message(
152    event: &Event,
153    trade_keys: &Keys,
154) -> Result<Option<UnwrappedMessage>, MostroError> {
155    if event.kind != Kind::GiftWrap {
156        return Err(MostroError::MostroInternalErr(
157            ServiceError::UnexpectedError("event is not a GiftWrap".to_string()),
158        ));
159    }
160
161    let unwrapped = match nip59::extract_rumor(trade_keys, event).await {
162        Ok(u) => u,
163        // Outer NIP-44 decrypt failed — wrap was not for us.
164        Err(nip59::Error::Signer(_)) => return Ok(None),
165        Err(e) => {
166            return Err(MostroError::MostroInternalErr(ServiceError::NostrError(
167                e.to_string(),
168            )));
169        }
170    };
171
172    let (message, sig_str): (Message, Option<String>) =
173        serde_json::from_str(&unwrapped.rumor.content)
174            .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?;
175
176    let signature = match sig_str {
177        Some(s) => {
178            let sig = Signature::from_str(&s).map_err(|e| {
179                MostroError::MostroInternalErr(ServiceError::UnexpectedError(format!(
180                    "malformed rumor signature: {e}"
181                )))
182            })?;
183            let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
184            if !Message::verify_signature(message_json, unwrapped.sender, sig) {
185                return Err(MostroError::MostroInternalErr(
186                    ServiceError::UnexpectedError(
187                        "rumor signature does not verify against sender".to_string(),
188                    ),
189                ));
190            }
191            Some(sig)
192        }
193        None => None,
194    };
195
196    Ok(Some(UnwrappedMessage {
197        message,
198        signature,
199        sender: unwrapped.sender,
200        created_at: unwrapped.rumor.created_at,
201    }))
202}
203
204/// Validate a response received from a Mostro node.
205///
206/// * Returns `Err(MostroCantDo(reason))` when the payload is `CantDo`.
207/// * Returns `Err(MostroInternalErr(...))` when `expected_request_id` is
208///   provided and the inner message carries a different id, or no id at all
209///   on an action that requires one.
210/// * Otherwise returns `Ok(())`.
211///
212/// The allow-list of actions that may arrive without a `request_id` (server
213/// push messages such as state transitions, DMs, payment failures, etc.) is
214/// intentionally kept on the caller side, because the exact set depends on
215/// the client flow; this function only enforces the universal rules.
216pub fn validate_response(
217    message: &Message,
218    expected_request_id: Option<u64>,
219) -> Result<(), MostroError> {
220    let inner = message.get_inner_message_kind();
221
222    if let Some(Payload::CantDo(reason)) = &inner.payload {
223        return Err(MostroError::MostroCantDo(
224            reason.clone().unwrap_or(CantDoReason::InvalidAction),
225        ));
226    }
227
228    if let Some(expected) = expected_request_id {
229        match inner.request_id {
230            Some(got) if got == expected => {}
231            Some(_) => {
232                return Err(MostroError::MostroInternalErr(
233                    ServiceError::UnexpectedError("mismatched request_id".to_string()),
234                ));
235            }
236            None => {
237                if !action_accepts_missing_request_id(&inner.action) {
238                    return Err(MostroError::MostroInternalErr(
239                        ServiceError::UnexpectedError(
240                            "missing request_id on a response that requires one".to_string(),
241                        ),
242                    ));
243                }
244            }
245        }
246    }
247
248    Ok(())
249}
250
251/// Actions that may legitimately arrive without a `request_id` even when the
252/// caller was waiting on one (unsolicited server-initiated events).
253fn action_accepts_missing_request_id(action: &Action) -> bool {
254    matches!(
255        action,
256        Action::BuyerTookOrder
257            | Action::HoldInvoicePaymentAccepted
258            | Action::HoldInvoicePaymentSettled
259            | Action::HoldInvoicePaymentCanceled
260            | Action::WaitingSellerToPay
261            | Action::WaitingBuyerInvoice
262            | Action::BuyerInvoiceAccepted
263            | Action::PurchaseCompleted
264            | Action::Released
265            | Action::FiatSentOk
266            | Action::Canceled
267            | Action::CooperativeCancelInitiatedByPeer
268            | Action::CooperativeCancelAccepted
269            | Action::DisputeInitiatedByPeer
270            | Action::AdminSettled
271            | Action::AdminCanceled
272            | Action::AdminTookDispute
273            | Action::PaymentFailed
274            | Action::InvoiceUpdated
275            | Action::Rate
276            | Action::RateReceived
277            | Action::SendDm
278    )
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::message::{Action, MessageKind, Payload};
285    use uuid::uuid;
286
287    fn sample_order_message(request_id: Option<u64>) -> Message {
288        let peer = crate::message::Peer::new(
289            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
290            None,
291        );
292        Message::Order(MessageKind::new(
293            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
294            request_id,
295            Some(1),
296            Action::FiatSentOk,
297            Some(Payload::Peer(peer)),
298        ))
299    }
300
301    #[tokio::test]
302    async fn wrap_then_unwrap_roundtrip() {
303        let trade_keys = Keys::generate();
304        let receiver_keys = Keys::generate();
305
306        let message = sample_order_message(Some(42));
307
308        let wrapped = wrap_message(
309            &message,
310            &trade_keys,
311            receiver_keys.public_key(),
312            WrapOptions::default(),
313        )
314        .await
315        .expect("wrap");
316
317        assert_eq!(wrapped.kind, Kind::GiftWrap);
318        assert!(wrapped
319            .tags
320            .iter()
321            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("p")));
322
323        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
324            .await
325            .expect("unwrap result")
326            .expect("unwrap some");
327
328        assert_eq!(unwrapped.sender, trade_keys.public_key());
329        assert_eq!(
330            unwrapped.message.as_json().unwrap(),
331            message.as_json().unwrap()
332        );
333        assert!(unwrapped.signature.is_some());
334    }
335
336    #[tokio::test]
337    async fn unwrap_with_corrupted_seal_returns_err() {
338        let receiver_keys = Keys::generate();
339        let ephemeral = Keys::generate();
340
341        // GiftWrap addressed to `receiver_keys` whose outer ciphertext
342        // decrypts successfully but yields a string that is not a valid
343        // seal Event JSON. `extract_rumor` must surface this as an error,
344        // not be silently absorbed as `Ok(None)`.
345        let encrypted = nip44::encrypt(
346            ephemeral.secret_key(),
347            &receiver_keys.public_key(),
348            "not a seal",
349            nip44::Version::default(),
350        )
351        .expect("encrypt");
352
353        let corrupted = EventBuilder::new(Kind::GiftWrap, encrypted)
354            .tags([Tag::public_key(receiver_keys.public_key())])
355            .sign_with_keys(&ephemeral)
356            .expect("sign");
357
358        let result = unwrap_message(&corrupted, &receiver_keys).await;
359        assert!(
360            matches!(result, Err(MostroError::MostroInternalErr(_))),
361            "expected Err for corrupted gift wrap, got {result:?}",
362        );
363    }
364
365    // Build a GiftWrap by hand with a custom inner rumor tuple so tests
366    // can inject a malformed or wrong-signature payload that `wrap_message`
367    // would never emit.
368    async fn wrap_with_raw_inner(
369        trade_keys: &Keys,
370        receiver: PublicKey,
371        inner: (&Message, Option<String>),
372    ) -> Event {
373        let content = serde_json::to_string(&inner).unwrap();
374        let rumor = EventBuilder::text_note(content).build(trade_keys.public_key());
375        let seal = EventBuilder::seal(trade_keys, &receiver, rumor)
376            .await
377            .unwrap()
378            .sign(trade_keys)
379            .await
380            .unwrap();
381        gift_wrap_from_seal_with_pow(&seal, receiver, 0, None).unwrap()
382    }
383
384    #[tokio::test]
385    async fn unwrap_with_malformed_signature_errors() {
386        let trade_keys = Keys::generate();
387        let receiver_keys = Keys::generate();
388        let msg = sample_order_message(Some(1));
389
390        let wrapped = wrap_with_raw_inner(
391            &trade_keys,
392            receiver_keys.public_key(),
393            (&msg, Some("not-a-hex-signature".to_string())),
394        )
395        .await;
396
397        let result = unwrap_message(&wrapped, &receiver_keys).await;
398        assert!(
399            matches!(result, Err(MostroError::MostroInternalErr(_))),
400            "malformed signature must surface as Err, got {result:?}",
401        );
402    }
403
404    #[tokio::test]
405    async fn unwrap_with_signature_for_other_content_errors() {
406        let trade_keys = Keys::generate();
407        let receiver_keys = Keys::generate();
408        let msg = sample_order_message(Some(1));
409        // Well-formed signature, but over a completely different payload.
410        let bogus = Message::sign("not the real message".to_string(), &trade_keys);
411
412        let wrapped = wrap_with_raw_inner(
413            &trade_keys,
414            receiver_keys.public_key(),
415            (&msg, Some(bogus.to_string())),
416        )
417        .await;
418
419        let result = unwrap_message(&wrapped, &receiver_keys).await;
420        assert!(
421            matches!(result, Err(MostroError::MostroInternalErr(_))),
422            "non-verifying signature must surface as Err, got {result:?}",
423        );
424    }
425
426    #[tokio::test]
427    async fn unwrap_with_wrong_trade_keys_returns_none() {
428        let trade_keys = Keys::generate();
429        let receiver_keys = Keys::generate();
430        let stranger_keys = Keys::generate();
431
432        let wrapped = wrap_message(
433            &sample_order_message(Some(1)),
434            &trade_keys,
435            receiver_keys.public_key(),
436            WrapOptions::default(),
437        )
438        .await
439        .expect("wrap");
440
441        let result = unwrap_message(&wrapped, &stranger_keys)
442            .await
443            .expect("call should not error");
444        assert!(result.is_none());
445    }
446
447    #[tokio::test]
448    async fn signature_is_verifiable_with_trade_pubkey() {
449        let trade_keys = Keys::generate();
450        let receiver_keys = Keys::generate();
451        let message = sample_order_message(Some(7));
452
453        let wrapped = wrap_message(
454            &message,
455            &trade_keys,
456            receiver_keys.public_key(),
457            WrapOptions::default(),
458        )
459        .await
460        .unwrap();
461
462        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
463            .await
464            .unwrap()
465            .unwrap();
466
467        let sig = unwrapped.signature.expect("signed");
468        let json = unwrapped.message.as_json().unwrap();
469        assert!(Message::verify_signature(
470            json,
471            trade_keys.public_key(),
472            sig
473        ));
474    }
475
476    #[tokio::test]
477    async fn unsigned_wrap_has_no_signature() {
478        let trade_keys = Keys::generate();
479        let receiver_keys = Keys::generate();
480
481        let wrapped = wrap_message(
482            &sample_order_message(Some(3)),
483            &trade_keys,
484            receiver_keys.public_key(),
485            WrapOptions {
486                signed: false,
487                ..WrapOptions::default()
488            },
489        )
490        .await
491        .expect("wrap");
492
493        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
494            .await
495            .unwrap()
496            .unwrap();
497        assert!(unwrapped.signature.is_none());
498    }
499
500    #[tokio::test]
501    async fn expiration_tag_is_set_when_provided() {
502        let trade_keys = Keys::generate();
503        let receiver_keys = Keys::generate();
504        let exp = Timestamp::from_secs(Timestamp::now().as_secs() + 3600);
505
506        let wrapped = wrap_message(
507            &sample_order_message(Some(1)),
508            &trade_keys,
509            receiver_keys.public_key(),
510            WrapOptions {
511                expiration: Some(exp),
512                ..WrapOptions::default()
513            },
514        )
515        .await
516        .expect("wrap");
517
518        let has_expiration = wrapped
519            .tags
520            .iter()
521            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("expiration"));
522        assert!(has_expiration);
523    }
524
525    #[test]
526    fn validate_response_cant_do_short_circuits() {
527        let msg = Message::cant_do(
528            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
529            Some(5),
530            Some(Payload::CantDo(Some(CantDoReason::NotAuthorized))),
531        );
532        let err = validate_response(&msg, Some(5)).unwrap_err();
533        match err {
534            MostroError::MostroCantDo(CantDoReason::NotAuthorized) => {}
535            _ => panic!("expected CantDo(NotAuthorized)"),
536        }
537    }
538
539    #[test]
540    fn validate_response_request_id_match() {
541        let msg = sample_order_message(Some(9));
542        validate_response(&msg, Some(9)).unwrap();
543    }
544
545    #[test]
546    fn validate_response_request_id_mismatch_errors() {
547        let msg = sample_order_message(Some(9));
548        let err = validate_response(&msg, Some(10)).unwrap_err();
549        assert!(matches!(err, MostroError::MostroInternalErr(_)));
550    }
551
552    #[test]
553    fn validate_response_allows_unsolicited_actions_without_request_id() {
554        let msg = Message::Order(MessageKind::new(
555            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
556            None,
557            None,
558            Action::BuyerTookOrder,
559            None,
560        ));
561        validate_response(&msg, Some(1)).unwrap();
562    }
563
564    #[test]
565    fn validate_response_with_no_expected_id_is_ok() {
566        let msg = sample_order_message(None);
567        validate_response(&msg, None).unwrap();
568    }
569}