feat(fof-layer1): publish path embeds VouchGrantBatch

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>
This commit is contained in:
Scott Reimers 2026-05-13 01:39:09 -04:00
parent bc008c5049
commit 3ee5c30ad2
4 changed files with 156 additions and 6 deletions

View file

@ -4945,6 +4945,19 @@ impl Storage {
Ok(())
}
/// Increment and return the next bio-publish epoch for a persona.
/// Counter is monotonic; used by receivers' scan cache to short-circuit
/// re-scanning unchanged bios. Stored in `settings` keyed by persona.
pub fn next_bio_epoch_for(&self, persona_id: &NodeId) -> anyhow::Result<u32> {
let key = format!("bio_epoch_{}", hex::encode(persona_id));
let current: u32 = self.get_setting(&key)?
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let next = current + 1;
self.set_setting(&key, &next.to_string())?;
Ok(next)
}
// --- File holders (flat, per-file, LRU-capped at 5) ---
//
// A single table for PostId-keyed engagement propagation and CID-keyed