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:
parent
ce710a6596
commit
12a305889e
4 changed files with 547 additions and 41 deletions
|
|
@ -1079,6 +1079,8 @@ impl Node {
|
|||
// FoF Layer 4: persist provenance so cascade-revocation can
|
||||
// resolve "which pub_x's on which of my posts were sealed
|
||||
// under V_me epoch N" later.
|
||||
// FoF Layer 5: cache the CEK + slot_binder_nonce for author-
|
||||
// direct decrypt without trial-unlocking on read.
|
||||
{
|
||||
let storage = self.storage.get().await;
|
||||
for entry in &provenance {
|
||||
|
|
@ -1087,6 +1089,14 @@ impl Node {
|
|||
&entry.v_x_owner, entry.v_x_epoch, &entry.pub_x,
|
||||
);
|
||||
}
|
||||
// Recover slot_binder_nonce via the Post we just built — it
|
||||
// lives inside fof_gating.
|
||||
if let Some(gating) = post.fof_gating.as_ref() {
|
||||
let _ = storage.cache_own_fof_post_cek(
|
||||
&self.default_posting_id, &post_id,
|
||||
&cek, &gating.slot_binder_nonce,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((post_id, post, visibility, cek))
|
||||
|
|
@ -1111,21 +1121,29 @@ impl Node {
|
|||
}
|
||||
let gating = match post.fof_gating.as_ref() {
|
||||
Some(g) => g,
|
||||
None => return Ok(None), // invariant violation; treat as opaque
|
||||
None => return Ok(None),
|
||||
};
|
||||
let slot_binder_nonce = gating.slot_binder_nonce;
|
||||
|
||||
let unlock = match crate::fof::find_unlock_for_post(&*storage, &post)? {
|
||||
Some(u) => u,
|
||||
None => return Ok(None), // we're not in the FoF set
|
||||
// FoF Layer 5: author-direct fast path. If this device authored
|
||||
// the post, the CEK was cached at publish time; skip the
|
||||
// wrap-slot trial entirely.
|
||||
let (cek, slot_binder_nonce) = if let Some((cek, nonce)) =
|
||||
storage.lookup_own_fof_post_cek(&post.author, post_id)?
|
||||
{
|
||||
(cek, nonce)
|
||||
} else {
|
||||
let unlock = match crate::fof::find_unlock_for_post(&*storage, &post)? {
|
||||
Some(u) => u,
|
||||
None => return Ok(None),
|
||||
};
|
||||
(unlock.cek, gating.slot_binder_nonce)
|
||||
};
|
||||
drop(storage);
|
||||
|
||||
// Decode the base64-wrapped ciphertext + decrypt.
|
||||
let body_ct = base64::engine::general_purpose::STANDARD
|
||||
.decode(post.content.as_bytes())
|
||||
.map_err(|e| anyhow::anyhow!("FoFClosed body base64 decode: {}", e))?;
|
||||
let plaintext = crate::fof::decrypt_fof_body(&body_ct, &unlock.cek, &slot_binder_nonce)?;
|
||||
let plaintext = crate::fof::decrypt_fof_body(&body_ct, &cek, &slot_binder_nonce)?;
|
||||
Ok(Some(plaintext))
|
||||
}
|
||||
|
||||
|
|
@ -1187,6 +1205,10 @@ impl Node {
|
|||
&entry.v_x_owner, entry.v_x_epoch, &entry.pub_x,
|
||||
);
|
||||
}
|
||||
// FoF Layer 5: cache CEK for author-direct decrypt.
|
||||
let _ = storage.cache_own_fof_post_cek(
|
||||
&self.default_posting_id, &post_id, &cek, &slot_binder_nonce,
|
||||
);
|
||||
}
|
||||
|
||||
self.update_neighbor_manifests_as(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue