spongefish_pow/lib.rs
1#[cfg(feature = "blake3")]
2pub mod blake3;
3#[cfg(feature = "keccak")]
4pub mod keccak;
5
6/// Standalone proof-of-work grinder that can work with any byte challenge.
7///
8/// This structure provides a clean separation between the PoW solving logic
9/// and the transcript/sponge operations.
10pub struct PoWGrinder<S: PowStrategy> {
11 strategy: S,
12}
13
14impl<S: PowStrategy> PoWGrinder<S> {
15 /// Creates a new PoW grounder with the given challenge and difficulty.
16 ///
17 /// # Arguments
18 /// * `challenge` - A 32-byte challenge array
19 /// * `bits` - The difficulty in bits (logarithm of expected work)
20 #[must_use]
21 pub fn new(challenge: [u8; 32], bits: f64) -> Self {
22 Self {
23 strategy: S::new(challenge, bits),
24 }
25 }
26
27 /// Attempts to find a nonce that satisfies the proof-of-work requirement.
28 ///
29 /// Returns the minimal nonce that makes the hash fall below the target threshold,
30 /// or None if no valid nonce is found (extremely unlikely for reasonable difficulty).
31 pub fn grind(&mut self) -> Option<PoWSolution> {
32 self.strategy.solve()
33 }
34
35 /// Verifies that a given nonce satisfies the proof-of-work requirement.
36 pub fn verify(&mut self, nonce: u64) -> bool {
37 self.strategy.check(nonce)
38 }
39}
40
41pub struct PoWSolution {
42 pub challenge: [u8; 32],
43 pub nonce: u64,
44}
45
46/// Convenience functions for using PoW with byte arrays.
47pub mod convenience {
48 use crate::{PoWGrinder, PoWSolution, PowStrategy};
49
50 /// Performs proof-of-work on a challenge and returns the solution.
51 ///
52 /// This is a simple wrapper that creates a grounder and immediately grinds.
53 #[must_use]
54 pub fn grind_pow<S: PowStrategy>(challenge: [u8; 32], bits: f64) -> Option<PoWSolution> {
55 let mut grounder = PoWGrinder::<S>::new(challenge, bits);
56 grounder.grind()
57 }
58
59 /// Verifies a proof-of-work nonce.
60 #[must_use]
61 pub fn verify_pow<S: PowStrategy>(challenge: [u8; 32], bits: f64, nonce: u64) -> bool {
62 let mut grounder = PoWGrinder::<S>::new(challenge, bits);
63 grounder.verify(nonce)
64 }
65}
66
67pub trait PowStrategy: Clone + Sync {
68 /// Creates a new proof-of-work challenge.
69 /// The `challenge` is a 32-byte array that represents the challenge.
70 /// The `bits` is the binary logarithm of the expected amount of work.
71 /// When `bits` is large (i.e. close to 64), a valid solution may not be found.
72 fn new(challenge: [u8; 32], bits: f64) -> Self;
73
74 /// Check if the `nonce` satisfies the challenge.
75 fn check(&mut self, nonce: u64) -> bool;
76
77 /// Builds a solution given the input nonce
78 fn solution(&self, nonce: u64) -> PoWSolution;
79
80 /// Finds the minimal `nonce` that satisfies the challenge.
81 #[cfg(not(feature = "parallel"))]
82 fn solve(&mut self) -> Option<PoWSolution> {
83 (0..=u64::MAX)
84 .find(|&nonce| self.check(nonce))
85 .map(|nonce| self.solution(nonce))
86 }
87
88 #[cfg(feature = "parallel")]
89 fn solve(&mut self) -> Option<PoWSolution> {
90 // Split the work across all available threads.
91 // Use atomics to find the unique deterministic lowest satisfying nonce.
92
93 use std::sync::atomic::{AtomicU64, Ordering};
94
95 use rayon::broadcast;
96 let global_min = AtomicU64::new(u64::MAX);
97 let _ = broadcast(|ctx| {
98 let mut worker = self.clone();
99 let nonces = (ctx.index() as u64..).step_by(ctx.num_threads());
100 for nonce in nonces {
101 // Use relaxed ordering to eventually get notified of another thread's solution.
102 // (Propagation delay should be in the order of tens of nanoseconds.)
103 if nonce >= global_min.load(Ordering::Relaxed) {
104 break;
105 }
106 if worker.check(nonce) {
107 // We found a solution, store it in the global_min.
108 // Use fetch_min to solve race condition with simultaneous solutions.
109 global_min.fetch_min(nonce, Ordering::SeqCst);
110 break;
111 }
112 }
113 });
114 let nonce = global_min.load(Ordering::SeqCst);
115 self.check(nonce).then(|| self.solution(nonce))
116 }
117}