diff --git a/build.zig b/build.zig index 6fc33cb..61e3b48 100644 --- a/build.zig +++ b/build.zig @@ -255,6 +255,17 @@ 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("time", time_mod); const l1_vector_tests = b.addTest(.{ .root_module = l1_vector_mod, @@ -304,6 +315,17 @@ pub fn build(b: *std.Build) void { 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 // ======================================================================== diff --git a/l1-identity/qvl.h b/l1-identity/qvl.h new file mode 100644 index 0000000..6013814 --- /dev/null +++ b/l1-identity/qvl.h @@ -0,0 +1,189 @@ +/** + * QVL C API - Quasar Vector Lattice Trust Substrate + * + * C ABI for L1 identity/trust layer. Enables Rust Membrane Agents + * and other C-compatible languages to consume QVL functions. + * + * Thread Safety: Single-threaded only (initial version) + * Memory Management: Caller owns context via qvl_init/qvl_deinit + */ + +#ifndef QVL_H +#define QVL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* ======================================================================== + * OPAQUE CONTEXT + * ======================================================================== */ + +/** + * Opaque handle for QVL context + * Contains: risk graph, reputation map, trust graph + */ +typedef struct QvlContext QvlContext; + +/* ======================================================================== + * ENUMS + * ======================================================================== */ + +/** + * Proof-of-Path verification verdict + */ +typedef enum { + QVL_POP_VALID = 0, /**< Path is valid */ + QVL_POP_INVALID_ENDPOINTS = 1, /**< Sender/receiver mismatch */ + QVL_POP_BROKEN_LINK = 2, /**< Missing trust edge in path */ + QVL_POP_REVOKED = 3, /**< Trust edge was revoked */ + QVL_POP_REPLAY = 4 /**< Replay attack detected */ +} QvlPopVerdict; + +/** + * Anomaly detection reason + */ +typedef enum { + QVL_ANOMALY_NONE = 0, /**< No anomaly */ + QVL_ANOMALY_NEGATIVE_CYCLE = 1, /**< Bellman-Ford negative cycle */ + QVL_ANOMALY_LOW_COVERAGE = 2, /**< Gossip partition detected */ + QVL_ANOMALY_BP_DIVERGENCE = 3 /**< Belief Propagation divergence */ +} QvlAnomalyReason; + +/* ======================================================================== + * STRUCTS + * ======================================================================== */ + +/** + * Anomaly score from betrayal detection + */ +typedef struct { + uint32_t node; /**< Node ID flagged */ + double score; /**< 0.0-1.0 (0.9+ = critical) */ + uint8_t reason; /**< QvlAnomalyReason enum */ +} QvlAnomalyScore; + +/** + * Risk edge for graph mutations + */ +typedef struct { + uint32_t from; /**< Source node ID */ + uint32_t to; /**< Target node ID */ + double risk; /**< -1.0 to 1.0 (negative = betrayal) */ + uint64_t timestamp_ns; /**< Nanoseconds since epoch */ + uint64_t nonce; /**< L0 sequence for path provenance */ + uint8_t level; /**< Trust level 0-3 */ + uint64_t expires_at_ns; /**< Expiration timestamp (ns) */ +} QvlRiskEdge; + +/* ======================================================================== + * CONTEXT MANAGEMENT + * ======================================================================== */ + +/** + * Initialize QVL context + * + * @return Opaque context handle, or NULL on allocation failure + */ +QvlContext* qvl_init(void); + +/** + * Cleanup and free QVL context + * + * @param ctx Context to destroy (NULL-safe) + */ +void qvl_deinit(QvlContext* ctx); + +/* ======================================================================== + * TRUST SCORING + * ======================================================================== */ + +/** + * Get trust score for a DID + * + * @param ctx QVL context + * @param did 32-byte DID + * @param did_len Length of DID (must be 32) + * @return Trust score 0.0-1.0, or -1.0 on error + */ +double qvl_get_trust_score( + QvlContext* ctx, + const uint8_t* did, + size_t did_len +); + +/** + * Get reputation score for a node ID + * + * @param ctx QVL context + * @param node_id Node identifier + * @return Reputation score 0.0-1.0, or -1.0 on error + */ +double qvl_get_reputation(QvlContext* ctx, uint32_t node_id); + +/* ======================================================================== + * PROOF-OF-PATH + * ======================================================================== */ + +/** + * Verify a serialized Proof-of-Path + * + * @param ctx QVL context + * @param proof_bytes Serialized proof data + * @param proof_len Length of proof bytes + * @param sender_did 32-byte sender DID + * @param receiver_did 32-byte receiver DID + * @return Verification verdict (QvlPopVerdict) + */ +QvlPopVerdict qvl_verify_pop( + QvlContext* ctx, + const uint8_t* proof_bytes, + size_t proof_len, + const uint8_t* sender_did, + const uint8_t* receiver_did +); + +/* ======================================================================== + * BETRAYAL DETECTION + * ======================================================================== */ + +/** + * Run Bellman-Ford betrayal detection from source node + * + * @param ctx QVL context + * @param source_node Starting node for detection + * @return Anomaly score (0.0 = clean, 0.9+ = critical) + */ +QvlAnomalyScore qvl_detect_betrayal(QvlContext* ctx, uint32_t source_node); + +/* ======================================================================== + * GRAPH MUTATIONS + * ======================================================================== */ + +/** + * Add trust edge to risk graph + * + * @param ctx QVL context + * @param edge Edge to add + * @return 0 on success, non-zero on error + */ +int qvl_add_trust_edge(QvlContext* ctx, const QvlRiskEdge* edge); + +/** + * Revoke trust edge + * + * @param ctx QVL context + * @param from Source node ID + * @param to Target node ID + * @return 0 on success, -2 if not found + */ +int qvl_revoke_trust_edge(QvlContext* ctx, uint32_t from, uint32_t to); + +#ifdef __cplusplus +} +#endif + +#endif /* QVL_H */ diff --git a/l1-identity/qvl.zig b/l1-identity/qvl.zig index 5d752b4..0d2b48a 100644 --- a/l1-identity/qvl.zig +++ b/l1-identity/qvl.zig @@ -13,6 +13,7 @@ pub const betrayal = @import("qvl/betrayal.zig"); pub const pathfinding = @import("qvl/pathfinding.zig"); pub const gossip = @import("qvl/gossip.zig"); pub const inference = @import("qvl/inference.zig"); +pub const pop = @import("qvl/pop_integration.zig"); pub const RiskEdge = types.RiskEdge; pub const NodeId = types.NodeId; diff --git a/l1-identity/qvl/betrayal.zig b/l1-identity/qvl/betrayal.zig index 92243aa..e7b6b75 100644 --- a/l1-identity/qvl/betrayal.zig +++ b/l1-identity/qvl/betrayal.zig @@ -8,6 +8,7 @@ //! Complexity: O(|V| × |E|) with early exit optimization. const std = @import("std"); +const time = @import("time"); const types = @import("types.zig"); const NodeId = types.NodeId; @@ -209,8 +210,8 @@ test "Bellman-Ford: No betrayal in clean graph" { try graph.addNode(1); try graph.addNode(2); - try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.5, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.3, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); + try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.5, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.3, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); var result = try detectBetrayal(&graph, 0, allocator); defer result.deinit(); @@ -230,9 +231,9 @@ test "Bellman-Ford: Detect negative cycle (betrayal ring)" { try graph.addNode(1); try graph.addNode(2); - try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.2, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.2, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 2, .to = 0, .risk = -0.8, .entropy_stamp = 0, .level = 1, .expires_at = 0 }); // Betrayal! + try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.2, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.2, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 2, .to = 0, .risk = -0.8, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 1, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); // Betrayal! var result = try detectBetrayal(&graph, 0, allocator); defer result.deinit(); @@ -252,11 +253,11 @@ test "Bellman-Ford: Sybil ring detection (5-node cartel)" { } // Each edge: 0.1 vouch, but one edge -0.6 betrayal - try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.1, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.1, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 2, .to = 3, .risk = 0.1, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 3, .to = 4, .risk = 0.1, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 4, .to = 0, .risk = -0.6, .entropy_stamp = 0, .level = 1, .expires_at = 0 }); // Betrayal closes ring + try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.1, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.1, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 2, .to = 3, .risk = 0.1, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 3, .to = 4, .risk = 0.1, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 4, .to = 0, .risk = -0.6, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 1, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); // Betrayal closes ring var result = try detectBetrayal(&graph, 0, allocator); defer result.deinit(); diff --git a/l1-identity/qvl/inference.zig b/l1-identity/qvl/inference.zig index 3d8b927..0f471f2 100644 --- a/l1-identity/qvl/inference.zig +++ b/l1-identity/qvl/inference.zig @@ -9,6 +9,7 @@ //! until convergence (delta < epsilon). Output: per-node anomaly scores. const std = @import("std"); +const time = @import("time"); const types = @import("types.zig"); const NodeId = types.NodeId; @@ -228,8 +229,8 @@ test "BP: Converges on clean graph" { try graph.addNode(1); try graph.addNode(2); - try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.8, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.7, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); + try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.8, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.7, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); var result = try runInference(&graph, .{}, allocator); defer result.deinit(); @@ -248,9 +249,9 @@ test "BP: Detects suspicious node" { try graph.addNode(1); try graph.addNode(2); - try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.9, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 0, .to = 2, .risk = -0.5, .entropy_stamp = 0, .level = 1, .expires_at = 0 }); // Betrayal - try graph.addEdge(.{ .from = 1, .to = 2, .risk = -0.3, .entropy_stamp = 0, .level = 1, .expires_at = 0 }); // Betrayal + try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.9, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 0, .to = 2, .risk = -0.5, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 1, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); // Betrayal + try graph.addEdge(.{ .from = 1, .to = 2, .risk = -0.3, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 1, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); // Betrayal var result = try runInference(&graph, .{ .max_iterations = 50 }, allocator); defer result.deinit(); diff --git a/l1-identity/qvl/pathfinding.zig b/l1-identity/qvl/pathfinding.zig index a8a8619..3b3e58c 100644 --- a/l1-identity/qvl/pathfinding.zig +++ b/l1-identity/qvl/pathfinding.zig @@ -7,6 +7,7 @@ //! Complexity: O(|E| + |V| log |V|) with binary heap. const std = @import("std"); +const time = @import("time"); const types = @import("types.zig"); const NodeId = types.NodeId; @@ -185,8 +186,8 @@ test "A* Pathfinding: Direct path" { try graph.addNode(1); try graph.addNode(2); - try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.3, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.2, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); + try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.3, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.2, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); const dummy_ctx: u8 = 0; var result = try findTrustPath(&graph, 0, 2, zeroHeuristic, @ptrCast(&dummy_ctx), allocator); @@ -243,9 +244,9 @@ test "A* Pathfinding: Multiple paths, chooses shortest" { try graph.addNode(1); try graph.addNode(2); - try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.4, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.4, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 0, .to = 2, .risk = 0.5, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); // Direct shorter + try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.4, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 1, .to = 2, .risk = 0.4, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); + try graph.addEdge(.{ .from = 0, .to = 2, .risk = 0.5, .timestamp = time.SovereignTimestamp.fromSeconds(0, .system_boot), .nonce = 0, .level = 3, .expires_at = time.SovereignTimestamp.fromSeconds(0, .system_boot) }); // Direct shorter const dummy_ctx: u8 = 0; var result = try findTrustPath(&graph, 0, 2, zeroHeuristic, @ptrCast(&dummy_ctx), allocator); diff --git a/l1-identity/qvl/pop_integration.zig b/l1-identity/qvl/pop_integration.zig new file mode 100644 index 0000000..00584d0 --- /dev/null +++ b/l1-identity/qvl/pop_integration.zig @@ -0,0 +1,305 @@ +//! QVL + Proof-of-Path Integration +//! +//! Bridges the existing `proof_of_path.zig` (Phase 3C) with the QVL graph engine. +//! Enables: +//! - Reputation scoring from PoP verification +//! - PoP-guided A* heuristic (prefer paths with proven trust) +//! - Real-time trust decay on PoP failures +//! +//! This is where PoP + Reputation become L1's "magic". + +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 NodeId = types.NodeId; +const RiskGraph = types.RiskGraph; +const RiskEdge = types.RiskEdge; +const ProofOfPath = pop.ProofOfPath; +const PathVerdict = pop.PathVerdict; + +/// Reputation score derived from PoP verification. +/// Range: [0.0, 1.0] +/// - 1.0 = Perfect PoP verification history +/// - 0.5 = Neutral (new node, no history) +/// - 0.0 = Consistent PoP failures (likely adversarial) +pub const ReputationScore = struct { + node: NodeId, + score: f64, + /// Total PoP verifications attempted + total_checks: u32, + /// Successful verifications + successful_checks: u32, + /// Last verified timestamp (entropy stamp) + last_verified: u64, + + pub fn init(node: NodeId) ReputationScore { + return .{ + .node = node, + .score = 0.5, // Neutral default + .total_checks = 0, + .successful_checks = 0, + .last_verified = 0, + }; + } + + /// Update reputation after a PoP verification attempt. + pub fn update(self: *ReputationScore, verdict: PathVerdict, entropy_stamp: u64) void { + self.total_checks += 1; + if (verdict == .valid) { + self.successful_checks += 1; + self.last_verified = entropy_stamp; + } + + // Bayesian update: score = successful / total (with prior weighting) + const success_rate = @as(f64, @floatFromInt(self.successful_checks)) / + @as(f64, @floatFromInt(self.total_checks)); + + // Apply damping to prevent extreme swings on single failures + const damping = 0.7; + self.score = damping * self.score + (1.0 - damping) * success_rate; + + // Clamp to [0, 1] + self.score = @max(0.0, @min(1.0, self.score)); + } + + /// Decay reputation over time if no recent verifications. + pub fn decay(self: *ReputationScore, current_entropy: u64, half_life_ns: u64) void { + const time_since = current_entropy - self.last_verified; + if (time_since == 0) return; + + // Exponential decay: score *= 0.5^(time_since / half_life) + const decay_factor = std.math.pow( + f64, + 0.5, + @as(f64, @floatFromInt(time_since)) / @as(f64, @floatFromInt(half_life_ns)), + ); + self.score *= decay_factor; + self.score = @max(0.0, self.score); + } +}; + +/// Reputation map for all nodes in the graph. +pub const ReputationMap = struct { + allocator: std.mem.Allocator, + scores: std.AutoHashMapUnmanaged(NodeId, ReputationScore), + /// Default half-life: 7 days in nanoseconds + decay_half_life: u64 = 7 * 24 * 3600 * 1_000_000_000, + + pub fn init(allocator: std.mem.Allocator) ReputationMap { + return .{ + .allocator = allocator, + .scores = .{}, + }; + } + + pub fn deinit(self: *ReputationMap) void { + self.scores.deinit(self.allocator); + } + + /// Get reputation score for a node (default: 0.5 if unknown). + pub fn get(self: *const ReputationMap, node: NodeId) f64 { + if (self.scores.get(node)) |score| { + return score.score; + } + return 0.5; // Neutral for unknown nodes + } + + /// Record a PoP verification result. + pub fn recordVerification( + self: *ReputationMap, + node: NodeId, + verdict: PathVerdict, + entropy_stamp: u64, + ) !void { + var entry = try self.scores.getOrPut(self.allocator, node); + if (!entry.found_existing) { + entry.value_ptr.* = ReputationScore.init(node); + } + entry.value_ptr.update(verdict, entropy_stamp); + } + + /// Decay all reputations based on current time. + pub fn applyDecay(self: *ReputationMap, current_entropy: u64) void { + var it = self.scores.iterator(); + while (it.next()) |entry| { + entry.value_ptr.decay(current_entropy, self.decay_half_life); + } + } + + /// Get all nodes with reputation below threshold. + pub fn getLowReputationNodes( + self: *const ReputationMap, + threshold: f64, + allocator: std.mem.Allocator, + ) ![]NodeId { + var result = std.ArrayListUnmanaged(NodeId){}; + + var it = self.scores.iterator(); + while (it.next()) |entry| { + if (entry.value_ptr.score < threshold) { + try result.append(allocator, entry.key_ptr.*); + } + } + + return result.toOwnedSlice(allocator); + } +}; + +/// PoP-aware A* heuristic. +/// Prioritizes paths through high-reputation nodes. +pub fn popReputationHeuristic( + node: NodeId, + target: NodeId, + context: *const anyopaque, +) f64 { + const rep_map: *const ReputationMap = @ptrCast(@alignCast(context)); + + // Base heuristic: assume 1 hop remaining + const base_cost = 1.0; + + // Reputation penalty: low reputation = higher cost + const rep = rep_map.get(node); + const rep_penalty = (1.0 - rep) * 2.0; // Max penalty: 2.0 for rep=0 + + _ = target; // Not used in admissible heuristic + return base_cost + rep_penalty; +} + +/// Verify a PoP and update reputation scores. +pub fn verifyAndUpdateReputation( + proof: *const ProofOfPath, + expected_receiver: [32]u8, + expected_sender: [32]u8, + graph: *const trust_graph.CompactTrustGraph, + rep_map: *ReputationMap, + current_entropy: u64, +) PathVerdict { + const verdict = proof.verify(expected_receiver, expected_sender, graph); + + // Update reputation for the sender + // (In a full impl, we'd extract NodeId from DID) + // For now, use a hash of the sender DID as NodeId + var hasher = std.hash.Wyhash.init(0); + hasher.update(&expected_sender); + const sender_id: NodeId = @truncate(hasher.final()); + + rep_map.recordVerification(sender_id, verdict, current_entropy) catch { + // If allocation fails, degrade gracefully (skip reputation update) + }; + + return verdict; +} + +/// Initialize RiskGraph edges with reputation-weighted risks. +pub fn populateRiskFromReputation( + risk_graph: *RiskGraph, + trust_compact: *const trust_graph.CompactTrustGraph, + rep_map: *const ReputationMap, +) !void { + // For each edge in the CompactTrustGraph, add to RiskGraph with risk = (1 - reputation) + const edges = trust_compact.getAllEdges(); + + for (edges) |edge| { + // Extract NodeIds (would use actual DID->NodeId mapping in full impl) + var hasher = std.hash.Wyhash.init(0); + hasher.update(&edge.did); + const to_id: NodeId = @truncate(hasher.final()); + + // Compute risk from reputation + const rep = rep_map.get(to_id); + const risk = 1.0 - rep; // High rep = low risk + + const risk_edge = RiskEdge{ + .from = 0, // Would map from trust_compact.root_idx + .to = to_id, + .risk = risk, + .entropy_stamp = 0, // Would extract from edge metadata + .level = edge.level, + .expires_at = edge.expires_at orelse 0, + }; + + try risk_graph.addEdge(risk_edge); + } +} + +// ============================================================================ +// TESTS +// ============================================================================ + +test "ReputationScore: initial neutral score" { + const score = ReputationScore.init(42); + try std.testing.expectEqual(score.score, 0.5); + try std.testing.expectEqual(score.total_checks, 0); +} + +test "ReputationScore: successful verifications increase score" { + var score = ReputationScore.init(42); + + score.update(.valid, 1000); + try std.testing.expect(score.score > 0.5); + + score.update(.valid, 2000); + score.update(.valid, 3000); + try std.testing.expect(score.score > 0.75); // Damping prevents rapid convergence +} + +test "ReputationScore: failed verifications decrease score" { + var score = ReputationScore.init(42); + + score.update(.broken_link, 1000); + try std.testing.expect(score.score < 0.5); + + score.update(.broken_link, 2000); + try std.testing.expect(score.score < 0.3); +} + +test "ReputationScore: decay over time" { + var score = ReputationScore.init(42); + score.update(.valid, 1000); + const initial = score.score; + + // Decay after half-life + const half_life: u64 = 1000; + score.decay(1000 + half_life, half_life); + + try std.testing.expect(score.score < initial); + try std.testing.expectApproxEqAbs(score.score, initial * 0.5, 0.1); +} + +test "ReputationMap: get unknown node" { + const allocator = std.testing.allocator; + var rep_map = ReputationMap.init(allocator); + defer rep_map.deinit(); + + const score = rep_map.get(999); + try std.testing.expectEqual(score, 0.5); // Default neutral +} + +test "ReputationMap: record verification" { + const allocator = std.testing.allocator; + var rep_map = ReputationMap.init(allocator); + defer rep_map.deinit(); + + try rep_map.recordVerification(42, .valid, 1000); + const score = rep_map.get(42); + try std.testing.expect(score > 0.5); +} + +test "ReputationMap: low reputation nodes" { + const allocator = std.testing.allocator; + var rep_map = ReputationMap.init(allocator); + defer rep_map.deinit(); + + try rep_map.recordVerification(1, .valid, 1000); + try rep_map.recordVerification(2, .broken_link, 1000); + try rep_map.recordVerification(2, .broken_link, 2000); + + const low_rep = try rep_map.getLowReputationNodes(0.4, allocator); + defer allocator.free(low_rep); + + try std.testing.expectEqual(low_rep.len, 1); + try std.testing.expectEqual(low_rep[0], 2); +} diff --git a/l1-identity/qvl/types.zig b/l1-identity/qvl/types.zig index c977198..9a6f4a5 100644 --- a/l1-identity/qvl/types.zig +++ b/l1-identity/qvl/types.zig @@ -3,6 +3,9 @@ //! Extends RFC-0120 TrustEdge with risk scoring for Bellman-Ford. const std = @import("std"); +const time = @import("time"); + +const SovereignTimestamp = time.SovereignTimestamp; /// Node identifier (compact u32 index into DID storage) pub const NodeId = u32; @@ -21,20 +24,22 @@ pub const RiskEdge = struct { /// 0.0 = Neutral/unknown /// +1.0 = Maximum trust risk: f64, - /// Entropy stamp for temporal anchoring (RFC-0100) - entropy_stamp: u64, + /// Temporal anchor for graph ordering (attosecond precision) + timestamp: SovereignTimestamp, + /// Nonce for path provenance (L0 sequence tied to trust transition) + /// Enables: replay protection, exact path reconstruction, routing verification + nonce: u64, /// Original trust level (for path verification) level: u8, /// Expiration timestamp - expires_at: u32, + expires_at: SovereignTimestamp, pub fn isBetrayal(self: RiskEdge) bool { return self.risk < 0.0; } - pub fn isExpired(self: RiskEdge, current_time: u64) bool { - if (self.expires_at == 0) return false; - return current_time > @as(u64, self.expires_at); + pub fn isExpired(self: RiskEdge, current_time: SovereignTimestamp) bool { + return current_time.isAfter(self.expires_at); } }; @@ -134,8 +139,9 @@ test "RiskGraph: basic operations" { try graph.addNode(1); try graph.addNode(2); - try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.5, .entropy_stamp = 0, .level = 3, .expires_at = 0 }); - try graph.addEdge(.{ .from = 1, .to = 2, .risk = -0.3, .entropy_stamp = 0, .level = 2, .expires_at = 0 }); // Betrayal + const ts = SovereignTimestamp.fromSeconds(0, .system_boot); + try graph.addEdge(.{ .from = 0, .to = 1, .risk = 0.5, .timestamp = ts, .nonce = 0, .level = 3, .expires_at = ts }); + try graph.addEdge(.{ .from = 1, .to = 2, .risk = -0.3, .timestamp = ts, .nonce = 1, .level = 2, .expires_at = ts }); // Betrayal try std.testing.expectEqual(graph.nodeCount(), 3); try std.testing.expectEqual(graph.edgeCount(), 2); diff --git a/l1-identity/qvl_ffi.zig b/l1-identity/qvl_ffi.zig new file mode 100644 index 0000000..d7f5b02 --- /dev/null +++ b/l1-identity/qvl_ffi.zig @@ -0,0 +1,297 @@ +//! QVL FFI - C ABI Exports for L2 Integration +//! +//! Provides C-compatible interface for: +//! - Trust scoring +//! - Proof-of-Path verification +//! - Betrayal detection (Bellman-Ford) +//! - Graph mutations +//! +//! Thread Safety: Single-threaded only (initial version) + +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 time = @import("time"); + +const RiskGraph = qvl.types.RiskGraph; +const RiskEdge = qvl.types.RiskEdge; +const ReputationMap = qvl.pop.ReputationMap; +const ProofOfPath = pop_mod.ProofOfPath; +const PathVerdict = pop_mod.PathVerdict; +const SovereignTimestamp = time.SovereignTimestamp; + +// ============================================================================ +// OPAQUE CONTEXT +// ============================================================================ + +/// Opaque handle for QVL context (hides Zig internals) +pub const QvlContext = struct { + allocator: std.mem.Allocator, + risk_graph: RiskGraph, + reputation: ReputationMap, + trust_graph: trust_graph.CompactTrustGraph, +}; + +// ============================================================================ +// C ABI TYPES +// ============================================================================ + +pub const PopVerdict = enum(c_int) { + valid = 0, + invalid_endpoints = 1, + broken_link = 2, + revoked = 3, + replay = 4, +}; + +pub const AnomalyReason = enum(u8) { + none = 0, + negative_cycle = 1, + low_coverage = 2, + bp_divergence = 3, +}; + +pub const AnomalyScore = extern struct { + node: u32, + score: f64, // 0.0-1.0 + reason: u8, // AnomalyReason enum +}; + +pub const RiskEdgeC = extern struct { + from: u32, + to: u32, + risk: f64, + timestamp_ns: u64, + nonce: u64, + level: u8, + expires_at_ns: u64, +}; + +// ============================================================================ +// CONTEXT MANAGEMENT +// ============================================================================ + +/// Initialize QVL context +/// Returns NULL on allocation failure +export fn qvl_init() callconv(.c) ?*QvlContext { + // Use C allocator for FFI (heap allocations) + const allocator = std.heap.c_allocator; + + const ctx = allocator.create(QvlContext) catch return null; + const default_root: [32]u8 = [_]u8{0} ** 32; + ctx.* = .{ + .allocator = allocator, + .risk_graph = RiskGraph.init(allocator), + .reputation = ReputationMap.init(allocator), + .trust_graph = trust_graph.CompactTrustGraph.init(allocator, default_root, .{}) catch { + allocator.destroy(ctx); + return null; + }, + }; + + return ctx; +} + +/// Cleanup and free QVL context +export fn qvl_deinit(ctx: ?*QvlContext) callconv(.c) void { + const context = ctx orelse return; + context.risk_graph.deinit(); + context.reputation.deinit(); + context.trust_graph.deinit(); + context.allocator.destroy(context); +} + +// ============================================================================ +// TRUST SCORING +// ============================================================================ + +/// Get trust score for a DID +/// Returns -1.0 on error (invalid DID, not found, etc.) +export fn qvl_get_trust_score( + ctx: ?*QvlContext, + did: [*c]const u8, + did_len: usize, +) callconv(.c) f64 { + const context = ctx orelse return -1.0; + if (did_len != 32) return -1.0; // DID must be 32 bytes + + const did_bytes = did[0..did_len]; + var did_array: [32]u8 = undefined; + @memcpy(&did_array, did_bytes); + + // Hash DID to NodeId (simplified; real impl would use node_map) + var hasher = std.hash.Wyhash.init(0); + hasher.update(&did_array); + const node_id: u32 = @truncate(hasher.final()); + + return context.reputation.get(node_id); +} + +/// Get reputation score for a NodeId +/// Returns -1.0 on error +export fn qvl_get_reputation(ctx: ?*QvlContext, node_id: u32) callconv(.c) f64 { + const context = ctx orelse return -1.0; + return context.reputation.get(node_id); +} + +// ============================================================================ +// PROOF-OF-PATH +// ============================================================================ + +/// Verify a serialized PoP proof +export fn qvl_verify_pop( + ctx: ?*QvlContext, + proof_bytes: [*c]const u8, + proof_len: usize, + sender_did: [*c]const u8, + receiver_did: [*c]const u8, +) callconv(.c) PopVerdict { + const context = ctx orelse return .invalid_endpoints; + + // Deserialize proof + const proof_slice = proof_bytes[0..proof_len]; + var proof = ProofOfPath.deserialize(context.allocator, proof_slice) catch { + return .invalid_endpoints; + }; + defer proof.deinit(); + + // Copy DIDs + var sender: [32]u8 = undefined; + var receiver: [32]u8 = undefined; + @memcpy(&sender, sender_did[0..32]); + @memcpy(&receiver, receiver_did[0..32]); + + // Verify + const verdict = proof.verify(receiver, sender, &context.trust_graph); + + // Convert to C enum + return switch (verdict) { + .valid => .valid, + .invalid_endpoints => .invalid_endpoints, + .broken_link => .broken_link, + .revoked => .revoked, + .replay => .replay, + else => .invalid_endpoints, // Catch-all for future enum additions + }; +} + +// ============================================================================ +// BETRAYAL DETECTION +// ============================================================================ + +/// Run Bellman-Ford betrayal detection from source node +/// Returns anomaly score (0.0 = clean, 0.9+ = critical) +export fn qvl_detect_betrayal( + ctx: ?*QvlContext, + source_node: u32, +) callconv(.c) AnomalyScore { + const context = ctx orelse return .{ .node = 0, .score = 0.0, .reason = @intFromEnum(AnomalyReason.none) }; + + var result = qvl.betrayal.detectBetrayal( + &context.risk_graph, + source_node, + context.allocator, + ) catch { + return .{ .node = 0, .score = 0.0, .reason = @intFromEnum(AnomalyReason.none) }; + }; + defer result.deinit(); + + if (result.betrayal_cycles.items.len > 0) { + // Betrayal detected - compute anomaly score + const score = result.computeAnomalyScore(); + return .{ + .node = source_node, + .score = score, + .reason = @intFromEnum(AnomalyReason.negative_cycle), + }; + } + + return .{ .node = source_node, .score = 0.0, .reason = @intFromEnum(AnomalyReason.none) }; +} + +// ============================================================================ +// GRAPH MUTATIONS +// ============================================================================ + +/// Add trust edge to risk graph +/// Returns 0 on success, non-zero on error +export fn qvl_add_trust_edge( + ctx: ?*QvlContext, + edge_c: [*c]const RiskEdgeC, +) callconv(.c) c_int { + const context = ctx orelse return -1; + const edge_ptr = edge_c orelse return -1; + const edge_val = edge_ptr.*; + + const edge = RiskEdge{ + .from = edge_val.from, + .to = edge_val.to, + .risk = edge_val.risk, + .timestamp = SovereignTimestamp.fromNanoseconds(edge_val.timestamp_ns, .unix_1970), + .nonce = edge_val.nonce, + .level = edge_val.level, + .expires_at = SovereignTimestamp.fromNanoseconds(edge_val.expires_at_ns, .unix_1970), + }; + + context.risk_graph.addEdge(edge) catch return -2; + return 0; +} + +/// Revoke trust edge +/// Returns 0 on success, non-zero on error (not found, etc.) +export fn qvl_revoke_trust_edge( + ctx: ?*QvlContext, + from: u32, + to: u32, +) callconv(.c) c_int { + const context = ctx orelse return -1; + + // Find and remove edge + var i: usize = 0; + while (i < context.risk_graph.edges.items.len) : (i += 1) { + const edge = &context.risk_graph.edges.items[i]; + if (edge.from == from and edge.to == to) { + _ = context.risk_graph.edges.swapRemove(i); + return 0; + } + } + + return -2; // Not found +} + +// ============================================================================ +// TESTS (C ABI validation) +// ============================================================================ + +test "FFI: context lifecycle" { + const ctx = qvl_init(); + try std.testing.expect(ctx != null); + qvl_deinit(ctx); +} + +test "FFI: trust scoring" { + const ctx = qvl_init() orelse return error.InitFailed; + defer qvl_deinit(ctx); + + const score = qvl_get_reputation(ctx, 42); + try std.testing.expectEqual(score, 0.5); // Default neutral +} + +test "FFI: add edge" { + const ctx = qvl_init() orelse return error.InitFailed; + defer qvl_deinit(ctx); + + const edge = RiskEdgeC{ + .from = 0, + .to = 1, + .risk = 0.5, + .timestamp_ns = 1000, + .nonce = 0, + .level = 3, + .expires_at_ns = 2000, + }; + + const result = qvl_add_trust_edge(ctx, &edge); + try std.testing.expectEqual(result, 0); +} diff --git a/l1-identity/test_qvl_ffi.c b/l1-identity/test_qvl_ffi.c new file mode 100644 index 0000000..9219ee8 --- /dev/null +++ b/l1-identity/test_qvl_ffi.c @@ -0,0 +1,138 @@ +/** + * QVL FFI Test - C ABI Validation + * + * Tests: + * - Context lifecycle + * - Trust scoring + * - Graph mutations + * - Error handling + */ + +#include "qvl.h" +#include +#include +#include +#include + +void test_context_lifecycle() { + printf("Test: Context lifecycle...\n"); + + QvlContext* ctx = qvl_init(); + assert(ctx != NULL && "qvl_init should return non-NULL"); + + qvl_deinit(ctx); + printf(" PASS\n"); +} + +void test_trust_scoring() { + printf("Test: Trust scoring...\n"); + + QvlContext* ctx = qvl_init(); + assert(ctx != NULL); + + // Get reputation for unknown node (should return neutral 0.5) + double score = qvl_get_reputation(ctx, 42); + assert(score == 0.5 && "Unknown node should have neutral reputation"); + + qvl_deinit(ctx); + printf(" PASS\n"); +} + +void test_add_edge() { + printf("Test: Add trust edge...\n"); + + QvlContext* ctx = qvl_init(); + assert(ctx != NULL); + + QvlRiskEdge edge = { + .from = 0, + .to = 1, + .risk = 0.5, + .timestamp_ns = 1000, + .nonce = 0, + .level = 3, + .expires_at_ns = 2000 + }; + + int result = qvl_add_trust_edge(ctx, &edge); + assert(result == 0 && "Adding edge should succeed"); + + qvl_deinit(ctx); + printf(" PASS\n"); +} + +void test_revoke_edge() { + printf("Test: Revoke trust edge...\n"); + + QvlContext* ctx = qvl_init(); + assert(ctx != NULL); + + // Add edge first + QvlRiskEdge edge = { + .from = 0, + .to = 1, + .risk = 0.5, + .timestamp_ns = 1000, + .nonce = 0, + .level = 3, + .expires_at_ns = 2000 + }; + qvl_add_trust_edge(ctx, &edge); + + // Revoke it + int result = qvl_revoke_trust_edge(ctx, 0, 1); + assert(result == 0 && "Revoking existing edge should succeed"); + + // Try to revoke again (should fail) + result = qvl_revoke_trust_edge(ctx, 0, 1); + assert(result == -2 && "Revoking non-existent edge should return -2"); + + qvl_deinit(ctx); + printf(" PASS\n"); +} + +void test_get_trust_score() { + printf("Test: Get trust score by DID...\n"); + + QvlContext* ctx = qvl_init(); + assert(ctx != NULL); + + uint8_t did[32]; + memset(did, 0x42, 32); + + double score = qvl_get_trust_score(ctx, did, 32); + assert(score == 0.5 && "Unknown DID should have neutral score"); + + // Invalid length + score = qvl_get_trust_score(ctx, did, 16); + assert(score == -1.0 && "Invalid DID length should return -1.0"); + + qvl_deinit(ctx); + printf(" PASS\n"); +} + +void test_null_safety() { + printf("Test: NULL safety...\n"); + + // All functions should handle NULL context gracefully + double score = qvl_get_reputation(NULL, 0); + assert(score == -1.0 && "NULL context should return error"); + + qvl_deinit(NULL); // Should not crash + + printf(" PASS\n"); +} + +int main() { + printf("=== QVL FFI C ABI Validation ===\n\n"); + + test_context_lifecycle(); + test_trust_scoring(); + test_add_edge(); + test_revoke_edge(); + test_get_trust_score(); + test_null_safety(); + + printf("\n=== All tests passed! ===\n"); + return 0; +}