openvm_native_recursion/challenger/
multi_field32.rs

1use openvm_native_compiler::ir::{Array, Builder, Config, Ext, Felt, RVar, Var};
2use openvm_stark_backend::p3_field::{
3    max_absorb_injective_limbs, squeeze_field_order_num_limbs, PrimeCharacteristicRing,
4};
5
6use crate::{
7    challenger::{
8        CanCheckWitness, CanObserveDigest, CanObserveVariable, CanSampleBitsVariable,
9        CanSampleVariable, ChallengerVariable, FeltChallenger,
10    },
11    digest::DigestVariable,
12    outer_poseidon2::{Poseidon2CircuitBuilder, RATE, SPONGE_SIZE},
13    utils::{absorb_radix_bits_for_config, reduce_packed, split_pf_to_field_order_limbs},
14    vars::OuterDigestVariable,
15};
16
17/// In-circuit port of `p3_challenger::MultiField32Challenger`. p3's nested `DuplexChallenger` is
18/// flattened: `sponge_state` and `inner_output_buffer` correspond to `inner.sponge_state` and
19/// `inner.output_buffer`. There is no analogue of `inner.input_buffer` because the wrapper never
20/// dispatches into a `DuplexChallenger::observe` path.
21#[derive(Clone)]
22pub struct MultiField32ChallengerVariable<C: Config> {
23    sponge_state: [Var<C::N>; SPONGE_SIZE],
24    inner_output_buffer: Vec<Var<C::N>>,
25    f_buffer: Vec<Felt<C::F>>,
26    f_squeeze_buffer: Vec<Felt<C::F>>,
27    absorb_num_f_elms: usize,
28    squeeze_num_f_elms: usize,
29}
30
31impl<C: Config> MultiField32ChallengerVariable<C> {
32    pub fn new(builder: &mut Builder<C>) -> Self {
33        assert!(builder.flags.static_only);
34        MultiField32ChallengerVariable::<C> {
35            sponge_state: [
36                builder.eval(C::N::ZERO),
37                builder.eval(C::N::ZERO),
38                builder.eval(C::N::ZERO),
39            ],
40            inner_output_buffer: vec![],
41            f_buffer: vec![],
42            f_squeeze_buffer: vec![],
43            absorb_num_f_elms: max_absorb_injective_limbs::<C::F, C::N>(),
44            squeeze_num_f_elms: squeeze_field_order_num_limbs::<C::N, C::F>(),
45        }
46    }
47
48    /// Mirrors `DuplexChallenger::absorb_rate_padded_with_tag`.
49    fn absorb_rate_padded_with_tag(
50        &mut self,
51        builder: &mut Builder<C>,
52        values: &[Var<C::N>],
53        length_tag: u8,
54    ) {
55        assert!(values.len() <= RATE);
56        self.inner_output_buffer.clear();
57
58        // Each state slot must be a *fresh* Var. `p2_permute_mut`'s halo2 lowering rebinds
59        // the value behind each state Var to the post-permutation value, so aliasing the
60        // caller's Vars (e.g. the digest words) here would corrupt them for subsequent uses.
61        for (i, &value) in values.iter().enumerate() {
62            self.sponge_state[i] = builder.eval(value);
63        }
64        for i in values.len()..RATE {
65            self.sponge_state[i] = builder.eval(C::N::ZERO);
66        }
67        self.sponge_state[RATE] = builder.eval(self.sponge_state[RATE] + C::N::from_u8(length_tag));
68
69        builder.p2_permute_mut(self.sponge_state);
70        self.inner_output_buffer
71            .extend_from_slice(&self.sponge_state[..RATE]);
72    }
73
74    /// Mirrors `DuplexChallenger::duplexing` (with the always-empty `inner.input_buffer`).
75    fn duplexing(&mut self, builder: &mut Builder<C>) {
76        builder.p2_permute_mut(self.sponge_state);
77        self.inner_output_buffer.clear();
78        self.inner_output_buffer
79            .extend_from_slice(&self.sponge_state[..RATE]);
80    }
81
82    fn flush_f_if_non_empty(&mut self, builder: &mut Builder<C>) {
83        if self.f_buffer.is_empty() {
84            return;
85        }
86        let n_in = self.f_buffer.len();
87        assert!(n_in <= self.absorb_num_f_elms * RATE);
88        let radix_bits = absorb_radix_bits_for_config::<C>();
89        let packed = self
90            .f_buffer
91            .chunks(self.absorb_num_f_elms)
92            .map(|f_chunk| reduce_packed(builder, f_chunk, radix_bits))
93            .collect::<Vec<_>>();
94        self.absorb_rate_padded_with_tag(builder, &packed, n_in as u8);
95        self.f_buffer.clear();
96        self.f_squeeze_buffer.clear();
97    }
98
99    fn refill_f_squeeze_from_inner(&mut self, builder: &mut Builder<C>) {
100        self.f_squeeze_buffer.clear();
101        for &pf_val in &self.inner_output_buffer {
102            let f_vals = split_pf_to_field_order_limbs(builder, pf_val, self.squeeze_num_f_elms);
103            self.f_squeeze_buffer.extend(f_vals);
104        }
105        // Match `DuplexChallenger` semantics: squeezing consumes the current rate row.
106        self.inner_output_buffer.clear();
107    }
108
109    pub fn observe(&mut self, builder: &mut Builder<C>, value: Felt<C::F>) {
110        self.inner_output_buffer.clear();
111        self.f_squeeze_buffer.clear();
112        self.f_buffer.push(value);
113        if self.f_buffer.len() == self.absorb_num_f_elms * RATE {
114            self.flush_f_if_non_empty(builder);
115        }
116    }
117
118    pub fn observe_commitment(&mut self, builder: &mut Builder<C>, value: OuterDigestVariable<C>) {
119        self.inner_output_buffer.clear();
120        self.f_squeeze_buffer.clear();
121        self.flush_f_if_non_empty(builder);
122
123        for chunk in value.chunks(RATE) {
124            self.absorb_rate_padded_with_tag(builder, chunk, chunk.len() as u8);
125            self.f_squeeze_buffer.clear();
126        }
127    }
128
129    pub fn sample(&mut self, builder: &mut Builder<C>) -> Felt<C::F> {
130        self.flush_f_if_non_empty(builder);
131        if self.f_squeeze_buffer.is_empty() {
132            // p3 also guards on `!inner.input_buffer.is_empty()`; that disjunct is vacuous here
133            // because no path on this wrapper writes into the inner `DuplexChallenger::observe`.
134            if self.inner_output_buffer.is_empty() {
135                self.duplexing(builder);
136            }
137            self.refill_f_squeeze_from_inner(builder);
138        }
139        self.f_squeeze_buffer
140            .pop()
141            .expect("output buffer should be non-empty")
142    }
143
144    pub fn sample_ext(&mut self, builder: &mut Builder<C>) -> Ext<C::F, C::EF> {
145        let a = self.sample(builder);
146        let b = self.sample(builder);
147        let c = self.sample(builder);
148        let d = self.sample(builder);
149        builder.felts2ext(&[a, b, c, d])
150    }
151
152    pub fn sample_bits(&mut self, builder: &mut Builder<C>, bits: usize) -> Var<C::N> {
153        let rand_f = self.sample(builder);
154        let rand_f_bits = builder.num2bits_f_circuit(rand_f);
155        builder.bits2num_v_circuit(&rand_f_bits[0..bits])
156    }
157
158    pub fn check_witness(&mut self, builder: &mut Builder<C>, bits: usize, witness: Felt<C::F>) {
159        if bits == 0 {
160            return;
161        }
162        self.observe(builder, witness);
163        let element = self.sample_bits(builder, bits);
164        builder.assert_var_eq(element, C::N::from_usize(0));
165    }
166}
167
168impl<C: Config> CanObserveVariable<C, Felt<C::F>> for MultiField32ChallengerVariable<C> {
169    fn observe(&mut self, builder: &mut Builder<C>, value: Felt<C::F>) {
170        MultiField32ChallengerVariable::observe(self, builder, value);
171    }
172
173    fn observe_slice(&mut self, builder: &mut Builder<C>, values: Array<C, Felt<C::F>>) {
174        values.vec().into_iter().for_each(|value| {
175            self.observe(builder, value);
176        });
177    }
178}
179
180impl<C: Config> CanSampleVariable<C, Felt<C::F>> for MultiField32ChallengerVariable<C> {
181    fn sample(&mut self, builder: &mut Builder<C>) -> Felt<C::F> {
182        MultiField32ChallengerVariable::sample(self, builder)
183    }
184}
185
186impl<C: Config> CanSampleBitsVariable<C> for MultiField32ChallengerVariable<C> {
187    fn sample_bits(
188        &mut self,
189        builder: &mut Builder<C>,
190        nb_bits: RVar<C::N>,
191    ) -> Array<C, Var<C::N>> {
192        let rand_f = self.sample(builder);
193        let rand_f_bits = builder.num2bits_f_circuit(rand_f);
194        builder.vec(rand_f_bits[..nb_bits.value()].to_vec())
195    }
196}
197
198impl<C: Config> CanObserveDigest<C> for MultiField32ChallengerVariable<C> {
199    fn observe_digest(&mut self, builder: &mut Builder<C>, commitment: DigestVariable<C>) {
200        if let DigestVariable::Var(v_commit) = commitment {
201            MultiField32ChallengerVariable::observe_commitment(
202                self,
203                builder,
204                v_commit.vec().try_into().unwrap(),
205            );
206        } else {
207            panic!("MultiField32ChallengerVariable expects Var commitment");
208        }
209    }
210}
211
212impl<C: Config> FeltChallenger<C> for MultiField32ChallengerVariable<C> {
213    fn sample_ext(&mut self, builder: &mut Builder<C>) -> Ext<C::F, C::EF> {
214        MultiField32ChallengerVariable::sample_ext(self, builder)
215    }
216}
217
218impl<C: Config> CanCheckWitness<C> for MultiField32ChallengerVariable<C> {
219    fn check_witness(&mut self, builder: &mut Builder<C>, nb_bits: usize, witness: Felt<C::F>) {
220        MultiField32ChallengerVariable::check_witness(self, builder, nb_bits, witness);
221    }
222}
223
224impl<C: Config> ChallengerVariable<C> for MultiField32ChallengerVariable<C> {
225    fn new(builder: &mut Builder<C>) -> Self {
226        MultiField32ChallengerVariable::new(builder)
227    }
228}
229// Testing depends on halo2. Put it inside src/halo2/tests/multi_field32.rs