Phase 2b: control-post flow (delete/visibility) + remove BlobDeleteNotice
Replaces two persona-signed direct pushes with CDN-propagated control posts:
a single `VisibilityIntent::Control` post type whose content is a signed
`ControlOp` the receiver verifies and applies. Deletes and visibility updates
now flow through the same neighbor-manifest CDN path as regular content — no
direct recipient push needed for persona-signed ops.
Core pieces:
- `VisibilityIntent::Control` + `VisibilityIntent::Profile` variants.
- `ControlOp::DeletePost` / `ControlOp::UpdateVisibility` (JSON, ed25519-signed
by the target post's author over op-specific byte strings).
- `crypto::{sign,verify}_control_{delete,visibility}` signing primitives.
- `control::build_delete_control_post` + `build_visibility_control_post`
for authors to construct control posts.
- `control::receive_post` — unified incoming-post path used by all 6 receive
sites. Verifies control signatures BEFORE storing, so bogus controls never
enter storage and can't be re-propagated via neighbor-manifest diffs.
- `control::apply_control_post_if_applicable` — executes the op under the
same storage guard as the insert.
Feed filter:
- Feeds (`get_feed`, `get_feed_page`, `list_posts_page`,
`list_posts_reverse_chron`) now exclude `Control` and `Profile` posts so
they propagate + tombstone without surfacing.
- Sync/export path (`list_posts_with_visibility`) keeps its own unfiltered
query so control posts still propagate via CDN.
Wire protocol:
- `SyncPost` carries `intent: Option<VisibilityIntent>` so control posts
arrive with their intent preserved.
- `BlobDeleteNotice` (0x95) removed — orphan blobs on remote holders evict
naturally via LRU rather than via a persona-signed push. Code path,
payload, sender, tests, and `delete_blob_with_cdn_notify` all gone.
Tests: control delete roundtrip (apply + tombstone) and wrong-author
rejection (not stored, not applied). 112/112 core tests pass.
This commit is contained in:
parent
4da6a8dc85
commit
36b6a466d2
10 changed files with 585 additions and 196 deletions
|
|
@ -289,6 +289,78 @@ pub fn sign_delete(seed: &[u8; 32], post_id: &PostId) -> Vec<u8> {
|
|||
sig.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Canonical bytes for a ControlOp::DeletePost signature.
|
||||
fn control_delete_bytes(post_id: &PostId, timestamp_ms: u64) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(12 + 32 + 8);
|
||||
buf.extend_from_slice(b"ctrl:delete:");
|
||||
buf.extend_from_slice(post_id);
|
||||
buf.extend_from_slice(×tamp_ms.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Sign a control-post DeletePost operation.
|
||||
pub fn sign_control_delete(seed: &[u8; 32], post_id: &PostId, timestamp_ms: u64) -> Vec<u8> {
|
||||
let signing_key = SigningKey::from_bytes(seed);
|
||||
let sig = signing_key.sign(&control_delete_bytes(post_id, timestamp_ms));
|
||||
sig.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
pub fn verify_control_delete(
|
||||
author: &NodeId,
|
||||
post_id: &PostId,
|
||||
timestamp_ms: u64,
|
||||
signature: &[u8],
|
||||
) -> bool {
|
||||
if signature.len() != 64 { return false; }
|
||||
let sig_bytes: [u8; 64] = match signature.try_into() { Ok(b) => b, Err(_) => return false };
|
||||
let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
|
||||
let Ok(vk) = VerifyingKey::from_bytes(author) else { return false };
|
||||
vk.verify_strict(&control_delete_bytes(post_id, timestamp_ms), &sig).is_ok()
|
||||
}
|
||||
|
||||
/// Canonical bytes for a ControlOp::UpdateVisibility signature. Uses JSON
|
||||
/// round-trip on the visibility payload because PostVisibility is an enum
|
||||
/// with variable shape; callers must pass the exact same bytes when verifying.
|
||||
fn control_visibility_bytes(
|
||||
post_id: &PostId,
|
||||
new_visibility_canonical: &[u8],
|
||||
timestamp_ms: u64,
|
||||
) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(10 + 32 + new_visibility_canonical.len() + 8);
|
||||
buf.extend_from_slice(b"ctrl:vis:");
|
||||
buf.extend_from_slice(post_id);
|
||||
buf.extend_from_slice(new_visibility_canonical);
|
||||
buf.extend_from_slice(×tamp_ms.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn sign_control_visibility(
|
||||
seed: &[u8; 32],
|
||||
post_id: &PostId,
|
||||
new_visibility: &crate::types::PostVisibility,
|
||||
timestamp_ms: u64,
|
||||
) -> Vec<u8> {
|
||||
let canon = serde_json::to_vec(new_visibility).unwrap_or_default();
|
||||
let signing_key = SigningKey::from_bytes(seed);
|
||||
let sig = signing_key.sign(&control_visibility_bytes(post_id, &canon, timestamp_ms));
|
||||
sig.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
pub fn verify_control_visibility(
|
||||
author: &NodeId,
|
||||
post_id: &PostId,
|
||||
new_visibility: &crate::types::PostVisibility,
|
||||
timestamp_ms: u64,
|
||||
signature: &[u8],
|
||||
) -> bool {
|
||||
if signature.len() != 64 { return false; }
|
||||
let sig_bytes: [u8; 64] = match signature.try_into() { Ok(b) => b, Err(_) => return false };
|
||||
let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
|
||||
let Ok(vk) = VerifyingKey::from_bytes(author) else { return false };
|
||||
let canon = match serde_json::to_vec(new_visibility) { Ok(v) => v, Err(_) => return false };
|
||||
vk.verify_strict(&control_visibility_bytes(post_id, &canon, timestamp_ms), &sig).is_ok()
|
||||
}
|
||||
|
||||
/// Verify an ed25519 delete signature: the author's public key signed the post_id.
|
||||
pub fn verify_delete_signature(author: &NodeId, post_id: &PostId, signature: &[u8]) -> bool {
|
||||
if signature.len() != 64 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue