chie_core/protocol/
mod.rs

1//! Bandwidth proof protocol implementation.
2
3use chie_shared::{BandwidthProof, ChunkRequest, ChunkResponse};
4use rand::RngCore;
5use thiserror::Error;
6use uuid::Uuid;
7
8/// Protocol validation errors.
9#[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/// Generate a challenge nonce for a chunk request.
34///
35/// # Examples
36///
37/// ```
38/// use chie_core::protocol::generate_challenge_nonce;
39///
40/// let nonce = generate_challenge_nonce();
41/// assert_eq!(nonce.len(), 32);
42/// ```
43#[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/// Create a new chunk request.
51#[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/// Create a bandwidth proof from a completed transfer.
69#[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
102/// Validate a chunk request.
103pub fn validate_chunk_request(request: &ChunkRequest) -> Result<(), ValidationError> {
104    // Validate CID format (basic check)
105    if request.content_cid.is_empty() {
106        return Err(ValidationError::InvalidCid(
107            "CID cannot be empty".to_string(),
108        ));
109    }
110
111    // Validate nonce
112    if request.challenge_nonce == [0u8; 32] {
113        return Err(ValidationError::InvalidNonce(
114            "Nonce cannot be all zeros".to_string(),
115        ));
116    }
117
118    // Validate timestamp (within reasonable range)
119    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    // Validate peer ID
135    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
144/// Validate a chunk response.
145pub fn validate_chunk_response(
146    response: &ChunkResponse,
147    request: &ChunkRequest,
148) -> Result<(), ValidationError> {
149    // Validate challenge echo
150    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    // Validate encrypted chunk is not empty
157    if response.encrypted_chunk.is_empty() {
158        return Err(ValidationError::InvalidSignature(
159            "Encrypted chunk cannot be empty".to_string(),
160        ));
161    }
162
163    // Validate chunk hash
164    if response.chunk_hash == [0u8; 32] {
165        return Err(ValidationError::InvalidSignature(
166            "Chunk hash cannot be all zeros".to_string(),
167        ));
168    }
169
170    // Validate signature
171    if response.provider_signature.is_empty() {
172        return Err(ValidationError::InvalidSignature(
173            "Provider signature cannot be empty".to_string(),
174        ));
175    }
176
177    // Validate timestamp
178    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
188/// Validate a bandwidth proof.
189pub fn validate_bandwidth_proof(proof: &BandwidthProof) -> Result<(), ValidationError> {
190    // Validate CID
191    if proof.content_cid.is_empty() {
192        return Err(ValidationError::InvalidCid(
193            "CID cannot be empty".to_string(),
194        ));
195    }
196
197    // Validate nonce
198    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    // Validate signatures
205    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    // Validate public keys
218    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    // Validate timestamps
231    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    // Validate latency is reasonable
238    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    // Validate bytes transferred
246    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/// Check if a CID format is valid (basic validation).
256#[inline]
257#[must_use]
258pub fn is_valid_cid(cid: &str) -> bool {
259    // Basic CID validation - starts with Qm and has reasonable length
260    if cid.is_empty() {
261        return false;
262    }
263
264    // Check if it's a valid base58 IPFS CID (simplified check)
265    if cid.starts_with("Qm") && cid.len() >= 46 {
266        return true;
267    }
268
269    // Check if it's a CIDv1 format (starts with b, z, f, etc.)
270    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/// Verify nonce uniqueness (in production, this would check against a database).
278#[inline]
279#[must_use]
280#[allow(dead_code)]
281pub fn is_nonce_unique(_nonce: &[u8], _peer_id: &str) -> bool {
282    // In production, this would check against a nonce cache/database
283    // For now, we assume all nonces are unique
284    true
285}
286
287/// Calculate expected latency from timestamps.
288#[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        // Test empty CID
338        let mut bad_request = request.clone();
339        bad_request.content_cid = String::new();
340        assert!(validate_chunk_request(&bad_request).is_err());
341
342        // Test zero nonce
343        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); // Handles negative
391    }
392}