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:
@@ -387,6 +387,23 @@ impl RpcHandler {
|
|||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.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(
|
let state = federation::build_local_state(
|
||||||
apps,
|
apps,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -398,6 +415,7 @@ impl RpcHandler {
|
|||||||
tor_active,
|
tor_active,
|
||||||
server_name,
|
server_name,
|
||||||
nostr_npub,
|
nostr_npub,
|
||||||
|
own_fips_npub,
|
||||||
&federated_peers,
|
&federated_peers,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,17 @@ pub async fn update_node_state(data_dir: &Path, did: &str, state: NodeStateSnaps
|
|||||||
node.name = Some(name.clone());
|
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);
|
node.last_state = Some(state);
|
||||||
save_nodes(data_dir, &nodes).await?;
|
save_nodes(data_dir, &nodes).await?;
|
||||||
}
|
}
|
||||||
@@ -314,6 +325,7 @@ mod tests {
|
|||||||
uptime_secs: Some(86400),
|
uptime_secs: Some(86400),
|
||||||
tor_active: Some(true),
|
tor_active: Some(true),
|
||||||
nostr_npub: None,
|
nostr_npub: None,
|
||||||
|
own_fips_npub: None,
|
||||||
federated_peers: Vec::new(),
|
federated_peers: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ pub fn build_local_state(
|
|||||||
tor_active: bool,
|
tor_active: bool,
|
||||||
server_name: Option<String>,
|
server_name: Option<String>,
|
||||||
nostr_npub: Option<String>,
|
nostr_npub: Option<String>,
|
||||||
|
own_fips_npub: Option<String>,
|
||||||
federated_peers: &[FederatedNode],
|
federated_peers: &[FederatedNode],
|
||||||
) -> NodeStateSnapshot {
|
) -> NodeStateSnapshot {
|
||||||
let hints = federated_peers
|
let hints = federated_peers
|
||||||
@@ -186,6 +187,7 @@ pub fn build_local_state(
|
|||||||
uptime_secs: Some(uptime),
|
uptime_secs: Some(uptime),
|
||||||
tor_active: Some(tor_active),
|
tor_active: Some(tor_active),
|
||||||
nostr_npub,
|
nostr_npub,
|
||||||
|
own_fips_npub,
|
||||||
federated_peers: hints,
|
federated_peers: hints,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,6 +276,7 @@ mod tests {
|
|||||||
true,
|
true,
|
||||||
Some("Test Node".to_string()),
|
Some("Test Node".to_string()),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
&[],
|
&[],
|
||||||
);
|
);
|
||||||
assert_eq!(state.apps.len(), 1);
|
assert_eq!(state.apps.len(), 1);
|
||||||
@@ -328,7 +331,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
let state = build_local_state(
|
let state = build_local_state(
|
||||||
vec![],
|
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.len(), 1);
|
||||||
assert_eq!(state.federated_peers[0].did, "did:key:zTrusted");
|
assert_eq!(state.federated_peers[0].did, "did:key:zTrusted");
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ pub struct NodeStateSnapshot {
|
|||||||
/// haven't synced after this field was added will report None.
|
/// haven't synced after this field was added will report None.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub nostr_npub: Option<String>,
|
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
|
/// Minimal summary of peers this node trusts, used for transitive
|
||||||
/// federation: when Alice syncs with Bob, she learns Bob's trusted
|
/// federation: when Alice syncs with Bob, she learns Bob's trusted
|
||||||
/// peers and adds them as Observers on her side so `fips_npub` is
|
/// peers and adds them as Observers on her side so `fips_npub` is
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ mod tests {
|
|||||||
uptime_secs: Some(86400),
|
uptime_secs: Some(86400),
|
||||||
tor_active: Some(true),
|
tor_active: Some(true),
|
||||||
nostr_npub: None,
|
nostr_npub: None,
|
||||||
|
own_fips_npub: None,
|
||||||
federated_peers: Vec::new(),
|
federated_peers: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,6 +257,7 @@ mod tests {
|
|||||||
uptime_secs: Some(86700), // Changed
|
uptime_secs: Some(86700), // Changed
|
||||||
tor_active: Some(true),
|
tor_active: Some(true),
|
||||||
nostr_npub: None,
|
nostr_npub: None,
|
||||||
|
own_fips_npub: None,
|
||||||
federated_peers: Vec::new(),
|
federated_peers: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user