use crate::traits::PairingEngine;
pub const GROTH16_PROOF_SIZE: usize = 256;
#[derive(Debug, Clone)]
pub struct Groth16Proof<E: PairingEngine> {
pub a: E::G1Affine,
pub b_bytes: [u8; 128],
pub c: E::G1Affine,
}
impl<E: PairingEngine> Groth16Proof<E> {
pub fn new(a: E::G1Affine, b_bytes: [u8; 128], c: E::G1Affine) -> Self {
Self { a, b_bytes, c }
}
pub fn from_components(a: E::G1Affine, b: &E::G2Affine, c: E::G1Affine) -> Self {
use group::GroupEncoding;
let b_encoded = b.to_bytes();
let b_ref: &[u8] = b_encoded.as_ref();
let mut b_bytes = [0u8; 128];
let len = b_ref.len().min(128);
b_bytes[..len].copy_from_slice(&b_ref[..len]);
Self { a, b_bytes, c }
}
pub fn to_abi_bytes(&self) -> Vec<u8> {
use group::GroupEncoding;
let mut bytes = Vec::with_capacity(GROTH16_PROOF_SIZE);
let a_bytes = self.a.to_bytes();
let a_ref: &[u8] = a_bytes.as_ref();
bytes.extend_from_slice(a_ref);
bytes.resize(64, 0);
bytes.extend_from_slice(&self.b_bytes);
let c_bytes = self.c.to_bytes();
let c_ref: &[u8] = c_bytes.as_ref();
bytes.extend_from_slice(c_ref);
bytes.resize(GROTH16_PROOF_SIZE, 0);
bytes
}
pub fn from_abi_bytes(data: &[u8]) -> Result<Self, String> {
if data.len() < GROTH16_PROOF_SIZE {
return Err(format!(
"proof data too short: {} < {}",
data.len(),
GROTH16_PROOF_SIZE
));
}
use group::GroupEncoding;
let mut a_repr = <E::G1Affine as GroupEncoding>::Repr::default();
let a_slice: &mut [u8] = a_repr.as_mut();
let a_len = a_slice.len().min(64);
a_slice[..a_len].copy_from_slice(&data[..a_len]);
let a = E::G1Affine::from_bytes(&a_repr);
if a.is_none().into() {
return Err("invalid A point".to_string());
}
let mut b_bytes = [0u8; 128];
b_bytes.copy_from_slice(&data[64..192]);
let mut c_repr = <E::G1Affine as GroupEncoding>::Repr::default();
let c_slice: &mut [u8] = c_repr.as_mut();
let c_len = c_slice.len().min(64);
c_slice[..c_len].copy_from_slice(&data[192..192 + c_len]);
let c = E::G1Affine::from_bytes(&c_repr);
if c.is_none().into() {
return Err("invalid C point".to_string());
}
Ok(Self {
a: a.unwrap(),
b_bytes,
c: c.unwrap(),
})
}
pub fn to_snarkjs_json(&self) -> String {
use group::GroupEncoding;
let a_bytes = self.a.to_bytes();
let c_bytes = self.c.to_bytes();
format!(
r#"{{
"pi_a": ["0x{}", "0x{}", "1"],
"pi_b": [["0x{}", "0x{}"], ["0x{}", "0x{}"], ["1", "0"]],
"pi_c": ["0x{}", "0x{}", "1"],
"protocol": "groth16"
}}"#,
hex::encode(&a_bytes.as_ref()[..32.min(a_bytes.as_ref().len())]),
hex::encode(
&a_bytes.as_ref()[32.min(a_bytes.as_ref().len())..64.min(a_bytes.as_ref().len())]
),
hex::encode(&self.b_bytes[..32]),
hex::encode(&self.b_bytes[32..64]),
hex::encode(&self.b_bytes[64..96]),
hex::encode(&self.b_bytes[96..128]),
hex::encode(&c_bytes.as_ref()[..32.min(c_bytes.as_ref().len())]),
hex::encode(
&c_bytes.as_ref()[32.min(c_bytes.as_ref().len())..64.min(c_bytes.as_ref().len())]
),
)
}
pub fn to_solidity_calldata(&self) -> SolidityCalldata {
use group::GroupEncoding;
let a = self.a.to_bytes();
let c = self.c.to_bytes();
let a_ref: &[u8] = a.as_ref();
let c_ref: &[u8] = c.as_ref();
SolidityCalldata {
a: [
bytes_to_u256(&a_ref[..32.min(a_ref.len())]),
bytes_to_u256(&a_ref[32.min(a_ref.len())..64.min(a_ref.len())]),
],
b: [
[
bytes_to_u256(&self.b_bytes[..32]),
bytes_to_u256(&self.b_bytes[32..64]),
],
[
bytes_to_u256(&self.b_bytes[64..96]),
bytes_to_u256(&self.b_bytes[96..128]),
],
],
c: [
bytes_to_u256(&c_ref[..32.min(c_ref.len())]),
bytes_to_u256(&c_ref[32.min(c_ref.len())..64.min(c_ref.len())]),
],
}
}
}
#[derive(Debug, Clone)]
pub struct SolidityCalldata {
pub a: [String; 2],
pub b: [[String; 2]; 2],
pub c: [String; 2],
}
impl SolidityCalldata {
pub fn to_solidity_args(&self) -> String {
format!(
"[{}, {}], [[{}, {}], [{}, {}]], [{}, {}]",
self.a[0],
self.a[1],
self.b[0][0],
self.b[0][1],
self.b[1][0],
self.b[1][1],
self.c[0],
self.c[1]
)
}
}
fn bytes_to_u256(bytes: &[u8]) -> String {
if bytes.is_empty() {
return "0".to_string();
}
let mut padded = [0u8; 32];
let len = bytes.len().min(32);
padded[32 - len..].copy_from_slice(&bytes[..len]);
format!("0x{}", hex::encode(padded))
}
pub fn convert_to_groth16<E: PairingEngine>(
aggregated_data: &[u8],
srs_g2: &E::G2Affine,
) -> Result<Groth16Proof<E>, String> {
use group::GroupEncoding;
if aggregated_data.len() < 100 {
return Err("aggregated data too short".to_string());
}
let mut a_repr = <E::G1Affine as GroupEncoding>::Repr::default();
let a_slice: &mut [u8] = a_repr.as_mut();
let len = a_slice.len().min(48);
a_slice[..len].copy_from_slice(&aggregated_data[..len]);
let a = E::G1Affine::from_bytes(&a_repr);
let mut c_repr = <E::G1Affine as GroupEncoding>::Repr::default();
let c_slice: &mut [u8] = c_repr.as_mut();
c_slice[..len].copy_from_slice(&aggregated_data[48..48 + len]);
let c = E::G1Affine::from_bytes(&c_repr);
if a.is_none().into() || c.is_none().into() {
return Err("invalid aggregated proof points".to_string());
}
let b_bytes_encoded = srs_g2.to_bytes();
let b_ref: &[u8] = b_bytes_encoded.as_ref();
let mut b_bytes = [0u8; 128];
let b_len = b_ref.len().min(128);
b_bytes[..b_len].copy_from_slice(&b_ref[..b_len]);
Ok(Groth16Proof {
a: a.unwrap(),
b_bytes,
c: c.unwrap(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::bn254::Bn254;
use group::{Curve, Group};
use halo2curves::bn256::{G1, G2};
use rand::rngs::OsRng;
#[test]
fn groth16_proof_serialization_roundtrip() {
use group::GroupEncoding;
let g2 = G2::random(OsRng).to_affine();
let g2_bytes = g2.to_bytes();
let g2_ref: &[u8] = g2_bytes.as_ref();
let mut b_bytes = [0u8; 128];
let len = g2_ref.len().min(128);
b_bytes[..len].copy_from_slice(&g2_ref[..len]);
let proof = Groth16Proof::<Bn254> {
a: G1::random(OsRng).to_affine(),
b_bytes,
c: G1::random(OsRng).to_affine(),
};
let bytes = proof.to_abi_bytes();
assert_eq!(bytes.len(), GROTH16_PROOF_SIZE);
let decoded = Groth16Proof::<Bn254>::from_abi_bytes(&bytes);
assert!(decoded.is_ok());
}
#[test]
fn groth16_proof_to_snarkjs_json() {
let proof = Groth16Proof::<Bn254> {
a: G1::generator().to_affine(),
b_bytes: [0u8; 128],
c: G1::generator().to_affine(),
};
let json = proof.to_snarkjs_json();
assert!(json.contains("pi_a"));
assert!(json.contains("pi_b"));
assert!(json.contains("pi_c"));
assert!(json.contains("groth16"));
}
#[test]
fn solidity_calldata_format() {
let proof = Groth16Proof::<Bn254> {
a: G1::generator().to_affine(),
b_bytes: [0u8; 128],
c: G1::generator().to_affine(),
};
let calldata = proof.to_solidity_calldata();
assert!(calldata.a[0].starts_with("0x"));
assert!(calldata.b[0][0].starts_with("0x"));
let args = calldata.to_solidity_args();
assert!(args.contains("0x"));
}
#[test]
fn bytes_to_u256_pads_correctly() {
let bytes = vec![0x01, 0x02];
let result = bytes_to_u256(&bytes);
assert!(result.starts_with("0x"));
assert_eq!(result.len(), 66); }
#[test]
fn from_components_creates_valid_proof() {
use group::GroupEncoding;
let a = G1::random(OsRng).to_affine();
let b = G2::random(OsRng).to_affine();
let c = G1::random(OsRng).to_affine();
let proof = Groth16Proof::<Bn254>::from_components(a.clone(), &b, c.clone());
assert_eq!(proof.a, a);
assert_eq!(proof.c, c);
assert!(!proof.b_bytes.iter().all(|&x| x == 0));
}
}