feat(capsule): implement discovery, federation, and persistence (Phase 10)

This commit is contained in:
Markus Maiwald 2026-01-31 08:35:22 +01:00
parent 8cb89065bd
commit 4498da5ce6
16 changed files with 1296 additions and 70 deletions

5
.gitignore vendored
View File

@ -26,6 +26,11 @@ STORIES/
logs/
*.log
# Operational Data
data/
config.json
capsule.log
# Editor & OS
.DS_Store
.idea/

View File

@ -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.

153
build.zig
View File

@ -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);
}

View File

@ -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);
}
};

140
capsule-core/src/dht.zig Normal file
View File

@ -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();
}
};

View File

@ -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);
}
}
};

View File

@ -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,
};
}
};

43
capsule-core/src/main.zig Normal file
View File

@ -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();
}

382
capsule-core/src/node.zig Normal file
View File

@ -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);
}
};

View File

@ -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.*});
}
}
}
}
};

View File

@ -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;
}
};

View File

@ -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

View File

@ -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;

View File

@ -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");

View File

@ -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

View File

@ -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) {