Core: network/posting key split + decrypt-all-personas

Fresh installs now generate two independent ed25519 keys — one as the
network (QUIC) identity in identity.key, and a SEPARATE one as the
default posting identity in posting_identities. They share nothing.

v0.6.0 upgraders: if the default posting key equals the network key
(the state Phase 4's migration left us in), rotate identity.key to a
fresh random value. The old key stays in posting_identities as the
default persona — peers keep seeing the same author on our posts; only
the QUIC NodeId changes. A one-shot reconnect-churn on upgrade, then
back to normal.

Storage:
- Drop seed_posting_identity_from_network (v0.6.0-specific helper)
- Add count_posting_identities()

Node::open_with_bind:
- Load identity.key (network secret — network-only from now on)
- Ensure posting_identities has at least one entry; if empty, generate
  an INDEPENDENT random posting key as the default
- Detect default-posting-key == network-key collision and rotate
  identity.key, logging the migration
- default_posting_id / default_posting_secret resolved from storage

Decrypt:
- decrypt_posts now takes &[PostingIdentity] and tries each held
  persona as a recipient candidate. Past DMs to any persona on this
  device (including ones added via Import as personas) decrypt
  correctly. Callers pre-load list_posting_identities() alongside
  group_seeds.
- decrypt_just_created looks up the author's specific posting identity
  rather than assuming the default.

Profile broadcasts (wire-level privacy):
- Profile stays keyed by network NodeId — the field is load-bearing
  for N1/N2/N3 social routing (anchors/recent_peers/preferred_peers
  feed build_preferred_tree_for and peer-anchor reachability lookup).
- But push_profile and InitialExchange now STRIP display_name, bio,
  and avatar_cid before sending, via new
  PublicProfile::sanitized_for_network_broadcast(). A name attached to
  the network id would correlate the QUIC endpoint to a human. Until
  v0.6.2 introduces persona-signed profile posts, peers display
  authors as hex.

Auto-follow only the default posting id (network id is never an
author, following it would be dead weight).

All 111 core tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-22 17:11:20 -04:00
parent b789ab5a19
commit 4a1db1ce7f
5 changed files with 134 additions and 72 deletions

View file

@ -1513,7 +1513,11 @@ impl ConnectionManager {
let storage = self.storage.get().await;
let n1 = storage.build_n1_share()?;
let n2 = storage.build_n2_share()?;
let profile = storage.get_profile(&self.our_node_id)?;
// Profile keyed by network id (used for N1/N2/N3 routing).
// Strip persona display data before sending so peers don't learn
// a human-readable name for our network id.
let profile = storage.get_profile(&self.our_node_id)?
.map(|p| p.sanitized_for_network_broadcast());
let deletes = storage.list_delete_records()?;
let post_ids = storage.list_post_ids()?;
let peer_addresses = storage.build_peer_addresses_for(&self.our_node_id)?;
@ -1656,7 +1660,11 @@ impl ConnectionManager {
let storage = self.storage.get().await;
let n1 = storage.build_n1_share()?;
let n2 = storage.build_n2_share()?;
let profile = storage.get_profile(&self.our_node_id)?;
// Profile keyed by network id (used for N1/N2/N3 routing).
// Strip persona display data before sending so peers don't learn
// a human-readable name for our network id.
let profile = storage.get_profile(&self.our_node_id)?
.map(|p| p.sanitized_for_network_broadcast());
let deletes = storage.list_delete_records()?;
let post_ids = storage.list_post_ids()?;
let peer_addresses = storage.build_peer_addresses_for(&self.our_node_id)?;
@ -8391,7 +8399,9 @@ pub async fn initial_exchange_connect(
let storage = storage.get().await;
let n1 = storage.build_n1_share()?;
let n2 = storage.build_n2_share()?;
let profile = storage.get_profile(our_node_id)?;
// Profile keyed by network id; strip persona display before send.
let profile = storage.get_profile(our_node_id)?
.map(|p| p.sanitized_for_network_broadcast());
let deletes = storage.list_delete_records()?;
let post_ids = storage.list_post_ids()?;
let peer_addresses = storage.build_peer_addresses_for(our_node_id)?;
@ -8470,7 +8480,9 @@ pub async fn initial_exchange_accept(
let storage = storage.get().await;
let n1 = storage.build_n1_share()?;
let n2 = storage.build_n2_share()?;
let profile = storage.get_profile(our_node_id)?;
// Profile keyed by network id; strip persona display before send.
let profile = storage.get_profile(our_node_id)?
.map(|p| p.sanitized_for_network_broadcast());
let deletes = storage.list_delete_records()?;
let post_ids = storage.list_post_ids()?;
let peer_addresses = storage.build_peer_addresses_for(our_node_id)?;