Compare commits

...

18 Commits

Author SHA1 Message Date
Paul 8b00c34c15
Merge 5f436dc338 into 7df93fd855 2024-03-01 11:27:10 -07:00
Daira-Emma Hopwood 7df93fd855
Merge pull request #814 from adria0/fix/mdbook
Fix MD book generation
2024-02-26 23:50:17 +00:00
adria0 daaa638966 fix(mdbook): fix generation 2024-02-22 22:28:36 +01:00
Constance 5f436dc338 Move mux functionality into CondSwap chip 2023-12-18 15:44:28 +01:00
Constance 622875ead0 Remove q_sinsemilla4_private 2023-12-18 15:43:57 +01:00
Constance ac7a90d9ad Some minor changes 2023-12-18 15:43:57 +01:00
Constance d76d2317b7 Reactivate i686 CI test 2023-12-18 15:43:57 +01:00
Constance 87464d4b3f Update comments 2023-12-18 15:43:57 +01:00
Constance Beguier cba30b1b84 Add functions to evaluate a Sinsemilla hash from an initial private point (#22)
To share ZEC and ZSA hash computations in Orchard circuit's note commitment evaluation, we need to compute a Sinsemille hash from a private input point.
2023-12-07 16:10:35 +01:00
Constance Beguier f51eebeb4e Add multiplexer chip (#23)
It is now possible to perform a mux between two points or between two non-identity points.
`mux(choice, left, right)` will return `left` when `choice=0` and `right` when `choice=1`.
`choice` must be constrained to `{0, 1}` outside the gate.

It is no longer needed to expose `from_coordinates_unchecked`.
2023-12-07 16:10:35 +01:00
Constance Beguier 4c3c00bced Optimized short range check on 4 and 5 bits (#21)
Short range checks on 4 and 5 bits are now performed with only one lookup (instead of 2).
To do that, we added a column `table_short_range_tag` in the lookup table.
This new column `table_short_range_tag` contains the value
- 4 for rows used in short range check on 4 bits
- 5 for rows used in short range check on 5 bits
- 0 for rows used in short range check on 10 bits

Disable tests on i686 and code coverage in CI
2023-12-07 16:10:35 +01:00
Constance Beguier 475f54daa4 Add Point::new_from_constant method (#17)
It is now possible to create a Point from a constant.
This functionality is required to evaluate the old nullifier.
- for non split_notes, nf_old = Extract_P([PRF^{nfOrchard}_{nk}(rho_old) + psi_nf) mod q_P] NullifierK + cm_old)
- for split notes, nf_old = Extract_P([PRF^{nfOrchard}_{nk}(rho_old) + psi_nf) mod q_P] NullifierK + cm_old + NullifierL)
2023-12-07 16:10:35 +01:00
Constance 4ce262d8e7 Add some functionalities for MuxChip 2023-12-07 16:10:35 +01:00
Constance 54697b22ed Add hash and blinding_factor functions 2023-12-07 16:10:35 +01:00
Paul 8cfe0ae67d updated naming 2023-12-07 16:10:35 +01:00
Constance b1e397f5b1 Add commit_from_hash_point 2023-12-07 16:10:35 +01:00
Constance 731bc1021a Add CommitDomain creation from two personalizations 2023-12-07 16:10:35 +01:00
Constance 35c815d36e Add variable-base sign-scalar multiplication 2023-12-07 16:10:35 +01:00
17 changed files with 1462 additions and 157 deletions

View File

@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
toolchain: '1.76.0'
override: true
# - name: Setup mdBook
@ -26,7 +26,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: install
args: mdbook --git https://github.com/HollowMan6/mdBook.git --rev 62e01b34c23b957579c04ee1b24b57814ed8a4d5
args: mdbook --git https://github.com/HollowMan6/mdBook.git --rev 5830c9555a4dc051675d17f1fcb04dd0920543e8
- name: Install mdbook-katex and mdbook-pdf
uses: actions-rs/cargo@v1
@ -40,6 +40,11 @@ jobs:
- name: Build halo2 book
run: mdbook build book/
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2023-10-05
override: true
- name: Build latest rustdocs
uses: actions-rs/cargo@v1
with:

View File

@ -125,33 +125,33 @@ jobs:
- name: Test halo2 book
run: mdbook test -L target/debug/deps book/
codecov:
name: Code coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Use stable for this to ensure that cargo-tarpaulin can be built.
- id: prepare
uses: ./.github/actions/prepare
with:
toolchain: stable
nightly-features: true
- name: Install cargo-tarpaulin
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-tarpaulin
- name: Generate coverage report
uses: actions-rs/cargo@v1
with:
command: tarpaulin
args: >
${{ steps.prepare.outputs.feature-flags }}
--timeout 600
--out Xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3.1.4
# codecov:
# name: Code coverage
# runs-on: ubuntu-latest
#
# steps:
# - uses: actions/checkout@v3
# # Use stable for this to ensure that cargo-tarpaulin can be built.
# - id: prepare
# uses: ./.github/actions/prepare
# with:
# toolchain: stable
# nightly-features: true
# - name: Install cargo-tarpaulin
# uses: actions-rs/cargo@v1
# with:
# command: install
# args: cargo-tarpaulin
# - name: Generate coverage report
# uses: actions-rs/cargo@v1
# with:
# command: tarpaulin
# args: >
# ${{ steps.prepare.outputs.feature-flags }}
# --timeout 600
# --out Xml
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v3.1.4
doc-links:
name: Intra-doc links

View File

@ -14,8 +14,6 @@ title = "The halo2 Book"
macros = "macros.txt"
renderers = ["html"]
[output.katex]
[output.html]
[output.html.print]

View File

@ -4,7 +4,7 @@ use std::fmt::Debug;
use halo2_proofs::{
arithmetic::CurveAffine,
circuit::{Chip, Layouter, Value},
circuit::{AssignedCell, Chip, Layouter, Value},
plonk::Error,
};
@ -60,6 +60,15 @@ pub trait EccInstructions<C: CurveAffine>:
value: Value<C>,
) -> Result<Self::Point, Error>;
/// Witnesses the given constant point as a private input to the circuit.
/// This allows the point to be the identity, mapped to (0, 0) in
/// affine coordinates.
fn witness_point_from_constant(
&self,
layouter: &mut impl Layouter<C::Base>,
value: C,
) -> Result<Self::Point, Error>;
/// Witnesses the given point as a private input to the circuit.
/// This returns an error if the point is the identity.
fn witness_point_non_id(
@ -111,6 +120,15 @@ pub trait EccInstructions<C: CurveAffine>:
b: &B,
) -> Result<Self::Point, Error>;
/// Performs variable-base sign-scalar multiplication, returning `[sign] point`
/// `sign` must be in {-1, 1}.
fn mul_sign(
&self,
layouter: &mut impl Layouter<C::Base>,
sign: &AssignedCell<C::Base, C::Base>,
point: &Self::Point,
) -> Result<Self::Point, Error>;
/// Performs variable-base scalar multiplication, returning `[scalar] base`.
fn mul(
&self,
@ -390,6 +408,16 @@ impl<C: CurveAffine, EccChip: EccInstructions<C> + Clone + Debug + Eq> Point<C,
point.map(|inner| Point { chip, inner })
}
/// Constructs a new point with the given fixed value.
pub fn new_from_constant(
chip: EccChip,
mut layouter: impl Layouter<C::Base>,
value: C,
) -> Result<Self, Error> {
let point = chip.witness_point_from_constant(&mut layouter, value);
point.map(|inner| Point { chip, inner })
}
/// Constrains this point to be equal in value to another point.
pub fn constrain_equal<Other: Into<Point<C, EccChip>> + Clone>(
&self,
@ -432,6 +460,21 @@ impl<C: CurveAffine, EccChip: EccInstructions<C> + Clone + Debug + Eq> Point<C,
inner,
})
}
/// Returns `[sign] self`.
/// `sign` must be in {-1, 1}.
pub fn mul_sign(
&self,
mut layouter: impl Layouter<C::Base>,
sign: &AssignedCell<C::Base, C::Base>,
) -> Result<Point<C, EccChip>, Error> {
self.chip
.mul_sign(&mut layouter, sign, &self.inner)
.map(|point| Point {
chip: self.chip.clone(),
inner: point,
})
}
}
/// The affine short Weierstrass x-coordinate of a point on a specific elliptic curve.
@ -750,6 +793,7 @@ pub(crate) mod tests {
meta.advice_column(),
];
let lookup_table = meta.lookup_table_column();
let table_range_check_tag = meta.lookup_table_column();
let lagrange_coeffs = [
meta.fixed_column(),
meta.fixed_column(),
@ -764,7 +808,12 @@ pub(crate) mod tests {
let constants = meta.fixed_column();
meta.enable_constant(constants);
let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup_table);
let range_check = LookupRangeCheckConfig::configure(
meta,
advices[9],
lookup_table,
table_range_check_tag,
);
EccChip::<TestFixedBases>::configure(meta, advices, lagrange_coeffs, range_check)
}
@ -865,6 +914,14 @@ pub(crate) mod tests {
)?;
}
// Test variable-base sign-scalar multiplication
{
super::chip::mul_fixed::short::tests::test_mul_sign(
chip.clone(),
layouter.namespace(|| "variable-base sign-scalar mul"),
)?;
}
// Test full-width fixed-base scalar multiplication
{
super::chip::mul_fixed::full_width::tests::test_mul_fixed(

View File

@ -453,6 +453,18 @@ where
)
}
fn witness_point_from_constant(
&self,
layouter: &mut impl Layouter<pallas::Base>,
value: pallas::Affine,
) -> Result<Self::Point, Error> {
let config = self.config().witness_point;
layouter.assign_region(
|| "witness point (constant)",
|mut region| config.constant_point(value, 0, &mut region),
)
}
fn witness_point_non_id(
&self,
layouter: &mut impl Layouter<pallas::Base>,
@ -532,6 +544,24 @@ where
)
}
/// Performs variable-base sign-scalar multiplication, returning `[sign] point`
/// `sign` must be in {-1, 1}.
fn mul_sign(
&self,
layouter: &mut impl Layouter<pallas::Base>,
sign: &AssignedCell<pallas::Base, pallas::Base>,
point: &Self::Point,
) -> Result<Self::Point, Error> {
// Multiply point by sign, using the same gate as mul_fixed::short.
// This also constrains sign to be in {-1, 1}.
let config_short = self.config().mul_fixed_short.clone();
config_short.assign_scalar_sign(
layouter.namespace(|| "variable-base sign-scalar mul"),
sign,
point,
)
}
fn mul(
&self,
layouter: &mut impl Layouter<pallas::Base>,

View File

@ -4,7 +4,7 @@ use super::super::{EccPoint, EccScalarFixedShort, FixedPoints, L_SCALAR_SHORT, N
use crate::{ecc::chip::MagnitudeSign, utilities::bool_check};
use halo2_proofs::{
circuit::{Layouter, Region},
circuit::{AssignedCell, Layouter, Region},
plonk::{ConstraintSystem, Constraints, Error, Expression, Selector},
poly::Rotation,
};
@ -241,11 +241,73 @@ impl<Fixed: FixedPoints<pallas::Affine>> Config<Fixed> {
Ok((result, scalar))
}
/// Multiply the point by sign, using the q_mul_fixed_short gate.
/// Constraints `sign` in {-1, 1}
pub fn assign_scalar_sign(
&self,
mut layouter: impl Layouter<pallas::Base>,
sign: &AssignedCell<pallas::Base, pallas::Base>,
point: &EccPoint,
) -> Result<EccPoint, Error> {
let signed_point = layouter.assign_region(
|| "Signed point",
|mut region| {
let offset = 0;
// Enable mul_fixed_short selector to check the sign logic.
self.q_mul_fixed_short.enable(&mut region, offset)?;
// Set "last window" to 0 (this field is irrelevant here).
region.assign_advice_from_constant(
|| "u=0",
self.super_config.u,
offset,
pallas::Base::zero(),
)?;
// Copy sign to `window` column
sign.copy_advice(|| "sign", &mut region, self.super_config.window, offset)?;
// Assign the input y-coordinate.
point.y.copy_advice(
|| "unsigned y",
&mut region,
self.super_config.add_config.y_qr,
offset,
)?;
// Conditionally negate y-coordinate according to the value of sign
let signed_y_val = sign.value().and_then(|sign| {
if sign == &-pallas::Base::one() {
-point.y.value()
} else {
point.y.value().cloned()
}
});
// Assign the output signed y-coordinate.
let signed_y = region.assign_advice(
|| "signed y",
self.super_config.add_config.y_p,
offset,
|| signed_y_val,
)?;
Ok(EccPoint {
x: point.x.clone(),
y: signed_y,
})
},
)?;
Ok(signed_point)
}
}
#[cfg(test)]
pub mod tests {
use group::{ff::PrimeField, Curve};
use group::{ff::PrimeField, Curve, Group};
use halo2_proofs::{
arithmetic::CurveAffine,
circuit::{AssignedCell, Chip, Layouter, Value},
@ -446,6 +508,7 @@ pub mod tests {
meta.advice_column(),
];
let lookup_table = meta.lookup_table_column();
let table_range_check_tag = meta.lookup_table_column();
let lagrange_coeffs = [
meta.fixed_column(),
meta.fixed_column(),
@ -461,7 +524,12 @@ pub mod tests {
let constants = meta.fixed_column();
meta.enable_constant(constants);
let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup_table);
let range_check = LookupRangeCheckConfig::configure(
meta,
advices[9],
lookup_table,
table_range_check_tag,
);
EccChip::<TestFixedBases>::configure(meta, advices, lagrange_coeffs, range_check)
}
@ -582,7 +650,7 @@ pub mod tests {
)],
},
VerifyFailure::Permutation {
column: (Any::Fixed, 9).into(),
column: (Any::Fixed, 10).into(),
location: FailureLocation::OutsideRegion { row: 0 },
},
VerifyFailure::Permutation {
@ -657,4 +725,223 @@ pub mod tests {
);
}
}
pub(crate) fn test_mul_sign(
chip: EccChip<TestFixedBases>,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), Error> {
// Generate a random non-identity point P
let p_val = pallas::Point::random(rand::rngs::OsRng).to_affine();
let p = Point::new(
chip.clone(),
layouter.namespace(|| "P"),
Value::known(p_val),
)?;
// Create -P
let p_neg_val = -p_val;
let p_neg = Point::new(
chip.clone(),
layouter.namespace(|| "-P"),
Value::known(p_neg_val),
)?;
// Create the identity point
let identity = Point::new(
chip.clone(),
layouter.namespace(|| "identity"),
Value::known(pallas::Point::identity().to_affine()),
)?;
// Create -1 and 1 scalars
let pos_sign = chip.load_private(
layouter.namespace(|| "positive sign"),
chip.config().advices[0],
Value::known(pallas::Base::one()),
)?;
let neg_sign = chip.load_private(
layouter.namespace(|| "negative sign"),
chip.config().advices[1],
Value::known(-pallas::Base::one()),
)?;
// [1] P == P
{
let result = p.mul_sign(layouter.namespace(|| "[1] P"), &pos_sign)?;
result.constrain_equal(layouter.namespace(|| "constrain [1] P"), &p)?;
}
// [-1] P == -P
{
let result = p.mul_sign(layouter.namespace(|| "[1] P"), &neg_sign)?;
result.constrain_equal(layouter.namespace(|| "constrain [1] P"), &p_neg)?;
}
// [1] 0 == 0
{
let result = identity.mul_sign(layouter.namespace(|| "[1] O"), &pos_sign)?;
result.constrain_equal(layouter.namespace(|| "constrain [1] 0"), &identity)?;
}
// [-1] 0 == 0
{
let result = identity.mul_sign(layouter.namespace(|| "[-1] O"), &neg_sign)?;
result.constrain_equal(layouter.namespace(|| "constrain [1] 0"), &identity)?;
}
Ok(())
}
#[test]
fn invalid_sign_in_mul_sign() {
use crate::{ecc::chip::EccConfig, utilities::UtilitiesInstructions};
use halo2_proofs::{
circuit::{Layouter, SimpleFloorPlanner},
dev::{FailureLocation, MockProver, VerifyFailure},
plonk::{Circuit, ConstraintSystem, Error},
};
#[derive(Default)]
struct MyCircuit {
base: Value<pallas::Affine>,
sign: Value<pallas::Base>,
}
impl UtilitiesInstructions<pallas::Base> for MyCircuit {
type Var = AssignedCell<pallas::Base, pallas::Base>;
}
impl Circuit<pallas::Base> for MyCircuit {
type Config = EccConfig<TestFixedBases>;
type FloorPlanner = SimpleFloorPlanner;
fn without_witnesses(&self) -> Self {
Self::default()
}
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self::Config {
let advices = [
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
];
let lookup_table = meta.lookup_table_column();
let table_range_check_tag = meta.lookup_table_column();
let lagrange_coeffs = [
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
];
// Shared fixed column for loading constants
let constants = meta.fixed_column();
meta.enable_constant(constants);
let range_check = LookupRangeCheckConfig::configure(
meta,
advices[9],
lookup_table,
table_range_check_tag,
);
EccChip::<TestFixedBases>::configure(meta, advices, lagrange_coeffs, range_check)
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), Error> {
let chip = EccChip::construct(config.clone());
let column = config.advices[0];
//let short_config = config.mul_fixed_short.clone();
let base = Point::new(chip, layouter.namespace(|| "load base"), self.base)?;
let sign =
self.load_private(layouter.namespace(|| "load sign"), column, self.sign)?;
base.mul_sign(layouter.namespace(|| "[sign] base"), &sign)?;
Ok(())
}
}
// Copied from halo2_proofs::dev::util
fn format_value(v: pallas::Base) -> String {
use ff::Field;
if v.is_zero_vartime() {
"0".into()
} else if v == pallas::Base::one() {
"1".into()
} else if v == -pallas::Base::one() {
"-1".into()
} else {
// Format value as hex.
let s = format!("{:?}", v);
// Remove leading zeroes.
let s = s.strip_prefix("0x").unwrap();
let s = s.trim_start_matches('0');
format!("0x{}", s)
}
}
// Sign that is not +/- 1 should fail
// Generate a random non-identity point
let point = pallas::Point::random(rand::rngs::OsRng);
let circuit = MyCircuit {
base: Value::known(point.to_affine()),
sign: Value::known(pallas::Base::zero()),
};
let prover = MockProver::<pallas::Base>::run(11, &circuit, vec![]).unwrap();
assert_eq!(
prover.verify(),
Err(vec![
VerifyFailure::ConstraintNotSatisfied {
constraint: ((17, "Short fixed-base mul gate").into(), 1, "sign_check").into(),
location: FailureLocation::InRegion {
region: (2, "Signed point").into(),
offset: 0,
},
cell_values: vec![(((Any::Advice, 4).into(), 0).into(), "0".to_string())],
},
VerifyFailure::ConstraintNotSatisfied {
constraint: (
(17, "Short fixed-base mul gate").into(),
3,
"negation_check"
)
.into(),
location: FailureLocation::InRegion {
region: (2, "Signed point").into(),
offset: 0,
},
cell_values: vec![
(
((Any::Advice, 1).into(), 0).into(),
format_value(*point.to_affine().coordinates().unwrap().y()),
),
(
((Any::Advice, 3).into(), 0).into(),
format_value(*point.to_affine().coordinates().unwrap().y()),
),
(((Any::Advice, 4).into(), 0).into(), "0".to_string()),
],
}
])
);
}
}

