offline_protocol/protocol/
custodian.rs1use std::time::{Duration, Instant};
11
12use chrono::Utc;
13use offline_protocol_core::{Message, MessageId, MessagePriority};
14use offline_protocol_router::RelayPriority;
15use tracing::{debug, info, warn};
16
17use super::custody::{
18 decode_receipt, encode_receipt, CustodyCandidate, CustodyRefusal, CustodyStats, HeldFrame,
19 MAX_CUSTODY_RECEIPTS_PER_MESSAGE,
20};
21use super::mesh_relay::{PendingRelay, RelayRejection};
22use super::prefixes::internal_prefixes;
23use super::storage::PruneAllowance;
24use super::types::{storage_keys, CustodyRecord, CUSTODY_RECORD_VERSION};
25use super::OfflineProtocol;
26use crate::config::CustodyConfig;
27use crate::{Error, ProtocolStateError, Result};
28
29pub(crate) const CUSTODY_SWEEP_INTERVAL: Duration = Duration::from_secs(60);
33
34impl OfflineProtocol {
35 pub fn custody_stats(&self) -> CustodyStats {
38 self.custody.stats()
39 }
40
41 pub fn custody_config(&self) -> &CustodyConfig {
43 self.custody.config()
44 }
45
46 pub fn erase_custody(&mut self) -> Result<()> {
54 let ids = self.custody.erase();
55 self.mesh_relay.drop_held();
57 self.custody_receipts.clear();
58 let Some(storage) = self.protocol_state_storage.clone() else {
59 return Ok(());
60 };
61
62 let mut first_error: Option<String> = None;
63 let mut record_error = |err: ProtocolStateError| {
64 if first_error.is_none() {
65 first_error = Some(err.to_string());
66 }
67 };
68
69 for id in ids {
70 if let Err(err) = storage.delete(storage_keys::CUSTODY, &id.as_str()) {
71 record_error(err);
72 }
73 }
74 match storage.list_keys(storage_keys::CUSTODY) {
78 Ok(keys) => {
79 for key in keys {
80 if let Err(err) = storage.delete(storage_keys::CUSTODY, &key) {
81 record_error(err);
82 }
83 }
84 }
85 Err(ProtocolStateError::NotFound(_)) => {}
86 Err(err) => record_error(err),
87 }
88
89 match first_error {
90 Some(detail) => Err(Error::Other(format!(
91 "failed to delete every custody record: {detail}"
92 ))),
93 None => Ok(()),
94 }
95 }
96
97 pub(super) fn judge_at_drop_point(&mut self, relay: PendingRelay) {
106 if relay.custody_target.is_some() {
107 self.return_held_to_store(relay);
108 return;
109 }
110
111 let PendingRelay {
112 message,
113 arrival_peer,
114 custody_request,
115 ..
116 } = relay;
117
118 let session_tier = arrival_peer
119 .as_deref()
120 .is_some_and(|peer| self.confirmed_sessions.contains(peer));
121 let battery_ok = !self.custody.is_enabled()
125 || custody_request.is_none()
126 || self.battery_allows_relaying();
127 let now_ms = Utc::now().timestamp_millis();
128
129 let message_id = message.id.clone();
130 let verdict = self.custody.judge(CustodyCandidate {
131 message,
132 request: custody_request.as_deref(),
133 arrival_peer: arrival_peer.as_deref(),
134 session_tier,
135 battery_ok,
136 now_ms,
137 });
138
139 match verdict {
140 Ok(accepted) => {
141 for evicted in &accepted.evicted {
142 self.delete_custody_record(evicted);
143 }
144 if let Some(held) = self.custody.get(&accepted.id) {
145 self.persist_custody_record(held);
146 }
147 debug!(
148 message_id = %message_id,
149 depositor = %accepted.depositor,
150 class = %accepted.class,
151 evicted = accepted.evicted.len(),
152 "Took a frame into custody"
153 );
154 self.send_custody_receipt(&accepted.depositor, &accepted.id);
155 }
156 Err(CustodyRefusal::Duplicate) => {
157 debug!(message_id = %message_id, "Deposit of a frame already held; absorbed");
158 }
159 Err(refusal) => {
160 debug!(
161 message_id = %message_id,
162 reason = refusal.as_str(),
163 "Frame abandoned at the drop point and not taken into custody"
164 );
165 }
166 }
167 }
168
169 pub(super) fn return_held_to_store(&mut self, relay: PendingRelay) {
171 self.custody.returned(&relay.message.id);
172 }
173
174 pub(super) fn record_custody_transmission(
177 &mut self,
178 relay: &PendingRelay,
179 reached_recipient: bool,
180 ) {
181 let id = &relay.message.id;
182 if reached_recipient {
183 if self.custody.record_delivered(id).is_some() {
184 self.delete_custody_record(id);
185 }
186 } else {
187 self.custody.record_re_originated(id);
188 }
189 }
190
191 pub(super) fn redeliver_custody_to(&mut self, peer: &str) {
205 if !self.custody.is_enabled() || self.custody.is_empty() {
206 return;
207 }
208 if !self.config.relay.allow_relay
209 || matches!(self.config.relay.relay_priority, RelayPriority::Never)
210 {
211 return;
212 }
213 if !self
214 .transport_manager
215 .mesh_neighbors()
216 .iter()
217 .any(|neighbor| neighbor.peer_id == peer)
218 {
219 return;
220 }
221 if !self.battery_allows_relaying() {
222 return;
223 }
224
225 let now = Instant::now();
226 for id in self.custody.candidates_for(peer) {
227 let Some(message) = self.custody.get(&id).map(|held| held.message.clone()) else {
228 continue;
229 };
230 match self.mesh_relay.admit_held(message, peer, now) {
231 Ok(()) => {
232 self.custody.mark_queued(&id, peer);
233 debug!(
234 message_id = %id,
235 neighbor = %peer,
236 cause = "custody_redelivery",
237 "Queued a held frame toward a neighbor"
238 );
239 }
240 Err(RelayRejection::QueueFull) => break,
241 Err(_) => continue,
242 }
243 }
244 }
245
246 pub(super) fn sweep_custody(&mut self) {
249 let now = Instant::now();
250 if now.duration_since(self.custody_last_sweep) < CUSTODY_SWEEP_INTERVAL {
251 return;
252 }
253 self.custody_last_sweep = now;
254 self.sweep_custody_now(now, Utc::now().timestamp_millis());
255 }
256
257 pub(super) fn sweep_custody_now(&mut self, _now: Instant, now_ms: i64) {
259 self.custody_receipts.retain(|_, custodians| {
260 custodians.retain(|_, until_ms| *until_ms > now_ms);
261 !custodians.is_empty()
262 });
263
264 if self.custody.is_empty() {
265 return;
266 }
267 let hold_ms = self.custody.config().hold_ms;
268 let expired = self.custody.expire(now_ms, hold_ms);
269 for id in &expired {
270 self.delete_custody_record(id);
271 }
272 if !expired.is_empty() {
273 debug!(
274 count = expired.len(),
275 "Expired held frames at the end of their hold"
276 );
277 }
278 }
279
280 fn send_custody_receipt(&mut self, depositor: &str, held_id: &MessageId) {
289 if !self.peer_data_custody.contains(depositor) {
290 debug!(
291 depositor = %depositor,
292 "Receipt withheld: the depositor does not advertise the custody entry"
293 );
294 self.custody.record_receipt_dropped();
295 return;
296 }
297 if self.mls_manager.is_none() {
298 self.custody.record_receipt_dropped();
301 return;
302 }
303
304 let content = encode_receipt(&held_id.as_str(), self.custody.config().hold_ms);
305 let mut message = match self.create_message(
306 depositor,
307 content,
308 Some(MessagePriority::Low),
309 None,
310 ) {
311 Ok(message) => message,
312 Err(err) => {
313 warn!(depositor = %depositor, error = %err, "Could not build a custody receipt");
314 self.custody.record_receipt_dropped();
315 return;
316 }
317 };
318 message.requires_ack = false;
319 if let Err(err) = self.sign_control_message(&mut message) {
320 warn!(depositor = %depositor, error = %err, "Could not sign a custody receipt");
321 self.custody.record_receipt_dropped();
322 return;
323 }
324
325 self.deduplicator.mark_seen_local(message.id.clone());
328 self.mesh_relay.mark_handled(&message.id.as_str());
329
330 match self.transport_manager.send_to_neighbor(depositor, &message) {
331 Ok(transport) => {
332 debug!(
333 depositor = %depositor,
334 held = %held_id,
335 transport = ?transport,
336 "Sent a custody receipt"
337 );
338 self.custody.record_receipt_sent();
339 }
340 Err(err) => {
341 debug!(
342 depositor = %depositor,
343 held = %held_id,
344 error = %err,
345 "Dropped a custody receipt: the arrival link is gone"
346 );
347 self.custody.record_receipt_dropped();
348 }
349 }
350 }
351
352 pub(super) fn handle_custody_receipt(
367 &mut self,
368 custodian: &str,
369 body: &str,
370 minted_at_ms: i64,
371 ) {
372 let receipt = match decode_receipt(body) {
373 Ok(receipt) => receipt,
374 Err(err) => {
375 debug!(custodian = %custodian, error = ?err, "Refused a malformed custody receipt");
376 self.custody.record_receipt_ignored();
377 return;
378 }
379 };
380 let Ok(id) = MessageId::from_str(&receipt.id) else {
381 self.custody.record_receipt_ignored();
382 return;
383 };
384 if !self.outbox.contains_key(&id) {
385 debug!(custodian = %custodian, message_id = %id, "Custody receipt names nothing in the outbox");
386 self.custody.record_receipt_ignored();
387 return;
388 }
389
390 let hold_ms = receipt
396 .hold_ms
397 .min(self.config.reliability.retry.outbox_max_lifetime_ms);
398 let until_ms = minted_at_ms
403 .min(Utc::now().timestamp_millis())
404 .saturating_add(i64::try_from(hold_ms).unwrap_or(i64::MAX));
405
406 let custodians = self.custody_receipts.entry(id.clone()).or_default();
407 if !custodians.contains_key(custodian)
408 && custodians.len() >= MAX_CUSTODY_RECEIPTS_PER_MESSAGE
409 {
410 if let Some(oldest) = custodians
413 .iter()
414 .min_by_key(|(_, until_ms)| **until_ms)
415 .map(|(peer, _)| peer.clone())
416 {
417 custodians.remove(&oldest);
418 }
419 }
420 custodians.insert(custodian.to_string(), until_ms);
421 debug!(custodian = %custodian, message_id = %id, hold_ms, "Recorded a custody receipt");
422 self.custody.record_receipt_received();
423 }
424
425 pub(super) fn custody_class_for(&self, message: &Message) -> Option<&'static str> {
434 if !message.content.starts_with(internal_prefixes::ENCRYPTED) {
435 return None;
436 }
437 let plaintext = self
438 .outbox
439 .get(&message.id)
440 .and_then(|entry| entry.reseal.as_ref())
441 .map(|reseal| reseal.content.as_str())
442 .or_else(|| {
443 self.pending_reseal
444 .get(&message.id)
445 .map(|reseal| reseal.content.as_str())
446 })?;
447 #[cfg(feature = "data")]
448 {
449 super::data_sync::custody_class_of_plaintext(plaintext)
450 }
451 #[cfg(not(feature = "data"))]
452 {
453 let _ = plaintext;
456 None
457 }
458 }
459
460 pub(super) fn custody_suppressed_toward(&self, id: &MessageId, custodian: &str) -> bool {
463 self.custody_receipts
464 .get(id)
465 .and_then(|custodians| custodians.get(custodian))
466 .is_some_and(|until_ms| *until_ms > Utc::now().timestamp_millis())
467 }
468
469 fn persist_custody_record(&self, held: &HeldFrame) {
473 let Some(storage) = &self.protocol_state_storage else {
474 return;
475 };
476 let record = CustodyRecord {
477 version: CUSTODY_RECORD_VERSION,
478 depositor: held.depositor.clone(),
479 message: held.message.clone(),
480 accepted_at_ms: held.accepted_at_ms,
481 class: held.class.clone(),
482 };
483 let data = match serde_json::to_vec(&record) {
484 Ok(data) => data,
485 Err(err) => {
486 warn!(message_id = %held.message.id, error = %err, "Failed to serialize a custody record");
487 return;
488 }
489 };
490 if let Err(err) = self.write_state_record(
491 storage.as_ref(),
492 storage_keys::CUSTODY,
493 &held.message.id.as_str(),
494 &data,
495 ) {
496 warn!(message_id = %held.message.id, error = %err, "Failed to persist a custody record");
497 }
498 }
499
500 fn delete_custody_record(&self, id: &MessageId) {
504 if let Some(storage) = &self.protocol_state_storage {
505 if let Err(err) = storage.delete(storage_keys::CUSTODY, &id.as_str()) {
506 warn!(message_id = %id, error = %err, "Failed to delete a custody record");
507 }
508 }
509 }
510
511 pub(super) fn restore_custody(&mut self, allowance: &mut PruneAllowance) {
522 let Some(storage) = self.protocol_state_storage.clone() else {
523 return;
524 };
525 let keys = match storage.list_keys(storage_keys::CUSTODY) {
526 Ok(keys) => keys,
527 Err(ProtocolStateError::NotFound(_)) => return,
528 Err(err) => {
529 warn!(error = %err, "Failed to list custody records");
530 return;
531 }
532 };
533 if keys.is_empty() {
534 return;
535 }
536
537 let cap = self.config.custody.max_entries.saturating_mul(4).max(1);
538 let enabled = self.custody.is_enabled();
539 let hold = i64::try_from(self.custody.config().hold_ms).unwrap_or(i64::MAX);
540 let now_ms = Utc::now().timestamp_millis();
541 let mut budget = allowance.refusing_private();
544 let mut erased = 0usize;
545 let mut expired = 0usize;
546 let mut deferred = 0usize;
547
548 let mut records: Vec<CustodyRecord> = Vec::new();
549 for key in keys.into_iter().take(cap) {
550 if !enabled {
551 if budget.claim() {
552 self.delete_custody_record_by_key(&key);
553 erased += 1;
554 } else {
555 deferred += 1;
556 }
557 continue;
558 }
559 let data = match self.read_state_record(storage.as_ref(), storage_keys::CUSTODY, &key) {
560 Ok(Some(data)) => data,
561 Ok(None) => continue,
562 Err(err) => {
563 debug!(key = %key, error = %err, "Custody record could not be read; leaving it");
564 continue;
565 }
566 };
567 let record = match serde_json::from_slice::<CustodyRecord>(&data) {
568 Ok(record) if record.version == CUSTODY_RECORD_VERSION => record,
569 Ok(record) => {
570 warn!(key = %key, version = record.version, "Dropping a custody record of an unknown version");
571 if budget.claim() {
572 self.delete_custody_record_by_key(&key);
573 }
574 continue;
575 }
576 Err(err) => {
577 warn!(key = %key, error = %err, "Dropping a corrupted custody record");
578 if budget.claim() {
579 self.delete_custody_record_by_key(&key);
580 }
581 continue;
582 }
583 };
584 if record.message.id.as_str() != key {
585 warn!(key = %key, message_id = %record.message.id, "Dropping a custody record filed under another id");
586 if budget.claim() {
587 self.delete_custody_record_by_key(&key);
588 }
589 continue;
590 }
591 if now_ms.saturating_sub(record.accepted_at_ms) > hold {
592 if budget.claim() {
593 self.delete_custody_record_by_key(&key);
594 expired += 1;
595 } else {
596 deferred += 1;
597 }
598 continue;
599 }
600 records.push(record);
601 }
602
603 records.sort_by(|left, right| {
604 left.accepted_at_ms
605 .cmp(&right.accepted_at_ms)
606 .then_with(|| left.message.id.as_str().cmp(&right.message.id.as_str()))
607 });
608
609 let mut restored = 0usize;
610 for record in records {
611 let key = record.message.id.as_str();
612 if self.custody.admit_restored(
613 record.message,
614 record.depositor,
615 record.accepted_at_ms,
616 record.class,
617 ) {
618 restored += 1;
619 } else if budget.claim() {
620 self.delete_custody_record_by_key(&key);
621 } else {
622 deferred += 1;
623 }
624 }
625
626 if enabled {
627 info!(restored, expired, deferred, "Restored custody records");
628 } else if erased > 0 || deferred > 0 {
629 info!(
630 erased,
631 deferred, "Erased custody records left by a launch with custody enabled"
632 );
633 }
634 }
635
636 fn delete_custody_record_by_key(&self, key: &str) {
637 if let Some(storage) = &self.protocol_state_storage {
638 if let Err(err) = storage.delete(storage_keys::CUSTODY, key) {
639 warn!(key = %key, error = %err, "Failed to delete a custody record");
640 }
641 }
642 }
643}