libertaria-stack/l1-identity/trust_graph.zig

575 lines
18 KiB
Zig

//! Quasar Vector Lattice (QVL) - Trust Graph Engine
//!
//! RFC-0120: Compact Trust Graph Implementation
//!
//! This module implements the foundational trust DAG for Libertaria.
//! Optimized for Kenya Rule compliance:
//! - u32 node indices instead of 64-byte DIDs
//! - 5-byte packed edge weights
//! - O(1) direct trust lookup
//! - O(depth) Proof-of-Path verification
//!
//! 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");
/// Trust visibility levels (privacy control)
/// Per RFC-0120 S4.3.1: Alice never broadcasts her full Trust DAG
pub const TrustVisibility = enum(u8) {
/// Only I can see this edge (default)
private = 0,
/// The trustee can see I trust them
bilateral = 1,
/// Anyone in my trust graph can see this edge
friends = 2,
/// Public: helps routing but leaks metadata
/// USE SPARINGLY - only for public figures/services
public = 3,
};
/// Trust level controlling transitive depth
pub const TrustLevel = enum(u8) {
/// Direct trust only (no transitivity)
direct = 0,
/// Trust their direct contacts
one_hop = 1,
/// Trust contacts of contacts
two_hop = 2,
/// Default maximum (RFC-0010 Membrane Agent)
full = 3,
};
/// Compact edge weight: 5 bytes vs ~100+ bytes
/// Per RFC-0120 S4.3.2
pub const TrustEdge = packed struct {
/// Target node index
target_idx: u32,
/// Trust level (controls transitive depth)
level: TrustLevel,
/// Unix timestamp expiration (fine until 2106)
expires_at: u32,
/// Visibility setting (privacy control)
visibility: TrustVisibility,
pub const SERIALIZED_SIZE = 10;
pub fn isExpired(self: TrustEdge, current_time: u64) bool {
if (self.expires_at == 0) return false; // No expiration
return current_time > @as(u64, self.expires_at);
}
pub fn serialize(self: TrustEdge) [SERIALIZED_SIZE]u8 {
var buf: [SERIALIZED_SIZE]u8 = undefined;
std.mem.writeInt(u32, buf[0..4], self.target_idx, .little);
buf[4] = @intFromEnum(self.level);
std.mem.writeInt(u32, buf[5..9], self.expires_at, .little);
buf[9] = @intFromEnum(self.visibility);
return buf;
}
pub fn deserialize(data: *const [SERIALIZED_SIZE]u8) TrustEdge {
return TrustEdge{
.target_idx = std.mem.readInt(u32, data[0..4], .little),
.level = @enumFromInt(data[4]),
.expires_at = std.mem.readInt(u32, data[5..9], .little),
.visibility = @enumFromInt(data[9]),
};
}
};
/// Edge list type (managed ArrayList)
const EdgeList = std.ArrayListUnmanaged(TrustEdge);
/// Compact trust graph optimized for mobile RAM
/// Per RFC-0120 S4.3.2
pub const CompactTrustGraph = struct {
/// Map DID hash (first 4 bytes) → node index
/// Collision handling: full DID stored in did_storage
node_map: std.AutoHashMap(u32, u32),
/// Adjacency list: each node has list of outgoing edges
adjacency: std.ArrayListUnmanaged(EdgeList),
/// DID storage for reverse lookup (32 bytes each)
did_storage: std.ArrayListUnmanaged([32]u8),
/// Root node index (my identity)
root_idx: u32,
/// Configuration
config: Config,
/// Allocator
allocator: std.mem.Allocator,
pub const Config = struct {
/// Maximum trust depth allowed
max_trust_depth: u8 = 3,
/// Maximum nodes to store (Kenya constraint)
max_nodes: u32 = 10_000,
/// Maximum edges per node
max_edges_per_node: u32 = 100,
};
pub const Error = error{
NodeLimitExceeded,
EdgeLimitExceeded,
NodeNotFound,
SelfTrustNotAllowed,
DuplicateEdge,
OutOfMemory,
};
/// Initialize a new trust graph with the given root DID
pub fn init(allocator: std.mem.Allocator, root_did: [32]u8, config: Config) Error!CompactTrustGraph {
var self = CompactTrustGraph{
.node_map = std.AutoHashMap(u32, u32).init(allocator),
.adjacency = .{},
.did_storage = .{},
.root_idx = 0,
.config = config,
.allocator = allocator,
};
// Insert root node
_ = try self.getOrInsertNode(root_did);
return self;
}
pub fn deinit(self: *CompactTrustGraph) void {
for (self.adjacency.items) |*adj| {
adj.deinit(self.allocator);
}
self.adjacency.deinit(self.allocator);
self.did_storage.deinit(self.allocator);
self.node_map.deinit();
}
/// Get or create node index for a DID
pub fn getOrInsertNode(self: *CompactTrustGraph, did: [32]u8) Error!u32 {
// Hash DID to u32 for map lookup
const did_hash = hashDid(did);
if (self.node_map.get(did_hash)) |idx| {
// Verify it's the same DID (handle collisions)
if (std.mem.eql(u8, &self.did_storage.items[idx], &did)) {
return idx;
}
// Collision: linear probe (rare case)
// For simplicity, just use sequential index
}
// Check limit
if (self.did_storage.items.len >= self.config.max_nodes) {
return Error.NodeLimitExceeded;
}
// Create new node
const idx: u32 = @intCast(self.did_storage.items.len);
self.did_storage.append(self.allocator, did) catch return Error.OutOfMemory;
self.adjacency.append(self.allocator, .{}) catch return Error.OutOfMemory;
self.node_map.put(did_hash, idx) catch return Error.OutOfMemory;
return idx;
}
/// Get node index for a DID (returns null if not found)
pub fn getNode(self: *const CompactTrustGraph, did: [32]u8) ?u32 {
const did_hash = hashDid(did);
if (self.node_map.get(did_hash)) |idx| {
if (std.mem.eql(u8, &self.did_storage.items[idx], &did)) {
return idx;
}
}
return null;
}
/// Get DID for a node index
pub fn getDid(self: *const CompactTrustGraph, idx: u32) ?[32]u8 {
if (idx >= self.did_storage.items.len) return null;
return self.did_storage.items[idx];
}
/// Check direct trust: O(E) where E is edges for truster
/// In practice, E << 100. so effectively O(1)
pub fn hasDirectTrust(self: *const CompactTrustGraph, truster_idx: u32, trustee_idx: u32) bool {
if (truster_idx >= self.adjacency.items.len) return false;
const edges = self.adjacency.items[truster_idx].items;
for (edges) |edge| {
if (edge.target_idx == trustee_idx) {
return true;
}
}
return false;
}
/// Check direct trust by DID
pub fn hasDirectTrustByDid(self: *const CompactTrustGraph, truster: [32]u8, trustee: [32]u8) bool {
const truster_idx = self.getNode(truster) orelse return false;
const trustee_idx = self.getNode(trustee) orelse return false;
return self.hasDirectTrust(truster_idx, trustee_idx);
}
/// Grant trust from root to target DID
pub fn grantTrust(
self: *CompactTrustGraph,
target_did: [32]u8,
level: TrustLevel,
visibility: TrustVisibility,
expires_at: u32,
) Error!void {
const target_idx = try self.getOrInsertNode(target_did);
if (target_idx == self.root_idx) {
return Error.SelfTrustNotAllowed;
}
// Check if edge already exists
var edges = &self.adjacency.items[self.root_idx];
for (edges.items) |*edge| {
if (edge.target_idx == target_idx) {
// Update existing edge
edge.level = level;
edge.visibility = visibility;
edge.expires_at = expires_at;
return;
}
}
// Check edge limit
if (edges.items.len >= self.config.max_edges_per_node) {
return Error.EdgeLimitExceeded;
}
// Add new edge
edges.append(self.allocator, TrustEdge{
.target_idx = target_idx,
.level = level,
.visibility = visibility,
.expires_at = expires_at,
}) catch return Error.OutOfMemory;
}
/// Revoke trust from root to target DID
pub fn revokeTrust(self: *CompactTrustGraph, target_did: [32]u8) Error!void {
const target_idx = self.getNode(target_did) orelse return Error.NodeNotFound;
var edges = &self.adjacency.items[self.root_idx];
var i: usize = 0;
while (i < edges.items.len) {
if (edges.items[i].target_idx == target_idx) {
_ = edges.swapRemove(i);
return;
}
i += 1;
}
}
/// Get trust edge from root to target (if exists)
pub fn getTrustEdge(self: *const CompactTrustGraph, target_did: [32]u8) ?TrustEdge {
const target_idx = self.getNode(target_did) orelse return null;
const edges = self.adjacency.items[self.root_idx].items;
for (edges) |edge| {
if (edge.target_idx == target_idx) {
return edge;
}
}
return null;
}
/// BFS path finding (sender-side only)
/// Returns path as list of node indices, or null if no path exists
pub fn findPath(
self: *const CompactTrustGraph,
from_did: [32]u8,
to_did: [32]u8,
) ?[]u32 {
const from_idx = self.getNode(from_did) orelse return null;
const to_idx = self.getNode(to_did) orelse return null;
if (from_idx == to_idx) {
// Same node - return single element path
var path = self.allocator.alloc(u32, 1) catch return null;
path[0] = from_idx;
return path;
}
// BFS with parent tracking
var visited = std.AutoHashMap(u32, u32).init(self.allocator);
defer visited.deinit();
var queue: std.ArrayListUnmanaged(u32) = .{};
defer queue.deinit(self.allocator);
queue.append(self.allocator, from_idx) catch return null;
visited.put(from_idx, from_idx) catch return null; // Mark start
while (queue.items.len > 0) {
const current = queue.orderedRemove(0);
if (current >= self.adjacency.items.len) continue;
for (self.adjacency.items[current].items) |edge| {
if (visited.contains(edge.target_idx)) continue;
visited.put(edge.target_idx, current) catch return null;
if (edge.target_idx == to_idx) {
// Found! Reconstruct path
return self.reconstructPath(visited, from_idx, to_idx);
}
// Check depth limit
const depth = self.pathDepth(visited, edge.target_idx, from_idx);
if (depth < self.config.max_trust_depth) {
queue.append(self.allocator, edge.target_idx) catch return null;
}
}
}
return null; // No path found
}
fn reconstructPath(
self: *const CompactTrustGraph,
parents: std.AutoHashMap(u32, u32),
from_idx: u32,
to_idx: u32,
) ?[]u32 {
// Count path length
var length: usize = 1;
var current = to_idx;
while (current != from_idx) {
current = parents.get(current) orelse return null;
length += 1;
if (length > self.config.max_trust_depth + 1) return null; // Safety
}
// Allocate and fill path
var path = self.allocator.alloc(u32, length) catch return null;
current = to_idx;
var i: usize = length;
while (i > 0) {
i -= 1;
path[i] = current;
if (current == from_idx) break;
current = parents.get(current) orelse {
self.allocator.free(path);
return null;
};
}
return path;
}
fn pathDepth(
self: *const CompactTrustGraph,
parents: std.AutoHashMap(u32, u32),
node: u32,
start: u32,
) u8 {
_ = self;
var depth: u8 = 0;
var current = node;
while (current != start and depth < 255) {
current = parents.get(current) orelse break;
depth += 1;
}
return depth;
}
/// Count total nodes in graph
pub fn nodeCount(self: *const CompactTrustGraph) usize {
return self.did_storage.items.len;
}
/// Count total edges from root
pub fn rootEdgeCount(self: *const CompactTrustGraph) usize {
if (self.root_idx >= self.adjacency.items.len) return 0;
return self.adjacency.items[self.root_idx].items.len;
}
/// Get all direct trustees (nodes I trust)
pub fn getDirectTrustees(self: *const CompactTrustGraph) []const TrustEdge {
if (self.root_idx >= self.adjacency.items.len) return &[_]TrustEdge{};
return self.adjacency.items[self.root_idx].items;
}
/// Hash DID to u32 for map key
fn hashDid(did: [32]u8) u32 {
// Use first 4 bytes as hash (collision handled by full DID comparison)
return std.mem.readInt(u32, did[0..4], .little);
}
};
// ============================================================================
// TESTS
// ============================================================================
test "CompactTrustGraph: init and basic operations" {
const allocator = std.testing.allocator;
var root_did: [32]u8 = undefined;
@memset(&root_did, 0x01);
var graph = try CompactTrustGraph.init(allocator, root_did, .{});
defer graph.deinit();
// Root should be node 0
try std.testing.expectEqual(@as(u32, 0), graph.root_idx);
try std.testing.expectEqual(@as(usize, 1), graph.nodeCount());
}
test "CompactTrustGraph: grant and revoke trust" {
const allocator = std.testing.allocator;
var root_did: [32]u8 = undefined;
@memset(&root_did, 0x01);
var target_did: [32]u8 = undefined;
@memset(&target_did, 0x02);
var graph = try CompactTrustGraph.init(allocator, root_did, .{});
defer graph.deinit();
// Grant trust
try graph.grantTrust(target_did, .full, .bilateral, 0);
try std.testing.expectEqual(@as(usize, 2), graph.nodeCount());
try std.testing.expectEqual(@as(usize, 1), graph.rootEdgeCount());
try std.testing.expect(graph.hasDirectTrustByDid(root_did, target_did));
// Revoke trust
try graph.revokeTrust(target_did);
try std.testing.expectEqual(@as(usize, 0), graph.rootEdgeCount());
try std.testing.expect(!graph.hasDirectTrustByDid(root_did, target_did));
}
test "CompactTrustGraph: find path" {
const allocator = std.testing.allocator;
// Create chain: A -> B -> C
var did_a: [32]u8 = undefined;
@memset(&did_a, 0x0A);
var did_b: [32]u8 = undefined;
@memset(&did_b, 0x0B);
var did_c: [32]u8 = undefined;
@memset(&did_c, 0x0C);
var graph = try CompactTrustGraph.init(allocator, did_a, .{});
defer graph.deinit();
// A trusts B
try graph.grantTrust(did_b, .full, .bilateral, 0);
// Manually add B -> C edge
const b_idx = graph.getNode(did_b).?;
const c_idx = try graph.getOrInsertNode(did_c);
try graph.adjacency.items[b_idx].append(allocator, TrustEdge{
.target_idx = c_idx,
.level = .full,
.visibility = .bilateral,
.expires_at = 0,
});
// Find path A -> C
const path = graph.findPath(did_a, did_c);
try std.testing.expect(path != null);
defer allocator.free(path.?);
try std.testing.expectEqual(@as(usize, 3), path.?.len);
try std.testing.expectEqual(@as(u32, 0), path.?[0]); // A
try std.testing.expectEqual(@as(u32, 1), path.?[1]); // B
try std.testing.expectEqual(@as(u32, 2), path.?[2]); // C
}
test "CompactTrustGraph: self trust not allowed" {
const allocator = std.testing.allocator;
var root_did: [32]u8 = undefined;
@memset(&root_did, 0x01);
var graph = try CompactTrustGraph.init(allocator, root_did, .{});
defer graph.deinit();
// Try to trust self
const result = graph.grantTrust(root_did, .full, .bilateral, 0);
try std.testing.expectError(CompactTrustGraph.Error.SelfTrustNotAllowed, result);
}
test "CompactTrustGraph: node limit respected" {
const allocator = std.testing.allocator;
var root_did: [32]u8 = undefined;
@memset(&root_did, 0x01);
var graph = try CompactTrustGraph.init(allocator, root_did, .{ .max_nodes = 3 });
defer graph.deinit();
var did2: [32]u8 = undefined;
@memset(&did2, 0x02);
try graph.grantTrust(did2, .full, .bilateral, 0);
var did3: [32]u8 = undefined;
@memset(&did3, 0x03);
try graph.grantTrust(did3, .full, .bilateral, 0);
// Should fail - at limit
var did4: [32]u8 = undefined;
@memset(&did4, 0x04);
const result = graph.grantTrust(did4, .full, .bilateral, 0);
try std.testing.expectError(CompactTrustGraph.Error.NodeLimitExceeded, result);
}
test "TrustEdge: serialization roundtrip" {
const edge = TrustEdge{
.target_idx = 12345,
.level = .two_hop,
.expires_at = 1706652000,
.visibility = .friends,
};
const serialized = edge.serialize();
const deserialized = TrustEdge.deserialize(&serialized);
try std.testing.expectEqual(edge.target_idx, deserialized.target_idx);
try std.testing.expectEqual(edge.level, deserialized.level);
try std.testing.expectEqual(edge.expires_at, deserialized.expires_at);
try std.testing.expectEqual(edge.visibility, deserialized.visibility);
}
test "TrustEdge: expiration check" {
const edge = TrustEdge{
.target_idx = 1,
.level = .full,
.expires_at = 1706652000, // Some timestamp
.visibility = .bilateral,
};
// Before expiration
try std.testing.expect(!edge.isExpired(1706651999));
// After expiration
try std.testing.expect(edge.isExpired(1706652001));
// No expiration (0)
const no_expire = TrustEdge{
.target_idx = 1,
.level = .full,
.expires_at = 0,
.visibility = .bilateral,
};
try std.testing.expect(!no_expire.isExpired(9999999999));
}