samaharam 0.1.0

Scalable heterogeneous zero-knowledge proof aggregation for EVM chains
Documentation
//! Adapter for gnark PLONK proofs.
//!
//! Supports proofs generated by gnark (https://github.com/ConsenSys/gnark)
//! using BN254 curve.

use super::external::{AdapterError, ExternalProof, ProofMetadata};
use super::parsing;
use crate::backend::bn254::Bn254;
use crate::crypto::AccumulatorInstance;
use group::Curve;
use halo2curves::bn256::{Fr, G1Affine, G1};

/// PLONK proof from gnark.
///
/// gnark uses a batched opening scheme, so we extract the relevant
/// KZG commitments for aggregation.
#[derive(Debug, Clone)]
pub struct GnarkPlonkProof {
    /// Wire commitments: L, R, O (left, right, output)
    pub lro: [G1Affine; 3],
    /// Permutation polynomial commitment Z
    pub z: G1Affine,
    /// Quotient polynomial parts H1, H2, H3
    pub h: [G1Affine; 3],
    /// Linearized polynomial commitment
    pub linearized_commitment: G1Affine,
    /// Opening proof for batched polynomial
    pub opening_proof: G1Affine,
    /// Opening proof for shifted polynomial
    pub shifted_opening_proof: G1Affine,
    /// Evaluated values at zeta
    pub evaluations: Vec<Fr>,
    /// Public inputs
    pub public_inputs: Vec<Fr>,
}

impl GnarkPlonkProof {
    /// Create from components.
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        lro: [G1Affine; 3],
        z: G1Affine,
        h: [G1Affine; 3],
        linearized_commitment: G1Affine,
        opening_proof: G1Affine,
        shifted_opening_proof: G1Affine,
        evaluations: Vec<Fr>,
        public_inputs: Vec<Fr>,
    ) -> Self {
        Self {
            lro,
            z,
            h,
            linearized_commitment,
            opening_proof,
            shifted_opening_proof,
            evaluations,
            public_inputs,
        }
    }

    /// Create a mock proof for testing or as placeholder.
    pub fn mock(public_inputs: Vec<Fr>) -> Self {
        let g1 = G1::generator().to_affine();
        let g1_2 = (G1::generator() * Fr::from(2u64)).to_affine();

        Self {
            lro: [g1, g1_2, g1],
            z: g1_2,
            h: [g1, g1, g1],
            linearized_commitment: g1_2,
            opening_proof: g1,
            shifted_opening_proof: g1_2,
            evaluations: vec![Fr::from(1u64), Fr::from(2u64), Fr::from(3u64)],
            public_inputs,
        }
    }

    /// Parse from gnark binary format.
    ///
    /// gnark proof binary layout (BN254):
    /// - LRO: 3 * 64 bytes (uncompressed G1)
    /// - Z: 64 bytes
    /// - H: 3 * 64 bytes
    /// - Linearized: 64 bytes
    /// - Opening: 64 bytes
    /// - ShiftedOpening: 64 bytes
    /// - Evaluations: variable
    pub fn from_bytes(data: &[u8]) -> Result<Self, AdapterError> {
        const G1_SIZE: usize = 64;
        const MIN_SIZE: usize = G1_SIZE * 10; // 10 G1 points minimum

        if data.len() < MIN_SIZE {
            return Err(AdapterError::InvalidFormat(format!(
                "Data too short: {} < {}",
                data.len(),
                MIN_SIZE
            )));
        }

        let mut offset = 0;

        // Parse LRO (3 G1 points)
        let mut lro = [G1::generator().to_affine(); 3];
        for point in lro.iter_mut() {
            *point = parsing::parse_g1_uncompressed(&data[offset..offset + G1_SIZE])
                .map_err(|e| AdapterError::InvalidPoint(format!("LRO: {}", e)))?;
            offset += G1_SIZE;
        }

        // Parse Z
        let z = parsing::parse_g1_uncompressed(&data[offset..offset + G1_SIZE])
            .map_err(|e| AdapterError::InvalidPoint(format!("Z: {}", e)))?;
        offset += G1_SIZE;

        // Parse H (3 G1 points)
        let mut h = [G1::generator().to_affine(); 3];
        for point in h.iter_mut() {
            *point = parsing::parse_g1_uncompressed(&data[offset..offset + G1_SIZE])
                .map_err(|e| AdapterError::InvalidPoint(format!("H: {}", e)))?;
            offset += G1_SIZE;
        }

        // Parse linearized commitment
        let linearized_commitment = parsing::parse_g1_uncompressed(&data[offset..offset + G1_SIZE])
            .map_err(|e| AdapterError::InvalidPoint(format!("Linearized: {}", e)))?;
        offset += G1_SIZE;

        // Parse opening proof
        let opening_proof = parsing::parse_g1_uncompressed(&data[offset..offset + G1_SIZE])
            .map_err(|e| AdapterError::InvalidPoint(format!("Opening: {}", e)))?;
        offset += G1_SIZE;

        // Parse shifted opening proof
        let shifted_opening_proof = parsing::parse_g1_uncompressed(&data[offset..offset + G1_SIZE])
            .map_err(|e| AdapterError::InvalidPoint(format!("ShiftedOpening: {}", e)))?;
        offset += G1_SIZE;

        // Parse evaluations (remaining bytes / 32)
        let mut evaluations = Vec::new();
        while offset + 32 <= data.len() {
            let eval = parsing::parse_fr_bytes(&data[offset..offset + 32])
                .map_err(|e| AdapterError::ParseError(format!("Evaluation: {}", e)))?;
            evaluations.push(eval);
            offset += 32;
        }

        Ok(Self {
            lro,
            z,
            h,
            linearized_commitment,
            opening_proof,
            shifted_opening_proof,
            evaluations,
            public_inputs: vec![],
        })
    }

    /// Set public inputs after parsing.
    pub fn with_public_inputs(mut self, inputs: Vec<Fr>) -> Self {
        self.public_inputs = inputs;
        self
    }
}

