Skip to main content

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}