use chie_shared::{BandwidthProof, ChunkRequest, ChunkResponse};
use rand::Rng as _;
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("Invalid timestamp: {0}")]
InvalidTimestamp(String),
#[error("Invalid nonce: {0}")]
InvalidNonce(String),
#[error("Invalid signature: {0}")]
InvalidSignature(String),
#[error("Invalid content CID: {0}")]
InvalidCid(String),
#[error("Invalid chunk index: {0}")]
InvalidChunkIndex(String),
#[error("Invalid latency: {0}")]
InvalidLatency(String),
#[error("Timestamp out of range: {0}")]
TimestampOutOfRange(String),
}
#[must_use]
pub fn generate_challenge_nonce() -> [u8; 32] {
let mut nonce = [0u8; 32];
rand::rng().fill_bytes(&mut nonce);
nonce
}
#[must_use]
pub fn create_chunk_request(
content_cid: String,
chunk_index: u64,
requester_peer_id: String,
requester_public_key: [u8; 32],
) -> ChunkRequest {
ChunkRequest {
content_cid,
chunk_index,
challenge_nonce: generate_challenge_nonce(),
requester_peer_id,
requester_public_key,
timestamp_ms: chrono::Utc::now().timestamp_millis(),
}
}
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn create_bandwidth_proof(
request: &ChunkRequest,
provider_peer_id: String,
provider_public_key: Vec<u8>,
bytes_transferred: u64,
provider_signature: Vec<u8>,
requester_signature: Vec<u8>,
chunk_hash: Vec<u8>,
start_timestamp_ms: i64,
end_timestamp_ms: i64,
latency_ms: u32,
) -> BandwidthProof {
BandwidthProof {
session_id: Uuid::new_v4(),
content_cid: request.content_cid.clone(),
chunk_index: request.chunk_index,
bytes_transferred,
provider_peer_id,
requester_peer_id: request.requester_peer_id.clone(),
provider_public_key,
requester_public_key: request.requester_public_key.to_vec(),
provider_signature,
requester_signature,
challenge_nonce: request.challenge_nonce.to_vec(),
chunk_hash,
start_timestamp_ms,
end_timestamp_ms,
latency_ms,
}
}
pub fn validate_chunk_request(request: &ChunkRequest) -> Result<(), ValidationError> {
if request.content_cid.is_empty() {
return Err(ValidationError::InvalidCid(
"CID cannot be empty".to_string(),
));
}
if request.challenge_nonce == [0u8; 32] {
return Err(ValidationError::InvalidNonce(
"Nonce cannot be all zeros".to_string(),
));
}
let now = chrono::Utc::now().timestamp_millis();
let five_minutes = 5 * 60 * 1000;
if request.timestamp_ms > now + five_minutes {
return Err(ValidationError::TimestampOutOfRange(
"Request timestamp is too far in the future".to_string(),
));
}
if request.timestamp_ms < now - five_minutes {
return Err(ValidationError::TimestampOutOfRange(
"Request timestamp is too old".to_string(),
));
}
if request.requester_peer_id.is_empty() {
return Err(ValidationError::InvalidSignature(
"Requester peer ID cannot be empty".to_string(),
));
}
Ok(())
}
pub fn validate_chunk_response(
response: &ChunkResponse,
request: &ChunkRequest,
) -> Result<(), ValidationError> {
if response.challenge_echo != request.challenge_nonce {
return Err(ValidationError::InvalidNonce(
"Challenge echo does not match request nonce".to_string(),
));
}
if response.encrypted_chunk.is_empty() {
return Err(ValidationError::InvalidSignature(
"Encrypted chunk cannot be empty".to_string(),
));
}
if response.chunk_hash == [0u8; 32] {
return Err(ValidationError::InvalidSignature(
"Chunk hash cannot be all zeros".to_string(),
));
}
if response.provider_signature.is_empty() {
return Err(ValidationError::InvalidSignature(
"Provider signature cannot be empty".to_string(),
));
}
let now = chrono::Utc::now().timestamp_millis();
if response.timestamp_ms > now {
return Err(ValidationError::TimestampOutOfRange(
"Response timestamp is in the future".to_string(),
));
}
Ok(())
}
pub fn validate_bandwidth_proof(proof: &BandwidthProof) -> Result<(), ValidationError> {
if proof.content_cid.is_empty() {
return Err(ValidationError::InvalidCid(
"CID cannot be empty".to_string(),
));
}
if proof.challenge_nonce.is_empty() || proof.challenge_nonce == vec![0u8; 32] {
return Err(ValidationError::InvalidNonce(
"Invalid challenge nonce".to_string(),
));
}
if proof.provider_signature.is_empty() {
return Err(ValidationError::InvalidSignature(
"Provider signature is empty".to_string(),
));
}
if proof.requester_signature.is_empty() {
return Err(ValidationError::InvalidSignature(
"Requester signature is empty".to_string(),
));
}
if proof.provider_public_key.len() != 32 {
return Err(ValidationError::InvalidSignature(
"Invalid provider public key length".to_string(),
));
}
if proof.requester_public_key.len() != 32 {
return Err(ValidationError::InvalidSignature(
"Invalid requester public key length".to_string(),
));
}
if proof.start_timestamp_ms >= proof.end_timestamp_ms {
return Err(ValidationError::InvalidTimestamp(
"Start timestamp must be before end timestamp".to_string(),
));
}
let duration_ms = (proof.end_timestamp_ms - proof.start_timestamp_ms) as u32;
if proof.latency_ms > duration_ms {
return Err(ValidationError::InvalidLatency(
"Latency cannot exceed transfer duration".to_string(),
));
}
if proof.bytes_transferred == 0 {
return Err(ValidationError::InvalidSignature(
"Bytes transferred cannot be zero".to_string(),
));
}
Ok(())
}
#[inline]
#[must_use]
pub fn is_valid_cid(cid: &str) -> bool {
if cid.is_empty() {
return false;
}
if cid.starts_with("Qm") && cid.len() >= 46 {
return true;
}
if cid.len() > 10 && (cid.starts_with('b') || cid.starts_with('z') || cid.starts_with('f')) {
return true;
}
false
}
#[inline]
#[must_use]
#[allow(dead_code)]
pub fn is_nonce_unique(_nonce: &[u8], _peer_id: &str) -> bool {
true
}
#[inline]
pub const fn calculate_latency(start_ms: i64, end_ms: i64) -> u32 {
let diff = end_ms.saturating_sub(start_ms);
if diff < 0 { 0 } else { diff as u32 }
}
#[cfg(test)]
mod tests {
use super::*;
use chie_crypto::KeyPair;
#[test]
fn test_generate_challenge_nonce() {
let nonce1 = generate_challenge_nonce();
let nonce2 = generate_challenge_nonce();
assert_eq!(nonce1.len(), 32);
assert_eq!(nonce2.len(), 32);
assert_ne!(nonce1, nonce2);
}
#[test]
fn test_create_chunk_request() {
let keypair = KeyPair::generate();
let request = create_chunk_request(
"QmTest123".to_string(),
5,
"peer-abc".to_string(),
keypair.public_key(),
);
assert_eq!(request.content_cid, "QmTest123");
assert_eq!(request.chunk_index, 5);
assert_eq!(request.requester_peer_id, "peer-abc");
assert_eq!(request.challenge_nonce.len(), 32);
}
#[test]
fn test_validate_chunk_request() {
let keypair = KeyPair::generate();
let request = create_chunk_request(
"QmTest".to_string(),
0,
"peer".to_string(),
keypair.public_key(),
);
assert!(validate_chunk_request(&request).is_ok());
let mut bad_request = request.clone();
bad_request.content_cid = String::new();
assert!(validate_chunk_request(&bad_request).is_err());
let mut bad_request = request;
bad_request.challenge_nonce = [0u8; 32];
assert!(validate_chunk_request(&bad_request).is_err());
}
#[test]
fn test_validate_bandwidth_proof() {
let keypair = KeyPair::generate();
let request = create_chunk_request(
"QmTest".to_string(),
0,
"peer".to_string(),
keypair.public_key(),
);
let proof = create_bandwidth_proof(
&request,
"provider".to_string(),
vec![1u8; 32],
1024,
vec![1u8; 64],
vec![2u8; 64],
vec![3u8; 32],
1000,
2000,
100,
);
assert!(validate_bandwidth_proof(&proof).is_ok());
}
#[test]
fn test_is_valid_cid() {
assert!(is_valid_cid(
"QmTest1234567890123456789012345678901234567890"
));
assert!(is_valid_cid(
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
));
assert!(!is_valid_cid(""));
assert!(!is_valid_cid("invalid"));
}
#[test]
fn test_calculate_latency() {
assert_eq!(calculate_latency(1000, 1500), 500);
assert_eq!(calculate_latency(2000, 2000), 0);
assert_eq!(calculate_latency(2000, 1500), 0); }
}