Skip to main content

nostro2_nips/nip_104/
manager.rs

1//! NIP-104 — Session manager (multi-device fan-out).
2//!
3//! [`crate::nip_104`] gives a single 1:1 [`Session`]; [`crate::nip_104::Invite`]
4//! bootstraps one from a QR/URL/event. Real chat clients juggle *many*
5//! sessions at once: a peer may run several devices, and each device is a
6//! separate ratchet. This module is the routing layer that sits on top —
7//! a dependency-light, synchronous distillation of the reference
8//! `SessionManager` from `mmalmi/nostr-double-ratchet`.
9//!
10//! The reference class also owns storage adapters, async pub/sub, message
11//! queues, receipts and expiration policy — all *runtime* concerns an
12//! application supplies. What is portable (and interop-critical) is the pure
13//! state machine:
14//!
15//! * **track** sessions keyed by `(peer, device)`,
16//! * **route** an inbound kind-1060 event to whichever session can decrypt it,
17//! * **fan out** an outbound message to *every* device a peer has, and
18//! * **bootstrap** new sessions from invites.
19//!
20//! Everything here is in-memory and side-effect free: methods return the
21//! events to publish (or the message decrypted), leaving transport, storage
22//! and scheduling to the caller. That keeps it `no_std`-friendly in spirit and
23//! trivially testable without a relay.
24//!
25//! ```ignore
26//! let mut alice = SessionManager::new(alice_identity);
27//! let response = alice.accept_invite(&bobs_invite, None, now)?; // publish it
28//! // …bob calls receive_invite_response with the same event…
29//! for event in alice.send(&bob_pubkey, b"hi", now)? { publish(event); }
30//! if let Some(msg) = alice.process_event(&incoming) { /* msg.plaintext */ }
31//! ```
32
33use std::collections::BTreeMap;
34
35use super::{Invite, MESSAGE_EVENT_KIND, Nip104Error, Session};
36use nostro2_traits::NostrKeypair;
37
38type Result<T> = std::result::Result<T, Nip104Error>;
39
40/// A message successfully decrypted by [`SessionManager::process_event`].
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ReceivedMessage {
43    /// The peer (owner) public key the session belongs to.
44    pub peer: String,
45    /// The specific device id (device identity pubkey) that sent it.
46    pub device_id: String,
47    /// The decrypted plaintext.
48    pub plaintext: Vec<u8>,
49}
50
51/// All sessions we hold for a single peer, one per device.
52#[derive(Debug, Clone)]
53struct PeerRecord<K: NostrKeypair> {
54    devices: BTreeMap<String, Session<K>>,
55}
56
57impl<K: NostrKeypair> Default for PeerRecord<K> {
58    fn default() -> Self {
59        Self {
60            devices: BTreeMap::new(),
61        }
62    }
63}
64
65/// Identifies one session: its peer (owner) key and device id.
66type SessionKey = (String, String);
67
68/// Routes double-ratchet sessions across many peers and devices.
69///
70/// Generic over the in-process keypair `K` exactly like [`Session`], so it
71/// works with the production `K256Keypair` and any test signer alike.
72///
73/// ## Inbound routing index
74///
75/// A naive router trial-decrypts an inbound event against every held session
76/// — O(sessions) Schnorr verifies per event, which a stranger can weaponise.
77/// Instead we keep `sender_index`: a map from each peer DH key a session will
78/// accept (see [`Session::accepted_senders`]) to that session's
79/// `(peer, device)`. Because DH ephemeral keys are globally unique, the map is
80/// an *exact* index — a message's `sender` routes to at most one session in
81/// O(log n), and a foreign sender misses instantly. The index is refreshed
82/// from a session's accepted set whenever it is installed or advances on
83/// receive.
84#[derive(Debug, Clone)]
85pub struct SessionManager<K: NostrKeypair> {
86    identity: K,
87    our_pubkey: String,
88    peers: BTreeMap<String, PeerRecord<K>>,
89    /// peer DH sender key (x-only hex) → the session that accepts it.
90    sender_index: BTreeMap<String, SessionKey>,
91}
92
93impl<K: NostrKeypair> SessionManager<K> {
94    /// Create a manager owning our long-term `identity` keypair (used to
95    /// authenticate invite handshakes).
96    #[must_use]
97    pub fn new(identity: K) -> Self {
98        let our_pubkey = identity.public_key();
99        Self {
100            identity,
101            our_pubkey,
102            peers: BTreeMap::new(),
103            sender_index: BTreeMap::new(),
104        }
105    }
106
107    /// Our identity public key (x-only hex).
108    #[must_use]
109    pub fn our_pubkey(&self) -> &str {
110        &self.our_pubkey
111    }
112
113    /// Whether we currently hold at least one session with `peer`.
114    #[must_use]
115    pub fn has_session(&self, peer: &str) -> bool {
116        self.peers.get(peer).is_some_and(|p| !p.devices.is_empty())
117    }
118
119    /// The peers (owner pubkeys) we hold sessions with, sorted.
120    pub fn peers(&self) -> impl Iterator<Item = &String> {
121        self.peers.keys()
122    }
123
124    /// The device ids we hold sessions with for `peer`, sorted.
125    #[must_use]
126    pub fn devices(&self, peer: &str) -> Vec<String> {
127        self.peers
128            .get(peer)
129            .map(|p| p.devices.keys().cloned().collect())
130            .unwrap_or_default()
131    }
132
133    /// Total number of sessions across all peers and devices.
134    #[must_use]
135    pub fn session_count(&self) -> usize {
136        self.peers.values().map(|p| p.devices.len()).sum()
137    }
138
139    /// Install (or replace) a session for `(peer, device_id)` directly — for
140    /// restoring persisted state or wiring an externally-bootstrapped session.
141    pub fn install_session(&mut self, peer: &str, device_id: &str, session: Session<K>) {
142        let slot = (peer.to_owned(), device_id.to_owned());
143        // Drop any stale index rows pointing at this slot before we overwrite
144        // it, then index the new session's accepted senders.
145        self.forget_in_index(&slot);
146        for sender in session.accepted_senders() {
147            self.sender_index.insert(sender, slot.clone());
148        }
149        self.peers
150            .entry(peer.to_owned())
151            .or_default()
152            .devices
153            .insert(device_id.to_owned(), session);
154    }
155
156    /// Enumerate every held session as `((peer, device_id), &SessionState)`,
157    /// in sorted `(peer, device)` order — the inverse of [`install_session`].
158    ///
159    /// Exposed for persistence: a caller can snapshot each session's
160    /// [`SessionState`] (all-public, [`Session::from_state`]-restorable) and
161    /// later replay them through [`install_session`] to revive the manager.
162    pub fn sessions(&self) -> impl Iterator<Item = ((&str, &str), &super::SessionState)> {
163        self.peers.iter().flat_map(|(peer, record)| {
164            record
165                .devices
166                .iter()
167                .map(move |(device, session)| ((peer.as_str(), device.as_str()), &session.state))
168        })
169    }
170
171    /// Remove every `sender_index` row that currently points at `slot`.
172    fn forget_in_index(&mut self, slot: &SessionKey) {
173        self.sender_index.retain(|_, v| v != slot);
174    }
175
176    /// Re-index `slot` from its session's current accepted-sender set, dropping
177    /// any rows that previously pointed at it. Called after a receive advances
178    /// the ratchet (new current/next keys, banked skipped keys).
179    fn reindex(&mut self, slot: &SessionKey) {
180        self.forget_in_index(slot);
181        if let Some(session) = self.peers.get(&slot.0).and_then(|p| p.devices.get(&slot.1)) {
182            for sender in session.accepted_senders() {
183                self.sender_index.insert(sender, slot.clone());
184            }
185        }
186    }
187
188    /// **Invitee side.** Accept `invite`, install the resulting initiator
189    /// session (keyed under the inviter), and return the signed
190    /// invite-response event to publish back.
191    ///
192    /// `owner_pubkey` is our optional multi-device owner key.
193    ///
194    /// # Errors
195    /// Propagates [`Invite::accept`] crypto/signing failures.
196    pub fn accept_invite(
197        &mut self,
198        invite: &Invite,
199        owner_pubkey: Option<&str>,
200        created_at: i64,
201    ) -> Result<nostro2::NostrNote> {
202        let (session, response) = invite.accept::<K>(&self.identity, owner_pubkey, created_at)?;
203        let device_id = invite
204            .device_id
205            .clone()
206            .unwrap_or_else(|| invite.inviter.clone());
207        self.install_session(&invite.inviter, &device_id, session);
208        Ok(response)
209    }
210
211    /// **Inviter side.** Consume an invite-response `event`, install the mirror
212    /// responder session, and return the peer (owner) pubkey we now have a
213    /// session with.
214    ///
215    /// The session is keyed under the invitee's owner pubkey when supplied
216    /// (so all of a multi-device peer's devices group together), else under
217    /// their identity; the device id is always the invitee's identity pubkey.
218    ///
219    /// # Errors
220    /// Propagates [`Invite::receive`] failures (bad signature, wrong kind,
221    /// missing ephemeral secret, crypto).
222    pub fn receive_invite_response(
223        &mut self,
224        invite: &Invite,
225        event: &nostro2::NostrNote,
226    ) -> Result<String> {
227        let (session, recovered) = invite.receive::<K>(event, &self.identity)?;
228        let peer = recovered
229            .owner_public_key
230            .clone()
231            .unwrap_or_else(|| recovered.invitee_identity.clone());
232        self.install_session(&peer, &recovered.invitee_identity, session);
233        Ok(peer)
234    }
235
236    /// Route an inbound kind-[`MESSAGE_EVENT_KIND`] event to the session that
237    /// accepts its sender, commit that session's ratchet advance, and return
238    /// the decrypted message.
239    ///
240    /// Routing is O(log n) via the [`sender_index`](SessionManager): the
241    /// event's `pubkey` (the sender's current DH key) is looked up directly,
242    /// so a foreign or forged event misses without touching any ratchet.
243    /// Returns `None` if the event is not a message event, names a sender we
244    /// don't hold, or fails to decrypt (replay/tamper — the codec verifies the
245    /// signature).
246    pub fn process_event(&mut self, event: &nostro2::NostrNote) -> Option<ReceivedMessage> {
247        if event.kind != MESSAGE_EVENT_KIND {
248            return None;
249        }
250        // Exact O(log n) route: the sender DH key indexes at most one session.
251        let slot = self.sender_index.get(&event.pubkey)?.clone();
252        let session = self.peers.get_mut(&slot.0)?.devices.get_mut(&slot.1)?;
253        let (next, plaintext) = session.plan_receive_event(event).ok()?;
254        session.apply(next);
255        // The receive may have turned the ratchet or banked skipped keys, so the
256        // accepted-sender set changed: refresh this slot's index rows.
257        self.reindex(&slot);
258        Some(ReceivedMessage {
259            peer: slot.0,
260            device_id: slot.1,
261            plaintext,
262        })
263    }
264
265    /// Fan out: encrypt `payload` to **every** device session held for `peer`,
266    /// committing each ratchet advance, and return one signed kind-1060 event
267    /// per device ready to publish.
268    ///
269    /// Devices that cannot send yet (their first inbound message hasn't
270    /// arrived) are skipped. The returned order follows sorted device id.
271    ///
272    /// # Errors
273    /// [`Nip104Error::UnknownPeer`] if we hold no sessions for `peer`; plus any
274    /// per-session send failure.
275    pub fn send(
276        &mut self,
277        peer: &str,
278        payload: &[u8],
279        created_at: i64,
280    ) -> Result<Vec<nostro2::NostrNote>> {
281        let record = self
282            .peers
283            .get_mut(peer)
284            .filter(|p| !p.devices.is_empty())
285            .ok_or_else(|| Nip104Error::UnknownPeer(peer.to_owned()))?;
286
287        let mut events = Vec::with_capacity(record.devices.len());
288        for session in record.devices.values_mut() {
289            if !session.can_send() {
290                continue;
291            }
292            let (next, event) = session.plan_send_event(payload, created_at)?;
293            session.apply(next);
294            events.push(event);
295        }
296        Ok(events)
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    // Concrete-typed `public_key()` calls need the trait in scope; library code
304    // reaches it through the `NostrKeypair: NostrSigner` bound.
305    use nostro2_traits::NostrSigner as _;
306
307    type K = crate::tests::NipTester;
308
309    fn ident(seed: u8) -> K {
310        K::from_secret_bytes(&[seed; 32]).unwrap()
311    }
312
313    const NOW: i64 = 1_700_000_000;
314
315    /// Two managers bootstrap via an invite, then chat *through the manager* —
316    /// routing and fan-out both exercised end to end.
317    #[test]
318    fn two_managers_handshake_and_chat() {
319        let mut alice = SessionManager::new(ident(0x01));
320        let mut bob = SessionManager::new(ident(0x02));
321
322        // Alice mints an invite and keeps it (holds the ephemeral secret).
323        let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
324
325        // Bob accepts → his manager installs an initiator session with Alice,
326        // and emits a response Alice must consume.
327        let response = bob.accept_invite(&invite, None, NOW).unwrap();
328        assert!(bob.has_session(alice.our_pubkey()));
329
330        let peer = alice.receive_invite_response(&invite, &response).unwrap();
331        assert_eq!(peer, bob.our_pubkey());
332        assert!(alice.has_session(bob.our_pubkey()));
333
334        // Bob (initiator) speaks first; Alice routes it home.
335        let outbound = bob.send(alice.our_pubkey(), b"hello alice", NOW).unwrap();
336        assert_eq!(outbound.len(), 1);
337        let got = alice.process_event(&outbound[0]).expect("alice decrypts");
338        assert_eq!(got.peer, bob.our_pubkey());
339        assert_eq!(got.plaintext, b"hello alice");
340
341        // And the reply direction.
342        let reply = alice.send(bob.our_pubkey(), b"hi bob", NOW).unwrap();
343        assert_eq!(reply.len(), 1);
344        let got = bob.process_event(&reply[0]).expect("bob decrypts");
345        assert_eq!(got.peer, alice.our_pubkey());
346        assert_eq!(got.plaintext, b"hi bob");
347    }
348
349    /// One peer, two devices, one owner: a single `send` fans out to both, and
350    /// each device's manager decrypts exactly its own copy.
351    #[test]
352    fn send_fans_out_to_every_device() {
353        let mut alice = SessionManager::new(ident(0x10));
354
355        // Bob runs two devices under one owner key.
356        let bob_owner = ident(0x20).public_key();
357        let mut bob_dev1 = SessionManager::new(ident(0x21));
358        let mut bob_dev2 = SessionManager::new(ident(0x22));
359
360        // One public invite; both devices accept it, each claiming the owner.
361        let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
362        let r1 = bob_dev1
363            .accept_invite(&invite, Some(&bob_owner), NOW)
364            .unwrap();
365        let r2 = bob_dev2
366            .accept_invite(&invite, Some(&bob_owner), NOW)
367            .unwrap();
368
369        // Alice receives both → one peer (the owner) with two device sessions.
370        let p1 = alice.receive_invite_response(&invite, &r1).unwrap();
371        let p2 = alice.receive_invite_response(&invite, &r2).unwrap();
372        assert_eq!(p1, bob_owner);
373        assert_eq!(p2, bob_owner);
374        assert_eq!(alice.devices(&bob_owner).len(), 2);
375
376        // Initiator devices must speak first to open their send chains.
377        let m1 = bob_dev1.send(alice.our_pubkey(), b"d1 up", NOW).unwrap();
378        let m2 = bob_dev2.send(alice.our_pubkey(), b"d2 up", NOW).unwrap();
379        assert_eq!(alice.process_event(&m1[0]).unwrap().plaintext, b"d1 up");
380        assert_eq!(alice.process_event(&m2[0]).unwrap().plaintext, b"d2 up");
381
382        // One send → two events, one per device.
383        let fanned = alice.send(&bob_owner, b"broadcast", NOW).unwrap();
384        assert_eq!(fanned.len(), 2);
385
386        // Each device decrypts exactly one of the two; neither takes both.
387        let to_dev1 = fanned
388            .iter()
389            .filter_map(|e| bob_dev1.process_event(e))
390            .collect::<Vec<_>>();
391        let to_dev2 = fanned
392            .iter()
393            .filter_map(|e| bob_dev2.process_event(e))
394            .collect::<Vec<_>>();
395        assert_eq!(to_dev1.len(), 1);
396        assert_eq!(to_dev2.len(), 1);
397        assert_eq!(to_dev1[0].plaintext, b"broadcast");
398        assert_eq!(to_dev2[0].plaintext, b"broadcast");
399    }
400
401    /// Snapshot every session out of one manager, rebuild a fresh manager by
402    /// replaying them through `install_session`, and confirm the restored
403    /// manager decrypts and continues the conversation.
404    #[test]
405    fn sessions_snapshot_round_trip() {
406        let mut alice = SessionManager::new(ident(0x51));
407        let mut bob = SessionManager::new(ident(0x52));
408
409        let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
410        let response = bob.accept_invite(&invite, None, NOW).unwrap();
411        alice.receive_invite_response(&invite, &response).unwrap();
412
413        // Bob (initiator) opens his chain; Alice receives.
414        let up = bob.send(alice.our_pubkey(), b"hello", NOW).unwrap();
415        assert_eq!(alice.process_event(&up[0]).unwrap().plaintext, b"hello");
416
417        // Snapshot Alice's sessions and rebuild a fresh manager from them.
418        let snaps: Vec<((String, String), super::super::SessionState)> = alice
419            .sessions()
420            .map(|((p, d), st)| ((p.to_owned(), d.to_owned()), st.clone()))
421            .collect();
422        assert_eq!(snaps.len(), 1);
423        let mut alice2 = SessionManager::new(ident(0x51));
424        for ((peer, device), state) in snaps {
425            alice2.install_session(&peer, &device, Session::from_state(state));
426        }
427
428        // The revived manager continues the conversation in both directions.
429        let up2 = bob.send(alice.our_pubkey(), b"again", NOW).unwrap();
430        assert_eq!(alice2.process_event(&up2[0]).unwrap().plaintext, b"again");
431        let reply = alice2.send(bob.our_pubkey(), b"hi back", NOW).unwrap();
432        assert_eq!(bob.process_event(&reply[0]).unwrap().plaintext, b"hi back");
433    }
434
435    #[test]
436    fn send_to_unknown_peer_errors() {
437        let mut alice = SessionManager::new(ident(0x30));
438        let err = alice.send("deadbeef", b"hi", NOW).unwrap_err();
439        assert!(matches!(err, Nip104Error::UnknownPeer(_)));
440    }
441
442    #[test]
443    fn process_ignores_foreign_and_non_message_events() {
444        let mut alice = SessionManager::new(ident(0x40));
445        let mut bob = SessionManager::new(ident(0x41));
446
447        let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
448        let response = bob.accept_invite(&invite, None, NOW).unwrap();
449        alice.receive_invite_response(&invite, &response).unwrap();
450        let outbound = bob.send(alice.our_pubkey(), b"hi", NOW).unwrap();
451
452        // A third party with no session ignores the message entirely.
453        let mut stranger = SessionManager::new(ident(0x42));
454        assert!(stranger.process_event(&outbound[0]).is_none());
455
456        // Non-message kinds (e.g. the invite response itself) are ignored too.
457        assert!(alice.process_event(&response).is_none());
458    }
459
460    // ── Adversarial / scale ───────────────────────────────────────
461
462    /// One owner running many devices: a single `send` fans out one event per
463    /// device, and each device's manager decrypts exactly its own copy and
464    /// none of the others'.
465    #[test]
466    fn fan_out_to_many_devices() {
467        const DEVICES: u8 = 24;
468        let mut alice = SessionManager::new(ident(0x01));
469        let owner = ident(0x02).public_key();
470
471        // Spin up DEVICES devices, all under one owner, all accepting one invite.
472        let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
473        let mut devices: Vec<SessionManager<K>> = Vec::new();
474        for d in 0..DEVICES {
475            let mut dev = SessionManager::new(ident(0x10 + d));
476            let resp = dev.accept_invite(&invite, Some(&owner), NOW).unwrap();
477            alice.receive_invite_response(&invite, &resp).unwrap();
478            // Initiator device must speak first to open its send chain.
479            let up = dev.send(alice.our_pubkey(), b"up", NOW).unwrap();
480            assert_eq!(alice.process_event(&up[0]).unwrap().plaintext, b"up");
481            devices.push(dev);
482        }
483        assert_eq!(alice.devices(&owner).len(), DEVICES as usize);
484
485        // One send → DEVICES distinct events.
486        let fanned = alice.send(&owner, b"broadcast", NOW).unwrap();
487        assert_eq!(fanned.len(), DEVICES as usize);
488
489        // Each device decrypts exactly one event across the whole batch.
490        for dev in &mut devices {
491            let hits: Vec<_> = fanned.iter().filter_map(|e| dev.process_event(e)).collect();
492            assert_eq!(hits.len(), 1, "each device takes exactly one copy");
493            assert_eq!(hits[0].plaintext, b"broadcast");
494        }
495    }
496
497    /// A long, turn-taking conversation through the managers: every change of
498    /// speaker turns the DH ratchet, and hundreds of messages stay in sync.
499    #[test]
500    fn sustained_bidirectional_conversation() {
501        let mut alice = SessionManager::new(ident(0x01));
502        let mut bob = SessionManager::new(ident(0x02));
503        let apk = alice.our_pubkey().to_owned();
504        let bpk = bob.our_pubkey().to_owned();
505
506        let invite = Invite::create_new::<K>(&apk, None).unwrap();
507        let resp = bob.accept_invite(&invite, None, NOW).unwrap();
508        alice.receive_invite_response(&invite, &resp).unwrap();
509
510        // Bob (initiator) opens.
511        let first = bob.send(&apk, b"hi", NOW).unwrap();
512        assert_eq!(alice.process_event(&first[0]).unwrap().plaintext, b"hi");
513
514        // 100 alternating turns.
515        for i in 0..100 {
516            let a_body = format!("a{i}");
517            let ev = alice.send(&bpk, a_body.as_bytes(), NOW).unwrap();
518            assert_eq!(
519                bob.process_event(&ev[0]).unwrap().plaintext,
520                a_body.as_bytes()
521            );
522
523            let b_body = format!("b{i}");
524            let ev = bob.send(&apk, b_body.as_bytes(), NOW).unwrap();
525            assert_eq!(
526                alice.process_event(&ev[0]).unwrap().plaintext,
527                b_body.as_bytes()
528            );
529        }
530    }
531
532    /// Replaying a captured message event is ignored: the routed session has
533    /// already advanced past it (no stored skipped key for an in-order index).
534    #[test]
535    fn replayed_message_event_ignored() {
536        let mut alice = SessionManager::new(ident(0x01));
537        let mut bob = SessionManager::new(ident(0x02));
538        let apk = alice.our_pubkey().to_owned();
539
540        let invite = Invite::create_new::<K>(&apk, None).unwrap();
541        let resp = bob.accept_invite(&invite, None, NOW).unwrap();
542        alice.receive_invite_response(&invite, &resp).unwrap();
543
544        let ev = bob.send(&apk, b"only once", NOW).unwrap();
545        assert_eq!(alice.process_event(&ev[0]).unwrap().plaintext, b"only once");
546        // Replay of the same wire event finds no session that still accepts it.
547        assert!(alice.process_event(&ev[0]).is_none());
548    }
549
550    /// A message addressed to one peer must not be decryptable by an unrelated
551    /// third party who holds a *different* session — trial-decrypt across
552    /// sessions must not cross conversations.
553    #[test]
554    fn message_does_not_decrypt_under_foreign_session() {
555        let mut alice = SessionManager::new(ident(0x01));
556        let mut bob = SessionManager::new(ident(0x02));
557        let mut mallory = SessionManager::new(ident(0x03));
558        let apk = alice.our_pubkey().to_owned();
559        let mpk_owner = mallory.our_pubkey().to_owned();
560        let _ = mpk_owner;
561
562        // Alice ↔ Bob session.
563        let inv_b = Invite::create_new::<K>(&apk, None).unwrap();
564        let rb = bob.accept_invite(&inv_b, None, NOW).unwrap();
565        alice.receive_invite_response(&inv_b, &rb).unwrap();
566
567        // Alice ↔ Mallory session (so Mallory's manager holds *a* session).
568        let inv_m = Invite::create_new::<K>(&apk, None).unwrap();
569        let rm = mallory.accept_invite(&inv_m, None, NOW).unwrap();
570        alice.receive_invite_response(&inv_m, &rm).unwrap();
571
572        // Bob sends to Alice; Mallory (a real peer of Alice) must not decrypt it.
573        let ev = bob.send(&apk, b"for alice only", NOW).unwrap();
574        assert!(mallory.process_event(&ev[0]).is_none());
575        // Alice still decrypts it correctly.
576        assert_eq!(
577            alice.process_event(&ev[0]).unwrap().plaintext,
578            b"for alice only"
579        );
580    }
581
582    /// Out-of-order arrival across the manager: a later message decrypts first,
583    /// then the earlier one backfills from a skipped key.
584    #[test]
585    fn out_of_order_events_route_and_backfill() {
586        let mut alice = SessionManager::new(ident(0x01));
587        let mut bob = SessionManager::new(ident(0x02));
588        let apk = alice.our_pubkey().to_owned();
589        let bpk = bob.our_pubkey().to_owned();
590
591        let invite = Invite::create_new::<K>(&apk, None).unwrap();
592        let resp = bob.accept_invite(&invite, None, NOW).unwrap();
593        alice.receive_invite_response(&invite, &resp).unwrap();
594        let first = bob.send(&apk, b"open", NOW).unwrap();
595        alice.process_event(&first[0]).unwrap();
596
597        // Alice emits three on one chain; Bob receives 1, 3, then 2.
598        let e1 = alice.send(&bpk, b"m1", NOW).unwrap().pop().unwrap();
599        let e2 = alice.send(&bpk, b"m2", NOW).unwrap().pop().unwrap();
600        let e3 = alice.send(&bpk, b"m3", NOW).unwrap().pop().unwrap();
601        assert_eq!(bob.process_event(&e1).unwrap().plaintext, b"m1");
602        assert_eq!(bob.process_event(&e3).unwrap().plaintext, b"m3");
603        assert_eq!(bob.process_event(&e2).unwrap().plaintext, b"m2");
604    }
605
606    /// The routing index must stay correct as the ratchet turns: across many
607    /// changes of speaker (each rotating the sender's DH key), every inbound
608    /// event still routes to the right session. Equivalent behaviour to the
609    /// old trial-decrypt loop, now O(log n).
610    #[test]
611    fn routing_index_tracks_ratchet_turns() {
612        let mut alice = SessionManager::new(ident(0x01));
613        let mut bob = SessionManager::new(ident(0x02));
614        let apk = alice.our_pubkey().to_owned();
615        let bpk = bob.our_pubkey().to_owned();
616
617        let invite = Invite::create_new::<K>(&apk, None).unwrap();
618        let resp = bob.accept_invite(&invite, None, NOW).unwrap();
619        alice.receive_invite_response(&invite, &resp).unwrap();
620        let first = bob.send(&apk, b"open", NOW).unwrap();
621        alice.process_event(&first[0]).unwrap();
622
623        // Alternate speakers many times — each turn rotates a DH key, so the
624        // index must be refreshed on every receive.
625        for i in 0..40 {
626            let a = format!("a{i}");
627            let ea = alice.send(&bpk, a.as_bytes(), NOW).unwrap();
628            assert_eq!(bob.process_event(&ea[0]).unwrap().plaintext, a.as_bytes());
629            let b = format!("b{i}");
630            let eb = bob.send(&apk, b.as_bytes(), NOW).unwrap();
631            assert_eq!(alice.process_event(&eb[0]).unwrap().plaintext, b.as_bytes());
632        }
633    }
634
635    /// After a ratchet turn, a *late* message from the previous sending chain
636    /// (old DH key) must still route and decrypt — the index keeps the old
637    /// sender reachable via the banked skipped key.
638    #[test]
639    fn routing_index_keeps_old_chain_reachable() {
640        let mut alice = SessionManager::new(ident(0x01));
641        let mut bob = SessionManager::new(ident(0x02));
642        let apk = alice.our_pubkey().to_owned();
643        let bpk = bob.our_pubkey().to_owned();
644
645        let invite = Invite::create_new::<K>(&apk, None).unwrap();
646        let resp = bob.accept_invite(&invite, None, NOW).unwrap();
647        alice.receive_invite_response(&invite, &resp).unwrap();
648        let first = bob.send(&apk, b"open", NOW).unwrap();
649        alice.process_event(&first[0]).unwrap();
650
651        // Alice sends two on her current chain; capture both events.
652        let e1 = alice.send(&bpk, b"old-1", NOW).unwrap().pop().unwrap();
653        let e2 = alice.send(&bpk, b"old-2", NOW).unwrap().pop().unwrap();
654
655        // Bob reads only the second → ratchet turns, e1's sender becomes a
656        // skipped key. Bob replies, turning his own ratchet too.
657        assert_eq!(bob.process_event(&e2).unwrap().plaintext, b"old-2");
658        let reply = bob.send(&apk, b"hi back", NOW).unwrap();
659        alice.process_event(&reply[0]).unwrap();
660
661        // The late e1 (old DH key) still routes and decrypts from the index.
662        assert_eq!(bob.process_event(&e1).unwrap().plaintext, b"old-1");
663    }
664
665    /// Replacing a session via `install_session` must purge the old session's
666    /// stale index rows, so an event for the *replaced* session no longer
667    /// routes to the wrong (or a dead) slot.
668    #[test]
669    fn reinstalling_session_clears_stale_index_rows() {
670        let mut alice = SessionManager::new(ident(0x01));
671        let mut bob = SessionManager::new(ident(0x02));
672        let apk = alice.our_pubkey().to_owned();
673
674        let invite = Invite::create_new::<K>(&apk, None).unwrap();
675        let resp = bob.accept_invite(&invite, None, NOW).unwrap();
676        let peer = alice.receive_invite_response(&invite, &resp).unwrap();
677        let ev = bob.send(&apk, b"hello", NOW).unwrap();
678
679        // Overwrite Alice's session for that peer/device with a brand-new,
680        // unrelated session (different ephemeral keys).
681        let mut carol = SessionManager::new(ident(0x03));
682        let inv2 = Invite::create_new::<K>(&apk, None).unwrap();
683        let resp2 = carol.accept_invite(&inv2, None, NOW).unwrap();
684        let (replacement, _rec) = inv2.receive::<K>(&resp2, &ident(0x01)).unwrap();
685        let device = alice.devices(&peer).pop().unwrap();
686        alice.install_session(&peer, &device, replacement);
687
688        // Bob's original event no longer routes anywhere (its sender was purged).
689        assert!(alice.process_event(&ev[0]).is_none());
690    }
691
692    #[test]
693    fn install_and_introspection() {
694        let mut alice = SessionManager::new(ident(0x50));
695        let mut bob = SessionManager::new(ident(0x51));
696
697        assert_eq!(alice.session_count(), 0);
698        assert!(!alice.has_session(bob.our_pubkey()));
699
700        let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
701        let response = bob.accept_invite(&invite, None, NOW).unwrap();
702        let peer = alice.receive_invite_response(&invite, &response).unwrap();
703
704        assert_eq!(alice.session_count(), 1);
705        assert!(alice.has_session(&peer));
706        assert_eq!(alice.peers().count(), 1);
707        assert_eq!(alice.devices(&peer), vec![bob.our_pubkey().to_owned()]);
708    }
709}