View File

@ -102,6 +102,21 @@ impl Config {
Ok((x_var, y_var))
}
fn assign_xy_from_constant(
&self,
value: (Assigned<pallas::Base>, Assigned<pallas::Base>),
offset: usize,
region: &mut Region<'_, pallas::Base>,
) -> Result<Coordinates, Error> {
// Assign `x` value
let x_var = region.assign_advice_from_constant(|| "x", self.x, offset, value.0)?;
// Assign `y` value
let y_var = region.assign_advice_from_constant(|| "y", self.y, offset, value.1)?;
Ok((x_var, y_var))
}
/// Assigns a point that can be the identity.
pub(super) fn point(
&self,
@ -126,6 +141,28 @@ impl Config {
.map(|(x, y)| EccPoint::from_coordinates_unchecked(x, y))
}
/// Assigns a constant point that can be the identity.
pub(super) fn constant_point(
&self,
value: pallas::Affine,
offset: usize,
region: &mut Region<'_, pallas::Base>,
) -> Result<EccPoint, Error> {
// Enable `q_point` selector
self.q_point.enable(region, offset)?;
let value = if value == pallas::Affine::identity() {
// Map the identity to (0, 0).
(Assigned::Zero, Assigned::Zero)
} else {
let value = value.coordinates().unwrap();
(value.x().into(), value.y().into())
};
self.assign_xy_from_constant(value, offset, region)
.map(|(x, y)| EccPoint::from_coordinates_unchecked(x, y))
}
/// Assigns a non-identity point.
pub(super) fn point_non_id(
&self,

View File

@ -78,7 +78,7 @@ pub trait SinsemillaInstructions<C: CurveAffine, const K: usize, const MAX_WORDS
/// This returns both the resulting point, as well as the message
/// decomposition in the form of intermediate values in a cumulative
/// sum.
///
/// The initial point `Q` is a public point.
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
fn hash_to_point(
@ -88,6 +88,20 @@ pub trait SinsemillaInstructions<C: CurveAffine, const K: usize, const MAX_WORDS
message: Self::Message,
) -> Result<(Self::NonIdentityPoint, Vec<Self::RunningSum>), Error>;
/// Hashes a message to an ECC curve point.
/// This returns both the resulting point, as well as the message
/// decomposition in the form of intermediate values in a cumulative
/// sum.
/// The initial point `Q` is a private point.
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
fn hash_to_point_with_private_init(
&self,
layouter: impl Layouter<C::Base>,
Q: &Self::NonIdentityPoint,
message: Self::Message,
) -> Result<(Self::NonIdentityPoint, Vec<Self::RunningSum>), Error>;
/// Extracts the x-coordinate of the output of a Sinsemilla hash.
fn extract(point: &Self::NonIdentityPoint) -> Self::X;
}
@ -329,6 +343,21 @@ where
.map(|(point, zs)| (ecc::NonIdentityPoint::from_inner(self.ecc_chip.clone(), point), zs))
}
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
/// Evaluate the Sinsemilla hash of `message` from the private initial point `Q`.
pub fn hash_to_point_with_private_init(
&self,
layouter: impl Layouter<C::Base>,
Q: &<SinsemillaChip as SinsemillaInstructions<C, K, MAX_WORDS>>::NonIdentityPoint,
message: Message<C, SinsemillaChip, K, MAX_WORDS>,
) -> Result<(ecc::NonIdentityPoint<C, EccChip>, Vec<SinsemillaChip::RunningSum>), Error> {
assert_eq!(self.sinsemilla_chip, message.chip);
self.sinsemilla_chip
.hash_to_point_with_private_init(layouter, Q, message.inner)
.map(|(point, zs)| (ecc::NonIdentityPoint::from_inner(self.ecc_chip.clone(), point), zs))
}
/// $\mathsf{SinsemillaHash}$ from [§ 5.4.1.9][concretesinsemillahash].
///
/// [concretesinsemillahash]: https://zips.z.cash/protocol/protocol.pdf#concretesinsemillahash
@ -412,6 +441,63 @@ where
}
}
#[allow(clippy::type_complexity)]
/// Evaluates the Sinsemilla hash of `message` from the public initial point `Q` stored
/// into `CommitDomain`.
pub fn hash(
&self,
layouter: impl Layouter<C::Base>,
message: Message<C, SinsemillaChip, K, MAX_WORDS>,
) -> Result<
(
ecc::NonIdentityPoint<C, EccChip>,
Vec<SinsemillaChip::RunningSum>,
),
Error,
> {
assert_eq!(self.M.sinsemilla_chip, message.chip);
self.M.hash_to_point(layouter, message)
}
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
/// Evaluates the Sinsemilla hash of `message` from the private initial point `Q`.
pub fn hash_with_private_init(
&self,
layouter: impl Layouter<C::Base>,
Q: &<SinsemillaChip as SinsemillaInstructions<C, K, MAX_WORDS>>::NonIdentityPoint,
message: Message<C, SinsemillaChip, K, MAX_WORDS>,
) -> Result<
(
ecc::NonIdentityPoint<C, EccChip>,
Vec<SinsemillaChip::RunningSum>,
),
Error,
> {
assert_eq!(self.M.sinsemilla_chip, message.chip);
self.M.hash_to_point_with_private_init(layouter, Q, message)
}
#[allow(clippy::type_complexity)]
/// Returns the public initial point `Q` stored into `CommitDomain`.
pub fn q_init(&self) -> C {
self.M.Q
}
#[allow(clippy::type_complexity)]
/// Evaluates the blinding factor equal to $\[r\] R$ where `r` is stored in the `CommitDomain`.
pub fn blinding_factor(
&self,
mut layouter: impl Layouter<C::Base>,
r: ecc::ScalarFixed<C, EccChip>,
) -> Result<
ecc::Point<C, EccChip>,
Error,
> {
let (blind, _) = self.R.mul(layouter.namespace(|| "[r] R"), r)?;
Ok(blind)
}
#[allow(clippy::type_complexity)]
/// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit].
///
@ -429,8 +515,8 @@ where
Error,
> {
assert_eq!(self.M.sinsemilla_chip, message.chip);
let (blind, _) = self.R.mul(layouter.namespace(|| "[r] R"), r)?;
let (p, zs) = self.M.hash_to_point(layouter.namespace(|| "M"), message)?;
let blind = self.blinding_factor(layouter.namespace(|| "[r] R"), r)?;
let (p, zs) = self.hash(layouter.namespace(|| "M"), message)?;
let commitment = p.add(layouter.namespace(|| "M + [r] R"), &blind)?;
Ok((commitment, zs))
}
@ -551,6 +637,7 @@ pub(crate) mod tests {
meta.enable_constant(constants);
let table_idx = meta.lookup_table_column();
let table_range_check_tag = meta.lookup_table_column();
let lagrange_coeffs = [
meta.fixed_column(),
meta.fixed_column(),
@ -567,9 +654,15 @@ pub(crate) mod tests {
table_idx,
meta.lookup_table_column(),
meta.lookup_table_column(),
table_range_check_tag,
);
let range_check = LookupRangeCheckConfig::configure(meta, advices[9], table_idx);
let range_check = LookupRangeCheckConfig::configure(
meta,
advices[9],
table_idx,
table_range_check_tag,
);
let ecc_config =
EccChip::<TestFixedBases>::configure(meta, advices, lagrange_coeffs, range_check);

View File

@ -153,7 +153,7 @@ where
advices: [Column<Advice>; 5],
witness_pieces: Column<Advice>,
fixed_y_q: Column<Fixed>,
lookup: (TableColumn, TableColumn, TableColumn),
lookup: (TableColumn, TableColumn, TableColumn, TableColumn),
range_check: LookupRangeCheckConfig<pallas::Base, { sinsemilla::K }>,
) -> <Self as Chip<pallas::Base>>::Config {
// Enable equality on all advice columns
@ -178,6 +178,7 @@ where
table_idx: lookup.0,
table_x: lookup.1,
table_y: lookup.2,
table_range_check_tag: lookup.3,
},
lookup_config: range_check,
_marker: PhantomData,
@ -203,7 +204,7 @@ where
// https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial
meta.create_gate("Initial y_Q", |meta| {
let q_s4 = meta.query_selector(config.q_sinsemilla4);
let y_q = meta.query_fixed(config.fixed_y_q);
let y_q = meta.query_advice(config.double_and_add.x_p, Rotation::prev());
// Y_A = (lambda_1 + lambda_2) * (x_a - x_r)
let Y_A_cur = Y_A(meta, Rotation::cur());
@ -321,6 +322,20 @@ where
)
}
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
fn hash_to_point_with_private_init(
&self,
mut layouter: impl Layouter<pallas::Base>,
Q: &Self::NonIdentityPoint,
message: Self::Message,
) -> Result<(Self::NonIdentityPoint, Vec<Self::RunningSum>), Error> {
layouter.assign_region(
|| "hash_to_point",
|mut region| self.hash_message_with_private_init(&mut region, Q, &message),
)
}
fn extract(point: &Self::NonIdentityPoint) -> Self::X {
point.x()
}

View File

@ -6,7 +6,7 @@ use halo2_proofs::{
};
use super::{CommitDomains, FixedPoints, HashDomains};
use crate::sinsemilla::primitives::{self as sinsemilla, SINSEMILLA_S};
use crate::sinsemilla::primitives::{self as sinsemilla, K, SINSEMILLA_S};
use pasta_curves::pallas;
/// Table containing independent generators S[0..2^k]
@ -15,6 +15,7 @@ pub struct GeneratorTableConfig {
pub table_idx: TableColumn,
pub table_x: TableColumn,
pub table_y: TableColumn,
pub table_range_check_tag: TableColumn,
}
impl GeneratorTableConfig {
@ -77,6 +78,22 @@ impl GeneratorTableConfig {
});
}
/// Load the generator table into the circuit.
///
/// | table_idx | table_x | table_y | table_range_check_tag |
/// -------------------------------------------------------------------
/// | 0 | X(S\[0\]) | Y(S\[0\]) | 0 |
/// | 1 | X(S\[1\]) | Y(S\[1\]) | 0 |
/// | ... | ... | ... | 0 |
/// | 2^10-1 | X(S\[2^10-1\]) | Y(S\[2^10-1\]) | 0 |
/// | 0 | X(S\[0\]) | Y(S\[0\]) | 4 |
/// | 1 | X(S\[1\]) | Y(S\[1\]) | 4 |
/// | ... | ... | ... | 4 |
/// | 2^4-1 | X(S\[2^4-1\]) | Y(S\[2^4-1\]) | 4 |
/// | 0 | X(S\[0\]) | Y(S\[0\]) | 5 |
/// | 1 | X(S\[1\]) | Y(S\[1\]) | 5 |
/// | ... | ... | ... | 5 |
/// | 2^5-1 | X(S\[2^5-1\]) | Y(S\[2^5-1\]) | 5 |
pub fn load(&self, layouter: &mut impl Layouter<pallas::Base>) -> Result<(), Error> {
layouter.assign_table(
|| "generator_table",
@ -90,6 +107,66 @@ impl GeneratorTableConfig {
)?;
table.assign_cell(|| "table_x", self.table_x, index, || Value::known(*x))?;
table.assign_cell(|| "table_y", self.table_y, index, || Value::known(*y))?;
table.assign_cell(
|| "table_range_check_tag",
self.table_range_check_tag,
index,
|| Value::known(pallas::Base::zero()),
)?;
if index < (1 << 4) {
let new_index = index + (1 << K);
table.assign_cell(
|| "table_idx",
self.table_idx,
new_index,
|| Value::known(pallas::Base::from(index as u64)),
)?;
table.assign_cell(
|| "table_x",
self.table_x,
new_index,
|| Value::known(*x),
)?;
table.assign_cell(
|| "table_y",
self.table_y,
new_index,
|| Value::known(*y),
)?;
table.assign_cell(
|| "table_range_check_tag",
self.table_range_check_tag,
new_index,
|| Value::known(pallas::Base::from(4_u64)),
)?;
}
if index < (1 << 5) {
let new_index = index + (1 << 10) + (1 << 4);
table.assign_cell(
|| "table_idx",
self.table_idx,
new_index,
|| Value::known(pallas::Base::from(index as u64)),
)?;
table.assign_cell(
|| "table_x",
self.table_x,
new_index,
|| Value::known(*x),
)?;
table.assign_cell(
|| "table_y",
self.table_y,
new_index,
|| Value::known(*y),
)?;
table.assign_cell(
|| "table_range_check_tag",
self.table_range_check_tag,
new_index,
|| Value::known(pallas::Base::from(5_u64)),
)?;
}
}
Ok(())
},

