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:
parent
27d182a117
commit
8b55df50b5
22
build.zig
22
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
|
||||
// ========================================================================
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue