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}