Skip to main content

iris_chat_protocol/protocol_engine/
engine_sender_key_repair.rs

1use super::*;
2
3impl ProtocolEngine {
4    /// Resolve an authenticated private carrier without advertising group authors.
5    pub fn group_id_for_sender_event_author(&self, author: PublicKey) -> Option<String> {
6        self.group_manager
7            .group_id_for_sender_event_pubkey(ndr_device(author))
8    }
9
10    pub fn has_pending_group_pairwise_payloads(&self) -> bool {
11        !self.pending_group_pairwise_payloads.is_empty()
12    }
13
14    /// A bounded rotating slice of durable, authenticated key distributions
15    /// still waiting for their group metadata, including after a restart.
16    pub fn missing_group_metadata_sources(&self) -> Vec<(PublicKey, String, PublicKey)> {
17        let pending = &self.pending_group_pairwise_payloads;
18        if pending.is_empty() {
19            return Vec::new();
20        }
21        let start = ((unix_now().get() / 60) as usize % pending.len().div_ceil(32)) * 32;
22        pending
23            .iter()
24            .skip(start)
25            .take(32)
26            .filter_map(|pending| {
27                let owner = match self.resolve_group_pairwise_sender_owner(
28                    pending.sender_owner,
29                    pending.sender_device,
30                ) {
31                    ProtocolSenderOwnerResolution::Verified { owner } => owner,
32                    _ => return None,
33                };
34                let GroupPairwiseCommand::SenderKeyDistribution { distribution } =
35                    JsonGroupPayloadCodecV1
36                        .decode_pairwise_command(&pending.payload)
37                        .ok()??
38                else {
39                    return None;
40                };
41                if self.group_manager.group(&distribution.group_id).is_some() {
42                    return None;
43                }
44                Some((
45                    public_owner(owner).ok()?,
46                    distribution.group_id,
47                    public_device(distribution.sender_event_pubkey).ok()?,
48                ))
49            })
50            .collect()
51    }
52
53    /// Resend only to an existing member. Ordinary members' metadata copies still
54    /// pass the normal admin admission check; they cannot establish authority.
55    pub fn resend_private_group_context(
56        &mut self,
57        group_id: &str,
58        requester: PublicKey,
59        author: Option<PublicKey>,
60    ) -> anyhow::Result<Vec<ProtocolEffect>> {
61        let Some(group) = self.group_manager.group(group_id) else {
62            return Ok(Vec::new());
63        };
64        if !group.members.contains(&self.local_owner)
65            || !group.members.contains(&ndr_owner(requester))
66        {
67            return Ok(Vec::new());
68        }
69        self.with_state_checkpoint(|engine| {
70            let effects = engine.sender_key_repair_response_effects(
71                ndr_owner(requester),
72                &SenderKeyRepairRequest {
73                    group_id: group.group_id,
74                    sender_event_pubkey: author.map(ndr_device).unwrap_or(engine.local_device),
75                    key_id: None,
76                    message_number: None,
77                    required_revision: Some(group.revision),
78                    created_at: NdrUnixSeconds(unix_now().get()),
79                },
80                NdrUnixSeconds(unix_now().get()),
81            )?;
82            engine.retain_group_publications(&effects, Some(ndr_owner(requester)))?;
83            engine.persist()?;
84            Ok(effects)
85        })
86    }
87    pub(super) fn queue_pending_group_sender_key_message(
88        &mut self,
89        parsed: nostr_double_ratchet::wire::ParsedGroupSenderKeyMessageEvent,
90    ) -> anyhow::Result<()> {
91        self.queue_group_sender_key_candidate(parsed, true)
92    }
93
94    pub(super) fn queue_group_sender_key_candidate(
95        &mut self,
96        parsed: nostr_double_ratchet::wire::ParsedGroupSenderKeyMessageEvent,
97        fresh: bool,
98    ) -> anyhow::Result<()> {
99        if self.unmapped_group_sender_key_candidate_is_known_message_author(&parsed) {
100            return Ok(());
101        }
102
103        if self
104            .group_sender_key_message_from_parsed(&parsed)
105            .is_some_and(|message| {
106                self.processed_group_sender_key_messages
107                    .contains(&group_sender_key_fingerprint(&message))
108            })
109        {
110            return Ok(());
111        }
112
113        if let Some(group_id) = self.inactive_local_group_id_for_sender_key_candidate(&parsed) {
114            if self.clear_group_sender_key_repairs(
115                &group_id,
116                parsed.sender_event_pubkey,
117                parsed.encrypted_header.is_none().then_some(parsed.key_id),
118                parsed
119                    .encrypted_header
120                    .is_none()
121                    .then_some(parsed.message_number),
122            ) {
123                self.persist()?;
124            }
125            return Ok(());
126        }
127
128        if !self.pending_group_sender_key_messages.contains(&parsed) {
129            if fresh {
130                self.admit_fresh_group_sender_key_candidate(
131                    &parsed,
132                    self.pending_group_sender_key_messages.len(),
133                );
134            }
135            // Authenticated group ingress can add a candidate after this
136            // stream's old backlog was already attempted. Admit that candidate
137            // once even if its key/revision inputs have not changed.
138            if let Some(message) = self.group_sender_key_message_from_parsed(&parsed) {
139                let fingerprint = group_sender_key_fingerprint(&message);
140                let mut retry = self.group_sender_key_retry.borrow_mut();
141                if let Some(fresh) = retry
142                    .fresh
143                    .iter_mut()
144                    .find(|fresh| fresh.fingerprint == fingerprint)
145                {
146                    fresh.pending_index = self.pending_group_sender_key_messages.len();
147                }
148                if retry.queued.insert(fingerprint.clone()) {
149                    retry
150                        .ready
151                        .push_back((parsed.sender_event_pubkey, fingerprint));
152                }
153            }
154            self.pending_group_sender_key_messages.push(parsed);
155            self.persist()?;
156        }
157        Ok(())
158    }
159
160    pub(super) fn clear_pending_group_sender_key_candidate_for_direct_event(
161        &mut self,
162        event: &Event,
163    ) {
164        if !protocol_event_has_tag(event, "header") {
165            return;
166        }
167        let Ok(parsed) = parse_group_sender_key_message_event_unchecked(event) else {
168            return;
169        };
170        self.clear_pending_group_sender_key_candidate(&parsed);
171    }
172
173    pub(super) fn clear_pending_group_sender_key_candidate(
174        &mut self,
175        parsed: &nostr_double_ratchet::wire::ParsedGroupSenderKeyMessageEvent,
176    ) -> bool {
177        {
178            let mut retry = self.group_sender_key_retry.borrow_mut();
179            retry.fresh.retain(|fresh| fresh.parsed.as_ref() != parsed);
180            if retry
181                .active
182                .as_ref()
183                .is_some_and(|active| active.matches(parsed))
184            {
185                retry.active = None;
186            }
187        }
188        let original_len = self.pending_group_sender_key_messages.len();
189        self.pending_group_sender_key_messages
190            .retain(|pending| pending != parsed);
191        if self.pending_group_sender_key_messages.len() == original_len {
192            return false;
193        }
194
195        let Some(group_id) = self
196            .group_manager
197            .group_id_for_sender_event_pubkey(parsed.sender_event_pubkey)
198        else {
199            return true;
200        };
201        if !self.has_pending_group_sender_key_candidate(&group_id, parsed.sender_event_pubkey) {
202            self.clear_group_sender_key_repairs(
203                &group_id,
204                parsed.sender_event_pubkey,
205                parsed.encrypted_header.is_none().then_some(parsed.key_id),
206                parsed
207                    .encrypted_header
208                    .is_none()
209                    .then_some(parsed.message_number),
210            );
211        }
212        true
213    }
214
215    fn has_pending_group_sender_key_candidate(
216        &self,
217        group_id: &str,
218        sender_event_pubkey: NdrDevicePubkey,
219    ) -> bool {
220        self.pending_group_sender_key_messages
221            .iter()
222            .any(|pending| {
223                pending.sender_event_pubkey == sender_event_pubkey
224                    && self
225                        .group_manager
226                        .group_id_for_sender_event_pubkey(pending.sender_event_pubkey)
227                        .as_deref()
228                        == Some(group_id)
229            })
230    }
231
232    pub(super) fn unmapped_group_sender_key_candidate_is_known_message_author(
233        &self,
234        parsed: &nostr_double_ratchet::wire::ParsedGroupSenderKeyMessageEvent,
235    ) -> bool {
236        if self
237            .group_manager
238            .group_id_for_sender_event_pubkey(parsed.sender_event_pubkey)
239            .is_some()
240        {
241            return false;
242        }
243
244        public_device(parsed.sender_event_pubkey)
245            .is_ok_and(|author| self.is_known_message_author(author))
246    }
247
248    fn inactive_local_group_id_for_sender_key_candidate(
249        &self,
250        parsed: &nostr_double_ratchet::wire::ParsedGroupSenderKeyMessageEvent,
251    ) -> Option<String> {
252        let group_id = self
253            .group_manager
254            .group_id_for_sender_event_pubkey(parsed.sender_event_pubkey)?;
255        self.local_owner_is_inactive_for_group(&group_id)
256            .then_some(group_id)
257    }
258
259    pub(super) fn local_owner_is_inactive_for_group(&self, group_id: &str) -> bool {
260        self.group_manager
261            .group_has_member(group_id, self.local_owner)
262            == Some(false)
263    }
264
265    fn inactive_local_group_ids(&self) -> HashSet<String> {
266        self.group_manager
267            .snapshot()
268            .groups
269            .into_iter()
270            .filter(|group| !group.members.contains(&self.local_owner))
271            .map(|group| group.group_id)
272            .collect()
273    }
274
275    pub(super) fn prune_pending_group_sender_key_work_for_inactive_local_groups(&mut self) -> bool {
276        let inactive_group_ids = self.inactive_local_group_ids();
277        if inactive_group_ids.is_empty() {
278            return false;
279        }
280        let inactive_sender_event_pubkeys = self
281            .group_manager
282            .snapshot()
283            .sender_keys
284            .into_iter()
285            .filter(|record| inactive_group_ids.contains(&record.group_id))
286            .map(|record| record.sender_event_pubkey)
287            .collect::<HashSet<_>>();
288
289        let original_message_len = self.pending_group_sender_key_messages.len();
290        self.pending_group_sender_key_messages.retain(|pending| {
291            !inactive_sender_event_pubkeys.contains(&pending.sender_event_pubkey)
292        });
293
294        let original_repair_len = self.pending_group_sender_key_repairs.len();
295        self.pending_group_sender_key_repairs
296            .retain(|pending| !inactive_group_ids.contains(&pending.group_id));
297
298        self.pending_group_sender_key_messages.len() != original_message_len
299            || self.pending_group_sender_key_repairs.len() != original_repair_len
300    }
301
302    pub(super) fn group_sender_key_message_from_parsed(
303        &self,
304        parsed: &nostr_double_ratchet::wire::ParsedGroupSenderKeyMessageEvent,
305    ) -> Option<GroupSenderKeyMessage> {
306        let group_id = self
307            .group_manager
308            .group_id_for_sender_event_pubkey(parsed.sender_event_pubkey)?;
309        Some(GroupSenderKeyMessage {
310            group_id,
311            sender_event_pubkey: parsed.sender_event_pubkey,
312            key_id: parsed.key_id,
313            message_number: parsed.message_number,
314            encrypted_header: parsed.encrypted_header.clone(),
315            created_at: parsed.created_at,
316            ciphertext: parsed.ciphertext.clone(),
317        })
318    }
319
320    pub(super) fn group_sender_key_message_has_pending_repair(
321        &self,
322        message: &GroupSenderKeyMessage,
323    ) -> bool {
324        let sender = message.sender_event_pubkey.to_hex();
325        let key_id = message.encrypted_header.is_none().then_some(message.key_id);
326        let number = message
327            .encrypted_header
328            .is_none()
329            .then_some(message.message_number);
330        self.pending_group_sender_key_repairs.iter().any(|pending| {
331            pending.group_id == message.group_id
332                && pending.sender_event_pubkey_hex == sender
333                && (message.encrypted_header.is_some()
334                    || (pending.key_id == key_id && pending.message_number == number))
335        })
336    }
337
338    pub(super) fn handle_group_sender_key_message(
339        &mut self,
340        message: GroupSenderKeyMessage,
341    ) -> anyhow::Result<ProtocolGroupIncomingResult> {
342        let fingerprint = group_sender_key_fingerprint(&message);
343        if self.local_owner_is_inactive_for_group(&message.group_id) {
344            let mut retry = self.group_sender_key_retry.borrow_mut();
345            if retry
346                .active
347                .as_ref()
348                .is_some_and(|active| active.fingerprint == fingerprint)
349            {
350                retry.active = None;
351            }
352            retry.prepared = None;
353            return Ok(ProtocolGroupIncomingResult {
354                consumed: true,
355                ..Default::default()
356            });
357        }
358        if self
359            .processed_group_sender_key_messages
360            .contains(&fingerprint)
361        {
362            return Ok(ProtocolGroupIncomingResult {
363                consumed: true,
364                ..Default::default()
365            });
366        }
367        // Our publishing chain has already advanced, so decrypting its relay echo
368        // searches every skipped key and starts a pointless repair. This durable
369        // secret identifies this device's own stream, including after restart.
370        if self.group_manager.is_local_sender_key_stream(
371            &message.group_id,
372            message.sender_event_pubkey,
373            self.local_device,
374        ) {
375            if self.clear_group_sender_key_repairs(
376                &message.group_id,
377                message.sender_event_pubkey,
378                None,
379                None,
380            ) {
381                self.persist()?;
382            }
383            return Ok(ProtocolGroupIncomingResult {
384                consumed: true,
385                ..Default::default()
386            });
387        }
388        // Planning and duplicate/self-echo checks do not mutate durable state.
389        // Only a ready receive (or duplicate marker) needs a rollback checkpoint.
390        let plan = match self.prepare_group_sender_key_message(&message, &fingerprint) {
391            Ok(Some(plan)) => Ok(plan),
392            Ok(None) => {
393                return Ok(ProtocolGroupIncomingResult {
394                    consumed: true,
395                    pending: true,
396                    ..Default::default()
397                })
398            }
399            Err(error @ nostr_double_ratchet::Error::Decryption(_))
400                if matches!(&error, nostr_double_ratchet::Error::Decryption(reason)
401                    if reason == "duplicate or missing sender-key message") =>
402            {
403                Err(error)
404            }
405            Err(error) => return Err(error.into()),
406        };
407        if self.batch_depth.get() > 0 {
408            return self.apply_group_sender_key_message(message, fingerprint, plan);
409        }
410        // The sender ratchet, replay marker and app delivery must advance together.
411        // The ciphertext queue is unchanged here; avoid cloning its backlog.
412        #[cfg(test)]
413        {
414            self.group_sender_key_retry.borrow_mut().receive_checkpoints += 1;
415        }
416        let checkpoint = (
417            self.group_manager.clone(),
418            self.session_manager.clone(),
419            self.pending_group_sender_key_repairs.clone(),
420            self.pending_group_fanouts.clone(),
421            self.processed_group_sender_key_messages.clone(),
422            self.pending_decrypted_deliveries.clone(),
423            self.batch_persist_dirty.get(),
424        );
425        let result = self.apply_group_sender_key_message(message, fingerprint, plan);
426        if result.is_err() {
427            self.group_manager = checkpoint.0;
428            self.session_manager = checkpoint.1;
429            self.pending_group_sender_key_repairs = checkpoint.2;
430            self.invalidate_pending_group_fanout_retry_deadline();
431            self.pending_group_fanouts = checkpoint.3;
432            self.processed_group_sender_key_messages = checkpoint.4;
433            self.pending_decrypted_deliveries = checkpoint.5;
434            self.batch_persist_dirty.set(checkpoint.6);
435            self.invalidate_known_message_author_cache();
436        }
437        result
438    }
439
440    fn apply_group_sender_key_message(
441        &mut self,
442        message: GroupSenderKeyMessage,
443        fingerprint: String,
444        plan: nostr_double_ratchet::Result<nostr_double_ratchet::GroupSenderKeyReceivePlan>,
445    ) -> anyhow::Result<ProtocolGroupIncomingResult> {
446        let message_repair_group_id = message.group_id.clone();
447        let message_repair_sender = message.sender_event_pubkey;
448        let message_repair_key_id = message.encrypted_header.is_none().then_some(message.key_id);
449        let message_repair_number = message
450            .encrypted_header
451            .is_none()
452            .then_some(message.message_number);
453        let result =
454            match plan.and_then(|plan| self.group_manager.apply_sender_key_receive_plan(plan)) {
455                Ok(result) => result,
456                Err(nostr_double_ratchet::Error::Decryption(error))
457                    if error == "duplicate or missing sender-key message" =>
458                {
459                    self.clear_group_sender_key_repairs(
460                        &message_repair_group_id,
461                        message_repair_sender,
462                        message_repair_key_id,
463                        message_repair_number,
464                    );
465                    self.processed_group_sender_key_messages.insert(fingerprint);
466                    self.persist()?;
467                    return Ok(ProtocolGroupIncomingResult {
468                        consumed: true,
469                        ..Default::default()
470                    });
471                }
472                Err(error) => return Err(error.into()),
473            };
474        let now = NdrUnixSeconds(unix_now().get());
475        let repair_request =
476            SenderKeyRepairRequest::from_pending_sender_key_message(&message, &result, now);
477        match result {
478            GroupSenderKeyHandleResult::Event(event) => {
479                let discard = matches!(&event, GroupIncomingEvent::Message(received)
480                    if received.sender_owner == self.local_owner
481                        && is_message_mutation_payload(&received.body));
482                if let GroupIncomingEvent::Message(received) = &event {
483                    if !discard {
484                        // Replay through the authenticated group envelope, retaining its
485                        // scope even if the inner rumor contains a conflicting group tag.
486                        let payload = JsonGroupPayloadCodecV1.encode_pairwise_command(
487                            nostr_double_ratchet::GroupPayloadEncodeContext {
488                                local_device_pubkey: received
489                                    .sender_device
490                                    .unwrap_or(self.local_device),
491                                created_at: message.created_at,
492                            },
493                            &GroupPairwiseCommand::GroupMessage {
494                                group_id: received.group_id.clone(),
495                                revision: received.revision,
496                                body: received.body.clone(),
497                            },
498                        )?;
499                        self.record_pending_decrypted_delivery(
500                            ProtocolDecryptedMessage {
501                                sender: public_owner(received.sender_owner)?,
502                                sender_device: received
503                                    .sender_device
504                                    .map(public_device)
505                                    .transpose()?,
506                                conversation_owner: None,
507                                content: String::from_utf8(payload)?,
508                                event_id: Some(format!("group-delivery:{fingerprint}")),
509                                created_at_secs: message.created_at.get(),
510                            },
511                            message.created_at.get(),
512                        );
513                    }
514                }
515                self.clear_group_sender_key_repairs(
516                    &message_repair_group_id,
517                    message_repair_sender,
518                    message_repair_key_id,
519                    message_repair_number,
520                );
521                self.processed_group_sender_key_messages.insert(fingerprint);
522                self.persist()?;
523                Ok(ProtocolGroupIncomingResult {
524                    events: if discard { Vec::new() } else { vec![event] },
525                    consumed: true,
526                    ..Default::default()
527                })
528            }
529            GroupSenderKeyHandleResult::PendingDistribution { .. }
530            | GroupSenderKeyHandleResult::PendingRevision { .. } => {
531                let Some(request) = repair_request else {
532                    anyhow::bail!("pending sender-key result did not produce a repair request");
533                };
534                let effects = self.sender_key_repair_request_effects(request, now)?;
535                Ok(ProtocolGroupIncomingResult {
536                    consumed: true,
537                    pending: true,
538                    effects,
539                    ..Default::default()
540                })
541            }
542            GroupSenderKeyHandleResult::Ignored => {
543                self.processed_group_sender_key_messages.insert(fingerprint);
544                self.clear_group_sender_key_repairs(
545                    &message_repair_group_id,
546                    message_repair_sender,
547                    message_repair_key_id,
548                    message_repair_number,
549                );
550                self.persist()?;
551                Ok(ProtocolGroupIncomingResult {
552                    consumed: true,
553                    ..Default::default()
554                })
555            }
556        }
557    }
558
559    pub(super) fn sender_key_repair_request_effects(
560        &mut self,
561        request: SenderKeyRepairRequest,
562        now: NdrUnixSeconds,
563    ) -> anyhow::Result<Vec<ProtocolEffect>> {
564        if self.local_owner_is_inactive_for_group(&request.group_id) {
565            if self.clear_group_sender_key_repairs(
566                &request.group_id,
567                request.sender_event_pubkey,
568                request.key_id,
569                request.message_number,
570            ) {
571                self.persist()?;
572            }
573            return Ok(Vec::new());
574        }
575
576        let sender_event_pubkey_hex = request.sender_event_pubkey.to_hex();
577        let position = self
578            .pending_group_sender_key_repairs
579            .iter()
580            .position(|pending| {
581                pending.group_id == request.group_id
582                    && pending.sender_event_pubkey_hex == sender_event_pubkey_hex
583                    && pending.key_id == request.key_id
584                    && pending.message_number == request.message_number
585                    && pending.required_revision == request.required_revision
586            });
587        let index = if let Some(index) = position {
588            index
589        } else {
590            self.pending_group_sender_key_repairs
591                .push(ProtocolPendingGroupSenderKeyRepair {
592                    group_id: request.group_id.clone(),
593                    sender_event_pubkey_hex,
594                    key_id: request.key_id,
595                    message_number: request.message_number,
596                    required_revision: request.required_revision,
597                    created_at_secs: now.get(),
598                    last_requested_at_secs: 0,
599                    request_count: 0,
600                    next_retry_at_secs: 0,
601                });
602            self.pending_group_sender_key_repairs.len() - 1
603        };
604        let Some(pending_repair) = self.pending_group_sender_key_repairs.get(index) else {
605            anyhow::bail!("pending sender-key repair index disappeared");
606        };
607        if Self::pending_group_sender_key_repair_due_at_secs(pending_repair) > now.get() {
608            return Ok(Vec::new());
609        }
610
611        let mut rng = OsRng;
612        let mut ctx = ProtocolContext::new(now, &mut rng);
613        let prepared = self.group_manager.request_sender_key_repair(
614            &mut self.session_manager,
615            &mut ctx,
616            &request,
617        )?;
618        let session_progress =
619            [&prepared.remote, &prepared.local_sibling]
620                .into_iter()
621                .any(|part| {
622                    !part.deliveries.is_empty()
623                        || !part.invite_responses.is_empty()
624                        || !part.sender_key_messages.is_empty()
625                });
626        // An attempted sibling produces a delivery or a gap. Keep gap-only
627        // sibling work durable too: it may reorganize retained inactive sessions.
628        let local_sibling_state_may_change = !prepared.local_sibling.relay_gaps.is_empty()
629            || !prepared.local_sibling.pending_fanouts.is_empty();
630        let (output, fanouts_changed) =
631            self.protocol_group_send_from_prepared_with_progress(&prepared, None)?;
632        if let Some(pending) = self.pending_group_sender_key_repairs.get_mut(index) {
633            pending.last_requested_at_secs = now.get();
634            pending.request_count = pending.request_count.saturating_add(1);
635            pending.next_retry_at_secs =
636                protocol_sender_key_repair_next_retry_at(now, pending.request_count).get();
637        }
638        if session_progress || fanouts_changed || local_sibling_state_may_change {
639            // Persist after updating the clocks so a delivered request remains
640            // throttled across restart, together with its ratchet and targets.
641            self.persist()?;
642        }
643        self.invalidate_known_message_author_cache();
644        Ok(output.effects)
645    }
646
647    pub(super) fn retry_pending_group_sender_key_repairs(
648        &mut self,
649        now: NdrUnixSeconds,
650    ) -> anyhow::Result<Vec<ProtocolEffect>> {
651        let requests = self
652            .pending_group_sender_key_repairs
653            .iter()
654            .filter(|pending| {
655                Self::pending_group_sender_key_repair_due_at_secs(pending) <= now.get()
656            })
657            .filter_map(|pending| {
658                let sender = PublicKey::parse(&pending.sender_event_pubkey_hex).ok()?;
659                Some(SenderKeyRepairRequest {
660                    group_id: pending.group_id.clone(),
661                    sender_event_pubkey: ndr_device(sender),
662                    key_id: pending.key_id,
663                    message_number: pending.message_number,
664                    required_revision: pending.required_revision,
665                    created_at: NdrUnixSeconds(pending.created_at_secs),
666                })
667            })
668            .collect::<Vec<_>>();
669        let mut effects = Vec::new();
670        for request in requests {
671            let repair_effects = self.sender_key_repair_request_effects(request, now)?;
672            effects.extend(repair_effects);
673        }
674        Ok(effects)
675    }
676
677    pub(super) fn pending_group_sender_key_repair_due_at_secs(
678        pending: &ProtocolPendingGroupSenderKeyRepair,
679    ) -> u64 {
680        if pending.request_count == 0 || pending.last_requested_at_secs == 0 {
681            return pending.next_retry_at_secs;
682        }
683        let capped_due_at = pending.last_requested_at_secs.saturating_add(
684            protocol_sender_key_repair_retry_delay_secs(pending.request_count),
685        );
686        if pending.next_retry_at_secs == 0 {
687            capped_due_at
688        } else {
689            pending.next_retry_at_secs.min(capped_due_at)
690        }
691    }
692
693    pub(super) fn sender_key_repair_response_effects(
694        &mut self,
695        requester_owner: NdrOwnerPubkey,
696        request: &SenderKeyRepairRequest,
697        now: NdrUnixSeconds,
698    ) -> anyhow::Result<Vec<ProtocolEffect>> {
699        if self.group_sender_key_repair_response_throttled(requester_owner, request, now) {
700            return Ok(Vec::new());
701        }
702        let mut rng = OsRng;
703        let mut ctx = ProtocolContext::new(now, &mut rng);
704        let prepared = self.group_manager.respond_to_sender_key_repair_request(
705            &mut self.session_manager,
706            &mut ctx,
707            requester_owner,
708            request,
709        )?;
710        let response_deferred = !prepared.remote.pending_fanouts.is_empty()
711            || !prepared.local_sibling.pending_fanouts.is_empty();
712        let output = self.protocol_group_send_from_prepared(&prepared, None)?;
713        if !output.effects.is_empty() || response_deferred {
714            self.remember_group_sender_key_repair_response(requester_owner, request, now);
715        }
716        self.invalidate_known_message_author_cache();
717        Ok(output.effects)
718    }
719
720    pub(super) fn group_sender_key_repair_response_throttled(
721        &self,
722        requester_owner: NdrOwnerPubkey,
723        request: &SenderKeyRepairRequest,
724        now: NdrUnixSeconds,
725    ) -> bool {
726        let requester_owner_hex = requester_owner.to_hex();
727        let sender_event_pubkey_hex = request.sender_event_pubkey.to_hex();
728        // Peers may recreate the same repair with a fresh timestamp. The missing
729        // stream/key/revision owns the backoff, not the request envelope.
730        self.answered_group_sender_key_repairs
731            .iter()
732            .any(|answered| {
733                answered.requester_owner_hex == requester_owner_hex
734                    && answered.group_id == request.group_id.as_str()
735                    && answered.sender_event_pubkey_hex == sender_event_pubkey_hex
736                    && answered.key_id == request.key_id
737                    && answered.message_number == request.message_number
738                    && answered.required_revision == request.required_revision
739                    && answered.next_response_at_secs > now.get()
740            })
741    }
742
743    fn remember_group_sender_key_repair_response(
744        &mut self,
745        requester_owner: NdrOwnerPubkey,
746        request: &SenderKeyRepairRequest,
747        now: NdrUnixSeconds,
748    ) {
749        let requester_owner_hex = requester_owner.to_hex();
750        let sender_event_pubkey_hex = request.sender_event_pubkey.to_hex();
751        let position = self
752            .answered_group_sender_key_repairs
753            .iter()
754            .position(|answered| {
755                answered.requester_owner_hex == requester_owner_hex
756                    && answered.group_id == request.group_id.as_str()
757                    && answered.sender_event_pubkey_hex == sender_event_pubkey_hex
758                    && answered.key_id == request.key_id
759                    && answered.message_number == request.message_number
760                    && answered.required_revision == request.required_revision
761            });
762        let response_count = position
763            .and_then(|index| {
764                self.answered_group_sender_key_repairs
765                    .get(index)
766                    .map(|answered| answered.response_count.saturating_add(1))
767            })
768            .unwrap_or(1);
769        let next_response_at_secs =
770            protocol_sender_key_repair_next_retry_at(now, response_count).get();
771        if let Some(index) = position {
772            if let Some(answered) = self.answered_group_sender_key_repairs.get_mut(index) {
773                answered.last_responded_at_secs = now.get();
774                answered.request_created_at_secs = request.created_at.get();
775                answered.response_count = response_count;
776                answered.next_response_at_secs = next_response_at_secs;
777            }
778        } else {
779            self.answered_group_sender_key_repairs
780                .push(ProtocolAnsweredGroupSenderKeyRepair {
781                    requester_owner_hex,
782                    group_id: request.group_id.clone(),
783                    sender_event_pubkey_hex,
784                    key_id: request.key_id,
785                    message_number: request.message_number,
786                    required_revision: request.required_revision,
787                    request_created_at_secs: request.created_at.get(),
788                    last_responded_at_secs: now.get(),
789                    response_count,
790                    next_response_at_secs,
791                });
792        }
793        let excess = self
794            .answered_group_sender_key_repairs
795            .len()
796            .saturating_sub(ANSWERED_GROUP_SENDER_KEY_REPAIR_LIMIT);
797        if excess > 0 {
798            self.answered_group_sender_key_repairs.drain(0..excess);
799        }
800    }
801
802    fn clear_group_sender_key_repairs(
803        &mut self,
804        group_id: &str,
805        sender_event_pubkey: NdrDevicePubkey,
806        key_id: Option<u32>,
807        message_number: Option<u32>,
808    ) -> bool {
809        let original_len = self.pending_group_sender_key_repairs.len();
810        let sender_event_pubkey_hex = sender_event_pubkey.to_hex();
811        self.pending_group_sender_key_repairs.retain(|pending| {
812            let position_matches = match (key_id, message_number) {
813                (Some(key_id), Some(message_number)) => {
814                    pending.key_id == Some(key_id) && pending.message_number == Some(message_number)
815                }
816                _ => true,
817            };
818            !(pending.group_id == group_id
819                && pending.sender_event_pubkey_hex == sender_event_pubkey_hex
820                && position_matches)
821        });
822        self.pending_group_sender_key_repairs.len() != original_len
823    }
824}