A1 — ID-class bug family (v0.6.1 persona split left network-NodeId comparisons
where posting IDs live). ~14 sites fixed with multi-persona semantics ("author is
one of MY posting identities", not == default):
- revoke_post_access / revoke_circle_access (revocation was broken for every post)
- group key create/add-member/rotate (add-member distribution now works)
- circle profile set/delete/get, encrypted-attachment CEK unwrap
- receipt/comment slot authorship, replication cycle, blob eviction own-tier
- orphaned GroupKeyRequest/Response (0xA1/0xA2) deleted (no send side existed)
A2 — Manifest privacy: AuthorManifest.author_addresses removed from struct AND
signature digest; CdnManifest tree-era fields dropped. Startup migrations re-sign
own manifests, purge unverifiable foreign copies, strip persona fields from legacy
network-id profile rows. Posting identities are now location-anonymous.
A3 — Discovery & first contact:
- Open-slot comments: derivable slot V_x (blake3 from author + slot_binder_nonce)
so strangers pass the CDN comment-verification gate; OpenSlotDecl in FoF gating
- Greetings: HPKE-style sealed GreetingBody (return_path + fresh reply_pubkey),
throwaway outer ID, size-bucketed; messaging-first (Reply/Dismiss, no vouch)
- Registry: frozen canonical registry post + REGISTRY_POST_ID, signed registration
comments (self-certifying deletes), newest-wins per persona, --publish-registry
- Comment TTL: expires_at_ms inside signed digest v2 (context bump), rand 30-365d
ordinary / fixed 30d registrations, expiry sweep on the eviction cycle
- Single accept_incoming_comment() gate wired into all three ingest sites, closing
a pre-existing hole where both pull paths stored comments with no verification
- UI: consent panel (visible/listed/greetings, active choice at first publish),
registry search in Discover, sealed "Say hi" compose, greetings inbox
Security fixes from review: forged-tombstone injection, delete-by-sender-trust,
remote SetPolicy poisoning, stored XSS (escapeHtml quotes, 7 pre-existing sites),
greeting off-switch now revokes prior bios' slots.
No PoW (rejected: inverted cost). Session-relay gating untouched.
201 core tests pass; CLI + desktop build; 3-node integration green.
DEPLOY GATE: wire-incompatible with v0.7.3 — ALPN bump to itsgoin/4 lands in
Iteration B. Do not build/deploy/anchor-swap before that.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGiPD2cF75mnvneSCjdDC5
Three new tables + cache/sweep wiring per docs/fof-spec/
layer-5-prefilter-and-cache.md:
vouch_unlock_cache (reader_persona, author) -> winning V_x
- Records the V_x that last unlocked a post from this author. Next
post from same author: hot path is 1 HMAC + 1 AEAD attempt.
vouch_unreadable_posts (reader_persona, post_id)
- Queue of FoF posts that no held V_x currently unlocks. Swept when
a new V_x lands in the persona's keyring.
own_fof_post_ceks (author_persona, post_id) -> (CEK, slot_binder_nonce)
- Author-direct decrypt fast path. Populated at publish so authors
skip wrap-slot trial entirely when reading their own posts.
find_unlock_for_post:
- Cache fast path: look up winning V_x, prefilter, single AEAD.
- Full scan fallback on cache miss; record cache hit on success;
record unreadable on full miss.
read_fof_closed_body:
- Author-direct fast path: lookup_own_fof_post_cek before trial-unlock.
sweep_unreadable_on_new_v_x:
- Walks ALL unreadable for the persona (the new V_x can unlock posts
authored by anyone — the V_x owner may be a chain-link in some
third party's keyring). Wired into the vouch-grant scan path so
every new V_x triggers a sweep automatically.
Both FoF publish paths (Mode 1 + Mode 2) now cache CEK at publish.
Bug fix found by the sweep test: storage::get_post_with_visibility
wasn't loading fof_gating_json (always returned None). Fixed; reader
paths through that function now see the gating block.
16 fof:: tests pass (2 new: cache populates+hits, sweep after V_x
arrival). 150 total tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional supersedes_post_id pointer to Post for the
"re-issue with narrower access" path. Author publishes a new post
that references an earlier one; receivers can render "this is a
re-issued version of an earlier post" + offer to view the original.
Covered by PostId = BLAKE3(Post) — receivers can verify it wasn't
forged by anyone but the author.
#[serde(default)] for back-compat; existing posts deserialize with
None. All existing Post construction sites bulk-updated to set the
field to None.
Storage::get_post returns None for the field today; a follow-up can
add a dedicated column if/when receivers need to render the
pointer. The author-side re-issue helper that creates posts with
this field set comes in the Tauri/UI slice — the wire shape is in
place now.
148 tests pass (no regressions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New crates/core/src/fof.rs module owns the author-side FoF Layer 2
publish path:
- build_fof_comment_gating(storage, author_persona_id): gathers the
author's keyring (own current V_me + every distinct received V_x),
generates a fresh CEK + slot_binder_nonce, generates a fresh per-V_x
(priv_x, pub_x) Ed25519 keypair per real slot, seals each slot, pads
with random-bytes dummies to the next bucket (min 8, power-of-2 to
256, +128 above per Layer 3), shuffles real + dummy together, and
returns the FoFCommentGating wire block + author-local CEK + the
slot_binder_nonce.
- Dedup at V_x byte level: a key held under multiple owners produces
exactly one slot.
- next_vouch_batch_bucket promoted to pub(crate) in profile.rs so the
Layer 2 fof module can reuse the bucket rule from Layer 3.
Three unit tests cover:
- Real-count + padding + roundtrip (Alice's own V_me unlocks her slot;
Bob's V_x unlocks his slot; both yield same CEK).
- No V_me → returns None (graceful).
- Duplicate V_x bytes across owners are deduped (single slot).
134 → 138 tests pass (no regressions).
Subsequent slices wire this into the post-create path, add the
reader/commenter side, the CDN four-check verification, and the
revocation/access-grant diff handlers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the on-wire shapes for FoF Mode 2 comment-gating per
docs/fof-spec/layer-2-mode2-fof-comments.md:
- WrapSlot: per-V_x slot with 2B prefilter_tag + 48B read_ciphertext
+ 48B sign_ciphertext (sealed CEK + sealed priv_x_seed). 98 bytes
total per slot. Receiver trial-decrypts via prefilter match.
- FoFCommentGating: author-published gating block embedded in
Post.fof_gating. Carries slot_binder_nonce (32B random; replaces
spec's circular "post_id in HKDF info"), pub_post_set (1:1 with
wrap_slots, includes dummy pubkeys), wrap_slots, and revocation_list
(initially empty; revocation diffs accumulate on the BlobHeader copy).
- RevocationEntry: author-signed entry triggering retroactive comment
delete + pub_post_set removal on every file-holder that receives it.
- CommentPermission gains FriendsOfFriends variant. Existing match arm
in connection.rs handle-incoming-diff path is extended with a
"drop pending CDN four-check verification" stub (full verify in a
later slice).
- InlineComment extended with three optional fields:
pub_x_index: index into parent post's pub_post_set
group_sig: 64B ed25519 sig under priv_x
encrypted_payload: ChaCha20-Poly1305 ciphertext under CEK_comments
All #[serde(default)] for back-compat. Old comments deserialize
cleanly with None.
- Post gains optional fof_gating field for the author-signed snapshot
at publish time. PostId = BLAKE3(Post) covers it, so any tampering
is detectable. Mutations (revocation, access-grant) arrive later as
diffs against the local BlobHeader copy.
All 21 existing Post construction sites + 4 existing InlineComment
sites updated via perl -0pe sweeps to pass None for the new fields.
Full test suite: 134/134 pass (4 new slot crypto + 130 existing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the receive side of Layer 1 vouch distribution:
- profile::scan_vouch_grants_for_all_personas reads the VouchGrantBatch
from an incoming profile post, trial-decrypts each wrapper against
every persona's X25519 private scalar, and inserts successful unlocks
into vouch_keys_received. Idempotent via the bio_scan_cache.
- apply_profile_post_if_applicable now calls the scan before the
timestamp-based last-writer-wins short-circuit on display_name/bio.
A profile post that arrives "older" than what we've stored can still
carry vouch grants we haven't seen — bio_epoch is the actual
freshness signal for the wrapper batch.
- Follow-gated per the spec: skipped if the bio author isn't in
follows. Self-authored posts skipped (we already have our own V_me).
- storage::is_follow helper added (cheap COUNT membership check).
Two new integration tests cover the wire:
- vouch_grant_end_to_end_via_bio_post: Bob's signed profile post
carries a real wrapper for Alice + 7 dummies; Alice's keyring picks
up V_bob and the scan cache records the hit.
- vouch_grant_skipped_for_non_followed_author: same post, but Alice
doesn't follow Bob → no scan, no keyring entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the publish side of FoF Layer 1 vouch distribution:
- VouchGrantBatch gains bio_pub_nonce (32B random per batch). Replaces
the spec's circular "bio_post_id in HKDF info" — BLAKE3(post)
depends on vouch_grants, so we need a content-independent binder.
Recipient-free per HPKE key-privacy; serves the same anti-replay
purpose as bio_post_id would have.
- profile::build_vouch_grant_batch reads current_own_vouch_key +
list_current_vouch_targets, generates eph keypair + bio_pub_nonce,
seals V_me for each target, bucket-pads with random 48B dummies,
and shuffles. Returns None when there are no targets.
- next_vouch_batch_bucket implements the FoF Layer 3 padding rule:
minimum bucket 8, power-of-2 up to 256, then linear +128 steps.
Bucket-padding-tests verifies all boundaries.
- Storage gains next_bio_epoch_for(persona_id): monotonic counter
per persona, used by receivers' scan cache. Stored in settings.
- build_profile_post signature extended to take Option<VouchGrantBatch>
+ bio_epoch: u32. Both publish_profile_post_as (initial post) and
set_profile (subsequent edits) build the batch and bump the epoch
on every publish.
- Test sites updated to pass None/0 for the new args.
Receive-side scan (next commit) reads VouchGrantBatch + bio_pub_nonce
to trial-decrypt wrappers and populate vouch_keys_received.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds VouchGrantBatch type to types.rs and extends ProfilePostContent
with optional vouch_grants + bio_epoch fields (back-compat via
#[serde(default)]). VouchGrantBatch carries one shared batch_eph_pub
+ a Vec<Vec<u8>> of 48-byte wrappers; receivers identify their wrapper
by AEAD success, not position.
Wires V_me auto-generation into both persona-creation paths:
- Node::open first-run auto-persona block now seeds vouch_keys_own
epoch=1 alongside upsert_posting_identity.
- create_posting_identity calls ensure_initial_v_me after the new
persona is stored.
Helpers live as private free functions at module scope so both the
sync (Node::open holds a Storage guard) and async (create_posting
holds a StoragePool) sites can share them. Idempotent — re-running
on a persona that already has a current key is a no-op.
Two existing ProfilePostContent construction sites updated to set the
new fields to defaults (None / 0); they'll get real values when the
publish path is wired up in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Display metadata (display_name, bio, avatar_cid) is no longer broadcast via
the ProfileUpdate direct push when the user edits their name. It travels as
a signed public post with VisibilityIntent::Profile, authored by the posting
identity, and propagates through the normal neighbor-manifest CDN path.
Core pieces:
- `types::ProfilePostContent` — JSON payload serialized into the post's
content field. Ed25519 signature by the posting secret over length-prefixed
display_name + bio + 32-byte avatar_cid (or zeros) + timestamp.
- `crypto::{sign,verify}_profile` with strict length prefixing to prevent
extension attacks.
- New `profile` module: `build_profile_post`, `verify_profile_post`,
`apply_profile_post_if_applicable`. Last-writer-wins by timestamp.
- `control::receive_post` now verifies Profile-intent posts upfront (same
as Control) so bogus signatures never enter storage, and applies them
after store so the `profiles` row updates atomically with the insert.
Node API:
- `Node::set_profile` rewritten: builds a signed Profile post, stores under
intent=Profile, applies it locally (upserts the profiles row keyed by the
posting identity), then propagates via `update_neighbor_manifests_as`.
Stops calling `network.push_profile` — display changes no longer trigger
a direct wire push.
- `Node::my_profile` / `has_profile` read by `default_posting_id` instead of
`node_id`, matching where the row is written now.
ProfileUpdate (0x50) and push_profile stay for now — they still carry
routing-only data (anchors, recent_peers, preferred_peers) via
`sanitized_for_network_broadcast` and are used by `set_anchors` /
`set_public_visible`. Removing the routing fields would be a broader
cleanup; scoped out of this phase.
Tests: roundtrip verify+store, wrong-author rejection (not stored), and
older-timestamp ignored. 114 / 114 core tests pass.