Skip to main content

spongefish_pow/
keccak.rs

1use ::keccak::{Keccak, State1600};
2
3use super::PowStrategy;
4use crate::PoWSolution;
5
6#[derive(Clone, Copy)]
7pub struct KeccakPoW {
8    challenge: [u64; 4],
9    threshold: u64,
10    state: [u64; 25],
11}
12
13impl PowStrategy for KeccakPoW {
14    /// Create a new `KeccakPoW` instance with a given challenge and difficulty.
15    ///
16    /// # Panics
17    /// - If `bits` is not in the range `[0.0, 60.0)`.
18    #[allow(clippy::cast_sign_loss)]
19    fn new(challenge: [u8; 32], bits: f64) -> Self {
20        // The difficulty must stay in a range where `2^(64 - bits)` is a meaningful
21        // `u64` threshold. With negative `bits` the threshold exceeds `2^64` and
22        // saturates to `u64::MAX` on the cast below, so *every* nonce verifies: a
23        // silent no-op proof of work. `bits == 0.0` is the explicit "no grinding"
24        // setting and stays allowed. The upper bound mirrors `Blake3PoW`: both
25        // engines compare a single little-endian 64-bit word against the threshold,
26        // so the representable range is identical, and past ~60 bits the expected
27        // grinding cost is already out of reach anyway.
28        assert!((0.0..60.0).contains(&bits), "bits must be smaller than 60");
29
30        let threshold = (64.0 - bits).exp2().ceil() as u64;
31        Self {
32            challenge: bytemuck::cast(challenge),
33            threshold,
34            state: [0; 25],
35        }
36    }
37
38    fn solution(&self, nonce: u64) -> PoWSolution {
39        PoWSolution {
40            challenge: bytemuck::cast(self.challenge),
41            nonce,
42        }
43    }
44
45    fn check(&mut self, nonce: u64) -> bool {
46        self.state[..4].copy_from_slice(&self.challenge);
47        self.state[4] = nonce;
48        for s in self.state.iter_mut().skip(5) {
49            *s = 0;
50        }
51        f1600(&mut self.state);
52        self.state[0] < self.threshold
53    }
54}
55
56fn f1600(state: &mut State1600) {
57    Keccak::new().with_f1600(|f1600| f1600(state));
58}
59
60#[test]
61fn test_pow_keccak() {
62    use crate::{convenience::*, PoWGrinder};
63
64    const BITS: f64 = 10.0;
65
66    // Test with a fixed challenge
67    let challenge = [42u8; 32];
68
69    // Generate a proof-of-work solution
70    let solution = grind_pow::<KeccakPoW>(challenge, BITS).expect("Should find a valid solution");
71
72    // Grinding is deterministic: it returns the minimal satisfying nonce, so
73    // re-grinding the same challenge must yield the very same solution.
74    let mut grinder = PoWGrinder::<KeccakPoW>::new(challenge, BITS);
75    let solution2 = grinder.grind().expect("Should find a valid solution");
76    assert_eq!(solution.nonce, solution2.nonce);
77
78    // And the nonce must verify against the original challenge.
79    assert!(verify_pow::<KeccakPoW>(challenge, BITS, solution.nonce));
80}
81
82/// A negative difficulty would saturate the threshold to `u64::MAX` and make every
83/// nonce verify, so it must be rejected loudly instead.
84#[test]
85#[should_panic(expected = "bits must be smaller than 60")]
86fn test_keccak_rejects_negative_bits() {
87    let _ = <KeccakPoW as PowStrategy>::new([0u8; 32], -1.0);
88}
89
90/// Difficulties at or above 60 bits are out of the supported range.
91#[test]
92#[should_panic(expected = "bits must be smaller than 60")]
93fn test_keccak_rejects_excessive_bits() {
94    let _ = <KeccakPoW as PowStrategy>::new([0u8; 32], 60.0);
95}
96
97/// A difficulty inside the supported range still grinds and verifies.
98#[test]
99fn test_keccak_valid_difficulty_round_trip() {
100    use crate::convenience::*;
101
102    let challenge = [7u8; 32];
103    for bits in [0.0, 1.0, 8.0, 12.0] {
104        let solution =
105            grind_pow::<KeccakPoW>(challenge, bits).expect("Should find a valid solution");
106        assert!(verify_pow::<KeccakPoW>(challenge, bits, solution.nonce));
107        assert_eq!(solution.challenge, challenge);
108    }
109}