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

@ -4353,31 +4353,11 @@ impl Storage {
self.set_setting("active_default_posting_id", &hex::encode(node_id))
}
/// Ensure the posting_identities table has at least one entry. On first
/// launch after 0.6.3 upgrade, copies the network key from
/// `identity.key` on disk into posting_identities and sets it as default,
/// preserving signature validity of all existing content.
pub fn seed_posting_identity_from_network(
&self,
network_node_id: &NodeId,
network_secret: &[u8; 32],
) -> anyhow::Result<()> {
let existing: i64 = self.conn.prepare(
pub fn count_posting_identities(&self) -> anyhow::Result<u64> {
let n: i64 = self.conn.prepare(
"SELECT COUNT(*) FROM posting_identities",
)?.query_row([], |row| row.get(0))?;
if existing == 0 {
let now = now_ms();
self.conn.execute(
"INSERT INTO posting_identities (node_id, secret_seed, display_name, created_at)
VALUES (?1, ?2, '', ?3)",
params![network_node_id.as_slice(), network_secret.as_slice(), now as i64],
)?;
}
// Always ensure a default is set (no-op if already pointing at a valid identity).
if self.get_default_posting_id()?.is_none() {
self.set_default_posting_id(network_node_id)?;
}
Ok(())
Ok(n as u64)
}
// --- File holders (flat, per-file, LRU-capped at 5) ---