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}