From b80896e6363ea34a8d1920a15be3ead9fb904d67 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Fri, 20 Jun 2025 12:43:26 +1000 Subject: [PATCH 1/3] Compress EC subgroup points before serialising --- src/backends/plonky2/primitives/ec/curve.rs | 85 ++++++++++++++++++--- src/backends/plonky2/signedpod.rs | 11 ++- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/backends/plonky2/primitives/ec/curve.rs b/src/backends/plonky2/primitives/ec/curve.rs index 38bc5211..6da7f6d4 100644 --- a/src/backends/plonky2/primitives/ec/curve.rs +++ b/src/backends/plonky2/primitives/ec/curve.rs @@ -11,10 +11,10 @@ use std::{ use num::{bigint::BigUint, Num, One}; 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}, @@ -35,6 +35,30 @@ use crate::backends::plonky2::{ type ECField = QuinticExtension; +/// 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 { + // 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 { x.0.iter() .flat_map(|f| { @@ -78,14 +102,39 @@ impl Point { pub fn as_fields(&self) -> Vec { self.x.0.iter().chain(self.u.0.iter()).cloned().collect() } - pub fn as_bytes(&self) -> Vec { - [ec_field_to_bytes(&self.x), ec_field_to_bytes(&self.u)].concat() + pub fn compress_from_subgroup(&self) -> Result { + 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 decompress_into_subgroup(u: &ECField) -> Result { + 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, Error> { + self.compress_from_subgroup().map(|u| ec_field_to_bytes(&u)) } - pub fn from_bytes(b: &[u8]) -> Result { - 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 from_bytes_into_subgroup(b: &[u8]) -> Result { + ec_field_from_bytes(b).and_then(|u| Self::decompress_into_subgroup(&u)) } } @@ -648,7 +697,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, @@ -659,7 +713,9 @@ mod test { 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, + }, }; #[test] @@ -688,6 +744,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(); diff --git a/src/backends/plonky2/signedpod.rs b/src/backends/plonky2/signedpod.rs index 88d47c2d..e579a918 100644 --- a/src/backends/plonky2/signedpod.rs +++ b/src/backends/plonky2/signedpod.rs @@ -138,7 +138,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(), )); @@ -149,7 +149,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 { @@ -197,7 +197,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, From 6f0bf40469ea294078cdc16dcb634ff98c9898a1 Mon Sep 17 00:00:00 2001 From: "Eduard S." Date: Fri, 20 Jun 2025 14:46:55 +0200 Subject: [PATCH 2/3] serialize and display point in base58 --- Cargo.toml | 1 + examples/signed_pod.rs | 2 +- src/backends/plonky2/primitives/ec/curve.rs | 84 ++++++++++++++++++++- src/middleware/mod.rs | 3 +- 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0144ee64..e4420de6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ plonky2 = { git = "https://github.com/0xPolygonZero/plonky2", optional = true } serde = "1.0.219" serde_json = "1.0.140" base64 = "0.22.1" +bs58 = "0.5.1" schemars = "0.8.22" num = { version = "0.4.3", features = ["num-bigint"] } num-bigint = { version = "0.4.6", features = ["rand"] } diff --git a/examples/signed_pod.rs b/examples/signed_pod.rs index 1081aa66..03db74cd 100644 --- a/examples/signed_pod.rs +++ b/examples/signed_pod.rs @@ -15,7 +15,7 @@ fn main() -> Result<(), Box> { // Create a schnorr key pair to sign the pod let sk = SecretKey::new_rand(); let pk = sk.public_key(); - println!("Public key: {:?}\n", pk); + println!("Public key: {}\n", pk); let mut signer = Signer(sk); diff --git a/src/backends/plonky2/primitives/ec/curve.rs b/src/backends/plonky2/primitives/ec/curve.rs index 6da7f6d4..6a91a4a7 100644 --- a/src/backends/plonky2/primitives/ec/curve.rs +++ b/src/backends/plonky2/primitives/ec/curve.rs @@ -3,7 +3,7 @@ //! We roughly follow pornin/ecgfp5. use core::ops::{Add, Mul}; use std::{ - array, + array, fmt, ops::{AddAssign, Neg, Sub}, sync::LazyLock, }; @@ -21,7 +21,7 @@ use plonky2::{ plonk::circuit_builder::CircuitBuilder, util::serialization::{Read, Write}, }; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::backends::plonky2::{ circuits::common::ValueTarget, @@ -92,12 +92,65 @@ fn ec_field_from_bytes(b: &[u8]) -> Result { Ok(QuinticExtension(array::from_fn(|i| fields[i]))) } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Point { pub x: ECField, pub u: ECField, } +impl fmt::Display for Point { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[allow(clippy::collapsible_else_if)] + if f.alternate() { + write!(f, "({}, {})", self.x, self.u) + } else { + if self.is_in_subgroup() { + // Compressed + let u_bytes = self.as_bytes_from_subgroup().expect("point in subgroup"); + let u_b58 = bs58::encode(u_bytes).into_string(); + write!(f, "{}", u_b58) + } else { + // Non-compressed + let xu_bytes = [ec_field_to_bytes(&self.x), ec_field_to_bytes(&self.u)].concat(); + let xu_b58 = bs58::encode(xu_bytes).into_string(); + write!(f, "{}", xu_b58) + } + } + } +} + +impl Serialize for Point { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let point_b58 = format!("{}", self); + serializer.serialize_str(&point_b58) + } +} + +impl<'de> Deserialize<'de> for Point { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let point_b58 = String::deserialize(deserializer)?; + let point_bytes: Vec = bs58::decode(point_b58) + .into_vec() + .map_err(serde::de::Error::custom)?; + if point_bytes.len() == 80 { + // Non-compressed + Ok(Point { + x: ec_field_from_bytes(&point_bytes[..40]).map_err(serde::de::Error::custom)?, + u: ec_field_from_bytes(&point_bytes[40..]).map_err(serde::de::Error::custom)?, + }) + } else { + // Compressed + Self::from_bytes_into_subgroup(&point_bytes).map_err(serde::de::Error::custom) + } + } +} + impl Point { pub fn as_fields(&self) -> Vec { self.x.0.iter().chain(self.u.0.iter()).cloned().collect() @@ -694,6 +747,7 @@ impl> WitnessWriteCurve for W {} #[cfg(test)] mod test { + use anyhow::anyhow; use num::{BigUint, FromPrimitive}; use num_bigint::RandBigInt; use plonky2::{ @@ -871,4 +925,28 @@ mod test { assert!(data.prove(pw).is_err()); Ok(()) } + + #[test] + fn test_point_serialize_deserialize() -> Result<(), anyhow::Error> { + // In subgroup + let g = Point::generator(); + + let serialized = serde_json::to_string_pretty(&g)?; + println!("g = {}", serialized); + let deserialized = serde_json::from_str(&serialized)?; + assert_eq!(g, deserialized); + + // Not in subgroup + let not_sub = Point { + x: Point::b() / g.x, + u: g.u, + }; + + let serialized = serde_json::to_string_pretty(¬_sub)?; + println!("not_sub = {}", serialized); + let deserialized = serde_json::from_str(&serialized)?; + assert_eq!(not_sub, deserialized); + + Ok(()) + } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index d921dc0d..048c0282 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -180,7 +180,7 @@ impl fmt::Display for TypedValue { TypedValue::Set(s) => write!(f, "set:{}", s.commitment()), TypedValue::Array(a) => write!(f, "arr:{}", a.commitment()), TypedValue::Raw(v) => write!(f, "{}", v), - TypedValue::PublicKey(p) => write!(f, "ecGFp5_pt:({},{})", p.x, p.u), + TypedValue::PublicKey(p) => write!(f, "pk:{}", p), TypedValue::PodId(id) => write!(f, "pod_id:{}", id), } } @@ -849,6 +849,7 @@ pub struct MainPodInputs<'a> { /// Statements that need to be made public (they can come from input pods or input /// statements) pub public_statements: &'a [Statement], + // TODO: REMOVE THIS pub vd_set: VDSet, } From 6ff7f2d5b08cb4531a4a6f99757d91ae053f845f Mon Sep 17 00:00:00 2001 From: Ahmad Date: Mon, 23 Jun 2025 13:09:00 +1000 Subject: [PATCH 3/3] Use Display for Points --- src/backends/plonky2/primitives/ec/curve.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backends/plonky2/primitives/ec/curve.rs b/src/backends/plonky2/primitives/ec/curve.rs index dc523d5c..af064d52 100644 --- a/src/backends/plonky2/primitives/ec/curve.rs +++ b/src/backends/plonky2/primitives/ec/curve.rs @@ -164,8 +164,8 @@ impl Point { match self.is_in_subgroup() { true => Ok(self.u), false => Err(Error::custom(format!( - "Point must lie in EC subgroup: ({}, {})", - self.x, self.u + "Point must lie in EC subgroup: {}", + self ))), } } @@ -863,7 +863,7 @@ mod test { match p == q { true => Ok(()), false => Err(Error::custom(format!( - "Roundtrip compression failed: {:?} ≠ {:?}", + "Roundtrip compression failed: {} ≠ {}", p, q ))), }