Skip to main content

spongefish_pow/
lib.rs

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