//! `agorapura-graph` — la red de confianza del ágora. //! //! Acumula [`Attestation`]s **verificadas** (una atestación con firma //! rota nunca entra) y responde preguntas de corroboración: *¿quién //! respalda este claim?* //! //! El grafo deliberadamente **no** emite un veredicto. La verdad del //! ágora no es absoluta: depende de cuánto peso le dé a cada atestador //! quien la consulta. Por eso [`TrustGraph::corroboration`] devuelve la //! evidencia cruda y [`TrustPolicy`] —un umbral *negociado*— la traduce //! a un sí/no. Dos consumidores con políticas distintas pueden mirar la //! misma red y discrepar legítimamente. #![forbid(unsafe_code)] use std::collections::HashMap; use agorapura_core::{AgoraError, Attestation, Identity, IdentityId}; use serde::{Deserialize, Serialize}; /// Evidencia acumulada a favor de un claim concreto. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Corroboration { /// Atestadores distintos que respaldan exactamente este claim. pub attesters: Vec, /// El propio sujeto figura entre los atestadores. pub self_attested: bool, } impl Corroboration { /// `true` si nadie respalda el claim. pub fn is_empty(&self) -> bool { self.attesters.is_empty() } /// Atestadores totales (incluido el sujeto si se auto-atestó). pub fn total(&self) -> usize { self.attesters.len() } /// Atestadores que no son el sujeto — la evidencia independiente. pub fn third_party(&self) -> usize { self.total() - usize::from(self.self_attested) } } /// Umbral *negociado* de aceptación de un claim. No es una verdad del /// sistema: cada consumidor adopta el suyo según lo que pacte. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct TrustPolicy { /// Atestadores terceros distintos exigidos como mínimo. pub min_third_party: usize, /// Si la auto-atestación del sujeto cuenta como respaldo válido. pub accept_self: bool, } impl TrustPolicy { /// Política estricta: al menos `n` terceros, la auto-atestación no /// suma. pub fn strict(min_third_party: usize) -> Self { Self { min_third_party, accept_self: false } } /// `true` si la evidencia satisface la política. pub fn accepts(&self, c: &Corroboration) -> bool { if c.third_party() >= self.min_third_party { return true; } self.accept_self && c.self_attested && self.min_third_party == 0 } } impl Default for TrustPolicy { /// Por defecto: un tercero basta, la auto-atestación no. fn default() -> Self { Self::strict(1) } } /// La red de confianza: identidades conocidas + atestaciones verificadas. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct TrustGraph { identities: HashMap, /// Atestaciones verificadas, en orden de inserción. attestations: Vec, } impl TrustGraph { pub fn new() -> Self { Self::default() } /// Registra (o actualiza) una identidad conocida. pub fn register(&mut self, identity: Identity) { self.identities.insert(identity.id(), identity); } /// Identidad registrada con ese id, si la hay. pub fn identity(&self, id: IdentityId) -> Option<&Identity> { self.identities.get(&id) } /// Cantidad de identidades registradas. pub fn identity_count(&self) -> usize { self.identities.len() } /// Cantidad de atestaciones verificadas almacenadas. pub fn attestation_count(&self) -> usize { self.attestations.len() } /// Verifica una atestación y, si es válida y no es duplicada exacta, /// la incorpora. Una firma rota se rechaza con error — la red sólo /// guarda evidencia comprobable. pub fn add_attestation(&mut self, att: Attestation) -> Result<(), AgoraError> { att.verify()?; if !self.attestations.contains(&att) { self.attestations.push(att); } Ok(()) } /// Atestaciones cuyo claim trata sobre `subject`. pub fn attestations_about(&self, subject: IdentityId) -> Vec<&Attestation> { self.attestations .iter() .filter(|a| a.claim.subject == subject) .collect() } /// Atestaciones emitidas por `attester`. pub fn attestations_by(&self, attester: IdentityId) -> Vec<&Attestation> { self.attestations .iter() .filter(|a| a.attester == attester) .collect() } /// Atestaciones que respaldan exactamente el claim /// `subject · predicate = value` (la marca de tiempo no importa). pub fn evidence_for( &self, subject: IdentityId, predicate: &str, value: &str, ) -> Vec<&Attestation> { self.attestations .iter() .filter(|a| { a.claim.subject == subject && a.claim.predicate == predicate && a.claim.value == value }) .collect() } /// Resume la corroboración de un claim: atestadores distintos y si /// el sujeto se lo auto-atestó. El veredicto lo pone una /// [`TrustPolicy`]. pub fn corroboration( &self, subject: IdentityId, predicate: &str, value: &str, ) -> Corroboration { let mut attesters: Vec = Vec::new(); let mut self_attested = false; for att in self.evidence_for(subject, predicate, value) { if att.is_self_attested() { self_attested = true; } if !attesters.contains(&att.attester) { attesters.push(att.attester); } } Corroboration { attesters, self_attested } } /// Atajo: `true` si la `policy` acepta el claim dada la evidencia. pub fn is_accepted( &self, subject: IdentityId, predicate: &str, value: &str, policy: &TrustPolicy, ) -> bool { policy.accepts(&self.corroboration(subject, predicate, value)) } } #[cfg(test)] mod tests { use super::*; use agorapura_core::{Attestation, Claim, IdentityKind, Keypair}; /// Mundo de prueba: Yumaira (persona) + tres atestadores. fn actors() -> (Keypair, Keypair, Keypair, Keypair) { ( Keypair::from_seed([20; 32]), // yumaira Keypair::from_seed([10; 32]), // venezuela (institución) Keypair::from_seed([30; 32]), // comunidad Keypair::from_seed([40; 32]), // vecina ) } fn attest(by: &Keypair, subject: IdentityId, pred: &str, val: &str) -> Attestation { Attestation::create(by, Claim::new(subject, pred, val, 1_700_000_000)) } #[test] fn rejects_attestation_with_broken_signature() { let (yumaira, venezuela, ..) = actors(); let mut att = attest(&venezuela, yumaira.identity_id(), "nacionalidad", "venezolana"); att.claim.value = "falsa".into(); // rompe la firma let mut g = TrustGraph::new(); assert!(g.add_attestation(att).is_err()); assert_eq!(g.attestation_count(), 0); } #[test] fn stores_and_queries_verified_attestations() { let (yumaira, venezuela, comunidad, _) = actors(); let mut g = TrustGraph::new(); g.register(venezuela.identity(IdentityKind::Institution, "Venezuela")); g.add_attestation(attest(&venezuela, yumaira.identity_id(), "nacionalidad", "venezolana")) .unwrap(); g.add_attestation(attest(&comunidad, yumaira.identity_id(), "miembro-de", "Valle")) .unwrap(); assert_eq!(g.attestations_about(yumaira.identity_id()).len(), 2); assert_eq!(g.attestations_by(venezuela.identity_id()).len(), 1); assert_eq!(g.identity_count(), 1); } #[test] fn duplicate_attestation_is_ignored() { let (yumaira, venezuela, ..) = actors(); let att = attest(&venezuela, yumaira.identity_id(), "nacionalidad", "venezolana"); let mut g = TrustGraph::new(); g.add_attestation(att.clone()).unwrap(); g.add_attestation(att).unwrap(); assert_eq!(g.attestation_count(), 1); } #[test] fn corroboration_counts_distinct_attesters() { let (yumaira, venezuela, comunidad, vecina) = actors(); let mut g = TrustGraph::new(); for who in [&venezuela, &comunidad, &vecina] { g.add_attestation(attest(who, yumaira.identity_id(), "nacionalidad", "venezolana")) .unwrap(); } let c = g.corroboration(yumaira.identity_id(), "nacionalidad", "venezolana"); assert_eq!(c.total(), 3); assert_eq!(c.third_party(), 3); assert!(!c.self_attested); } #[test] fn self_attestation_is_distinguished_from_third_party() { let (yumaira, venezuela, ..) = actors(); let mut g = TrustGraph::new(); g.add_attestation(attest(&yumaira, yumaira.identity_id(), "habilidad", "soldadura")) .unwrap(); g.add_attestation(attest(&venezuela, yumaira.identity_id(), "habilidad", "soldadura")) .unwrap(); let c = g.corroboration(yumaira.identity_id(), "habilidad", "soldadura"); assert_eq!(c.total(), 2); assert_eq!(c.third_party(), 1); assert!(c.self_attested); } #[test] fn negotiated_policy_decides_acceptance() { let (yumaira, venezuela, comunidad, _) = actors(); let mut g = TrustGraph::new(); g.add_attestation(attest(&venezuela, yumaira.identity_id(), "oficio", "partera")) .unwrap(); g.add_attestation(attest(&comunidad, yumaira.identity_id(), "oficio", "partera")) .unwrap(); let (sub, pred, val) = (yumaira.identity_id(), "oficio", "partera"); // Dos terceros: una política laxa acepta, una exigente no. assert!(g.is_accepted(sub, pred, val, &TrustPolicy::strict(2))); assert!(!g.is_accepted(sub, pred, val, &TrustPolicy::strict(3))); } #[test] fn empty_corroboration_for_unknown_claim() { let (yumaira, ..) = actors(); let g = TrustGraph::new(); let c = g.corroboration(yumaira.identity_id(), "nada", "nada"); assert!(c.is_empty() && c.third_party() == 0); } #[test] fn policy_can_accept_self_attestation_when_negotiated() { let (yumaira, ..) = actors(); let mut g = TrustGraph::new(); g.add_attestation(attest(&yumaira, yumaira.identity_id(), "lema", "sembrar")) .unwrap(); let lax = TrustPolicy { min_third_party: 0, accept_self: true }; assert!(g.is_accepted(yumaira.identity_id(), "lema", "sembrar", &lax)); // La política por defecto, sin terceros, no la acepta. assert!(!g.is_accepted(yumaira.identity_id(), "lema", "sembrar", &TrustPolicy::default())); } }