_hope_core/
bft_watchdog.rs

1//! # Hope Genome v1.8.0 - Byzantine Fault Tolerant Watchdog
2//!
3//! **THE MULTI-HEADED CERBERUS** - No single point of failure
4//!
5//! ## Problem
6//!
7//! Single Watchdog = Single Point of Failure
8//! - What if the Watchdog server is compromised?
9//! - What if a hardware fault corrupts a decision?
10//! - What if an insider attacks the Watchdog?
11//!
12//! ## Solution: Byzantine Fault Tolerance
13//!
14//! ```text
15//! ┌─────────────────────────────────────────────────────────────────┐
16//! │              BFT WATCHDOG COUNCIL (3f+1 = 4 nodes)               │
17//! │                                                                  │
18//! │   ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐        │
19//! │   │Watchdog │   │Watchdog │   │Watchdog │   │Watchdog │        │
20//! │   │   #1    │   │   #2    │   │   #3    │   │   #4    │        │
21//! │   │ (TEE)   │   │ (Cloud) │   │ (HSM)   │   │ (Edge)  │        │
22//! │   └────┬────┘   └────┬────┘   └────┬────┘   └────┬────┘        │
23//! │        │             │             │             │              │
24//! │        └─────────────┴──────┬──────┴─────────────┘              │
25//! │                             ▼                                   │
26//! │                    ┌─────────────────┐                          │
27//! │                    │  BFT Consensus  │                          │
28//! │                    │   (2f+1 = 3)    │                          │
29//! │                    └────────┬────────┘                          │
30//! │                             │                                   │
31//! │              ┌──────────────┼──────────────┐                    │
32//! │              ▼              ▼              ▼                    │
33//! │         [APPROVE]       [DENY]        [RESET]                  │
34//! │                                                                  │
35//! │   Security: Even if 1 Watchdog is compromised (f=1),           │
36//! │             the system remains 100% secure!                     │
37//! └─────────────────────────────────────────────────────────────────┘
38//! ```
39//!
40//! ## Security Guarantees
41//!
42//! - **Safety**: Compromised minority cannot affect decisions
43//! - **Liveness**: System continues even with f failures
44//! - **Non-repudiation**: Threshold signature proves council consensus
45//!
46//! ---
47//!
48//! **Date**: 2026-01-01
49//! **Version**: 1.8.0 (Betonozás Edition - BFT)
50//! **Author**: Máté Róbert <stratosoiteam@gmail.com>
51
52use crate::crypto::{CryptoError, KeyStore, Result, SoftwareKeyStore};
53use crate::proof::Action;
54use crate::watchdog::{Watchdog, WatchdogError};
55use parking_lot::RwLock;
56use serde::{Deserialize, Serialize};
57use sha2::{Digest, Sha256};
58use std::collections::HashMap;
59use std::sync::Arc;
60use std::time::{SystemTime, UNIX_EPOCH};
61
62// ============================================================================
63// BFT TYPES
64// ============================================================================
65
66/// Watchdog Council member identity
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct MemberId(pub String);
69
70/// Vote from a council member
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Vote {
73    /// Member who cast the vote
74    pub member_id: MemberId,
75
76    /// The decision
77    pub decision: VoteDecision,
78
79    /// Signature over the vote
80    pub signature: Vec<u8>,
81
82    /// Timestamp
83    pub timestamp: u64,
84}
85
86/// Possible vote outcomes
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
88pub enum VoteDecision {
89    /// Action approved
90    Approve,
91    /// Action denied (rule violated)
92    Deny,
93    /// Hard reset required
94    HardReset,
95    /// Member abstained (couldn't evaluate)
96    Abstain,
97}
98
99/// Council consensus result
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ConsensusResult {
102    /// The final decision
103    pub decision: VoteDecision,
104
105    /// Number of votes for this decision
106    pub vote_count: usize,
107
108    /// Required quorum
109    pub quorum: usize,
110
111    /// All votes
112    pub votes: Vec<Vote>,
113
114    /// Threshold signature (aggregated from voters)
115    pub threshold_signature: ThresholdSignature,
116
117    /// Action hash (for verification)
118    pub action_hash: [u8; 32],
119
120    /// Consensus timestamp
121    pub timestamp: u64,
122}
123
124/// Threshold signature from council
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ThresholdSignature {
127    /// Combined signature data
128    pub combined_signature: Vec<u8>,
129
130    /// Public keys of signers
131    pub signer_pubkeys: Vec<Vec<u8>>,
132
133    /// Signature count
134    pub count: usize,
135}
136
137/// Council member (wraps a Watchdog)
138pub struct CouncilMember {
139    /// Member identity
140    pub id: MemberId,
141
142    /// The underlying Watchdog
143    watchdog: Watchdog,
144
145    /// Signing key
146    keystore: SoftwareKeyStore,
147
148    /// Member is active
149    pub active: bool,
150}
151
152/// BFT Watchdog Council
153///
154/// Implements Byzantine Fault Tolerant consensus for Watchdog decisions.
155/// Requires 2f+1 out of 3f+1 members to agree for a valid decision.
156#[allow(dead_code)]
157pub struct WatchdogCouncil {
158    /// Council members
159    members: Vec<Arc<RwLock<CouncilMember>>>,
160
161    /// Total members (n = 3f+1)
162    total_members: usize,
163
164    /// Maximum faulty members (f)
165    max_faulty: usize,
166
167    /// Required quorum (2f+1)
168    quorum: usize,
169
170    /// Council ID
171    pub council_id: String,
172
173    /// Shared rules (all members enforce same rules)
174    rules: Vec<String>,
175}
176
177// ============================================================================
178// IMPLEMENTATION
179// ============================================================================
180
181impl CouncilMember {
182    /// Create new council member
183    pub fn new(
184        id: impl Into<String>,
185        rules: Vec<String>,
186        capsule_hash: impl Into<String>,
187    ) -> Result<Self> {
188        let keystore = SoftwareKeyStore::generate()?;
189        let watchdog = Watchdog::new(rules, capsule_hash.into(), Box::new(keystore.clone()));
190
191        Ok(CouncilMember {
192            id: MemberId(id.into()),
193            watchdog,
194            keystore,
195            active: true,
196        })
197    }
198
199    /// Cast vote on action
200    pub fn vote(&self, action: &Action) -> Result<Vote> {
201        if !self.active {
202            return Ok(Vote {
203                member_id: self.id.clone(),
204                decision: VoteDecision::Abstain,
205                signature: vec![],
206                timestamp: Self::now(),
207            });
208        }
209
210        // Evaluate action with Watchdog
211        // Ok(None) = approved, Ok(Some(proof)) = denied
212        let decision = match self.watchdog.verify_action(action) {
213            Ok(None) => VoteDecision::Approve,
214            Ok(Some(_denial_proof)) => VoteDecision::Deny,
215            Err(WatchdogError::HardResetRequired(..)) => VoteDecision::HardReset,
216            Err(_) => VoteDecision::Deny,
217        };
218
219        let timestamp = Self::now();
220
221        // Sign the vote
222        let vote_data = self.serialize_vote_data(action, decision, timestamp);
223        let signature = self.keystore.sign(&vote_data)?;
224
225        Ok(Vote {
226            member_id: self.id.clone(),
227            decision,
228            signature,
229            timestamp,
230        })
231    }
232
233    /// Serialize vote data for signing
234    fn serialize_vote_data(
235        &self,
236        action: &Action,
237        decision: VoteDecision,
238        timestamp: u64,
239    ) -> Vec<u8> {
240        let mut data = Vec::new();
241        data.extend_from_slice(self.id.0.as_bytes());
242        data.extend_from_slice(&action.hash());
243        data.push(decision as u8);
244        data.extend_from_slice(&timestamp.to_le_bytes());
245        data
246    }
247
248    /// Get public key
249    pub fn public_key(&self) -> Vec<u8> {
250        self.keystore.public_key_bytes()
251    }
252
253    fn now() -> u64 {
254        SystemTime::now()
255            .duration_since(UNIX_EPOCH)
256            .unwrap()
257            .as_secs()
258    }
259}
260
261impl WatchdogCouncil {
262    /// Create new BFT Watchdog Council
263    ///
264    /// # Arguments
265    ///
266    /// * `num_members` - Total council size (should be 3f+1)
267    /// * `rules` - Ethical rules to enforce
268    /// * `capsule_hash` - Genome capsule hash
269    ///
270    /// # Panics
271    ///
272    /// Panics if num_members < 4 (minimum for f=1 fault tolerance)
273    ///
274    /// # Example
275    ///
276    /// ```rust,ignore
277    /// let council = WatchdogCouncil::new(
278    ///     4,  // 3*1+1 = can tolerate 1 faulty member
279    ///     vec!["Do no harm".to_string()],
280    ///     "capsule_hash"
281    /// )?;
282    /// ```
283    pub fn new(
284        num_members: usize,
285        rules: Vec<String>,
286        capsule_hash: impl Into<String> + Clone,
287    ) -> Result<Self> {
288        if num_members < 4 {
289            return Err(CryptoError::InvalidState(
290                "BFT requires at least 4 members (3f+1 where f=1)".into(),
291            ));
292        }
293
294        // Calculate f (max faulty)
295        // n = 3f+1 => f = (n-1)/3
296        let max_faulty = (num_members - 1) / 3;
297        let quorum = 2 * max_faulty + 1;
298
299        let mut members = Vec::new();
300        for i in 0..num_members {
301            let member =
302                CouncilMember::new(format!("member-{}", i), rules.clone(), capsule_hash.clone())?;
303            members.push(Arc::new(RwLock::new(member)));
304        }
305
306        let council_id = format!(
307            "council-{:x}",
308            Sha256::digest(format!("{:?}-{}", rules, num_members).as_bytes())[..8]
309                .iter()
310                .fold(0u64, |acc, &b| acc << 8 | b as u64)
311        );
312
313        Ok(WatchdogCouncil {
314            members,
315            total_members: num_members,
316            max_faulty,
317            quorum,
318            council_id,
319            rules,
320        })
321    }
322
323    /// Verify action with BFT consensus
324    ///
325    /// All active members vote, and the decision is made by quorum (2f+1).
326    ///
327    /// # Returns
328    ///
329    /// ConsensusResult with the final decision and threshold signature
330    pub fn verify_action(&self, action: &Action) -> Result<ConsensusResult> {
331        let mut votes = Vec::new();
332
333        // Collect votes from all members
334        for member_arc in &self.members {
335            let member = member_arc.read();
336            match member.vote(action) {
337                Ok(vote) => votes.push(vote),
338                Err(_) => {
339                    // Member failed to vote - count as abstain
340                    votes.push(Vote {
341                        member_id: member.id.clone(),
342                        decision: VoteDecision::Abstain,
343                        signature: vec![],
344                        timestamp: CouncilMember::now(),
345                    });
346                }
347            }
348        }
349
350        // Count votes
351        let mut vote_counts: HashMap<VoteDecision, usize> = HashMap::new();
352        for vote in &votes {
353            if vote.decision != VoteDecision::Abstain {
354                *vote_counts.entry(vote.decision).or_insert(0) += 1;
355            }
356        }
357
358        // Find winning decision (must have quorum)
359        let (decision, count) = vote_counts
360            .iter()
361            .max_by_key(|(_, &count)| count)
362            .map(|(&d, &c)| (d, c))
363            .unwrap_or((VoteDecision::Abstain, 0));
364
365        if count < self.quorum {
366            return Err(CryptoError::VerificationFailed(format!(
367                "No quorum reached: {} votes, {} required",
368                count, self.quorum
369            )));
370        }
371
372        // Create threshold signature
373        let threshold_sig = self.create_threshold_signature(&votes, action)?;
374
375        Ok(ConsensusResult {
376            decision,
377            vote_count: count,
378            quorum: self.quorum,
379            votes,
380            threshold_signature: threshold_sig,
381            action_hash: action.hash(),
382            timestamp: CouncilMember::now(),
383        })
384    }
385
386    /// Create threshold signature from votes
387    fn create_threshold_signature(
388        &self,
389        votes: &[Vote],
390        _action: &Action,
391    ) -> Result<ThresholdSignature> {
392        let mut combined = Vec::new();
393        let mut pubkeys = Vec::new();
394        let mut count = 0;
395
396        for vote in votes {
397            if vote.decision != VoteDecision::Abstain && !vote.signature.is_empty() {
398                combined.extend_from_slice(&vote.signature);
399                count += 1;
400
401                // Get pubkey from member
402                for member_arc in &self.members {
403                    let member = member_arc.read();
404                    if member.id == vote.member_id {
405                        pubkeys.push(member.public_key());
406                        break;
407                    }
408                }
409            }
410        }
411
412        // Hash the combined signatures for compactness
413        let combined_hash = Sha256::digest(&combined);
414
415        Ok(ThresholdSignature {
416            combined_signature: combined_hash.to_vec(),
417            signer_pubkeys: pubkeys,
418            count,
419        })
420    }
421
422    /// Disable a member (simulate failure/compromise)
423    pub fn disable_member(&self, member_idx: usize) -> Result<()> {
424        if member_idx >= self.total_members {
425            return Err(CryptoError::InvalidState("Invalid member index".into()));
426        }
427
428        let mut member = self.members[member_idx].write();
429        member.active = false;
430        Ok(())
431    }
432
433    /// Re-enable a member
434    pub fn enable_member(&self, member_idx: usize) -> Result<()> {
435        if member_idx >= self.total_members {
436            return Err(CryptoError::InvalidState("Invalid member index".into()));
437        }
438
439        let mut member = self.members[member_idx].write();
440        member.active = true;
441        Ok(())
442    }
443
444    /// Get council status
445    pub fn status(&self) -> CouncilStatus {
446        let active_count = self.members.iter().filter(|m| m.read().active).count();
447
448        CouncilStatus {
449            council_id: self.council_id.clone(),
450            total_members: self.total_members,
451            active_members: active_count,
452            max_faulty: self.max_faulty,
453            quorum: self.quorum,
454            healthy: active_count >= self.quorum,
455        }
456    }
457}
458
459/// Council status information
460#[derive(Debug, Clone, Serialize, Deserialize)]
461pub struct CouncilStatus {
462    pub council_id: String,
463    pub total_members: usize,
464    pub active_members: usize,
465    pub max_faulty: usize,
466    pub quorum: usize,
467    pub healthy: bool,
468}
469
470// ============================================================================
471// DISTRIBUTED COUNCIL (for network deployment)
472// ============================================================================
473
474/// Remote member configuration
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct RemoteMemberConfig {
477    /// Member ID
478    pub id: String,
479    /// Network address (e.g., "https://watchdog1.example.com")
480    pub address: String,
481    /// Public key for signature verification
482    pub public_key: Vec<u8>,
483    /// TEE attestation (if available)
484    pub attestation: Option<Vec<u8>>,
485}
486
487/// Distributed Council Configuration
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct DistributedCouncilConfig {
490    /// Remote members
491    pub members: Vec<RemoteMemberConfig>,
492    /// Vote timeout (milliseconds)
493    pub vote_timeout_ms: u64,
494    /// Retry count
495    pub retry_count: u32,
496}
497
498// ============================================================================
499// TESTS
500// ============================================================================
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    fn create_test_action() -> Action {
507        Action::delete("test.txt")
508    }
509
510    #[test]
511    fn test_council_creation() {
512        let council =
513            WatchdogCouncil::new(4, vec!["Do no harm".to_string()], "test_capsule").unwrap();
514
515        assert_eq!(council.total_members, 4);
516        assert_eq!(council.max_faulty, 1); // (4-1)/3 = 1
517        assert_eq!(council.quorum, 3); // 2*1+1 = 3
518    }
519
520    #[test]
521    fn test_council_consensus() {
522        let council = WatchdogCouncil::new(4, vec!["Allow all".to_string()], "test").unwrap();
523
524        let action = create_test_action();
525        let result = council.verify_action(&action).unwrap();
526
527        assert_eq!(result.decision, VoteDecision::Approve);
528        assert!(result.vote_count >= 3); // quorum
529    }
530
531    #[test]
532    fn test_council_survives_one_failure() {
533        let council = WatchdogCouncil::new(4, vec!["Allow all".to_string()], "test").unwrap();
534
535        // Disable one member
536        council.disable_member(0).unwrap();
537
538        let status = council.status();
539        assert_eq!(status.active_members, 3);
540        assert!(status.healthy); // Still have quorum
541
542        // Should still reach consensus
543        let action = create_test_action();
544        let result = council.verify_action(&action).unwrap();
545        assert_eq!(result.decision, VoteDecision::Approve);
546    }
547
548    #[test]
549    fn test_council_fails_with_too_many_failures() {
550        let council = WatchdogCouncil::new(4, vec!["Allow all".to_string()], "test").unwrap();
551
552        // Disable two members (more than f=1)
553        council.disable_member(0).unwrap();
554        council.disable_member(1).unwrap();
555
556        let status = council.status();
557        assert_eq!(status.active_members, 2);
558        assert!(!status.healthy); // Lost quorum
559
560        // Consensus should fail
561        let action = create_test_action();
562        assert!(council.verify_action(&action).is_err());
563    }
564
565    #[test]
566    fn test_threshold_signature() {
567        let council = WatchdogCouncil::new(4, vec!["Allow all".to_string()], "test").unwrap();
568
569        let action = create_test_action();
570        let result = council.verify_action(&action).unwrap();
571
572        // Threshold signature should have at least quorum signers
573        assert!(result.threshold_signature.count >= council.quorum);
574        assert!(!result.threshold_signature.combined_signature.is_empty());
575    }
576
577    #[test]
578    fn test_minimum_council_size() {
579        // Should fail with less than 4 members
580        let result = WatchdogCouncil::new(3, vec!["test".to_string()], "test");
581
582        assert!(result.is_err());
583    }
584
585    #[test]
586    fn test_larger_council() {
587        // 7 members: f=2, quorum=5
588        let council = WatchdogCouncil::new(7, vec!["Allow all".to_string()], "test").unwrap();
589
590        assert_eq!(council.max_faulty, 2); // (7-1)/3 = 2
591        assert_eq!(council.quorum, 5); // 2*2+1 = 5
592
593        // Can survive 2 failures
594        council.disable_member(0).unwrap();
595        council.disable_member(1).unwrap();
596
597        let action = create_test_action();
598        let result = council.verify_action(&action).unwrap();
599        assert_eq!(result.decision, VoteDecision::Approve);
600    }
601}