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}