Skip to main content

midnight_circuits/field/decomposition/
chip.rs

1// This file is part of MIDNIGHT-ZK.
2// Copyright (C) Midnight Foundation
3// SPDX-License-Identifier: Apache-2.0
4// Licensed under the Apache License, Version 2.0 (the "License");
5// You may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14//! The P2RDecomposition chip perfoming the in-circuit operations
15
16use std::{collections::HashMap, marker::PhantomData};
17
18use midnight_proofs::{
19    circuit::{Chip, Layouter, Value},
20    plonk::{ConstraintSystem, Error},
21};
22use num_traits::Zero;
23
24use super::{
25    cpu_utils::{compute_optimal_limb_sizes, process_limb_sizes},
26    instructions::CoreDecompositionInstructions,
27    pow2range::{Pow2RangeChip, Pow2RangeConfig},
28};
29use crate::{
30    field::{
31        decomposition::cpu_utils::{
32            decompose_in_variable_limbsizes, variable_limbsize_coefficients,
33        },
34        NativeChip, NativeConfig,
35    },
36    types::AssignedNative,
37    utils::ComposableChip,
38    CircuitField,
39};
40
41#[derive(Clone, Debug)]
42/// A decomposition config consists of a NativeConfig and a Pow2RangeConfig. It
43/// assumes that the chips share the lookup enabled columns.
44pub struct P2RDecompositionConfig {
45    pub(crate) native_config: NativeConfig,
46    pub(crate) pow2range_config: Pow2RangeConfig,
47}
48
49impl P2RDecompositionConfig {
50    /// Creates the config from the configs of a native and a pow2range chips.
51    ///
52    /// It assumes that the advice columns of the pow2range_chip are exactly
53    /// advice_cols[1..`ZkStdLibArch::nr_pow2range_cols`+1] of the
54    /// native chip.
55    ///
56    /// # Panics
57    ///
58    /// If the above condition does not hold.
59    pub fn new(native_config: &NativeConfig, pow2range_config: &Pow2RangeConfig) -> Self {
60        #[cfg(not(test))]
61        assert!(
62            native_config.value_cols[1..]
63                .iter()
64                .zip(pow2range_config.val_cols.iter())
65                .all(|(n_col, p2r_col)| n_col == p2r_col),
66            "DecompositionChip: Native and Pow2Range configs do not agree on the first {} columns",
67            pow2range_config.val_cols.len()
68        );
69        Self {
70            native_config: native_config.clone(),
71            pow2range_config: pow2range_config.clone(),
72        }
73    }
74
75    /// Returns the native config used for the decomposition config
76    pub fn native_config(&self) -> &NativeConfig {
77        &self.native_config
78    }
79
80    /// Returns the pow2range config used for the decomposition config
81    pub fn pow2range_config(&self) -> &Pow2RangeConfig {
82        &self.pow2range_config
83    }
84}
85
86#[derive(Clone, Debug)]
87/// A decomposition chip
88pub struct P2RDecompositionChip<F: CircuitField> {
89    // a hash map that contains the optimal (in number of rows) limb decomposition of a number
90    // that is a power of two. Check
91    // [compute_optimal_limb_sizes](cpu_utils::compute_optimal_limb_sizes) for more information on
92    // this hash map
93    opt_limbs: HashMap<i32, Vec<Vec<usize>>>,
94    config: P2RDecompositionConfig,
95    // the maximum limb length supported by the Pow2Range lookup
96    max_bit_len: usize,
97    native_chip: NativeChip<F>,
98    pow2range_chip: Pow2RangeChip<F>,
99    _marker: PhantomData<F>,
100}
101
102impl<F: CircuitField> Chip<F> for P2RDecompositionChip<F> {
103    type Config = P2RDecompositionConfig;
104    type Loaded = ();
105
106    fn config(&self) -> &Self::Config {
107        &self.config
108    }
109
110    fn loaded(&self) -> &Self::Loaded {
111        &()
112    }
113}
114
115impl<F: CircuitField> ComposableChip<F> for P2RDecompositionChip<F> {
116    type SharedResources = (NativeConfig, Pow2RangeConfig);
117
118    type InstructionDeps = usize;
119
120    /// Creates the chips given the two sub-chips it consists of
121    fn new(config: &Self::Config, &max_bit_len: &usize) -> Self {
122        let mut opt_limbs = HashMap::new();
123        // fill the HashMap. We do this here to avoid mutable references
124        // TODO: Consider hard coding this values
125        for bound in 0..=F::NUM_BITS {
126            compute_optimal_limb_sizes(
127                &mut opt_limbs,
128                config.pow2range_config.val_cols.len(),
129                max_bit_len,
130                bound as i32,
131            );
132        }
133
134        Self {
135            opt_limbs,
136            config: config.clone(),
137            max_bit_len,
138            native_chip: NativeChip::new(&config.native_config, &()),
139            pow2range_chip: Pow2RangeChip::new(&config.pow2range_config, max_bit_len),
140            _marker: PhantomData,
141        }
142    }
143
144    fn configure(
145        _meta: &mut ConstraintSystem<F>,
146        shared_resources: &Self::SharedResources,
147    ) -> Self::Config {
148        Self::Config {
149            native_config: shared_resources.0.clone(),
150            pow2range_config: shared_resources.1.clone(),
151        }
152    }
153
154    fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
155        self.pow2range_chip.load_table(layouter)
156    }
157}
158impl<F: CircuitField> P2RDecompositionChip<F> {
159    /// Gives direct access to the NativeChip
160    pub fn native_chip(&self) -> &NativeChip<F> {
161        &self.native_chip
162    }
163
164    /// Gives direct access to the Pow2RangeChip
165    pub fn pow2range_chip(&self) -> &Pow2RangeChip<F> {
166        &self.pow2range_chip
167    }
168}
169
170impl<F: CircuitField> P2RDecompositionChip<F> {
171    /// Takes a `Value<F>` and decomposes it in limbs according to the given
172    /// `limb_sizes`. It then assigns those limbs and range-checks them.
173    /// It also adds the limbs, returning an `AssignedNative<F>` encoding the
174    /// initial value, but guaranteed to be range-checked in the range induced
175    /// by the limbs.
176    ///
177    /// The assigned limbs are returned as a second argument, for testing
178    /// purposes only.
179    ///
180    /// More concretely, it decomposes a field element in variable limbs sizes:
181    /// given x in F and a slice `limb_sizes` that represents bit lengths,
182    /// it returns [a_1, ..., a_m] such that:
183    ///      (1) x = sum c_i a_i
184    ///      (2) 0 <= a_i < 2^{limbs_size{i}}
185    ///      (3) c_i = 2^{sum_{j=1}^{i-1} limb_sizes\[i\]}
186    ///             if limb_sizes\[i\]>0 and 0 otherwise.
187    /// This allows us to not restrict the corresponding a_i to equal 0
188    ///
189    ///  It assumes that the following about the limbs:
190    ///  - all limbs are all smaller than or equal to self.max_bit_len
191    ///  - the total number of limbs is a multiple of
192    ///    `ZkStdLibArch::nr_pow2range_cols`
193    ///  - each `ZkStdLibArch::nr_pow2range_cols` - chunk consists of a single
194    ///    limb_bit_size and possibly zeros, the latter coming always at the end
195    ///
196    ///  # Panics
197    ///
198    ///  - if the field element cannot be represented with limb_sizes
199    ///  - if the limb sizes are not correct.
200    pub(crate) fn decompose_core(
201        &self,
202        layouter: &mut impl Layouter<F>,
203        x: Value<F>,
204        limb_sizes: &[usize],
205    ) -> Result<(AssignedNative<F>, Vec<AssignedNative<F>>), Error> {
206        let nr_pow2range_cols = self.pow2range_chip.config().val_cols.len();
207
208        // assert limb_sizes structure is correct
209
210        // 1. max limb length is not bigger than max_bit_length of chip
211        #[cfg(not(test))]
212        assert!(
213            *limb_sizes.iter().max().unwrap_or(&0) <= self.max_bit_len,
214            "Decomposition chip: Try to use decompose_core with limb sizes greater than the supported max limb length",
215        );
216
217        // 2. the number of given limbs is multiple of ZkStdLibArch::nr_pow2range_cols
218        #[cfg(not(test))]
219        assert!(
220            limb_sizes.len().is_multiple_of(nr_pow2range_cols),
221            "Decomposition chip: number of limbs passed in decompose_core is not a multiple of ZkStdLibArch::nr_pow2range_cols",
222        );
223
224        // 3. each ZkStdLibArch::nr_pow2range_cols chunk is the same number and possibly
225        // some zeros
226        #[cfg(not(test))]
227        {
228            let limb_sizes_structure = limb_sizes.chunks(nr_pow2range_cols).all(|chunk| {
229                let mut v = chunk.to_vec();
230                v.sort();
231                v.dedup();
232                v.reverse(); // zeros at the end
233                             // length at most 2 ==> two possible limb sizes per chunk
234                let condition1 = v.len() <= 2;
235                // should not panic if length is 1 due to short_circuiting
236                let condition2 = (v.len() == 1) || (v[0] == 0) || (v[1] == 0);
237                // length is less than 2 AND if length is 2 then one of the elements should be
238                // zero
239                condition1 && condition2
240            });
241            assert!(
242                limb_sizes_structure,
243                "Decomposition chip: malformed limb sizes in decompose_core",
244            );
245        }
246
247        layouter.assign_region(
248            || "decompose core",
249            |mut region| {
250                let mut offset = 0;
251
252                // compute the range_check tags for each column
253                let tags = limb_sizes.chunks(nr_pow2range_cols).map(|x| x[0]).collect::<Vec<_>>();
254
255                // compute the linear combination terms, i.e. (coef, limb) pairs
256                // by convention the coefficient of a zero sized limb is 0 so no constraint
257                // needs to be imposed in the corresponding limb
258                let coefficients = variable_limbsize_coefficients::<F>(limb_sizes);
259                let limbs = x
260                    .map(|x_value| decompose_in_variable_limbsizes(&x_value, limb_sizes))
261                    .transpose_vec(limb_sizes.len());
262
263                // we create the terms for the linear combination.
264                let terms = coefficients.into_iter().zip(limbs.iter().copied()).collect::<Vec<_>>();
265
266                // assign terms for linear combination
267                let native_chip = self.native_chip();
268                let (assigned_limbs, assigned_result) = native_chip.assign_linear_combination_aux(
269                    &mut region,
270                    terms.as_slice(),
271                    F::ZERO,
272                    &x,
273                    nr_pow2range_cols,
274                    &mut offset,
275                )?;
276                offset += 1;
277
278                // enable the appropriate copy constraints in the rows where we assigned the
279                // linear combination terms
280                let pow2range_chip = self.pow2range_chip();
281
282                // we reverse since we do the range-checks from higher end to start
283                for (i, tag) in tags.into_iter().rev().enumerate() {
284                    let current_row = offset - (i + 1);
285                    pow2range_chip.assert_row_lower_than_2_pow_n(&mut region, tag, current_row)?;
286                }
287
288                // we remove the trivial coefficients, i.e. those corresponding to a zero size
289                // limb, and return
290                let limbs = assigned_limbs
291                    .into_iter()
292                    .zip(limb_sizes.iter())
293                    .filter(|(_assigned_limb, limb_size)| !limb_size.is_zero())
294                    .map(|(assigned_limb, _)| assigned_limb)
295                    .collect::<Vec<_>>();
296                Ok((assigned_result, limbs))
297            },
298        )
299    }
300}
301
302impl<F: CircuitField> CoreDecompositionInstructions<F> for P2RDecompositionChip<F> {
303    // TODO: This can be further optimized by parallelizing lookups for the
304    // decomposed limbs. Perhaps using a function for optimal decomposing
305    // multiple terms?
306    fn decompose_fixed_limb_size(
307        &self,
308        layouter: &mut impl Layouter<F>,
309        x: &AssignedNative<F>,
310        bit_length: usize,
311        limb_size: usize,
312    ) -> Result<Vec<AssignedNative<F>>, Error> {
313        // compute the limbs_sizes. The last limb size might be smaller than limb size
314        let number_of_limbs = bit_length / limb_size;
315        let last_limb_size = bit_length % limb_size;
316
317        let nr_pow2range_cols = self.pow2range_chip.config().val_cols.len();
318
319        // limb decomposition can be supported natively by the lookup table
320        if limb_size <= self.max_bit_len {
321            // prepare the limb_size slice by filling with zeros to do parallel lookups
322            let mut limb_sizes = vec![limb_size; number_of_limbs];
323            process_limb_sizes(nr_pow2range_cols, &mut limb_sizes);
324            // prepare the limb sizes for last (possibly smaller limb). This is either empty
325            // or contains exactly ZkStdLibArch::nr_pow2range_cols elements where the last
326            // ZkStdLibArch::nr_pow2range_cols-1 are 0s
327            if last_limb_size != 0 {
328                limb_sizes.push(last_limb_size);
329                process_limb_sizes(nr_pow2range_cols, &mut limb_sizes);
330            }
331
332            // we call the core function to retrieve the result
333            let (y, assigned_limbs) = self.decompose_core(
334                &mut layouter.namespace(|| "decompose fixed"),
335                x.value().copied(),
336                limb_sizes.as_slice(),
337            )?;
338
339            layouter.assign_region(
340                || "copy",
341                |mut region| region.constrain_equal(x.cell(), y.cell()),
342            )?;
343
344            Ok(assigned_limbs)
345        }
346        // limbs should be further range-checked since they are larger than the supported lookup
347        // bounds
348        else {
349            // prepare the limb_size slice by filling with zeros to do parallel lookups
350            let mut limb_sizes = vec![limb_size; number_of_limbs];
351
352            // if the last limb is non-zero sized add it to the limb sizes
353            if last_limb_size != 0 {
354                limb_sizes.push(last_limb_size);
355            }
356
357            let assigned_limbs = layouter.assign_region(
358                || "assign limbs decompose fixed",
359                |mut region| {
360                    let mut offset = 0;
361
362                    // compue the linear combination terms, i.e. (coef, limb) pairs
363                    let coefficients = variable_limbsize_coefficients::<F>(limb_sizes.as_slice());
364                    let limbs = x
365                        .value()
366                        .map(|x_value| {
367                            decompose_in_variable_limbsizes(x_value, limb_sizes.as_slice())
368                        })
369                        .transpose_vec(limb_sizes.len());
370
371                    // we create the terms for the linear combination
372                    let terms =
373                        coefficients.into_iter().zip(limbs.iter().copied()).collect::<Vec<_>>();
374
375                    // assign terms for linear combination to assert correct decomposition
376                    let (assigned_limbs, assigned_result) =
377                        self.native_chip().assign_linear_combination_aux(
378                            &mut region,
379                            terms.as_slice(),
380                            F::ZERO,
381                            &x.value().copied(),
382                            nr_pow2range_cols,
383                            &mut offset,
384                        )?;
385
386                    // copy constraint the linear combination result to equal the decomposed value
387                    region.constrain_equal(assigned_result.cell(), x.cell())?;
388
389                    Ok(assigned_limbs)
390                },
391            )?;
392
393            // range check assigned limbs and return them
394            assigned_limbs
395                .iter()
396                .zip(limb_sizes.iter())
397                .try_for_each(|(x_limb, &limb_size)| {
398                    self.assert_less_than_pow2(layouter, x_limb, limb_size)
399                })?;
400
401            Ok(assigned_limbs)
402        }
403    }
404
405    fn assign_less_than_pow2(
406        &self,
407        layouter: &mut impl Layouter<F>,
408        value: Value<F>,
409        bit_length: usize,
410    ) -> Result<AssignedNative<F>, Error> {
411        #[cfg(not(test))]
412        assert!((bit_length as u32) < F::NUM_BITS);
413
414        // 1. get the limb sizes that minimize the number of parallel lookup rangechecks
415        // should never panic since the HashMap contains all possible solutions
416        let mut optimal_limb_sizes = self.opt_limbs.get(&(bit_length as i32)).unwrap().clone();
417
418        // 2. process them by adding 0 terms in non-full rows
419        optimal_limb_sizes
420            .iter_mut()
421            .for_each(|row| process_limb_sizes(self.pow2range_chip.config().val_cols.len(), row));
422        let limb_sizes = optimal_limb_sizes.concat();
423
424        // 3. use decompose_core to compute the result
425        let (y, _) = self.decompose_core(layouter, value, &limb_sizes)?;
426
427        Ok(y)
428    }
429
430    fn assign_many_small(
431        &self,
432        layouter: &mut impl Layouter<F>,
433        values: &[Value<F>],
434        bit_length: usize,
435    ) -> Result<Vec<AssignedNative<F>>, Error> {
436        assert!(8 < F::NUM_BITS);
437        assert!(
438            bit_length <= 8,
439            "assign_many_small expects a bit_length <= 8"
440        );
441
442        let mut assigned_values = Vec::with_capacity(values.len());
443        for chunk in values.chunks(self.pow2range_chip.config().val_cols.len()) {
444            let assigned_chunk: Vec<_> = layouter.assign_region(
445                || "assign_many_small",
446                |mut region| {
447                    let mut padded_chunk = chunk.to_vec();
448                    padded_chunk.resize(
449                        self.pow2range_chip.config().val_cols.len(),
450                        Value::known(F::ZERO),
451                    );
452
453                    self.pow2range_chip().assert_row_lower_than_2_pow_n(
454                        &mut region,
455                        bit_length,
456                        0,
457                    )?;
458
459                    padded_chunk
460                        .iter()
461                        .zip(self.config.pow2range_config.val_cols.iter())
462                        .map(|(value, col)| {
463                            region.assign_advice(|| "assign small", *col, 0, || *value)
464                        })
465                        .collect()
466                },
467            )?;
468            assigned_values.extend_from_slice(&assigned_chunk[..chunk.len()]);
469        }
470        Ok(assigned_values)
471    }
472}