Skip to main content

ironshield_core/
solve.rs

1//! Proof-of-work solving functions for IronShield challenges.
2//!
3//! This module contains functions for finding valid nonces that satisfy
4//! the proof-of-work requirements for IronShieldChallenge struct-based challenges.
5
6use hex;
7use sha2::{Digest, Sha256};
8use serde::{Serialize, Deserialize};
9
10use ironshield_types::*;
11
12const  PROGRESS_REPORTING_INTERVAL: u64 = 200_000;
13const MAX_ATTEMPTS_SINGLE_THREADED: i64 = 100_000_000;
14const  MAX_ATTEMPTS_MULTI_THREADED: i64 = 1_000_000_000; // Higher limit for parallel execution
15
16/// Configuration parameters for proof-of-work challenges.
17///
18/// # Arguments
19/// * `max_attempts`:                number of nonces to try
20///                                  before terminating (giving
21///                                  up).
22/// * `progress_reporting_interval`: The interval for every
23///                                  progress report callback
24///                                  (in attempts).
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PoWConfig {
27    pub max_attempts:                i64,
28    pub progress_reporting_interval: u64,
29}
30
31impl Default for PoWConfig {
32    fn default() -> Self {
33        Self {
34            max_attempts:                MAX_ATTEMPTS_SINGLE_THREADED,
35            progress_reporting_interval: PROGRESS_REPORTING_INTERVAL,
36        }
37    }
38}
39
40impl PoWConfig {
41    pub fn single_threaded() -> Self {
42        Self::default()
43    }
44
45    pub fn multi_threaded() -> Self {
46        Self {
47            max_attempts:                MAX_ATTEMPTS_MULTI_THREADED,
48            progress_reporting_interval: PROGRESS_REPORTING_INTERVAL,
49        }
50    }
51
52    /// Create a custom configuration with specified
53    /// parameters.
54    ///
55    /// # Arguments
56    /// * `max_attempts`:                number of nonces to try
57    ///                                  before terminating (giving
58    ///                                  up).
59    /// * `progress_reporting_interval`: The interval for every
60    ///                                  progress report callback
61    ///                                  (in attempts).
62    pub fn custom(
63        max_attempts:                i64,
64        progress_reporting_interval: u64
65    ) -> Self {
66        Self {
67            max_attempts,
68            progress_reporting_interval,
69        }
70    }
71}
72
73/// Find a solution for the given IronShieldChallenge using optimized
74/// multithreaded computation.
75///
76/// Implements thread-stride approach delegated to multiple threads
77/// (e.g. Web Workers or Tokio tasks) for maximum performance.
78/// The lack of chunking of nonces between threads dramatically increases
79/// performance since threads don't have to constantly communicate and
80/// distribute work. Each thread is mathematically guaranteed to not check
81/// overlapping nonces with other threads.
82///
83/// ## Algorithm:
84/// 1. Pre-computes the random_nonce bytes once to avoid repeated hex decoding
85/// 2. Assigns each thread a start_offset and stride
86/// 3. Each thread performs the single-threaded algorithm with a stride and offset
87/// 4. Returns immediately when a solution is found
88///
89/// # Arguments
90/// * `challenge`:    The IronShieldChallenge struct containing random_nonce and challenge_param
91/// * `num_threads`:  Ignored (for compatibility only)
92/// * `start_offset`: Starting nonce for this worker's search range (JavaScript coordination)
93/// * `stride`:       Nonce increment step for thread-stride pattern (JavaScript coordination)
94///
95/// # Returns
96/// * `Result<IronShieldChallengeResponse, String>`: `Ok(IronShieldChallengeResponse)`
97///                                                  that contains the successful nonce,
98///                                                  or an error (`Err(String)`) message
99///                                                  if no solution is found within
100///                                                  `config.max_attempts`.
101///
102/// # Example
103/// ```
104/// use ironshield_core::{
105///     IronShieldChallenge,
106///     find_solution,
107///     SigningKey
108/// };
109///
110/// # fn example() -> Result<(), String> {
111///     let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
112///     let challenge = IronShieldChallenge::new(
113///         "website".to_string(),
114///         1,           // difficulty
115///         dummy_key,
116///         [0x00; 32],  // public_key
117///      );
118///
119///     // JavaScript worker coordination mode
120///     let response = find_solution(&challenge, None, Some(0), Some(8), None)?;
121///     println!("Found solution: {}", response.solution);
122/// #   Ok(())
123/// # }
124/// ```
125pub fn find_solution(
126    challenge:         &IronShieldChallenge,
127    config:            Option<PoWConfig>,
128    start_offset:      Option<usize>,
129    stride:            Option<usize>,
130    progress_callback: Option<&dyn Fn(u64)>,
131) -> Result<IronShieldChallengeResponse, String> {
132    let config: PoWConfig = config.unwrap_or_else(PoWConfig::multi_threaded);
133
134    // Set the start nonce and nonce increment based on the start_offset and stride.
135    let (start_nonce, nonce_increment) =
136        if let (Some(start), Some(step)) = (start_offset, stride) {
137            (start as i64, step as i64)
138        } else {
139            (0i64, 1i64) // Single-threaded fallback
140        };
141
142    execute_proof_of_work(
143        start_nonce,
144        nonce_increment,
145        &config,
146        progress_callback,
147        challenge,
148    ).map_err(|_| {
149        format!("Could not find solution within {} attempts", config.max_attempts)
150    })
151}
152
153/// Proof-of-work function that handles both worker coordination
154/// and single-threaded fallback modes.
155///
156/// # Arguments
157/// * `start_nonce`:        Starting nonce value (0 for single-threaded,
158///                         offset for worker coordination).
159/// * `nonce_increment`:    How much to increment nonce each iteration
160///                         (1 for single-threaded, stride for workers).
161/// * `config`:             `PoWConfig` containing `max_attempts` and
162///                         `progress_reporting_interval`.
163/// * `progress_callback`:  Optional callback for progress reporting.
164/// * `challenge`:          Original challenge for constructing the
165///                         response.
166///
167/// # Returns
168/// * `Result<IronShieldChallengeResponse, String>`: `Ok(IronShieldChallengeResponse)`
169///                                                  that contains the successful nonce,
170///                                                  or an error (`Err(String)`) message
171///                                                  if no solution is found within
172///                                                  `config.max_attempts`.
173pub fn execute_proof_of_work(
174    start_nonce:        i64,
175    nonce_increment:    i64,
176    config:             &PoWConfig,
177    progress_callback:  Option<&dyn Fn(u64)>,
178    challenge:          &IronShieldChallenge,
179) -> Result<IronShieldChallengeResponse, String> {
180    let mut      nonce_bytes: [u8; 8] = start_nonce.to_le_bytes();
181    let     increment_amount:     u64 = nonce_increment as u64;
182    let mut            nonce:     i64 = start_nonce;
183    let mut attempts_counter:     u64 = 0;
184
185    // Extract the random nonce and threshold from the challenge
186    let random_nonce_bytes: Vec<u8> = hex::decode(&challenge.random_nonce)
187        .map_err(|e: hex::FromHexError| format!("Failed to decode random_nonce hex: {}", e))?;
188    let target_threshold: &[u8; 32] = &challenge.challenge_param;
189
190    // Pre-compute the hash of the random nonce
191    let mut base_hasher: Sha256 = Sha256::new();
192    base_hasher.update(random_nonce_bytes);
193
194    while nonce < config.max_attempts {
195        // Hash the random nonce and nonce bytes
196        let mut hasher = base_hasher.clone();
197        hasher.update(&nonce_bytes);
198        let hash_result = hasher.finalize();
199
200        // Upon finding a valid solution convert bytes back to i64 and return the solution
201        if hash_result.as_slice() < target_threshold {
202            let final_nonce: i64 = le_bytes_to_i64(&nonce_bytes);
203            return Ok(IronShieldChallengeResponse::new(
204                challenge.clone(),
205                final_nonce,
206            ));
207        }
208
209        // Increment the attempts counter and report progress if a callback is provided
210        attempts_counter += 1;
211        if attempts_counter == config.progress_reporting_interval {
212            if let Some(callback) = progress_callback {
213                callback(attempts_counter);
214            }
215            attempts_counter = 0;
216        }
217
218        // Increment nonce byte directly, avoid i64 conversion.
219        increment_le_bytes(&mut nonce_bytes, increment_amount);
220        nonce += nonce_increment;
221    }
222
223    Err(format!("Could not find solution within {} attempts", config.max_attempts))
224}
225
226/// Increment little-endian bytes by a specified amount.
227///
228/// Avoids the overhead of converting to/from i64 in the hot loop.
229///
230/// # Arguments
231/// * `bytes`:     Mutable reference to the 8-byte little-endian
232///                array to increment.
233/// * `increment`: The amount to add (must be positive).
234#[inline]
235fn increment_le_bytes(bytes: &mut [u8; 8], increment: u64) {
236    let mut carry: u64 = increment;
237    for byte in bytes.iter_mut() {
238        if carry == 0 {
239            break;
240        }
241        let sum: u64 = *byte as u64 + carry;
242        *byte   = sum as u8;
243        carry   = sum >> 8;
244    }
245}
246
247#[inline]
248fn le_bytes_to_i64(bytes: &[u8; 8]) -> i64 {
249    i64::from_le_bytes(*bytes)
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_increment_le_bytes() {
258        let mut bytes = [0u8; 8];
259        increment_le_bytes(&mut bytes, 1);
260        assert_eq!(bytes, [1, 0, 0, 0, 0, 0, 0, 0]);
261
262        let mut bytes = [0u8; 8];
263        increment_le_bytes(&mut bytes, 256);
264        assert_eq!(bytes, [0, 1, 0, 0, 0, 0, 0, 0]);
265
266        let mut bytes = [255u8; 8];
267        increment_le_bytes(&mut bytes, 1);
268        assert_eq!(bytes, [0, 0, 0, 0, 0, 0, 0, 0]); // Wraps around.
269
270        let mut bytes = [0u8; 8];
271        increment_le_bytes(&mut bytes, 8); // Common stride for 8 threads.
272        assert_eq!(bytes, [8, 0, 0, 0, 0, 0, 0, 0]);
273    }
274
275    #[test]
276    fn test_le_bytes_to_i64_roundtrip() {
277        // Test that we can convert i64 -> bytes -> i64 without loss.
278        let original_values = [0i64, 1, 255, 256, 65535, 65536, 16777215, 16777216];
279
280        for &original in &original_values {
281            let bytes = original.to_le_bytes();
282            let recovered = le_bytes_to_i64(&bytes);
283            assert_eq!(original, recovered, "Roundtrip failed for value {}", original);
284        }
285    }
286
287    #[test]
288    fn test_performance_optimization_correctness() {
289        // This test ensures that our optimization produces the same results
290        // as the original Vec-based approach would have.
291
292        let random_nonce = "deadbeefcafe1234";
293        let random_nonce_bytes = hex::decode(random_nonce).unwrap();
294        let nonce: i64 = 12345;
295        let nonce_bytes = nonce.to_le_bytes();
296
297        // Method 1: Optimized approach (multiple hasher updates).
298        let mut hasher1 = Sha256::new();
299        hasher1.update(&random_nonce_bytes);
300        hasher1.update(&nonce_bytes);
301        let hash1: [u8; 32] = hasher1.finalize().into();
302
303        // Method 2: Traditional approach (Vec concatenation) - for comparison.
304        let mut input_data = Vec::with_capacity(random_nonce_bytes.len() + 8);
305        input_data.extend_from_slice(&random_nonce_bytes);
306        input_data.extend_from_slice(&nonce_bytes);
307        let mut hasher2 = Sha256::new();
308        hasher2.update(&input_data);
309        let hash2: [u8; 32] = hasher2.finalize().into();
310
311        // Both methods should produce identical results.
312        assert_eq!(hash1, hash2, "Optimized and traditional methods should produce identical hashes");
313    }
314
315    #[test]
316    fn test_find_solution_easy() {
317        // Create a challenge with very high threshold (easy to solve).
318        let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
319        let challenge = IronShieldChallenge::new(
320            "test_website".to_string(),
321            1, // Easiest difficulty.
322            dummy_key,
323            [0x00; 32],
324        );
325
326        let result = find_solution(&challenge, None, None, None, None);
327        assert!(result.is_ok(), "Should find solution for easy challenge");
328
329        let response = result.unwrap();
330        assert_eq!(response.solved_challenge.challenge_signature, challenge.challenge_signature);
331        assert!(response.solution >= 0, "Solution should be non-negative");
332
333        // Verify the solution using the verification function.
334        assert!(crate::verify::verify_ironshield_solution(&response),
335                "Multi-threaded solution should pass verification");
336    }
337
338
339    #[test]
340    fn test_find_solution_deterministic_correctness() {
341        // Test that the multithreaded function produces correct results
342        // by testing with a known challenge where we can predict the solution range.
343        let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
344        let challenge = IronShieldChallenge::new(
345            "test_website".to_string(),
346            2, // ~50% probability per hash.
347            dummy_key,
348            [0x00; 32],
349        );
350
351        // Should find a solution relatively quickly with 50% probability per attempt.
352        let result = find_solution(&challenge, None, None, None, None);
353        assert!(result.is_ok(), "Should find solution for medium difficulty challenge");
354
355        let response = result.unwrap();
356
357        // Manually verify the solution using the same algorithm.
358        let random_nonce_bytes = hex::decode(&challenge.random_nonce).unwrap();
359        let nonce_bytes = response.solution.to_le_bytes();
360        let mut hasher = Sha256::new();
361        hasher.update(&random_nonce_bytes);
362        hasher.update(&nonce_bytes);
363        let hash_bytes: [u8; 32] = hasher.finalize().into();
364
365        assert!(hash_bytes < challenge.challenge_param,
366                "Solution should satisfy the challenge requirement");
367        assert_eq!(response.solved_challenge.challenge_signature, challenge.challenge_signature,
368                   "Response should preserve challenge signature");
369    }
370
371    #[test]
372    fn test_execute_proof_of_work_single_threaded_mode() {
373        // Test the internal function with single-threaded parameters.
374        let target_threshold = [0xFF; 32]; // Very easy threshold.
375        let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
376        let challenge = IronShieldChallenge::new(
377            "test".to_string(),
378            1,
379            dummy_key,
380            target_threshold,
381        );
382
383        let result = execute_proof_of_work(
384            0, // start_nonce
385            1, // nonce_increment
386            &PoWConfig::default(), // conf
387            None, // progress_callback
388            &challenge,
389        );
390
391        assert!(result.is_ok(), "Should find solution with easy threshold");
392    }
393
394    #[test]
395    fn test_execute_proof_of_work_worker_coordination_mode() {
396        // Test the internal function with worker coordination parameters.
397        let target_threshold = [0xFF; 32]; // Very easy threshold.
398        let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
399        let challenge = IronShieldChallenge::new(
400            "test".to_string(),
401            1,
402            dummy_key,
403            target_threshold,
404        );
405
406        let result = execute_proof_of_work(
407            5,  // start_nonce (worker offset)
408            8,  // nonce_increment (worker stride)
409            &PoWConfig::default(), // conf
410            None, // progress_callback
411            &challenge,
412        );
413
414        assert!(result.is_ok(), "Should find solution with worker coordination parameters");
415    }
416}