feat(fof-layer2): revocation diff — sign + verify + propagate + cascade
Wires the full revocation primitive end-to-end:
Wire format:
- BlobHeaderDiffOp::FoFRevocation { post_id, revoked_pub_x,
revoked_at_ms, reason_code, author_sig }. 64-byte Ed25519 sig by
post author over (post_id || revoked_pub_x || ms_le || reason).
fof.rs additions:
- sign_fof_revocation(author_secret, ...): builds the canonical
signing tuple and signs.
- verify_fof_revocation(post_author, ...): Ed25519 verify; false on
any shape/key/sig failure. CDN-verified before any side effect.
- apply_fof_revocation_locally(storage, ...): records in
fof_revocations + cascades retroactive delete of locally-stored
comments matching the revoked pub_x via pub_post_set lookup.
Receive path (connection.rs): new arm for FoFRevocation diffs.
Looks up post.author from storage, verifies author_sig (rejects
diffs where payload.author != post author or sig invalid), then
applies locally. Propagation continues via existing mechanism.
Author API (node.rs): Node::revoke_fof_commenter(post_id,
pub_x_index, reason_code) resolves pub_x from gating.pub_post_set,
signs with the persona's identity secret, applies locally for
immediate UI update, then propagates via propagate_engagement_diff.
Two new fof tests bring the suite to 141 passing:
- fof_revocation_cascades: full author → publish → commenter →
revoke → cascade-delete + recorded-in-storage roundtrip.
- fof_revocation_wrong_author_rejected: Mallory signs claiming
Alice's authorship → verify rejects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
583033e065
commit
6a76adef8f
4 changed files with 294 additions and 0 deletions
|
|
@ -4753,6 +4753,70 @@ impl Node {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// FoF Layer 2: revoke a specific pub_x from a FoF-gated post the
|
||||
/// caller authored. Builds a signed FoFRevocation diff, applies it
|
||||
/// locally (record + cascade delete), and propagates via the
|
||||
/// standard engagement-diff path. Idempotent.
|
||||
///
|
||||
/// Caller passes the `pub_x_index` (from a stored comment they want
|
||||
/// to revoke). The pub_x bytes are resolved via the post's
|
||||
/// pub_post_set; if the post or index is missing, returns Err.
|
||||
pub async fn revoke_fof_commenter(
|
||||
&self,
|
||||
post_id: PostId,
|
||||
pub_x_index: u32,
|
||||
reason_code: u8,
|
||||
) -> anyhow::Result<()> {
|
||||
// Resolve pub_x bytes + confirm we authored the post.
|
||||
let (post_author, posting_secret, revoked_pub_x) = {
|
||||
let storage = self.storage.get().await;
|
||||
let post = storage.get_post(&post_id)?
|
||||
.ok_or_else(|| anyhow::anyhow!("post not found"))?;
|
||||
let gating = post.fof_gating.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("post is not FoF-gated"))?;
|
||||
let pub_x = gating.pub_post_set.get(pub_x_index as usize).copied()
|
||||
.ok_or_else(|| anyhow::anyhow!("pub_x_index out of bounds"))?;
|
||||
let identity = storage.get_posting_identity(&post.author)?
|
||||
.ok_or_else(|| anyhow::anyhow!("post author not on this device"))?;
|
||||
(post.author, identity.secret_seed, pub_x)
|
||||
};
|
||||
|
||||
let revoked_at_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_millis() as u64;
|
||||
let author_sig = crate::fof::sign_fof_revocation(
|
||||
&posting_secret, &post_id, &revoked_pub_x, revoked_at_ms, reason_code,
|
||||
);
|
||||
|
||||
// Apply locally first so the author's UI updates immediately.
|
||||
{
|
||||
let storage = self.storage.get().await;
|
||||
let _ = crate::fof::apply_fof_revocation_locally(
|
||||
&*storage, &post_id, &revoked_pub_x, revoked_at_ms, reason_code, &author_sig,
|
||||
);
|
||||
}
|
||||
|
||||
// Propagate the diff.
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_millis() as u64;
|
||||
let diff = crate::protocol::BlobHeaderDiffPayload {
|
||||
post_id,
|
||||
author: post_author,
|
||||
ops: vec![crate::types::BlobHeaderDiffOp::FoFRevocation {
|
||||
post_id,
|
||||
revoked_pub_x,
|
||||
revoked_at_ms,
|
||||
reason_code,
|
||||
author_sig,
|
||||
}],
|
||||
timestamp_ms: now,
|
||||
};
|
||||
self.network.propagate_engagement_diff(&post_id, &diff, &post_author).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the comment policy for a post.
|
||||
pub async fn get_comment_policy(&self, post_id: PostId) -> anyhow::Result<Option<crate::types::CommentPolicy>> {
|
||||
let storage = self.storage.get().await;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue