Skip to main content

iris_chat_protocol/protocol_engine/
engine_app_keys.rs

1use super::*;
2
3impl ProtocolEngine {
4    pub fn ingest_app_keys_snapshot(
5        &mut self,
6        owner_pubkey: PublicKey,
7        app_keys: AppKeys,
8        created_at: u64,
9    ) -> anyhow::Result<ProtocolRetryBatch> {
10        Ok(self
11            .ingest_app_keys_snapshot_inner(owner_pubkey, app_keys, created_at, None)?
12            .retry_batch)
13    }
14
15    pub fn ingest_app_keys_event(&mut self, event: &Event) -> anyhow::Result<ProtocolRetryBatch> {
16        Ok(self.ingest_app_keys_event_with_outcome(event)?.retry_batch)
17    }
18
19    pub fn ingest_app_keys_event_with_outcome(
20        &mut self,
21        event: &Event,
22    ) -> anyhow::Result<ProtocolMetadataIngestionResult> {
23        let app_keys = AppKeys::from_event(event)?;
24        self.ingest_app_keys_snapshot_inner(
25            event.pubkey,
26            app_keys,
27            event.created_at.as_secs(),
28            Some(event),
29        )
30    }
31
32    fn ingest_app_keys_snapshot_inner(
33        &mut self,
34        owner_pubkey: PublicKey,
35        app_keys: AppKeys,
36        created_at: u64,
37        source_event: Option<&Event>,
38    ) -> anyhow::Result<ProtocolMetadataIngestionResult> {
39        let owner = ndr_owner(owner_pubkey);
40        // Authorization replay commonly leaves the engine unchanged. Retain
41        // only the fields this admission step can mutate; copying every queued
42        // fanout before discovering a duplicate makes cold restore quadratic.
43        let now = unix_now();
44        let session_change_possible = match source_event {
45            Some(event) if owner_pubkey != self.owner_pubkey => self
46                .session_manager
47                .peer_app_keys_event_would_change(event, NdrUnixSeconds(now.get()))?,
48            _ => true,
49        };
50        let previous_session = session_change_possible.then(|| self.session_manager.clone());
51        let previous_evidence = self.invite_owner_app_keys_evidence.get(&owner).cloned();
52        let previously_verified = self.verified_app_keys_owners.contains(&owner);
53        let previously_observed = self.local_app_keys_observed;
54        let roster = DeviceRoster::new(
55            NdrUnixSeconds(created_at),
56            app_keys
57                .get_all_devices()
58                .into_iter()
59                .map(|entry| {
60                    AuthorizedDevice::new(
61                        ndr_device(entry.identity_pubkey),
62                        NdrUnixSeconds(entry.created_at),
63                    )
64                })
65                .collect(),
66        );
67        let mut roster_changed = if owner_pubkey == self.owner_pubkey {
68            if should_replace_provisional_local_roster(
69                &self.session_manager.snapshot(),
70                self.owner_pubkey,
71                self.local_device,
72                &roster,
73            ) {
74                !matches!(
75                    self.session_manager.replace_local_roster(roster),
76                    nostr_double_ratchet::RosterSnapshotDecision::Stale
77                )
78            } else {
79                !matches!(
80                    self.session_manager.apply_local_roster(roster),
81                    nostr_double_ratchet::RosterSnapshotDecision::Stale
82                )
83            }
84        } else if let Some(event) = source_event {
85            if session_change_possible {
86                self.session_manager
87                    .observe_peer_app_keys_event(event.clone(), NdrUnixSeconds(now.get()))?
88            } else {
89                false
90            }
91        } else {
92            !matches!(
93                self.session_manager.observe_peer_roster(owner, roster),
94                nostr_double_ratchet::RosterSnapshotDecision::Stale
95            )
96        };
97        if roster_changed && (owner_pubkey == self.owner_pubkey || source_event.is_none()) {
98            // Equal-time roster merges can be exact replays. Compare all of
99            // the manager state, not just membership, so reconciliation of
100            // claimed devices and ratchets is still persisted when it changes.
101            roster_changed = self.session_manager.snapshot()
102                != previous_session
103                    .as_ref()
104                    .expect("local and projected roster checkpoint")
105                    .snapshot();
106        }
107        let exact_source_is_valid = source_event.is_some_and(|event| {
108            app_keys_event_is_acceptable_for_owner(owner_pubkey, event, now.get())
109        });
110        let exact_evidence_changed = if exact_source_is_valid {
111            let event = source_event.expect("validated AppKeys source event");
112            self.update_invite_owner_app_keys_evidence(owner, event, &app_keys)
113        } else {
114            false
115        };
116        // Roster projections continue to drive established protocol routing.
117        // Invite acceptance is stricter and consults only exact signed evidence.
118        let provenance_changed = if owner_pubkey == self.owner_pubkey || exact_source_is_valid {
119            self.verified_app_keys_owners.insert(owner)
120        } else {
121            false
122        };
123        let local_observation_changed =
124            owner_pubkey == self.owner_pubkey && !self.local_app_keys_observed;
125        if owner_pubkey == self.owner_pubkey {
126            self.local_app_keys_observed = true;
127        }
128        if !roster_changed
129            && !exact_evidence_changed
130            && !provenance_changed
131            && !local_observation_changed
132        {
133            return Ok(ProtocolMetadataIngestionResult::default());
134        }
135        // A real authorization change can wake retry deadlines. Take the full
136        // rollback generation only now, before that work, and restore the
137        // admission fields to their pre-event values inside it.
138        let mut checkpoint = self.state_checkpoint();
139        if let Some(previous_session) = previous_session {
140            checkpoint.session_manager = previous_session;
141        }
142        checkpoint.local_app_keys_observed = previously_observed;
143        if !previously_verified {
144            checkpoint.verified_app_keys_owners.remove(&owner);
145        }
146        match previous_evidence {
147            Some(evidence) => {
148                checkpoint
149                    .invite_owner_app_keys_evidence
150                    .insert(owner, evidence);
151            }
152            None => {
153                checkpoint.invite_owner_app_keys_evidence.remove(&owner);
154            }
155        }
156        self.invalidate_known_message_author_cache();
157        if owner == self.local_owner && roster_changed {
158            if let Some(roster) = self.session_manager.roster(owner) {
159                // Remove old delivery rights when the membership change is observed,
160                // even if a retry is held until after the device links again.
161                let source_authorized = roster.get_device(&self.local_device).is_some();
162                for pending in &mut self.pending_local_sibling_sends {
163                    if let Some(eligible) = &mut pending.eligible_devices {
164                        eligible.retain(|device| {
165                            source_authorized && roster.get_device(device).is_some()
166                        });
167                    }
168                }
169            }
170        }
171        self.wake_pending_protocol_for_owner(owner);
172        if let Err(error) = self.persist() {
173            self.restore_checkpoint(checkpoint);
174            self.invalidate_known_message_author_cache();
175            return Err(error);
176        }
177        Ok(ProtocolMetadataIngestionResult {
178            changed: true,
179            retry_batch: self.retry_pending_protocol(NdrUnixSeconds(now.get()))?,
180        })
181    }
182}