This commit includes comprehensive work on Phase 6 features: ## Admission Control (Phase 6 admission middleware) - AdmissionStore implementation backed by TrustRankStore - PoW verification with tier-based difficulty computation - Trust tier progression (Newcomer → Established → Trusted → Authority) - API integration with admission status endpoints ## HLC Recency Lens (Phase 6C) - HlcRecencyLens for distributed system ordering - Hybrid logical clock integration with causality preservation ## Cluster Coordination (Phase 6C) - Multi-node cluster tests (availability, partition tolerance) - CRDT convergence tests for anti-entropy sync - Gateway handler improvements ## Aphoria Code Linter (Phase 2A) - RFC/OWASP corpus builders with network fetching and caching - Concept hierarchy with auto-alias creation on conflict detection - Multiple security extractors (TLS, JWT, CORS, secrets, rate limiting) ## Code Organization - Split large files into modules to comply with 500-line limit - Improved test organization with separate test modules - Fixed rkyv serialization for EigenTrustState (AgentScore struct) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
480 lines
19 KiB
Rust
480 lines
19 KiB
Rust
//! 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<T, D> {
|
||
trust_graph_store: Arc<T>,
|
||
domain_trust_store: Arc<D>,
|
||
}
|
||
|
||
impl<T: TrustGraphStore, D: DomainTrustStore> EigenTrustAuthorityLens<T, D> {
|
||
/// 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<T>, domain_trust_store: Arc<D>) -> 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<T: TrustGraphStore + 'static, D: DomainTrustStore + 'static> AsyncLens
|
||
for EigenTrustAuthorityLens<T, D>
|
||
{
|
||
#[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<RankedAssertion> = 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<T: TrustGraphStore + 'static, D: DomainTrustStore + 'static> EigenTrustAuthorityLens<T, D> {
|
||
/// 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");
|
||
}
|
||
}
|