feat(fof-layer1): wire types + V_me auto-gen on persona creation

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>
This commit is contained in:
Scott Reimers 2026-05-13 01:35:19 -04:00
parent 8a53d83306
commit bc008c5049
3 changed files with 79 additions and 0 deletions

View file

@ -63,6 +63,35 @@ pub struct Node {
budget_last_reset_ms: Arc<AtomicU64>,
}
/// FoF Layer 1: generate a fresh 32B `V_me` and insert it as the
/// persona's current epoch (epoch=1). Idempotent if the persona already
/// has a current key — does nothing in that case.
fn generate_and_store_initial_v_me(
storage: &crate::storage::Storage,
persona_id: &NodeId,
now_ms: u64,
) -> anyhow::Result<()> {
use rand::RngCore;
if storage.current_own_vouch_key(persona_id)?.is_some() {
return Ok(());
}
let mut key = [0u8; 32];
rand::rng().fill_bytes(&mut key);
storage.insert_own_vouch_key(persona_id, 1, &key, now_ms)?;
Ok(())
}
/// Async wrapper used by `Node::create_posting_identity`. Acquires the
/// storage handle and delegates to the sync helper.
async fn ensure_initial_v_me(
storage: &StoragePool,
persona_id: &NodeId,
now_ms: u64,
) -> anyhow::Result<()> {
let s = storage.get().await;
generate_and_store_initial_v_me(&s, persona_id, now_ms)
}
impl Node {
/// Create or open a node in the given data directory (Desktop profile)
pub async fn open(data_dir: impl AsRef<Path>) -> anyhow::Result<Self> {
@ -133,6 +162,8 @@ impl Node {
created_at: now,
})?;
s.set_default_posting_id(&nid)?;
// FoF Layer 1: auto-gen V_me epoch 1 for this fresh persona.
generate_and_store_initial_v_me(&s, &nid, now)?;
// Mark this as the disposable auto-gen persona from the
// fresh-install flow. If the user subsequently imports, we
// prune this id iff it's still pristine (no name, no posts,
@ -684,6 +715,12 @@ impl Node {
}
}
// FoF Layer 1: every persona owns its own V_me (symmetric 32B key).
// Auto-generate epoch 1 at creation. Stored in vouch_keys_own with
// is_current=1. Per Layer 4, rotations append new epochs; this row
// is never deleted automatically.
ensure_initial_v_me(&self.storage, &node_id, now).await?;
Ok(identity)
}
@ -772,6 +809,8 @@ impl Node {
avatar_cid: None,
timestamp_ms: pi.created_at,
signature,
vouch_grants: None,
bio_epoch: 0,
};
let post = Post {
author: pi.node_id,