View File

@ -41,85 +41,9 @@ where
),
Error,
> {
let config = self.config().clone();
let mut offset = 0;
let (offset, x_a, y_a) = self.public_initialization(region, Q)?;
// Get the `x`- and `y`-coordinates of the starting `Q` base.
let x_q = *Q.coordinates().unwrap().x();
let y_q = *Q.coordinates().unwrap().y();
// Constrain the initial x_a, lambda_1, lambda_2, x_p using the q_sinsemilla4
// selector.
let mut y_a: Y<pallas::Base> = {
// Enable `q_sinsemilla4` on the first row.
config.q_sinsemilla4.enable(region, offset)?;
region.assign_fixed(
|| "fixed y_q",
config.fixed_y_q,
offset,
|| Value::known(y_q),
)?;
Value::known(y_q.into()).into()
};
// Constrain the initial x_q to equal the x-coordinate of the domain's `Q`.
let mut x_a: X<pallas::Base> = {
let x_a = region.assign_advice_from_constant(
|| "fixed x_q",
config.double_and_add.x_a,
offset,
x_q.into(),
)?;
x_a.into()
};
let mut zs_sum: Vec<Vec<AssignedCell<pallas::Base, pallas::Base>>> = Vec::new();
// Hash each piece in the message.
for (idx, piece) in message.iter().enumerate() {
let final_piece = idx == message.len() - 1;
// The value of the accumulator after this piece is processed.
let (x, y, zs) = self.hash_piece(region, offset, piece, x_a, y_a, final_piece)?;
// Since each message word takes one row to process, we increase
// the offset by `piece.num_words` on each iteration.
offset += piece.num_words();
// Update the accumulator to the latest value.
x_a = x;
y_a = y;
zs_sum.push(zs);
}
// Assign the final y_a.
let y_a = {
// Assign the final y_a.
let y_a_cell =
region.assign_advice(|| "y_a", config.double_and_add.lambda_1, offset, || y_a.0)?;
// Assign lambda_2 and x_p zero values since they are queried
// in the gate. (The actual values do not matter since they are
// multiplied by zero.)
{
region.assign_advice(
|| "dummy lambda2",
config.double_and_add.lambda_2,
offset,
|| Value::known(pallas::Base::zero()),
)?;
region.assign_advice(
|| "dummy x_p",
config.double_and_add.x_p,
offset,
|| Value::known(pallas::Base::zero()),
)?;
}
y_a_cell
};
let (x_a, y_a, zs_sum) = self.hash_all_pieces(region, offset, message, x_a, y_a)?;
#[cfg(test)]
#[allow(non_snake_case)]
@ -169,6 +93,239 @@ where
))
}
/// [Specification](https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial).
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
pub(super) fn hash_message_with_private_init(
&self,
region: &mut Region<'_, pallas::Base>,
Q: &NonIdentityEccPoint,
message: &<Self as SinsemillaInstructions<
pallas::Affine,
{ sinsemilla::K },
{ sinsemilla::C },
>>::Message,
) -> Result<
(
NonIdentityEccPoint,
Vec<Vec<AssignedCell<pallas::Base, pallas::Base>>>,
),
Error,
> {
let (offset, x_a, y_a) = self.private_initialization(region, Q)?;
let (x_a, y_a, zs_sum) = self.hash_all_pieces(region, offset, message, x_a, y_a)?;
#[cfg(test)]
#[allow(non_snake_case)]
// Check equivalence to result from primitives::sinsemilla::hash_to_point
{
use crate::sinsemilla::primitives::{K, S_PERSONALIZATION};
use group::{prime::PrimeCurveAffine, Curve};
use pasta_curves::arithmetic::CurveExt;
let field_elems: Value<Vec<_>> = message
.iter()
.map(|piece| piece.field_elem().map(|elem| (elem, piece.num_words())))
.collect();
field_elems
.zip(x_a.value().zip(y_a.value()))
.zip(Q.point())
.assert_if_known(|((field_elems, (x_a, y_a)), Q)| {
// Get message as a bitstring.
let bitstring: Vec<bool> = field_elems
.iter()
.flat_map(|(elem, num_words)| {
elem.to_le_bits().into_iter().take(K * num_words)
})
.collect();
let hasher_S = pallas::Point::hash_to_curve(S_PERSONALIZATION);
let S = |chunk: &[bool]| hasher_S(&lebs2ip_k(chunk).to_le_bytes());
// We can use complete addition here because it differs from
// incomplete addition with negligible probability.
let expected_point = bitstring
.chunks(K)
.fold(Q.to_curve(), |acc, chunk| (acc + S(chunk)) + acc);
let actual_point =
pallas::Affine::from_xy(x_a.evaluate(), y_a.evaluate()).unwrap();
expected_point.to_affine() == actual_point
});
}
x_a.value()
.zip(y_a.value())
.error_if_known_and(|(x_a, y_a)| x_a.is_zero_vartime() || y_a.is_zero_vartime())?;
Ok((
NonIdentityEccPoint::from_coordinates_unchecked(x_a.0, y_a),
zs_sum,
))
}
#[allow(non_snake_case)]
/// Assign the coordinates of the initial public point `Q`
///
/// | offset | x_A | x_P | q_sinsemilla4 |
/// --------------------------------------
/// | 0 | | y_Q | |
/// | 1 | x_Q | | 1 |
fn public_initialization(
&self,
region: &mut Region<'_, pallas::Base>,
Q: pallas::Affine,
) -> Result<(usize, X<pallas::Base>, Y<pallas::Base>), Error> {
let config = self.config().clone();
let mut offset = 0;
// Get the `x`- and `y`-coordinates of the starting `Q` base.
let x_q = *Q.coordinates().unwrap().x();
let y_q = *Q.coordinates().unwrap().y();
// Constrain the initial x_a, lambda_1, lambda_2, x_p using the q_sinsemilla4
// selector.
let y_a: Y<pallas::Base> = {
// Enable `q_sinsemilla4` on the second row.
config.q_sinsemilla4.enable(region, offset + 1)?;
let y_a: AssignedCell<Assigned<pallas::Base>, pallas::Base> = region
.assign_advice_from_constant(
|| "fixed y_q",
config.double_and_add.x_p,
offset,
y_q.into(),
)?;
y_a.value_field().into()
};
offset += 1;
// Constrain the initial x_q to equal the x-coordinate of the domain's `Q`.
let x_a: X<pallas::Base> = {
let x_a = region.assign_advice_from_constant(
|| "fixed x_q",
config.double_and_add.x_a,
offset,
x_q.into(),
)?;
x_a.into()
};
Ok((offset, x_a, y_a))
}
#[allow(non_snake_case)]
/// Assign the coordinates of the initial private point `Q`
///
/// | offset | x_A | x_P | q_sinsemilla4 |
/// --------------------------------------
/// | 0 | | y_Q | |
/// | 1 | x_Q | | 1 |
fn private_initialization(
&self,
region: &mut Region<'_, pallas::Base>,
Q: &NonIdentityEccPoint,
) -> Result<(usize, X<pallas::Base>, Y<pallas::Base>), Error> {
let config = self.config().clone();
let mut offset = 0;
// Assign `x_Q` and `y_Q` in the region and constrain the initial x_a, lambda_1, lambda_2,
// x_p, y_Q using the q_sinsemilla4 selector.
let y_a: Y<pallas::Base> = {
// Enable `q_sinsemilla4` on the second row.
config.q_sinsemilla4.enable(region, offset + 1)?;
let q_y: AssignedCell<Assigned<pallas::Base>, pallas::Base> = Q.y().into();
let y_a: AssignedCell<Assigned<pallas::Base>, pallas::Base> =
q_y.copy_advice(|| "fixed y_q", region, config.double_and_add.x_p, offset)?;
y_a.value_field().into()
};
offset += 1;
let x_a: X<pallas::Base> = {
let q_x: AssignedCell<Assigned<pallas::Base>, pallas::Base> = Q.x().into();
let x_a = q_x.copy_advice(|| "fixed x_q", region, config.double_and_add.x_a, offset)?;
x_a.into()
};
Ok((offset, x_a, y_a))
}
#[allow(clippy::type_complexity)]
/// Hash `message` from the initial point `Q`.
fn hash_all_pieces(
&self,
region: &mut Region<'_, pallas::Base>,
mut offset: usize,
message: &<Self as SinsemillaInstructions<
pallas::Affine,
{ sinsemilla::K },
{ sinsemilla::C },
>>::Message,
mut x_a: X<pallas::Base>,
mut y_a: Y<pallas::Base>,
) -> Result<
(
X<pallas::Base>,
AssignedCell<Assigned<pallas::Base>, pallas::Base>,
Vec<Vec<AssignedCell<pallas::Base, pallas::Base>>>,
),
Error,
> {
let config = self.config().clone();
let mut zs_sum: Vec<Vec<AssignedCell<pallas::Base, pallas::Base>>> = Vec::new();
// Hash each piece in the message.
for (idx, piece) in message.iter().enumerate() {
let final_piece = idx == message.len() - 1;
// The value of the accumulator after this piece is processed.
let (x, y, zs) = self.hash_piece(region, offset, piece, x_a, y_a, final_piece)?;
// Since each message word takes one row to process, we increase
// the offset by `piece.num_words` on each iteration.
offset += piece.num_words();
// Update the accumulator to the latest value.
x_a = x;
y_a = y;
zs_sum.push(zs);
}
// Assign the final y_a.
let y_a = {
// Assign the final y_a.
let y_a_cell =
region.assign_advice(|| "y_a", config.double_and_add.lambda_1, offset, || y_a.0)?;
// Assign lambda_2 and x_p zero values since they are queried
// in the gate. (The actual values do not matter since they are
// multiplied by zero.)
{
region.assign_advice(
|| "dummy lambda2",
config.double_and_add.lambda_2,
offset,
|| Value::known(pallas::Base::zero()),
)?;
region.assign_advice(
|| "dummy x_p",
config.double_and_add.x_p,
offset,
|| Value::known(pallas::Base::zero()),
)?;
}
y_a_cell
};
Ok((x_a, y_a, zs_sum))
}
#[allow(clippy::type_complexity)]
/// Hashes a message piece containing `piece.length` number of `K`-bit words.
///

