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.