Display point in base58 (#305)

* Compress EC subgroup points before serialising

* serialize and display point in base58

* Use Display for Points

---------

Co-authored-by: Ahmad <root@ahmadafuni.com>
This commit is contained in:
Eduard S. 2025-06-23 16:18:58 +02:00 committed by GitHub
parent 151419ec88
commit d5da9d8593
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 87 additions and 8 deletions

View file

@ -23,6 +23,7 @@ plonky2 = { git = "https://github.com/0xPolygonZero/plonky2", optional = true }
serde = "1.0.219" serde = "1.0.219"
serde_json = "1.0.140" serde_json = "1.0.140"
base64 = "0.22.1" base64 = "0.22.1"
bs58 = "0.5.1"
schemars = "0.8.22" schemars = "0.8.22"
num = { version = "0.4.3", features = ["num-bigint"] } num = { version = "0.4.3", features = ["num-bigint"] }
num-bigint = { version = "0.4.6", features = ["rand"] } num-bigint = { version = "0.4.6", features = ["rand"] }

View file

@ -15,7 +15,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a schnorr key pair to sign the pod // Create a schnorr key pair to sign the pod
let sk = SecretKey::new_rand(); let sk = SecretKey::new_rand();
let pk = sk.public_key(); let pk = sk.public_key();
println!("Public key: {:?}\n", pk); println!("Public key: {}\n", pk);
let mut signer = Signer(sk); let mut signer = Signer(sk);

View file

@ -3,7 +3,7 @@
//! We roughly follow pornin/ecgfp5. //! We roughly follow pornin/ecgfp5.
use core::ops::{Add, Mul}; use core::ops::{Add, Mul};
use std::{ use std::{
array, array, fmt,
ops::{AddAssign, Neg, Sub}, ops::{AddAssign, Neg, Sub},
sync::LazyLock, sync::LazyLock,
}; };
@ -23,7 +23,7 @@ use plonky2::{
util::serialization::{Read, Write}, util::serialization::{Read, Write},
}; };
use rand::rngs::OsRng; use rand::rngs::OsRng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::backends::plonky2::{ use crate::backends::plonky2::{
circuits::common::ValueTarget, circuits::common::ValueTarget,
@ -94,12 +94,65 @@ fn ec_field_from_bytes(b: &[u8]) -> Result<ECField, Error> {
Ok(QuinticExtension(array::from_fn(|i| fields[i]))) 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 struct Point {
pub x: ECField, pub x: ECField,
pub u: 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let point_b58 = format!("{}", self);
serializer.serialize_str(&point_b58)
}
}
impl<'de> Deserialize<'de> for Point {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let point_b58 = String::deserialize(deserializer)?;
let point_bytes: Vec<u8> = 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 { impl Point {
pub fn new_rand_from_subgroup() -> Self { pub fn new_rand_from_subgroup() -> Self {
&OsRng.gen_biguint_below(&GROUP_ORDER) * Self::generator() &OsRng.gen_biguint_below(&GROUP_ORDER) * Self::generator()
@ -111,8 +164,8 @@ impl Point {
match self.is_in_subgroup() { match self.is_in_subgroup() {
true => Ok(self.u), true => Ok(self.u),
false => Err(Error::custom(format!( false => Err(Error::custom(format!(
"Point must lie in EC subgroup: ({}, {})", "Point must lie in EC subgroup: {}",
self.x, self.u self
))), ))),
} }
} }
@ -810,7 +863,7 @@ mod test {
match p == q { match p == q {
true => Ok(()), true => Ok(()),
false => Err(Error::custom(format!( false => Err(Error::custom(format!(
"Roundtrip compression failed: {:?} ≠ {:?}", "Roundtrip compression failed: {} ≠ {}",
p, q p, q
))), ))),
} }
@ -897,4 +950,28 @@ mod test {
assert!(data.prove(pw).is_err()); assert!(data.prove(pw).is_err());
Ok(()) 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(&not_sub)?;
println!("not_sub = {}", serialized);
let deserialized = serde_json::from_str(&serialized)?;
assert_eq!(not_sub, deserialized);
Ok(())
}
} }

View file

@ -180,7 +180,7 @@ impl fmt::Display for TypedValue {
TypedValue::Set(s) => write!(f, "set:{}", s.commitment()), TypedValue::Set(s) => write!(f, "set:{}", s.commitment()),
TypedValue::Array(a) => write!(f, "arr:{}", a.commitment()), TypedValue::Array(a) => write!(f, "arr:{}", a.commitment()),
TypedValue::Raw(v) => write!(f, "{}", v), 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), 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 that need to be made public (they can come from input pods or input
/// statements) /// statements)
pub public_statements: &'a [Statement], pub public_statements: &'a [Statement],
// TODO: REMOVE THIS
pub vd_set: VDSet, pub vd_set: VDSet,
} }