feat: DID persistence + federation node names in sync

Part 1 — DID Persistence:
- Deploy script creates /var/lib/archipelago/identity/ directory
- First-boot script creates identity dir with proper ownership
- Identity load now logs pubkey to confirm persistence across restarts

Part 2 — Node Names:
- NodeStateSnapshot includes node_name field
- build_local_state() passes server name to sync responses
- update_node_state() stores peer's announced name on the FederatedNode
- Names propagate automatically during federation.sync-state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-19 19:19:13 +00:00
parent f8eefa87d2
commit f8794791f3
8 changed files with 66 additions and 6 deletions

View File

@@ -328,8 +328,9 @@ impl RpcHandler {
let tor_active = data.server_info.tor_address.is_some();
let server_name = data.server_info.name.clone().filter(|n| !n.is_empty());
let state = federation::build_local_state(
apps, 0.0, 0, 0, 0, 0, 0, tor_active,
apps, 0.0, 0, 0, 0, 0, 0, tor_active, server_name,
);
Ok(serde_json::to_value(&state)?)

View File

@@ -53,6 +53,8 @@ pub struct FederatedNode {
pub struct NodeStateSnapshot {
pub timestamp: String,
#[serde(default)]
pub node_name: Option<String>,
#[serde(default)]
pub apps: Vec<AppStatus>,
#[serde(default)]
pub cpu_usage_percent: Option<f64>,
@@ -184,6 +186,12 @@ pub async fn update_node_state(
let mut nodes = load_nodes(data_dir).await?;
if let Some(node) = nodes.iter_mut().find(|n| n.did == did) {
node.last_seen = Some(state.timestamp.clone());
// Update node name from sync if provided (peer announced their name)
if let Some(ref name) = state.node_name {
if !name.is_empty() {
node.name = Some(name.clone());
}
}
node.last_state = Some(state);
save_nodes(data_dir, &nodes).await?;
}
@@ -495,9 +503,11 @@ pub fn build_local_state(
disk_total: u64,
uptime: u64,
tor_active: bool,
server_name: Option<String>,
) -> NodeStateSnapshot {
NodeStateSnapshot {
timestamp: chrono::Utc::now().to_rfc3339(),
node_name: server_name,
apps,
cpu_usage_percent: Some(cpu),
mem_used_bytes: Some(mem_used),
@@ -813,9 +823,11 @@ mod tests {
500_000_000_000,
3600,
true,
Some("Test Node".to_string()),
);
assert_eq!(state.apps.len(), 1);
assert_eq!(state.cpu_usage_percent, Some(25.5));
assert_eq!(state.tor_active, Some(true));
assert_eq!(state.node_name, Some("Test Node".to_string()));
}
}

View File

@@ -35,7 +35,10 @@ impl NodeIdentity {
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid node key length"))?;
SigningKey::from_bytes(&arr)
let key = SigningKey::from_bytes(&arr);
let pubkey_hex = hex::encode(key.verifying_key().as_bytes());
tracing::info!("Loaded existing node identity (pubkey: {}...)", &pubkey_hex[..16]);
key
} else {
let signing_key = SigningKey::generate(&mut OsRng);
fs::write(&key_path, signing_key.to_bytes())

View File

@@ -195,6 +195,7 @@ mod tests {
fn sample_snapshot_a() -> NodeStateSnapshot {
NodeStateSnapshot {
timestamp: "2026-03-16T12:00:00Z".to_string(),
node_name: Some("Test Node A".to_string()),
apps: vec![
AppStatus {
id: "bitcoin-knots".to_string(),
@@ -225,6 +226,7 @@ mod tests {
fn sample_snapshot_b() -> NodeStateSnapshot {
NodeStateSnapshot {
timestamp: "2026-03-16T12:05:00Z".to_string(),
node_name: Some("Test Node A".to_string()),
apps: vec![
AppStatus {
id: "bitcoin-knots".to_string(),