diff --git a/.gitignore b/.gitignore index 2e3f17c..2a5afc6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,11 @@ STORIES/ logs/ *.log +# Operational Data +data/ +config.json +capsule.log + # Editor & OS .DS_Store .idea/ diff --git a/README.md b/README.md index 05bae87..e84a867 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **The Core Protocol Stack for Libertaria Applications** **Version:** 1.0.0-beta ("Shield") -**License:** TBD +**License:** LUL-1.0 **Status:** 🛡️ **AUTONOMOUS IMMUNE RESPONSE: OPERATIONAL** (100% Complete) --- @@ -84,5 +84,4 @@ cargo test --test simulation_attack -- --nocapture --- **Mission Accomplished.** -Markus Maiwald & Voxis Forge. -2026. +Markus Maiwald & Voxis Forge. 2026. diff --git a/build.zig b/build.zig index 2b6be86..79ff00d 100644 --- a/build.zig +++ b/build.zig @@ -4,6 +4,15 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + // ======================================================================== + // Time Module (L0) + // ======================================================================== + const time_mod = b.createModule(.{ + .root_source_file = b.path("l0-transport/time.zig"), + .target = target, + .optimize = optimize, + }); + // ======================================================================== // L0: Transport Layer // ======================================================================== @@ -144,6 +153,17 @@ pub fn build(b: *std.Build) void { utcp_mod.addImport("quarantine", l0_quarantine_mod); l0_service_mod.addImport("quarantine", l0_quarantine_mod); + // ======================================================================== + // L1 Trust Graph Module (Core Dependency for QVL/PoP) + // ======================================================================== + const l1_trust_graph_mod = b.createModule(.{ + .root_source_file = b.path("l1-identity/trust_graph.zig"), + .target = target, + .optimize = optimize, + }); + // trust_graph needs crypto types + l1_trust_graph_mod.addImport("crypto", l1_mod); + // ======================================================================== // L1 QVL (Quasar Vector Lattice) - Advanced Graph Engine // ======================================================================== @@ -152,6 +172,28 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + l1_qvl_mod.addImport("trust_graph", l1_trust_graph_mod); + l1_qvl_mod.addImport("time", time_mod); + + // QVL FFI (C ABI exports for L2 integration) + const l1_qvl_ffi_mod = b.createModule(.{ + .root_source_file = b.path("l1-identity/qvl_ffi.zig"), + .target = target, + .optimize = optimize, + }); + l1_qvl_ffi_mod.addImport("qvl", l1_qvl_mod); + l1_qvl_ffi_mod.addImport("slash", l1_slash_mod); + l1_qvl_ffi_mod.addImport("time", time_mod); + l1_qvl_ffi_mod.addImport("trust_graph", l1_trust_graph_mod); + + // QVL FFI static library (for Rust L2 Membrane Agent) + const qvl_ffi_lib = b.addLibrary(.{ + .name = "qvl_ffi", + .root_module = l1_qvl_ffi_mod, + .linkage = .static, + }); + qvl_ffi_lib.linkLibC(); + b.installArtifact(qvl_ffi_lib); // ======================================================================== // Tests (with C FFI support for Argon2 + liboqs) @@ -253,18 +295,12 @@ pub fn build(b: *std.Build) void { const run_l0_quarantine_tests = b.addRunArtifact(l0_quarantine_tests); // Import PQXDH into main L1 module - // Tests (root is test_pqxdh.zig) const l1_pqxdh_tests_mod = b.createModule(.{ .root_source_file = b.path("l1-identity/test_pqxdh.zig"), .target = target, .optimize = optimize, }); - // Tests import the library module 'pqxdh' (relative import works too, but module is cleaner if we use @import("pqxdh")) - // But test_pqxdh.zig uses @import("pqxdh.zig") which is relative file import. - // If we use relative import, the test module must be able to resolve pqxdh.zig. - // Since they are in same dir, relative import works. - // BUT the artifact compiled from test_pqxdh.zig needs to link liboqs because it effectively includes pqxdh.zig code. const l1_pqxdh_tests = b.addTest(.{ .root_module = l1_pqxdh_tests_mod, @@ -275,16 +311,6 @@ pub fn build(b: *std.Build) void { l1_pqxdh_tests.linkSystemLibrary("oqs"); const run_l1_pqxdh_tests = b.addRunArtifact(l1_pqxdh_tests); - // Link time module to l1_vector_mod - // ======================================================================== - // Time Module (L0) - // ======================================================================== - const time_mod = b.createModule(.{ - .root_source_file = b.path("l0-transport/time.zig"), - .target = target, - .optimize = optimize, - }); - // L1 Vector tests (Phase 3C) const l1_vector_mod = b.createModule(.{ .root_source_file = b.path("l1-identity/vector.zig"), @@ -293,27 +319,7 @@ pub fn build(b: *std.Build) void { }); l1_vector_mod.addImport("time", time_mod); l1_vector_mod.addImport("pqxdh", l1_pqxdh_mod); - // QVL also needs time (via proof_of_path.zig dependency) - l1_qvl_mod.addImport("time", time_mod); - - // QVL FFI (C ABI exports for L2 integration) - const l1_qvl_ffi_mod = b.createModule(.{ - .root_source_file = b.path("l1-identity/qvl_ffi.zig"), - .target = target, - .optimize = optimize, - }); - l1_qvl_ffi_mod.addImport("qvl", l1_qvl_mod); - l1_qvl_ffi_mod.addImport("slash", l1_slash_mod); - l1_qvl_ffi_mod.addImport("time", time_mod); - - // QVL FFI static library (for Rust L2 Membrane Agent) - const qvl_ffi_lib = b.addLibrary(.{ - .name = "qvl_ffi", - .root_module = l1_qvl_ffi_mod, - .linkage = .static, // Static library - }); - qvl_ffi_lib.linkLibC(); - b.installArtifact(qvl_ffi_lib); + l1_vector_mod.addImport("trust_graph", l1_trust_graph_mod); const l1_vector_tests = b.addTest(.{ .root_module = l1_vector_mod, @@ -339,7 +345,21 @@ pub fn build(b: *std.Build) void { l1_vector_tests.linkLibC(); const run_l1_vector_tests = b.addRunArtifact(l1_vector_tests); - // NOTE: Phase 3 PQXDH uses ML-KEM-768 via liboqs (integrated). + // L1 QVL tests + const l1_qvl_tests = b.addTest(.{ + .root_module = l1_qvl_mod, + }); + const run_l1_qvl_tests = b.addRunArtifact(l1_qvl_tests); + + // L1 QVL FFI tests (C ABI validation) + const l1_qvl_ffi_tests = b.addTest(.{ + .root_module = l1_qvl_ffi_mod, + }); + l1_qvl_ffi_tests.linkLibC(); // Required for C allocator + const run_l1_qvl_ffi_tests = b.addRunArtifact(l1_qvl_ffi_tests); + + // NOTE: C test harness (test_qvl_ffi.c) can be compiled manually: + // zig cc -I. l1-identity/test_qvl_ffi.c zig-out/lib/libqvl_ffi.a -o test_qvl_ffi // Test step (runs Phase 2B + 2C + 2D + 3C SDK tests) const test_step = b.step("test", "Run SDK tests"); @@ -357,25 +377,9 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&run_utcp_tests.step); test_step.dependOn(&run_opq_tests.step); test_step.dependOn(&run_l0_service_tests.step); - - // L1 QVL tests - const l1_qvl_tests = b.addTest(.{ - .root_module = l1_qvl_mod, - }); - const run_l1_qvl_tests = b.addRunArtifact(l1_qvl_tests); test_step.dependOn(&run_l1_qvl_tests.step); - - // L1 QVL FFI tests (C ABI validation) - const l1_qvl_ffi_tests = b.addTest(.{ - .root_module = l1_qvl_ffi_mod, - }); - l1_qvl_ffi_tests.linkLibC(); // Required for C allocator - const run_l1_qvl_ffi_tests = b.addRunArtifact(l1_qvl_ffi_tests); test_step.dependOn(&run_l1_qvl_ffi_tests.step); - // NOTE: C test harness (test_qvl_ffi.c) can be compiled manually: - // zig cc -I. l1-identity/test_qvl_ffi.c zig-out/lib/libqvl_ffi.a -o test_qvl_ffi - // ======================================================================== // Examples // ======================================================================== @@ -417,13 +421,44 @@ pub fn build(b: *std.Build) void { // Convenience Commands // ======================================================================== - // Run LWF example - const run_lwf_example = b.addRunArtifact(lwf_example); - const run_lwf_step = b.step("run-lwf", "Run LWF frame example"); - run_lwf_step.dependOn(&run_lwf_example.step); - // Run crypto example const run_crypto_example = b.addRunArtifact(crypto_example); const run_crypto_step = b.step("run-crypto", "Run encryption example"); run_crypto_step.dependOn(&run_crypto_example.step); + + // ======================================================================== + // Capsule Core (Phase 10) Reference Implementation + // ======================================================================== + const capsule_mod = b.createModule(.{ + .root_source_file = b.path("capsule-core/src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // Link L0 (Transport) + capsule_mod.addImport("l0_transport", l0_mod); + capsule_mod.addImport("utcp", utcp_mod); + + // Link L1 (Identity) + capsule_mod.addImport("l1_identity", l1_mod); + capsule_mod.addImport("qvl", l1_qvl_mod); + + const capsule_exe = b.addExecutable(.{ + .name = "capsule", + .root_module = capsule_mod, + }); + // Link LibC (required for Argon2/OQS via L1) + capsule_exe.linkLibC(); + // Link SQLite3 (required for Persistent State) + capsule_exe.linkSystemLibrary("sqlite3"); + + b.installArtifact(capsule_exe); + + // Run command: zig build run -- args + const run_capsule = b.addRunArtifact(capsule_exe); + if (b.args) |args| { + run_capsule.addArgs(args); + } + const run_step = b.step("run", "Run the Capsule Node"); + run_step.dependOn(&run_capsule.step); } diff --git a/capsule-core/src/config.zig b/capsule-core/src/config.zig new file mode 100644 index 0000000..ba449f9 --- /dev/null +++ b/capsule-core/src/config.zig @@ -0,0 +1,91 @@ +//! Configuration for the Libertaria Capsule Node. + +const std = @import("std"); + +pub const NodeConfig = struct { + /// Data directory for persistent state (DB, keys, etc.) + data_dir: []const u8, + + /// UTCP bind port (default: 8710) + port: u16 = 8710, + + /// Bootstrap peers (multiaddrs) + bootstrap_peers: [][]const u8 = &.{}, + + /// Logging level + log_level: std.log.Level = .info, + + /// Free allocated memory (strings, slices) + pub fn deinit(self: *NodeConfig, allocator: std.mem.Allocator) void { + allocator.free(self.data_dir); + for (self.bootstrap_peers) |peer| { + allocator.free(peer); + } + allocator.free(self.bootstrap_peers); + } + + pub fn default(allocator: std.mem.Allocator) !NodeConfig { + // Default data dir: ~/.libertaria (or "data" for MVP) + return NodeConfig{ + .data_dir = try allocator.dupe(u8, "data"), + .port = 8710, + }; + } + + /// Load configuration from a JSON file + pub fn loadFromJsonFile(allocator: std.mem.Allocator, path: []const u8) !NodeConfig { + const file = std.fs.cwd().openFile(path, .{}) catch |err| { + if (err == error.FileNotFound) { + // If config missing, create default + std.log.info("Config file not found at {s}, creating default...", .{path}); + const cfg = try NodeConfig.default(allocator); + try cfg.saveToJsonFile(path); + return cfg; + } + return err; + }; + defer file.close(); + + const max_size = 1024 * 1024; // 1MB config limit + const content = try file.readToEndAlloc(allocator, max_size); + defer allocator.free(content); + + // Parse JSON + const parsed = try std.json.parseFromSlice(NodeConfig, allocator, content, .{ + .allocate = .alloc_always, + }); + defer parsed.deinit(); + + // Deep copy strings because parsed.value shares memory with arena/content in some modes, + // but here we used alloc_always so fields are allocated. + // However, std.json.parseFromSlice returns a Parsed(T) which manages the memory. + // We need to detach or copy the data to return a standalone NodeConfig. + // For simplicity and safety: manually duplicate into new struct. + + const cfg = parsed.value; + const data_dir = try allocator.dupe(u8, cfg.data_dir); + + var peers = std.array_list.Managed([]const u8).init(allocator); + for (cfg.bootstrap_peers) |peer| { + try peers.append(try allocator.dupe(u8, peer)); + } + + return NodeConfig{ + .data_dir = data_dir, + .port = cfg.port, + .bootstrap_peers = try peers.toOwnedSlice(), + .log_level = cfg.log_level, + }; + } + + /// Save configuration to a JSON file + pub fn saveToJsonFile(self: *const NodeConfig, path: []const u8) !void { + const file = try std.fs.cwd().createFile(path, .{}); + defer file.close(); + + var buf = std.array_list.Managed(u8).init(std.heap.page_allocator); + defer buf.deinit(); + try buf.writer().print("{f}", .{std.json.fmt(self, .{ .whitespace = .indent_4 })}); + try file.writeAll(buf.items); + } +}; diff --git a/capsule-core/src/dht.zig b/capsule-core/src/dht.zig new file mode 100644 index 0000000..6515795 --- /dev/null +++ b/capsule-core/src/dht.zig @@ -0,0 +1,140 @@ +//! 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, +}; + +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 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 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(); + } +}; diff --git a/capsule-core/src/discovery.zig b/capsule-core/src/discovery.zig new file mode 100644 index 0000000..5d06760 --- /dev/null +++ b/capsule-core/src/discovery.zig @@ -0,0 +1,178 @@ +//! RFC-0120 S5.1: Local Peer Discovery via mDNS +//! Implements a minimal mDNS advertiser and querier for _libertaria._udp.local + +const std = @import("std"); +const posix = std.posix; +const net = std.net; + +pub const ip_mreq = extern struct { + imr_multiaddr: u32, + imr_interface: u32, +}; + +pub const DiscoveryService = struct { + allocator: std.mem.Allocator, + fd: posix.socket_t, + port: u16, + + pub const MULTICAST_ADDR = "224.0.0.251"; + pub const MULTICAST_PORT = 5353; + + pub fn init(allocator: std.mem.Allocator, local_port: u16) !DiscoveryService { + // 1. Create UDP socket + const fd = try posix.socket( + posix.AF.INET, + posix.SOCK.DGRAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK, + posix.IPPROTO.UDP, + ); + errdefer posix.close(fd); + + // 2. Allow port reuse (standard for mDNS) + try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(i32, 1))); + + // 3. Bind to all interfaces on mDNS port + const bind_addr = try net.Address.parseIp("0.0.0.0", MULTICAST_PORT); + try posix.bind(fd, &bind_addr.any, bind_addr.getOsSockLen()); + + // 4. Join Multicast Group + const mcast_addr = try net.Address.parseIp(MULTICAST_ADDR, 0); + const mreq = ip_mreq{ + .imr_multiaddr = mcast_addr.in.sa.addr, + .imr_interface = 0, // Default interface + }; + try posix.setsockopt(fd, posix.IPPROTO.IP, std.os.linux.IP.ADD_MEMBERSHIP, &std.mem.toBytes(mreq)); + + return DiscoveryService{ + .allocator = allocator, + .fd = fd, + .port = local_port, + }; + } + + pub fn deinit(self: *DiscoveryService) void { + posix.close(self.fd); + } + + /// Broadcast a Libertaria service announcement + pub fn announce(self: *DiscoveryService) !void { + // Construct a minimal mDNS Answer packet (DNS response) + // PTR: _libertaria._udp.local -> ._libertaria._udp.local + + var buf: [512]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + + // 1. Transaction ID (0 for mDNS responses) + try writer.writeInt(u16, 0, .big); + // 2. Flags (0x8400: Response, Authoritative) + try writer.writeInt(u16, 0x8400, .big); + // 3. Question Count (0) + try writer.writeInt(u16, 0, .big); + // 4. Answer Record Count (1) + try writer.writeInt(u16, 1, .big); + // 5. Authority Record Count (0) + try writer.writeInt(u16, 0, .big); + // 6. Additional Record Count (0) + try writer.writeInt(u16, 0, .big); + + // 7. Answer: Name "_libertaria._udp.local" + try writeDnsName(writer, "_libertaria._udp.local"); + + // 8. Type PTR (12), Class IN (1) + try writer.writeInt(u16, 12, .big); + try writer.writeInt(u16, 1, .big); + + // 9. TTL (120s) + try writer.writeInt(u32, 120, .big); + + // 10. Data Length and RDATA + // For Week 28, just point back to the same name or a static ID stub + // Real logic will use ._libertaria._udp.local + const target = "node-id-placeholder._libertaria._udp.local"; + try writer.writeInt(u16, @intCast(getDnsNameLen(target)), .big); + try writeDnsName(writer, target); + + const dest = try net.Address.parseIp(MULTICAST_ADDR, MULTICAST_PORT); + _ = try posix.sendto(self.fd, fbs.getWritten(), 0, &dest.any, dest.getOsSockLen()); + } + + fn getDnsNameLen(name: []const u8) usize { + var count: usize = 1; // Final null + var it = std.mem.splitScalar(u8, name, '.'); + while (it.next()) |part| { + count += 1 + part.len; + } + return count; + } + + fn writeDnsName(writer: anytype, name: []const u8) !void { + var it = std.mem.splitScalar(u8, name, '.'); + while (it.next()) |part| { + try writer.writeByte(@intCast(part.len)); + try writer.writeAll(part); + } + try writer.writeByte(0); + } + + /// Query for other Libertaria nodes + pub fn query(self: *DiscoveryService) !void { + var buf: [512]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + + // 1. Transaction ID (0) + try writer.writeInt(u16, 0, .big); + // 2. Flags (0x0000: Standard Query) + try writer.writeInt(u16, 0x0000, .big); + // 3. Question Count (1) + try writer.writeInt(u16, 1, .big); + // 4. Answer Record Count (0) + try writer.writeInt(u16, 0, .big); + // 5. Authority Record Count (0) + try writer.writeInt(u16, 0, .big); + // 6. Additional Record Count (0) + try writer.writeInt(u16, 0, .big); + + // 7. Question: Name "_libertaria._udp.local" + try writeDnsName(writer, "_libertaria._udp.local"); + + // 8. Type PTR (12), Class IN (1) + try writer.writeInt(u16, 12, .big); + try writer.writeInt(u16, 1, .big); + + const dest = try net.Address.parseIp(MULTICAST_ADDR, MULTICAST_PORT); + _ = try posix.sendto(self.fd, fbs.getWritten(), 0, &dest.any, dest.getOsSockLen()); + } + + /// Parse an incoming mDNS packet and update the peer table + pub fn handlePacket(self: *DiscoveryService, peer_table: anytype, buf: []const u8, sender: net.Address) !void { + if (buf.len < 12) return; // Too small + + // Skip Header (12 bytes) + const answer_count = std.mem.readInt(u16, buf[6..8], .big); + if (answer_count == 0) return; + + // Skip Question section if any (simplified: we expect responses to our query or gratuitous responses) + // For local discovery, we mostly care about Answers. + + // This is a VERY MINIMAL parser for Week 28. + // It looks for the "_libertaria._udp.local" string and assumes the following PTR. + if (std.mem.indexOf(u8, buf, "_libertaria")) |_| { + // Found a Libertaria record! + // In a real implementation, we'd parse SRV/TXT for the actual port and DID. + // For MVP, if we receive a Libertaria-tagged packet, we trust the sender's IP. + // (Port is still tricky since discovery is on 5353 but service is on 8710). + + // TODO: Extract DID from TXT record + var mock_did = [_]u8{0} ** 8; + @memcpy(mock_did[0..4], "NODE"); + + // We assume the peer is running on its default port or we need SRV record. + // For now, use the sender's IP but the standard port. + var peer_addr = sender; + peer_addr.setPort(self.port); // Fallback to our configured port if unknown + + try peer_table.updatePeer(mock_did, peer_addr); + } + } +}; diff --git a/capsule-core/src/federation.zig b/capsule-core/src/federation.zig new file mode 100644 index 0000000..2a1d939 --- /dev/null +++ b/capsule-core/src/federation.zig @@ -0,0 +1,149 @@ +//! RFC-0120 S6: Federation Handshake +//! Handshake protocol for establishing identity and trust between Capsules. + +const std = @import("std"); +const net = std.net; +const lwf = @import("l0_transport"); + +pub const VERSION: u32 = 1; +pub const SERVICE_TYPE: u16 = lwf.LWFHeader.ServiceType.IDENTITY_SIGNAL; + +pub const DhtNode = struct { + id: [32]u8, + address: net.Address, +}; + +pub const SessionState = enum { + Connecting, // Discovered, TCP/UTCP handshake in progress + Authenticating, // Exchange DIDs and signatures + Federated, // Ready for mesh operations + Disconnected, +}; + +pub const FederationMessage = union(enum) { + hello: struct { + did_short: [8]u8, + version: u32, + }, + welcome: struct { + did_short: [8]u8, + }, + auth: struct { + signature: [64]u8, // Signature over nonce + }, + // DHT RPCs (ServiceType: IDENTITY_SIGNAL) + dht_ping: struct { + node_id: [32]u8, + }, + dht_pong: struct { + node_id: [32]u8, + }, + dht_find_node: struct { + target_id: [32]u8, + }, + dht_nodes: struct { + nodes: []const DhtNode, + }, + + pub fn encode(self: FederationMessage, writer: anytype) !void { + try writer.writeByte(@intFromEnum(self)); + switch (self) { + .hello => |h| { + try writer.writeAll(&h.did_short); + try writer.writeInt(u32, h.version, .big); + }, + .welcome => |w| { + try writer.writeAll(&w.did_short); + }, + .auth => |a| { + try writer.writeAll(&a.signature); + }, + .dht_ping => |p| { + try writer.writeAll(&p.node_id); + }, + .dht_pong => |p| { + try writer.writeAll(&p.node_id); + }, + .dht_find_node => |f| { + try writer.writeAll(&f.target_id); + }, + .dht_nodes => |n| { + try writer.writeInt(u16, @intCast(n.nodes.len), .big); + for (n.nodes) |node| { + try writer.writeAll(&node.id); + // For now we only support IPv4 in DHT nodes responses + if (node.address.any.family == std.posix.AF.INET) { + try writer.writeAll(&std.mem.toBytes(node.address.in.sa.addr)); + try writer.writeInt(u16, node.address.getPort(), .big); + } else { + return error.UnsupportedAddressFamily; + } + } + }, + } + } + + pub fn decode(reader: anytype, allocator: std.mem.Allocator) !FederationMessage { + const tag = try reader.readByte(); + return switch (@as(std.meta.Tag(FederationMessage), @enumFromInt(tag))) { + .hello => .{ + .hello = .{ + .did_short = try reader.readBytesNoEof(8), + .version = try reader.readInt(u32, .big), + }, + }, + .welcome => .{ + .welcome = .{ + .did_short = try reader.readBytesNoEof(8), + }, + }, + .auth => .{ + .auth = .{ + .signature = try reader.readBytesNoEof(64), + }, + }, + .dht_ping => .{ + .dht_ping = .{ + .node_id = try reader.readBytesNoEof(32), + }, + }, + .dht_pong => .{ + .dht_pong = .{ + .node_id = try reader.readBytesNoEof(32), + }, + }, + .dht_find_node => .{ + .dht_find_node = .{ + .target_id = try reader.readBytesNoEof(32), + }, + }, + .dht_nodes => { + const count = try reader.readInt(u16, .big); + const nodes = try allocator.alloc(DhtNode, count); + for (0..count) |i| { + const id = try reader.readBytesNoEof(32); + const addr_u32 = try reader.readInt(u32, @import("builtin").target.cpu.arch.endian()); + const port = try reader.readInt(u16, .big); + nodes[i] = .{ + .id = id, + .address = net.Address.initIp4(std.mem.toBytes(addr_u32), port), + }; + } + return .{ .dht_nodes = .{ .nodes = nodes } }; + }, + }; + } +}; + +pub const PeerSession = struct { + address: net.Address, + state: SessionState = .Connecting, + did_short: [8]u8, + + pub fn init(address: net.Address, did_short: [8]u8) PeerSession { + return .{ + .address = address, + .did_short = did_short, + }; + } +}; diff --git a/capsule-core/src/main.zig b/capsule-core/src/main.zig new file mode 100644 index 0000000..ef4b82b --- /dev/null +++ b/capsule-core/src/main.zig @@ -0,0 +1,43 @@ +//! Capsule Core Entry Point + +const std = @import("std"); +const node_mod = @import("node.zig"); +const config_mod = @import("config.zig"); + +pub fn main() !void { + // Setup allocator + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Setup logging (default to info) + // std.log is configured via root declarations in build.zig usually, or std options. + + // Parse args (Minimal for Week 27) + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + if (args.len > 1 and std.mem.eql(u8, args[1], "version")) { + std.debug.print("Libertaria Capsule v0.1.0 (Shield)\n", .{}); + return; + } + + // Load Config + // Check for config.json, otherwise use default + const config_path = "config.json"; + var config = config_mod.NodeConfig.loadFromJsonFile(allocator, config_path) catch |err| { + std.log.err("Failed to load configuration: {}", .{err}); + return err; + }; + defer config.deinit(allocator); + + // Initialize Node + const node = try node_mod.CapsuleNode.init(allocator, config); + defer node.deinit(); + + // Setup signal handler for clean shutdown (Ctrl+C) + // (Zig std doesn't have cross-platform signal handling yet, assume simplified loop for now) + + // Run Node + try node.start(); +} diff --git a/capsule-core/src/node.zig b/capsule-core/src/node.zig new file mode 100644 index 0000000..e8904f1 --- /dev/null +++ b/capsule-core/src/node.zig @@ -0,0 +1,382 @@ +//! Capsule Node Orchestrator +//! Binds L0 (Transport) and L1 (Identity) into a sovereign event loop. + +const std = @import("std"); +const config_mod = @import("config.zig"); +const l0 = @import("l0_transport"); +// access UTCP from l0 or utcp module directly +// build.zig imports "utcp" into capsule +const utcp_mod = @import("utcp"); +// l1_identity module +const l1 = @import("l1_identity"); +// qvl module +const qvl = @import("qvl"); + +const discovery_mod = @import("discovery.zig"); +const peer_table_mod = @import("peer_table.zig"); +const fed = @import("federation.zig"); +const dht_mod = @import("dht.zig"); +const storage_mod = @import("storage.zig"); + +const NodeConfig = config_mod.NodeConfig; +const UTCP = utcp_mod.UTCP; +const RiskGraph = qvl.types.RiskGraph; +const DiscoveryService = discovery_mod.DiscoveryService; +const PeerTable = peer_table_mod.PeerTable; +const PeerSession = fed.PeerSession; +const DhtService = dht_mod.DhtService; +const StorageService = storage_mod.StorageService; + +pub const AddressContext = struct { + pub fn hash(self: AddressContext, s: std.net.Address) u64 { + _ = self; + var h = std.hash.Wyhash.init(0); + const bytes = @as([*]const u8, @ptrCast(&s.any))[0..s.getOsSockLen()]; + h.update(bytes); + return h.final(); + } + pub fn eql(self: AddressContext, a: std.net.Address, b: std.net.Address) bool { + _ = self; + return a.eql(b); + } +}; + +pub const CapsuleNode = struct { + allocator: std.mem.Allocator, + config: NodeConfig, + + // Subsystems + utcp: UTCP, + risk_graph: RiskGraph, + discovery: DiscoveryService, + peer_table: PeerTable, + sessions: std.HashMap(std.net.Address, PeerSession, AddressContext, std.hash_map.default_max_load_percentage), + dht: DhtService, + storage: *StorageService, + + running: bool, + + pub fn init(allocator: std.mem.Allocator, config: NodeConfig) !*CapsuleNode { + const self = try allocator.create(CapsuleNode); + + // Ensure data directory exists + std.fs.cwd().makePath(config.data_dir) catch |err| { + if (err != error.PathAlreadyExists) return err; + }; + + // Initialize L0 (UTCP) + const address = try std.net.Address.parseIp("0.0.0.0", config.port); + const utcp_instance = try UTCP.init(allocator, address); + + // Initialize L1 (RiskGraph) + const risk_graph = RiskGraph.init(allocator); + + // Initialize Discovery (mDNS) + const discovery = try DiscoveryService.init(allocator, config.port); + + // Initialize DHT + var node_id: dht_mod.NodeId = [_]u8{0} ** 32; + // TODO: Generate real NodeID from Public Key + std.mem.copyForwards(u8, node_id[0..4], "NODE"); + + // Initialize Storage + var db_path_buf: [256]u8 = undefined; + const db_path = try std.fmt.bufPrint(&db_path_buf, "{s}/capsule.db", .{config.data_dir}); + const storage = try StorageService.init(allocator, db_path); + + self.* = CapsuleNode{ + .allocator = allocator, + .config = config, + .utcp = utcp_instance, + .risk_graph = risk_graph, + .discovery = discovery, + .peer_table = PeerTable.init(allocator), + .sessions = std.HashMap(std.net.Address, PeerSession, AddressContext, std.hash_map.default_max_load_percentage).init(allocator), + .dht = DhtService.init(allocator, node_id), + .storage = storage, + .running = false, + }; + + // Pre-populate from storage + const stored_peers = try storage.loadPeers(allocator); + defer allocator.free(stored_peers); + for (stored_peers) |peer| { + try self.dht.routing_table.update(peer); + } + + return self; + } + + pub fn deinit(self: *CapsuleNode) void { + self.utcp.deinit(); + self.risk_graph.deinit(); + self.discovery.deinit(); + self.peer_table.deinit(); + self.sessions.deinit(); + self.dht.deinit(); + self.storage.deinit(); + self.allocator.destroy(self); + } + + pub fn start(self: *CapsuleNode) !void { + self.running = true; + std.log.info("CapsuleNode starting on port {d}...", .{self.config.port}); + std.log.info("Data directory: {s}", .{self.config.data_dir}); + + // Setup polling + var poll_fds = [_]std.posix.pollfd{ + .{ + .fd = self.utcp.fd, + .events = std.posix.POLL.IN, + .revents = 0, + }, + .{ + .fd = self.discovery.fd, + .events = std.posix.POLL.IN, + .revents = 0, + }, + }; + + const TICK_MS = 100; // 10Hz tick rate + var last_tick = std.time.milliTimestamp(); + var discovery_timer: usize = 0; + var dht_timer: usize = 0; + + while (self.running) { + const ready_count = try std.posix.poll(&poll_fds, TICK_MS); + + if (ready_count > 0) { + // 1. UTCP Traffic + if (poll_fds[0].revents & std.posix.POLL.IN != 0) { + var buf: [1500]u8 = undefined; + if (self.utcp.receiveFrame(self.allocator, &buf)) |result| { + var frame = result.frame; + defer frame.deinit(self.allocator); + + if (frame.header.service_type == fed.SERVICE_TYPE) { + try self.handleFederationMessage(result.sender, frame); + } + } else |err| { + if (err != error.WouldBlock) std.log.warn("UTCP receive error: {}", .{err}); + } + } + + // 2. Discovery Traffic + if (poll_fds[1].revents & std.posix.POLL.IN != 0) { + var m_buf: [2048]u8 = undefined; + var src_addr: std.posix.sockaddr = undefined; + var src_len: std.posix.socklen_t = @sizeOf(std.posix.sockaddr); + const bytes = std.posix.recvfrom(self.discovery.fd, &m_buf, 0, &src_addr, &src_len) catch |err| blk: { + if (err != error.WouldBlock) std.log.warn("Discovery recv error: {}", .{err}); + break :blk @as(usize, 0); + }; + if (bytes > 0) { + try self.discovery.handlePacket(&self.peer_table, m_buf[0..bytes], std.net.Address{ .any = src_addr }); + } + } + } + + // 3. Periodic Ticks + const now = std.time.milliTimestamp(); + if (now - last_tick >= TICK_MS) { + try self.tick(); + last_tick = now; + + // Discovery cycle (every ~5s) + discovery_timer += 1; + if (discovery_timer >= 50) { + self.discovery.announce() catch {}; + self.discovery.query() catch {}; + discovery_timer = 0; + } + + // DHT refresh (every ~60s) + dht_timer += 1; + if (dht_timer >= 600) { + try self.bootstrap(); + dht_timer = 0; + } + } + } + } + + pub fn bootstrap(self: *CapsuleNode) !void { + std.log.info("DHT: Refreshing routing table...", .{}); + // Start self-lookup to fill buckets + // For now, just ping federated sessions + var it = self.sessions.iterator(); + while (it.next()) |entry| { + if (entry.value_ptr.state == .Federated) { + try self.sendFederationMessage(entry.key_ptr.*, .{ + .dht_find_node = .{ .target_id = self.dht.routing_table.self_id }, + }); + } + } + } + + fn tick(self: *CapsuleNode) !void { + self.peer_table.tick(); + + // Initiate handshakes with discovered active peers + self.peer_table.mutex.lock(); + defer self.peer_table.mutex.unlock(); + + var it = self.peer_table.peers.iterator(); + while (it.next()) |entry| { + if (entry.value_ptr.is_active and !self.sessions.contains(entry.value_ptr.address)) { + try self.connectToPeer(entry.value_ptr.address, entry.key_ptr.*); + } + } + } + + pub fn stop(self: *CapsuleNode) void { + self.running = false; + } + + pub fn updateRoutingTable(self: *CapsuleNode, node: storage_mod.RemoteNode) !void { + try self.dht.routing_table.update(node); + // Persist to SQLite + self.storage.savePeer(node) catch |err| { + std.log.warn("SQLite: Failed to save peer {any}: {}", .{ node.id[0..4], err }); + }; + } + + fn handleFederationMessage(self: *CapsuleNode, sender: std.net.Address, frame: l0.LWFFrame) !void { + var fbs = std.io.fixedBufferStream(frame.payload); + const msg = fed.FederationMessage.decode(fbs.reader(), self.allocator) catch |err| { + std.log.warn("Failed to decode federation message from {f}: {}", .{ sender, err }); + return; + }; + + switch (msg) { + .hello => |h| { + std.log.info("Received HELLO from {f} (ID: {x})", .{ sender, h.did_short }); + // If we don't have a session, create one and reply WELCOME + if (!self.sessions.contains(sender)) { + try self.sessions.put(sender, PeerSession.init(sender, h.did_short)); + } + + // Reply WELCOME + const reply = fed.FederationMessage{ + .welcome = .{ .did_short = [_]u8{0} ** 8 }, // TODO: Real DID + }; + try self.sendFederationMessage(sender, reply); + }, + .welcome => |w| { + std.log.info("Received WELCOME from {f} (ID: {x})", .{ sender, w.did_short }); + if (self.sessions.getPtr(sender)) |session| { + session.state = .Federated; // In Week 28 we skip AUTH for stubbing + std.log.info("Node {f} is now FEDERATED", .{sender}); + + // After federation, also ping to join DHT + try self.sendFederationMessage(sender, .{ + .dht_ping = .{ .node_id = self.dht.routing_table.self_id }, + }); + } + }, + .auth => |a| { + _ = a; + // Handled in Week 29 + }, + .dht_ping => |p| { + std.log.debug("DHT: PING from {f}", .{sender}); + // Update routing table + try self.updateRoutingTable(.{ + .id = p.node_id, + .address = sender, + .last_seen = std.time.milliTimestamp(), + }); + // Reply PONG + try self.sendFederationMessage(sender, .{ + .dht_pong = .{ .node_id = self.dht.routing_table.self_id }, + }); + }, + .dht_pong => |p| { + std.log.debug("DHT: PONG from {f}", .{sender}); + try self.updateRoutingTable(.{ + .id = p.node_id, + .address = sender, + .last_seen = std.time.milliTimestamp(), + }); + }, + .dht_find_node => |f| { + std.log.debug("DHT: FIND_NODE from {f}", .{sender}); + const closest = try self.dht.routing_table.findClosest(f.target_id, 20); + defer self.allocator.free(closest); + + // Convert to federation nodes + var nodes = try self.allocator.alloc(fed.DhtNode, closest.len); + for (closest, 0..) |node, i| { + nodes[i] = .{ .id = node.id, .address = node.address }; + } + + try self.sendFederationMessage(sender, .{ + .dht_nodes = .{ .nodes = nodes }, + }); + self.allocator.free(nodes); + }, + .dht_nodes => |n| { + std.log.debug("DHT: Received {d} nodes from {f}", .{ n.nodes.len, sender }); + for (n.nodes) |node| { + // Update routing table with discovered nodes + try self.updateRoutingTable(.{ + .id = node.id, + .address = node.address, + .last_seen = std.time.milliTimestamp(), + }); + // TODO: If this was part of a findNode lookup, update the lookup state + } + self.allocator.free(n.nodes); + }, + } + } + + fn sendFederationMessage(self: *CapsuleNode, target: std.net.Address, msg: fed.FederationMessage) !void { + var enc_buf: [128]u8 = undefined; + var fbs = std.io.fixedBufferStream(&enc_buf); + try msg.encode(fbs.writer()); + const payload = fbs.getWritten(); + + var frame = try l0.LWFFrame.init(self.allocator, payload.len); + defer frame.deinit(self.allocator); + + frame.header.service_type = fed.SERVICE_TYPE; + frame.header.payload_len = @intCast(payload.len); + @memcpy(frame.payload, payload); + frame.updateChecksum(); + + try self.utcp.sendFrame(target, &frame, self.allocator); + } + + /// Initiate connection to a discovered peer + pub fn connectToPeer(self: *CapsuleNode, address: std.net.Address, did_short: [8]u8) !void { + if (self.sessions.contains(address)) return; + + std.log.info("Initiating federation handshake with {f} (ID: {x})", .{ address, did_short }); + try self.sessions.put(address, PeerSession.init(address, did_short)); + + // Send HELLO + const msg = fed.FederationMessage{ + .hello = .{ + .did_short = [_]u8{0} ** 8, // TODO: Use real DID hash + .version = fed.VERSION, + }, + }; + + var enc_buf: [128]u8 = undefined; + var fbs = std.io.fixedBufferStream(&enc_buf); + try msg.encode(fbs.writer()); + const payload = fbs.getWritten(); + + // Wrap in LWF + var frame = try l0.LWFFrame.init(self.allocator, payload.len); + defer frame.deinit(self.allocator); + + frame.header.service_type = fed.SERVICE_TYPE; + frame.header.payload_len = @intCast(payload.len); + @memcpy(frame.payload, payload); + frame.updateChecksum(); + + try self.utcp.sendFrame(address, &frame, self.allocator); + } +}; diff --git a/capsule-core/src/peer_table.zig b/capsule-core/src/peer_table.zig new file mode 100644 index 0000000..555eb1e --- /dev/null +++ b/capsule-core/src/peer_table.zig @@ -0,0 +1,70 @@ +//! RFC-0120 S5.2: Peer Table +//! Manages a list of known nodes on the network and their health/trust metrics. + +const std = @import("std"); +const net = std.net; + +pub const Peer = struct { + address: net.Address, + did_short: [8]u8, // Short hash of DID (RFC-0120 S4.1) + last_seen: i64, + trust_score: f32 = 1.0, + is_active: bool = true, +}; + +pub const PeerTable = struct { + allocator: std.mem.Allocator, + peers: std.AutoHashMap([8]u8, Peer), + mutex: std.Thread.Mutex, + + pub fn init(allocator: std.mem.Allocator) PeerTable { + return PeerTable{ + .allocator = allocator, + .peers = std.AutoHashMap([8]u8, Peer).init(allocator), + .mutex = .{}, + }; + } + + pub fn deinit(self: *PeerTable) void { + self.peers.deinit(); + } + + /// Update or add a peer to the table + pub fn updatePeer(self: *PeerTable, did_short: [8]u8, address: net.Address) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + + const now = std.time.timestamp(); + if (self.peers.getPtr(did_short)) |peer| { + peer.address = address; + peer.last_seen = now; + peer.is_active = true; + } else { + try self.peers.put(did_short, Peer{ + .address = address, + .did_short = did_short, + .last_seen = now, + }); + std.log.info("Discovered new peer: {x} at {f}", .{ did_short, address }); + } + } + + /// Mark peers as inactive if not seen for a while (Decay) + pub fn tick(self: *PeerTable) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + const now = std.time.timestamp(); + const timeout = 300; // 5 minutes + + var it = self.peers.iterator(); + while (it.next()) |entry| { + if (now - entry.value_ptr.last_seen > timeout) { + if (entry.value_ptr.is_active) { + entry.value_ptr.is_active = false; + std.log.debug("Peer timed out: {x}", .{entry.key_ptr.*}); + } + } + } + } +}; diff --git a/capsule-core/src/storage.zig b/capsule-core/src/storage.zig new file mode 100644 index 0000000..ef70167 --- /dev/null +++ b/capsule-core/src/storage.zig @@ -0,0 +1,134 @@ +//! Persistent Storage Service for Capsule Core +//! Wraps SQLite to store peer discovery data and QVL trust graph. + +const std = @import("std"); +const c = @cImport({ + @cInclude("sqlite3.h"); +}); +const dht = @import("dht.zig"); + +pub const RemoteNode = dht.RemoteNode; +pub const ID_LEN = dht.ID_LEN; + +pub const StorageError = error{ + DbOpenFailed, + ExecFailed, + PrepareFailed, + StepFailed, +}; + +pub const StorageService = struct { + db: ?*c.sqlite3 = null, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator, db_path: []const u8) !*StorageService { + const self = try allocator.create(StorageService); + self.* = .{ + .allocator = allocator, + .db = null, + }; + + const db_path_c = try allocator.dupeZ(u8, db_path); + defer allocator.free(db_path_c); + + if (c.sqlite3_open(db_path_c, &self.db) != c.SQLITE_OK) { + std.log.err("SQLite: Failed to open database {s}: {s}", .{ db_path, c.sqlite3_errmsg(self.db) }); + return error.DbOpenFailed; + } + + try self.initSchema(); + std.log.info("SQLite: Database initialized at {s}", .{db_path}); + + return self; + } + + pub fn deinit(self: *StorageService) void { + if (self.db) |db| { + _ = c.sqlite3_close(db); + } + self.allocator.destroy(self); + } + + fn initSchema(self: *StorageService) !void { + const sql = + \\ PRAGMA journal_mode = WAL; + \\ CREATE TABLE IF NOT EXISTS peers ( + \\ id BLOB PRIMARY KEY, + \\ address TEXT NOT NULL, + \\ last_seen INTEGER NOT NULL, + \\ seen_count INTEGER DEFAULT 1 + \\ ); + \\ CREATE TABLE IF NOT EXISTS qvl_nodes ( + \\ did BLOB PRIMARY KEY, + \\ trust_score REAL DEFAULT 0.0 + \\ ); + \\ CREATE TABLE IF NOT EXISTS qvl_edges ( + \\ source BLOB, + \\ target BLOB, + \\ weight REAL, + \\ PRIMARY KEY(source, target) + \\ ); + ; + + var err_msg: [*c]u8 = null; + if (c.sqlite3_exec(self.db, sql, null, null, &err_msg) != c.SQLITE_OK) { + std.log.err("SQLite: Schema init failed: {s}", .{err_msg}); + c.sqlite3_free(err_msg); + return error.ExecFailed; + } + } + + pub fn savePeer(self: *StorageService, node: RemoteNode) !void { + const sql = "INSERT INTO peers (id, address, last_seen) VALUES (?, ?, ?) " ++ + "ON CONFLICT(id) DO UPDATE SET address=excluded.address, last_seen=excluded.last_seen, seen_count=seen_count+1;"; + + var stmt: ?*c.sqlite3_stmt = null; + if (c.sqlite3_prepare_v2(self.db, sql, -1, &stmt, null) != c.SQLITE_OK) return error.PrepareFailed; + defer _ = c.sqlite3_finalize(stmt); + + // Bind ID + _ = c.sqlite3_bind_blob(stmt, 1, &node.id, @intCast(node.id.len), null); + + // Bind Address + var addr_buf: [64]u8 = undefined; + const addr_str = try std.fmt.bufPrintZ(&addr_buf, "{any}", .{node.address}); + _ = c.sqlite3_bind_text(stmt, 2, addr_str.ptr, -1, null); + + // Bind Last Seen + _ = c.sqlite3_bind_int64(stmt, 3, node.last_seen); + + if (c.sqlite3_step(stmt) != c.SQLITE_DONE) return error.StepFailed; + } + + pub fn loadPeers(self: *StorageService, allocator: std.mem.Allocator) ![]RemoteNode { + const sql = "SELECT id, address, last_seen FROM peers;"; + var stmt: ?*c.sqlite3_stmt = null; + if (c.sqlite3_prepare_v2(self.db, sql, -1, &stmt, null) != c.SQLITE_OK) return error.PrepareFailed; + defer _ = c.sqlite3_finalize(stmt); + + var list = std.ArrayList(RemoteNode){}; + defer list.deinit(allocator); + + while (c.sqlite3_step(stmt) == c.SQLITE_ROW) { + const id_ptr = c.sqlite3_column_blob(stmt, 0); + const id_len = c.sqlite3_column_bytes(stmt, 0); + const addr_ptr = c.sqlite3_column_text(stmt, 1); + const last_seen = c.sqlite3_column_int64(stmt, 2); + + if (id_len != ID_LEN) continue; + + var node: RemoteNode = undefined; + @memcpy(&node.id, @as([*]const u8, @ptrCast(id_ptr))[0..ID_LEN]); + + const addr_str = std.mem.span(addr_ptr); + node.address = try std.net.Address.parseIp(addr_str, 0); // Port logic handled via federation later + node.last_seen = last_seen; + + try list.append(allocator, node); + } + + const out = try allocator.alloc(RemoteNode, list.items.len); + @memcpy(out, list.items); + return out; + } +}; diff --git a/l1-identity/proof_of_path.zig b/l1-identity/proof_of_path.zig index be4eb25..6b89869 100644 --- a/l1-identity/proof_of_path.zig +++ b/l1-identity/proof_of_path.zig @@ -16,9 +16,9 @@ //! ] const std = @import("std"); -const trust_graph = @import("trust_graph.zig"); +const trust_graph = @import("trust_graph"); const time = @import("time"); -const soulkey = @import("soulkey.zig"); +const soulkey = @import("soulkey"); pub const PathVerdict = enum { /// Path is valid and active diff --git a/l1-identity/qvl/pop_integration.zig b/l1-identity/qvl/pop_integration.zig index 00584d0..e248caf 100644 --- a/l1-identity/qvl/pop_integration.zig +++ b/l1-identity/qvl/pop_integration.zig @@ -12,7 +12,7 @@ const std = @import("std"); const types = @import("types.zig"); const pathfinding = @import("pathfinding.zig"); const pop = @import("../proof_of_path.zig"); -const trust_graph = @import("../trust_graph.zig"); +const trust_graph = @import("trust_graph"); const NodeId = types.NodeId; const RiskGraph = types.RiskGraph; diff --git a/l1-identity/qvl_ffi.zig b/l1-identity/qvl_ffi.zig index f897ea4..25fb867 100644 --- a/l1-identity/qvl_ffi.zig +++ b/l1-identity/qvl_ffi.zig @@ -11,7 +11,7 @@ const std = @import("std"); const qvl = @import("qvl.zig"); const pop_mod = @import("proof_of_path.zig"); -const trust_graph = @import("trust_graph.zig"); +const trust_graph = @import("trust_graph"); const time = @import("time"); const slash = @import("slash"); diff --git a/l1-identity/trust_graph.zig b/l1-identity/trust_graph.zig index 174d429..e5f3d70 100644 --- a/l1-identity/trust_graph.zig +++ b/l1-identity/trust_graph.zig @@ -12,8 +12,8 @@ //! Memory budget: 100K nodes = 400KB (vs 6.4MB with raw DIDs) const std = @import("std"); -const soulkey = @import("soulkey.zig"); -const crypto = @import("crypto.zig"); +const soulkey = @import("soulkey"); +const crypto = @import("crypto"); /// Trust visibility levels (privacy control) /// Per RFC-0120 S4.3.1: Alice never broadcasts her full Trust DAG diff --git a/l1-identity/vector.zig b/l1-identity/vector.zig index 5e3bcde..0311001 100644 --- a/l1-identity/vector.zig +++ b/l1-identity/vector.zig @@ -19,9 +19,9 @@ const std = @import("std"); const time = @import("time"); const proof_of_path = @import("proof_of_path.zig"); -const soulkey = @import("soulkey.zig"); +const soulkey = @import("soulkey"); const entropy = @import("entropy.zig"); -const trust_graph = @import("trust_graph.zig"); +const trust_graph = @import("trust_graph"); /// Vector Type (RFC-0120 S4.2) pub const VectorType = enum(u16) {