feat(capsule): implement discovery, federation, and persistence (Phase 10)
This commit is contained in:
parent
8cb89065bd
commit
4498da5ce6
|
|
@ -26,6 +26,11 @@ STORIES/
|
||||||
logs/
|
logs/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Operational Data
|
||||||
|
data/
|
||||||
|
config.json
|
||||||
|
capsule.log
|
||||||
|
|
||||||
# Editor & OS
|
# Editor & OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
**The Core Protocol Stack for Libertaria Applications**
|
**The Core Protocol Stack for Libertaria Applications**
|
||||||
|
|
||||||
**Version:** 1.0.0-beta ("Shield")
|
**Version:** 1.0.0-beta ("Shield")
|
||||||
**License:** TBD
|
**License:** LUL-1.0
|
||||||
**Status:** 🛡️ **AUTONOMOUS IMMUNE RESPONSE: OPERATIONAL** (100% Complete)
|
**Status:** 🛡️ **AUTONOMOUS IMMUNE RESPONSE: OPERATIONAL** (100% Complete)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -84,5 +84,4 @@ cargo test --test simulation_attack -- --nocapture
|
||||||
---
|
---
|
||||||
|
|
||||||
**Mission Accomplished.**
|
**Mission Accomplished.**
|
||||||
Markus Maiwald & Voxis Forge.
|
Markus Maiwald & Voxis Forge. 2026.
|
||||||
2026.
|
|
||||||
|
|
|
||||||
153
build.zig
153
build.zig
|
|
@ -4,6 +4,15 @@ pub fn build(b: *std.Build) void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
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
|
// L0: Transport Layer
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
@ -144,6 +153,17 @@ pub fn build(b: *std.Build) void {
|
||||||
utcp_mod.addImport("quarantine", l0_quarantine_mod);
|
utcp_mod.addImport("quarantine", l0_quarantine_mod);
|
||||||
l0_service_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
|
// L1 QVL (Quasar Vector Lattice) - Advanced Graph Engine
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
@ -152,6 +172,28 @@ pub fn build(b: *std.Build) void {
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.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)
|
// 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);
|
const run_l0_quarantine_tests = b.addRunArtifact(l0_quarantine_tests);
|
||||||
|
|
||||||
// Import PQXDH into main L1 module
|
// Import PQXDH into main L1 module
|
||||||
|
|
||||||
// Tests (root is test_pqxdh.zig)
|
// Tests (root is test_pqxdh.zig)
|
||||||
const l1_pqxdh_tests_mod = b.createModule(.{
|
const l1_pqxdh_tests_mod = b.createModule(.{
|
||||||
.root_source_file = b.path("l1-identity/test_pqxdh.zig"),
|
.root_source_file = b.path("l1-identity/test_pqxdh.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.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(.{
|
const l1_pqxdh_tests = b.addTest(.{
|
||||||
.root_module = l1_pqxdh_tests_mod,
|
.root_module = l1_pqxdh_tests_mod,
|
||||||
|
|
@ -275,16 +311,6 @@ pub fn build(b: *std.Build) void {
|
||||||
l1_pqxdh_tests.linkSystemLibrary("oqs");
|
l1_pqxdh_tests.linkSystemLibrary("oqs");
|
||||||
const run_l1_pqxdh_tests = b.addRunArtifact(l1_pqxdh_tests);
|
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)
|
// L1 Vector tests (Phase 3C)
|
||||||
const l1_vector_mod = b.createModule(.{
|
const l1_vector_mod = b.createModule(.{
|
||||||
.root_source_file = b.path("l1-identity/vector.zig"),
|
.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("time", time_mod);
|
||||||
l1_vector_mod.addImport("pqxdh", l1_pqxdh_mod);
|
l1_vector_mod.addImport("pqxdh", l1_pqxdh_mod);
|
||||||
// QVL also needs time (via proof_of_path.zig dependency)
|
l1_vector_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);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
const l1_vector_tests = b.addTest(.{
|
const l1_vector_tests = b.addTest(.{
|
||||||
.root_module = l1_vector_mod,
|
.root_module = l1_vector_mod,
|
||||||
|
|
@ -339,7 +345,21 @@ pub fn build(b: *std.Build) void {
|
||||||
l1_vector_tests.linkLibC();
|
l1_vector_tests.linkLibC();
|
||||||
const run_l1_vector_tests = b.addRunArtifact(l1_vector_tests);
|
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)
|
// Test step (runs Phase 2B + 2C + 2D + 3C SDK tests)
|
||||||
const test_step = b.step("test", "Run 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_utcp_tests.step);
|
||||||
test_step.dependOn(&run_opq_tests.step);
|
test_step.dependOn(&run_opq_tests.step);
|
||||||
test_step.dependOn(&run_l0_service_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);
|
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);
|
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
|
// Examples
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
@ -417,13 +421,44 @@ pub fn build(b: *std.Build) void {
|
||||||
// Convenience Commands
|
// 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
|
// Run crypto example
|
||||||
const run_crypto_example = b.addRunArtifact(crypto_example);
|
const run_crypto_example = b.addRunArtifact(crypto_example);
|
||||||
const run_crypto_step = b.step("run-crypto", "Run encryption example");
|
const run_crypto_step = b.step("run-crypto", "Run encryption example");
|
||||||
run_crypto_step.dependOn(&run_crypto_example.step);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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 -> <short_did>._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 <short_did>._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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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.*});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
//! ]
|
//! ]
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const trust_graph = @import("trust_graph.zig");
|
const trust_graph = @import("trust_graph");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
const soulkey = @import("soulkey.zig");
|
const soulkey = @import("soulkey");
|
||||||
|
|
||||||
pub const PathVerdict = enum {
|
pub const PathVerdict = enum {
|
||||||
/// Path is valid and active
|
/// Path is valid and active
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const std = @import("std");
|
||||||
const types = @import("types.zig");
|
const types = @import("types.zig");
|
||||||
const pathfinding = @import("pathfinding.zig");
|
const pathfinding = @import("pathfinding.zig");
|
||||||
const pop = @import("../proof_of_path.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 NodeId = types.NodeId;
|
||||||
const RiskGraph = types.RiskGraph;
|
const RiskGraph = types.RiskGraph;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const qvl = @import("qvl.zig");
|
const qvl = @import("qvl.zig");
|
||||||
const pop_mod = @import("proof_of_path.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 time = @import("time");
|
||||||
const slash = @import("slash");
|
const slash = @import("slash");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
//! Memory budget: 100K nodes = 400KB (vs 6.4MB with raw DIDs)
|
//! Memory budget: 100K nodes = 400KB (vs 6.4MB with raw DIDs)
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const soulkey = @import("soulkey.zig");
|
const soulkey = @import("soulkey");
|
||||||
const crypto = @import("crypto.zig");
|
const crypto = @import("crypto");
|
||||||
|
|
||||||
/// Trust visibility levels (privacy control)
|
/// Trust visibility levels (privacy control)
|
||||||
/// Per RFC-0120 S4.3.1: Alice never broadcasts her full Trust DAG
|
/// Per RFC-0120 S4.3.1: Alice never broadcasts her full Trust DAG
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
const proof_of_path = @import("proof_of_path.zig");
|
const proof_of_path = @import("proof_of_path.zig");
|
||||||
const soulkey = @import("soulkey.zig");
|
const soulkey = @import("soulkey");
|
||||||
const entropy = @import("entropy.zig");
|
const entropy = @import("entropy.zig");
|
||||||
const trust_graph = @import("trust_graph.zig");
|
const trust_graph = @import("trust_graph");
|
||||||
|
|
||||||
/// Vector Type (RFC-0120 S4.2)
|
/// Vector Type (RFC-0120 S4.2)
|
||||||
pub const VectorType = enum(u16) {
|
pub const VectorType = enum(u16) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue