use crate::{
bls12381::primitives::group::{Scalar, ScalarReadCfg},
zk::circuit::{BoolVar, Context, Selector, Var},
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use blst::blst_fr;
use bytes::{Buf, BufMut};
use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt, Write};
use commonware_math::algebra::{
msm_naive, Additive, CryptoGroup, Field, HashToGroup, Multiplicative, Object, Random, Ring,
Space,
};
use commonware_parallel::Strategy;
use core::{
array,
ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct F {
limbs: [u64; 4],
}
impl F {
const BITS: usize = 253;
const R: [u64; 4] = [
0x74fd_06b5_2876_e7e1,
0xff8f_8700_7419_0471,
0x0cce_7602_0268_7600,
0x1cfb_69d4_ca67_5f52,
];
pub fn bits(&self) -> Vec<bool> {
(0..Self::BITS)
.map(|i| (self.limbs[i / 64] >> (i % 64)) & 1 == 1)
.collect()
}
}
impl Random for F {
fn random(mut rng: impl rand_core::CryptoRng) -> Self {
loop {
let mut limbs: [u64; 4] = array::from_fn(|_| rng.next_u64());
limbs[3] &= (1u64 << (Self::BITS - 192)) - 1;
let mut borrow = false;
for (&x, &r) in limbs.iter().zip(Self::R.iter()) {
let (d, b) = x.overflowing_sub(r);
let (_, c) = d.overflowing_sub(borrow as u64);
borrow = b | c;
}
if borrow {
return Self { limbs };
}
}
}
}
impl<'a> AddAssign<&'a Self> for F {
fn add_assign(&mut self, rhs: &'a Self) {
let mut sum = [0u64; 4];
let mut carry = false;
for (s, (&x, &y)) in sum.iter_mut().zip(self.limbs.iter().zip(rhs.limbs.iter())) {
let (a, b) = x.overflowing_add(y);
let (a, c) = a.overflowing_add(carry as u64);
*s = a;
carry = b | c;
}
let mut diff = [0u64; 4];
let mut borrow = false;
for (d, (&s, &r)) in diff.iter_mut().zip(sum.iter().zip(Self::R.iter())) {
let (x, b) = s.overflowing_sub(r);
let (x, c) = x.overflowing_sub(borrow as u64);
*d = x;
borrow = b | c;
}
self.limbs = if borrow { sum } else { diff };
}
}
impl<'a> Add<&'a Self> for F {
type Output = Self;
fn add(mut self, rhs: &'a Self) -> Self::Output {
self += rhs;
self
}
}
impl Neg for F {
type Output = Self;
fn neg(self) -> Self::Output {
if self.limbs == [0u64; 4] {
return self;
}
let mut diff = [0u64; 4];
let mut borrow = false;
for (d, (&r, &x)) in diff.iter_mut().zip(Self::R.iter().zip(self.limbs.iter())) {
let (v, b) = r.overflowing_sub(x);
let (v, c) = v.overflowing_sub(borrow as u64);
*d = v;
borrow = b | c;
}
Self { limbs: diff }
}
}
impl<'a> SubAssign<&'a Self> for F {
fn sub_assign(&mut self, rhs: &'a Self) {
let rhs = -rhs.clone();
*self += &rhs;
}
}
impl<'a> Sub<&'a Self> for F {
type Output = Self;
fn sub(mut self, rhs: &'a Self) -> Self::Output {
self -= rhs;
self
}
}
impl<'a> MulAssign<&'a Self> for F {
fn mul_assign(&mut self, rhs: &'a Self) {
*self = self.scale(&rhs.limbs);
}
}
impl<'a> Mul<&'a Self> for F {
type Output = Self;
fn mul(mut self, rhs: &'a Self) -> Self::Output {
self *= rhs;
self
}
}
impl Object for F {}
impl Additive for F {
fn zero() -> Self {
Self { limbs: [0u64; 4] }
}
}
impl Multiplicative for F {}
impl Ring for F {
fn one() -> Self {
Self {
limbs: [1, 0, 0, 0],
}
}
}
impl Field for F {
fn inv(&self) -> Self {
self.exp(&[Self::R[0] - 2, Self::R[1], Self::R[2], Self::R[3]])
}
}
impl FixedSize for F {
const SIZE: usize = 32;
}
impl Write for F {
fn write(&self, buf: &mut impl BufMut) {
for limb in self.limbs {
buf.put_u64_le(limb);
}
}
}
impl Read for F {
type Cfg = ();
fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
let bytes = <[u8; 32]>::read(buf)?;
let limbs =
array::from_fn(|i| u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().unwrap()));
let mut borrow = false;
for (&x, &r) in limbs.iter().zip(Self::R.iter()) {
let (d, b) = x.overflowing_sub(r);
let (_, c) = d.overflowing_sub(borrow as u64);
borrow = b | c;
}
if !borrow {
return Err(CodecError::Invalid(
"banderwagon::F",
"scalar not canonical",
));
}
Ok(Self { limbs })
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl arbitrary::Arbitrary<'_> for F {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let limbs: [u64; 4] = [
u.arbitrary()?,
u.arbitrary()?,
u.arbitrary()?,
u.arbitrary()?,
];
Ok(Self::one().scale(&limbs))
}
}
#[allow(dead_code)]
const A: Scalar = Scalar(blst_fr {
l: [
0xffff_fff4_0000_000c,
0xece3_b023_ffec_4ff3,
0x66b6_2060_7396_203f,
0x6f23_d7e5_f361_df62,
],
});
#[allow(dead_code)]
const D: Scalar = Scalar(blst_fr {
l: [
0xa8dc_ed1b_47a2_c730,
0x381c_065a_ad3c_ccc7,
0x53ff_52e1_1883_51f8,
0x362e_8d63_990f_e940,
],
});
#[derive(Clone, Debug)]
pub struct G {
x: Scalar,
y: Scalar,
t: Scalar,
z: Scalar,
}
impl PartialEq for G {
fn eq(&self, other: &Self) -> bool {
self.x.clone() * &other.y == other.x.clone() * &self.y
}
}
impl Eq for G {}
impl CryptoGroup for G {
type Scalar = F;
fn generator() -> Self {
Self {
x: Scalar(blst_fr {
l: [
0xec26_27e1_e7ab_47f5,
0x3e63_de48_4f01_aa9c,
0xfe0f_5c3b_5394_6dc4,
0x2d71_920b_aeb2_cfcd,
],
}),
y: Scalar(blst_fr {
l: [
0x4e30_593e_1895_bd34,
0x156d_738f_32af_be4b,
0x45ef_0b1c_cdeb_75f4,
0x6a7c_ca00_37d2_e71f,
],
}),
t: Scalar(blst_fr {
l: [
0x5a92_e8f6_97ad_b6b9,
0xf138_8d46_06b1_4609,
0x101c_7836_40a6_4516,
0x1e9a_e707_3cc7_a9fc,
],
}),
z: Scalar(blst_fr {
l: [
0x0000_0001_ffff_fffe,
0x5884_b7fa_0003_4802,
0x998c_4fef_ecbc_4ff5,
0x1824_b159_acc5_056f,
],
}),
}
}
}
impl Object for G {}
impl<'a> AddAssign<&'a Self> for G {
fn add_assign(&mut self, rhs: &'a Self) {
let Self {
x: x1,
y: y1,
t: t1,
z: z1,
} = core::mem::replace(self, Self::zero());
let Self {
x: x2,
y: y2,
t: t2,
z: z2,
} = rhs;
let x1_y2 = x1.clone() * y2;
let y1_x2 = y1.clone() * x2;
let y1_y2 = y1 * y2;
let x1_x2 = x1 * x2;
let z1_z2 = z1 * z2;
let t1_t2 = t1 * t2;
let x1_y2_plus_y1_x2 = x1_y2 + &y1_x2;
let y1_y2_minus_a_x1_x2 = y1_y2 - &(x1_x2 * &A);
let d_t1_t2 = t1_t2 * &D;
let z_minus_d_t = z1_z2.clone() - &d_t1_t2;
let z_plus_d_t = z1_z2 + &d_t1_t2;
*self = Self {
x: x1_y2_plus_y1_x2.clone() * &z_minus_d_t,
y: y1_y2_minus_a_x1_x2.clone() * &z_plus_d_t,
t: x1_y2_plus_y1_x2 * &y1_y2_minus_a_x1_x2,
z: z_minus_d_t * &z_plus_d_t,
}
}
}
impl<'a> Add<&'a Self> for G {
type Output = Self;
fn add(mut self, rhs: &'a Self) -> Self::Output {
self += rhs;
self
}
}
impl<'a> SubAssign<&'a Self> for G {
fn sub_assign(&mut self, rhs: &'a Self) {
let rhs = -rhs.clone();
*self += &rhs;
}
}
impl<'a> Sub<&'a Self> for G {
type Output = Self;
fn sub(mut self, rhs: &'a Self) -> Self::Output {
self -= rhs;
self
}
}
impl Neg for G {
type Output = Self;
fn neg(self) -> Self::Output {
Self {
x: -self.x,
y: self.y,
t: -self.t,
z: self.z,
}
}
}
impl Additive for G {
fn zero() -> Self {
Self {
x: Scalar::zero(),
y: Scalar::one(),
t: Scalar::zero(),
z: Scalar::one(),
}
}
fn double(&mut self) {
let x_sq = {
let mut out = self.x.clone();
out.square();
out
};
let y_sq = {
let mut out = self.y.clone();
out.square();
out
};
let z_sq_twice = {
let mut out = self.z.clone();
out.square();
out.double();
out
};
let a_x_sq = x_sq.clone() * &A;
let x_plus_y_sq = {
let mut out = self.x.clone() + &self.y;
out.square();
out -= &x_sq;
out -= &y_sq;
out
};
let g = a_x_sq.clone() + &y_sq;
let f = g.clone() - &z_sq_twice;
let h = a_x_sq - &y_sq;
self.x = x_plus_y_sq.clone() * &f;
self.y = g.clone() * &h;
self.t = x_plus_y_sq * &h;
self.z = f * &g;
}
}
impl<'a> MulAssign<&'a F> for G {
fn mul_assign(&mut self, rhs: &'a F) {
*self = self.scale(&rhs.limbs);
}
}
impl<'a> Mul<&'a F> for G {
type Output = Self;
fn mul(mut self, rhs: &'a F) -> Self::Output {
self *= rhs;
self
}
}
impl Space<F> for G {
fn msm(points: &[Self], scalars: &[F], _strategy: &impl Strategy) -> Self {
msm_naive(points, scalars)
}
}
impl Write for G {
fn write(&self, buf: &mut impl BufMut) {
let z_inv = self.z.inv();
let x = self.x.clone() * &z_inv;
let y = self.y.clone() * &z_inv;
let u = if y.is_positive() { x } else { -x };
u.write(buf);
}
}
impl FixedSize for G {
const SIZE: usize = Scalar::SIZE;
}
impl G {
fn affine(&self) -> (Scalar, Scalar) {
let z_inv = self.z.inv();
(self.x.clone() * &z_inv, self.y.clone() * &z_inv)
}
pub fn scalar_mul_x_squared_base(&self, scalar: &Scalar) -> Scalar {
let (mut x, _) = self.scale(&scalar_limbs(scalar)).affine();
x.square();
x
}
pub fn scalar_mul_x_squared_f(&self, x: &F) -> Scalar {
let (mut a, _) = (self.clone() * x).affine();
a.square();
a
}
fn canonicalize(&self) -> Self {
let (x, y) = self.affine();
let u = if y.is_positive() { x } else { -x };
Self::from_x(u).expect("a valid subgroup element re-derives from its abscissa")
}
fn from_x(x: Scalar) -> Option<Self> {
let one = Scalar::one();
let x_sq = {
let mut out = x.clone();
out.square();
out
};
let num = one.clone() - &(x_sq.clone() * &A);
let den = one.clone() - &(x_sq * &D);
if !num.is_square() {
return None;
}
let ratio = num * &den.inv();
let mut y = ratio.sqrt()?;
if !y.is_positive() {
y = -y;
}
let t = x.clone() * &y;
Some(Self { x, y, t, z: one })
}
}
impl HashToGroup for G {
fn hash_to_group(domain_separator: &[u8], message: &[u8]) -> Self {
let mut data = Vec::with_capacity(message.len() + 8);
data.extend_from_slice(message);
data.extend_from_slice(&[0u8; 8]);
let counter_at = message.len();
let mut counter: u64 = 0;
loop {
data[counter_at..].copy_from_slice(&counter.to_be_bytes());
if let Some(p) = Self::from_x(Scalar::map(domain_separator, &data)) {
return p;
}
counter += 1;
}
}
}
impl Read for G {
type Cfg = ();
fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
let bytes = <[u8; 32]>::read(buf)?;
let mut bytes = bytes.as_ref();
let x = Scalar::read_cfg(&mut bytes, &ScalarReadCfg::AllowZero)
.map_err(|_| CodecError::Invalid("Banderwagon", "x not a canonical field element"))?;
Self::from_x(x).ok_or(CodecError::Invalid("Banderwagon", "point not in subgroup"))
}
}
#[derive(Clone)]
pub struct GVar<'ctx> {
x: Var<'ctx, Scalar>,
y: Var<'ctx, Scalar>,
}
impl<'ctx> AddAssign<&Self> for GVar<'ctx> {
fn add_assign(&mut self, rhs: &Self) {
let Self { x: x1, y: y1 } = core::mem::replace(self, Self::identity());
let Self { x: x2, y: y2 } = rhs;
let a = Var::native(A);
let d = Var::native(D);
let x1_x2 = x1.clone() * x2; let y1_y2 = y1.clone() * y2; let u = (x1 + &y1) * &(x2.clone() + y2); let ab = x1_x2.clone() * &y1_y2; let c = d * &ab;
let x_num = u - &x1_x2 - &y1_y2; let y_num = y1_y2 - &(a * &x1_x2); let one = Var::one();
let x_den = one.clone() + &c; let y_den = one - &c;
*self = Self {
x: x_num / &x_den,
y: y_num / &y_den,
};
}
}
impl<'ctx> Add<&Self> for GVar<'ctx> {
type Output = Self;
fn add(mut self, rhs: &Self) -> Self::Output {
self += rhs;
self
}
}
const SCALAR_BITS: usize = 255;
impl<'ctx> GVar<'ctx> {
fn identity() -> Self {
Self {
x: Var::zero(),
y: Var::one(),
}
}
pub fn constant(point: &G) -> Self {
let (x, y) = point.canonicalize().affine();
Self {
x: Var::native(x),
y: Var::native(y),
}
}
fn select(bit: &BoolVar<'ctx, Scalar>, on_true: &Self, on_false: &Self) -> Self {
Self {
x: bit.select(&on_true.x, &on_false.x),
y: bit.select(&on_true.y, &on_false.y),
}
}
pub fn assert_eq(&self, other: &Self) {
(self.x.clone() * &other.y).assert_eq(&(other.x.clone() * &self.y));
}
pub fn x_squared(&self) -> Var<'ctx, Scalar> {
self.x.clone() * &self.x
}
fn mul_bits(self, bits: &[BoolVar<'ctx, Scalar>]) -> Self {
let mut acc = Self::identity();
let mut cur = self;
for bit in bits {
let added = acc.clone() + &cur;
acc = Self::select(bit, &added, &acc);
cur = cur.clone() + &cur;
}
acc
}
}
fn scalar_limbs(x: &Scalar) -> [u64; 4] {
let bytes = x.as_blst_scalar().b;
array::from_fn(|i| u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().unwrap()))
}
pub fn scalar_bits_le<'ctx>(
ctx: Context<'ctx, Scalar>,
x: &Var<'ctx, Scalar>,
) -> Vec<BoolVar<'ctx, Scalar>> {
let bits: Vec<BoolVar<'ctx, Scalar>> = (0..SCALAR_BITS)
.map(|i| {
let x = x.clone();
BoolVar::witness(ctx, move |v| {
(x.value(v).as_blst_scalar().b[i / 8] >> (i % 8)) & 1 == 1
})
})
.collect();
let mut acc = Var::zero();
let mut pow = Scalar::one();
for bit in &bits {
acc += &(bit.var().clone() * &Var::native(pow.clone()));
pow.double();
}
acc.assert_eq(x);
let c = (-Scalar::one()).as_blst_scalar().b;
let mut run = BoolVar::constant(true);
for i in (0..SCALAR_BITS).rev() {
if (c[i / 8] >> (i % 8)) & 1 == 1 {
run = run & bits[i].clone();
} else {
(run.clone() & bits[i].clone()).assert_eq(&BoolVar::constant(false));
}
}
bits
}
impl G {
pub(crate) fn scalar_mul<'ctx>(
&self,
ctx: Context<'ctx, Scalar>,
scalar: &Var<'ctx, Scalar>,
) -> GVar<'ctx> {
let bits = scalar_bits_le(ctx, scalar);
GVar::constant(self).mul_bits(&bits)
}
pub fn scalar_mul_many_in_circuit<'ctx, const WINDOW: usize>(
bases: &[Self],
bits: &[BoolVar<'ctx, Scalar>],
) -> Vec<GVar<'ctx>> {
const {
assert!(WINDOW >= 1 && WINDOW <= 8, "window size must be in 1..=8");
}
let mut accs = vec![GVar::identity(); bases.len()];
let mut shifted = bases.to_vec();
for window in bits.chunks(WINDOW) {
let selector = Selector::new(window);
for (acc, base) in accs.iter_mut().zip(&shifted) {
let (xs, ys): (Vec<_>, Vec<_>) = (0..(1usize << window.len()))
.scan(Self::zero(), |p, _| {
let (x, y) = p.canonicalize().affine();
*p += base;
Some((x, y))
})
.unzip();
*acc += &GVar {
x: selector.select_constant(&xs),
y: selector.select_constant(&ys),
}
}
for base in &mut shifted {
for _ in 0..window.len() {
base.double();
}
}
}
accs
}
pub fn scalar_mul_x_squared<'ctx>(
&self,
ctx: Context<'ctx, Scalar>,
scalar: &Var<'ctx, Scalar>,
) -> Var<'ctx, Scalar> {
let x = self.scalar_mul(ctx, scalar).x;
x.clone() * &x
}
pub fn scalar_mul_bits<'ctx>(&self, bits: &[BoolVar<'ctx, Scalar>]) -> GVar<'ctx> {
GVar::constant(self).mul_bits(bits)
}
}
#[cfg(test)]
mod tests {
use super::*;
use arbitrary::Unstructured;
use commonware_codec::{DecodeExt, Encode, EncodeFixed};
use commonware_invariants::minifuzz;
fn arbitrary_point(u: &mut Unstructured<'_>) -> arbitrary::Result<G> {
Ok(G::generator()
* &F {
limbs: [u.arbitrary()?, 0, 0, 0],
})
}
#[test]
fn test_field_laws() {
minifuzz::test(commonware_math::algebra::test_suites::fuzz_field::<F>);
}
#[test]
fn test_random_canonical_and_full_range() {
let mut rng = commonware_utils::test_rng();
let mut saw_top_bit = false;
for _ in 0..1000 {
let f = F::random(&mut rng);
let mut borrow = false;
for (&x, &r) in f.limbs.iter().zip(F::R.iter()) {
let (d, b) = x.overflowing_sub(r);
let (_, c) = d.overflowing_sub(borrow as u64);
borrow = b | c;
}
assert!(borrow, "random() produced a non-canonical value: {f:?}");
saw_top_bit |= f.bits()[F::BITS - 1];
}
assert!(
saw_top_bit,
"random() never set the top bit; range is capped"
);
}
#[test]
fn test_f_codec_round_trip() {
minifuzz::test(|u| {
let f: F = u.arbitrary()?;
assert_eq!(F::decode(f.encode()).unwrap(), f);
Ok(())
});
}
#[test]
fn test_f_codec_rejects_non_canonical() {
let mut bytes = [0u8; 32];
for (i, limb) in F::R.iter().enumerate() {
bytes[i * 8..i * 8 + 8].copy_from_slice(&limb.to_le_bytes());
}
assert!(F::decode(&bytes[..]).is_err());
assert!(F::decode(&[0xffu8; 32][..]).is_err());
}
#[test]
fn test_field_modulus_matches_group_order() {
minifuzz::test(|u| {
let a: F = u.arbitrary()?;
let b: F = u.arbitrary()?;
let g = G::generator();
assert_eq!((g.clone() * &a) * &b, g * &(a * &b));
Ok(())
});
}
#[test]
fn test_eq_identity() {
assert_eq!(G::zero(), G::zero());
}
#[test]
fn test_eq_reflexive() {
minifuzz::test(|u| {
let p = arbitrary_point(u)?;
assert_eq!(p, p.clone());
Ok(())
});
}
#[test]
fn test_eq_invariant_under_projective_scaling() {
minifuzz::test(|u| {
let p = arbitrary_point(u)?;
let mut lambda: Scalar = u.arbitrary()?;
if lambda == Scalar::zero() {
lambda = Scalar::one();
}
let scaled = G {
x: p.x.clone() * &lambda,
y: p.y.clone() * &lambda,
t: p.t.clone() * &lambda,
z: p.z.clone() * &lambda,
};
assert_eq!(p, scaled);
Ok(())
});
}
#[test]
fn test_eq_invariant_under_two_torsion() {
minifuzz::test(|u| {
let p = arbitrary_point(u)?;
let twin = G {
x: -p.x.clone(),
y: -p.y.clone(),
t: p.t.clone(),
z: p.z.clone(),
};
assert_eq!(p, twin);
Ok(())
});
}
#[test]
fn test_neq_distinct_points() {
minifuzz::test(|u| {
let p = arbitrary_point(u)?;
assert_ne!(p, p.clone() + &G::generator());
Ok(())
});
}
#[test]
fn test_codec_fixed_size() {
assert_eq!(G::SIZE, 32);
}
#[test]
fn test_codec_round_trip_identity() {
let encoded = G::zero().encode();
assert_eq!(encoded.as_ref(), &[0u8; 32]);
let decoded = G::decode(encoded).unwrap();
assert_eq!(decoded, G::zero());
}
#[test]
fn test_codec_round_trip_generator() {
let g = G::generator();
let decoded = G::decode(g.encode()).unwrap();
assert_eq!(decoded, g);
}
const TEST_DST: &[u8] = b"COMMONWARE_BANDERWAGON_HASH_TO_CURVE_TEST";
#[test]
fn test_hash_to_group_deterministic() {
let p = G::hash_to_group(TEST_DST, b"hello");
let q = G::hash_to_group(TEST_DST, b"hello");
assert_eq!(p, q);
assert_eq!(G::decode(p.encode()).unwrap(), p);
}
#[test]
fn test_hash_to_group_distinct_messages() {
let p = G::hash_to_group(TEST_DST, b"message-a");
let q = G::hash_to_group(TEST_DST, b"message-b");
assert_ne!(p, q);
}
#[test]
fn test_hash_to_group_in_subgroup() {
minifuzz::test(|u| {
let msg: Vec<u8> = u.arbitrary()?;
let p = G::hash_to_group(TEST_DST, &msg);
assert_eq!(G::decode(p.encode()).unwrap(), p);
Ok(())
});
}
#[test]
fn test_hash_to_group_laws() {
minifuzz::test(commonware_math::algebra::test_suites::fuzz_hash_to_group::<G>);
}
#[test]
fn test_codec_fixed_vectors() {
let expected = [
"4a2c7486fd924882bf02c6908de395122843e3e05264d7991e18e7985dad51e9",
"43aa74ef706605705989e8fd38df46873b7eae5921fbed115ac9d937399ce4d5",
"5e5f550494159f38aa54d2ed7f11a7e93e4968617990445cc93ac8e59808c126",
"0e7e3748db7c5c999a7bcd93d71d671f1f40090423792266f94cb27ca43fce5c",
"14ddaa48820cb6523b9ae5fe9fe257cbbd1f3d598a28e670a40da5d1159d864a",
"6989d1c82b2d05c74b62fb0fbdf8843adae62ff720d370e209a7b84e14548a7d",
"26b8df6fa414bf348a3dc780ea53b70303ce49f3369212dec6fbe4b349b832bf",
"37e46072db18f038f2cc7d3d5b5d1374c0eb86ca46f869d6a95fc2fb092c0d35",
"2c1ce64f26e1c772282a6633fac7ca73067ae820637ce348bb2c8477d228dc7d",
"297ab0f5a8336a7a4e2657ad7a33a66e360fb6e50812d4be3326fab73d6cee07",
"5b285811efa7a965bd6ef5632151ebf399115fcc8f5b9b8083415ce533cc39ce",
"1f939fa2fd457b3effb82b25d3fe8ab965f54015f108f8c09d67e696294ab626",
"3088dcb4d3f4bacd706487648b239e0be3072ed2059d981fe04ce6525af6f1b8",
"35fbc386a16d0227ff8673bc3760ad6b11009f749bb82d4facaea67f58fc60ed",
"00f29b4f3255e318438f0a31e058e4c081085426adb0479f14c64985d0b956e0",
"3fa4384b2fa0ecc3c0582223602921daaa893a97b64bdf94dcaa504e8b7b9e5f",
];
let mut point = G::generator();
for (i, want) in expected.into_iter().enumerate() {
let encoded = point.encode();
assert_eq!(
commonware_formatting::hex(encoded.as_ref()),
want,
"encoding mismatch at index {i}"
);
let decoded = G::decode(encoded).unwrap();
assert_eq!(decoded, point, "decode mismatch at index {i}");
point.double();
}
}
#[test]
fn test_codec_round_trip() {
minifuzz::test(|u| {
let p = arbitrary_point(u)?;
let decoded = G::decode(p.encode()).unwrap();
assert_eq!(decoded, p);
Ok(())
});
}
#[test]
fn test_codec_canonical_for_twin() {
minifuzz::test(|u| {
let p = arbitrary_point(u)?;
let twin = G {
x: -p.x.clone(),
y: -p.y.clone(),
t: p.t.clone(),
z: p.z.clone(),
};
assert_eq!(p.encode(), twin.encode());
Ok(())
});
}
#[test]
fn test_codec_canonical_under_projective_scaling() {
minifuzz::test(|u| {
let p = arbitrary_point(u)?;
let mut lambda: Scalar = u.arbitrary()?;
if lambda == Scalar::zero() {
lambda = Scalar::one();
}
let scaled = G {
x: p.x.clone() * &lambda,
y: p.y.clone() * &lambda,
t: p.t.clone() * &lambda,
z: p.z.clone() * &lambda,
};
assert_eq!(p.encode(), scaled.encode());
Ok(())
});
}
#[test]
fn test_decode_rejects_non_canonical_field_element() {
let r_bytes: [u8; 32] = [
0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1,
0xd8, 0x05, 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x01,
];
assert!(G::decode(&r_bytes[..]).is_err());
assert!(G::decode(&[0xffu8; 32][..]).is_err());
}
#[test]
fn test_decode_rejects_off_subgroup() {
minifuzz::test(|u| {
let x: Scalar = u.arbitrary()?;
let x_sq = {
let mut out = x.clone();
out.square();
out
};
let num = Scalar::one() - &(x_sq * &A);
if num != Scalar::zero() && !num.is_square() {
let bytes = x.encode_fixed::<32>();
assert!(G::decode(&bytes[..]).is_err());
}
Ok(())
});
}
fn run_scalar_mul(base: &G, scalar: &Scalar, expected: &G, witness: bool) -> bool {
use crate::zk::circuit::build_with_values;
let base = base.clone();
let expected = expected.clone();
let scalar = scalar.clone();
let (valued, _) = build_with_values(move |ctx| {
let s = if witness {
Var::witness(ctx, move |_| scalar)
} else {
Var::constant(ctx, scalar)
};
base.scalar_mul(ctx, &s)
.assert_eq(&GVar::constant(&expected));
Vec::new()
});
valued.is_satisfied()
}
#[test]
fn test_scalar_mul_small() {
let base = G::generator();
for k in [0u64, 1, 2, 3, 4, 7, 8, 15, 16, 1_000_000, u64::MAX] {
let scalar = Scalar::from_u64(k);
let expected = base.scale(&[k, 0, 0, 0]);
assert!(
run_scalar_mul(&base, &scalar, &expected, false),
"constant scalar k={k}"
);
assert!(
run_scalar_mul(&base, &scalar, &expected, true),
"witness scalar k={k}"
);
}
}
#[test]
fn test_scalar_mul_rejects_wrong_result() {
let base = G::generator();
let scalar = Scalar::from_u64(5);
let correct = base.scale(&[5, 0, 0, 0]);
let wrong = correct.clone() + &G::generator();
assert!(run_scalar_mul(&base, &scalar, &correct, true));
assert!(!run_scalar_mul(&base, &scalar, &wrong, true));
}
#[test]
fn test_scalar_mul_random() {
minifuzz::test(|u| {
let base = arbitrary_point(u)?;
let scalar: Scalar = u.arbitrary()?;
let bytes = scalar.as_blst_scalar().b;
let limbs: [u64; 4] =
array::from_fn(|i| u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().unwrap()));
let expected = base.scale(&limbs);
assert!(run_scalar_mul(&base, &scalar, &expected, true));
Ok(())
});
}
}