saorsa_node/client/
quantum.rs

1//! Quantum-resistant client operations for chunk storage.
2//!
3//! This module provides content-addressed chunk storage operations on the saorsa network
4//! using post-quantum cryptography (ML-KEM-768 for key exchange, ML-DSA-65 for signatures).
5//!
6//! ## Data Model
7//!
8//! Chunks are the only data type supported:
9//! - **Content-addressed**: Address = SHA256(content)
10//! - **Immutable**: Once stored, content cannot change
11//! - **Paid**: All storage requires EVM payment on Arbitrum
12//!
13//! ## Security Features
14//!
15//! - **ML-KEM-768**: NIST FIPS 203 compliant key encapsulation for encryption
16//! - **ML-DSA-65**: NIST FIPS 204 compliant signatures for authentication
17//! - **ChaCha20-Poly1305**: Symmetric encryption for data at rest
18
19use super::data_types::{DataChunk, XorName};
20use crate::error::{Error, Result};
21use bytes::Bytes;
22use saorsa_core::P2PNode;
23use std::sync::Arc;
24use tracing::{debug, info};
25
26/// Configuration for the quantum-resistant client.
27#[derive(Debug, Clone)]
28pub struct QuantumConfig {
29    /// Timeout for network operations in seconds.
30    pub timeout_secs: u64,
31    /// Number of replicas for data redundancy.
32    pub replica_count: u8,
33    /// Enable encryption for all stored data.
34    pub encrypt_data: bool,
35}
36
37impl Default for QuantumConfig {
38    fn default() -> Self {
39        Self {
40            timeout_secs: 30,
41            replica_count: 4,
42            encrypt_data: true,
43        }
44    }
45}
46
47/// Client for quantum-resistant chunk operations on the saorsa network.
48///
49/// This client uses post-quantum cryptography for all operations:
50/// - ML-KEM-768 for key encapsulation
51/// - ML-DSA-65 for digital signatures
52/// - ChaCha20-Poly1305 for symmetric encryption
53///
54/// ## Chunk Storage Model
55///
56/// Chunks are content-addressed: the address is the SHA256 hash of the content.
57/// This ensures data integrity - if the content matches the address, the data
58/// is authentic. All chunk storage requires EVM payment on Arbitrum.
59pub struct QuantumClient {
60    config: QuantumConfig,
61    p2p_node: Option<Arc<P2PNode>>,
62}
63
64impl QuantumClient {
65    /// Create a new quantum client with the given configuration.
66    #[must_use]
67    pub fn new(config: QuantumConfig) -> Self {
68        debug!("Creating quantum-resistant saorsa client");
69        Self {
70            config,
71            p2p_node: None,
72        }
73    }
74
75    /// Create a quantum client with default configuration.
76    #[must_use]
77    pub fn with_defaults() -> Self {
78        Self::new(QuantumConfig::default())
79    }
80
81    /// Set the P2P node for network operations.
82    #[must_use]
83    pub fn with_node(mut self, node: Arc<P2PNode>) -> Self {
84        self.p2p_node = Some(node);
85        self
86    }
87
88    /// Get a chunk from the saorsa network.
89    ///
90    /// # Arguments
91    ///
92    /// * `address` - The `XorName` address of the chunk (SHA256 of content)
93    ///
94    /// # Returns
95    ///
96    /// The chunk data if found, or None if not present in the network.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the network operation fails.
101    pub async fn get_chunk(&self, address: &XorName) -> Result<Option<DataChunk>> {
102        debug!(
103            "Querying saorsa network for chunk: {}",
104            hex::encode(address)
105        );
106
107        let Some(ref node) = self.p2p_node else {
108            return Err(Error::Network("P2P node not configured".into()));
109        };
110
111        let _ = self.config.timeout_secs; // Use config for future timeout implementation
112
113        // Lookup chunk in DHT
114        match node.dht_get(*address).await {
115            Ok(Some(data)) => {
116                debug!(
117                    "Found chunk {} on saorsa network ({} bytes)",
118                    hex::encode(address),
119                    data.len()
120                );
121                Ok(Some(DataChunk::new(*address, Bytes::from(data))))
122            }
123            Ok(None) => {
124                debug!("Chunk {} not found on saorsa network", hex::encode(address));
125                Ok(None)
126            }
127            Err(e) => Err(Error::Network(format!(
128                "DHT lookup failed for {}: {}",
129                hex::encode(address),
130                e
131            ))),
132        }
133    }
134
135    /// Store a chunk on the saorsa network.
136    ///
137    /// The chunk address is computed as SHA256(content), ensuring content-addressing.
138    /// The `P2PNode` handles ML-DSA-65 signing internally.
139    ///
140    /// # Arguments
141    ///
142    /// * `content` - The data to store
143    ///
144    /// # Returns
145    ///
146    /// The `XorName` address where the chunk was stored.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the store operation fails.
151    pub async fn put_chunk(&self, content: Bytes) -> Result<XorName> {
152        use sha2::{Digest, Sha256};
153
154        debug!("Storing chunk on saorsa network ({} bytes)", content.len());
155
156        let Some(ref node) = self.p2p_node else {
157            return Err(Error::Network("P2P node not configured".into()));
158        };
159
160        // Compute content address using SHA-256
161        let mut hasher = Sha256::new();
162        hasher.update(&content);
163        let hash = hasher.finalize();
164
165        let mut address = [0u8; 32];
166        address.copy_from_slice(&hash);
167
168        let _ = self.config.replica_count; // Used for future replication verification
169
170        // Store in DHT - P2PNode handles ML-DSA-65 signing internally
171        node.dht_put(address, content.to_vec()).await.map_err(|e| {
172            Error::Network(format!(
173                "DHT store failed for {}: {}",
174                hex::encode(address),
175                e
176            ))
177        })?;
178
179        info!(
180            "Chunk stored at address: {} ({} bytes)",
181            hex::encode(address),
182            content.len()
183        );
184        Ok(address)
185    }
186
187    /// Check if a chunk exists on the saorsa network.
188    ///
189    /// # Arguments
190    ///
191    /// * `address` - The `XorName` to check
192    ///
193    /// # Returns
194    ///
195    /// True if the chunk exists, false otherwise.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the network operation fails.
200    pub async fn exists(&self, address: &XorName) -> Result<bool> {
201        debug!(
202            "Checking existence on saorsa network: {}",
203            hex::encode(address)
204        );
205
206        let Some(ref node) = self.p2p_node else {
207            return Err(Error::Network("P2P node not configured".into()));
208        };
209
210        // Check if data exists in DHT
211        match node.dht_get(*address).await {
212            Ok(Some(_)) => {
213                debug!("Chunk {} exists on saorsa network", hex::encode(address));
214                Ok(true)
215            }
216            Ok(None) => {
217                debug!("Chunk {} not found on saorsa network", hex::encode(address));
218                Ok(false)
219            }
220            Err(e) => Err(Error::Network(format!(
221                "DHT lookup failed for {}: {}",
222                hex::encode(address),
223                e
224            ))),
225        }
226    }
227}
228
229#[cfg(test)]
230#[allow(clippy::unwrap_used, clippy::expect_used)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_quantum_config_default() {
236        let config = QuantumConfig::default();
237        assert_eq!(config.timeout_secs, 30);
238        assert_eq!(config.replica_count, 4);
239        assert!(config.encrypt_data);
240    }
241
242    #[test]
243    fn test_quantum_client_creation() {
244        let client = QuantumClient::with_defaults();
245        assert_eq!(client.config.timeout_secs, 30);
246        assert!(client.p2p_node.is_none());
247    }
248
249    #[tokio::test]
250    async fn test_get_chunk_without_node_fails() {
251        let client = QuantumClient::with_defaults();
252        let address = [0; 32];
253
254        let result = client.get_chunk(&address).await;
255        assert!(result.is_err());
256    }
257
258    #[tokio::test]
259    async fn test_put_chunk_without_node_fails() {
260        let client = QuantumClient::with_defaults();
261        let content = Bytes::from("test data");
262
263        let result = client.put_chunk(content).await;
264        assert!(result.is_err());
265    }
266
267    #[tokio::test]
268    async fn test_exists_without_node_fails() {
269        let client = QuantumClient::with_defaults();
270        let address = [0; 32];
271
272        let result = client.exists(&address).await;
273        assert!(result.is_err());
274    }
275}