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#[derive(Clone, Copy)]
19pub struct Blake3PoW {
20 challenge: [u8; 32],
22 threshold: u64,
24 platform: Platform,
26 inputs: [[u8; BLOCK_LEN]; MAX_SIMD_DEGREE],
28 outputs: [u8; OUT_LEN * MAX_SIMD_DEGREE],
30}
31
32impl PowStrategy for Blake3PoW {
33 #[allow(clippy::cast_sign_loss)]
43 fn new(challenge: [u8; 32], bits: f64) -> Self {
44 assert_eq!(BLOCK_LEN, 64);
46 assert_eq!(OUT_LEN, 32);
48 assert!((0.0..60.0).contains(&bits), "bits must be smaller than 60");
50
51 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 challenge,
60 threshold: (64.0 - bits).exp2().ceil() as u64,
62 platform: Platform::detect(),
64 inputs,
66 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 fn check(&mut self, nonce: u64) -> bool {
86 let mut hasher = blake3::Hasher::new();
88
89 hasher.update(&self.challenge);
91 hasher.update(&nonce.to_le_bytes());
93 hasher.update(&[0; 24]);
95
96 let mut hash = [0u8; 8];
98 hasher.finalize_xof().fill(&mut hash);
99
100 u64::from_le_bytes(hash) < self.threshold
102 }
103
104 #[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 #[cfg(feature = "parallel")]
119 fn solve(&mut self) -> Option<PoWSolution> {
120 use std::sync::atomic::{AtomicU64, Ordering};
121
122 let global_min = AtomicU64::new(u64::MAX);
125
126 let _ = broadcast(|ctx| {
128 let mut worker = *self;
130
131 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 if nonce >= global_min.load(Ordering::Relaxed) {
141 break;
142 }
143 if let Some(nonce) = worker.check_many(nonce) {
145 global_min.fetch_min(nonce, Ordering::SeqCst);
148 break;
149 }
150 }
151 });
152
153 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 #[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; fn check_many(&mut self, nonce: u64) -> Option<u64> {
175 for (i, input) in self.inputs.iter_mut().enumerate() {
177 let n = (nonce + i as u64).to_le_bytes();
179 input[32..40].copy_from_slice(&n);
180 }
181
182 let input_refs: [&[u8; BLOCK_LEN]; MAX_SIMD_DEGREE] =
184 std::array::from_fn(|i| &self.inputs[i]);
185
186 self.platform.hash_many::<BLOCK_LEN>(
188 &input_refs,
189 &Self::BLAKE3_IV, 0, IncrementCounter::No, Self::BLAKE3_FLAGS, 0,
194 0, &mut self.outputs,
196 );
197
198 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
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], [0xFF; 32], [42u8; 32], (0..32).collect::<Vec<u8>>().try_into().unwrap(), (0..32).rev().collect::<Vec<u8>>().try_into().unwrap(), ]
224 }
225
226 #[test]
227 fn test_pow_blake3() {
228 const BITS: f64 = 10.0;
229
230 for challenge in sample_challenges() {
231 let _solution =
233 grind_pow::<Blake3PoW>(challenge, BITS).expect("Should find a valid solution");
234
235 assert!(grind_pow::<Blake3PoW>(challenge, BITS).is_some());
237 }
238
239 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 }
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 }
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 let base_nonce = 12_345_678;
332 let _ = pow.check_many(base_nonce);
333
334 for (i, input) in pow.inputs.iter().enumerate() {
335 assert_eq!(&input[..32], &challenge);
337 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 let solution = pow.solve().expect("Should find a solution");
358
359 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 assert!(found > 0, "Expected to find at least one solution");
384 }
385}