From 7c8e5cfa3de95b6662208b6effe214b3ba9f5dc6 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Mon, 22 Dec 2025 03:58:39 +0100 Subject: [PATCH] feat: Record node metadata including public key, hostname, and status in the root store during initialization. --- docs/roadmap.md | 1 + lattice-cli/Cargo.toml | 2 + lattice-cli/src/commands.rs | 13 ++---- lattice-cli/src/node.rs | 86 ++++++++++++++++++++++++------------- 4 files changed, 64 insertions(+), 38 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index d7466b4..0be7c30 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -94,6 +94,7 @@ - [x] Multi-store sync test: compute diff, fetch entries, apply, verify same state **Phase 2: Iroh Integration** +- [x] Node info in root store on init: `/nodes/{pubkey}/info` + `/status` - [ ] Iroh integration (peer discovery, connection) - [ ] Sync protocol (push missing entries over network) - [ ] CLI: `peers`, `connect`/`join` commands diff --git a/lattice-cli/Cargo.toml b/lattice-cli/Cargo.toml index 58bf50a..02cbb53 100644 --- a/lattice-cli/Cargo.toml +++ b/lattice-cli/Cargo.toml @@ -16,3 +16,5 @@ hex = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } shlex = "1" +hostname = "0.4" +serde_json = "1" diff --git a/lattice-cli/src/commands.rs b/lattice-cli/src/commands.rs index 69ad722..5431182 100644 --- a/lattice-cli/src/commands.rs +++ b/lattice-cli/src/commands.rs @@ -126,16 +126,11 @@ pub fn commands() -> Vec { // --- Store management --- fn cmd_init(node: &LatticeNode, _store: Option<&StoreHandle>, _args: &[String]) -> CommandResult { - match node.init() { - Ok(store_id) => { + match block_async(node.init()) { + Ok((store_id, handle)) => { println!("Initialized with root store: {}", store_id); - match node.open_store(store_id) { - Ok((handle, _)) => CommandResult::SwitchTo(handle), - Err(e) => { - eprintln!("Warning: {}", e); - CommandResult::Ok - } - } + println!("Node pubkey stored in /nodes/{}/info", hex::encode(node.node_id())); + CommandResult::SwitchTo(handle) } Err(e) => { eprintln!("Error: {}", e); diff --git a/lattice-cli/src/node.rs b/lattice-cli/src/node.rs index 8d5c77d..280f4fd 100644 --- a/lattice-cli/src/node.rs +++ b/lattice-cli/src/node.rs @@ -115,7 +115,6 @@ impl LatticeNode { pub fn root_store(&self) -> Result, NodeError> { Ok(self.meta.root_store()?) } - /// Open the root store if set pub fn open_root_store(&self) -> Result, NodeError> { match self.meta.root_store()? { @@ -124,14 +123,39 @@ impl LatticeNode { } } - /// Initialize the node with a root store (fails if already initialized) - pub fn init(&self) -> Result { + /// Initialize the node with a root store (fails if already initialized). + /// Writes the node's pubkey to `/nodes/{pubkey}/info` in the root store. + pub async fn init(&self) -> Result<(Uuid, StoreHandle), NodeError> { if self.meta.root_store()?.is_some() { return Err(NodeError::AlreadyInitialized); } let store_id = self.create_store()?; self.meta.set_root_store(store_id)?; - Ok(store_id) + + // Open the store and write our node info + let (handle, _) = self.open_store(store_id)?; + let pubkey_hex = hex::encode(self.node.public_key_bytes()); + let key = format!("/nodes/{}/info", pubkey_hex); + + // Store node metadata: name (hostname), added_at (timestamp) + let hostname = hostname::get() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|_| "unknown".to_string()); + let added_at = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + let info = serde_json::json!({ + "name": hostname, + "added_at": added_at + }); + handle.put(key.as_bytes(), info.to_string().as_bytes()).await?; + + // Write status = active + let status_key = format!("/nodes/{}/status", pubkey_hex); + handle.put(status_key.as_bytes(), b"active").await?; + + Ok((store_id, handle)) } pub fn list_stores(&self) -> Result, NodeError> { @@ -341,8 +365,8 @@ mod tests { let _ = std::fs::remove_dir_all(data_dir.base()); } - #[test] - fn test_init_creates_root_store() { + #[tokio::test] + async fn test_init_creates_root_store() { let data_dir = temp_data_dir("init_root"); let node = LatticeNodeBuilder { data_dir: data_dir.clone() } @@ -353,42 +377,44 @@ mod tests { assert!(node.root_store().unwrap().is_none()); // Init creates root store - let root_id = node.init().expect("init failed"); + let (root_id, _handle) = node.init().await.expect("init failed"); assert_eq!(node.root_store().unwrap(), Some(root_id)); let _ = std::fs::remove_dir_all(data_dir.base()); } - #[test] - fn test_duplicate_init_fails() { + #[tokio::test] + async fn test_duplicate_init_fails() { let data_dir = temp_data_dir("init_dup"); let node = LatticeNodeBuilder { data_dir: data_dir.clone() } .build() .expect("create node"); - node.init().expect("first init"); + node.init().await.expect("first init"); // Second init should fail - match node.init() { - Err(NodeError::AlreadyInitialized) => (), - other => panic!("Expected AlreadyInitialized, got {:?}", other), + match node.init().await { + Ok(_) => panic!("Expected AlreadyInitialized error"), + Err(e) => match e { + NodeError::AlreadyInitialized => (), + _ => panic!("Expected AlreadyInitialized, got {:?}", e), + }, } let _ = std::fs::remove_dir_all(data_dir.base()); } - #[test] - fn test_root_store_in_info_after_init() { + #[tokio::test] + async fn test_root_store_in_info_after_init() { let data_dir = temp_data_dir("init_info"); // First session: init - let root_id = { - let node = LatticeNodeBuilder { data_dir: data_dir.clone() } - .build() - .expect("create node"); - node.init().expect("init") - }; + let node = LatticeNodeBuilder { data_dir: data_dir.clone() } + .build() + .expect("create node"); + let (root_id, _) = node.init().await.expect("init"); + drop(node); // End first session // Second session: root_store should persist let node = LatticeNodeBuilder { data_dir: data_dir.clone() } @@ -407,26 +433,28 @@ mod tests { let node = LatticeNodeBuilder { data_dir: data_dir.clone() } .build() .expect("create node"); - let store_id = node.init().expect("init"); - let (store, _) = node.open_store(store_id).expect("open store"); + let (_, store) = node.init().await.expect("init"); + + // Get baseline seq after init + let baseline = store.log_seq().await; // Put twice with same value - second should be idempotent let seq1 = store.put(b"/key", b"value").await.expect("put 1"); - assert_eq!(seq1, 1); + assert_eq!(seq1, baseline + 1); let seq2 = store.put(b"/key", b"value").await.expect("put 2"); - assert_eq!(seq2, 1, "Second put with same value should be idempotent (no new entry)"); + assert_eq!(seq2, baseline + 1, "Second put should be idempotent (no new entry)"); - assert_eq!(store.log_seq().await, 1, "Log should have 1 entry, not 2"); + assert_eq!(store.log_seq().await, baseline + 1); // Delete twice - second should be idempotent let seq3 = store.delete(b"/key").await.expect("delete 1"); - assert_eq!(seq3, 2); + assert_eq!(seq3, baseline + 2); let seq4 = store.delete(b"/key").await.expect("delete 2"); - assert_eq!(seq4, 2, "Second delete should be idempotent (no new entry)"); + assert_eq!(seq4, baseline + 2, "Second delete should be idempotent (no new entry)"); - assert_eq!(store.log_seq().await, 2, "Log should have 2 entries, not 3"); + assert_eq!(store.log_seq().await, baseline + 2); let _ = std::fs::remove_dir_all(data_dir.base()); }