// SPDX-License-Identifier: LCL-1.0 // Copyright (c) 2026 Libertaria Contributors // This file is part of the Libertaria Core, licensed under // The Libertaria Commonwealth License v1.0. //! RFC-0122: Kademlia-lite DHT for Capsule Discovery //! Implements wide-area peer discovery using XOR distance metric. const std = @import("std"); const net = std.net; pub const K = 20; // Bucket size pub const ID_LEN = 32; // 256-bit IDs (truncated Blake3) pub const NodeId = [ID_LEN]u8; /// XOR distance metric pub fn distance(a: NodeId, b: NodeId) NodeId { var result: NodeId = undefined; for (0..ID_LEN) |i| { result[i] = a[i] ^ b[i]; } return result; } /// Returns the index of the first set bit (distance order) pub fn commonPrefixLen(id1: NodeId, id2: NodeId) usize { var count: usize = 0; for (0..ID_LEN) |i| { const x = id1[i] ^ id2[i]; if (x == 0) { count += 8; } else { count += @clz(x); break; } } return count; } pub const RemoteNode = struct { id: NodeId, address: net.Address, last_seen: i64, key: [32]u8 = [_]u8{0} ** 32, // X25519 Public Key }; pub const KBucket = struct { nodes: std.ArrayList(RemoteNode) = .{}, pub fn deinit(self: *KBucket, allocator: std.mem.Allocator) void { self.nodes.deinit(allocator); } }; pub const RoutingTable = struct { self_id: NodeId, buckets: [ID_LEN * 8]KBucket, allocator: std.mem.Allocator, pub fn init(allocator: std.mem.Allocator, self_id: NodeId) RoutingTable { return RoutingTable{ .self_id = self_id, .buckets = [_]KBucket{.{}} ** (ID_LEN * 8), .allocator = allocator, }; } pub fn deinit(self: *RoutingTable) void { for (0..self.buckets.len) |i| { self.buckets[i].deinit(self.allocator); } } pub fn update(self: *RoutingTable, node: RemoteNode) !void { const cpl = commonPrefixLen(self.self_id, node.id); const bucket_idx = if (cpl == ID_LEN * 8) ID_LEN * 8 - 1 else cpl; var bucket = &self.buckets[bucket_idx]; // 1. If node exists, move to end (most recent) for (bucket.nodes.items, 0..) |existing, i| { if (std.mem.eql(u8, &existing.id, &node.id)) { _ = bucket.nodes.orderedRemove(i); try bucket.nodes.append(self.allocator, node); return; } } // 2. If bucket not full, add to end if (bucket.nodes.items.len < K) { try bucket.nodes.append(self.allocator, node); } else { // 3. Bucket full, ping oldest (front) // For now, we just don't add. TODO: Implement ping-and-replace } } pub fn findNode(self: *RoutingTable, target: NodeId) ?RemoteNode { const cpl = commonPrefixLen(self.self_id, target); const bucket_idx = if (cpl == ID_LEN * 8) ID_LEN * 8 - 1 else cpl; const bucket = &self.buckets[bucket_idx]; for (bucket.nodes.items) |node| { if (std.mem.eql(u8, &node.id, &target)) { return node; } } return null; } pub fn findClosest(self: *RoutingTable, target: NodeId, count: usize) ![]RemoteNode { var results = std.ArrayList(RemoteNode){}; defer results.deinit(self.allocator); // Collect all nodes from all buckets for (self.buckets) |bucket| { for (bucket.nodes.items) |node| { try results.append(self.allocator, node); } } // Sort by distance to target const SortContext = struct { target: NodeId, pub fn lessThan(ctx: @This(), a: RemoteNode, b: RemoteNode) bool { const dist_a = distance(a.id, ctx.target); const dist_b = distance(b.id, ctx.target); for (0..ID_LEN) |i| { if (dist_a[i] < dist_b[i]) return true; if (dist_a[i] > dist_b[i]) return false; } return false; } }; std.sort.block(RemoteNode, results.items, SortContext{ .target = target }, SortContext.lessThan); const actual_count = if (results.items.len < count) results.items.len else count; const out = try self.allocator.alloc(RemoteNode, actual_count); @memcpy(out, results.items[0..actual_count]); return out; } pub fn getNodeCount(self: *const RoutingTable) usize { var count: usize = 0; for (self.buckets) |bucket| { count += bucket.nodes.items.len; } return count; } }; pub const DhtService = struct { allocator: std.mem.Allocator, routing_table: RoutingTable, pub fn init(allocator: std.mem.Allocator, self_id: NodeId) DhtService { return .{ .allocator = allocator, .routing_table = RoutingTable.init(allocator, self_id), }; } pub fn deinit(self: *DhtService) void { self.routing_table.deinit(); } pub fn getKnownNodeCount(self: *const DhtService) usize { return self.routing_table.getNodeCount(); } };