Skip to content

Commit 151419e

Browse files
authored
feat: compress EC subgroup points before serialising (#304)
* Compress EC subgroup points before serialising * Code review
1 parent b7ac54d commit 151419e

File tree

2 files changed

+110
-16
lines changed

2 files changed

+110
-16
lines changed

src/backends/plonky2/primitives/ec/curve.rs

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@ use std::{
99
};
1010

1111
use num::{bigint::BigUint, Num, One};
12+
use num_bigint::RandBigInt;
1213
use plonky2::{
1314
field::{
14-
extension::{quintic::QuinticExtension, Extendable, FieldExtension},
15+
extension::{quintic::QuinticExtension, Extendable, FieldExtension, Frobenius},
1516
goldilocks_field::GoldilocksField,
1617
ops::Square,
17-
types::{Field, PrimeField},
18+
types::{Field, Field64, PrimeField},
1819
},
1920
hash::poseidon::PoseidonHash,
2021
iop::{generator::SimpleGenerator, target::BoolTarget, witness::WitnessWrite},
2122
plonk::circuit_builder::CircuitBuilder,
2223
util::serialization::{Read, Write},
2324
};
25+
use rand::rngs::OsRng;
2426
use serde::{Deserialize, Serialize};
2527

2628
use crate::backends::plonky2::{
@@ -35,6 +37,30 @@ use crate::backends::plonky2::{
3537

3638
type ECField = QuinticExtension<GoldilocksField>;
3739

40+
/// Computes sqrt in ECField as sqrt(x) = sqrt(x^r)/x^((r-1)/2) with r
41+
/// = 1 + p + ... + p^4, where the numerator involves a sqrt in
42+
/// GoldilocksField, cf.
43+
/// https://github.com/pornin/ecgfp5/blob/ce059c6d1e1662db437aecbf3db6bb67fe63c716/rust/src/field.rs#L1041
44+
pub fn ec_field_sqrt(x: &ECField) -> Option<ECField> {
45+
// Compute x^r.
46+
let x_to_the_r = (0..5)
47+
.map(|i| x.repeated_frobenius(i))
48+
.reduce(|a, b| a * b)
49+
.expect("Iterator should be nonempty.");
50+
let num = QuinticExtension([
51+
x_to_the_r.0[0].sqrt()?,
52+
GoldilocksField::ZERO,
53+
GoldilocksField::ZERO,
54+
GoldilocksField::ZERO,
55+
GoldilocksField::ZERO,
56+
]);
57+
// Compute x^((r-1)/2) = x^(p*((1+p)/2)*(1+p^2))
58+
let x1 = x.frobenius();
59+
let x2 = x1.exp_u64((1 + GoldilocksField::ORDER) / 2);
60+
let den = x2 * x2.repeated_frobenius(2);
61+
Some(num / den)
62+
}
63+
3864
fn ec_field_to_bytes(x: &ECField) -> Vec<u8> {
3965
x.0.iter()
4066
.flat_map(|f| {
@@ -75,17 +101,45 @@ pub struct Point {
75101
}
76102

77103
impl Point {
104+
pub fn new_rand_from_subgroup() -> Self {
105+
&OsRng.gen_biguint_below(&GROUP_ORDER) * Self::generator()
106+
}
78107
pub fn as_fields(&self) -> Vec<crate::middleware::F> {
79108
self.x.0.iter().chain(self.u.0.iter()).cloned().collect()
80109
}
81-
pub fn as_bytes(&self) -> Vec<u8> {
82-
[ec_field_to_bytes(&self.x), ec_field_to_bytes(&self.u)].concat()
110+
pub fn compress_from_subgroup(&self) -> Result<ECField, Error> {
111+
match self.is_in_subgroup() {
112+
true => Ok(self.u),
113+
false => Err(Error::custom(format!(
114+
"Point must lie in EC subgroup: ({}, {})",
115+
self.x, self.u
116+
))),
117+
}
83118
}
84-
pub fn from_bytes(b: &[u8]) -> Result<Self, Error> {
85-
let x_bytes = &b[..40];
86-
let u_bytes = &b[40..];
87-
ec_field_from_bytes(x_bytes)
88-
.and_then(|x| ec_field_from_bytes(u_bytes).map(|u| Self { x, u }))
119+
pub fn decompress_into_subgroup(u: &ECField) -> Result<Self, Error> {
120+
if u == &ECField::ZERO {
121+
return Ok(Self::ZERO);
122+
}
123+
// Figure out x.
124+
let b = ECField::TWO - ECField::ONE / (u.square());
125+
let d = b.square() - ECField::TWO.square() * Self::b();
126+
let alpha = ECField::NEG_ONE * b / ECField::TWO;
127+
let beta = ec_field_sqrt(&d)
128+
.ok_or(Error::custom(format!("Not a quadratic residue: {}", d)))?
129+
/ ECField::TWO;
130+
let mut points = [ECField::ONE, ECField::NEG_ONE].into_iter().map(|s| Point {
131+
x: alpha + s * beta,
132+
u: *u,
133+
});
134+
points.find(|p| p.is_in_subgroup()).ok_or(Error::custom(
135+
"One of the points must lie in the EC subgroup.".into(),
136+
))
137+
}
138+
pub fn as_bytes_from_subgroup(&self) -> Result<Vec<u8>, Error> {
139+
self.compress_from_subgroup().map(|u| ec_field_to_bytes(&u))
140+
}
141+
pub fn from_bytes_into_subgroup(b: &[u8]) -> Result<Self, Error> {
142+
ec_field_from_bytes(b).and_then(|u| Self::decompress_into_subgroup(&u))
89143
}
90144
}
91145

@@ -648,7 +702,12 @@ mod test {
648702
use num::{BigUint, FromPrimitive};
649703
use num_bigint::RandBigInt;
650704
use plonky2::{
651-
field::{goldilocks_field::GoldilocksField, types::Field},
705+
field::{
706+
extension::quintic::QuinticExtension,
707+
goldilocks_field::GoldilocksField,
708+
ops::Square,
709+
types::{Field, Sample},
710+
},
652711
iop::witness::PartialWitness,
653712
plonk::{
654713
circuit_builder::CircuitBuilder, circuit_data::CircuitConfig,
@@ -657,9 +716,15 @@ mod test {
657716
};
658717
use rand::rngs::OsRng;
659718

660-
use crate::backends::plonky2::primitives::ec::{
661-
bits::CircuitBuilderBits,
662-
curve::{CircuitBuilderElliptic, ECField, Point, WitnessWriteCurve, GROUP_ORDER},
719+
use crate::backends::plonky2::{
720+
primitives::ec::{
721+
bits::CircuitBuilderBits,
722+
curve::{
723+
ec_field_sqrt, CircuitBuilderElliptic, ECField, Point, WitnessWriteCurve,
724+
GROUP_ORDER,
725+
},
726+
},
727+
Error,
663728
};
664729

665730
#[test]
@@ -688,6 +753,13 @@ mod test {
688753
assert_eq!(p2, p3);
689754
}
690755

756+
#[test]
757+
fn test_sqrt() {
758+
let x = QuinticExtension::rand().square();
759+
let y = ec_field_sqrt(&x);
760+
assert_eq!(y.map(|a| a.square()), Some(x));
761+
}
762+
691763
#[test]
692764
fn test_associativity() {
693765
let g = Point::generator();
@@ -728,6 +800,23 @@ mod test {
728800
assert!(!not_sub.is_in_subgroup());
729801
}
730802

803+
#[test]
804+
fn test_roundtrip_compression() -> Result<(), Error> {
805+
(0..10).try_for_each(|_| {
806+
let p = Point::new_rand_from_subgroup();
807+
let p_compressed = p.compress_from_subgroup()?;
808+
let q = Point::decompress_into_subgroup(&p_compressed)?;
809+
810+
match p == q {
811+
true => Ok(()),
812+
false => Err(Error::custom(format!(
813+
"Roundtrip compression failed: {:?} ≠ {:?}",
814+
p, q
815+
))),
816+
}
817+
})
818+
}
819+
731820
#[test]
732821
fn test_double_circuit() -> Result<(), anyhow::Error> {
733822
let config = CircuitConfig::standard_recursion_config();

src/backends/plonky2/signedpod.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ impl SignedPod {
9797
let signer_bytes = deserialize_bytes(&data.signer)?;
9898
let signature_bytes = deserialize_bytes(&data.signature)?;
9999

100-
if signer_bytes.len() != 80 {
100+
if signer_bytes.len() != 40 {
101101
return Err(Error::custom(
102102
"Invalid byte encoding of signed POD signer.".to_string(),
103103
));
@@ -108,7 +108,7 @@ impl SignedPod {
108108
));
109109
}
110110

111-
let signer = Point::from_bytes(&signer_bytes)?;
111+
let signer = Point::from_bytes_into_subgroup(&signer_bytes)?;
112112
let signature = Signature::from_bytes(&signature_bytes)?;
113113

114114
Ok(Box::new(Self {
@@ -190,7 +190,12 @@ impl Pod for SignedPod {
190190
}
191191

192192
fn serialize_data(&self) -> serde_json::Value {
193-
let signer = serialize_bytes(&self.signer.as_bytes());
193+
let signer = serialize_bytes(
194+
&self
195+
.signer
196+
.as_bytes_from_subgroup()
197+
.expect("Signer public key must lie in EC subgroup."),
198+
);
194199
let signature = serialize_bytes(&self.signature.as_bytes());
195200
serde_json::to_value(Data {
196201
signer,

0 commit comments

Comments
 (0)