Phase 6A: QVL FFI C exports for L2 integration

- Created qvl_ffi.zig: C ABI exports (qvl_init, qvl_deinit, trust scoring, PoP verification, betrayal detection, graph mutations)
- Created qvl.h: C header with full API documentation
- Created test_qvl_ffi.c: C test harness (manual compilation)
- Added FFI tests to build.zig with libc linking
- Fixed API mismatches: TrustGraph.init (3 args), BellmanFordResult.betrayal_cycles usage
- All tests passing (173/173: 137 SDK + 36 FFI)

FFI enables Rust Membrane Agents (L2) to consume L1 trust functions.
This commit is contained in:
Markus Maiwald 2026-01-31 03:06:20 +01:00
parent 27d182a117
commit 8b55df50b5
10 changed files with 989 additions and 28 deletions

View File

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

189
l1-identity/qvl.h Normal file
View File

@ -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 <stddef.h>
#include <stdint.h>
/* ========================================================================
* 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 */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

297
l1-identity/qvl_ffi.zig Normal file
View File

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

138
l1-identity/test_qvl_ffi.c Normal file
View File

@ -0,0 +1,138 @@
/**
* QVL FFI Test - C ABI Validation
*
* Tests:
* - Context lifecycle
* - Trust scoring
* - Graph mutations
* - Error handling
*/
#include "qvl.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
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;
}