View File

@ -246,9 +246,11 @@ pub mod tests {
meta.lookup_table_column(),
meta.lookup_table_column(),
meta.lookup_table_column(),
meta.lookup_table_column(),
);
let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup.0);
let range_check =
LookupRangeCheckConfig::configure(meta, advices[9], lookup.0, lookup.3);
let sinsemilla_config_1 = SinsemillaChip::configure(
meta,

View File

@ -441,6 +441,18 @@ where
let chip = CondSwapChip::<pallas::Base>::construct(config);
chip.swap(layouter, pair, swap)
}
fn mux(
&self,
layouter: &mut impl Layouter<pallas::Base>,
choice: Self::Var,
left: Self::Var,
right: Self::Var,
) -> Result<Self::Var, Error> {
let config = self.config().cond_swap_config.clone();
let chip = CondSwapChip::<pallas::Base>::construct(config);
chip.mux(layouter, choice, left, right)
}
}
impl<Hash, Commit, F> SinsemillaInstructions<pallas::Affine, { sinsemilla::K }, { sinsemilla::C }>
@ -523,6 +535,19 @@ where
chip.hash_to_point(layouter, Q, message)
}
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
fn hash_to_point_with_private_init(
&self,
layouter: impl Layouter<pallas::Base>,
Q: &Self::NonIdentityPoint,
message: Self::Message,
) -> Result<(Self::NonIdentityPoint, Vec<Vec<Self::CellValue>>), Error> {
let config = self.config().sinsemilla_config.clone();
let chip = SinsemillaChip::<Hash, Commit, F>::construct(config);
chip.hash_to_point_with_private_init(layouter, Q, message)
}
fn extract(point: &Self::NonIdentityPoint) -> Self::X {
SinsemillaChip::<Hash, Commit, F>::extract(point)
}

