1use chrono::{DateTime, Duration, Utc};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8use std::collections::{HashMap, HashSet};
9use std::sync::Mutex;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TamperProofEntry {
16 pub sequence: u64,
17 pub timestamp: DateTime<Utc>,
18 pub event_type: String,
19 pub payload_hex: String,
20 pub prev_hash_hex: String,
21 pub entry_hash_hex: String,
22}
23
24pub struct TamperProofLog {
25 entries: Mutex<Vec<TamperProofEntry>>,
26 root: Mutex<String>,
27}
28
29impl TamperProofLog {
30 pub fn new() -> Self {
31 Self {
32 entries: Mutex::new(Vec::new()),
33 root: Mutex::new("0".repeat(64)),
34 }
35 }
36
37 pub fn append(&self, event_type: &str, payload: &[u8]) -> u64 {
38 let mut entries = self.entries.lock().unwrap();
39 let seq = entries.len() as u64 + 1;
40 let prev_hash = entries
41 .last()
42 .map(|e| e.entry_hash_hex.clone())
43 .unwrap_or_else(|| "0".repeat(64));
44 let mut hasher = Sha256::new();
45 hasher.update(seq.to_be_bytes());
46 hasher.update(event_type.as_bytes());
47 hasher.update(payload);
48 hasher.update(prev_hash.as_bytes());
49 let entry_hash = hex::encode(hasher.finalize());
50 let entry = TamperProofEntry {
51 sequence: seq,
52 timestamp: Utc::now(),
53 event_type: event_type.into(),
54 payload_hex: hex::encode(payload),
55 prev_hash_hex: prev_hash.clone(),
56 entry_hash_hex: entry_hash.clone(),
57 };
58 entries.push(entry);
59 *self.root.lock().unwrap() = entry_hash;
60 seq
61 }
62
63 pub fn verify_integrity(&self) -> bool {
64 let entries = self.entries.lock().unwrap();
65 let mut prev_hash = "0".repeat(64);
66 for entry in entries.iter() {
67 if entry.prev_hash_hex != prev_hash {
68 return false;
69 }
70 let mut hasher = Sha256::new();
71 hasher.update(entry.sequence.to_be_bytes());
72 hasher.update(entry.event_type.as_bytes());
73 let payload = match hex::decode(&entry.payload_hex) {
74 Ok(p) => p,
75 Err(_) => return false,
76 };
77 hasher.update(&payload);
78 hasher.update(entry.prev_hash_hex.as_bytes());
79 let computed = hex::encode(hasher.finalize());
80 if computed != entry.entry_hash_hex {
81 return false;
82 }
83 prev_hash = entry.entry_hash_hex.clone();
84 }
85 true
86 }
87
88 pub fn root_hash(&self) -> String {
89 self.root.lock().unwrap().clone()
90 }
91 pub fn entry_count(&self) -> usize {
92 self.entries.lock().unwrap().len()
93 }
94 pub fn entries(&self) -> Vec<TamperProofEntry> {
95 self.entries.lock().unwrap().clone()
96 }
97}
98
99impl Default for TamperProofLog {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105#[derive(Debug, Clone)]
108pub struct GcConfig {
109 pub max_completed_age: Duration,
110 pub max_expired_age: Duration,
111 pub max_retained: usize,
112 pub gc_interval_secs: u64,
113}
114
115impl Default for GcConfig {
116 fn default() -> Self {
117 Self {
118 max_completed_age: Duration::hours(24),
119 max_expired_age: Duration::hours(48),
120 max_retained: 10_000,
121 gc_interval_secs: 300,
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
127pub struct GcSessionInfo {
128 pub session_id: String,
129 pub state: String,
130 pub last_updated: DateTime<Utc>,
131}
132
133pub struct SessionGarbageCollector {
134 config: GcConfig,
135 collected: AtomicU64,
136}
137
138impl SessionGarbageCollector {
139 pub fn new(config: GcConfig) -> Self {
140 Self {
141 config,
142 collected: AtomicU64::new(0),
143 }
144 }
145
146 pub fn collect(&self, sessions: &mut Vec<GcSessionInfo>) -> usize {
147 let now = Utc::now();
148 let before = sessions.len();
149 sessions.retain(|s| {
150 let age = now - s.last_updated;
151 match s.state.as_str() {
152 "completed" => age < self.config.max_completed_age,
153 "expired" | "aborted" => age < self.config.max_expired_age,
154 _ => true, }
156 });
157 if sessions.len() > self.config.max_retained {
159 let excess = sessions.len() - self.config.max_retained;
160 sessions.drain(..excess);
161 }
162 let collected = before - sessions.len();
163 self.collected.fetch_add(collected as u64, Ordering::SeqCst);
164 collected
165 }
166
167 pub fn total_collected(&self) -> u64 {
168 self.collected.load(Ordering::SeqCst)
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct SignerReputation {
176 pub signer_id: String,
177 pub good_count: u32,
178 pub bad_count: u32,
179 pub quarantined: bool,
180 pub quarantine_until: Option<DateTime<Utc>>,
181}
182
183pub struct SignerQuarantine {
184 reputations: Mutex<HashMap<String, SignerReputation>>,
185 bad_threshold: u32,
186 quarantine_duration: Duration,
187}
188
189impl SignerQuarantine {
190 pub fn new(bad_threshold: u32, quarantine_duration: Duration) -> Self {
191 Self {
192 reputations: Mutex::new(HashMap::new()),
193 bad_threshold,
194 quarantine_duration,
195 }
196 }
197
198 pub fn record_good(&self, signer_id: &str) {
199 let mut reps = self.reputations.lock().unwrap();
200 let rep = reps
201 .entry(signer_id.into())
202 .or_insert_with(|| SignerReputation {
203 signer_id: signer_id.into(),
204 good_count: 0,
205 bad_count: 0,
206 quarantined: false,
207 quarantine_until: None,
208 });
209 rep.good_count += 1;
210 if rep.quarantined && rep.good_count >= 10 {
211 rep.quarantined = false;
212 rep.quarantine_until = None;
213 }
214 }
215
216 pub fn record_bad(&self, signer_id: &str) {
217 let mut reps = self.reputations.lock().unwrap();
218 let rep = reps
219 .entry(signer_id.into())
220 .or_insert_with(|| SignerReputation {
221 signer_id: signer_id.into(),
222 good_count: 0,
223 bad_count: 0,
224 quarantined: false,
225 quarantine_until: None,
226 });
227 rep.bad_count += 1;
228 if rep.bad_count >= self.bad_threshold {
229 rep.quarantined = true;
230 rep.quarantine_until = Some(Utc::now() + self.quarantine_duration);
231 }
232 }
233
234 pub fn is_quarantined(&self, signer_id: &str) -> bool {
235 let reps = self.reputations.lock().unwrap();
236 reps.get(signer_id)
237 .map(|r| {
238 if !r.quarantined {
239 return false;
240 }
241 if let Some(until) = r.quarantine_until {
242 return Utc::now() < until;
243 }
244 true
245 })
246 .unwrap_or(false)
247 }
248
249 pub fn reputation(&self, signer_id: &str) -> Option<SignerReputation> {
250 self.reputations.lock().unwrap().get(signer_id).cloned()
251 }
252
253 pub fn quarantined_count(&self) -> usize {
254 self.reputations
255 .lock()
256 .unwrap()
257 .values()
258 .filter(|r| r.quarantined)
259 .count()
260 }
261
262 pub fn release(&self, signer_id: &str) {
263 if let Some(rep) = self.reputations.lock().unwrap().get_mut(signer_id) {
264 rep.quarantined = false;
265 rep.quarantine_until = None;
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct TimeLockCondition {
274 pub unlock_at: DateTime<Utc>,
275}
276
277pub struct TimeLockedSession {
278 pub session_id: String,
279 pub condition: TimeLockCondition,
280 pub signature: Option<Vec<u8>>,
281 pub ready: bool,
282}
283
284impl TimeLockedSession {
285 pub fn new(session_id: &str, unlock_at: DateTime<Utc>) -> Self {
286 Self {
287 session_id: session_id.into(),
288 condition: TimeLockCondition { unlock_at },
289 signature: None,
290 ready: false,
291 }
292 }
293
294 pub fn check_unlock(&mut self) -> bool {
295 if Utc::now() >= self.condition.unlock_at {
296 self.ready = true;
297 }
298 self.ready
299 }
300
301 pub fn store_signature(&mut self, sig: Vec<u8>) -> Result<(), String> {
302 if !self.ready {
303 return Err("not unlocked yet".into());
304 }
305 self.signature = Some(sig);
306 Ok(())
307 }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
313pub enum SigningCondition {
314 Always,
315 QuorumVote {
316 required: u32,
317 },
318 OracleValue {
319 oracle_id: String,
320 min_value: f64,
321 },
322 TimeWindow {
323 start: DateTime<Utc>,
324 end: DateTime<Utc>,
325 },
326}
327
328impl SigningCondition {
329 pub fn evaluate(&self, context: &SigningContext) -> bool {
330 match self {
331 Self::Always => true,
332 Self::QuorumVote { required } => context.vote_count >= *required as usize,
333 Self::OracleValue { min_value, .. } => {
334 context.oracle_value.unwrap_or(f64::MIN) >= *min_value
335 }
336 Self::TimeWindow { start, end } => {
337 let now = Utc::now();
338 now >= *start && now <= *end
339 }
340 }
341 }
342}
343
344#[derive(Debug, Clone, Default)]
345pub struct SigningContext {
346 pub vote_count: usize,
347 pub oracle_value: Option<f64>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct CrossQuorumAggSig {
354 pub quorum_ids: Vec<String>,
355 pub message_hash_hex: String,
356 pub aggregate_signature_hex: String,
357 pub timestamp: DateTime<Utc>,
358}
359
360pub fn aggregate_cross_quorum(
361 quorum_sigs: &[(String, Vec<u8>)],
362 message_hash: &[u8],
363) -> CrossQuorumAggSig {
364 let mut agg = vec![0u8; 64];
365 for (_, sig) in quorum_sigs {
366 for (i, &b) in sig.iter().take(64).enumerate() {
367 agg[i] ^= b;
368 }
369 }
370 CrossQuorumAggSig {
371 quorum_ids: quorum_sigs.iter().map(|(q, _)| q.clone()).collect(),
372 message_hash_hex: hex::encode(message_hash),
373 aggregate_signature_hex: hex::encode(&agg),
374 timestamp: Utc::now(),
375 }
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct CoordinatorConfig {
382 pub max_sessions: usize,
383 pub session_timeout_secs: u64,
384 pub rate_limit_per_minute: u32,
385 pub allowed_schemes: Vec<String>,
386 pub config_version: u32,
387}
388
389impl Default for CoordinatorConfig {
390 fn default() -> Self {
391 Self {
392 max_sessions: 100,
393 session_timeout_secs: 3600,
394 rate_limit_per_minute: 100,
395 allowed_schemes: vec!["CMP20".into(), "FROST-P256".into()],
396 config_version: 1,
397 }
398 }
399}
400
401pub struct ConfigManager {
402 config: Mutex<CoordinatorConfig>,
403 reload_count: AtomicU64,
404 last_reload: Mutex<Option<DateTime<Utc>>>,
405}
406
407impl ConfigManager {
408 pub fn new(initial: CoordinatorConfig) -> Self {
409 Self {
410 config: Mutex::new(initial),
411 reload_count: AtomicU64::new(0),
412 last_reload: Mutex::new(None),
413 }
414 }
415
416 pub fn reload(&self, new_config: CoordinatorConfig) {
417 let mut config = self.config.lock().unwrap();
418 let old_version = config.config_version;
419 *config = CoordinatorConfig {
420 config_version: old_version + 1,
421 ..new_config
422 };
423 self.reload_count.fetch_add(1, Ordering::SeqCst);
424 *self.last_reload.lock().unwrap() = Some(Utc::now());
425 }
426
427 pub fn config(&self) -> CoordinatorConfig {
428 self.config.lock().unwrap().clone()
429 }
430 pub fn reload_count(&self) -> u64 {
431 self.reload_count.load(Ordering::SeqCst)
432 }
433 pub fn last_reload(&self) -> Option<DateTime<Utc>> {
434 *self.last_reload.lock().unwrap()
435 }
436 pub fn config_version(&self) -> u32 {
437 self.config.lock().unwrap().config_version
438 }
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct WalletPolicy {
445 pub required_signers: u32,
446 pub total_signers: u32,
447 pub max_amount: Option<u64>,
448 pub allowed_recipients: Option<HashSet<String>>,
449 pub time_window: Option<(DateTime<Utc>, DateTime<Utc>)>,
450}
451
452impl WalletPolicy {
453 pub fn evaluate(&self, ctx: &WalletTxContext) -> Result<(), String> {
454 if ctx.signer_count < self.required_signers as usize {
455 return Err(format!(
456 "need {} signers, got {}",
457 self.required_signers, ctx.signer_count
458 ));
459 }
460 if let Some(max) = self.max_amount {
461 if ctx.amount > max {
462 return Err(format!("amount {} exceeds max {}", ctx.amount, max));
463 }
464 }
465 if let Some(ref allowed) = self.allowed_recipients {
466 if !allowed.contains(&ctx.recipient) {
467 return Err(format!("recipient {} not in allowlist", ctx.recipient));
468 }
469 }
470 if let Some((start, end)) = self.time_window {
471 let now = Utc::now();
472 if now < start || now > end {
473 return Err("outside time window".into());
474 }
475 }
476 Ok(())
477 }
478}
479
480#[derive(Debug, Clone)]
481pub struct WalletTxContext {
482 pub signer_count: usize,
483 pub amount: u64,
484 pub recipient: String,
485}
486
487pub struct ReplayProtection {
490 seen_nonces: Mutex<HashSet<Vec<u8>>>,
491 max_cache_size: usize,
492}
493
494impl ReplayProtection {
495 pub fn new(max_cache_size: usize) -> Self {
496 Self {
497 seen_nonces: Mutex::new(HashSet::new()),
498 max_cache_size,
499 }
500 }
501
502 pub fn check_and_consume(&self, nonce: &[u8]) -> bool {
503 let mut seen = self.seen_nonces.lock().unwrap();
504 if seen.contains(nonce) {
505 return false;
506 }
507 if seen.len() >= self.max_cache_size {
508 let to_remove = self.max_cache_size / 10;
510 let keys: Vec<Vec<u8>> = seen.iter().take(to_remove).cloned().collect();
511 for k in keys {
512 seen.remove(&k);
513 }
514 }
515 seen.insert(nonce.to_vec());
516 true
517 }
518
519 pub fn is_seen(&self, nonce: &[u8]) -> bool {
520 self.seen_nonces.lock().unwrap().contains(nonce)
521 }
522
523 pub fn cache_size(&self) -> usize {
524 self.seen_nonces.lock().unwrap().len()
525 }
526}
527
528pub struct ProactiveScheduler {
531 last_refresh: Mutex<DateTime<Utc>>,
532 refresh_interval: Duration,
533 refresh_count: AtomicU64,
534 auto_refresh: Mutex<bool>,
535}
536
537impl ProactiveScheduler {
538 pub fn new(refresh_interval: Duration) -> Self {
539 Self {
540 last_refresh: Mutex::new(Utc::now()),
541 refresh_interval,
542 refresh_count: AtomicU64::new(0),
543 auto_refresh: Mutex::new(true),
544 }
545 }
546
547 pub fn should_refresh(&self) -> bool {
548 if !*self.auto_refresh.lock().unwrap() {
549 return false;
550 }
551 let last = *self.last_refresh.lock().unwrap();
552 Utc::now() - last >= self.refresh_interval
553 }
554
555 pub fn mark_refreshed(&self) {
556 *self.last_refresh.lock().unwrap() = Utc::now();
557 self.refresh_count.fetch_add(1, Ordering::SeqCst);
558 }
559
560 pub fn set_auto_refresh(&self, enabled: bool) {
561 *self.auto_refresh.lock().unwrap() = enabled;
562 }
563 pub fn refresh_count(&self) -> u64 {
564 self.refresh_count.load(Ordering::SeqCst)
565 }
566 pub fn last_refresh(&self) -> DateTime<Utc> {
567 *self.last_refresh.lock().unwrap()
568 }
569
570 pub fn next_refresh_at(&self) -> DateTime<Utc> {
571 *self.last_refresh.lock().unwrap() + self.refresh_interval
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
581 fn audit_append_and_verify() {
582 let log = TamperProofLog::new();
583 log.append("created", b"session-1");
584 log.append("signed", b"sig-data");
585 assert!(log.verify_integrity());
586 assert_eq!(log.entry_count(), 2);
587 }
588
589 #[test]
590 fn audit_detects_tampering() {
591 let log = TamperProofLog::new();
592 log.append("event", b"data1");
593 log.append("event", b"data2");
594 log.entries.lock().unwrap()[0].payload_hex = hex::encode(b"tampered");
596 assert!(!log.verify_integrity());
597 }
598
599 #[test]
600 fn audit_root_changes() {
601 let log = TamperProofLog::new();
602 let root1 = log.root_hash();
603 log.append("e", b"d");
604 let root2 = log.root_hash();
605 assert_ne!(root1, root2);
606 }
607
608 #[test]
610 fn gc_collects_old_completed() {
611 let gc = SessionGarbageCollector::new(GcConfig {
612 max_completed_age: Duration::seconds(0),
613 ..Default::default()
614 });
615 let mut sessions = vec![
616 GcSessionInfo {
617 session_id: "s1".into(),
618 state: "completed".into(),
619 last_updated: Utc::now() - Duration::hours(48),
620 },
621 GcSessionInfo {
622 session_id: "s2".into(),
623 state: "pending".into(),
624 last_updated: Utc::now(),
625 },
626 ];
627 let collected = gc.collect(&mut sessions);
628 assert_eq!(collected, 1);
629 assert_eq!(sessions.len(), 1);
630 }
631
632 #[test]
633 fn gc_preserves_pending() {
634 let gc = SessionGarbageCollector::new(GcConfig::default());
635 let mut sessions = vec![GcSessionInfo {
636 session_id: "s1".into(),
637 state: "pending".into(),
638 last_updated: Utc::now() - Duration::days(365),
639 }];
640 gc.collect(&mut sessions);
641 assert_eq!(sessions.len(), 1);
642 }
643
644 #[test]
646 fn quarantine_after_bad_threshold() {
647 let q = SignerQuarantine::new(3, Duration::hours(1));
648 q.record_bad("s1");
649 q.record_bad("s1");
650 assert!(!q.is_quarantined("s1"));
651 q.record_bad("s1");
652 assert!(q.is_quarantined("s1"));
653 }
654
655 #[test]
656 fn quarantine_release() {
657 let q = SignerQuarantine::new(1, Duration::hours(1));
658 q.record_bad("s1");
659 assert!(q.is_quarantined("s1"));
660 q.release("s1");
661 assert!(!q.is_quarantined("s1"));
662 }
663
664 #[test]
665 fn quarantine_good_records_recover() {
666 let q = SignerQuarantine::new(1, Duration::hours(1));
667 q.record_bad("s1");
668 assert!(q.is_quarantined("s1"));
669 for _ in 0..10 {
670 q.record_good("s1");
671 }
672 assert!(!q.is_quarantined("s1"));
673 }
674
675 #[test]
677 fn time_lock_not_ready_before() {
678 let mut session = TimeLockedSession::new("s1", Utc::now() + Duration::hours(1));
679 assert!(!session.check_unlock());
680 assert!(session.store_signature(vec![1]).is_err());
681 }
682
683 #[test]
684 fn time_lock_ready_after() {
685 let mut session = TimeLockedSession::new("s1", Utc::now() - Duration::seconds(1));
686 assert!(session.check_unlock());
687 assert!(session.store_signature(vec![1]).is_ok());
688 }
689
690 #[test]
692 fn condition_always_passes() {
693 let cond = SigningCondition::Always;
694 assert!(cond.evaluate(&SigningContext::default()));
695 }
696
697 #[test]
698 fn condition_quorum_vote() {
699 let cond = SigningCondition::QuorumVote { required: 3 };
700 assert!(!cond.evaluate(&SigningContext {
701 vote_count: 2,
702 ..Default::default()
703 }));
704 assert!(cond.evaluate(&SigningContext {
705 vote_count: 3,
706 ..Default::default()
707 }));
708 }
709
710 #[test]
711 fn condition_oracle() {
712 let cond = SigningCondition::OracleValue {
713 oracle_id: "price".into(),
714 min_value: 100.0,
715 };
716 assert!(!cond.evaluate(&SigningContext {
717 oracle_value: Some(50.0),
718 ..Default::default()
719 }));
720 assert!(cond.evaluate(&SigningContext {
721 oracle_value: Some(150.0),
722 ..Default::default()
723 }));
724 }
725
726 #[test]
727 fn condition_time_window() {
728 let now = Utc::now();
729 let cond = SigningCondition::TimeWindow {
730 start: now - Duration::hours(1),
731 end: now + Duration::hours(1),
732 };
733 assert!(cond.evaluate(&SigningContext::default()));
734 }
735
736 #[test]
738 fn cross_quorum_aggregate() {
739 let sigs = vec![
740 ("q1".to_string(), vec![0xAA; 64]),
741 ("q2".to_string(), vec![0xBB; 64]),
742 ];
743 let agg = aggregate_cross_quorum(&sigs, &[0x42; 32]);
744 assert_eq!(agg.quorum_ids.len(), 2);
745 assert!(!agg.aggregate_signature_hex.is_empty());
746 }
747
748 #[test]
750 fn config_reload_updates() {
751 let mgr = ConfigManager::new(CoordinatorConfig::default());
752 assert_eq!(mgr.config_version(), 1);
753 let new_config = CoordinatorConfig {
754 max_sessions: 200,
755 ..Default::default()
756 };
757 mgr.reload(new_config);
758 assert_eq!(mgr.config().max_sessions, 200);
759 assert_eq!(mgr.config_version(), 2);
760 assert_eq!(mgr.reload_count(), 1);
761 }
762
763 #[test]
765 fn wallet_policy_passes() {
766 let policy = WalletPolicy {
767 required_signers: 2,
768 total_signers: 3,
769 max_amount: Some(1000),
770 allowed_recipients: None,
771 time_window: None,
772 };
773 let ctx = WalletTxContext {
774 signer_count: 2,
775 amount: 500,
776 recipient: "bob".into(),
777 };
778 assert!(policy.evaluate(&ctx).is_ok());
779 }
780
781 #[test]
782 fn wallet_policy_fails_insufficient_signers() {
783 let policy = WalletPolicy {
784 required_signers: 3,
785 total_signers: 5,
786 max_amount: None,
787 allowed_recipients: None,
788 time_window: None,
789 };
790 let ctx = WalletTxContext {
791 signer_count: 2,
792 amount: 100,
793 recipient: "x".into(),
794 };
795 assert!(policy.evaluate(&ctx).is_err());
796 }
797
798 #[test]
799 fn wallet_policy_fails_exceeds_amount() {
800 let policy = WalletPolicy {
801 required_signers: 1,
802 total_signers: 1,
803 max_amount: Some(100),
804 allowed_recipients: None,
805 time_window: None,
806 };
807 let ctx = WalletTxContext {
808 signer_count: 1,
809 amount: 200,
810 recipient: "x".into(),
811 };
812 assert!(policy.evaluate(&ctx).is_err());
813 }
814
815 #[test]
816 fn wallet_policy_recipient_allowlist() {
817 let mut allowed = HashSet::new();
818 allowed.insert("alice".into());
819 let policy = WalletPolicy {
820 required_signers: 1,
821 total_signers: 1,
822 max_amount: None,
823 allowed_recipients: Some(allowed),
824 time_window: None,
825 };
826 assert!(
827 policy
828 .evaluate(&WalletTxContext {
829 signer_count: 1,
830 amount: 0,
831 recipient: "alice".into()
832 })
833 .is_ok()
834 );
835 assert!(
836 policy
837 .evaluate(&WalletTxContext {
838 signer_count: 1,
839 amount: 0,
840 recipient: "bob".into()
841 })
842 .is_err()
843 );
844 }
845
846 #[test]
848 fn replay_first_use_accepted() {
849 let rp = ReplayProtection::new(1000);
850 assert!(rp.check_and_consume(b"nonce1"));
851 }
852
853 #[test]
854 fn replay_duplicate_rejected() {
855 let rp = ReplayProtection::new(1000);
856 rp.check_and_consume(b"nonce1");
857 assert!(!rp.check_and_consume(b"nonce1"));
858 }
859
860 #[test]
861 fn replay_different_nonces_accepted() {
862 let rp = ReplayProtection::new(1000);
863 assert!(rp.check_and_consume(b"n1"));
864 assert!(rp.check_and_consume(b"n2"));
865 }
866
867 #[test]
868 fn replay_cache_eviction() {
869 let rp = ReplayProtection::new(10);
870 for i in 0..15 {
871 rp.check_and_consume(&[i as u8; 8]);
872 }
873 assert!(rp.cache_size() <= 15);
875 }
876
877 #[test]
879 fn proactive_should_refresh_after_interval() {
880 let scheduler = ProactiveScheduler::new(Duration::milliseconds(1));
881 std::thread::sleep(std::time::Duration::from_millis(5));
882 assert!(scheduler.should_refresh());
883 }
884
885 #[test]
886 fn proactive_not_ready_before_interval() {
887 let scheduler = ProactiveScheduler::new(Duration::hours(1));
888 assert!(!scheduler.should_refresh());
889 }
890
891 #[test]
892 fn proactive_mark_refreshed_resets() {
893 let scheduler = ProactiveScheduler::new(Duration::hours(1));
894 assert!(!scheduler.should_refresh());
895 scheduler.mark_refreshed();
896 assert_eq!(scheduler.refresh_count(), 1);
897 assert!(!scheduler.should_refresh());
898 }
899
900 #[test]
901 fn proactive_auto_refresh_toggle() {
902 let scheduler = ProactiveScheduler::new(Duration::milliseconds(1));
903 std::thread::sleep(std::time::Duration::from_millis(5));
904 scheduler.set_auto_refresh(false);
905 assert!(!scheduler.should_refresh());
906 }
907}