⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd
crates/modules/semantic_dht/minga-core/src/attestation.rs
Historia
Quién tocó qué
crudo
chore: monorepo inicial con arje + minga + yahweh absorbidos
1//! Atestaciones firmadas: la sustancia material de la atribución
2//! irrefutable. Una `Attestation` es una firma criptográfica sobre un
3//! `ContentHash` que vincula a su autor (un `Did`) con un fragmento
4//! concreto de contenido del repositorio.
5//!
6//! Modelo: cada hash del MST puede tener cero o más atestaciones,
7//! provenientes de autores distintos. La existencia de una atestación
8//! válida prueba que el dueño de cierta clave privada **vio y firmó
9//! exactamente ese hash** — no puede negarlo después sin admitir que
10//! filtró su llave. Es el equivalente a un commit firmado en Git pero
11//! a granularidad arbitraria: una función, un módulo, o un estado del
12//! repositorio entero.
13//!
14//! `AttestationStore` solo acepta atestaciones criptográficamente
15//! válidas: el `add` rechaza cualquier intento de inyectar firmas
16//! falsificadas. Esto convierte al store en una fuente confiable de
17//! la pregunta "¿quién ha respaldado este contenido?".
18
19use crate::cas::ContentHash;
20use crate::identity::{Did, Keypair, Signature};
21use std::collections::HashMap;
22
23#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
24pub struct Attestation {
25 pub content: ContentHash,
26 pub author: Did,
27 pub signature: Signature,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum AttestationError {
32 InvalidSignature,
33}
34
35impl std::fmt::Display for AttestationError {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::InvalidSignature => write!(f, "firma de la atestación no verifica"),
39 }
40 }
41}
42
43impl std::error::Error for AttestationError {}
44
45impl Attestation {
46 /// Crea una atestación firmando el `ContentHash` con la `Keypair`
47 /// del autor. El `Did` queda registrado a partir de la `Keypair`
48 /// — no se acepta un `Did` arbitrario, lo que descarta de raíz
49 /// las atestaciones donde alguien dice ser otro.
50 pub fn create(keypair: &Keypair, content: ContentHash) -> Self {
51 Self {
52 content,
53 author: keypair.did(),
54 signature: keypair.sign(&content.0),
55 }
56 }
57
58 /// Verifica que `signature` es una firma válida sobre `content`
59 /// hecha con la llave privada del `author`. Cualquier modificación
60 /// de cualquiera de los tres campos invalida la atestación.
61 pub fn verify(&self) -> bool {
62 self.author.verify(&self.content.0, &self.signature)
63 }
64}
65
66/// Registro de atestaciones por `ContentHash`.
67///
68/// Idempotente por `(author, content)`: insertar dos veces la misma
69/// atestación no la duplica. Pero un mismo `ContentHash` puede tener
70/// atestaciones de **autores distintos** — es la base de los "filtros
71/// de convergencia" del spec, donde el peso de un cambio se mide por
72/// cuántas identidades reputadas lo respaldan.
73#[derive(Debug, Default, Clone)]
74pub struct AttestationStore {
75 by_content: HashMap<ContentHash, Vec<Attestation>>,
76}
77
78impl AttestationStore {
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 /// Inserta una atestación. Devuelve `Err(InvalidSignature)` si la
84 /// firma no verifica — el store NUNCA almacena firmas rotas, así
85 /// que cualquier consulta posterior puede confiar en lo que lee.
86 pub fn add(&mut self, att: Attestation) -> Result<(), AttestationError> {
87 if !att.verify() {
88 return Err(AttestationError::InvalidSignature);
89 }
90 let entry = self.by_content.entry(att.content).or_default();
91 if !entry.iter().any(|a| a.author == att.author) {
92 entry.push(att);
93 }
94 Ok(())
95 }
96
97 pub fn get(&self, content: &ContentHash) -> &[Attestation] {
98 self.by_content
99 .get(content)
100 .map(Vec::as_slice)
101 .unwrap_or(&[])
102 }
103
104 /// Conjunto de DIDs que han atestado este contenido. Cada autor
105 /// aparece como máximo una vez (deduplicación por `add`).
106 pub fn authors_of(&self, content: &ContentHash) -> Vec<Did> {
107 self.by_content
108 .get(content)
109 .map(|v| v.iter().map(|a| a.author).collect())
110 .unwrap_or_default()
111 }
112
113 pub fn len(&self) -> usize {
114 self.by_content.values().map(Vec::len).sum()
115 }
116
117 pub fn is_empty(&self) -> bool {
118 self.by_content.values().all(Vec::is_empty)
119 }
120
121 /// Itera todas las atestaciones del store (orden no especificado).
122 /// Usado por el protocolo de sync para enumerar lo que tenemos y
123 /// empujarlo al peer.
124 pub fn all(&self) -> impl Iterator<Item = &Attestation> + '_ {
125 self.by_content.values().flat_map(|v| v.iter())
126 }
127}