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_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"] }

View file

@ -15,7 +15,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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);

View file

@ -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,
};
@ -23,7 +23,7 @@ use plonky2::{
util::serialization::{Read, Write},
};
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::backends::plonky2::{
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])))
}
#[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<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 {
pub fn new_rand_from_subgroup() -> Self {
&OsRng.gen_biguint_below(&GROUP_ORDER) * Self::generator()
@ -111,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
))),
}
}
@ -810,7 +863,7 @@ mod test {
match p == q {
true => Ok(()),
false => Err(Error::custom(format!(
"Roundtrip compression failed: {:?} ≠ {:?}",
"Roundtrip compression failed: {} ≠ {}",
p, q
))),
}
@ -897,4 +950,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(&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::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,
}