//! 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"); 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); }