diff --git a/Cargo.toml b/Cargo.toml index 11f325628..2c670e469 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ license-file = "LICENSE" ahash = { version = "0.8.11", default-features = false } alloy-sol-types = { version = "0.8.5" } ark-bls12-381 = { version = "0.5.0" } +ark-bn254 = { version = "0.5.0" } ark-curve25519 = { version = "0.5.0" } ark-ec = { version = "0.5.0" } ark-ff = { version = "0.5.0" } @@ -35,6 +36,7 @@ chrono = { version = "0.4.38", default-features = false } curve25519-dalek = { version = "4", features = ["rand_core"] } derive_more = { version = "0.99" } enum_dispatch = { version = "0.3.13" } +ff = { version = "0.13.0"} flexbuffers = { version = "2.0.0" } indexmap = { version = "2.1", default-features = false } indicatif = "0.17.8" @@ -42,6 +44,7 @@ itertools = { version = "0.13.0", default-features = false, features = ["use_all lalrpop = { version = "0.22.0" } lalrpop-util = { version = "0.22.0", default-features = false } merlin = { version = "2" } +nova-snark = { version = "0.39.0" } num-traits = { version = "0.2", default-features = false } num-bigint = { version = "0.4.4", default-features = false } opentelemetry = { version = "0.23.0" } diff --git a/crates/proof-of-sql/Cargo.toml b/crates/proof-of-sql/Cargo.toml index bb02f7cac..60efb8f26 100644 --- a/crates/proof-of-sql/Cargo.toml +++ b/crates/proof-of-sql/Cargo.toml @@ -16,6 +16,7 @@ test = true [dependencies] ahash = { workspace = true } ark-bls12-381 = { workspace = true } +ark-bn254 = { workspace = true } ark-curve25519 = { workspace = true } ark-ec = { workspace = true } ark-ff = { workspace = true } @@ -35,10 +36,12 @@ curve25519-dalek = { workspace = true, features = ["serde"] } chrono = { workspace = true, features = ["serde"] } derive_more = { workspace = true } enum_dispatch = { workspace = true } +ff = { workspace = true } indexmap = { workspace = true, features = ["serde"] } indicatif = { workspace = true } itertools = { workspace = true } merlin = { workspace = true, optional = true } +nova-snark = { workspace = true } num-traits = { workspace = true } num-bigint = { workspace = true, default-features = false } postcard = { workspace = true, features = ["alloc"] } diff --git a/crates/proof-of-sql/src/proof_primitive/hyperkzg.rs b/crates/proof-of-sql/src/proof_primitive/hyperkzg.rs new file mode 100644 index 000000000..f8251371d --- /dev/null +++ b/crates/proof-of-sql/src/proof_primitive/hyperkzg.rs @@ -0,0 +1,358 @@ +use crate::base::{ + commitment::{Commitment, CommitmentEvaluationProof, CommittableColumn}, + scalar::{MontScalar, Scalar}, + slice_ops, +}; +use alloc::vec::Vec; +use core::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; +use ff::{Field, PrimeField}; +use itertools::Itertools; +use nova_snark::{ + errors::NovaError, + provider::{ + bn256_grumpkin::bn256::Scalar as NovaScalar, + hyperkzg::{ + CommitmentEngine, CommitmentKey, EvaluationArgument, EvaluationEngine, VerifierKey, + }, + Bn256EngineKZG, + }, + traits::{ + commitment::CommitmentEngineTrait, evaluation::EvaluationEngineTrait, Engine, + TranscriptEngineTrait, TranscriptReprTrait, + }, +}; +use serde::{Deserialize, Serialize}; + +/// The +/// The scalar used in the `HyperKZG` PCS. This is the BN254 scalar. +pub type BNScalar = MontScalar; + +type NovaCommitment = nova_snark::provider::hyperkzg::Commitment; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +/// A newtype wrapper of nova's hyperkzg commitment. +/// This is the commitment type used in the hyperkzg proof system. +pub struct HyperKZGCommitment { + /// The underlying commitment. + pub commitment: NovaCommitment, +} + +/// The evaluation proof for the `HyperKZG` PCS. +pub type HyperKZGCommitmentEvaluationProof = EvaluationArgument; + +impl AddAssign for HyperKZGCommitment { + fn add_assign(&mut self, rhs: Self) { + self.commitment = self.commitment + rhs.commitment; + } +} +impl From<&BNScalar> for NovaScalar { + fn from(value: &BNScalar) -> Self { + ff::PrimeField::from_repr_vartime(bytemuck::cast::<[u64; 4], [u8; 32]>(value.into()).into()) + .unwrap() + } +} +impl Mul<&HyperKZGCommitment> for BNScalar { + type Output = HyperKZGCommitment; + fn mul(self, rhs: &HyperKZGCommitment) -> Self::Output { + Self::Output { + commitment: rhs.commitment * NovaScalar::from(self), + } + } +} +impl From for NovaScalar { + fn from(value: BNScalar) -> Self { + Self::from(&value) + } +} +impl Mul for BNScalar { + type Output = HyperKZGCommitment; + #[allow(clippy::op_ref)] + fn mul(self, rhs: HyperKZGCommitment) -> Self::Output { + self * &rhs + } +} +impl Neg for HyperKZGCommitment { + type Output = Self; + fn neg(self) -> Self::Output { + (-BNScalar::ONE) * self + } +} +impl SubAssign for HyperKZGCommitment { + fn sub_assign(&mut self, rhs: Self) { + *self += -rhs; + } +} +impl Sub for HyperKZGCommitment { + type Output = Self; + fn sub(mut self, rhs: Self) -> Self::Output { + self -= rhs; + self + } +} + +impl Scalar for BNScalar { + const MAX_SIGNED: Self = Self(ark_ff::MontFp!( + "10944121435919637611123202872628637544274182200208017171849102093287904247808" + )); + const ZERO: Self = Self(ark_ff::MontFp!("0")); + const ONE: Self = Self(ark_ff::MontFp!("1")); + const TWO: Self = Self(ark_ff::MontFp!("2")); + const TEN: Self = Self(ark_ff::MontFp!("10")); +} + +fn compute_commitments_impl + Clone>( + setup: &CommitmentKey, + offset: usize, + scalars: &[T], +) -> HyperKZGCommitment { + let commitment = CommitmentEngine::commit( + setup, + &itertools::repeat_n(MontScalar::ZERO, offset) + .chain(scalars.iter().map(Into::into)) + .map(Into::into) + .collect_vec(), + &NovaScalar::ZERO, + ); + HyperKZGCommitment { commitment } +} +impl Commitment for HyperKZGCommitment { + type Scalar = BNScalar; + type PublicSetup<'a> = &'a CommitmentKey; + fn compute_commitments( + committable_columns: &[crate::base::commitment::CommittableColumn], + offset: usize, + setup: &Self::PublicSetup<'_>, + ) -> Vec { + committable_columns + .iter() + .map(|column| match column { + CommittableColumn::Boolean(vals) => compute_commitments_impl(setup, offset, vals), + CommittableColumn::TinyInt(vals) => compute_commitments_impl(setup, offset, vals), + CommittableColumn::SmallInt(vals) => compute_commitments_impl(setup, offset, vals), + CommittableColumn::Int(vals) => compute_commitments_impl(setup, offset, vals), + CommittableColumn::BigInt(vals) | CommittableColumn::TimestampTZ(_, _, vals) => { + compute_commitments_impl(setup, offset, vals) + } + CommittableColumn::Int128(vals) => compute_commitments_impl(setup, offset, vals), + CommittableColumn::Decimal75(_, _, vals) + | CommittableColumn::Scalar(vals) + | CommittableColumn::VarChar(vals) => compute_commitments_impl(setup, offset, vals), + CommittableColumn::RangeCheckWord(vals) => { + compute_commitments_impl(setup, offset, vals) + } + }) + .collect() + } + fn append_to_transcript(&self, transcript: &mut impl crate::base::proof::Transcript) { + transcript.extend_as_le(self.commitment.to_transcript_bytes()); + } +} + +impl CommitmentEvaluationProof for HyperKZGCommitmentEvaluationProof { + type Scalar = BNScalar; + type Commitment = HyperKZGCommitment; + type Error = NovaError; + type ProverPublicSetup<'a> = &'a CommitmentKey; + type VerifierPublicSetup<'a> = &'a VerifierKey; + + fn new( + transcript: &mut impl crate::base::proof::Transcript, + a: &[Self::Scalar], + b_point: &[Self::Scalar], + generators_offset: u64, + setup: &Self::ProverPublicSetup<'_>, + ) -> Self { + assert_eq!(generators_offset, 0); + let mut nova_transcript = ::TE::new(&[]); + nova_transcript.absorb( + &[], + &NovaScalar::from(transcript.scalar_challenge_as_be::()), + ); + let mut nova_point = slice_ops::slice_cast(b_point); + nova_point.reverse(); + if nova_point.is_empty() { + nova_point.push(NovaScalar::ZERO); + } + let mut nova_a = slice_ops::slice_cast(a); + nova_a.extend(itertools::repeat_n( + NovaScalar::ZERO, + (1 << nova_point.len()) - nova_a.len(), + )); + let proof = EvaluationEngine::prove( + *setup, + &EvaluationEngine::setup(*setup).0, // This parameter is unused + &mut nova_transcript, + &NovaCommitment::default(), // This parameter is unused + &nova_a, + &nova_point, + &NovaScalar::default(), // This parameter is unused + ) + .unwrap(); + let post_proof_state: NovaScalar = nova_transcript.squeeze(&[]).unwrap(); + transcript.extend_as_le(<[u8; 32]>::from(post_proof_state.to_repr())); + proof + } + + fn verify_batched_proof( + &self, + transcript: &mut impl crate::base::proof::Transcript, + commit_batch: &[Self::Commitment], + batching_factors: &[Self::Scalar], + evaluations: &[Self::Scalar], + b_point: &[Self::Scalar], + generators_offset: u64, + _table_length: usize, + setup: &Self::VerifierPublicSetup<'_>, + ) -> Result<(), Self::Error> { + if generators_offset != 0 { + Err(NovaError::InvalidPCS)?; + } + let mut nova_transcript = ::TE::new(&[]); + nova_transcript.absorb( + &[], + &NovaScalar::from(transcript.scalar_challenge_as_be::()), + ); + let nova_commit = commit_batch + .iter() + .zip(batching_factors) + .map(|(c, m)| c.commitment * NovaScalar::from(m)) + .fold(NovaCommitment::default(), Add::add); + let nova_eval = evaluations + .iter() + .zip(batching_factors) + .map(|(&e, &f)| e * f) + .sum::(); + let mut nova_point = slice_ops::slice_cast(b_point); + nova_point.reverse(); + if nova_point.is_empty() { + nova_point.push(NovaScalar::ZERO); + } + EvaluationEngine::::verify( + setup, + &mut nova_transcript, + &nova_commit, + &nova_point, + &nova_eval.into(), + self, + )?; + let post_proof_state: NovaScalar = nova_transcript.squeeze(&[]).unwrap(); + transcript.extend_as_le(<[u8; 32]>::from(post_proof_state.to_repr())); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::base::commitment::commitment_evaluation_proof_test::{ + test_commitment_evaluation_proof_with_length_1, test_random_commitment_evaluation_proof, + test_simple_commitment_evaluation_proof, + }; + use ark_std::UniformRand; + use nova_snark::provider::hyperkzg::CommitmentEngine; + use num_traits::Inv; + + #[test] + fn we_can_get_bn_scalar_constants_from_z_p() { + assert_eq!(BNScalar::from(0), BNScalar::ZERO); + assert_eq!(BNScalar::from(1), BNScalar::ONE); + assert_eq!(BNScalar::from(2), BNScalar::TWO); + // -1/2 == least upper bound + assert_eq!(-BNScalar::TWO.inv().unwrap(), BNScalar::MAX_SIGNED); + assert_eq!(BNScalar::from(10), BNScalar::TEN); + } + + #[test] + fn we_can_convert_from_posql_scalar_to_nova_scalar() { + // Test zero + assert_eq!(NovaScalar::from(0_u64), NovaScalar::from(BNScalar::ZERO)); + + // Test one + assert_eq!(NovaScalar::from(1_u64), NovaScalar::from(BNScalar::ONE)); + + // Test negative one + assert_eq!(-NovaScalar::from(1_u64), NovaScalar::from(-BNScalar::ONE)); + + // Test two + assert_eq!(NovaScalar::from(2_u64), NovaScalar::from(BNScalar::TWO)); + + // Test ten + assert_eq!(NovaScalar::from(10_u64), NovaScalar::from(BNScalar::TEN)); + + // Test a large value + let large_value = BNScalar::from(123_456_789_u64); + assert_eq!( + NovaScalar::from(123_456_789_u64), + NovaScalar::from(large_value) + ); + + let mut rng = ark_std::test_rng(); + + for _ in 0..10 { + let a = BNScalar::rand(&mut rng); + let b = BNScalar::rand(&mut rng); + assert_eq!( + NovaScalar::from(a + b), + NovaScalar::from(a) + NovaScalar::from(b) + ); + assert_eq!( + NovaScalar::from(a * b), + NovaScalar::from(a) * NovaScalar::from(b) + ); + } + } + + #[test] + fn we_can_create_small_hyperkzg_evaluation_proofs() { + let ck: CommitmentKey = CommitmentEngine::setup(b"test", 32); + let (_, vk) = EvaluationEngine::setup(&ck); + test_simple_commitment_evaluation_proof::(&&ck, &&vk); + test_commitment_evaluation_proof_with_length_1::( + &&ck, &&vk, + ); + } + + #[test] + fn we_can_create_hyperkzg_evaluation_proofs_with_various_lengths() { + let ck: CommitmentKey = CommitmentEngine::setup(b"test", 128); + let (_, vk) = EvaluationEngine::setup(&ck); + test_random_commitment_evaluation_proof::( + 2, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 3, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 4, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 5, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 8, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 10, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 16, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 20, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 32, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 50, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 64, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 100, 0, &&ck, &&vk, + ); + test_random_commitment_evaluation_proof::( + 128, 0, &&ck, &&vk, + ); + } +} diff --git a/crates/proof-of-sql/src/proof_primitive/mod.rs b/crates/proof-of-sql/src/proof_primitive/mod.rs index 35514b959..b3ea2b825 100644 --- a/crates/proof-of-sql/src/proof_primitive/mod.rs +++ b/crates/proof-of-sql/src/proof_primitive/mod.rs @@ -4,3 +4,6 @@ pub mod dory; pub(super) mod dynamic_matrix_utils; /// TODO: add docs pub(crate) mod sumcheck; + +/// An implementation of hyper-kzg PCS. This is a wrapper around nova's hyper-kzg implementation. +pub mod hyperkzg; diff --git a/crates/proof-of-sql/tests/integration_tests.rs b/crates/proof-of-sql/tests/integration_tests.rs index d479ab118..ca07b0aa2 100644 --- a/crates/proof-of-sql/tests/integration_tests.rs +++ b/crates/proof-of-sql/tests/integration_tests.rs @@ -169,6 +169,41 @@ fn we_can_prove_a_basic_equality_query_with_dory() { assert_eq!(owned_table_result, expected_result); } +#[test] +fn we_can_prove_a_basic_equality_query_with_hyperkzg() { + use nova_snark::{ + provider::{ + hyperkzg::{CommitmentEngine, CommitmentKey, EvaluationArgument, EvaluationEngine}, + Bn256EngineKZG, + }, + traits::{commitment::CommitmentEngineTrait, evaluation::EvaluationEngineTrait}, + }; + type CP = EvaluationArgument; + + let ck: CommitmentKey = CommitmentEngine::setup(b"test", 32); + let (_, vk) = EvaluationEngine::setup(&ck); + + let mut accessor = OwnedTableTestAccessor::::new_empty_with_setup(&ck); + accessor.add_table( + "sxt.table".parse().unwrap(), + owned_table([bigint("a", [1, 2, 3]), bigint("b", [1, 0, 1])]), + 0, + ); + let query = QueryExpr::try_new( + "SELECT * FROM table WHERE b = 1".parse().unwrap(), + "sxt".into(), + &accessor, + ) + .unwrap(); + let verifiable_result = VerifiableQueryResult::::new(query.proof_expr(), &accessor, &&ck); + let owned_table_result = verifiable_result + .verify(query.proof_expr(), &accessor, &&vk) + .unwrap() + .table; + let expected_result = owned_table([bigint("a", [1, 3]), bigint("b", [1, 1])]); + assert_eq!(owned_table_result, expected_result); +} + #[test] #[cfg(feature = "blitzar")] fn we_can_prove_a_basic_inequality_query_with_curve25519() {