1use std::sync::atomic::{AtomicUsize, Ordering};
38use rand::RngCore;
39use zeroize::Zeroize;
40
41const ARRAY_SIZE: usize = 4096;
44const ARRAY_COUNT: usize = 128;
48const NUM_SHARES: usize = 4;
50const USIZES_PER_SHARE: usize = 32 / std::mem::size_of::<usize>();
52const SHARE_ENTRIES: usize = NUM_SHARES * USIZES_PER_SHARE;
54const LANE_COUNT: usize = 16;
61const LANE_WIDTH: usize = ARRAY_SIZE / LANE_COUNT;
63
64static VAULTS: [[AtomicUsize; ARRAY_SIZE]; ARRAY_COUNT] = {
67 const ZERO: AtomicUsize = AtomicUsize::new(0);
68 const ROW: [AtomicUsize; ARRAY_SIZE] = [ZERO; ARRAY_SIZE];
69 [ROW; ARRAY_COUNT]
70};
71
72fn ensure_vaults() {
75 if VAULTS[0][0].load(Ordering::Relaxed) == 0 {
76
77 let mut rng = rand::rngs::OsRng;
78 for row in VAULTS.iter() {
79 for slot in row.iter() {
80 let mut val = rng.next_u64() as usize;
81 if val == 0 { val = 1; }
82 let _ = slot.compare_exchange(0, val, Ordering::SeqCst, Ordering::Relaxed);
83 }
84 }
85 }
86}
87
88#[derive(Clone, Copy, PartialEq)]
90#[cfg_attr(test, derive(Debug))]
91struct VaultPos {
92 array: usize,
93 slot: usize,
94}
95
96#[inline]
100fn mix_iterations(instance_addr: usize) -> usize {
101 let base = VAULTS.as_ptr() as usize;
102 let h = (instance_addr ^ base).wrapping_mul(instance_addr | 1);
103 4096 + ((h >> 7) & 4095)
104}
105
106#[inline]
110fn addr_mix(mut h: u64, m1: u64, m2: u64, iterations: usize) -> u64 {
111 for _ in 0..iterations {
112 h ^= h >> 33;
113 h = h.wrapping_mul(m1);
114 h ^= h >> 29;
115 h = h.wrapping_mul(m2);
116 h ^= h >> 31;
117 }
118 h
119}
120
121#[inline]
126fn lane_slot(raw: usize, lane: usize) -> usize {
127 (lane << LANE_WIDTH.trailing_zeros()) | (raw & (LANE_WIDTH - 1))
128}
129
130fn config_positions(instance_addr: usize, lane: usize) -> (VaultPos, VaultPos, VaultPos) {
138 let base = VAULTS.as_ptr() as usize;
139 let m1 = (instance_addr as u64) | 1;
142 let m2 = (base as u64) | 1;
143 let iters = mix_iterations(instance_addr);
144 let mut h = addr_mix((instance_addr as u64) ^ (base as u64).rotate_left(19), m1, m2, iters);
145
146 let mut positions = [VaultPos { array: 0, slot: 0 }; 3];
147 for i in 0..3 {
148 loop {
149 let candidate = VaultPos {
150 array: ((h >> 32) as usize) & (ARRAY_COUNT - 1),
151 slot: lane_slot(h as usize, lane),
152 };
153 if !positions[..i].contains(&candidate) {
154 positions[i] = candidate;
155 h = addr_mix(h, m1, m2, iters);
156 break;
157 }
158 h = addr_mix(h, m1, m2, iters);
159 }
160 }
161
162 (positions[0], positions[1], positions[2])
163}
164
165fn share_positions(instance_addr: usize, lane: usize) -> [VaultPos; SHARE_ENTRIES] {
171 let (seed_pos, mul1_pos, mul2_pos) = config_positions(instance_addr, lane);
172 let seed = VAULTS[seed_pos.array][seed_pos.slot].load(Ordering::Relaxed) as u64;
173 let mul1 = VAULTS[mul1_pos.array][mul1_pos.slot].load(Ordering::Relaxed) as u64;
174 let mul2 = VAULTS[mul2_pos.array][mul2_pos.slot].load(Ordering::Relaxed) as u64;
175
176 let mut h = seed ^ (instance_addr as u64).rotate_left(23);
177 let mut positions = [VaultPos { array: 0, slot: 0 }; SHARE_ENTRIES];
178 let config = [seed_pos, mul1_pos, mul2_pos];
179
180 for i in 0..SHARE_ENTRIES {
181 loop {
182 h ^= h >> 17;
183 h = h.wrapping_mul(mul1 | 1);
184 h ^= h >> 13;
185 h = h.wrapping_mul(mul2 | 1);
186 h ^= h >> 16;
187 let candidate = VaultPos {
188 array: ((h >> 32) as usize) & (ARRAY_COUNT - 1),
189 slot: lane_slot(h as usize, lane),
190 };
191 if !config.contains(&candidate) && !positions[..i].contains(&candidate) {
192 positions[i] = candidate;
193 break;
194 }
195 }
197 }
198
199 positions
200}
201
202fn write_decoys(protected: &[VaultPos]) {
207
208 let mut rng = rand::rngs::OsRng;
209 for (array_idx, row) in VAULTS.iter().enumerate() {
210 for _ in 0..SHARE_ENTRIES {
211 let mut slot = (rng.next_u64() as usize) & (ARRAY_SIZE - 1);
212 while protected.iter().any(|p| p.array == array_idx && p.slot == slot) {
214 slot = (rng.next_u64() as usize) & (ARRAY_SIZE - 1);
215 }
216 let mut val = rng.next_u64() as usize;
217 if val == 0 { val = 1; }
218 row[slot].store(val, Ordering::Release);
219 }
220 }
221}
222
223pub struct GuardedKey {
225 active: AtomicUsize,
228}
229
230impl GuardedKey {
231 pub const fn empty() -> Self {
232 Self { active: AtomicUsize::new(0) }
233 }
234
235 #[inline]
236 fn instance_addr(&self) -> usize {
237 &self.active as *const _ as usize
238 }
239
240 #[inline]
244 fn lane_of(marker: usize) -> usize {
245 marker & (LANE_COUNT - 1)
246 }
247
248 #[inline]
250 fn lane(&self) -> usize {
251 Self::lane_of(self.active.load(Ordering::Acquire))
252 }
253
254 fn pick_marker(&self, others: &[&GuardedKey], rng: &mut rand::rngs::OsRng) -> usize {
261 let mut taken = [false; LANE_COUNT];
262 for &key in others {
263 if std::ptr::eq(key, self) || !key.has_key() { continue; }
264 taken[key.lane()] = true;
265 }
266 loop {
267 let mut marker = rng.next_u64() as usize;
268 if marker == 0 { marker = 1; }
269 if !taken[Self::lane_of(marker)] {
270 return marker;
271 }
272 }
273 }
274
275 fn collect_other_protected(&self, others: &[&GuardedKey]) -> ([VaultPos; (3 + SHARE_ENTRIES) * LANE_COUNT], usize) {
286 let mut buf = [VaultPos { array: 0, slot: 0 }; (3 + SHARE_ENTRIES) * LANE_COUNT];
287 let mut n = 0;
288 for &key in others {
289 if std::ptr::eq(key, self) || !key.has_key() { continue; }
290 if n + 3 + SHARE_ENTRIES > buf.len() { break; }
291 let addr = key.instance_addr();
292 let lane = key.lane();
293 let (s, m1, m2) = config_positions(addr, lane);
294 buf[n] = s; n += 1;
295 buf[n] = m1; n += 1;
296 buf[n] = m2; n += 1;
297 for &pos in share_positions(addr, lane).iter() {
298 buf[n] = pos;
299 n += 1;
300 }
301 }
302 (buf, n)
303 }
304
305 #[inline]
312 pub fn store_from_keys(&self, keys: &nostr_sdk::prelude::Keys, others: &[&GuardedKey]) {
313 let mut sk_bytes = keys.secret_key().secret_bytes();
314 self.set(sk_bytes, others);
315 sk_bytes.zeroize();
316 }
317
318 pub fn set(&self, mut key: [u8; 32], others: &[&GuardedKey]) {
327
328 let mut rng = rand::rngs::OsRng;
329 ensure_vaults();
330
331 let marker = self.pick_marker(others, &mut rng);
337 let lane = Self::lane_of(marker);
338
339 let old_marker = self.active.load(Ordering::Acquire);
342 if old_marker != 0 {
343 let old_lane = Self::lane_of(old_marker);
344 if old_lane != lane {
345 for pos in share_positions(self.instance_addr(), old_lane).iter() {
346 let mut val = rng.next_u64() as usize;
347 if val == 0 { val = 1; }
348 VAULTS[pos.array][pos.slot].store(val, Ordering::Release);
349 }
350 }
351 }
352
353 let (protected, pcount) = self.collect_other_protected(others);
355
356 write_decoys(&protected[..pcount]);
360
361 let (_, mul1_pos, mul2_pos) = config_positions(self.instance_addr(), lane);
364 let v = VAULTS[mul1_pos.array][mul1_pos.slot].load(Ordering::Relaxed);
365 VAULTS[mul1_pos.array][mul1_pos.slot].store(v | 1, Ordering::Relaxed);
366 let v = VAULTS[mul2_pos.array][mul2_pos.slot].load(Ordering::Relaxed);
367 VAULTS[mul2_pos.array][mul2_pos.slot].store(v | 1, Ordering::Relaxed);
368
369 let mut shares = [[0u8; 32]; NUM_SHARES];
371 for share in shares.iter_mut().take(NUM_SHARES - 1) {
372 rng.fill_bytes(share);
373 }
374 shares[NUM_SHARES - 1] = key;
375 for i in 0..NUM_SHARES - 1 {
376 for j in 0..32 {
377 shares[NUM_SHARES - 1][j] ^= shares[i][j];
378 }
379 }
380 key.zeroize();
381
382 let positions = share_positions(self.instance_addr(), lane);
384 for (share_idx, share) in shares.iter().enumerate() {
385 for u_idx in 0..USIZES_PER_SHARE {
386 let byte_off = u_idx * std::mem::size_of::<usize>();
387 let val = usize::from_ne_bytes(
388 share[byte_off..byte_off + std::mem::size_of::<usize>()]
389 .try_into().unwrap()
390 );
391 let pos = positions[share_idx * USIZES_PER_SHARE + u_idx];
392 VAULTS[pos.array][pos.slot].store(val, Ordering::Release);
393 }
394 }
395 for share in shares.iter_mut() { share.zeroize(); }
396
397 self.active.store(marker, Ordering::Release);
400 }
401
402 pub fn get(&self) -> Option<[u8; 32]> {
404 let marker = self.active.load(Ordering::Acquire);
405 if marker == 0 {
406 return None;
407 }
408
409 let positions = share_positions(self.instance_addr(), Self::lane_of(marker));
411 let mut key = [0u8; 32];
412
413 for share_idx in 0..NUM_SHARES {
414 let mut share = [0u8; 32];
415 for u_idx in 0..USIZES_PER_SHARE {
416 let pos = positions[share_idx * USIZES_PER_SHARE + u_idx];
417 let val = VAULTS[pos.array][pos.slot].load(Ordering::Acquire);
418 let byte_off = u_idx * std::mem::size_of::<usize>();
419 share[byte_off..byte_off + std::mem::size_of::<usize>()]
420 .copy_from_slice(&val.to_ne_bytes());
421 }
422 for (a, b) in key.iter_mut().zip(share.iter()) {
423 *a ^= *b;
424 }
425 }
426
427 Some(key)
428 }
429
430 pub fn clear(&self, others: &[&GuardedKey]) {
435 let old_marker = self.active.swap(0, Ordering::SeqCst);
437 if old_marker != 0 {
438
439 let mut rng = rand::rngs::OsRng;
440 let positions = share_positions(self.instance_addr(), Self::lane_of(old_marker));
442 for pos in &positions {
443 let mut val = rng.next_u64() as usize;
444 if val == 0 { val = 1; }
445 VAULTS[pos.array][pos.slot].store(val, Ordering::Release);
446 }
447 let (protected, pcount) = self.collect_other_protected(others);
448 write_decoys(&protected[..pcount]);
449 }
450 }
451
452 pub fn has_key(&self) -> bool {
453 self.active.load(Ordering::Acquire) != 0
454 }
455
456 pub fn to_keys(&self) -> Option<nostr_sdk::prelude::Keys> {
457 let mut bytes = self.get()?;
458 let result = nostr_sdk::prelude::SecretKey::from_slice(&bytes);
459 bytes.zeroize();
460 Some(nostr_sdk::prelude::Keys::new(result.ok()?))
461 }
462}
463
464#[cfg(test)]
469mod tests {
470 use super::*;
471
472 static TEST_KEY_A: GuardedKey = GuardedKey::empty();
474 static TEST_KEY_B: GuardedKey = GuardedKey::empty();
475
476 fn reset() {
487 TEST_KEY_A.active.store(0, Ordering::SeqCst);
488 TEST_KEY_B.active.store(0, Ordering::SeqCst);
489 ensure_vaults();
490 }
491
492 fn others_for_a() -> [&'static GuardedKey; 1] {
494 [&TEST_KEY_B]
495 }
496
497 fn others_for_b() -> [&'static GuardedKey; 1] {
499 [&TEST_KEY_A]
500 }
501
502 fn test_key(seed: u8) -> [u8; 32] {
504 let mut k = [0u8; 32];
505 for (i, b) in k.iter_mut().enumerate() {
506 *b = seed.wrapping_add(i as u8).wrapping_mul(37).wrapping_add(7);
507 }
508 k
509 }
510
511 #[test]
516 fn set_get_roundtrip() {
517 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
518 reset();
519 let key = test_key(42);
520 TEST_KEY_A.set(key, &others_for_a());
521 assert_eq!(TEST_KEY_A.get(), Some(key));
522 }
523
524 #[test]
525 #[ignore = "vault stress: hundreds of guarded set/get rounds, serialized on the vault statics (~41s of a 52s suite). Run in CI by NAME (`-- --ignored guarded_key`), never --include-ignored: that also sweeps in the live-relay tests."]
526 fn set_get_1000_iterations() {
527 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
528 for i in 0..1000u16 {
529 reset();
530 let key = test_key((i ^ (i >> 3)) as u8);
531 TEST_KEY_A.set(key, &others_for_a());
532 assert_eq!(
533 TEST_KEY_A.get(), Some(key),
534 "Roundtrip failed at iteration {i}"
535 );
536 }
537 }
538
539 #[test]
540 fn empty_returns_none() {
541 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
542 reset();
543 assert_eq!(TEST_KEY_A.get(), None);
544 assert_eq!(TEST_KEY_B.get(), None);
545 }
546
547 #[test]
548 fn has_key_lifecycle() {
549 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
550 reset();
551 assert!(!TEST_KEY_A.has_key());
552 TEST_KEY_A.set(test_key(1), &others_for_a());
553 assert!(TEST_KEY_A.has_key());
554 TEST_KEY_A.clear(&others_for_a());
555 assert!(!TEST_KEY_A.has_key());
556 }
557
558 #[test]
559 fn clear_returns_none() {
560 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
561 reset();
562 TEST_KEY_A.set(test_key(99), &others_for_a());
563 assert!(TEST_KEY_A.get().is_some());
564 TEST_KEY_A.clear(&others_for_a());
565 assert_eq!(TEST_KEY_A.get(), None);
566 }
567
568 #[test]
569 fn set_overwrites_previous() {
570 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
571 reset();
572 let a = test_key(10);
573 let b = test_key(20);
574 TEST_KEY_A.set(a, &others_for_a());
575 assert_eq!(TEST_KEY_A.get(), Some(a));
576 TEST_KEY_A.set(b, &others_for_a());
577 assert_eq!(TEST_KEY_A.get(), Some(b));
578 }
579
580 #[test]
581 fn clear_idempotent() {
582 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
583 reset();
584 TEST_KEY_A.clear(&others_for_a());
585 TEST_KEY_A.clear(&others_for_a());
586 assert_eq!(TEST_KEY_A.get(), None);
587 TEST_KEY_A.set(test_key(5), &others_for_a());
588 TEST_KEY_A.clear(&others_for_a());
589 TEST_KEY_A.clear(&others_for_a());
590 assert_eq!(TEST_KEY_A.get(), None);
591 }
592
593 #[test]
594 fn encryption_key_basic() {
595 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
596 reset();
597 let key = test_key(0xEE);
598 TEST_KEY_B.set(key, &others_for_b());
599 assert_eq!(TEST_KEY_B.get(), Some(key));
600 TEST_KEY_B.clear(&others_for_b());
601 assert_eq!(TEST_KEY_B.get(), None);
602 }
603
604 #[test]
611 #[ignore = "vault stress: hundreds of guarded set/get rounds, serialized on the vault statics (~41s of a 52s suite). Run in CI by NAME (`-- --ignored guarded_key`), never --include-ignored: that also sweeps in the live-relay tests."]
612 fn cross_key_set_then_set_500() {
613 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
614 let key_a = test_key(0xAA);
615 let key_b = test_key(0xBB);
616 for i in 0..500 {
617 reset();
618 TEST_KEY_A.set(key_a, &others_for_a());
619 TEST_KEY_B.set(key_b, &others_for_b());
620 assert_eq!(
621 TEST_KEY_A.get(), Some(key_a),
622 "TEST_KEY_A corrupted at iteration {i}"
623 );
624 assert_eq!(
625 TEST_KEY_B.get(), Some(key_b),
626 "TEST_KEY_B corrupted at iteration {i}"
627 );
628 }
629 }
630
631 #[test]
632 #[ignore = "vault stress: hundreds of guarded set/get rounds, serialized on the vault statics (~41s of a 52s suite). Run in CI by NAME (`-- --ignored guarded_key`), never --include-ignored: that also sweeps in the live-relay tests."]
633 fn cross_key_reverse_order_500() {
634 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
635 let key_a = test_key(0xCC);
636 let key_b = test_key(0xDD);
637 for i in 0..500 {
638 reset();
639 TEST_KEY_B.set(key_b, &others_for_b());
640 TEST_KEY_A.set(key_a, &others_for_a());
641 assert_eq!(
642 TEST_KEY_B.get(), Some(key_b),
643 "TEST_KEY_B corrupted at iteration {i}"
644 );
645 assert_eq!(
646 TEST_KEY_A.get(), Some(key_a),
647 "TEST_KEY_A corrupted at iteration {i}"
648 );
649 }
650 }
651
652 #[test]
653 #[ignore = "vault stress: hundreds of guarded set/get rounds, serialized on the vault statics (~41s of a 52s suite). Run in CI by NAME (`-- --ignored guarded_key`), never --include-ignored: that also sweeps in the live-relay tests."]
654 fn cross_key_clear_preserves_other_500() {
655 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
656 let key_a = test_key(0x11);
657 let key_b = test_key(0x22);
658 for i in 0..500 {
659 reset();
661 TEST_KEY_A.set(key_a, &others_for_a());
662 TEST_KEY_B.set(key_b, &others_for_b());
663 TEST_KEY_A.clear(&others_for_a());
664 assert_eq!(
665 TEST_KEY_B.get(), Some(key_b),
666 "KEY_B corrupted after KEY_A.clear() at iteration {i}"
667 );
668 reset();
670 TEST_KEY_A.set(key_a, &others_for_a());
671 TEST_KEY_B.set(key_b, &others_for_b());
672 TEST_KEY_B.clear(&others_for_b());
673 assert_eq!(
674 TEST_KEY_A.get(), Some(key_a),
675 "KEY_A corrupted after KEY_B.clear() at iteration {i}"
676 );
677 }
678 }
679
680 #[test]
681 #[ignore = "vault stress: hundreds of guarded set/get rounds, serialized on the vault statics (~41s of a 52s suite). Run in CI by NAME (`-- --ignored guarded_key`), never --include-ignored: that also sweeps in the live-relay tests."]
682 fn cross_key_alternating_500() {
683 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
684 for i in 0..500u16 {
685 reset();
686 let ka = test_key(i as u8);
687 let kb = test_key(!(i as u8));
688 TEST_KEY_A.set(ka, &others_for_a());
689 TEST_KEY_B.set(kb, &others_for_b());
690 assert_eq!(TEST_KEY_A.get(), Some(ka), "KEY_A wrong at iter {i}");
691 assert_eq!(TEST_KEY_B.get(), Some(kb), "KEY_B wrong at iter {i}");
692 TEST_KEY_A.clear(&others_for_a());
693 assert_eq!(TEST_KEY_B.get(), Some(kb), "KEY_B wrong after KEY_A clear at iter {i}");
694 }
695 }
696
697 #[test]
701 #[ignore = "vault stress: hundreds of guarded set/get rounds, serialized on the vault statics (~41s of a 52s suite). Run in CI by NAME (`-- --ignored guarded_key`), never --include-ignored: that also sweeps in the live-relay tests."]
702 fn stress_both_keys_1000() {
703 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
704 for i in 0..1000u32 {
705 reset();
706 let ka = test_key((i & 0xFF) as u8);
707 let kb = test_key(!((i & 0xFF) as u8));
708 if i % 2 == 0 {
709 TEST_KEY_A.set(ka, &others_for_a());
710 TEST_KEY_B.set(kb, &others_for_b());
711 } else {
712 TEST_KEY_B.set(kb, &others_for_b());
713 TEST_KEY_A.set(ka, &others_for_a());
714 }
715 assert_eq!(TEST_KEY_A.get(), Some(ka), "KEY_A wrong at iter {i}");
716 assert_eq!(TEST_KEY_B.get(), Some(kb), "KEY_B wrong at iter {i}");
717 }
718 }
719
720 #[test]
725 fn config_positions_all_unique() {
726 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
727 ensure_vaults();
728 for lane in 0..LANE_COUNT {
729 for addr in (0x1000..0x2000usize).step_by(8) {
730 let (a, b, c) = config_positions(addr, lane);
731 assert_ne!(a, b, "config collision a==b at addr {addr:#x} lane {lane}");
732 assert_ne!(a, c, "config collision a==c at addr {addr:#x} lane {lane}");
733 assert_ne!(b, c, "config collision b==c at addr {addr:#x} lane {lane}");
734 }
735 }
736 }
737
738 #[test]
739 fn share_positions_all_unique() {
740 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
741 ensure_vaults();
742 for lane in 0..LANE_COUNT {
743 for addr in (0x2000..0x2100usize).step_by(8) {
744 let positions = share_positions(addr, lane);
745 for i in 0..SHARE_ENTRIES {
746 for j in (i + 1)..SHARE_ENTRIES {
747 assert_ne!(
748 positions[i], positions[j],
749 "share collision [{i}]==[{j}] at addr {addr:#x} lane {lane}"
750 );
751 }
752 }
753 }
754 }
755 }
756
757 #[test]
758 fn share_positions_no_config_overlap() {
759 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
760 ensure_vaults();
761 for lane in 0..LANE_COUNT {
762 for addr in (0x3000..0x3100usize).step_by(8) {
763 let (s, m1, m2) = config_positions(addr, lane);
764 let config = [s, m1, m2];
765 let shares = share_positions(addr, lane);
766 for (i, pos) in shares.iter().enumerate() {
767 assert!(
768 !config.contains(pos),
769 "share[{i}] collides with config at addr {addr:#x} lane {lane}"
770 );
771 }
772 }
773 }
774 }
775
776 #[test]
780 fn distinct_lanes_are_disjoint() {
781 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
782 ensure_vaults();
783 for addr in (0x5000..0x5100usize).step_by(8) {
784 for la in 0..LANE_COUNT {
785 for lb in 0..LANE_COUNT {
786 if la == lb { continue; }
787 let (sa, m1a, m2a) = config_positions(addr, la);
788 let a_all: Vec<VaultPos> = [sa, m1a, m2a]
789 .into_iter()
790 .chain(share_positions(addr, la))
791 .collect();
792 let (sb, m1b, m2b) = config_positions(addr, lb);
793 let b_all: Vec<VaultPos> = [sb, m1b, m2b]
794 .into_iter()
795 .chain(share_positions(addr, lb))
796 .collect();
797 for p in &a_all {
798 assert!(
799 !b_all.contains(p),
800 "lane {la} and lane {lb} share {p:?} at addr {addr:#x}"
801 );
802 }
803 }
804 }
805 }
806 }
807
808 #[test]
809 fn positions_deterministic() {
810 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
811 ensure_vaults();
812 let addr = TEST_KEY_A.instance_addr();
813 let cfg1 = config_positions(addr, 3);
814 let cfg2 = config_positions(addr, 3);
815 assert_eq!(cfg1, cfg2);
816 let sp1 = share_positions(addr, 3);
817 let sp2 = share_positions(addr, 3);
818 assert_eq!(sp1, sp2);
819 }
820
821 #[test]
822 fn all_positions_in_bounds() {
823 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
824 ensure_vaults();
825 for lane in 0..LANE_COUNT {
826 for addr in (0x4000..0x4200usize).step_by(8) {
827 let (a, b, c) = config_positions(addr, lane);
828 for p in [a, b, c] {
829 assert!(p.array < ARRAY_COUNT);
830 assert!(p.slot < ARRAY_SIZE);
831 }
832 for p in share_positions(addr, lane) {
833 assert!(p.array < ARRAY_COUNT);
834 assert!(p.slot < ARRAY_SIZE);
835 }
836 }
837 }
838 }
839
840 #[test]
845 fn mix_iterations_in_range() {
846 for addr in 0..10000usize {
847 let n = mix_iterations(addr);
848 assert!((4096..=8191).contains(&n), "mix_iterations({addr}) = {n}");
849 }
850 }
851
852 #[test]
853 fn addr_mix_zero_iterations_is_identity() {
854 let h: u64 = 0xDEADBEEFCAFEBABE;
855 assert_eq!(addr_mix(h, 123, 456, 0), h);
856 }
857
858 #[test]
859 fn addr_mix_varies_output() {
860 let a = addr_mix(1, 3, 5, 10);
861 let b = addr_mix(2, 3, 5, 10);
862 let c = addr_mix(1, 7, 5, 10);
863 let d = addr_mix(1, 3, 11, 10);
864 assert_ne!(a, b);
865 assert_ne!(a, c);
866 assert_ne!(a, d);
867 }
868
869 #[test]
870 fn ensure_vaults_all_nonzero() {
871 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
872 ensure_vaults();
873 for (r, row) in VAULTS.iter().enumerate() {
874 for (s, slot) in row.iter().enumerate() {
875 assert_ne!(
876 slot.load(Ordering::Relaxed), 0,
877 "VAULTS[{r}][{s}] is zero after ensure_vaults"
878 );
879 }
880 }
881 }
882
883 #[test]
884 fn ensure_vaults_idempotent() {
885 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
886 ensure_vaults();
887 let samples: Vec<_> = (0..20)
888 .map(|i| {
889 let r = i * 13 % ARRAY_COUNT;
890 let s = i * 397 % ARRAY_SIZE;
891 (r, s, VAULTS[r][s].load(Ordering::Relaxed))
892 })
893 .collect();
894 ensure_vaults();
895 for (r, s, val) in &samples {
896 assert_eq!(
897 VAULTS[*r][*s].load(Ordering::Relaxed), *val,
898 "ensure_vaults changed VAULTS[{r}][{s}]"
899 );
900 }
901 }
902
903 #[test]
907 #[ignore = "vault stress: hundreds of guarded set/get rounds, serialized on the vault statics (~41s of a 52s suite). Run in CI by NAME (`-- --ignored guarded_key`), never --include-ignored: that also sweeps in the live-relay tests."]
908 fn write_decoys_respects_exclusions_500() {
909 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
910 ensure_vaults();
911 let protected = [
912 VaultPos { array: 0, slot: 100 },
913 VaultPos { array: 0, slot: 200 },
914 VaultPos { array: 50, slot: 2000 },
915 VaultPos { array: 50, slot: 3000 },
916 VaultPos { array: 100, slot: 500 },
917 VaultPos { array: 127, slot: 4095 },
918 ];
919 let before: Vec<usize> = protected.iter()
920 .map(|p| VAULTS[p.array][p.slot].load(Ordering::Relaxed))
921 .collect();
922 for _ in 0..500 {
923 write_decoys(&protected);
924 }
925 for (i, p) in protected.iter().enumerate() {
926 assert_eq!(
927 VAULTS[p.array][p.slot].load(Ordering::Relaxed),
928 before[i],
929 "Protected position ({}, {}) overwritten after 500 rounds",
930 p.array, p.slot
931 );
932 }
933 }
934
935 #[test]
936 fn write_decoys_empty_exclusion_works() {
937 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
938 ensure_vaults();
939 write_decoys(&[]);
940 }
941
942 #[test]
947 fn zero_key_roundtrip() {
948 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
949 reset();
950 let key = [0u8; 32];
951 TEST_KEY_A.set(key, &others_for_a());
952 assert_eq!(TEST_KEY_A.get(), Some(key));
953 }
954
955 #[test]
956 fn max_key_roundtrip() {
957 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
958 reset();
959 let key = [0xFFu8; 32];
960 TEST_KEY_A.set(key, &others_for_a());
961 assert_eq!(TEST_KEY_A.get(), Some(key));
962 }
963
964 #[test]
965 fn to_keys_roundtrip() {
966 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
967 reset();
968 let mut sk_bytes = [0u8; 32];
969 sk_bytes[31] = 1; TEST_KEY_A.set(sk_bytes, &others_for_a());
971 let keys = TEST_KEY_A.to_keys();
972 assert!(keys.is_some(), "to_keys returned None for valid key");
973 assert_eq!(keys.unwrap().secret_key().secret_bytes(), sk_bytes);
974 }
975
976 #[test]
977 fn to_keys_none_when_empty() {
978 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
979 reset();
980 assert!(TEST_KEY_A.to_keys().is_none());
981 }
982
983 #[test]
984 fn store_from_keys_roundtrip() {
985 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
986 reset();
987 let keys = nostr_sdk::prelude::Keys::generate();
988 let expected = keys.secret_key().secret_bytes();
989 TEST_KEY_A.store_from_keys(&keys, &others_for_a());
990 assert_eq!(TEST_KEY_A.get(), Some(expected));
991 }
992
993 }