_hope_core/
consensus.rs

1// TODO v1.5.0: Migrate ConsensusVerifier to use KeyStore trait instead of deprecated KeyPair
2#![allow(deprecated)]
3
4use crate::crypto::KeyPair;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[derive(Debug, Error)]
9pub enum ConsensusError {
10    #[error("No consensus achieved: required {required} sources, got {achieved}")]
11    NoConsensus { required: usize, achieved: usize },
12
13    #[error("Invalid signature from source: {0}")]
14    InvalidSignature(String),
15
16    #[error("Insufficient readings: required at least {required}, got {actual}")]
17    InsufficientReadings { required: usize, actual: usize },
18
19    #[error("Crypto error: {0}")]
20    CryptoError(#[from] crate::crypto::CryptoError),
21}
22
23pub type Result<T> = std::result::Result<T, ConsensusError>;
24
25/// A sensor reading with cryptographic signature
26///
27/// Used for multi-source reality verification (Byzantine Fault Tolerance)
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SensorReading {
30    /// The measured value
31    pub value: f64,
32
33    /// Unix timestamp when reading was taken
34    pub timestamp: u64,
35
36    /// Unique identifier for the sensor
37    pub source_id: String,
38
39    /// Cryptographic signature of (value, timestamp, source_id)
40    pub signature: Vec<u8>,
41}
42
43impl SensorReading {
44    /// Create a new sensor reading
45    pub fn new(value: f64, source_id: String) -> Self {
46        let timestamp = chrono::Utc::now().timestamp() as u64;
47
48        SensorReading {
49            value,
50            timestamp,
51            source_id,
52            signature: Vec::new(), // Will be filled by signing
53        }
54    }
55
56    /// Get the data to be signed
57    pub fn signing_data(&self) -> Vec<u8> {
58        let mut data = Vec::new();
59        data.extend_from_slice(&self.value.to_le_bytes());
60        data.extend_from_slice(&self.timestamp.to_le_bytes());
61        data.extend_from_slice(self.source_id.as_bytes());
62        data
63    }
64
65    /// Sign this reading
66    pub fn sign(&mut self, keypair: &KeyPair) -> Result<()> {
67        let data = self.signing_data();
68        self.signature = keypair.sign(&data)?;
69        Ok(())
70    }
71
72    /// Verify signature
73    pub fn verify(&self, keypair: &KeyPair) -> Result<()> {
74        let data = self.signing_data();
75        keypair
76            .verify(&data, &self.signature)
77            .map_err(|_| ConsensusError::InvalidSignature(self.source_id.clone()))?;
78        Ok(())
79    }
80}
81
82/// Multi-source consensus verifier (Byzantine Fault Tolerant)
83///
84/// Prevents "oracle attacks" on sensor data by requiring
85/// agreement from multiple independent sources.
86pub struct ConsensusVerifier {
87    /// Minimum number of sources that must agree
88    required_sources: usize,
89
90    /// Tolerance for values to be considered "in agreement"
91    tolerance: f64,
92}
93
94impl ConsensusVerifier {
95    /// Create a new consensus verifier
96    ///
97    /// # Arguments
98    /// * `required_sources` - Minimum sources that must agree (e.g., 3 for 2/3 majority)
99    /// * `tolerance` - Max difference from median to be considered agreeing
100    pub fn new(required_sources: usize, tolerance: f64) -> Self {
101        ConsensusVerifier {
102            required_sources,
103            tolerance,
104        }
105    }
106
107    /// Verify readings and return consensus value
108    ///
109    /// This implements Byzantine Fault Tolerance:
110    /// 1. Verify each sensor signature
111    /// 2. Calculate median value
112    /// 3. Count how many sensors agree (within tolerance)
113    /// 4. Require minimum number of agreeing sensors
114    pub fn verify_readings(&self, readings: &[SensorReading], keypairs: &[KeyPair]) -> Result<f64> {
115        // Check we have enough readings
116        if readings.len() < self.required_sources {
117            return Err(ConsensusError::InsufficientReadings {
118                required: self.required_sources,
119                actual: readings.len(),
120            });
121        }
122
123        // Verify each sensor signature
124        for (reading, keypair) in readings.iter().zip(keypairs.iter()) {
125            reading.verify(keypair)?;
126        }
127
128        // Calculate median
129        let median = self.calculate_median(readings);
130
131        // Count consensus (how many within tolerance of median)
132        let consensus_count = readings
133            .iter()
134            .filter(|r| (r.value - median).abs() < self.tolerance)
135            .count();
136
137        // Check if we have consensus
138        if consensus_count < self.required_sources {
139            return Err(ConsensusError::NoConsensus {
140                required: self.required_sources,
141                achieved: consensus_count,
142            });
143        }
144
145        Ok(median)
146    }
147
148    /// Calculate median of sensor readings
149    fn calculate_median(&self, readings: &[SensorReading]) -> f64 {
150        let mut values: Vec<f64> = readings.iter().map(|r| r.value).collect();
151        values.sort_by(|a, b| a.partial_cmp(b).unwrap());
152
153        let len = values.len();
154        if len.is_multiple_of(2) {
155            (values[len / 2 - 1] + values[len / 2]) / 2.0
156        } else {
157            values[len / 2]
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn create_signed_reading(value: f64, source_id: &str, keypair: &KeyPair) -> SensorReading {
167        let mut reading = SensorReading::new(value, source_id.to_string());
168        reading.sign(keypair).unwrap();
169        reading
170    }
171
172    #[test]
173    fn test_sensor_reading_creation() {
174        let reading = SensorReading::new(42.0, "sensor1".to_string());
175        assert_eq!(reading.value, 42.0);
176        assert_eq!(reading.source_id, "sensor1");
177    }
178
179    #[test]
180    fn test_sensor_reading_signature() {
181        let keypair = KeyPair::generate().unwrap();
182        let mut reading = SensorReading::new(42.0, "sensor1".to_string());
183
184        reading.sign(&keypair).unwrap();
185        assert!(!reading.signature.is_empty());
186
187        // Verify should succeed
188        assert!(reading.verify(&keypair).is_ok());
189    }
190
191    #[test]
192    fn test_consensus_with_agreement() {
193        let verifier = ConsensusVerifier::new(3, 0.1);
194
195        let keypairs: Vec<KeyPair> = (0..4).map(|_| KeyPair::generate().unwrap()).collect();
196
197        let readings = vec![
198            create_signed_reading(10.0, "A", &keypairs[0]),
199            create_signed_reading(10.05, "B", &keypairs[1]),
200            create_signed_reading(10.02, "C", &keypairs[2]),
201            create_signed_reading(10.01, "D", &keypairs[3]),
202        ];
203
204        let result = verifier.verify_readings(&readings, &keypairs).unwrap();
205        assert!((result - 10.015).abs() < 0.01); // Median should be ~10.015
206    }
207
208    #[test]
209    fn test_consensus_fails_with_disagreement() {
210        let verifier = ConsensusVerifier::new(3, 0.1);
211
212        let keypairs: Vec<KeyPair> = (0..3).map(|_| KeyPair::generate().unwrap()).collect();
213
214        let readings = vec![
215            create_signed_reading(10.0, "A", &keypairs[0]),
216            create_signed_reading(10.05, "B", &keypairs[1]),
217            create_signed_reading(50.0, "C", &keypairs[2]), // Outlier
218        ];
219
220        let result = verifier.verify_readings(&readings, &keypairs);
221        assert!(matches!(result, Err(ConsensusError::NoConsensus { .. })));
222    }
223
224    #[test]
225    fn test_insufficient_readings() {
226        let verifier = ConsensusVerifier::new(5, 0.1);
227
228        let keypairs: Vec<KeyPair> = (0..2).map(|_| KeyPair::generate().unwrap()).collect();
229
230        let readings = vec![
231            create_signed_reading(10.0, "A", &keypairs[0]),
232            create_signed_reading(10.0, "B", &keypairs[1]),
233        ];
234
235        let result = verifier.verify_readings(&readings, &keypairs);
236        assert!(matches!(
237            result,
238            Err(ConsensusError::InsufficientReadings { .. })
239        ));
240    }
241
242    #[test]
243    fn test_invalid_signature_detected() {
244        let verifier = ConsensusVerifier::new(2, 0.1);
245
246        let keypairs: Vec<KeyPair> = (0..2).map(|_| KeyPair::generate().unwrap()).collect();
247
248        let mut readings = vec![
249            create_signed_reading(10.0, "A", &keypairs[0]),
250            create_signed_reading(10.0, "B", &keypairs[1]),
251        ];
252
253        // Tamper with signature
254        readings[0].signature[0] ^= 0xFF;
255
256        let result = verifier.verify_readings(&readings, &keypairs);
257        assert!(matches!(
258            result,
259            Err(ConsensusError::InvalidSignature { .. })
260        ));
261    }
262
263    #[test]
264    fn test_median_calculation_odd() {
265        let verifier = ConsensusVerifier::new(3, 0.1);
266
267        let keypairs: Vec<KeyPair> = (0..5).map(|_| KeyPair::generate().unwrap()).collect();
268
269        let readings = vec![
270            create_signed_reading(1.0, "A", &keypairs[0]),
271            create_signed_reading(2.0, "B", &keypairs[1]),
272            create_signed_reading(3.0, "C", &keypairs[2]),
273            create_signed_reading(4.0, "D", &keypairs[3]),
274            create_signed_reading(5.0, "E", &keypairs[4]),
275        ];
276
277        let median = verifier.calculate_median(&readings);
278        assert_eq!(median, 3.0);
279    }
280
281    #[test]
282    fn test_median_calculation_even() {
283        let verifier = ConsensusVerifier::new(2, 0.1);
284
285        let keypairs: Vec<KeyPair> = (0..4).map(|_| KeyPair::generate().unwrap()).collect();
286
287        let readings = vec![
288            create_signed_reading(1.0, "A", &keypairs[0]),
289            create_signed_reading(2.0, "B", &keypairs[1]),
290            create_signed_reading(3.0, "C", &keypairs[2]),
291            create_signed_reading(4.0, "D", &keypairs[3]),
292        ];
293
294        let median = verifier.calculate_median(&readings);
295        assert_eq!(median, 2.5);
296    }
297}