libertaria-stack/l1-identity/qvl/betrayal.zig

273 lines
9.7 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! RFC-0120 Extension: Bellman-Ford Betrayal Detection
//!
//! Detects negative cycles in the trust graph, which indicate:
//! - Collusion rings (Sybil attacks)
//! - Decade-level betrayals (cascading trust decay)
//! - Cartel behavior (coordinated false vouches)
//!
//! 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;
const RiskGraph = types.RiskGraph;
const RiskEdge = types.RiskEdge;
const AnomalyScore = types.AnomalyScore;
/// Result of Bellman-Ford betrayal detection.
pub const BellmanFordResult = struct {
allocator: std.mem.Allocator,
/// Shortest distances from source (accounting for negative edges)
distances: std.AutoHashMapUnmanaged(NodeId, f64),
/// Predecessor map for path reconstruction
predecessors: std.AutoHashMapUnmanaged(NodeId, ?NodeId),
/// Detected betrayal cycles (negative cycles)
betrayal_cycles: std.ArrayListUnmanaged([]NodeId),
pub fn deinit(self: *BellmanFordResult) void {
self.distances.deinit(self.allocator);
self.predecessors.deinit(self.allocator);
for (self.betrayal_cycles.items) |cycle| {
self.allocator.free(cycle);
}
self.betrayal_cycles.deinit(self.allocator);
}
/// Compute anomaly score based on detected cycles.
/// Score is normalized to [0, 1].
pub fn computeAnomalyScore(self: *const BellmanFordResult) f64 {
if (self.betrayal_cycles.items.len > 0) return 1.0; // Any negative cycle is critical
return 0.0;
}
/// Get nodes involved in any betrayal cycle.
pub fn getCompromisedNodes(self: *const BellmanFordResult, allocator: std.mem.Allocator) ![]NodeId {
var seen = std.AutoHashMapUnmanaged(NodeId, void){};
defer seen.deinit(allocator);
for (self.betrayal_cycles.items) |cycle| {
for (cycle) |node| {
try seen.put(allocator, node, {});
}
}
var result = try allocator.alloc(NodeId, seen.count());
var i: usize = 0;
var it = seen.keyIterator();
while (it.next()) |key| {
result[i] = key.*;
i += 1;
}
return result;
}
/// Generate cryptographic evidence of betrayal (serialized cycle with weights)
/// Format: version(1) + cycle_len(4) + [NodeID(4) + Risk(8)]...
pub fn generateEvidence(
self: *const BellmanFordResult,
graph: *const RiskGraph,
allocator: std.mem.Allocator,
) ![]u8 {
if (self.betrayal_cycles.items.len == 0) return error.NoEvidence;
const cycle = self.betrayal_cycles.items[0];
var evidence = std.ArrayListUnmanaged(u8){};
errdefer evidence.deinit(allocator);
try evidence.writer(allocator).writeByte(0x01); // Version
try evidence.writer(allocator).writeInt(u32, @intCast(cycle.len), .little);
for (cycle, 0..) |node, i| {
try evidence.writer(allocator).writeInt(u32, node, .little);
// Find edge to next
const next = cycle[(i + 1) % cycle.len];
var risk: f64 = 0.0;
if (graph.getEdge(node, next)) |edge| {
risk = edge.risk;
}
try evidence.writer(allocator).writeAll(std.mem.asBytes(&risk));
}
return evidence.toOwnedSlice(allocator);
}
};
/// Run Bellman-Ford from source, detecting negative cycles (betrayal rings).
///
/// Algorithm:
/// 1. Relax all edges |V|-1 times.
/// 2. On |V|th pass: If any edge still improves → negative cycle exists.
/// 3. Trace cycle via predecessor map.
pub fn detectBetrayal(
graph: *const RiskGraph,
source: NodeId,
allocator: std.mem.Allocator,
) !BellmanFordResult {
const n = graph.nodeCount();
if (n == 0) {
return BellmanFordResult{
.allocator = allocator,
.distances = .{},
.predecessors = .{},
.betrayal_cycles = .{},
};
}
var dist = std.AutoHashMapUnmanaged(NodeId, f64){};
var prev = std.AutoHashMapUnmanaged(NodeId, ?NodeId){};
// Initialize distances
for (graph.nodes.items) |node| {
try dist.put(allocator, node, std.math.inf(f64));
try prev.put(allocator, node, null);
}
try dist.put(allocator, source, 0.0);
// Relax edges |V|-1 times
for (0..n - 1) |_| {
var improved = false;
for (graph.edges.items) |edge| {
const d_from = dist.get(edge.from) orelse continue;
if (d_from == std.math.inf(f64)) continue;
const d_to = dist.get(edge.to) orelse std.math.inf(f64);
const new_dist = d_from + edge.risk;
if (new_dist < d_to) {
try dist.put(allocator, edge.to, new_dist);
try prev.put(allocator, edge.to, edge.from);
improved = true;
}
}
if (!improved) break; // Early exit: no more improvements
}
// Detect negative cycles (betrayal rings)
var cycles = std.ArrayListUnmanaged([]NodeId){};
var in_cycle = std.AutoHashMapUnmanaged(NodeId, bool){};
defer in_cycle.deinit(allocator);
for (graph.edges.items) |edge| {
const d_from = dist.get(edge.from) orelse continue;
if (d_from == std.math.inf(f64)) continue;
const d_to = dist.get(edge.to) orelse continue;
if (d_from + edge.risk < d_to) {
// Negative cycle detected; trace it
if (in_cycle.get(edge.to)) |_| continue; // Already traced
const cycle = try traceCycle(edge.to, &prev, allocator);
if (cycle.len > 0) {
for (cycle) |node| {
try in_cycle.put(allocator, node, true);
}
try cycles.append(allocator, cycle);
}
}
}
return BellmanFordResult{
.allocator = allocator,
.distances = dist,
.predecessors = prev,
.betrayal_cycles = cycles,
};
}
/// Trace a cycle starting from a node in a negative cycle.
fn traceCycle(
start: NodeId,
prev: *std.AutoHashMapUnmanaged(NodeId, ?NodeId),
allocator: std.mem.Allocator,
) ![]NodeId {
var visited = std.AutoHashMapUnmanaged(NodeId, usize){};
defer visited.deinit(allocator);
var path = std.ArrayListUnmanaged(NodeId){};
defer path.deinit(allocator);
var current: ?NodeId = start;
var idx: usize = 0;
// Walk backward until we hit a repeat (cycle entry)
while (current) |curr| {
if (visited.get(curr)) |cycle_start_idx| {
// Found cycle; extract it
const cycle_len = idx - cycle_start_idx;
if (cycle_len == 0) return &[_]NodeId{};
const cycle = try allocator.alloc(NodeId, cycle_len);
@memcpy(cycle, path.items[cycle_start_idx..idx]);
return cycle;
}
try visited.put(allocator, curr, idx);
try path.append(allocator, curr);
current = if (prev.get(curr)) |p| p else null;
idx += 1;
if (idx > 10000) return error.CycleTooLong; // Safety limit
}
return &[_]NodeId{}; // No cycle found
}
// ============================================================================
// TESTS
// ============================================================================
test "Bellman-Ford: No betrayal in clean graph" {
const allocator = std.testing.allocator;
var graph = RiskGraph.init(allocator);
defer graph.deinit();
// A -> B -> C (all positive)
try graph.addNode(0);
try graph.addNode(1);
try graph.addNode(2);
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();
try std.testing.expectEqual(result.betrayal_cycles.items.len, 0);
try std.testing.expectEqual(result.computeAnomalyScore(), 0.0);
}
test "Bellman-Ford: Detect negative cycle (betrayal ring)" {
const allocator = std.testing.allocator;
var graph = RiskGraph.init(allocator);
defer graph.deinit();
// Triangle: A -> B -> C -> A with negative total weight
// A --0.2-> B --0.2-> C ---(-0.8)--> A = total -0.4 (negative)
try graph.addNode(0);
try graph.addNode(1);
try graph.addNode(2);
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();
try std.testing.expectEqual(result.betrayal_cycles.items.len, 1);
try std.testing.expect(result.computeAnomalyScore() > 0.0);
// Check evidence generation
const evidence = try result.generateEvidence(&graph, allocator);
defer allocator.free(evidence);
try std.testing.expect(evidence.len > 0);
try std.testing.expectEqual(evidence[0], 0x01); // Version
}