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, CanSampleBits, CanSampleUniformBits, DuplexChallenger, MultiField32Challenger,
10 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.
22pub trait GrindingChallenger:
23 CanObserve<Self::Witness> + CanSampleBits<usize> + Sync + Clone
24{
25 /// The underlying field element type used as the witness.
26 type Witness: Field;
27
28 /// Perform a brute-force search to find a valid PoW witness.
29 ///
30 /// Given a `bits` parameter, this function searches for a field element
31 /// `witness` such that after observing it, the next `bits` bits that challenger outputs
32 /// are all `0`.
33 fn grind(&mut self, bits: usize) -> Self::Witness;
34
35 /// Check whether a given `witness` satisfies the PoW condition.
36 ///
37 /// After absorbing the witness, the challenger samples `bits` random bits
38 /// and verifies that all bits sampled are zero.
39 ///
40 /// Returns `true` if the witness passes the PoW check, `false` otherwise.
41 #[must_use]
42 fn check_witness(&mut self, bits: usize, witness: Self::Witness) -> bool {
43 if bits == 0 {
44 return true;
45 }
46 self.observe(witness);
47 self.sample_bits(bits) == 0
48 }
49}
50
51/// Trait for challengers that support proof-of-work (PoW) grinding with
52/// guaranteed uniformly sampled bits.
53pub trait UniformGrindingChallenger:
54 GrindingChallenger + CanSampleUniformBits<Self::Witness>
55{
56 /// Grinds based on *uniformly sampled bits*. This variant is allowed to do rejection
57 /// sampling if a value is sampled that would violate our uniformity requirement
58 /// (chance of about 1/P).
59 ///
60 /// Use this together with `check_witness_uniform`.
61 fn grind_uniform(&mut self, bits: usize) -> Self::Witness;
62
63 /// Grinds based on *uniformly sampled bits*. This variant errors if a value is
64 /// sampled, which would violate our uniformity requirement (chance of about 1/P).
65 /// See the `UniformSamplingField` trait implemented for each field for details.
66 ///
67 /// Use this together with `check_witness_uniform_may_error`.
68 fn grind_uniform_may_error(&mut self, bits: usize) -> Self::Witness;
69
70 /// Check whether a given `witness` satisfies the PoW condition.
71 ///
72 /// After absorbing the witness, the challenger samples `bits` random bits
73 /// *uniformly* and verifies that all bits sampled are zero. The uniform
74 /// sampling implies we do rejection sampling in about ~1/P cases.
75 ///
76 /// Returns `true` if the witness passes the PoW check, `false` otherwise.
77 fn check_witness_uniform(&mut self, bits: usize, witness: Self::Witness) -> bool {
78 self.observe(witness);
79 self.sample_uniform_bits::<true>(bits)
80 .expect("Error impossible here due to resampling strategy")
81 == 0
82 }
83
84 /// Check whether a given `witness` satisfies the PoW condition.
85 ///
86 /// After absorbing the witness, the challenger samples `bits` random bits
87 /// *uniformly* and verifies that all bits sampled are zero. In about ~1/P
88 /// cases this function may error if a sampled value lies outside a range
89 /// in which we can guarantee uniform bits.
90 ///
91 /// Returns `true` if the witness passes the PoW check, `false` otherwise.
92 fn check_witness_uniform_may_error(&mut self, bits: usize, witness: Self::Witness) -> bool {
93 self.observe(witness);
94 self.sample_uniform_bits::<false>(bits)
95 .is_ok_and(|v| v == 0)
96 }
97}
98
99impl<F, P, const WIDTH: usize, const RATE: usize> GrindingChallenger
100 for DuplexChallenger<F, P, WIDTH, RATE>
101where
102 F: PrimeField64,
103 P: CryptographicPermutation<[F; WIDTH]>
104 + CryptographicPermutation<[<F as Field>::Packing; WIDTH]>,
105{
106 type Witness = F;
107
108 #[instrument(name = "grind for proof-of-work witness", skip_all, level = "debug")]
109 fn grind(&mut self, bits: usize) -> Self::Witness {
110 // Ensure `bits` is small enough to be used in a shift.
111 assert!(bits < 64, "bit count must be valid");
112
113 // Ensure the PoW target 2^bits is smaller than the field order.
114 // Otherwise, the probability analysis for grinding would break.
115 assert!((1u64 << bits) < F::ORDER_U64);
116
117 // Trivial case: 0 bits mean no PoW is required and any witness is valid.
118 if bits == 0 {
119 return F::ZERO;
120 }
121
122 // SIMD width: number of field elements processed in parallel.
123 // Each SIMD lane corresponds to one candidate witness.
124 let lanes = F::Packing::WIDTH;
125
126 // Total number of batches needed to cover all field elements.
127 // Each batch tests `lanes` witnesses in parallel.
128 let num_batches = F::ORDER_U64.div_ceil(lanes as u64);
129
130 // Cache the field order.
131 let order = F::ORDER_U64;
132
133 // Bitmask used to check the PoW condition. eg. bits = 3 => mask = 0b111
134 // We accept a witness if (sample & mask) == 0. This verifies 'bits' trailing zeros.
135 let mask = (1u64 << bits) - 1;
136
137 // New inputs are absorbed sequentially starting at the next free rate slot.
138 // The grinding witness is absorbed at that position.
139 let witness_idx = self.input_buffer.len();
140
141 // The witness counts as one absorbed element on top of the buffered inputs.
142 let num_absorbed = witness_idx + 1;
143
144 // The absorb binds its length into the first capacity element.
145 let tagged_capacity = self.sponge_state[RATE] + F::from_u8(num_absorbed as u8);
146
147 // Build the pre-permutation sponge state shared by every candidate, broadcast to all
148 // SIMD lanes. This mirrors a scalar absorb and is invariant across batches.
149 let base_packed_state: [_; WIDTH] = core::array::from_fn(|i| {
150 if i < witness_idx {
151 // Buffered transcript elements fill the leading rate slots.
152 F::Packing::from(self.input_buffer[i])
153 } else if i < RATE {
154 // The witness slot (overwritten below) and the unused rate slots are zeroed.
155 F::Packing::ZERO
156 } else if i == RATE {
157 // The first capacity element carries the length tag.
158 F::Packing::from(tagged_capacity)
159 } else {
160 // The rest of the capacity carries forward unchanged.
161 F::Packing::from(self.sponge_state[i])
162 }
163 });
164
165 // Grinding is implemented via parallel brute-force search over candidate witnesses.
166 //
167 // For efficiency, the search is vectorized using SIMD:
168 // It is semantically equivalent to serially trying witnesses until the PoW condition is met.
169 //
170 // - Each SIMD lane corresponds to a distinct candidate witness
171 // - All lanes share the same transcript prefix
172 // - A single permutation evaluates multiple candidates in parallel
173 let witness = (0..num_batches)
174 .into_par_iter()
175 .find_map_any(|batch| {
176 // Compute the starting candidate for this batch.
177 //
178 // Each batch processes `F::Packing::WIDTH` candidates:
179 // - Batch 0 -> candidates [0, 1, ..., F::Packing::WIDTH - 1]
180 // - Batch 1 -> candidates [F::Packing::WIDTH, ..., 2 * F::Packing::WIDTH - 1]
181 // - Batch k -> candidates [k * F::Packing::WIDTH, ..., (k+1) * F::Packing::WIDTH - 1]
182 let base = batch * lanes as u64;
183
184 // Start with a copy of the precomputed base state.
185 let mut packed_state = base_packed_state;
186
187 // Generate SIMD-packed candidate witnesses.
188 // Each lane receives a distinct field element.
189 // [base + 0, base + 1, ..., base + F::Packing::WIDTH - 1]
190 let packed_witnesses = F::Packing::from_fn(|lane| {
191 let candidate = base + lane as u64;
192 if candidate < order {
193 // SAFETY: candidate < field order, so this is a valid canonical field element.
194 unsafe { F::from_canonical_unchecked(candidate) }
195 } else {
196 // Values outside the field order can never satisfy PoW, so we repeat the last potential witness
197 F::NEG_ONE
198 }
199 });
200
201 // Insert the candidate witnesses at the next absorption position.
202 //
203 // This simulates absorbing `transcript || witness` before the Fiat–Shamir challenge is derived.
204 packed_state[witness_idx] = packed_witnesses;
205
206 // Apply the cryptographic permutation (SIMD version)
207 //
208 // This permutes all `lanes` candidates simultaneously.
209 self.permutation.permute_mut(&mut packed_state);
210
211 // Check each lane for the PoW condition
212 //
213 // - In a duplex sponge, output is read from position [RATE-1] (last rate element).
214 // - We check if the low `bits` of each sample are all zeros.
215 //
216 // We scan SIMD lanes to find the first candidate whose output satisfies the PoW condition.
217 packed_state[RATE - 1]
218 .as_slice()
219 .iter()
220 .zip(packed_witnesses.as_slice())
221 .find(|(sample, _)| {
222 // Accept if the low `bits` bits are all zero.
223 (sample.as_canonical_u64() & mask) == 0
224 })
225 .map(|(_, &witness)| witness)
226 })
227 .expect("failed to find proof-of-work witness");
228
229 // Double-check the witness using the standard verifier logic and update the challenger state.
230 assert!(self.check_witness(bits, witness));
231
232 witness
233 }
234}
235
236impl<F, P, const WIDTH: usize, const RATE: usize> UniformGrindingChallenger
237 for DuplexChallenger<F, P, WIDTH, RATE>
238where
239 F: UniformSamplingField + PrimeField64,
240 P: CryptographicPermutation<[F; WIDTH]>
241 + CryptographicPermutation<[<F as Field>::Packing; WIDTH]>,
242{
243 #[instrument(
244 name = "grind uniform for proof-of-work witness",
245 skip_all,
246 level = "debug"
247 )]
248 fn grind_uniform(&mut self, bits: usize) -> Self::Witness {
249 // Call the generic grinder with the "resample" checking logic.
250 self.grind_generic(bits, |challenger, witness| {
251 challenger.check_witness_uniform(bits, witness)
252 })
253 }
254 #[instrument(
255 name = "grind uniform may error for proof-of-work witness",
256 skip_all,
257 level = "debug"
258 )]
259 fn grind_uniform_may_error(&mut self, bits: usize) -> Self::Witness {
260 // Call the generic grinder with the "error" checking logic.
261 self.grind_generic(bits, |challenger, witness| {
262 challenger.check_witness_uniform_may_error(bits, witness)
263 })
264 }
265}
266impl<F, P, const WIDTH: usize, const RATE: usize> DuplexChallenger<F, P, WIDTH, RATE>
267where
268 F: PrimeField64,
269 P: CryptographicPermutation<[F; WIDTH]>,
270{
271 /// A generic, private helper for PoW grinding, parameterized by the checking function.
272 fn grind_generic<CHECK>(&mut self, bits: usize, check_fn: CHECK) -> F
273 where
274 CHECK: Fn(&mut Self, F) -> bool + Sync + Send,
275 {
276 // Maybe check that bits is greater than 0?
277 assert!(bits < (usize::BITS as usize), "bit count must be valid");
278 assert!(
279 (1u64 << bits) < F::ORDER_U64,
280 "bit count exceeds field order"
281 );
282 // The core parallel brute-force search logic.
283 let witness = (0..F::ORDER_U64)
284 .into_par_iter()
285 .map(|i| unsafe {
286 // This is safe as i is always in range.
287 F::from_canonical_unchecked(i)
288 })
289 .find_any(|&witness| check_fn(&mut self.clone(), witness))
290 .expect("failed to find proof-of-work witness");
291 // Run the check one last time on the *original* challenger to update its state
292 // and confirm the witness is valid.
293 assert!(check_fn(self, witness));
294 witness
295 }
296}
297
298impl<F, PF, P, const WIDTH: usize, const RATE: usize> GrindingChallenger
299 for MultiField32Challenger<F, PF, P, WIDTH, RATE>
300where
301 F: PrimeField32,
302 PF: PrimeField,
303 P: CryptographicPermutation<[PF; WIDTH]>,
304{
305 type Witness = F;
306
307 #[instrument(name = "grind for proof-of-work witness", skip_all, level = "debug")]
308 fn grind(&mut self, bits: usize) -> Self::Witness {
309 assert!(bits < (usize::BITS as usize), "bit count must be valid");
310 // Evaluate the bound in `u64` to keep the shift within its type width.
311 // A `u32` shift by `bits >= 32` would wrap and accept a trivial proof-of-work.
312 assert!(
313 (1u64 << bits) < F::ORDER_U64,
314 "requested bit count must fit within the field order"
315 );
316
317 // Trivial case: 0 bits mean no PoW is required and any witness is valid.
318 if bits == 0 {
319 return F::ZERO;
320 }
321
322 // The candidate-independent transcript work happens once inside `pow_check_fn`.
323 // The parallel search then runs one stack-state permutation per candidate.
324 let witness = {
325 let accepts = self.pow_check_fn(bits);
326 (0..F::ORDER_U32)
327 .into_par_iter()
328 .find_any(|&candidate| accepts(candidate))
329 .expect("failed to find proof-of-work witness")
330 };
331 // candidate < F::ORDER_U32 by construction so this is safe.
332 let witness = unsafe { F::from_canonical_unchecked(witness) };
333
334 // Re-run the standard verifier logic to validate the witness and
335 // advance the real transcript state.
336 assert!(self.check_witness(bits, witness));
337 witness
338 }
339}