Adds the reader/commenter side of FoF Layer 2 in crates/core/src/fof.rs:
- find_unlock_for_post(storage, post): scans every persona × every
held V_x (own V_me + received) against the post's wrap_slots using
the 2B prefilter tag. Returns first successful unlock as a
PostUnlock { persona_id, slot_index, cek, priv_x_seed }.
- FoFCommentPayload: serializable inner-plaintext shape (body +
parent_comment_id + optional vouch_mac). Wrapped under CEK_comments
inside InlineComment.encrypted_payload.
- build_fof_comment(parent_post_id, unlock, slot_binder_nonce,
commenter_id, commenter_secret, body, parent_comment_id, now_ms)
→ InlineComment: encrypts the body under CEK_comments, signs
(encrypted_payload || post_id || pub_x_index_le) with priv_x for
group_sig, signs the conventional comment tuple with commenter's
identity key, fills pub_x_index from the unlock.
- verify_fof_group_sig(comment, gating): CDN-level Ed25519 verify of
group_sig against pub_post_set[pub_x_index]. Returns false on any
shape/index/key/signature failure. Used by the four-check accept
rule (next slice).
- decrypt_fof_comment_payload(comment, cek, slot_binder_nonce): inverse
of build_fof_comment's encryption step. Used by authors with cached
CEKs and by readers who just unlocked.
End-to-end roundtrip test covers: Alice publishes Mode 2 FoF post with
Bob's V_x in the gating; Bob's device unlocks via his V_me; Bob
authors a comment; verify_fof_group_sig accepts it; tampered payload
and wrong pub_x_index both reject; Alice decrypts the payload.
138 → 139 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>