feat: audit codebase (#72)

Reviewed-on: #72
This commit was merged in pull request #72.
This commit is contained in:
2026-11-14 01:27:25 +00:00
parent 8fa79aef33
commit 620cbaf89c
60 changed files with 3493 additions and 888 deletions
-1
View File
@@ -11,7 +11,6 @@ futures.workspace = true
serde_json.workspace = true
state = { path = "../state" }
common = { path = "../common" }
person = { path = "../person" }
chat = { path = "../chat" }
gpui.workspace = true
+16 -3
View File
@@ -13,7 +13,7 @@ use crate::LOCAL_KEYS;
use crate::protocol::{
ACTIVITY_KINDS, Activity, GroupAdmins, GroupId, GroupKey, GroupMembers, GroupMetadata,
GroupPins, GroupRoles, MEMBERSHIP_KINDS, Membership, PIN_LIST, Pin, RENDER_KINDS, STATE_KINDS,
TimelineWindow, tag_value,
TimelineWindow, is_newer_event, tag_value,
};
const REPLAY_LIMIT: usize = 100;
@@ -91,7 +91,8 @@ pub struct Group {
membership: Membership,
membership_events: Vec<Event>,
elsewhere: Option<GroupKey>,
state_ids: HashMap<Kind, EventId>,
/// The last accepted (created_at, id) per state kind.
state_ids: HashMap<Kind, (Timestamp, EventId)>,
rows: Vec<Row>,
index: HashSet<EventId>,
reactions: HashMap<EventId, Vec<Reaction>>,
@@ -694,6 +695,15 @@ impl Group {
}
fn absorb_state(&mut self, event: &Event, me: PublicKey) -> bool {
// Relays deliver same-kind state events in any order.
if !is_newer_event(
self.state_ids.get(&event.kind).copied(),
event.created_at,
event.id,
) {
return false;
}
match event.kind {
Kind::GroupMetadata => match GroupMetadata::parse(event) {
Ok(metadata) => self.metadata = Some(metadata),
@@ -724,7 +734,10 @@ impl Group {
_ => return false,
}
self.state_ids.insert(event.kind, event.id) != Some(event.id)
self.state_ids
.insert(event.kind, (event.created_at, event.id));
true
}
fn adopt_roster_membership(&mut self, me: PublicKey) -> bool {
+43 -17
View File
@@ -76,6 +76,8 @@ pub struct GroupsRegistry {
groups: Vec<Entity<Group>>,
previews: HashMap<GroupKey, Entity<Group>>,
list: GroupList,
/// The (created_at, id) of the last accepted group list event.
list_stamp: Option<(Timestamp, EventId)>,
browse: HashMap<SubscriptionId, RelayUrl>,
routes: HashMap<SubscriptionId, GroupKey>,
observers: HashMap<GroupKey, Subscription>,
@@ -132,6 +134,7 @@ impl GroupsRegistry {
groups: Vec::new(),
previews: HashMap::new(),
list: GroupList::default(),
list_stamp: None,
browse: HashMap::new(),
routes: HashMap::new(),
observers: HashMap::new(),
@@ -455,6 +458,8 @@ impl GroupsRegistry {
self.fork_authors.clear();
self.syncing.clear();
self.list = GroupList::default();
// A new signer's list events must not be judged against the old stamp.
self.list_stamp = None;
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
@@ -488,24 +493,26 @@ impl GroupsRegistry {
let signer = nostr.read(cx).signer();
// Show the cached groups before the relays answer.
let task: Task<Result<GroupList, Error>> = cx.background_spawn(async move {
let me = signer.get_public_key_async().await?;
let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1);
let task: Task<Result<(GroupList, Option<Event>), Error>> =
cx.background_spawn(async move {
let me = signer.get_public_key_async().await?;
let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1);
match client.database().query(filter).await?.into_iter().next() {
Some(event) => {
let list = GroupList::parse(&event)?;
Ok(Self::hydrate_list(list, &signer, &me).await)
match client.database().query(filter).await?.into_iter().next() {
Some(event) => {
let list =
Self::hydrate_list(GroupList::parse(&event)?, &signer, &me).await;
Ok((list, Some(event)))
}
None => Ok((GroupList::default(), None)),
}
None => Ok(GroupList::default()),
}
});
});
self.tasks
.push(cx.spawn(async move |this, cx| match task.await {
Ok(list) => {
Ok((list, event)) => {
this.update(cx, |this, cx| {
this.merge(list, cx);
this.merge(list, event.as_ref(), cx);
})
.ok();
}
@@ -538,7 +545,16 @@ impl GroupsRegistry {
self.watch_forks(cx);
}
fn merge(&mut self, list: GroupList, cx: &mut Context<Self>) {
fn merge(&mut self, list: GroupList, event: Option<&Event>, cx: &mut Context<Self>) {
if let Some(event) = event {
// Relays replay list events out of order; only strictly newer applies.
if !is_newer_event(self.list_stamp, event.created_at, event.id) {
return;
}
self.list_stamp = Some((event.created_at, event.id));
}
// Carry the event's private section so a later publish can preserve it.
self.list
.set_content(list.content().map(str::to_owned), list.content_readable());
@@ -589,20 +605,21 @@ impl GroupsRegistry {
};
if list.content().is_none() {
self.merge(list, cx);
self.merge(list, Some(&event), cx);
return;
}
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let Some(me) = nostr.read(cx).current_user() else {
self.merge(list, cx);
self.merge(list, Some(&event), cx);
return;
};
self.tasks.push(cx.spawn(async move |this, cx| {
let list = Self::hydrate_list(list, &signer, &me).await;
this.update(cx, |this, cx| this.merge(list, cx)).ok();
this.update(cx, |this, cx| this.merge(list, Some(&event), cx))
.ok();
}));
}
@@ -864,7 +881,7 @@ impl GroupsRegistry {
let me = nostr.read(cx).current_user();
let list = self.list.clone();
self.tasks.push(cx.spawn(async move |_this, _cx| {
self.tasks.push(cx.spawn(async move |this, cx| {
// Re-encrypt the private section when it carries local entries; a
// section this session could not read is carried verbatim.
let content = match (list.has_private(), list.content_readable()) {
@@ -893,6 +910,15 @@ impl GroupsRegistry {
}
};
// Our own publish is the newest list until a newer event arrives.
if let Err(error) = this.update(cx, |this, _cx| {
if is_newer_event(this.list_stamp, event.created_at, event.id) {
this.list_stamp = Some((event.created_at, event.id));
}
}) {
log::warn!("recording the published list failed: {error}");
}
match client.send_event(&event).to_nip65().await {
Ok(output) if output.failed.is_empty() => {}
Ok(output) => {
+26 -5
View File
@@ -1,10 +1,9 @@
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use anyhow::{Result, anyhow, bail};
use common::stable_id_u64;
use nostr_sdk::prelude::*;
pub(crate) const TIMELINE_WINDOW: usize = 50;
@@ -50,6 +49,15 @@ pub(crate) fn tag_value<'a>(tags: &'a Tags, name: &str) -> Option<&'a str> {
.and_then(|tag| tag.content())
}
/// Whether `(created_at, id)` is strictly newer than the last accepted stamp.
pub(crate) fn is_newer_event(
accepted: Option<(Timestamp, EventId)>,
created_at: Timestamp,
id: EventId,
) -> bool {
accepted.is_none_or(|last| (created_at, id) > last)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GroupId(String);
@@ -280,9 +288,7 @@ impl GroupKey {
}
pub fn uniq_id(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
stable_id_u64(&[self.relay.as_str(), self.id.as_str()])
}
}
@@ -1152,4 +1158,19 @@ mod tests {
let malformed = Tag::custom("previous", ["nothex"]);
assert_eq!(window.references_seen(Some(&malformed)), Some(false));
}
#[test]
fn is_newer_event_accepts_only_strictly_newer_events() {
let lower = EventId::from_byte_array([0x00; 32]);
let higher = EventId::from_byte_array([0xff; 32]);
let earlier = Timestamp::from(100);
let later = Timestamp::from(200);
assert!(is_newer_event(None, earlier, lower));
assert!(!is_newer_event(Some((earlier, lower)), earlier, lower));
assert!(is_newer_event(Some((earlier, lower)), later, lower));
assert!(!is_newer_event(Some((later, lower)), earlier, higher));
assert!(is_newer_event(Some((earlier, lower)), earlier, higher));
assert!(!is_newer_event(Some((earlier, higher)), earlier, lower));
}
}
+5 -6
View File
@@ -1,7 +1,6 @@
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use common::stable_id_u64;
use nostr_sdk::prelude::*;
use crate::{GroupId, GroupKey};
@@ -25,10 +24,10 @@ pub(crate) fn subscription_id(key: &GroupKey) -> SubscriptionId {
}
pub(crate) fn browse_subscription(relay: &RelayUrl) -> SubscriptionId {
let mut hasher = DefaultHasher::new();
relay.hash(&mut hasher);
SubscriptionId::new(format!("{BROWSE_PREFIX}{:08x}", hasher.finish() as u32))
SubscriptionId::new(format!(
"{BROWSE_PREFIX}{:08x}",
stable_id_u64(&[relay.as_str()]) as u32
))
}
pub(crate) fn page_subscription(key: &GroupKey) -> SubscriptionId {