Skip to main content

iris_chat_protocol/protocol_engine/
engine_core.rs

1use super::*;
2
3impl ProtocolEngine {
4    pub fn load_or_create_for_local_device(
5        storage: Arc<dyn StorageAdapter>,
6        owner_pubkey: PublicKey,
7        device_keys: &Keys,
8    ) -> anyhow::Result<Self> {
9        let local_owner = ndr_owner(owner_pubkey);
10        let local_device = ndr_device(device_keys.public_key());
11        let device_secret = device_keys.secret_key().to_secret_bytes();
12        let mut engine = Self::load_persisted_state(
13            Arc::clone(&storage),
14            owner_pubkey,
15            local_owner,
16            local_device,
17            device_secret,
18            false,
19        )?
20        .unwrap_or_else(|| Self {
21            owner_pubkey,
22            local_owner,
23            local_device,
24            local_device_secret: device_secret,
25            storage,
26            session_manager: SessionManager::new(local_owner, device_secret),
27            group_manager: GroupEventManager::new(local_owner),
28            pending_inbound: Vec::new(),
29            pending_inbound_retry_deadline: std::cell::Cell::new(None),
30            failed_inbound_receives: BTreeMap::new(),
31            pending_group_fanouts: Vec::new(),
32            pending_group_fanout_retry_deadline: std::cell::Cell::new(None),
33            ready_group_publications: BTreeMap::new(),
34            pending_local_sibling_sends: Vec::new(),
35            held_direct_chats: BTreeSet::new(),
36            pending_remote_sends: Vec::new(),
37            pending_group_pairwise_payloads: Vec::new(),
38            pending_group_sender_key_messages: PendingGroupMessages::default(),
39            pending_group_sender_key_repairs: Vec::new(),
40            processed_group_sender_key_messages: ProtocolGroupReplayCache::default(),
41            group_sender_key_retry: std::cell::RefCell::new(ProtocolGroupSenderKeyRetry::default()),
42            group_retry_pacing: std::cell::RefCell::new(GroupRetryPacing::default()),
43            answered_group_sender_key_repairs: Vec::new(),
44            pending_decrypted_deliveries: Vec::new(),
45            group_roster_fact_histories: BTreeMap::new(),
46            handshake_proof_cache: std::cell::RefCell::new(HandshakeProofCache::default()),
47            known_message_author_cache: std::cell::RefCell::new(None),
48            known_message_author_cache_build_count: std::cell::Cell::new(0),
49            verified_app_keys_owners: BTreeSet::new(),
50            invite_owner_app_keys_evidence: BTreeMap::new(),
51            processed_private_invite_response_ids: Vec::new(),
52            group_invite_uses: BTreeMap::new(),
53            local_app_keys_observed: false,
54            local_owner_authenticated: false,
55            subscription_generation: 0,
56            batch_depth: std::cell::Cell::new(0),
57            batch_persist_dirty: std::cell::Cell::new(false),
58            checkpoint_layout: std::cell::RefCell::new(ProtocolCheckpointLayout::default()),
59        });
60
61        let local_invite = if let Some(invite) = engine.session_manager.snapshot().local_invite {
62            let invite = normalize_local_invite_owner(invite, owner_pubkey);
63            engine.session_manager.replace_local_invite(invite.clone());
64            invite
65        } else {
66            let device_id = device_keys.public_key().to_hex();
67            let invite = load_or_create_local_invite(
68                engine.storage.as_ref(),
69                device_keys.public_key(),
70                &device_id,
71                owner_pubkey,
72            )?;
73            engine.session_manager.replace_local_invite(invite.clone());
74            invite
75        };
76        engine.finish_local_device_startup(local_invite.created_at)?;
77        Ok(engine)
78    }
79
80    pub(super) fn load_persisted_state(
81        storage: Arc<dyn StorageAdapter>,
82        owner_pubkey: PublicKey,
83        local_owner: NdrOwnerPubkey,
84        local_device: NdrDevicePubkey,
85        device_secret: [u8; 32],
86        notification_preview: bool,
87    ) -> anyhow::Result<Option<Self>> {
88        let Some(raw) = storage.get(PROTOCOL_ENGINE_STATE_KEY)? else {
89            return Ok(None);
90        };
91        let mut state = if notification_preview {
92            serde_json::from_str::<NotificationPreviewPersistedState>(&raw)
93                .map(|preview| preview.state)
94        } else {
95            serde_json::from_str::<ProtocolEnginePersistedState>(&raw)
96        }
97        .map_err(|_| {
98            anyhow::anyhow!("Saved chat state could not be read. Existing data has been kept.")
99        })?;
100        anyhow::ensure!(
101            state.version == PROTOCOL_ENGINE_STATE_VERSION,
102            "Saved chat state uses an unsupported version. Existing data has been kept."
103        );
104        quarantine_legacy_group_ingress(storage.as_ref(), &raw, &mut state)?;
105        sanitize_invite_owner_persisted_state(&mut state);
106        restore_signed_peer_evidence_for_session_manager(&mut state);
107        quarantine_unverified_owner_rosters(
108            &mut state.session_manager,
109            &state.verified_app_keys_owners,
110        );
111        let checkpoint_layout = if notification_preview {
112            ProtocolCheckpointLayout::default()
113        } else {
114            ProtocolCheckpointLayout::from_json(&raw)
115        };
116        drop(raw);
117        let session_manager = SessionManager::from_snapshot(state.session_manager, device_secret)?;
118        let group_manager = GroupEventManager::from_snapshot(state.group_manager)?;
119        let mut answered_group_sender_key_repairs = state.answered_group_sender_key_repairs;
120        let excess = answered_group_sender_key_repairs
121            .len()
122            .saturating_sub(ANSWERED_GROUP_SENDER_KEY_REPAIR_LIMIT);
123        if excess > 0 {
124            answered_group_sender_key_repairs.drain(0..excess);
125        }
126
127        Ok(Some(Self {
128            owner_pubkey,
129            local_owner,
130            local_device,
131            local_device_secret: device_secret,
132            storage,
133            session_manager,
134            group_manager,
135            pending_inbound: state.pending_inbound,
136            pending_inbound_retry_deadline: std::cell::Cell::new(None),
137            failed_inbound_receives: BTreeMap::new(),
138            pending_group_fanouts: state.pending_group_fanouts,
139            pending_group_fanout_retry_deadline: std::cell::Cell::new(None),
140            ready_group_publications: state.ready_group_publications,
141            pending_local_sibling_sends: state.pending_local_sibling_sends,
142            held_direct_chats: BTreeSet::new(),
143            pending_remote_sends: state.pending_remote_sends,
144            pending_group_pairwise_payloads: state.pending_group_pairwise_payloads,
145            pending_group_sender_key_messages: state.pending_group_sender_key_messages,
146            pending_group_sender_key_repairs: state.pending_group_sender_key_repairs,
147            processed_group_sender_key_messages: state.processed_group_sender_key_messages,
148            group_sender_key_retry: std::cell::RefCell::new(ProtocolGroupSenderKeyRetry::default()),
149            group_retry_pacing: std::cell::RefCell::new(GroupRetryPacing::default()),
150            answered_group_sender_key_repairs,
151            pending_decrypted_deliveries: state.pending_decrypted_deliveries,
152            group_roster_fact_histories: state.group_roster_fact_histories,
153            handshake_proof_cache: std::cell::RefCell::new(HandshakeProofCache::default()),
154            known_message_author_cache: std::cell::RefCell::new(None),
155            known_message_author_cache_build_count: std::cell::Cell::new(0),
156            local_app_keys_observed: state.verified_app_keys_owners.contains(&local_owner),
157            verified_app_keys_owners: state.verified_app_keys_owners,
158            invite_owner_app_keys_evidence: state.invite_owner_app_keys_evidence,
159            processed_private_invite_response_ids: state.processed_private_invite_response_ids,
160            group_invite_uses: state.group_invite_uses,
161            local_owner_authenticated: false,
162            subscription_generation: state.subscription_generation,
163            batch_depth: std::cell::Cell::new(0),
164            batch_persist_dirty: std::cell::Cell::new(false),
165            checkpoint_layout: std::cell::RefCell::new(checkpoint_layout),
166        }))
167    }
168
169    fn finish_local_device_startup(
170        &mut self,
171        local_invite_created_at: NdrUnixSeconds,
172    ) -> anyhow::Result<()> {
173        self.ensure_local_roster(local_invite_created_at);
174        self.discard_pending_messages_for_other_devices()?;
175        self.hydrate_pending_inbound_metadata();
176        // First-contact packets can precede their invite response. Keep their
177        // signed bytes across restart; new untracked admissions are bounded at
178        // the shared queue entry point rather than dropping them on startup.
179        self.prune_superseded_local_group_sync_fanouts();
180        self.prune_pending_group_sender_key_work_for_inactive_local_groups();
181        self.persist()
182    }
183
184    pub(super) fn prune_superseded_local_group_sync_fanouts(&mut self) -> bool {
185        let original_len = self.pending_group_fanouts.len();
186        if original_len < 2 {
187            return false;
188        }
189
190        let mut seen_groups = HashSet::new();
191        let mut compacted = Vec::with_capacity(original_len);
192        for pending in std::mem::take(&mut self.pending_group_fanouts)
193            .into_iter()
194            .rev()
195        {
196            let keep = if pending.inner_event_id.is_none()
197                && matches!(
198                    pending.fanout.as_ref(),
199                    GroupPendingFanout::LocalSiblings { .. }
200                ) {
201                seen_groups.insert(pending.group_id.clone())
202            } else {
203                true
204            };
205            if keep {
206                compacted.push(pending);
207            }
208        }
209        compacted.reverse();
210        self.pending_group_fanouts = compacted;
211        let changed = self.pending_group_fanouts.len() != original_len;
212        if changed {
213            self.invalidate_pending_group_fanout_retry_deadline();
214        }
215        changed
216    }
217
218    fn hydrate_pending_inbound_metadata(&mut self) {
219        let metadata = self
220            .pending_inbound
221            .iter()
222            .map(|pending| {
223                self.pending_inbound_metadata_for_event(
224                    &pending.event,
225                    pending.envelope.as_ref(),
226                    None,
227                )
228            })
229            .collect::<Vec<_>>();
230        for (pending, metadata) in self.pending_inbound.iter_mut().zip(metadata) {
231            apply_pending_inbound_metadata(pending, metadata);
232        }
233    }
234
235    pub fn debug_snapshot(&self) -> ProtocolEngineDebugSnapshot {
236        let pending_group_sender_key_retry_count = self.pending_group_sender_key_retry_count();
237        let pending_group_sender_key_unmapped_count = self
238            .pending_group_sender_key_messages
239            .len()
240            .saturating_sub(pending_group_sender_key_retry_count);
241        let known_message_author_pubkeys = self
242            .known_message_author_pubkeys()
243            .into_iter()
244            .map(|pubkey| pubkey.to_hex())
245            .collect::<Vec<_>>();
246        let known_group_sender_key_author_pubkeys = self
247            .known_group_sender_event_pubkeys()
248            .into_iter()
249            .map(|pubkey| pubkey.to_hex())
250            .collect::<Vec<_>>();
251        ProtocolEngineDebugSnapshot {
252            known_message_author_count: known_message_author_pubkeys.len(),
253            known_message_author_pubkeys,
254            known_group_sender_key_author_count: known_group_sender_key_author_pubkeys.len(),
255            known_group_sender_key_author_pubkeys,
256            pending_inbound_count: self.pending_inbound.len(),
257            pending_group_fanout_count: self.pending_group_fanouts.len(),
258            pending_group_pairwise_payload_count: self.pending_group_pairwise_payloads.len(),
259            pending_group_sender_key_message_count: self.pending_group_sender_key_messages.len(),
260            pending_group_sender_key_retry_count,
261            pending_group_sender_key_unmapped_count,
262            pending_group_sender_key_repair_count: self.pending_group_sender_key_repairs.len(),
263            pending_group_sender_key_repair_last_requested_at_secs: self
264                .pending_group_sender_key_repairs
265                .iter()
266                .map(|repair| repair.last_requested_at_secs)
267                .max()
268                .unwrap_or_default(),
269            pending_group_sender_key_repair_next_retry_at_secs: self
270                .pending_group_sender_key_repairs
271                .iter()
272                .map(Self::pending_group_sender_key_repair_due_at_secs)
273                .min()
274                .unwrap_or_default(),
275            pending_group_sender_key_repair_max_request_count: self
276                .pending_group_sender_key_repairs
277                .iter()
278                .map(|repair| repair.request_count)
279                .max()
280                .unwrap_or_default(),
281            pending_group_fanout_targets: self.queued_group_targets(),
282            subscription_generation: self.subscription_generation,
283        }
284    }
285
286    pub fn is_known_local_owner_device(&self, device_pubkey: PublicKey) -> bool {
287        let device_pubkey = ndr_device(device_pubkey);
288        if device_pubkey == self.local_device {
289            return true;
290        }
291        if !self.verified_app_keys_owners.contains(&self.local_owner) {
292            return false;
293        }
294        self.session_manager
295            .snapshot()
296            .users
297            .into_iter()
298            .find(|user| user.owner_pubkey == self.local_owner)
299            .is_some_and(|user| {
300                user.roster
301                    .as_ref()
302                    .is_some_and(|roster| roster.get_device(&device_pubkey).is_some())
303                    && user.devices.iter().any(|device| {
304                        device.device_pubkey == device_pubkey
305                            && device.authorized
306                            && !device.is_stale
307                    })
308            })
309    }
310
311    pub fn owner_hint_for_device(
312        &self,
313        device_pubkey: PublicKey,
314    ) -> Option<ProtocolDeviceOwnerHint> {
315        let device = ndr_device(device_pubkey);
316        let provisional_owner = ndr_owner(device_pubkey);
317        let mut claimed_owner = None;
318        for user in self.session_manager.snapshot().users {
319            for record in user.devices {
320                if record.device_pubkey != device {
321                    continue;
322                }
323                if user.owner_pubkey != provisional_owner {
324                    if self.verified_app_keys_owners.contains(&user.owner_pubkey)
325                        && user.roster.as_ref().is_some_and(|roster| {
326                            roster.get_device(&record.device_pubkey).is_some()
327                        })
328                    {
329                        return public_owner(user.owner_pubkey).ok().map(|owner| {
330                            ProtocolDeviceOwnerHint {
331                                owner,
332                                verified: true,
333                            }
334                        });
335                    }
336                    if claimed_owner.is_none() {
337                        claimed_owner = Some(user.owner_pubkey);
338                    }
339                    continue;
340                }
341                if claimed_owner.is_none() {
342                    claimed_owner = record.claimed_owner_pubkey;
343                }
344            }
345        }
346        claimed_owner
347            .and_then(|owner| public_owner(owner).ok())
348            .map(|owner| ProtocolDeviceOwnerHint {
349                owner,
350                verified: false,
351            })
352    }
353
354    pub(super) fn verified_roster_owner_for_device(
355        &self,
356        device_pubkey: NdrDevicePubkey,
357    ) -> Option<NdrOwnerPubkey> {
358        let provisional_owner = NdrOwnerPubkey::from_bytes(device_pubkey.to_bytes());
359        let mut provisional_match = None;
360        for user in self.session_manager.snapshot().users {
361            let Some(roster) = user.roster.as_ref() else {
362                continue;
363            };
364            if roster.get_device(&device_pubkey).is_none() {
365                continue;
366            }
367            if user.owner_pubkey == self.local_owner {
368                continue;
369            }
370            if user.owner_pubkey != provisional_owner {
371                if self.verified_app_keys_owners.contains(&user.owner_pubkey) {
372                    return Some(user.owner_pubkey);
373                }
374                continue;
375            }
376            provisional_match = Some(user.owner_pubkey);
377        }
378        provisional_match
379    }
380
381    pub fn has_pending_inbound_direct_events(&self) -> bool {
382        !self.pending_inbound.is_empty()
383    }
384
385    pub fn has_pending_retry_work(&self) -> bool {
386        !self.pending_decrypted_deliveries.is_empty()
387            || !self.pending_inbound.is_empty()
388            || !self.pending_group_fanouts.is_empty()
389            || !self.ready_group_publications.is_empty()
390            || !self.pending_local_sibling_sends.is_empty()
391            || !self.pending_remote_sends.is_empty()
392            || !self.pending_group_pairwise_payloads.is_empty()
393            || self.has_ready_group_sender_key_retry_work()
394            || !self.pending_group_sender_key_repairs.is_empty()
395    }
396
397    /// Whether another bounded history-recovery pass can make progress now.
398    /// This excludes unchanged failed ciphertexts and ignores this batch's spent budget.
399    pub fn has_ready_group_sender_key_retry_work(&self) -> bool {
400        self.refresh_group_sender_key_retry_inputs();
401        !self.group_sender_key_retry.borrow().ready.is_empty()
402    }
403
404    fn pending_group_sender_key_retry_count(&self) -> usize {
405        self.pending_group_sender_key_messages
406            .iter()
407            .filter(|pending| {
408                self.group_manager
409                    .group_id_for_sender_event_pubkey(pending.sender_event_pubkey)
410                    .is_some()
411            })
412            .count()
413    }
414
415    pub fn has_pending_inbound_direct_event_id(&self, event_id: &str) -> bool {
416        self.pending_inbound.iter().any(|pending| {
417            let pending_event_id = if pending.event_id.is_empty() {
418                pending.event.id.to_string()
419            } else {
420                pending.event_id.clone()
421            };
422            pending_event_id == event_id
423        })
424    }
425
426    pub fn queued_owner_claim_targets(&self) -> Vec<String> {
427        let mut targets = self.pending_inbound_owner_claim_targets();
428        targets.extend(self.pending_group_pairwise_owner_claim_targets());
429        targets.sort();
430        targets.dedup();
431        targets
432    }
433
434    pub fn direct_send_readiness(&self, peer_pubkey: PublicKey) -> DirectSendReadiness {
435        // Possessing the account key locally does not give recipients proof
436        // that this device belongs to it. Restores must finish signing their
437        // approval before a send can create the first handshake.
438        if (self.local_owner_authenticated && self.local_handshake_owner_proof().is_none())
439            || (!self.local_app_keys_observed && !self.has_authoritative_local_roster())
440        {
441            return DirectSendReadiness::MissingLocalAppKeys;
442        }
443
444        let peer_owner = ndr_owner(peer_pubkey);
445        let Some(roster) = self.session_manager.roster(peer_owner) else {
446            return DirectSendReadiness::MissingPeerAppKeys;
447        };
448        let peer_devices = roster.devices();
449        if peer_devices.is_empty() {
450            return DirectSendReadiness::MissingPeerAppKeys;
451        }
452        if peer_owner == self.local_owner {
453            // A single-device account can keep the note locally. With linked
454            // devices, one usable session or invite is enough to start fanout.
455            if matches!(peer_devices, [device] if device.device_pubkey == self.local_device)
456                || peer_devices.iter().any(|device| {
457                    device.device_pubkey != self.local_device
458                        && self
459                            .session_manager
460                            .can_send_to_device(peer_owner, device.device_pubkey)
461                })
462            {
463                return DirectSendReadiness::Ready;
464            }
465            return DirectSendReadiness::MissingLocalSiblingInviteOrSession;
466        }
467        if !peer_devices.iter().any(|device| {
468            self.session_manager
469                .can_send_to_device(peer_owner, device.device_pubkey)
470        }) {
471            return DirectSendReadiness::MissingPeerInviteOrSession;
472        }
473
474        // Missing peer and sibling devices have durable retry queues. Their
475        // discovery cannot prevent delivery to an independently ready recipient.
476
477        DirectSendReadiness::Ready
478    }
479
480    pub fn authenticate_local_owner_for_sending(
481        &mut self,
482        owner_keys: &Keys,
483    ) -> anyhow::Result<()> {
484        if owner_keys.public_key() != self.owner_pubkey {
485            anyhow::bail!("local owner key does not match protocol owner");
486        }
487        self.local_owner_authenticated = true;
488        Ok(())
489    }
490
491    pub fn local_invite(&self) -> Option<Invite> {
492        self.session_manager.snapshot().local_invite
493    }
494
495    pub fn local_invite_response_pubkey(&self) -> Option<PublicKey> {
496        self.local_invite()?
497            .inviter_ephemeral_public_key
498            .to_nostr()
499            .ok()
500    }
501
502    pub fn known_message_author_pubkeys(&self) -> Vec<PublicKey> {
503        self.with_known_message_author_cache(|cache| cache.pubkeys.clone())
504    }
505
506    /// Walks every session and returns its expected event-author
507    /// pubkeys, but only for sessions whose peer owner passes the
508    /// `accept_owner` predicate. The owner-aware variant lets the
509    /// caller drop blocked / non-accepted peers from the subscription
510    /// filter without losing the device-ephemeral keys that nostr
511    /// actually filters on.
512    pub fn message_author_pubkeys_filtered<F>(&self, accept_owner: F) -> Vec<PublicKey>
513    where
514        F: Fn(PublicKey) -> bool,
515    {
516        self.message_session_projection(accept_owner).0
517    }
518
519    fn message_session_projection<F>(&self, accept_owner: F) -> (Vec<PublicKey>, HashSet<PublicKey>)
520    where
521        F: Fn(PublicKey) -> bool,
522    {
523        let mut authors = HashSet::new();
524        let mut usable_session_owners = HashSet::new();
525        for user in self.session_manager.snapshot().users {
526            let Ok(owner) = PublicKey::parse(&user.owner_pubkey.to_string()) else {
527                continue;
528            };
529            if !accept_owner(owner) {
530                continue;
531            }
532            let roster = user.roster.clone();
533            for device in user.devices {
534                if !device.authorized
535                    || device.is_stale
536                    || !self.owner_device_binding_is_verified_in_roster(
537                        user.owner_pubkey,
538                        device.device_pubkey,
539                        roster.as_ref(),
540                    )
541                {
542                    continue;
543                }
544                for session in device
545                    .active_session
546                    .iter()
547                    .chain(&device.inactive_sessions)
548                {
549                    collect_expected_sender_pubkeys(session, &mut authors);
550                    // Match NDR Session::can_send without cloning a ratchet to query it.
551                    if session.their_next_nostr_public_key.is_some()
552                        && session.our_current_nostr_key.is_some()
553                    {
554                        usable_session_owners.insert(owner);
555                    }
556                }
557            }
558        }
559        let mut authors = authors.into_iter().collect::<Vec<_>>();
560        authors.sort_unstable();
561        (authors, usable_session_owners)
562    }
563
564    /// Whether an authorized, verified peer device has a send-capable session.
565    /// Shares the message-author projection and its session/roster invalidation.
566    pub fn has_usable_direct_session_for_owner(&self, owner: PublicKey) -> bool {
567        self.with_known_message_author_cache(|cache| cache.usable_session_owners.contains(&owner))
568    }
569
570    pub fn is_known_message_author(&self, author: PublicKey) -> bool {
571        self.with_known_message_author_cache(|cache| cache.pubkey_set.contains(&author))
572    }
573
574    fn with_known_message_author_cache<T>(
575        &self,
576        read: impl FnOnce(&KnownMessageAuthorCache) -> T,
577    ) -> T {
578        let mut cached = self.known_message_author_cache.borrow_mut();
579        let cache = cached.get_or_insert_with(|| self.build_known_message_author_cache());
580        read(cache)
581    }
582
583    fn build_known_message_author_cache(&self) -> KnownMessageAuthorCache {
584        self.known_message_author_cache_build_count
585            .set(self.known_message_author_cache_build_count.get() + 1);
586
587        let (pubkeys, usable_session_owners) = self.message_session_projection(|_| true);
588        KnownMessageAuthorCache {
589            pubkey_set: pubkeys.iter().copied().collect(),
590            pubkeys,
591            usable_session_owners,
592        }
593    }
594
595    pub(super) fn invalidate_known_message_author_cache(&self) {
596        self.known_message_author_cache.borrow_mut().take();
597    }
598
599    pub fn known_group_sender_event_pubkeys(&self) -> Vec<PublicKey> {
600        let mut authors = self
601            .group_manager
602            .known_sender_event_pubkeys()
603            .into_iter()
604            .filter_map(|pubkey| public_device(pubkey).ok())
605            .collect::<Vec<_>>();
606        authors.sort_by_key(|pubkey| pubkey.to_hex());
607        authors.dedup();
608        authors
609    }
610
611    pub fn group_sender_event_pubkeys_for_owner(&self, owner: PublicKey) -> Vec<PublicKey> {
612        self.group_manager
613            .snapshot()
614            .sender_keys
615            .into_iter()
616            .filter(|record| record.sender_owner == ndr_owner(owner))
617            .filter_map(|record| public_device(record.sender_event_pubkey).ok())
618            .collect()
619    }
620
621    pub fn group_sender_event_pubkeys_for_group(&self, group_id: &str) -> Vec<PublicKey> {
622        self.group_manager
623            .known_sender_event_pubkeys()
624            .into_iter()
625            .filter(|author| {
626                self.group_manager
627                    .group_id_for_sender_event_pubkey(*author)
628                    .as_deref()
629                    == Some(group_id)
630            })
631            .filter_map(|author| public_device(author).ok())
632            .collect()
633    }
634
635    pub fn is_known_group_sender_event_author(&self, author: PublicKey) -> bool {
636        self.group_manager
637            .group_id_for_sender_event_pubkey(ndr_device(author))
638            .is_some()
639    }
640
641    pub fn is_potential_group_sender_key_event(&self, event: &Event) -> bool {
642        parse_group_sender_key_message_event_unchecked(event).is_ok_and(|parsed| {
643            self.group_manager
644                .group_id_for_sender_event_pubkey(parsed.sender_event_pubkey)
645                .is_some()
646        })
647    }
648
649    pub fn is_group_sender_key_candidate_with_local_group_context(&self, event: &Event) -> bool {
650        if !protocol_event_has_tag(event, "header")
651            || self.group_manager.snapshot().groups.is_empty()
652        {
653            return false;
654        }
655        parse_group_sender_key_message_event_unchecked(event).is_ok()
656    }
657
658    pub fn header_message_sender_has_verified_owner(&self, event: &Event) -> bool {
659        if !protocol_event_has_tag(event, "header") {
660            return false;
661        }
662        let Ok(envelope) = parse_message_event(event) else {
663            return false;
664        };
665        let Ok(sender) = public_device(envelope.sender) else {
666            return false;
667        };
668        self.owner_hint_for_device(sender)
669            .is_some_and(|hint| hint.verified)
670    }
671
672    pub fn header_message_sender_has_tracked_session(&self, event: &Event) -> bool {
673        if !protocol_event_has_tag(event, "header") {
674            return false;
675        }
676        let Ok(envelope) = parse_message_event(event) else {
677            return false;
678        };
679        !matches!(
680            self.resolve_message_sender_owner(&envelope),
681            ProtocolSenderOwnerResolution::ProvisionalDeviceOwner { .. }
682        )
683    }
684
685    pub fn known_device_identity_pubkeys_for_owner(
686        &self,
687        owner_pubkey: PublicKey,
688    ) -> Vec<PublicKey> {
689        self.known_device_identity_pubkeys_for_owner_with_snapshot(
690            &self.session_manager.snapshot(),
691            owner_pubkey,
692        )
693    }
694
695    pub fn known_device_identity_pubkeys_for_owner_with_snapshot(
696        &self,
697        snapshot: &SessionManagerSnapshot,
698        owner_pubkey: PublicKey,
699    ) -> Vec<PublicKey> {
700        let owner = ndr_owner(owner_pubkey);
701        let mut devices = snapshot
702            .users
703            .iter()
704            .find(|user| user.owner_pubkey == owner)
705            .and_then(|user| {
706                user.roster.as_ref().map(|roster| {
707                    roster
708                        .devices()
709                        .iter()
710                        .filter(|device| {
711                            self.owner_device_binding_is_verified_in_roster(
712                                owner,
713                                device.device_pubkey,
714                                Some(roster),
715                            )
716                        })
717                        .filter_map(|device| public_device(device.device_pubkey).ok())
718                        .collect::<Vec<_>>()
719                })
720            })
721            .unwrap_or_default();
722        devices.sort_by_key(|pubkey| pubkey.to_hex());
723        devices.dedup();
724        devices
725    }
726
727    pub fn message_author_pubkeys_for_owner(&self, owner_pubkey: PublicKey) -> Vec<PublicKey> {
728        let mut authors = HashSet::new();
729        let owner = ndr_owner(owner_pubkey);
730        for user in self.session_manager.snapshot().users {
731            if user.owner_pubkey != owner {
732                continue;
733            }
734            let roster = user.roster.clone();
735            for device in user.devices {
736                if !device.authorized
737                    || device.is_stale
738                    || !self.owner_device_binding_is_verified_in_roster(
739                        owner,
740                        device.device_pubkey,
741                        roster.as_ref(),
742                    )
743                {
744                    continue;
745                }
746                if let Some(session) = device.active_session.as_ref() {
747                    collect_expected_sender_pubkeys(session, &mut authors);
748                }
749                for session in &device.inactive_sessions {
750                    collect_expected_sender_pubkeys(session, &mut authors);
751                }
752            }
753        }
754        let mut authors = authors.into_iter().collect::<Vec<_>>();
755        authors.sort_by_key(|pubkey| pubkey.to_hex());
756        authors
757    }
758
759    /// `SessionManager::snapshot` clones every user record + every
760    /// device state — the runtime debug builder fans out per known
761    /// user, so callers that hit multiple owners in one pass must
762    /// share a single snapshot via the `_with_snapshot` helpers
763    /// below instead of paying that clone cost per owner.
764    pub fn session_manager_snapshot(&self) -> SessionManagerSnapshot {
765        self.session_manager.snapshot()
766    }
767
768    pub fn message_session_debug_snapshots_with_snapshot(
769        snapshot: &SessionManagerSnapshot,
770        owner_pubkey: PublicKey,
771    ) -> Vec<ProtocolMessageSessionDebugSnapshot> {
772        let owner = ndr_owner(owner_pubkey);
773        snapshot
774            .users
775            .iter()
776            .filter(|user| user.owner_pubkey == owner)
777            .flat_map(|user| user.devices.iter())
778            .flat_map(|device| {
779                device
780                    .active_session
781                    .iter()
782                    .chain(device.inactive_sessions.iter())
783            })
784            .map(|state| {
785                let mut tracked = HashSet::new();
786                collect_expected_sender_pubkeys(state, &mut tracked);
787                let mut tracked_sender_pubkeys = tracked.into_iter().collect::<Vec<_>>();
788                tracked_sender_pubkeys.sort_by_key(|pubkey| pubkey.to_hex());
789                ProtocolMessageSessionDebugSnapshot {
790                    has_receiving_capability: state.receiving_chain_key.is_some()
791                        || state.their_current_nostr_public_key.is_some(),
792                    state: state.clone(),
793                    tracked_sender_pubkeys,
794                }
795            })
796            .collect()
797    }
798
799    pub fn verified_message_session_snapshots_for_owner(
800        &self,
801        owner_pubkey: PublicKey,
802    ) -> Vec<ProtocolMessageSessionDebugSnapshot> {
803        let owner = ndr_owner(owner_pubkey);
804        let mut snapshot = self.session_manager.snapshot();
805        for user in &mut snapshot.users {
806            if user.owner_pubkey != owner {
807                continue;
808            }
809            let roster = user.roster.clone();
810            user.devices.retain(|device| {
811                device.authorized
812                    && !device.is_stale
813                    && self.owner_device_binding_is_verified_in_roster(
814                        owner,
815                        device.device_pubkey,
816                        roster.as_ref(),
817                    )
818            });
819        }
820        Self::message_session_debug_snapshots_with_snapshot(&snapshot, owner_pubkey)
821    }
822
823    pub fn active_session_count_for_owner_with_snapshot(
824        snapshot: &SessionManagerSnapshot,
825        owner_pubkey: PublicKey,
826    ) -> usize {
827        let owner = ndr_owner(owner_pubkey);
828        snapshot
829            .users
830            .iter()
831            .filter(|user| user.owner_pubkey == owner)
832            .flat_map(|user| user.devices.iter())
833            .filter(|device| device.active_session.is_some())
834            .count()
835    }
836
837    pub fn active_session_count_for_owner(&self, owner_pubkey: PublicKey) -> usize {
838        let owner = ndr_owner(owner_pubkey);
839        self.session_manager
840            .snapshot()
841            .users
842            .into_iter()
843            .filter(|user| user.owner_pubkey == owner)
844            .map(|user| {
845                let roster = user.roster;
846                user.devices
847                    .into_iter()
848                    .filter(|device| {
849                        device.active_session.is_some()
850                            && device.authorized
851                            && !device.is_stale
852                            && self.owner_device_binding_is_verified_in_roster(
853                                owner,
854                                device.device_pubkey,
855                                roster.as_ref(),
856                            )
857                    })
858                    .count()
859            })
860            .sum()
861    }
862
863    pub fn active_roster_session_count_for_owner(&self, owner_pubkey: PublicKey) -> usize {
864        self.active_roster_session_count_for_owner_with_snapshot(
865            &self.session_manager.snapshot(),
866            owner_pubkey,
867        )
868    }
869
870    pub fn active_roster_session_count_for_owner_with_snapshot(
871        &self,
872        snapshot: &SessionManagerSnapshot,
873        owner_pubkey: PublicKey,
874    ) -> usize {
875        let owner = ndr_owner(owner_pubkey);
876        snapshot
877            .users
878            .iter()
879            .find(|user| user.owner_pubkey == owner)
880            .and_then(|user| {
881                let roster = user.roster.as_ref()?;
882                Some(
883                    user.devices
884                        .iter()
885                        .filter(|device| {
886                            device.active_session.is_some()
887                                && device.authorized
888                                && !device.is_stale
889                                && self.owner_device_binding_is_verified_in_roster(
890                                    owner,
891                                    device.device_pubkey,
892                                    Some(roster),
893                                )
894                                && roster
895                                    .devices()
896                                    .iter()
897                                    .any(|entry| entry.device_pubkey == device.device_pubkey)
898                        })
899                        .count(),
900                )
901            })
902            .unwrap_or_default()
903    }
904
905    pub fn owner_device_binding_is_verified(
906        &self,
907        owner_pubkey: PublicKey,
908        device_pubkey: PublicKey,
909    ) -> bool {
910        self.has_verified_device_owner_claim(ndr_owner(owner_pubkey), ndr_device(device_pubkey))
911    }
912
913    pub(super) fn owner_device_binding_is_verified_in_roster(
914        &self,
915        owner: NdrOwnerPubkey,
916        device: NdrDevicePubkey,
917        roster: Option<&DeviceRoster>,
918    ) -> bool {
919        owner == provisional_owner_from_sender_pubkey(device)
920            || (self.verified_app_keys_owners.contains(&owner)
921                && roster.is_some_and(|roster| roster.get_device(&device).is_some()))
922    }
923
924    pub fn known_verified_peer_owner_pubkeys(&self) -> Vec<PublicKey> {
925        let mut owners = self
926            .session_manager
927            .snapshot()
928            .users
929            .into_iter()
930            .filter(|user| {
931                let roster = user.roster.as_ref();
932                user.devices.iter().any(|device| {
933                    self.owner_device_binding_is_verified_in_roster(
934                        user.owner_pubkey,
935                        device.device_pubkey,
936                        roster,
937                    )
938                })
939            })
940            .filter_map(|user| public_owner(user.owner_pubkey).ok())
941            .collect::<Vec<_>>();
942        owners.sort_by_key(|owner| owner.to_hex());
943        owners.dedup();
944        owners
945    }
946
947    pub fn has_delivery_blocking_message_work(&self, message_id: &str) -> bool {
948        self.pending_group_fanouts
949            .iter()
950            .any(|pending| pending.inner_event_id.as_deref() == Some(message_id))
951    }
952}