View File

@ -184,6 +184,7 @@ impl HashDomain {
#[derive(Debug)]
#[allow(non_snake_case)]
pub struct CommitDomain {
/// A domain in which $\mathsf{SinsemillaHashToPoint}$ and $\mathsf{SinsemillaHash}$ can be used
M: HashDomain,
R: pallas::Point,
}
@ -200,6 +201,17 @@ impl CommitDomain {
}
}
/// Constructs a new `CommitDomain` from different values for `hash_domain` and `blind_domain`
pub fn new_with_personalization(hash_domain: &str, blind_domain: &str) -> Self {
let m_prefix = format!("{}-M", hash_domain);
let r_prefix = format!("{}-r", blind_domain);
let hasher_r = pallas::Point::hash_to_curve(&r_prefix);
CommitDomain {
M: HashDomain::new(&m_prefix),
R: hasher_r(&[]),
}
}
/// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit].
///
/// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit
@ -214,6 +226,26 @@ impl CommitDomain {
.map(|p| p + Wnaf::new().scalar(r).base(self.R))
}
/// $\mathsf{SinsemillaHashToPoint}$ from [§ 5.4.1.9][concretesinsemillahash].
///
/// [concretesinsemillahash]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash
pub fn hash_to_point(&self, msg: impl Iterator<Item = bool>) -> CtOption<pallas::Point> {
self.M.hash_to_point(msg)
}
/// Returns `SinsemillaCommit_r(personalization, msg) = hash_point + [r]R`
/// where `SinsemillaHash(personalization, msg) = hash_point`
/// and `R` is derived from the `personalization`.
#[allow(non_snake_case)]
pub fn commit_from_hash_point(
&self,
hash_point: CtOption<pallas::Point>,
r: &pallas::Scalar,
) -> CtOption<pallas::Point> {
// We use complete addition for the blinding factor.
hash_point.map(|p| p + Wnaf::new().scalar(r).base(self.R))
}
/// $\mathsf{SinsemillaShortCommit}$ from [§ 5.4.8.4][concretesinsemillacommit].
///
/// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit
@ -305,4 +337,32 @@ mod tests {
assert_eq!(computed, actual);
}
}
#[test]
fn commit_in_several_steps() {
use rand::{rngs::OsRng, Rng};
use ff::Field;
use crate::sinsemilla::primitives::CommitDomain;
let domain = CommitDomain::new("z.cash:ZSA-NoteCommit");
let mut os_rng = OsRng::default();
let msg: Vec<bool> = (0..36).map(|_| os_rng.gen::<bool>()).collect();
let rcm = pallas::Scalar::random(&mut os_rng);
// Evaluate the commitment with commit function
let commit1 = domain.commit(msg.clone().into_iter(), &rcm);
// Evaluate the commitment with the following steps
// 1. hash msg
// 2. evaluate the commitment from the hash point
let hash_point = domain.M.hash_to_point(msg.into_iter());
let commit2 = domain.commit_from_hash_point(hash_point, &rcm);
// Test equality
assert_eq!(commit1.unwrap(), commit2.unwrap());
}
}

