diff --git a/Cargo.lock b/Cargo.lock index e718fc12..0d23ddd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8140,6 +8140,7 @@ dependencies = [ "openvm-pairing", "openvm-pairing-guest", "openvm-sha2", + "rand 0.9.4", "sbv-primitives", "scroll-zkvm-types-base", "serde", diff --git a/crates/types/batch/Cargo.toml b/crates/types/batch/Cargo.toml index e2836f22..af502445 100644 --- a/crates/types/batch/Cargo.toml +++ b/crates/types/batch/Cargo.toml @@ -31,3 +31,4 @@ host = ["dep:sbv-primitives", "dep:c-kzg"] [dev-dependencies] c-kzg = { workspace = true } +rand = { version = "0.9" } diff --git a/crates/types/batch/src/blob_consistency/mod.rs b/crates/types/batch/src/blob_consistency/mod.rs index a6498d47..d7d502a6 100644 --- a/crates/types/batch/src/blob_consistency/mod.rs +++ b/crates/types/batch/src/blob_consistency/mod.rs @@ -7,7 +7,7 @@ mod openvm; mod types; pub use openvm::point_evaluation; -pub use openvm::{kzg_to_versioned_hash, verify_kzg_proof}; +pub use openvm::{is_in_g1_subgroup, kzg_to_versioned_hash, verify_kzg_proof}; pub use types::ToIntrinsic; // Number of bytes in a u256. diff --git a/crates/types/batch/src/blob_consistency/openvm.rs b/crates/types/batch/src/blob_consistency/openvm.rs index 0439b802..a8313fe2 100644 --- a/crates/types/batch/src/blob_consistency/openvm.rs +++ b/crates/types/batch/src/blob_consistency/openvm.rs @@ -1,12 +1,13 @@ use std::ops::{AddAssign, MulAssign}; +use std::slice; use std::sync::LazyLock; use algebra::{Field, IntMod}; -use alloy_primitives::U256; +use alloy_primitives::{U256, hex}; use halo2curves_axiom::bls12_381::G2Affine as Bls12_381_G2; use itertools::Itertools; use openvm_ecc_guest::{AffinePoint, CyclicGroup, msm, weierstrass::WeierstrassPoint}; -use openvm_pairing::bls12_381::{Bls12_381, G1Affine, G2Affine, Scalar}; +use openvm_pairing::bls12_381::{Bls12_381, Fp, G1Affine, G2Affine, Scalar}; use openvm_pairing_guest::{algebra, pairing::PairingCheck}; use super::types::ToIntrinsic; @@ -51,6 +52,16 @@ static KZG_G2_SETUP: LazyLock = LazyLock::new(|| { .to_intrinsic() }); +// Nontrivial BLS12-381 Fp cube root of unity used by the G1 endomorphism; +// bytes are little-endian. +static BETA: Fp = Fp::from_const_bytes(hex!( + "fefffeffffff012e02000a6213d817de8896f8e63ba9b3ddea770f6a07c669ba51ce76df2f67195f0000000000000000" +)); +// BLS_X^2 in the BLS12-381 scalar field; bytes are little-endian. +static X_SQUARE: Scalar = Scalar::from_const_bytes(hex!( + "000000000100000002a4010001a445ac00000000000000000000000000000000" +)); + /// The version for KZG as per EIP-4844. const VERSIONED_HASH_VERSION_KZG: u8 = 1; @@ -145,6 +156,17 @@ fn interpolate(z: &Scalar, coefficients: &[Scalar; BLOB_WIDTH]) -> Scalar { * Scalar::from_u64(blob_width).invert() } +pub fn is_in_g1_subgroup(p: &G1Affine) -> bool { + let expected_x = p.x() * Β + let expected_y = -p.y(); + + // [x^2] * P + let actual_point = msm(slice::from_ref(&X_SQUARE), slice::from_ref(p)); + + // [x^2]P == (BETA * x, -y) + actual_point.x() == &expected_x && actual_point.y() == &expected_y +} + #[cfg(test)] mod test { use super::*; @@ -192,4 +214,69 @@ mod test { let proof_ok = verify_kzg_proof(z, y, commitment, proof); assert!(proof_ok, "verify failed"); } + + /// BETA is a nontrivial cube root of unity mod p + #[test] + fn test_beta() { + use halo2curves_axiom::bls12_381::Fq; + let beta = Fq::from_bytes_be(&BETA.to_be_bytes()).unwrap(); + + // BETA != 1 + assert_ne!(beta, Fq::one(), "BETA != 1"); + // BETA^3 mod p == 1 + let beta_cubed = &beta * &beta * β + assert_eq!(beta_cubed, Fq::one(), "BETA^3 == 1"); + // BETA^2 + BETA + 1 mod p == 0 + assert_eq!( + &beta * &beta + beta + Fq::one(), + Fq::zero(), + "BETA^2 + BETA + 1 == 0" + ); + } + + #[test] + fn test_x_square() { + use halo2curves_axiom::bls12_381::BLS_X; + let x = -Scalar::from_u64(BLS_X); + let x_square = &x * &x; + assert_eq!(x_square, X_SQUARE); + } + + #[test] + fn test_g1_generator_is_in_subgroup() { + assert!(is_in_g1_subgroup(&G1Affine::GENERATOR)); + } + + #[test] + fn test_point_on_curve_but_not_in_subgroup() { + use halo2curves_axiom::{CurveAffine, bls12_381::Fq}; + + let four = Fq::one() + Fq::one() + Fq::one() + Fq::one(); + + for _ in 0..1024 { + let x_bytes: [u8; 48] = rand::random(); + let Some(x) = Fq::from_bytes(&x_bytes).into_option() else { + continue; + }; + let x3 = x.square() * x.clone(); + let x3_plus_4 = x3 + four.clone(); + + let Some(y) = x3_plus_4.sqrt().into_option() else { + continue; + }; + + let point_axiom = Bls12_381_G1::from_xy(x, y).unwrap(); + + // point in group + let is_torsion_free: bool = point_axiom.is_torsion_free().into(); + if is_torsion_free { + continue; + } + + assert!(!is_in_g1_subgroup(&point_axiom.to_intrinsic())); + return; + } + + panic!("failed to find a non-subgroup curve point"); + } } diff --git a/crates/types/batch/src/witness.rs b/crates/types/batch/src/witness.rs index fb7e569c..7af70c90 100644 --- a/crates/types/batch/src/witness.rs +++ b/crates/types/batch/src/witness.rs @@ -8,6 +8,7 @@ use types_base::{ }; use crate::{ + blob_consistency::{ToIntrinsic, is_in_g1_subgroup}, builder::{ BatchInfoBuilder, BatchInfoBuilderV6, BatchInfoBuilderV7, BuilderArgsV6, BuilderArgsV7, validium::{ValidiumBatchInfoBuilder, ValidiumBuilderArgs}, @@ -74,14 +75,23 @@ pub fn build_intrinsic_point( use openvm_pairing::bls12_381::{Fp, G1Affine}; let x = Fp::from_be_bytes(&x)?; let y = Fp::from_be_bytes(&y)?; - unsafe { G1Affine::from_xy(x, y) } + // SAFETY: We have already verified the point is in the G1 subgroup via `is_in_g1_subgroup` check. + let p = unsafe { G1Affine::from_xy(x, y)? }; + is_in_g1_subgroup(&p).then_some(p) } pub fn build_point(x: Bytes48, y: Bytes48) -> Option { use halo2curves_axiom::bls12_381::{Fq, G1Affine}; let x = Fq::from_bytes_be(&x).into_option()?; let y = Fq::from_bytes_be(&y).into_option()?; - G1Affine::from_xy(x, y).into_option() + let p = G1Affine::from_xy(x, y).into_option()?; + + let is_in_g1_subgroup = is_in_g1_subgroup(&p.to_intrinsic()); + + #[cfg(feature = "host")] + assert_eq!(is_in_g1_subgroup, bool::from(p.is_torsion_free())); + + is_in_g1_subgroup.then_some(p) } /// Witness to the batch circuit.