feat(fof-layer5): unlock cache + retry sweep + author-direct fast path

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>
This commit is contained in:
Scott Reimers 2026-05-14 19:29:12 -06:00
parent ce710a6596
commit 12a305889e
4 changed files with 547 additions and 41 deletions

View file

@ -148,6 +148,14 @@ pub fn scan_vouch_grants_for_all_personas(
}
}
// FoF Layer 5: if a new V_x just landed for this persona,
// sweep the unreadable-posts queue for (persona, author) and
// re-attempt unlock. Posts that were previously not-in-set
// become readable as soon as this V_x lands.
if unlocked.is_some() {
let _ = crate::fof::sweep_unreadable_on_new_v_x(s, &persona.node_id, author);
}
s.record_bio_scan_result(
&persona.node_id,
author,