v0.4.1: Security hardening, lock contention fixes, data cleanup

Security:
- Reaction signatures: ed25519 sign/verify (sign_reaction, verify_reaction_signature)
  Backward-compatible — unsigned reactions from old nodes still accepted
- Comment signature verification: verify_comment_signature now called on receipt
- Reaction removal authorization: only reactor or post author can remove
- BlobHeader author verification: lookup actual author from storage, don't trust payload

Lock contention (4 fixes):
- ManifestPush discovery: cm lock released before PostFetch I/O
- Pull request handler: load under lock, filter without lock, brief re-lock for is_deleted
- Pull sender: split into two brief locks (store posts, then batch upstream+sync)
- Engagement checker: batch all chunk results, single lock for writes

Data cleanup:
- Post deletion cleans post_downstream, post_upstream, seen_engagement tables
- Added TODO-hardening.md documenting remaining DOS/security/lock/data issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-21 19:30:38 -04:00
parent bbaacf9b6c
commit bb6f2b64b0
11 changed files with 500 additions and 138 deletions

View file

@ -2197,12 +2197,18 @@ impl Storage {
Ok(inserted > 0)
}
/// Apply a delete: remove the post from the posts table if author matches.
/// Apply a delete: remove the post from the posts table if author matches,
/// and clean up associated downstream/upstream/engagement tracking rows.
pub fn apply_delete(&self, record: &DeleteRecord) -> anyhow::Result<bool> {
let deleted = self.conn.execute(
"DELETE FROM posts WHERE id = ?1 AND author = ?2",
params![record.post_id.as_slice(), record.author.as_slice()],
)?;
if deleted > 0 {
self.conn.execute("DELETE FROM post_downstream WHERE post_id = ?1", params![record.post_id.as_slice()])?;
self.conn.execute("DELETE FROM post_upstream WHERE post_id = ?1", params![record.post_id.as_slice()])?;
self.conn.execute("DELETE FROM seen_engagement WHERE post_id = ?1", params![record.post_id.as_slice()])?;
}
Ok(deleted > 0)
}
@ -4253,6 +4259,7 @@ impl Storage {
timestamp_ms: ts as u64,
encrypted_payload: enc,
deleted_at: None,
signature: vec![],
});
}
Ok(result)
@ -4286,6 +4293,7 @@ impl Storage {
timestamp_ms: ts as u64,
encrypted_payload: enc,
deleted_at: del.map(|v| v as u64),
signature: vec![],
});
}
Ok(result)
@ -5894,6 +5902,7 @@ mod tests {
timestamp_ms: 1000,
encrypted_payload: None,
deleted_at: None,
signature: vec![],
}).unwrap();
s.store_reaction(&Reaction {
@ -5903,6 +5912,7 @@ mod tests {
timestamp_ms: 1001,
encrypted_payload: None,
deleted_at: None,
signature: vec![],
}).unwrap();
s.store_reaction(&Reaction {
@ -5912,6 +5922,7 @@ mod tests {
timestamp_ms: 1002,
encrypted_payload: None,
deleted_at: None,
signature: vec![],
}).unwrap();
let reactions = s.get_reactions(&post_id).unwrap();