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)]
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(name = "grind uniform for proof-of-work witness", skip_all)]
244 fn grind_uniform(&mut self, bits: usize) -> Self::Witness {
245 // Call the generic grinder with the "resample" checking logic.
246 self.grind_generic(bits, |challenger, witness| {
247 challenger.check_witness_uniform(bits, witness)
248 })
249 }
250 #[instrument(name = "grind uniform may error for proof-of-work witness", skip_all)]
251 fn grind_uniform_may_error(&mut self, bits: usize) -> Self::Witness {
252 // Call the generic grinder with the "error" checking logic.
253 self.grind_generic(bits, |challenger, witness| {
254 challenger.check_witness_uniform_may_error(bits, witness)
255 })
256 }
257}
258impl<F, P, const WIDTH: usize, const RATE: usize> DuplexChallenger<F, P, WIDTH, RATE>
259where
260 F: PrimeField64,
261 P: CryptographicPermutation<[F; WIDTH]>,
262{
263 /// A generic, private helper for PoW grinding, parameterized by the checking function.
264 fn grind_generic<CHECK>(&mut self, bits: usize, check_fn: CHECK) -> F
265 where
266 CHECK: Fn(&mut Self, F) -> bool + Sync + Send,
267 {
268 // Maybe check that bits is greater than 0?
269 assert!(bits < (usize::BITS as usize), "bit count must be valid");
270 assert!(
271 (1u64 << bits) < F::ORDER_U64,
272 "bit count exceeds field order"
273 );
274 // The core parallel brute-force search logic.
275 let witness = (0..F::ORDER_U64)
276 .into_par_iter()
277 .map(|i| unsafe {
278 // This is safe as i is always in range.
279 F::from_canonical_unchecked(i)
280 })
281 .find_any(|&witness| check_fn(&mut self.clone(), witness))
282 .expect("failed to find proof-of-work witness");
283 // Run the check one last time on the *original* challenger to update its state
284 // and confirm the witness is valid.
285 assert!(check_fn(self, witness));
286 witness
287 }
288}
289
290impl<F, PF, P, const WIDTH: usize, const RATE: usize> GrindingChallenger
291 for MultiField32Challenger<F, PF, P, WIDTH, RATE>
292where
293 F: PrimeField32,
294 PF: PrimeField,
295 P: CryptographicPermutation<[PF; WIDTH]>,
296{
297 type Witness = F;
298
299 #[instrument(name = "grind for proof-of-work witness", skip_all)]
300 fn grind(&mut self, bits: usize) -> Self::Witness {
301 assert!(bits < (usize::BITS as usize), "bit count must be valid");
302 // Evaluate the bound in `u64` to keep the shift within its type width.
303 // A `u32` shift by `bits >= 32` would wrap and accept a trivial proof-of-work.
304 assert!(
305 (1u64 << bits) < F::ORDER_U64,
306 "requested bit count must fit within the field order"
307 );
308
309 // Trivial case: 0 bits mean no PoW is required and any witness is valid.
310 if bits == 0 {
311 return F::ZERO;
312 }
313
314 // The candidate-independent transcript work happens once inside `pow_check_fn`.
315 // The parallel search then runs one stack-state permutation per candidate.
316 let witness = {
317 let accepts = self.pow_check_fn(bits);
318 (0..F::ORDER_U32)
319 .into_par_iter()
320 .find_any(|&candidate| accepts(candidate))
321 .expect("failed to find proof-of-work witness")
322 };
323 // candidate < F::ORDER_U32 by construction so this is safe.
324 let witness = unsafe { F::from_canonical_unchecked(witness) };
325
326 // Re-run the standard verifier logic to validate the witness and
327 // advance the real transcript state.
328 assert!(self.check_witness(bits, witness));
329 witness
330 }
331}