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 #[allow(clippy::declare_interior_mutable_const)]
69 const ZERO: AtomicUsize = AtomicUsize::new(0);
70 #[allow(clippy::declare_interior_mutable_const, clippy::large_const_arrays)]
71 const ROW: [AtomicUsize; ARRAY_SIZE] = [ZERO; ARRAY_SIZE];
72 [ROW; ARRAY_COUNT]
73};
74
75fn ensure_vaults() {
78 if VAULTS[0][0].load(Ordering::Relaxed) == 0 {
79
80 let mut rng = rand::rngs::OsRng;
81 for row in VAULTS.iter() {
82 for slot in row.iter() {
83 let mut val = rng.next_u64() as usize;
84 if val == 0 { val = 1; }
85 let _ = slot.compare_exchange(0, val, Ordering::SeqCst, Ordering::Relaxed);
86 }
87 }
88 }
89}
90
91#[derive(Clone, Copy, PartialEq)]
93#[cfg_attr(test, derive(Debug))]
94struct VaultPos {
95 array: usize,
96 slot: usize,
97}
98
99#[inline]
103fn mix_iterations(instance_addr: usize) -> usize {
104 let base = VAULTS.as_ptr() as usize;
105 let h = (instance_addr ^ base).wrapping_mul(instance_addr | 1);
106 4096 + ((h >> 7) & 4095)
107}
108
109#[inline]
113fn addr_mix(mut h: u64, m1: u64, m2: u64, iterations: usize) -> u64 {
114 for _ in 0..iterations {
115 h ^= h >> 33;
116 h = h.wrapping_mul(m1);
117 h ^= h >> 29;
118 h = h.wrapping_mul(m2);
119 h ^= h >> 31;
120 }
121 h
122}
123
124#[inline]
129fn lane_slot(raw: usize, lane: usize) -> usize {
130 (lane << LANE_WIDTH.trailing_zeros()) | (raw & (LANE_WIDTH - 1))
131}
132
133fn config_positions(instance_addr: usize, lane: usize) -> (VaultPos, VaultPos, VaultPos) {
141 let base = VAULTS.as_ptr() as usize;
142 let m1 = (instance_addr as u64) | 1;
145 let m2 = (base as u64) | 1;
146 let iters = mix_iterations(instance_addr);
147 let mut h = addr_mix((instance_addr as u64) ^ (base as u64).rotate_left(19), m1, m2, iters);
148
149 let mut positions = [VaultPos { array: 0, slot: 0 }; 3];
150 for i in 0..3 {
151 loop {
152 let candidate = VaultPos {
153 array: ((h >> 32) as usize) & (ARRAY_COUNT - 1),
154 slot: lane_slot(h as usize, lane),
155 };
156 if !positions[..i].contains(&candidate) {
157 positions[i] = candidate;
158 h = addr_mix(h, m1, m2, iters);
159 break;
160 }
161 h = addr_mix(h, m1, m2, iters);
162 }
163 }
164
165 (positions[0], positions[1], positions[2])
166}
167
168fn share_positions(instance_addr: usize, lane: usize) -> [VaultPos; SHARE_ENTRIES] {
174 let (seed_pos, mul1_pos, mul2_pos) = config_positions(instance_addr, lane);
175 let seed = VAULTS[seed_pos.array][seed_pos.slot].load(Ordering::Relaxed) as u64;
176 let mul1 = VAULTS[mul1_pos.array][mul1_pos.slot].load(Ordering::Relaxed) as u64;
177 let mul2 = VAULTS[mul2_pos.array][mul2_pos.slot].load(Ordering::Relaxed) as u64;
178
179 let mut h = seed ^ (instance_addr as u64).rotate_left(23);
180 let mut positions = [VaultPos { array: 0, slot: 0 }; SHARE_ENTRIES];
181 let config = [seed_pos, mul1_pos, mul2_pos];
182
183 for i in 0..SHARE_ENTRIES {
184 loop {
185 h ^= h >> 17;
186 h = h.wrapping_mul(mul1 | 1);
187 h ^= h >> 13;
188 h = h.wrapping_mul(mul2 | 1);
189 h ^= h >> 16;
190 let candidate = VaultPos {
191 array: ((h >> 32) as usize) & (ARRAY_COUNT - 1),
192 slot: lane_slot(h as usize, lane),
193 };
194 if !config.contains(&candidate) && !positions[..i].contains(&candidate) {
195 positions[i] = candidate;
196 break;
197 }
198 }
200 }
201
202 positions
203}
204
205fn write_decoys(protected: &[VaultPos]) {
210
211 let mut rng = rand::rngs::OsRng;
212 for (array_idx, row) in VAULTS.iter().enumerate() {
213 for _ in 0..SHARE_ENTRIES {
214 let mut slot = (rng.next_u64() as usize) & (ARRAY_SIZE - 1);
215 while protected.iter().any(|p| p.array == array_idx && p.slot == slot) {
217 slot = (rng.next_u64() as usize) & (ARRAY_SIZE - 1);
218 }
219 let mut val = rng.next_u64() as usize;
220 if val == 0 { val = 1; }
221 row[slot].store(val, Ordering::Release);
222 }
223 }
224}
225
226pub struct GuardedKey {
228 active: AtomicUsize,
231}
232
233impl GuardedKey {
234 pub const fn empty() -> Self {
235 Self { active: AtomicUsize::new(0) }
236 }
237
238 #[inline]
239 fn instance_addr(&self) -> usize {
240 &self.active as *const _ as usize
241 }
242
243 #[inline]
247 fn lane_of(marker: usize) -> usize {
248 marker & (LANE_COUNT - 1)
249 }
250
251 #[inline]
253 fn lane(&self) -> usize {
254 Self::lane_of(self.active.load(Ordering::Acquire))
255 }
256
257 fn pick_marker(&self, others: &[&GuardedKey], rng: &mut rand::rngs::OsRng) -> usize {
264 let mut taken = [false; LANE_COUNT];
265 for &key in others {
266 if std::ptr::eq(key, self) || !key.has_key() { continue; }
267 taken[key.lane()] = true;
268 }
269 loop {
270 let mut marker = rng.next_u64() as usize;
271 if marker == 0 { marker = 1; }
272 if !taken[Self::lane_of(marker)] {
273 return marker;
274 }
275 }
276 }
277
278 fn collect_other_protected(&self, others: &[&GuardedKey]) -> ([VaultPos; (3 + SHARE_ENTRIES) * LANE_COUNT], usize) {
289 let mut buf = [VaultPos { array: 0, slot: 0 }; (3 + SHARE_ENTRIES) * LANE_COUNT];
290 let mut n = 0;
291 for &key in others {
292 if std::ptr::eq(key, self) || !key.has_key() { continue; }
293 if n + 3 + SHARE_ENTRIES > buf.len() { break; }
294 let addr = key.instance_addr();
295 let lane = key.lane();
296 let (s, m1, m2) = config_positions(addr, lane);
297 buf[n] = s; n += 1;
298 buf[n] = m1; n += 1;
299 buf[n] = m2; n += 1;
300 for &pos in share_positions(addr, lane).iter() {
301 buf[n] = pos;
302 n += 1;
303 }
304 }
305 (buf, n)
306 }
307
308 #[inline]
315 pub fn store_from_keys(&self, keys: &nostr_sdk::prelude::Keys, others: &[&GuardedKey]) {
316 let mut sk_bytes = keys.secret_key().secret_bytes();
317 self.set(sk_bytes, others);
318 sk_bytes.zeroize();
319 }
320
321 pub fn set(&self, mut key: [u8; 32], others: &[&GuardedKey]) {
330
331 let mut rng = rand::rngs::OsRng;
332 ensure_vaults();
333
334 let marker = self.pick_marker(others, &mut rng);
340 let lane = Self::lane_of(marker);
341
342 let old_marker = self.active.load(Ordering::Acquire);
345 if old_marker != 0 {
346 let old_lane = Self::lane_of(old_marker);
347 if old_lane != lane {
348 for pos in share_positions(self.instance_addr(), old_lane).iter() {
349 let mut val = rng.next_u64() as usize;
350 if val == 0 { val = 1; }
351 VAULTS[pos.array][pos.slot].store(val, Ordering::Release);
352 }
353 }
354 }
355
356 let (protected, pcount) = self.collect_other_protected(others);
358
359 write_decoys(&protected[..pcount]);
363
364 let (_, mul1_pos, mul2_pos) = config_positions(self.instance_addr(), lane);
367 let v = VAULTS[mul1_pos.array][mul1_pos.slot].load(Ordering::Relaxed);
368 VAULTS[mul1_pos.array][mul1_pos.slot].store(v | 1, Ordering::Relaxed);
369 let v = VAULTS[mul2_pos.array][mul2_pos.slot].load(Ordering::Relaxed);
370 VAULTS[mul2_pos.array][mul2_pos.slot].store(v | 1, Ordering::Relaxed);
371
372 let mut shares = [[0u8; 32]; NUM_SHARES];
374 for share in shares.iter_mut().take(NUM_SHARES - 1) {
375 rng.fill_bytes(share);
376 }
377 shares[NUM_SHARES - 1] = key;
378 for i in 0..NUM_SHARES - 1 {
379 #[allow(clippy::needless_range_loop)] for j in 0..32 {
381 shares[NUM_SHARES - 1][j] ^= shares[i][j];
382 }
383 }
384 key.zeroize();
385
386 let positions = share_positions(self.instance_addr(), lane);
388 for (share_idx, share) in shares.iter().enumerate() {
389 for u_idx in 0..USIZES_PER_SHARE {
390 let byte_off = u_idx * std::mem::size_of::<usize>();
391 let val = usize::from_ne_bytes(
392 share[byte_off..byte_off + std::mem::size_of::<usize>()]
393 .try_into().unwrap()
394 );
395 let pos = positions[share_idx * USIZES_PER_SHARE + u_idx];
396 VAULTS[pos.array][pos.slot].store(val, Ordering::Release);
397 }
398 }
399 for share in shares.iter_mut() { share.zeroize(); }
400
401 self.active.store(marker, Ordering::Release);
404 }
405
406 pub fn get(&self) -> Option<[u8; 32]> {
408 let marker = self.active.load(Ordering::Acquire);
409 if marker == 0 {
410 return None;
411 }
412
413 let positions = share_positions(self.instance_addr(), Self::lane_of(marker));
415 let mut key = [0u8; 32];
416
417 for share_idx in 0..NUM_SHARES {
418 let mut share = [0u8; 32];
419 for u_idx in 0..USIZES_PER_SHARE {
420 let pos = positions[share_idx * USIZES_PER_SHARE + u_idx];
421 let val = VAULTS[pos.array][pos.slot].load(Ordering::Acquire);
422 let byte_off = u_idx * std::mem::size_of::<usize>();
423 share[byte_off..byte_off + std::mem::size_of::<usize>()]
424 .copy_from_slice(&val.to_ne_bytes());
425 }
426 for (a, b) in key.iter_mut().zip(share.iter()) {
427 *a ^= *b;
428 }
429 }
430
431 Some(key)
432 }
433
434 pub fn clear(&self, others: &[&GuardedKey]) {
439 let old_marker = self.active.swap(0, Ordering::SeqCst);
441 if old_marker != 0 {
442
443 let mut rng = rand::rngs::OsRng;
444 let positions = share_positions(self.instance_addr(), Self::lane_of(old_marker));
446 for pos in &positions {
447 let mut val = rng.next_u64() as usize;
448 if val == 0 { val = 1; }
449 VAULTS[pos.array][pos.slot].store(val, Ordering::Release);
450 }
451 let (protected, pcount) = self.collect_other_protected(others);
452 write_decoys(&protected[..pcount]);
453 }
454 }
455
456 pub fn has_key(&self) -> bool {
457 self.active.load(Ordering::Acquire) != 0
458 }
459
460 pub fn to_keys(&self) -> Option<nostr_sdk::prelude::Keys> {
461 let mut bytes = self.get()?;
462 let result = nostr_sdk::prelude::SecretKey::from_slice(&bytes);
463 bytes.zeroize();
464 Some(nostr_sdk::prelude::Keys::new(result.ok()?))
465 }
466}
467
468#[cfg(test)]
473mod tests {
474 use super::*;
475
476 static TEST_KEY_A: GuardedKey = GuardedKey::empty();
478 static TEST_KEY_B: GuardedKey = GuardedKey::empty();
479
480 fn reset() {
491 TEST_KEY_A.active.store(0, Ordering::SeqCst);
492 TEST_KEY_B.active.store(0, Ordering::SeqCst);
493 ensure_vaults();
494 }
495
496 fn others_for_a() -> [&'static GuardedKey; 1] {
498 [&TEST_KEY_B]
499 }
500
501 fn others_for_b() -> [&'static GuardedKey; 1] {
503 [&TEST_KEY_A]
504 }
505
506 fn test_key(seed: u8) -> [u8; 32] {
508 let mut k = [0u8; 32];
509 for (i, b) in k.iter_mut().enumerate() {
510 *b = seed.wrapping_add(i as u8).wrapping_mul(37).wrapping_add(7);
511 }
512 k
513 }
514
515 #[test]
520 fn set_get_roundtrip() {
521 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
522 reset();
523 let key = test_key(42);
524 TEST_KEY_A.set(key, &others_for_a());
525 assert_eq!(TEST_KEY_A.get(), Some(key));
526 }
527
528 #[test]
529 #[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."]
530 fn set_get_1000_iterations() {
531 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
532 for i in 0..1000u16 {
533 reset();
534 let key = test_key((i ^ (i >> 3)) as u8);
535 TEST_KEY_A.set(key, &others_for_a());
536 assert_eq!(
537 TEST_KEY_A.get(), Some(key),
538 "Roundtrip failed at iteration {i}"
539 );
540 }
541 }
542
543 #[test]
544 fn empty_returns_none() {
545 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
546 reset();
547 assert_eq!(TEST_KEY_A.get(), None);
548 assert_eq!(TEST_KEY_B.get(), None);
549 }
550
551 #[test]
552 fn has_key_lifecycle() {
553 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
554 reset();
555 assert!(!TEST_KEY_A.has_key());
556 TEST_KEY_A.set(test_key(1), &others_for_a());
557 assert!(TEST_KEY_A.has_key());
558 TEST_KEY_A.clear(&others_for_a());
559 assert!(!TEST_KEY_A.has_key());
560 }
561
562 #[test]
563 fn clear_returns_none() {
564 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
565 reset();
566 TEST_KEY_A.set(test_key(99), &others_for_a());
567 assert!(TEST_KEY_A.get().is_some());
568 TEST_KEY_A.clear(&others_for_a());
569 assert_eq!(TEST_KEY_A.get(), None);
570 }
571
572 #[test]
573 fn set_overwrites_previous() {
574 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
575 reset();
576 let a = test_key(10);
577 let b = test_key(20);
578 TEST_KEY_A.set(a, &others_for_a());
579 assert_eq!(TEST_KEY_A.get(), Some(a));
580 TEST_KEY_A.set(b, &others_for_a());
581 assert_eq!(TEST_KEY_A.get(), Some(b));
582 }
583
584 #[test]
585 fn clear_idempotent() {
586 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
587 reset();
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 TEST_KEY_A.set(test_key(5), &others_for_a());
592 TEST_KEY_A.clear(&others_for_a());
593 TEST_KEY_A.clear(&others_for_a());
594 assert_eq!(TEST_KEY_A.get(), None);
595 }
596
597 #[test]
598 fn encryption_key_basic() {
599 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
600 reset();
601 let key = test_key(0xEE);
602 TEST_KEY_B.set(key, &others_for_b());
603 assert_eq!(TEST_KEY_B.get(), Some(key));
604 TEST_KEY_B.clear(&others_for_b());
605 assert_eq!(TEST_KEY_B.get(), None);
606 }
607
608 #[test]
615 #[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."]
616 fn cross_key_set_then_set_500() {
617 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
618 let key_a = test_key(0xAA);
619 let key_b = test_key(0xBB);
620 for i in 0..500 {
621 reset();
622 TEST_KEY_A.set(key_a, &others_for_a());
623 TEST_KEY_B.set(key_b, &others_for_b());
624 assert_eq!(
625 TEST_KEY_A.get(), Some(key_a),
626 "TEST_KEY_A corrupted at iteration {i}"
627 );
628 assert_eq!(
629 TEST_KEY_B.get(), Some(key_b),
630 "TEST_KEY_B corrupted at iteration {i}"
631 );
632 }
633 }
634
635 #[test]
636 #[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."]
637 fn cross_key_reverse_order_500() {
638 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
639 let key_a = test_key(0xCC);
640 let key_b = test_key(0xDD);
641 for i in 0..500 {
642 reset();
643 TEST_KEY_B.set(key_b, &others_for_b());
644 TEST_KEY_A.set(key_a, &others_for_a());
645 assert_eq!(
646 TEST_KEY_B.get(), Some(key_b),
647 "TEST_KEY_B corrupted at iteration {i}"
648 );
649 assert_eq!(
650 TEST_KEY_A.get(), Some(key_a),
651 "TEST_KEY_A corrupted at iteration {i}"
652 );
653 }
654 }
655
656 #[test]
657 #[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."]
658 fn cross_key_clear_preserves_other_500() {
659 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
660 let key_a = test_key(0x11);
661 let key_b = test_key(0x22);
662 for i in 0..500 {
663 reset();
665 TEST_KEY_A.set(key_a, &others_for_a());
666 TEST_KEY_B.set(key_b, &others_for_b());
667 TEST_KEY_A.clear(&others_for_a());
668 assert_eq!(
669 TEST_KEY_B.get(), Some(key_b),
670 "KEY_B corrupted after KEY_A.clear() at iteration {i}"
671 );
672 reset();
674 TEST_KEY_A.set(key_a, &others_for_a());
675 TEST_KEY_B.set(key_b, &others_for_b());
676 TEST_KEY_B.clear(&others_for_b());
677 assert_eq!(
678 TEST_KEY_A.get(), Some(key_a),
679 "KEY_A corrupted after KEY_B.clear() at iteration {i}"
680 );
681 }
682 }
683
684 #[test]
685 #[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."]
686 fn cross_key_alternating_500() {
687 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
688 for i in 0..500u16 {
689 reset();
690 let ka = test_key(i as u8);
691 let kb = test_key(!(i as u8));
692 TEST_KEY_A.set(ka, &others_for_a());
693 TEST_KEY_B.set(kb, &others_for_b());
694 assert_eq!(TEST_KEY_A.get(), Some(ka), "KEY_A wrong at iter {i}");
695 assert_eq!(TEST_KEY_B.get(), Some(kb), "KEY_B wrong at iter {i}");
696 TEST_KEY_A.clear(&others_for_a());
697 assert_eq!(TEST_KEY_B.get(), Some(kb), "KEY_B wrong after KEY_A clear at iter {i}");
698 }
699 }
700
701 #[test]
705 #[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."]
706 fn stress_both_keys_1000() {
707 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
708 for i in 0..1000u32 {
709 reset();
710 let ka = test_key((i & 0xFF) as u8);
711 let kb = test_key(!((i & 0xFF) as u8));
712 if i % 2 == 0 {
713 TEST_KEY_A.set(ka, &others_for_a());
714 TEST_KEY_B.set(kb, &others_for_b());
715 } else {
716 TEST_KEY_B.set(kb, &others_for_b());
717 TEST_KEY_A.set(ka, &others_for_a());
718 }
719 assert_eq!(TEST_KEY_A.get(), Some(ka), "KEY_A wrong at iter {i}");
720 assert_eq!(TEST_KEY_B.get(), Some(kb), "KEY_B wrong at iter {i}");
721 }
722 }
723
724 #[test]
729 fn config_positions_all_unique() {
730 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
731 ensure_vaults();
732 for lane in 0..LANE_COUNT {
733 for addr in (0x1000..0x2000usize).step_by(8) {
734 let (a, b, c) = config_positions(addr, lane);
735 assert_ne!(a, b, "config collision a==b at addr {addr:#x} lane {lane}");
736 assert_ne!(a, c, "config collision a==c at addr {addr:#x} lane {lane}");
737 assert_ne!(b, c, "config collision b==c at addr {addr:#x} lane {lane}");
738 }
739 }
740 }
741
742 #[test]
743 fn share_positions_all_unique() {
744 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
745 ensure_vaults();
746 for lane in 0..LANE_COUNT {
747 for addr in (0x2000..0x2100usize).step_by(8) {
748 let positions = share_positions(addr, lane);
749 for i in 0..SHARE_ENTRIES {
750 for j in (i + 1)..SHARE_ENTRIES {
751 assert_ne!(
752 positions[i], positions[j],
753 "share collision [{i}]==[{j}] at addr {addr:#x} lane {lane}"
754 );
755 }
756 }
757 }
758 }
759 }
760
761 #[test]
762 fn share_positions_no_config_overlap() {
763 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
764 ensure_vaults();
765 for lane in 0..LANE_COUNT {
766 for addr in (0x3000..0x3100usize).step_by(8) {
767 let (s, m1, m2) = config_positions(addr, lane);
768 let config = [s, m1, m2];
769 let shares = share_positions(addr, lane);
770 for (i, pos) in shares.iter().enumerate() {
771 assert!(
772 !config.contains(pos),
773 "share[{i}] collides with config at addr {addr:#x} lane {lane}"
774 );
775 }
776 }
777 }
778 }
779
780 #[test]
784 fn distinct_lanes_are_disjoint() {
785 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
786 ensure_vaults();
787 for addr in (0x5000..0x5100usize).step_by(8) {
788 for la in 0..LANE_COUNT {
789 for lb in 0..LANE_COUNT {
790 if la == lb { continue; }
791 let (sa, m1a, m2a) = config_positions(addr, la);
792 let a_all: Vec<VaultPos> = [sa, m1a, m2a]
793 .into_iter()
794 .chain(share_positions(addr, la))
795 .collect();
796 let (sb, m1b, m2b) = config_positions(addr, lb);
797 let b_all: Vec<VaultPos> = [sb, m1b, m2b]
798 .into_iter()
799 .chain(share_positions(addr, lb))
800 .collect();
801 for p in &a_all {
802 assert!(
803 !b_all.contains(p),
804 "lane {la} and lane {lb} share {p:?} at addr {addr:#x}"
805 );
806 }
807 }
808 }
809 }
810 }
811
812 #[test]
813 fn positions_deterministic() {
814 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
815 ensure_vaults();
816 let addr = TEST_KEY_A.instance_addr();
817 let cfg1 = config_positions(addr, 3);
818 let cfg2 = config_positions(addr, 3);
819 assert_eq!(cfg1, cfg2);
820 let sp1 = share_positions(addr, 3);
821 let sp2 = share_positions(addr, 3);
822 assert_eq!(sp1, sp2);
823 }
824
825 #[test]
826 fn all_positions_in_bounds() {
827 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
828 ensure_vaults();
829 for lane in 0..LANE_COUNT {
830 for addr in (0x4000..0x4200usize).step_by(8) {
831 let (a, b, c) = config_positions(addr, lane);
832 for p in [a, b, c] {
833 assert!(p.array < ARRAY_COUNT);
834 assert!(p.slot < ARRAY_SIZE);
835 }
836 for p in share_positions(addr, lane) {
837 assert!(p.array < ARRAY_COUNT);
838 assert!(p.slot < ARRAY_SIZE);
839 }
840 }
841 }
842 }
843
844 #[test]
849 fn mix_iterations_in_range() {
850 for addr in 0..10000usize {
851 let n = mix_iterations(addr);
852 assert!((4096..=8191).contains(&n), "mix_iterations({addr}) = {n}");
853 }
854 }
855
856 #[test]
857 fn addr_mix_zero_iterations_is_identity() {
858 let h: u64 = 0xDEADBEEFCAFEBABE;
859 assert_eq!(addr_mix(h, 123, 456, 0), h);
860 }
861
862 #[test]
863 fn addr_mix_varies_output() {
864 let a = addr_mix(1, 3, 5, 10);
865 let b = addr_mix(2, 3, 5, 10);
866 let c = addr_mix(1, 7, 5, 10);
867 let d = addr_mix(1, 3, 11, 10);
868 assert_ne!(a, b);
869 assert_ne!(a, c);
870 assert_ne!(a, d);
871 }
872
873 #[test]
874 fn ensure_vaults_all_nonzero() {
875 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
876 ensure_vaults();
877 for (r, row) in VAULTS.iter().enumerate() {
878 for (s, slot) in row.iter().enumerate() {
879 assert_ne!(
880 slot.load(Ordering::Relaxed), 0,
881 "VAULTS[{r}][{s}] is zero after ensure_vaults"
882 );
883 }
884 }
885 }
886
887 #[test]
888 fn ensure_vaults_idempotent() {
889 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
890 ensure_vaults();
891 let samples: Vec<_> = (0..20)
892 .map(|i| {
893 let r = i * 13 % ARRAY_COUNT;
894 let s = i * 397 % ARRAY_SIZE;
895 (r, s, VAULTS[r][s].load(Ordering::Relaxed))
896 })
897 .collect();
898 ensure_vaults();
899 for (r, s, val) in &samples {
900 assert_eq!(
901 VAULTS[*r][*s].load(Ordering::Relaxed), *val,
902 "ensure_vaults changed VAULTS[{r}][{s}]"
903 );
904 }
905 }
906
907 #[test]
911 #[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."]
912 fn write_decoys_respects_exclusions_500() {
913 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
914 ensure_vaults();
915 let protected = [
916 VaultPos { array: 0, slot: 100 },
917 VaultPos { array: 0, slot: 200 },
918 VaultPos { array: 50, slot: 2000 },
919 VaultPos { array: 50, slot: 3000 },
920 VaultPos { array: 100, slot: 500 },
921 VaultPos { array: 127, slot: 4095 },
922 ];
923 let before: Vec<usize> = protected.iter()
924 .map(|p| VAULTS[p.array][p.slot].load(Ordering::Relaxed))
925 .collect();
926 for _ in 0..500 {
927 write_decoys(&protected);
928 }
929 for (i, p) in protected.iter().enumerate() {
930 assert_eq!(
931 VAULTS[p.array][p.slot].load(Ordering::Relaxed),
932 before[i],
933 "Protected position ({}, {}) overwritten after 500 rounds",
934 p.array, p.slot
935 );
936 }
937 }
938
939 #[test]
940 fn write_decoys_empty_exclusion_works() {
941 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
942 ensure_vaults();
943 write_decoys(&[]);
944 }
945
946 #[test]
951 fn zero_key_roundtrip() {
952 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
953 reset();
954 let key = [0u8; 32];
955 TEST_KEY_A.set(key, &others_for_a());
956 assert_eq!(TEST_KEY_A.get(), Some(key));
957 }
958
959 #[test]
960 fn max_key_roundtrip() {
961 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
962 reset();
963 let key = [0xFFu8; 32];
964 TEST_KEY_A.set(key, &others_for_a());
965 assert_eq!(TEST_KEY_A.get(), Some(key));
966 }
967
968 #[test]
969 fn to_keys_roundtrip() {
970 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
971 reset();
972 let mut sk_bytes = [0u8; 32];
973 sk_bytes[31] = 1; TEST_KEY_A.set(sk_bytes, &others_for_a());
975 let keys = TEST_KEY_A.to_keys();
976 assert!(keys.is_some(), "to_keys returned None for valid key");
977 assert_eq!(keys.unwrap().secret_key().secret_bytes(), sk_bytes);
978 }
979
980 #[test]
981 fn to_keys_none_when_empty() {
982 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
983 reset();
984 assert!(TEST_KEY_A.to_keys().is_none());
985 }
986
987 #[test]
988 fn store_from_keys_roundtrip() {
989 let _l = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
990 reset();
991 let keys = nostr_sdk::prelude::Keys::generate();
992 let expected = keys.secret_key().secret_bytes();
993 TEST_KEY_A.store_from_keys(&keys, &others_for_a());
994 assert_eq!(TEST_KEY_A.get(), Some(expected));
995 }
996
997 }