feat(federation): transitive peer learning via state-sync

When Alice syncs state with a Trusted peer Bob, she now learns about
Bob's other Trusted peers and auto-adds them as Observers on her side
— so Carol's fips_npub is known locally and subsequent federation
traffic to Carol can route directly over FIPS without a separate
invite round-trip.

- NodeStateSnapshot gains a `federated_peers: Vec<FederationPeerHint>`
  field (serde default for backward compat with v1.4 snapshots).
- FederationPeerHint is a minimal projection: did, pubkey, onion,
  name, fips_npub — excludes per-receiver fields (trust_level,
  added_at, last_seen, last_state).
- build_local_state takes the local federation list and includes only
  Trusted peers. Observer/Untrusted peers are NOT re-exported — a
  node shouldn't launder other people's federation through its own
  authority.
- sync_with_peer merges the received hints via merge_transitive_peers
  when the source is Trusted: existing entries get fips_npub
  refreshed if missing; unknown DIDs are added at Observer trust
  (never auto-promoted to Trusted).
- Bounded to 1 hop: merged Observer entries do NOT get re-exported in
  the local node's own snapshots. So Bob → Alice learns Carol, but
  Alice's snapshots to Dave do not include Carol.
- Tests: round-trip + filter-non-trusted-from-snapshot coverage.
- Storage + delta test fixtures updated for the new field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-19 01:58:21 -04:00
co-authored by Claude Opus 4.7
parent d80b8ce513
commit 6ddce90e45
5 changed files with 181 additions and 1 deletions
@@ -379,6 +379,14 @@ impl RpcHandler {
.and_then(|hex| nostr_sdk::PublicKey::from_hex(&hex).ok())
.and_then(|pk| nostr_sdk::ToBech32::to_bech32(&pk).ok());
// Pass the current federated-peer list so the snapshot can include
// a `federated_peers` hint for transitive federation — receivers
// who trust us learn our Trusted peers and can route to them
// over FIPS without a separate invite round-trip.
let federated_peers = federation::load_nodes(&self.config.data_dir)
.await
.unwrap_or_default();
let state = federation::build_local_state(
apps,
0.0,
@@ -390,6 +398,7 @@ impl RpcHandler {
tor_active,
server_name,
nostr_npub,
&federated_peers,
);
Ok(serde_json::to_value(&state)?)