feat: compress EC subgroup points before serialising (#304)

* Compress EC subgroup points before serialising

* Code review
This commit is contained in:
Ahmad Afuni 2025-06-20 23:01:11 +10:00 committed by GitHub
parent b7ac54d972
commit 151419ec88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 110 additions and 16 deletions

View file

@ -9,18 +9,20 @@ use std::{
};
use num::{bigint::BigUint, Num, One};
use num_bigint::RandBigInt;
use plonky2::{
field::{
extension::{quintic::QuinticExtension, Extendable, FieldExtension},
extension::{quintic::QuinticExtension, Extendable, FieldExtension, Frobenius},
goldilocks_field::GoldilocksField,
ops::Square,
types::{Field, PrimeField},
types::{Field, Field64, PrimeField},
},
hash::poseidon::PoseidonHash,
iop::{generator::SimpleGenerator, target::BoolTarget, witness::WitnessWrite},
plonk::circuit_builder::CircuitBuilder,
util::serialization::{Read, Write},
};
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use crate::backends::plonky2::{
@ -35,6 +37,30 @@ use crate::backends::plonky2::{
type ECField = QuinticExtension<GoldilocksField>;
/// Computes sqrt in ECField as sqrt(x) = sqrt(x^r)/x^((r-1)/2) with r
/// = 1 + p + ... + p^4, where the numerator involves a sqrt in
/// GoldilocksField, cf.
/// https://github.com/pornin/ecgfp5/blob/ce059c6d1e1662db437aecbf3db6bb67fe63c716/rust/src/field.rs#L1041
pub fn ec_field_sqrt(x: &ECField) -> Option<ECField> {
// Compute x^r.
let x_to_the_r = (0..5)
.map(|i| x.repeated_frobenius(i))
.reduce(|a, b| a * b)
.expect("Iterator should be nonempty.");
let num = QuinticExtension([
x_to_the_r.0[0].sqrt()?,
GoldilocksField::ZERO,
GoldilocksField::ZERO,
GoldilocksField::ZERO,
GoldilocksField::ZERO,
]);
// Compute x^((r-1)/2) = x^(p*((1+p)/2)*(1+p^2))
let x1 = x.frobenius();
let x2 = x1.exp_u64((1 + GoldilocksField::ORDER) / 2);
let den = x2 * x2.repeated_frobenius(2);
Some(num / den)
}
fn ec_field_to_bytes(x: &ECField) -> Vec<u8> {
x.0.iter()
.flat_map(|f| {
@ -75,17 +101,45 @@ pub struct Point {
}
impl Point {
pub fn new_rand_from_subgroup() -> Self {
&OsRng.gen_biguint_below(&GROUP_ORDER) * Self::generator()
}
pub fn as_fields(&self) -> Vec<crate::middleware::F> {
self.x.0.iter().chain(self.u.0.iter()).cloned().collect()
}
pub fn as_bytes(&self) -> Vec<u8> {
[ec_field_to_bytes(&self.x), ec_field_to_bytes(&self.u)].concat()
pub fn compress_from_subgroup(&self) -> Result<ECField, Error> {
match self.is_in_subgroup() {
true => Ok(self.u),
false => Err(Error::custom(format!(
"Point must lie in EC subgroup: ({}, {})",
self.x, self.u
))),
}
pub fn from_bytes(b: &[u8]) -> Result<Self, Error> {
let x_bytes = &b[..40];
let u_bytes = &b[40..];
ec_field_from_bytes(x_bytes)
.and_then(|x| ec_field_from_bytes(u_bytes).map(|u| Self { x, u }))
}
pub fn decompress_into_subgroup(u: &ECField) -> Result<Self, Error> {
if u == &ECField::ZERO {
return Ok(Self::ZERO);
}
// Figure out x.
let b = ECField::TWO - ECField::ONE / (u.square());
let d = b.square() - ECField::TWO.square() * Self::b();
let alpha = ECField::NEG_ONE * b / ECField::TWO;
let beta = ec_field_sqrt(&d)
.ok_or(Error::custom(format!("Not a quadratic residue: {}", d)))?
/ ECField::TWO;
let mut points = [ECField::ONE, ECField::NEG_ONE].into_iter().map(|s| Point {
x: alpha + s * beta,
u: *u,
});
points.find(|p| p.is_in_subgroup()).ok_or(Error::custom(
"One of the points must lie in the EC subgroup.".into(),
))
}
pub fn as_bytes_from_subgroup(&self) -> Result<Vec<u8>, Error> {
self.compress_from_subgroup().map(|u| ec_field_to_bytes(&u))
}
pub fn from_bytes_into_subgroup(b: &[u8]) -> Result<Self, Error> {
ec_field_from_bytes(b).and_then(|u| Self::decompress_into_subgroup(&u))
}
}
@ -648,7 +702,12 @@ mod test {
use num::{BigUint, FromPrimitive};
use num_bigint::RandBigInt;
use plonky2::{
field::{goldilocks_field::GoldilocksField, types::Field},
field::{
extension::quintic::QuinticExtension,
goldilocks_field::GoldilocksField,
ops::Square,
types::{Field, Sample},
},
iop::witness::PartialWitness,
plonk::{
circuit_builder::CircuitBuilder, circuit_data::CircuitConfig,
@ -657,9 +716,15 @@ mod test {
};
use rand::rngs::OsRng;
use crate::backends::plonky2::primitives::ec::{
use crate::backends::plonky2::{
primitives::ec::{
bits::CircuitBuilderBits,
curve::{CircuitBuilderElliptic, ECField, Point, WitnessWriteCurve, GROUP_ORDER},
curve::{
ec_field_sqrt, CircuitBuilderElliptic, ECField, Point, WitnessWriteCurve,
GROUP_ORDER,
},
},
Error,
};
#[test]
@ -688,6 +753,13 @@ mod test {
assert_eq!(p2, p3);
}
#[test]
fn test_sqrt() {
let x = QuinticExtension::rand().square();
let y = ec_field_sqrt(&x);
assert_eq!(y.map(|a| a.square()), Some(x));
}
#[test]
fn test_associativity() {
let g = Point::generator();
@ -728,6 +800,23 @@ mod test {
assert!(!not_sub.is_in_subgroup());
}
#[test]
fn test_roundtrip_compression() -> Result<(), Error> {
(0..10).try_for_each(|_| {
let p = Point::new_rand_from_subgroup();
let p_compressed = p.compress_from_subgroup()?;
let q = Point::decompress_into_subgroup(&p_compressed)?;
match p == q {
true => Ok(()),
false => Err(Error::custom(format!(
"Roundtrip compression failed: {:?} ≠ {:?}",
p, q
))),
}
})
}
#[test]
fn test_double_circuit() -> Result<(), anyhow::Error> {
let config = CircuitConfig::standard_recursion_config();

View file

@ -97,7 +97,7 @@ impl SignedPod {
let signer_bytes = deserialize_bytes(&data.signer)?;
let signature_bytes = deserialize_bytes(&data.signature)?;
if signer_bytes.len() != 80 {
if signer_bytes.len() != 40 {
return Err(Error::custom(
"Invalid byte encoding of signed POD signer.".to_string(),
));
@ -108,7 +108,7 @@ impl SignedPod {
));
}
let signer = Point::from_bytes(&signer_bytes)?;
let signer = Point::from_bytes_into_subgroup(&signer_bytes)?;
let signature = Signature::from_bytes(&signature_bytes)?;
Ok(Box::new(Self {
@ -190,7 +190,12 @@ impl Pod for SignedPod {
}
fn serialize_data(&self) -> serde_json::Value {
let signer = serialize_bytes(&self.signer.as_bytes());
let signer = serialize_bytes(
&self
.signer
.as_bytes_from_subgroup()
.expect("Signer public key must lie in EC subgroup."),
);
let signature = serialize_bytes(&self.signature.as_bytes());
serde_json::to_value(Data {
signer,