Skip to content

Commit

Permalink
Merge pull request privacy-scaling-explorations#132 from input-output…
Browse files Browse the repository at this point in the history
…-hk/dev-test/endoscalar-bitstring-extraction

Add tests comparing in- and out-of-circuit endoscaling methods
  • Loading branch information
b13decker authored Feb 27, 2024
2 parents cb0d9ff + 56706bb commit 1b01f78
Show file tree
Hide file tree
Showing 2 changed files with 354 additions and 10 deletions.
356 changes: 353 additions & 3 deletions halo2_gadgets/src/endoscale/chip/alg_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,11 +581,361 @@ impl<F: PrimeFieldBits> Bitstring<F> {
.collect()
}

/// Produces an even-length bool array, the out-of-circuit equivalent to the `Bitstring`.
#[cfg(test)]
fn bitstring(&self) -> Value<Vec<bool>> {
let num_bits = self.num_bits();
self.zs()[0]
.value()
.map(|v| v.to_le_bits().iter().by_vals().take(num_bits).collect())
self.zs()[0].value().map(|v| {
v.to_le_bits()
.iter()
.by_vals()
.take(num_bits)
// If there are an odd number of meaningful bits, add padding
.chain((num_bits % 2 == 1).then_some(false))
.collect()
})
}
}

#[cfg(test)]
mod tests {
use ff::PrimeFieldBits;
use group::{Curve, Group};
use halo2_proofs::dev::MockProver;
use halo2_proofs::transcript::endoscale::{compute_endoscalar, Setting};
use halo2_proofs::transcript::{EncodedChallenge, EndoscalarChallenge};
use halo2curves::pasta::pallas;
use halo2curves::CurveAffine;
use rand::thread_rng;

use halo2_proofs::{
circuit::{Layouter, SimpleFloorPlanner, Value},
plonk::Circuit,
};

use super::*;

/// Helper method to assign a curve point `C` into a circuit; this places the x and y
/// coordinates on top of each other in the advice column.
fn assign_base_point<C: CurveAffine>(
layouter: &mut impl Layouter<C::Base>,
base_point: Value<C>,
config: &EndoscaleTestConfig<C>,
) -> Result<NonIdentityEccPoint<C>, Error>
where
C::Base: PrimeFieldBits,
{
// Convert base curve point to the expected in-circuit type (NonIdentityEccPoint).
layouter.assign_region(
|| "convert base point to NIEP",
|mut region| {
let (x, y): (Value<Assigned<_>>, Value<Assigned<_>>) = base_point
.map(|point| {
let coords = point.coordinates().unwrap();
(coords.x().into(), coords.y().into())
})
.unzip();

let x = region.assign_advice(|| "x", config.advice_column, 0, || x)?;
let y = region.assign_advice(|| "y", config.advice_column, 1, || y)?;

Ok(NonIdentityEccPoint::<C>::from_coordinates_unchecked(x, y))
},
)
}

/// Helper method to assign a challenge, which is a base field element, into a circuit.
fn assign_challenge<C: CurveAffine>(
layouter: &mut impl Layouter<C::Base>,
challenge: Value<C::Base>,
config: &EndoscaleTestConfig<C>,
) -> Result<AssignedCell<C::Base, C::Base>, Error>
where
C::Base: PrimeFieldBits,
{
layouter.assign_region(
|| "assign_challenge",
|mut region| {
let offset = 0;
region.assign_advice(|| "bitstring", config.advice_column, offset, || challenge)
},
)
}

#[derive(Clone)]
struct EndoscaleTestConfig<C>
where
C: CurveAffine,
C::Base: PrimeFieldBits,
{
config: Alg1Config<C>,
advice_column: Column<Advice>,
}

#[derive(Clone)]
struct CompareEndoscalingCircuit<C: CurveAffine>
where
C::Base: PrimeFieldBits,
{
challenge: Value<C::Base>,
base_point: Value<C>,
}

impl<C: CurveAffine> Circuit<C::Base> for CompareEndoscalingCircuit<C>
where
C::Base: PrimeFieldBits,
{
type Config = EndoscaleTestConfig<C>;
type FloorPlanner = SimpleFloorPlanner;
#[cfg(feature = "circuit-params")]
type Params = ();

fn without_witnesses(&self) -> Self {
Self {
challenge: Value::unknown(),
base_point: Value::unknown(),
}
}

fn configure(meta: &mut ConstraintSystem<C::Base>) -> Self::Config {
let constants = meta.fixed_column();
meta.enable_constant(constants);

let advices: [Column<Advice>; 8] = (0..8)
.map(|_| meta.advice_column())
.collect::<Vec<_>>()
.try_into()
.unwrap();

let running_sum = meta.advice_column();
let running_sum_pairs = {
let q_pairs = meta.selector();
RunningSumConfig::configure(meta, q_pairs, running_sum)
};

let config = Alg1Config::configure(
meta,
(advices[0], advices[1]),
(advices[2], advices[3]),
(advices[4], advices[5], advices[6], advices[7]),
running_sum_pairs,
);

Self::Config {
config,
advice_column: running_sum,
}
}

fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<C::Base>,
) -> Result<(), Error> {
// Convert base curve point to the expected in-circuit type (NonIdentityEccPoint).
let base_point = assign_base_point(&mut layouter, self.base_point, &config)?;

// Get a fake challenge - this is passed from outside the circuit instead of being
// squeezed out of a transcript, but it's the right type
let challenge = assign_challenge(&mut layouter, self.challenge, &config)?;

// Convert challenge to bitstring
let bitstring = config
.config
.convert_to_bitstring(layouter.namespace(|| "challenge"), &challenge)?;

// Compute in-circuit endoscaled value using the normal endoscaling algorithm (alg 1)
let target =
config
.config
.endoscale_var_base(&mut layouter, &bitstring, &base_point)?;

// Extract the bitstring out of the circuit and compute the equivalent endoscalar (alg 2)
let endoscalar: Value<C::Scalar> = bitstring
.bitstring()
.map(|v| compute_endoscalar(Setting::Full, &v));

// Compute out-of-circuit endoscaled value using normal scalar multiplication with the
// challenge-derived endoscalar and the random curve point
let guess = endoscalar
.zip(base_point.point())
.map(|(e, g)| (g.to_curve() * e).to_affine());

// Make sure the in- and out-of-circuit values match
guess.zip(target.point()).assert_if_known(|(g, t)| g == t);

Ok(())
}
}

