p3_challenger/grinding_challenger.rs
1use p3_field::{
2 Field, PackedValue, PrimeCharacteristicRing, PrimeField, PrimeField32, PrimeField64,
3};
4use p3_maybe_rayon::prelude::*;
5use p3_symmetric::CryptographicPermutation;
6use tracing::instrument;
7
8use crate::{
9 CanObserve, CanSample, CanSampleBits, CanSampleUniformBits, DuplexChallenger,
10 MultiField32Challenger, UniformSamplingField,
11};
12
13/// Trait for challengers that support proof-of-work (PoW) grinding.
14///
15/// A `GrindingChallenger` can:
16/// - Absorb a candidate witness into the transcript
17/// - Sample random bitstrings to check the PoW condition
18/// - Brute-force search for a valid witness that satisfies the PoW
19///
20/// This trait is typically used in protocols requiring computational effort
21/// from the prover.
22///
23/// # Why cloning is not required here
24///
25/// A parallel witness search needs a cloneable, shareable transcript.
26/// Checking a witness needs neither.
27///
28/// Those bounds therefore sit on the implementations that search.
29/// A mutable reference to a challenger can then grind too.
30pub trait GrindingChallenger: CanObserve<Self::Witness> + CanSampleBits<usize> {
31 /// The underlying field element type used as the witness.
32 type Witness: Field;
33
34 /// Perform a brute-force search to find a valid PoW witness.
35 ///
36 /// Given a `bits` parameter, this function searches for a field element
37 /// `witness` such that after observing it, the next `bits` bits that challenger outputs
38 /// are all `0`.
39 fn grind(&mut self, bits: usize) -> Self::Witness;
40
41 /// Check whether a given `witness` satisfies the PoW condition.
42 ///
43 /// After absorbing the witness, the challenger samples `bits` random bits
44 /// and verifies that all bits sampled are zero.
45 ///
46 /// Returns `true` if the witness passes the PoW check, `false` otherwise.
47 #[must_use]
48 fn check_witness(&mut self, bits: usize, witness: Self::Witness) -> bool {
49 if bits == 0 {
50 return true;
51 }
52 self.observe(witness);
53 self.sample_bits(bits) == 0
54 }
55}
56
57impl<C: GrindingChallenger> GrindingChallenger for &mut C {
58 type Witness = C::Witness;
59
60 fn grind(&mut self, bits: usize) -> Self::Witness {
61 // Reborrow through both layers so the search advances the original transcript.
62 (**self).grind(bits)
63 }
64
65 fn check_witness(&mut self, bits: usize, witness: Self::Witness) -> bool {
66 // Absorbing the witness must advance the original transcript, not a copy of it.
67 (**self).check_witness(bits, witness)
68 }
69}
70
71/// A byte challenger that can test many proof-of-work candidates against one transcript.
72///
73/// [`SerializingChallenger32`](crate::SerializingChallenger32) and
74/// [`SerializingChallenger64`](crate::SerializingChallenger64) grind through this trait.
75///
76/// The default method clones the challenger once per candidate and runs the check on the clone,
77/// so an empty `impl` serves any byte challenger. An override may share the candidate-independent
78/// work between candidates, but it must accept exactly the candidates the default accepts.
79pub trait ByteGrindingChallenger: CanObserve<u8> + CanSample<u8> + Clone + Send + Sync {
80 /// Search `0..num_candidates` for a candidate that passes.
81 ///
82 /// Candidate `c` passes when a copy of `self` that observes `encode(c)` as one slice and then
83 /// samples `S` bytes produces bytes on which `accepts` returns `true`.
84 ///
85 /// # Returns
86 ///
87 /// Any passing candidate, or `None` when none passes.
88 /// When `p3_maybe_rayon::PARALLEL_ENABLED` is false it is the smallest passing candidate.
89 fn find_witness<const W: usize, const S: usize>(
90 &self,
91 num_candidates: u64,
92 encode: impl Fn(u64) -> [u8; W] + Sync,
93 accepts: impl Fn([u8; S]) -> bool + Sync,
94 ) -> Option<u64> {
95 find_witness_by_cloning(self, num_candidates, encode, accepts)
96 }
97}
98
99/// Run the [`ByteGrindingChallenger::find_witness`] check on a fresh clone for every candidate.
100pub(crate) fn find_witness_by_cloning<C, const W: usize, const S: usize>(
101 challenger: &C,
102 num_candidates: u64,
103 encode: impl Fn(u64) -> [u8; W] + Sync,
104 accepts: impl Fn([u8; S]) -> bool + Sync,
105) -> Option<u64>
106where
107 C: CanObserve<u8> + CanSample<u8> + Clone + Sync,
108{
109 (0..num_candidates).into_par_iter().find_any(|&candidate| {
110 let mut challenger = challenger.clone();
111 challenger.observe_slice(&encode(candidate));
112 accepts(challenger.sample_array())
113 })
114}
115
116/// Trait for challengers that support proof-of-work (PoW) grinding with
117/// guaranteed uniformly sampled bits.
118pub trait UniformGrindingChallenger:
119 GrindingChallenger + CanSampleUniformBits<Self::Witness>
120{
121 /// Grinds based on *uniformly sampled bits*. This variant is allowed to do rejection
122 /// sampling if a value is sampled that would violate our uniformity requirement
123 /// (chance of about 1/P).
124 ///
125 /// Use this together with `check_witness_uniform`.
126 /// At zero difficulty, returns the canonical zero witness without changing the transcript.
127 fn grind_uniform(&mut self, bits: usize) -> Self::Witness;
128
129 /// Grinds based on *uniformly sampled bits*. This variant errors if a value is
130 /// sampled, which would violate our uniformity requirement (chance of about 1/P).
131 /// See the `UniformSamplingField` trait implemented for each field for details.
132 ///
133 /// Use this together with `check_witness_uniform_may_error`.
134 /// At zero difficulty, returns the canonical zero witness without changing the transcript.
135 fn grind_uniform_may_error(&mut self, bits: usize) -> Self::Witness;
136
137 /// Check whether a given `witness` satisfies the PoW condition.
138 ///
139 /// After absorbing the witness, the challenger samples `bits` random bits
140 /// *uniformly* and verifies that all bits sampled are zero. The uniform
141 /// sampling implies we do rejection sampling in about ~1/P cases.
142 /// At zero difficulty, accepts any witness without changing the transcript;
143 /// protocols requiring canonical proof encodings must separately require zero.
144 ///
145 /// Returns `true` if the witness passes the PoW check, `false` otherwise.
146 fn check_witness_uniform(&mut self, bits: usize, witness: Self::Witness) -> bool {
147 if bits == 0 {
148 return true;
149 }
150 self.observe(witness);
151 self.sample_uniform_bits::<true>(bits)
152 .expect("Error impossible here due to resampling strategy")
153 == 0
154 }
155
156 /// Check whether a given `witness` satisfies the PoW condition.
157 ///
158 /// After absorbing the witness, the challenger samples `bits` random bits
159 /// *uniformly* and verifies that all bits sampled are zero. In about ~1/P
160 /// cases this function may error if a sampled value lies outside a range
161 /// in which we can guarantee uniform bits.
162 /// At zero difficulty, accepts any witness without changing the transcript;
163 /// protocols requiring canonical proof encodings must separately require zero.
164 ///
165 /// Returns `true` if the witness passes the PoW check, `false` otherwise.
166 fn check_witness_uniform_may_error(&mut self, bits: usize, witness: Self::Witness) -> bool {
167 if bits == 0 {
168 return true;
169 }
170 self.observe(witness);
171 self.sample_uniform_bits::<false>(bits)
172 .is_ok_and(|v| v == 0)
173 }
174}
175
176impl<F, P, const WIDTH: usize, const RATE: usize> GrindingChallenger
177 for DuplexChallenger<F, P, WIDTH, RATE>
178where
179 F: PrimeField64,
180 P: CryptographicPermutation<[F; WIDTH]>
181 + CryptographicPermutation<[<F as Field>::Packing; WIDTH]>,
182{
183 type Witness = F;
184
185 #[instrument(name = "grind for proof-of-work witness", skip_all, level = "debug")]
186 fn grind(&mut self, bits: usize) -> Self::Witness {
187 // Ensure `bits` is small enough to be used in a shift.
188 assert!(bits < 64, "bit count must be valid");
189
190 // Ensure the PoW target 2^bits is smaller than the field order.
191 // Otherwise, the probability analysis for grinding would break.
192 assert!((1u64 << bits) < F::ORDER_U64);
193
194 // Trivial case: 0 bits mean no PoW is required and any witness is valid.
195 if bits == 0 {
196 return F::ZERO;
197 }
198
199 // SIMD width: number of field elements processed in parallel.
200 // Each SIMD lane corresponds to one candidate witness.
201 let lanes = F::Packing::WIDTH;
202
203 // Total number of batches needed to cover all field elements.
204 // Each batch tests `lanes` witnesses in parallel.
205 let num_batches = F::ORDER_U64.div_ceil(lanes as u64);
206
207 // Cache the field order.
208 let order = F::ORDER_U64;
209
210 // Bitmask used to check the PoW condition. eg. bits = 3 => mask = 0b111
211 // We accept a witness if (sample & mask) == 0. This verifies 'bits' trailing zeros.
212 let mask = (1u64 << bits) - 1;
213
214 // New inputs are absorbed sequentially starting at the next free rate slot.
215 // The grinding witness is absorbed at that position.
216 let witness_idx = self.input_buffer.len();
217
218 // The witness counts as one absorbed element on top of the buffered inputs.
219 let num_absorbed = witness_idx + 1;
220
221 // The absorb binds its length into the first capacity element.
222 let tagged_capacity = self.sponge_state[RATE] + F::from_u8(num_absorbed as u8);
223
224 // Build the pre-permutation sponge state shared by every candidate, broadcast to all
225 // SIMD lanes. This mirrors a scalar absorb and is invariant across batches.
226 let base_packed_state: [_; WIDTH] = core::array::from_fn(|i| {
227 if i < witness_idx {
228 // Buffered transcript elements fill the leading rate slots.
229 F::Packing::from(self.input_buffer[i])
230 } else if i < RATE {
231 // The witness slot (overwritten below) and the unused rate slots are zeroed.
232 F::Packing::ZERO
233 } else if i == RATE {
234 // The first capacity element carries the length tag.
235 F::Packing::from(tagged_capacity)
236 } else {
237 // The rest of the capacity carries forward unchanged.
238 F::Packing::from(self.sponge_state[i])
239 }
240 });
241
242 // Grinding is implemented via parallel brute-force search over candidate witnesses.
243 //
244 // For efficiency, the search is vectorized using SIMD:
245 // It is semantically equivalent to serially trying witnesses until the PoW condition is met.
246 //
247 // - Each SIMD lane corresponds to a distinct candidate witness
248 // - All lanes share the same transcript prefix
249 // - A single permutation evaluates multiple candidates in parallel
250 let witness = (0..num_batches)
251 .into_par_iter()
252 .find_map_any(|batch| {
253 // Compute the starting candidate for this batch.
254 //
255 // Each batch processes `F::Packing::WIDTH` candidates:
256 // - Batch 0 -> candidates [0, 1, ..., F::Packing::WIDTH - 1]
257 // - Batch 1 -> candidates [F::Packing::WIDTH, ..., 2 * F::Packing::WIDTH - 1]
258 // - Batch k -> candidates [k * F::Packing::WIDTH, ..., (k+1) * F::Packing::WIDTH - 1]
259 let base = batch * lanes as u64;
260
261 // Start with a copy of the precomputed base state.
262 let mut packed_state = base_packed_state;
263
264 // Generate SIMD-packed candidate witnesses.
265 // Each lane receives a distinct field element.
266 // [base + 0, base + 1, ..., base + F::Packing::WIDTH - 1]
267 let packed_witnesses = F::Packing::from_fn(|lane| {
268 let candidate = base + lane as u64;
269 if candidate < order {
270 // SAFETY: candidate < field order, so this is a valid canonical field element.
271 unsafe { F::from_canonical_unchecked(candidate) }
272 } else {
273 // Values outside the field order can never satisfy PoW, so we repeat the last potential witness
274 F::NEG_ONE
275 }
276 });
277
278 // Insert the candidate witnesses at the next absorption position.
279 //
280 // This simulates absorbing `transcript || witness` before the Fiat–Shamir challenge is derived.
281 packed_state[witness_idx] = packed_witnesses;
282
283 // Apply the cryptographic permutation (SIMD version)
284 //
285 // This permutes all `lanes` candidates simultaneously.
286 self.permutation.permute_mut(&mut packed_state);
287
288 // Check each lane for the PoW condition
289 //
290 // - In a duplex sponge, output is read from position [RATE-1] (last rate element).
291 // - We check if the low `bits` of each sample are all zeros.
292 //
293 // We scan SIMD lanes to find the first candidate whose output satisfies the PoW condition.
294 packed_state[RATE - 1]
295 .as_slice()
296 .iter()
297 .zip(packed_witnesses.as_slice())
298 .find(|(sample, _)| {
299 // Accept if the low `bits` bits are all zero.
300 (sample.as_canonical_u64() & mask) == 0
301 })
302 .map(|(_, &witness)| witness)
303 })
304 .expect("failed to find proof-of-work witness");
305
306 // Double-check the witness using the standard verifier logic and update the challenger state.
307 assert!(self.check_witness(bits, witness));
308
309 witness
310 }
311}
312
313impl<F, P, const WIDTH: usize, const RATE: usize> UniformGrindingChallenger
314 for DuplexChallenger<F, P, WIDTH, RATE>
315where
316 F: UniformSamplingField + PrimeField64,
317 P: CryptographicPermutation<[F; WIDTH]>
318 + CryptographicPermutation<[<F as Field>::Packing; WIDTH]>,
319{
320 #[instrument(
321 name = "grind uniform for proof-of-work witness",
322 skip_all,
323 level = "debug"
324 )]
325 fn grind_uniform(&mut self, bits: usize) -> Self::Witness {
326 // Call the generic grinder with the "resample" checking logic.
327 self.grind_generic(bits, |challenger, witness| {
328 challenger.check_witness_uniform(bits, witness)
329 })
330 }
331 #[instrument(
332 name = "grind uniform may error for proof-of-work witness",
333 skip_all,
334 level = "debug"
335 )]
336 fn grind_uniform_may_error(&mut self, bits: usize) -> Self::Witness {
337 // Call the generic grinder with the "error" checking logic.
338 self.grind_generic(bits, |challenger, witness| {
339 challenger.check_witness_uniform_may_error(bits, witness)
340 })
341 }
342}
343impl<F, P, const WIDTH: usize, const RATE: usize> DuplexChallenger<F, P, WIDTH, RATE>
344where
345 F: PrimeField64,
346 P: CryptographicPermutation<[F; WIDTH]>,
347{
348 /// A generic, private helper for PoW grinding, parameterized by the checking function.
349 fn grind_generic<CHECK>(&mut self, bits: usize, check_fn: CHECK) -> F
350 where
351 CHECK: Fn(&mut Self, F) -> bool + Sync + Send,
352 {
353 assert!(bits < (usize::BITS as usize), "bit count must be valid");
354 assert!(
355 (1u64 << bits) < F::ORDER_U64,
356 "bit count exceeds field order"
357 );
358 // Canonical witness for disabled PoW; leave the transcript untouched.
359 if bits == 0 {
360 return F::ZERO;
361 }
362 // The core parallel brute-force search logic.
363 let witness = (0..F::ORDER_U64)
364 .into_par_iter()
365 .map(|i| unsafe {
366 // This is safe as i is always in range.
367 F::from_canonical_unchecked(i)
368 })
369 .find_any(|&witness| check_fn(&mut self.clone(), witness))
370 .expect("failed to find proof-of-work witness");
371 // Run the check one last time on the *original* challenger to update its state
372 // and confirm the witness is valid.
373 assert!(check_fn(self, witness));
374 witness
375 }
376}
377
378impl<F, PF, P, const WIDTH: usize, const RATE: usize> GrindingChallenger
379 for MultiField32Challenger<F, PF, P, WIDTH, RATE>
380where
381 F: PrimeField32,
382 PF: PrimeField,
383 P: CryptographicPermutation<[PF; WIDTH]>,
384{
385 type Witness = F;
386
387 #[instrument(name = "grind for proof-of-work witness", skip_all, level = "debug")]
388 fn grind(&mut self, bits: usize) -> Self::Witness {
389 assert!(bits < (usize::BITS as usize), "bit count must be valid");
390 // Evaluate the bound in `u64` to keep the shift within its type width.
391 // A `u32` shift by `bits >= 32` would wrap and accept a trivial proof-of-work.
392 assert!(
393 (1u64 << bits) < F::ORDER_U64,
394 "requested bit count must fit within the field order"
395 );
396
397 // Trivial case: 0 bits mean no PoW is required and any witness is valid.
398 if bits == 0 {
399 return F::ZERO;
400 }
401
402 // The candidate-independent transcript work happens once inside `pow_check_fn`.
403 // The parallel search then runs one stack-state permutation per candidate.
404 let witness = {
405 let accepts = self.pow_check_fn(bits);
406 (0..F::ORDER_U32)
407 .into_par_iter()
408 .find_any(|&candidate| accepts(candidate))
409 .expect("failed to find proof-of-work witness")
410 };
411 // candidate < F::ORDER_U32 by construction so this is safe.
412 let witness = unsafe { F::from_canonical_unchecked(witness) };
413
414 // Re-run the standard verifier logic to validate the witness and
415 // advance the real transcript state.
416 assert!(self.check_witness(bits, witness));
417 witness
418 }
419}