use super::state_crypto::{StateRecordCipher, SEALED_RECORD_OVERHEAD, STATE_RECORD_KEY_BYTES};
use super::{
lifetime_expired, storage_keys, MediaTransferDescriptor, OfflineProtocol, OutboxEntry,
PeerCapabilities, PendingDecryptRecord, PendingMessage, PendingMessageRecord,
ReceivedKeyPackage, SessionState, WelcomeDeliveryState, WelcomeLifecycleRecord,
DATA_CUSTODY_V1, DATA_GROUP_BLOB_V1, DATA_GROUP_V1, DATA_INTEREST_V1, DATA_MEDIA_V1,
DATA_SYNC_V1, DATA_TOMBSTONE_V1, MAX_BLOCKED_USERS, MAX_KEY_PACKAGE_SENT_TO,
MAX_MIGRATED_PENDING_WRITES_PER_LAUNCH, MAX_PENDING_KEY_PACKAGES, MAX_PENDING_MESSAGES_GLOBAL,
MAX_PENDING_MESSAGES_PER_PEER, MAX_PENDING_MESSAGE_BYTES_GLOBAL,
MAX_PENDING_MESSAGE_BYTES_PER_PEER, MAX_PERSISTED_CAPABILITY_VERSIONS,
MAX_PROTOCOL_STATE_RECORD_BYTES, MLS_ENVELOPE_COMPACT_V1, PENDING_DECRYPT_RECORD_VERSION,
RICH_PAYLOAD_V1, WELCOME_LIFECYCLE_TTL_SECS,
};
use super::{
DedupSeenRecord, DEDUP_PERSIST_DIRTY_THRESHOLD, DEDUP_PERSIST_INTERVAL,
DEDUP_SEEN_RECORD_VERSION, MAX_PERSISTED_DEDUP_IDS,
};
use crate::constants::{MAX_MEDIA_DESCRIPTORS, MAX_OUTBOX_ENTRIES};
use crate::events::DecryptionFailureCode;
use crate::{Error, Event, ProtocolStateError, ProtocolStateResult, ProtocolStateStorage, Result};
use chrono::{Duration as ChronoDuration, Utc};
use offline_protocol_core::{LamportClock, Message, MessageId};
use offline_protocol_mls::{MlsManager, MlsStorage, StorageError};
use offline_protocol_transport::{NostrKeypair, NostrTransport, TransportType};
use serde::de::DeserializeOwned;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
use std::time::Instant;
use tracing::{debug, info, warn};
use zeroize::Zeroizing;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StateCategory {
PendingMessages,
PendingMessageEntries,
PendingDecryptEntries,
Custody,
Outbox,
MediaDescriptors,
PeerKeyPackages,
PeerCapabilities,
SessionStates,
WelcomeLifecycles,
BlockedUsers,
BothCreateAwaitingDecrypt,
LamportClock,
DedupSeenIds,
NostrWatermark,
NostrKeyPackageSlots,
NostrDiscoveryClaim,
StateAdoption,
DataDocs,
DataDeltaLog,
DataSpaces,
DataSync,
DataDocMeta,
}
impl StateCategory {
pub(crate) fn from_key_type(key_type: &str) -> Option<Self> {
Some(match key_type {
storage_keys::PENDING_MESSAGES => Self::PendingMessages,
storage_keys::PENDING_MESSAGE_ENTRIES => Self::PendingMessageEntries,
storage_keys::PENDING_DECRYPT_ENTRIES => Self::PendingDecryptEntries,
storage_keys::CUSTODY => Self::Custody,
storage_keys::OUTBOX => Self::Outbox,
storage_keys::MEDIA_DESCRIPTORS => Self::MediaDescriptors,
storage_keys::PEER_KEY_PACKAGES => Self::PeerKeyPackages,
storage_keys::PEER_CAPABILITIES => Self::PeerCapabilities,
storage_keys::SESSION_STATES => Self::SessionStates,
storage_keys::WELCOME_LIFECYCLES => Self::WelcomeLifecycles,
storage_keys::BLOCKED_USERS => Self::BlockedUsers,
storage_keys::BOTH_CREATE_AWAITING_DECRYPT => Self::BothCreateAwaitingDecrypt,
storage_keys::LAMPORT_CLOCK => Self::LamportClock,
storage_keys::DEDUP_SEEN_IDS => Self::DedupSeenIds,
storage_keys::NOSTR_WATERMARK => Self::NostrWatermark,
storage_keys::NOSTR_KEY_PACKAGE_SLOTS => Self::NostrKeyPackageSlots,
storage_keys::NOSTR_DISCOVERY_CLAIM => Self::NostrDiscoveryClaim,
storage_keys::STATE_ADOPTION => Self::StateAdoption,
storage_keys::DATA_DOCS => Self::DataDocs,
storage_keys::DATA_DELTA_LOG => Self::DataDeltaLog,
storage_keys::DATA_SPACES => Self::DataSpaces,
storage_keys::DATA_SYNC => Self::DataSync,
storage_keys::DATA_DOC_META => Self::DataDocMeta,
_ => return None,
})
}
#[cfg(any(test, feature = "file-store"))]
pub(crate) const ALL: &'static [Self] = &[
Self::PendingMessages,
Self::PendingMessageEntries,
Self::PendingDecryptEntries,
Self::Custody,
Self::Outbox,
Self::MediaDescriptors,
Self::PeerKeyPackages,
Self::PeerCapabilities,
Self::SessionStates,
Self::WelcomeLifecycles,
Self::BlockedUsers,
Self::BothCreateAwaitingDecrypt,
Self::LamportClock,
Self::DedupSeenIds,
Self::NostrWatermark,
Self::NostrKeyPackageSlots,
Self::NostrDiscoveryClaim,
Self::StateAdoption,
Self::DataDocs,
Self::DataDeltaLog,
Self::DataSpaces,
Self::DataSync,
Self::DataDocMeta,
];
#[cfg(any(test, feature = "file-store"))]
pub(crate) fn key_type(self) -> &'static str {
match self {
Self::PendingMessages => storage_keys::PENDING_MESSAGES,
Self::PendingMessageEntries => storage_keys::PENDING_MESSAGE_ENTRIES,
Self::PendingDecryptEntries => storage_keys::PENDING_DECRYPT_ENTRIES,
Self::Custody => storage_keys::CUSTODY,
Self::Outbox => storage_keys::OUTBOX,
Self::MediaDescriptors => storage_keys::MEDIA_DESCRIPTORS,
Self::PeerKeyPackages => storage_keys::PEER_KEY_PACKAGES,
Self::PeerCapabilities => storage_keys::PEER_CAPABILITIES,
Self::SessionStates => storage_keys::SESSION_STATES,
Self::WelcomeLifecycles => storage_keys::WELCOME_LIFECYCLES,
Self::BlockedUsers => storage_keys::BLOCKED_USERS,
Self::BothCreateAwaitingDecrypt => storage_keys::BOTH_CREATE_AWAITING_DECRYPT,
Self::LamportClock => storage_keys::LAMPORT_CLOCK,
Self::DedupSeenIds => storage_keys::DEDUP_SEEN_IDS,
Self::NostrWatermark => storage_keys::NOSTR_WATERMARK,
Self::NostrKeyPackageSlots => storage_keys::NOSTR_KEY_PACKAGE_SLOTS,
Self::NostrDiscoveryClaim => storage_keys::NOSTR_DISCOVERY_CLAIM,
Self::StateAdoption => storage_keys::STATE_ADOPTION,
Self::DataDocs => storage_keys::DATA_DOCS,
Self::DataDeltaLog => storage_keys::DATA_DELTA_LOG,
Self::DataSpaces => storage_keys::DATA_SPACES,
Self::DataSync => storage_keys::DATA_SYNC,
Self::DataDocMeta => storage_keys::DATA_DOC_META,
}
}
fn requires_sealing(self) -> bool {
match self {
Self::PendingMessages
| Self::PendingMessageEntries
| Self::PendingDecryptEntries
| Self::Custody
| Self::Outbox
| Self::MediaDescriptors
| Self::PeerKeyPackages
| Self::NostrKeyPackageSlots
| Self::NostrDiscoveryClaim
| Self::DataDocs
| Self::DataDeltaLog
| Self::DataSpaces
| Self::DataSync
| Self::DataDocMeta
| Self::DedupSeenIds => true,
Self::PeerCapabilities
| Self::SessionStates
| Self::WelcomeLifecycles
| Self::BlockedUsers
| Self::BothCreateAwaitingDecrypt
| Self::LamportClock
| Self::NostrWatermark
| Self::StateAdoption => false,
}
}
}
#[cfg(feature = "file-store")]
pub(crate) fn sealed_state_key_types() -> impl Iterator<Item = &'static str> {
StateCategory::ALL
.iter()
.filter(|category| category.requires_sealing())
.map(|category| category.key_type())
}
pub(crate) enum StoredRecordKey {
Usable(StateRecordCipher),
Absent,
Unrecoverable(String),
}
pub(crate) fn stored_state_record_key(
storage: &dyn MlsStorage,
) -> std::result::Result<StoredRecordKey, StorageError> {
match storage.load(
storage_keys::STATE_RECORD_KEY,
storage_keys::STATE_RECORD_KEY_ID,
) {
Ok(Some(bytes)) if bytes.len() == STATE_RECORD_KEY_BYTES => {
let bytes = Zeroizing::new(bytes);
let mut key = Zeroizing::new([0u8; STATE_RECORD_KEY_BYTES]);
key.copy_from_slice(&bytes);
Ok(StoredRecordKey::Usable(StateRecordCipher::new(&key)))
}
Ok(None) => Ok(StoredRecordKey::Absent),
Ok(Some(bytes)) => Ok(StoredRecordKey::Unrecoverable(format!(
"stored key is {} bytes, expected {STATE_RECORD_KEY_BYTES}",
bytes.len()
))),
Err(StorageError::CorruptedData(reason)) => Ok(StoredRecordKey::Unrecoverable(reason)),
Err(err) => Err(err),
}
}
fn record_requires_sealing(key_type: &str) -> bool {
StateCategory::from_key_type(key_type).is_some_and(StateCategory::requires_sealing)
}
pub(crate) enum StateRecord {
Missing,
Unreadable,
Unavailable,
Present(Vec<u8>),
}
impl StateRecord {
fn into_bytes(self) -> Option<Vec<u8>> {
match self {
Self::Present(data) => Some(data),
Self::Missing | Self::Unreadable | Self::Unavailable => None,
}
}
}
enum PendingRestore {
Absent,
Lost,
Unavailable,
Restored(Vec<PendingMessage>),
}
pub(crate) enum RestorableRecord<T> {
Present(T),
Absent,
Unavailable,
}
pub(super) const MAX_RESTORE_KEYS_PER_CATEGORY: usize = 4 * MAX_PENDING_MESSAGES_GLOBAL;
pub(crate) const PENDING_DECRYPT_PERSISTED_MAX_AGE_MS: i64 = 7 * 24 * 60 * 60 * 1000;
const OUTBOX_RESTORE_KEY_CAP: usize = 4 * MAX_OUTBOX_ENTRIES;
pub(super) const MAX_PENDING_RESTORE_ENTRIES: usize = 4 * MAX_PENDING_MESSAGES_GLOBAL;
pub(super) const MAX_RESTORE_PRUNE_DELETES: usize = 512;
#[derive(Default)]
struct RestoredPendingAdmission {
global_count: usize,
global_bytes: usize,
capacity_evicted: Vec<MessageId>,
}
#[derive(Default)]
struct PendingRestoreWalk {
grouped: HashMap<String, Vec<PendingMessage>>,
seen: HashSet<String>,
lost_ids: Vec<String>,
seen_in_legacy: HashMap<String, String>,
migrated: HashMap<String, String>,
migrated_recipients: Vec<String>,
lost_recipients: Vec<String>,
unaddressable: Vec<MessageId>,
examined_entries: usize,
entry_bound_reached: bool,
prune_bound_reached: bool,
write_bound_reached: bool,
}
fn sort_pending_queue(messages: &mut [PendingMessage]) {
messages.sort_by(|left, right| {
left.queued_at
.cmp(&right.queued_at)
.then_with(|| left.message_id.as_str().cmp(&right.message_id.as_str()))
});
}
pub(super) const ADVISORY_PRUNE_WALKS: usize = 5;
pub(super) const MIN_ADVISORY_PRUNE_DELETES: usize = 64;
const _: () = assert!(
ADVISORY_PRUNE_WALKS * MIN_ADVISORY_PRUNE_DELETES <= MAX_RESTORE_PRUNE_DELETES,
"the advisory floors must fit inside one pool, or the first walk's ceiling \
underflows to zero and it can never prune at all"
);
const _: () = assert!(
MAX_RESTORE_PRUNE_DELETES >= MAX_OUTBOX_ENTRIES,
"an outbox full of legacy confirmation probes must clear in one launch, \
or the walk breaks on them and defers restoring real messages"
);
pub(crate) struct PruneAllowance {
remaining: usize,
advisory_walks_left: usize,
}
impl PruneAllowance {
pub(crate) fn pool() -> Self {
Self {
remaining: MAX_RESTORE_PRUNE_DELETES,
advisory_walks_left: ADVISORY_PRUNE_WALKS,
}
}
pub(super) fn refusing(&mut self) -> PruneBudget<'_> {
let later = self.advisory_walks_left.saturating_sub(1);
self.advisory_walks_left = later;
let reserved = later.saturating_mul(MIN_ADVISORY_PRUNE_DELETES);
let ceiling = self.remaining.saturating_sub(reserved);
PruneBudget::new(&mut self.remaining, ceiling, true)
}
pub(super) fn refusing_private(&mut self) -> PruneBudget<'_> {
let ceiling = self.remaining;
PruneBudget::new(&mut self.remaining, ceiling, true)
}
fn counting(&mut self) -> PruneBudget<'_> {
let ceiling = self.remaining;
PruneBudget::new(&mut self.remaining, ceiling, false)
}
}
pub(super) struct PruneBudget<'a> {
remaining: &'a mut usize,
ceiling: usize,
drawn: usize,
pub(super) spent: usize,
pub(super) exhausted: bool,
refusable: bool,
}
impl<'a> PruneBudget<'a> {
fn new(remaining: &'a mut usize, ceiling: usize, refusable: bool) -> Self {
Self {
remaining,
ceiling,
drawn: 0,
spent: 0,
exhausted: false,
refusable,
}
}
pub(super) fn claim(&mut self) -> bool {
if self.is_spent() {
self.exhausted = true;
if self.refusable {
return false;
}
} else {
*self.remaining -= 1;
self.drawn = self.drawn.saturating_add(1);
}
self.spent = self.spent.saturating_add(1);
true
}
fn is_spent(&self) -> bool {
*self.remaining == 0 || self.drawn >= self.ceiling
}
}
fn claim_prune(budget: &mut Option<&mut PruneBudget<'_>>) -> bool {
match budget {
Some(budget) => budget.claim(),
None => true,
}
}
impl OfflineProtocol {
pub(crate) fn write_state_record(
&self,
storage: &dyn ProtocolStateStorage,
key_type: &str,
key_id: &str,
data: &[u8],
) -> ProtocolStateResult<()> {
if data.len() > MAX_PROTOCOL_STATE_RECORD_BYTES {
return Err(ProtocolStateError::StoreFailed(format!(
"record is {} bytes, over the {} byte limit",
data.len(),
MAX_PROTOCOL_STATE_RECORD_BYTES
)));
}
let Some(category) = StateCategory::from_key_type(key_type) else {
return Err(ProtocolStateError::StoreFailed(format!(
"unknown protocol-state category '{}'; refusing to persist \
a record whose sensitivity has not been decided",
key_type
)));
};
if !category.requires_sealing() {
return storage.store(key_type, key_id, data);
}
let Some(cipher) = &self.state_record_cipher else {
return Err(ProtocolStateError::StoreFailed(
"protocol state record key unavailable; refusing to persist in the clear"
.to_string(),
));
};
let Some(sealed) = cipher.seal(key_type, key_id, data) else {
return Err(ProtocolStateError::StoreFailed(
"failed to seal protocol state record".to_string(),
));
};
storage.store(key_type, key_id, &sealed)
}
pub(crate) fn read_state_record(
&self,
storage: &dyn ProtocolStateStorage,
key_type: &str,
key_id: &str,
) -> ProtocolStateResult<Option<Vec<u8>>> {
Ok(self
.read_state_record_detailed(storage, key_type, key_id)?
.into_bytes())
}
fn load_state_bytes(
storage: &dyn ProtocolStateStorage,
key_type: &str,
key_id: &str,
) -> ProtocolStateResult<Option<Vec<u8>>> {
match storage.load(key_type, key_id) {
Ok(data) => Ok(data),
Err(ProtocolStateError::NotFound(_) | ProtocolStateError::Corrupted(_)) => Ok(None),
Err(e) => Err(e),
}
}
fn list_state_keys(
storage: &dyn ProtocolStateStorage,
key_type: &str,
) -> ProtocolStateResult<Vec<String>> {
match storage.list_keys(key_type) {
Ok(keys) => Ok(keys),
Err(ProtocolStateError::NotFound(_)) => Ok(Vec::new()),
Err(e) => Err(e),
}
}
pub(crate) fn read_state_record_detailed(
&self,
storage: &dyn ProtocolStateStorage,
key_type: &str,
key_id: &str,
) -> ProtocolStateResult<StateRecord> {
self.read_state_record_detailed_budgeted(storage, key_type, key_id, None)
}
fn read_state_record_detailed_budgeted(
&self,
storage: &dyn ProtocolStateStorage,
key_type: &str,
key_id: &str,
mut budget: Option<&mut PruneBudget<'_>>,
) -> ProtocolStateResult<StateRecord> {
let data = match storage.load(key_type, key_id) {
Ok(Some(data)) => data,
Ok(None) => return Ok(StateRecord::Missing),
Err(ProtocolStateError::NotFound(_)) => return Ok(StateRecord::Missing),
Err(ProtocolStateError::Corrupted(detail)) => {
warn!(
key_type = %key_type,
key_id = %key_id,
detail = %detail,
"Protocol state record reported corrupt by its store; dropping"
);
if claim_prune(&mut budget) {
let _ = storage.delete(key_type, key_id);
}
return Ok(StateRecord::Unreadable);
}
Err(e) => return Err(e),
};
let sealed = record_requires_sealing(key_type);
let limit = if sealed {
MAX_PROTOCOL_STATE_RECORD_BYTES.saturating_add(SEALED_RECORD_OVERHEAD)
} else {
MAX_PROTOCOL_STATE_RECORD_BYTES
};
if data.len() > limit {
warn!(
key_type = %key_type,
key_id = %key_id,
len = data.len(),
limit,
"Dropping oversized protocol state record"
);
if claim_prune(&mut budget) {
let _ = storage.delete(key_type, key_id);
}
return Ok(StateRecord::Unreadable);
}
if !sealed {
return Ok(StateRecord::Present(data));
}
let Some(cipher) = &self.state_record_cipher else {
warn!(
key_type = %key_type,
key_id = %key_id,
"Protocol state record key unavailable; skipping sealed record"
);
return Ok(StateRecord::Unavailable);
};
match cipher.open(key_type, key_id, &data) {
Some(plaintext) => Ok(StateRecord::Present(plaintext)),
None => {
warn!(
key_type = %key_type,
key_id = %key_id,
sealed = StateRecordCipher::looks_sealed(&data),
"Protocol state record failed to open; deleting"
);
if claim_prune(&mut budget) {
let _ = storage.delete(key_type, key_id);
}
Ok(StateRecord::Unreadable)
}
}
}
pub(crate) fn adopt_legacy_protocol_state(&mut self) {
let (Some(secure), Some(state)) = (
self.secure_storage.clone(),
self.protocol_state_storage.clone(),
) else {
return;
};
match Self::load_state_bytes(
state.as_ref(),
storage_keys::STATE_ADOPTION,
storage_keys::STATE_ADOPTION_ID,
) {
Ok(Some(_)) => return,
Ok(None) => {}
Err(e) => {
warn!(error = %e, "Could not read the protocol-state adoption marker; skipping adoption");
return;
}
}
let mut adopted = 0usize;
let mut failed = false;
let mut unadoptable: Vec<Event> = Vec::new();
for key_type in storage_keys::ADOPTABLE_STATE_KEY_TYPES {
let key_ids = match secure.list_keys(key_type) {
Ok(ids) => ids,
Err(e) => {
warn!(key_type = %key_type, error = %e, "Failed to list legacy protocol state");
failed = true;
continue;
}
};
if key_ids.len() > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
key_type = %key_type,
listed = key_ids.len(),
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Legacy protocol state listed more entries than any legitimate run can produce; adopting a prefix and retrying the rest next launch"
);
failed = true;
}
for key_id in key_ids.into_iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
match self.adopt_one_legacy_record(
secure.as_ref(),
state.as_ref(),
key_type,
&key_id,
&mut unadoptable,
) {
Ok(true) => adopted += 1,
Ok(false) => {}
Err(()) => failed = true,
}
}
}
for event in unadoptable {
self.settle_restored_message_failure(event);
}
if failed {
warn!(
adopted,
"Pre-split protocol state only partially adopted; will retry on the next launch"
);
return;
}
if let Err(e) = self.write_state_record(
state.as_ref(),
storage_keys::STATE_ADOPTION,
storage_keys::STATE_ADOPTION_ID,
&[],
) {
warn!(error = %e, "Failed to record the protocol-state adoption marker");
}
if adopted > 0 {
info!(
count = adopted,
"Adopted pre-split protocol state from secure storage"
);
}
}
fn adopt_one_legacy_record(
&self,
secure: &dyn MlsStorage,
state: &dyn ProtocolStateStorage,
key_type: &str,
key_id: &str,
unadoptable: &mut Vec<Event>,
) -> std::result::Result<bool, ()> {
let destination_destroyed = match self.read_state_record_detailed(state, key_type, key_id) {
Ok(StateRecord::Present(_)) => return Ok(false),
Ok(StateRecord::Missing) => false,
Ok(StateRecord::Unreadable) => true,
Ok(StateRecord::Unavailable) => {
warn!(
key_type = %key_type,
"Protocol state record unreadable this session; deferring adoption of its legacy twin"
);
return Err(());
}
Err(e) => {
warn!(key_type = %key_type, error = %e, "Failed to probe protocol state during adoption");
return Err(());
}
};
let data = match secure.load(key_type, key_id) {
Ok(Some(data)) => data,
Ok(None) => {
if destination_destroyed {
unadoptable.extend(Self::unadoptable_record_settlement(key_type, key_id));
}
return Ok(false);
}
Err(e) => {
warn!(key_type = %key_type, error = %e, "Failed to read legacy protocol state");
return Err(());
}
};
if data.len() > MAX_PROTOCOL_STATE_RECORD_BYTES {
warn!(
key_type = %key_type,
len = data.len(),
"Dropping oversized legacy protocol state record"
);
unadoptable.extend(Self::unadoptable_record_settlement(key_type, key_id));
let _ = secure.delete(key_type, key_id);
return Ok(false);
}
if let Err(e) = self.write_state_record(state, key_type, key_id, &data) {
warn!(key_type = %key_type, error = %e, "Failed to adopt legacy protocol state record");
return Err(());
}
if let Err(e) = secure.delete(key_type, key_id) {
debug!(key_type = %key_type, error = %e, "Failed to delete adopted legacy protocol state record");
}
Ok(true)
}
fn unadoptable_record_settlement(key_type: &str, key_id: &str) -> Option<Event> {
match key_type {
storage_keys::OUTBOX => Some(Self::unrecoverable_message_settlement(
key_id,
"Outbox entry from a previous version was too large to migrate",
0,
)),
storage_keys::PENDING_MESSAGES => Some(Event::convergence_diag(
"pending_state_lost".to_string(),
key_id.to_string(),
"Messages queued before the storage split exceeded the protocol-state record \
limit and could not be migrated"
.to_string(),
)),
_ => None,
}
}
fn unrecoverable_message_settlement(
key_id: &str,
reason: &'static str,
attempts: u32,
) -> Event {
MessageId::from_str(key_id).map_or_else(
|_| {
Event::convergence_diag(
"pending_state_lost".to_string(),
key_id.to_string(),
format!(
"{reason}, and its message id could not be recovered from the record key"
),
)
},
|message_id| Event::message_failed(message_id, reason, attempts),
)
}
pub(crate) fn restore_or_init_state_record_key(&mut self) {
self.state_record_cipher = None;
let Some(storage) = &self.secure_storage else {
return;
};
let unrecoverable = match stored_state_record_key(storage.as_ref()) {
Ok(StoredRecordKey::Usable(cipher)) => {
debug!("Restored protocol state record key from secure storage");
self.state_record_cipher = Some(cipher);
return;
}
Ok(StoredRecordKey::Absent) => None,
Ok(StoredRecordKey::Unrecoverable(reason)) => Some(reason),
Err(e) => {
warn!(
error = %e,
"Failed to load protocol state record key; \
sensitive protocol state will not be persisted this session"
);
return;
}
};
if let Some(reason) = &unrecoverable {
warn!(
reason = %reason,
"Protocol state record key is not a key and cannot be recovered; \
regenerating. Every record sealed under the old key is \
unrecoverable and will be settled as failed on restore; this is \
not routine key generation"
);
}
let key: Zeroizing<[u8; STATE_RECORD_KEY_BYTES]> = StateRecordCipher::generate_key();
if let Err(e) = storage.store(
storage_keys::STATE_RECORD_KEY,
storage_keys::STATE_RECORD_KEY_ID,
&*key,
) {
warn!(
error = %e,
"Failed to persist protocol state record key; \
sensitive protocol state will not be persisted this session"
);
return;
}
info!("Generated and persisted per-install protocol state record key");
self.state_record_cipher = Some(StateRecordCipher::new(&key));
}
pub(crate) fn persist_pending_message(
&self,
recipient: &str,
message: &PendingMessage,
) -> bool {
let Some(storage) = &self.protocol_state_storage else {
return false;
};
let record = PendingMessageRecord {
recipient: recipient.to_string(),
message: message.clone(),
};
match serde_json::to_vec(&record) {
Ok(data) => {
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::PENDING_MESSAGE_ENTRIES,
&record.message.message_id.as_str(),
&data,
) {
warn!(
recipient = %recipient,
message_id = %record.message.message_id,
error = %e,
"Failed to persist pending message"
);
return false;
}
true
}
Err(e) => {
warn!(
recipient = %recipient,
message_id = %record.message.message_id,
error = %e,
"Failed to serialize pending message"
);
false
}
}
}
pub(crate) fn delete_pending_message_from_storage(&self, message_id: &MessageId) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
if let Err(e) = storage.delete(storage_keys::PENDING_MESSAGE_ENTRIES, &message_id.as_str())
{
warn!(
message_id = %message_id,
error = %e,
"Failed to clear persisted pending message"
);
}
}
pub(crate) fn delete_pending_messages_from_storage<'a>(
&self,
message_ids: impl IntoIterator<Item = &'a MessageId>,
) {
for message_id in message_ids {
self.delete_pending_message_from_storage(message_id);
}
}
#[cfg(test)]
pub(crate) fn load_pending_messages_from_storage(
&self,
recipient: &str,
) -> Option<Vec<PendingMessage>> {
let storage = self.protocol_state_storage.as_ref()?;
let keys =
Self::list_state_keys(storage.as_ref(), storage_keys::PENDING_MESSAGE_ENTRIES).ok()?;
let mut messages: Vec<PendingMessage> = keys
.iter()
.filter_map(|key| {
let data = self
.read_state_record_detailed(
storage.as_ref(),
storage_keys::PENDING_MESSAGE_ENTRIES,
key,
)
.ok()?
.into_bytes()?;
let record = serde_json::from_slice::<PendingMessageRecord>(&data).ok()?;
(record.recipient == recipient).then_some(record.message)
})
.collect();
if messages.is_empty() {
return None;
}
sort_pending_queue(&mut messages);
for message in &mut messages {
message.measure();
}
Some(messages)
}
fn load_legacy_pending_queue(
&self,
recipient: &str,
mut budget: Option<&mut PruneBudget<'_>>,
) -> PendingRestore {
let Some(storage) = self.protocol_state_storage.as_ref() else {
return PendingRestore::Absent;
};
let record = self.read_state_record_detailed_budgeted(
storage.as_ref(),
storage_keys::PENDING_MESSAGES,
recipient,
budget.as_deref_mut(),
);
let data = match record {
Ok(StateRecord::Present(data)) => data,
Ok(StateRecord::Missing) => return PendingRestore::Absent,
Ok(StateRecord::Unreadable) => return PendingRestore::Lost,
Ok(StateRecord::Unavailable) | Err(_) => return PendingRestore::Unavailable,
};
let Ok(messages) = serde_json::from_slice::<Vec<PendingMessage>>(&data) else {
warn!(recipient = %recipient, "Dropping corrupted pending message record");
claim_prune(&mut budget);
let _ = storage.delete(storage_keys::PENDING_MESSAGES, recipient);
return PendingRestore::Lost;
};
PendingRestore::Restored(messages)
}
fn delete_legacy_pending_queue(&self, recipient: &str) -> bool {
if let Some(storage) = &self.protocol_state_storage {
if let Err(e) = storage.delete(storage_keys::PENDING_MESSAGES, recipient) {
warn!(
recipient = %recipient,
error = %e,
"Failed to clear migrated legacy pending queue"
);
return false;
}
}
true
}
fn admit_restored_pending_queue(
&mut self,
recipient: &str,
mut messages: Vec<PendingMessage>,
admission: &mut RestoredPendingAdmission,
) {
let overflow = messages.len().saturating_sub(MAX_PENDING_MESSAGES_PER_PEER);
if overflow > 0 {
admission
.capacity_evicted
.extend(messages.drain(..overflow).map(|message| message.message_id));
}
for message in &mut messages {
message.measure();
}
let mut peer_bytes: usize = messages
.iter()
.map(|message| message.serialized_bytes)
.sum();
let mut dropped_for_bytes = 0usize;
while peer_bytes > MAX_PENDING_MESSAGE_BYTES_PER_PEER && !messages.is_empty() {
let message = messages.remove(0);
peer_bytes = peer_bytes.saturating_sub(message.serialized_bytes);
admission.capacity_evicted.push(message.message_id);
dropped_for_bytes += 1;
}
if dropped_for_bytes > 0 {
warn!(
recipient = %recipient,
dropped = dropped_for_bytes,
limit = MAX_PENDING_MESSAGE_BYTES_PER_PEER,
"Restored pending queue exceeded the per-peer byte budget"
);
}
if !messages.is_empty() {
info!(recipient = %recipient, count = messages.len(), "Restored pending messages from storage");
admission.global_count += messages.len();
admission.global_bytes += peer_bytes;
self.pending_encrypted_messages
.insert(recipient.to_string(), messages);
}
while admission.global_count > MAX_PENDING_MESSAGES_GLOBAL
|| admission.global_bytes > MAX_PENDING_MESSAGE_BYTES_GLOBAL
{
let Some((_, message)) = self.evict_oldest_pending_message() else {
break;
};
admission.global_count = admission.global_count.saturating_sub(1);
admission.global_bytes = admission
.global_bytes
.saturating_sub(message.serialized_bytes);
admission.capacity_evicted.push(message.message_id);
}
}
pub(crate) fn restore_pending_messages(
&mut self,
allowance: &mut PruneAllowance,
) -> Result<()> {
let Some(storage) = self.protocol_state_storage.clone() else {
return Ok(());
};
let mut budget = allowance.counting();
let mut walk = PendingRestoreWalk::default();
self.restore_pending_message_entries(storage.as_ref(), &mut budget, &mut walk)?;
self.migrate_legacy_pending_queues(storage.as_ref(), &mut budget, &mut walk)?;
let mut admission = RestoredPendingAdmission::default();
let mut grouped: Vec<(String, Vec<PendingMessage>)> =
std::mem::take(&mut walk.grouped).into_iter().collect();
grouped.sort_by(|left, right| left.0.cmp(&right.0));
for (recipient, mut messages) in grouped {
sort_pending_queue(&mut messages);
self.admit_restored_pending_queue(&recipient, messages, &mut admission);
}
let RestoredPendingAdmission {
capacity_evicted, ..
} = admission;
let abandoned = self.persist_migrated_pending_queues(&mut budget, &mut walk);
let mut capacity_settlements = Vec::new();
for message_id in capacity_evicted {
match walk.migrated.get(&message_id.as_str()) {
Some(recipient) if abandoned.contains(recipient) => continue,
Some(_) => {}
None => {
if budget.is_spent() {
walk.prune_bound_reached = true;
continue;
}
budget.claim();
self.delete_pending_message_from_storage(&message_id);
}
}
capacity_settlements.push(Event::message_failed(
message_id,
"Pending session queue capacity exceeded",
0,
));
}
self.settle_restored_message_failures(capacity_settlements);
if walk.entry_bound_reached {
warn!(
examined = walk.examined_entries,
cap = MAX_PENDING_RESTORE_ENTRIES,
"Pending message store held more queued entries than one restore may walk; \
the remainder is left on disk for a later launch"
);
}
if walk.prune_bound_reached {
warn!(
deleted = budget.spent,
budget = MAX_RESTORE_PRUNE_DELETES,
"Pending message restore spent its per-launch delete budget; the remainder \
is left on disk for a later launch"
);
}
if walk.write_bound_reached {
warn!(
cap = MAX_MIGRATED_PENDING_WRITES_PER_LAUNCH,
"Legacy pending migration reached its per-launch write budget; the remaining \
queues are left on disk for a later launch"
);
}
self.settle_restored_message_failures(walk.unaddressable.into_iter().map(|message_id| {
Event::message_failed(
message_id,
"Recipient is not a valid user ID; queued message cannot be delivered",
0,
)
}));
self.settle_restored_message_failures(
walk.lost_ids
.iter()
.filter(|key| {
!walk
.seen_in_legacy
.get(*key)
.is_some_and(|recipient| abandoned.contains(recipient))
})
.map(|key| {
Self::unrecoverable_message_settlement(
key,
"Queued message awaiting session establishment could not be recovered \
from protocol-state storage",
0,
)
}),
);
for recipient in &walk.lost_recipients {
warn!(recipient = %recipient, "Persisted legacy pending queue was unreadable and has been dropped");
}
self.settle_restored_message_failures(walk.lost_recipients.into_iter().map(|recipient| {
Event::convergence_diag(
"pending_state_lost".to_string(),
recipient,
"Queued messages awaiting session establishment could not be recovered \
from protocol-state storage and have been dropped"
.to_string(),
)
}));
self.recompute_next_pending_message_expiry();
self.cleanup_expired_pending_messages();
Ok(())
}
fn persist_migrated_pending_queues(
&mut self,
budget: &mut PruneBudget<'_>,
walk: &mut PendingRestoreWalk,
) -> HashSet<String> {
let mut abandoned = HashSet::new();
let recipients = std::mem::take(&mut walk.migrated_recipients);
let mut written = 0usize;
for recipient in recipients {
if budget.is_spent() {
walk.prune_bound_reached = true;
self.forget_migrated_entries(&recipient, &walk.migrated, false);
abandoned.insert(recipient);
continue;
}
let survivors = self
.pending_encrypted_messages
.get(&recipient)
.map_or(0, |messages| {
messages
.iter()
.filter(|message| walk.migrated.contains_key(&message.message_id.as_str()))
.count()
});
if written.saturating_add(survivors) > MAX_MIGRATED_PENDING_WRITES_PER_LAUNCH {
walk.write_bound_reached = true;
self.forget_migrated_entries(&recipient, &walk.migrated, false);
abandoned.insert(recipient);
continue;
}
let mut wrote_all = true;
if let Some(messages) = self.pending_encrypted_messages.get(&recipient) {
for message in messages {
if walk.migrated.contains_key(&message.message_id.as_str()) {
wrote_all &= self.persist_pending_message(&recipient, message);
}
}
}
if !wrote_all {
self.forget_migrated_entries(&recipient, &walk.migrated, true);
abandoned.insert(recipient);
continue;
}
written = written.saturating_add(survivors);
budget.claim();
if !self.delete_legacy_pending_queue(&recipient) {
self.forget_migrated_entries(&recipient, &walk.migrated, true);
abandoned.insert(recipient);
}
}
abandoned
}
fn forget_migrated_entries(
&mut self,
recipient: &str,
migrated: &HashMap<String, String>,
persisted: bool,
) {
let mut removed = Vec::new();
if let Some(messages) = self.pending_encrypted_messages.get_mut(recipient) {
messages.retain(|message| {
if migrated.contains_key(&message.message_id.as_str()) {
removed.push(message.message_id.clone());
false
} else {
true
}
});
}
if self
.pending_encrypted_messages
.get(recipient)
.is_some_and(Vec::is_empty)
{
self.pending_encrypted_messages.remove(recipient);
}
if persisted {
self.delete_pending_messages_from_storage(removed.iter());
}
if !removed.is_empty() {
warn!(
recipient = %recipient,
count = removed.len(),
"Legacy pending queue could not be migrated this launch; leaving it on disk for a later one"
);
}
}
fn restore_pending_message_entries(
&mut self,
storage: &dyn ProtocolStateStorage,
budget: &mut PruneBudget<'_>,
walk: &mut PendingRestoreWalk,
) -> Result<()> {
let keys = Self::list_state_keys(storage, storage_keys::PENDING_MESSAGE_ENTRIES)
.map_err(|e| Error::Other(format!("Failed to list pending messages: {}", e)))?;
let listed = keys.len();
for key in keys.into_iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
if walk.examined_entries >= MAX_PENDING_RESTORE_ENTRIES {
walk.entry_bound_reached = true;
break;
}
if budget.is_spent() {
walk.prune_bound_reached = true;
break;
}
let data = match self.read_state_record_detailed_budgeted(
storage,
storage_keys::PENDING_MESSAGE_ENTRIES,
&key,
Some(budget),
) {
Ok(StateRecord::Present(data)) => data,
Ok(StateRecord::Missing) => continue,
Ok(StateRecord::Unreadable) => {
warn!(message_id = %key, "Dropping unreadable pending message");
walk.seen.insert(key.clone());
walk.lost_ids.push(key);
continue;
}
Ok(StateRecord::Unavailable) | Err(_) => {
warn!(
message_id = %key,
"Pending message could not be read this session; leaving it in place"
);
continue;
}
};
walk.examined_entries = walk.examined_entries.saturating_add(1);
let record = match serde_json::from_slice::<PendingMessageRecord>(&data) {
Ok(record) => record,
Err(e) => {
warn!(message_id = %key, error = %e, "Dropping corrupted pending message");
budget.claim();
let _ = storage.delete(storage_keys::PENDING_MESSAGE_ENTRIES, &key);
walk.seen.insert(key.clone());
walk.lost_ids.push(key);
continue;
}
};
if record.message.message_id.as_str() != key {
warn!(
message_id = %key,
record_id = %record.message.message_id,
"Dropping pending message whose record does not name its own key"
);
budget.claim();
let _ = storage.delete(storage_keys::PENDING_MESSAGE_ENTRIES, &key);
walk.seen.insert(key.clone());
walk.lost_ids.push(key);
continue;
}
if Self::validate_outbound_recipient(&record.recipient).is_err() {
warn!(
message_id = %key,
"Dropping persisted pending message for an invalid recipient"
);
budget.claim();
let _ = storage.delete(storage_keys::PENDING_MESSAGE_ENTRIES, &key);
walk.seen.insert(key);
walk.unaddressable.push(record.message.message_id);
continue;
}
walk.seen.insert(key);
walk.grouped
.entry(record.recipient)
.or_default()
.push(record.message);
}
if listed > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
listed,
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Pending message store listed more records than any legitimate run can \
produce; the tail is left on disk for a later launch"
);
}
Ok(())
}
fn migrate_legacy_pending_queues(
&mut self,
storage: &dyn ProtocolStateStorage,
budget: &mut PruneBudget<'_>,
walk: &mut PendingRestoreWalk,
) -> Result<()> {
let recipients = Self::list_state_keys(storage, storage_keys::PENDING_MESSAGES)
.map_err(|e| Error::Other(format!("Failed to list legacy pending messages: {}", e)))?;
if recipients.is_empty() {
return Ok(());
}
let listed = recipients.len();
for recipient in recipients.into_iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
if walk.examined_entries >= MAX_PENDING_RESTORE_ENTRIES {
walk.entry_bound_reached = true;
break;
}
if budget.is_spent() {
walk.prune_bound_reached = true;
break;
}
if Self::validate_outbound_recipient(&recipient).is_err() {
match self.load_legacy_pending_queue(&recipient, Some(budget)) {
PendingRestore::Restored(messages) => {
walk.examined_entries =
walk.examined_entries.saturating_add(messages.len());
walk.unaddressable
.extend(messages.into_iter().map(|m| m.message_id));
}
PendingRestore::Absent => continue,
PendingRestore::Lost => {
walk.lost_recipients.push(recipient);
continue;
}
PendingRestore::Unavailable => {
warn!(
recipient = %recipient,
"Legacy pending queue for an invalid recipient could not be read this session; leaving it in place"
);
continue;
}
}
warn!(recipient = %recipient, "Dropping legacy pending queue for an invalid recipient");
budget.claim();
let _ = self.delete_legacy_pending_queue(&recipient);
continue;
}
let messages = match self.load_legacy_pending_queue(&recipient, Some(budget)) {
PendingRestore::Restored(messages) => messages,
PendingRestore::Absent => continue,
PendingRestore::Lost => {
walk.lost_recipients.push(recipient);
continue;
}
PendingRestore::Unavailable => {
warn!(
recipient = %recipient,
"Legacy pending queue could not be read this session; leaving it in place"
);
continue;
}
};
walk.examined_entries = walk.examined_entries.saturating_add(messages.len());
let mut migrated = 0usize;
for message in messages {
let message_id = message.message_id.as_str();
if walk.seen.contains(&message_id) {
walk.seen_in_legacy.insert(message_id, recipient.clone());
continue;
}
walk.seen.insert(message_id.clone());
walk.migrated.insert(message_id, recipient.clone());
walk.grouped
.entry(recipient.clone())
.or_default()
.push(message);
migrated = migrated.saturating_add(1);
}
info!(
recipient = %recipient,
migrated,
"Recovered legacy pending queue for migration"
);
walk.migrated_recipients.push(recipient);
}
if listed > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
listed,
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Legacy pending message store listed more recipients than any legitimate run \
can produce; the tail is left on disk for a later launch"
);
}
Ok(())
}
pub(crate) fn persist_peer_key_package(&self, peer_id: &str, pkg: &ReceivedKeyPackage) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
match serde_json::to_vec(pkg) {
Ok(data) => {
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::PEER_KEY_PACKAGES,
peer_id,
&data,
) {
warn!(peer_id = %peer_id, error = %e, "Failed to persist peer key package");
}
}
Err(e) => {
warn!(peer_id = %peer_id, error = %e, "Failed to serialize peer key package");
}
}
}
pub(crate) fn load_peer_key_package_from_storage(
&self,
peer_id: &str,
) -> Option<ReceivedKeyPackage> {
self.load_peer_key_package_bounded(peer_id, None)
}
fn load_peer_key_package_bounded(
&self,
peer_id: &str,
mut budget: Option<&mut PruneBudget<'_>>,
) -> Option<ReceivedKeyPackage> {
let storage = self.protocol_state_storage.as_ref()?;
let data = self
.read_state_record_detailed_budgeted(
storage.as_ref(),
storage_keys::PEER_KEY_PACKAGES,
peer_id,
budget.as_deref_mut(),
)
.ok()?
.into_bytes()?;
let pkg: ReceivedKeyPackage = serde_json::from_slice(&data).ok()?;
let now_ms = Utc::now().timestamp_millis() as u64;
if now_ms >= pkg.local_expires_at_ms {
if claim_prune(&mut budget) {
let _ = storage.delete(storage_keys::PEER_KEY_PACKAGES, peer_id);
}
return None;
}
Some(pkg)
}
pub(crate) fn delete_peer_key_package_from_storage(&self, peer_id: &str) {
if let Some(storage) = &self.protocol_state_storage {
let _ = storage.delete(storage_keys::PEER_KEY_PACKAGES, peer_id);
}
}
pub(crate) fn try_load_key_package_from_storage_into_memory(&mut self, peer_id: &str) -> bool {
if self.pending_key_packages.contains_key(peer_id) {
return true;
}
if let Some(pkg) = self.load_peer_key_package_from_storage(peer_id) {
self.pending_key_packages.insert(peer_id.to_string(), pkg);
return true;
}
false
}
pub(crate) fn restore_peer_key_packages(
&mut self,
mls: &Arc<RwLock<MlsManager>>,
allowance: &mut PruneAllowance,
) -> Result<()> {
let Some(storage) = &self.protocol_state_storage else {
return Ok(());
};
let peer_ids = Self::list_state_keys(storage.as_ref(), storage_keys::PEER_KEY_PACKAGES)
.map_err(|e| Error::Other(format!("Failed to list peer key packages: {}", e)))?;
let listed = peer_ids.len();
let sessions = {
let manager = mls
.read()
.map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
manager.list_sessions().map_err(Error::Mls)?
};
let session_set: std::collections::HashSet<_> = sessions.into_iter().collect();
let mut budget = allowance.refusing();
let mut over_cap_pruned = 0usize;
for peer_id in peer_ids.into_iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
if session_set.contains(&peer_id) {
continue;
}
if self.pending_key_packages.len() >= MAX_PENDING_KEY_PACKAGES {
if !budget.claim() {
break;
}
self.delete_peer_key_package_from_storage(&peer_id);
over_cap_pruned += 1;
continue;
}
if let Some(pkg) = self.load_peer_key_package_bounded(&peer_id, Some(&mut budget)) {
info!(peer_id = %peer_id, "Restored peer key package from storage");
self.pending_key_packages.insert(peer_id, pkg);
}
}
if over_cap_pruned > 0 {
warn!(
cap = MAX_PENDING_KEY_PACKAGES,
pruned = over_cap_pruned,
"Peer key package store exceeded the cap on restore; pruned overflow from durable storage"
);
}
let dropped = budget.spent.saturating_sub(over_cap_pruned);
if dropped > 0 {
debug!(
pruned = dropped,
"Dropped expired or unreadable peer key packages from durable storage on restore"
);
}
if budget.exhausted {
warn!(
deleted = budget.spent,
budget = MAX_RESTORE_PRUNE_DELETES,
"Peer key package prune hit its share of the launch delete budget; the rest is left on disk for a later launch"
);
}
if listed > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
listed,
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Peer key package store listed more peers than any legitimate run can produce; deferring the tail to a later launch"
);
}
Ok(())
}
pub(crate) fn persist_peer_capabilities(&self, peer_id: &str, caps: &PeerCapabilities) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
match serde_json::to_vec(caps) {
Ok(data) => {
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::PEER_CAPABILITIES,
peer_id,
&data,
) {
warn!(peer_id = %peer_id, error = %e, "Failed to persist peer capabilities");
}
}
Err(e) => {
warn!(peer_id = %peer_id, error = %e, "Failed to serialize peer capabilities");
}
}
}
pub(crate) fn delete_peer_capabilities_from_storage(&self, peer_id: &str) {
if let Some(storage) = &self.protocol_state_storage {
let _ = storage.delete(storage_keys::PEER_CAPABILITIES, peer_id);
}
}
fn load_peer_capabilities(
&self,
peer_id: &str,
) -> std::result::Result<Option<PeerCapabilities>, ()> {
let Some(storage) = self.protocol_state_storage.as_ref() else {
return Ok(None);
};
match self.read_state_record_detailed(
storage.as_ref(),
storage_keys::PEER_CAPABILITIES,
peer_id,
) {
Ok(StateRecord::Present(data)) => Ok(serde_json::from_slice(&data).ok()),
Ok(StateRecord::Missing | StateRecord::Unreadable) => Ok(None),
Ok(StateRecord::Unavailable) | Err(_) => Err(()),
}
}
pub(crate) fn load_peer_capabilities_record(&self, peer_id: &str) -> Option<PeerCapabilities> {
self.load_peer_capabilities(peer_id).ok().flatten()
}
#[cfg(test)]
pub(crate) fn outgoing_key_package_nostr_pubkey_for_test(&self) -> Option<String> {
self.transport_manager.nostr_public_key()
}
pub(crate) fn record_attested_rich(&mut self, peer_id: &str, versions: &[u8]) {
if peer_id == self.local_id || !versions.contains(&RICH_PAYLOAD_V1) {
return;
}
if self.peer_rich_payload.contains(peer_id) {
return;
}
if self.config.encryption.rich_payload_enabled {
if !self.peer_rich_attested.contains(peer_id)
&& self.peer_rich_attested.len() >= MAX_KEY_PACKAGE_SENT_TO
{
self.peer_rich_attested.clear();
}
self.peer_rich_attested.insert(peer_id.to_string());
}
let Ok(existing) = self.load_peer_capabilities(peer_id) else {
warn!(
peer_id = %peer_id,
"Peer capability record unreadable this session; not persisting the attested capability"
);
return;
};
let mut caps = existing.unwrap_or_default();
caps.attested_rich_versions = versions
.iter()
.copied()
.take(MAX_PERSISTED_CAPABILITY_VERSIONS)
.collect();
self.persist_peer_capabilities(peer_id, &caps);
}
#[cfg_attr(not(feature = "data"), allow(dead_code))]
pub(crate) fn record_attested_data(&mut self, peer_id: &str, versions: &[u8]) {
if peer_id == self.local_id || !versions.contains(&DATA_GROUP_V1) {
return;
}
if self.peer_data_group.contains(peer_id) {
return;
}
if self.config.data.enabled {
if !self.peer_data_group_attested.contains(peer_id)
&& self.peer_data_group_attested.len() >= MAX_KEY_PACKAGE_SENT_TO
{
self.peer_data_group_attested.clear();
}
self.peer_data_group_attested.insert(peer_id.to_string());
if versions.contains(&DATA_GROUP_BLOB_V1) {
if !self.peer_data_group_blob_attested.contains(peer_id)
&& self.peer_data_group_blob_attested.len() >= MAX_KEY_PACKAGE_SENT_TO
{
self.peer_data_group_blob_attested.clear();
}
self.peer_data_group_blob_attested
.insert(peer_id.to_string());
}
}
let Ok(existing) = self.load_peer_capabilities(peer_id) else {
warn!(
peer_id = %peer_id,
"Peer capability record unreadable this session; not persisting the attested capability"
);
return;
};
let mut caps = existing.unwrap_or_default();
caps.attested_data_versions = versions
.iter()
.copied()
.take(MAX_PERSISTED_CAPABILITY_VERSIONS)
.collect();
self.persist_peer_capabilities(peer_id, &caps);
}
pub(crate) fn restore_peer_capabilities(
&mut self,
mls: &Arc<RwLock<MlsManager>>,
allowance: &mut PruneAllowance,
) {
let Some(storage) = self.protocol_state_storage.clone() else {
return;
};
let peer_ids =
match Self::list_state_keys(storage.as_ref(), storage_keys::PEER_CAPABILITIES) {
Ok(ids) => ids,
Err(e) => {
warn!(error = %e, "Failed to list peer capabilities, skipping restore");
return;
}
};
let session_set: std::collections::HashSet<String> = mls
.read()
.ok()
.and_then(|manager| manager.list_sessions().ok())
.map(|sessions| sessions.into_iter().collect())
.unwrap_or_default();
let listed = peer_ids.len();
let (mut peer_ids, non_session_ids): (Vec<_>, Vec<_>) = peer_ids
.into_iter()
.partition(|peer_id| session_set.contains(peer_id));
peer_ids.extend(non_session_ids);
let mut kept = 0usize;
let mut over_cap_pruned = 0usize;
let mut budget = allowance.refusing();
for peer_id in peer_ids.into_iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
if kept >= MAX_KEY_PACKAGE_SENT_TO {
if !budget.claim() {
break;
}
self.delete_peer_capabilities_from_storage(&peer_id);
over_cap_pruned += 1;
continue;
}
let Ok(StateRecord::Present(data)) = self.read_state_record_detailed_budgeted(
storage.as_ref(),
storage_keys::PEER_CAPABILITIES,
&peer_id,
Some(&mut budget),
) else {
continue;
};
let Ok(caps) = serde_json::from_slice::<PeerCapabilities>(&data) else {
warn!(peer_id = %peer_id, "Corrupt peer capability record, deleting");
if budget.claim() {
self.delete_peer_capabilities_from_storage(&peer_id);
}
continue;
};
if !caps.is_any() {
if budget.claim() {
self.delete_peer_capabilities_from_storage(&peer_id);
}
continue;
}
if self.config.encryption.compact_envelope_enabled
&& caps.env_versions.contains(&MLS_ENVELOPE_COMPACT_V1)
{
self.peer_compact_envelope.insert(peer_id.clone());
}
if self.config.encryption.rich_payload_enabled
&& caps.rich_versions.contains(&RICH_PAYLOAD_V1)
{
self.peer_rich_payload.insert(peer_id.clone());
}
if self.config.encryption.encrypt_service_messages
&& caps
.svc_versions
.contains(&offline_protocol_sealed::SVC_SEALED_V1)
{
self.peer_svc_sealed.insert(peer_id.clone());
}
if caps
.ctrl_versions
.contains(&offline_protocol_sealed::CTRL_SIGN_V2)
{
self.peer_ctrl_freshness.insert(peer_id.clone());
}
if caps
.ack_versions
.contains(&offline_protocol_sealed::ACK_UNDECRYPTABLE_V1)
{
self.peer_undecryptable_ack.insert(peer_id.clone());
}
if self.config.encryption.rich_payload_enabled
&& caps.attested_rich_versions.contains(&RICH_PAYLOAD_V1)
{
self.peer_rich_attested.insert(peer_id.clone());
}
if self.config.data.enabled && caps.data_versions.contains(&DATA_SYNC_V1) {
self.peer_data_sync.insert(peer_id.clone());
}
if self.config.data.enabled && caps.data_versions.contains(&DATA_GROUP_V1) {
self.peer_data_group.insert(peer_id.clone());
}
if self.config.data.enabled && caps.attested_data_versions.contains(&DATA_GROUP_V1) {
self.peer_data_group_attested.insert(peer_id.clone());
}
if self.config.data.enabled && caps.data_versions.contains(&DATA_MEDIA_V1) {
self.peer_data_media.insert(peer_id.clone());
}
if self.config.data.enabled && caps.data_versions.contains(&DATA_TOMBSTONE_V1) {
self.peer_data_tombstones.insert(peer_id.clone());
}
if self.config.data.enabled && caps.data_versions.contains(&DATA_INTEREST_V1) {
self.peer_data_interest.insert(peer_id.clone());
}
if self.config.data.enabled && caps.data_versions.contains(&DATA_GROUP_BLOB_V1) {
self.peer_data_group_blob.insert(peer_id.clone());
}
if self.config.data.enabled && caps.data_versions.contains(&DATA_CUSTODY_V1) {
self.peer_data_custody.insert(peer_id.clone());
}
if self.config.data.enabled && caps.attested_data_versions.contains(&DATA_GROUP_BLOB_V1)
{
self.peer_data_group_blob_attested.insert(peer_id.clone());
}
if let Some(pubkey) = caps.nostr_pubkey.as_deref() {
self.transport_manager
.mark_peer_nostr_pubkey(&peer_id, Some(pubkey));
}
kept += 1;
}
if kept > 0 {
info!(
count = kept,
"Restored peer capability records from storage"
);
}
if over_cap_pruned > 0 {
warn!(
cap = MAX_KEY_PACKAGE_SENT_TO,
pruned = over_cap_pruned,
"Peer capability store exceeded the cap on restore; pruned overflow from durable storage"
);
}
let unreadable_pruned = budget.spent.saturating_sub(over_cap_pruned);
if unreadable_pruned > 0 {
debug!(
pruned = unreadable_pruned,
"Dropped unreadable peer capability records from durable storage on restore"
);
}
if budget.exhausted {
warn!(
deleted = budget.spent,
budget = MAX_RESTORE_PRUNE_DELETES,
"Peer capability prune hit its share of the launch delete budget; the rest is left on disk for a later launch"
);
}
if listed > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
listed,
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Peer capability store listed more peers than any legitimate run can produce; deferring the tail to a later launch"
);
}
}
fn load_restorable_state_record<T: DeserializeOwned>(
&self,
key_type: &str,
key_id: &str,
mut budget: Option<&mut PruneBudget<'_>>,
) -> RestorableRecord<T> {
let Some(storage) = &self.protocol_state_storage else {
return RestorableRecord::Absent;
};
let data = match self.read_state_record_detailed_budgeted(
storage.as_ref(),
key_type,
key_id,
budget.as_deref_mut(),
) {
Ok(StateRecord::Present(data)) => data,
Ok(StateRecord::Missing | StateRecord::Unreadable) => return RestorableRecord::Absent,
Ok(StateRecord::Unavailable) => return RestorableRecord::Unavailable,
Err(e) => {
debug!(
key_type = %key_type,
key_id = %key_id,
error = %e,
"Protocol state record could not be read this session; leaving it in place"
);
return RestorableRecord::Unavailable;
}
};
match serde_json::from_slice::<T>(&data) {
Ok(value) => RestorableRecord::Present(value),
Err(e) => {
warn!(
key_type = %key_type,
key_id = %key_id,
error = %e,
"Dropping a protocol-state record whose bytes are not the type \
its category holds"
);
if claim_prune(&mut budget) {
let _ = storage.delete(key_type, key_id);
}
RestorableRecord::Absent
}
}
}
pub(super) fn load_session_state_for_restore(
&self,
peer_id: &str,
budget: Option<&mut PruneBudget<'_>>,
) -> RestorableRecord<SessionState> {
self.load_restorable_state_record(storage_keys::SESSION_STATES, peer_id, budget)
}
fn load_welcome_lifecycle_for_restore(
&self,
peer_id: &str,
budget: Option<&mut PruneBudget<'_>>,
) -> RestorableRecord<WelcomeLifecycleRecord> {
self.load_restorable_state_record(storage_keys::WELCOME_LIFECYCLES, peer_id, budget)
}
pub(crate) fn load_session_state_entry(&self, peer_id: &str) -> Result<Option<SessionState>> {
let Some(storage) = &self.protocol_state_storage else {
return Ok(None);
};
let Some(data) = self
.read_state_record(storage.as_ref(), storage_keys::SESSION_STATES, peer_id)
.map_err(|e| {
Error::Other(format!(
"Failed to load session state for {}: {}",
peer_id, e
))
})?
else {
return Ok(None);
};
let state = serde_json::from_slice::<SessionState>(&data).map_err(|e| {
Error::Other(format!(
"Failed to deserialize session state for {}: {}",
peer_id, e
))
})?;
Ok(Some(state))
}
pub(crate) fn persist_session_state(
&self,
peer_id: &str,
new_state: SessionState,
source_event: &str,
) -> Result<()> {
let Some(storage) = &self.protocol_state_storage else {
return Err(Error::MlsNotInitialized);
};
let encoded = serde_json::to_vec(&new_state).map_err(|e| {
Error::Serialization(format!("Failed to serialize session state: {}", e))
})?;
self.write_state_record(
storage.as_ref(),
storage_keys::SESSION_STATES,
peer_id,
&encoded,
)
.map_err(|e| {
Error::Other(format!(
"Failed to persist session state for {}: {}",
peer_id, e
))
})?;
if matches!(new_state, SessionState::Confirmed) {
info!(
event = "confirmation_persisted",
session_or_group_id = %peer_id,
previous_state = "Pending",
new_state = "Confirmed",
source_event = %source_event,
"confirmation_persisted"
);
}
Ok(())
}
pub(crate) fn clear_session_state_entry(&self, peer_id: &str) -> Result<()> {
let Some(storage) = &self.protocol_state_storage else {
return Ok(());
};
storage
.delete(storage_keys::SESSION_STATES, peer_id)
.map_err(|e| {
Error::Other(format!(
"Failed to clear session state for {}: {}",
peer_id, e
))
})
}
pub(crate) fn persist_welcome_lifecycle_entry(
&self,
record: &WelcomeLifecycleRecord,
) -> Result<()> {
let Some(storage) = &self.protocol_state_storage else {
return Err(Error::MlsNotInitialized);
};
let encoded = serde_json::to_vec(record).map_err(|e| {
Error::Serialization(format!("Failed to serialize welcome lifecycle: {}", e))
})?;
self.write_state_record(
storage.as_ref(),
storage_keys::WELCOME_LIFECYCLES,
&record.peer_id,
&encoded,
)
.map_err(|e| {
Error::Other(format!(
"Failed to persist welcome lifecycle for {}: {}",
record.peer_id, e
))
})
}
pub(crate) fn clear_welcome_lifecycle_entry(&self, peer_id: &str) -> Result<()> {
let Some(storage) = &self.protocol_state_storage else {
return Ok(());
};
storage
.delete(storage_keys::WELCOME_LIFECYCLES, peer_id)
.map_err(|e| {
Error::Other(format!(
"Failed to clear welcome lifecycle for {}: {}",
peer_id, e
))
})
}
fn repair_restored_welcome_lifecycle(
peer_id: &str,
record: &mut WelcomeLifecycleRecord,
) -> bool {
let mut repaired = false;
if matches!(
record.state,
WelcomeDeliveryState::Created | WelcomeDeliveryState::SendAttempted
) {
record.state = WelcomeDeliveryState::Failed;
record.next_retry_at = Some(Utc::now());
repaired = true;
warn!(
event = "welcome_lifecycle_repaired",
session_or_group_id = %peer_id,
repair_action = "in_flight_to_failed_retry_now",
state = record.state.as_str(),
attempt = record.attempt,
"welcome_lifecycle_repaired"
);
}
if matches!(record.state, WelcomeDeliveryState::Failed) && record.next_retry_at.is_none() {
if matches!(
record.last_reason_code,
Some(crate::events::WelcomeReasonCode::RetryExhausted)
) {
record.state = WelcomeDeliveryState::Expired;
warn!(
event = "welcome_lifecycle_repaired",
session_or_group_id = %peer_id,
repair_action = "failed_no_retry_to_expired",
state = record.state.as_str(),
attempt = record.attempt,
"welcome_lifecycle_repaired"
);
} else {
record.next_retry_at = Some(Utc::now());
warn!(
event = "welcome_lifecycle_repaired",
session_or_group_id = %peer_id,
repair_action = "failed_no_retry_to_failed_retry_now",
state = record.state.as_str(),
attempt = record.attempt,
"welcome_lifecycle_repaired"
);
}
repaired = true;
}
if matches!(record.state, WelcomeDeliveryState::Failed) && record.expires_at <= Utc::now() {
record.expires_at = Utc::now() + ChronoDuration::seconds(WELCOME_LIFECYCLE_TTL_SECS);
repaired = true;
warn!(
event = "welcome_lifecycle_repaired",
session_or_group_id = %peer_id,
repair_action = "ttl_refreshed_carrier_relative",
state = record.state.as_str(),
attempt = record.attempt,
"welcome_lifecycle_repaired"
);
}
if matches!(
record.state,
WelcomeDeliveryState::Sent | WelcomeDeliveryState::Expired
) && record.next_retry_at.is_some()
{
record.next_retry_at = None;
repaired = true;
warn!(
event = "welcome_lifecycle_repaired",
session_or_group_id = %peer_id,
repair_action = "terminal_clear_retry_schedule",
state = record.state.as_str(),
attempt = record.attempt,
"welcome_lifecycle_repaired"
);
}
repaired
}
pub(crate) fn restore_welcome_lifecycles(
&mut self,
allowance: &mut PruneAllowance,
) -> Result<()> {
self.welcome_lifecycles.clear();
let Some(storage) = &self.protocol_state_storage else {
return Ok(());
};
let peers = Self::list_state_keys(storage.as_ref(), storage_keys::WELCOME_LIFECYCLES)
.map_err(|e| Error::Other(format!("Failed to list welcome lifecycles: {}", e)))?;
let listed = peers.len();
let mut budget = allowance.refusing();
let mut unavailable = 0usize;
for peer_id in peers.into_iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
let mut record =
match self.load_welcome_lifecycle_for_restore(&peer_id, Some(&mut budget)) {
RestorableRecord::Present(record) => record,
RestorableRecord::Absent => continue,
RestorableRecord::Unavailable => {
unavailable += 1;
continue;
}
};
if Self::repair_restored_welcome_lifecycle(&peer_id, &mut record) {
if let Err(e) = self.persist_welcome_lifecycle_entry(&record) {
warn!(
session_or_group_id = %peer_id,
error = %e,
"Failed to persist a repaired Welcome lifecycle; the repair holds in \
memory and is re-derived on the next launch"
);
}
}
self.welcome_lifecycles.insert(peer_id.clone(), record);
info!(
event = "welcome_lifecycle_restored",
session_or_group_id = %peer_id,
"welcome_lifecycle_restored"
);
}
if unavailable > 0 {
warn!(
unavailable,
"Welcome lifecycle records could not be read this session; left on disk for a \
later launch"
);
}
if listed > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
listed,
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Welcome lifecycle store listed more peers than any legitimate run can produce; ignoring the tail"
);
}
if budget.exhausted {
warn!(
deleted = budget.spent,
budget = MAX_RESTORE_PRUNE_DELETES,
"Welcome lifecycle restore hit its share of the launch delete budget; the rest is left on disk for a later launch"
);
}
Ok(())
}
pub(crate) fn persist_outbox_entry(&self, entry: &OutboxEntry) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
if Self::is_media_outbox_message(&entry.message)
|| Self::is_confirmation_probe(&entry.message)
{
return;
}
match serde_json::to_vec(entry) {
Ok(data) => {
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::OUTBOX,
&entry.message.id.as_str(),
&data,
) {
warn!(message_id = %entry.message.id, error = %e, "Failed to persist outbox entry");
}
}
Err(e) => {
warn!(message_id = %entry.message.id, error = %e, "Failed to serialize outbox entry");
}
}
}
pub(crate) fn clear_outbox_entry_from_storage(&self, message_id: &MessageId) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
if let Err(e) = storage.delete(storage_keys::OUTBOX, &message_id.as_str()) {
warn!(message_id = %message_id, error = %e, "Failed to clear persisted outbox entry");
}
}
pub(crate) fn restore_outbox(&mut self, allowance: &mut PruneAllowance) -> Result<()> {
let Some(storage) = self.protocol_state_storage.clone() else {
return Ok(());
};
let message_ids = Self::list_state_keys(storage.as_ref(), storage_keys::OUTBOX)
.map_err(|e| Error::Other(format!("Failed to list outbox entries: {}", e)))?;
let lifetime_ms = self.config.reliability.retry.outbox_max_lifetime_ms;
let listed = message_ids.len();
let mut restored: Vec<OutboxEntry> = Vec::new();
let mut unrecoverable: Vec<String> = Vec::new();
let mut budget = allowance.counting();
let mut prune_bound_reached = false;
let mut legacy_probes_dropped = 0usize;
for message_id in message_ids.into_iter().take(OUTBOX_RESTORE_KEY_CAP) {
if budget.is_spent() {
prune_bound_reached = true;
break;
}
let data = match self.read_state_record_detailed_budgeted(
storage.as_ref(),
storage_keys::OUTBOX,
&message_id,
Some(&mut budget),
) {
Ok(StateRecord::Present(data)) => data,
Ok(StateRecord::Missing) => continue,
Ok(StateRecord::Unreadable) => {
warn!(message_id = %message_id, "Dropping unreadable outbox entry");
unrecoverable.push(message_id);
continue;
}
Ok(StateRecord::Unavailable) | Err(_) => {
warn!(
message_id = %message_id,
"Outbox entry could not be read this session; leaving it in place"
);
continue;
}
};
let entry = match serde_json::from_slice::<OutboxEntry>(&data) {
Ok(entry) => entry,
Err(e) => {
warn!(message_id = %message_id, error = %e, "Dropping corrupted outbox entry");
budget.claim();
self.delete_outbox_key(&message_id);
unrecoverable.push(message_id);
continue;
}
};
if Self::is_media_outbox_message(&entry.message) {
warn!(message_id = %message_id, "Dropping persisted media outbox entry");
budget.claim();
self.delete_outbox_key(&message_id);
continue;
}
if Self::is_confirmation_probe(&entry.message) {
budget.claim();
self.delete_outbox_key(&message_id);
legacy_probes_dropped += 1;
continue;
}
restored.push(entry);
}
if legacy_probes_dropped > 0 {
info!(
event = "outbox_entry_dropped",
repair_action = "legacy_confirmation_probe",
count = legacy_probes_dropped,
"outbox_entry_dropped"
);
}
let absolute_lifetime_ms =
lifetime_ms.saturating_mul(crate::constants::OUTBOX_ABSOLUTE_LIFETIME_FACTOR as u64);
let now = Utc::now();
let mut absolutely_expired: Vec<OutboxEntry> = Vec::new();
restored.retain(|entry| {
if super::relay_push_window_closed(now, entry)
|| (lifetime_expired(now, entry.last_sent_at, lifetime_ms)
&& lifetime_expired(now, entry.first_sent_at, absolute_lifetime_ms))
{
absolutely_expired.push(entry.clone());
return false;
}
true
});
let mut expiry_settlements: Vec<Event> = Vec::new();
for entry in &absolutely_expired {
if budget.is_spent() {
prune_bound_reached = true;
break;
}
budget.claim();
self.delete_outbox_key(&entry.message.id.as_str());
if entry.relay_pushed {
info!(
event = "outbox_entry_dropped",
message_id = %entry.message.id,
repair_action = "relay_push_window_closed",
"outbox_entry_dropped"
);
continue;
}
info!(
event = "outbox_entry_dropped",
message_id = %entry.message.id,
repair_action = "absolute_lifetime_exceeded",
"outbox_entry_dropped"
);
expiry_settlements.push(Event::message_failed(
entry.message.id.clone(),
"Outbox lifetime exceeded",
entry.attempt_count,
));
}
self.settle_restored_message_failures(expiry_settlements);
if restored.len() > MAX_OUTBOX_ENTRIES {
restored.sort_by_key(|e| std::cmp::Reverse(e.last_sent_at));
let mut capacity_settlements: Vec<Event> = Vec::new();
for entry in restored.drain(MAX_OUTBOX_ENTRIES..) {
if budget.is_spent() {
prune_bound_reached = true;
break;
}
budget.claim();
self.delete_outbox_key(&entry.message.id.as_str());
capacity_settlements.push(Event::message_failed(
entry.message.id.clone(),
"Outbox capacity exceeded",
entry.attempt_count,
));
}
self.settle_restored_message_failures(capacity_settlements);
}
self.settle_restored_message_failures(unrecoverable.iter().map(|key| {
Self::unrecoverable_message_settlement(
key,
"Outbox entry could not be recovered from protocol-state storage",
0,
)
}));
if listed > OUTBOX_RESTORE_KEY_CAP {
warn!(
listed,
cap = OUTBOX_RESTORE_KEY_CAP,
"Outbox store listed more entries than any legitimate run can produce; ignoring the tail"
);
}
if prune_bound_reached || budget.exhausted {
warn!(
deleted = budget.spent,
budget = MAX_RESTORE_PRUNE_DELETES,
"Outbox restore spent its delete budget; the remaining entries are left on disk \
and restored on a later launch"
);
}
let mut refreshed: Vec<OutboxEntry> = Vec::new();
for entry in &mut restored {
if lifetime_expired(now, entry.last_sent_at, lifetime_ms) {
entry.last_sent_at = now;
refreshed.push(entry.clone());
info!(
event = "outbox_entry_restored",
message_id = %entry.message.id,
repair_action = "ttl_refreshed_carrier_relative",
"outbox_entry_restored"
);
}
}
let restored_ids: std::collections::HashSet<String> =
restored.iter().map(|e| e.message.id.as_str()).collect();
let orphans: Vec<OutboxEntry> = self
.outbox
.values()
.filter(|e| !restored_ids.contains(&e.message.id.as_str()))
.cloned()
.collect();
let count = restored.len();
for entry in restored {
self.outbox.insert(entry.message.id.clone(), entry);
}
for entry in &refreshed {
self.persist_outbox_entry(entry);
}
for entry in &orphans {
self.persist_outbox_entry(entry);
}
if count > 0 {
info!(count = count, "Restored outbox entries from storage");
}
Ok(())
}
fn delete_outbox_key(&self, message_id: &str) {
if let Some(storage) = &self.protocol_state_storage {
if let Err(e) = storage.delete(storage_keys::OUTBOX, message_id) {
warn!(message_id = %message_id, error = %e, "Failed to delete outbox key");
}
}
}
pub(crate) fn persist_media_descriptor(&self, descriptor: &MediaTransferDescriptor) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
match serde_json::to_vec(descriptor) {
Ok(data) => {
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::MEDIA_DESCRIPTORS,
&descriptor.file_id,
&data,
) {
warn!(file_id = %descriptor.file_id, error = %e, "Failed to persist media descriptor");
}
}
Err(e) => {
warn!(file_id = %descriptor.file_id, error = %e, "Failed to serialize media descriptor");
}
}
}
pub(crate) fn remove_media_descriptor(&mut self, file_id: &str) {
self.restored_media_descriptors.remove(file_id);
let Some(storage) = &self.protocol_state_storage else {
return;
};
if let Err(e) = storage.delete(storage_keys::MEDIA_DESCRIPTORS, file_id) {
warn!(file_id = %file_id, error = %e, "Failed to clear persisted media descriptor");
}
}
pub(crate) fn restore_media_descriptors(
&mut self,
allowance: &mut PruneAllowance,
) -> Result<()> {
let Some(storage) = self.protocol_state_storage.clone() else {
return Ok(());
};
let file_ids = Self::list_state_keys(storage.as_ref(), storage_keys::MEDIA_DESCRIPTORS)
.map_err(|e| Error::Other(format!("Failed to list media descriptors: {}", e)))?;
let lifetime_ms = self.config.reliability.retry.outbox_max_lifetime_ms;
let now = Utc::now();
let mut restored: Vec<MediaTransferDescriptor> = Vec::new();
let mut budget = allowance.refusing();
for file_id in file_ids.into_iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
let data = match self.read_state_record_detailed_budgeted(
storage.as_ref(),
storage_keys::MEDIA_DESCRIPTORS,
&file_id,
Some(&mut budget),
) {
Ok(StateRecord::Present(data)) => data,
Ok(StateRecord::Missing | StateRecord::Unreadable | StateRecord::Unavailable)
| Err(_) => continue,
};
let descriptor = match serde_json::from_slice::<MediaTransferDescriptor>(&data) {
Ok(descriptor) => descriptor,
Err(e) => {
warn!(file_id = %file_id, error = %e, "Dropping corrupted media descriptor");
if budget.claim() {
self.delete_media_descriptor_key(&file_id);
}
continue;
}
};
if lifetime_expired(now, descriptor.queued_at, lifetime_ms) {
debug!(file_id = %file_id, "Dropping expired media descriptor");
if budget.claim() {
self.delete_media_descriptor_key(&file_id);
}
continue;
}
restored.push(descriptor);
}
if restored.len() > MAX_MEDIA_DESCRIPTORS {
restored.sort_by_key(|d| std::cmp::Reverse(d.queued_at));
for descriptor in restored.drain(MAX_MEDIA_DESCRIPTORS..) {
if budget.claim() {
self.delete_media_descriptor_key(&descriptor.file_id);
}
}
}
if budget.exhausted {
warn!(
deleted = budget.spent,
budget = MAX_RESTORE_PRUNE_DELETES,
"Media descriptor prune hit its share of the launch delete budget; the rest is left on disk for a later launch"
);
}
let count = restored.len();
for descriptor in restored {
self.restored_media_descriptors
.insert(descriptor.file_id.clone(), descriptor);
}
if count > 0 {
info!(
count = count,
"Restored media transfer descriptors from storage"
);
}
Ok(())
}
fn delete_media_descriptor_key(&self, file_id: &str) {
if let Some(storage) = &self.protocol_state_storage {
if let Err(e) = storage.delete(storage_keys::MEDIA_DESCRIPTORS, file_id) {
warn!(file_id = %file_id, error = %e, "Failed to delete media descriptor key");
}
}
}
pub(crate) fn persist_blocked_user(&self, user_id: &str) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
if let Err(e) =
self.write_state_record(storage.as_ref(), storage_keys::BLOCKED_USERS, user_id, &[])
{
warn!(user_id = %user_id, error = %e, "Failed to persist blocked user");
}
}
pub(crate) fn delete_blocked_user(&self, user_id: &str) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
if let Err(e) = storage.delete(storage_keys::BLOCKED_USERS, user_id) {
warn!(user_id = %user_id, error = %e, "Failed to delete blocked user from storage");
}
}
pub(crate) fn restore_blocked_users(&mut self) -> Result<()> {
let Some(storage) = &self.protocol_state_storage else {
return Ok(());
};
let user_ids = Self::list_state_keys(storage.as_ref(), storage_keys::BLOCKED_USERS)
.map_err(|e| Error::Other(format!("Failed to list blocked users: {}", e)))?;
let listed = user_ids.len();
for user_id in user_ids.iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
if offline_protocol_core::UserId::new(user_id).is_err() {
warn!(user_id = %user_id, "Skipping blocked user entry with invalid user ID");
continue;
}
if user_id.as_str() == self.local_id {
warn!("Skipping blocked user entry naming the local user");
continue;
}
if self.blocked_users.len() >= MAX_BLOCKED_USERS {
warn!(
listed,
cap = MAX_BLOCKED_USERS,
"Blocked-user store holds more entries than the live cap admits; \
restoring the first {} and ignoring the rest",
MAX_BLOCKED_USERS
);
break;
}
self.blocked_users.insert(user_id.clone());
}
if listed > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
listed,
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Blocked-user store listed more entries than any legitimate run can produce; ignoring the tail"
);
}
if !self.blocked_users.is_empty() {
info!(
count = self.blocked_users.len(),
"Restored blocked users from storage"
);
}
Ok(())
}
pub(crate) fn persist_both_create_awaiting_decrypt(&self, peer_id: &str) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::BOTH_CREATE_AWAITING_DECRYPT,
peer_id,
&[],
) {
warn!(peer_id = %peer_id, error = %e, "Failed to persist both-create owner gate");
}
}
pub(crate) fn delete_both_create_awaiting_decrypt(&self, peer_id: &str) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
if let Err(e) = storage.delete(storage_keys::BOTH_CREATE_AWAITING_DECRYPT, peer_id) {
warn!(peer_id = %peer_id, error = %e, "Failed to delete both-create owner gate");
}
}
pub(crate) fn restore_both_create_awaiting_decrypt(&mut self) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
let peer_ids = match Self::list_state_keys(
storage.as_ref(),
storage_keys::BOTH_CREATE_AWAITING_DECRYPT,
) {
Ok(keys) => keys,
Err(e) => {
warn!(error = %e, "Failed to list both-create owner gate from storage");
return;
}
};
let listed = peer_ids.len();
for peer_id in peer_ids.iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
self.both_create_awaiting_decrypt.insert(peer_id.clone());
}
if listed > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
listed,
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Both-create owner gate listed more peers than any legitimate run can produce; ignoring the tail"
);
}
if !self.both_create_awaiting_decrypt.is_empty() {
info!(
count = self.both_create_awaiting_decrypt.len(),
"Restored both-create owner gate from storage"
);
}
}
pub(crate) fn persist_pending_decrypt_entry(
&self,
peer_id: &str,
message: &Message,
first_received_at_ms: i64,
received_via: Option<TransportType>,
) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
let record = PendingDecryptRecord {
version: PENDING_DECRYPT_RECORD_VERSION,
peer_id: peer_id.to_string(),
message: message.clone(),
first_received_at_ms,
received_via,
};
let data = match serde_json::to_vec(&record) {
Ok(data) => data,
Err(e) => {
warn!(
peer_id = %peer_id,
message_id = %message.id,
error = %e,
"Failed to serialize pending decryption entry"
);
return;
}
};
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::PENDING_DECRYPT_ENTRIES,
&message.id.as_str(),
&data,
) {
warn!(
peer_id = %peer_id,
message_id = %message.id,
error = %e,
"Failed to persist pending decryption entry"
);
}
}
pub(crate) fn delete_pending_decrypt_entry_from_storage(&self, message_id: &MessageId) {
let Some(storage) = &self.protocol_state_storage else {
return;
};
if let Err(e) = storage.delete(storage_keys::PENDING_DECRYPT_ENTRIES, &message_id.as_str())
{
warn!(
message_id = %message_id,
error = %e,
"Failed to clear persisted pending decryption entry"
);
}
}
pub(crate) fn delete_pending_decrypt_entries_from_storage<'a>(
&self,
message_ids: impl IntoIterator<Item = &'a MessageId>,
) {
for message_id in message_ids {
self.delete_pending_decrypt_entry_from_storage(message_id);
}
}
#[cfg(test)]
pub(crate) fn persisted_pending_decrypt_ids(&self) -> Vec<String> {
let Some(storage) = self.protocol_state_storage.as_ref() else {
return Vec::new();
};
Self::list_state_keys(storage.as_ref(), storage_keys::PENDING_DECRYPT_ENTRIES)
.unwrap_or_default()
}
pub(crate) fn restore_pending_decrypt_entries(&mut self, allowance: &mut PruneAllowance) {
let Some(storage) = self.protocol_state_storage.clone() else {
return;
};
let key_ids =
match Self::list_state_keys(storage.as_ref(), storage_keys::PENDING_DECRYPT_ENTRIES) {
Ok(keys) => keys,
Err(e) => {
warn!(error = %e, "Failed to list pending decryption entries from storage");
return;
}
};
let listed = key_ids.len();
if listed > MAX_RESTORE_KEYS_PER_CATEGORY {
warn!(
listed,
cap = MAX_RESTORE_KEYS_PER_CATEGORY,
"Pending decryption entries listed more records than any legitimate run can produce; ignoring the tail"
);
}
let now_ms = Utc::now().timestamp_millis();
let mut budget = allowance.refusing_private();
let mut restored: Vec<PendingDecryptRecord> = Vec::new();
let mut settlements: Vec<Event> = Vec::new();
for key_id in key_ids.into_iter().take(MAX_RESTORE_KEYS_PER_CATEGORY) {
let data = match self.read_state_record_detailed_budgeted(
storage.as_ref(),
storage_keys::PENDING_DECRYPT_ENTRIES,
&key_id,
Some(&mut budget),
) {
Ok(StateRecord::Present(data)) => data,
Ok(StateRecord::Missing | StateRecord::Unreadable | StateRecord::Unavailable)
| Err(_) => continue,
};
let record = match serde_json::from_slice::<PendingDecryptRecord>(&data) {
Ok(record) if record.version == PENDING_DECRYPT_RECORD_VERSION => record,
Ok(record) => {
warn!(
key_id = %key_id,
version = record.version,
"Dropping pending decryption entry with an unknown record version"
);
if budget.claim() {
self.delete_pending_decrypt_key(&key_id);
}
continue;
}
Err(e) => {
warn!(
key_id = %key_id,
error = %e,
"Dropping corrupted pending decryption entry"
);
if budget.claim() {
self.delete_pending_decrypt_key(&key_id);
}
continue;
}
};
if record.message.id.as_str() != key_id {
warn!(
key_id = %key_id,
message_id = %record.message.id,
"Dropping pending decryption entry whose key does not match its message id"
);
if budget.claim() {
self.delete_pending_decrypt_key(&key_id);
}
continue;
}
if now_ms.saturating_sub(record.first_received_at_ms)
>= PENDING_DECRYPT_PERSISTED_MAX_AGE_MS
{
if !budget.claim() {
continue;
}
debug!(
key_id = %key_id,
peer_id = %record.peer_id,
"Dropping pending decryption entry that aged out on disk"
);
self.delete_pending_decrypt_key(&key_id);
settlements.push(Event::message_decryption_failed(
record.message.id.clone(),
record.message.sender.as_str().to_string(),
DecryptionFailureCode::PendingQueueDropped,
"encrypted message evicted from pending queue (expired_persisted); \
recoverable only if the sender resends"
.to_string(),
));
continue;
}
restored.push(record);
}
restored.sort_by(|left, right| {
left.first_received_at_ms
.cmp(&right.first_received_at_ms)
.then_with(|| left.message.id.as_str().cmp(&right.message.id.as_str()))
});
let count = restored.len();
for record in restored {
let dropped = self.enqueue_restored_pending_decryption(
&record.peer_id,
&record.message,
record.received_via,
);
let mut deleted = Vec::with_capacity(dropped.len());
for entry in dropped {
if budget.claim() {
self.delete_pending_decrypt_entry_from_storage(&entry.message.id);
deleted.push(entry);
}
}
settlements.extend(Self::pending_drop_events(deleted));
}
self.settle_restored_message_failures(settlements);
if budget.exhausted {
warn!(
deleted = budget.spent,
budget = MAX_RESTORE_PRUNE_DELETES,
"Pending decryption prune hit its share of the launch delete budget; the rest is left on disk for a later launch"
);
}
if count > 0 {
info!(count, "Restored pending decryption entries from storage");
}
}
fn delete_pending_decrypt_key(&self, key_id: &str) {
if let Some(storage) = &self.protocol_state_storage {
if let Err(e) = storage.delete(storage_keys::PENDING_DECRYPT_ENTRIES, key_id) {
warn!(key_id = %key_id, error = %e, "Failed to delete pending decryption entry");
}
}
}
pub(crate) fn restore_or_init_scrub_secret(&mut self) {
if self.telemetry_secret_persisted {
return;
}
let Some(storage) = &self.secure_storage else {
return;
};
let secret: [u8; 16] = match storage
.load(storage_keys::SCRUB_SECRET, storage_keys::SCRUB_SECRET_ID)
{
Ok(Some(bytes)) if bytes.len() == 16 => {
let mut secret = [0u8; 16];
secret.copy_from_slice(&bytes);
debug!("Restored persistent telemetry scrub secret from storage");
secret
}
Ok(other) => {
if other.is_some() {
warn!("Persisted scrub secret had unexpected length; regenerating");
}
let fresh = *uuid::Uuid::new_v4().as_bytes();
if let Err(e) = storage.store(
storage_keys::SCRUB_SECRET,
storage_keys::SCRUB_SECRET_ID,
&fresh,
) {
warn!(error = %e, "Failed to persist telemetry scrub secret; using session-local secret");
return self.adopt_fallback_secret(fresh);
}
info!("Generated and persisted per-install telemetry scrub secret");
fresh
}
Err(e) => {
warn!(error = %e, "Failed to load telemetry scrub secret; keeping random fallback");
return;
}
};
self.telemetry_secret_persisted = true;
self.adopt_fallback_secret(secret);
}
pub(crate) fn restore_or_init_nostr_signing_secret(&mut self) {
if self.nostr_secret_persisted {
return;
}
let Some(storage) = self.secure_storage.clone() else {
return;
};
let Some(nostr_arc) = self.transport_manager.get_transport(TransportType::Nostr) else {
return;
};
let (secret, persisted): (Zeroizing<[u8; 32]>, bool) = match storage.load(
storage_keys::NOSTR_SIGNING_SECRET,
storage_keys::NOSTR_SIGNING_SECRET_ID,
) {
Ok(Some(bytes)) if bytes.len() == 32 => {
let bytes = Zeroizing::new(bytes);
let mut secret = Zeroizing::new([0u8; 32]);
secret.copy_from_slice(&bytes);
self.nostr_unpersisted_secret = None;
debug!("Restored persistent Nostr signing secret from storage");
(secret, true)
}
Ok(other) => {
if other.is_some() {
warn!("Persisted Nostr signing secret had unexpected length; regenerating");
}
let fresh = match self.nostr_unpersisted_secret.take() {
Some(unpersisted) => unpersisted,
None => match NostrKeypair::generate_install_secret() {
Ok(fresh) => fresh,
Err(e) => {
warn!(error = %e, "Failed to generate Nostr signing secret; keeping ephemeral key");
return;
}
},
};
match storage.store(
storage_keys::NOSTR_SIGNING_SECRET,
storage_keys::NOSTR_SIGNING_SECRET_ID,
&*fresh,
) {
Ok(()) => {
info!("Generated and persisted per-install Nostr signing secret");
(fresh, true)
}
Err(e) => {
warn!(error = %e, "Failed to persist Nostr signing secret; Nostr identity is session-local");
(fresh, false)
}
}
}
Err(e) => {
warn!(error = %e, "Failed to load Nostr signing secret; keeping ephemeral key");
return;
}
};
let installed = match nostr_arc.as_any().downcast_ref::<NostrTransport>() {
Some(nostr) => match nostr.install_signing_secret(&*secret) {
Ok(()) => true,
Err(e) => {
warn!(error = %e, "Failed to install Nostr signing key; keeping ephemeral key");
false
}
},
None => {
warn!("Transport registered as Nostr is not a NostrTransport; cannot install signing key");
false
}
};
if !persisted {
self.nostr_unpersisted_secret = Some(secret);
}
self.nostr_secret_persisted = installed && persisted;
}
fn with_nostr_transport<R>(&self, f: impl FnOnce(&NostrTransport) -> R) -> Option<R> {
let arc = self.transport_manager.get_transport(TransportType::Nostr)?;
let nostr = arc.as_any().downcast_ref::<NostrTransport>()?;
Some(f(nostr))
}
pub fn note_nostr_event_received(&mut self, created_at_secs: i64) {
let advanced = self
.with_nostr_transport(|nostr| nostr.advance_receive_watermark(created_at_secs))
.unwrap_or(false);
if advanced {
self.persist_nostr_watermark();
}
}
pub(crate) fn persist_nostr_watermark(&mut self) {
let Some(current) = self.with_nostr_transport(NostrTransport::receive_watermark_secs)
else {
return;
};
let Some(current) = current else {
return;
};
let due = match self.last_persisted_nostr_watermark {
Some(last) => {
current.saturating_sub(last) >= super::NOSTR_WATERMARK_PERSIST_INTERVAL_SECS
}
None => true,
};
if due {
self.write_nostr_watermark_to_storage(current);
}
}
pub(crate) fn flush_nostr_watermark(&mut self) {
let Some(Some(current)) = self.with_nostr_transport(NostrTransport::receive_watermark_secs)
else {
return;
};
if self.last_persisted_nostr_watermark != Some(current) {
self.write_nostr_watermark_to_storage(current);
}
}
fn write_nostr_watermark_to_storage(&mut self, value: i64) {
let Some(storage) = self.protocol_state_storage.clone() else {
return;
};
let bytes = value.to_le_bytes();
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::NOSTR_WATERMARK,
storage_keys::NOSTR_WATERMARK_ID,
&bytes,
) {
warn!(error = %e, "Failed to persist Nostr receive watermark");
return;
}
self.last_persisted_nostr_watermark = Some(value);
}
pub(crate) fn restore_nostr_watermark(&mut self) {
let Some(storage) = self.protocol_state_storage.clone() else {
return;
};
if self
.transport_manager
.get_transport(TransportType::Nostr)
.is_none()
{
return;
}
let data = match self.read_state_record(
storage.as_ref(),
storage_keys::NOSTR_WATERMARK,
storage_keys::NOSTR_WATERMARK_ID,
) {
Ok(Some(data)) => data,
Ok(None) => return,
Err(e) => {
warn!(
error = %e,
"Failed to read the Nostr receive watermark; \
this launch falls back to the first-run backfill window"
);
return;
}
};
let Ok(bytes) = <[u8; 8]>::try_from(data.as_slice()) else {
warn!(
len = data.len(),
"Corrupted Nostr receive watermark in storage (expected 8 bytes), starting fresh"
);
return;
};
let restored = i64::from_le_bytes(bytes);
self.with_nostr_transport(|nostr| nostr.advance_receive_watermark(restored));
let live = self
.with_nostr_transport(NostrTransport::receive_watermark_secs)
.flatten();
let adopted = matches!(live, Some(live) if live >= restored);
self.last_persisted_nostr_watermark = adopted.then_some(restored);
if adopted {
debug!(
watermark = restored,
live, "Restored Nostr receive watermark from storage"
);
} else {
warn!(
watermark = restored,
live,
"Refused a future-dated Nostr receive watermark from storage; \
keeping the live mark and rewriting on the next accepted event"
);
}
}
pub fn restore_nostr_transport_state(&mut self) {
self.restore_or_init_nostr_signing_secret();
self.restore_nostr_watermark();
}
fn adopt_fallback_secret(&mut self, secret: [u8; 16]) {
self.telemetry_fallback_secret = secret;
self.telemetry_scrubber = crate::telemetry::Scrubber::from_config(
&crate::telemetry::TelemetryConfig::default(),
secret,
);
}
pub fn telemetry_install_id(&self) -> Option<String> {
const TELEMETRY_INSTALL_ID_DOMAIN: &str = "telemetry:install-id";
self.telemetry_secret_persisted.then(|| {
crate::telemetry::scrubber::opaque_id(
TELEMETRY_INSTALL_ID_DOMAIN,
&self.telemetry_fallback_secret,
)
})
}
pub(crate) fn persist_lamport_clock(&mut self) {
let current = self.lamport_clock.value();
if current.wrapping_sub(self.last_persisted_lamport) < super::LAMPORT_PERSIST_INTERVAL {
return;
}
self.write_lamport_clock_to_storage(current);
}
pub(crate) fn flush_lamport_clock(&mut self) {
self.write_lamport_clock_to_storage(self.lamport_clock.value());
}
fn write_lamport_clock_to_storage(&mut self, value: u64) {
let Some(storage) = self.protocol_state_storage.clone() else {
return;
};
let bytes = value.to_le_bytes();
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::LAMPORT_CLOCK,
storage_keys::LAMPORT_CLOCK_ID,
&bytes,
) {
warn!(error = %e, "Failed to persist Lamport clock");
return;
}
self.last_persisted_lamport = value;
}
pub(crate) fn restore_lamport_clock(&mut self) {
let Some(storage) = self.protocol_state_storage.clone() else {
return;
};
if let Ok(Some(data)) = self.read_state_record(
storage.as_ref(),
storage_keys::LAMPORT_CLOCK,
storage_keys::LAMPORT_CLOCK_ID,
) {
if data.len() == 8 {
let restored = u64::from_le_bytes(data.try_into().expect("verified length is 8"));
let restored_clock = LamportClock::from_value(restored);
if restored_clock > self.lamport_clock {
self.lamport_clock = restored_clock;
}
self.last_persisted_lamport = self.lamport_clock.value();
debug!(clock = %self.lamport_clock, "Restored Lamport clock from storage");
} else {
warn!(
len = data.len(),
"Corrupted Lamport clock in storage (expected 8 bytes), starting fresh"
);
}
}
}
}
impl OfflineProtocol {
pub(crate) fn note_dedup_change(&mut self) {
self.dedup_dirty = self.dedup_dirty.saturating_add(1);
}
pub(crate) fn mark_seen_persisted(&mut self, message_id: MessageId) -> bool {
let fresh = self.deduplicator.mark_seen(message_id);
if fresh {
self.note_dedup_change();
}
fresh
}
pub(crate) fn unmark_seen_persisted(&mut self, message_id: &MessageId) -> bool {
let removed = self.deduplicator.unmark_seen(message_id);
if removed {
self.note_dedup_change();
}
removed
}
pub(crate) fn persist_dedup_seen_if_due(&mut self) {
if self.dedup_dirty == 0 {
return;
}
let due = self.dedup_dirty >= DEDUP_PERSIST_DIRTY_THRESHOLD
|| self.dedup_last_persist.elapsed() >= DEDUP_PERSIST_INTERVAL;
if due {
self.write_dedup_seen_to_storage();
}
}
pub(crate) fn flush_dedup_seen(&mut self) {
if self.dedup_dirty == 0 {
return;
}
self.write_dedup_seen_to_storage();
}
fn write_dedup_seen_to_storage(&mut self) {
let Some(storage) = self.protocol_state_storage.clone() else {
return;
};
let record = DedupSeenRecord {
version: DEDUP_SEEN_RECORD_VERSION,
entries: self.deduplicator.export_seen(MAX_PERSISTED_DEDUP_IDS),
};
let data = match serde_json::to_vec(&record) {
Ok(data) => data,
Err(e) => {
warn!(error = %e, "Failed to serialize deduplicator seen set");
return;
}
};
if let Err(e) = self.write_state_record(
storage.as_ref(),
storage_keys::DEDUP_SEEN_IDS,
storage_keys::DEDUP_SEEN_IDS_ID,
&data,
) {
warn!(error = %e, "Failed to persist deduplicator seen set");
return;
}
self.dedup_dirty = 0;
self.dedup_last_persist = Instant::now();
}
pub(crate) fn restore_dedup_seen(&mut self, allowance: &mut PruneAllowance) {
let Some(storage) = self.protocol_state_storage.clone() else {
return;
};
let mut budget = allowance.refusing_private();
let data = match self.read_state_record_detailed_budgeted(
storage.as_ref(),
storage_keys::DEDUP_SEEN_IDS,
storage_keys::DEDUP_SEEN_IDS_ID,
Some(&mut budget),
) {
Ok(StateRecord::Present(data)) => data,
Ok(StateRecord::Missing | StateRecord::Unreadable | StateRecord::Unavailable)
| Err(_) => return,
};
let record = match serde_json::from_slice::<DedupSeenRecord>(&data) {
Ok(record) if record.version == DEDUP_SEEN_RECORD_VERSION => record,
Ok(record) => {
warn!(
version = record.version,
"Dropping deduplicator seen set with an unknown record version"
);
if budget.claim() {
let _ = storage.delete(
storage_keys::DEDUP_SEEN_IDS,
storage_keys::DEDUP_SEEN_IDS_ID,
);
}
return;
}
Err(e) => {
warn!(error = %e, "Dropping corrupted deduplicator seen set");
if budget.claim() {
let _ = storage.delete(
storage_keys::DEDUP_SEEN_IDS,
storage_keys::DEDUP_SEEN_IDS_ID,
);
}
return;
}
};
let listed = record.entries.len();
let imported = self.deduplicator.import_seen(record.entries, Utc::now());
if imported > 0 {
debug!(
imported,
listed, "Restored deduplicator seen set from storage"
);
}
self.dedup_last_persist = Instant::now();
}
}
#[cfg(test)]
mod category_registration_tests {
use super::*;
#[test]
fn every_category_maps_back_from_its_key_type() {
for category in StateCategory::ALL {
let key_type = category.key_type();
assert_eq!(
StateCategory::from_key_type(key_type),
Some(*category),
"{category:?} has no from_key_type arm for {key_type:?}"
);
}
}
#[test]
fn key_types_are_unique_across_categories() {
let mut seen = std::collections::HashSet::new();
for category in StateCategory::ALL {
assert!(
seen.insert(category.key_type()),
"{:?} reuses key type {:?}",
category,
category.key_type()
);
}
}
#[test]
fn data_categories_are_sealed_at_rest() {
for category in [
StateCategory::DataDocs,
StateCategory::DataDeltaLog,
StateCategory::DataSpaces,
StateCategory::DataSync,
StateCategory::DataDocMeta,
] {
assert!(
category.requires_sealing(),
"{category:?} must be sealed: it carries document content"
);
}
}
#[test]
fn data_categories_are_not_adoptable() {
for key_type in [
storage_keys::DATA_DOCS,
storage_keys::DATA_DELTA_LOG,
storage_keys::DATA_SPACES,
storage_keys::DATA_SYNC,
storage_keys::DATA_DOC_META,
] {
assert!(
!storage_keys::ADOPTABLE_STATE_KEY_TYPES.contains(&key_type),
"{key_type} must not be adoptable"
);
}
}
}