From bfe2603f699e089c8598bf19663b0ba8aeb1ce21 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 19 Apr 2026 04:16:05 -0400 Subject: [PATCH] feat(federation): advertise own_fips_npub in state snapshots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-v1.4 federation pairs (who exchanged invites before fips_npub was part of the invite code) had no path to learn each other's FIPS npub — they'd stay Tor-only forever even after upgrading. Fix: every state snapshot now carries the sender's own_fips_npub, and update_node_state refreshes the stored fips_npub on the receiver side whenever it differs. - NodeStateSnapshot.own_fips_npub (serde default for back-compat). - build_local_state takes own_fips_npub alongside the other single-value fields. - handle_federation_get_state populates own_fips_npub from identity::fips_npub, with a fallback to the upstream daemon's /etc/fips/fips.pub for legacy nodes that never materialised a seed-derived key. - storage::update_node_state now writes fips_npub into the FederatedNode when a new value arrives and trims whitespace before comparing, so key rotations also flow through. - Test fixtures (storage + transport/delta + sync) updated for the new field; existing tests pass. Net effect: on the next sync, .116 and .228 learn each other's fips_npub (currently null from the old invite) and subsequent federation calls route FIPS-first automatically. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/api/rpc/federation/handlers.rs | 18 ++++++++++++++++++ core/archipelago/src/federation/storage.rs | 12 ++++++++++++ core/archipelago/src/federation/sync.rs | 5 ++++- core/archipelago/src/federation/types.rs | 7 +++++++ core/archipelago/src/transport/delta.rs | 2 ++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/core/archipelago/src/api/rpc/federation/handlers.rs b/core/archipelago/src/api/rpc/federation/handlers.rs index 27cd89d8..ef62f1f6 100644 --- a/core/archipelago/src/api/rpc/federation/handlers.rs +++ b/core/archipelago/src/api/rpc/federation/handlers.rs @@ -387,6 +387,23 @@ impl RpcHandler { .await .unwrap_or_default(); + // Our own FIPS npub, so pre-v1.4 federation pairs (whose + // invite codes didn't carry it) can learn it on the next sync. + let identity_dir = self.config.data_dir.join("identity"); + let own_fips_npub = crate::identity::fips_npub(&identity_dir) + .await + .ok() + .flatten() + .or_else(|| { + // Legacy/dev nodes without a seed-derived key fall back + // to the upstream daemon's public key on disk. + None + }); + let own_fips_npub = match own_fips_npub { + Some(n) => Some(n), + None => crate::fips::service::read_upstream_npub().await.ok().flatten(), + }; + let state = federation::build_local_state( apps, 0.0, @@ -398,6 +415,7 @@ impl RpcHandler { tor_active, server_name, nostr_npub, + own_fips_npub, &federated_peers, ); diff --git a/core/archipelago/src/federation/storage.rs b/core/archipelago/src/federation/storage.rs index c643302a..429a2c93 100644 --- a/core/archipelago/src/federation/storage.rs +++ b/core/archipelago/src/federation/storage.rs @@ -177,6 +177,17 @@ pub async fn update_node_state(data_dir: &Path, did: &str, state: NodeStateSnaps node.name = Some(name.clone()); } } + // Learn the peer's FIPS npub from their state snapshot so + // federations established before v1.4 (pre-fips_npub) start + // routing over FIPS on the very next sync. Refresh if the peer + // rotated their FIPS key, too. + if let Some(ref npub) = state.own_fips_npub { + if !npub.is_empty() + && node.fips_npub.as_deref().map(str::trim) != Some(npub.trim()) + { + node.fips_npub = Some(npub.clone()); + } + } node.last_state = Some(state); save_nodes(data_dir, &nodes).await?; } @@ -314,6 +325,7 @@ mod tests { uptime_secs: Some(86400), tor_active: Some(true), nostr_npub: None, + own_fips_npub: None, federated_peers: Vec::new(), }; diff --git a/core/archipelago/src/federation/sync.rs b/core/archipelago/src/federation/sync.rs index 18dec72a..b7d08079 100644 --- a/core/archipelago/src/federation/sync.rs +++ b/core/archipelago/src/federation/sync.rs @@ -160,6 +160,7 @@ pub fn build_local_state( tor_active: bool, server_name: Option, nostr_npub: Option, + own_fips_npub: Option, federated_peers: &[FederatedNode], ) -> NodeStateSnapshot { let hints = federated_peers @@ -186,6 +187,7 @@ pub fn build_local_state( uptime_secs: Some(uptime), tor_active: Some(tor_active), nostr_npub, + own_fips_npub, federated_peers: hints, } } @@ -274,6 +276,7 @@ mod tests { true, Some("Test Node".to_string()), None, + None, &[], ); assert_eq!(state.apps.len(), 1); @@ -328,7 +331,7 @@ mod tests { ]; let state = build_local_state( vec![], - 0.0, 0, 0, 0, 0, 0, true, None, None, &peers, + 0.0, 0, 0, 0, 0, 0, true, None, None, None, &peers, ); assert_eq!(state.federated_peers.len(), 1); assert_eq!(state.federated_peers[0].did, "did:key:zTrusted"); diff --git a/core/archipelago/src/federation/types.rs b/core/archipelago/src/federation/types.rs index 8a3cc9ee..332e1046 100644 --- a/core/archipelago/src/federation/types.rs +++ b/core/archipelago/src/federation/types.rs @@ -78,6 +78,13 @@ pub struct NodeStateSnapshot { /// haven't synced after this field was added will report None. #[serde(default)] pub nostr_npub: Option, + /// The sender's own FIPS npub (bech32). Lets pre-FIPS federation + /// pairs — who federated before v1.4 added fips_npub to the invite + /// code — discover each other's FIPS identity on the next state + /// sync and route over FIPS from then on. Optional for back-compat + /// with older peers. + #[serde(default)] + pub own_fips_npub: Option, /// Minimal summary of peers this node trusts, used for transitive /// federation: when Alice syncs with Bob, she learns Bob's trusted /// peers and adds them as Observers on her side so `fips_npub` is diff --git a/core/archipelago/src/transport/delta.rs b/core/archipelago/src/transport/delta.rs index a99c910d..9172e8d4 100644 --- a/core/archipelago/src/transport/delta.rs +++ b/core/archipelago/src/transport/delta.rs @@ -223,6 +223,7 @@ mod tests { uptime_secs: Some(86400), tor_active: Some(true), nostr_npub: None, + own_fips_npub: None, federated_peers: Vec::new(), } } @@ -256,6 +257,7 @@ mod tests { uptime_secs: Some(86700), // Changed tor_active: Some(true), nostr_npub: None, + own_fips_npub: None, federated_peers: Vec::new(), } }