#[test]
fn endoscaling_without_decomposition_matches_out_of_circuit_scalar_multiplication() {
let mut rng = thread_rng();

// This tests the following:
// - the test-only method to pull a decomposition out-of-circuit works
// - out-of-circuit alg 2 + scalar multiplication is the same as in-circuit alg_1
let circuit = CompareEndoscalingCircuit::<pallas::Affine> {
challenge: Value::known(pallas::Base::random(&mut rng)),
base_point: Value::known(pallas::Point::random(&mut rng).to_affine()),
};

let proof = MockProver::run(9, &circuit, vec![]).unwrap();
proof.assert_satisfied();
}

#[derive(Clone)]
struct CompareDecompAndEndoscalingCircuit<C: CurveAffine>
where
C::Base: PrimeFieldBits,
{
challenge: Value<C::Base>,
base_point: Value<C>,
out_of_circuit_result: Value<C>,
}

impl<C: CurveAffine> Circuit<C::Base> for CompareDecompAndEndoscalingCircuit<C>
where
C::Base: PrimeFieldBits,
{
type Config = EndoscaleTestConfig<C>;
type FloorPlanner = SimpleFloorPlanner;
#[cfg(feature = "circuit-params")]
type Params = ();

fn without_witnesses(&self) -> Self {
Self {
challenge: Value::unknown(),
base_point: Value::unknown(),
out_of_circuit_result: Value::unknown(),
}
}

fn configure(meta: &mut ConstraintSystem<C::Base>) -> Self::Config {
// We'll reuse the same configuration for this test
CompareEndoscalingCircuit::configure(meta)
}

fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<C::Base>,
) -> Result<(), Error> {
// Convert base curve point to the expected in-circuit type (NonIdentityEccPoint).
let base_point = assign_base_point(&mut layouter, self.base_point, &config)?;

// Get a fake challenge - this is passed from outside the circuit instead of being
// squeezed out of a transcript, but it's the right type
let challenge = assign_challenge(&mut layouter, self.challenge, &config)?;

// Convert challenge to bitstring
let bitstring = config
.config
.convert_to_bitstring(layouter.namespace(|| "challenge"), &challenge)?;

// Compute in-circuit endoscaled value using the normal endoscaling algorithm (alg 1)
let in_circuit_result =
config
.config
.endoscale_var_base(&mut layouter, &bitstring, &base_point)?;

// Make sure the in- and out- points are the same
self.out_of_circuit_result
.zip(in_circuit_result.point())
.assert_if_known(|(o, i)| o == i);
Ok(())
}
}

