chie_core/
validation.rs

1//! Content validation utilities for CHIE Protocol.
2//!
3//! This module provides comprehensive validation functions for content,
4//! chunks, proofs, and other protocol elements.
5
6use chie_shared::{BandwidthProof, CHUNK_SIZE, ChunkRequest, ChunkResponse};
7use std::time::{Duration, SystemTime};
8
9/// Content validation errors.
10#[derive(Debug, thiserror::Error)]
11pub enum ContentValidationError {
12    #[error("Invalid content size: {0} bytes")]
13    InvalidContentSize(u64),
14
15    #[error("Invalid chunk index: {index} out of {total}")]
16    InvalidChunkIndex { index: u64, total: u64 },
17
18    #[error("Invalid timestamp: {0}")]
19    InvalidTimestamp(String),
20
21    #[error("Invalid signature length: expected {expected}, got {actual}")]
22    InvalidSignatureLength { expected: usize, actual: usize },
23
24    #[error("Invalid public key length: expected {expected}, got {actual}")]
25    InvalidPublicKeyLength { expected: usize, actual: usize },
26
27    #[error("Invalid bandwidth value: {0}")]
28    InvalidBandwidth(String),
29
30    #[error("Content too large: {size} bytes exceeds max {max} bytes")]
31    ContentTooLarge { size: u64, max: u64 },
32
33    #[error("Content too small: {size} bytes is below min {min} bytes")]
34    ContentTooSmall { size: u64, min: u64 },
35}
36
37/// Content size limits.
38pub struct ContentLimits {
39    /// Minimum content size (1 KB).
40    pub min_size: u64,
41    /// Maximum content size (10 GB).
42    pub max_size: u64,
43    /// Maximum chunks per content.
44    pub max_chunks: u64,
45}
46
47impl Default for ContentLimits {
48    fn default() -> Self {
49        Self {
50            min_size: 1024,                    // 1 KB
51            max_size: 10 * 1024 * 1024 * 1024, // 10 GB
52            max_chunks: 100_000,               // ~400 GB at 4MB chunks
53        }
54    }
55}
56
57/// Validate content size against limits.
58#[inline]
59pub fn validate_content_size(
60    size: u64,
61    limits: &ContentLimits,
62) -> Result<(), ContentValidationError> {
63    if size < limits.min_size {
64        return Err(ContentValidationError::ContentTooSmall {
65            size,
66            min: limits.min_size,
67        });
68    }
69
70    if size > limits.max_size {
71        return Err(ContentValidationError::ContentTooLarge {
72            size,
73            max: limits.max_size,
74        });
75    }
76
77    Ok(())
78}
79
80/// Validate chunk index is within bounds.
81#[inline]
82pub fn validate_chunk_index(
83    chunk_index: u64,
84    total_chunks: u64,
85) -> Result<(), ContentValidationError> {
86    if chunk_index >= total_chunks {
87        return Err(ContentValidationError::InvalidChunkIndex {
88            index: chunk_index,
89            total: total_chunks,
90        });
91    }
92
93    Ok(())
94}
95
96/// Validate chunk request timestamp is within acceptable window.
97#[inline]
98pub fn validate_request_timestamp(
99    request: &ChunkRequest,
100    max_age: Duration,
101) -> Result<(), ContentValidationError> {
102    let now = SystemTime::now();
103    let request_time = SystemTime::UNIX_EPOCH + Duration::from_millis(request.timestamp_ms as u64);
104
105    let age = now.duration_since(request_time).map_err(|_| {
106        ContentValidationError::InvalidTimestamp("Request timestamp is in the future".to_string())
107    })?;
108
109    if age > max_age {
110        return Err(ContentValidationError::InvalidTimestamp(format!(
111            "Request is too old: {:?} > {:?}",
112            age, max_age
113        )));
114    }
115
116    Ok(())
117}
118
119/// Validate chunk response has proper signature length.
120#[inline]
121pub fn validate_response_signature(response: &ChunkResponse) -> Result<(), ContentValidationError> {
122    // Ed25519 signatures are 64 bytes
123    const ED25519_SIG_LEN: usize = 64;
124
125    if response.provider_signature.len() != ED25519_SIG_LEN {
126        return Err(ContentValidationError::InvalidSignatureLength {
127            expected: ED25519_SIG_LEN,
128            actual: response.provider_signature.len(),
129        });
130    }
131
132    if response.provider_public_key.len() != 32 {
133        return Err(ContentValidationError::InvalidPublicKeyLength {
134            expected: 32,
135            actual: response.provider_public_key.len(),
136        });
137    }
138
139    Ok(())
140}
141
142/// Validate bandwidth proof structure.
143#[inline]
144pub fn validate_proof_structure(proof: &BandwidthProof) -> Result<(), ContentValidationError> {
145    // Validate signature lengths
146    const ED25519_SIG_LEN: usize = 64;
147    const ED25519_KEY_LEN: usize = 32;
148
149    if proof.provider_signature.len() != ED25519_SIG_LEN {
150        return Err(ContentValidationError::InvalidSignatureLength {
151            expected: ED25519_SIG_LEN,
152            actual: proof.provider_signature.len(),
153        });
154    }
155
156    if proof.requester_signature.len() != ED25519_SIG_LEN {
157        return Err(ContentValidationError::InvalidSignatureLength {
158            expected: ED25519_SIG_LEN,
159            actual: proof.requester_signature.len(),
160        });
161    }
162
163    if proof.provider_public_key.len() != ED25519_KEY_LEN {
164        return Err(ContentValidationError::InvalidPublicKeyLength {
165            expected: ED25519_KEY_LEN,
166            actual: proof.provider_public_key.len(),
167        });
168    }
169
170    if proof.requester_public_key.len() != ED25519_KEY_LEN {
171        return Err(ContentValidationError::InvalidPublicKeyLength {
172            expected: ED25519_KEY_LEN,
173            actual: proof.requester_public_key.len(),
174        });
175    }
176
177    // Validate timestamps
178    if proof.start_timestamp_ms >= proof.end_timestamp_ms {
179        return Err(ContentValidationError::InvalidTimestamp(
180            "Start timestamp must be before end timestamp".to_string(),
181        ));
182    }
183
184    // Validate bytes transferred
185    if proof.bytes_transferred == 0 {
186        return Err(ContentValidationError::InvalidBandwidth(
187            "Bytes transferred cannot be zero".to_string(),
188        ));
189    }
190
191    Ok(())
192}
193
194/// Calculate expected number of chunks for content.
195#[must_use]
196#[inline]
197pub const fn calculate_expected_chunks(content_size: u64) -> u64 {
198    let chunks = content_size / CHUNK_SIZE as u64;
199    let remainder = content_size % CHUNK_SIZE as u64;
200
201    if remainder > 0 {
202        chunks + 1
203    } else if chunks == 0 {
204        1
205    } else {
206        chunks
207    }
208}
209
210/// Validate bandwidth calculation is reasonable.
211#[inline]
212pub fn validate_bandwidth(
213    bytes: u64,
214    duration_ms: u64,
215    max_bandwidth_mbps: f64,
216) -> Result<(), ContentValidationError> {
217    if duration_ms == 0 {
218        return Err(ContentValidationError::InvalidBandwidth(
219            "Duration cannot be zero".to_string(),
220        ));
221    }
222
223    // Calculate bandwidth in Mbps
224    let bits = bytes as f64 * 8.0;
225    let seconds = duration_ms as f64 / 1000.0;
226    let mbps = bits / seconds / 1_000_000.0;
227
228    // Check if bandwidth is suspiciously high (> 10 Gbps or specified max)
229    let max_mbps = max_bandwidth_mbps.max(10_000.0);
230    if mbps > max_mbps {
231        return Err(ContentValidationError::InvalidBandwidth(format!(
232            "Bandwidth {:.2} Mbps exceeds maximum {:.2} Mbps",
233            mbps, max_mbps
234        )));
235    }
236
237    Ok(())
238}
239
240/// Sanitize content ID (CID) for safe filesystem usage.
241#[must_use]
242#[inline]
243pub fn sanitize_cid(cid: &str) -> String {
244    // Remove any characters that might be problematic for filesystems
245    cid.chars()
246        .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
247        .collect()
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use chie_crypto::KeyPair;
254    use chie_shared::ChunkRequest;
255
256    #[test]
257    fn test_validate_content_size() {
258        let limits = ContentLimits::default();
259
260        // Valid size
261        assert!(validate_content_size(1024 * 1024, &limits).is_ok());
262
263        // Too small
264        assert!(validate_content_size(512, &limits).is_err());
265
266        // Too large
267        assert!(validate_content_size(20 * 1024 * 1024 * 1024, &limits).is_err());
268    }
269
270    #[test]
271    fn test_validate_chunk_index() {
272        assert!(validate_chunk_index(0, 10).is_ok());
273        assert!(validate_chunk_index(9, 10).is_ok());
274        assert!(validate_chunk_index(10, 10).is_err());
275        assert!(validate_chunk_index(100, 10).is_err());
276    }
277
278    #[test]
279    fn test_validate_request_timestamp() {
280        let keypair = KeyPair::generate();
281        let request = ChunkRequest {
282            content_cid: "QmTest".to_string(),
283            chunk_index: 0,
284            challenge_nonce: [1u8; 32],
285            requester_peer_id: "peer1".to_string(),
286            requester_public_key: keypair.public_key(),
287            timestamp_ms: chrono::Utc::now().timestamp_millis(),
288        };
289
290        // Should be valid within 5 minutes
291        assert!(validate_request_timestamp(&request, Duration::from_secs(300)).is_ok());
292
293        // Old request
294        let old_request = ChunkRequest {
295            timestamp_ms: chrono::Utc::now().timestamp_millis() - 600_000, // 10 minutes ago
296            ..request
297        };
298        assert!(validate_request_timestamp(&old_request, Duration::from_secs(300)).is_err());
299    }
300
301    #[test]
302    fn test_calculate_expected_chunks() {
303        assert_eq!(calculate_expected_chunks(0), 1);
304        assert_eq!(calculate_expected_chunks(CHUNK_SIZE as u64), 1);
305        assert_eq!(calculate_expected_chunks(CHUNK_SIZE as u64 + 1), 2);
306        assert_eq!(calculate_expected_chunks(CHUNK_SIZE as u64 * 10), 10);
307        assert_eq!(calculate_expected_chunks(CHUNK_SIZE as u64 * 10 + 100), 11);
308    }
309
310    #[test]
311    fn test_validate_bandwidth() {
312        // Valid bandwidth: 100 MB in 10 seconds = 80 Mbps
313        assert!(validate_bandwidth(100_000_000, 10_000, 10_000.0).is_ok());
314
315        // Zero duration
316        assert!(validate_bandwidth(100_000_000, 0, 10_000.0).is_err());
317
318        // Suspiciously high bandwidth: 10 GB in 1 second = 80 Gbps
319        assert!(validate_bandwidth(10_000_000_000, 1_000, 10_000.0).is_err());
320    }
321
322    #[test]
323    fn test_sanitize_cid() {
324        assert_eq!(sanitize_cid("QmTest123"), "QmTest123");
325        assert_eq!(sanitize_cid("Qm../../../etc/passwd"), "Qmetcpasswd");
326        assert_eq!(sanitize_cid("Qm Test@123!"), "QmTest123");
327        assert_eq!(sanitize_cid("valid-cid_123"), "valid-cid_123");
328    }
329
330    #[test]
331    fn test_validate_response_signature() {
332        let valid_response = ChunkResponse {
333            encrypted_chunk: vec![1u8; 100],
334            chunk_hash: [2u8; 32],
335            provider_signature: vec![3u8; 64],
336            provider_public_key: [4u8; 32],
337            challenge_echo: [5u8; 32],
338            timestamp_ms: chrono::Utc::now().timestamp_millis(),
339        };
340
341        assert!(validate_response_signature(&valid_response).is_ok());
342
343        // Invalid signature length
344        let invalid_sig = ChunkResponse {
345            provider_signature: vec![3u8; 32],
346            ..valid_response.clone()
347        };
348        assert!(validate_response_signature(&invalid_sig).is_err());
349    }
350}