From 7e05a973d4a8d0fcc338978647aa6cc2c08bcf82 Mon Sep 17 00:00:00 2001 From: Vladislav Volosnikov Date: Fri, 15 May 2026 10:03:41 -0500 Subject: [PATCH] perf(point_evaluation): rearrange KZG verifier to use G1 scalar mul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the scalar multiplication by z from G2 to G1: before: e(yG1 - P, G2) · e(proof, τG2 - zG2) == 1 after: e(yG1 - P - z·proof, G2) · e(proof, τG2) == 1 Algebraically equivalent by bilinearity. G2 lives in Fq², so a G2 scalar mul is materially more expensive than the G1 equivalent; the rearranged form also eliminates one G2 subtraction and one G2 into_affine (Montgomery inversion). Implemented as a local function in basic_system::system_functions:: point_evaluation, parallel to the existing crypto::bls12_381:: verify_kzg_proof (kept upstream as the reference). Wired into both the EIP-4844 point-evaluation precompile and the bootloader's blob commitment generator post-tx-op. Bench (test_kzg_regression under ZKSYNC_RISC_V_RUN=true, for-tests-benchmarking RISC-V binary, A/B at same parent commit): point_evaluation raw cycles 38,992,778 → 31,791,155 −18.5% point_evaluation bigint delegations 3,087,009 → 2,513,437 −18.6% effective (raw + 4·bigint) 51,340,814 → 41,844,903 −18.5% dist/for_tests/app.bin size 1,353,100 → 1,337,196 −1.18% Binary shrinks, so no RISC-V I-cache regression risk despite #[inline(always)] on the new function. Includes test_rearranged_kzg_verifier_matches_reference that asserts byte-equal output vs crypto::bls12_381::verify_kzg_proof on a valid proof, an unrelated 128-bit z, and a zero y. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../blob_commitment_generator/mod.rs | 4 +- .../src/system_functions/point_evaluation.rs | 75 ++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/da_commitment_generator/blob_commitment_generator/mod.rs b/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/da_commitment_generator/blob_commitment_generator/mod.rs index e36a8a8cd..5442bd1cd 100644 --- a/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/da_commitment_generator/blob_commitment_generator/mod.rs +++ b/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/da_commitment_generator/blob_commitment_generator/mod.rs @@ -4,7 +4,7 @@ use self::commitment_and_proof_advice::{ use super::DACommitmentGenerator; use arrayvec::ArrayVec; use basic_system::system_functions::point_evaluation::{ - parse_g1_compressed, versioned_hash_for_kzg, + parse_g1_compressed, verify_kzg_proof, versioned_hash_for_kzg, }; use crypto::ark_ff::Field; use crypto::ark_ff::One; @@ -113,7 +113,7 @@ pub fn blob_versioned_hash_with_advisor( let opening_value = polynomial_evaluation::evaluate_blob_polynomial(data, &evaluation_point); assert!( - crypto::bls12_381::verify_kzg_proof( + verify_kzg_proof( commitment, proof, evaluation_point.into_bigint(), diff --git a/basic_system/src/system_functions/point_evaluation.rs b/basic_system/src/system_functions/point_evaluation.rs index 460218e38..0ec088456 100644 --- a/basic_system/src/system_functions/point_evaluation.rs +++ b/basic_system/src/system_functions/point_evaluation.rs @@ -1,11 +1,15 @@ use crate::cost_constants::{POINT_EVALUATION_COST_ERGS, POINT_EVALUATION_NATIVE_COST}; -use crypto::ark_ff::PrimeField; +use crypto::ark_ec::pairing::Pairing; +use crypto::ark_ec::{AffineRepr, CurveGroup}; +use crypto::ark_ff::{Field, PrimeField}; use zk_ee::common_traits::TryExtend; use zk_ee::interface_error; use zk_ee::out_of_return_memory; use zk_ee::system::errors::subsystem::SubsystemError; use zk_ee::system::*; +pub type KzgScalar = ::BigInt; + /// /// Point evaluation system function implementation. /// @@ -51,7 +55,7 @@ pub fn versioned_hash_for_kzg(data: &[u8]) -> [u8; 32] { } // We do not need internal representation, just canonical scalar -fn parse_scalar(input: &[u8; 32]) -> Result<::BigInt, ()> { +fn parse_scalar(input: &[u8; 32]) -> Result { // Arkworks has strange format for integer serialization, so we do manually let result = crypto::parse_u256_be(input); if result >= crypto::bls12_381::Fr::MODULUS { @@ -67,6 +71,36 @@ pub fn parse_g1_compressed(input: &[u8]) -> Result bool { + // Original check: + // e(yG1 - commitment, G2) * e(proof, tauG2 - zG2) == 1. + // + // Move the z multiplication from G2 to G1: + // e(yG1 - commitment - z*proof, G2) * e(proof, tauG2) == 1. + let mut left_g1 = crypto::bls12_381::G1Affine::generator().mul_bigint(&y); + left_g1 -= &commitment; + left_g1 -= proof.mul_bigint(&z); + + let left_g1 = left_g1.into_affine(); + let tau_g2_prepared: ::G2Prepared = + crypto::bls12_381::consts::G2_BY_TAU_POINT.into(); + + let gt_el = crypto::bls12_381::curves::Bls12_381::multi_pairing( + [left_g1, proof], + [ + crypto::bls12_381::consts::PREPARED_G2_GENERATOR.clone(), + tau_g2_prepared, + ], + ); + gt_el.0 == ::TargetField::ONE +} + fn point_evaluation_as_system_function_inner, R: Resources>( input: &[u8], dst: &mut D, @@ -121,7 +155,7 @@ fn point_evaluation_as_system_function_inner, R: Resou )); }; - if crypto::bls12_381::verify_kzg_proof(commitment_point, proof, z, y) { + if verify_kzg_proof(commitment_point, proof, z, y) { dst.try_extend(POINT_EVAL_PRECOMPILE_SUCCESS_RESPONSE) .map_err(|_| out_of_return_memory!())?; Ok(()) @@ -183,6 +217,41 @@ mod tests { assert_eq!(output[..], expected_output); } + #[test] + fn test_rearranged_kzg_verifier_matches_reference() { + let commitment = + hex!("8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7"); + let proof = + hex!("a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc2160744faf0070725e00b60ad9a026a15b1a8c"); + let z = hex!("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000"); + let y = hex!("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9"); + + let commitment = parse_g1_compressed(&commitment).unwrap(); + let proof = parse_g1_compressed(&proof).unwrap(); + let z = parse_scalar(&z).unwrap(); + let y = parse_scalar(&y).unwrap(); + + assert!(verify_kzg_proof(commitment, proof, z, y)); + assert_eq!( + verify_kzg_proof(commitment, proof, z, y), + crypto::bls12_381::verify_kzg_proof(commitment, proof, z, y) + ); + + let unrelated_128_bit_z = + hex!("000000000000000000000000000000000123456789abcdef0123456789abcdef"); + let unrelated_128_bit_z = parse_scalar(&unrelated_128_bit_z).unwrap(); + assert_eq!( + verify_kzg_proof(commitment, proof, unrelated_128_bit_z, y), + crypto::bls12_381::verify_kzg_proof(commitment, proof, unrelated_128_bit_z, y) + ); + + let zero_y = parse_scalar(&[0u8; 32]).unwrap(); + assert_eq!( + verify_kzg_proof(commitment, proof, z, zero_y), + crypto::bls12_381::verify_kzg_proof(commitment, proof, z, zero_y) + ); + } + #[test] fn test_invalid_input() { let commitment = hex!("c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").to_vec();