Skip to content

Commit

Permalink
Merge pull request privacy-scaling-explorations#123 from input-output…
Browse files Browse the repository at this point in the history
…-hk/dev-feature/add-endoscale-method

Add method to prepare challenges for endoscaling
  • Loading branch information
b13decker authored Feb 27, 2024
2 parents b812bdb + 19531f3 commit 41c586b
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 98 deletions.
4 changes: 2 additions & 2 deletions halo2_gadgets/benches/endoscale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ where
config: Self::Config,
mut layouter: impl Layouter<C::Base>,
) -> Result<(), Error> {
config.0.alg_2.table.load(&mut layouter)?;
config.0.alg_2().table.load(&mut layouter)?;

let bitstring =
config
Expand Down Expand Up @@ -181,7 +181,7 @@ where
config: Self::Config,
mut layouter: impl Layouter<C::Base>,
) -> Result<(), Error> {
config.alg_2.table.load(&mut layouter)?;
config.alg_2().table.load(&mut layouter)?;

let bitstring =
config.witness_bitstring(&mut layouter, &self.bitstring.transpose_array(), false)?;
Expand Down
68 changes: 7 additions & 61 deletions halo2_gadgets/examples/endoscaling_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,13 @@
use std::marker::PhantomData;

use ff::{Field, PrimeField};
use ff::Field;
use group::{prime::PrimeCurveAffine, Curve};
use halo2_gadgets::{
ecc::chip::NonIdentityEccPoint,
endoscale::{
chip::{Bitstring, EndoscaleConfig},
EndoscaleInstructions,
},
endoscale::{chip::EndoscaleConfig, EndoscaleInstructions},
poseidon::{Pow5Chip, Pow5Config},
transcript::{chip::TranscriptChipP128Pow5T3, TranscriptInstructions},
utilities::decompose_running_sum::RunningSumConfig,
};
use halo2_proofs::{
circuit::{Layouter, Region, SimpleFloorPlanner, Value},
Expand Down Expand Up @@ -236,12 +232,6 @@ struct ChallengeMultiplicationCircuit {
secret_input: Value<pallas::Affine>,
}

/// Window size for a running sum used in endoscaling's algorithm 1.
///
/// This will always be 2! The program won't compile if it's set to anything else.
/// Algorithm 1 requires a bitstring as input that is divided into pairs.
const RUNNING_SUM_WINDOW_SIZE: usize = 2;

#[derive(Debug, Clone)]
struct ChallengeMultiplicationConfig {
silly_equality_config: SillyEqualityConfig<pallas::Base>,
Expand All @@ -252,12 +242,6 @@ struct ChallengeMultiplicationConfig {
/// Contains the expected output of the transcript squeeze (used for testing).
outputs: Column<Instance>,

/// Configuration for breaking down a challenge into a set of pairs of bits.
/// - First parameter is the type of the field over which the circuit operates.
/// - Second parameter defines the number of bits per window, where we intend to break the
/// input into a bitstring grouped into windows. This should always be 2 for endoscaling.
running_sum_config: RunningSumConfig<pallas::Base, RUNNING_SUM_WINDOW_SIZE>,

/// Configuration for the endoscaling bit of the circuit.
/// Endoscaling provides two different algorithms: Alg 1 actually computes endoscaling, and
/// Alg 2 computes the scalar value from a challenge that's equivalent to replacing
Expand Down Expand Up @@ -301,8 +285,6 @@ impl Circuit<pallas::Base> for ChallengeMultiplicationCircuit {
.collect::<Vec<_>>();
meta.enable_constant(fixeds[0]);

let selector = meta.selector();

// Set up the configuration for poseidon + transcripts.
let pow5 = Pow5Chip::configure::<P128Pow5T3>(
meta,
Expand All @@ -329,9 +311,6 @@ impl Circuit<pallas::Base> for ChallengeMultiplicationCircuit {
.expect("won't fail because length is hardcoded"),
);

// Set up config for the running sum needed to configure the challenge for endoscaling
let running_sum_config = RunningSumConfig::configure(meta, selector, advices[0]);

// Set up config for endoscaling
let endoscale_config = EndoscaleConfig::configure(
meta,
Expand All @@ -346,7 +325,6 @@ impl Circuit<pallas::Base> for ChallengeMultiplicationCircuit {
silly_equality_config,
pow5,
outputs: instance,
running_sum_config,
endoscale_config,
}
}
Expand Down Expand Up @@ -426,43 +404,11 @@ impl Circuit<pallas::Base> for ChallengeMultiplicationCircuit {
let challenge = chip.squeeze_challenge(layouter.namespace(|| "squeeze challenge"))?;
layouter.constrain_instance(challenge.cell(), config.outputs, 0)?;

// Use the running sum gadget to convert the challenge to a bitstring
let bitstring = layouter.assign_region(
|| "decompose challenge into bits",
|mut region| {
// It's necessary to strictly contstrain the running sum to be the correct size!
// Otherwise a malicious prover can construct an invalid decomposition.
//
// Future work: This section should also constrain the decomposed running sum to
// be in canonical form. Without this check, a malicious prover gets a small
// amount of freedom to choose the representation here, which we generally don't
// want to provide to them.
let strict = true;

// Offset is 0 because we're only doing one thing in this region, the decomposition.
let offset = 0;

// This literally refers to the number of bits in the challenge we're passing.
// It would need to be modified if you use this gadget in an arbitrary field.
let word_num_bits = pallas::Base::NUM_BITS as usize;

// Number of windows; this refers to the number of bits in the challenge
// divided by the decomposed-window size.
let num_windows = (word_num_bits + 1) / RUNNING_SUM_WINDOW_SIZE;

config
.running_sum_config
.copy_decompose(
&mut region,
offset,
challenge.clone(),
strict,
word_num_bits,
num_windows,
)
.map(Bitstring::Pair)
},
)?;
// Use the endoscale config to convert that challenge into a bitstring that can be used
// with algorithm 1.
let bitstring = config
.endoscale_config
.convert_to_bitstring(&mut layouter, &challenge)?;

// Endoscale two points with algorithm 1. We used `var_base` because the "base" point -- our
// `known_point` -- isn't known to both parties.
Expand Down
8 changes: 8 additions & 0 deletions halo2_gadgets/src/endoscale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ where
for_base: bool,
) -> Result<Vec<Self::Bitstring>, Error>;

/// Transform an assigned cell, which is typically a random challenge generated from a
/// transcript, into a [`Self::Bitstring`] suitable for use in endoscaling.
fn convert_to_bitstring(
&self,
layouter: &mut impl Layouter<C::Base>,
challenge: &AssignedCell<C::Base, C::Base>,
) -> Result<Self::Bitstring, Error>;

/// Computes commitment (Alg 1) to a variable-length bitstring using the endoscaling
/// algorithm. Uses the fixed bases defined in [`Self::FixedBases`].
///
Expand Down
118 changes: 86 additions & 32 deletions halo2_gadgets/src/endoscale/chip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use halo2_proofs::{
plonk::{Advice, Assigned, Column, ConstraintSystem, Error, Instance},
};
use halo2curves::{pasta, pluto_eris};
use std::iter::zip;

mod alg_1;
mod alg_2;
Expand All @@ -17,12 +18,22 @@ use alg_1::Alg1Config;
use alg_2::Alg2Config;

/// Bitstring used in endoscaling.
///
/// It's recommended to endoscale with a bitstring of length at least 160 but this is not
/// enforced by this type.
#[derive(Clone, Debug)]
#[allow(clippy::type_complexity)]
pub enum Bitstring<F: PrimeFieldBits, const K: usize> {
/// TODO: docs
/// A bitstring broken into 2-bit windows for use in "algorithm 1".
///
/// Note: the constant parameter `K` is not used with this variant; we recommend setting it to
/// 0 if the use case does not involve constructing `KBit` variants to avoid unnecessary work.
Pair(alg_1::Bitstring<F>),
/// TODO: docs

/// A bitstring broken into `K`-bit windows for use in "algorithm 2".
///
/// The algorithm uses a lookup table to compute the scalar for each window, then combines
/// those values into the actual "endoscalar". A larger `K` value means the lookup table will
/// be larger but reduces the cost of the recombination step.
KBit(alg_2::Bitstring<F, K>),
}

Expand All @@ -32,20 +43,33 @@ pub struct EndoscaleConfig<C: CurveAffine, const K: usize, const MAX_BITSTRING_L
where
C::Base: PrimeFieldBits,
{
/// TODO: docs
pub alg_1: Alg1Config<C>,
/// TODO: docs
pub alg_2: Alg2Config<C, K, MAX_BITSTRING_LENGTH>,
/// Configuration for algorithm 1, primarily used to execute endoscaling of a curve point with
/// a challenge, encoded as a [`Bitstring::Pair`].
alg_1: Alg1Config<C>,

/// Configuration for algorithm 2, primarily used to compute the "endoscalar".
///
/// That is, given a bitstring, this computes the scalar value that, when scalar-multiplied
/// by a curve point, provides the same result as endoscaling with the bitstring.
alg_2: Alg2Config<C, K, MAX_BITSTRING_LENGTH>,
}

impl<C: CurveAffine, const K: usize, const MAX_BITSTRING_LENGTH: usize>
EndoscaleConfig<C, K, MAX_BITSTRING_LENGTH>
where
C::Base: PrimeFieldBits,
{
/// Get the config for algorithm 1.
pub fn alg_1(&self) -> &Alg1Config<C> {
&self.alg_1
}

/// Get the config for algorithm 2 .
pub fn alg_2(&self) -> &Alg2Config<C, K, MAX_BITSTRING_LENGTH> {
&self.alg_2
}

/// TODO: docs
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
pub fn configure(
meta: &mut ConstraintSystem<C::Base>,
// Advice columns not shared across alg_1 and alg_2
Expand Down Expand Up @@ -140,23 +164,42 @@ where
.collect()
}

#[allow(clippy::type_complexity)]
fn convert_to_bitstring(
&self,
layouter: &mut impl Layouter<C::Base>,
challenge: &AssignedCell<C::Base, C::Base>,
) -> Result<Self::Bitstring, Error> {
// NB: This outputs a `Bitstring::Pair` variant compatible with Alg 1 only.
// There is currently no equivalent method for Alg 2.
self.alg_1
.convert_to_bitstring(
layouter.namespace(|| "alg 1: convert challenge to bitstring"),
challenge,
)
.map(Bitstring::Pair)
}

fn endoscale_fixed_base(
&self,
layouter: &mut impl Layouter<C::Base>,
bitstring: Vec<Self::Bitstring>,
bases: Vec<Self::FixedBases>,
) -> Result<Vec<Self::NonIdentityPoint>, Error> {
let mut points = Vec::new();
for (bitstring, base) in bitstring.iter().zip(bases.iter()) {
match bitstring {
Bitstring::Pair(bitstring) => {
points.push(self.alg_1.endoscale_fixed_base(layouter, bitstring, base)?)
}
_ => unreachable!(),
}
if bitstring.len() != bases.len() {
Err(Error::Synthesis)?
}
Ok(points)

zip(bitstring, bases)
.map(|(bitstring, base)| match bitstring {
Bitstring::Pair(bitstring) => self
.alg_1()
.endoscale_fixed_base(layouter, &bitstring, &base),
// Endoscaling only makes sense when the input is the `Bitstring::Pair` type.
// Otherwise you've prepared the input for computing the _endoscalar_ and should
// use the `compute_endoscalar` or `constrain_bitstring` methods.
_ => Err(Error::Synthesis),
})
.collect()
}

fn endoscale_var_base(
Expand All @@ -165,16 +208,21 @@ where
bitstring: Vec<Self::Bitstring>,
bases: Vec<Self::NonIdentityPoint>,
) -> Result<Vec<Self::NonIdentityPoint>, Error> {
let mut points = Vec::new();
for (bitstring, base) in bitstring.iter().zip(bases.iter()) {
match bitstring {
if bitstring.len() != bases.len() {
Err(Error::Synthesis)?
}

zip(bitstring, bases)
.map(|(bitstring, base)| match bitstring {
Bitstring::Pair(bitstring) => {
points.push(self.alg_1.endoscale_var_base(layouter, bitstring, base)?)
self.alg_1().endoscale_var_base(layouter, &bitstring, &base)
}
_ => unreachable!(),
}
}
Ok(points)
// Endoscaling only makes sense when the input is the `Bitstring::Pair` type.
// Otherwise you've prepared the input for computing the _endoscalar_ and should
// use the `compute_endoscalar` or `constrain_bitstring` methods.
_ => Err(Error::Synthesis),
})
.collect()
}

fn compute_endoscalar(
Expand All @@ -183,8 +231,11 @@ where
bitstring: &Self::Bitstring,
) -> Result<AssignedCell<Assigned<C::Base>, C::Base>, Error> {
match bitstring {
Bitstring::KBit(bitstring) => self.alg_2.compute_endoscalar(layouter, bitstring),
_ => unreachable!(),
Bitstring::KBit(bitstring) => self.alg_2().compute_endoscalar(layouter, bitstring),
// Computing the endoscalar only makes sense when the input is the `Bitstring::Kbit`
// type. Otherwise you've prepared the input for _endoscaling_ and should use the
// `endoscale_{var | fixed}_base` methods.
_ => Err(Error::Synthesis),
}
}

Expand All @@ -199,7 +250,10 @@ where
self.alg_2
.constrain_bitstring(layouter, bitstring, pub_input_rows)
}
_ => unreachable!(),
// Constraining the bitstring only makes sense when the input is the `Bitstring::Kbit`
// type. Otherwise you've prepared the input for _endoscaling_ and should use the
// `endoscale_{var | fixed}_base` methods.
_ => Err(Error::Synthesis),
}
}
}
Expand Down Expand Up @@ -286,7 +340,7 @@ mod tests {
config: Self::Config,
mut layouter: impl Layouter<C::Base>,
) -> Result<(), Error> {
config.0.alg_2.table.load(&mut layouter)?;
config.0.alg_2().table.load(&mut layouter)?;

let bitstring = config.0.witness_bitstring(
&mut layouter,
Expand Down Expand Up @@ -389,7 +443,7 @@ mod tests {
config: Self::Config,
mut layouter: impl Layouter<C::Base>,
) -> Result<(), Error> {
config.alg_2.table.load(&mut layouter)?;
config.alg_2().table.load(&mut layouter)?;

let bitstring = config.witness_bitstring(
&mut layouter,
Expand Down
Loading

0 comments on commit 41c586b

Please sign in to comment.