feat(federation): advertise own_fips_npub in state snapshots

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) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-19 04:16:05 -04:00
parent 3c83440a60
commit bfe2603f69
5 changed files with 43 additions and 1 deletions

View File

@ -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,
);

View File

@ -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(),
};

View File

@ -160,6 +160,7 @@ pub fn build_local_state(
tor_active: bool,
server_name: Option<String>,
nostr_npub: Option<String>,
own_fips_npub: Option<String>,
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");

View File

@ -78,6 +78,13 @@ pub struct NodeStateSnapshot {
/// haven't synced after this field was added will report None.
#[serde(default)]
pub nostr_npub: Option<String>,
/// 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<String>,
/// 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

View File

@ -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(),
}
}