stemedb/crates/stemedb-lens/src/eigentrust_authority.rs
jordan d3a88585fe feat: Phase 6 UAT - Admission control, HLC recency, cluster coordination
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>
2026-02-03 00:43:37 -07:00

480 lines
19 KiB
Rust
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.

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