//! EigenTrust Authority Lens: Resolves based on global + domain trust. //! //! This lens integrates with both EigenTrust (global trust) and DomainTrust //! (domain-specific expertise) to weight assertions by the combined reputation //! and expertise of the signing agent. //! //! # Design Philosophy //! //! Follows the "Deep Module" principle: //! - Simple interface: `resolve_async(&[Assertion])` returns winner //! - Complex implementation: Queries TrustGraphStore for EigenTrust, DomainTrustStore for expertise //! - Sybil-resistant: Only seed-connected agents have meaningful global trust //! - Domain-aware: Expertise in the relevant domain boosts effective weight //! //! # Resolution Formula //! //! ```text //! weight = confidence × eigentrust_score × domain_factor //! ``` //! //! Where: //! - confidence: The assertion's self-declared confidence (0.0 - 1.0) //! - eigentrust_score: Global trust from power iteration (0.0 - 1.0) //! - domain_factor: 0.5 + (domain_score × 0.5), ranges from 0.5 to 1.0 use crate::traits::{compute_conflict_score, Resolution}; use crate::vote_aware_consensus::AsyncLens; use async_trait::async_trait; use std::sync::Arc; use stemedb_core::types::Assertion; use stemedb_storage::domain_trust_store::DomainTrustStore; use stemedb_storage::trust_graph_store::TrustGraphStore; use tracing::{debug, instrument}; /// EigenTrust Authority Lens: Returns the assertion with the highest /// global + domain trust-weighted score. /// /// # Resolution Strategy /// /// 1. For each candidate assertion, extract the primary signer's agent_id /// 2. Lookup the agent's EigenTrust score (global trust) /// 3. Lookup the agent's domain trust for this assertion's predicate /// 4. Calculate: `weight = confidence × eigentrust × domain_factor` /// 5. Return the assertion with highest weighted score /// 6. Tiebreaker: If scores are equal, prefer most recent timestamp /// 7. Agents with no EigenTrust score get 0.0 (Sybil protection) /// 8. Agents with no domain trust get default 0.5 (neutral) /// /// # Sybil Resistance /// /// The key insight is that isolated agents (not connected to seed trust) /// have near-zero EigenTrust scores, effectively filtering out Sybil attacks. /// /// # Example /// /// ```ignore /// use stemedb_lens::EigenTrustAuthorityLens; /// use stemedb_storage::{HybridStore, GenericTrustGraphStore, GenericDomainTrustStore}; /// use std::sync::Arc; /// /// let store = Arc::new(HybridStore::open("./data")?); /// let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); /// let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); /// let lens = EigenTrustAuthorityLens::new(trust_graph, domain_trust); /// /// let resolution = lens.resolve_async(&candidates).await; /// ``` pub struct EigenTrustAuthorityLens { trust_graph_store: Arc, domain_trust_store: Arc, } impl EigenTrustAuthorityLens { /// Create a new EigenTrustAuthorityLens with the given stores. /// /// Both stores are wrapped in Arc for shared ownership, allowing /// the lens to be used in multiple contexts. pub fn new(trust_graph_store: Arc, domain_trust_store: Arc) -> Self { Self { trust_graph_store, domain_trust_store } } /// Extract the primary agent ID from an assertion. /// /// Uses the first signature's agent_id. Returns None if no signatures exist. fn get_primary_agent(assertion: &Assertion) -> Option<[u8; 32]> { assertion.signatures.first().map(|sig| sig.agent_id) } } /// Internal struct to track assertion ranking data. #[derive(Debug)] struct RankedAssertion<'a> { assertion: &'a Assertion, eigentrust_score: f32, domain_factor: f32, weighted_score: f32, } #[async_trait] impl AsyncLens for EigenTrustAuthorityLens { #[instrument(skip(self, candidates), fields(candidates_count = candidates.len()))] async fn resolve_async(&self, candidates: &[Assertion]) -> Resolution { if candidates.is_empty() { return Resolution::empty(); } // For single candidate, still calculate weighted score if candidates.len() == 1 { let assertion = &candidates[0]; let (eigentrust_score, domain_factor, weighted_score) = self.calculate_weight(assertion).await; debug!( subject = %assertion.subject, eigentrust_score, domain_factor, weighted_score, "Single candidate resolution" ); return Resolution::with_winner(assertion.clone(), 1, weighted_score, 0.0); } // Collect trust-weighted scores for all candidates let mut ranked: Vec = Vec::with_capacity(candidates.len()); for assertion in candidates { let (eigentrust_score, domain_factor, weighted_score) = self.calculate_weight(assertion).await; debug!( subject = %assertion.subject, eigentrust_score, domain_factor, weighted_score, "Calculated weighted score" ); ranked.push(RankedAssertion { assertion, eigentrust_score, domain_factor, weighted_score, }); } // Sort by weighted score (descending), then by timestamp (descending) for ties ranked.sort_by(|a, b| { b.weighted_score .partial_cmp(&a.weighted_score) .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| b.assertion.timestamp.cmp(&a.assertion.timestamp)) }); // Select the winner (highest ranked) if let Some(winner) = ranked.first() { let conflict = compute_conflict_score(candidates); debug!( winner_subject = %winner.assertion.subject, eigentrust = winner.eigentrust_score, domain_factor = winner.domain_factor, weighted_score = winner.weighted_score, conflict, "Resolved via EigenTrust + domain authority" ); Resolution::with_winner( winner.assertion.clone(), candidates.len(), winner.weighted_score, conflict, ) } else { // Should never happen since we checked for empty candidates above Resolution::empty() } } fn name(&self) -> &'static str { "EigenTrustAuthority" } } impl EigenTrustAuthorityLens { /// Calculate the weighted score for an assertion. /// /// Returns (eigentrust_score, domain_factor, weighted_score). async fn calculate_weight(&self, assertion: &Assertion) -> (f32, f32, f32) { // Extract primary agent let agent_id = match Self::get_primary_agent(assertion) { Some(id) => id, None => { debug!( subject = %assertion.subject, "Assertion has no signatures, treating as untrusted" ); return (0.0, 1.0, 0.0); } }; // Get EigenTrust score (global trust) let eigentrust_score = match self.trust_graph_store.get_eigentrust_score(&agent_id).await { Ok(score) => score, Err(e) => { debug!( agent_id = %hex::encode(agent_id), error = %e, "Failed to get EigenTrust score, using 0.0" ); 0.0 // No EigenTrust score = untrusted (Sybil protection) } }; // Get domain factor (domain-specific expertise) let domain_factor = match self .domain_trust_store .get_effective_trust(&agent_id, &assertion.predicate, 1.0) .await { Ok(effective) => effective, // get_effective_trust returns eigentrust × factor, so with 1.0 it returns just the factor Err(e) => { debug!( agent_id = %hex::encode(agent_id), predicate = %assertion.predicate, error = %e, "Failed to get domain trust, using default factor 0.75" ); 0.75 // Default domain factor (0.5 score → 0.75 factor) } }; // Calculate weighted score // weight = confidence × eigentrust × domain_factor let weighted_score = assertion.confidence * eigentrust_score * domain_factor; (eigentrust_score, domain_factor, weighted_score) } } #[cfg(test)] mod tests { use super::*; use stemedb_core::testing::AssertionBuilder; use stemedb_storage::domain_trust_store::{DomainTrust, GenericDomainTrustStore}; use stemedb_storage::trust_graph_store::{ EigenTrustConfig, GenericTrustGraphStore, TrustEdge, TrustGraphStore, }; use stemedb_storage::HybridStore; fn agent(id: u8) -> [u8; 32] { let mut arr = [0u8; 32]; arr[0] = id; arr } fn create_assertion( subject: &str, predicate: &str, confidence: f32, agent_id: [u8; 32], timestamp: u64, ) -> Assertion { AssertionBuilder::new() .subject(subject) .predicate(predicate) .confidence(confidence) .agent_id(agent_id) .timestamp(timestamp) .build() } #[tokio::test] async fn test_empty_candidates() { let store = Arc::new(HybridStore::open_temp().expect("Failed to create store")); let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); let lens = EigenTrustAuthorityLens::new(trust_graph, domain_trust); let resolution = lens.resolve_async(&[]).await; assert!(resolution.winner.is_none()); assert_eq!(resolution.candidates_count, 0); } #[tokio::test] async fn test_single_candidate_no_eigentrust() { let store = Arc::new(HybridStore::open_temp().expect("Failed to create store")); let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); let lens = EigenTrustAuthorityLens::new(trust_graph, domain_trust); // Agent with no EigenTrust score let assertion = create_assertion("Subject", "predicate", 0.8, agent(1), 1000); let resolution = lens.resolve_async(&[assertion]).await; assert!(resolution.winner.is_some()); // No EigenTrust = 0.0, so weighted score = 0.8 * 0.0 * factor = 0.0 assert!((resolution.resolution_confidence - 0.0).abs() < 0.01); } #[tokio::test] async fn test_eigentrust_integrated() { let store = Arc::new(HybridStore::open_temp().expect("Failed to create store")); let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); let lens = EigenTrustAuthorityLens::new(Arc::clone(&trust_graph), Arc::clone(&domain_trust)); // Set up trust graph: seed → agent1 trust_graph.set_seed_trust(&agent(0), 1.0).await.expect("set seed"); trust_graph .add_trust_edge(&TrustEdge::new(agent(0), agent(1), 1.0, 1000, None)) .await .expect("add edge"); // Compute EigenTrust trust_graph.compute_eigentrust(&EigenTrustConfig::default()).await.expect("compute"); // Create assertion from agent 1 let assertion = create_assertion("Subject", "predicate", 0.8, agent(1), 1000); let resolution = lens.resolve_async(&[assertion]).await; assert!(resolution.winner.is_some()); // Agent 1 should have non-zero EigenTrust score assert!(resolution.resolution_confidence > 0.0); } #[tokio::test] async fn test_sybil_agent_gets_low_score() { let store = Arc::new(HybridStore::open_temp().expect("Failed to create store")); let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); let lens = EigenTrustAuthorityLens::new(Arc::clone(&trust_graph), Arc::clone(&domain_trust)); // Set up: seed has trust, Sybil ring is isolated trust_graph.set_seed_trust(&agent(0), 1.0).await.expect("set seed"); trust_graph .add_trust_edge(&TrustEdge::new(agent(0), agent(1), 1.0, 1000, None)) .await .expect("add edge"); // Sybil ring: 10 → 11 → 12 → 10 (not connected to seed) trust_graph .add_trust_edge(&TrustEdge::new(agent(10), agent(11), 1.0, 1000, None)) .await .expect("add edge"); trust_graph .add_trust_edge(&TrustEdge::new(agent(11), agent(12), 1.0, 1000, None)) .await .expect("add edge"); trust_graph .add_trust_edge(&TrustEdge::new(agent(12), agent(10), 1.0, 1000, None)) .await .expect("add edge"); // Compute EigenTrust trust_graph.compute_eigentrust(&EigenTrustConfig::default()).await.expect("compute"); // Legitimate agent vs Sybil agent let legitimate = create_assertion("Subject", "predicate", 0.8, agent(1), 1000); let sybil = create_assertion("Subject", "predicate", 1.0, agent(10), 1100); // Higher confidence! let resolution = lens.resolve_async(&[legitimate.clone(), sybil]).await; // Legitimate agent should win despite Sybil having higher confidence assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().unwrap().signatures[0].agent_id, agent(1)); } #[tokio::test] async fn test_domain_expertise_matters() { let store = Arc::new(HybridStore::open_temp().expect("Failed to create store")); let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); let lens = EigenTrustAuthorityLens::new(Arc::clone(&trust_graph), Arc::clone(&domain_trust)); // Set up: both agents have same EigenTrust trust_graph.set_seed_trust(&agent(0), 1.0).await.expect("set seed"); trust_graph .add_trust_edge(&TrustEdge::new(agent(0), agent(1), 1.0, 1000, None)) .await .expect("add edge"); trust_graph .add_trust_edge(&TrustEdge::new(agent(0), agent(2), 1.0, 1000, None)) .await .expect("add edge"); trust_graph.compute_eigentrust(&EigenTrustConfig::default()).await.expect("compute"); // Agent 1: Expert in medicine (score 0.95) let mut dt1 = DomainTrust::new(agent(1), "medicine".to_string(), 1000); dt1.score = 0.95; domain_trust.put_domain_trust(&dt1).await.expect("put"); // Agent 2: Novice in medicine (score 0.3) let mut dt2 = DomainTrust::new(agent(2), "medicine".to_string(), 1000); dt2.score = 0.3; domain_trust.put_domain_trust(&dt2).await.expect("put"); // Same confidence, same predicate (medicine domain) let expert_assertion = create_assertion("Drug", "treats_condition", 0.8, agent(1), 1000); let novice_assertion = create_assertion("Drug", "treats_condition", 0.8, agent(2), 1100); let resolution = lens.resolve_async(&[expert_assertion.clone(), novice_assertion]).await; // Expert should win due to higher domain trust assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().unwrap().signatures[0].agent_id, agent(1)); } #[tokio::test] async fn test_no_signatures_treated_as_untrusted() { let store = Arc::new(HybridStore::open_temp().expect("Failed to create store")); let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); let lens = EigenTrustAuthorityLens::new(Arc::clone(&trust_graph), Arc::clone(&domain_trust)); // Set up trusted agent trust_graph.set_seed_trust(&agent(0), 1.0).await.expect("set seed"); trust_graph .add_trust_edge(&TrustEdge::new(agent(0), agent(1), 1.0, 1000, None)) .await .expect("add edge"); trust_graph.compute_eigentrust(&EigenTrustConfig::default()).await.expect("compute"); let signed = create_assertion("Subject", "predicate", 0.7, agent(1), 1000); let mut unsigned = create_assertion("Subject", "predicate", 1.0, agent(99), 1100); unsigned.signatures.clear(); let resolution = lens.resolve_async(&[signed.clone(), unsigned]).await; // Signed assertion should win even with lower confidence assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().unwrap().signatures.len(), 1); } #[tokio::test] async fn test_tie_breaking_by_timestamp() { let store = Arc::new(HybridStore::open_temp().expect("Failed to create store")); let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); let lens = EigenTrustAuthorityLens::new(Arc::clone(&trust_graph), Arc::clone(&domain_trust)); // Set up: same agent makes two assertions trust_graph.set_seed_trust(&agent(0), 1.0).await.expect("set seed"); trust_graph .add_trust_edge(&TrustEdge::new(agent(0), agent(1), 1.0, 1000, None)) .await .expect("add edge"); trust_graph.compute_eigentrust(&EigenTrustConfig::default()).await.expect("compute"); // Same agent, same confidence, different timestamps let older = create_assertion("Subject", "predicate", 0.8, agent(1), 1000); let newer = create_assertion("Subject", "predicate", 0.8, agent(1), 2000); let resolution = lens.resolve_async(&[older, newer.clone()]).await; // Newer should win on tiebreak assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().unwrap().timestamp, 2000); } #[tokio::test] async fn test_lens_name() { let store = Arc::new(HybridStore::open_temp().expect("Failed to create store")); let trust_graph = Arc::new(GenericTrustGraphStore::new(store.clone())); let domain_trust = Arc::new(GenericDomainTrustStore::new(store)); let lens = EigenTrustAuthorityLens::new(trust_graph, domain_trust); assert_eq!(lens.name(), "EigenTrustAuthority"); } }