1use chie_shared::{BandwidthProof, ChunkRequest, ChunkResponse};
4use rand::RngCore;
5use thiserror::Error;
6use uuid::Uuid;
7
8#[derive(Debug, Error)]
10pub enum ValidationError {
11 #[error("Invalid timestamp: {0}")]
12 InvalidTimestamp(String),
13
14 #[error("Invalid nonce: {0}")]
15 InvalidNonce(String),
16
17 #[error("Invalid signature: {0}")]
18 InvalidSignature(String),
19
20 #[error("Invalid content CID: {0}")]
21 InvalidCid(String),
22
23 #[error("Invalid chunk index: {0}")]
24 InvalidChunkIndex(String),
25
26 #[error("Invalid latency: {0}")]
27 InvalidLatency(String),
28
29 #[error("Timestamp out of range: {0}")]
30 TimestampOutOfRange(String),
31}
32
33#[must_use]
44pub fn generate_challenge_nonce() -> [u8; 32] {
45 let mut nonce = [0u8; 32];
46 rand::thread_rng().fill_bytes(&mut nonce);
47 nonce
48}
49
50#[must_use]
52pub fn create_chunk_request(
53 content_cid: String,
54 chunk_index: u64,
55 requester_peer_id: String,
56 requester_public_key: [u8; 32],
57) -> ChunkRequest {
58 ChunkRequest {
59 content_cid,
60 chunk_index,
61 challenge_nonce: generate_challenge_nonce(),
62 requester_peer_id,
63 requester_public_key,
64 timestamp_ms: chrono::Utc::now().timestamp_millis(),
65 }
66}
67
68#[allow(clippy::too_many_arguments)]
70#[must_use]
71pub fn create_bandwidth_proof(
72 request: &ChunkRequest,
73 provider_peer_id: String,
74 provider_public_key: Vec<u8>,
75 bytes_transferred: u64,
76 provider_signature: Vec<u8>,
77 requester_signature: Vec<u8>,
78 chunk_hash: Vec<u8>,
79 start_timestamp_ms: i64,
80 end_timestamp_ms: i64,
81 latency_ms: u32,
82) -> BandwidthProof {
83 BandwidthProof {
84 session_id: Uuid::new_v4(),
85 content_cid: request.content_cid.clone(),
86 chunk_index: request.chunk_index,
87 bytes_transferred,
88 provider_peer_id,
89 requester_peer_id: request.requester_peer_id.clone(),
90 provider_public_key,
91 requester_public_key: request.requester_public_key.to_vec(),
92 provider_signature,
93 requester_signature,
94 challenge_nonce: request.challenge_nonce.to_vec(),
95 chunk_hash,
96 start_timestamp_ms,
97 end_timestamp_ms,
98 latency_ms,
99 }
100}
101
102pub fn validate_chunk_request(request: &ChunkRequest) -> Result<(), ValidationError> {
104 if request.content_cid.is_empty() {
106 return Err(ValidationError::InvalidCid(
107 "CID cannot be empty".to_string(),
108 ));
109 }
110
111 if request.challenge_nonce == [0u8; 32] {
113 return Err(ValidationError::InvalidNonce(
114 "Nonce cannot be all zeros".to_string(),
115 ));
116 }
117
118 let now = chrono::Utc::now().timestamp_millis();
120 let five_minutes = 5 * 60 * 1000;
121
122 if request.timestamp_ms > now + five_minutes {
123 return Err(ValidationError::TimestampOutOfRange(
124 "Request timestamp is too far in the future".to_string(),
125 ));
126 }
127
128 if request.timestamp_ms < now - five_minutes {
129 return Err(ValidationError::TimestampOutOfRange(
130 "Request timestamp is too old".to_string(),
131 ));
132 }
133
134 if request.requester_peer_id.is_empty() {
136 return Err(ValidationError::InvalidSignature(
137 "Requester peer ID cannot be empty".to_string(),
138 ));
139 }
140
141 Ok(())
142}
143
144pub fn validate_chunk_response(
146 response: &ChunkResponse,
147 request: &ChunkRequest,
148) -> Result<(), ValidationError> {
149 if response.challenge_echo != request.challenge_nonce {
151 return Err(ValidationError::InvalidNonce(
152 "Challenge echo does not match request nonce".to_string(),
153 ));
154 }
155
156 if response.encrypted_chunk.is_empty() {
158 return Err(ValidationError::InvalidSignature(
159 "Encrypted chunk cannot be empty".to_string(),
160 ));
161 }
162
163 if response.chunk_hash == [0u8; 32] {
165 return Err(ValidationError::InvalidSignature(
166 "Chunk hash cannot be all zeros".to_string(),
167 ));
168 }
169
170 if response.provider_signature.is_empty() {
172 return Err(ValidationError::InvalidSignature(
173 "Provider signature cannot be empty".to_string(),
174 ));
175 }
176
177 let now = chrono::Utc::now().timestamp_millis();
179 if response.timestamp_ms > now {
180 return Err(ValidationError::TimestampOutOfRange(
181 "Response timestamp is in the future".to_string(),
182 ));
183 }
184
185 Ok(())
186}
187
188pub fn validate_bandwidth_proof(proof: &BandwidthProof) -> Result<(), ValidationError> {
190 if proof.content_cid.is_empty() {
192 return Err(ValidationError::InvalidCid(
193 "CID cannot be empty".to_string(),
194 ));
195 }
196
197 if proof.challenge_nonce.is_empty() || proof.challenge_nonce == vec![0u8; 32] {
199 return Err(ValidationError::InvalidNonce(
200 "Invalid challenge nonce".to_string(),
201 ));
202 }
203
204 if proof.provider_signature.is_empty() {
206 return Err(ValidationError::InvalidSignature(
207 "Provider signature is empty".to_string(),
208 ));
209 }
210
211 if proof.requester_signature.is_empty() {
212 return Err(ValidationError::InvalidSignature(
213 "Requester signature is empty".to_string(),
214 ));
215 }
216
217 if proof.provider_public_key.len() != 32 {
219 return Err(ValidationError::InvalidSignature(
220 "Invalid provider public key length".to_string(),
221 ));
222 }
223
224 if proof.requester_public_key.len() != 32 {
225 return Err(ValidationError::InvalidSignature(
226 "Invalid requester public key length".to_string(),
227 ));
228 }
229
230 if proof.start_timestamp_ms >= proof.end_timestamp_ms {
232 return Err(ValidationError::InvalidTimestamp(
233 "Start timestamp must be before end timestamp".to_string(),
234 ));
235 }
236
237 let duration_ms = (proof.end_timestamp_ms - proof.start_timestamp_ms) as u32;
239 if proof.latency_ms > duration_ms {
240 return Err(ValidationError::InvalidLatency(
241 "Latency cannot exceed transfer duration".to_string(),
242 ));
243 }
244
245 if proof.bytes_transferred == 0 {
247 return Err(ValidationError::InvalidSignature(
248 "Bytes transferred cannot be zero".to_string(),
249 ));
250 }
251
252 Ok(())
253}
254
255#[inline]
257#[must_use]
258pub fn is_valid_cid(cid: &str) -> bool {
259 if cid.is_empty() {
261 return false;
262 }
263
264 if cid.starts_with("Qm") && cid.len() >= 46 {
266 return true;
267 }
268
269 if cid.len() > 10 && (cid.starts_with('b') || cid.starts_with('z') || cid.starts_with('f')) {
271 return true;
272 }
273
274 false
275}
276
277#[inline]
279#[must_use]
280#[allow(dead_code)]
281pub fn is_nonce_unique(_nonce: &[u8], _peer_id: &str) -> bool {
282 true
285}
286
287#[inline]
289pub const fn calculate_latency(start_ms: i64, end_ms: i64) -> u32 {
290 let diff = end_ms.saturating_sub(start_ms);
291 if diff < 0 { 0 } else { diff as u32 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use chie_crypto::KeyPair;
298
299 #[test]
300 fn test_generate_challenge_nonce() {
301 let nonce1 = generate_challenge_nonce();
302 let nonce2 = generate_challenge_nonce();
303
304 assert_eq!(nonce1.len(), 32);
305 assert_eq!(nonce2.len(), 32);
306 assert_ne!(nonce1, nonce2);
307 }
308
309 #[test]
310 fn test_create_chunk_request() {
311 let keypair = KeyPair::generate();
312 let request = create_chunk_request(
313 "QmTest123".to_string(),
314 5,
315 "peer-abc".to_string(),
316 keypair.public_key(),
317 );
318
319 assert_eq!(request.content_cid, "QmTest123");
320 assert_eq!(request.chunk_index, 5);
321 assert_eq!(request.requester_peer_id, "peer-abc");
322 assert_eq!(request.challenge_nonce.len(), 32);
323 }
324
325 #[test]
326 fn test_validate_chunk_request() {
327 let keypair = KeyPair::generate();
328 let request = create_chunk_request(
329 "QmTest".to_string(),
330 0,
331 "peer".to_string(),
332 keypair.public_key(),
333 );
334
335 assert!(validate_chunk_request(&request).is_ok());
336
337 let mut bad_request = request.clone();
339 bad_request.content_cid = String::new();
340 assert!(validate_chunk_request(&bad_request).is_err());
341
342 let mut bad_request = request;
344 bad_request.challenge_nonce = [0u8; 32];
345 assert!(validate_chunk_request(&bad_request).is_err());
346 }
347
348 #[test]
349 fn test_validate_bandwidth_proof() {
350 let keypair = KeyPair::generate();
351 let request = create_chunk_request(
352 "QmTest".to_string(),
353 0,
354 "peer".to_string(),
355 keypair.public_key(),
356 );
357
358 let proof = create_bandwidth_proof(
359 &request,
360 "provider".to_string(),
361 vec![1u8; 32],
362 1024,
363 vec![1u8; 64],
364 vec![2u8; 64],
365 vec![3u8; 32],
366 1000,
367 2000,
368 100,
369 );
370
371 assert!(validate_bandwidth_proof(&proof).is_ok());
372 }
373
374 #[test]
375 fn test_is_valid_cid() {
376 assert!(is_valid_cid(
377 "QmTest1234567890123456789012345678901234567890"
378 ));
379 assert!(is_valid_cid(
380 "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
381 ));
382 assert!(!is_valid_cid(""));
383 assert!(!is_valid_cid("invalid"));
384 }
385
386 #[test]
387 fn test_calculate_latency() {
388 assert_eq!(calculate_latency(1000, 1500), 500);
389 assert_eq!(calculate_latency(2000, 2000), 0);
390 assert_eq!(calculate_latency(2000, 1500), 0); }
392}