Skip to main content

spongefish_pow/
lib.rs

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