impl ExternalProof<Bn254> for GnarkPlonkProof {
    fn to_accumulator_instances(&self) -> Result<Vec<AccumulatorInstance<Bn254>>, AdapterError> {
        // gnark PLONK proofs have multiple polynomial commitments.
        // We create one accumulator instance per major commitment.

        let mut instances = Vec::new();

        // Get evaluation point (would come from transcript in real impl)
        let zeta = Fr::from(7u64);

        // Instance for each wire commitment
        for (i, wire) in self.lro.iter().enumerate() {
            let eval = self.evaluations.get(i).copied().unwrap_or(Fr::from(1u64));
            instances.push(AccumulatorInstance {
                commitment: *wire,
                evaluation: eval,
                point: zeta,
                quotient: self.opening_proof,
            });
        }

        // Instance for Z polynomial
        instances.push(AccumulatorInstance {
            commitment: self.z,
            evaluation: self.evaluations.get(3).copied().unwrap_or(Fr::from(1u64)),
            point: zeta,
            quotient: self.shifted_opening_proof,
        });

        Ok(instances)
    }

    fn public_inputs(&self) -> &[Fr] {
        &self.public_inputs
    }

    fn metadata(&self) -> ProofMetadata {
        ProofMetadata {
            system: "gnark",
            proof_type: "plonk",
            curve: "bn254",
            num_public_inputs: self.public_inputs.len(),
        }
    }

    fn validate_format(&self) -> Result<(), AdapterError> {
        use group::prime::PrimeCurveAffine;

        // Validate wire commitments
        for (i, comm) in self.lro.iter().enumerate() {
            if comm.is_identity().into() {
                return Err(AdapterError::InvalidPoint(format!("lro[{}] is identity", i)));
            }
        }

        // Validate Z
        if self.z.is_identity().into() {
            return Err(AdapterError::InvalidPoint("z is identity".to_string()));
        }

        // Validate opening proof
        if self.opening_proof.is_identity().into() {
            return Err(AdapterError::InvalidPoint("opening_proof is identity".to_string()));
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn gnark_proof_creation() {
        let proof = GnarkPlonkProof::mock(vec![Fr::from(42u64)]);

        assert_eq!(proof.public_inputs().len(), 1);
        assert_eq!(proof.metadata().system, "gnark");
        assert_eq!(proof.metadata().proof_type, "plonk");
    }

    #[test]
    fn gnark_to_accumulator() {
        let proof = GnarkPlonkProof::mock(vec![Fr::from(100u64)]);

        let instances = proof.to_accumulator_instances().unwrap();
        assert_eq!(instances.len(), 4); // 3 wires + Z
    }

    #[test]
    fn gnark_validate_format() {
        let proof = GnarkPlonkProof::mock(vec![]);
        assert!(proof.validate_format().is_ok());
    }

    #[test]
    fn gnark_with_evaluations() {
        let mut proof = GnarkPlonkProof::mock(vec![]);
        proof.evaluations = vec![Fr::from(10u64), Fr::from(20u64), Fr::from(30u64), Fr::from(40u64)];

        let instances = proof.to_accumulator_instances().unwrap();
        assert_eq!(instances[0].evaluation, Fr::from(10u64));
        assert_eq!(instances[3].evaluation, Fr::from(40u64));
    }
}