#[cfg(test)]
use ff::Field;
use halo2_gadgets::ecc::{
chip::EccChip, FixedPointBaseField, FixedPointShort, NonIdentityPoint, ScalarFixedShort,
ScalarVar,
};
use halo2_proofs::{
circuit::{AssignedCell, Layouter},
plonk::{Advice, Column, Error, Instance as InstanceColumn},
};
use orchard::constants::{OrchardBaseFieldBases, OrchardFixedBases, OrchardShortScalarBases};
#[cfg(test)]
use pasta_curves::arithmetic::CurveAffine;
use pasta_curves::pallas;
use super::nonzero::NonZeroConfig;
pub(crate) struct EaPkInstanceLoc {
pub instance: Column<InstanceColumn>,
pub x_row: usize,
pub y_row: usize,
}
pub fn spend_auth_g_affine() -> pallas::Affine {
orchard::constants::fixed_bases::spend_auth_g::generator()
}
pub(crate) fn base_to_scalar(b: pallas::Base) -> Option<pallas::Scalar> {
use ff::PrimeField;
pallas::Scalar::from_repr(b.to_repr()).into()
}
#[cfg(test)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ElGamalEncryptError {
ZeroRandomness,
InvalidShareValue,
InvalidRandomness,
IdentityCiphertext { component: &'static str },
}
#[cfg(test)]
impl core::fmt::Display for ElGamalEncryptError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ElGamalEncryptError::ZeroRandomness => {
write!(f, "El Gamal randomness must be non-zero")
}
ElGamalEncryptError::InvalidShareValue => {
write!(f, "share value is not representable as a scalar")
}
ElGamalEncryptError::InvalidRandomness => {
write!(f, "randomness is not representable as a scalar")
}
ElGamalEncryptError::IdentityCiphertext { component } => {
write!(f, "El Gamal {component} point is the identity")
}
}
}
}
#[cfg(test)]
impl std::error::Error for ElGamalEncryptError {}
#[cfg(test)]
pub(crate) fn elgamal_encrypt(
share_value: pallas::Base,
randomness: pallas::Base,
ea_pk: pallas::Affine,
) -> Result<(pallas::Base, pallas::Base, pallas::Base, pallas::Base), ElGamalEncryptError> {
use group::Curve;
let g = spend_auth_g_affine();
if bool::from(randomness.is_zero()) {
return Err(ElGamalEncryptError::ZeroRandomness);
}
let r_scalar = base_to_scalar(randomness).ok_or(ElGamalEncryptError::InvalidRandomness)?;
let v_scalar = base_to_scalar(share_value).ok_or(ElGamalEncryptError::InvalidShareValue)?;
let c1 = g * r_scalar;
let c2 = g * v_scalar + ea_pk * r_scalar;
let c1_affine = c1.to_affine();
let c2_affine = c2.to_affine();
let c1_coords = c1_affine
.coordinates()
.into_option()
.ok_or(ElGamalEncryptError::IdentityCiphertext { component: "C1" })?;
let c2_coords = c2_affine
.coordinates()
.into_option()
.ok_or(ElGamalEncryptError::IdentityCiphertext { component: "C2" })?;
Ok((
*c1_coords.x(),
*c2_coords.x(),
*c1_coords.y(),
*c2_coords.y(),
))
}
pub(crate) fn prove_elgamal_encryptions(
ecc_chip: EccChip<OrchardFixedBases>,
nonzero: NonZeroConfig,
mut layouter: impl Layouter<pallas::Base>,
namespace: &str,
ea_pk: halo2_proofs::circuit::Value<pallas::Affine>,
ea_pk_loc: EaPkInstanceLoc,
advice_col: Column<Advice>,
share_cells: [AssignedCell<pallas::Base, pallas::Base>; 16],
r_cells: [AssignedCell<pallas::Base, pallas::Base>; 16],
enc_c1_cells: [AssignedCell<pallas::Base, pallas::Base>; 16],
enc_c2_cells: [AssignedCell<pallas::Base, pallas::Base>; 16],
enc_c1_y_cells: [AssignedCell<pallas::Base, pallas::Base>; 16],
enc_c2_y_cells: [AssignedCell<pallas::Base, pallas::Base>; 16],
) -> Result<(), Error> {
let ea_pk_point = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| format!("{namespace} ea_pk witness")),
ea_pk,
)?;
layouter.constrain_instance(
ea_pk_point.inner().x().cell(),
ea_pk_loc.instance,
ea_pk_loc.x_row,
)?;
layouter.constrain_instance(
ea_pk_point.inner().y().cell(),
ea_pk_loc.instance,
ea_pk_loc.y_row,
)?;
let spend_auth_g_base =
FixedPointBaseField::from_inner(ecc_chip.clone(), OrchardBaseFieldBases::SpendAuthGBase);
let spend_auth_g_short =
FixedPointShort::from_inner(ecc_chip.clone(), OrchardShortScalarBases::SpendAuthGShort);
for i in 0..16 {
nonzero.constrain_nonzero(
layouter.namespace(|| format!("{namespace} r[{i}] != 0")),
"El Gamal randomness != 0",
&r_cells[i],
)?;
let c1_point = spend_auth_g_base.clone().mul(
layouter.namespace(|| format!("{namespace} [r_{i}] * G")),
r_cells[i].clone(),
)?;
let c1_x = c1_point.extract_p().inner().clone();
layouter.assign_region(
|| format!("{namespace} C1[{i}] x == enc_c1_x[{i}]"),
|mut region| region.constrain_equal(c1_x.cell(), enc_c1_cells[i].cell()),
)?;
let c1_y = c1_point.inner().y();
layouter.assign_region(
|| format!("{namespace} C1[{i}] y == enc_c1_y[{i}]"),
|mut region| region.constrain_equal(c1_y.cell(), enc_c1_y_cells[i].cell()),
)?;
let sign_one = layouter.assign_region(
|| format!("{namespace} sign_one[{i}]"),
|mut region| {
region.assign_advice_from_constant(
|| "sign = +1",
advice_col,
0,
pallas::Base::one(),
)
},
)?;
let v_scalar = ScalarFixedShort::new(
ecc_chip.clone(),
layouter.namespace(|| format!("{namespace} v_{i} short scalar")),
(share_cells[i].clone(), sign_one),
)?;
let (v_g_point, _) = spend_auth_g_short.clone().mul(
layouter.namespace(|| format!("{namespace} [v_{i}] * G")),
v_scalar,
)?;
let r_i_scalar = ScalarVar::from_base(
ecc_chip.clone(),
layouter.namespace(|| format!("{namespace} r[{i}] to ScalarVar")),
&r_cells[i],
)?;
let (r_ea_pk_point, _) = ea_pk_point.mul(
layouter.namespace(|| format!("{namespace} [r_{i}] * ea_pk")),
r_i_scalar,
)?;
let c2_point = v_g_point.add(
layouter.namespace(|| format!("{namespace} C2[{i}] = vG + rP")),
&r_ea_pk_point,
)?;
let c2_x = c2_point.extract_p().inner().clone();
layouter.assign_region(
|| format!("{namespace} C2[{i}] x == enc_c2_x[{i}]"),
|mut region| region.constrain_equal(c2_x.cell(), enc_c2_cells[i].cell()),
)?;
let c2_y = c2_point.inner().y();
layouter.assign_region(
|| format!("{namespace} C2[{i}] y == enc_c2_y[{i}]"),
|mut region| region.constrain_equal(c2_y.cell(), enc_c2_y_cells[i].cell()),
)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use group::{prime::PrimeCurveAffine, Curve};
#[test]
fn elgamal_encrypt_rejects_zero_randomness() {
let ea_pk = spend_auth_g_affine();
let err = elgamal_encrypt(pallas::Base::from(1), pallas::Base::zero(), ea_pk)
.expect_err("zero randomness should be rejected without panicking");
assert_eq!(err, ElGamalEncryptError::ZeroRandomness);
}
#[test]
fn elgamal_encrypt_returns_slots_in_documented_order() {
let share_value = pallas::Base::from(7u64);
let randomness = pallas::Base::from(11u64);
let g = spend_auth_g_affine();
let ea_pk = (g * pallas::Scalar::from(13u64)).to_affine();
let (c1_x, c2_x, c1_y, c2_y) = elgamal_encrypt(share_value, randomness, ea_pk)
.expect("test encryption inputs should produce non-identity ciphertext points");
let r_scalar = base_to_scalar(randomness).expect("test randomness should fit scalar field");
let v_scalar = base_to_scalar(share_value).expect("test share should fit scalar field");
let expected_c1 = (g * r_scalar).to_affine();
let expected_c2 = (g * v_scalar + ea_pk * r_scalar).to_affine();
let expected_c1_coords = expected_c1
.coordinates()
.into_option()
.expect("non-zero randomness should produce non-identity C1");
let expected_c2_coords = expected_c2
.coordinates()
.into_option()
.expect("chosen test inputs should produce non-identity C2");
assert_eq!(c1_x, *expected_c1_coords.x(), "slot 0 must be C1.x");
assert_eq!(c2_x, *expected_c2_coords.x(), "slot 1 must be C2.x");
assert_eq!(c1_y, *expected_c1_coords.y(), "slot 2 must be C1.y");
assert_eq!(c2_y, *expected_c2_coords.y(), "slot 3 must be C2.y");
}
#[test]
fn elgamal_encrypt_rejects_identity_c2() {
let err = elgamal_encrypt(
pallas::Base::zero(),
pallas::Base::from(1),
pallas::Affine::identity(),
)
.expect_err("zero share under identity key should produce identity C2");
assert_eq!(
err,
ElGamalEncryptError::IdentityCiphertext { component: "C2" }
);
}
}