View File

@ -2,12 +2,14 @@
use super::{bool_check, ternary, UtilitiesInstructions};
use crate::ecc::chip::{EccPoint, NonIdentityEccPoint};
use group::ff::{Field, PrimeField};
use halo2_proofs::{
circuit::{AssignedCell, Chip, Layouter, Value},
plonk::{Advice, Column, ConstraintSystem, Constraints, Error, Selector},
plonk::{self, Advice, Column, ConstraintSystem, Constraints, Error, Selector},
poly::Rotation,
};
use pasta_curves::pallas;
use std::marker::PhantomData;
/// Instructions for a conditional swap gadget.
@ -24,6 +26,16 @@ pub trait CondSwapInstructions<F: Field>: UtilitiesInstructions<F> {
pair: (Self::Var, Value<F>),
swap: Value<bool>,
) -> Result<(Self::Var, Self::Var), Error>;
/// Given an input `(choice, left, right)` where `choice` is a boolean flag,
/// returns `left` if `choice` is not set and `right` if `choice` is set.
fn mux(
&self,
layouter: &mut impl Layouter<F>,
choice: Self::Var,
left: Self::Var,
right: Self::Var,
) -> Result<Self::Var, Error>;
}
/// A chip implementing a conditional swap.
@ -121,6 +133,97 @@ impl<F: PrimeField> CondSwapInstructions<F> for CondSwapChip<F> {
},
)
}
fn mux(
&self,
layouter: &mut impl Layouter<F>,
choice: Self::Var,
left: Self::Var,
right: Self::Var,
) -> Result<Self::Var, Error> {
let config = self.config();
layouter.assign_region(
|| "mux",
|mut region| {
// Enable `q_swap` selector
config.q_swap.enable(&mut region, 0)?;
// Copy in `a` value
let left = left.copy_advice(|| "copy left", &mut region, config.a, 0)?;
// Copy in `b` value
let right = right.copy_advice(|| "copy right", &mut region, config.b, 0)?;
// Copy `choice` value
let choice = choice.copy_advice(|| "copy choice", &mut region, config.swap, 0)?;
let a_swapped = left
.value()
.zip(right.value())
.zip(choice.value())
.map(|((left, right), choice)| {
if *choice == F::from(0_u64) {
left
} else {
right
}
})
.cloned();
let b_swapped = left
.value()
.zip(right.value())
.zip(choice.value())
.map(|((left, right), choice)| {
if *choice == F::from(0_u64) {
right
} else {
left
}
})
.cloned();
region.assign_advice(|| "out b_swap", self.config.b_swapped, 0, || b_swapped)?;
region.assign_advice(|| "out a_swap", self.config.a_swapped, 0, || a_swapped)
},
)
}
}
impl CondSwapChip<pallas::Base> {
/// Given an input `(choice, left, right)` where `choice` is a boolean flag and `left` and `right` are `EccPoint`,
/// returns `left` if `choice` is not set and `right` if `choice` is set.
pub fn mux_on_points(
&self,
mut layouter: impl Layouter<pallas::Base>,
choice: &AssignedCell<pallas::Base, pallas::Base>,
left: &EccPoint,
right: &EccPoint,
) -> Result<EccPoint, plonk::Error> {
let x_cell = self.mux(&mut layouter, choice.clone(), left.x(), right.x())?;
let y_cell = self.mux(&mut layouter, choice.clone(), left.y(), right.y())?;
Ok(EccPoint::from_coordinates_unchecked(
x_cell.into(),
y_cell.into(),
))
}
/// Given an input `(choice, left, right)` where `choice` is a boolean flag and `left` and `right` are
/// `NonIdentityEccPoint`, returns `left` if `choice` is not set and `right` if `choice` is set.
pub fn mux_on_non_identity_points(
&self,
mut layouter: impl Layouter<pallas::Base>,
choice: &AssignedCell<pallas::Base, pallas::Base>,
left: &NonIdentityEccPoint,
right: &NonIdentityEccPoint,
) -> Result<NonIdentityEccPoint, plonk::Error> {
let x_cell = self.mux(&mut layouter, choice.clone(), left.x(), right.x())?;
let y_cell = self.mux(&mut layouter, choice.clone(), left.y(), right.y())?;
Ok(NonIdentityEccPoint::from_coordinates_unchecked(
x_cell.into(),
y_cell.into(),
))
}
}
impl<F: PrimeField> CondSwapChip<F> {
@ -291,4 +394,231 @@ mod tests {
assert_eq!(prover.verify(), Ok(()));
}
}
#[test]
fn test_mux() {
use crate::{
ecc::{
chip::{EccChip, EccConfig},
tests::TestFixedBases,
NonIdentityPoint, Point,
},
utilities::lookup_range_check::LookupRangeCheckConfig,
};
use group::{cofactor::CofactorCurveAffine, Curve, Group};
use halo2_proofs::{
circuit::{Layouter, SimpleFloorPlanner, Value},
dev::MockProver,
plonk::{Advice, Circuit, Column, ConstraintSystem, Error, Instance},
};
use pasta_curves::arithmetic::CurveAffine;
use pasta_curves::{pallas, EpAffine};
use rand::rngs::OsRng;
#[derive(Clone, Debug)]
pub struct MyConfig {
primary: Column<Instance>,
advice: Column<Advice>,
cond_swap_config: CondSwapConfig,
ecc_config: EccConfig<TestFixedBases>,
}
#[derive(Default)]
struct MyCircuit {
left_point: Value<EpAffine>,
right_point: Value<EpAffine>,
choice: Value<pallas::Base>,
}
impl Circuit<pallas::Base> for MyCircuit {
type Config = MyConfig;
type FloorPlanner = SimpleFloorPlanner;
fn without_witnesses(&self) -> Self {
Self::default()
}
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> Self::Config {
let advices = [
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
];
for advice in advices.iter() {
meta.enable_equality(*advice);
}
// Instance column used for public inputs
let primary = meta.instance_column();
meta.enable_equality(primary);
let cond_swap_config =
CondSwapChip::configure(meta, advices[0..5].try_into().unwrap());
let table_idx = meta.lookup_table_column();
let table_range_check_tag = meta.lookup_table_column();
let lagrange_coeffs = [
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
];
meta.enable_constant(lagrange_coeffs[0]);
let range_check = LookupRangeCheckConfig::configure(
meta,
advices[9],
table_idx,
table_range_check_tag,
);
let ecc_config = EccChip::<TestFixedBases>::configure(
meta,
advices,
lagrange_coeffs,
range_check,
);
MyConfig {
primary,
advice: advices[0],
cond_swap_config,
ecc_config,
}
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), Error> {
// Construct a CondSwap chip
let cond_swap_chip = CondSwapChip::construct(config.cond_swap_config);
// Construct an ECC chip
let ecc_chip = EccChip::construct(config.ecc_config);
// Assign choice
let choice = layouter.assign_region(
|| "load private",
|mut region| {
region.assign_advice(|| "load private", config.advice, 0, || self.choice)
},
)?;
// Test mux on non identity points
// Assign left point
let left_non_identity_point = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| "left point"),
self.left_point.map(|left_point| left_point),
)?;
// Assign right point
let right_non_identity_point = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| "right point"),
self.right_point.map(|right_point| right_point),
)?;
// Apply mux
let result_non_identity_point = cond_swap_chip.mux_on_non_identity_points(
layouter.namespace(|| "MUX"),
&choice,
left_non_identity_point.inner(),
right_non_identity_point.inner(),
)?;
// Check equality with instance
layouter.constrain_instance(
result_non_identity_point.x().cell(),
config.primary,
0,
)?;
layouter.constrain_instance(
result_non_identity_point.y().cell(),
config.primary,
1,
)?;
// Test mux on points
// Assign left point
let left_point = Point::new(
ecc_chip.clone(),
layouter.namespace(|| "left point"),
self.left_point.map(|left_point| left_point),
)?;
// Assign right point
let right_point = Point::new(
ecc_chip,
layouter.namespace(|| "right point"),
self.right_point.map(|right_point| right_point),
)?;
// Apply mux
let result = cond_swap_chip.mux_on_points(
layouter.namespace(|| "MUX"),
&choice,
left_point.inner(),
right_point.inner(),
)?;
// Check equality with instance
layouter.constrain_instance(result.x().cell(), config.primary, 0)?;
layouter.constrain_instance(result.y().cell(), config.primary, 1)
}
}
// Test different circuits
let mut circuits = vec![];
let mut instances = vec![];
for choice in [false, true] {
let choice_value = if choice {
pallas::Base::one()
} else {
pallas::Base::zero()
};
let left_point = pallas::Point::random(OsRng).to_affine();
let right_point = pallas::Point::random(OsRng).to_affine();
circuits.push(MyCircuit {
left_point: Value::known(left_point),
right_point: Value::known(right_point),
choice: Value::known(choice_value),
});
let expected_output = if choice { right_point } else { left_point };
let (expected_x, expected_y) = if bool::from(expected_output.is_identity()) {
(pallas::Base::zero(), pallas::Base::zero())
} else {
let coords = expected_output.coordinates().unwrap();
(*coords.x(), *coords.y())
};
instances.push([[expected_x, expected_y]]);
}
for (circuit, instance) in circuits.iter().zip(instances.iter()) {
let prover = MockProver::<pallas::Base>::run(
5,
circuit,
instance.iter().map(|p| p.to_vec()).collect(),
)
.unwrap();
assert_eq!(prover.verify(), Ok(()));
}
}
}