#[test]
fn endoscaling_matches_out_of_circuit_scalar_multiplication() {
// Tests that in-circuit and out-of-circuit challenge decomposition produces the same
// bitstring
let mut rng = thread_rng();

let challenge = pallas::Base::random(&mut rng);
let base_point = pallas::Point::random(&mut rng);

// Convert challenge to bits / an endoscalar (out-of-circuit "alg 2")...
let out_of_circuit_endoscalar: pallas::Scalar =
EndoscalarChallenge::<pallas::Affine>::new(&challenge).get_scalar();
// ...then compute the relevant endoscaled value using scalar multiplication
let out_of_circuit_result = base_point * out_of_circuit_endoscalar;

// In-circuit, compute the endoscaled result ("alg 1") and make sure it's the same
let circuit = CompareDecompAndEndoscalingCircuit {
challenge: Value::known(challenge),
base_point: Value::known(base_point.to_affine()),
out_of_circuit_result: Value::known(out_of_circuit_result.to_affine()),
};

let proof = MockProver::run(9, &circuit, vec![]).unwrap();
proof.assert_satisfied();
}

#[test]
#[should_panic(expected = "assertion failed: f(value)")]
fn endoscaling_depends_on_base_point() {
let mut rng = thread_rng();

// Use the same challenge
let challenge = pallas::Base::random(&mut rng);

// Use different base points for in- and out-of-circuit
let in_circuit_base_point = pallas::Point::random(&mut rng);
let out_of_circuit_base_point = pallas::Point::random(&mut rng);
assert_ne!(in_circuit_base_point, out_of_circuit_base_point);

// Convert challenge to bits / an endoscalar (out-of-circuit "alg 2")...
let out_of_circuit_endoscalar: pallas::Scalar =
EndoscalarChallenge::<pallas::Affine>::new(&challenge).get_scalar();
// ...then compute the relevant endoscaled value using scalar multiplication
let out_of_circuit_result = out_of_circuit_base_point * out_of_circuit_endoscalar;

// In-circuit, compute the endoscaled result ("alg 1")
let circuit = CompareDecompAndEndoscalingCircuit {
challenge: Value::known(challenge),
base_point: Value::known(in_circuit_base_point.to_affine()),
out_of_circuit_result: Value::known(out_of_circuit_result.to_affine()),
};

let proof = MockProver::run(9, &circuit, vec![]).unwrap();

// Actually, this will panic instead of being handled nicely because we use an assert in the
// circuit to check equality.
assert!(proof.verify().is_err());
}

#[test]
#[should_panic(expected = "assertion failed: f(value)")]
fn endoscaling_depends_on_scalar() {
let mut rng = thread_rng();

// Use different challenges
let in_circuit_challenge = pallas::Base::random(&mut rng);
let out_of_circuit_challenge = pallas::Base::random(&mut rng);
assert_ne!(in_circuit_challenge, out_of_circuit_challenge);

// Use the same base point
let base_point = pallas::Point::random(&mut rng);

// Convert challenge to bits / an endoscalar (out-of-circuit "alg 2")...
let out_of_circuit_endoscalar: pallas::Scalar =
EndoscalarChallenge::<pallas::Affine>::new(&out_of_circuit_challenge).get_scalar();
// ...then compute the relevant endoscaled value using scalar multiplication
let out_of_circuit_result = base_point * out_of_circuit_endoscalar;

// In-circuit, compute the endoscaled result ("alg 1")
let circuit = CompareDecompAndEndoscalingCircuit {
challenge: Value::known(in_circuit_challenge),
base_point: Value::known(base_point.to_affine()),
out_of_circuit_result: Value::known(out_of_circuit_result.to_affine()),
};

let proof = MockProver::run(9, &circuit, vec![]).unwrap();

// Actually, this will panic instead of being handled nicely because we use an assert in the
// circuit to check equality.
assert!(proof.verify().is_err());
}
}
8 changes: 1 addition & 7 deletions halo2_proofs/src/transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,13 +590,7 @@ where
}

fn get_scalar(&self) -> C::Scalar {
let bits = self
.0
.to_le_bits()
.iter()
.by_vals()
.take(CHALLENGE_LENGTH)
.collect::<Vec<_>>();
let bits = self.0.to_le_bits().iter().by_vals().collect::<Vec<_>>();
compute_endoscalar(Setting::Full, &bits)
}

Expand Down

0 comments on commit 1b01f78

Please sign in to comment.