1use spongefish::{
4 DuplexSpongeInit, DuplexSpongeInterface, Encoding, FromNarg, FromUniform, ProverState,
5 Transcript, VerificationError, Witness,
6};
7
8pub trait PowStrategy: Clone {
28 type Nonce: From<u64> + Into<u64> + Encoding + FromNarg;
32
33 fn enabled(&self) -> bool {
37 true
38 }
39
40 fn last_nonce(&self) -> u64 {
42 u64::MAX
43 }
44
45 fn challenge_len(&self) -> usize;
49
50 fn accepts(&self, nonce: u64, challenge: &[u8]) -> bool;
58
59 fn solve<E: Fn(&[u8], &mut [u8]) + Clone>(self, evaluate: E) -> Option<u64> {
66 let mut challenge = vec![0; self.challenge_len()];
67 (0..=self.last_nonce()).find(|&nonce| {
68 evaluate(Self::Nonce::from(nonce).encode().as_ref(), &mut challenge);
69 self.accepts(nonce, &challenge)
70 })
71 }
72
73 fn solve_parallel<E>(self, evaluate: E) -> Option<u64>
84 where
85 Self: Send,
86 E: Fn(&[u8], &mut [u8]) + Clone + Send,
87 {
88 #[cfg(feature = "parallel")]
89 {
90 let challenge = vec![0; self.challenge_len()];
91 let nonces = 0..=self.last_nonce();
92 search_parallel(
93 (self, evaluate, challenge),
94 nonces,
95 |(config, evaluate, challenge), nonce| {
96 evaluate(Self::Nonce::from(nonce).encode().as_ref(), challenge);
97 config.accepts(nonce, challenge)
98 },
99 )
100 }
101 #[cfg(not(feature = "parallel"))]
102 {
103 self.solve(evaluate)
104 }
105 }
106}
107
108pub trait PowTranscriptExt: Transcript {
126 fn verifier_message_pow<T: FromUniform>(
128 &mut self,
129 config: impl PowStrategy,
130 ) -> Result<T, VerificationError> {
131 if config.enabled() && config.challenge_len() == 0 {
132 self.check(|| false)?;
133 return Err(VerificationError);
134 }
135 let nonce = if config.enabled() {
136 self.prover_only(|| {
137 let evaluate = self
138 .verifier_message_oracle()
139 .into_known()
140 .ok_or(VerificationError)?;
141 config.clone().solve(evaluate).ok_or(VerificationError)
142 })
143 .transpose()?
144 } else {
145 Witness::unknown()
146 };
147 finish(self, &config, nonce)
148 }
149}
150
151impl<T: Transcript + ?Sized> PowTranscriptExt for T {}
152
153pub fn verifier_message_pow_parallel<T: FromUniform>(
165 prover: &mut ProverState<
166 impl DuplexSpongeInterface<U = u8> + Send,
167 impl DuplexSpongeInit<U = u8>,
168 >,
169 config: impl PowStrategy + Send,
170) -> Result<T, VerificationError> {
171 if !config.enabled() {
172 return finish(prover, &config, Witness::unknown());
173 }
174 if config.challenge_len() == 0 {
175 prover.check(|| false)?;
176 return Err(VerificationError);
177 }
178 let replay = config.clone();
179 let nonce = config
180 .solve_parallel(prover.verifier_message_oracle())
181 .ok_or(VerificationError)?;
182 finish(prover, &replay, Witness::known(nonce))
183}
184
185fn finish<T: FromUniform, C: PowStrategy>(
186 transcript: &mut (impl Transcript + ?Sized),
187 config: &C,
188 nonce: Witness<u64>,
189) -> Result<T, VerificationError> {
190 if config.enabled() {
191 let value: u64 = transcript.prover_message(nonce.map(C::Nonce::from))?.into();
192 let in_range = value <= config.last_nonce();
193 if nonce.map(|_| in_range).into_known() == Some(false) {
194 return Err(VerificationError);
195 }
196 transcript.check(|| in_range)?;
197 let challenge: Vec<u8> = (0..config.challenge_len())
198 .map(|_| transcript.verifier_message())
199 .collect();
200 let accepted = config.accepts(value, &challenge);
201 if nonce.map(|_| accepted).into_known() == Some(false) {
204 return Err(VerificationError);
205 }
206 transcript.check(|| accepted)?;
207 } else {
208 transcript.check(|| true)?;
209 }
210 Ok(transcript.verifier_message())
211}
212
213#[cfg(feature = "parallel")]
215fn search_parallel<S: Clone + Send>(
216 state: S,
217 nonces: core::ops::RangeInclusive<u64>,
218 check: fn(&mut S, u64) -> bool,
219) -> Option<u64> {
220 use rayon::prelude::*;
221
222 nonces
223 .into_par_iter()
224 .map_with(state, |state, nonce| check(state, nonce).then_some(nonce))
225 .find_first(Option::is_some)
226 .flatten()
227}
228
229#[cfg(test)]
230mod tests {
231 use spongefish::{
232 instantiations::TurboShake128, DuplexSpongeInit, DuplexSpongeInterface, Narg, ProverState,
233 VerifierState,
234 };
235
236 use super::{verifier_message_pow_parallel, PowStrategy, PowTranscriptExt};
237
238 #[derive(Clone)]
240 struct Seeded(bool);
241
242 fn hash_accepts(seed: &[u8], nonce: u64) -> bool {
243 let mut hash = TurboShake128::init(&[19; 32]);
244 hash.absorb(seed).absorb(&nonce.to_le_bytes());
245 let mut result = [0; 1];
246 hash.squeeze(&mut result);
247 result[0].trailing_zeros() >= 3
248 }
249
250 impl PowStrategy for Seeded {
251 type Nonce = u64;
252
253 fn enabled(&self) -> bool {
254 self.0
255 }
256 fn last_nonce(&self) -> u64 {
257 511
258 }
259 fn challenge_len(&self) -> usize {
260 32
261 }
262 fn accepts(&self, nonce: u64, seed: &[u8]) -> bool {
263 hash_accepts(seed, nonce)
264 }
265 }
266
267 #[test]
268 fn seeded_round_binds_each_counter_before_its_seed_and_checks_before_challenge() {
269 let session = Narg::derive_session_id(b"pow/nonce-first-seeded/v1");
270 for enabled in [false, true] {
271 let mut before = TurboShake128::init(session.as_bytes());
272 before.absorb(&0u32.to_le_bytes()).absorb(&[42]);
273 let candidate = |nonce: u64| {
274 let mut after = before.clone();
275 after.absorb(&nonce.to_le_bytes());
276 let mut seed = [0; 32];
277 after.squeeze(&mut seed);
278 (hash_accepts(&seed, nonce), after)
279 };
280 let (proof, mut after) = if enabled {
281 (0..=511)
282 .find_map(|nonce| {
283 let (accepted, after) = candidate(nonce);
284 accepted.then_some((nonce.to_le_bytes().to_vec(), after))
285 })
286 .unwrap()
287 } else {
288 (vec![], before.clone())
289 };
290 let challenge = u64::from_le_bytes(after.squeeze_array());
291 let following = u64::from_le_bytes(after.squeeze_array());
292 for parallel in [false, true] {
293 let mut prover =
294 ProverState::<TurboShake128>::new_with_seed(&session, &0u32, [7; 32]);
295 prover.public_message(&42u8);
296 let result = if parallel {
297 verifier_message_pow_parallel::<u64>(&mut prover, Seeded(enabled))
298 } else {
299 prover.verifier_message_pow::<u64>(Seeded(enabled))
300 };
301 assert_eq!(result.unwrap(), challenge);
302 assert_eq!(prover.verifier_message::<u64>(), following);
303 assert_eq!(prover.into_narg_string(), proof);
304 let mut verifier = VerifierState::<TurboShake128>::new(&session, &0u32, &proof);
305 verifier.public_message(&42u8);
306 let result = verifier.verifier_message_pow::<u64>(Seeded(enabled));
307 assert_eq!(result.unwrap(), challenge);
308 assert_eq!(verifier.verifier_message::<u64>(), following);
309 verifier.check_eof().unwrap();
310 }
311 if enabled {
312 let bad = (0..=511).find(|&n| !candidate(n).0).unwrap().to_le_bytes();
313 let out_of_range = 512u64.to_le_bytes();
314 for proof in [&bad[..], &out_of_range[..], &bad[..7]] {
315 let mut verifier = VerifierState::<TurboShake128>::new(&session, &0u32, proof);
316 verifier.public_message(&42u8);
317 assert!(verifier.verifier_message_pow::<u64>(Seeded(true)).is_err());
318 assert!(verifier.verifier_message_pow::<u64>(Seeded(false)).is_err());
319 assert!(verifier.check_eof().is_err());
320 }
321 }
322 }
323 }
324
325 #[derive(Clone)]
326 struct Problem {
327 limit: u64,
328 accepted: &'static [u64],
329 }
330
331 impl PowStrategy for Problem {
332 type Nonce = u64;
333
334 fn challenge_len(&self) -> usize {
335 1
336 }
337
338 fn accepts(&self, nonce: u64, _challenge: &[u8]) -> bool {
339 self.accepted.contains(&nonce)
340 }
341
342 fn last_nonce(&self) -> u64 {
343 self.limit
344 }
345 }
346
347 fn evaluate(_candidate: &[u8], output: &mut [u8]) {
348 output.fill(0);
349 }
350
351 #[test]
352 fn search_respects_nonce_order_and_bounds() {
353 const CASES: &[(u64, &[u64])] = &[
354 (0, &[]),
355 (0, &[0]),
356 (0, &[1]),
357 (8, &[]),
358 (8, &[0, 4, 8]),
359 (8, &[7, 1, 4]),
360 (8, &[8]),
361 (8, &[9, u64::MAX]),
362 ];
363 for &(limit, accepted) in CASES {
364 let problem = Problem { limit, accepted };
365 let expected = accepted.iter().copied().filter(|&n| n <= limit).min();
366 assert_eq!(problem.clone().solve(evaluate), expected);
367 #[cfg(not(feature = "parallel"))]
368 assert_eq!(problem.solve_parallel(evaluate), expected);
369 #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
370 for workers in [1, 3, 4] {
371 let pool = rayon::ThreadPoolBuilder::new()
372 .num_threads(workers)
373 .build()
374 .unwrap();
375 assert_eq!(
376 pool.install(|| problem.clone().solve_parallel(evaluate)),
377 expected
378 );
379 }
380 }
381 }
382
383 #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
384 #[test]
385 fn parallel_search_handles_empty_ranges_and_u64_max() {
386 const HITS: &[&[u64]] = &[
387 &[],
388 &[0],
389 &[8],
390 &[9],
391 &[u64::MAX - 8],
392 &[u64::MAX - 1, u64::MAX],
393 &[u64::MAX],
394 ];
395 for workers in [1, 3, 4] {
396 let pool = rayon::ThreadPoolBuilder::new()
397 .num_threads(workers)
398 .build()
399 .unwrap();
400 for nonces in [
401 0..=8,
402 u64::MAX - 8..=u64::MAX,
403 core::ops::RangeInclusive::new(1, 0),
404 ] {
405 for &accepted in HITS {
406 let problem = Problem {
407 limit: *nonces.end(),
408 accepted,
409 };
410 let expected = accepted
411 .iter()
412 .copied()
413 .filter(|n| nonces.contains(n))
414 .min();
415 assert_eq!(
416 pool.install(|| super::search_parallel(
417 problem,
418 nonces.clone(),
419 |problem, nonce| problem.accepts(nonce, &[0])
420 )),
421 expected
422 );
423 }
424 }
425 }
426 }
427}