diff --git a/Cargo.toml b/Cargo.toml index 551bcf27..8a6bc82a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "nomos-services/mempool", "nomos-services/http", "nomos-da-core/reed-solomon", + "nomos-da-core/kzg", "nodes/nomos-node", "simulations", "consensus-engine", diff --git a/nomos-da-core/kzg/Cargo.toml b/nomos-da-core/kzg/Cargo.toml new file mode 100644 index 00000000..2052cfe9 --- /dev/null +++ b/nomos-da-core/kzg/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nomos-kzg" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +kzg = { git = "https://github.com/sifraitech/rust-kzg.git", package = "rust-kzg-blst", features = ["parallel"] } +kzg_traits = { git = "https://github.com/sifraitech/rust-kzg.git", package = "kzg" } + +[dev-dependencies] +criterion = "0.5.1" + +[[bench]] +name = "nomos_kzg" +harness = false diff --git a/nomos-da-core/kzg/benches/nomos_kzg.rs b/nomos-da-core/kzg/benches/nomos_kzg.rs new file mode 100644 index 00000000..e34d32e4 --- /dev/null +++ b/nomos-da-core/kzg/benches/nomos_kzg.rs @@ -0,0 +1,37 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use kzg::{types::kzg_settings::FsKZGSettings, utils::generate_trusted_setup}; +use kzg_traits::{FFTSettings, KZGSettings}; +use nomos_kzg::Blob; + +fn nomos_dynamic_vs_external(c: &mut Criterion) { + let (g1s, g2s) = generate_trusted_setup(4096, [0; 32]); + let fft_settings = kzg::types::fft_settings::FsFFTSettings::new(8).unwrap(); + let settings = FsKZGSettings::new(&g1s, &g2s, 4096, &fft_settings).unwrap(); + let blob = Blob::from_bytes(&[5; 4096 * 32]).unwrap(); + + let mut group = c.benchmark_group("KZG Commitment Benchmarks"); + + group.bench_function("nomos blob commitment", |b| { + b.iter(|| { + nomos_kzg::blob_to_kzg_commitment( + black_box(&blob), + black_box(&settings), + black_box(4096), + ) + }) + }); + + group.bench_function("external blob commitment", |b| { + b.iter(|| { + kzg::eip_4844::blob_to_kzg_commitment_rust( + black_box(&blob.inner()), + black_box(&settings), + ) + }) + }); + + group.finish(); +} + +criterion_group!(benches, nomos_dynamic_vs_external); +criterion_main!(benches); diff --git a/nomos-da-core/kzg/src/dynamic_kzg.rs b/nomos-da-core/kzg/src/dynamic_kzg.rs new file mode 100644 index 00000000..e9e0e717 --- /dev/null +++ b/nomos-da-core/kzg/src/dynamic_kzg.rs @@ -0,0 +1,246 @@ +//! Custom variant of rust-kzg that supports dynamic sized blobs. +//! https://github.com/sifraitech/rust-kzg +//! Some types were changed to fit our API for comfort. +//! Blob related constants were removed and we use a config based approach. + +use crate::types::{Blob, Commitment, KzgSettings, Proof}; +use kzg::eip_4844::{hash_to_bls_field, verify_kzg_proof_rust}; +use kzg::kzg_proofs::g1_linear_combination; +use kzg::types::fr::FsFr; +use kzg::types::g1::FsG1; +use kzg::types::kzg_settings::FsKZGSettings; +use kzg::types::poly::FsPoly; +use kzg_traits::eip_4844::{ + bytes_of_uint64, hash, CHALLENGE_INPUT_SIZE, FIAT_SHAMIR_PROTOCOL_DOMAIN, +}; +use kzg_traits::{Fr, Poly, G1}; + +pub fn blob_to_kzg_commitment( + blob: &Blob, + s: &FsKZGSettings, + field_elements_per_blob: usize, +) -> FsG1 { + let mut out = FsG1::default(); + g1_linear_combination(&mut out, &s.secret_g1, &blob.inner, field_elements_per_blob); + out +} + +pub fn compute_blob_kzg_proof( + blob: &Blob, + commitment: &Commitment, + settings: &KzgSettings, +) -> Result { + if !commitment.0.is_valid() { + return Err("Invalid commitment".to_string()); + } + + let evaluation_challenge_fr = compute_challenge(blob, &commitment.0, settings); + let (proof, _) = compute_kzg_proof(blob, &evaluation_challenge_fr, settings); + Ok(proof) +} + +pub fn verify_blob_kzg_proof( + blob: &Blob, + commitment: &Commitment, + proof: &Proof, + settings: &KzgSettings, +) -> Result { + if !commitment.0.is_valid() { + return Err("Invalid commitment".to_string()); + } + if !proof.0.is_valid() { + return Err("Invalid proof".to_string()); + } + + let polynomial = blob_to_polynomial(&blob.inner); + let evaluation_challenge_fr = compute_challenge(blob, &commitment.0, settings); + let y_fr = evaluate_polynomial_in_evaluation_form_rust( + &polynomial, + &evaluation_challenge_fr, + &settings.settings, + ); + verify_kzg_proof_rust( + &commitment.0, + &evaluation_challenge_fr, + &y_fr, + &proof.0, + &settings.settings, + ) +} + +fn compute_challenge(blob: &Blob, commitment: &FsG1, settings: &KzgSettings) -> FsFr { + let mut bytes: Vec = vec![0; CHALLENGE_INPUT_SIZE]; + + // Copy domain separator + bytes[..16].copy_from_slice(&FIAT_SHAMIR_PROTOCOL_DOMAIN); + bytes_of_uint64(&mut bytes[16..24], blob.len() as u64); + // Set all other bytes of this 16-byte (little-endian) field to zero + bytes_of_uint64(&mut bytes[24..32], 0); + + // Copy blob + for i in 0..blob.len() { + let v = blob.inner[i].to_bytes(); + bytes[(32 + i * settings.bytes_per_field_element) + ..(32 + (i + 1) * settings.bytes_per_field_element)] + .copy_from_slice(&v); + } + + // Copy commitment + let v = commitment.to_bytes(); + for i in 0..v.len() { + bytes[32 + settings.bytes_per_field_element * blob.len() + i] = v[i]; + } + + // Now let's create the challenge! + let eval_challenge = hash(&bytes); + hash_to_bls_field(&eval_challenge) +} + +fn compute_kzg_proof(blob: &Blob, z: &FsFr, s: &KzgSettings) -> (FsG1, FsFr) { + let polynomial = blob_to_polynomial(blob.inner.as_slice()); + let poly_len = polynomial.coeffs.len(); + let y = evaluate_polynomial_in_evaluation_form_rust(&polynomial, z, &s.settings); + + let mut tmp: FsFr; + let roots_of_unity: &Vec = &s.settings.fs.roots_of_unity; + + let mut m: usize = 0; + let mut q: FsPoly = FsPoly::new(poly_len).unwrap(); + + let mut inverses_in: Vec = vec![FsFr::default(); poly_len]; + let mut inverses: Vec = vec![FsFr::default(); poly_len]; + + for i in 0..poly_len { + if z.equals(&roots_of_unity[i]) { + // We are asked to compute a KZG proof inside the domain + m = i + 1; + inverses_in[i] = FsFr::one(); + continue; + } + // (p_i - y) / (ω_i - z) + q.coeffs[i] = polynomial.coeffs[i].sub(&y); + inverses_in[i] = roots_of_unity[i].sub(z); + } + + fr_batch_inv(&mut inverses, &inverses_in, poly_len); + + for (i, inverse) in inverses.iter().enumerate().take(poly_len) { + q.coeffs[i] = q.coeffs[i].mul(inverse); + } + + if m != 0 { + // ω_{m-1} == z + m -= 1; + q.coeffs[m] = FsFr::zero(); + for i in 0..poly_len { + if i == m { + continue; + } + // Build denominator: z * (z - ω_i) + tmp = z.sub(&roots_of_unity[i]); + inverses_in[i] = tmp.mul(z); + } + + fr_batch_inv(&mut inverses, &inverses_in, poly_len); + + for i in 0..poly_len { + if i == m { + continue; + } + // Build numerator: ω_i * (p_i - y) + tmp = polynomial.coeffs[i].sub(&y); + tmp = tmp.mul(&roots_of_unity[i]); + // Do the division: (p_i - y) * ω_i / (z * (z - ω_i)) + tmp = tmp.mul(&inverses[i]); + q.coeffs[m] = q.coeffs[m].add(&tmp); + } + } + + let proof = g1_lincomb(&s.settings.secret_g1, &q.coeffs, poly_len); + (proof, y) +} + +fn evaluate_polynomial_in_evaluation_form_rust(p: &FsPoly, x: &FsFr, s: &FsKZGSettings) -> FsFr { + let poly_len = p.coeffs.len(); + let roots_of_unity: &Vec = &s.fs.roots_of_unity; + let mut inverses_in: Vec = vec![FsFr::default(); poly_len]; + let mut inverses: Vec = vec![FsFr::default(); poly_len]; + + for i in 0..poly_len { + if x.equals(&roots_of_unity[i]) { + return p.get_coeff_at(i); + } + inverses_in[i] = x.sub(&roots_of_unity[i]); + } + + fr_batch_inv(&mut inverses, &inverses_in, poly_len); + + let mut tmp: FsFr; + let mut out = FsFr::zero(); + + for i in 0..poly_len { + tmp = inverses[i].mul(&roots_of_unity[i]); + tmp = tmp.mul(&p.coeffs[i]); + out = out.add(&tmp); + } + + tmp = FsFr::from_u64(poly_len as u64); + out = out.div(&tmp).unwrap(); + tmp = x.pow(poly_len); + tmp = tmp.sub(&FsFr::one()); + out = out.mul(&tmp); + out +} + +fn fr_batch_inv(out: &mut [FsFr], a: &[FsFr], len: usize) { + assert!(len > 0); + + let mut accumulator = FsFr::one(); + + for i in 0..len { + out[i] = accumulator; + accumulator = accumulator.mul(&a[i]); + } + + accumulator = accumulator.eucl_inverse(); + + for i in (0..len).rev() { + out[i] = out[i].mul(&accumulator); + accumulator = accumulator.mul(&a[i]); + } +} + +fn g1_lincomb(points: &[FsG1], scalars: &[FsFr], length: usize) -> FsG1 { + let mut out = FsG1::default(); + g1_linear_combination(&mut out, points, scalars, length); + out +} + +fn blob_to_polynomial(blob: &[FsFr]) -> FsPoly { + let mut p: FsPoly = FsPoly::new(blob.len()).unwrap(); + p.coeffs = blob.to_vec(); + p +} + +#[cfg(test)] +mod test { + use super::*; + use kzg::eip_4844::blob_to_kzg_commitment_rust; + use kzg::utils::generate_trusted_setup; + use kzg_traits::{FFTSettings, KZGSettings}; + + #[test] + fn test_blob_to_kzg_commitment() { + let (g1s, g2s) = generate_trusted_setup(4096, [0; 32]); + let fft_settings = kzg::types::fft_settings::FsFFTSettings::new(8).unwrap(); + let settings = FsKZGSettings::new(&g1s, &g2s, 4096, &fft_settings).unwrap(); + let kzg_settings = KzgSettings { + settings, + bytes_per_field_element: 32, + }; + let blob = Blob::from_bytes(&[5; 4096 * 32], &kzg_settings).unwrap(); + let commitment = blob_to_kzg_commitment(&blob, &kzg_settings.settings, 4096); + let commitment2 = blob_to_kzg_commitment_rust(&blob.inner, &kzg_settings.settings); + assert_eq!(commitment, commitment2); + } +} diff --git a/nomos-da-core/kzg/src/lib.rs b/nomos-da-core/kzg/src/lib.rs new file mode 100644 index 00000000..a0b3fcff --- /dev/null +++ b/nomos-da-core/kzg/src/lib.rs @@ -0,0 +1,83 @@ +mod dynamic_kzg; +mod types; + +use crate::types::KzgSettings; +pub use crate::types::{Blob, Commitment, Proof}; +pub use dynamic_kzg::{blob_to_kzg_commitment, compute_blob_kzg_proof, verify_blob_kzg_proof}; +use std::error::Error; + +pub const BYTES_PER_PROOF: usize = 48; +pub const BYTES_PER_COMMITMENT: usize = 48; + +/// Compute a kzg commitment for the given data. +/// It works for arbitrary data, but the data must be a multiple of **32 bytes**. +/// The data is interpreted as a sequence of field elements. Each consisting of **32 bytes**. +pub fn compute_commitment( + data: &[u8], + settings: &KzgSettings, +) -> Result> { + let blob = Blob::from_bytes(data, settings)?; + Ok(Commitment(blob_to_kzg_commitment( + &blob, + &settings.settings, + data.len() / settings.bytes_per_field_element, + ))) +} + +/// Compute a kzg proof for each field element in the given data. +/// It works for arbitrary data, but the data must be a multiple of **32 bytes**. +/// The data is interpreted as a sequence of field elements. Each consisting of **32 bytes**. +pub fn compute_proofs( + data: &[u8], + commitment: &Commitment, + settings: &KzgSettings, +) -> Result, Box> { + let blobs = data + .chunks(settings.bytes_per_field_element) + .map(|b| Blob::from_bytes(b, settings)); + let mut res = Vec::new(); + for blob in blobs { + let blob = blob?; + res.push(Proof(compute_blob_kzg_proof(&blob, commitment, settings)?)) + } + Ok(res) +} + +/// Verify a kzg proof for the given blob. +/// It works for arbitrary data, but the data must be a multiple of **32 bytes**. +/// The data is interpreted as a sequence of field elements. Each consisting of **32 bytes**. +pub fn verify_blob( + blob: &[u8], + proof: &Proof, + commitment: &Commitment, + settings: &KzgSettings, +) -> Result> { + let blob = Blob::from_bytes(blob, settings)?; + verify_blob_kzg_proof(&blob, commitment, proof, settings).map_err(|e| e.into()) +} + +#[cfg(test)] +mod test { + use super::*; + use kzg::types::kzg_settings::FsKZGSettings; + use kzg::utils::generate_trusted_setup; + use kzg_traits::{FFTSettings, KZGSettings}; + + #[test] + fn test_compute_and_verify() -> Result<(), Box> { + let (g1s, g2s) = generate_trusted_setup(4096, [0; 32]); + let fft_settings = kzg::types::fft_settings::FsFFTSettings::new(8).unwrap(); + let settings = FsKZGSettings::new(&g1s, &g2s, 4096, &fft_settings).unwrap(); + let kzg_settings = KzgSettings { + settings, + bytes_per_field_element: 32, + }; + let blob = vec![0; 4096]; + let commitment = compute_commitment(&blob, &kzg_settings)?; + let proofs = compute_proofs(&blob, &commitment, &kzg_settings)?; + for proof in proofs { + assert!(verify_blob(&blob, &proof, &commitment, &kzg_settings)?); + } + Ok(()) + } +} diff --git a/nomos-da-core/kzg/src/types.rs b/nomos-da-core/kzg/src/types.rs new file mode 100644 index 00000000..3d6054ff --- /dev/null +++ b/nomos-da-core/kzg/src/types.rs @@ -0,0 +1,63 @@ +use crate::{BYTES_PER_COMMITMENT, BYTES_PER_PROOF}; +use kzg::types::fr::FsFr; +use kzg::types::g1::FsG1; +use kzg::types::kzg_settings::FsKZGSettings; +use kzg_traits::{Fr, G1}; +use std::error::Error; + +/// A wrapper around the KZG settings that also stores the number of bytes per field element. +pub struct KzgSettings { + pub settings: FsKZGSettings, + pub bytes_per_field_element: usize, +} + +/// A KZG commitment. +pub struct Commitment(pub(crate) FsG1); + +/// A KZG proof. +pub struct Proof(pub(crate) FsG1); + +/// A blob of data. +pub struct Blob { + pub(crate) inner: Vec, +} + +impl Commitment { + pub fn as_bytes_owned(&self) -> [u8; BYTES_PER_COMMITMENT] { + self.0.to_bytes() + } +} + +impl Proof { + pub fn as_bytes_owned(&self) -> [u8; BYTES_PER_PROOF] { + self.0.to_bytes() + } +} + +impl Blob { + pub fn from_bytes(data: &[u8], settings: &KzgSettings) -> Result> { + let mut inner = Vec::with_capacity(data.len() / settings.bytes_per_field_element); + for chunk in data.chunks(settings.bytes_per_field_element) { + if chunk.len() < settings.bytes_per_field_element { + let mut padded_chunk = vec![0; settings.bytes_per_field_element]; + padded_chunk[..chunk.len()].copy_from_slice(chunk); + inner.push(FsFr::from_bytes(&padded_chunk)?); + } else { + inner.push(FsFr::from_bytes(chunk)?); + } + } + Ok(Self { inner }) + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub fn inner(&self) -> Vec { + self.inner.clone() + } +}