feat: Record node metadata including public key, hostname, and status in the root store during initialization.

This commit is contained in:
2025-12-22 03:58:49 +01:00
parent 1943e06509
commit 7c8e5cfa3d
4 changed files with 64 additions and 38 deletions
+1
View File
@@ -94,6 +94,7 @@
- [x] Multi-store sync test: compute diff, fetch entries, apply, verify same state - [x] Multi-store sync test: compute diff, fetch entries, apply, verify same state
**Phase 2: Iroh Integration** **Phase 2: Iroh Integration**
- [x] Node info in root store on init: `/nodes/{pubkey}/info` + `/status`
- [ ] Iroh integration (peer discovery, connection) - [ ] Iroh integration (peer discovery, connection)
- [ ] Sync protocol (push missing entries over network) - [ ] Sync protocol (push missing entries over network)
- [ ] CLI: `peers`, `connect`/`join` commands - [ ] CLI: `peers`, `connect`/`join` commands
+2
View File
@@ -16,3 +16,5 @@ hex = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
shlex = "1" shlex = "1"
hostname = "0.4"
serde_json = "1"
+4 -9
View File
@@ -126,16 +126,11 @@ pub fn commands() -> Vec<Command> {
// --- Store management --- // --- Store management ---
fn cmd_init(node: &LatticeNode, _store: Option<&StoreHandle>, _args: &[String]) -> CommandResult { fn cmd_init(node: &LatticeNode, _store: Option<&StoreHandle>, _args: &[String]) -> CommandResult {
match node.init() { match block_async(node.init()) {
Ok(store_id) => { Ok((store_id, handle)) => {
println!("Initialized with root store: {}", store_id); println!("Initialized with root store: {}", store_id);
match node.open_store(store_id) { println!("Node pubkey stored in /nodes/{}/info", hex::encode(node.node_id()));
Ok((handle, _)) => CommandResult::SwitchTo(handle), CommandResult::SwitchTo(handle)
Err(e) => {
eprintln!("Warning: {}", e);
CommandResult::Ok
}
}
} }
Err(e) => { Err(e) => {
eprintln!("Error: {}", e); eprintln!("Error: {}", e);
+54 -26
View File
@@ -115,7 +115,6 @@ impl LatticeNode {
pub fn root_store(&self) -> Result<Option<Uuid>, NodeError> { pub fn root_store(&self) -> Result<Option<Uuid>, NodeError> {
Ok(self.meta.root_store()?) Ok(self.meta.root_store()?)
} }
/// Open the root store if set /// Open the root store if set
pub fn open_root_store(&self) -> Result<Option<(StoreHandle, StoreInfo)>, NodeError> { pub fn open_root_store(&self) -> Result<Option<(StoreHandle, StoreInfo)>, NodeError> {
match self.meta.root_store()? { match self.meta.root_store()? {
@@ -124,14 +123,39 @@ impl LatticeNode {
} }
} }
/// Initialize the node with a root store (fails if already initialized) /// Initialize the node with a root store (fails if already initialized).
pub fn init(&self) -> Result<Uuid, NodeError> { /// 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() { if self.meta.root_store()?.is_some() {
return Err(NodeError::AlreadyInitialized); return Err(NodeError::AlreadyInitialized);
} }
let store_id = self.create_store()?; let store_id = self.create_store()?;
self.meta.set_root_store(store_id)?; 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<Vec<Uuid>, NodeError> { pub fn list_stores(&self) -> Result<Vec<Uuid>, NodeError> {
@@ -341,8 +365,8 @@ mod tests {
let _ = std::fs::remove_dir_all(data_dir.base()); let _ = std::fs::remove_dir_all(data_dir.base());
} }
#[test] #[tokio::test]
fn test_init_creates_root_store() { async fn test_init_creates_root_store() {
let data_dir = temp_data_dir("init_root"); let data_dir = temp_data_dir("init_root");
let node = LatticeNodeBuilder { data_dir: data_dir.clone() } let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
@@ -353,42 +377,44 @@ mod tests {
assert!(node.root_store().unwrap().is_none()); assert!(node.root_store().unwrap().is_none());
// Init creates root store // 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)); assert_eq!(node.root_store().unwrap(), Some(root_id));
let _ = std::fs::remove_dir_all(data_dir.base()); let _ = std::fs::remove_dir_all(data_dir.base());
} }
#[test] #[tokio::test]
fn test_duplicate_init_fails() { async fn test_duplicate_init_fails() {
let data_dir = temp_data_dir("init_dup"); let data_dir = temp_data_dir("init_dup");
let node = LatticeNodeBuilder { data_dir: data_dir.clone() } let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build() .build()
.expect("create node"); .expect("create node");
node.init().expect("first init"); node.init().await.expect("first init");
// Second init should fail // Second init should fail
match node.init() { match node.init().await {
Err(NodeError::AlreadyInitialized) => (), Ok(_) => panic!("Expected AlreadyInitialized error"),
other => panic!("Expected AlreadyInitialized, got {:?}", other), Err(e) => match e {
NodeError::AlreadyInitialized => (),
_ => panic!("Expected AlreadyInitialized, got {:?}", e),
},
} }
let _ = std::fs::remove_dir_all(data_dir.base()); let _ = std::fs::remove_dir_all(data_dir.base());
} }
#[test] #[tokio::test]
fn test_root_store_in_info_after_init() { async fn test_root_store_in_info_after_init() {
let data_dir = temp_data_dir("init_info"); let data_dir = temp_data_dir("init_info");
// First session: init // First session: init
let root_id = {
let node = LatticeNodeBuilder { data_dir: data_dir.clone() } let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build() .build()
.expect("create node"); .expect("create node");
node.init().expect("init") let (root_id, _) = node.init().await.expect("init");
}; drop(node); // End first session
// Second session: root_store should persist // Second session: root_store should persist
let node = LatticeNodeBuilder { data_dir: data_dir.clone() } let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
@@ -407,26 +433,28 @@ mod tests {
let node = LatticeNodeBuilder { data_dir: data_dir.clone() } let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build() .build()
.expect("create node"); .expect("create node");
let store_id = node.init().expect("init"); let (_, store) = node.init().await.expect("init");
let (store, _) = node.open_store(store_id).expect("open store");
// Get baseline seq after init
let baseline = store.log_seq().await;
// Put twice with same value - second should be idempotent // Put twice with same value - second should be idempotent
let seq1 = store.put(b"/key", b"value").await.expect("put 1"); 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"); 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 // Delete twice - second should be idempotent
let seq3 = store.delete(b"/key").await.expect("delete 1"); 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"); 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()); let _ = std::fs::remove_dir_all(data_dir.base());
} }