spongefish_pow/
blake3.rs

1use blake3::{
2    guts::BLOCK_LEN,
3    platform::{Platform, MAX_SIMD_DEGREE},
4    IncrementCounter, OUT_LEN,
5};
6#[cfg(feature = "parallel")]
7use rayon::broadcast;
8
9use super::PowStrategy;
10use crate::PoWSolution;
11
12/// A SIMD-accelerated BLAKE3-based proof-of-work engine.
13///
14/// This struct encapsulates the state needed to search for a nonce such that
15/// `BLAKE3(challenge || nonce)` is below a difficulty threshold.
16///
17/// It leverages `Platform::hash_many` for parallel hash evaluation using `MAX_SIMD_DEGREE` lanes.
18#[derive(Clone, Copy)]
19pub struct Blake3PoW {
20    /// The 32-byte challenge seed used as a prefix to every hash input.
21    challenge: [u8; 32],
22    /// Difficulty target: hashes must be less than this 64-bit threshold.
23    threshold: u64,
24    /// Platform-specific SIMD hashing backend selected at runtime.
25    platform: Platform,
26    /// SIMD batch of hash inputs, each 64 bytes (challenge + nonce).
27    inputs: [[u8; BLOCK_LEN]; MAX_SIMD_DEGREE],
28    /// SIMD batch of hash outputs (32 bytes each).
29    outputs: [u8; OUT_LEN * MAX_SIMD_DEGREE],
30}
31
32impl PowStrategy for Blake3PoW {
33    /// Create a new Blake3PoW instance with a given challenge and difficulty.
34    ///
35    /// The `bits` parameter controls the difficulty. A higher number means
36    /// lower probability of success per nonce. This function prepares the SIMD
37    /// input buffer with the challenge prefix and sets the internal threshold.
38    ///
39    /// # Panics
40    /// - If `bits` is not in the range [0.0, 60.0).
41    /// - If `BLOCK_LEN` or `OUT_LEN` do not match expected values.
42    #[allow(clippy::cast_sign_loss)]
43    fn new(challenge: [u8; 32], bits: f64) -> Self {
44        // BLAKE3 block size must be 64 bytes.
45        assert_eq!(BLOCK_LEN, 64);
46        // BLAKE3 output size must be 32 bytes.
47        assert_eq!(OUT_LEN, 32);
48        // Ensure the difficulty is within supported range.
49        assert!((0.0..60.0).contains(&bits), "bits must be smaller than 60");
50
51        // Prepare SIMD input buffer: fill each lane with the challenge prefix.
52        let mut inputs = [[0u8; BLOCK_LEN]; MAX_SIMD_DEGREE];
53        for input in &mut inputs {
54            input[..32].copy_from_slice(&challenge);
55        }
56
57        Self {
58            // Store challenge prefix.
59            challenge,
60            // Compute threshold: smaller means harder PoW.
61            threshold: (64.0 - bits).exp2().ceil() as u64,
62            // Detect SIMD platform (e.g., AVX2, NEON, etc).
63            platform: Platform::detect(),
64            // Pre-filled SIMD inputs (nonce injected later).
65            inputs,
66            // Zero-initialized output buffer for SIMD hashes.
67            outputs: [0; OUT_LEN * MAX_SIMD_DEGREE],
68        }
69    }
70
71    fn solution(&self, nonce: u64) -> PoWSolution {
72        PoWSolution {
73            challenge: self.challenge,
74            nonce,
75        }
76    }
77
78    /// Check if a given `nonce` satisfies the challenge.
79    ///
80    /// This uses the standard high-level BLAKE3 interface to ensure
81    /// full compatibility with reference implementations.
82    ///
83    /// A nonce is valid if the first 8 bytes of the hash output,
84    /// interpreted as a little-endian `u64`, are below the internal threshold.
85    fn check(&mut self, nonce: u64) -> bool {
86        // Create a new BLAKE3 hasher instance.
87        let mut hasher = blake3::Hasher::new();
88
89        // Feed the challenge prefix.
90        hasher.update(&self.challenge);
91        // Feed the nonce as little-endian bytes.
92        hasher.update(&nonce.to_le_bytes());
93        // Zero-extend the nonce to 32 bytes (challenge + nonce = full block).
94        hasher.update(&[0; 24]);
95
96        // Hash the input and extract the first 8 bytes.
97        let mut hash = [0u8; 8];
98        hasher.finalize_xof().fill(&mut hash);
99
100        // Check whether the result is below the threshold.
101        u64::from_le_bytes(hash) < self.threshold
102    }
103
104    /// Finds the minimal `nonce` that satisfies the challenge.
105    #[cfg(not(feature = "parallel"))]
106    fn solve(&mut self) -> Option<PoWSolution> {
107        (0..)
108            .step_by(MAX_SIMD_DEGREE)
109            .find_map(|nonce| self.check_many(nonce))
110            .map(|nonce| self.solution(nonce))
111    }
112
113    /// Search for the lowest `nonce` that satisfies the challenge using parallel threads.
114    ///
115    /// Each thread scans disjoint chunks of the nonce space in stride-sized steps.
116    /// The first thread to find a satisfying nonce updates a shared atomic minimum,
117    /// and all others check against it to avoid unnecessary work.
118    #[cfg(feature = "parallel")]
119    fn solve(&mut self) -> Option<PoWSolution> {
120        use std::sync::atomic::{AtomicU64, Ordering};
121
122        // Split the work across all available threads.
123        // Use atomics to find the unique deterministic lowest satisfying nonce.
124        let global_min = AtomicU64::new(u64::MAX);
125
126        // Spawn parallel workers using Rayon's broadcast.
127        let _ = broadcast(|ctx| {
128            // Copy the PoW instance for thread-local use.
129            let mut worker = *self;
130
131            // Each thread searches a distinct subset of nonces.
132            let nonces = ((MAX_SIMD_DEGREE * ctx.index()) as u64..)
133                .step_by(MAX_SIMD_DEGREE * ctx.num_threads());
134
135            for nonce in nonces {
136                // Skip work if another thread already found a lower valid nonce.
137                //
138                // Use relaxed ordering to eventually get notified of another thread's solution.
139                // (Propagation delay should be in the order of tens of nanoseconds.)
140                if nonce >= global_min.load(Ordering::Relaxed) {
141                    break;
142                }
143                // Check a batch of nonces starting from `nonce`.
144                if let Some(nonce) = worker.check_many(nonce) {
145                    // We found a solution, store it in the global_min.
146                    // Use fetch_min to solve race condition with simultaneous solutions.
147                    global_min.fetch_min(nonce, Ordering::SeqCst);
148                    break;
149                }
150            }
151        });
152
153        // Return the best found nonce, or fallback check on `u64::MAX`.
154        match global_min.load(Ordering::SeqCst) {
155            u64::MAX => self.check(u64::MAX).then(|| self.solution(u64::MAX)),
156            nonce => Some(self.solution(nonce)),
157        }
158    }
159}
160
161impl Blake3PoW {
162    /// Default Blake3 initialization vector. Copied here because it is not publicly exported.
163    #[allow(clippy::unreadable_literal)]
164    const BLAKE3_IV: [u32; 8] = [
165        0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB,
166        0x5BE0CD19,
167    ];
168    const BLAKE3_FLAGS: u8 = 0x0B; // CHUNK_START | CHUNK_END | ROOT
169
170    /// Check a SIMD-width batch of nonces starting at `nonce`.
171    ///
172    /// Returns the first nonce in the batch that satisfies the challenge threshold,
173    /// or `None` if none do.
174    fn check_many(&mut self, nonce: u64) -> Option<u64> {
175        // Fill each SIMD input block with the challenge + nonce suffix.
176        for (i, input) in self.inputs.iter_mut().enumerate() {
177            // Write the nonce as little-endian into bytes 32..40.
178            let n = (nonce + i as u64).to_le_bytes();
179            input[32..40].copy_from_slice(&n);
180        }
181
182        // Create references required by `hash_many`.
183        let input_refs: [&[u8; BLOCK_LEN]; MAX_SIMD_DEGREE] =
184            std::array::from_fn(|i| &self.inputs[i]);
185
186        // Perform parallel hashing over the input blocks.
187        self.platform.hash_many::<BLOCK_LEN>(
188            &input_refs,
189            &Self::BLAKE3_IV,     // Initialization vector
190            0,                    // Counter
191            IncrementCounter::No, // Do not increment counter
192            Self::BLAKE3_FLAGS,   // Default flags
193            0,
194            0, // No start/end flags
195            &mut self.outputs,
196        );
197
198        // Scan results and return the first nonce under the threshold.
199        for (i, chunk) in self.outputs.chunks_exact(OUT_LEN).enumerate() {
200            let hash = u64::from_le_bytes(chunk[..8].try_into().unwrap());
201            if hash < self.threshold {
202                return Some(nonce + i as u64);
203            }
204        }
205
206        // None of the batch satisfied the condition.
207        None
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::{convenience::*, PoWGrinder};
215
216    fn sample_challenges() -> Vec<[u8; 32]> {
217        vec![
218            [0u8; 32],                                              // All zeroes
219            [0xFF; 32],                                             // All ones
220            [42u8; 32],                                             // Constant value
221            (0..32).collect::<Vec<u8>>().try_into().unwrap(),       // Increasing
222            (0..32).rev().collect::<Vec<u8>>().try_into().unwrap(), // Decreasing
223        ]
224    }
225
226    #[test]
227    fn test_pow_blake3() {
228        const BITS: f64 = 10.0;
229
230        for challenge in sample_challenges() {
231            // Generate a proof-of-work solution
232            let _solution =
233                grind_pow::<Blake3PoW>(challenge, BITS).expect("Should find a valid solution");
234
235            // Verify we can generate solutions consistently
236            assert!(grind_pow::<Blake3PoW>(challenge, BITS).is_some());
237        }
238
239        // Test using the PoWGrinder directly
240        let challenge = [42u8; 32];
241        let mut grinder = PoWGrinder::<Blake3PoW>::new(challenge, BITS);
242        let _solution = grinder.grind().expect("Should find a valid solution");
243    }
244
245    #[test]
246    #[allow(clippy::cast_sign_loss)]
247    fn test_new_pow_valid_bits() {
248        for bits in [0.1, 10.0, 20.0, 40.0, 59.99] {
249            let challenge = [1u8; 32];
250            let pow = Blake3PoW::new(challenge, bits);
251            let expected_threshold = (64.0 - bits).exp2().ceil() as u64;
252            assert_eq!(pow.threshold, expected_threshold);
253            assert_eq!(pow.challenge, challenge);
254        }
255    }
256
257    #[test]
258    #[should_panic]
259    fn test_new_invalid_bits() {
260        let _ = Blake3PoW::new([0u8; 32], 60.0);
261    }
262
263    #[test]
264    fn test_check_function_basic() {
265        let challenge = [0u8; 32];
266        let mut pow = Blake3PoW::new(challenge, 8.0);
267        for nonce in (0u64..10000).step_by(MAX_SIMD_DEGREE) {
268            if let Some(solution) = pow.check_many(nonce) {
269                assert!(pow.check(solution), "check() should match check_many()");
270                return;
271            }
272        }
273        panic!("Expected at least one valid nonce under threshold using check_many");
274    }
275
276    #[cfg(not(feature = "parallel"))]
277    #[test]
278    fn test_solve_sequential() {
279        let challenge = [2u8; 32];
280        let mut pow = Blake3PoW::new(challenge, 10.0);
281        let nonce = pow.solve().expect("Should find a nonce");
282        assert!(pow.check(nonce), "Found nonce does not satisfy challenge");
283    }
284
285    #[cfg(feature = "parallel")]
286    #[test]
287    fn test_solve_parallel() {
288        let challenge = [3u8; 32];
289        let mut pow = Blake3PoW::new(challenge, 10.0);
290        let _solution = pow.solve().expect("Should find a solution");
291        // Solution is valid by construction
292    }
293
294    #[test]
295    fn test_different_challenges_consistency() {
296        let bits = 8.0;
297        for challenge in sample_challenges() {
298            let mut pow = Blake3PoW::new(challenge, bits);
299            let _solution = pow.solve().expect("Must find solution for low difficulty");
300            // Solution is valid by construction
301        }
302    }
303
304    #[test]
305    fn test_check_many_determinism() {
306        let challenge = [42u8; 32];
307        let mut pow1 = Blake3PoW::new(challenge, 10.0);
308        let mut pow2 = Blake3PoW::new(challenge, 10.0);
309
310        let n1 = pow1.check_many(0);
311        let n2 = pow2.check_many(0);
312        assert_eq!(n1, n2, "check_many should be deterministic");
313    }
314
315    #[test]
316    #[allow(clippy::cast_sign_loss)]
317    fn test_threshold_rounding_boundaries() {
318        let c = [7u8; 32];
319        let bits = 24.5;
320        let pow = Blake3PoW::new(c, bits);
321        let expected = (64.0 - bits).exp2().ceil() as u64;
322        assert_eq!(pow.threshold, expected);
323    }
324
325    #[test]
326    fn test_check_many_inserts_nonce_bytes() {
327        let challenge = [0xAB; 32];
328        let mut pow = Blake3PoW::new(challenge, 50.0);
329
330        // Run check_many to populate nonces.
331        let base_nonce = 12_345_678;
332        let _ = pow.check_many(base_nonce);
333
334        for (i, input) in pow.inputs.iter().enumerate() {
335            // Confirm prefix is unchanged
336            assert_eq!(&input[..32], &challenge);
337            // Confirm suffix is the correct nonce bytes
338            let expected_nonce = base_nonce + i as u64;
339            let actual = u64::from_le_bytes(input[32..40].try_into().unwrap());
340            assert_eq!(actual, expected_nonce);
341        }
342    }
343
344    #[test]
345    fn test_solve_returns_minimal_nonce() {
346        let c = [123; 32];
347        let mut pow = Blake3PoW::new(c, 10.0);
348        let mut best_nonce = None;
349        for nonce in (0..10000).step_by(MAX_SIMD_DEGREE) {
350            if let Some(found) = pow.check_many(nonce) {
351                best_nonce = Some(found);
352                break;
353            }
354        }
355
356        // Get solution from solve()
357        let solution = pow.solve().expect("Should find a solution");
358
359        // If we found a nonce manually, create the same solution and compare
360        if let Some(nonce) = best_nonce {
361            let expected_solution = pow.solution(nonce);
362
363            assert_eq!(
364                solution.nonce, expected_solution.nonce,
365                "solve should return the first valid nonce"
366            );
367        }
368    }
369
370    #[test]
371    fn stress_test_check_many_entropy() {
372        let challenge = [42u8; 32];
373        let mut pow = Blake3PoW::new(challenge, 16.0);
374
375        let mut found = 0;
376        for nonce in (0..1_000_000).step_by(MAX_SIMD_DEGREE) {
377            if pow.check_many(nonce).is_some() {
378                found += 1;
379            }
380        }
381
382        // Should find some hits at low difficulty
383        assert!(found > 0, "Expected to find at least one solution");
384    }
385}