View File

@ -60,8 +60,11 @@ pub struct LookupRangeCheckConfig<F: PrimeFieldBits, const K: usize> {
q_lookup: Selector,
q_running: Selector,
q_bitshift: Selector,
q_range_check_4: Selector,
q_range_check_5: Selector,
running_sum: Column<Advice>,
table_idx: TableColumn,
table_range_check_tag: TableColumn,
_marker: PhantomData<F>,
}
@ -81,18 +84,24 @@ impl<F: PrimeFieldBits, const K: usize> LookupRangeCheckConfig<F, K> {
meta: &mut ConstraintSystem<F>,
running_sum: Column<Advice>,
table_idx: TableColumn,
table_range_check_tag: TableColumn,
) -> Self {
meta.enable_equality(running_sum);
let q_lookup = meta.complex_selector();
let q_running = meta.complex_selector();
let q_bitshift = meta.selector();
let q_range_check_4 = meta.complex_selector();
let q_range_check_5 = meta.complex_selector();
let config = LookupRangeCheckConfig {
q_lookup,
q_running,
q_bitshift,
q_range_check_4,
q_range_check_5,
running_sum,
table_idx,
table_range_check_tag,
_marker: PhantomData,
};
@ -100,7 +109,10 @@ impl<F: PrimeFieldBits, const K: usize> LookupRangeCheckConfig<F, K> {
meta.lookup(|meta| {
let q_lookup = meta.query_selector(config.q_lookup);
let q_running = meta.query_selector(config.q_running);
let q_range_check_4 = meta.query_selector(config.q_range_check_4);
let q_range_check_5 = meta.query_selector(config.q_range_check_5);
let z_cur = meta.query_advice(config.running_sum, Rotation::cur());
let one = Expression::Constant(F::ONE);
// In the case of a running sum decomposition, we recover the word from
// the difference of the running sums:
@ -117,17 +129,40 @@ impl<F: PrimeFieldBits, const K: usize> LookupRangeCheckConfig<F, K> {
// In the short range check, the word is directly witnessed.
let short_lookup = {
let short_word = z_cur;
let q_short = Expression::Constant(F::ONE) - q_running;
let short_word = z_cur.clone();
let q_short = one.clone() - q_running;
q_short * short_word
};
// Combine the running sum and short lookups:
vec![(
q_lookup * (running_sum_lookup + short_lookup),
config.table_idx,
)]
// q_range_check is equal to
// - 1 if q_range_check_4 = 1 or q_range_check_5 = 1
// - 0 otherwise
let q_range_check = one.clone()
- (one.clone() - q_range_check_4.clone()) * (one.clone() - q_range_check_5.clone());
// num_bits is equal to
// - 5 if q_range_check_5 = 1
// - 4 if q_range_check_4 = 1 and q_range_check_5 = 0
// - 0 otherwise
let num_bits = q_range_check_5.clone() * Expression::Constant(F::from(5_u64))
+ (one.clone() - q_range_check_5)
* q_range_check_4
* Expression::Constant(F::from(4_u64));
// Combine the running sum, short lookups and optimized range checks:
vec![
(
q_lookup.clone()
* ((one - q_range_check.clone()) * (running_sum_lookup + short_lookup)
+ q_range_check.clone() * z_cur),
config.table_idx,
),
(
q_lookup * q_range_check * num_bits,
config.table_range_check_tag,
),
]
});
// For short lookups, check that the word has been shifted by the correct number of bits.
@ -152,9 +187,9 @@ impl<F: PrimeFieldBits, const K: usize> LookupRangeCheckConfig<F, K> {
}
#[cfg(test)]
// Loads the values [0..2^K) into `table_idx`. This is only used in testing
// for now, since the Sinsemilla chip provides a pre-loaded table in the
// Orchard context.
// Fill `table_idx` and `table_range_check_tag`.
// This is only used in testing for now, since the Sinsemilla chip provides a pre-loaded table
// in the Orchard context.
pub fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
layouter.assign_table(
|| "table_idx",
@ -167,6 +202,42 @@ impl<F: PrimeFieldBits, const K: usize> LookupRangeCheckConfig<F, K> {
index,
|| Value::known(F::from(index as u64)),
)?;
table.assign_cell(
|| "table_range_check_tag",
self.table_range_check_tag,
index,
|| Value::known(F::ZERO),
)?;
}
for index in 0..(1 << 4) {
let new_index = index + (1 << K);
table.assign_cell(
|| "table_idx",
self.table_idx,
new_index,
|| Value::known(F::from(index as u64)),
)?;
table.assign_cell(
|| "table_range_check_tag",
self.table_range_check_tag,
new_index,
|| Value::known(F::from(4_u64)),
)?;
}
for index in 0..(1 << 5) {
let new_index = index + (1 << K) + (1 << 4);
table.assign_cell(
|| "table_idx",
self.table_idx,
new_index,
|| Value::known(F::from(index as u64)),
)?;
table.assign_cell(
|| "table_range_check_tag",
self.table_range_check_tag,
new_index,
|| Value::known(F::from(5_u64)),
)?;
}
Ok(())
},
@ -350,33 +421,43 @@ impl<F: PrimeFieldBits, const K: usize> LookupRangeCheckConfig<F, K> {
element: AssignedCell<F, F>,
num_bits: usize,
) -> Result<(), Error> {
// Enable lookup for `element`, to constrain it to 10 bits.
// Enable lookup for `element`.
self.q_lookup.enable(region, 0)?;
// Enable lookup for shifted element, to constrain it to 10 bits.
self.q_lookup.enable(region, 1)?;
match num_bits {
4 => {
self.q_range_check_4.enable(region, 0)?;
}
5 => {
self.q_range_check_5.enable(region, 0)?;
}
_ => {
// Enable lookup for shifted element, to constrain it to 10 bits.
self.q_lookup.enable(region, 1)?;
// Check element has been shifted by the correct number of bits.
self.q_bitshift.enable(region, 1)?;
// Check element has been shifted by the correct number of bits.
self.q_bitshift.enable(region, 1)?;
// Assign shifted `element * 2^{K - num_bits}`
let shifted = element.value().into_field() * F::from(1 << (K - num_bits));
// Assign shifted `element * 2^{K - num_bits}`
let shifted = element.value().into_field() * F::from(1 << (K - num_bits));
region.assign_advice(
|| format!("element * 2^({}-{})", K, num_bits),
self.running_sum,
1,
|| shifted,
)?;
region.assign_advice(
|| format!("element * 2^({}-{})", K, num_bits),
self.running_sum,
1,
|| shifted,
)?;
// Assign 2^{-num_bits} from a fixed column.
let inv_two_pow_s = F::from(1 << num_bits).invert().unwrap();
region.assign_advice_from_constant(
|| format!("2^(-{})", num_bits),
self.running_sum,
2,
inv_two_pow_s,
)?;
// Assign 2^{-num_bits} from a fixed column.
let inv_two_pow_s = F::from(1 << num_bits).invert().unwrap();
region.assign_advice_from_constant(
|| format!("2^(-{})", num_bits),
self.running_sum,
2,
inv_two_pow_s,
)?;
}
}
Ok(())
}
@ -418,10 +499,16 @@ mod tests {
fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
let running_sum = meta.advice_column();
let table_idx = meta.lookup_table_column();
let table_range_check_tag = meta.lookup_table_column();
let constants = meta.fixed_column();
meta.enable_constant(constants);
LookupRangeCheckConfig::<F, K>::configure(meta, running_sum, table_idx)
LookupRangeCheckConfig::<F, K>::configure(
meta,
running_sum,
table_idx,
table_range_check_tag,
)
}
fn synthesize(
@ -517,10 +604,16 @@ mod tests {
fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
let running_sum = meta.advice_column();
let table_idx = meta.lookup_table_column();
let table_range_check_tag = meta.lookup_table_column();
let constants = meta.fixed_column();
meta.enable_constant(constants);
LookupRangeCheckConfig::<F, K>::configure(meta, running_sum, table_idx)
LookupRangeCheckConfig::<F, K>::configure(
meta,
running_sum,
table_idx,
table_range_check_tag,
)
}
fn synthesize(
@ -646,5 +739,34 @@ mod tests {
}])
);
}
// Element within 4 bits
{
let circuit: MyCircuit<pallas::Base> = MyCircuit {
element: Value::known(pallas::Base::from((1 << 4) - 1)),
num_bits: 4,
};
let prover = MockProver::<pallas::Base>::run(11, &circuit, vec![]).unwrap();
assert_eq!(prover.verify(), Ok(()));
}
// Element larger than 5 bits
{
let circuit: MyCircuit<pallas::Base> = MyCircuit {
element: Value::known(pallas::Base::from(1 << 5)),
num_bits: 5,
};
let prover = MockProver::<pallas::Base>::run(11, &circuit, vec![]).unwrap();
assert_eq!(
prover.verify(),
Err(vec![VerifyFailure::Lookup {
lookup_index: 0,
location: FailureLocation::InRegion {
region: (1, "Range check 5 bits").into(),
offset: 0,
},
}])
);
}
}
}

View File

@ -139,6 +139,16 @@ impl<F: Field> AssignedCell<Assigned<F>, F> {
}
}
impl<F: Field> From<AssignedCell<F, F>> for AssignedCell<Assigned<F>, F> {
fn from(ac: AssignedCell<F, F>) -> Self {
AssignedCell {
value: ac.value.map(|a| a.into()),
cell: ac.cell,
_marker: Default::default(),
}
}
}
impl<V: Clone, F: Field> AssignedCell<V, F>
where
for<'v> Assigned<F>: From<&'v V>,