itsgoin/docs/fof-spec/layer-2-mode2-fof-comments.md
Scott Reimers 1fdf9a94cc docs: FoF-gating spec skeleton (hand-off to Opus)
Drafts the Friend-of-Friend post-gating spec with crypto specifics
marked TBD — OPUS for Opus to fill in. Six-layer implementation plan;
each layer independently shippable.

Includes README overview + six layer files:
- Layer 1: V_me vouch primitive (keys, keyring, VouchGrant wire format)
- Layer 2: Mode 2 — public post + FoF-gated comments
- Layer 3: Mode 1 — FoFClosed (encrypted body via wrap_slots + prefilter)
- Layer 4: per-post keypair rotation
- Layer 5: unlock cache + prefilter optimization (perf-critical)
- Layer 6: revocation (stub; likely deferred post-v1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:20:56 -04:00

127 lines
7.6 KiB
Markdown

# Layer 2 — Mode 2: Public Posts with FoF-Gated Comments
**Scope**: Extend `CommentPolicy` with a `FriendsOfFriends` variant. Post body is public (indexable, cacheable, shardable via existing CDN). Comments on the post must prove FoF-reachability to the author before being accepted.
This layer ships before Mode 1 because it reuses the existing public-post path and only adds a verification gate on comments. No new `PostVisibility` variant needed.
---
## Goal
- An author creates a public post with `comment_policy = FriendsOfFriends`.
- Body is encrypted to no one — plaintext in the CDN, same as a normal public post today.
- Comments on the post include a proof artifact that lets readers (and the author in strict mode) verify the commenter is reachable through the author's FoF graph.
- Non-FoF readers can still READ the post. They cannot post an accepted comment.
---
## Lead decisions
- **Mode 2 does not gate readership.** The post body is genuinely public. Only comments are gated. This is intentional — it preserves CDN shardability for the body and gives authors a lightweight "comments to my circle, body to the world" mode.
- **`pub_post` is included in the public post header.** Every FoF-eligible post (both modes) has a per-post ephemeral ed25519 keypair. `pub_post` in the header, `priv_post` in wrap slots (Mode 1) or directly in a control record (Mode 2 — TBD below).
- **Comments carry a `group_sig` signed with `priv_post`.** Any device that can unwrap `priv_post` (i.e., holds a matching `V_x`) can sign. `group_sig` verification against `pub_post` is the cryptographic proof of FoF-reachability. Verifiable by any observer holding the public post.
- **Optional `vouch_mac` for strict mode.** `HMAC(V_x, post_id || comment_hash)` identifies *which* voucher's chain a commenter holds. Author can run in strict mode and reject comments whose `vouch_mac` doesn't match a `V_x` the author has a record of distributing. Non-strict mode accepts any valid `group_sig`.
- **Comment still signed by commenter's identity key as well.** `group_sig` proves FoF-membership; identity sig binds the comment to a specific persona for display / abuse reporting / author-side blocklist.
- **Non-FoF devices can still render the post.** Read path is unchanged — comments missing `group_sig` (or failing verification) are filtered out in the feed renderer rather than hard-rejected in storage. This leaves forensic traces and is cheaper than re-verifying in the renderer vs at ingest. TBD open question below.
---
## Data model
### Extend `CommentPolicy`
```rust
pub enum CommentPolicy {
Public,
FollowersOnly,
None,
FriendsOfFriends, // NEW — Layer 2
}
```
### Post header additions (for posts with `comment_policy = FriendsOfFriends`)
```rust
struct PostHeader {
// ... existing fields ...
// NEW for Mode 2 comment gating:
pub_post: Option<[u8; 32]>, // ed25519 public key of per-post ephemeral keypair
wrap_slots: Vec<WrapSlot>, // wraps priv_post under each V_x in author's keyring
// TBD — OPUS: WrapSlot byte layout (same type as Layer 3 uses)
}
```
**Note**: Mode 2 posts carry `wrap_slots` even though the body is public, because commenters need `priv_post` to sign. The wrap slot set IS the FoF membership definition.
### Extend `InlineComment`
```rust
struct InlineComment {
// ... existing fields ...
#[serde(default, skip_serializing_if = "Option::is_none")]
group_sig: Option<[u8; 64]>, // ed25519 signature over comment_hash, verifies against parent post's pub_post
#[serde(default, skip_serializing_if = "Option::is_none")]
vouch_mac: Option<[u8; 16]>, // HMAC(V_x, post_id || comment_hash), truncated
}
```
Back-compat: old comments without these fields are treated as "not FoF-signed" — accepted on non-FoF posts, rejected on FoF posts.
---
## Comment creation (author of the comment)
1. Commenter fetches parent post. Reads `pub_post` from header.
2. Commenter iterates their keyring. For each `V_x` held, computes `HMAC(V_x, post_id)[:2B]`. Compares against each `WrapSlot.prefilter_tag`. On match, trial-decrypts the slot to get `priv_post`.
3. If any slot unwraps successfully: commenter now holds `priv_post`. Signs `comment_hash` to produce `group_sig`. Computes `vouch_mac = HMAC(V_x_winner, post_id || comment_hash)`. Attaches both to comment.
4. Publishes comment through normal comment-propagation path.
If NO slot unwraps: commenter is not in the author's FoF set. UI reports "You can't comment on this post." Pure client-side enforcement — no wire attempt.
---
## Comment verification (reader side)
At feed render time, for every comment on a `FriendsOfFriends`-policy post:
1. If `group_sig` is missing → filter out (strict) or show with "unverified" badge (permissive). Lead leaning: **filter out**.
2. Verify `group_sig` against parent post's `pub_post` over `comment_hash`. If fail → filter out.
3. Verify identity sig of comment as normal. If fail → filter out.
At author side (strict mode, optional):
4. Recompute expected `vouch_mac` candidates from the set of `V_x` the author has distributed. If `comment.vouch_mac` doesn't match any → discard at ingest (don't store, don't propagate).
---
## Propagation
No changes to comment propagation path. Comments still flow via existing `BlobHeaderDiff` / engagement-diff mechanism. Verification is done at display time (reader) and optionally at ingest (author-strict).
---
## Open questions
- **Filter-out vs reject-at-storage for bad `group_sig`.** Filter-out preserves forensic trail and avoids double-verify cost. Reject-at-storage saves disk and sync bandwidth. Lead leaning: filter-out at render, reject-at-storage only on author-side strict mode.
- **`priv_post` distribution for Mode 2.** In Mode 1, `priv_post` is inside `wrap_slots`. In Mode 2, body is plaintext, but commenters still need `priv_post`. Options: (A) `wrap_slots` still present in Mode 2 headers, carrying only `priv_post`; (B) separate `comment_priv_key` control record distributed out of band. Lead leaning: A (uniform structure with Mode 1).
- **Wrap slot prefilter tag.** 2B = 65536 buckets, false-positive 1/65536. For Mode 2 this also defines the commenter-eligibility filter. TBD — OPUS: confirm 2B is enough for realistic keyring sizes.
- **Author's own comments.** Author has `priv_post` directly (generated it). Do they self-vouch-mac against `V_me`? Leaning: yes, so strict mode uniformly verifies all comments.
- **Displaying vouch path to the author.** Mode 2 strict mode knows which `V_x` a commenter arrived through. Should the author's UI show "comment arrived via your vouch to Alice" or keep it opaque? Lead leaning: opaque by default; optional power-user setting.
- **Rate-limiting / spam.** A malicious FoF member can flood comments. `vouch_mac` identifies the chain, so author can block a chain. Out-of-scope for Layer 2 ship (tracked separately).
---
## Ship criteria for Layer 2
- `CommentPolicy::FriendsOfFriends` variant exists end-to-end (storage, protocol, UI picker).
- Authors can create public posts with FoF-gated comments.
- `pub_post` / `priv_post` / `wrap_slots` generated on post creation, wrapped under author's full keyring.
- Commenters: client-side check of FoF eligibility before offering comment box; `group_sig` + `vouch_mac` attached on send.
- Readers: filter-out comments failing `group_sig` verification.
- Author strict-mode: optional ingress rejection on unknown `vouch_mac`.
- Back-compat: old clients see FoF posts as readable (body is public) but can't comment (missing `priv_post`); old comments on new posts are filtered at render.
- Integration test: 3-node FoF chain (A→B→C). A posts Mode 2. B comments (reachable). C comments (reachable via B). D (unrelated) cannot.