Skip to main content

lib_q_keccak/
multithreading.rs

1//! Multi-threading implementations for Keccak operations
2//!
3//! This module provides thread-safe multi-threading capabilities for cryptographic
4//! operations, following secure development practices and proper architecture.
5//! It leverages Rust's ownership model and safe concurrency primitives to ensure
6//! thread safety without data races.
7//!
8//! Note: This module requires the `std` feature to be enabled.
9
10// In Rust 2024, extern crate is not idiomatic
11// Core, std, and alloc types are available by default through the prelude
12
13// Use std types when available, alloc types as fallback
14#[cfg(all(not(feature = "std"), feature = "alloc"))]
15use alloc::boxed::Box;
16#[cfg(all(not(feature = "std"), feature = "alloc"))]
17use alloc::vec::Vec;
18#[cfg(all(not(feature = "std"), feature = "alloc"))]
19use alloc::{
20    format,
21    vec,
22};
23use core::sync::atomic::{
24    AtomicBool,
25    AtomicUsize,
26    Ordering,
27};
28#[cfg(feature = "std")]
29use std::boxed::Box;
30use std::sync::{
31    Arc,
32    OnceLock,
33    RwLock,
34    RwLockReadGuard,
35    RwLockWriteGuard,
36};
37use std::thread;
38use std::time::Duration;
39#[cfg(feature = "std")]
40use std::vec::Vec;
41#[cfg(feature = "std")]
42use std::{
43    format,
44    vec,
45};
46
47use crate::{
48    OptimizationLevel,
49    keccak_p,
50};
51
52/// Cross-platform thread affinity implementation
53/// Sets thread affinity to a specific CPU core for optimal cache performance
54#[cfg(all(
55    feature = "thread-affinity",
56    any(target_os = "linux", target_os = "windows", target_os = "macos")
57))]
58fn set_thread_affinity(thread_id: usize, strategy: AffinityStrategy) {
59    use std::sync::OnceLock;
60
61    // Early return if affinity is disabled
62    if matches!(strategy, AffinityStrategy::Disabled) {
63        return;
64    }
65
66    // Cache the number of available CPUs to avoid repeated system calls
67    static CPU_COUNT: OnceLock<usize> = OnceLock::new();
68
69    let cpu_count = CPU_COUNT.get_or_init(|| {
70        core_affinity::get_core_ids()
71            .map(|ids| ids.len())
72            .unwrap_or_else(num_cpus::get)
73    });
74
75    if *cpu_count == 0 {
76        return; // No CPUs available, skip affinity setting
77    }
78
79    // Calculate target CPU based on strategy
80    let target_cpu = match strategy {
81        AffinityStrategy::Disabled => return,
82        AffinityStrategy::Spread => {
83            // Distribute threads across all available cores
84            thread_id % *cpu_count
85        }
86        AffinityStrategy::Compact => {
87            // Group threads on fewer cores for better cache sharing
88            let active_cores = cpu_count.div_ceil(2); // Use roughly half the cores
89            thread_id % active_cores
90        }
91        AffinityStrategy::Custom => {
92            // For now, fall back to spread strategy
93            thread_id % *cpu_count
94        }
95    };
96
97    // Get the core ID for the target CPU
98    if let Some(core_ids) = core_affinity::get_core_ids() &&
99        let Some(core_id) = core_ids.get(target_cpu)
100    {
101        // Attempt to set thread affinity - ignore errors gracefully
102        // This is a performance optimization, so we don't fail on errors
103        let _ = core_affinity::set_for_current(*core_id);
104    }
105}
106
107/// `core_affinity` is not built for targets like `wasm32-unknown-unknown` (see `Cargo.toml`).
108#[cfg(all(
109    feature = "thread-affinity",
110    not(any(target_os = "linux", target_os = "windows", target_os = "macos"))
111))]
112fn set_thread_affinity(_thread_id: usize, _strategy: AffinityStrategy) {}
113
114/// Fallback when the `thread-affinity` feature is disabled
115#[cfg(not(feature = "thread-affinity"))]
116fn set_thread_affinity(_thread_id: usize, _strategy: AffinityStrategy) {
117    // No-op implementation for systems without thread affinity support
118}
119
120/// Thread affinity strategy for optimizing cache performance
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum AffinityStrategy {
123    /// Disable thread affinity completely
124    Disabled,
125    /// Spread threads across all available CPU cores
126    Spread,
127    /// Group threads on fewer cores for better cache sharing
128    Compact,
129    /// Custom affinity pattern (future extension)
130    Custom,
131}
132
133/// Thread-safe configuration for multi-threading operations
134#[derive(Debug, Clone)]
135pub struct ThreadingConfig {
136    /// Number of worker threads to use
137    pub num_threads: usize,
138    /// Minimum work size to trigger multi-threading
139    pub min_work_size: usize,
140    /// Maximum work size per thread
141    pub max_work_per_thread: usize,
142    /// Thread pool timeout
143    pub timeout: Duration,
144    /// Enable thread affinity for better cache performance
145    pub enable_affinity: bool,
146    /// Thread affinity strategy
147    pub affinity_strategy: AffinityStrategy,
148}
149
150impl Default for ThreadingConfig {
151    fn default() -> Self {
152        Self {
153            num_threads: num_cpus::get(),
154            min_work_size: 1024,            // 1KB minimum for multi-threading
155            max_work_per_thread: 64 * 1024, // 64KB per thread
156            timeout: Duration::from_secs(30),
157            enable_affinity: true,
158            affinity_strategy: AffinityStrategy::Spread,
159        }
160    }
161}
162
163impl ThreadingConfig {
164    /// Create a security-optimized configuration
165    pub fn security_optimized() -> Self {
166        Self {
167            num_threads: 1,            // Single thread for maximum security
168            min_work_size: usize::MAX, // Disable multi-threading
169            max_work_per_thread: usize::MAX,
170            timeout: Duration::from_secs(5),
171            enable_affinity: false,
172            affinity_strategy: AffinityStrategy::Disabled,
173        }
174    }
175
176    /// Create a performance-optimized configuration
177    pub fn performance_optimized() -> Self {
178        Self {
179            num_threads: num_cpus::get(),
180            min_work_size: 512,             // Lower threshold for more parallelism
181            max_work_per_thread: 32 * 1024, // Smaller chunks for better load balancing
182            timeout: Duration::from_secs(60),
183            enable_affinity: true,
184            affinity_strategy: AffinityStrategy::Spread,
185        }
186    }
187
188    /// Create a balanced configuration
189    pub fn balanced() -> Self {
190        Self {
191            num_threads: num_cpus::get().div_ceil(2), // Half the cores
192            min_work_size: 2048,                      // Higher threshold for better efficiency
193            max_work_per_thread: 128 * 1024,          // Larger chunks
194            timeout: Duration::from_secs(30),
195            enable_affinity: true,
196            affinity_strategy: AffinityStrategy::Compact,
197        }
198    }
199}
200
201/// Worker statistics for monitoring and debugging
202#[derive(Debug, Clone)]
203pub struct WorkerStats {
204    /// Unique worker thread identifier
205    pub worker_id: usize,
206    /// Number of work items processed by this worker
207    pub work_items_processed: usize,
208}
209
210/// Thread-safe work distribution for cryptographic operations
211#[derive(Debug)]
212struct WorkDistribution {
213    /// Total number of items to process
214    total_items: usize,
215    /// Current position in the work queue
216    current_position: AtomicUsize,
217    /// Work completion status
218    completed: AtomicBool,
219    /// Number of items completed
220    completed_count: AtomicUsize,
221}
222
223impl WorkDistribution {
224    fn new(total_items: usize) -> Self {
225        Self {
226            total_items,
227            current_position: AtomicUsize::new(0),
228            completed: AtomicBool::new(false),
229            completed_count: AtomicUsize::new(0),
230        }
231    }
232
233    /// Get next work chunk for a thread
234    fn get_next_chunk(&self, chunk_size: usize) -> Option<(usize, usize)> {
235        let start = self
236            .current_position
237            .fetch_add(chunk_size, Ordering::AcqRel);
238        if start >= self.total_items {
239            return None;
240        }
241        let end = (start + chunk_size).min(self.total_items);
242        Some((start, end))
243    }
244
245    /// Mark work as completed
246    fn mark_completed(&self) {
247        self.completed.store(true, Ordering::Release);
248    }
249
250    /// Increment the completed work count
251    fn increment_completed(&self, count: usize) {
252        self.completed_count.fetch_add(count, Ordering::AcqRel);
253    }
254
255    /// Check if all work has been completed
256    fn is_all_work_completed(&self) -> bool {
257        self.completed_count.load(Ordering::Acquire) >= self.total_items
258    }
259
260    /// Check if all work is completed (primarily for testing/monitoring)
261    #[allow(dead_code)] // Used in tests and available for monitoring
262    fn is_completed(&self) -> bool {
263        self.completed.load(Ordering::Acquire)
264    }
265}
266
267/// Thread-safe cryptographic worker
268#[derive(Debug)]
269struct CryptoWorker {
270    /// Worker thread ID
271    #[allow(dead_code)] // Used in tests and available for monitoring
272    id: usize,
273    /// Thread-safe work distribution
274    work_dist: Arc<WorkDistribution>,
275    /// Thread-safe result storage
276    results: Arc<RwLock<Vec<[u64; 25]>>>,
277    /// Configuration
278    config: ThreadingConfig,
279}
280
281/// Exclusive access to the shared Keccak result lanes.
282///
283/// Recovers from a poisoned [`RwLock`] via [`std::sync::PoisonError::into_inner`].
284/// Workers publish disjoint index ranges; if a peer panics while holding the
285/// lock, returning `Err` here would drop locally permuted state after chunk
286/// offsets were already advanced in [`WorkDistribution`], leaving gaps that are
287/// never retried.
288fn acquire_results_write<'a>(
289    results: &'a RwLock<Vec<[u64; 25]>>,
290) -> RwLockWriteGuard<'a, Vec<[u64; 25]>> {
291    results.write().unwrap_or_else(|e| e.into_inner())
292}
293
294/// Shared read access for returning parallel results after workers join.
295fn acquire_results_read<'a>(
296    results: &'a RwLock<Vec<[u64; 25]>>,
297) -> RwLockReadGuard<'a, Vec<[u64; 25]>> {
298    results.read().unwrap_or_else(|e| e.into_inner())
299}
300
301impl CryptoWorker {
302    /// Get worker statistics for monitoring and debugging
303    #[allow(dead_code)] // Used in tests and available for production monitoring
304    pub fn get_stats(&self) -> WorkerStats {
305        WorkerStats {
306            worker_id: self.id,
307            work_items_processed: self.work_dist.completed_count.load(Ordering::Acquire),
308        }
309    }
310
311    /// Get worker identifier for thread management and monitoring
312    #[allow(dead_code)] // Used in tests and available for production monitoring
313    pub fn get_worker_id(&self) -> usize {
314        self.id
315    }
316
317    fn new(
318        id: usize,
319        work_dist: Arc<WorkDistribution>,
320        results: Arc<RwLock<Vec<[u64; 25]>>>,
321        config: ThreadingConfig,
322    ) -> Self {
323        Self {
324            id,
325            work_dist,
326            results,
327            config,
328        }
329    }
330
331    /// Process Keccak permutations in parallel
332    fn process_keccak_parallel(&self, states: &[[u64; 25]], level: OptimizationLevel) {
333        let chunk_size = self
334            .config
335            .max_work_per_thread
336            .min(states.len() / self.config.num_threads);
337
338        while let Some((start, end)) = self.work_dist.get_next_chunk(chunk_size) {
339            let mut local_results = Vec::new();
340
341            // Worker ID available via get_worker_id() for monitoring
342
343            for i in start..end {
344                if i < states.len() {
345                    let mut state = states[i];
346                    self.apply_keccak_optimization(&mut state, level);
347                    local_results.push(state);
348                }
349            }
350
351            // Store results thread-safely with bounds checking (never drop work on
352            // poisoned lock — recover the guard; each worker owns disjoint indices).
353            let mut results_guard = acquire_results_write(self.results.as_ref());
354            let results_len = results_guard.len();
355            let mut valid_results = 0;
356
357            for (i, result) in local_results.iter().enumerate() {
358                let global_index = start + i;
359                if global_index < results_len && global_index < states.len() {
360                    results_guard[global_index] = *result;
361                    valid_results += 1;
362                }
363            }
364
365            if valid_results > 0 {
366                self.work_dist.increment_completed(valid_results);
367            }
368        }
369
370        // Worker completion statistics available via get_stats()
371        // Use worker.get_stats() for monitoring in production code
372    }
373
374    /// Apply Keccak optimization based on level
375    fn apply_keccak_optimization(&self, state: &mut [u64; 25], level: OptimizationLevel) {
376        match level {
377            OptimizationLevel::Reference => {
378                keccak_p(state, 24);
379            }
380            OptimizationLevel::Basic | OptimizationLevel::Advanced | OptimizationLevel::Maximum => {
381                crate::f1600(state)
382            }
383        }
384    }
385}
386
387/// Thread-safe cryptographic thread pool
388#[derive(Debug)]
389pub struct CryptoThreadPool {
390    /// Thread pool configuration
391    config: ThreadingConfig,
392    /// Thread-safe shutdown flag
393    shutdown: Arc<AtomicBool>,
394}
395
396impl CryptoThreadPool {
397    /// Create a new thread pool with the specified configuration
398    pub fn new(config: ThreadingConfig) -> Self {
399        Self {
400            config,
401            shutdown: Arc::new(AtomicBool::new(false)),
402        }
403    }
404
405    /// Process multiple Keccak states using multiple threads
406    pub fn process_keccak_states(
407        &self,
408        states: &[[u64; 25]],
409        level: OptimizationLevel,
410    ) -> Result<Vec<[u64; 25]>, Box<dyn std::error::Error + Send + Sync>> {
411        // Check if multi-threading is beneficial
412        if states.len() < self.config.min_work_size || self.config.num_threads <= 1 {
413            return self.process_sequential(states, level);
414        }
415
416        // Create thread-safe work distribution
417        let work_dist = Arc::new(WorkDistribution::new(states.len()));
418        let results = Arc::new(RwLock::new(vec![[0u64; 25]; states.len()]));
419        let shutdown = Arc::clone(&self.shutdown);
420
421        // Spawn worker threads
422        let mut handles = Vec::new();
423        for thread_id in 0..self.config.num_threads {
424            let worker = CryptoWorker::new(
425                thread_id,
426                Arc::clone(&work_dist),
427                Arc::clone(&results),
428                self.config.clone(),
429            );
430
431            let states_clone = states.to_vec();
432            let handle = thread::spawn(move || {
433                // Set thread affinity for optimal cache performance
434                if worker.config.enable_affinity {
435                    set_thread_affinity(thread_id, worker.config.affinity_strategy);
436                }
437
438                worker.process_keccak_parallel(&states_clone, level);
439            });
440
441            handles.push(handle);
442        }
443
444        // Wait for all threads to complete
445        for handle in handles {
446            if let Err(e) = handle.join() {
447                shutdown.store(true, Ordering::Release);
448                return Err(format!("Thread join error: {:?}", e).into());
449            }
450        }
451
452        // Mark work as completed
453        work_dist.mark_completed();
454
455        // Robust completion verification with timeout protection
456        let max_retries = 100; // Prevent infinite waiting
457        let mut retries = 0;
458
459        while !work_dist.is_all_work_completed() && retries < max_retries {
460            // Brief yield to allow threads to complete
461            thread::yield_now();
462            retries += 1;
463
464            // Check for completion every few iterations to reduce overhead
465            if retries % 10 == 0 {
466                let completed = work_dist.completed_count.load(Ordering::Acquire);
467                if completed >= work_dist.total_items {
468                    break;
469                }
470            }
471        }
472
473        // Final verification
474        if !work_dist.is_all_work_completed() {
475            let completed = work_dist.completed_count.load(Ordering::Acquire);
476            return Err(format!(
477                "Incomplete processing after timeout: {} of {} items completed",
478                completed, work_dist.total_items
479            )
480            .into());
481        }
482
483        // Extract results (recover from poison so callers get the buffer workers wrote)
484        let results_guard = acquire_results_read(results.as_ref());
485        Ok(results_guard.clone())
486    }
487
488    /// Process states sequentially (fallback)
489    fn process_sequential(
490        &self,
491        states: &[[u64; 25]],
492        level: OptimizationLevel,
493    ) -> Result<Vec<[u64; 25]>, Box<dyn std::error::Error + Send + Sync>> {
494        let mut results = Vec::with_capacity(states.len());
495
496        for state in states {
497            let mut result_state = *state;
498            match level {
499                OptimizationLevel::Reference => {
500                    keccak_p(&mut result_state, 24);
501                }
502                OptimizationLevel::Basic => {
503                    #[cfg(all(
504                        target_arch = "x86_64",
505                        feature = "asm",
506                        target_feature = "avx2",
507                        not(cross_compile)
508                    ))]
509                    unsafe {
510                        crate::x86::p1600_avx2(&mut result_state);
511                    }
512                    #[cfg(not(all(
513                        target_arch = "x86_64",
514                        target_feature = "avx2",
515                        not(cross_compile)
516                    )))]
517                    {
518                        keccak_p(&mut result_state, 24);
519                    }
520                }
521                OptimizationLevel::Advanced => {
522                    #[cfg(all(
523                        target_arch = "x86_64",
524                        feature = "asm",
525                        target_feature = "avx2",
526                        not(cross_compile)
527                    ))]
528                    unsafe {
529                        crate::x86::p1600_avx2(&mut result_state);
530                    }
531                    #[cfg(not(all(
532                        target_arch = "x86_64",
533                        target_feature = "avx2",
534                        not(cross_compile)
535                    )))]
536                    {
537                        keccak_p(&mut result_state, 24);
538                    }
539                }
540                OptimizationLevel::Maximum => {
541                    #[cfg(all(
542                        target_arch = "x86_64",
543                        feature = "asm",
544                        target_feature = "avx512f"
545                    ))]
546                    unsafe {
547                        crate::x86::p1600_avx512(&mut result_state);
548                    }
549                    #[cfg(all(
550                        target_arch = "x86_64",
551                        feature = "asm",
552                        target_feature = "avx2",
553                        not(target_feature = "avx512f"),
554                        not(cross_compile)
555                    ))]
556                    unsafe {
557                        crate::x86::p1600_avx2(&mut result_state);
558                    }
559                    #[cfg(not(all(
560                        target_arch = "x86_64",
561                        any(target_feature = "avx2", target_feature = "avx512f")
562                    )))]
563                    {
564                        keccak_p(&mut result_state, 24);
565                    }
566                }
567            }
568            results.push(result_state);
569        }
570
571        Ok(results)
572    }
573
574    /// Shutdown the thread pool
575    pub fn shutdown(&self) {
576        self.shutdown.store(true, Ordering::Release);
577    }
578}
579
580/// Global thread pool instance
581static GLOBAL_THREAD_POOL: OnceLock<Arc<CryptoThreadPool>> = OnceLock::new();
582
583/// Initialize the global thread pool (first call wins; later configs are ignored).
584pub fn init_global_thread_pool(config: ThreadingConfig) {
585    GLOBAL_THREAD_POOL.get_or_init(|| Arc::new(CryptoThreadPool::new(config)));
586}
587
588/// Get the global thread pool instance
589pub fn get_global_thread_pool() -> Option<Arc<CryptoThreadPool>> {
590    GLOBAL_THREAD_POOL.get().cloned()
591}
592
593/// Process Keccak states using the global thread pool
594pub fn process_keccak_states_global(
595    states: &[[u64; 25]],
596    level: OptimizationLevel,
597) -> Result<Vec<[u64; 25]>, Box<dyn std::error::Error + Send + Sync>> {
598    if let Some(pool) = get_global_thread_pool() {
599        pool.process_keccak_states(states, level)
600    } else {
601        // Fallback to sequential processing
602        let config = ThreadingConfig::default();
603        let pool = CryptoThreadPool::new(config);
604        pool.process_keccak_states(states, level)
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    #[test]
613    #[cfg(feature = "std")]
614    fn test_threading_config_defaults() {
615        let config = ThreadingConfig::default();
616        assert!(config.num_threads > 0);
617        assert!(config.min_work_size > 0);
618        assert!(config.max_work_per_thread > 0);
619        assert_eq!(config.affinity_strategy, AffinityStrategy::Spread);
620        assert!(config.enable_affinity);
621    }
622
623    #[test]
624    #[cfg(feature = "std")]
625    fn test_threading_config_security_optimized() {
626        let config = ThreadingConfig::security_optimized();
627        assert_eq!(config.num_threads, 1);
628        assert_eq!(config.min_work_size, usize::MAX);
629        assert_eq!(config.affinity_strategy, AffinityStrategy::Disabled);
630        assert!(!config.enable_affinity);
631    }
632
633    #[test]
634    #[cfg(feature = "std")]
635    fn test_threading_config_performance_optimized() {
636        let config = ThreadingConfig::performance_optimized();
637        assert!(config.num_threads > 0);
638        assert!(config.min_work_size < usize::MAX);
639        assert_eq!(config.affinity_strategy, AffinityStrategy::Spread);
640        assert!(config.enable_affinity);
641    }
642
643    #[test]
644    #[cfg(feature = "std")]
645    fn test_threading_config_balanced() {
646        let config = ThreadingConfig::balanced();
647        assert!(config.num_threads > 0);
648        assert!(config.min_work_size > 0);
649        assert_eq!(config.affinity_strategy, AffinityStrategy::Compact);
650        assert!(config.enable_affinity);
651    }
652
653    #[test]
654    #[cfg(feature = "std")]
655    fn test_work_distribution() {
656        let work_dist = WorkDistribution::new(100);
657        assert!(!work_dist.is_completed());
658
659        // Test chunk distribution
660        let chunk1 = work_dist.get_next_chunk(25);
661        assert_eq!(chunk1, Some((0, 25)));
662
663        let chunk2 = work_dist.get_next_chunk(25);
664        assert_eq!(chunk2, Some((25, 50)));
665
666        work_dist.mark_completed();
667        assert!(work_dist.is_completed());
668    }
669
670    /// Regression: workers must not drop permuted lanes when the results `RwLock`
671    /// is poisoned after another thread panicked while holding the write guard.
672    #[test]
673    #[cfg(feature = "std")]
674    fn poisoned_results_lock_still_persists_writes() {
675        let results = Arc::new(RwLock::new(vec![[0u64; 25]; 2]));
676        let results_for_panic = Arc::clone(&results);
677
678        let panicker = thread::spawn(move || {
679            let _guard = results_for_panic
680                .write()
681                .expect("lock results buffer for poison test");
682            panic!("intentional test panic while holding write lock");
683        });
684        assert!(panicker.join().is_err());
685
686        {
687            let mut guard = acquire_results_write(results.as_ref());
688            guard[1] = [42u64; 25];
689        }
690
691        let guard = acquire_results_read(results.as_ref());
692        assert_eq!(guard[1], [42u64; 25]);
693    }
694
695    #[test]
696    #[cfg(feature = "std")]
697    fn test_worker_id_and_stats() {
698        let work_dist = Arc::new(WorkDistribution::new(10));
699        let results = Arc::new(RwLock::new(vec![[0u64; 25]; 10]));
700        let config = ThreadingConfig::default();
701
702        // Create worker with specific ID
703        let worker = CryptoWorker::new(42, Arc::clone(&work_dist), Arc::clone(&results), config);
704
705        // Test worker ID retrieval
706        assert_eq!(worker.get_worker_id(), 42);
707
708        // Test initial stats
709        let stats = worker.get_stats();
710        assert_eq!(stats.worker_id, 42);
711        assert_eq!(stats.work_items_processed, 0);
712
713        // Worker ID and stats are properly accessible for monitoring
714    }
715
716    #[test]
717    #[cfg(feature = "std")]
718    fn test_sequential_processing() {
719        let config = ThreadingConfig::security_optimized();
720        let pool = CryptoThreadPool::new(config);
721
722        let states = vec![[0u64; 25], [1u64; 25], [2u64; 25]];
723
724        let results = pool
725            .process_keccak_states(&states, OptimizationLevel::Reference)
726            .expect("Failed to process Keccak states in thread pool");
727        assert_eq!(results.len(), states.len());
728
729        // Verify that states were modified
730        for (original, result) in states.iter().zip(results.iter()) {
731            assert_ne!(original, result);
732        }
733    }
734
735    #[test]
736    #[cfg(feature = "std")]
737    fn test_global_thread_pool() {
738        let config = ThreadingConfig::balanced();
739        init_global_thread_pool(config);
740
741        let pool = get_global_thread_pool();
742        assert!(pool.is_some());
743
744        let states = vec![[0u64; 25]; 10];
745        let results = process_keccak_states_global(&states, OptimizationLevel::Reference)
746            .expect("Failed to process Keccak states with global thread pool");
747        assert_eq!(results.len(), states.len());
748    }
749}