feat(fof-layer2): access-grant — retroactive read+comment widening
Wires the access-grant primitive end-to-end:
Wire format:
- BlobHeaderDiffOp::FoFAccessGrant { post_id, new_pub_x,
new_wrap_slot, granted_at_ms, author_sig }. 64-byte Ed25519 sig
by post author over canonicalized tuple.
fof.rs:
- sign_fof_access_grant / verify_fof_access_grant: identical shape
to revocation but covers (pub_x, wrap_slot, granted_at).
- apply_fof_access_grant_locally: appends to local pub_post_set +
wrap_slots. Refuses to apply if new_pub_x is already revoked
(prevents accidental re-admission of a previously-blocked signer
per Layer 4 resolved decision). Idempotent on (post_id, new_pub_x).
storage.rs:
- append_fof_access_grant(post_id, new_pub_x, new_wrap_slot): mutates
the stored post's fof_gating_json column to append the new entry.
PostId (in id column) is unaffected — local-evolution semantics:
the stored gating diverges from the original t=0 snapshot as
access-grants and revocations land.
connection.rs: receive arm verifies author_sig + applies locally.
Author API (node.rs):
- Node::grant_fof_access(post_id, new_v_x): recovers the post's CEK
by trial-unwrapping the author's own slot (find_unlock_for_post),
generates a fresh per-V_x keypair, seals a new wrap slot under
new_v_x with the same CEK + slot_binder_nonce, signs the grant,
applies locally for immediate UI, then propagates via
propagate_engagement_diff.
New test brings the suite to 142 passing:
- fof_access_grant_appends_and_unlocks: pre-grant Carol cannot
unlock; Alice grants; post-grant Carol unlocks and recovers the
CEK; duplicate grant skipped; revoked pub_x cannot be re-admitted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a76adef8f
commit
96118d7ce8
5 changed files with 310 additions and 0 deletions
|
|
@ -5024,6 +5024,30 @@ impl Storage {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// FoF Layer 2: append a new (pub_x, wrap_slot) entry to a stored
|
||||
/// post's fof_gating. Local-only mutation; PostId (in the `id`
|
||||
/// column) is unaffected. Idempotent on `(post_id, new_pub_x)`.
|
||||
pub fn append_fof_access_grant(
|
||||
&self,
|
||||
post_id: &PostId,
|
||||
new_pub_x: &[u8; 32],
|
||||
new_wrap_slot: &crate::types::WrapSlot,
|
||||
) -> anyhow::Result<bool> {
|
||||
let Some(mut post) = self.get_post(post_id)? else { return Ok(false); };
|
||||
let Some(mut gating) = post.fof_gating.take() else { return Ok(false); };
|
||||
if gating.pub_post_set.iter().any(|p| p == new_pub_x) {
|
||||
return Ok(false); // already present
|
||||
}
|
||||
gating.pub_post_set.push(*new_pub_x);
|
||||
gating.wrap_slots.push(new_wrap_slot.clone());
|
||||
let gating_json = serde_json::to_string(&gating)?;
|
||||
self.conn.execute(
|
||||
"UPDATE posts SET fof_gating_json = ?1 WHERE id = ?2",
|
||||
params![gating_json, post_id.as_slice()],
|
||||
)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// FoF Layer 2: record a post-level revocation locally. Idempotent
|
||||
/// on `(post_id, revoked_pub_x)`. Subsequent incoming comments
|
||||
/// where `pub_post_set[pub_x_index] == revoked_pub_x` are rejected
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue