//! Decomposes an $n$-bit field element $\alpha$ into $W$ windows, each window //! being a $K$-bit word, using a running sum $z$. //! We constrain $K \leq 3$ for this helper. //! $$\alpha = k_0 + (2^K) k_1 + (2^{2K}) k_2 + ... + (2^{(W-1)K}) k_{W-1}$$ //! //! $z_0$ is initialized as $\alpha$. Each successive $z_{i+1}$ is computed as //! $$z_{i+1} = (z_{i} - k_i) / (2^K).$$ //! $z_W$ is constrained to be zero. //! The difference between each interstitial running sum output is constrained //! to be $K$ bits, i.e. //! `range_check`($k_i$, $2^K$), //! where //! ```text //! range_check(word, range) //! = word * (1 - word) * (2 - word) * ... * ((range - 1) - word) //! ``` //! //! Given that the `range_check` constraint will be toggled by a selector, in //! practice we will have a `selector * range_check(word, range)` expression //! of degree `range + 1`. //! //! This means that $2^K$ has to be at most `degree_bound - 1` in order for //! the range check constraint to stay within the degree bound. use ff::PrimeFieldBits; use halo2_proofs::{ circuit::{AssignedCell, Region, Value}, plonk::{Advice, Column, ConstraintSystem, Constraints, Error, Selector}, poly::Rotation, }; use super::range_check; use std::marker::PhantomData; /// The running sum $[z_0, ..., z_W]$. If created in strict mode, $z_W = 0$. #[derive(Debug)] pub struct RunningSum(Vec>); impl std::ops::Deref for RunningSum { type Target = Vec>; fn deref(&self) -> &Vec> { &self.0 } } /// Configuration that provides methods for running sum decomposition. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct RunningSumConfig { q_range_check: Selector, z: Column, _marker: PhantomData, } impl RunningSumConfig { /// Returns the q_range_check selector of this [`RunningSumConfig`]. pub(crate) fn q_range_check(&self) -> Selector { self.q_range_check } /// `perm` MUST include the advice column `z`. /// /// # Panics /// /// Panics if WINDOW_NUM_BITS > 3. /// /// # Side-effects /// /// `z` will be equality-enabled. pub fn configure( meta: &mut ConstraintSystem, q_range_check: Selector, z: Column, ) -> Self { assert!(WINDOW_NUM_BITS <= 3); meta.enable_equality(z); let config = Self { q_range_check, z, _marker: PhantomData, }; // https://p.z.cash/halo2-0.1:decompose-short-range meta.create_gate("range check", |meta| { let q_range_check = meta.query_selector(config.q_range_check); let z_cur = meta.query_advice(config.z, Rotation::cur()); let z_next = meta.query_advice(config.z, Rotation::next()); // z_i = 2^{K}⋅z_{i + 1} + k_i // => k_i = z_i - 2^{K}⋅z_{i + 1} let word = z_cur - z_next * F::from(1 << WINDOW_NUM_BITS); Constraints::with_selector(q_range_check, Some(range_check(word, 1 << WINDOW_NUM_BITS))) }); config } /// Decompose a field element alpha that is witnessed in this helper. /// /// `strict` = true constrains the final running sum to be zero, i.e. /// constrains alpha to be within WINDOW_NUM_BITS * num_windows bits. pub fn witness_decompose( &self, region: &mut Region<'_, F>, offset: usize, alpha: Value, strict: bool, word_num_bits: usize, num_windows: usize, ) -> Result, Error> { let z_0 = region.assign_advice(|| "z_0 = alpha", self.z, offset, || alpha)?; self.decompose(region, offset, z_0, strict, word_num_bits, num_windows) } /// Decompose an existing variable alpha that is copied into this helper. /// /// `strict` = true constrains the final running sum to be zero, i.e. /// constrains alpha to be within WINDOW_NUM_BITS * num_windows bits. pub fn copy_decompose( &self, region: &mut Region<'_, F>, offset: usize, alpha: AssignedCell, strict: bool, word_num_bits: usize, num_windows: usize, ) -> Result, Error> { let z_0 = alpha.copy_advice(|| "copy z_0 = alpha", region, self.z, offset)?; self.decompose(region, offset, z_0, strict, word_num_bits, num_windows) } /// `z_0` must be the cell at `(self.z, offset)` in `region`. /// /// # Panics /// /// Panics if there are too many windows for the given word size. fn decompose( &self, region: &mut Region<'_, F>, offset: usize, z_0: AssignedCell, strict: bool, word_num_bits: usize, num_windows: usize, ) -> Result, Error> { // Make sure that we do not have more windows than required for the number // of bits in the word. In other words, every window must contain at least // one bit of the word (no empty windows). // // For example, let: // - word_num_bits = 64 // - WINDOW_NUM_BITS = 3 // In this case, the maximum allowed num_windows is 22: // 3 * 22 < 64 + 3 // assert!(WINDOW_NUM_BITS * num_windows < word_num_bits + WINDOW_NUM_BITS); // Enable selectors for idx in 0..num_windows { self.q_range_check.enable(region, offset + idx)?; } // Decompose base field element into K-bit words. let words = z_0 .value() .map(|word| super::decompose_word::(word, word_num_bits, WINDOW_NUM_BITS)) .transpose_vec(num_windows); // Initialize empty vector to store running sum values [z_0, ..., z_W]. let mut zs: Vec> = vec![z_0.clone()]; let mut z = z_0; // Assign running sum `z_{i+1}` = (z_i - k_i) / (2^K) for i = 0..=n-1. // Outside of this helper, z_0 = alpha must have already been loaded into the // `z` column at `offset`. let two_pow_k_inv = Value::known(F::from(1 << WINDOW_NUM_BITS as u64).invert().unwrap()); for (i, word) in words.iter().enumerate() { // z_next = (z_cur - word) / (2^K) let z_next = { let z_cur_val = z.value().copied(); let word = word.map(|word| F::from(word as u64)); let z_next_val = (z_cur_val - word) * two_pow_k_inv; region.assign_advice( || format!("z_{:?}", i + 1), self.z, offset + i + 1, || z_next_val, )? }; // Update `z`. z = z_next; zs.push(z.clone()); } assert_eq!(zs.len(), num_windows + 1); if strict { // Constrain the final running sum output to be zero. region.constrain_constant(zs.last().unwrap().cell(), F::ZERO)?; } Ok(RunningSum(zs)) } } #[cfg(test)] mod tests { use super::*; use group::ff::{Field, PrimeField}; use halo2_proofs::{ circuit::{Layouter, SimpleFloorPlanner}, dev::{FailureLocation, MockProver, VerifyFailure}, plonk::{Any, Circuit, ConstraintSystem, Error}, }; use pasta_curves::pallas; use rand::rngs::OsRng; use crate::ecc::chip::{ FIXED_BASE_WINDOW_SIZE, L_SCALAR_SHORT as L_SHORT, NUM_WINDOWS, NUM_WINDOWS_SHORT, }; const L_BASE: usize = pallas::Base::NUM_BITS as usize; #[test] fn test_running_sum() { struct MyCircuit< F: PrimeFieldBits, const WORD_NUM_BITS: usize, const WINDOW_NUM_BITS: usize, const NUM_WINDOWS: usize, > { alpha: Value, strict: bool, } impl< F: PrimeFieldBits, const WORD_NUM_BITS: usize, const WINDOW_NUM_BITS: usize, const NUM_WINDOWS: usize, > Circuit for MyCircuit { type Config = RunningSumConfig; type FloorPlanner = SimpleFloorPlanner; fn without_witnesses(&self) -> Self { Self { alpha: Value::unknown(), strict: self.strict, } } fn configure(meta: &mut ConstraintSystem) -> Self::Config { let z = meta.advice_column(); let q_range_check = meta.selector(); let constants = meta.fixed_column(); meta.enable_constant(constants); RunningSumConfig::::configure(meta, q_range_check, z) } fn synthesize( &self, config: Self::Config, mut layouter: impl Layouter, ) -> Result<(), Error> { layouter.assign_region( || "decompose", |mut region| { let offset = 0; let zs = config.witness_decompose( &mut region, offset, self.alpha, self.strict, WORD_NUM_BITS, NUM_WINDOWS, )?; let alpha = zs[0].clone(); let offset = offset + NUM_WINDOWS + 1; config.copy_decompose( &mut region, offset, alpha, self.strict, WORD_NUM_BITS, NUM_WINDOWS, )?; Ok(()) }, ) } } // Random base field element { let alpha = pallas::Base::random(OsRng); // Strict full decomposition should pass. let circuit: MyCircuit = MyCircuit { alpha: Value::known(alpha), strict: true, }; let prover = MockProver::::run(8, &circuit, vec![]).unwrap(); assert_eq!(prover.verify(), Ok(())); } // Random 64-bit word { let alpha = pallas::Base::from(rand::random::()); // Strict full decomposition should pass. let circuit: MyCircuit< pallas::Base, L_SHORT, FIXED_BASE_WINDOW_SIZE, { NUM_WINDOWS_SHORT }, > = MyCircuit { alpha: Value::known(alpha), strict: true, }; let prover = MockProver::::run(8, &circuit, vec![]).unwrap(); assert_eq!(prover.verify(), Ok(())); } // 2^66 { let alpha = pallas::Base::from_u128(1 << 66); // Strict partial decomposition should fail. let circuit: MyCircuit< pallas::Base, L_SHORT, FIXED_BASE_WINDOW_SIZE, { NUM_WINDOWS_SHORT }, > = MyCircuit { alpha: Value::known(alpha), strict: true, }; let prover = MockProver::::run(8, &circuit, vec![]).unwrap(); assert_eq!( prover.verify(), Err(vec![ VerifyFailure::Permutation { column: (Any::Fixed, 0).into(), location: FailureLocation::OutsideRegion { row: 0 }, }, VerifyFailure::Permutation { column: (Any::Fixed, 0).into(), location: FailureLocation::OutsideRegion { row: 1 }, }, VerifyFailure::Permutation { column: (Any::Advice, 0).into(), location: FailureLocation::InRegion { region: (0, "decompose").into(), offset: 22, }, }, VerifyFailure::Permutation { column: (Any::Advice, 0).into(), location: FailureLocation::InRegion { region: (0, "decompose").into(), offset: 45, }, }, ]) ); // Non-strict partial decomposition should pass. let circuit: MyCircuit< pallas::Base, { L_SHORT }, FIXED_BASE_WINDOW_SIZE, { NUM_WINDOWS_SHORT }, > = MyCircuit { alpha: Value::known(alpha), strict: false, }; let prover = MockProver::::run(8, &circuit, vec![]).unwrap(); assert_eq!(prover.verify(), Ok(())); } } }