offline_protocol/protocol/mod.rs
1//! Main protocol engine.
2
3mod blocking;
4mod config_accessors;
5mod custodian;
6pub(crate) mod custody;
7#[cfg(feature = "data")]
8pub(crate) mod data;
9#[cfg(feature = "data")]
10mod data_sync;
11mod decryption_queue;
12pub(crate) mod mesh_relay;
13mod message_dispatch;
14mod nostr_discovery;
15mod nostr_publication;
16mod observability;
17mod pending_queue;
18mod prefixes;
19pub(crate) mod reachability;
20mod receive;
21mod security;
22mod send;
23mod service_seal;
24mod session;
25pub(crate) mod state_crypto;
26mod storage;
27mod types;
28
29pub use custody::{CustodyRefusal, CustodyStats};
30pub(crate) use decryption_queue::PendingDecryptionQueue;
31pub use decryption_queue::PendingQueueMetrics;
32pub(crate) use prefixes::*;
33#[cfg(feature = "file-store")]
34pub(crate) use storage::{sealed_state_key_types, stored_state_record_key, StoredRecordKey};
35pub(crate) use storage::{PruneAllowance, RestorableRecord};
36pub(crate) use types::*;
37pub use types::{GatewayCarrier, MediaSendOptions, ProtocolState, SendMessageOptions};
38
39use crate::file_transfer::{FileTransferManager, OutboundTransferState};
40use crate::group_mesh::{MAX_RELAY_CAPABILITIES, MAX_RELAY_CAPABILITY_TOKEN_BYTES};
41use crate::mls_observability::{MlsEventEmitter, MlsEventRateLimiter, NoopMlsEventEmitter};
42use crate::telemetry::aggregator::{
43 build_metrics_frame, device_battery_from_available, diff_device_capability,
44 diff_transport_state, DeviceSnap,
45};
46#[cfg(feature = "telemetry-pipe")]
47use crate::telemetry::pipe::{
48 store::Backend, AppState, PipeParts, TelemetryHost, TelemetryPipe, TelemetryStats,
49 FINAL_FLUSH_BUDGET,
50};
51use crate::telemetry::{
52 dispatch_record, Scrubber, TelemetryConfig, TelemetryContext, TelemetryRecord, TelemetrySink,
53};
54use crate::{
55 Error, EstablishmentState, Event, ProtocolConfig, ProtocolStateStorage, Result,
56 TransportManager,
57};
58use chrono::{DateTime, Utc};
59use mesh_relay::{MeshRelayGovernor, RelayActivity};
60use offline_protocol_core::{LamportClock, Message, MessageId, MutexExt, Username};
61use offline_protocol_mls::{EncryptedMessage, MlsManager, MlsStorage, WelcomeMessage};
62use offline_protocol_reliability::{AckManager, Deduplicator, RetryQueue};
63use offline_protocol_router::{RelayPriority, RelayRole, TransportSelector};
64use offline_protocol_services::MeshServices;
65use offline_protocol_transport::{BleTransport, TransportStatus, TransportType};
66use std::collections::HashMap;
67use std::collections::HashSet;
68use std::collections::VecDeque;
69use std::sync::{Arc, Mutex, RwLock};
70use std::time::Instant;
71use tracing::{debug, error, info, warn};
72use zeroize::Zeroizing;
73
74/// Returns whether `timestamp` is at least `lifetime_ms` old without relying
75/// on Chrono's panicking `DateTime - Duration` operator.
76///
77/// A lifetime too large to represent, or whose cutoff predates Chrono's
78/// calendar range, cannot expire a representable timestamp.
79fn lifetime_expired(now: DateTime<Utc>, timestamp: DateTime<Utc>, lifetime_ms: u64) -> bool {
80 let Ok(lifetime_ms) = i64::try_from(lifetime_ms) else {
81 return false;
82 };
83 now.checked_sub_signed(chrono::Duration::milliseconds(lifetime_ms))
84 .is_some_and(|cutoff| timestamp <= cutoff)
85}
86
87/// Whether a relay-pushed outbox entry has outlived its fixed redelivery
88/// window ([`offline_protocol_reliability::constants::REDELIVERY_WINDOW_MS`]
89/// from `first_sent_at`). Neither the sliding window nor the restore refresh
90/// moves this: every re-send past it would earn a second visible push, and
91/// the recipient no longer recognises the id as a duplicate.
92fn relay_push_window_closed(now: DateTime<Utc>, entry: &OutboxEntry) -> bool {
93 entry.relay_pushed
94 && lifetime_expired(
95 now,
96 entry.first_sent_at,
97 offline_protocol_reliability::constants::REDELIVERY_WINDOW_MS,
98 )
99}
100
101/// Main entry point for the Offline Protocol SDK.
102///
103/// This struct combines all protocol components and provides a unified API
104/// for sending/receiving messages with automatic transport selection and
105/// reliable delivery.
106pub struct OfflineProtocol {
107 /// Configuration.
108 pub(crate) config: ProtocolConfig,
109
110 /// This device's identity on the wire: the address derived from the
111 /// identity key in this profile's storage.
112 ///
113 /// Every outbound frame is stamped with it, every self-filter compares
114 /// against it, and it is one half of every 1:1 session slot. It is set
115 /// once, by `initialize_mls`, from the key that storage holds — the
116 /// protocol cannot know it before then, because storage is what carries
117 /// the key and storage does not arrive until that call.
118 ///
119 /// Until then it holds [`ProtocolConfig::profile`], which is *not* an
120 /// address and is never a peer's real identity. That fallback keeps the
121 /// pre-MLS surface (transport wiring, local bookkeeping) working on an
122 /// instance that has not been initialized.
123 ///
124 /// One configuration can put the fallback on the wire: the explicit
125 /// `require_encryption = false` opt-out lets a send go out in plaintext
126 /// with MLS never initialized, and such a frame is stamped with the
127 /// profile. It is the same fall-through that already emits
128 /// [`crate::events::SecurityWarningCode::PlaintextSend`], so the condition
129 /// is reported rather than silent. The default config forbids it —
130 /// `require_encryption` is `true`, which fails the send closed.
131 pub(crate) local_id: String,
132
133 /// Whether [`Self::local_id`] is the derived address rather than the
134 /// profile fallback.
135 pub(crate) identity_established: bool,
136
137 /// Transport manager (manages all transports with DORS).
138 pub(crate) transport_manager: TransportManager,
139
140 /// ACK manager for tracking acknowledgments.
141 ack_manager: AckManager,
142
143 /// Retry queue for failed messages.
144 retry_queue: RetryQueue,
145
146 /// Deduplicator for preventing duplicates.
147 pub(crate) deduplicator: Deduplicator,
148
149 /// Decides which frames this device carries for other people, when they
150 /// go out, and how fast. Kept separate from [`Self::deduplicator`], whose
151 /// entries track messages addressed to us and are removed again when
152 /// delivery has to be retried.
153 pub(crate) mesh_relay: MeshRelayGovernor,
154
155 /// Frames held in custody for a neighbour, with the quotas and the
156 /// counters (`docs/spec/custody.md`). Disjoint from
157 /// [`Self::mesh_relay`]'s suppression cache by construction: accepting a
158 /// frame never records its id there, and redelivering one never consults
159 /// it, so a custodian never blanks the route it is holding.
160 pub(crate) custody: custody::CustodyStore,
161
162 /// Receipts this device holds as a *depositor*: for each outbox entry,
163 /// the custodians holding it and until when a further deposit request
164 /// toward each is suppressed, in Unix milliseconds from the receipt's own
165 /// timestamp. In memory only, bounded by the outbox and
166 /// by [`custody::MAX_CUSTODY_RECEIPTS_PER_MESSAGE`]; losing it at a
167 /// restart costs one duplicate deposit, which the custodian absorbs.
168 pub(crate) custody_receipts: HashMap<MessageId, HashMap<String, i64>>,
169
170 /// When the custody store was last swept for expired records.
171 custody_last_sweep: Instant,
172
173 /// Shared mutable state.
174 shared_state: Arc<Mutex<SharedState>>,
175
176 /// Messages awaiting delivery/acknowledgment (store-and-forward outbox).
177 outbox: HashMap<MessageId, OutboxEntry>,
178
179 /// Transient staging for outbox re-seal provenance, keyed by message id.
180 /// Populated at send time (where the plaintext is in scope) and consumed
181 /// when the outbox entry is first created (which happens deep in the send/
182 /// retry machinery, from the already-sealed `Message`). Once consumed it
183 /// lives on the `OutboxEntry` itself. See [`OutboxReseal`].
184 pending_reseal: HashMap<MessageId, OutboxReseal>,
185 /// The message the app send path is dispatching right now, read by the two
186 /// places an outbox entry is created so the entry carries
187 /// [`OutboxEntry::app_send`] from its first write to storage. Set around
188 /// the dispatch and restored to its previous value afterwards, so it never
189 /// outlives one call and a nested dispatch cannot clear an outer one's.
190 ///
191 /// Staged rather than set on the entry afterwards: an entry is persisted
192 /// when it is created and not again on later attempts, so a flag set after
193 /// creation stayed in memory, and every app message restored after a
194 /// restart came back as not the app's and dropped out of telemetry.
195 staged_app_send: Option<MessageId>,
196
197 /// Outbox ids already re-sealed and resent once in answer to an
198 /// `undecryptable` ACK. A second such answer leaves the entry to its retry
199 /// ladder, so two devices cannot trade undecryptable ACKs and resends at
200 /// network speed. Cleared with the outbox entry.
201 undecryptable_resent: HashSet<MessageId>,
202
203 /// Dedicated outbox for file chunk messages, separate from the main outbox
204 /// to prevent large file transfers from evicting regular messages.
205 media_outbox: HashMap<MessageId, OutboxEntry>,
206
207 /// Outbox ids held back by the per-peer send window, per recipient, in
208 /// the order the app sent them. Sent oldest first as the peer's
209 /// acknowledgements free the window (`flush_peer_windows`). An id whose
210 /// outbox entry is gone (settled, expired, evicted) is skipped and dropped
211 /// there, so no removal path has to know about this. In-memory only: a
212 /// restart re-drives the outbox, and the gate holds it again.
213 window_held: HashMap<String, VecDeque<MessageId>>,
214
215 /// Consecutive unreachable-park counter per recipient for plain DMs,
216 /// driving the escalating reachability-probe interval on every carrier
217 /// (see `handle_recipient_unreachable_for_message` —
218 /// mirrors `WelcomeLifecycleRecord::unreachable_parks`). Reset on every
219 /// reachability edge (`flush_outbox_for_peer_via` / `flush_outbox_all`) and
220 /// on delivery; pruned alongside `cleanup_outbox` so only peers that
221 /// still hold outbox entries can retain a counter. While live, ACK
222 /// exhaustion for the recipient's plain DMs re-parks instead of
223 /// settling terminally (`try_repark_exhausted_dm`). In-memory only: a
224 /// restart re-drives the outbox anyway, which is itself a fresh probe.
225 dm_unreachable_parks: HashMap<String, u32>,
226
227 /// Delivery acknowledgements no carrier could take when they were owed,
228 /// with the transport the acknowledged frame arrived on and when they
229 /// were held. Re-routed on the next carrier-level edge
230 /// (`flush_unrouted_acks` from `flush_outbox_all`); bounded by
231 /// `MAX_UNROUTED_ACKS` and `UNROUTED_ACK_TTL`. In-memory only.
232 unrouted_acks: VecDeque<(Message, TransportType, Instant)>,
233
234 /// Per-recipient reachability claims, keyed by recipient and carrier.
235 ///
236 /// The generalisation of the counter above: that one records *how often*
237 /// a recipient has been parked, this one records *what a carrier said*
238 /// about reaching them, so a decision can be made before the send rather
239 /// than only after it failed. Written by the same producers (a gateway
240 /// verdict, a gateway presence answer), read at the send and carrying
241 /// seams. In-memory only, and absent facts mean today's behaviour.
242 pub(crate) reachability: reachability::ReachabilityFacts,
243
244 /// Capability tokens the attached gateway daemon advertised.
245 ///
246 /// Delivered at attach, before the bridge reports the carrier available,
247 /// and cleared when the carrier drops: a stale advertisement outlives the
248 /// gateway that made it, and a reconnect may land on a different one.
249 ///
250 /// Stored and never read on a decision. The backbone behind the daemon
251 /// is the gateway's own property and reaches this device only as a
252 /// `backbone_<kind>_v1` token in this set; the set is for an application
253 /// to show, and the first production reader would turn a string set by
254 /// whoever holds the socket into a routing input (ADR 0026).
255 ///
256 /// Kept apart from `group_mesh.relay_capabilities` deliberately, though
257 /// both are capability sets from a gateway. That one gates the relay
258 /// group-broadcast path, and a token advertised by whatever box is
259 /// plugged into the venue Wi-Fi must not be able to open it.
260 gateway_capabilities: HashSet<String>,
261
262 /// MLS manager for end-to-end encryption.
263 mls_manager: Option<Arc<RwLock<MlsManager>>>,
264
265 /// Pending messages waiting for session establishment (recipient -> messages).
266 pending_encrypted_messages: HashMap<String, Vec<PendingMessage>>,
267
268 /// Terminal message settlements produced while restoring, held until the
269 /// event pipeline is live.
270 ///
271 /// Restore runs inside `initialize_mls`, which apps routinely call before
272 /// installing an event callback — so a `message_failed` emitted there would
273 /// be dropped, and the app would keep an id that never resolves. `start()`
274 /// drains this, mirroring how restored media descriptors already wait to be
275 /// announced.
276 ///
277 /// Explicitly capped at [`MAX_DEFERRED_RESTORE_SETTLEMENTS`] rather than
278 /// left to the restore caps that feed it. Those caps do bound it, but they
279 /// bound it as a *sum* across every category, and nothing drains this until
280 /// `start()` — which an app that only ever calls `initialize_mls`, or that
281 /// retries it against a failing store, may never reach.
282 /// Each settlement carries whether it reaches telemetry; see
283 /// [`Self::emit_settlement`].
284 deferred_restore_settlements: Vec<(Event, bool)>,
285
286 /// Number of settlements suppressed by the cap above, so the count is
287 /// reported even though the individual events are not.
288 suppressed_restore_settlements: usize,
289
290 /// Earliest wall-clock expiry in `pending_encrypted_messages`.
291 ///
292 /// `process()` consults this before scanning the bounded queue, avoiding an
293 /// O(N) walk on every 100 ms tick while preserving exact configured expiry.
294 /// A stale early deadline is harmless: it causes one extra scan, which then
295 /// recomputes the real minimum.
296 next_pending_message_expiry: Option<DateTime<Utc>>,
297
298 /// Key packages received but not yet used (sender_id -> package).
299 pub(crate) pending_key_packages: HashMap<String, ReceivedKeyPackage>,
300
301 /// Peers we have pushed our key package to, with when we last did and how
302 /// many times the push has been re-armed since.
303 ///
304 /// The instant is what lets a push that produced no session be re-armed
305 /// (`Self::rearm_key_package_for_peer`) instead of standing in for a
306 /// delivery it never proved; the count is that re-arm's backoff.
307 pub(crate) key_package_sent_to: HashMap<String, (Instant, u32)>,
308
309 /// All discovered/connected peers, tracked independently of encryption.
310 /// Used by service discovery to know who to broadcast queries to.
311 /// Values are last-seen instants: refreshed on every discovery signal,
312 /// swept by [`Self::prune_stale_known_peers`] after `KNOWN_PEER_TTL_SECS`,
313 /// and used to pick the least-recently-seen victim when an insert hits
314 /// `MAX_KNOWN_PEERS`.
315 known_peers: HashMap<String, Instant>,
316
317 /// Sessions confirmed established (received Welcome or successful decrypt).
318 /// Only encrypt messages when the session is confirmed to avoid race conditions.
319 confirmed_sessions: std::collections::HashSet<String>,
320
321 /// Peers that have demonstrated they run MLS, used by
322 /// [`Self::accept_plaintext_content`] to reject cleartext claiming to come
323 /// from an encryption-capable peer.
324 ///
325 /// # Why this is not `confirmed_sessions`, and not a storage read
326 ///
327 /// The gate used to ask `is_session_confirmed`, which reads the
328 /// `session_states` protocol-state record. That record lives in the
329 /// install-scoped app container, where an attacker with write access can
330 /// simply **delete** it: restore then treats absence as `Pending` (it must
331 /// — see `bootstrap_missing_session_state`), the peer drops out of
332 /// `confirmed_sessions`, and the gate opens for unauthenticated cleartext
333 /// carrying an attacker-chosen `sender`. Sealing the record does not help;
334 /// a seal authenticates bytes that are present, and this attack removes
335 /// them. So the signal has to come from somewhere the app container cannot
336 /// reach.
337 ///
338 /// Both sources here are credential-store-resident: the MLS session list
339 /// and the durable capability records
340 /// ([`storage_keys::ENCRYPTION_CAPABLE_PEERS`]). Neither is sufficient
341 /// alone. A session can exist for a peer that has never signed a control
342 /// frame we verified (it is established from key packages, and the Welcome
343 /// path marks capability without a signature), and a record can outlive the
344 /// session it was learned alongside. The union is what actually tracks
345 /// "this peer speaks MLS".
346 ///
347 /// # Monotone within a run — deliberately, and this is load-bearing
348 ///
349 /// Entries are added and **never removed on session teardown**. That looks
350 /// like a leak and is in fact the whole security property. Teardown is
351 /// reachable from an *unauthenticated* frame: an injected `__MLS_ENC__`
352 /// classifies as `SessionDesync`, `schedule_session_rekey` tears down our
353 /// session and advertises a `session_reset` key package, and the peer's
354 /// `handle_key_package_message` then deletes its own. A set that shrank on
355 /// teardown would therefore let one injected frame re-open the very gate
356 /// this field exists to hold shut — moving the attack from "needs
357 /// app-container write access" to "needs one forged packet", which is
358 /// strictly worse than the bug it fixes.
359 ///
360 /// Keeping a peer after teardown costs nothing real: a peer that spoke MLS
361 /// a moment ago is still encryption-capable, so its cleartext is still a
362 /// downgrade. The invariant to preserve is one-directional — this set may
363 /// **over**-approximate (which fails closed) but must never
364 /// **under**-approximate (which fails open). Anything that populates it may
365 /// be lossy; nothing may prune it.
366 ///
367 /// There is no longer any way out at all, and that is now *correct* rather
368 /// than merely safe. The old escape hatch (`reset_tofu_for_peer`) meant
369 /// "this identity has re-keyed, treat it as new" — a thing that could
370 /// happen when an identity was a name. Under self-certifying addressing an
371 /// address is the hash of its key, so a peer that re-keys becomes a
372 /// *different address*: nothing about the old one ever stops being true,
373 /// and forgetting it could only ever be wrong.
374 ///
375 /// Bounded by [`MAX_ENCRYPTION_CAPABLE_PEERS`], enforced as a refusal rather
376 /// than an eviction so the monotonicity above survives the cap — see
377 /// [`Self::mark_encryption_capable`].
378 encryption_capable_peers: std::collections::HashSet<String>,
379
380 /// Peers whose key package advertised the compact MLS envelope
381 /// ([`MLS_ENVELOPE_COMPACT_V1`] in `env_versions`), so
382 /// `seal_encrypted_content` may emit it instead of the legacy JSON form.
383 /// Learned from key-package exchange, persisted per peer as
384 /// [`PeerCapabilities`], and repopulated by `restore_peer_capabilities`
385 /// on `initialize_mls` — unlike the transport manager's binary-wire
386 /// registry, which stays in-memory because direct connections re-exchange
387 /// key packages anyway. Bounded like `key_package_sent_to` (it is keyed
388 /// by the wire-claimed sender).
389 peer_compact_envelope: std::collections::HashSet<String>,
390
391 /// Peers whose key package advertised that they verify the
392 /// freshness-bound control payload ([`CTRL_SIGN_V2`] in `ctrl_versions`),
393 /// so control frames addressed to them are signed under
394 /// `offline-ctrl-v2`. Same lifecycle as `peer_compact_envelope` above:
395 /// learned from key-package exchange, persisted as [`PeerCapabilities`],
396 /// restored on `initialize_mls`, bounded like `key_package_sent_to`.
397 ///
398 /// This is the **send** half, and it is a hint: forgetting a peer costs
399 /// them the older payload, which they still verify. The **accept** half is
400 /// [`Self::control_freshness_peers`], which is a ratchet and is not
401 /// allowed to forget.
402 ///
403 /// [`CTRL_SIGN_V2`]: offline_protocol_sealed::CTRL_SIGN_V2
404 peer_ctrl_freshness: std::collections::HashSet<String>,
405
406 /// Peers that read an `undecryptable` ACK as "not delivered"
407 /// ([`ACK_UNDECRYPTABLE_V1`] in `ack_versions`). Only these are answered
408 /// when a frame of theirs can never decrypt: an older SDK reads any ACK as
409 /// a delivery. Same lifecycle as `peer_ctrl_freshness` above.
410 ///
411 /// [`ACK_UNDECRYPTABLE_V1`]: offline_protocol_sealed::ACK_UNDECRYPTABLE_V1
412 pub(crate) peer_undecryptable_ack: std::collections::HashSet<String>,
413
414 /// Peers that read `ack_more` on a delivery ACK ([`ACK_COALESCED_V1`] in
415 /// `ack_versions`), so several of their frames may be answered by one ACK
416 /// frame. Same lifecycle as `peer_undecryptable_ack` above.
417 ///
418 /// [`ACK_COALESCED_V1`]: offline_protocol_sealed::ACK_COALESCED_V1
419 pub(crate) peer_coalesced_ack: std::collections::HashSet<String>,
420
421 /// Delivery ACKs waiting to be coalesced, per peer: the carrier the
422 /// answered frames arrived on, when the first was held, and the answered
423 /// ids with the first one's hop count. Sent as one frame once
424 /// `ACK_COALESCE_MAX` are held or `ACK_COALESCE_WINDOW` has passed
425 /// (`flush_coalesced_acks`). In-memory only: an ACK lost with it is
426 /// answered again when the resend arrives as a duplicate.
427 coalesced_acks_out: HashMap<String, CoalescedAcks>,
428
429 /// Peers that have presented a control-frame signature over the
430 /// freshness-bound payload, so their frames under the older one are
431 /// refused.
432 ///
433 /// A **ratchet**, and the difference from every capability set above is
434 /// the whole point: those record what a peer said and may be forgotten
435 /// harmlessly, this records what a peer *proved* and forgetting it
436 /// re-opens the replay it exists to close. It is durable (in the peer's
437 /// [`EncryptionCapableEntry`], not in [`PeerCapabilities`], which is
438 /// overwritten wholesale by every key package) and has no removal path
439 /// short of forgetting the peer entirely.
440 ///
441 /// A strict subset of [`Self::encryption_capable_peers`] by construction,
442 /// so `MAX_ENCRYPTION_CAPABLE_PEERS` bounds it too. See
443 /// `record_control_freshness_proved` for why the ordering that makes that
444 /// true is load-bearing.
445 pub(crate) control_freshness_peers: std::collections::HashSet<String>,
446
447 /// Per-peer high-water mark: the signed timestamp of the most recent
448 /// session reset acted on from that peer.
449 ///
450 /// A reset is honoured only when its signed timestamp is strictly newer
451 /// than this, which is what stops one captured reset frame from being
452 /// spent again and again inside the freshness window. Chosen over a set of
453 /// spent frame ids because it is one integer per peer instead of a list
454 /// that grows with traffic, and because it is strictly stronger: it
455 /// refuses *every* older reset, including ones this node has no memory of.
456 ///
457 /// Durable in the peer's [`EncryptionCapableEntry`], and bounded by
458 /// construction like [`Self::control_freshness_peers`]: entries are only
459 /// written for peers the capped encryption-capable set accepted.
460 pub(crate) control_reset_watermark: std::collections::HashMap<String, i64>,
461
462 /// Peers whose key package advertised the sealed rich payload
463 /// ([`RICH_PAYLOAD_V1`] in `rich_versions`), so the send path may seal
464 /// rich extras (reply context, rich media metadata, forward attribution)
465 /// inside the `__RICH_V1__` body. Same lifecycle as
466 /// `peer_compact_envelope` above: learned from key-package exchange,
467 /// persisted, restored on `initialize_mls`, bounded like
468 /// `key_package_sent_to`. Forgetting a peer only costs silently dropped
469 /// rich extras — never a cleartext fallback.
470 peer_rich_payload: std::collections::HashSet<String>,
471
472 /// Peers that route a sealed service frame ([`SVC_SEALED_V1`] in
473 /// `svc_versions`), so a service request or response addressed to them
474 /// may travel inside `__MLS_ENC__` rather than as signed plaintext.
475 ///
476 /// The **advertised** half: learned only from a **signed** key package,
477 /// like the sets above (persisted as [`PeerCapabilities`], restored on
478 /// `initialize_mls`), and rewritten by every key package, so a fresh one
479 /// that stops advertising removes the peer. A strict subset of
480 /// [`Self::encryption_capable_peers`], refused at its cap rather than
481 /// cleared when full: clearing would send every established peer's
482 /// service bodies in plaintext on a flood. The proved half is
483 /// [`Self::svc_sealed_proved_peers`], and either one is enough to seal.
484 ///
485 /// [`SVC_SEALED_V1`]: offline_protocol_sealed::SVC_SEALED_V1
486 pub(crate) peer_svc_sealed: std::collections::HashSet<String>,
487
488 /// Peers that have sent us a service frame sealed inside the 1:1 session,
489 /// which proves they route the sealed form.
490 ///
491 /// A **ratchet**, unlike [`Self::peer_svc_sealed`]: no key package can
492 /// remove a peer from it. The failure it prevents is a replayed key
493 /// package from before the peer upgraded, which would otherwise drop the
494 /// peer from the advertised set and send its service bodies in signed
495 /// plaintext again. A key package signed under `offline-ctrl-v1` is
496 /// admitted at any age, so nothing else bounds that replay. Durable in
497 /// the peer's [`EncryptionCapableEntry`] beside `ctrl_freshness_proved`,
498 /// and a strict subset of [`Self::encryption_capable_peers`] by
499 /// construction, so `MAX_ENCRYPTION_CAPABLE_PEERS` bounds it too.
500 ///
501 /// Knowledge, not policy: recorded whatever our own switch says, and the
502 /// switch gates only whether we seal.
503 pub(crate) svc_sealed_proved_peers: std::collections::HashSet<String>,
504
505 /// Peers whose key package advertised replicated-document sync
506 /// ([`DATA_SYNC_V1`] in `data_versions`), so the send path may push
507 /// `__DATA_V1__` frames to them. Same lifecycle as the two capability
508 /// sets above: learned from key-package exchange, persisted, restored on
509 /// `initialize_mls`, bounded like `key_package_sent_to`.
510 ///
511 /// Forgetting a peer costs replication with them until the next live
512 /// exchange, which is worse than it sounds: documents stay editable on
513 /// both sides and diverge quietly, so the symptom is not an error but
514 /// two people looking at different text. It is why this set is restored
515 /// rather than relearned.
516 ///
517 /// [`DATA_SYNC_V1`]: crate::protocol::types::DATA_SYNC_V1
518 peer_data_sync: std::collections::HashSet<String>,
519
520 /// Peers whose sealed-rich-payload support we learned *indirectly*: a
521 /// group inviter attested it on the Add commit (to existing members) or
522 /// the Welcome (to the joining member), because the members of a group
523 /// never directly exchange key packages with everyone else. Kept
524 /// separate from `peer_rich_payload` (direct self-advertisement) so
525 /// direct knowledge can stay authoritative: any directly received key
526 /// package from a peer evicts its attested entry. Consulted only by
527 /// `group_rich_seal_active` — never DM sealing (which always has direct
528 /// knowledge: session establishment exchanges key packages) and never
529 /// envelope selection (a stale attestation there could corrupt
530 /// decoding; here the worst case is one member rendering a literal
531 /// `__RICH_V1__` body). Persisted inside `PeerCapabilities`, restored
532 /// on `initialize_mls`, bounded like `key_package_sent_to`.
533 peer_rich_attested: std::collections::HashSet<String>,
534
535 /// Peers whose key package advertised *group* document replication
536 /// ([`DATA_GROUP_V1`] in `data_versions`), so a group they share with
537 /// this device may carry `__DATA_V1__` frames.
538 ///
539 /// Separate from `peer_data_sync` because the two entries mean
540 /// different things and a peer can advertise the first without the
541 /// second: an install predating group spaces replicates 1:1 perfectly
542 /// well and would render a group sync frame as chat text. Same
543 /// lifecycle as every capability set: learned from key-package
544 /// exchange, persisted, restored on `initialize_mls`, bounded like
545 /// `key_package_sent_to`.
546 ///
547 /// [`DATA_GROUP_V1`]: crate::protocol::types::DATA_GROUP_V1
548 peer_data_group: std::collections::HashSet<String>,
549
550 /// Peers whose *group* replication support we learned indirectly, from
551 /// an inviter's attestation on the Add commit or Welcome.
552 ///
553 /// The group analogue of `peer_rich_attested`, and load-bearing for the
554 /// same structural reason: members of a group never exchange key
555 /// packages with each other, so without attestation a group would
556 /// replicate only among whichever members happened to have met
557 /// directly. Direct knowledge stays authoritative — a key package from
558 /// the peer evicts its attested entry. Consulted only by
559 /// `group_data_sync_active`, never by 1:1 sync.
560 peer_data_group_attested: std::collections::HashSet<String>,
561
562 /// Peers whose key package advertised attachment and snapshot carriage
563 /// over the media path ([`DATA_MEDIA_V1`] in `data_versions`), so this
564 /// device may ask them for a blob and may hand them a document too large
565 /// to travel inside a sync frame.
566 ///
567 /// A third set rather than a flag on the first, for the same reason
568 /// `peer_data_group` is separate: a peer can advertise 1:1 replication
569 /// without this, and toward such a peer a data-purposed media transfer
570 /// arrives as an ordinary received file. Same lifecycle as every
571 /// capability set: learned from key-package exchange, persisted inside
572 /// `PeerCapabilities`, restored on `initialize_mls`, bounded like
573 /// `key_package_sent_to`.
574 ///
575 /// [`DATA_MEDIA_V1`]: crate::protocol::types::DATA_MEDIA_V1
576 peer_data_media: std::collections::HashSet<String>,
577
578 /// Peers whose key package advertised document removals
579 /// ([`DATA_TOMBSTONE_V1`] in `data_versions`), so a version offer toward
580 /// them carries the `gone` field.
581 ///
582 /// A fourth set for the same reason as the third, with one difference
583 /// worth keeping in mind: this one gates traffic, not safety. A peer
584 /// missing from it ignores the field rather than mishandling it, and
585 /// this device still refuses that peer's offers of removed content
586 /// against its own floor.
587 ///
588 /// [`DATA_TOMBSTONE_V1`]: crate::protocol::types::DATA_TOMBSTONE_V1
589 peer_data_tombstones: std::collections::HashSet<String>,
590
591 /// Peers whose key package advertised interest-scoped replication
592 /// ([`DATA_INTEREST_V1`] in `data_versions`), so a version offer toward
593 /// them carries the `want` field.
594 ///
595 /// A fifth set, and a traffic gate like the fourth: a peer missing from
596 /// it answers with everything it holds, and this device refuses what it
597 /// did not ask for on arrival.
598 ///
599 /// [`DATA_INTEREST_V1`]: crate::protocol::types::DATA_INTEREST_V1
600 peer_data_interest: std::collections::HashSet<String>,
601
602 /// Peers whose key package advertised attachment bytes inside a group
603 /// ([`DATA_GROUP_BLOB_V1`] in `data_versions`).
604 ///
605 /// Direct knowledge only. Members of a group do not exchange key
606 /// packages with each other, so absence from this set is "we have not
607 /// heard" far more often than "they cannot", which is why the attested
608 /// sibling below exists and why the refusal reads both.
609 ///
610 /// [`DATA_GROUP_BLOB_V1`]: crate::protocol::types::DATA_GROUP_BLOB_V1
611 peer_data_group_blob: std::collections::HashSet<String>,
612
613 /// Peers an inviter attested carry attachment bytes inside a group.
614 ///
615 /// The sibling of `peer_data_group_attested`, and it exists for the same
616 /// reason: this is the only way to learn anything about a member whose
617 /// key package this device has never seen. Evicted by any directly
618 /// received key package, in either direction.
619 peer_data_group_blob_attested: std::collections::HashSet<String>,
620
621 /// Peers whose key package advertised the custody receipt
622 /// ([`DATA_CUSTODY_V1`] in `data_versions`), so this device may answer
623 /// their deposits with one. Gates the receipt and nothing else: a deposit
624 /// is judged by the quotas. Persisted inside `PeerCapabilities`, restored
625 /// on `initialize_mls`, bounded like `key_package_sent_to`.
626 ///
627 /// [`DATA_CUSTODY_V1`]: crate::protocol::types::DATA_CUSTODY_V1
628 peer_data_custody: std::collections::HashSet<String>,
629
630 /// Peers already flagged with a `PlaintextSend` security warning, so the
631 /// explicit-opt-out plaintext path warns once per peer instead of once
632 /// per message.
633 pub(crate) plaintext_send_warned: std::collections::HashSet<String>,
634
635 /// Peers already flagged with a `PlaintextReceiveRejected` security
636 /// warning, so a chatty legacy or malicious peer warns once per peer
637 /// instead of once per rejected message. Keys are wire-claimed
638 /// (attacker-controllable) sender ids, so the set is bounded: it resets
639 /// at `MAX_PLAINTEXT_RECEIVE_WARNED_PEERS` instead of growing without
640 /// limit.
641 pub(crate) plaintext_receive_warned: std::collections::HashSet<String>,
642
643 /// Peers whose durable `encryption_capable_peers` record is known to be on
644 /// disk — either restored from it at boot or written successfully this run.
645 ///
646 /// Purely a write-elision cache for
647 /// [`OfflineProtocol::record_encryption_capable`]: a verified control frame
648 /// arrives on chatty prefixes (`__TYPING__`, `__PRESENCE__`), and the record
649 /// it would rewrite carries no field any decision reads. Tracking *landed
650 /// writes* rather than set membership is what keeps a best-effort storage
651 /// failure retryable on the peer's next frame instead of stranded for the
652 /// process.
653 ///
654 /// A subset of [`Self::encryption_capable_peers`] by construction, so
655 /// `MAX_ENCRYPTION_CAPABLE_PEERS` bounds it too.
656 pub(crate) encryption_capable_persisted: HashSet<String>,
657
658 /// Which control-gate rejection codes have already been reported per peer,
659 /// as a bitmask (see `OfflineProtocol::control_gate_warning_bit`), so a
660 /// peer whose control traffic keeps being refused warns once per code
661 /// instead of once per frame.
662 ///
663 /// Keys are wire-claimed (attacker-controllable) sender ids, so the map is
664 /// bounded: it resets at `MAX_CONTROL_GATE_WARNED_PEERS` instead of growing
665 /// without limit.
666 pub(crate) control_gate_warned: HashMap<String, u8>,
667
668 /// Bounded pending decryption queue for encrypted messages received before
669 /// the MLS session is ready.
670 pub(crate) pending_queue: PendingDecryptionQueue,
671
672 /// Secure storage for MLS/key material and SDK-owned secrets.
673 secure_storage: Option<Arc<dyn MlsStorage>>,
674
675 /// Install-scoped storage for restartable message-plane and protocol state.
676 protocol_state_storage: Option<Arc<dyn ProtocolStateStorage>>,
677
678 /// Seals sensitive protocol-state records before they reach the
679 /// install-scoped store, with a per-install key held in `secure_storage`.
680 ///
681 /// `None` until that key is available (and if it never becomes available,
682 /// for the whole session). Sealed categories then fail *closed*: they are
683 /// not persisted at all rather than written in the clear, since losing
684 /// crash recovery is recoverable and losing at-rest confidentiality is not.
685 /// See `state_crypto` and `restore_or_init_state_record_key`.
686 ///
687 /// Always belongs to the currently attached `secure_storage` — it is
688 /// re-derived, never carried across a storage swap.
689 state_record_cipher: Option<state_crypto::StateRecordCipher>,
690
691 /// Open replicated documents and their per-space indexes.
692 #[cfg(feature = "data")]
693 pub(crate) data: data::DataLayer,
694
695 /// When each peer was last offered our document versions.
696 ///
697 /// Neighbour rediscovery is not a rare event: a peer walking in and out
698 /// of Bluetooth range fires it repeatedly, and an offer per discovery
699 /// would put a frame on the mesh every time. The offer is not urgent
700 /// (anything committed locally is pushed as it happens) and it is not
701 /// load-bearing (the next trigger repeats it), so it is exactly the kind
702 /// of traffic that should be rate limited rather than merely small.
703 #[cfg(feature = "data")]
704 pub(crate) last_data_sync_offer: HashMap<String, Instant>,
705
706 /// Settle offers waiting for local commits to a space to go quiet, keyed
707 /// by space: when the offer is due, and the documents committed since
708 /// the last one. Pushed out by every local commit and removed when it
709 /// fires. See [`DATA_SYNC_SETTLE_DELAY`].
710 ///
711 /// [`DATA_SYNC_SETTLE_DELAY`]: crate::protocol::data_sync::DATA_SYNC_SETTLE_DELAY
712 #[cfg(feature = "data")]
713 pub(crate) data_sync_settle: HashMap<String, (Instant, std::collections::BTreeSet<String>)>,
714
715 /// Attachment fetches asked for and not yet answered, keyed by space and
716 /// blob hash, valued by the last sign that an answer is coming: the
717 /// moment the question went out, or the arrival of a chunk of its
718 /// answer.
719 ///
720 /// The refresh is what keeps this record's two jobs consistent. It bounds
721 /// how long a question is remembered, and it is also what admits the
722 /// bytes when they finally land, so a record that expired while its own
723 /// answer was arriving would discard a blob that fully crossed.
724 ///
725 /// Load-bearing for more than bookkeeping: arriving blob bytes are
726 /// admitted only against an entry here, checked when the transfer's
727 /// chunk 0 identifies it and again before the bytes reach the
728 /// application. Without it a peer could push blobs this device never
729 /// asked for and the application would be handed files nobody wanted.
730 ///
731 /// The bound it gives is on delivery and on the bulk of the buffering,
732 /// not on every byte: chunks need not arrive in order, so any that land
733 /// before chunk 0 are buffered before there is anything to judge, and
734 /// are released when it arrives. What remains is bounded by the media
735 /// layer's own resource caps, the same ones every unsolicited file
736 /// transfer has always been subject to.
737 ///
738 /// Gated on the data feature, which the record's own type now is: what
739 /// a fetch is waiting on, and what it has assembled so far, are facts
740 /// about the data layer and have no meaning without it.
741 #[cfg(feature = "data")]
742 pub(crate) pending_attachment_fetches:
743 HashMap<String, crate::protocol::data_sync::PendingFetch>,
744
745 /// Blob requests recently acted on, keyed by asking peer and blob hash.
746 ///
747 /// Its own map rather than a corner of `last_data_sync_offer`, because
748 /// this is the only rate-limit key a peer chooses freely: every other one
749 /// is a peer or a peer-and-group, bounded by a roster. Bounded here by
750 /// [`MAX_BLOB_REQUESTS_PER_WINDOW`] per peer and
751 /// [`MAX_BLOB_REQUEST_WINDOWS`] overall, and swept on every read.
752 ///
753 /// [`MAX_BLOB_REQUESTS_PER_WINDOW`]: crate::protocol::data_sync::MAX_BLOB_REQUESTS_PER_WINDOW
754 /// [`MAX_BLOB_REQUEST_WINDOWS`]: crate::protocol::data_sync::MAX_BLOB_REQUEST_WINDOWS
755 pub(crate) blob_request_windows: HashMap<String, Instant>,
756
757 /// Whether the MLS group list has been read into `group_mesh.members`
758 /// once this session, so the shared-group rediscovery sweep can see the
759 /// groups this device is in.
760 ///
761 /// That cache is filled on demand by group traffic and is empty after a
762 /// restart, so a sweep that only read it would find nothing on exactly
763 /// the launch where two members have drifted apart and neither is
764 /// editing. Enumerating is one key listing plus, per group not already
765 /// cached, the group-state read the next group send would have paid
766 /// anyway.
767 ///
768 /// One shot rather than per sweep because the cache stays complete once
769 /// filled: creating a group inserts, a Welcome inserts, and leaving
770 /// removes. Deliberately not a cold-start sweep, which was rejected for
771 /// putting an offer per member per group on the mesh at every launch;
772 /// this is per peer, only when that peer is actually in front of us, and
773 /// still behind the offer window.
774 #[cfg(feature = "data")]
775 pub(crate) group_spaces_enumerated: bool,
776
777 /// Lamport logical clock for causal message ordering.
778 /// Ticked on send, merged on receive.
779 lamport_clock: LamportClock,
780
781 /// Retry schedule for peers whose confirmation persistence failed.
782 confirmation_retry_due_at: HashMap<String, DateTime<Utc>>,
783
784 /// Probe schedule for pending sessions to guarantee post-restart convergence.
785 confirmation_probe_due_at: HashMap<String, DateTime<Utc>>,
786
787 /// Consecutive relay "recipient unreachable" verdicts per peer for the
788 /// session-confirmation probe. Drives an escalating backoff of
789 /// `confirmation_probe_due_at` (15s -> 600s cap, the same ladder the welcome
790 /// lifecycle uses) so a peer that established a session then vanished is not
791 /// probed every `CONFIRMATION_PROBE_INTERVAL_SECS` forever; a fleet of such
792 /// peers would otherwise exceed the relay's per-connection rate limit and
793 /// flap the connection. Reset on a reachability edge (`on_peer_presence`).
794 confirmation_probe_unreachable_parks: HashMap<String, u32>,
795
796 /// The confirmation probe each pending peer last sent. At most one probe
797 /// per peer lives in the outbox: a new one supersedes the last. Stacking a
798 /// fresh probe (new id, full retry ladder) every
799 /// `CONFIRMATION_PROBE_INTERVAL_SECS` on top of the unanswered ones filled
800 /// the outbox whenever no relay verdict (`unreachable` or
801 /// `relay_pushed`) arrived to back the schedule off, as on a mesh-only
802 /// carrier. Capacity eviction then failed real messages.
803 confirmation_probe_outstanding: HashMap<String, MessageId>,
804
805 /// The probe each pending peer's current one superseded. A relay verdict
806 /// finds its peer through the outbox entry, which the supersede removed,
807 /// so a verdict slower than `CONFIRMATION_PROBE_INTERVAL_SECS` would
808 /// otherwise be dropped and never back the schedule off, leaving the
809 /// peer probed every 5s against a relay that has said it is not there.
810 /// One id per peer, so this covers a verdict up to two intervals late;
811 /// one later than that is dropped, and the next probe's verdict backs
812 /// the schedule off instead.
813 confirmation_probe_superseded: HashMap<String, MessageId>,
814
815 /// Token bucket that caps how fast the RETRY/PROBE path
816 /// (`process_retry_queue`) re-sends, so a large backlog of resends to
817 /// unreachable peers cannot burst past the relay's per-connection rate limit
818 /// and trigger the disconnect->reconnect->reflood loop ("flapping"). Only
819 /// consulted when `RetryConfig::edge_driven_unreachable_dm` is opted in. It
820 /// bounds ONLY resends — first-attempt sends, ACKs, control frames and
821 /// handshakes take other paths and are never throttled, so live traffic
822 /// keeps full throughput/latency. `retry_drain_last` is None until first use.
823 retry_drain_tokens: f64,
824 retry_drain_last: Option<Instant>,
825
826 /// Rate-limit schedule for 1:1 session re-keys triggered by an epoch-desync
827 /// decrypt failure. Bounds the re-key to at most one per peer per
828 /// `REKEY_INTERVAL_SECS` so a peer replaying stale-epoch ciphertext (or an
829 /// injected wrong-epoch frame) cannot drive a re-key storm. The floor is
830 /// **never reset early** — not even by a successful decrypt on the healed
831 /// session — because a genuine re-fork and a replay are indistinguishable
832 /// here; it lapses only by the interval elapsing (see
833 /// `schedule_session_rekey`). In-memory only: a re-key is a live-connectivity
834 /// action, and a fresh desync after restart simply re-arms it.
835 rekey_due_at: HashMap<String, DateTime<Utc>>,
836
837 /// Outbound welcome lifecycle records keyed by peer id.
838 welcome_lifecycles: HashMap<String, WelcomeLifecycleRecord>,
839
840 /// Outbound connection requests still awaiting a transport outcome,
841 /// keyed by message id → recipient. Lets `on_transport_send_failed`
842 /// turn the relay's authoritative "recipient offline" DeliveryError
843 /// into a typed `ConnectionRequestUndeliverable` event instead of
844 /// silently discarding it (the welcome-only path never matches a
845 /// connection request's message id). Entries are dropped on emission
846 /// and on proof of delivery (the request's delivery ack, or an inbound
847 /// accept/reject from the recipient), and TTL-pruned on insert and at
848 /// read; bounded, so an app spamming requests cannot grow it
849 /// unboundedly.
850 pending_connection_requests: HashMap<String, PendingConnectionRequest>,
851
852 /// Backoff state for presence-driven welcome rescue, keyed by peer id.
853 /// Bounds how often an online-but-never-confirming peer triggers a
854 /// welcome re-arm/re-send (see `on_peer_presence`). Entries are dropped
855 /// lazily once the peer has no unconfirmed welcome left.
856 welcome_presence_rescue: HashMap<String, PresenceRescueThrottle>,
857
858 /// Peers for which we are the both-create "owner" (kept our own session
859 /// group on the lexicographic tiebreak) and are still awaiting a
860 /// *group-aware* proof that the peer adopted our group. While a peer is in
861 /// this set, only `decrypt_success` may confirm the session — a plaintext
862 /// confirmation probe/ack is NOT group-aware (it only proves the peer holds
863 /// *some* session, possibly its own pre-adoption group) and must not be
864 /// allowed to stop our Welcome retransmission, or the peer could be left
865 /// stranded on a divergent group. Persisted (see
866 /// `restore_both_create_awaiting_decrypt`) so an owner restart mid-convergence
867 /// keeps the gate, rather than letting a stale plaintext probe/ack confirm
868 /// prematurely. Mutate only via `mark_both_create_awaiting_decrypt` /
869 /// `clear_both_create_awaiting_decrypt` to keep memory and storage in sync.
870 both_create_awaiting_decrypt: std::collections::HashSet<String>,
871
872 /// Sink for MLS lifecycle telemetry.
873 mls_event_emitter: Arc<dyn MlsEventEmitter>,
874
875 /// Rate limiting policy for MLS failure event floods.
876 mls_event_rate_limiter: MlsEventRateLimiter,
877
878 /// Pre-install scrubber used by MLS emit sites before a sink is
879 /// installed (`enable_telemetry`, or `install_sink` beneath it). Once one
880 /// is installed, emit sites read `self.telemetry.scrubber` instead via
881 /// `current_scrubber()`.
882 /// Both scrubbers share `telemetry_fallback_secret` so opaque identifiers
883 /// observed by the legacy `MlsEventEmitter` stay consistent across the
884 /// install boundary.
885 telemetry_scrubber: Scrubber,
886
887 /// Per-instance fallback secret for identifier scrubbing. Random at
888 /// construction. Reused by both the pre-install `telemetry_scrubber`
889 /// and by any scrubber built inside `install_sink` (unless the
890 /// installed `TelemetryConfig` carries its own `scrub_secret`). Keeping
891 /// the fallback stable across installs means the legacy
892 /// `MlsEventEmitter` path observes consistent opaque IDs before and
893 /// after a sink is installed on the same protocol instance.
894 telemetry_fallback_secret: [u8; 16],
895
896 /// Whether the persistent per-install scrub secret has been loaded from
897 /// (or initialized into) secure storage yet. Starts `false`; set `true`
898 /// the first time `restore_or_init_scrub_secret` succeeds so the load is
899 /// idempotent across the two storage-entry paths
900 /// (`initialize_mls` also enables persistence, which would otherwise
901 /// re-load and rebuild the scrubber a second time).
902 telemetry_secret_persisted: bool,
903
904 /// Whether the persistent per-install Nostr signing secret has been
905 /// loaded from (or initialized into) secure storage and installed into
906 /// the Nostr transport yet. Same idempotency role across the two
907 /// storage-entry paths as `telemetry_secret_persisted`; see
908 /// `restore_or_init_nostr_signing_secret`.
909 nostr_secret_persisted: bool,
910
911 /// A Nostr signing secret that was installed into the transport but
912 /// could not be persisted (storage `store` failed). Kept so the next
913 /// `restore_or_init_nostr_signing_secret` attempt retries persisting
914 /// this same secret instead of rotating the install's relay-visible
915 /// identity again mid-session.
916 nostr_unpersisted_secret: Option<Zeroizing<[u8; 32]>>,
917
918 /// Installed telemetry context (sink + config + scrubber). `None` until
919 /// `enable_telemetry` installs one; thereafter shared with
920 /// `SharedState` via `Arc` clone so both emit paths dispatch through the
921 /// same configuration.
922 ///
923 /// The duplication with [`SharedState::telemetry`] is deliberate: MLS
924 /// lifecycle emission is driven from `&self` on `OfflineProtocol` and
925 /// does not hold the shared-state lock, while protocol-event emission
926 /// happens inside `SharedState::emit_event` under the lock. Each path
927 /// reads the context from whichever side it already has in hand, and
928 /// `install_sink` is the single writer that keeps both copies in sync
929 /// (guaranteed atomic from the caller's perspective because `&mut self`
930 /// excludes concurrent calls), and `detach_telemetry_pipe` the single
931 /// clearer.
932 pub(crate) telemetry: Option<Arc<TelemetryContext>>,
933
934 /// The running telemetry pipe, when `enable_telemetry` built one. Its
935 /// sink is what `telemetry` dispatches into; the pipe itself is kept so
936 /// the tick can wake it and `initialize_mls` can hand it storage.
937 #[cfg(feature = "telemetry-pipe")]
938 telemetry_pipe: Option<Arc<TelemetryPipe>>,
939
940 /// File transfer manager for chunking outbound and reassembling inbound media.
941 file_transfer_manager: FileTransferManager,
942
943 /// Stashed (content_type, media_metadata) from chunk-0 of incoming file transfers,
944 /// used to populate the FileReceived event once all chunks arrive.
945 pending_media_metadata: HashMap<String, PendingMediaMetadataEntry>,
946 /// Tracks outbound media transfer delivery progress keyed by file_id.
947 outbound_media_transfers: HashMap<String, OutboundMediaTransfer>,
948 /// Maps each outbound media chunk message ID to (file_id, chunk_index).
949 outbound_media_chunks: HashMap<MessageId, (String, u32)>,
950 /// Sliding-window state for outbound file transfers, keyed by file_id.
951 /// Chunks are only sent when the window has capacity (previous chunks ACKed).
952 outbound_media_windows: HashMap<String, OutboundTransferState>,
953 /// Descriptors of transfers that were in flight when the previous process
954 /// died, restored from storage. `start()` emits one `MediaResendRequired`
955 /// event each but deliberately does NOT drain them: entries stay parked
956 /// (and persisted) until a same-`file_id` resend consumes them — after
957 /// `send_media_with` checksum-validates the re-supplied bytes against
958 /// the descriptor — or the restore TTL prunes them, so an app that
959 /// misses the signal gets it again next restart.
960 restored_media_descriptors: HashMap<String, MediaTransferDescriptor>,
961
962 /// Mesh service registry and handler (extracted crate).
963 mesh_services: MeshServices,
964
965 /// Bundled state for mesh group messaging (member cache, dedup, pending commits).
966 pub(crate) group_mesh: crate::group_mesh::GroupMeshState,
967
968 /// Set of blocked user IDs. Messages from blocked users are silently
969 /// dropped (no ACK, no event). Persisted via `ProtocolStateStorage`.
970 blocked_users: HashSet<String>,
971
972 /// Timestamp of the last `kick_pending_session_reconciliation` execution.
973 /// Used to throttle reconciliation to avoid expensive storage I/O
974 /// (list_sessions → Keychain/Keystore) on every process tick / receive poll.
975 last_reconciliation_at: Option<Instant>,
976
977 /// Instant of the last periodic `MetricsFrame` emission, used to rate
978 /// the telemetry aggregator to `TelemetryConfig::metrics_cadence`.
979 last_metrics_emit_at: Option<Instant>,
980
981 /// Last observed per-transport status map, used by the telemetry
982 /// aggregator to diff against the current map and emit one
983 /// `TransportStateEvent` per transition.
984 transport_status_snapshot: HashMap<TransportType, TransportStatus>,
985
986 /// Last observed (battery, charging, relay_role) snapshot, used by the
987 /// telemetry aggregator to fire `DeviceCapabilitySnapshot` on change.
988 device_capability_snapshot: Option<DeviceSnap>,
989
990 /// The Lamport clock value at the time of the last storage write.
991 /// Used to debounce `persist_lamport_clock()` — only write when the
992 /// in-memory value has advanced past this by `LAMPORT_PERSIST_INTERVAL`
993 /// ticks. On crash, at most `LAMPORT_PERSIST_INTERVAL` ticks are lost,
994 /// which is harmless for a logical clock (the gap is absorbed by the
995 /// next merge with any peer).
996 last_persisted_lamport: u64,
997
998 /// Receive-side changes to the deduplicator's seen set since it was last
999 /// written. Drives the batched write in `persist_dedup_seen_if_due`;
1000 /// see `DedupSeenRecord` for why the set is persisted at all.
1001 dedup_dirty: u32,
1002
1003 /// When the seen set was last written (or restored), for the time half of
1004 /// the batching rule. Starts at construction so the first write waits for
1005 /// either the interval or the dirty threshold like every later one.
1006 dedup_last_persist: Instant,
1007
1008 /// The Nostr receive watermark last written to storage, or `None` if this
1009 /// session has neither written nor restored one. Debounces
1010 /// `persist_nostr_watermark()` the same way `last_persisted_lamport`
1011 /// debounces the clock.
1012 ///
1013 /// Deliberately *not* captured for `initialize_mls` rollback, matching
1014 /// where the restore itself runs: the mark lives in the transport, which
1015 /// the rollback cannot reach, and re-installing a durable value that only
1016 /// ever moves forward is not something a failed MLS restore needs undone.
1017 last_persisted_nostr_watermark: Option<i64>,
1018
1019 /// Which MLS key package stands in each published Nostr slot.
1020 ///
1021 /// Persisted, because the alternative to remembering it is republishing
1022 /// under fresh slot ids on every launch — leaving the old records standing
1023 /// with packages we would no longer track for consumption.
1024 nostr_publication_slots: Vec<nostr_publication::NostrPublicationSlot>,
1025
1026 /// Slot ids published during *this* process.
1027 ///
1028 /// Deliberately not persisted: an addressable record lives on the relays,
1029 /// so a launch cannot know which of them still hold it. Publishing each
1030 /// slot once per process is the cheap way to be sure — it replaces rather
1031 /// than accumulates, so the cost of being wrong is one redundant event.
1032 nostr_published_slots: HashSet<String>,
1033
1034 /// When the publication slots were last re-scanned, throttling the MLS
1035 /// storage reads that scan costs. See `nostr_slot_refresh_due`.
1036 last_nostr_slot_refresh: Option<Instant>,
1037
1038 /// Per-slot republish backoff after publications that never reached a
1039 /// relay. Memory-only, and pruned to the live slot ids each refresh.
1040 nostr_publication_backoff: HashMap<String, nostr_publication::PublicationBackoff>,
1041
1042 /// When the slot-exhaustion security warning was last emitted, suppressing
1043 /// repeats for a cause that by nature persists.
1044 last_nostr_slot_warning: Option<Instant>,
1045
1046 /// The username this install has published a discovery claim for, as last
1047 /// persisted. `None` means no claim stands.
1048 ///
1049 /// Persisted, because it is the only thing that knows *which* name to
1050 /// retract when the profile changes or the feature is turned off. See
1051 /// `storage_keys::NOSTR_DISCOVERY_CLAIM`.
1052 nostr_discovery_claim: Option<Username>,
1053
1054 /// Names whose tombstone has not yet reached a relay, oldest first.
1055 ///
1056 /// Persisted beside the claim, and for a stronger reason than the claim
1057 /// is: the claim can be rebuilt from `config.profile`, while a retracted
1058 /// name has no other record anywhere once the profile has moved on.
1059 /// Dropping it before the tombstone lands strands a human-readable name in
1060 /// a public directory pointing at an address whose key packages are still
1061 /// being published, so nothing downstream expires it.
1062 nostr_discovery_retracting: Vec<Username>,
1063
1064 /// Whether the standing claim has been published during *this* process.
1065 ///
1066 /// Not persisted, for the same reason `nostr_published_slots` is not: an
1067 /// addressable record lives on the relays and a launch cannot know which
1068 /// of them still hold it. Republishing once per process replaces rather
1069 /// than accumulates.
1070 nostr_discovery_published: bool,
1071
1072 /// Republish backoff for the discovery claim, keyed by its tag.
1073 ///
1074 /// Memory-only and structurally identical to
1075 /// `nostr_publication_backoff`: a relay that rejects kind 30777 outright
1076 /// would otherwise be retried once per refresh forever.
1077 nostr_discovery_backoff: HashMap<String, nostr_publication::PublicationBackoff>,
1078
1079 /// When the discovery claim was last re-examined, throttling the tick.
1080 last_nostr_discovery_refresh: Option<Instant>,
1081
1082 /// In-flight username resolutions, keyed by query id.
1083 ///
1084 /// A resolution accumulates every verified claim for its username and
1085 /// emits **one** event carrying the whole set. See
1086 /// `nostr_discovery::PendingResolution` for why the set is the only thing
1087 /// that is ever emitted.
1088 nostr_resolutions: HashMap<String, nostr_discovery::PendingResolution>,
1089
1090 /// Username lookups requested but not yet minted into a relay query, with
1091 /// the time each was asked for.
1092 ///
1093 /// The transport holds the queue; this holds the *deadline*. They are
1094 /// separate because minting is what may never happen: the platform pumps
1095 /// queries only while the relay socket is up, so a lookup requested offline
1096 /// would otherwise have no clock running behind it and no event to arrive.
1097 /// Bounded by the transport's own resolve-queue ceiling, since an entry is
1098 /// recorded only when the transport accepts the lookup.
1099 nostr_resolution_requests: HashMap<Username, Instant>,
1100
1101 /// When the push key-package pool-exhaustion warning was last emitted.
1102 ///
1103 /// Suppressed for the same reason the slot warning is: the condition is a
1104 /// pool at its ceiling, which persists until packages are consumed or
1105 /// expire, and every push while it holds would otherwise emit.
1106 last_push_key_package_warning: Option<Instant>,
1107
1108 /// When the last `GatewayAddressDeclarationRefused` and
1109 /// `GatewayAddressBindingMismatch` warnings were emitted, for the same
1110 /// suppression the push-key-package warning uses.
1111 last_gateway_refusal_warning: Option<Instant>,
1112 last_gateway_binding_mismatch_warning: Option<Instant>,
1113}
1114
1115impl Drop for OfflineProtocol {
1116 fn drop(&mut self) {
1117 // Flush debounced Lamport clock so no ticks are lost when the
1118 // protocol is dropped without an explicit stop() call.
1119 self.flush_lamport_clock();
1120 self.flush_nostr_watermark();
1121 // And the deduplicator's seen set, whose write is batched the same
1122 // way: a copy of a message received in the last few seconds before
1123 // the drop would otherwise be re-processed by the next launch.
1124 self.flush_dedup_seen();
1125 // Same reason: edits batch before they reach a record, so without a
1126 // flush here the debounce window between an edit and its delta is a
1127 // window in which work is lost.
1128 #[cfg(feature = "data")]
1129 let _ = self.data_flush_all();
1130 }
1131}
1132
1133#[cfg(test)]
1134struct TestProtocolStateStorage {
1135 storage: Arc<dyn MlsStorage>,
1136}
1137
1138/// Maps the fixture's MLS-storage failures onto the protocol-state contract,
1139/// mirroring what the UniFFI adapter does for real providers.
1140#[cfg(test)]
1141pub(crate) fn map_test_storage_error(
1142 error: offline_protocol_mls::StorageError,
1143) -> crate::ProtocolStateError {
1144 use crate::ProtocolStateError as P;
1145 use offline_protocol_mls::StorageError as S;
1146 match error {
1147 S::KeyNotFound(detail) => P::NotFound(detail),
1148 S::CorruptedData(detail) => P::Corrupted(detail),
1149 S::StoreFailed(detail) => P::StoreFailed(detail),
1150 S::LoadFailed(detail) => P::LoadFailed(detail),
1151 S::DeleteFailed(detail) => P::DeleteFailed(detail),
1152 S::Unavailable(detail) => P::LoadFailed(detail),
1153 // Required by `#[non_exhaustive]` on StorageError. This fixture mirrors
1154 // the UniFFI adapter, whose own fallback also degrades to `LoadFailed`.
1155 ref other => P::LoadFailed(other.to_string()),
1156 }
1157}
1158
1159#[cfg(test)]
1160impl ProtocolStateStorage for TestProtocolStateStorage {
1161 fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> crate::ProtocolStateResult<()> {
1162 self.storage
1163 .store(key_type, key_id, data)
1164 .map_err(map_test_storage_error)
1165 }
1166
1167 fn load(&self, key_type: &str, key_id: &str) -> crate::ProtocolStateResult<Option<Vec<u8>>> {
1168 self.storage
1169 .load(key_type, key_id)
1170 .map_err(map_test_storage_error)
1171 }
1172
1173 fn delete(&self, key_type: &str, key_id: &str) -> crate::ProtocolStateResult<()> {
1174 self.storage
1175 .delete(key_type, key_id)
1176 .map_err(map_test_storage_error)
1177 }
1178
1179 fn list_keys(&self, key_type: &str) -> crate::ProtocolStateResult<Vec<String>> {
1180 self.storage
1181 .list_keys(key_type)
1182 .map_err(map_test_storage_error)
1183 }
1184}
1185
1186impl OfflineProtocol {
1187 /// Creates a new protocol instance.
1188 ///
1189 /// # Arguments
1190 ///
1191 /// * `config` - Protocol configuration
1192 ///
1193 /// # Returns
1194 ///
1195 /// Returns `Ok(OfflineProtocol)` if successful, `Err` if configuration is invalid.
1196 pub fn new(config: ProtocolConfig) -> Result<Self> {
1197 // Validate configuration
1198 config.validate()?;
1199
1200 // Create transport selector for DORS
1201 let transport_selector = TransportSelector::with_config(config.dors.clone());
1202
1203 // Create transport manager
1204 let transport_manager = TransportManager::new(transport_selector);
1205
1206 // Per-instance fallback secret for identifier scrubbing. Stable for
1207 // the life of this protocol instance so opaque IDs stay consistent
1208 // across any later `install_telemetry_sink` call that does not
1209 // supply its own `scrub_secret`.
1210 let telemetry_fallback_secret = *uuid::Uuid::new_v4().as_bytes();
1211
1212 Ok(Self {
1213 transport_manager,
1214 ack_manager: AckManager::with_config(config.reliability.ack.clone()),
1215 retry_queue: RetryQueue::with_config(config.reliability.retry.clone()),
1216 deduplicator: Deduplicator::with_config(config.reliability.dedup.clone()),
1217 mesh_relay: MeshRelayGovernor::with_config(
1218 config.profile.clone(),
1219 config.mesh_relay.clone(),
1220 ),
1221 custody: custody::CustodyStore::new(config.custody.clone()),
1222 custody_receipts: HashMap::new(),
1223 custody_last_sweep: Instant::now(),
1224 local_id: config.profile.clone(),
1225 identity_established: false,
1226 shared_state: Arc::new(Mutex::new(SharedState::new())),
1227 outbox: HashMap::new(),
1228 pending_reseal: HashMap::new(),
1229 staged_app_send: None,
1230 undecryptable_resent: HashSet::new(),
1231 media_outbox: HashMap::new(),
1232 window_held: HashMap::new(),
1233 dm_unreachable_parks: HashMap::new(),
1234 unrouted_acks: VecDeque::new(),
1235 reachability: reachability::ReachabilityFacts::default(),
1236 gateway_capabilities: HashSet::new(),
1237 mls_manager: None,
1238 pending_encrypted_messages: HashMap::new(),
1239 deferred_restore_settlements: Vec::new(),
1240 suppressed_restore_settlements: 0,
1241 next_pending_message_expiry: None,
1242 pending_key_packages: HashMap::new(),
1243 key_package_sent_to: HashMap::new(),
1244 known_peers: HashMap::new(),
1245 confirmed_sessions: std::collections::HashSet::new(),
1246 encryption_capable_peers: std::collections::HashSet::new(),
1247 peer_compact_envelope: std::collections::HashSet::new(),
1248 peer_ctrl_freshness: std::collections::HashSet::new(),
1249 peer_undecryptable_ack: std::collections::HashSet::new(),
1250 peer_coalesced_ack: std::collections::HashSet::new(),
1251 coalesced_acks_out: HashMap::new(),
1252 control_freshness_peers: std::collections::HashSet::new(),
1253 control_reset_watermark: std::collections::HashMap::new(),
1254 peer_rich_payload: std::collections::HashSet::new(),
1255 peer_svc_sealed: std::collections::HashSet::new(),
1256 svc_sealed_proved_peers: std::collections::HashSet::new(),
1257 peer_data_sync: std::collections::HashSet::new(),
1258 peer_data_group: std::collections::HashSet::new(),
1259 peer_data_group_attested: std::collections::HashSet::new(),
1260 peer_data_media: std::collections::HashSet::new(),
1261 peer_data_tombstones: std::collections::HashSet::new(),
1262 peer_data_interest: std::collections::HashSet::new(),
1263 peer_data_group_blob: std::collections::HashSet::new(),
1264 peer_data_group_blob_attested: std::collections::HashSet::new(),
1265 peer_data_custody: std::collections::HashSet::new(),
1266 peer_rich_attested: std::collections::HashSet::new(),
1267 plaintext_send_warned: std::collections::HashSet::new(),
1268 plaintext_receive_warned: std::collections::HashSet::new(),
1269 encryption_capable_persisted: HashSet::new(),
1270 control_gate_warned: HashMap::new(),
1271 pending_queue: PendingDecryptionQueue::default(),
1272 secure_storage: None,
1273 protocol_state_storage: None,
1274 state_record_cipher: None,
1275 #[cfg(feature = "data")]
1276 data: data::DataLayer::default(),
1277 #[cfg(feature = "data")]
1278 last_data_sync_offer: HashMap::new(),
1279 #[cfg(feature = "data")]
1280 data_sync_settle: HashMap::new(),
1281 #[cfg(feature = "data")]
1282 pending_attachment_fetches: HashMap::new(),
1283 blob_request_windows: HashMap::new(),
1284 #[cfg(feature = "data")]
1285 group_spaces_enumerated: false,
1286 lamport_clock: LamportClock::new(),
1287 confirmation_retry_due_at: HashMap::new(),
1288 confirmation_probe_due_at: HashMap::new(),
1289 confirmation_probe_unreachable_parks: HashMap::new(),
1290 confirmation_probe_outstanding: HashMap::new(),
1291 confirmation_probe_superseded: HashMap::new(),
1292 retry_drain_tokens: 0.0,
1293 retry_drain_last: None,
1294 rekey_due_at: HashMap::new(),
1295 welcome_lifecycles: HashMap::new(),
1296 pending_connection_requests: HashMap::new(),
1297 welcome_presence_rescue: HashMap::new(),
1298 both_create_awaiting_decrypt: std::collections::HashSet::new(),
1299 mls_event_emitter: Arc::new(NoopMlsEventEmitter),
1300 mls_event_rate_limiter: MlsEventRateLimiter::default(),
1301 // The pre-install scrubber uses `TelemetryConfig::default()` —
1302 // `scrub_ids=true`, no explicit `scrub_secret`. Identifier hashing
1303 // is therefore on from the moment the protocol is constructed,
1304 // matching today's always-scrub MLS observability semantics. When
1305 // `install_telemetry_sink` later supplies a config with
1306 // `scrub_ids(false)`, a new scrubber is derived from that config
1307 // but reuses `telemetry_fallback_secret` so opaque IDs remain
1308 // stable across the install boundary.
1309 telemetry_scrubber: Scrubber::from_config(
1310 &TelemetryConfig::default(),
1311 telemetry_fallback_secret,
1312 ),
1313 telemetry_fallback_secret,
1314 telemetry_secret_persisted: false,
1315 nostr_secret_persisted: false,
1316 nostr_unpersisted_secret: None,
1317 telemetry: None,
1318 #[cfg(feature = "telemetry-pipe")]
1319 telemetry_pipe: None,
1320 file_transfer_manager: FileTransferManager::new(),
1321 pending_media_metadata: HashMap::new(),
1322 outbound_media_transfers: HashMap::new(),
1323 outbound_media_chunks: HashMap::new(),
1324 outbound_media_windows: HashMap::new(),
1325 restored_media_descriptors: HashMap::new(),
1326 mesh_services: MeshServices::new(),
1327 group_mesh: crate::group_mesh::GroupMeshState::default(),
1328 blocked_users: HashSet::new(),
1329 last_reconciliation_at: None,
1330 last_metrics_emit_at: None,
1331 transport_status_snapshot: HashMap::new(),
1332 device_capability_snapshot: None,
1333 last_persisted_lamport: 0,
1334 dedup_dirty: 0,
1335 dedup_last_persist: Instant::now(),
1336 last_persisted_nostr_watermark: None,
1337 nostr_publication_slots: Vec::new(),
1338 nostr_published_slots: HashSet::new(),
1339 last_nostr_slot_refresh: None,
1340 nostr_publication_backoff: HashMap::new(),
1341 last_nostr_slot_warning: None,
1342 nostr_discovery_claim: None,
1343 nostr_discovery_retracting: Vec::new(),
1344 nostr_discovery_published: false,
1345 nostr_discovery_backoff: HashMap::new(),
1346 last_nostr_discovery_refresh: None,
1347 nostr_resolutions: HashMap::new(),
1348 nostr_resolution_requests: HashMap::new(),
1349 last_push_key_package_warning: None,
1350 last_gateway_refusal_warning: None,
1351 last_gateway_binding_mismatch_warning: None,
1352 config,
1353 })
1354 }
1355
1356 /// Initializes MLS encryption and protocol persistence.
1357 ///
1358 /// This must be called before encryption can be used. The storage
1359 /// `secure_storage` must be a platform-native credential store.
1360 /// `protocol_state_storage` must be scoped to the app container and is
1361 /// removed when that container is deleted.
1362 ///
1363 /// Ownership model:
1364 /// - `OfflineProtocol` is the single authoritative owner of `MlsManager`
1365 /// - initialization is idempotent per protocol instance
1366 /// - subsequent calls return without replacing the existing manager
1367 /// - manager publication is transactional: restore must succeed before
1368 /// `mls_manager` becomes visible to callers
1369 ///
1370 /// The two storage domains are intentionally different trait objects. This
1371 /// prevents message-plane state from inheriting the lifecycle of Keychain
1372 /// or another credential store.
1373 pub fn initialize_mls(
1374 &mut self,
1375 secure_storage: Arc<dyn MlsStorage>,
1376 protocol_state_storage: Arc<dyn ProtocolStateStorage>,
1377 ) -> Result<()> {
1378 self.initialize_mls_inner(secure_storage, protocol_state_storage, true)
1379 }
1380
1381 /// Returns this device's address, once it has one.
1382 ///
1383 /// `None` before `initialize_mls`: the address is the hash of an identity
1384 /// key that lives in this profile's storage, and storage does not arrive
1385 /// until then. It never changes afterwards, and it is stable across
1386 /// restarts of the same profile because the key is.
1387 pub fn local_address(&self) -> Option<&str> {
1388 self.identity_established.then_some(self.local_id.as_str())
1389 }
1390
1391 /// `adopt_legacy_state` exists only so test fixtures can point both handles
1392 /// at one backend. Adoption moves records *between* the two stores, which
1393 /// on a shared backend would mean deleting a record through the same store
1394 /// it was just read from.
1395 ///
1396 /// There is one identity path and every caller takes it — a fixture that
1397 /// wants a particular identity seeds the key into `secure_storage` first
1398 /// (see [`crate::test_identity`]) rather than naming an id here, because an
1399 /// id nothing holds a key for is exactly what `MlsManager::new` refuses.
1400 fn initialize_mls_inner(
1401 &mut self,
1402 secure_storage: Arc<dyn MlsStorage>,
1403 protocol_state_storage: Arc<dyn ProtocolStateStorage>,
1404 adopt_legacy_state: bool,
1405 ) -> Result<()> {
1406 if self.mls_manager.is_some() {
1407 return Ok(());
1408 }
1409
1410 // The bootstrap inversion: load (or mint) the identity key first, and
1411 // take this device's wire identity from it. Before this point the
1412 // protocol has no address — `local_id` is still the profile — which is
1413 // exactly why the key has to be resolved here, ahead of the manager
1414 // that embeds the id in its credential and in every session slot.
1415 let (_, address) = MlsManager::load_or_create_identity(&secure_storage)?;
1416 let local_id = address.to_string();
1417
1418 let mut manager = MlsManager::new(&local_id, secure_storage.clone())?;
1419 // Installed before the manager becomes shared: the enforcement flag is
1420 // read on every group decrypt, and this is the one point where the
1421 // manager is still uniquely owned.
1422 manager.set_enforce_admin_commits(self.config.group.enforce_admin_commits);
1423 let manager = Arc::new(RwLock::new(manager));
1424
1425 // Keep initialization transactional so a restore failure cannot leave
1426 // partially-initialized MLS state visible and then permanently block retries.
1427 let previous_secure_storage = self.secure_storage.clone();
1428 let previous_protocol_state_storage = self.protocol_state_storage.clone();
1429 // The record cipher belongs to the secure store it was loaded from, so
1430 // it is part of the same transaction as the storage handles: taken here
1431 // so a failed init cannot leave the new store's key installed next to
1432 // the rolled-back handles, and restored below alongside them.
1433 let previous_state_record_cipher = self.state_record_cipher.take();
1434 let previous_pending_messages = self.pending_encrypted_messages.clone();
1435 let previous_pending_message_expiry = self.next_pending_message_expiry;
1436 // The inbound half of the same transaction: the restore re-admits
1437 // parked frames from the store being attached, so a rollback has to put
1438 // back what the queue held before, not an empty queue.
1439 let previous_pending_queue = self.pending_queue.clone();
1440 // Populated by restore steps that run before other fallible ones, so
1441 // they belong in the transaction like everything else: a failed init
1442 // must not leave a key-package cache, parked media descriptors, or an
1443 // owner gate sourced from a store the rollback has just detached.
1444 let previous_pending_key_packages = self.pending_key_packages.clone();
1445 let previous_restored_media_descriptors = self.restored_media_descriptors.clone();
1446 let previous_both_create_awaiting_decrypt = self.both_create_awaiting_decrypt.clone();
1447 let previous_confirmed_sessions = self.confirmed_sessions.clone();
1448 let previous_encryption_capable_peers = self.encryption_capable_peers.clone();
1449 let previous_welcome_lifecycles = self.welcome_lifecycles.clone();
1450 let previous_lamport_clock = self.lamport_clock.value();
1451 // The debounce watermark travels with the clock: `restore_lamport_clock`
1452 // sets both, so rolling back only the clock leaves a watermark ahead of
1453 // it and `persist_lamport_clock`'s `wrapping_sub` reads that as an
1454 // enormous delta — writing on every tick instead of every interval.
1455 let previous_last_persisted_lamport = self.last_persisted_lamport;
1456 let previous_blocked_users = self.blocked_users.clone();
1457 let previous_outbox = self.outbox.clone();
1458 let previous_peer_compact_envelope = self.peer_compact_envelope.clone();
1459 let previous_peer_ctrl_freshness = self.peer_ctrl_freshness.clone();
1460 let previous_peer_undecryptable_ack = self.peer_undecryptable_ack.clone();
1461 let previous_peer_coalesced_ack = self.peer_coalesced_ack.clone();
1462 // Snapshotted with its siblings so a failed re-initialize restores it
1463 // too. It is a ratchet, so the direction that matters is that a
1464 // rollback must not *lose* it: a peer we hold to the newer payload has
1465 // to still be held to it if the re-initialize is abandoned.
1466 let previous_control_freshness_peers = self.control_freshness_peers.clone();
1467 let previous_control_reset_watermark = self.control_reset_watermark.clone();
1468 let previous_peer_rich_payload = self.peer_rich_payload.clone();
1469 let previous_peer_svc_sealed = self.peer_svc_sealed.clone();
1470 let previous_svc_sealed_proved_peers = self.svc_sealed_proved_peers.clone();
1471 let previous_peer_data_sync = self.peer_data_sync.clone();
1472 let previous_peer_rich_attested = self.peer_rich_attested.clone();
1473 let previous_peer_data_group = self.peer_data_group.clone();
1474 let previous_peer_data_group_attested = self.peer_data_group_attested.clone();
1475 let previous_peer_data_media = self.peer_data_media.clone();
1476 let previous_peer_data_tombstones = self.peer_data_tombstones.clone();
1477 let previous_peer_data_interest = self.peer_data_interest.clone();
1478 let previous_peer_data_group_blob = self.peer_data_group_blob.clone();
1479 let previous_peer_data_group_blob_attested = self.peer_data_group_blob_attested.clone();
1480
1481 let previous_local_id = std::mem::replace(&mut self.local_id, local_id.clone());
1482 let previous_identity_established = self.identity_established;
1483 self.identity_established = true;
1484 // The governor took the profile at construction because no address
1485 // existed yet; it keys relay fan-out and jitter off the local id, so it
1486 // has to move to the real one here.
1487 self.mesh_relay.set_local_id(local_id);
1488
1489 self.secure_storage = Some(secure_storage);
1490 self.protocol_state_storage = Some(protocol_state_storage);
1491
1492 // Must precede every restore below: sealed protocol-state records
1493 // (pending messages, outbox, media descriptors) are unreadable without
1494 // this key, so restoring first would silently start from empty and then
1495 // overwrite durable state with that empty view.
1496 self.restore_or_init_state_record_key();
1497
1498 // Then adopt anything the pre-split build left in secure storage, so
1499 // the restores below see one complete view. Must follow the record key
1500 // (adoption seals what it moves) and precede every restore (a restore
1501 // that ran first would find nothing and then overwrite the legacy state
1502 // with that empty view). Best-effort and resumable: a failure here
1503 // leaves the legacy records in place for the next launch.
1504 if adopt_legacy_state {
1505 self.adopt_legacy_protocol_state();
1506 }
1507
1508 // The settlement queue is deliberately NOT captured for rollback, and
1509 // that is the whole point rather than an omission.
1510 //
1511 // The rollback below restores *in-memory* state. It restores no storage
1512 // state, because restore has none to give back: by the time a later
1513 // step fails, an earlier one has already deleted an unaddressable
1514 // pending queue, dropped a record its store reported corrupt, and
1515 // rewritten a peer's snapshot without the entries it evicted for
1516 // capacity. Those records are gone. So the invariant adoption is
1517 // documented with — nothing can re-derive a settlement for a record
1518 // that no longer exists — is not special to adoption; it holds for
1519 // every settlement produced under this transaction, and rolling any of
1520 // them back means the application keeps an id that resolves to nothing,
1521 // on this launch and on every launch after it.
1522 //
1523 // The cost of not rolling back is that a retry which re-examines a
1524 // still-present record can settle the same id twice. `message_failed`
1525 // is terminal and idempotent for any reasonable consumer, and a
1526 // duplicate terminal event is a far smaller lie than silence.
1527 //
1528 // Load the persistent scrub secret outside the transactional restore
1529 // below: it is independent of MLS state, and a later MLS-restore
1530 // rollback must not undo it (the secret is idempotent and reused on
1531 // the next attempt).
1532 self.restore_or_init_scrub_secret();
1533
1534 // Same reasoning for the Nostr signing secret: independent of MLS
1535 // state, idempotent, and must survive an MLS-restore rollback.
1536 self.restore_or_init_nostr_signing_secret();
1537
1538 // And for the Nostr receive watermark, which sits outside the
1539 // transaction below for a third reason on top of those two: it does
1540 // not live in `self` at all. The mark is transport state, so the
1541 // rollback — which restores in-memory fields of `OfflineProtocol` —
1542 // could not put it back even if it wanted to. Installing it under a
1543 // later failure is harmless: it only ever moves forward, and the worst
1544 // a stale-but-installed mark does is narrow one replay window.
1545 self.restore_nostr_watermark();
1546 self.restore_nostr_publication_slots();
1547 self.restore_nostr_discovery_claim();
1548
1549 // The launch's whole durable-delete allowance, in one place because the
1550 // bound is on the launch. A device-barrier storm kills *this call*, not
1551 // any single walk in it, so no walk may hand itself a pool — see
1552 // `PruneAllowance::pool`. Four of them, and the ceiling is their sum:
1553 //
1554 // - `advisory_prunes` is shared by the `ADVISORY_PRUNE_WALKS` walks
1555 // whose prunes are caches or advisory signals. They draw in the order
1556 // written below, and that order is part of the contract: each leaves
1557 // the ones after it a `MIN_ADVISORY_PRUNE_DELETES` floor and may take
1558 // the rest, so an earlier walk gets first call on whatever the later
1559 // ones do not reserve. Reordering them, or adding a sixth without
1560 // bumping `ADVISORY_PRUNE_WALKS`, changes who gets pruned under load.
1561 // - `pending_prunes` and `outbox_prunes` are private to the two
1562 // settlement-paired walks, because being starved there defers a
1563 // diagnostic or a *delivery* rather than a cache eviction, and
1564 // neither may be held hostage to a key-package flood in a category it
1565 // has nothing to do with.
1566 // - `inbound_prunes` is private to the two receive-side walks for the
1567 // same reason: the pending-decryption walk holds ciphertext the app
1568 // is told about when it is lost. It draws first, and the seen-set
1569 // restore takes whatever is left for its one possible delete.
1570 //
1571 // See `storage::MAX_RESTORE_PRUNE_DELETES`.
1572 let mut advisory_prunes = PruneAllowance::pool();
1573 let mut pending_prunes = PruneAllowance::pool();
1574 let mut outbox_prunes = PruneAllowance::pool();
1575 let mut inbound_prunes = PruneAllowance::pool();
1576
1577 // Restore state from previous session
1578 let restore_result = (|| {
1579 self.restore_pending_messages(&mut pending_prunes)?;
1580 self.restore_pending_decrypt_entries(&mut inbound_prunes);
1581 self.restore_custody(&mut inbound_prunes);
1582 self.restore_lamport_clock();
1583 self.restore_dedup_seen(&mut inbound_prunes);
1584 self.restore_encryption_capable_peers();
1585 self.restore_blocked_users()?;
1586 self.restore_session_states_from_manager(manager.clone(), &mut advisory_prunes)?;
1587 self.restore_peer_key_packages(&manager, &mut advisory_prunes)?;
1588 // Must precede start(): flush_restored_confirmed_pending_messages
1589 // re-makes the rich seal decision against these sets, and an
1590 // empty set there silently drops queued rich extras.
1591 self.restore_peer_capabilities(&manager, &mut advisory_prunes);
1592 self.restore_welcome_lifecycles(&mut advisory_prunes)?;
1593 self.restore_outbox(&mut outbox_prunes)?;
1594 self.restore_media_descriptors(&mut advisory_prunes)?;
1595 self.restore_both_create_awaiting_decrypt();
1596 Ok(())
1597 })();
1598
1599 if let Err(err) = restore_result {
1600 self.mesh_relay.set_local_id(previous_local_id.clone());
1601 self.local_id = previous_local_id;
1602 self.identity_established = previous_identity_established;
1603 self.secure_storage = previous_secure_storage;
1604 self.protocol_state_storage = previous_protocol_state_storage;
1605 self.state_record_cipher = previous_state_record_cipher;
1606 self.pending_encrypted_messages = previous_pending_messages;
1607 self.next_pending_message_expiry = previous_pending_message_expiry;
1608 // Back to what the queue held before this call. Emptying it lost
1609 // frames that were never persisted (no store was attached, or the
1610 // best-effort write failed). Keeping what the restore added would
1611 // leave entries sourced from the store the rollback just detached:
1612 // draining one deletes nothing on disk, so the next launch restores
1613 // it and drains it again into a spent ratchet generation.
1614 self.pending_queue = previous_pending_queue;
1615 // `deferred_restore_settlements` is deliberately left alone — see
1616 // the comment where the other baselines are captured.
1617 self.pending_key_packages = previous_pending_key_packages;
1618 self.restored_media_descriptors = previous_restored_media_descriptors;
1619 self.both_create_awaiting_decrypt = previous_both_create_awaiting_decrypt;
1620 self.confirmed_sessions = previous_confirmed_sessions;
1621 // Restored for consistency with the rest of the transaction, not
1622 // for safety: this set only ever grows, so keeping the wider view
1623 // would also have been sound. Rolling it back keeps "a failed
1624 // initialize_mls leaves no trace" true without exception.
1625 self.encryption_capable_peers = previous_encryption_capable_peers;
1626 self.welcome_lifecycles = previous_welcome_lifecycles;
1627 self.lamport_clock = LamportClock::from_value(previous_lamport_clock);
1628 self.last_persisted_lamport = previous_last_persisted_lamport;
1629 self.blocked_users = previous_blocked_users;
1630 self.outbox = previous_outbox;
1631 self.peer_compact_envelope = previous_peer_compact_envelope;
1632 self.peer_ctrl_freshness = previous_peer_ctrl_freshness;
1633 self.peer_undecryptable_ack = previous_peer_undecryptable_ack;
1634 self.peer_coalesced_ack = previous_peer_coalesced_ack;
1635 self.control_freshness_peers = previous_control_freshness_peers;
1636 self.control_reset_watermark = previous_control_reset_watermark;
1637 self.peer_rich_payload = previous_peer_rich_payload;
1638 self.peer_svc_sealed = previous_peer_svc_sealed;
1639 self.svc_sealed_proved_peers = previous_svc_sealed_proved_peers;
1640 self.peer_data_sync = previous_peer_data_sync;
1641 self.peer_rich_attested = previous_peer_rich_attested;
1642 self.peer_data_group = previous_peer_data_group;
1643 self.peer_data_group_attested = previous_peer_data_group_attested;
1644 self.peer_data_media = previous_peer_data_media;
1645 self.peer_data_tombstones = previous_peer_data_tombstones;
1646 self.peer_data_interest = previous_peer_data_interest;
1647 self.peer_data_group_blob = previous_peer_data_group_blob;
1648 self.peer_data_group_blob_attested = previous_peer_data_group_blob_attested;
1649 return Err(err);
1650 }
1651
1652 self.mls_manager = Some(manager);
1653 #[cfg(feature = "telemetry-pipe")]
1654 self.attach_telemetry_storage();
1655 // A re-init can swap the identity, and with it which groups exist, so
1656 // the one-shot group enumeration has to run again. Past the last
1657 // `return Err` above, so a failed init leaves the flag alone.
1658 #[cfg(feature = "data")]
1659 {
1660 self.group_spaces_enumerated = false;
1661 }
1662 self.emit_mls_initialized();
1663
1664 // Announced only here, past the last `return Err` — a restore failure
1665 // rolls the address back, and announcing an identity the instance then
1666 // abandoned would leave the app addressing a device that is not this
1667 // one.
1668 self.emit_event(Event::identity_ready(self.local_id.clone()));
1669
1670 info!(profile = %self.config.profile, "MLS encryption initialized with split secure and protocol-state storage");
1671 Ok(())
1672 }
1673
1674 /// Test-only adapter for fixtures that intentionally use one
1675 /// in-memory/fault-injection backend to observe both storage domains.
1676 #[cfg(test)]
1677 pub(crate) fn initialize_mls_for_test(&mut self, storage: Arc<dyn MlsStorage>) -> Result<()> {
1678 let protocol_state_storage = Arc::new(TestProtocolStateStorage {
1679 storage: storage.clone(),
1680 });
1681 // Give the storage the identity this fixture's profile label stands
1682 // for, then take the production path over it. The instance comes up as
1683 // a genuine derived address, and it is the *same* address every time a
1684 // fixture says `"alice"` — which is what lets tests keep readable
1685 // labels while still exercising derived identity.
1686 crate::test_identity::seed_identity(&storage, &self.config.profile);
1687 self.initialize_mls_inner(storage, protocol_state_storage, false)
1688 }
1689
1690 /// Test-only init that takes the **production** identity path: the address
1691 /// is derived from the identity key in `storage` rather than forced.
1692 #[cfg(test)]
1693 pub(crate) fn initialize_mls_for_test_derived(
1694 &mut self,
1695 storage: Arc<dyn MlsStorage>,
1696 ) -> Result<()> {
1697 let protocol_state_storage = Arc::new(TestProtocolStateStorage {
1698 storage: storage.clone(),
1699 });
1700 self.initialize_mls_inner(storage, protocol_state_storage, false)
1701 }
1702
1703 /// Test-only storage initialization for persistence-focused fixtures that
1704 /// do not need an MLS manager.
1705 #[cfg(test)]
1706 pub(crate) fn enable_message_persistence_for_test(
1707 &mut self,
1708 storage: Arc<dyn MlsStorage>,
1709 ) -> Result<()> {
1710 self.secure_storage = Some(storage.clone());
1711 self.protocol_state_storage = Some(Arc::new(TestProtocolStateStorage { storage }));
1712 self.restore_or_init_state_record_key();
1713 self.restore_or_init_scrub_secret();
1714 self.restore_or_init_nostr_signing_secret();
1715 self.restore_nostr_watermark();
1716 self.restore_nostr_publication_slots();
1717 self.restore_nostr_discovery_claim();
1718 // Same four pools as `initialize_mls_inner`, for the same reason.
1719 let mut advisory_prunes = PruneAllowance::pool();
1720 let mut pending_prunes = PruneAllowance::pool();
1721 let mut outbox_prunes = PruneAllowance::pool();
1722 let mut inbound_prunes = PruneAllowance::pool();
1723 self.restore_pending_messages(&mut pending_prunes)?;
1724 self.restore_pending_decrypt_entries(&mut inbound_prunes);
1725 self.restore_custody(&mut inbound_prunes);
1726 self.restore_lamport_clock();
1727 self.restore_dedup_seen(&mut inbound_prunes);
1728 self.restore_encryption_capable_peers();
1729 self.restore_blocked_users()?;
1730 self.restore_outbox(&mut outbox_prunes)?;
1731 self.restore_media_descriptors(&mut advisory_prunes)?;
1732 Ok(())
1733 }
1734
1735 /// Checks if MLS encryption is initialized.
1736 pub fn is_mls_initialized(&self) -> bool {
1737 self.mls_manager.is_some()
1738 }
1739
1740 /// Records that `peer_id` has demonstrated it runs MLS.
1741 ///
1742 /// The only way into [`Self::encryption_capable_peers`]. Call it from
1743 /// anywhere a peer proves encryption capability — creating or joining a 1:1
1744 /// session with them, or pinning their signing key. Calling it redundantly
1745 /// is free and calling it too often is safe; the failure that matters is
1746 /// *not* calling it, which leaves the plaintext gate open for that peer.
1747 ///
1748 /// # Bounded by refusal, never by eviction
1749 ///
1750 /// The keys are wire-claimed ids and need their own cap, like every other
1751 /// map keyed that way — but not for the reason this comment used to give.
1752 /// It cited an unsigned `__MLS_WELCOME__` reaching the marking path; the
1753 /// control gate now rejects unsigned control frames before dispatch, and
1754 /// `__MLS_WELCOME__` is in neither exemption list, so `handle_welcome_message`
1755 /// only ever runs on a verified frame.
1756 ///
1757 /// The cap survives that because a signature was never the bound. A
1758 /// signature proves the signer holds the key its address derives from; it
1759 /// does not prove the address belongs to anyone real, and minting a fresh
1760 /// one costs a keygen. So an attacker willing to spend keygens can still
1761 /// present an unbounded supply of distinct, perfectly well-signed peers —
1762 /// which is precisely what a cap on a wire-keyed map is for.
1763 ///
1764 /// It has to be a refusal. Evicting would un-mark a peer that really is
1765 /// encryption-capable — the fail-open direction this field exists to
1766 /// prevent — and would hand an attacker a way to unprotect a *chosen*
1767 /// victim by flooding. Refusing only declines to add knowledge: that peer
1768 /// falls through to the `is_session_confirmed` check in
1769 /// [`Self::accept_plaintext_content`], which is exactly what shipped before
1770 /// this set existed. So a flood costs later peers the improvement and never
1771 /// costs anyone the protection they already had.
1772 ///
1773 /// Legitimate peers are also first in line by construction: restore seeds
1774 /// the set from the session list and the durable capability records during
1775 /// `initialize_mls`, before `start()` admits any traffic.
1776 ///
1777 /// # Return value
1778 ///
1779 /// `true` when `peer_id` is in the set on return — whether this call added
1780 /// it or a previous one did. This is what bounds the *durable* record:
1781 /// [`Self::record_encryption_capable`](crate::OfflineProtocol) persists only
1782 /// when this returns `true`, so the storage category can never hold more
1783 /// peers than the cap below. Without that coupling a Sybil flood — cheap,
1784 /// since each identity is one Ed25519 keygen that signs honestly as itself
1785 /// — would write unbounded records, and the restore walk (bounded by
1786 /// `MAX_RESTORE_KEYS_PER_CATEGORY`) could then miss a legitimate peer's
1787 /// record and re-open the plaintext gate for them on the next launch.
1788 pub(crate) fn mark_encryption_capable(&mut self, peer_id: &str) -> bool {
1789 if peer_id.is_empty() || peer_id == self.local_id {
1790 return false;
1791 }
1792 if self.encryption_capable_peers.contains(peer_id) {
1793 return true;
1794 }
1795 if self.encryption_capable_peers.len() >= MAX_ENCRYPTION_CAPABLE_PEERS {
1796 debug!(
1797 peer_id = %peer_id,
1798 cap = MAX_ENCRYPTION_CAPABLE_PEERS,
1799 "Encryption-capability set at capacity; peer falls back to the session-state check"
1800 );
1801 return false;
1802 }
1803 self.encryption_capable_peers.insert(peer_id.to_string());
1804 debug!(peer_id = %peer_id, "Peer marked encryption-capable");
1805 if self.encryption_capable_peers.len() == MAX_ENCRYPTION_CAPABLE_PEERS {
1806 // Emitted on the transition into full, so it cannot become a
1807 // per-frame flood: the set only grows and has no removal path, so
1808 // this line is reachable exactly once per process.
1809 warn!(
1810 cap = MAX_ENCRYPTION_CAPABLE_PEERS,
1811 "Encryption-capability set reached capacity; further peers fall back to the \
1812 session-state check. Expected only under a forged-sender flood."
1813 );
1814 }
1815 true
1816 }
1817
1818 /// Whether `peer_id` is known to run MLS. See
1819 /// [`Self::encryption_capable_peers`] for why this is not a storage read.
1820 pub(crate) fn is_encryption_capable(&self, peer_id: &str) -> bool {
1821 self.encryption_capable_peers.contains(peer_id)
1822 }
1823
1824 /// Drops `peer_id` from the in-memory capability set **only**, leaving its
1825 /// durable record and write-elision cache entry in place.
1826 ///
1827 /// Test-only, and deliberately not a general "forget" — the set has no
1828 /// removal path by design (see [`Self::encryption_capable_peers`]). It
1829 /// reproduces one specific state that production does reach: the rollback in
1830 /// `initialize_mls` restores the pre-restore snapshot of the set while
1831 /// [`Self::encryption_capable_persisted`] keeps what the restore walk read,
1832 /// so the two disagree until the peer's next verified frame re-marks them.
1833 #[cfg(test)]
1834 pub(crate) fn forget_encryption_capable_for_test(&mut self, peer_id: &str) {
1835 self.encryption_capable_peers.remove(peer_id);
1836 }
1837
1838 /// Returns whether auto-encryption should be applied.
1839 fn should_auto_encrypt(&self) -> bool {
1840 self.config.encryption.enabled && self.mls_manager.is_some()
1841 }
1842
1843 /// Configures the MLS lifecycle event emitter.
1844 pub fn set_mls_event_emitter(&mut self, emitter: Arc<dyn MlsEventEmitter>) {
1845 self.mls_event_emitter = emitter;
1846 }
1847
1848 /// Installs the unified telemetry sink for this protocol instance.
1849 ///
1850 /// Every subsequent protocol [`Event`] and MLS lifecycle event is
1851 /// additionally forwarded to `sink` as a
1852 /// [`crate::telemetry::TelemetryRecord`], gated by the verbosity tier
1853 /// and identifier-scrubbing preferences in `config`. Protocol events
1854 /// flow through as [`TelemetryRecord::Protocol`], MLS lifecycle events
1855 /// as [`TelemetryRecord::Mls`].
1856 ///
1857 /// This does not replace the legacy [`crate::EventCallback`] and
1858 /// [`MlsEventEmitter`] paths — they continue to fire independently for
1859 /// backward compatibility.
1860 ///
1861 /// Events emitted before this call (for example
1862 /// [`crate::mls_observability::MlsLifecycleEvent::Initialized`] fired
1863 /// during `initialize_mls`) are not replayed to a sink installed later.
1864 ///
1865 /// # Re-install semantics
1866 ///
1867 /// Calling this method a second time *replaces* the previously installed
1868 /// sink and config — the old sink stops receiving records as soon as
1869 /// this call returns. The per-instance fallback secret is preserved so
1870 /// opaque identifiers stay stable across the swap when the new config
1871 /// does not carry its own `scrub_secret`.
1872 ///
1873 /// The per-tick diff snapshots (transport status, device capability,
1874 /// metrics cadence) are **rearmed** on every install: the next
1875 /// `process()` tick observes the current state as its baseline and emits
1876 /// no synthetic transitions for transports that were already available
1877 /// at install time. Apps that need the current transport statuses at
1878 /// install time should pull them explicitly via
1879 /// [`TransportManager::get_all_transport_statuses`].
1880 ///
1881 /// The escalation-trigger dedupe window (`ESCALATION_TRIGGER_DEDUPE_SECS`
1882 /// inside [`TransportManager`]) is **not** rearmed. A sink installed
1883 /// within that window after an escalation event fired to a previous
1884 /// sink may miss the next occurrence of the same reason until the
1885 /// window elapses. This is intentional: the dedupe is a property of
1886 /// the routing engine, not of the observer.
1887 ///
1888 /// # Lifecycle
1889 ///
1890 /// Installing a sink wires the structured routing-decision callback
1891 /// on the underlying [`TransportManager`]. The callback lives for the
1892 /// life of the protocol instance or until another
1893 /// `install_telemetry_sink` call replaces it — `start()` and `stop()`
1894 /// do not toggle it. This means apps can install a sink before
1895 /// `start()` and see routing records from the very first `send()`,
1896 /// and a `stop() → start()` cycle preserves the wiring so no
1897 /// re-install is needed. Conversely, apps that never install a sink
1898 /// pay no per-`send()` routing-emission overhead.
1899 ///
1900 /// # Errors
1901 ///
1902 /// Returns `Err` if the shared-state mutex is poisoned (indicating an
1903 /// earlier panic in another thread while holding the lock). On error no
1904 /// sink is installed, and the legacy emission paths are unaffected.
1905 ///
1906 /// [`TelemetryRecord::Protocol`]: crate::telemetry::TelemetryRecord::Protocol
1907 /// [`TelemetryRecord::Mls`]: crate::telemetry::TelemetryRecord::Mls
1908 // Crate-private since 0.26: the pipe is the one production sink, and the
1909 // engine's own tests are the remaining callers.
1910 #[cfg_attr(not(test), allow(dead_code))]
1911 pub(crate) fn install_telemetry_sink(
1912 &mut self,
1913 sink: Arc<dyn TelemetrySink>,
1914 config: TelemetryConfig,
1915 ) -> Result<()> {
1916 self.install_sink(
1917 sink,
1918 config,
1919 Arc::new(std::sync::atomic::AtomicBool::new(true)),
1920 )
1921 }
1922
1923 /// Installs `sink` behind `gate`, the flag every emit site reads first.
1924 fn install_sink(
1925 &mut self,
1926 sink: Arc<dyn TelemetrySink>,
1927 config: TelemetryConfig,
1928 gate: Arc<std::sync::atomic::AtomicBool>,
1929 ) -> Result<()> {
1930 // Forward the routing-diagnostic preference to the TransportManager
1931 // before wiring any callbacks so the very first routing decision
1932 // post-install already reflects the requested tier.
1933 self.transport_manager
1934 .set_routing_diagnostic(config.routing_diagnostic());
1935
1936 let ctx = TelemetryContext::with_gate(sink, config, self.telemetry_fallback_secret, gate);
1937 let mut state = lock_shared_state(&self.shared_state).map_err(|err| {
1938 error!(
1939 error = %err,
1940 "install_telemetry_sink: shared-state mutex poisoned; sink NOT installed",
1941 );
1942 err
1943 })?;
1944 state.telemetry = Some(ctx.clone());
1945 drop(state);
1946 self.telemetry = Some(ctx);
1947
1948 // Wire the structured routing-decision callback here so the
1949 // `TransportManager` pays the per-`send()` emission cost only when
1950 // a sink is actually installed, and so the wiring persists across
1951 // `stop() → start()` cycles (the callback reads `s.telemetry` on
1952 // every invocation and that field stays set). A subsequent
1953 // `install_telemetry_sink` replaces the closure with one capturing
1954 // the fresh `shared_routing` clone; both closures read the same
1955 // `SharedState::telemetry` slot, so even a concurrent in-flight
1956 // emission from the pre-replace closure would still resolve to
1957 // the currently-installed sink.
1958 let shared_routing = self.shared_state.clone();
1959 self.transport_manager
1960 .set_routing_decision_callback(Some(Arc::new(move |decision| {
1961 let s = shared_routing.lock_or_recover();
1962 if let Some(ctx) = s.telemetry.as_ref().filter(|ctx| ctx.enabled()) {
1963 let record = TelemetryRecord::Routing(Box::new(decision));
1964 // Dispatch is panic-isolated so a sink that panics
1965 // here cannot unwind through the live `MutexGuard`
1966 // and poison `SharedState`.
1967 dispatch_record(&ctx.sink, &record);
1968 }
1969 })));
1970
1971 // Rearm the per-tick diff snapshots so the first tick after install
1972 // reports honest transitions only. Without this, a sink installed
1973 // after start() would observe a synthetic `Unavailable → Available`
1974 // transition for every already-running transport on the next tick.
1975 let (statuses, available) = self.transport_manager.snapshot_status_and_available();
1976 self.transport_status_snapshot = statuses;
1977 let (battery_level, is_charging) =
1978 device_battery_from_available(self.transport_manager.current_transport(), &available);
1979 let relay_role = self.relay_role();
1980 self.device_capability_snapshot = Some(DeviceSnap::from_parts(
1981 battery_level,
1982 is_charging,
1983 relay_role,
1984 ));
1985 // Leave `last_metrics_emit_at` at None so the first tick after
1986 // install fires a fresh metrics snapshot — that is the bootstrap
1987 // payload a new sink actually wants (full counter state in one
1988 // record).
1989 self.last_metrics_emit_at = None;
1990
1991 Ok(())
1992 }
1993
1994 /// Enables telemetry: the SDK collects, batches and uploads accepted
1995 /// events to the ingest itself.
1996 ///
1997 /// `config` must carry the `api_key` and `app_id` the developer portal
1998 /// issued; [`TelemetryConfig::validate`] names any field that is refused,
1999 /// and the refusal is [`Error::TelemetryConfigInvalid`]. `host` names the
2000 /// platform stamped on every batch and the application state right now.
2001 ///
2002 /// Nothing waits on I/O here: the pipe's storage load and first upload
2003 /// run on its own thread. Batches are durable across a restart once a
2004 /// protocol-state store is attached, which on the mobile bindings
2005 /// happens inside `initialize_mls`; before that they are held in memory.
2006 /// Calling this again replaces the running pipe after its final flush.
2007 ///
2008 /// What leaves the device, when, and how to switch it off are in
2009 /// `docs/telemetry.md`.
2010 #[cfg(feature = "telemetry-pipe")]
2011 pub fn enable_telemetry(&mut self, config: TelemetryConfig, host: TelemetryHost) -> Result<()> {
2012 if let Err(field) = config.validate() {
2013 return Err(Error::TelemetryConfigInvalid(field.to_string()));
2014 }
2015 if let Some(previous) = self.detach_telemetry_pipe() {
2016 previous.stop(FINAL_FLUSH_BUDGET);
2017 }
2018 // Always started in memory, even when a store is already attached.
2019 // Adopting a backend loads the durable queue: an index read, up to
2020 // `MAX_PENDING_BATCHES` sealed record reads with a decrypt and a parse
2021 // each, a `list_keys` and the orphan sweep. Doing that here would run
2022 // it on the caller, holding `&mut self`, which on the mobile bindings
2023 // is the common order: `initialize_mls` has already attached storage
2024 // by the time an app enables telemetry. `attach_telemetry_storage`
2025 // hands the backend over instead, and the pipe's own thread adopts it.
2026 let device_id = self.telemetry_install_id();
2027 let pipe = TelemetryPipe::start(
2028 &config,
2029 PipeParts::production(host, Backend::Memory, device_id),
2030 )?;
2031 self.install_sink(pipe.sink(), config, pipe.enabled_flag())?;
2032 self.telemetry_pipe = Some(pipe);
2033 self.attach_telemetry_storage();
2034 Ok(())
2035 }
2036
2037 /// Disables telemetry: one final flush within [`FINAL_FLUSH_BUDGET`], then
2038 /// the uploader thread stops and every emit site goes back to its
2039 /// telemetry-off cost. Callers holding an outer lock should prefer
2040 /// [`Self::detach_telemetry_pipe`] and stop the pipe outside it.
2041 #[cfg(feature = "telemetry-pipe")]
2042 pub fn disable_telemetry(&mut self) {
2043 if let Some(pipe) = self.detach_telemetry_pipe() {
2044 pipe.stop(FINAL_FLUSH_BUDGET);
2045 }
2046 }
2047
2048 /// Detaches the running pipe without stopping it: the sink is
2049 /// uninstalled, the routing callback cleared, and the pipe handed back
2050 /// for the caller to stop once it holds no engine lock. Its final flush
2051 /// may block for up to [`FINAL_FLUSH_BUDGET`], which is why the two steps
2052 /// are separable.
2053 #[cfg(feature = "telemetry-pipe")]
2054 pub fn detach_telemetry_pipe(&mut self) -> Option<Arc<TelemetryPipe>> {
2055 let pipe = self.telemetry_pipe.take()?;
2056 self.telemetry = None;
2057 self.shared_state.lock_or_recover().telemetry = None;
2058 self.transport_manager.set_routing_decision_callback(None);
2059 self.transport_manager.set_routing_diagnostic(false);
2060 Some(pipe)
2061 }
2062
2063 /// The running telemetry pipe, if any.
2064 #[cfg(feature = "telemetry-pipe")]
2065 pub fn telemetry_pipe(&self) -> Option<Arc<TelemetryPipe>> {
2066 self.telemetry_pipe.clone()
2067 }
2068
2069 /// Stops or resumes collection without tearing the pipe down. Off, every
2070 /// emit site costs one atomic load; what is already queued still drains.
2071 #[cfg(feature = "telemetry-pipe")]
2072 pub fn set_telemetry_enabled(&self, enabled: bool) {
2073 if let Some(pipe) = &self.telemetry_pipe {
2074 pipe.set_enabled(enabled);
2075 }
2076 }
2077
2078 /// Asks the uploader to flush now. Returns immediately.
2079 #[cfg(feature = "telemetry-pipe")]
2080 pub fn flush_telemetry(&self) {
2081 if let Some(pipe) = &self.telemetry_pipe {
2082 pipe.flush();
2083 }
2084 }
2085
2086 /// Flushes and waits up to `budget` for the uploader to finish.
2087 #[cfg(feature = "telemetry-pipe")]
2088 pub fn flush_telemetry_blocking(&self, budget: std::time::Duration) -> bool {
2089 self.telemetry_pipe
2090 .as_ref()
2091 .is_some_and(|pipe| pipe.flush_blocking(budget))
2092 }
2093
2094 /// The pipe's counters, or `None` while telemetry is not enabled.
2095 #[cfg(feature = "telemetry-pipe")]
2096 pub fn telemetry_stats(&self) -> Option<TelemetryStats> {
2097 self.telemetry_pipe.as_ref().map(|pipe| pipe.stats())
2098 }
2099
2100 /// Closes the current telemetry session: summary, a blocking flush,
2101 /// then a fresh session id.
2102 #[cfg(feature = "telemetry-pipe")]
2103 pub fn end_telemetry_session(&self) {
2104 if let Some(pipe) = &self.telemetry_pipe {
2105 pipe.end_session();
2106 }
2107 }
2108
2109 /// Reports an application lifecycle transition to the pipe.
2110 #[cfg(feature = "telemetry-pipe")]
2111 pub fn notify_app_state(&self, state: AppState) {
2112 if let Some(pipe) = &self.telemetry_pipe {
2113 pipe.notify_app_state(state);
2114 }
2115 }
2116
2117 /// Hands the pipe the protocol-state store and the per-install id once
2118 /// `initialize_mls` has them. Adoption happens on the pipe's own thread.
2119 #[cfg(feature = "telemetry-pipe")]
2120 fn attach_telemetry_storage(&self) {
2121 let Some(pipe) = &self.telemetry_pipe else {
2122 return;
2123 };
2124 if let (Some(storage), Some(cipher)) =
2125 (&self.protocol_state_storage, &self.state_record_cipher)
2126 {
2127 pipe.attach_storage(storage.clone(), cipher.clone());
2128 }
2129 pipe.set_device_id(self.telemetry_install_id());
2130 }
2131
2132 /// Starts the protocol.
2133 ///
2134 /// # Returns
2135 ///
2136 /// Returns `Ok(())` if started successfully, `Err` if already started.
2137 pub fn start(&mut self) -> Result<()> {
2138 let state = lock_shared_state(&self.shared_state)?;
2139
2140 if state.state != ProtocolState::Stopped {
2141 return Err(Error::AlreadyStarted);
2142 }
2143
2144 // Start all transports
2145 drop(state);
2146 self.transport_manager.start()?;
2147
2148 // Apply the Nostr sealing kill switch here rather than at transport
2149 // construction: the transport is built by the bindings layer, so this
2150 // is the one place every caller — FFI, RN, and a pure-Rust embedder
2151 // that installed its own `NostrTransport` — passes through with the
2152 // config in hand. A no-op when Nostr is not installed.
2153 self.transport_manager
2154 .set_nostr_sealing_enabled(self.config.transport.nostr_sealing_enabled);
2155 self.transport_manager
2156 .set_nostr_cold_contact_enabled(self.config.transport.nostr_cold_contact_enabled);
2157 self.transport_manager.set_nostr_username_discovery_enabled(
2158 self.config.transport.nostr_username_discovery_enabled,
2159 );
2160
2161 // Wire DORS event callback so app receives dors_score_updated, dors_transport_selected,
2162 let shared = self.shared_state.clone();
2163 self.transport_manager
2164 .set_dors_event_callback(Some(Arc::new(move |event| {
2165 shared.lock_or_recover().emit_event(event);
2166 })));
2167
2168 // NOTE: the structured routing-decision callback is wired by
2169 // `install_telemetry_sink`, not here. Its lifetime is tied to sink
2170 // presence rather than protocol start/stop — see the docstring on
2171 // `install_telemetry_sink` for the rationale.
2172
2173 // Wire BLE fragment eviction callback so app receives FragmentAssemblyEvicted.
2174 if let Some(ble_arc) = self.transport_manager.get_transport(TransportType::BLE) {
2175 let shared = self.shared_state.clone();
2176 if let Some(ble) = ble_arc.as_any().downcast_ref::<BleTransport>() {
2177 ble.set_fragment_eviction_callback(Some(Arc::new(move |info| {
2178 shared
2179 .lock_or_recover()
2180 .emit_event(Event::fragment_assembly_evicted(
2181 info.message_id,
2182 info.completion_percent,
2183 "capacity".to_string(),
2184 ));
2185 })));
2186 }
2187 }
2188
2189 let mut state = lock_shared_state(&self.shared_state)?;
2190
2191 state.state = ProtocolState::Running;
2192 drop(state);
2193
2194 // Settle what restore could not recover, now that the event pipeline is
2195 // live. These ids were handed to the app by `send_message*` before the
2196 // process died; without this they would never resolve to anything.
2197 // Drained before the flush below so an app sees the failures first and
2198 // cannot mistake a restored send for the settlement of a lost one.
2199 self.drain_deferred_restore_settlements();
2200
2201 self.flush_restored_confirmed_pending_messages();
2202
2203 // Cold start: sessions restored from storage are already confirmed,
2204 // so no confirmation transition will fire for them and nothing else
2205 // would offer versions until the peer happened to be rediscovered.
2206 // Frames toward an unreachable peer queue in the outbox, which is the
2207 // ladder doing its job rather than a reason not to try.
2208 #[cfg(feature = "data")]
2209 {
2210 // The space list is derived from the records themselves rather
2211 // than from the space index, because the index is a cache: a
2212 // document written without ever being explicitly created leaves
2213 // records but no index entry, and those are precisely the
2214 // documents no peer has seen yet.
2215 // Group spaces appear in this list and are deliberately not
2216 // swept: `kick_data_sync` answers only for a space named after
2217 // a peer, so a group space falls through it. A cold-start sweep
2218 // for groups would mean an offer per member per group at every
2219 // launch, and what it would recover is narrow — a local commit
2220 // is already pushed to the roster when it happens, and the
2221 // per-member outbox carries it across a restart. Rediscovery
2222 // and joins reconcile the rest.
2223 for space in self.data_list_spaces().unwrap_or_default() {
2224 self.kick_data_sync(&space, "start");
2225 }
2226 }
2227
2228 self.kick_pending_session_reconciliation("start");
2229 self.process_welcome_retry_queue()?;
2230
2231 // Re-drive delivery of any outbox entries restored from a previous
2232 // session. They land here in the "stranded" state (not in the retry
2233 // queue, not awaiting an ACK), which flush_outbox_all already recovers:
2234 // it sends immediately where a transport is available and re-enqueues
2235 // the rest with backoff. Runs at start() rather than at restore time
2236 // because transports aren't up yet during initialize_mls.
2237 self.flush_outbox_all();
2238
2239 // Media transfers that died with the previous process cannot be
2240 // re-driven — only their descriptors survive (never chunk bytes).
2241 // Tell the app which transfers to re-initiate, now that the event
2242 // pipeline is live. The descriptors stay parked (and persisted)
2243 // until the resend consumes them or the restore TTL prunes them,
2244 // so an app that misses this signal gets it again next restart.
2245 //
2246 // Except the layer's own. An application cannot answer one of those:
2247 // the only surface that takes a caller-supplied `file_id` is
2248 // `send_media_with`, which forces the purpose to `None`, so a
2249 // well-behaved app re-supplying the bytes would deliver a document
2250 // snapshot to the peer's user as a downloaded file. They are dropped
2251 // instead, and nothing is stranded: the requester's fetch expires and
2252 // can be asked again, and a snapshot is re-offered by the next
2253 // anti-entropy exchange.
2254 let (recoverable, internal): (Vec<_>, Vec<_>) = self
2255 .restored_media_descriptors
2256 .values()
2257 .cloned()
2258 .partition(|descriptor| descriptor.data_purpose.is_none());
2259 for descriptor in &internal {
2260 debug!(
2261 file_id = %descriptor.file_id,
2262 "Dropping an interrupted document-layer transfer; the asking side will retry"
2263 );
2264 self.restored_media_descriptors.remove(&descriptor.file_id);
2265 self.remove_media_descriptor(&descriptor.file_id);
2266 }
2267 for descriptor in recoverable {
2268 self.emit_event(Event::media_resend_required(
2269 descriptor.file_id.clone(),
2270 descriptor.recipient.clone(),
2271 descriptor.file_name.clone(),
2272 descriptor.file_size,
2273 descriptor.app_id.clone(),
2274 ));
2275 }
2276
2277 Ok(())
2278 }
2279
2280 /// Stops the protocol gracefully.
2281 ///
2282 /// # Returns
2283 ///
2284 /// Returns `Ok(())` if stopped successfully, `Err` if not started.
2285 pub fn stop(&mut self) -> Result<()> {
2286 let state = lock_shared_state(&self.shared_state)?;
2287
2288 if state.state == ProtocolState::Stopped {
2289 return Ok(()); // Already stopped
2290 }
2291
2292 // Flush debounced Lamport clock before stopping so no ticks are lost.
2293 drop(state);
2294 self.flush_lamport_clock();
2295 // Same for the Nostr receive watermark: an un-flushed tail costs the
2296 // next launch a wider replay window.
2297 self.flush_nostr_watermark();
2298 // And the deduplicator's seen set: an un-flushed tail is a message
2299 // whose second copy the next launch fails to recognise.
2300 self.flush_dedup_seen();
2301 // Answer any lookup still in flight while the transport is still up to
2302 // take the cancellations. Nothing pumps relays or sweeps deadlines once
2303 // stopped, so a resolution left here is an event that never comes.
2304 self.flush_username_resolutions_for_stop();
2305
2306 // Clear event callbacks to release shared_state references.
2307 // NOTE: the routing-decision callback is deliberately NOT cleared
2308 // here. Its lifetime is sink-scoped (installed by
2309 // `install_telemetry_sink`, replaced by a subsequent install),
2310 // not protocol-running-scoped — so a `stop() → start()` cycle
2311 // preserves the wiring without requiring the app to re-install.
2312 self.transport_manager.set_dors_event_callback(None);
2313 if let Some(ble_arc) = self.transport_manager.get_transport(TransportType::BLE) {
2314 if let Some(ble) = ble_arc.as_any().downcast_ref::<BleTransport>() {
2315 ble.set_fragment_eviction_callback(None);
2316 }
2317 }
2318
2319 self.transport_manager.stop()?;
2320 let mut state = lock_shared_state(&self.shared_state)?;
2321
2322 state.state = ProtocolState::Stopped;
2323
2324 Ok(())
2325 }
2326
2327 /// Pauses the protocol (for background mode).
2328 pub fn pause(&mut self) -> Result<()> {
2329 self.flush_lamport_clock();
2330 self.flush_nostr_watermark();
2331 let mut state = lock_shared_state(&self.shared_state)?;
2332
2333 if state.state != ProtocolState::Running {
2334 return Err(Error::NotStarted);
2335 }
2336
2337 state.state = ProtocolState::Paused;
2338 Ok(())
2339 }
2340
2341 /// Resumes the protocol from pause.
2342 pub fn resume(&mut self) -> Result<()> {
2343 {
2344 let mut state = lock_shared_state(&self.shared_state)?;
2345
2346 if state.state != ProtocolState::Paused {
2347 return Err(Error::InvalidConfiguration(
2348 "Protocol is not paused".to_string(),
2349 ));
2350 }
2351
2352 state.state = ProtocolState::Running;
2353 }
2354
2355 // A pause is the other edge back into a live event pipeline, so it owes
2356 // the same drain `start()` does. `settle_restored_message_failure` parks
2357 // anything it produces while the protocol is not `Running`, and
2358 // `update_retry_config` reaches it at runtime: shortening
2359 // `pending_message_max_lifetime_ms` in the background expires queued
2360 // messages and parks their terminal `message_failed`. Without this the
2361 // app would hold those ids until a `start()` that may never come again.
2362 self.drain_deferred_restore_settlements();
2363 Ok(())
2364 }
2365
2366 /// Emits every terminal settlement parked while the event pipeline was not
2367 /// live, and reports the count of any the cap dropped.
2368 ///
2369 /// Called from both edges into `Running` ([`Self::start`] and
2370 /// [`Self::resume`]) so a parked settlement has no state to be stranded in.
2371 /// Idempotent: draining an empty queue emits nothing.
2372 fn drain_deferred_restore_settlements(&mut self) {
2373 for (event, telemetry) in std::mem::take(&mut self.deferred_restore_settlements) {
2374 self.emit_settlement(event, telemetry);
2375 }
2376 let suppressed = std::mem::take(&mut self.suppressed_restore_settlements);
2377 if suppressed > 0 {
2378 warn!(
2379 suppressed,
2380 cap = MAX_DEFERRED_RESTORE_SETTLEMENTS,
2381 "Restore produced more terminal settlements than any legitimate run can produce; \
2382 the ids past the cap were not reported individually"
2383 );
2384 }
2385 }
2386
2387 /// Forget that a peer replicates documents.
2388 ///
2389 /// The capability and the offer window are two halves of one fact, and
2390 /// they have to be dropped together: a window left behind outlives the
2391 /// capability and then suppresses the first offer to the same peer once
2392 /// it is relearned, which is a document that silently does not sync.
2393 pub(crate) fn forget_data_sync_peer(&mut self, peer: &str) {
2394 self.peer_data_sync.remove(peer);
2395 self.peer_data_group.remove(peer);
2396 self.peer_data_group_attested.remove(peer);
2397 self.peer_data_media.remove(peer);
2398 self.peer_data_tombstones.remove(peer);
2399 self.peer_data_interest.remove(peer);
2400 self.peer_data_group_blob.remove(peer);
2401 self.peer_data_group_blob_attested.remove(peer);
2402 self.peer_data_custody.remove(peer);
2403 // A peer we have stopped replicating with cannot answer anything we
2404 // asked them for, so the questions go too. Left behind they would
2405 // hold slots against the fetch bound until they timed out.
2406 #[cfg(feature = "data")]
2407 self.forget_attachment_state(peer);
2408 #[cfg(feature = "data")]
2409 self.forget_data_sync_offer_windows(peer);
2410 }
2411
2412 /// [`Self::forget_data_sync_peer`] for every peer at once.
2413 pub(crate) fn forget_every_data_sync_peer(&mut self) {
2414 self.peer_data_sync.clear();
2415 self.peer_data_group.clear();
2416 self.peer_data_group_attested.clear();
2417 self.peer_data_media.clear();
2418 self.peer_data_tombstones.clear();
2419 self.peer_data_interest.clear();
2420 self.peer_data_group_blob.clear();
2421 self.peer_data_group_blob_attested.clear();
2422 // Reported, not merely dropped, exactly as the single-peer road
2423 // reports. Nothing the application did reaches this one: the bound
2424 // on remembered peers is hit, a stranger's key package forgets every
2425 // capability at once, and a fetch outstanding at that moment ends
2426 // for a reason its asker has no way to see. Dropping the record
2427 // silently also forecloses the expiry that would have reported it
2428 // later, so the spinner this event exists to end never ends at all.
2429 #[cfg(feature = "data")]
2430 self.end_every_pending_fetch();
2431 self.blob_request_windows.clear();
2432 #[cfg(feature = "data")]
2433 self.last_data_sync_offer.clear();
2434 }
2435
2436 /// Called when a new neighbor is discovered.
2437 ///
2438 /// When auto key exchange is enabled, this method sends our key package
2439 /// to the newly discovered peer to enable encrypted communication.
2440 ///
2441 /// # Arguments
2442 ///
2443 /// * `peer_id` - The ID of the discovered peer
2444 pub fn on_neighbor_discovered(&mut self, peer_id: &str) {
2445 self.on_neighbor_discovered_via(peer_id, None);
2446 }
2447
2448 /// [`Self::on_neighbor_discovered`] with an optional transport override
2449 /// for the outbox flush (see [`Self::flush_outbox_for_peer_via`]).
2450 /// `unpark_via` is `Some(Internet)` when the relay presence-online edge
2451 /// re-enters discovery through its rescue branch: a message whose
2452 /// forced-internet re-drive just failed sits in the retry queue with no
2453 /// pending ACK, so this inner flush would pick it up again — and
2454 /// without the override DORS could route it into the mesh void with the
2455 /// park counter already cleared, past re-park's reach.
2456 fn on_neighbor_discovered_via(&mut self, peer_id: &str, unpark_via: Option<TransportType>) {
2457 // Don't track ourselves
2458 if peer_id == self.local_id {
2459 return;
2460 }
2461
2462 // Don't track or auto-exchange keys with blocked users
2463 if self.is_user_blocked(peer_id) {
2464 debug!(peer_id = %peer_id, "Ignoring neighbor discovery for blocked user");
2465 return;
2466 }
2467
2468 // Track discovered peers for service discovery and routing. Existing
2469 // peers get their last-seen refreshed; a new peer at capacity evicts
2470 // the least-recently-seen entry so a genuinely present neighbor is
2471 // never locked out by stale message-path senders (issue #140).
2472 if let Some(last_seen) = self.known_peers.get_mut(peer_id) {
2473 *last_seen = Instant::now();
2474 } else {
2475 if self.known_peers.len() >= MAX_KNOWN_PEERS {
2476 if let Some(victim) = self
2477 .known_peers
2478 .iter()
2479 .min_by_key(|(_, seen)| **seen)
2480 .map(|(id, _)| id.clone())
2481 {
2482 debug!(peer_id = %peer_id, evicted = %victim, cap = MAX_KNOWN_PEERS, "Known peers at capacity, evicting least-recently-seen");
2483 self.evict_known_peer(&victim);
2484 }
2485 }
2486 self.known_peers.insert(peer_id.to_string(), Instant::now());
2487 }
2488
2489 // Flush any pending outbox messages destined for this peer
2490 self.flush_outbox_for_peer_via(peer_id, unpark_via);
2491
2492 // Held frames for this neighbour, or to try through it
2493 // (`docs/spec/custody.md`, "Redelivery").
2494 self.redeliver_custody_to(peer_id);
2495
2496 // A Welcome that stalled or expired while this peer was unreachable now
2497 // has a fresh delivery opportunity over the carrier that surfaced this
2498 // peer — re-arm it. No-op when there is no pending Welcome for the peer.
2499 self.rearm_welcome_for_peer(peer_id, "peer_rediscovered");
2500
2501 // Offer document versions while the peer is in front of us. This has
2502 // to sit above the key-package guard below, which returns early for
2503 // any peer we have already exchanged with — that is to say, for
2504 // exactly the established peers whose documents drifted apart during
2505 // the partition this discovery just ended.
2506 #[cfg(feature = "data")]
2507 self.kick_data_sync(peer_id, "peer_rediscovered");
2508
2509 // The same reconciliation for every group space shared with this
2510 // peer. A group space cannot ride the 1:1 trigger above: two
2511 // members of a group need not have a session with each other at
2512 // all, so for them this discovery is the only moment anything knows
2513 // they are both reachable.
2514 #[cfg(feature = "data")]
2515 self.kick_shared_group_data_sync(peer_id, "peer_rediscovered");
2516
2517 // Only send key package if encryption is enabled and auto key exchange is on
2518 if !self.config.encryption.enabled || !self.config.encryption.auto_key_exchange {
2519 return;
2520 }
2521
2522 // The first push is discovery's. A later discovery re-arms it only
2523 // when that push produced no session, at most once per interval.
2524 if self.key_package_sent_to.contains_key(peer_id) {
2525 self.rearm_key_package_for_peer(peer_id);
2526 return;
2527 }
2528
2529 // Only if MLS is initialized
2530 if self.mls_manager.is_none() {
2531 return;
2532 }
2533
2534 if let Err(e) = self.send_key_package_to(peer_id, false) {
2535 warn!(error = %e, peer_id = %peer_id, "Failed to send key package on discovery");
2536 }
2537 }
2538
2539 /// Immediately attempts to send all pending outbox messages destined
2540 /// for a specific peer, bypassing backoff timers. Called on per-peer
2541 /// reachability edges (discovery, relay presence-online) to flush
2542 /// messages that were queued while the peer was unreachable.
2543 ///
2544 /// `unpark_via` is an optional transport override for the re-driven
2545 /// sends: `Some(Internet)` on the relay presence-online edge
2546 /// (`on_peer_presence`, including its rescue branch's re-entry through
2547 /// `on_neighbor_discovered_via`): the reachability
2548 /// proof is relay-scoped, so the re-drive must go out over the internet
2549 /// transport — DORS could otherwise route it back into the mesh, where
2550 /// the send locally succeeds against a peer that is not there,
2551 /// re-registering an unanswerable ACK that re-strands the message (and,
2552 /// with the platform bridge unwatching the peer on the online answer,
2553 /// no further presence edge would arrive to save it). Pinned media
2554 /// transports still win over the override.
2555 fn flush_outbox_for_peer_via(&mut self, peer_id: &str, unpark_via: Option<TransportType>) {
2556 // Probe ACKs first: with a live park counter, the peer's DMs may be
2557 // mid-probe — awaiting a probe ACK that may never arrive (never, over
2558 // a mesh carrier) — and the awaiting-ACK filter below would skip
2559 // exactly the messages this edge exists to re-drive.
2560 self.cancel_probe_state_for_parked_peer(peer_id);
2561 // A per-peer reachability edge: the escalating unreachable-probe
2562 // interval starts over (mirrors rearm_welcome_for_peer resetting
2563 // unreachable_parks). Snapshotted, not dropped: an edge whose
2564 // re-drives all fail didn't actually take, and restores it below.
2565 let prior_parks = self.dm_unreachable_parks.remove(peer_id);
2566
2567 // Collect matching messages from outbox + media_outbox
2568 let mut to_send: Vec<(Message, u32)> = Vec::new();
2569
2570 for entry in self.outbox.values() {
2571 if entry.message.recipient.as_str() == peer_id
2572 && !self.ack_manager.is_waiting_for_ack(&entry.message.id)
2573 {
2574 to_send.push((entry.message.clone(), entry.attempt_count));
2575 }
2576 }
2577 for entry in self.media_outbox.values() {
2578 if entry.message.recipient.as_str() == peer_id
2579 && !self.ack_manager.is_waiting_for_ack(&entry.message.id)
2580 {
2581 to_send.push((entry.message.clone(), entry.attempt_count));
2582 }
2583 }
2584
2585 if to_send.is_empty() {
2586 return;
2587 }
2588
2589 let count = to_send.len().min(crate::constants::FLUSH_BATCH_LIMIT);
2590 debug!(peer_id = %peer_id, count = count, "Flushing outbox for discovered peer");
2591
2592 let mut iter = to_send.into_iter();
2593 let mut parked_dm_seen = false;
2594 let mut parked_dm_redriven = false;
2595
2596 // Only process up to FLUSH_BATCH_LIMIT messages per edge.
2597 for (message, attempt_count) in iter.by_ref().take(crate::constants::FLUSH_BATCH_LIMIT) {
2598 let parkable = self.is_parkable_plain_dm(&message.id);
2599 // Remove from retry queue since we're sending immediately
2600 self.retry_queue.remove(&message.id.as_str());
2601 let sent = self.try_flush_send_via(message, attempt_count, unpark_via);
2602 if parkable {
2603 parked_dm_seen = true;
2604 parked_dm_redriven |= sent.is_some();
2605 }
2606 }
2607
2608 // Re-enqueue the overflow (mirrors flush_outbox_all): the probe
2609 // cancel above may have stripped a parked message's retry entry, so
2610 // "still holds its backoff timer" cannot be assumed past the batch
2611 // limit. Enqueue dedupes by id — entries still scheduled keep their
2612 // existing timer, canceled ones get a fresh backoff slot instead of
2613 // stranding edge-only with the park counter cleared.
2614 for (message, attempt_count) in iter {
2615 parked_dm_seen = parked_dm_seen || self.is_parkable_plain_dm(&message.id);
2616 let _ = self.retry_queue.enqueue(message, attempt_count);
2617 }
2618
2619 // An edge that re-drove no parkable DM at all didn't actually take:
2620 // the DMs now sit in the retry queue, whose later sends are
2621 // DORS-routed (no transport override there), and a send that succeeds
2622 // locally without reaching the peer would re-register an ACK nothing
2623 // will answer. Restore the counter so exhaustion stays within
2624 // `try_repark_exhausted_dm`'s reach instead of settling terminally
2625 // with the counter cleared.
2626 //
2627 // Restore granularity is per-peer, matching the counter: one
2628 // successful re-drive clears it even when a sibling's send failed on
2629 // the same edge, leaving that sibling on DORS-routed backoff exposed
2630 // to terminal exhaustion. Accepted gap — it requires one transport
2631 // to both succeed and fail within a single flush loop.
2632 if let Some(parks) = prior_parks {
2633 if parked_dm_seen && !parked_dm_redriven {
2634 self.dm_unreachable_parks.insert(peer_id.to_string(), parks);
2635 }
2636 }
2637 }
2638
2639 /// Immediately attempts to send all pending outbox messages across all peers.
2640 ///
2641 /// Called when a transport becomes available (e.g. internet reconnects) to
2642 /// flush all queued messages, bypassing backoff timers.
2643 pub fn flush_outbox_all(&mut self) {
2644 // Acknowledgements we owe first: they are what stop peers re-sending
2645 // to us, and they have no outbox entry for the walk below to find.
2646 self.flush_unrouted_acks();
2647 // Probe ACKs first (see `flush_outbox_for_peer_via`): mid-probe DMs
2648 // are awaiting unanswerable mesh ACKs, and the awaiting-ACK guard
2649 // below would strand them past this edge with their counters
2650 // cleared. Canceled entries surface through the stranded-outbox
2651 // collection.
2652 let parked_peers: Vec<String> = self.dm_unreachable_parks.keys().cloned().collect();
2653 for peer_id in parked_peers {
2654 self.cancel_probe_state_for_parked_peer(&peer_id);
2655 }
2656 // A carrier-level reachability edge (reconnect / start): every
2657 // escalating unreachable-probe interval starts over. Snapshotted,
2658 // not dropped: a peer whose re-drives all fail gets its counter
2659 // restored below (see `flush_outbox_for_peer_via`).
2660 let prior_parks = std::mem::take(&mut self.dm_unreachable_parks);
2661
2662 // Drain all entries from the retry queue (ignores timing)
2663 let retry_entries = self.retry_queue.drain_all();
2664
2665 // Also collect outbox entries NOT in the retry queue (stranded after
2666 // previous max_retries rejection)
2667 let mut all_messages: Vec<(Message, u32)> = retry_entries
2668 .into_iter()
2669 .map(|e| (e.message, e.retry_count))
2670 .collect();
2671
2672 // Add outbox entries that weren't in the retry queue (stranded) AND
2673 // aren't already waiting for an ACK (successfully sent, just awaiting
2674 // confirmation). Without the ACK check, a prior flush that succeeded
2675 // would cause these messages to be re-sent unnecessarily.
2676 let retry_ids: std::collections::HashSet<String> =
2677 all_messages.iter().map(|(m, _)| m.id.as_str()).collect();
2678
2679 for entry in self.outbox.values() {
2680 if !retry_ids.contains(&entry.message.id.as_str())
2681 && !self.ack_manager.is_waiting_for_ack(&entry.message.id)
2682 {
2683 all_messages.push((entry.message.clone(), entry.attempt_count));
2684 }
2685 }
2686 for entry in self.media_outbox.values() {
2687 if !retry_ids.contains(&entry.message.id.as_str())
2688 && !self.ack_manager.is_waiting_for_ack(&entry.message.id)
2689 {
2690 all_messages.push((entry.message.clone(), entry.attempt_count));
2691 }
2692 }
2693
2694 // Opt-in (`edge_driven_unreachable_dm`): on the start/reconnect re-drive,
2695 // leave durably-failing messages (first sent longer ago than
2696 // OUTBOX_DURABLE_UNREACHABLE_AGE_SECS, still undelivered) edge-driven
2697 // rather than re-driving them, so a restart does not re-ramp a large
2698 // dead-peer backlog. They rest in the outbox and flush the instant their
2699 // peer next proves reachable. Default-off preserves the documented
2700 // "reconnect re-drives, ignoring timing" behaviour for every existing
2701 // app. drain_all() already removed these from the retry queue and the
2702 // outbox entry is untouched, so "not collected" == "resting edge-driven".
2703 if self.config.reliability.retry.edge_driven_unreachable_dm {
2704 let now = Utc::now();
2705 let durable_age = chrono::Duration::seconds(
2706 crate::protocol::types::OUTBOX_DURABLE_UNREACHABLE_AGE_SECS,
2707 );
2708 all_messages.retain(|(m, _)| {
2709 match self
2710 .outbox
2711 .get(&m.id)
2712 .or_else(|| self.media_outbox.get(&m.id))
2713 .map(|e| e.first_sent_at)
2714 {
2715 Some(first_sent) => now.signed_duration_since(first_sent) < durable_age,
2716 None => true,
2717 }
2718 });
2719 }
2720
2721 if all_messages.is_empty() {
2722 return;
2723 }
2724
2725 let count = all_messages.len().min(crate::constants::FLUSH_BATCH_LIMIT);
2726 debug!(
2727 count = count,
2728 total = all_messages.len(),
2729 "Flushing all outbox messages"
2730 );
2731
2732 let mut iter = all_messages.into_iter();
2733 let mut parked_dm_seen: std::collections::HashSet<String> =
2734 std::collections::HashSet::new();
2735 let mut parked_dm_redriven: std::collections::HashSet<String> =
2736 std::collections::HashSet::new();
2737
2738 for (message, attempt_count) in iter.by_ref().take(crate::constants::FLUSH_BATCH_LIMIT) {
2739 let recipient = message.recipient.as_str().to_string();
2740 let parkable =
2741 prior_parks.contains_key(&recipient) && self.is_parkable_plain_dm(&message.id);
2742 let sent_via = self.try_flush_send(message, attempt_count);
2743 if parkable {
2744 // Unlike the per-peer edges (discovery carries mesh-scoped
2745 // proof, presence-online forces the answering carrier), this
2746 // carrier-level edge re-drives via DORS with no per-peer
2747 // reachability proof: a mesh-routed send "locally succeeds"
2748 // into the mesh whether or not the peer is there, so only a
2749 // send over a carrier that answers verdicts counts as
2750 // re-driven for counter-clearing — that is the send whose
2751 // failure would say something. A mesh-routed success keeps
2752 // the counter restore-eligible; if its ACK genuinely
2753 // arrives, delivery prunes the counter.
2754 if sent_via.and_then(GatewayCarrier::from_transport).is_some() {
2755 parked_dm_redriven.insert(recipient);
2756 } else {
2757 parked_dm_seen.insert(recipient);
2758 }
2759 }
2760 }
2761
2762 // Re-enqueue any messages beyond the batch limit so they aren't lost
2763 for (message, attempt_count) in iter {
2764 let recipient = message.recipient.as_str();
2765 if prior_parks.contains_key(recipient) && self.is_parkable_plain_dm(&message.id) {
2766 parked_dm_seen.insert(recipient.to_string());
2767 }
2768 let _ = self.retry_queue.enqueue(message, attempt_count);
2769 }
2770
2771 // Restore the counter of every parked peer none of whose DMs were
2772 // re-driven on this edge (see `flush_outbox_for_peer_via`): their
2773 // retry-queue sends are DORS-routed, and a send that succeeds locally
2774 // without reaching the peer must leave exhaustion re-parkable rather
2775 // than terminal.
2776 for (peer_id, parks) in prior_parks {
2777 if parked_dm_seen.contains(&peer_id) && !parked_dm_redriven.contains(&peer_id) {
2778 self.dm_unreachable_parks.insert(peer_id, parks);
2779 }
2780 }
2781 }
2782
2783 /// Records what the attached gateway daemon says it can do.
2784 ///
2785 /// Wholesale replace, like the relay's: each attach describes the gateway
2786 /// actually connected now. The bridge calls this **before** it reports the
2787 /// carrier available, which is the contract's own ordering — a device must
2788 /// not drain into a gateway whose features it has not been told — and it
2789 /// clears the set when the carrier drops.
2790 ///
2791 /// The list is wire-supplied, so it is bounded exactly as the relay's is,
2792 /// which is what the contract points at rather than inventing a second
2793 /// pair of numbers: at most 64 tokens of at most 128 bytes each, the same
2794 /// `MAX_RELAY_CAPABILITIES` and `MAX_RELAY_CAPABILITY_TOKEN_BYTES` the
2795 /// relay's advertisement is bounded by. Oversized tokens are
2796 /// dropped *before* the count is applied, so padding cannot evict real
2797 /// ones.
2798 ///
2799 /// Nothing here gates a security decision. Tokens gate features, and an
2800 /// attacker who can set them is already the gateway.
2801 pub fn set_gateway_capabilities(&mut self, capabilities: Vec<String>) {
2802 let caps: HashSet<String> = capabilities
2803 .into_iter()
2804 .filter(|c| !c.is_empty() && c.len() <= MAX_RELAY_CAPABILITY_TOKEN_BYTES)
2805 .take(MAX_RELAY_CAPABILITIES)
2806 .collect();
2807 debug!(count = caps.len(), "Gateway capabilities updated");
2808 self.gateway_capabilities = caps;
2809 }
2810
2811 /// Forgets the attached gateway's advertisement, on carrier drop.
2812 pub fn clear_gateway_capabilities(&mut self) {
2813 if !self.gateway_capabilities.is_empty() {
2814 debug!("Gateway capabilities cleared on carrier drop");
2815 self.gateway_capabilities.clear();
2816 }
2817 }
2818
2819 /// The attached gateway's advertised capability tokens, sorted so the
2820 /// order a caller sees does not depend on hashing.
2821 pub fn gateway_capabilities(&self) -> Vec<String> {
2822 let mut tokens: Vec<String> = self.gateway_capabilities.iter().cloned().collect();
2823 tokens.sort();
2824 tokens
2825 }
2826
2827 /// Peers the platform layer should watch via a gateway `CheckPresence`
2828 /// query: every peer with an undelivered or session-unproven MLS welcome
2829 /// ([`Self::welcome_pending_peers`]) plus every recipient of an outbox
2830 /// message not currently awaiting an ACK — which includes DMs parked on a
2831 /// `recipient_unreachable` verdict. A gateway's presence-online answer
2832 /// lands in [`Self::on_peer_presence_via`], whose
2833 /// `flush_outbox_for_peer_via` is what re-drives (un-parks) those
2834 /// messages, so the SDK — not the app — owns the "watch my DeliveryError
2835 /// recipients" duty. Bounded by the outbox cap (500 entries).
2836 ///
2837 /// Carrier-agnostic on purpose: the set is "who this device is waiting to
2838 /// hear about", which does not depend on who is asked. Every attached
2839 /// gateway watches the same peers.
2840 pub fn presence_watch_peers(&self) -> Vec<String> {
2841 let mut peers = self.welcome_pending_peers();
2842 let mut seen: std::collections::HashSet<String> = peers.iter().cloned().collect();
2843 for entry in self.outbox.values() {
2844 if self.ack_manager.is_waiting_for_ack(&entry.message.id) {
2845 continue;
2846 }
2847 let recipient = entry.message.recipient.as_str();
2848 if seen.insert(recipient.to_string()) {
2849 peers.push(recipient.to_string());
2850 }
2851 }
2852 peers
2853 }
2854
2855 /// Attempts to send a single message as part of a flush operation.
2856 ///
2857 /// Ensures the outbox entry exists before sending. A message reaching here
2858 /// normally has one: every path that gives a message up retires its retry
2859 /// entry with it (`retire_undeliverable_message`), so an eviction no longer
2860 /// leaves one behind to resurrect here, and re-creating a settled message
2861 /// would evict a live one at capacity. On success,
2862 /// registers ACK tracking and updates the outbox entry. On failure,
2863 /// re-enqueues the message to the retry queue with its current attempt count
2864 /// so backoff resumes. Returns the transport the send went out over, or
2865 /// `None` when it failed (or, defensively, when the transport could not
2866 /// be attributed — the park restore logic treats both as not-redriven,
2867 /// the safe direction; in practice a successful send always has one).
2868 fn try_flush_send(&mut self, message: Message, attempt_count: u32) -> Option<TransportType> {
2869 self.try_flush_send_via(message, attempt_count, None)
2870 }
2871
2872 /// [`Self::try_flush_send`] with an optional transport override (see
2873 /// [`Self::flush_outbox_for_peer_via`]). A pinned media transport takes
2874 /// precedence over the override; without either, DORS picks.
2875 fn try_flush_send_via(
2876 &mut self,
2877 mut message: Message,
2878 attempt_count: u32,
2879 transport_override: Option<TransportType>,
2880 ) -> Option<TransportType> {
2881 // No slot to track it: leave the send to the retry queue, which waits
2882 // for one, instead of transmitting a frame nobody tracks.
2883 if message.requires_ack && !self.ack_manager.has_capacity_for(&message.id) {
2884 self.ensure_outbox_entry(&message);
2885 let _ = self.retry_queue.enqueue(message, attempt_count);
2886 return None;
2887 }
2888 // A full peer window holds a frame that has not gone out yet; the
2889 // window flush sends it when an acknowledgement frees a slot.
2890 if !self.peer_window_admits(&message) {
2891 self.hold_for_peer_window(&message);
2892 return None;
2893 }
2894 self.ensure_outbox_entry(&message);
2895 // Tier 2: re-seal against the peer's current session before flushing, so
2896 // a post-re-key epoch change no longer leaves the bytes undecryptable.
2897 // No-op for media/plaintext/unconfirmed sessions.
2898 self.reseal_for_resend_in_place(&mut message);
2899 let forced_transport = self
2900 .pinned_media_transport_for_message(&message.id)
2901 .or(transport_override);
2902 let send_result = if let Some(transport) = forced_transport {
2903 self.transport_manager
2904 .send_via_transport(&message, transport)
2905 } else {
2906 self.transport_manager.send(&message)
2907 };
2908 let current_transport =
2909 forced_transport.or_else(|| self.transport_manager.current_transport());
2910
2911 match send_result {
2912 Ok(()) => {
2913 if let Err(e) = self.ensure_ack_registration(&message) {
2914 warn!(message_id = %message.id, error = %e, "ACK registration failed during flush");
2915 }
2916 self.mark_message_sent(
2917 &message,
2918 current_transport,
2919 Some(attempt_count.saturating_add(1)),
2920 );
2921 self.offer_resend_to_mesh(&message, true);
2922 debug!(message_id = %message.id, "Flush send succeeded");
2923 current_transport
2924 }
2925 Err(e) => {
2926 self.offer_resend_to_mesh(&message, false);
2927 let _ = self.retry_queue.enqueue(message.clone(), attempt_count);
2928 debug!(message_id = %message.id, error = %e, "Flush send failed, re-enqueued");
2929 None
2930 }
2931 }
2932 }
2933
2934 /// Sends held messages into each peer's free window slots, oldest first,
2935 /// at most a flush batch per peer per tick.
2936 ///
2937 /// Runs every tick rather than from the acknowledgement handler alone,
2938 /// because a slot is also freed by give-up, expiry and eviction, and a
2939 /// peer whose mesh link went away stops being governed by the window, so
2940 /// everything held for it goes to transport selection.
2941 fn flush_peer_windows(&mut self) {
2942 if self.window_held.is_empty() {
2943 return;
2944 }
2945 let peers: Vec<String> = self.window_held.keys().cloned().collect();
2946 for peer in peers {
2947 let mut budget = crate::constants::FLUSH_BATCH_LIMIT;
2948 while budget > 0 {
2949 let Some(message_id) = self
2950 .window_held
2951 .get(&peer)
2952 .and_then(|held| held.front().cloned())
2953 else {
2954 break;
2955 };
2956 let Some((message, attempt_count)) = self
2957 .outbox
2958 .get(&message_id)
2959 .map(|entry| (entry.message.clone(), entry.attempt_count))
2960 else {
2961 // Settled, expired or evicted while held.
2962 if let Some(held) = self.window_held.get_mut(&peer) {
2963 held.pop_front();
2964 }
2965 continue;
2966 };
2967 if !self.peer_window_admits(&message) {
2968 break;
2969 }
2970 if let Some(held) = self.window_held.get_mut(&peer) {
2971 held.pop_front();
2972 }
2973 budget -= 1;
2974 let _ = self.try_flush_send_via(message, attempt_count, None);
2975 }
2976 if self
2977 .window_held
2978 .get(&peer)
2979 .is_some_and(|held| held.is_empty())
2980 {
2981 self.window_held.remove(&peer);
2982 }
2983 }
2984 }
2985
2986 /// Called when a neighbor is lost.
2987 ///
2988 /// Cleans up tracking state for the lost peer.
2989 ///
2990 /// # Arguments
2991 ///
2992 /// * `peer_id` - The ID of the lost peer
2993 pub fn on_neighbor_lost(&mut self, peer_id: &str) {
2994 self.evict_known_peer(peer_id);
2995 }
2996
2997 /// Drops a peer from discovery tracking. Shared by explicit neighbor
2998 /// loss, the TTL sweep, and least-recently-seen eviction at capacity —
2999 /// all three mean "treat this peer as gone until re-seen", so all three
3000 /// also clear the key-package marker, letting a re-appearing peer
3001 /// receive a fresh key package exactly as on a BLE reconnect.
3002 fn evict_known_peer(&mut self, peer_id: &str) {
3003 self.key_package_sent_to.remove(peer_id);
3004 self.known_peers.remove(peer_id);
3005 // Release the forwarding allowance held for this link. A peer that
3006 // reconnects starts with a full allowance, which is correct: the limit
3007 // exists to pace a live link, not to punish one that came back.
3008 self.mesh_relay.forget_peer(peer_id);
3009 }
3010
3011 /// Tears down the 1:1 session with `peer_id` and rebuilds it, advancing
3012 /// post-compromise security.
3013 ///
3014 /// # Why an application has to ask
3015 ///
3016 /// Post-compromise security arrives when a commit rotates a member's leaf
3017 /// in the ratchet tree. This engine originates one on a re-key, and until
3018 /// now the only thing that drove a re-key was an **epoch desync**: a
3019 /// healthy pair that never forked never rotated, so the window an attacker
3020 /// holding old key material keeps open was bounded by nothing.
3021 ///
3022 /// That is felt hardest against a leaf node, which never commits at all, so
3023 /// every rotation in such a pair is the phone's to originate. It is not
3024 /// exclusive to one: two phones that simply never desync are in the same
3025 /// position.
3026 ///
3027 /// The cadence is the application's because this engine cannot pick one.
3028 /// A rotation costs a teardown, a key-package exchange and a re-establish,
3029 /// and what that is worth depends on the deployment: a door lock on mains
3030 /// power and a phone on a metered link do not want the same interval, and
3031 /// nothing on the wire says which one is on the other end.
3032 ///
3033 /// # What the peer sees
3034 ///
3035 /// Exactly what a desync-driven re-key sends, because it is the same code:
3036 /// a `__MLS_KEY_PKG__` frame carrying `session_reset`. A peer running this
3037 /// engine discards its stale session and converges in one round trip; a
3038 /// leaf node discards its session, mints a fresh key package and re-pairs.
3039 /// Queued plaintext survives, because it is sealed at flush time against
3040 /// whatever session is current then.
3041 ///
3042 /// # Rate limit
3043 ///
3044 /// Shared with the desync path, one re-key per peer per window. A caller
3045 /// looping on this cannot do to a pair what that bound exists to stop an
3046 /// attacker doing.
3047 ///
3048 /// # Returns
3049 ///
3050 /// * `Ok(true)` - the reset was advertised and the session torn down.
3051 /// * `Ok(false)` - the rate-limit window has not lapsed; nothing was done,
3052 /// and a later call succeeds. Not an error: a caller on a fixed schedule
3053 /// that briefly runs faster than the floor is behaving correctly.
3054 ///
3055 /// # A rotation that fails changes nothing
3056 ///
3057 /// The reset is advertised **before** the local session is torn down, so
3058 /// an `Err` leaves the pair exactly as it was: the session is still there,
3059 /// still usable, and the rate-limit window is not spent. Rotate while the
3060 /// peer is reachable, and treat a failure as "try again later" rather than
3061 /// as a session to rebuild.
3062 ///
3063 /// The order matters most against a leaf node, which speaks only when
3064 /// spoken to. Tearing down first and failing to send would leave this side
3065 /// with no session, the device holding one it will never be told to drop,
3066 /// and nothing on either side that re-opens the exchange.
3067 ///
3068 /// # Errors
3069 ///
3070 /// * [`Error::MlsNotInitialized`] - encryption is not set up.
3071 /// * [`Error::UserBlocked`] - the peer is blocked.
3072 /// * [`Error::Mls`] with `SessionNotFound` - there is no session to rebuild.
3073 /// Establishing a first session is [`Self::establish_secure_session`];
3074 /// this heals one that exists.
3075 /// * [`Error::Transport`] - no transport accepted the reset frame. The
3076 /// session is untouched, as above.
3077 pub fn rekey_session(&mut self, peer_id: &str) -> Result<bool> {
3078 if self.is_user_blocked(peer_id) {
3079 return Err(Error::UserBlocked(peer_id.to_string()));
3080 }
3081 if self.mls_manager.is_none() {
3082 return Err(Error::MlsNotInitialized);
3083 }
3084 // A session has to exist for there to be one to heal. Checked before
3085 // the slot is claimed, so a refused call does not spend the window and
3086 // leave a real re-key waiting on it.
3087 if !self.has_mls_session(peer_id)? {
3088 return Err(Error::Mls(offline_protocol_mls::MlsError::SessionNotFound(
3089 peer_id.to_string(),
3090 )));
3091 }
3092 if !self.claim_rekey_slot(peer_id) {
3093 debug!(
3094 peer_id = %peer_id,
3095 "rekey_session: the rate-limit window has not lapsed"
3096 );
3097 return Ok(false);
3098 }
3099 if let Err(err) = self.drive_session_rekey(peer_id, "requested by the application") {
3100 // Nothing was sent and nothing was torn down, so nothing was
3101 // spent: a caller back in range a second from now is asking for a
3102 // rotation this pair has not had.
3103 self.release_rekey_slot(peer_id);
3104 return Err(err);
3105 }
3106 Ok(true)
3107 }
3108
3109 /// Establishes a secure MLS session with a peer.
3110 ///
3111 /// This high-level method handles the complete session establishment flow:
3112 /// 1. Checks if a session already exists (returns Ok(None) if so)
3113 /// 2. Checks for a pending key package from the peer
3114 /// 3. If found, imports the key package, creates the session, and sends the Welcome message
3115 /// 4. If no key package is available, returns an error
3116 ///
3117 /// This method is designed for use by application code that needs explicit control
3118 /// over session establishment, as opposed to the automatic encryption flow.
3119 ///
3120 /// # Arguments
3121 ///
3122 /// * `peer_id` - The ID of the peer to establish a session with
3123 ///
3124 /// # Returns
3125 ///
3126 /// * `Ok(Some(WelcomeMessage))` - Session created, Welcome message returned (and sent to peer)
3127 /// * `Ok(None)` - Session already exists
3128 /// * `Err(SessionNotReady(state))` - Establishment in progress; caller can retry or show "Establishing…"
3129 pub fn establish_secure_session(&mut self, peer_id: &str) -> Result<Option<WelcomeMessage>> {
3130 if self.is_user_blocked(peer_id) {
3131 return Err(Error::UserBlocked(peer_id.to_string()));
3132 }
3133
3134 let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3135
3136 // Check if session already exists
3137 let has_session = {
3138 let manager = mls
3139 .read()
3140 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3141 manager.has_session(peer_id)?
3142 };
3143
3144 if has_session {
3145 debug!(peer_id = %peer_id, "Session already exists");
3146 return Ok(None);
3147 }
3148
3149 // Try loading key package from storage (e.g. after restart) before giving up
3150 self.try_load_key_package_from_storage_into_memory(peer_id);
3151
3152 // Check for pending key package (memory, possibly just restored from storage)
3153 // Clone first, only remove after all operations succeed to avoid losing the key package on failure
3154 if let Some(received_pkg) = self.pending_key_packages.get(peer_id).cloned() {
3155 // Check if key package has expired (using local clock)
3156 let now_ms = Utc::now().timestamp_millis() as u64;
3157 if now_ms >= received_pkg.local_expires_at_ms {
3158 warn!(peer_id = %peer_id, "Received key package has expired, discarding");
3159 self.pending_key_packages.remove(peer_id);
3160 self.delete_peer_key_package_from_storage(peer_id);
3161 } else {
3162 self.import_pending_key_package(&mls, peer_id, &received_pkg.key_package_data)?;
3163
3164 // Create session and get welcome message
3165 let welcome = {
3166 let manager = mls
3167 .read()
3168 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3169 manager.create_session(peer_id)?
3170 };
3171 // The handshake starts here. This is the auto-establish path
3172 // taken the moment a peer's key package arrives, which is how
3173 // an ordinary session begins and which raises no
3174 // `session_missing` at all; see `emit_mls_session_establishing`.
3175 self.emit_mls_session_establishing(
3176 peer_id,
3177 welcome.group_id.as_str(),
3178 crate::mls_observability::MlsOperationContext::Welcome,
3179 );
3180 // We only ever build a session from a key package the peer
3181 // published, so reaching here proves they run MLS.
3182 self.mark_encryption_capable(peer_id);
3183
3184 // Send welcome message to peer
3185 let welcome_sent = self.send_welcome_message(peer_id, &welcome, false)?;
3186
3187 // All operations succeeded, now safe to remove the key package
3188 self.pending_key_packages.remove(peer_id);
3189 self.delete_peer_key_package_from_storage(peer_id);
3190
3191 let group_id = welcome.group_id.as_str().to_string();
3192 let is_session = group_id.starts_with("session:");
3193 if let Err(err) = self.ensure_session_state_entry(peer_id, "session_created_local")
3194 {
3195 warn!(
3196 peer_id = %peer_id,
3197 error = %err,
3198 "Failed to persist pending session state"
3199 );
3200 }
3201
3202 info!(peer_id = %peer_id, group_id = %group_id, "Established secure session");
3203
3204 // Send a fresh key package so the peer has one available for
3205 // group invites (the original was consumed to create this session).
3206 if self.config.encryption.enabled {
3207 let _ = self.send_key_package_to(peer_id, false);
3208 }
3209
3210 if welcome_sent {
3211 debug!(peer_id = %peer_id, group_id = %group_id, is_session, "Welcome synchronously sent");
3212 }
3213
3214 return Ok(Some(welcome));
3215 }
3216 }
3217
3218 // No key package available (memory nor storage) — return non-terminal state so caller can retry
3219 Err(Error::SessionNotReady(self.establishment_state(peer_id)?))
3220 }
3221
3222 /// Imports `peer_id`'s pending key package into MLS, reporting a refusal
3223 /// by this device's clock.
3224 ///
3225 /// The one place the engine admits a pending package, so that the refusal
3226 /// is reported whichever route reached it: `establish_secure_session`,
3227 /// taken when a package arrives, and the send path's
3228 /// `ensure_session_establishment`, taken on the first send after a
3229 /// restart or with `auto_key_exchange` off. Reported on only the first,
3230 /// the restart case left a message queued "anyway" with nothing to say why,
3231 /// which is the failure the code exists for. The throttle makes a second
3232 /// route free: one event per peer either way.
3233 ///
3234 /// A window that has not started keeps the package, since it becomes valid
3235 /// once the clocks agree. A window that has closed discards it, exactly as
3236 /// an expired cached package is discarded, because it may be a package a
3237 /// relay held for days rather than a clock, and kept it would fail every
3238 /// attempt until its cached expiry. The next package the peer sends
3239 /// replaces it.
3240 pub(super) fn import_pending_key_package(
3241 &mut self,
3242 mls: &Arc<RwLock<MlsManager>>,
3243 peer_id: &str,
3244 key_package_data: &[u8],
3245 ) -> Result<()> {
3246 let imported = {
3247 let manager = mls
3248 .read()
3249 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3250 manager.import_key_package(peer_id, key_package_data)
3251 };
3252 if let Err(offline_protocol_mls::MlsError::KeyPackageOutsideValidityWindow { expired }) =
3253 &imported
3254 {
3255 let reason = if *expired {
3256 warn!(
3257 peer_id = %peer_id,
3258 "Peer key package's validity window has closed by this device's \
3259 clock; discarding it until the peer sends a fresh one"
3260 );
3261 self.pending_key_packages.remove(peer_id);
3262 self.delete_peer_key_package_from_storage(peer_id);
3263 "The peer's key package has expired by this device's clock. Either \
3264 this device's date is ahead, or the package is older than its 30-day \
3265 window; it is discarded and the next one the peer sends replaces it."
3266 } else {
3267 warn!(
3268 peer_id = %peer_id,
3269 "Peer key package's validity window has not started by this \
3270 device's clock; this device cannot start a session with the peer \
3271 until the clocks agree"
3272 );
3273 "The peer's key package is not valid yet at this device's time. \
3274 Check this device's date and time, then the peer's."
3275 };
3276 self.warn_control_gate_rejection(
3277 peer_id,
3278 crate::events::SecurityWarningCode::KeyPackageOutsideValidityWindow,
3279 reason,
3280 );
3281 }
3282 Ok(imported?)
3283 }
3284
3285 /// Checks if a pending key package is available for a peer.
3286 ///
3287 /// This can be used to check if session establishment is possible
3288 /// before calling `establish_secure_session`.
3289 ///
3290 /// # Arguments
3291 ///
3292 /// * `peer_id` - The ID of the peer to check
3293 ///
3294 /// # Returns
3295 ///
3296 /// `true` if a key package is available, `false` otherwise
3297 pub fn has_pending_key_package(&self, peer_id: &str) -> bool {
3298 self.pending_key_packages.contains_key(peer_id)
3299 }
3300
3301 /// Returns the current establishment state for a peer.
3302 ///
3303 /// Use this to show "Establishing…" or drive retries without calling send/establish.
3304 pub fn get_establishment_state(&self, peer_id: &str) -> Result<EstablishmentState> {
3305 self.establishment_state(peer_id)
3306 }
3307
3308 /// Whether rich extras (reply context, rich media metadata, forward
3309 /// attribution) on a send toward `peer_id` would travel in the sealed
3310 /// rich body — i.e. the peer's last key package advertised the
3311 /// capability (surviving restarts) and our own `rich_payload_enabled`
3312 /// kill switch is on. When `false` the extras are silently dropped and
3313 /// the message degrades to plain text, so apps can use this to render
3314 /// honest degraded-reply UX instead of discovering the drop after the
3315 /// fact.
3316 pub fn peer_supports_rich_payload(&self, peer_id: &str) -> bool {
3317 self.rich_seal_active(peer_id)
3318 }
3319
3320 /// Creates a session using manually imported key material.
3321 ///
3322 /// This entrypoint is for bindings that expose low-level MLS APIs. It must
3323 /// still keep protocol-level session lifecycle state in sync with MLS state.
3324 pub fn manual_mls_create_session(&mut self, peer_id: &str) -> Result<WelcomeMessage> {
3325 let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3326 let welcome = {
3327 let manager = mls
3328 .read()
3329 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3330 manager.create_session(peer_id)?
3331 };
3332 self.mark_encryption_capable(peer_id);
3333 if let Err(err) = self.ensure_session_state_entry(peer_id, "manual_session_created") {
3334 warn!(
3335 peer_id = %peer_id,
3336 error = %err,
3337 "Failed to persist pending session state after manual session create"
3338 );
3339 }
3340 Ok(welcome)
3341 }
3342
3343 /// Imports a contact's key package and records them encryption-capable.
3344 ///
3345 /// The entry point for bindings that expose low-level MLS APIs. It exists
3346 /// because the FFI wrapper used to reach straight for the `MlsManager` and
3347 /// pass the since-deleted `KeyPackageTrust::FirstUse` — so an app could
3348 /// import a key package
3349 /// under any peer id with no correlation to that peer, bypassing the check
3350 /// `establish_secure_session` performs. The identity claim no longer needs
3351 /// the detour: `import_key_package` re-derives the address from the leaf
3352 /// signature key and refuses a mismatch on its own. The routing stays
3353 /// because the capability record lives here, not on the manager.
3354 pub fn manual_mls_import_key_package(
3355 &mut self,
3356 peer_id: &str,
3357 key_package_data: &[u8],
3358 ) -> Result<()> {
3359 let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3360 {
3361 let manager = mls
3362 .read()
3363 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3364 manager.import_key_package(peer_id, key_package_data)?;
3365 }
3366 // A key package we accepted for this peer is proof they run MLS.
3367 self.mark_encryption_capable(peer_id);
3368 Ok(())
3369 }
3370
3371 /// Deletes a 1:1 session and clears protocol-level lifecycle state.
3372 pub fn manual_mls_delete_session(&mut self, peer_id: &str) -> Result<()> {
3373 let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3374 let manager = mls
3375 .read()
3376 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3377 manager.delete_session(peer_id)?;
3378
3379 // Apply protocol-state cleanup only after MLS deletion succeeds.
3380 self.confirmed_sessions.remove(peer_id);
3381 self.clear_confirmation_recovery_tracking(peer_id);
3382 self.welcome_lifecycles.remove(peer_id);
3383 // Drop the both-create owner gate too (memory + storage). A leaked gate
3384 // entry would make `can_confirm_from_source` reject every non-decrypt
3385 // confirmation source — including `welcome_received` — on the NEXT
3386 // session with this peer, re-stranding it in Pending (and surviving
3387 // restart via `restore_both_create_awaiting_decrypt`).
3388 self.clear_both_create_awaiting_decrypt(peer_id);
3389 self.clear_session_state_entry(peer_id)?;
3390 self.clear_welcome_lifecycle_entry(peer_id)?;
3391 Ok(())
3392 }
3393
3394 /// Joins a session from a Welcome message and synchronizes confirmation state.
3395 pub fn manual_mls_join_session(
3396 &mut self,
3397 welcome: &WelcomeMessage,
3398 ) -> Result<offline_protocol_mls::GroupInfo> {
3399 let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3400 let group_info = {
3401 let manager = mls
3402 .read()
3403 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3404 manager.join_session(welcome)?
3405 };
3406 self.handle_manual_welcome_confirmation(&welcome.inviter_id);
3407 Ok(group_info)
3408 }
3409
3410 /// Processes an MLS Welcome message and synchronizes confirmation state.
3411 pub fn manual_mls_process_welcome(
3412 &mut self,
3413 welcome: &WelcomeMessage,
3414 ) -> Result<offline_protocol_mls::GroupInfo> {
3415 let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3416 let group_info = {
3417 let manager = mls
3418 .read()
3419 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3420 manager.process_welcome(welcome)?
3421 };
3422 self.handle_manual_welcome_confirmation(&welcome.inviter_id);
3423 Ok(group_info)
3424 }
3425
3426 /// Decrypts a user-scoped MLS message and synchronizes confirmation state.
3427 pub fn manual_mls_decrypt_from_user(
3428 &mut self,
3429 encrypted: &EncryptedMessage,
3430 ) -> Result<Option<Vec<u8>>> {
3431 let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3432 let plaintext = {
3433 let manager = mls
3434 .read()
3435 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3436 manager.decrypt_from_user(encrypted, &encrypted.sender_id)?
3437 };
3438 if plaintext.is_some() {
3439 self.handle_manual_decrypt_confirmation(&encrypted.sender_id);
3440 }
3441 Ok(plaintext)
3442 }
3443
3444 /// Decrypts any MLS message and synchronizes confirmation state for 1:1 flows.
3445 pub fn manual_mls_decrypt(&mut self, encrypted: &EncryptedMessage) -> Result<Option<Vec<u8>>> {
3446 let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3447 let plaintext = {
3448 let manager = mls
3449 .read()
3450 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3451 manager.decrypt(encrypted, &encrypted.sender_id)?
3452 };
3453 if plaintext.is_some() && Self::is_session_group_id(encrypted.group_id.as_str()) {
3454 self.handle_manual_decrypt_confirmation(&encrypted.sender_id);
3455 }
3456 Ok(plaintext)
3457 }
3458
3459 pub(crate) fn is_session_group_id(group_id: &str) -> bool {
3460 group_id.starts_with("session:")
3461 }
3462
3463 fn handle_manual_welcome_confirmation(&mut self, peer_id: &str) {
3464 // Shared by `manual_mls_join_session` and `manual_mls_process_welcome`:
3465 // in both cases we just joined a session this peer built for us.
3466 self.mark_encryption_capable(peer_id);
3467 match self.confirm_session_state(peer_id, "welcome_received") {
3468 Ok(true) => {
3469 let _ = self.flush_pending_messages(peer_id);
3470 self.process_pending_decryption(peer_id);
3471 }
3472 Ok(false) => {}
3473 Err(err) => {
3474 warn!(
3475 peer_id = %peer_id,
3476 error = %err,
3477 "Failed to persist session confirmation after manual welcome processing"
3478 );
3479 }
3480 }
3481 }
3482
3483 fn handle_manual_decrypt_confirmation(&mut self, peer_id: &str) {
3484 if !self.can_confirm_from_source(peer_id, "decrypt_success") {
3485 return;
3486 }
3487 match self.confirm_session_state(peer_id, "decrypt_success") {
3488 Ok(true) => {
3489 let _ = self.flush_pending_messages(peer_id);
3490 }
3491 Ok(false) => {}
3492 Err(err) => {
3493 warn!(
3494 peer_id = %peer_id,
3495 error = %err,
3496 "Failed to persist session confirmation after manual decrypt"
3497 );
3498 }
3499 }
3500 }
3501
3502 /// Processes internal MLS protocol messages.
3503 ///
3504 /// Returns `Some(InternalMessageResult::Consumed)` if the message was an internal
3505 /// protocol message that should not be surfaced to the application.
3506 /// Returns `Some(InternalMessageResult::Decrypted(plaintext))` if the message was
3507 /// encrypted and successfully decrypted.
3508 /// Returns `None` if the message is not an internal message.
3509 pub(crate) fn process_internal_message(
3510 &mut self,
3511 message: &Message,
3512 ) -> Option<InternalMessageResult> {
3513 self.process_internal_message_via(message, None)
3514 }
3515
3516 /// [`Self::process_internal_message`] with the transport the frame
3517 /// arrived on, when the caller knows it. Handlers that trust relay-server
3518 /// answers (`__GROUP_CREATED__` setting `relay_synced`) require the
3519 /// Internet transport; with `None` they treat the frame as untrusted.
3520 pub(crate) fn process_internal_message_via(
3521 &mut self,
3522 message: &Message,
3523 arrival_transport: Option<TransportType>,
3524 ) -> Option<InternalMessageResult> {
3525 let content = &message.content;
3526
3527 // Run the security gate for control messages (transport identity +
3528 // signature verification + freshness). `Rejected` drops the message;
3529 // `Proceed` carries whether the frame was actually signed, for the one
3530 // handler that consumes a payload field as authenticated data, and
3531 // whether its age was established, for the one that acts on a
3532 // directive worth replaying.
3533 let (signed, freshness_bound) =
3534 match self.security_gate_control_message(message, arrival_transport) {
3535 ControlGateOutcome::Rejected(result) => return Some(result),
3536 ControlGateOutcome::Proceed {
3537 signed,
3538 freshness_bound,
3539 } => (signed, freshness_bound),
3540 };
3541
3542 let sender = message.sender.as_str();
3543
3544 // Handle key package messages
3545 if let Some(data) = content.strip_prefix(internal_prefixes::KEY_PACKAGE) {
3546 // The stamp travels only when it was signed *and* checked, so a
3547 // handler cannot reach for it in the case where it means nothing.
3548 let freshness = freshness_bound.then(|| message.timestamp.as_millis());
3549 self.handle_key_package_message(sender, data, signed, freshness);
3550 return Some(InternalMessageResult::Consumed);
3551 }
3552
3553 if content.starts_with(internal_prefixes::SESSION_CONFIRM_PROBE) {
3554 self.handle_session_confirm_probe(sender, content);
3555 return Some(InternalMessageResult::Consumed);
3556 }
3557
3558 if content.starts_with(internal_prefixes::SESSION_CONFIRM_ACK) {
3559 self.handle_session_confirm_ack(sender, content);
3560 return Some(InternalMessageResult::Consumed);
3561 }
3562
3563 // Handle welcome messages (session invitation)
3564 if let Some(data) = content.strip_prefix(internal_prefixes::WELCOME) {
3565 self.handle_welcome_message(sender, data);
3566 return Some(InternalMessageResult::Consumed);
3567 }
3568
3569 // Handle encrypted messages
3570 if let Some(data) = content.strip_prefix(internal_prefixes::ENCRYPTED) {
3571 if let Some(result) =
3572 self.handle_encrypted_message(sender, data, message, arrival_transport)
3573 {
3574 return Some(result);
3575 }
3576 return Some(InternalMessageResult::Consumed);
3577 }
3578
3579 // Handle connection request messages
3580 if let Some(data) = content.strip_prefix(internal_prefixes::CONN_REQUEST) {
3581 self.handle_connection_request(sender, data);
3582 return Some(InternalMessageResult::Consumed);
3583 }
3584
3585 // Handle connection accepted messages
3586 if let Some(data) = content.strip_prefix(internal_prefixes::CONN_ACCEPT) {
3587 self.handle_connection_accepted(sender, data);
3588 return Some(InternalMessageResult::Consumed);
3589 }
3590
3591 // Handle connection rejected messages
3592 if content
3593 .strip_prefix(internal_prefixes::CONN_REJECT)
3594 .is_some()
3595 {
3596 self.handle_connection_rejected(sender);
3597 return Some(InternalMessageResult::Consumed);
3598 }
3599
3600 // Handle connection cancelled messages
3601 if content
3602 .strip_prefix(internal_prefixes::CONN_CANCEL)
3603 .is_some()
3604 {
3605 self.handle_connection_cancelled(sender);
3606 return Some(InternalMessageResult::Consumed);
3607 }
3608
3609 // --- Presence, typing, and read receipt messages ---
3610
3611 if let Some(data) = content.strip_prefix(internal_prefixes::PRESENCE) {
3612 self.handle_presence_message(sender, data);
3613 return Some(InternalMessageResult::Consumed);
3614 }
3615
3616 if let Some(data) = content.strip_prefix(internal_prefixes::TYPING_INDICATOR) {
3617 self.handle_typing_indicator(sender, data);
3618 return Some(InternalMessageResult::Consumed);
3619 }
3620
3621 if let Some(data) = content.strip_prefix(internal_prefixes::READ_RECEIPT) {
3622 self.handle_read_receipt(sender, data);
3623 return Some(InternalMessageResult::Consumed);
3624 }
3625
3626 // A custody receipt from a neighbour holding one of our frames. Past
3627 // the control gate like every signed frame; it suppresses re-deposit
3628 // toward that custodian and settles nothing (`docs/spec/custody.md`).
3629 // Consumed whatever the body says: a malformed one is refused
3630 // silently, and a receipt never requests an acknowledgement.
3631 if let Some(data) = content.strip_prefix(internal_prefixes::CUSTODY_RECEIPT) {
3632 self.handle_custody_receipt(sender, data, message.timestamp.as_millis());
3633 return Some(InternalMessageResult::Consumed);
3634 }
3635
3636 // --- Group (mesh/MLS) messages ---
3637
3638 if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_MLS_MSG) {
3639 return Some(self.handle_group_mls_msg_via(message, sender, data, arrival_transport));
3640 }
3641
3642 if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_MLS_WELCOME) {
3643 let mid = message.id.as_str();
3644 self.handle_group_mls_welcome(&mid, sender, data);
3645 return Some(InternalMessageResult::Consumed);
3646 }
3647
3648 if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_MLS_COMMIT) {
3649 let mid = message.id.as_str();
3650 self.handle_group_mls_commit(&mid, sender, data);
3651 return Some(InternalMessageResult::Consumed);
3652 }
3653
3654 if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_MLS_LEAVE) {
3655 let mid = message.id.as_str();
3656 self.handle_group_mls_leave(&mid, sender, data);
3657 return Some(InternalMessageResult::Consumed);
3658 }
3659
3660 if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_ROLE_CHANGE) {
3661 let mid = message.id.as_str();
3662 self.handle_group_role_change(&mid, sender, data);
3663 return Some(InternalMessageResult::Consumed);
3664 }
3665
3666 if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_RENAME) {
3667 let mid = message.id.as_str();
3668 self.handle_group_rename(&mid, sender, data);
3669 return Some(InternalMessageResult::Consumed);
3670 }
3671
3672 // --- Group (relay) and service messages ---
3673
3674 if content.starts_with(internal_prefixes::GROUP_CREATED)
3675 || content.starts_with(internal_prefixes::GROUP_MSG)
3676 || content.starts_with(internal_prefixes::GROUP_MEMBER_ADDED)
3677 || content.starts_with(internal_prefixes::GROUP_MEMBER_REMOVED)
3678 || content.starts_with(internal_prefixes::GROUP_INFO)
3679 || content.starts_with(internal_prefixes::USER_GROUPS)
3680 || content.starts_with(internal_prefixes::GROUP_ERROR)
3681 {
3682 self.handle_group_relay_message(sender, content, arrival_transport);
3683 return Some(InternalMessageResult::Consumed);
3684 }
3685
3686 // --- Service discovery & request/response ---
3687 if content.starts_with(offline_protocol_services::SVC_MESSAGE_PREFIX) {
3688 self.handle_service_message(sender, content, message, false);
3689 return Some(InternalMessageResult::Consumed);
3690 }
3691
3692 None // Not an internal message
3693 }
3694
3695 /// Registers an event handler.
3696 ///
3697 /// # Arguments
3698 ///
3699 /// * `handler` - Callback function that will be called for each event
3700 pub fn on_event<F>(&mut self, handler: F)
3701 where
3702 F: Fn(Event) + Send + Sync + 'static,
3703 {
3704 let Ok(mut state) = lock_shared_state(&self.shared_state) else {
3705 error!("Failed to lock shared state in on_event");
3706 return;
3707 };
3708 state.event_handlers.push(Arc::new(handler));
3709 }
3710
3711 /// Processes pending operations (retries, timeouts, etc.).
3712 ///
3713 /// This should be called periodically to handle background tasks.
3714 pub fn process(&mut self) -> Result<()> {
3715 {
3716 let state = lock_shared_state(&self.shared_state)?;
3717 if state.state != ProtocolState::Running {
3718 return Ok(()); // Don't process if not running
3719 }
3720 }
3721
3722 // Transmit forwards whose hold has elapsed. Runs early and before the
3723 // retry ladders: other people's traffic is time-sensitive in a way our
3724 // own retries are not — a frame held too long is dropped rather than
3725 // sent late.
3726 self.flush_mesh_relays();
3727 // Held frames past their hold, and receipt suppressions past their
3728 // end. Throttled inside; hours-long holds need no per-tick walk.
3729 self.sweep_custody();
3730
3731 // One step's failure never starves another. Every fallible step runs
3732 // on every tick and the first error is reported once they all have.
3733 // The steps are coupled: the retry queue fills the pending-ACK table
3734 // and the timeout sweep is what empties it, so a retry error that
3735 // returned early used to stop the sweep for good.
3736 let mut first_error: Option<crate::Error> = None;
3737 let mut note = |step: &'static str, result: Result<()>| {
3738 if let Err(err) = result {
3739 warn!(step, error = %err, "process() step failed; continuing the tick");
3740 first_error.get_or_insert(err);
3741 }
3742 };
3743 // Before the retry ladder, so a frame the app sent first is not
3744 // overtaken by one that only reached the retry queue later.
3745 self.flush_peer_windows();
3746 self.flush_due_coalesced_acks();
3747 note("retry_queue", self.process_retry_queue());
3748 note("welcome_retry_queue", self.process_welcome_retry_queue());
3749 self.process_relay_register_retries();
3750 self.process_relay_broadcast_report_timeouts();
3751 note("timed_out_acks", self.process_timed_out_acks());
3752
3753 // Throttle session reconciliation to avoid expensive storage I/O
3754 // (list_sessions → Keychain/Keystore) on every tick. Only run when
3755 // there's pending work AND enough time has elapsed since the last run.
3756 self.run_throttled_reconciliation("process_tick");
3757
3758 let _ = self.prune_expired_pending_global_front(Instant::now(), 256);
3759 // Batched like the Lamport clock: the seen set reaches disk on a
3760 // change threshold or a time cadence, never per message.
3761 self.persist_dedup_seen_if_due();
3762 self.pump_media_transfers();
3763 #[cfg(feature = "data")]
3764 self.send_due_data_settle_offers(Instant::now());
3765 self.refresh_nostr_key_package_slots();
3766 self.refresh_nostr_discovery_claim();
3767 // A relay is free never to send end-of-stored-events, and that is the
3768 // only natural completion signal a resolution has. Without this sweep
3769 // a quiet relay turns a lookup into a hang rather than an empty answer.
3770 self.sweep_username_resolutions();
3771 self.cleanup_expired_entries();
3772 self.evaluate_relay_role();
3773 self.tick_telemetry_categories();
3774
3775 match first_error {
3776 Some(err) => Err(err),
3777 None => Ok(()),
3778 }
3779 }
3780
3781 /// Reports this device's condition to the forwarding governor and announces
3782 /// any change in whether it is acting as a relay (`RelayPromoted`,
3783 /// `RelayDemoted`, `RelayDemotedBattery`).
3784 ///
3785 /// Runs every tick independently of telemetry: these are app-facing events
3786 /// delivered through the `EventCallback` channel, which is live whether or
3787 /// not a telemetry sink is installed. It also keeps the
3788 /// `DeviceCapabilitySnapshot.relay_role` signal honest, since
3789 /// `tick_telemetry_categories` reads the standing immediately after.
3790 ///
3791 /// **The relay standing is observed, not predicted.** It says this device
3792 /// has been carrying other people's traffic, not that its battery and
3793 /// neighbor count suggest it could — those only bias *how much* it carries
3794 /// (`set_conditions`), continuously and with no threshold to cross. So the
3795 /// promotion here is a report about the mesh, and an app can trust
3796 /// `is_relay()` to mean the device is doing the work.
3797 ///
3798 /// Two things still transition on policy rather than on observation, and
3799 /// both are gates rather than predictions: relaying switched off in
3800 /// configuration, and the battery falling through the forwarding floor.
3801 /// Each stops forwarding outright, so the standing is surrendered at once
3802 /// instead of decaying over the activity windows — the window measures
3803 /// quiet, and this is refusal.
3804 ///
3805 /// Unlike the threshold policy this replaces, a device with no battery
3806 /// reading still participates: forwarding is what is being observed, and
3807 /// it happens whether or not the host has reported a level.
3808 fn evaluate_relay_role(&mut self) {
3809 let (_statuses, available) = self.transport_manager.snapshot_status_and_available();
3810 let (battery_level, is_charging) =
3811 device_battery_from_available(self.transport_manager.current_transport(), &available);
3812
3813 let eager = matches!(self.config.relay.relay_priority, RelayPriority::Always);
3814 // The bias ramp is measured from the level at which forwarding actually
3815 // stops, which is the configured minimum raised to the hard critical
3816 // floor — not the raw configured value, which may be set below it.
3817 // Passing the unclamped one would start the ramp under the gate and
3818 // scale a device that is about to stop forwarding altogether.
3819 let forwarding_floor = self.relay_battery_floor(is_charging);
3820 self.mesh_relay
3821 .set_conditions(battery_level, is_charging, eager, forwarding_floor);
3822 let relay = &self.config.relay;
3823
3824 // Relaying switched off in configuration.
3825 if !relay.allow_relay || matches!(relay.relay_priority, RelayPriority::Never) {
3826 if self.mesh_relay.force_inactive() {
3827 self.emit_event(Event::relay_demoted("relaying disabled by configuration"));
3828 }
3829 return;
3830 }
3831
3832 // Battery through the forwarding floor. Same gate `try_relay_message`
3833 // applies to each frame, evaluated from the reading already in hand.
3834 if !self.battery_allows_relaying_with(battery_level, is_charging) {
3835 if self.mesh_relay.force_inactive() {
3836 self.emit_event(Event::relay_demoted_battery(
3837 battery_level.unwrap_or(0),
3838 self.relay_battery_floor(is_charging),
3839 ));
3840 }
3841 return;
3842 }
3843
3844 match self.mesh_relay.observe_activity(Instant::now()) {
3845 Some(RelayActivity::Began { .. }) => {
3846 let connection_count = self.known_peers.len();
3847 self.emit_event(Event::relay_promoted(connection_count, battery_level));
3848 }
3849 Some(RelayActivity::Ceased) => {
3850 self.emit_event(Event::relay_demoted(
3851 "no traffic carried for other devices recently",
3852 ));
3853 }
3854 None => {}
3855 }
3856 }
3857
3858 /// The relay standing this device currently holds, for telemetry.
3859 fn relay_role(&self) -> RelayRole {
3860 if self.mesh_relay.is_active_relay() {
3861 RelayRole::Relay
3862 } else {
3863 RelayRole::Regular
3864 }
3865 }
3866
3867 /// Per-tick telemetry work: diff transport statuses, diff device
3868 /// capability, and emit a `MetricsFrame` when `metrics_cadence` has
3869 /// elapsed. No-ops unless a sink has been installed.
3870 ///
3871 /// Runs at the end of `process()` so reliability/queue state observed
3872 /// here is the same state the rest of the tick has already advanced.
3873 /// Snapshots `get_available_transports()` exactly once per tick and
3874 /// reuses the map across every downstream helper that needs it.
3875 ///
3876 /// If an earlier step in `process()` returns `Err(...)`, this method is
3877 /// skipped and telemetry emission for that interval is deferred to the
3878 /// next successful tick — the cadence guarantee relaxes under sustained
3879 /// partial failure.
3880 ///
3881 /// # Sink-panic semantics
3882 ///
3883 /// Each per-tick cursor is advanced *before* the emit call that
3884 /// observes it, so a panicking sink does not cause re-delivery of the
3885 /// record it panicked on. The transport-status path advances
3886 /// **per-transport**: when multiple transitions fall in a single tick,
3887 /// a panic on transition K commits K's entry (at-most-once for K) but
3888 /// leaves entries L..N in `transport_status_snapshot` at their previous
3889 /// values, so the next tick re-diffs and emits them fresh. This avoids
3890 /// silent data loss for transitions that never reached the sink.
3891 ///
3892 /// The device-capability and metrics-frame paths emit at most one
3893 /// record per tick each, so a simpler "advance then emit" discipline
3894 /// suffices there: a panic advances the cursor and the same record is
3895 /// not re-emitted on the next tick.
3896 fn tick_telemetry_categories(&mut self) {
3897 let Some(ctx) = self.telemetry.clone().filter(|ctx| ctx.enabled()) else {
3898 return;
3899 };
3900 let now_ms = Utc::now().timestamp_millis();
3901 let now = Instant::now();
3902
3903 // One lock-per-transport pass: get statuses (for the transition
3904 // diff) and the available-only metrics map (for the metrics frame
3905 // and the device-capability diff). Reused across every helper
3906 // below.
3907 let (current_statuses, available) = self.transport_manager.snapshot_status_and_available();
3908
3909 // Transport-status diff: one record per changed transport. Commit
3910 // each transport's new snapshot entry *before* emitting its event
3911 // so a panic on event K is at-most-once for K; untouched entries
3912 // L..N stay at their previous values and get re-diffed next tick.
3913 let transitions =
3914 diff_transport_state(now_ms, &self.transport_status_snapshot, ¤t_statuses);
3915 for event in transitions {
3916 let transport = event.transport;
3917 match current_statuses.get(&transport) {
3918 Some(status) => {
3919 self.transport_status_snapshot.insert(transport, *status);
3920 }
3921 None => {
3922 self.transport_status_snapshot.remove(&transport);
3923 }
3924 }
3925 dispatch_record(&ctx.sink, &TelemetryRecord::TransportState(event));
3926 // An internet-capable transport coming up is the pipe's cue to
3927 // send what an offline stretch queued; the pipe decides which
3928 // transports count.
3929 #[cfg(feature = "telemetry-pipe")]
3930 if let Some(pipe) = &self.telemetry_pipe {
3931 pipe.notify_transport(event.transport, event.current);
3932 }
3933 }
3934
3935 // Device capability diff. At-most-one emission per tick, so the
3936 // simple advance-before-emit pattern preserves at-most-once.
3937 let (battery_level, is_charging) =
3938 device_battery_from_available(self.transport_manager.current_transport(), &available);
3939 let relay_role = self.relay_role();
3940 let device_now = DeviceSnap::from_parts(battery_level, is_charging, relay_role);
3941 let device_change =
3942 diff_device_capability(now_ms, self.device_capability_snapshot, device_now);
3943 self.device_capability_snapshot = Some(device_now);
3944 if let Some(snapshot) = device_change {
3945 dispatch_record(&ctx.sink, &TelemetryRecord::Device(snapshot));
3946 }
3947
3948 // Periodic metrics snapshot. Rearm cadence before emit so a
3949 // panicking sink doesn't cause an immediate retry next tick.
3950 if let Some(cadence) = ctx.config.metrics_cadence() {
3951 let due = match self.last_metrics_emit_at {
3952 None => true,
3953 Some(prev) => now.saturating_duration_since(prev) >= cadence,
3954 };
3955 if due {
3956 let is_local_relay = matches!(relay_role, RelayRole::Relay);
3957 let frame = build_metrics_frame(
3958 now_ms,
3959 self.transport_manager.current_transport(),
3960 &available,
3961 &self.retry_queue,
3962 &self.deduplicator,
3963 &self.ack_manager,
3964 self.known_peers.len(),
3965 is_local_relay,
3966 );
3967 self.last_metrics_emit_at = Some(now);
3968 dispatch_record(
3969 &ctx.sink,
3970 &TelemetryRecord::MetricsSnapshot(Box::new(frame)),
3971 );
3972 }
3973 }
3974 }
3975
3976 /// Processes messages ready for retry from the retry queue.
3977 ///
3978 /// EDGE CASE HANDLING:
3979 /// - Checks transport availability before each retry attempt
3980 /// - Handles transport switch mid-retry
3981 /// - Properly tracks retry counts and transport failures
3982 fn process_retry_queue(&mut self) -> Result<()> {
3983 // Limit batch size to prevent blocking on large queues
3984 let mut max_batch_size = crate::constants::FLUSH_BATCH_LIMIT;
3985
3986 // Opt-in resend rate cap: bound how fast the RETRY/PROBE path re-sends,
3987 // so a backlog of resends to unreachable peers cannot burst past the
3988 // relay's per-connection rate limit and flap the connection. Refills a
3989 // token bucket by elapsed wall-time and caps this call's batch to the
3990 // whole tokens available. Only resends are bounded; first-attempt sends,
3991 // ACKs, control frames and handshakes take other paths and are never
3992 // throttled, so live traffic keeps full throughput and latency. Fully
3993 // off by default (unchanged retry behaviour for every existing app).
3994 if self.config.reliability.retry.edge_driven_unreachable_dm {
3995 let now = Instant::now();
3996 match self.retry_drain_last {
3997 Some(last) => {
3998 let elapsed = now.duration_since(last).as_secs_f64();
3999 self.retry_drain_tokens = (self.retry_drain_tokens
4000 + elapsed * crate::protocol::types::RETRY_DRAIN_RATE_PER_SEC)
4001 .min(crate::protocol::types::RETRY_DRAIN_BURST);
4002 }
4003 None => {
4004 // First drain: start with a full burst budget.
4005 self.retry_drain_tokens = crate::protocol::types::RETRY_DRAIN_BURST;
4006 }
4007 }
4008 self.retry_drain_last = Some(now);
4009 let budget = self.retry_drain_tokens.floor() as usize;
4010 max_batch_size = max_batch_size.min(budget);
4011 }
4012
4013 let mut processed = 0;
4014
4015 while processed < max_batch_size {
4016 let mut entry = match self.retry_queue.dequeue_ready() {
4017 Some(e) => e,
4018 None => break,
4019 };
4020
4021 processed += 1;
4022
4023 // No slot to track this resend: put it back unchanged and try
4024 // again shortly. Checked before the re-seal, so a held resend
4025 // burns no ratchet generation, and before the send, so nothing
4026 // goes out that cannot be tracked. The retry count is not
4027 // advanced: being held is not an attempt, so the wait is the
4028 // ladder's first rung rather than this entry's next one.
4029 if entry.message.requires_ack && !self.ack_manager.has_capacity_for(&entry.message.id) {
4030 let recheck_ms = self.config.reliability.retry.initial_delay_ms;
4031 let _ = self.retry_queue.enqueue_with_delay(
4032 entry.message,
4033 entry.retry_count,
4034 recheck_ms,
4035 );
4036 continue;
4037 }
4038
4039 // A frame that has not gone out yet waits for the peer's window
4040 // instead, out of this queue: re-checking it every second would
4041 // spend the batch on frames that cannot move.
4042 if !self.peer_window_admits(&entry.message) {
4043 self.hold_for_peer_window(&entry.message);
4044 continue;
4045 }
4046
4047 let previous_transport = self.transport_manager.current_transport();
4048 // Normally finds the entry already there: a retry entry outlives
4049 // its outbox entry only if a give-up path skipped
4050 // `retire_undeliverable_message`, and at capacity this call would
4051 // then evict a live message to bring a settled one back.
4052 self.ensure_outbox_entry(&entry.message);
4053
4054 // Tier 2: re-seal against the peer's current session before this
4055 // resend, so a post-re-key epoch change no longer leaves the bytes
4056 // undecryptable. No-op for media/plaintext/unconfirmed sessions.
4057 self.reseal_for_resend_in_place(&mut entry.message);
4058
4059 let forced_transport = self.pinned_media_transport_for_message(&entry.message.id);
4060 let send_result = if let Some(transport) = forced_transport {
4061 self.transport_manager
4062 .send_via_transport(&entry.message, transport)
4063 } else {
4064 self.transport_manager.send(&entry.message)
4065 };
4066 let current_transport =
4067 forced_transport.or_else(|| self.transport_manager.current_transport());
4068
4069 match send_result {
4070 Ok(()) => {
4071 // Never `?`. This propagated once, and `process()` then
4072 // returned before `process_timed_out_acks` on every tick:
4073 // the step that frees slots never ran again, so the table
4074 // stayed full and the engine never recovered. The capacity
4075 // check above makes a failure here a priority-eviction
4076 // race at most.
4077 let ack_registered_now = match self.ensure_ack_registration(&entry.message) {
4078 Ok(registered) => registered,
4079 Err(err) => {
4080 warn!(
4081 message_id = %entry.message.id,
4082 error = %err,
4083 "ACK registration failed after a retry send"
4084 );
4085 false
4086 }
4087 };
4088
4089 if ack_registered_now {
4090 // The resend registered a brand-new ACK (the prior one
4091 // was cleared, e.g. by an ACK timeout that re-queued
4092 // this message). A fresh ACK starts at retry_count 0, so
4093 // carry the count the retry-queue entry already holds —
4094 // advanced by this attempt — onto it. Without this the
4095 // backoff ladder resets to 0 on every relay-accepted
4096 // resend and `delay_for_retry` is pinned at its 1s floor
4097 // forever for a peer that never ACKs (an offline
4098 // recipient), flooding the relay ~once per second.
4099 self.ack_manager.set_retry_count(
4100 &entry.message.id,
4101 entry.retry_count.saturating_add(1),
4102 );
4103 } else {
4104 self.ack_manager.increment_retry_count(&entry.message.id);
4105 }
4106 self.mark_message_sent(
4107 &entry.message,
4108 current_transport,
4109 Some(entry.retry_count.saturating_add(1)),
4110 );
4111
4112 if let Some(transport) = current_transport {
4113 self.transport_manager.reset_retry_count(transport);
4114 }
4115 self.offer_resend_to_mesh(&entry.message, true);
4116
4117 debug!(
4118 message_id = %entry.message.id,
4119 retry_count = entry.retry_count,
4120 transport = ?current_transport,
4121 "Retry send succeeded"
4122 );
4123 }
4124 Err(e) => {
4125 self.offer_resend_to_mesh(&entry.message, false);
4126 // Re-enqueue with incremented retry count for backoff
4127 let next_retry_at = self
4128 .retry_queue
4129 .enqueue(entry.message.clone(), entry.retry_count + 1);
4130
4131 if let Some(transport) = forced_transport.or(previous_transport) {
4132 self.transport_manager.record_retry_failure(transport);
4133 }
4134
4135 // Surface the schedule so apps can show retry state
4136 // instead of inferring it from silence. None = the id
4137 // was already queued; the earlier emission stands.
4138 if let Some(retry_at) = next_retry_at {
4139 self.emit_event(Event::message_retrying(
4140 entry.message.id.clone(),
4141 entry.message.recipient.as_str().to_string(),
4142 entry.retry_count + 1,
4143 retry_at.timestamp_millis(),
4144 ));
4145 }
4146
4147 debug!(
4148 message_id = %entry.message.id,
4149 retry_count = entry.retry_count,
4150 transport = ?current_transport,
4151 error = %e,
4152 "Retry send failed, will retry later"
4153 );
4154 }
4155 }
4156 }
4157
4158 if processed > 0 {
4159 debug!(processed = processed, "Processed retry queue entries");
4160 // Spend the resend-rate tokens actually used (opt-in cap only).
4161 if self.config.reliability.retry.edge_driven_unreachable_dm {
4162 self.retry_drain_tokens = (self.retry_drain_tokens - processed as f64).max(0.0);
4163 }
4164 }
4165
4166 Ok(())
4167 }
4168
4169 /// Processes timed out ACKs and handles retry or failure.
4170 fn process_timed_out_acks(&mut self) -> Result<()> {
4171 let timed_out = self.ack_manager.drain_timed_out();
4172 if timed_out.is_empty() {
4173 return Ok(());
4174 }
4175 // A frame still waiting in a local queue has not been answered because
4176 // it has not been sent. Retrying it would queue a second, re-sealed
4177 // copy behind the first: on a congested link that is what grew the
4178 // queue, the round trip and the receiver's ratchet gap together in the
4179 // 2026-10-19 volume test. It gets a bounded extension instead.
4180 let queued = self.transport_manager.queued_message_ids();
4181 for pending in timed_out {
4182 let message_id = pending.message_id.clone();
4183
4184 if queued.contains(&message_id.as_str())
4185 && self
4186 .ack_manager
4187 .extend_while_queued(&message_id, QUEUED_ACK_GRACE_MAX)
4188 {
4189 continue;
4190 }
4191
4192 if pending.retry_count >= self.config.reliability.retry.max_retries {
4193 self.handle_max_retries_exceeded(&message_id, pending.retry_count)?;
4194 continue;
4195 }
4196
4197 self.handle_ack_timeout_retry(&message_id, pending.retry_count)?;
4198 }
4199 Ok(())
4200 }
4201
4202 /// Handles a message that has exceeded maximum retries.
4203 fn handle_max_retries_exceeded(
4204 &mut self,
4205 message_id: &MessageId,
4206 retry_count: u32,
4207 ) -> Result<()> {
4208 // A plain DM whose recipient still holds a live unreachable-park
4209 // counter re-parks instead of settling: the exhausted budget was burnt
4210 // by reachability probes — sends that succeed locally without proving
4211 // the peer is back — not by a peer believed reachable (see
4212 // try_repark_exhausted_dm).
4213 if self.try_repark_exhausted_dm(message_id) {
4214 return Ok(());
4215 }
4216
4217 // Retry exhaustion is terminal for a connection request: settle the
4218 // pending entry and surface the typed undeliverable signal alongside
4219 // the generic message_failed, so apps keep connection-request
4220 // context without parsing reason strings.
4221 let undeliverable_recipient = self.take_undeliverable_connection_request(message_id);
4222 // Read before `retire_undeliverable_message` below removes the entry.
4223 let app_send = self.is_app_send(message_id);
4224
4225 let state = lock_shared_state(&self.shared_state).map_err(|e| {
4226 error!(
4227 "Failed to lock shared state for message failed event: {}",
4228 e
4229 );
4230 e
4231 })?;
4232 let failed = Event::message_failed(message_id.clone(), "Max retries exceeded", retry_count);
4233 // Telemetry counts the app's own messages only; see `emit_settlement`.
4234 state.emit_settlement(failed, app_send);
4235 if let Some(recipient) = undeliverable_recipient {
4236 warn!(
4237 recipient = %recipient,
4238 message_id = %message_id,
4239 "Connection request undeliverable: max retries exceeded"
4240 );
4241 state.emit_event(Event::connection_request_undeliverable(
4242 recipient,
4243 message_id.as_str(),
4244 "max_retries_exceeded",
4245 ));
4246 }
4247 drop(state);
4248
4249 self.retire_undeliverable_message(message_id, "max retries exceeded");
4250 Ok(())
4251 }
4252
4253 /// Handles retry logic for a timed out ACK.
4254 fn handle_ack_timeout_retry(&mut self, message_id: &MessageId, retry_count: u32) -> Result<()> {
4255 if let Some(entry) = self
4256 .outbox
4257 .get(message_id)
4258 .or_else(|| self.media_outbox.get(message_id))
4259 {
4260 let message_clone = entry.message.clone();
4261 let recipient = entry.message.recipient.as_str().to_string();
4262 let last_transport = entry.last_transport;
4263
4264 // enqueue is infallible (retry queue has no attempt limit)
4265 let next_retry_at = self.retry_queue.enqueue(message_clone, retry_count);
4266 if let Some(transport) = last_transport {
4267 self.transport_manager.record_retry_failure(transport);
4268 }
4269 if let Some(retry_at) = next_retry_at {
4270 self.emit_event(Event::message_retrying(
4271 message_id.clone(),
4272 recipient,
4273 retry_count,
4274 retry_at.timestamp_millis(),
4275 ));
4276 }
4277 } else {
4278 self.handle_missing_outbox_entry(message_id, retry_count)?;
4279 }
4280 Ok(())
4281 }
4282
4283 /// Emits a protocol event to all registered handlers.
4284 ///
4285 /// Silently no-ops if the shared state lock is poisoned.
4286 pub(crate) fn emit_event(&self, event: Event) {
4287 if let Ok(state) = lock_shared_state(&self.shared_state) {
4288 state.emit_event(event);
4289 }
4290 }
4291
4292 /// Emits a message's terminal event (`MessageDelivered` or
4293 /// `MessageFailed`) to the app, and to telemetry only when `telemetry` is
4294 /// set.
4295 ///
4296 /// Telemetry's delivered and failed counts are read against its sent
4297 /// count, which only the app send path reports, so all three count the
4298 /// same messages: the app's own. A settlement for a media chunk, an
4299 /// internal frame, or a message whose origin is no longer known still
4300 /// reaches `on_event` and stays out of telemetry. See
4301 /// [`OutboxEntry::app_send`].
4302 pub(crate) fn emit_settlement(&self, event: Event, telemetry: bool) {
4303 if let Ok(state) = lock_shared_state(&self.shared_state) {
4304 state.emit_settlement(event, telemetry);
4305 }
4306 }
4307
4308 /// Whether `message_id` is an outbox entry the app send path created.
4309 pub(crate) fn is_app_send(&self, message_id: &MessageId) -> bool {
4310 self.outbox
4311 .get(message_id)
4312 .is_some_and(|entry| entry.app_send)
4313 }
4314
4315 /// Emits a terminal settlement, or parks it until `start()` if the event
4316 /// pipeline is not live yet.
4317 ///
4318 /// Restore paths settle messages the app is still holding ids for. They run
4319 /// from `initialize_mls`, before `start()` and often before the app has
4320 /// installed its event callback, so emitting directly there would silently
4321 /// discard exactly the signals that exist to stop an id from hanging
4322 /// forever. Once running, this is a plain emit — the same call is used from
4323 /// `process()`-driven expiry, where no deferral is wanted.
4324 pub(crate) fn settle_restored_message_failure(&mut self, event: Event) {
4325 let running = self.protocol_is_running();
4326 self.settle_one_restored_message_failure(event, true, running);
4327 }
4328
4329 /// [`Self::settle_restored_message_failures`] for restored outbox entries,
4330 /// each paired with its [`OutboxEntry::app_send`]: only the app's own
4331 /// reach telemetry, as on the live path.
4332 pub(crate) fn settle_restored_outbox_failures(
4333 &mut self,
4334 settlements: impl IntoIterator<Item = (Event, bool)>,
4335 ) {
4336 let running = self.protocol_is_running();
4337 for (event, telemetry) in settlements {
4338 self.settle_one_restored_message_failure(event, telemetry, running);
4339 }
4340 }
4341
4342 /// Settles a batch, taking the shared-state lock once rather than once per
4343 /// event. Restore emits these in loops that a tampered or pre-split store
4344 /// can drive into the thousands.
4345 pub(crate) fn settle_restored_message_failures(
4346 &mut self,
4347 events: impl IntoIterator<Item = Event>,
4348 ) {
4349 let running = self.protocol_is_running();
4350 for event in events {
4351 self.settle_one_restored_message_failure(event, true, running);
4352 }
4353 }
4354
4355 /// Whether [`Self::start`] has run and nothing has stopped it since.
4356 ///
4357 /// Two callers want different things from the same fact. The restore
4358 /// paths above want to know whether an emit reaches an application at
4359 /// all. The document layer wants to know whether a frame it is about to
4360 /// build can leave the device, because a send attempted before the
4361 /// transports are up stamps a rate-limit window against an offer that
4362 /// was never carried.
4363 pub(crate) fn protocol_is_running(&self) -> bool {
4364 lock_shared_state(&self.shared_state)
4365 .map(|state| state.state == ProtocolState::Running)
4366 .unwrap_or(false)
4367 }
4368
4369 fn settle_one_restored_message_failure(
4370 &mut self,
4371 event: Event,
4372 telemetry: bool,
4373 running: bool,
4374 ) {
4375 if running {
4376 self.emit_settlement(event, telemetry);
4377 return;
4378 }
4379 // Every other accumulation on the restore path has an explicit ceiling
4380 // and logs what it dropped; this one is retained until `start()`, which
4381 // may never come, so it gets the same treatment. Keeping the *oldest*
4382 // is deliberate: the settlements a restore produces first are the ones
4383 // for records it examined first, and dropping those in favour of later
4384 // ones would bias the survivors by backend listing order.
4385 if self.deferred_restore_settlements.len() >= MAX_DEFERRED_RESTORE_SETTLEMENTS {
4386 self.suppressed_restore_settlements += 1;
4387 return;
4388 }
4389 self.deferred_restore_settlements.push((event, telemetry));
4390 }
4391
4392 /// Returns an iterator over outbox messages (test-only).
4393 #[cfg(test)]
4394 pub(crate) fn outbox_messages(&self) -> impl Iterator<Item = &Message> {
4395 self.outbox.values().map(|e| &e.message)
4396 }
4397
4398 /// Returns the total number of entries across outbox and media_outbox (test-only).
4399 #[cfg(test)]
4400 pub(crate) fn outbox_entry_count(&self) -> usize {
4401 self.outbox.len() + self.media_outbox.len()
4402 }
4403
4404 /// Clears all outbox entries (test-only).
4405 #[cfg(test)]
4406 pub(crate) fn clear_outbox(&mut self) {
4407 self.outbox.clear();
4408 }
4409
4410 /// Returns `true` when `message_id` is anywhere in the reliability
4411 /// machinery — awaiting an ACK, held in the outbox, or queued for retry
4412 /// (test-only).
4413 ///
4414 /// Exists for the fire-and-forget frames (relay hints, delivery ACKs),
4415 /// whose correctness is defined by leaving *no* trace here: an un-ACKable
4416 /// frame that registers a pending ACK is retransmitted until the retry cap
4417 /// and then reported failed.
4418 #[cfg(test)]
4419 pub(crate) fn is_tracked_for_delivery(&self, message_id: &MessageId) -> bool {
4420 self.ack_manager.is_waiting_for_ack(message_id)
4421 || self.outbox.contains_key(message_id)
4422 || self.media_outbox.contains_key(message_id)
4423 || self.retry_queue.contains(&message_id.as_str())
4424 }
4425
4426 /// Returns a reference to the MLS manager Arc (test-only).
4427 #[cfg(test)]
4428 pub(crate) fn mls_manager_for_testing(&self) -> &Arc<RwLock<offline_protocol_mls::MlsManager>> {
4429 self.mls_manager
4430 .as_ref()
4431 .expect("MLS not initialized in test")
4432 }
4433
4434 /// Acquires a read guard on the MLS manager.
4435 ///
4436 /// Returns an error if MLS is not initialized or the lock is poisoned.
4437 pub(crate) fn read_mls_guard(
4438 &self,
4439 ) -> Result<std::sync::RwLockReadGuard<'_, offline_protocol_mls::MlsManager>> {
4440 let mls = self
4441 .mls_manager
4442 .as_ref()
4443 .ok_or_else(|| Error::Other("MLS not initialized".to_string()))?;
4444 mls.read()
4445 .map_err(|_| Error::Other("MLS lock poisoned".to_string()))
4446 }
4447}
4448
4449#[cfg(test)]
4450pub(crate) mod tests;