Skip to main content

iris_chat_protocol/protocol_engine/
engine_incoming_retry.rs

1use super::*;
2
3impl ProtocolEngine {
4    /// Shared bootstrap authors can expose ciphertext addressed to other devices.
5    /// Untargeted session messages remain eligible for authenticated decryption.
6    pub fn message_targets_another_device(&self, event: &Event) -> bool {
7        if event.kind.as_u16() as u32 != MESSAGE_EVENT_KIND
8            || !protocol_event_has_tag(event, "header")
9        {
10            return false;
11        }
12        let mut has_recipient = false;
13        for recipient in event.tags.public_keys() {
14            has_recipient = true;
15            if *recipient == self.owner_pubkey || ndr_device(*recipient) == self.local_device {
16                return false;
17            }
18        }
19        has_recipient
20    }
21
22    pub(super) fn invalidate_pending_inbound_retry_deadline(&self) {
23        self.pending_inbound_retry_deadline.set(None);
24    }
25
26    fn refresh_pending_inbound_retry_deadline(&self) -> PendingInboundRetryDeadline {
27        let deadline = PendingInboundRetryDeadline {
28            queue_len: self.pending_inbound.len(),
29            next_retry_at_secs: self
30                .pending_inbound
31                .iter()
32                .map(|pending| {
33                    #[cfg(test)]
34                    tests::record_inbound_retry_deadline_scan();
35                    pending.next_retry_at_secs
36                })
37                .min(),
38        };
39        self.pending_inbound_retry_deadline.set(Some(deadline));
40        deadline
41    }
42
43    pub(super) fn has_due_pending_inbound_retry(&self, now: NdrUnixSeconds) -> bool {
44        let deadline = self
45            .pending_inbound_retry_deadline
46            .get()
47            .filter(|deadline| deadline.queue_len == self.pending_inbound.len())
48            .unwrap_or_else(|| self.refresh_pending_inbound_retry_deadline());
49        deadline
50            .next_retry_at_secs
51            .is_some_and(|next_retry| next_retry <= now.get())
52    }
53
54    pub(super) fn invalidate_pending_group_fanout_retry_deadline(&self) {
55        self.pending_group_fanout_retry_deadline.set(None);
56    }
57
58    pub(super) fn has_due_pending_group_fanout_retry(&self, now: NdrUnixSeconds) -> bool {
59        if let Some(deadline) = self
60            .pending_group_fanout_retry_deadline
61            .get()
62            .filter(|deadline| deadline.queue_len == self.pending_group_fanouts.len())
63        {
64            return deadline
65                .next_retry_at_secs
66                .is_some_and(|next_retry| next_retry <= now.get());
67        }
68        let mut next_retry_at_secs: Option<u64> = None;
69        for pending in &self.pending_group_fanouts {
70            #[cfg(test)]
71            tests::record_fanout_retry_deadline_scan();
72            let next_retry = pending.next_retry_at_secs;
73            // Preserve the early positive query across successive due batches;
74            // only a fully deferred queue needs a cached minimum.
75            if next_retry <= now.get() {
76                return true;
77            }
78            next_retry_at_secs =
79                Some(next_retry_at_secs.map_or(next_retry, |earliest| earliest.min(next_retry)));
80        }
81        self.pending_group_fanout_retry_deadline
82            .set(Some(PendingGroupFanoutRetryDeadline {
83                queue_len: self.pending_group_fanouts.len(),
84                next_retry_at_secs,
85            }));
86        false
87    }
88
89    pub fn has_due_pending_retry_work(&self, now: NdrUnixSeconds) -> bool {
90        // Signed group batches remain immediately runnable until the app outbox
91        // acknowledges them. A retry also retires any now-ineligible entries.
92        if !self.pending_decrypted_deliveries.is_empty()
93            || !self.ready_group_publications.is_empty()
94        {
95            return true;
96        }
97        if !self.pending_group_sender_key_messages.is_empty() {
98            let retry = self.group_sender_key_retry.borrow();
99            // Refresh leaves a frozen search's inputs untouched. Its established
100            // continuation already makes this turn runnable, without due scans.
101            if !retry.ready.is_empty() && (retry.active.is_some() || retry.prepared.is_some()) {
102                return true;
103            }
104        }
105        let now_secs = now.get();
106        self.pending_remote_sends
107            .iter()
108            .any(|pending| pending.next_retry_at_secs <= now_secs)
109            || self
110                .pending_local_sibling_sends
111                .iter()
112                .any(|pending| pending.next_retry_at_secs <= now_secs)
113            || self.has_due_pending_inbound_retry(now)
114            || self.has_due_pending_group_fanout_retry(now)
115            || self
116                .pending_group_pairwise_payloads
117                .iter()
118                .any(|pending| pending.next_retry_at_secs <= now_secs)
119            || self.pending_group_sender_key_repairs.iter().any(|pending| {
120                Self::pending_group_sender_key_repair_due_at_secs(pending) <= now_secs
121            })
122            || self.has_ready_group_sender_key_retry_work()
123            || !self.pending_decrypted_deliveries.is_empty()
124    }
125
126    pub fn process_direct_message_event(
127        &mut self,
128        event: &Event,
129    ) -> anyhow::Result<Option<ProtocolDecryptedMessage>> {
130        let envelope = parse_message_event(event)?;
131        if self.message_targets_another_device(event) {
132            return Ok(None);
133        }
134        let resolution = self.resolve_message_sender_owner(&envelope);
135        match resolution {
136            ProtocolSenderOwnerResolution::Verified { .. }
137            | ProtocolSenderOwnerResolution::ProvisionalDeviceOwner { .. } => {}
138            ProtocolSenderOwnerResolution::PendingOwnerClaim { .. } => {
139                self.queue_pending_inbound_direct_event(
140                    event.clone(),
141                    event.created_at.as_secs(),
142                    Some(&envelope),
143                    Some(resolution),
144                )?;
145                return Ok(None);
146            }
147        };
148        match self.decrypt_direct_message_envelope(event, &envelope, true) {
149            Ok(DirectReceive::Message(decrypted)) => return Ok(Some(decrypted)),
150            Ok(DirectReceive::Discarded) => return Ok(None),
151            Ok(DirectReceive::Pending) => {}
152            Err(error) => {
153                if protocol_event_has_tag(event, "header")
154                    && !self.is_known_message_author(event.pubkey)
155                {
156                    self.queue_pending_inbound_direct_event(
157                        event.clone(),
158                        event.created_at.as_secs(),
159                        Some(&envelope),
160                        Some(resolution),
161                    )?;
162                    return Ok(None);
163                }
164                return Err(error);
165            }
166        }
167        self.queue_pending_inbound_direct_event(
168            event.clone(),
169            event.created_at.as_secs(),
170            Some(&envelope),
171            Some(resolution),
172        )?;
173        Ok(None)
174    }
175
176    /// Process a local group ciphertext extracted from an authenticated pairwise
177    /// wrapper after its sender/group binding was checked. Never pass a raw
178    /// network event here or use it as a fallback for failed direct decryption.
179    pub fn process_group_outer_event(
180        &mut self,
181        event: &Event,
182    ) -> anyhow::Result<ProtocolGroupIncomingResult> {
183        if self.message_targets_another_device(event) {
184            return Ok(ProtocolGroupIncomingResult {
185                consumed: true,
186                ..Default::default()
187            });
188        }
189        let has_header = protocol_event_has_tag(event, "header");
190        let parsed = if has_header {
191            parse_group_sender_key_message_event_unchecked(event)
192        } else {
193            parse_group_sender_key_message_event(event)
194        };
195        let Ok(parsed) = parsed else {
196            return Ok(ProtocolGroupIncomingResult::default());
197        };
198        let Some(message) = self.group_sender_key_message_from_parsed(&parsed) else {
199            self.queue_pending_group_sender_key_message(parsed)?;
200            return Ok(ProtocolGroupIncomingResult {
201                consumed: true,
202                ..Default::default()
203            });
204        };
205        if self
206            .processed_group_sender_key_messages
207            .contains(&group_sender_key_fingerprint(&message))
208        {
209            return Ok(ProtocolGroupIncomingResult {
210                consumed: true,
211                ..Default::default()
212            });
213        }
214        // Authenticated nested ciphertext may already be queued. Suppress only
215        // candidates already waiting for a repair.
216        let already_pending = self.pending_group_sender_key_messages.contains(&parsed);
217        if already_pending && self.group_sender_key_message_has_pending_repair(&message) {
218            return Ok(ProtocolGroupIncomingResult {
219                consumed: true,
220                pending: true,
221                ..Default::default()
222            });
223        }
224        if !already_pending {
225            self.admit_fresh_group_sender_key_candidate(&parsed, usize::MAX);
226        }
227        let mut result = self.handle_group_sender_key_message(message)?;
228        if result.pending {
229            // The initial attempt already admitted (or spent) its fresh probe.
230            self.queue_group_sender_key_candidate(parsed, false)?;
231        } else if self.clear_pending_group_sender_key_candidate(&parsed) {
232            self.persist()?;
233        }
234        result.source_event_count = result.events.len();
235        if !result.pending {
236            let retry = self.retry_pending_group_inputs(NdrUnixSeconds(unix_now().get()))?;
237            result.events.extend(retry.events);
238            result.effects.extend(retry.effects);
239        }
240        Ok(ProtocolGroupIncomingResult {
241            consumed: true,
242            ..result
243        })
244    }
245
246    pub fn process_group_pairwise_payload(
247        &mut self,
248        payload: &[u8],
249        from_owner_pubkey: PublicKey,
250        from_sender_device_pubkey: Option<PublicKey>,
251    ) -> anyhow::Result<ProtocolGroupIncomingResult> {
252        self.process_group_pairwise_payload_inner(
253            payload,
254            from_owner_pubkey,
255            from_sender_device_pubkey,
256            true,
257            false,
258        )
259    }
260
261    pub fn process_local_sibling_group_pairwise_payload(
262        &mut self,
263        payload: &[u8],
264        from_owner_pubkey: PublicKey,
265        from_sender_device_pubkey: Option<PublicKey>,
266    ) -> anyhow::Result<ProtocolGroupIncomingResult> {
267        self.process_group_pairwise_payload_inner(
268            payload,
269            from_owner_pubkey,
270            from_sender_device_pubkey,
271            false,
272            true,
273        )
274    }
275
276    fn process_group_pairwise_payload_inner(
277        &mut self,
278        payload: &[u8],
279        from_owner_pubkey: PublicKey,
280        from_sender_device_pubkey: Option<PublicKey>,
281        forward_to_local_siblings: bool,
282        trust_local_sibling_attribution: bool,
283    ) -> anyhow::Result<ProtocolGroupIncomingResult> {
284        let (is_group_payload, is_supported_group_payload) =
285            classify_group_pairwise_payload(payload).unwrap_or((false, false));
286        if is_group_payload && !is_supported_group_payload {
287            return Ok(ProtocolGroupIncomingResult {
288                consumed: true,
289                ..Default::default()
290            });
291        }
292        let sender_device = from_sender_device_pubkey.map(ndr_device);
293        let sender_owner = ndr_owner(from_owner_pubkey);
294        if is_supported_group_payload && sender_device.is_none() && !trust_local_sibling_attribution
295        {
296            // A remote owner claim without the authenticated device identity D
297            // cannot be checked against AppKeys. Consume the control payload
298            // without applying it; normal pairwise decrypts always supply D.
299            return Ok(ProtocolGroupIncomingResult {
300                consumed: true,
301                ..Default::default()
302            });
303        }
304        let sender_owner = if trust_local_sibling_attribution {
305            // The outer pairwise session already authenticated a current local
306            // sibling. Its wrapper can carry the original remote owner/device
307            // so every sibling does not need to race the same AppKeys event.
308            sender_owner
309        } else {
310            match self.resolve_group_pairwise_sender_owner(sender_owner, sender_device) {
311                ProtocolSenderOwnerResolution::Verified { owner }
312                | ProtocolSenderOwnerResolution::ProvisionalDeviceOwner { owner } => owner,
313                ProtocolSenderOwnerResolution::PendingOwnerClaim {
314                    storage_owner,
315                    claimed_owner: _,
316                    sender_device,
317                } => {
318                    if is_supported_group_payload {
319                        self.queue_pending_group_pairwise_payload(
320                            storage_owner,
321                            Some(sender_device),
322                            payload.to_vec(),
323                            unix_now().get(),
324                        )?;
325                        return Ok(ProtocolGroupIncomingResult {
326                            consumed: true,
327                            ..Default::default()
328                        });
329                    }
330                    storage_owner
331                }
332            }
333        };
334        let remote_sender_key_distribution_group_id =
335            if sender_owner != self.local_owner && sender_device.is_some() {
336                JsonGroupPayloadCodecV1
337                    .decode_pairwise_command(payload)
338                    .ok()
339                    .flatten()
340                    .and_then(|command| match command {
341                        GroupPairwiseCommand::SenderKeyDistribution { distribution } => {
342                            Some(distribution.group_id)
343                        }
344                        _ => None,
345                    })
346            } else {
347                None
348            };
349        let previous_groups = is_supported_group_payload.then(|| self.group_manager.snapshot());
350        let result = match sender_device {
351            Some(device_pubkey) => {
352                self.group_manager
353                    .handle_pairwise_payload(sender_owner, device_pubkey, payload)
354            }
355            None => self.group_manager.handle_incoming(sender_owner, payload),
356        };
357
358        let now = NdrUnixSeconds(unix_now().get());
359        match result {
360            Ok(Some(event)) => {
361                let group_changed = previous_groups
362                    .as_ref()
363                    .is_some_and(|previous| *previous != self.group_manager.snapshot());
364                if let GroupIncomingEvent::SenderKeyRepairRequested(repair) = event {
365                    let effects = self.sender_key_repair_response_effects(
366                        repair.requester_owner,
367                        &repair.request,
368                        NdrUnixSeconds(unix_now().get()),
369                    )?;
370                    self.persist()?;
371                    return Ok(ProtocolGroupIncomingResult {
372                        effects,
373                        consumed: true,
374                        ..Default::default()
375                    });
376                }
377                let mut effects = Vec::new();
378                if group_changed && matches!(&event, GroupIncomingEvent::MetadataUpdated(_)) {
379                    for pending in &mut self.pending_group_pairwise_payloads {
380                        pending.next_retry_at_secs = 0;
381                    }
382                }
383                if group_changed && forward_to_local_siblings && sender_owner != self.local_owner {
384                    // Learning a sender chain also returns MetadataUpdated, although the
385                    // roster itself did not change. Forward only the control that changed.
386                    if let (Some(group_id), Some(sender_device)) =
387                        (&remote_sender_key_distribution_group_id, sender_device)
388                    {
389                        effects.extend(
390                            self.sync_remote_group_sender_key_distribution_to_local_siblings(
391                                group_id,
392                                sender_owner,
393                                sender_device,
394                                payload,
395                            )?,
396                        );
397                    } else if let GroupIncomingEvent::MetadataUpdated(group) = &event {
398                        effects.extend(self.sync_group_to_local_siblings(group)?);
399                    }
400                }
401                let mut events = vec![event];
402                let retry = self.retry_pending_group_inputs(now)?;
403                events.extend(retry.events);
404                effects.extend(retry.effects);
405                let fanout_retry = self.retry_pending_group_fanouts(now)?;
406                effects.extend(fanout_retry.effects);
407                if group_changed || !effects.is_empty() {
408                    self.persist()?;
409                }
410                Ok(ProtocolGroupIncomingResult {
411                    events,
412                    effects,
413                    consumed: true,
414                    ..Default::default()
415                })
416            }
417            Ok(None) => Ok(ProtocolGroupIncomingResult {
418                consumed: is_group_payload,
419                ..Default::default()
420            }),
421            Err(error) => {
422                if is_supported_group_payload {
423                    self.queue_pending_group_pairwise_payload(
424                        sender_owner,
425                        sender_device,
426                        payload.to_vec(),
427                        unix_now().get(),
428                    )?;
429                    Ok(ProtocolGroupIncomingResult {
430                        consumed: true,
431                        ..Default::default()
432                    })
433                } else {
434                    Err(error.into())
435                }
436            }
437        }
438    }
439
440    /// Resolve a sender-key delivery already authenticated and durably journaled.
441    /// External pairwise group-message payloads remain unsupported. The caller
442    /// acknowledges this ID only after its application snapshot is saved.
443    pub fn pending_group_decrypted_delivery(
444        &self,
445        event_id: &str,
446    ) -> anyhow::Result<Option<GroupIncomingEvent>> {
447        if !event_id.starts_with("group-delivery:") {
448            return Ok(None);
449        }
450        let pending = self
451            .pending_decrypted_deliveries
452            .iter()
453            .find(|pending| pending.event_id.as_deref() == Some(event_id))
454            .ok_or_else(|| anyhow::anyhow!("group delivery is not in the authenticated journal"))?;
455        let Some(GroupPairwiseCommand::GroupMessage {
456            group_id,
457            revision,
458            body,
459        }) = JsonGroupPayloadCodecV1.decode_pairwise_command(pending.content.as_bytes())?
460        else {
461            anyhow::bail!("invalid group delivery journal envelope");
462        };
463        Ok(Some(GroupIncomingEvent::Message(
464            nostr_double_ratchet::GroupReceivedMessage {
465                group_id,
466                revision,
467                body,
468                sender_owner: ndr_owner(pending.sender),
469                sender_device: pending.sender_device.map(ndr_device),
470            },
471        )))
472    }
473
474    pub fn retry_pending_protocol(
475        &mut self,
476        now: NdrUnixSeconds,
477    ) -> anyhow::Result<ProtocolRetryBatch> {
478        {
479            #[cfg(all(test, target_os = "linux"))]
480            let _phase = tests::RetryPhaseTimer::start("retirement");
481            self.retire_ineligible_group_publications()?;
482            self.flush_discarded_direct_receives()?;
483        }
484        let group_result = {
485            #[cfg(all(test, target_os = "linux"))]
486            let _phase = tests::RetryPhaseTimer::start("group_inputs");
487            self.retry_pending_group_inputs(now)?
488        };
489        let group_fanout_result = {
490            #[cfg(all(test, target_os = "linux"))]
491            let _phase = tests::RetryPhaseTimer::start("group_fanouts");
492            self.retry_pending_group_fanouts(now)?
493        };
494        let mut group_result = group_result;
495        group_result.effects.extend(group_fanout_result.effects);
496        {
497            #[cfg(all(test, target_os = "linux"))]
498            let _phase = tests::RetryPhaseTimer::start("inbound");
499            self.retry_pending_inbound_direct_events(now)?;
500        }
501        let direct_messages = self
502            .pending_decrypted_deliveries
503            .iter()
504            .filter(|delivery| !delivery.discarded)
505            .cloned()
506            .map(ProtocolDecryptedMessage::from)
507            .collect::<Vec<_>>();
508        let mut effects = {
509            #[cfg(all(test, target_os = "linux"))]
510            let _phase = tests::RetryPhaseTimer::start("remote_sends");
511            self.retry_pending_remote_sends(now)?
512        };
513        {
514            #[cfg(all(test, target_os = "linux"))]
515            let _phase = tests::RetryPhaseTimer::start("local_siblings");
516            effects.extend(self.retry_pending_local_sibling_sends(now)?);
517        }
518        let offered = group_result
519            .effects
520            .iter()
521            .chain(&effects)
522            .map(|effect| {
523                let ProtocolEffect::Publish(publish) = effect;
524                publish.event.id
525            })
526            .collect::<HashSet<_>>();
527        effects.extend(
528            self.pending_group_publish_effects()
529                .into_iter()
530                .filter(|effect| {
531                    let ProtocolEffect::Publish(publish) = effect;
532                    !offered.contains(&publish.event.id)
533                }),
534        );
535        let batch = ProtocolRetryBatch {
536            group_result,
537            direct_messages,
538            effects,
539        };
540        if !batch.is_empty() {
541            self.subscription_generation = self.subscription_generation.wrapping_add(1);
542        }
543        Ok(batch)
544    }
545
546    pub(super) fn discard_pending_messages_for_other_devices(&mut self) -> anyhow::Result<()> {
547        let foreign_events = self
548            .pending_inbound
549            .iter()
550            .filter(|pending| self.message_targets_another_device(&pending.event))
551            .map(|pending| pending.event.clone())
552            .collect::<Vec<_>>();
553        if foreign_events.is_empty() {
554            return Ok(());
555        }
556        let ids = foreign_events
557            .iter()
558            .map(|event| event.id)
559            .collect::<HashSet<_>>();
560        self.with_state_checkpoint(|engine| {
561            engine.invalidate_pending_inbound_retry_deadline();
562            engine
563                .pending_inbound
564                .retain(|pending| !ids.contains(&pending.event.id));
565            // Older clients also tried these packets as camouflaged group messages.
566            for event in &foreign_events {
567                engine.clear_pending_group_sender_key_candidate_for_direct_event(event);
568            }
569            engine.persist()
570        })
571    }
572
573    pub fn ack_pending_decrypted_deliveries(&mut self) -> anyhow::Result<()> {
574        let ids = self
575            .pending_decrypted_deliveries
576            .iter()
577            .filter(|delivery| !delivery.discarded)
578            .filter_map(|delivery| delivery.event_id.clone())
579            .collect();
580        self.ack_decrypted_delivery_ids(&ids)
581    }
582
583    /// Only acknowledge deliveries that the application has durably applied.
584    /// Other saves must not discard plaintext waiting for its first delivery.
585    pub fn ack_decrypted_delivery_ids(
586        &mut self,
587        event_ids: &HashSet<String>,
588    ) -> anyhow::Result<()> {
589        if !self.pending_decrypted_deliveries.iter().any(|delivery| {
590            delivery
591                .event_id
592                .as_ref()
593                .is_some_and(|id| event_ids.contains(id))
594        }) {
595            return Ok(());
596        }
597        let before = self.pending_decrypted_deliveries.clone();
598        self.pending_decrypted_deliveries.retain(|delivery| {
599            delivery.discarded
600                || !delivery
601                    .event_id
602                    .as_ref()
603                    .is_some_and(|id| event_ids.contains(id))
604        });
605        if let Err(error) = self.persist() {
606            self.pending_decrypted_deliveries = before;
607            return Err(error);
608        }
609        Ok(())
610    }
611
612    pub(super) fn retry_pending_inbound_direct_events_inner(
613        &mut self,
614        now: NdrUnixSeconds,
615        fingerprints: &mut InboundReceiveFingerprints,
616    ) -> anyhow::Result<Vec<ProtocolDecryptedMessage>> {
617        // A failed save can leave a ratchet advanced with its delivery journal.
618        // Keep the hint unknown until the entire pass succeeds, so retry and
619        // rollback paths rederive it from the retained queue.
620        self.invalidate_pending_inbound_retry_deadline();
621        // Keep the queue installed while decrypting: decryption persists the
622        // ratchet and its delivery journal, including all not-yet-tried events.
623        // Group continuations run every 25 ms even while inbound packets are
624        // backed off. Copy only due packets, not the entire deferred backlog.
625        let pending = self
626            .pending_inbound
627            .iter()
628            .enumerate()
629            .filter(|(_, pending)| {
630                #[cfg(test)]
631                tests::record_inbound_retry_deadline_scan();
632                pending.next_retry_at_secs <= now.get()
633            })
634            .map(|(index, pending)| (index, pending.clone()))
635            .collect::<Vec<_>>();
636        let mut messages = Vec::new();
637        for (original_index, pending) in pending {
638            match self.decrypt_pending_direct_message_with_cache(&pending, fingerprints) {
639                Ok(DirectReceive::Message(message)) => {
640                    self.pending_inbound
641                        .retain(|item| item.event.id != pending.event.id);
642                    messages.push(message);
643                }
644                Ok(DirectReceive::Discarded) => {
645                    self.pending_inbound
646                        .retain(|item| item.event.id != pending.event.id);
647                }
648                Err(error) if error.downcast_ref::<StorageError>().is_some() => {
649                    // A failed save may follow a successful ratchet advance.
650                    // Keep the delivery journal intact and let the caller retry.
651                    return Err(error);
652                }
653                Ok(DirectReceive::Pending) | Err(_) => {
654                    // One undecryptable event must not block other senders or
655                    // outgoing acknowledgements. Retain it with backoff so a
656                    // later handshake can still make it decryptable.
657                    // Failed receives normally leave the queue in place. Reuse
658                    // that position instead of searching the whole queue for
659                    // every failed packet. Earlier deliveries can remove entries,
660                    // so verify the ID and fall back to a search after a shift.
661                    let matches_id = |item: &ProtocolPendingInbound| {
662                        #[cfg(test)]
663                        tests::record_inbound_retry_lookup();
664                        item.event.id == pending.event.id
665                    };
666                    let index = if self
667                        .pending_inbound
668                        .get(original_index)
669                        .is_some_and(matches_id)
670                    {
671                        Some(original_index)
672                    } else {
673                        self.pending_inbound.iter().position(matches_id)
674                    };
675                    if let Some(index) = index {
676                        self.pending_inbound[index].next_retry_at_secs =
677                            next_pending_retry_at_secs(pending.created_at_secs, now);
678                    }
679                }
680            }
681        }
682        self.refresh_pending_inbound_retry_deadline();
683        Ok(messages)
684    }
685
686    #[cfg(test)]
687    pub(super) fn decrypt_pending_direct_message_event(
688        &mut self,
689        pending: &ProtocolPendingInbound,
690    ) -> anyhow::Result<DirectReceive> {
691        if let Some(envelope) = pending.envelope.as_ref() {
692            return self.decrypt_direct_message_envelope(&pending.event, envelope, true);
693        }
694        self.decrypt_direct_message_event(&pending.event)
695    }
696
697    #[cfg(test)]
698    fn decrypt_direct_message_event(&mut self, event: &Event) -> anyhow::Result<DirectReceive> {
699        let envelope = parse_message_event(event)?;
700        self.decrypt_direct_message_envelope(event, &envelope, true)
701    }
702
703    fn decrypt_direct_message_envelope(
704        &mut self,
705        event: &Event,
706        envelope: &MessageEnvelope,
707        record_delivery: bool,
708    ) -> anyhow::Result<DirectReceive> {
709        self.decrypt_direct_message_envelope_with_cache(event, envelope, record_delivery, None)
710    }
711
712    pub(super) fn decrypt_direct_message_envelope_with_cache(
713        &mut self,
714        event: &Event,
715        envelope: &MessageEnvelope,
716        record_delivery: bool,
717        fingerprints: Option<&mut InboundReceiveFingerprints>,
718    ) -> anyhow::Result<DirectReceive> {
719        if let Some(delivery) = self
720            .pending_decrypted_deliveries
721            .iter()
722            .find(|delivery| delivery.event_id.as_deref() == Some(event.id.to_hex().as_str()))
723            .cloned()
724        {
725            // The ratchet may already have advanced before a previous save failed.
726            if delivery.discarded {
727                self.flush_discarded_direct_receives()?;
728                return Ok(DirectReceive::Discarded);
729            }
730            self.persist()?;
731            return Ok(DirectReceive::Message(delivery.into()));
732        }
733        let sender_owner = match self.resolve_message_sender_owner(envelope) {
734            ProtocolSenderOwnerResolution::Verified { owner }
735            | ProtocolSenderOwnerResolution::ProvisionalDeviceOwner { owner } => owner,
736            ProtocolSenderOwnerResolution::PendingOwnerClaim { .. } => {
737                return Ok(DirectReceive::Pending);
738            }
739        };
740        let Some(received) =
741            self.receive_direct_message_with_cache(event, envelope, sender_owner, fingerprints)?
742        else {
743            return Ok(DirectReceive::Pending);
744        };
745        self.clear_pending_group_sender_key_candidate_for_direct_event(event);
746        self.invalidate_known_message_author_cache();
747        let from_local_sibling = received.owner_pubkey == self.local_owner;
748        let local_sibling = from_local_sibling
749            .then(|| decode_local_sibling_payload(&received.payload))
750            .flatten();
751        let (conversation_owner, sender, sender_device, payload) = match local_sibling {
752            Some((owner, original_device, payload)) => (
753                Some(owner),
754                original_device.map_or(self.owner_pubkey, |_| owner),
755                original_device.or(Some(public_device(received.device_pubkey)?)),
756                payload,
757            ),
758            _ => (
759                None,
760                public_owner(received.owner_pubkey)?,
761                Some(public_device(received.device_pubkey)?),
762                received.payload,
763            ),
764        };
765        // Own-device controls use history-aware reconciliation. Never put their
766        // replacement text in the delivery journal before application admission.
767        if from_local_sibling && is_message_mutation_payload(&payload) {
768            self.pending_decrypted_deliveries
769                .push(ProtocolPendingDecryptedDelivery {
770                    discarded: true,
771                    sender,
772                    sender_device,
773                    conversation_owner,
774                    content: String::new(),
775                    event_id: Some(event.id.to_string()),
776                    created_at_secs: event.created_at.as_secs(),
777                });
778            self.invalidate_pending_inbound_retry_deadline();
779            self.pending_inbound
780                .retain(|pending| pending.event.id != event.id);
781            self.flush_discarded_direct_receives()?;
782            return Ok(DirectReceive::Discarded);
783        }
784        let content = String::from_utf8(payload)?;
785        let decrypted = ProtocolDecryptedMessage {
786            created_at_secs: event.created_at.as_secs(),
787            sender,
788            sender_device,
789            conversation_owner,
790            content,
791            event_id: Some(event.id.to_string()),
792        };
793        if record_delivery {
794            self.record_pending_decrypted_delivery(decrypted.clone(), event.created_at.as_secs());
795        }
796        self.persist()?;
797        Ok(DirectReceive::Message(decrypted))
798    }
799
800    fn retry_pending_group_inputs(
801        &mut self,
802        now: NdrUnixSeconds,
803    ) -> anyhow::Result<ProtocolGroupIncomingResult> {
804        let mut result = ProtocolGroupIncomingResult {
805            consumed: false,
806            ..Default::default()
807        };
808
809        let pairwise = std::mem::take(&mut self.pending_group_pairwise_payloads);
810        let mut still_pairwise = Vec::new();
811        let mut persist_needed = false;
812        for mut pending in pairwise {
813            if pending.next_retry_at_secs > now.get() {
814                still_pairwise.push(pending);
815                continue;
816            }
817            let (_, is_supported_group_payload) =
818                classify_group_pairwise_payload(&pending.payload).unwrap_or((false, false));
819            if !is_supported_group_payload {
820                persist_needed = true;
821                continue;
822            }
823            if pending.sender_device.is_none() {
824                // Legacy device-less entries have no cryptographic identity to
825                // authorize against the claimed owner and can never be promoted.
826                persist_needed = true;
827                continue;
828            }
829            let sender_resolution = self
830                .resolve_group_pairwise_sender_owner(pending.sender_owner, pending.sender_device);
831            let sender_owner = match sender_resolution {
832                ProtocolSenderOwnerResolution::Verified { owner }
833                | ProtocolSenderOwnerResolution::ProvisionalDeviceOwner { owner } => owner,
834                ProtocolSenderOwnerResolution::PendingOwnerClaim { .. } => {
835                    pending.next_retry_at_secs =
836                        next_pending_retry_at_secs(pending.created_at_secs, now);
837                    still_pairwise.push(pending);
838                    continue;
839                }
840            };
841            let outcome = match pending.sender_device {
842                Some(device_pubkey) => self.group_manager.handle_pairwise_payload(
843                    sender_owner,
844                    device_pubkey,
845                    &pending.payload,
846                ),
847                None => self
848                    .group_manager
849                    .handle_incoming(sender_owner, &pending.payload),
850            };
851            match outcome {
852                Ok(Some(event)) => {
853                    if let GroupIncomingEvent::SenderKeyRepairRequested(repair) = event {
854                        let effects = self.sender_key_repair_response_effects(
855                            repair.requester_owner,
856                            &repair.request,
857                            now,
858                        )?;
859                        result.effects.extend(effects);
860                    } else {
861                        result.events.push(event);
862                    }
863                    persist_needed = true;
864                }
865                Ok(None) => {
866                    persist_needed = true;
867                }
868                Err(_) => {
869                    pending.next_retry_at_secs =
870                        next_pending_retry_at_secs(pending.created_at_secs, now);
871                    still_pairwise.push(pending);
872                }
873            }
874        }
875        self.pending_group_pairwise_payloads = still_pairwise;
876
877        let sender_keys = self.retry_eligible_group_sender_key_messages()?;
878        result.events.extend(sender_keys.events);
879        result.effects.extend(sender_keys.effects);
880        let repair_effects = self.retry_pending_group_sender_key_repairs(now)?;
881        if !repair_effects.is_empty() {
882            result.effects.extend(repair_effects);
883            persist_needed = true;
884        }
885        if persist_needed {
886            self.persist()?;
887        }
888        Ok(result)
889    }
890}