use super::*;
type VerifiedSignature = (nostr::EventId, nostr::secp256k1::schnorr::Signature);
#[derive(PartialEq, Eq)]
enum AppKeysEventOutcome {
Invalid,
Unchanged,
Changed,
}
#[derive(Default)]
pub(super) struct EventValidationCache {
verified: HashSet<VerifiedSignature>,
order: VecDeque<VerifiedSignature>,
#[cfg(test)]
pub(super) signature_checks: usize,
}
impl EventValidationCache {
pub(super) fn verify(&mut self, event: &Event) -> bool {
if !event.verify_id() {
return false;
}
let key = (event.id, event.sig);
if self.verified.contains(&key) {
return true;
}
#[cfg(test)]
{
self.signature_checks += 1;
}
if !event.verify_signature() {
return false;
}
self.verified.insert(key);
self.order.push_back(key);
if self.order.len() > MAX_SEEN_EVENT_IDS {
if let Some(expired) = self.order.pop_front() {
self.verified.remove(&expired);
}
}
true
}
}
#[cfg(test)]
#[path = "relay_validation_tests.rs"]
mod validation_tests;
impl AppCore {
pub(super) fn handle_relay_event(&mut self, event: Event) {
self.handle_relay_event_with_channel(event, "message servers");
}
pub(super) fn handle_relay_event_with_channel(&mut self, event: Event, channel: &str) -> bool {
self.ingest_event(event, channel, false)
}
pub(super) fn handle_directed_private_event(&mut self, event: Event, channel: &str) -> bool {
self.ingest_event(event, channel, true)
}
fn ingest_event(&mut self, event: Event, channel: &str, directed: bool) -> bool {
if !self.event_validation.verify(&event) {
return false;
}
self.handle_verified_relay_event_with_channel(event, channel, directed);
true
}
fn handle_verified_relay_event_with_channel(
&mut self,
event: Event,
channel: &str,
directed: bool,
) {
if self.receive_private_contact_event(&event) {
return;
}
if self
.protocol_engine
.as_ref()
.is_some_and(|engine| engine.message_targets_another_device(&event))
{
return;
}
self.cache_local_fips_identity(&event);
let event_id = event.id.to_string();
let kind = event.kind.as_u16() as u32;
let is_app_keys_protocol_event = kind == APP_KEYS_EVENT_KIND && is_app_keys_event(&event);
let is_invite_protocol_event = is_protocol_invite_event(&event);
let is_invite_response_protocol_event = kind == INVITE_RESPONSE_KIND;
let message_has_header = kind == MESSAGE_EVENT_KIND && event_has_tag(&event, "header");
let is_known_group_sender_key_event = kind == MESSAGE_EVENT_KIND
&& self
.protocol_engine
.as_ref()
.is_some_and(|engine| engine.is_known_group_sender_event_author(event.pubkey));
if nostr_double_ratchet::is_group_roster_fact_event(&event)
|| (kind == MESSAGE_EVENT_KIND && !message_has_header)
{
self.remember_event(event_id);
return;
}
if is_known_group_sender_key_event {
if self.has_seen_event(&event_id) {
return;
}
let eligible = self.protocol_engine.as_ref().is_some_and(|engine| {
engine
.subscribable_group_sender_event_pubkeys()
.contains(&event.pubkey)
});
if eligible
&& publishing::message_event_names_nobody(&event)
&& self.receive_group_data(&event)
{
self.remember_event(event_id);
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
}
return;
}
let already_seen = self.has_seen_event(&event_id);
let must_reapply_app_keys = already_seen
&& is_app_keys_protocol_event
&& (!self.app_keys.contains_key(&event.pubkey.to_hex())
|| self.logged_in.as_ref().is_some_and(|logged_in| {
self.protocol_engine.as_ref().is_some_and(|engine| {
let missing = if logged_in.owner_pubkey == event.pubkey {
DirectSendReadiness::MissingLocalAppKeys
} else {
DirectSendReadiness::MissingPeerAppKeys
};
engine.direct_send_readiness(event.pubkey) == missing
})
}));
if already_seen && !must_reapply_app_keys {
if self.add_transport_channel_for_event_id(&event_id, channel) {
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
}
return;
}
self.event_transport_channels
.insert(event_id.clone(), channel.to_string());
if self.logged_in.is_none() {
if self.handle_pending_link_device_response(event) {
self.remember_event(event_id);
}
return;
}
if kind == MESSAGE_EVENT_KIND
&& self
.protocol_engine
.as_ref()
.is_some_and(|engine| engine.has_pending_inbound_direct_event_id(&event_id))
{
self.push_debug_log(
"appcore.protocol.message.pending_replay",
"already stored as pending inbound",
);
return;
}
self.push_debug_log("relay.event", format!("kind_raw={} id={event_id}", kind));
let protocol_inputs_changed = is_app_keys_protocol_event
|| is_invite_protocol_event
|| is_invite_response_protocol_event;
if kind == 0 {
if self.apply_profile_metadata_event(&event) {
self.remember_event(event_id);
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
return;
}
self.remember_event(event_id);
return;
}
match kind {
APP_KEYS_EVENT_KIND if is_app_keys_protocol_event => {
self.debug_event_counters.app_keys_events += 1;
match self.apply_app_keys_event_with_outcome(&event) {
Ok(outcome) => {
self.remember_event(event_id);
if outcome == AppKeysEventOutcome::Changed {
self.request_protocol_subscription_refresh();
}
}
Err(error) => {
self.push_debug_log("appcore.protocol.app_keys.error", error.to_string());
self.rebuild_state();
self.emit_state();
}
}
return;
}
INVITE_EVENT_KIND if is_invite_protocol_event => {
self.debug_event_counters.invite_events += 1;
let retry_results = self
.protocol_engine
.as_mut()
.map(|engine| engine.observe_invite_event_with_outcome(&event))
.transpose();
match retry_results {
Ok(Some(results)) => {
if !results.changed {
self.remember_event(event_id);
return;
}
self.process_protocol_engine_retry_batch(
"invite_event",
results.retry_batch,
);
}
Ok(None) => {}
Err(error) => {
self.push_debug_log("appcore.protocol.invite.error", error.to_string());
self.rebuild_state();
self.emit_state();
return;
}
}
}
INVITE_EVENT_KIND => {
self.remember_event(event_id);
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
return;
}
INVITE_RESPONSE_KIND => {
self.debug_event_counters.invite_response_events += 1;
match self.handle_private_chat_invite_response(&event) {
super::invites::PrivateInviteResponseDisposition::Handled => {
self.remember_event(event_id);
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
return;
}
super::invites::PrivateInviteResponseDisposition::RetryableFailure => {
self.rebuild_state();
self.emit_state();
return;
}
super::invites::PrivateInviteResponseDisposition::NotMatched => {}
}
let retry_results = self
.protocol_engine
.as_mut()
.map(|engine| engine.observe_invite_response_event(&event))
.transpose();
match retry_results {
Ok(Some(results)) => {
self.process_protocol_engine_retry_batch("invite_response", results);
self.refresh_local_authorization_state();
}
Ok(None) => {}
Err(error) => {
self.push_debug_log(
"appcore.protocol.invite_response.error",
error.to_string(),
);
self.rebuild_state();
self.emit_state();
return;
}
}
}
MESSAGE_EVENT_KIND => {
let unknown_message_author = self
.protocol_engine
.as_ref()
.is_some_and(|engine| !engine.is_known_message_author(event.pubkey));
if unknown_message_author {
let names_local_device =
message_has_header && self.message_names_local_device(&event);
let targets_local_recipient =
names_local_device && (directed || self.peer_session_bootstrap_needed());
let has_tracked_sender_session = message_has_header
&& self.protocol_engine.as_ref().is_some_and(|engine| {
engine.header_message_sender_has_tracked_session(&event)
});
let awaits_invite_response = directed
&& message_has_header
&& crate::core::publishing::message_event_names_nobody(&event)
&& self
.protocol_engine
.as_ref()
.and_then(ProtocolEngine::local_invite_response_pubkey)
.is_some();
let should_keep_unknown_header = has_tracked_sender_session
|| !self.private_chat_invites.is_empty()
|| targets_local_recipient
|| awaits_invite_response;
if !message_has_header || !should_keep_unknown_header {
self.push_debug_log(
"appcore.protocol.message.ignored",
format!(
"unknown message author header={message_has_header} targets_local_recipient={targets_local_recipient} tracked_sender_session={has_tracked_sender_session} awaits_invite_response={awaits_invite_response}"
),
);
return;
}
self.push_debug_log(
"appcore.protocol.message.pending_header",
format!(
"unknown message author targets_local_recipient={targets_local_recipient} tracked_sender_session={has_tracked_sender_session} awaits_invite_response={awaits_invite_response}"
),
);
}
self.debug_event_counters.message_events += 1;
}
_ => {
self.debug_event_counters.other_events += 1;
}
}
if kind == MESSAGE_EVENT_KIND {
if let Some(protocol_engine) = self.protocol_engine.as_mut() {
match protocol_engine.process_direct_message_event(&event) {
Ok(Some(decrypted)) => {
let event_id = decrypted.event_id.clone();
if !self.apply_decrypted_runtime_message_with_metadata(
decrypted.sender,
decrypted.sender_device,
decrypted.conversation_owner,
decrypted.content,
decrypted.event_id,
decrypted.created_at_secs,
) {
self.schedule_fast_protocol_retry_if_pending();
return;
}
if let Some(event_id) = event_id {
self.pending_decrypted_delivery_acks
.insert(event_id.clone());
self.remember_event(event_id);
}
self.mark_mobile_push_dirty();
self.request_protocol_subscription_refresh();
self.retry_protocol_engine_pending_work("direct_message");
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
return;
}
Ok(None) => {
self.push_debug_log(
"appcore.protocol.message.pending",
format!("event_id={event_id} author={}", event.pubkey),
);
self.request_protocol_subscription_refresh();
self.schedule_fast_protocol_retry_if_pending();
}
Err(error) => {
self.push_debug_log(
"appcore.protocol.message.error",
format!("event_id={event_id} author={} error={error}", event.pubkey),
);
if error.is::<iris_chat_protocol::PendingInboundAdmissionError>() {
return;
}
if error
.downcast_ref::<iris_chat_protocol::StorageError>()
.is_some()
{
self.schedule_fast_protocol_retry_if_pending();
} else {
self.remember_event(event_id);
}
return;
}
}
}
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
return;
}
self.remember_event(event_id);
if protocol_inputs_changed {
self.request_protocol_subscription_refresh();
self.schedule_fast_protocol_retry_if_pending();
}
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
}
fn message_names_local_device(&self, event: &Event) -> bool {
self.logged_in
.as_ref()
.is_some_and(|local| event_has_pubkey_tag(event, local.device_keys.public_key()))
}
fn handle_pending_link_device_response(&mut self, event: Event) -> bool {
let kind = event.kind.as_u16() as u32;
if kind == APP_KEYS_EVENT_KIND && is_app_keys_event(&event) {
let event_id = event.id.to_string();
let (owner_pubkey, pending_response, device_keys) = {
let Some(pending) = self.pending_linked_device.as_mut() else {
return false;
};
let owner = match resolve_app_keys_owner_for_device(
&event,
pending.device_keys.public_key(),
) {
Ok(Some(owner)) => owner,
_ => return false,
};
if pending.pending_response.as_ref().is_some_and(|response| {
!account_pending_link::link_approver_is_authorized(
&event,
response.owner_pubkey,
&response.peer_device_id,
)
}) {
return false;
}
pending.authorized_app_keys_event = Some(event.clone());
(
owner,
pending.pending_response.take(),
pending.device_keys.clone(),
)
};
self.push_debug_log(
"session.link_authorized",
format!("event_id={event_id} owner={}", owner_pubkey.to_hex()),
);
if let Some(response) = pending_response {
if let Err(error) = self.complete_pending_linked_device(
owner_pubkey,
response.peer_device_id,
response.session_state,
device_keys,
event,
) {
self.state.toast = Some(error.to_string());
self.rebuild_state();
self.emit_state();
}
}
return true;
}
if kind != INVITE_RESPONSE_KIND {
return false;
}
let Some((pairing_invite, device_secret)) =
self.pending_linked_device.as_ref().map(|pending| {
(
pending.pairing_invite.clone(),
pending.device_keys.secret_key().to_secret_bytes(),
)
})
else {
return false;
};
self.debug_event_counters.invite_response_events += 1;
let event_id = event.id.to_string();
let response = match nostr_double_ratchet::process_invite_response_event(
&pairing_invite,
&event,
device_secret,
) {
Ok(Some(response)) => response,
Ok(None) => return false,
Err(error) => {
self.push_debug_log(
"session.link_response.error",
format!("event_id={event_id} error={error}"),
);
return false;
}
};
let owner_pubkey = match (response.owner_public_key, response.invitee_owner_pubkey) {
(Some(owner), Some(claim)) if owner.to_bytes() == claim.to_bytes() => owner,
(None, None) => response.invitee_identity,
_ => return false,
};
let mut pending_response = Some(PendingLinkInviteResponse {
owner_pubkey,
peer_device_id: response.invitee_identity.to_hex(),
session_state: response.session.state,
});
let owner_and_device = {
let Some(pending) = self.pending_linked_device.as_mut() else {
return false;
};
if let Some(app_keys_event) =
pending.authorized_app_keys_event.clone().filter(|event| {
account_pending_link::link_approver_is_authorized(
event,
owner_pubkey,
&response.invitee_identity.to_hex(),
)
})
{
resolve_app_keys_owner_for_device(&app_keys_event, pending.device_keys.public_key())
.ok()
.flatten()
.map(|owner_pubkey| (owner_pubkey, pending.device_keys.clone(), app_keys_event))
} else {
pending.authorized_app_keys_event = None;
pending.pending_response = pending_response.take();
None
}
};
let Some((owner_pubkey, device_keys, app_keys_event)) = owner_and_device else {
self.push_debug_log(
"session.link_response",
format!("event_id={event_id} waiting_for_device_approval"),
);
return true;
};
let Some(response) = pending_response else {
return false;
};
let peer_device_id = response.peer_device_id;
let session_state = response.session_state;
self.push_debug_log(
"session.link_response",
format!(
"event_id={event_id} owner={} peer_device={peer_device_id}",
owner_pubkey.to_hex()
),
);
if let Err(error) = self.complete_pending_linked_device(
owner_pubkey,
peer_device_id,
session_state,
device_keys,
app_keys_event,
) {
self.state.toast = Some(error.to_string());
self.rebuild_state();
self.emit_state();
}
true
}
pub(super) fn process_protocol_engine_effects(&mut self, effects: Vec<ProtocolEffect>) {
if !self.persist_group_publish_batch(&effects) {
return;
}
for effect in effects {
match effect {
ProtocolEffect::Publish(publish) => {
self.publish_protocol_event(publish);
}
}
}
}
pub(super) fn ack_pending_decrypted_deliveries_after_app_persist(&mut self) {
if let Some(protocol_engine) = self.protocol_engine.as_mut() {
if let Err(error) =
protocol_engine.ack_decrypted_delivery_ids(&self.pending_decrypted_delivery_acks)
{
self.push_debug_log("appcore.protocol.decrypted_ack.error", error.to_string());
return;
}
}
self.pending_decrypted_delivery_acks.clear();
}
pub(super) fn apply_app_keys_event(&mut self, event: &Event) -> anyhow::Result<bool> {
Ok(self.apply_app_keys_event_with_outcome(event)? != AppKeysEventOutcome::Invalid)
}
fn apply_app_keys_event_with_outcome(
&mut self,
event: &Event,
) -> anyhow::Result<AppKeysEventOutcome> {
let should_publish_backfilled_owner_app_keys =
self.logged_in.as_ref().is_some_and(|logged_in| {
self.defer_owner_app_keys_publish
&& logged_in.owner_keys.is_some()
&& logged_in.owner_pubkey == event.pubkey
});
let app_keys = self
.logged_in
.as_ref()
.and_then(|logged_in| {
logged_in
.owner_keys
.as_ref()
.filter(|keys| keys.public_key() == event.pubkey)
.and_then(|keys| AppKeys::from_event_with_labels(event, keys).ok())
})
.or_else(|| AppKeys::from_event(event).ok());
let Some(app_keys) = app_keys else {
return Ok(AppKeysEventOutcome::Invalid);
};
self.capture_authorization_event(event)?;
let owner_hex = event.pubkey.to_hex();
let current = self.app_keys.get(&owner_hex).cloned();
let current_app_keys = current.as_ref().map(known_app_keys_to_ndr);
let current_created_at = current
.as_ref()
.map(|known| known.created_at_secs)
.unwrap_or_default();
let required_device = self
.logged_in
.as_ref()
.filter(|logged_in| {
self.defer_owner_app_keys_publish
&& logged_in.owner_keys.is_some()
&& logged_in.owner_pubkey == event.pubkey
})
.map(|logged_in| {
DeviceEntry::new(logged_in.device_keys.public_key(), unix_now().get())
});
let (effective_app_keys, effective_created_at) = if should_publish_backfilled_owner_app_keys
{
let mut merged = current_app_keys.unwrap_or_else(|| AppKeys::new(Vec::new()));
for device in app_keys.get_all_devices() {
merged.add_device(device);
}
if let Some(required_device) = required_device {
merged.add_device(required_device);
}
(
merged,
next_app_keys_created_at(
unix_now().get(),
current_created_at.max(event.created_at.as_secs()),
),
)
} else {
let applied = apply_app_keys_snapshot_with_required_device(
current_app_keys.as_ref(),
current_created_at,
&app_keys,
event.created_at.as_secs(),
required_device,
);
(applied.app_keys, applied.created_at)
};
self.invalidate_removed_device_history(
event.pubkey,
current.as_ref(),
&known_app_keys_from_ndr(event.pubkey, &effective_app_keys, effective_created_at),
)?;
let protocol_result = if let Some(protocol_engine) = self.protocol_engine.as_mut() {
protocol_engine.ingest_app_keys_event_with_outcome(event)?
} else {
iris_chat_protocol::ProtocolMetadataIngestionResult::default()
};
self.cache_local_fips_identity(event);
let mut known =
known_app_keys_from_ndr(event.pubkey, &effective_app_keys, effective_created_at);
if should_publish_backfilled_owner_app_keys {
if let Some(device_pubkey) = self
.logged_in
.as_ref()
.filter(|logged_in| logged_in.owner_pubkey == event.pubkey)
.map(|logged_in| logged_in.device_keys.public_key())
{
self.apply_current_device_labels_to_known_app_keys(&mut known, device_pubkey);
}
}
let app_keys_changed = current.as_ref() != Some(&known);
if app_keys_changed {
self.app_keys.insert(owner_hex, known);
self.bump_user_discovery_revision();
}
if should_publish_backfilled_owner_app_keys {
self.defer_owner_app_keys_publish = false;
self.publish_local_app_keys();
}
let threads_changed =
self.migrate_verified_device_owner_threads(event.pubkey, &effective_app_keys);
self.complete_authorized_device_link();
let authorization_changed = self.refresh_local_authorization_state();
let changed = app_keys_changed
|| protocol_result.changed
|| threads_changed
|| authorization_changed
|| should_publish_backfilled_owner_app_keys;
if changed {
if !self.state.busy.restoring_session {
self.reconcile_device_sync();
}
self.mark_mobile_push_dirty();
self.rebuild_persist_and_emit_state();
self.process_protocol_engine_retry_batch("app_keys", protocol_result.retry_batch);
}
self.retry_pending_private_invite_responses(event.pubkey);
self.resume_pending_outgoing_invite_acceptance(event.pubkey);
if app_keys_changed && self.device_sync_tracks_app_keys_owner(event.pubkey) {
self.broadcast_device_sync_snapshot();
}
if should_publish_backfilled_owner_app_keys {
self.retry_protocol_engine_pending_work("owner_registration_backfill");
}
if app_keys_changed && !self.fips_nearby_links.is_empty() {
self.emit_fips_nearby_peers();
}
Ok(if changed {
AppKeysEventOutcome::Changed
} else {
AppKeysEventOutcome::Unchanged
})
}
}
fn is_protocol_invite_event(event: &Event) -> bool {
event.kind.as_u16() as u32 == INVITE_EVENT_KIND
&& event.tags.iter().any(|tag| {
let values = tag.as_slice();
values.first().map(|value| value.as_str()) == Some("d")
&& values
.get(1)
.is_some_and(|value| value.starts_with(NDR_INVITES_D_TAG_PREFIX))
})
}
fn event_has_tag(event: &Event, name: &str) -> bool {
event
.tags
.iter()
.any(|tag| tag.as_slice().first().map(|value| value.as_str()) == Some(name))
}
fn event_has_pubkey_tag(event: &Event, pubkey: PublicKey) -> bool {
let pubkey_hex = pubkey.to_hex();
event.tags.iter().any(|tag| {
let values = tag.as_slice();
values.first().map(|value| value.as_str()) == Some("p")
&& values.get(1).map(|value| value.as_str()) == Some(pubkey_hex.as_str())
})
}
#[cfg(test)]
mod tests {
use super::*;
use nostr::Tag;
#[test]
fn parses_message_expiration_tag_seconds_and_milliseconds() {
let tags = vec![Tag::parse(["expiration", "1704067260123"]).expect("expiration tag")];
assert_eq!(message_expiration_from_tags(&tags), Some(1_704_067_260));
}
}