tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ d3649bfd1a80153652c2a028315369bb4b184024 crates/modules/semantic_dht/minga-p2p/tests/sync_invariants.rs Rust · 798 líneas · 28.7 KB Historia Quién tocó qué crudo

chore: monorepo inicial con arje + minga + yahweh absorbidos 53dbdf0f1 · Sergio ·

1//! Invariantes del protocolo de sincronización recursivo.
2//!
3//! Tres familias de tests:
4//! - **Convergencia funcional**: tras `run_sync`, ambos peers tienen
5//!   el mismo `root_hash`, `MemStore` equivalente, y reconstruyen los
6//!   árboles bit a bit.
7//! - **Eficiencia estructural**: el short-circuit por hash de subárbol
8//!   reduce probes y delivers cuando los repos comparten ramas.
9//! - **Seguridad**: el receptor verifica `hash_stored(stored) == hash`
10//!   y rechaza nodos manipulados.
11
12use minga_core::{
13    cas::hash_components, hash_node, hash_stored, parse, ContentHash, Keypair, MemStore, Mst,
14    NodeStore, Signature, StoredNode,
15};
16use minga_p2p::{run_sync, Message, SyncSession};
17
18fn kp(seed: u8) -> Keypair {
19    Keypair::from_seed(&[seed; 32])
20}
21
22/// Helper que replica la construcción del payload firmado del `Hello`
23/// dentro del protocolo Minga. Usado por los tests que inyectan
24/// mensajes manualmente.
25fn hello_payload(nonce: &[u8; 32], did: &minga_core::Did, root: &ContentHash) -> [u8; 96] {
26    let mut p = [0u8; 96];
27    p[..32].copy_from_slice(nonce);
28    p[32..64].copy_from_slice(&did.0);
29    p[64..96].copy_from_slice(&root.0);
30    p
31}
32
33fn build_repo(sources: &[&str]) -> (Mst, MemStore, Vec<ContentHash>) {
34    let mut mst = Mst::new();
35    let mut store = MemStore::new();
36    let mut roots = Vec::new();
37    for src in sources {
38        let n = parse::rust(src).unwrap();
39        let h = store.put(&n);
40        mst.insert(h);
41        roots.push(h);
42    }
43    (mst, store, roots)
44}
45
46// ─── Convergencia funcional ────────────────────────────────────────
47
48#[test]
49fn sync_identical_is_noop() {
50    let sources = &[
51        "fn add(x: i32, y: i32) -> i32 { x + y }",
52        "fn neg(x: i32) -> i32 { -x }",
53    ];
54    let (mst_a, store_a, _) = build_repo(sources);
55    let (mst_b, store_b, _) = build_repo(sources);
56
57    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
58    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
59    let stats = run_sync(&mut a, &mut b);
60
61    // Mismas raíces de MST: el short-circuit en Hello evita cualquier
62    // probe o transferencia. Solo cruzan los 2 Hellos y los 2 Dones.
63    assert_eq!(stats.hellos, 2);
64    assert_eq!(stats.probe_reqs, 0);
65    assert_eq!(stats.probe_ress, 0);
66    assert_eq!(stats.fetches, 0);
67    assert_eq!(stats.delivers, 0);
68    assert_eq!(stats.dones, 2);
69
70    assert_eq!(a.mst().root_hash(), b.mst().root_hash());
71}
72
73#[test]
74fn sync_one_empty_pulls_everything() {
75    let sources = &["fn f(x: i32) -> i32 { x * 2 }"];
76    let (mst_a, store_a, _) = build_repo(sources);
77    let (mst_b, store_b, _) = build_repo(&[]);
78
79    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
80    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
81    run_sync(&mut a, &mut b);
82
83    assert_eq!(a.mst().root_hash(), b.mst().root_hash());
84    assert_eq!(a.store().len(), b.store().len());
85
86    for h in a.mst().iter() {
87        assert!(b.store().contains(h));
88        let a_tree = a.store().reconstruct(h).unwrap();
89        let b_tree = b.store().reconstruct(h).unwrap();
90        assert_eq!(a_tree, b_tree);
91    }
92}
93
94#[test]
95fn sync_disjoint_sets_merge() {
96    let only_a = &[
97        "fn alpha() -> i32 { 1 }",
98        "fn beta(x: i32) -> i32 { x + 1 }",
99    ];
100    let only_b = &[
101        "fn gamma(y: i32) -> bool { y > 0 }",
102        "fn delta() -> &'static str { \"hello\" }",
103    ];
104
105    let (mst_a, store_a, _) = build_repo(only_a);
106    let (mst_b, store_b, _) = build_repo(only_b);
107
108    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
109    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
110    run_sync(&mut a, &mut b);
111
112    assert_eq!(a.mst().root_hash(), b.mst().root_hash());
113    assert_eq!(a.mst().len(), 4);
114    assert_eq!(b.mst().len(), 4);
115}
116
117#[test]
118fn sync_partial_overlap_converges() {
119    let common = &[
120        "fn shared_one() -> i32 { 42 }",
121        "fn shared_two(n: i32) -> i32 { n + 1 }",
122    ];
123    let extra_a = &["fn only_in_a() -> bool { true }"];
124    let extra_b = &["fn only_in_b(s: &str) -> usize { s.len() }"];
125
126    let mut sources_a: Vec<&str> = common.to_vec();
127    sources_a.extend_from_slice(extra_a);
128    let mut sources_b: Vec<&str> = common.to_vec();
129    sources_b.extend_from_slice(extra_b);
130
131    let (mst_a, store_a, _) = build_repo(&sources_a);
132    let (mst_b, store_b, _) = build_repo(&sources_b);
133
134    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
135    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
136    run_sync(&mut a, &mut b);
137
138    assert_eq!(a.mst().root_hash(), b.mst().root_hash());
139    assert_eq!(a.mst().len(), 4);
140}
141
142#[test]
143fn sync_transitive_children_pulled() {
144    let big_src = r#"
145        fn complicated(x: i32, y: i32) -> i32 {
146            let a = x + y;
147            let b = a * 2;
148            match b {
149                n if n > 100 => n - 50,
150                n if n < 0 => -n,
151                _ => b,
152            }
153        }
154    "#;
155    let (mst_a, store_a, roots) = build_repo(&[big_src]);
156    let store_a_size = store_a.len();
157    let root_hash = roots[0];
158
159    let (mst_b, store_b, _) = build_repo(&[]);
160
161    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
162    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
163    run_sync(&mut a, &mut b);
164
165    assert!(b.store().contains(&root_hash));
166    assert_eq!(b.store().len(), store_a_size);
167
168    let a_tree = a.store().reconstruct(&root_hash).unwrap();
169    let b_tree = b.store().reconstruct(&root_hash).unwrap();
170    assert_eq!(a_tree, b_tree);
171}
172
173#[test]
174fn sync_idempotent_after_convergence() {
175    let sources = &["fn p() -> i32 { 1 }", "fn q(x: i32) -> i32 { x + 1 }"];
176    let (mst_a, store_a, _) = build_repo(sources);
177    let (mst_b, store_b, _) = build_repo(&["fn r(y: i32) -> i32 { y - 1 }"]);
178
179    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
180    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
181    run_sync(&mut a, &mut b);
182
183    let (mst_a, store_a, _) = a.into_parts();
184    let (mst_b, store_b, _) = b.into_parts();
185    let mut a2 = SyncSession::without_attestations(mst_a, store_a, kp(1));
186    let mut b2 = SyncSession::without_attestations(mst_b, store_b, kp(2));
187    let stats = run_sync(&mut a2, &mut b2);
188
189    // Tras converger, la segunda corrida es 2 Hellos + 2 Dones, nada
190    // estructural ni transferencias.
191    assert_eq!(stats.probe_reqs, 0);
192    assert_eq!(stats.probe_ress, 0);
193    assert_eq!(stats.fetches, 0);
194    assert_eq!(stats.delivers, 0);
195    assert_eq!(stats.hellos, 2);
196    assert_eq!(stats.dones, 2);
197}
198
199#[test]
200fn sync_both_empty_terminates() {
201    let mut a = SyncSession::without_attestations(Mst::new(), MemStore::new(), kp(1));
202    let mut b = SyncSession::without_attestations(Mst::new(), MemStore::new(), kp(2));
203    let stats = run_sync(&mut a, &mut b);
204    assert_eq!(stats.hellos, 2);
205    assert_eq!(stats.probe_reqs, 0);
206    assert_eq!(stats.dones, 2);
207    assert!(a.mst().is_empty());
208    assert!(b.mst().is_empty());
209}
210
211#[test]
212fn sync_three_way_via_pairwise_runs() {
213    let sources_a = &["fn a1() -> i32 { 1 }", "fn shared() -> i32 { 0 }"];
214    let sources_b = &["fn b1(x: i32) -> i32 { x }", "fn shared() -> i32 { 0 }"];
215    let sources_c = &["fn c1() -> bool { true }"];
216
217    let (mst_a, store_a, _) = build_repo(sources_a);
218    let (mst_b, store_b, _) = build_repo(sources_b);
219    let (mst_c, store_c, _) = build_repo(sources_c);
220
221    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
222    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
223    run_sync(&mut a, &mut b);
224    let (mst_a, store_a, _) = a.into_parts();
225    let (mst_b, store_b, _) = b.into_parts();
226
227    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
228    let mut c = SyncSession::without_attestations(mst_c, store_c, kp(3));
229    run_sync(&mut b, &mut c);
230    let (mst_b, _, _) = b.into_parts();
231    let (mst_c, store_c, _) = c.into_parts();
232
233    let mut c = SyncSession::without_attestations(mst_c, store_c, kp(3));
234    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
235    run_sync(&mut c, &mut a);
236    let (mst_c, _, _) = c.into_parts();
237    let (mst_a, _, _) = a.into_parts();
238
239    assert_eq!(mst_a.root_hash(), mst_b.root_hash());
240    assert_eq!(mst_b.root_hash(), mst_c.root_hash());
241    assert_eq!(mst_a.len(), 4);
242}
243
244// ─── Eficiencia estructural ────────────────────────────────────────
245
246#[test]
247fn sync_subtree_short_circuit_skips_shared_branches() {
248    // Construimos dos repos que comparten muchos nodos pero difieren en
249    // uno. El short-circuit por hash de subárbol debería podar las
250    // ramas compartidas: el número de probes y delivers debe estar
251    // dominado por la divergencia, no por el tamaño total.
252    let common: Vec<String> = (0..50)
253        .map(|i| format!("fn shared_{}() -> i32 {{ {} }}", i, i))
254        .collect();
255    let common_refs: Vec<&str> = common.iter().map(|s| s.as_str()).collect();
256
257    let extra_a = "fn only_a() -> bool { true }".to_string();
258    let mut sources_a: Vec<&str> = common_refs.clone();
259    sources_a.push(&extra_a);
260
261    let extra_b = "fn only_b() -> bool { false }".to_string();
262    let mut sources_b: Vec<&str> = common_refs.clone();
263    sources_b.push(&extra_b);
264
265    let (mst_a, store_a, _) = build_repo(&sources_a);
266    let (mst_b, store_b, _) = build_repo(&sources_b);
267    let store_a_size = store_a.len();
268
269    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
270    let mut b = SyncSession::without_attestations(mst_b, store_b, kp(2));
271    let stats = run_sync(&mut a, &mut b);
272
273    assert_eq!(a.mst().root_hash(), b.mst().root_hash());
274
275    // Cota de eficiencia: cada peer debe pedir como máximo lo que
276    // realmente le falta. En este escenario, cada peer ignora una sola
277    // función nueva (~docena de StoredNodes). Si el short-circuit
278    // estuviera roto, transferiríamos cerca del store entero (~varios
279    // cientos). La cota es laxa pero detectaría esa regresión.
280    assert!(
281        stats.delivers < store_a_size / 2,
282        "demasiados delivers ({}); esperaba << {}",
283        stats.delivers,
284        store_a_size,
285    );
286}
287
288// ─── Seguridad: verificación criptográfica ─────────────────────────
289
290#[test]
291fn cas_hash_node_equals_hash_stored() {
292    // El invariante fundacional para verificación: hashear el árbol
293    // como `SemanticNode` y como `StoredNode` produce idéntico hash.
294    // Sin esto, el receptor no podría confiar en lo que recibe.
295    let node = parse::rust("fn add(x: i32, y: i32) -> i32 { x + y }").unwrap();
296    let direct = hash_node(&node);
297
298    let mut store = MemStore::new();
299    let via_store = store.put(&node);
300    assert_eq!(direct, via_store);
301
302    let stored = store.get(&direct).unwrap();
303    let recomputed = hash_stored(stored);
304    assert_eq!(direct, recomputed);
305}
306
307#[test]
308fn sync_rejects_tampered_deliver() {
309    // Construimos un mensaje Deliver donde `hash` y `stored` no son
310    // consistentes — simulando un peer malicioso o un bit flip en el
311    // transporte. La sesión debe rechazarlo y no contaminar su estado.
312    let (mst_a, store_a, _) = build_repo(&[]);
313    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
314    let initial_store_size = a.store().len();
315    let initial_mst_size = a.mst().len();
316
317    // Forjamos un StoredNode con identidad falsa: anunciamos un hash
318    // arbitrario pero adjuntamos contenido distinto.
319    let fake_stored = StoredNode {
320        kind: "function_item".to_string(),
321        field_name: None,
322        leaf_text: None,
323        children: Vec::new(),
324    };
325    // El hash real de fake_stored es x; anunciamos como otra cosa.
326    let real_hash = hash_components("function_item", None, None, &[]);
327    let bogus_hash = ContentHash([0xAB; 32]);
328    assert_ne!(real_hash, bogus_hash);
329
330    // Inyectamos como si viniera del peer (sesión recibe Hello primero
331    // para que received_hello sea true; luego le metemos el Deliver
332    // tóxico). El Hello se firma con la llave del peer simulado.
333    let peer_kp = kp(99);
334    let peer_root = minga_core::empty_subtree_hash();
335    let peer_sig = peer_kp.sign(peer_root.as_bytes());
336    a.handle(Message::Hello {
337        peer_did: peer_kp.did(),
338        root_subtree_hash: peer_root,
339        signature: peer_sig,
340    });
341    let _ = a.handle(Message::Deliver {
342        hash: bogus_hash,
343        stored: fake_stored,
344    });
345
346    // El store y MST no deben cambiar; el contador de rechazos sí.
347    assert_eq!(a.store().len(), initial_store_size);
348    assert_eq!(a.mst().len(), initial_mst_size);
349    assert_eq!(a.rejected_delivers(), 1);
350    assert!(!a.store().contains(&bogus_hash));
351}
352
353#[test]
354fn sync_accepts_well_formed_deliver() {
355    // Contraprueba del anterior: un Deliver con hash válido sí se
356    // acepta. Verifica que el rechazo es selectivo, no global.
357    let (mst_a, store_a, _) = build_repo(&[]);
358    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
359
360    let stored = StoredNode {
361        kind: "integer_literal".to_string(),
362        field_name: None,
363        leaf_text: Some(b"42".to_vec()),
364        children: Vec::new(),
365    };
366    let real_hash = hash_stored(&stored);
367
368    let peer_kp = kp(99);
369    let peer_root = minga_core::empty_subtree_hash();
370    let peer_sig = peer_kp.sign(peer_root.as_bytes());
371    a.handle(Message::Hello {
372        peer_did: peer_kp.did(),
373        root_subtree_hash: peer_root,
374        signature: peer_sig,
375    });
376    a.handle(Message::Deliver {
377        hash: real_hash,
378        stored,
379    });
380
381    // No estaba en awaiting_root (no llegó por probe), así que no
382    // entra al MST — pero sí al store.
383    assert!(a.store().contains(&real_hash));
384    assert_eq!(a.rejected_delivers(), 0);
385}
386
387// ─── Identidad y autenticación ─────────────────────────────────────
388
389#[test]
390fn sync_captures_peer_did_after_valid_hello() {
391    // Tras un sync exitoso, cada sesión conoce el DID del otro peer
392    // — la primera afirmación criptográficamente verificable de la
393    // identidad del interlocutor.
394    let sources = &["fn f() -> i32 { 1 }"];
395    let (mst_a, store_a, _) = build_repo(sources);
396    let (mst_b, store_b, _) = build_repo(sources);
397
398    let kp_a = kp(10);
399    let kp_b = kp(20);
400    let did_a = kp_a.did();
401    let did_b = kp_b.did();
402
403    let mut a = SyncSession::without_attestations(mst_a, store_a, kp_a);
404    let mut b = SyncSession::without_attestations(mst_b, store_b, kp_b);
405
406    assert_eq!(a.peer_did(), None);
407    assert_eq!(b.peer_did(), None);
408
409    run_sync(&mut a, &mut b);
410
411    // Cada peer ahora tiene la identidad verificada del otro.
412    assert_eq!(a.peer_did(), Some(did_b));
413    assert_eq!(b.peer_did(), Some(did_a));
414    assert_eq!(a.local_did(), did_a);
415    assert_eq!(b.local_did(), did_b);
416}
417
418#[test]
419fn sync_rejects_hello_with_tampered_signature() {
420    // Un atacante que captura un Hello legítimo pero modifica un byte
421    // de la firma debe ser rechazado. La sesión no marca
422    // received_hello, no procesa el root, no emite ProbeReq — el
423    // contador de rechazos se incrementa en su lugar.
424    let (mst_a, store_a, _) = build_repo(&[]);
425    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
426
427    let attacker = kp(2);
428    let root = minga_core::empty_subtree_hash();
429    let mut sig = attacker.sign(root.as_bytes());
430    sig.0[5] ^= 0xFF;
431
432    let out = a.handle(Message::Hello {
433        peer_did: attacker.did(),
434        root_subtree_hash: root,
435        signature: sig,
436    });
437
438    assert!(out.is_empty(), "Hello con firma rota no debe producir respuesta");
439    assert_eq!(a.rejected_hellos(), 1);
440    assert_eq!(a.peer_did(), None);
441}
442
443#[test]
444fn sync_rejects_hello_with_swapped_did() {
445    // Otro vector: la firma es válida bajo el DID original, pero el
446    // atacante reemplaza el campo `peer_did` por uno distinto. La
447    // verificación falla porque la firma no fue producida por la
448    // llave privada correspondiente al DID anunciado.
449    let (mst_a, store_a, _) = build_repo(&[]);
450    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
451
452    let real_signer = kp(50);
453    let imposter = kp(51);
454    let root = minga_core::empty_subtree_hash();
455    let sig = real_signer.sign(root.as_bytes());
456
457    a.handle(Message::Hello {
458        peer_did: imposter.did(), // dice ser imposter pero la firma es de real_signer
459        root_subtree_hash: root,
460        signature: sig,
461    });
462
463    assert_eq!(a.rejected_hellos(), 1);
464    assert_eq!(a.peer_did(), None);
465}
466
467#[test]
468fn sync_rejects_hello_signed_over_different_root() {
469    // El atacante firma un root diferente al que anuncia. La firma es
470    // válida sobre `wrong_root`, pero el mensaje dice `claimed_root`.
471    let (mst_a, store_a, _) = build_repo(&[]);
472    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
473
474    let signer = kp(60);
475    let claimed_root = ContentHash([0xAA; 32]);
476    let wrong_root = ContentHash([0xBB; 32]);
477    let sig_over_wrong = signer.sign(wrong_root.as_bytes());
478
479    a.handle(Message::Hello {
480        peer_did: signer.did(),
481        root_subtree_hash: claimed_root,
482        signature: sig_over_wrong,
483    });
484
485    assert_eq!(a.rejected_hellos(), 1);
486    assert_eq!(a.peer_did(), None);
487}
488
489#[test]
490fn sync_rejects_replay_of_hello_from_different_session() {
491    // El test del bloque CRÍTICO: anti-replay anti-replay.
492    //
493    // Sesión 1: el peer "alice" responde a un Challenge de A1
494    // firmando un Hello con el nonce de A1.
495    //
496    // Sesión 2: la misma A vuelve a abrir sesión (A2). A2 genera un
497    // nonce nuevo. Un atacante intenta replicar el Hello capturado de
498    // la sesión 1. Como el nonce es distinto, la firma no verifica.
499    let alice = kp(50);
500    let alice_root = ContentHash([0xAA; 32]);
501
502    // Sesión 1.
503    let mut a1 = SyncSession::without_attestations(Mst::new(), MemStore::new(), kp(1));
504    let nonce_a1 = a1.self_nonce();
505
506    // Alice firma su Hello sobre el nonce que A1 emitió.
507    let payload_1 = hello_payload(&nonce_a1, &alice.did(), &alice_root);
508    let sig_1 = alice.sign(&payload_1);
509    let captured_hello = Message::Hello {
510        peer_did: alice.did(),
511        root_subtree_hash: alice_root,
512        signature: sig_1,
513    };
514
515    // En sesión 1, el Hello se acepta limpiamente.
516    a1.handle(captured_hello.clone());
517    assert_eq!(a1.peer_did(), Some(alice.did()));
518    assert_eq!(a1.rejected_hellos(), 0);
519
520    // Sesión 2: A2 con nonce nuevo. El atacante replica `captured_hello`.
521    let mut a2 = SyncSession::without_attestations(Mst::new(), MemStore::new(), kp(2));
522    assert_ne!(a2.self_nonce(), nonce_a1, "los nonces son distintos por sesión");
523
524    a2.handle(captured_hello);
525
526    // Replay rechazado: la firma estaba sobre nonce_a1, A2 verifica
527    // contra su propio nonce, mismatch criptográfico.
528    assert_eq!(a2.rejected_hellos(), 1);
529    assert_eq!(a2.peer_did(), None);
530}
531
532#[test]
533fn sync_proceeds_after_valid_hello_following_rejection() {
534    // Si llega un Hello inválido seguido de uno válido, la sesión se
535    // recupera: acepta el válido y captura ese DID. No hay
536    // "envenenamiento" persistente del estado.
537    let (mst_a, store_a, _) = build_repo(&[]);
538    let mut a = SyncSession::without_attestations(mst_a, store_a, kp(1));
539
540    let bad_signer = kp(70);
541    let mut bad_sig = bad_signer.sign(b"otro mensaje");
542    bad_sig.0[0] ^= 0xFF;
543    let root = minga_core::empty_subtree_hash();
544    a.handle(Message::Hello {
545        peer_did: bad_signer.did(),
546        root_subtree_hash: root,
547        signature: bad_sig,
548    });
549    assert_eq!(a.rejected_hellos(), 1);
550    assert_eq!(a.peer_did(), None);
551
552    let good_signer = kp(71);
553    let nonce = a.self_nonce();
554    let good_payload = hello_payload(&nonce, &good_signer.did(), &root);
555    let good_sig = good_signer.sign(&good_payload);
556    a.handle(Message::Hello {
557        peer_did: good_signer.did(),
558        root_subtree_hash: root,
559        signature: good_sig,
560    });
561    assert_eq!(a.rejected_hellos(), 1);
562    assert_eq!(a.peer_did(), Some(good_signer.did()));
563}
564
565// Aux: dejamos `Signature` importado para que el bloque arriba siga
566// compilando en futuras refactorizaciones que lo necesiten.
567#[allow(dead_code)]
568fn _signature_marker(_: Signature) {}
569
570// ─── Propagación de atestaciones ───────────────────────────────────
571
572use minga_core::{Attestation, AttestationStore, Did};
573
574fn build_repo_with_attests(
575    sources: &[&str],
576    signers: &[&Keypair],
577) -> (Mst, MemStore, AttestationStore, Vec<ContentHash>) {
578    let mut mst = Mst::new();
579    let mut store = MemStore::new();
580    let mut attests = AttestationStore::new();
581    let mut roots = Vec::new();
582    for src in sources {
583        let n = parse::rust(src).unwrap();
584        let h = store.put(&n);
585        mst.insert(h);
586        for kp in signers {
587            attests.add(Attestation::create(kp, h)).unwrap();
588        }
589        roots.push(h);
590    }
591    (mst, store, attests, roots)
592}
593
594#[test]
595fn sync_propagates_attestations_for_owned_content() {
596    // Cada peer tiene su propio contenido y firma sus propias claves.
597    // Tras sync, ambos peers conocen ambas atestaciones.
598    let kp_a = kp(10);
599    let kp_b = kp(20);
600
601    let (mst_a, store_a, atts_a, roots_a) =
602        build_repo_with_attests(&["fn from_a() -> i32 { 1 }"], &[&kp_a]);
603    let (mst_b, store_b, atts_b, roots_b) =
604        build_repo_with_attests(&["fn from_b() -> i32 { 2 }"], &[&kp_b]);
605
606    let mut a = SyncSession::new(mst_a, store_a, atts_a, kp_a.clone());
607    let mut b = SyncSession::new(mst_b, store_b, atts_b, kp_b.clone());
608    run_sync(&mut a, &mut b);
609
610    // A debe ahora conocer la atestación de B sobre roots_b[0], y
611    // viceversa. Ambas verificables criptográficamente.
612    let h_a = roots_a[0];
613    let h_b = roots_b[0];
614
615    let a_authors_for_a: Vec<Did> = a.attestations().authors_of(&h_a);
616    let a_authors_for_b: Vec<Did> = a.attestations().authors_of(&h_b);
617    assert_eq!(a_authors_for_a, vec![kp_a.did()]);
618    assert_eq!(a_authors_for_b, vec![kp_b.did()]);
619
620    let b_authors_for_a: Vec<Did> = b.attestations().authors_of(&h_a);
621    let b_authors_for_b: Vec<Did> = b.attestations().authors_of(&h_b);
622    assert_eq!(b_authors_for_a, vec![kp_a.did()]);
623    assert_eq!(b_authors_for_b, vec![kp_b.did()]);
624}
625
626#[test]
627fn sync_merges_multiple_authors_for_shared_content() {
628    // Ambos peers tienen el MISMO contenido (mismo hash) pero
629    // atestaciones de autores DISTINTOS. Tras sync, cada peer ve el
630    // conjunto completo de autores que han respaldado ese contenido.
631    let kp_a = kp(30);
632    let kp_b = kp(40);
633    let kp_c = kp(50);
634    let kp_d = kp(60);
635
636    let src = "fn shared() -> i32 { 99 }";
637
638    // A tiene firmas de A y C sobre el contenido.
639    let (mst_a, store_a, atts_a, _) = build_repo_with_attests(&[src], &[&kp_a, &kp_c]);
640    // B tiene firmas de B y D sobre el MISMO contenido.
641    let (mst_b, store_b, atts_b, roots_b) = build_repo_with_attests(&[src], &[&kp_b, &kp_d]);
642    let h = roots_b[0];
643
644    let mut a = SyncSession::new(mst_a, store_a, atts_a, kp_a.clone());
645    let mut b = SyncSession::new(mst_b, store_b, atts_b, kp_b.clone());
646    run_sync(&mut a, &mut b);
647
648    // Ambos peers ven los cuatro autores.
649    let mut a_authors = a.attestations().authors_of(&h);
650    let mut b_authors = b.attestations().authors_of(&h);
651    a_authors.sort_by_key(|d| d.0);
652    b_authors.sort_by_key(|d| d.0);
653    assert_eq!(a_authors, b_authors);
654    assert_eq!(a_authors.len(), 4);
655    assert!(a_authors.contains(&kp_a.did()));
656    assert!(a_authors.contains(&kp_b.did()));
657    assert!(a_authors.contains(&kp_c.did()));
658    assert!(a_authors.contains(&kp_d.did()));
659}
660
661#[test]
662fn sync_attestations_are_verified_at_receiver() {
663    // Inyectamos manualmente un AttestPush con una firma corrupta
664    // entre las legítimas. La sesión solo acepta las legítimas e
665    // incrementa rejected_attests.
666    let mut a = SyncSession::without_attestations(Mst::new(), MemStore::new(), kp(1));
667
668    // Hello válido del peer simulado, para que received_hello sea true.
669    let peer_kp = kp(80);
670    let peer_root = minga_core::empty_subtree_hash();
671    let nonce = a.self_nonce();
672    let peer_payload = hello_payload(&nonce, &peer_kp.did(), &peer_root);
673    let peer_sig = peer_kp.sign(&peer_payload);
674    a.handle(Message::Hello {
675        peer_did: peer_kp.did(),
676        root_subtree_hash: peer_root,
677        signature: peer_sig,
678    });
679
680    // Tres atestaciones: dos legítimas y una con firma rota.
681    let alice = kp(81);
682    let bob = kp(82);
683    let h1 = ContentHash([1u8; 32]);
684    let h2 = ContentHash([2u8; 32]);
685    let h3 = ContentHash([3u8; 32]);
686
687    let valid1 = Attestation::create(&alice, h1);
688    let valid2 = Attestation::create(&bob, h2);
689    let mut tampered = Attestation::create(&alice, h3);
690    tampered.signature.0[10] ^= 0xFF;
691
692    a.handle(Message::AttestPush {
693        attestations: vec![valid1.clone(), tampered, valid2.clone()],
694    });
695
696    // Las dos válidas se mergean; la corrupta se rechaza.
697    assert_eq!(a.attestations().len(), 2);
698    assert_eq!(a.rejected_attests(), 1);
699    assert_eq!(a.attestations().authors_of(&h1), vec![alice.did()]);
700    assert_eq!(a.attestations().authors_of(&h2), vec![bob.did()]);
701    assert!(a.attestations().get(&h3).is_empty());
702}
703
704#[test]
705fn sync_attest_push_before_hello_is_rejected() {
706    // Una atestación que llega antes del Hello autenticado se descarta
707    // — no podemos confiar en lo que dice el remitente hasta saber
708    // quién es.
709    let mut a = SyncSession::without_attestations(Mst::new(), MemStore::new(), kp(1));
710
711    let alice = kp(90);
712    let h = ContentHash([7u8; 32]);
713    let att = Attestation::create(&alice, h);
714
715    let out = a.handle(Message::AttestPush {
716        attestations: vec![att],
717    });
718    assert!(out.is_empty());
719    assert_eq!(a.rejected_attests(), 1);
720    assert_eq!(a.attestations().len(), 0);
721}
722
723#[test]
724fn sync_attestations_are_idempotent_across_runs() {
725    // Re-correr el sync no duplica atestaciones (gracias a la
726    // idempotencia de AttestationStore::add por (autor, contenido)).
727    let kp_a = kp(100);
728    let kp_b = kp(101);
729
730    let (mst_a, store_a, atts_a, _) =
731        build_repo_with_attests(&["fn run_one() -> i32 { 1 }"], &[&kp_a]);
732    let (mst_b, store_b, atts_b, _) =
733        build_repo_with_attests(&["fn run_two() -> i32 { 2 }"], &[&kp_b]);
734
735    let mut a = SyncSession::new(mst_a, store_a, atts_a, kp_a.clone());
736    let mut b = SyncSession::new(mst_b, store_b, atts_b, kp_b.clone());
737    run_sync(&mut a, &mut b);
738    let after_first_a = a.attestations().len();
739    let after_first_b = b.attestations().len();
740    assert_eq!(after_first_a, 2);
741    assert_eq!(after_first_b, 2);
742
743    let (mst_a, store_a, atts_a) = a.into_parts();
744    let (mst_b, store_b, atts_b) = b.into_parts();
745    let mut a2 = SyncSession::new(mst_a, store_a, atts_a, kp_a);
746    let mut b2 = SyncSession::new(mst_b, store_b, atts_b, kp_b);
747    run_sync(&mut a2, &mut b2);
748
749    assert_eq!(a2.attestations().len(), after_first_a);
750    assert_eq!(b2.attestations().len(), after_first_b);
751}
752
753#[test]
754fn sync_attestations_about_remote_content() {
755    // Caso interesante: A tiene una atestación sobre contenido que
756    // **NO** posee (lo recibió por gossip de un tercero). Tras sync
757    // con B, B aprende esa atestación aunque A nunca tuvo el contenido
758    // en su store.
759    let kp_a = kp(110);
760    let kp_third_party = kp(111);
761
762    // A no tiene contenido propio pero sí una atestación de
763    // `kp_third_party` sobre un hash arbitrario.
764    let phantom_hash = ContentHash([0xCD; 32]);
765    let mut atts_a = AttestationStore::new();
766    atts_a
767        .add(Attestation::create(&kp_third_party, phantom_hash))
768        .unwrap();
769
770    let kp_b = kp(112);
771    let mut a = SyncSession::new(Mst::new(), MemStore::new(), atts_a, kp_a);
772    let mut b = SyncSession::without_attestations(Mst::new(), MemStore::new(), kp_b);
773    run_sync(&mut a, &mut b);
774
775    // B ahora conoce la atestación, aunque ni A ni B tienen el
776    // contenido en su store.
777    assert_eq!(b.attestations().len(), 1);
778    assert_eq!(b.attestations().authors_of(&phantom_hash), vec![kp_third_party.did()]);
779    assert!(!b.store().contains(&phantom_hash));
780}
781
782#[test]
783fn sync_attest_push_count_in_stats() {
784    // Cuando ambos peers tienen atestaciones, el harness registra dos
785    // AttestPushes (uno por dirección).
786    let kp_a = kp(120);
787    let kp_b = kp(121);
788    let (mst_a, store_a, atts_a, _) =
789        build_repo_with_attests(&["fn ax() -> i32 { 0 }"], &[&kp_a]);
790    let (mst_b, store_b, atts_b, _) =
791        build_repo_with_attests(&["fn bx() -> i32 { 0 }"], &[&kp_b]);
792
793    let mut a = SyncSession::new(mst_a, store_a, atts_a, kp_a);
794    let mut b = SyncSession::new(mst_b, store_b, atts_b, kp_b);
795    let stats = run_sync(&mut a, &mut b);
796
797    assert_eq!(stats.attest_pushes, 2);
798}