use std::collections::{HashMap, HashSet, VecDeque};
fn xorshift64(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
#[derive(Debug, Clone, PartialEq)]
pub enum GraphError {
PeerNotFound(String),
EdgeNotFound { from: String, to: String },
SelfLoop(String),
InvalidWeight(f64),
MaxPeersExceeded,
}
impl std::fmt::Display for GraphError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphError::PeerNotFound(id) => write!(f, "peer not found: {id}"),
GraphError::EdgeNotFound { from, to } => {
write!(f, "edge not found: {from} → {to}")
}
GraphError::SelfLoop(id) => write!(f, "self-loop not allowed for peer {id}"),
GraphError::InvalidWeight(w) => {
write!(f, "invalid edge weight {w}; must be in [0.0, 1.0]")
}
GraphError::MaxPeersExceeded => write!(f, "maximum peer count exceeded"),
}
}
}
impl std::error::Error for GraphError {}
#[derive(Debug, Clone)]
pub struct TrustEdge {
pub from_peer: String,
pub to_peer: String,
pub weight: f64,
pub created_at: u64,
pub updated_at: u64,
pub interaction_count: u64,
}
#[derive(Debug, Clone)]
pub struct ReputationScore {
pub peer_id: String,
pub direct_score: f64,
pub propagated_score: f64,
pub combined_score: f64,
pub confidence: f64,
pub percentile: f64,
}
#[derive(Debug, Clone)]
pub enum ReputationEvent {
PositiveInteraction { peer_id: String, magnitude: f64 },
NegativeInteraction { peer_id: String, magnitude: f64 },
EdgeAdded {
from: String,
to: String,
weight: f64,
},
EdgeRemoved { from: String, to: String },
ScoreDecayed {
peer_id: String,
old_score: f64,
new_score: f64,
},
}
#[derive(Debug, Clone)]
pub struct GraphConfig {
pub trust_decay_factor: f64,
pub propagation_depth: u8,
pub propagation_damping: f64,
pub min_edge_weight: f64,
pub max_peers: usize,
pub combination_weight_direct: f64,
pub combination_weight_propagated: f64,
}
impl Default for GraphConfig {
fn default() -> Self {
Self {
trust_decay_factor: 0.99,
propagation_depth: 3,
propagation_damping: 0.5,
min_edge_weight: 0.01,
max_peers: 10_000,
combination_weight_direct: 0.6,
combination_weight_propagated: 0.4,
}
}
}
#[derive(Debug, Clone)]
pub struct GraphStats {
pub peer_count: usize,
pub edge_count: usize,
pub avg_out_degree: f64,
pub avg_reputation: f64,
pub top_peers: Vec<String>,
pub isolated_peers: usize,
}
#[derive(Debug, Clone)]
struct PeerState {
peer_id: String,
direct_score: f64,
propagated_score: f64,
interaction_count: u64,
}
impl PeerState {
fn new(peer_id: String) -> Self {
Self {
peer_id,
direct_score: 0.5, propagated_score: 0.0,
interaction_count: 0,
}
}
}
pub struct PeerReputationGraph {
config: GraphConfig,
peers: HashMap<String, PeerState>,
edges: HashMap<(String, String), TrustEdge>,
out_adj: HashMap<String, HashSet<String>>,
in_adj: HashMap<String, HashSet<String>>,
clock: u64,
rng_state: u64,
}
impl PeerReputationGraph {
pub fn new(config: GraphConfig) -> Self {
Self {
config,
peers: HashMap::new(),
edges: HashMap::new(),
out_adj: HashMap::new(),
in_adj: HashMap::new(),
clock: 1,
rng_state: 0xdeadbeef_cafebabe,
}
}
pub fn add_peer(&mut self, peer_id: String) -> Result<(), GraphError> {
if self.peers.contains_key(&peer_id) {
return Ok(());
}
if self.peers.len() >= self.config.max_peers {
return Err(GraphError::MaxPeersExceeded);
}
self.out_adj.entry(peer_id.clone()).or_default();
self.in_adj.entry(peer_id.clone()).or_default();
self.peers.insert(peer_id.clone(), PeerState::new(peer_id));
self.tick();
Ok(())
}
pub fn remove_peer(&mut self, peer_id: &str) -> Result<(), GraphError> {
if !self.peers.contains_key(peer_id) {
return Err(GraphError::PeerNotFound(peer_id.to_string()));
}
let out_peers: Vec<String> = self
.out_adj
.get(peer_id)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default();
let in_peers: Vec<String> = self
.in_adj
.get(peer_id)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default();
for to in &out_peers {
self.edges.remove(&(peer_id.to_string(), to.clone()));
if let Some(set) = self.in_adj.get_mut(to) {
set.remove(peer_id);
}
}
for from in &in_peers {
self.edges.remove(&(from.clone(), peer_id.to_string()));
if let Some(set) = self.out_adj.get_mut(from) {
set.remove(peer_id);
}
}
self.out_adj.remove(peer_id);
self.in_adj.remove(peer_id);
self.peers.remove(peer_id);
self.tick();
Ok(())
}
pub fn add_edge(&mut self, from: &str, to: &str, weight: f64) -> Result<(), GraphError> {
if from == to {
return Err(GraphError::SelfLoop(from.to_string()));
}
if !(0.0..=1.0).contains(&weight) {
return Err(GraphError::InvalidWeight(weight));
}
if !self.peers.contains_key(from) {
return Err(GraphError::PeerNotFound(from.to_string()));
}
if !self.peers.contains_key(to) {
return Err(GraphError::PeerNotFound(to.to_string()));
}
let now = self.tick();
let key = (from.to_string(), to.to_string());
if let Some(edge) = self.edges.get_mut(&key) {
edge.weight = weight;
edge.updated_at = now;
edge.interaction_count += 1;
} else {
self.edges.insert(
key.clone(),
TrustEdge {
from_peer: from.to_string(),
to_peer: to.to_string(),
weight,
created_at: now,
updated_at: now,
interaction_count: 1,
},
);
self.out_adj
.entry(from.to_string())
.or_default()
.insert(to.to_string());
self.in_adj
.entry(to.to_string())
.or_default()
.insert(from.to_string());
}
Ok(())
}
pub fn remove_edge(&mut self, from: &str, to: &str) -> Result<(), GraphError> {
let key = (from.to_string(), to.to_string());
if self.edges.remove(&key).is_none() {
return Err(GraphError::EdgeNotFound {
from: from.to_string(),
to: to.to_string(),
});
}
if let Some(set) = self.out_adj.get_mut(from) {
set.remove(to);
}
if let Some(set) = self.in_adj.get_mut(to) {
set.remove(from);
}
self.tick();
Ok(())
}
pub fn record_interaction(
&mut self,
peer_id: &str,
positive: bool,
magnitude: f64,
) -> Result<ReputationEvent, GraphError> {
const ALPHA: f64 = 0.1;
let state = self
.peers
.get_mut(peer_id)
.ok_or_else(|| GraphError::PeerNotFound(peer_id.to_string()))?;
let delta = if positive { magnitude } else { -magnitude };
let target = (state.direct_score + delta).clamp(0.0, 1.0);
let ema = state.direct_score + ALPHA * (target - state.direct_score);
state.direct_score = ema.clamp(0.0, 1.0);
state.interaction_count += 1;
let event = if positive {
ReputationEvent::PositiveInteraction {
peer_id: peer_id.to_string(),
magnitude,
}
} else {
ReputationEvent::NegativeInteraction {
peer_id: peer_id.to_string(),
magnitude,
}
};
self.tick();
Ok(event)
}
pub fn propagate_trust(&mut self) -> usize {
let direct_scores: HashMap<String, f64> = self
.peers
.iter()
.map(|(id, s)| (id.clone(), s.direct_score))
.collect();
let mut propagated: HashMap<String, f64> =
self.peers.keys().map(|id| (id.clone(), 0.0_f64)).collect();
let depth = self.config.propagation_depth as usize;
let damping = self.config.propagation_damping;
for (src_id, &src_direct) in &direct_scores {
if src_direct == 0.0 {
continue;
}
let mut queue: VecDeque<(String, f64, usize)> = VecDeque::new();
queue.push_back((src_id.clone(), src_direct, 0));
let mut visited: HashSet<String> = HashSet::new();
visited.insert(src_id.clone());
while let Some((node, trust, hop)) = queue.pop_front() {
if hop >= depth {
continue;
}
let neighbors: Vec<(String, f64)> = self
.out_adj
.get(&node)
.map(|set| {
set.iter()
.filter_map(|nb| {
let key = (node.clone(), nb.clone());
self.edges.get(&key).map(|e| (nb.clone(), e.weight))
})
.collect()
})
.unwrap_or_default();
for (nb, edge_weight) in neighbors {
if visited.contains(&nb) {
continue;
}
visited.insert(nb.clone());
let contribution = edge_weight * trust * damping.powi((hop + 1) as i32);
if let Some(acc) = propagated.get_mut(&nb) {
*acc += contribution;
}
queue.push_back((nb, trust * edge_weight, hop + 1));
}
}
}
let mut updated = 0_usize;
for (id, new_prop) in propagated {
if let Some(state) = self.peers.get_mut(&id) {
let clamped = new_prop.clamp(0.0, 1.0);
if (clamped - state.propagated_score).abs() > f64::EPSILON {
state.propagated_score = clamped;
updated += 1;
}
}
}
updated
}
pub fn decay_scores(&mut self) -> Vec<ReputationEvent> {
let factor = self.config.trust_decay_factor;
let min_weight = self.config.min_edge_weight;
let mut events = Vec::new();
for state in self.peers.values_mut() {
let old = state.direct_score;
let new = (old * factor).clamp(0.0, 1.0);
if (new - old).abs() > 0.001 {
events.push(ReputationEvent::ScoreDecayed {
peer_id: state.peer_id.clone(),
old_score: old,
new_score: new,
});
}
state.direct_score = new;
}
let to_prune: Vec<(String, String)> = self
.edges
.iter()
.filter(|(_, e)| e.weight < min_weight)
.map(|(k, _)| (k.0.clone(), k.1.clone()))
.collect();
for (from, to) in to_prune {
let _ = self.remove_edge(&from, &to);
}
self.tick();
events
}
pub fn reputation(&self, peer_id: &str) -> Result<ReputationScore, GraphError> {
let state = self
.peers
.get(peer_id)
.ok_or_else(|| GraphError::PeerNotFound(peer_id.to_string()))?;
let wd = self.config.combination_weight_direct;
let wp = self.config.combination_weight_propagated;
let total_weight = wd + wp;
let combined = if total_weight > 0.0 {
(wd * state.direct_score + wp * state.propagated_score) / total_weight
} else {
state.direct_score
};
let confidence = (state.interaction_count as f64 / 10.0).min(1.0);
let combined_clamped = combined.clamp(0.0, 1.0);
let percentile = self.percentile_of(peer_id, combined_clamped);
Ok(ReputationScore {
peer_id: peer_id.to_string(),
direct_score: state.direct_score,
propagated_score: state.propagated_score,
combined_score: combined_clamped,
confidence,
percentile,
})
}
pub fn top_peers(&self, n: usize) -> Vec<ReputationScore> {
let mut scores: Vec<ReputationScore> = self
.peers
.keys()
.filter_map(|id| self.reputation(id).ok())
.collect();
scores.sort_by(|a, b| {
b.combined_score
.partial_cmp(&a.combined_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
scores.truncate(n);
scores
}
pub fn peers_trusting(&self, peer_id: &str) -> Vec<String> {
self.in_adj
.get(peer_id)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default()
}
pub fn peers_trusted_by(&self, peer_id: &str) -> Vec<String> {
self.out_adj
.get(peer_id)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default()
}
pub fn stats(&self) -> GraphStats {
let peer_count = self.peers.len();
let edge_count = self.edges.len();
let avg_out_degree = if peer_count > 0 {
edge_count as f64 / peer_count as f64
} else {
0.0
};
let avg_reputation = if peer_count > 0 {
let sum: f64 = self
.peers
.keys()
.filter_map(|id| self.reputation(id).ok())
.map(|r| r.combined_score)
.sum();
sum / peer_count as f64
} else {
0.0
};
let mut top = self.top_peers(5);
top.sort_by(|a, b| {
b.combined_score
.partial_cmp(&a.combined_score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.peer_id.cmp(&b.peer_id))
});
let top_peers = top.into_iter().map(|r| r.peer_id).collect();
let isolated_peers = self
.peers
.keys()
.filter(|id| {
self.out_adj.get(*id).map(|s| s.is_empty()).unwrap_or(true)
&& self.in_adj.get(*id).map(|s| s.is_empty()).unwrap_or(true)
})
.count();
GraphStats {
peer_count,
edge_count,
avg_out_degree,
avg_reputation,
top_peers,
isolated_peers,
}
}
pub fn prune_isolated(&mut self) -> usize {
let to_remove: Vec<String> = self
.peers
.iter()
.filter(|(id, state)| {
let out_empty = self.out_adj.get(*id).map(|s| s.is_empty()).unwrap_or(true);
let in_empty = self.in_adj.get(*id).map(|s| s.is_empty()).unwrap_or(true);
if !out_empty || !in_empty {
return false;
}
let wd = self.config.combination_weight_direct;
let wp = self.config.combination_weight_propagated;
let total = wd + wp;
let combined = if total > 0.0 {
(wd * state.direct_score + wp * state.propagated_score) / total
} else {
state.direct_score
};
combined < 0.1
})
.map(|(id, _)| id.clone())
.collect();
let count = to_remove.len();
for id in to_remove {
let _ = self.remove_peer(&id);
}
count
}
pub fn all_edges(&self) -> Vec<TrustEdge> {
self.edges.values().cloned().collect()
}
pub fn edge(&self, from: &str, to: &str) -> Option<&TrustEdge> {
self.edges.get(&(from.to_string(), to.to_string()))
}
pub fn contains_peer(&self, peer_id: &str) -> bool {
self.peers.contains_key(peer_id)
}
pub fn peer_count(&self) -> usize {
self.peers.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
fn tick(&mut self) -> u64 {
self.clock += 1;
let _ = xorshift64(&mut self.rng_state);
self.clock
}
fn percentile_of(&self, peer_id: &str, combined: f64) -> f64 {
if self.peers.len() <= 1 {
return 0.5;
}
let below = self
.peers
.iter()
.filter(|(id, state)| {
if id.as_str() == peer_id {
return false;
}
let wd = self.config.combination_weight_direct;
let wp = self.config.combination_weight_propagated;
let total = wd + wp;
let other_combined = if total > 0.0 {
(wd * state.direct_score + wp * state.propagated_score) / total
} else {
state.direct_score
};
other_combined < combined
})
.count();
let n = self.peers.len() - 1; if n == 0 {
0.5
} else {
below as f64 / n as f64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_graph() -> PeerReputationGraph {
PeerReputationGraph::new(GraphConfig::default())
}
#[test]
fn test_add_peer_basic() {
let mut g = make_graph();
g.add_peer("alice".to_string())
.expect("test: add_peer alice should succeed");
assert!(g.contains_peer("alice"));
}
#[test]
fn test_add_peer_idempotent() {
let mut g = make_graph();
g.add_peer("alice".to_string())
.expect("test: add_peer alice should succeed");
g.add_peer("alice".to_string())
.expect("test: re-adding alice should succeed");
assert_eq!(g.peer_count(), 1);
}
#[test]
fn test_add_peer_max_exceeded() {
let mut g = PeerReputationGraph::new(GraphConfig {
max_peers: 2,
..Default::default()
});
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
let err = g
.add_peer("c".to_string())
.expect_err("test: expected error when exceeding max peers");
assert_eq!(err, GraphError::MaxPeersExceeded);
}
#[test]
fn test_remove_peer_basic() {
let mut g = make_graph();
g.add_peer("alice".to_string())
.expect("test: add_peer alice should succeed");
g.remove_peer("alice")
.expect("test: remove_peer alice should succeed");
assert!(!g.contains_peer("alice"));
}
#[test]
fn test_remove_peer_not_found() {
let mut g = make_graph();
let err = g
.remove_peer("ghost")
.expect_err("test: expected error removing nonexistent peer");
assert!(matches!(err, GraphError::PeerNotFound(_)));
}
#[test]
fn test_remove_peer_removes_edges() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_peer("c".to_string())
.expect("test: add_peer c should succeed");
g.add_edge("a", "b", 0.8)
.expect("test: add_edge a->b should succeed");
g.add_edge("c", "a", 0.6)
.expect("test: add_edge c->a should succeed");
g.remove_peer("a")
.expect("test: remove_peer a should succeed");
assert_eq!(g.edge_count(), 0);
assert!(g.peers_trusting("b").is_empty());
assert!(g.peers_trusted_by("c").is_empty());
}
#[test]
fn test_add_edge_basic() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 0.7)
.expect("test: add_edge a->b should succeed");
assert_eq!(g.edge_count(), 1);
assert_eq!(
g.edge("a", "b")
.expect("test: edge a->b should exist")
.weight,
0.7
);
}
#[test]
fn test_add_edge_update_existing() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 0.5)
.expect("test: add_edge a->b 0.5 should succeed");
g.add_edge("a", "b", 0.9)
.expect("test: update edge a->b to 0.9 should succeed");
assert_eq!(g.edge_count(), 1);
assert_eq!(
g.edge("a", "b")
.expect("test: edge a->b should exist")
.weight,
0.9
);
assert_eq!(
g.edge("a", "b")
.expect("test: edge a->b should exist")
.interaction_count,
2
);
}
#[test]
fn test_add_edge_self_loop() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
let err = g
.add_edge("a", "a", 0.5)
.expect_err("test: expected error for self-loop edge");
assert!(matches!(err, GraphError::SelfLoop(_)));
}
#[test]
fn test_add_edge_invalid_weight_negative() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
let err = g
.add_edge("a", "b", -0.1)
.expect_err("test: expected error for negative weight");
assert!(matches!(err, GraphError::InvalidWeight(_)));
}
#[test]
fn test_add_edge_invalid_weight_above_one() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
let err = g
.add_edge("a", "b", 1.5)
.expect_err("test: expected error for weight above 1.0");
assert!(matches!(err, GraphError::InvalidWeight(_)));
}
#[test]
fn test_add_edge_peer_not_found() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
let err = g
.add_edge("a", "ghost", 0.5)
.expect_err("test: expected error for nonexistent target peer");
assert!(matches!(err, GraphError::PeerNotFound(_)));
}
#[test]
fn test_remove_edge_basic() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 0.5)
.expect("test: add_edge a->b should succeed");
g.remove_edge("a", "b")
.expect("test: remove_edge a->b should succeed");
assert_eq!(g.edge_count(), 0);
}
#[test]
fn test_remove_edge_not_found() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
let err = g
.remove_edge("a", "b")
.expect_err("test: expected error removing nonexistent edge");
assert!(matches!(err, GraphError::EdgeNotFound { .. }));
}
#[test]
fn test_positive_interaction_increases_score() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
let initial = g
.reputation("a")
.expect("test: reputation for a should exist")
.direct_score;
let event = g
.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
let after = g
.reputation("a")
.expect("test: reputation for a should exist")
.direct_score;
assert!(after >= initial);
assert!(matches!(event, ReputationEvent::PositiveInteraction { .. }));
}
#[test]
fn test_negative_interaction_decreases_score() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
let initial = g
.reputation("a")
.expect("test: reputation for a should exist")
.direct_score;
let event = g
.record_interaction("a", false, 1.0)
.expect("test: record negative interaction for a should succeed");
let after = g
.reputation("a")
.expect("test: reputation for a should exist")
.direct_score;
assert!(after <= initial);
assert!(matches!(event, ReputationEvent::NegativeInteraction { .. }));
}
#[test]
fn test_interaction_clamps_score() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
for _ in 0..100 {
g.record_interaction("a", true, 10.0)
.expect("test: record large positive interaction should succeed");
}
let score = g
.reputation("a")
.expect("test: reputation for a should exist")
.direct_score;
assert!(score <= 1.0);
for _ in 0..100 {
g.record_interaction("a", false, 10.0)
.expect("test: record large negative interaction should succeed");
}
let score = g
.reputation("a")
.expect("test: reputation for a should exist")
.direct_score;
assert!(score >= 0.0);
}
#[test]
fn test_interaction_peer_not_found() {
let mut g = make_graph();
let err = g
.record_interaction("ghost", true, 0.5)
.expect_err("test: expected error for nonexistent peer interaction");
assert!(matches!(err, GraphError::PeerNotFound(_)));
}
#[test]
fn test_interaction_count_increments() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.record_interaction("a", true, 0.5)
.expect("test: record positive interaction should succeed");
g.record_interaction("a", false, 0.2)
.expect("test: record negative interaction should succeed");
let score = g
.reputation("a")
.expect("test: reputation for a should exist");
assert!(score.confidence > 0.0);
}
#[test]
fn test_propagate_trust_basic() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 1.0)
.expect("test: add_edge a->b should succeed");
for _ in 0..50 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
let updated = g.propagate_trust();
assert!(updated > 0);
let b_score = g
.reputation("b")
.expect("test: reputation for b should exist");
assert!(b_score.propagated_score > 0.0);
}
#[test]
fn test_propagate_trust_no_edges() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.propagate_trust();
assert_eq!(
g.reputation("b")
.expect("test: reputation for b should exist")
.propagated_score,
0.0
);
}
#[test]
fn test_propagate_trust_depth_limit() {
let mut g = PeerReputationGraph::new(GraphConfig {
propagation_depth: 1,
propagation_damping: 0.5,
..Default::default()
});
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_peer("c".to_string())
.expect("test: add_peer c should succeed");
g.add_edge("a", "b", 1.0)
.expect("test: add_edge a->b should succeed");
g.add_edge("b", "c", 1.0)
.expect("test: add_edge b->c should succeed");
for _ in 0..50 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
g.propagate_trust();
assert!(
g.reputation("b")
.expect("test: reputation for b should exist")
.propagated_score
> 0.0
);
let b_prop = g
.reputation("b")
.expect("test: reputation for b should exist")
.propagated_score;
let c_prop = g
.reputation("c")
.expect("test: reputation for c should exist")
.propagated_score;
assert!(b_prop >= c_prop);
}
#[test]
fn test_propagate_trust_multi_hop() {
let mut g = PeerReputationGraph::new(GraphConfig {
propagation_depth: 2,
propagation_damping: 0.5,
..Default::default()
});
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_peer("c".to_string())
.expect("test: add_peer c should succeed");
g.add_edge("a", "b", 1.0)
.expect("test: add_edge a->b should succeed");
g.add_edge("b", "c", 1.0)
.expect("test: add_edge b->c should succeed");
for _ in 0..50 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
g.record_interaction("b", true, 1.0)
.expect("test: record positive interaction for b should succeed");
}
g.propagate_trust();
assert!(
g.reputation("c")
.expect("test: reputation for c should exist")
.propagated_score
> 0.0
);
}
#[test]
fn test_propagate_returns_count() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 1.0)
.expect("test: add_edge a->b should succeed");
for _ in 0..20 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
let count = g.propagate_trust();
assert!(count >= 1);
}
#[test]
fn test_decay_scores_reduces_score() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
for _ in 0..50 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
let before = g
.reputation("a")
.expect("test: reputation for a should exist")
.direct_score;
g.decay_scores();
let after = g
.reputation("a")
.expect("test: reputation for a after decay should exist")
.direct_score;
assert!(after <= before);
}
#[test]
fn test_decay_emits_events() {
let mut g = PeerReputationGraph::new(GraphConfig {
trust_decay_factor: 0.5, ..Default::default()
});
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
for _ in 0..50 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
let events = g.decay_scores();
assert!(!events.is_empty());
assert!(events
.iter()
.any(|e| matches!(e, ReputationEvent::ScoreDecayed { .. })));
}
#[test]
fn test_decay_prunes_weak_edges() {
let mut g = PeerReputationGraph::new(GraphConfig {
min_edge_weight: 0.5,
..Default::default()
});
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 0.3)
.expect("test: add weak edge a->b should succeed"); g.decay_scores();
assert_eq!(g.edge_count(), 0, "weak edge should be pruned");
}
#[test]
fn test_decay_keeps_strong_edges() {
let mut g = PeerReputationGraph::new(GraphConfig {
min_edge_weight: 0.1,
..Default::default()
});
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 0.9)
.expect("test: add strong edge a->b should succeed"); g.decay_scores();
assert_eq!(g.edge_count(), 1, "strong edge should remain");
}
#[test]
fn test_reputation_not_found() {
let g = make_graph();
let err = g
.reputation("ghost")
.expect_err("test: expected error for nonexistent peer reputation");
assert!(matches!(err, GraphError::PeerNotFound(_)));
}
#[test]
fn test_reputation_combined_score_range() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
for _ in 0..20 {
g.record_interaction("a", true, 0.5)
.expect("test: record positive interaction for a should succeed");
}
let r = g
.reputation("a")
.expect("test: reputation for a should exist");
assert!((0.0..=1.0).contains(&r.combined_score));
}
#[test]
fn test_reputation_confidence_grows() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
let c0 = g
.reputation("a")
.expect("test: reputation for a should exist")
.confidence;
for _ in 0..10 {
g.record_interaction("a", true, 0.5)
.expect("test: record positive interaction should succeed");
}
let c10 = g
.reputation("a")
.expect("test: reputation for a should exist")
.confidence;
assert!(c10 > c0);
}
#[test]
fn test_reputation_confidence_caps_at_one() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
for _ in 0..200 {
g.record_interaction("a", true, 0.3)
.expect("test: record positive interaction should succeed");
}
let c = g
.reputation("a")
.expect("test: reputation for a should exist")
.confidence;
assert!(c <= 1.0);
}
#[test]
fn test_top_peers_empty() {
let g = make_graph();
assert!(g.top_peers(5).is_empty());
}
#[test]
fn test_top_peers_order() {
let mut g = make_graph();
for name in &["a", "b", "c"] {
g.add_peer(name.to_string())
.expect("test: add_peer should succeed");
}
for _ in 0..20 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
let top = g.top_peers(3);
assert!(!top.is_empty());
assert_eq!(top[0].peer_id, "a");
}
#[test]
fn test_top_peers_limit() {
let mut g = make_graph();
for i in 0..10 {
g.add_peer(format!("peer{i}"))
.expect("test: add_peer should succeed");
}
assert!(g.top_peers(5).len() <= 5);
}
#[test]
fn test_top_peers_fewer_than_n() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
let top = g.top_peers(10);
assert_eq!(top.len(), 1);
}
#[test]
fn test_peers_trusting() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_peer("c".to_string())
.expect("test: add_peer c should succeed");
g.add_edge("b", "a", 0.5)
.expect("test: add_edge b->a should succeed");
g.add_edge("c", "a", 0.7)
.expect("test: add_edge c->a should succeed");
let trusting = g.peers_trusting("a");
assert!(trusting.contains(&"b".to_string()));
assert!(trusting.contains(&"c".to_string()));
}
#[test]
fn test_peers_trusted_by() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_peer("c".to_string())
.expect("test: add_peer c should succeed");
g.add_edge("a", "b", 0.5)
.expect("test: add_edge a->b should succeed");
g.add_edge("a", "c", 0.7)
.expect("test: add_edge a->c should succeed");
let trusted = g.peers_trusted_by("a");
assert!(trusted.contains(&"b".to_string()));
assert!(trusted.contains(&"c".to_string()));
}
#[test]
fn test_peers_trusting_empty() {
let mut g = make_graph();
g.add_peer("loner".to_string())
.expect("test: add_peer loner should succeed");
assert!(g.peers_trusting("loner").is_empty());
}
#[test]
fn test_stats_empty() {
let g = make_graph();
let s = g.stats();
assert_eq!(s.peer_count, 0);
assert_eq!(s.edge_count, 0);
assert_eq!(s.avg_out_degree, 0.0);
assert_eq!(s.isolated_peers, 0);
}
#[test]
fn test_stats_peer_and_edge_count() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 0.5)
.expect("test: add_edge a->b should succeed");
let s = g.stats();
assert_eq!(s.peer_count, 2);
assert_eq!(s.edge_count, 1);
}
#[test]
fn test_stats_isolated_peers() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
for _ in 0..50 {
g.record_interaction("b", false, 1.0)
.expect("test: record negative interaction for b should succeed");
}
let s = g.stats();
assert_eq!(s.isolated_peers, 2);
}
#[test]
fn test_stats_top_peers_max_five() {
let mut g = make_graph();
for i in 0..10 {
g.add_peer(format!("p{i}"))
.expect("test: add_peer should succeed");
}
let s = g.stats();
assert!(s.top_peers.len() <= 5);
}
#[test]
fn test_prune_isolated_removes_low_score_no_edge() {
let mut g = PeerReputationGraph::new(GraphConfig {
trust_decay_factor: 0.5,
..Default::default()
});
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
for _ in 0..50 {
g.record_interaction("b", false, 1.0)
.expect("test: record negative interaction for b should succeed");
}
let pruned = g.prune_isolated();
assert!(pruned >= 1);
assert!(!g.contains_peer("b"));
}
#[test]
fn test_prune_isolated_keeps_high_score() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
for _ in 0..50 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
let pruned = g.prune_isolated();
assert_eq!(pruned, 0);
assert!(g.contains_peer("a"));
}
#[test]
fn test_prune_isolated_keeps_connected_peers() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 0.8)
.expect("test: add_edge a->b should succeed");
for _ in 0..100 {
g.record_interaction("a", false, 1.0)
.expect("test: record negative interaction for a should succeed");
}
let pruned = g.prune_isolated();
assert_eq!(pruned, 0);
assert!(g.contains_peer("a"));
}
#[test]
fn test_graph_error_display_peer_not_found() {
let err = GraphError::PeerNotFound("x".to_string());
assert!(err.to_string().contains("x"));
}
#[test]
fn test_graph_error_display_edge_not_found() {
let err = GraphError::EdgeNotFound {
from: "a".to_string(),
to: "b".to_string(),
};
let s = err.to_string();
assert!(s.contains("a"));
assert!(s.contains("b"));
}
#[test]
fn test_graph_error_display_self_loop() {
let err = GraphError::SelfLoop("a".to_string());
assert!(err.to_string().contains("a"));
}
#[test]
fn test_graph_error_display_invalid_weight() {
let err = GraphError::InvalidWeight(2.5);
assert!(err.to_string().contains("2.5"));
}
#[test]
fn test_graph_error_max_peers() {
assert_eq!(
GraphError::MaxPeersExceeded.to_string(),
"maximum peer count exceeded"
);
}
#[test]
fn test_edge_interaction_count() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_edge("a", "b", 0.5)
.expect("test: add_edge a->b 0.5 should succeed");
g.add_edge("a", "b", 0.6)
.expect("test: update edge a->b to 0.6 should succeed");
g.add_edge("a", "b", 0.7)
.expect("test: update edge a->b to 0.7 should succeed");
assert_eq!(
g.edge("a", "b")
.expect("test: edge a->b should exist")
.interaction_count,
3
);
}
#[test]
fn test_graph_config_defaults() {
let cfg = GraphConfig::default();
assert!((cfg.trust_decay_factor - 0.99).abs() < 1e-9);
assert_eq!(cfg.propagation_depth, 3);
assert!((cfg.propagation_damping - 0.5).abs() < 1e-9);
assert_eq!(cfg.max_peers, 10_000);
}
#[test]
fn test_xorshift64_produces_nonzero() {
let mut state = 12345u64;
let v = xorshift64(&mut state);
assert_ne!(v, 0);
assert_ne!(state, 12345);
}
#[test]
fn test_xorshift64_sequence_unique() {
let mut state = 0xdeadbeef_u64;
let a = xorshift64(&mut state);
let b = xorshift64(&mut state);
let c = xorshift64(&mut state);
assert_ne!(a, b);
assert_ne!(b, c);
}
#[test]
fn test_combined_score_weights() {
let cfg = GraphConfig {
combination_weight_direct: 1.0,
combination_weight_propagated: 0.0,
..Default::default()
};
let mut g = PeerReputationGraph::new(cfg);
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
for _ in 0..50 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
g.propagate_trust();
let r = g
.reputation("a")
.expect("test: reputation for a should exist");
assert!((r.combined_score - r.direct_score).abs() < 1e-9);
}
#[test]
fn test_percentile_single_peer() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
let r = g
.reputation("a")
.expect("test: reputation for a should exist");
assert_eq!(r.percentile, 0.5);
}
#[test]
fn test_percentile_ordering() {
let mut g = make_graph();
g.add_peer("low".to_string())
.expect("test: add_peer low should succeed");
g.add_peer("high".to_string())
.expect("test: add_peer high should succeed");
for _ in 0..50 {
g.record_interaction("high", true, 1.0)
.expect("test: record positive interaction for high should succeed");
g.record_interaction("low", false, 1.0)
.expect("test: record negative interaction for low should succeed");
}
let low_pct = g
.reputation("low")
.expect("test: reputation for low should exist")
.percentile;
let high_pct = g
.reputation("high")
.expect("test: reputation for high should exist")
.percentile;
assert!(high_pct > low_pct);
}
#[test]
fn test_all_edges() {
let mut g = make_graph();
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_peer("c".to_string())
.expect("test: add_peer c should succeed");
g.add_edge("a", "b", 0.5)
.expect("test: add_edge a->b should succeed");
g.add_edge("b", "c", 0.3)
.expect("test: add_edge b->c should succeed");
let edges = g.all_edges();
assert_eq!(edges.len(), 2);
}
#[test]
fn test_propagation_damping_reduces_with_hops() {
let cfg = GraphConfig {
propagation_depth: 3,
propagation_damping: 0.5,
..Default::default()
};
let mut g = PeerReputationGraph::new(cfg);
g.add_peer("a".to_string())
.expect("test: add_peer a should succeed");
g.add_peer("b".to_string())
.expect("test: add_peer b should succeed");
g.add_peer("c".to_string())
.expect("test: add_peer c should succeed");
g.add_peer("d".to_string())
.expect("test: add_peer d should succeed");
g.add_edge("a", "b", 1.0)
.expect("test: add_edge a->b should succeed");
g.add_edge("b", "c", 1.0)
.expect("test: add_edge b->c should succeed");
g.add_edge("c", "d", 1.0)
.expect("test: add_edge c->d should succeed");
for _ in 0..100 {
g.record_interaction("a", true, 1.0)
.expect("test: record positive interaction for a should succeed");
}
for _ in 0..100 {
g.record_interaction("b", false, 1.0)
.expect("test: record negative interaction for b should succeed");
g.record_interaction("c", false, 1.0)
.expect("test: record negative interaction for c should succeed");
g.record_interaction("d", false, 1.0)
.expect("test: record negative interaction for d should succeed");
}
g.propagate_trust();
let b_prop = g
.reputation("b")
.expect("test: reputation for b should exist")
.propagated_score;
let d_prop = g
.reputation("d")
.expect("test: reputation for d should exist")
.propagated_score;
assert!(b_prop > d_prop, "b_prop={b_prop}, d_prop={d_prop}");
}
}