⎇ d3649bfd1a80153652c2a028315369bb4b184024
crates/modules/semantic_dht/minga-p2p/tests/sync_invariants.rs
Historia
Quién tocó qué
crudo
chore: monorepo inicial con arje + minga + yahweh absorbidos
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}