diff --git a/Cargo.lock b/Cargo.lock index d5eb1780..4c8a39b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,6 +754,7 @@ dependencies = [ name = "gkr" version = "0.1.0" dependencies = [ + "common", "field", "num-traits", "poly", @@ -1817,6 +1818,7 @@ dependencies = [ name = "transcript" version = "0.1.0" dependencies = [ + "blake3", "field", "spongefish", ] diff --git a/README.md b/README.md index df5954ad..d57a1ae1 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,12 @@ Select the workload with `--circuit`: `--num-blocks` to set the chain or block-aligned message length in 64-byte blocks. Use `--threads` to set the number of worker threads. +Use `--pcs-security-bits 100` or `128` to select the PCS round budget; the default is `100`. +The implementation derives internal parameters from the padded witness size. +The `100` target uses list decoding and an initial out-of-domain (OOD) check. +The `128` target uses unique decoding and omits that check. +Spartan still uses `Q100`; this option does not set Spartan security. + ## Benchmarks Run all SHA-256 circuit benchmarks with one Rayon worker: diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 1897a00b..e6105294 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -8,6 +8,7 @@ pub mod claim; pub mod fold; pub mod opening; pub mod params; +pub mod security; pub mod shape; pub mod table; pub mod virtual_map; @@ -18,6 +19,7 @@ pub use fold::{ }; pub use opening::OpeningQuery; pub use params::{BitZParams, ParamsError, VirtualParams, VirtualParamsError}; +pub use security::SecurityLevel; pub use shape::{Shape, ShapeError}; pub use table::{BitTable, TableError, TransposeError, TransposedBitTable}; pub use virtual_map::{ diff --git a/crates/common/src/security.rs b/crates/common/src/security.rs new file mode 100644 index 00000000..eb0217b5 --- /dev/null +++ b/crates/common/src/security.rs @@ -0,0 +1,47 @@ +//! Shared protocol security targets and grinding requirements. + +/// The target for each classical PCS challenge block over `F128`. +/// +/// This target does not certify the complete protocol or quantum security. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SecurityLevel { + Bits100, + Bits128, +} + +impl SecurityLevel { + /// Returns the classical security target in bits. + pub const fn bits(self) -> u32 { + match self { + Self::Bits100 => 100, + Self::Bits128 => 128, + } + } + + /// Returns `ceil(log2(coefficient)) + target - 128`, bounded below by zero. + /// + /// This covers a challenge block with error at most `coefficient / 2^128`. + /// A zero coefficient needs no grinding. + pub const fn grinding_bits(self, coefficient: usize) -> u32 { + if coefficient == 0 { + return 0; + } + let log_coefficient = usize::BITS - (coefficient - 1).leading_zeros(); + (self.bits() + log_coefficient).saturating_sub(128) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grinding_covers_the_integer_error_coefficient() { + for (coefficient, expected) in [(0, 0), (1, 0), (2, 1), (3, 2), (7, 3), (8, 3), (15, 4)] { + assert_eq!(SecurityLevel::Bits128.grinding_bits(coefficient), expected); + assert_eq!(SecurityLevel::Bits100.grinding_bits(coefficient), 0); + } + assert_eq!(SecurityLevel::Bits100.grinding_bits(1 << 28), 0); + assert_eq!(SecurityLevel::Bits100.grinding_bits((1 << 28) + 1), 1); + } +} diff --git a/crates/common/src/shape.rs b/crates/common/src/shape.rs index 22333b41..8270f04f 100644 --- a/crates/common/src/shape.rs +++ b/crates/common/src/shape.rs @@ -4,12 +4,8 @@ /// packed field element carries. pub const PACK_BITS: u32 = 7; -/// The commitment size window the opening parameters are fixed for. -/// -/// `22..=35` is not derived from a security bound here; it is the range -/// `flock-core`'s Ligerito configs are precomputed for (one shipped TOML per -/// `m` in that range, per profile). Sizes outside it have no config to load. -pub const MIN_LOG_BITS: usize = 22; +/// The size window for dynamically derived opening parameters. +pub const MIN_LOG_BITS: usize = 20; /// The upper end of that window. pub const MAX_LOG_BITS: usize = 35; @@ -19,7 +15,7 @@ pub enum ShapeError { /// Fewer than seven row-index bits: a packed row would not fill one /// codeword position. The paper writes this count `t`. RowIndexTooNarrow, - /// The total bit count falls outside `2^22..=2^35`. + /// The total bit count falls outside `2^20..=2^35`. CommitmentSizeOutOfRange, } @@ -118,8 +114,8 @@ mod tests { #[test] fn rejects_a_commitment_size_outside_the_window() { - // m = 21, then m = 36. - assert_eq!(Shape::new(7, 14), Err(ShapeError::CommitmentSizeOutOfRange)); + // m = 19, then m = 36. + assert_eq!(Shape::new(7, 12), Err(ShapeError::CommitmentSizeOutOfRange)); assert_eq!( Shape::new(13, 23), Err(ShapeError::CommitmentSizeOutOfRange) @@ -157,7 +153,7 @@ mod tests { #[test] fn accepts_the_window_boundaries() { - assert_eq!(Shape::new(7, 15).unwrap().log_bits(), MIN_LOG_BITS); + assert_eq!(Shape::new(7, 13).unwrap().log_bits(), MIN_LOG_BITS); assert_eq!(Shape::new(14, 21).unwrap().log_bits(), MAX_LOG_BITS); } } diff --git a/crates/gkr/Cargo.toml b/crates/gkr/Cargo.toml index 7781751f..bc0be426 100644 --- a/crates/gkr/Cargo.toml +++ b/crates/gkr/Cargo.toml @@ -6,6 +6,7 @@ rust-version.workspace = true license.workspace = true [dependencies] +common = { workspace = true } field = { path = "../field", features = ["spongefish"] } transcript = {path ="../transcript"} tracing = { workspace = true } diff --git a/crates/gkr/src/lib.rs b/crates/gkr/src/lib.rs index 1f390e6a..c486919a 100644 --- a/crates/gkr/src/lib.rs +++ b/crates/gkr/src/lib.rs @@ -1,5 +1,6 @@ use std::collections::VecDeque; +use common::SecurityLevel; use field::{F128, Wide256}; use num_traits::{ConstOne, ConstZero}; use rayon::prelude::*; @@ -9,6 +10,8 @@ pub type Field = F128; type Point = VecDeque; +const CUBIC_GRINDING_LABEL: &[u8] = b"gkr/cubic/v1"; + /// Proves the layer-by-layer sumcheck reduction from a claim at `point` /// (an evaluation point on the output layer) down to a claim on the leaves. // TODO #[must_use], requires changing the test suite @@ -19,6 +22,7 @@ pub fn gpgkr_prove( point: &[F128], // All the intermediate witnesses + the input layer. Doesn't contain the output layer witnesses: LayerWitnesses, + security: SecurityLevel, ) -> (Vec, Field) { // Edge cases // - empty witnesses -> single constant circuit -> one verifier message that permutes the proof state, but a single constant can't have an MLE @@ -30,7 +34,7 @@ pub fn gpgkr_prove( let mut claim = Field::ZERO; let mut storage = SuffixTable::alloc_storage(log_bits); for wnext in witnesses.into_iter() { - (point, claim) = prove_layer(ps, &mut storage, point, wnext); + (point, claim) = prove_layer(ps, &mut storage, point, wnext, security); } let mut point = Vec::from(point); @@ -43,6 +47,7 @@ fn prove_layer( storage: &mut [Field], mut point: Point, mut wnext: Vec, + security: SecurityLevel, ) -> (Point, Field) { let suffix_table = SuffixTable::new(storage, &point); let mut factor = Field::ONE; @@ -82,6 +87,7 @@ fn prove_layer( ps.prover_message(&[factor * sum_endpoint.reduce(), factor * sum_inf.reduce()]); + ps.grind(CUBIC_GRINDING_LABEL, security.grinding_bits(3)); let r = ps.verifier_message(); next_point.push_back(r); @@ -127,6 +133,7 @@ fn prove_layer( ps.prover_message(&[factor * sum_endpoint.reduce(), factor * sum_inf.reduce()]); + ps.grind(CUBIC_GRINDING_LABEL, security.grinding_bits(3)); let r = ps.verifier_message(); next_point.push_back(r); @@ -268,6 +275,7 @@ pub fn gpgkr_verify( mut claim: Field, point: &[F128], rounds: u32, + security: SecurityLevel, ) -> Option<(Vec, Field)> { // Edge cases around input lenghts, 0 meaning empty // | circuit | last value | @@ -284,7 +292,7 @@ pub fn gpgkr_verify( let mut point = VecDeque::from(point); for _i in 0..rounds { - (point, claim) = verify_layer(vs, claim, point)? + (point, claim) = verify_layer(vs, claim, point, security)? } let mut point = Vec::from(point); @@ -294,7 +302,12 @@ pub fn gpgkr_verify( } /// Point's orientation is the reverse of gpgkr_verify -fn verify_layer(vs: &mut VerifierState, mut claim: Field, point: Point) -> Option<(Point, Field)> { +fn verify_layer( + vs: &mut VerifierState, + mut claim: Field, + point: Point, + security: SecurityLevel, +) -> Option<(Point, Field)> { let mut prefix = Field::ONE; let mut next_point: Point = VecDeque::new(); @@ -310,6 +323,8 @@ fn verify_layer(vs: &mut VerifierState, mut claim: Field, point: Point) -> Optio (sum_endpoint, (claim - eqjsum0) / z) }; + vs.grind(CUBIC_GRINDING_LABEL, security.grinding_bits(3)) + .ok()?; let r = vs.verifier_message(); next_point.push_back(r); let factor = eq_factor(r, z); @@ -393,6 +408,7 @@ impl IntoIterator for LayerWitnesses { #[cfg(test)] mod tests { use super::*; + use common::SecurityLevel::{Bits100, Bits128}; use proptest::prelude::*; fn field() -> impl Strategy { @@ -500,6 +516,50 @@ mod tests { } } + #[test] + fn gpgkr_security_targets_replay_and_reject_changed_nonces() { + let leaves: Vec = (1u128..=16).map(Field::from).collect(); + let point = [Field::from(5u128), Field::from(7u128)]; + let instance = (leaves.clone(), point.to_vec()); + let mut unground_len = 0; + + for security in [Bits100, Bits128] { + let circuit = GrandProductCircuit::new(leaves.clone()); + let (output, witnesses) = circuit.batched_eval(4); + let claim = mle(output, &point); + let mut prover = transcript::build_prover("gkr-security", &instance); + let terminal = gpgkr_prove( + &mut prover, + leaves.len().ilog2() as usize, + &point, + witnesses, + security, + ); + assert_eq!(terminal.1, mle(leaves.clone(), &terminal.0)); + let mut proof = prover.finish(); + + let mut verifier = transcript::build_verifier("gkr-security", &instance, &proof); + assert_eq!( + gpgkr_verify(&mut verifier, claim, &point, 2, security), + Some(terminal) + ); + verifier.check_eof().unwrap(); + + match security { + Bits100 => unground_len = proof.narg_string.len(), + Bits128 => { + // Two layers have five cubic challenges and five eight-byte nonces. + assert_eq!(proof.narg_string.len(), unground_len + 40); + // The first cubic message occupies two canonical field elements. + proof.narg_string[32] ^= 1; + let mut verifier = + transcript::build_verifier("gkr-security", &instance, &proof); + assert!(gpgkr_verify(&mut verifier, claim, &point, 2, security).is_none()); + } + } + } + } + #[test] fn gpgkr_round_trip_with_zero_coordinates() { let leaves: Vec = (1u128..=16).map(Field::from).collect(); @@ -521,19 +581,20 @@ mod tests { leaves.len().ilog2() as usize, &point, witnesses, + Bits100, ); assert_eq!(terminal.1, mle(leaves.clone(), &terminal.0)); let proof = prover.finish(); let mut verifier = transcript::build_verifier("gkr-zero", &instance, &proof); assert_eq!( - gpgkr_verify(&mut verifier, claim, &point, 2), + gpgkr_verify(&mut verifier, claim, &point, 2, Bits100), Some(terminal) ); verifier.check_eof().unwrap(); let mut verifier = transcript::build_verifier("gkr-zero", &instance, &proof); - assert!(gpgkr_verify(&mut verifier, claim + Field::ONE, &point, 2).is_none()); + assert!(gpgkr_verify(&mut verifier, claim + Field::ONE, &point, 2, Bits100).is_none()); } } @@ -576,7 +637,7 @@ mod tests { let log_groups = last_value.len().max(1).ilog2(); let point: Vec = (0..log_groups).map(|_| prover.verifier_message()).collect(); - gpgkr_prove(&mut prover, log_bits, &point, witnesses); + gpgkr_prove(&mut prover, log_bits, &point, witnesses, Bits100); (last_value, prover.finish()) } @@ -595,7 +656,7 @@ mod tests { let log_leafs = circuit.leafs.len().max(1).ilog2(); let rounds = log_leafs.saturating_sub(log_groups); - match gpgkr_verify(&mut verifier, claim, &point, rounds) { + match gpgkr_verify(&mut verifier, claim, &point, rounds, Bits100) { Some((point, claim)) => { let leaf_check = mle(circuit.leafs, &point); diff --git a/crates/pcs/src/challenger.rs b/crates/pcs/src/challenger.rs index 0a341c49..62b40bba 100644 --- a/crates/pcs/src/challenger.rs +++ b/crates/pcs/src/challenger.rs @@ -1,10 +1,10 @@ //! Flock challenger adapters over the project transcript. use crate::bridge::{as_flock_f128, from_flock_f128}; -use crate::pow::{find as find_pow, valid as pow_valid}; use field::F128 as LocalF128; use flock_core::challenger::Challenger; use flock_core::field::F128 as FlockF128; +use transcript::pow::{find as find_pow, valid as pow_valid}; use transcript::{ProverState, VerifierState}; const VECTOR_SQUEEZE_TAG: &[u8] = b"pcs/flock/sample-vector/v1"; @@ -12,6 +12,12 @@ const POW_TAG: &[u8] = b"pcs/flock/pow/v1"; const LIGERITO_BASIS_LABEL: &[u8] = b"flock-ligerito-basis-v0"; const MAX_OBSERVED_BYTES: usize = 32; +/// Checks the backend call order before applying the selected difficulty. +fn pow_difficulty(calls: &mut std::slice::Iter<'_, (u32, u32)>, native: u32) -> Option { + let &(expected, effective) = calls.next()?; + (native == expected).then_some(effective) +} + #[derive(Clone, Copy)] struct OpeningTargetPrefix { expected_target: FlockF128, @@ -22,14 +28,16 @@ pub(crate) struct ProverChallenger<'a> { transcript: &'a mut ProverState, failed: bool, opening_target: Option, + pow_schedule: std::slice::Iter<'a, (u32, u32)>, } impl<'a> ProverChallenger<'a> { #[cfg(test)] - pub(crate) fn new(transcript: &'a mut ProverState) -> Self { + pub(crate) fn new(transcript: &'a mut ProverState, calls: &'a [(u32, u32)]) -> Self { Self { transcript, failed: false, + pow_schedule: calls.iter(), opening_target: None, } } @@ -37,10 +45,12 @@ impl<'a> ProverChallenger<'a> { pub(crate) fn new_ligerito( transcript: &'a mut ProverState, expected_target: FlockF128, + calls: &'a [(u32, u32)], ) -> Self { Self { transcript, failed: false, + pow_schedule: calls.iter(), opening_target: Some(OpeningTargetPrefix { expected_target, label_seen: false, @@ -49,7 +59,7 @@ impl<'a> ProverChallenger<'a> { } pub(crate) fn failed(&self) -> bool { - self.failed || self.opening_target.is_some() + self.failed || self.opening_target.is_some() || self.pow_schedule.len() != 0 } } @@ -57,14 +67,16 @@ pub(crate) struct VerifierChallenger<'a, 'proof> { transcript: &'a mut VerifierState<'proof>, failed: bool, opening_target: Option, + pow_schedule: std::slice::Iter<'a, (u32, u32)>, } impl<'a, 'proof> VerifierChallenger<'a, 'proof> { #[cfg(test)] - pub(crate) fn new(transcript: &'a mut VerifierState<'proof>) -> Self { + pub(crate) fn new(transcript: &'a mut VerifierState<'proof>, calls: &'a [(u32, u32)]) -> Self { Self { transcript, failed: false, + pow_schedule: calls.iter(), opening_target: None, } } @@ -72,10 +84,12 @@ impl<'a, 'proof> VerifierChallenger<'a, 'proof> { pub(crate) fn new_ligerito( transcript: &'a mut VerifierState<'proof>, expected_target: FlockF128, + calls: &'a [(u32, u32)], ) -> Self { Self { transcript, failed: false, + pow_schedule: calls.iter(), opening_target: Some(OpeningTargetPrefix { expected_target, label_seen: false, @@ -84,7 +98,7 @@ impl<'a, 'proof> VerifierChallenger<'a, 'proof> { } pub(crate) fn failed(&self) -> bool { - self.failed || self.opening_target.is_some() + self.failed || self.opening_target.is_some() || self.pow_schedule.len() != 0 } fn read(&mut self) -> Option @@ -149,6 +163,10 @@ impl Challenger for ProverChallenger<'_> { } fn grind_pow(&mut self, bits: u32) -> u64 { + let Some(bits) = pow_difficulty(&mut self.pow_schedule, bits) else { + self.failed = true; + return 0; + }; self.transcript.public_message(POW_TAG); self.transcript.public_message(&bits); let seed = self.transcript.verifier_message::().to_bytes(); @@ -220,6 +238,10 @@ impl Challenger for VerifierChallenger<'_, '_> { } fn verify_pow(&mut self, nonce: u64, bits: u32) -> bool { + let Some(bits) = pow_difficulty(&mut self.pow_schedule, bits) else { + self.failed = true; + return false; + }; self.transcript.public_message(POW_TAG); self.transcript.public_message(&bits); let seed = self.transcript.verifier_message::().to_bytes(); @@ -240,6 +262,43 @@ mod tests { use super::*; + #[test] + fn constant_schedule_replays_and_rejects_changed_call_order() { + let calls = vec![(5, 5), (4, 5), (3, 5), (0, 0)]; + let mut transcript = build_prover(b"schedule", b"instance"); + let mut prover = ProverChallenger::new(&mut transcript, &calls); + assert!(prover.failed()); // An incomplete schedule cannot succeed. + let nonces: Vec<_> = calls + .iter() + .map(|&(native, _)| prover.grind_pow(native)) + .collect(); + assert!(!prover.failed()); + let proof = transcript.finish(); + + let mut transcript = build_verifier(b"schedule", b"instance", &proof); + let mut verifier = VerifierChallenger::new(&mut transcript, &calls); + for (&nonce, &(native, _)) in nonces.iter().zip(&calls) { + assert!(verifier.verify_pow(nonce, native)); + } + assert!(!verifier.failed()); + transcript.check_eof().unwrap(); + + let mut transcript = build_verifier(b"schedule", b"instance", &proof); + let mut verifier = VerifierChallenger::new(&mut transcript, &calls); + verifier.verify_pow(nonces[0], 4); + assert!(verifier.failed()); + } + + #[test] + fn extra_pow_calls_fail_after_schedule_completion() { + let mut transcript = build_prover(b"schedule", b"instance"); + let mut prover = ProverChallenger::new(&mut transcript, &[(0, 0)]); + prover.grind_pow(0); + assert!(!prover.failed()); + prover.grind_pow(0); + assert!(prover.failed()); + } + #[test] fn zero_bit_pow_has_one_canonical_nonce() { assert!(pow_valid(&[0; 16], 0, 0)); @@ -262,7 +321,7 @@ mod tests { fn observed_root_changes_the_following_challenge() { fn sample_after_root(root: &[u8; 32]) -> FlockF128 { let mut transcript = build_prover(b"pcs-challenger-test", b"root-binding"); - let mut challenger = ProverChallenger::new(&mut transcript); + let mut challenger = ProverChallenger::new(&mut transcript, &[]); challenger.observe_bytes(root); challenger.sample_f128() } @@ -289,7 +348,7 @@ mod tests { let mut prover = build_prover(SESSION, INSTANCE); let nonce = { - let mut challenger = ProverChallenger::new(&mut prover); + let mut challenger = ProverChallenger::new(&mut prover, &[(BITS, BITS)]); challenger.grind_pow(BITS) }; let mut proof = prover.finish(); @@ -304,7 +363,7 @@ mod tests { let mut verifier = build_verifier(SESSION, INSTANCE, &proof); { - let mut challenger = VerifierChallenger::new(&mut verifier); + let mut challenger = VerifierChallenger::new(&mut verifier, &[(BITS, BITS)]); assert!(!challenger.verify_pow(changed_nonce, BITS)); assert!(challenger.failed()); } @@ -317,7 +376,7 @@ mod tests { let next_message = FlockF128::new(3, 4); let mut prover = build_prover(b"pcs-challenger-test", b"public-opening-target"); { - let mut challenger = ProverChallenger::new_ligerito(&mut prover, target); + let mut challenger = ProverChallenger::new_ligerito(&mut prover, target, &[]); challenger.observe_label(LIGERITO_BASIS_LABEL); challenger.observe_f128(target); challenger.observe_f128(next_message); @@ -328,7 +387,7 @@ mod tests { let mut verifier = build_verifier(b"pcs-challenger-test", b"public-opening-target", &proof); { - let mut challenger = VerifierChallenger::new_ligerito(&mut verifier, target); + let mut challenger = VerifierChallenger::new_ligerito(&mut verifier, target, &[]); challenger.observe_label(LIGERITO_BASIS_LABEL); challenger.observe_f128(target); challenger.observe_f128(next_message); @@ -341,7 +400,7 @@ mod tests { fn ligerito_public_target_changes_the_challenge() { fn sample(target: FlockF128) -> FlockF128 { let mut prover = build_prover(b"pcs-challenger-test", b"public-opening-target"); - let mut challenger = ProverChallenger::new_ligerito(&mut prover, target); + let mut challenger = ProverChallenger::new_ligerito(&mut prover, target, &[]); challenger.observe_label(LIGERITO_BASIS_LABEL); challenger.observe_f128(target); challenger.sample_f128() @@ -367,9 +426,10 @@ mod tests { .map(|&(lo, hi)| FlockF128::new(lo, hi)) .collect::>(); + let calls = [(pow_bits, pow_bits)]; let mut prover = build_prover(b"pcs-challenger-test", b"method-round-trip"); let (sampled_scalar, sampled_vector, nonce) = { - let mut challenger = ProverChallenger::new(&mut prover); + let mut challenger = ProverChallenger::new(&mut prover, &calls); challenger.observe_label(b"test-label"); challenger.observe_f128(scalar); challenger.observe_f128_slice(&slice); @@ -388,7 +448,7 @@ mod tests { &proof, ); { - let mut challenger = VerifierChallenger::new(&mut verifier); + let mut challenger = VerifierChallenger::new(&mut verifier, &calls); challenger.observe_label(b"test-label"); challenger.observe_f128(scalar); challenger.observe_f128_slice(&slice); diff --git a/crates/pcs/src/commitment.rs b/crates/pcs/src/commitment.rs index d49af9e9..d4c2dccf 100644 --- a/crates/pcs/src/commitment.rs +++ b/crates/pcs/src/commitment.rs @@ -6,20 +6,22 @@ //! 3. Expose the Merkle root as the public commitment. //! 4. Retain Flock prover data for later openings. -use core::mem::size_of; - use crate::VerifyError; use crate::bridge::as_flock_f128s; use crate::ligerito::CheckedLigerito; -use crate::ood::{OodClaim, prove, verify}; -use crate::profiles::{ood_grinding_bits, security_config}; -use common::{Root, Shape}; +use crate::ood::{self, OodClaim}; +use crate::profiles::security_config; +use common::{Root, SecurityLevel, Shape}; use field::F128; -pub use flock_core::hash::HashKind; +use flock_core::hash::HashKind; use flock_core::pcs::Commitment as FlockCommitment; use flock_core::pcs::ligerito::LigeritoProfile; -use flock_core::pcs::{PcsParams, ProverData as FlockProverData, commit}; -use transcript::{Encoding, ProverState, VerifierState}; +use flock_core::pcs::{LOG_PACKING, PcsParams, ProverData as FlockProverData, commit}; +use transcript::{Encoding, ProverState, PublicTranscript, VerifierState}; + +// Increment this version when parameter derivation or transcript rules change. +// This includes protocol changes in Flock or the selected hash. +const PROTOCOL_VERSION: &[u8] = b"bitz/pcs/security/v1"; /// Errors from PCS configuration. #[derive(Clone, Debug, PartialEq, Eq)] @@ -41,14 +43,14 @@ pub enum CommitError { pub struct Pcs { params: PcsParams, checked_ligerito: CheckedLigerito, - ood_grinding_bits: Option, bit_len: usize, packed_len: usize, + security_level: SecurityLevel, } /// Commitment and private Flock data retained for proving openings. pub struct ProverData { - commitment: Commitment, + pub(crate) commitment: Commitment, flock_prover_data: FlockProverData, } @@ -58,6 +60,7 @@ pub struct ProverData { #[derive(Debug)] pub struct Commitment { flock: FlockCommitment, + security_level: SecurityLevel, pub(crate) ood: Option, } @@ -75,43 +78,39 @@ impl Commitment { && expected.log_batch_size == actual.log_batch_size && expected.profile == actual.profile && expected.merkle_hash == actual.merkle_hash - && self.ood.is_some() == pcs.ood_grinding_bits().is_some() + && self.security_level == pcs.security_level() + && self.ood.is_some() == (pcs.security_level() == SecurityLevel::Bits100) } } impl Pcs { - pub fn new( - shape: &Shape, - security_profile: LigeritoProfile, - merkle_hash: HashKind, - ) -> Result { + /// Derives the selected round budget from the padded witness size. + /// The 100-bit profile uses Johnson decoding; the 128-bit profile uses unique decoding. + pub fn new(shape: &Shape, security_level: SecurityLevel) -> Result { let m = shape.log_bits(); + let security = security_config(m, security_level)?; let bit_len = 1usize .checked_shl(m as u32) .ok_or(ConfigError::Invalid("bit length overflow"))?; // The ladder fixes the L0 interleaving: the commit must use the same // `log_batch_size` as the opening's `initial_k`, or the L0 tree is not // reusable as Ligerito's first oracle. - let security = security_config(m, security_profile, merkle_hash)?; let params = PcsParams { m, - log_inv_rate: security_profile.log_inv_rate(), + log_inv_rate: security.levels[0].log_inv_rate, log_batch_size: security.initial_k, - profile: security_profile, - merkle_hash, + // Flock retains this tag; explicit geometry controls our encoding. + profile: LigeritoProfile::Fast, + merkle_hash: HashKind::Blake3, }; let checked_ligerito = CheckedLigerito::new(¶ms, &security)?; - let ood_grinding_bits = ood_grinding_bits(&security, checked_ligerito.log_n_u32() as usize); - let packed_len = 1usize - .checked_shl(checked_ligerito.log_n_u32()) - .ok_or(ConfigError::Invalid("packed length overflow"))?; - + let packed_len = bit_len >> LOG_PACKING; Ok(Self { params, checked_ligerito, - ood_grinding_bits, bit_len, packed_len, + security_level, }) } @@ -134,7 +133,8 @@ impl Pcs { // 3. Build Public Commitment let root = Root(flock_commitment.root); - let ood = prove(self, &root.0, packed_witness, transcript); + self.bind_commitment(root, transcript); + let ood = ood::prove(self, packed_witness, transcript); // 4. Retain Opening Data Ok(( @@ -142,6 +142,7 @@ impl Pcs { ProverData { commitment: Commitment { flock: flock_commitment, + security_level: self.security_level, ood, }, flock_prover_data, @@ -149,26 +150,31 @@ impl Pcs { )) } - /// Receives the OOD claim for the public root before subsequent protocol challenges. - /// - /// Mirrors [`Self::commit`]. Invalid grinding or a truncated evaluation - /// returns [`VerifyError::MalformedProof`]; authentication of the evaluation is - /// deferred to [`CommitScheme::verify_lin`](crate::CommitScheme::verify_lin). + /// Receives commitment state before subsequent protocol challenges. + /// Verification of an opening authenticates the retained initial claim. pub fn receive_commitment( &self, root: Root, transcript: &mut VerifierState<'_>, ) -> Result { - let ood = verify(self, &root.0, transcript)?; + self.bind_commitment(root, transcript); + let ood = ood::verify(self, transcript)?; Ok(Commitment { flock: FlockCommitment { root: root.0, params: self.params.clone(), }, + security_level: self.security_level, ood, }) } + fn bind_commitment(&self, root: Root, transcript: &mut impl PublicTranscript) { + transcript.public_message(b"bitz/pcs/commit/v1" as &[u8]); + transcript.public_message(&root.0); + transcript.public_message(self); + } + pub fn bit_len(&self) -> usize { self.bit_len } @@ -182,8 +188,14 @@ impl Pcs { &self.params } - pub(crate) fn ood_grinding_bits(&self) -> Option { - self.ood_grinding_bits + /// Returns the selected classical PCS round budget. + pub fn security_level(&self) -> SecurityLevel { + self.security_level + } + + /// Maps each native PoW call to its checked effective difficulty. + pub(crate) fn pow_schedule(&self) -> &[(u32, u32)] { + self.checked_ligerito.pow_schedule() } pub(crate) fn prover_config(&self) -> &flock_core::pcs::ligerito::ProverConfig { @@ -201,27 +213,9 @@ impl Pcs { impl Encoding<[u8]> for Pcs { fn encode(&self) -> impl AsRef<[u8]> { - let profile_tag = match self.params.profile { - LigeritoProfile::Fast => 0, - LigeritoProfile::Slim => 1, - LigeritoProfile::Secure => 2, - }; - let hash_tag = match self.params.merkle_hash { - HashKind::Sha256 => 0, - HashKind::Blake3 => 1, - }; - - let tags = [ - self.params.m as u64, - self.params.log_inv_rate as u64, - self.params.log_batch_size as u64, - profile_tag, - hash_tag, - ]; - let mut encoded = [0u8; 5 * size_of::()]; - for (chunk, tag) in encoded.chunks_exact_mut(size_of::()).zip(tags) { - chunk.copy_from_slice(&tag.to_le_bytes()); - } + let mut encoded = PROTOCOL_VERSION.to_vec(); + encoded.extend_from_slice(&(self.bit_len as u64).to_le_bytes()); + encoded.extend_from_slice(&self.security_level.bits().to_le_bytes()); encoded } } @@ -259,26 +253,43 @@ mod tests { Shape::new(7, 15).unwrap() } + #[test] + fn every_dynamic_size_builds_a_complete_native_pow_schedule() { + for m in 20..=35 { + let shape = Shape::new(7, m - 7).unwrap(); + for level in [SecurityLevel::Bits100, SecurityLevel::Bits128] { + let pcs = Pcs::new(&shape, level).unwrap(); + let config = security_config(m, level).unwrap(); + let expected: usize = config + .levels + .iter() + .map(|level| 1 + level.fold_grinding_bits.min(level.k_recursive)) + .sum(); + assert_eq!(pcs.pow_schedule().len(), expected); + } + } + } + #[test] fn commitment_profiles_select_ood_and_preserve_transcript_agreement() { - for profile in [ - LigeritoProfile::Fast, - LigeritoProfile::Slim, - LigeritoProfile::Secure, - ] { - let pcs = Pcs::new(&shape(), profile, HashKind::Blake3).unwrap(); + for security in [SecurityLevel::Bits100, SecurityLevel::Bits128] { + let pcs = Pcs::new(&shape(), security).unwrap(); let witness = vec![F128::ZERO; pcs.packed_len()]; let mut prover = build_prover(b"commit-test", b"profile"); let (root, data) = pcs.commit(&witness, &mut prover).unwrap(); - let expected_ood = profile != LigeritoProfile::Secure; + let expected_ood = security == SecurityLevel::Bits100; assert_eq!(data.commitment().ood.is_some(), expected_ood); let next_challenge = prover.verifier_message::(); let proof = prover.finish(); - assert_eq!(proof.narg_string.is_empty(), !expected_ood); + assert_eq!(proof.narg_string.len(), if expected_ood { 16 } else { 0 }); let mut verifier = build_verifier(b"commit-test", b"profile", &proof); let received = pcs.receive_commitment(root, &mut verifier).unwrap(); assert_eq!(received.root(), data.commitment().root()); assert_eq!(received.ood.is_some(), expected_ood); + if let Some(claim) = received.ood.as_ref() { + assert_eq!(claim.point, data.commitment().ood.as_ref().unwrap().point); + assert_eq!(claim.point.len(), pcs.packed_len().ilog2() as usize); + } assert!(received.matches(&pcs)); assert_eq!(verifier.verifier_message::(), next_challenge); verifier.check_eof().unwrap(); @@ -287,7 +298,7 @@ mod tests { #[test] fn commitment_is_deterministic_for_packed_boundary_bits() { - let scheme = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let scheme = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let mut packed_witness = vec![F128::ZERO; scheme.packed_len()]; packed_witness[0] = F128::new(1 | (1 << 1) | (1 << 63), 1 | (1 << 63)); packed_witness[1] = F128::new(1, 0); @@ -321,7 +332,7 @@ mod tests { #[test] fn caller_packing_layout_matches_flock() { - let scheme = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let scheme = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let mut bits = vec![false; scheme.bit_len()]; for index in [0, 63, 64, 127, 128, bits.len() - 1] { bits[index] = true; @@ -338,35 +349,31 @@ mod tests { } #[test] - fn encoding_covers_every_profile_and_hash() { - for (profile, profile_tag) in [ - (LigeritoProfile::Fast, 0), - (LigeritoProfile::Slim, 1), - (LigeritoProfile::Secure, 2), - ] { - for (hash, hash_tag) in [(HashKind::Sha256, 0), (HashKind::Blake3, 1)] { - let pcs = Pcs::new(&shape(), profile, hash).unwrap(); - // `Fast` runs the k = 4 ladder; the other profiles keep flock's - // embedded k = 6 generation. - let initial_k = match profile { - LigeritoProfile::Fast => 4, - LigeritoProfile::Slim | LigeritoProfile::Secure => 6, - }; - let expected_tags = [ - 22, - profile.log_inv_rate() as u64, - initial_k, - profile_tag, - hash_tag, - ]; - let encoded = pcs.encode(); - let encoded = encoded.as_ref(); - assert_eq!(encoded.len(), 5 * size_of::()); - for (chunk, tag) in encoded.chunks_exact(size_of::()).zip(expected_tags) { - assert_eq!(chunk, tag.to_le_bytes()); - } - } - } + fn explicit_profiles_bind_the_target_and_witness_size() { + let shape = Shape::new(7, 13).unwrap(); + let low = Pcs::new(&shape, SecurityLevel::Bits100).unwrap(); + let high = Pcs::new(&shape, SecurityLevel::Bits128).unwrap(); + // Fixed encoding: version tag, padded bit count (u64 LE), target (u32 LE). + assert_eq!( + low.encode().as_ref(), + b"bitz/pcs/security/v1\x00\x00\x10\x00\x00\x00\x00\x00\x64\x00\x00\x00" + ); + assert_ne!(low.encode().as_ref(), high.encode().as_ref()); + let (_, data) = low + .commit( + &vec![F128::ZERO; low.packed_len()], + &mut build_prover(b"commit-test", b"witness"), + ) + .unwrap(); + assert_eq!(crate::ligerito::validate_prover_data(&low, &data), Ok(())); + assert_eq!( + crate::ligerito::validate_prover_data(&high, &data), + Err(crate::ProveError::ProverDataMismatch) + ); + let larger_shape = Shape::new(7, 14).unwrap(); + let changed = Pcs::new(&larger_shape, SecurityLevel::Bits100).unwrap(); + assert_ne!(low.encode().as_ref(), changed.encode().as_ref()); + assert!(crate::ligerito::validate_prover_data(&changed, &data).is_err()); } proptest! { @@ -374,7 +381,7 @@ mod tests { #[test] fn rejects_arbitrary_short_packed_witnesses(len in 0usize..4096) { - let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; len]; let mut transcript = build_prover(b"commit-test", b"short"); diff --git a/crates/pcs/src/lib.rs b/crates/pcs/src/lib.rs index 5cb613a6..0c2a533b 100644 --- a/crates/pcs/src/lib.rs +++ b/crates/pcs/src/lib.rs @@ -28,13 +28,15 @@ //! //! - [`Pcs`] stores trusted Flock parameters and the expected bit length. //! - [`Root`] is the public Merkle root. -//! - [`ProverData`] retains the codeword and Merkle tree after commitment. +//! - [`ProverData`] retains the commitment, codeword, and Merkle tree. //! - [`Commitment`] retains the root, parameters, and optional OOD claim on both sides. //! - [`OpeningQuery`] contains an MLE point and target, or a `common::LinearClaim`. //! - [`CommitScheme`] connects commitment, proving, and verification to project transcripts. //! - [`ConfigError`] reports configuration failures. //! - [`CommitError`], [`ProveError`], and [`VerifyError`] report operation-specific failures. //! +//! The 100-bit profile includes initial and recursive OOD checks. +//! The 128-bit profile uses unique decoding without OOD checks. //! The caller packs and retains the witness after [`CommitScheme::commit`]. //! [`CommitScheme::prove_lin`] dispatches both query variants. //! It consumes the packed witness and borrows [`ProverData`]. @@ -53,13 +55,13 @@ //! use field::F128; //! use num_traits::ConstZero; //! use pcs::{ -//! CommitScheme, HashKind, LigeritoProfile, OpeningQuery, Pcs, StatementBinding, +//! CommitScheme, SecurityLevel, OpeningQuery, Pcs, StatementBinding, //! }; //! use transcript::{build_prover, build_verifier}; //! //! const M: usize = 22; //! let shape = Shape::new(7, 15).unwrap(); -//! let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); +//! let pcs = Pcs::new(&shape, SecurityLevel::Bits100).unwrap(); //! let packed_witness = vec![F128::ZERO; pcs.packed_len()]; //! let point = (0..M) //! .map(|coordinate| F128::from(coordinate as u64 + 2)) @@ -100,7 +102,6 @@ mod ligerito; mod mle; mod ood; mod opening; -mod pow; mod profiles; mod transpose; @@ -112,9 +113,8 @@ use field::F128; use opening::{prove, verify}; use transcript::{ProverState, VerifierState}; -pub use commitment::{CommitError, Commitment, ConfigError, HashKind, Pcs, ProverData}; -pub use common::{OpeningQuery, Root}; -pub use flock_core::pcs::ligerito::LigeritoProfile; +pub use commitment::{CommitError, Commitment, ConfigError, Pcs, ProverData}; +pub use common::{OpeningQuery, Root, SecurityLevel}; pub use opening::{ProveError, VerifyError}; /// Controls statement binding for one opening. diff --git a/crates/pcs/src/ligerito.rs b/crates/pcs/src/ligerito.rs index 2c388759..9fe448db 100644 --- a/crates/pcs/src/ligerito.rs +++ b/crates/pcs/src/ligerito.rs @@ -10,7 +10,7 @@ use flock_core::field::F128 as FlockF128; use flock_core::pcs::LOG_PACKING; use flock_core::pcs::PcsParams; use flock_core::pcs::ligerito::{ - LigeritoProof, LigeritoSecurityConfig, ProverConfig, VerifierConfig, + LigeritoProof, LigeritoSecurityConfig, ProverConfig, SoundnessRegime, VerifierConfig, recursive_prover_with_basis, recursive_verifier_with_basis_succinct, }; use transcript::{ProverState, VerifierState}; @@ -25,8 +25,8 @@ const PROOF_HINT_LIMIT: usize = 64 * 1024 * 1024; pub(crate) struct CheckedLigerito { prover_config: ProverConfig, verifier_config: VerifierConfig, - log_n_u32: u32, final_log_n: usize, + pow_schedule: Vec<(u32, u32)>, } impl CheckedLigerito { @@ -38,19 +38,31 @@ impl CheckedLigerito { .m .checked_sub(LOG_PACKING) .ok_or(ConfigError::Invalid("m below packing width"))?; - let log_n_u32 = - u32::try_from(log_n).map_err(|_| ConfigError::Invalid("log_n exceeds u32"))?; let (prover_config, verifier_config) = security .to_prover_verifier_configs() .map_err(|_| ConfigError::Invalid("prover config"))?; validate_pcs_verifier_prover(params, &prover_config, &verifier_config)?; let final_log_n = validate_verifier_config(&verifier_config, log_n, params.log_batch_size)?; + let mut pow_schedule = Vec::new(); + for level in &security.levels { + let bits = level.fold_grinding_bits as u32; + // Both profiles keep positive grinding through every fold; tests cover all supported sizes. + for round in 0..level.k_recursive { + let native = bits - round as u32; + let effective = match level.regime { + SoundnessRegime::Udr => bits, + SoundnessRegime::JohnsonOod => native, + }; + pow_schedule.push((native, effective)); + } + pow_schedule.push((level.grinding_bits as u32, level.grinding_bits as u32)); + } Ok(Self { prover_config, verifier_config, - log_n_u32, final_log_n, + pow_schedule, }) } @@ -62,13 +74,13 @@ impl CheckedLigerito { &self.verifier_config } - pub(crate) fn log_n_u32(&self) -> u32 { - self.log_n_u32 - } - pub(crate) fn final_log_n(&self) -> usize { self.final_log_n } + + pub(crate) fn pow_schedule(&self) -> &[(u32, u32)] { + &self.pow_schedule + } } pub(crate) struct ReducedClaim { @@ -116,7 +128,8 @@ impl<'a> ReducedProver<'a> { packed_basis, packed_target, } = claim; - let mut challenger = ProverChallenger::new_ligerito(transcript, packed_target); + let mut challenger = + ProverChallenger::new_ligerito(transcript, packed_target, self.pcs.pow_schedule()); let flock_data = self.data.flock_data(); let ligerito = recursive_prover_with_basis( self.pcs.prover_config(), @@ -183,7 +196,7 @@ pub(crate) fn validate_verifier_config( return Err(ConfigError::Invalid("initial_log_num_interleaved mismatch")); } if config.ood_samples[0] != 0 { - return Err(ConfigError::Invalid("ood_samples[0] is nonzero")); + return Err(ConfigError::Invalid("initial OOD belongs to commitment")); } if config.log_inv_rates.contains(&0) { return Err(ConfigError::Invalid("log_inv_rates contains zero")); @@ -283,7 +296,8 @@ pub(crate) fn verify_succinct( where F: Fn(&[FlockF128], usize) -> Vec, { - let mut challenger = VerifierChallenger::new_ligerito(transcript, packed_target); + let mut challenger = + VerifierChallenger::new_ligerito(transcript, packed_target, pcs.pow_schedule()); let valid = recursive_verifier_with_basis_succinct( pcs.verifier_config(), proof, @@ -465,7 +479,7 @@ fn proof_options() -> impl Options { #[cfg(test)] mod tests { use super::*; - use crate::{CommitScheme, HashKind, LigeritoProfile, OpeningQuery, StatementBinding}; + use crate::{CommitScheme, OpeningQuery, SecurityLevel, StatementBinding}; use common::Shape; use flock_core::pcs::LOG_PACKING; use num_traits::ConstZero; @@ -517,7 +531,7 @@ mod tests { fn registered_config() -> (VerifierConfig, usize, usize) { let shape = Shape::new(7, 15).unwrap(); - let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape, SecurityLevel::Bits100).unwrap(); let config = pcs.verifier_config().clone(); ( config, @@ -589,7 +603,7 @@ mod tests { const SESSION: &[u8] = b"pcs-proof-shape-test"; const INSTANCE: &[u8] = b"zero-polynomial"; let shape = Shape::new(7, 15).unwrap(); - let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape, SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; let mut prover = build_prover(SESSION, INSTANCE); let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); @@ -619,13 +633,16 @@ mod tests { ); type ProofMutation = fn(&mut LigeritoProof); - let mutations: [(&str, ProofMutation); 2] = [ + let mutations: [(&str, ProofMutation); 3] = [ ("recursive-root count", |proof| { proof.recursive_roots.pop(); }), ("opened-row width", |proof| { proof.initial_proof.opened_rows[0].pop(); }), + ("unexpected OOD value", |proof| { + proof.ood_values.push(FlockF128::ZERO); + }), ]; for (case, mutate) in mutations { diff --git a/crates/pcs/src/ood.rs b/crates/pcs/src/ood.rs index 47ac1a28..63ff2b5d 100644 --- a/crates/pcs/src/ood.rs +++ b/crates/pcs/src/ood.rs @@ -1,10 +1,15 @@ //! Initial out-of-domain claim on the packed commitment polynomial. //! -//! After binding the root and PCS parameters, the prover performs any configured -//! grinding, samples `zeta`, and sends `value = p(point)`, where `p` is the packed -//! witness MLE and `point[i] = zeta^(2^i)` in low-bit-first order. +//! The caller binds the root and PCS parameters before this round. +//! We sample one independent field coordinate per packed variable, as in Flock's recursive OOD checks. +//! The prover sends `value = p(point)`, where `p` is the packed witness MLE. //! The verifier derives the same point and reads the claimed value. //! +//! This adapts BitZ Lemma B.2 with independent coordinates and the multilinear Schwartz-Zippel bound. +//! Two distinct candidates differ by a nonzero multilinear polynomial of total degree at most `point.len()`. +//! For at most `L` candidates, the collision bound is `L^2 * point.len() / (2 * 2^128)`. +//! This meets the 100-bit target for every supported witness size without initial OOD grinding. +//! //! After ring switching, a fresh `coefficient` batches this claim into Ligerito: //! `basis += coefficient * eq(point, ยท)` and `target += coefficient * value`. //! The claim remains borrowed from commitment state so it can be used by multiple @@ -18,19 +23,17 @@ use rayon::{current_num_threads, prelude::*}; use transcript::{ProverState, PublicTranscript, VerifierState}; use crate::bridge::{as_flock_f128, from_flock_f128}; -use crate::pow::{find, valid}; -use crate::{Pcs, VerifyError}; +use crate::{Pcs, SecurityLevel, VerifyError}; const OOD_ROUND_TAG: &[u8] = b"bitz/pcs/ood/v1"; const OOD_BATCHING_TAG: &[u8] = b"bitz/pcs/ood-batching/v1"; -const OOD_POW_TAG: &[u8] = b"bitz/pcs/ood-pow/v1"; const BLOCK_LOG: usize = 12; const PARALLEL_MIN_LEN: usize = 1 << 18; /// An evaluation of the packed witness MLE, authenticated by the batched opening. #[derive(Debug)] pub(crate) struct OodClaim { - /// Successive squares of the sampled challenge, in low-bit-first order. + /// Independent field coordinates in low-bit-first variable order. pub(crate) point: Vec, /// Claimed MLE evaluation at `point`. pub(crate) value: F128, @@ -38,38 +41,26 @@ pub(crate) struct OodClaim { /// Sends the initial evaluation after binding the commitment and configuration. /// Returns `None` without transcript events when the profile omits OOD sampling. -pub(crate) fn prove( - pcs: &Pcs, - root: &[u8; 32], - packed: &[F128], - transcript: &mut ProverState, -) -> Option { - let grinding_bits = pcs.ood_grinding_bits()?; - absorb_header(pcs, root, grinding_bits, transcript); - if grinding_bits != 0 { - prove_pow(transcript, grinding_bits); +pub(crate) fn prove(pcs: &Pcs, packed: &[F128], transcript: &mut ProverState) -> Option { + if pcs.security_level() != SecurityLevel::Bits100 { + return None; } - let point = ood_point(transcript.verifier_message_f128(), pcs.packed_len()); + let point = sample_point(pcs.packed_len(), transcript); let value = DenseMultilinearExtension::evaluate_exact(packed, &point); transcript.prover_message(&value); Some(OodClaim { point, value }) } -/// Reads the initial evaluation, checking grinding before sampling its point. +/// Reads the initial evaluation after sampling its point. /// Reading the value does not authenticate it; the caller must verify its opening. pub(crate) fn verify( pcs: &Pcs, - root: &[u8; 32], transcript: &mut VerifierState<'_>, ) -> Result, VerifyError> { - let Some(grinding_bits) = pcs.ood_grinding_bits() else { + if pcs.security_level() != SecurityLevel::Bits100 { return Ok(None); - }; - absorb_header(pcs, root, grinding_bits, transcript); - if grinding_bits != 0 { - verify_pow(transcript, grinding_bits).map_err(|_| VerifyError::MalformedProof)?; } - let point = ood_point(transcript.verifier_message_f128(), pcs.packed_len()); + let point = sample_point(pcs.packed_len(), transcript); let value = transcript .prover_message::() .map_err(|_| VerifyError::MalformedProof)?; @@ -127,56 +118,40 @@ pub(crate) fn add_succinct_basis( } } -fn absorb_header( - pcs: &Pcs, - root: &[u8; 32], - grinding_bits: u32, - transcript: &mut impl PublicTranscript, -) { +fn sample_point(packed_len: usize, transcript: &mut impl PublicTranscript) -> Vec { transcript.public_message(OOD_ROUND_TAG); - transcript.public_message(root); - transcript.public_message(pcs); - transcript.public_message(&(pcs.packed_len() as u64)); - transcript.public_message(&grinding_bits); -} - -fn ood_point(zeta: F128, packed_len: usize) -> Vec { - let mut point = Vec::with_capacity(packed_len.ilog2() as usize); - let mut coordinate = zeta; - for _ in 0..packed_len.ilog2() { - point.push(coordinate); - coordinate *= coordinate; - } - point -} - -fn prove_pow(transcript: &mut ProverState, bits: u32) { - transcript.public_message(OOD_POW_TAG); - transcript.public_message(&bits); - let seed = transcript.verifier_message::().to_bytes(); - let nonce = find(&seed, bits); - transcript.prover_message(&nonce.to_le_bytes()); -} - -fn verify_pow(transcript: &mut VerifierState<'_>, bits: u32) -> Result<(), ()> { - transcript.public_message(OOD_POW_TAG); - transcript.public_message(&bits); - let seed = transcript.verifier_message::().to_bytes(); - let nonce = transcript - .prover_message::<[u8; 8]>() - .map(u64::from_le_bytes) - .map_err(|_| ())?; - valid(&seed, nonce, bits).then_some(()).ok_or(()) + (0..packed_len.ilog2()) + .map(|_| transcript.verifier_message_f128()) + .collect() } #[cfg(test)] mod tests { use num_traits::ConstZero; use rayon::ThreadPoolBuilder; - use transcript::{build_prover, build_verifier}; + use transcript::build_prover; use super::*; + #[test] + fn each_ood_coordinate_consumes_a_fresh_challenge() { + for variables in 13..=28 { + let mut prover = build_prover(b"ood-test", b"independent-coordinates"); + let mut expected = build_prover(b"ood-test", b"independent-coordinates"); + expected.public_message(OOD_ROUND_TAG); + let point = sample_point(1 << variables, &mut prover); + assert_eq!(point.len(), variables); + for coordinate in point { + assert_eq!(coordinate, expected.verifier_message::()); + } + assert_eq!( + prover.verifier_message::(), + expected.verifier_message::() + ); + assert!(prover.finish().narg_string.is_empty()); + } + } + #[test] fn dense_basis_updates_match_the_full_equality_table_across_thread_counts() { let pools: Vec<_> = [1, 2, 4, 8] @@ -191,7 +166,7 @@ mod tests { for log_len in [0, 8, 12, 17, 18, 19] { let len = 1usize << log_len; let claim = OodClaim { - point: ood_point(F128::new(7, 11), len), + point: sample_point(len, &mut build_prover(b"ood-test", b"dense-basis")), value: F128::ZERO, }; let weights = eq_table(&claim.point); @@ -220,7 +195,7 @@ mod tests { #[test] fn dense_and_succinct_ood_bases_agree_after_folding() { - let point = ood_point(F128::new(7, 11), 1 << 14); + let point = sample_point(1 << 14, &mut build_prover(b"ood-test", b"folded-basis")); let coefficient = F128::new(13, 17); let claim = OodClaim { point, @@ -241,29 +216,4 @@ mod tests { add_succinct_basis(&mut succinct, &claim, coefficient, &queries); assert_eq!(dense, succinct); } - - #[test] - fn grinding_binds_the_following_challenge_and_rejects_invalid_nonces() { - const BITS: u32 = 8; - let mut prover = build_prover(b"ood-test", b"grinding"); - prove_pow(&mut prover, BITS); - let challenge = prover.verifier_message::(); - let mut proof = prover.finish(); - let mut verifier = build_verifier(b"ood-test", b"grinding", &proof); - verify_pow(&mut verifier, BITS).unwrap(); - assert_eq!(verifier.verifier_message::(), challenge); - verifier.check_eof().unwrap(); - - let mut seed_transcript = build_prover(b"ood-test", b"grinding"); - seed_transcript.public_message(OOD_POW_TAG); - seed_transcript.public_message(&BITS); - let seed = seed_transcript.verifier_message::().to_bytes(); - let invalid = (0..).find(|&nonce| !valid(&seed, nonce, BITS)).unwrap(); - proof.narg_string.copy_from_slice(&invalid.to_le_bytes()); - let mut verifier = build_verifier(b"ood-test", b"grinding", &proof); - assert!(verify_pow(&mut verifier, BITS).is_err()); - proof.narg_string.truncate(7); - let mut verifier = build_verifier(b"ood-test", b"grinding", &proof); - assert!(verify_pow(&mut verifier, BITS).is_err()); - } } diff --git a/crates/pcs/src/opening.rs b/crates/pcs/src/opening.rs index a7b1d542..90929faf 100644 --- a/crates/pcs/src/opening.rs +++ b/crates/pcs/src/opening.rs @@ -1,6 +1,6 @@ //! Transcript orchestration for MLE openings and inner-product sumcheck reduction. -use common::LinearClaim; +use common::{LinearClaim, SecurityLevel}; use field::F128; use flock_core::field::F128 as FlockF128; use flock_core::pcs::pack::PACKING_WIDTH as CLAIM_COUNT; @@ -21,6 +21,7 @@ const INNER_PRODUCT_DIGEST_CONTEXT: &str = "bitz/pcs/bit-inner-product-weights/v const SUMCHECK_LABEL: &[u8] = b"bitz/pcs/inner-product-sumcheck/v2"; const MLE_CLAIMS_LABEL: &[u8] = b"bitz/pcs/mle-claims/v1"; const CHALLENGES_LABEL: &[u8] = b"bitz/pcs/ring-switch-challenges/v1"; +const RING_GRINDING_LABEL: &[u8] = b"bitz/pcs/ring/v1"; /// Errors from opening proof creation. #[derive(Clone, Debug, PartialEq, Eq)] @@ -129,17 +130,21 @@ pub(crate) fn prove( ring_switch, *target, commitment.ood.as_ref(), + pcs.security_level(), transcript, ) } OpeningQuery::InnerProduct { claim } => { validate_inner_product_claim(pcs, claim)?; validate_prover_data(pcs, data)?; + if packed_witness.len() != pcs.packed_len() { + return Err(ProveError::PackedWitnessLengthMismatch); + } if statement_binding == StatementBinding::Bind { bind_inner_product_statement(pcs, &root.0, claim, transcript); } transcript.public_message(SUMCHECK_LABEL); - let reduced = prove_post_gkr(claim, &packed_witness, transcript)?; + let reduced = prove_post_gkr(claim, &packed_witness, pcs.security_level(), transcript)?; // The evaluation claim the sumcheck leaves is opened like any // other, and bound whatever the caller's mode: `AlreadyBound` // covers the original claim only. @@ -182,7 +187,7 @@ pub(crate) fn verify( bind_inner_product_statement(pcs, &root.0, claim, transcript); } transcript.public_message(SUMCHECK_LABEL); - let reduced = verify_post_gkr(claim, transcript)?; + let reduced = verify_post_gkr(claim, pcs.security_level(), transcript)?; verify( pcs, commitment, @@ -213,6 +218,7 @@ fn prove_mle( ring_switch: mle::RingSwitch<'_>, target: F128, ood_claim: Option<&OodClaim>, + security: SecurityLevel, transcript: &mut ProverState, ) -> Result<(), ProveError> { let dense_reduction = { @@ -220,6 +226,11 @@ fn prove_mle( let prepared_claims = ring_switch.prepare_claims(as_flock_f128s(prover.witness()), target)?; write_claims(transcript, &prepared_claims.claims); + // The seven coordinates and optional OOD coefficient share one challenge block. + transcript.grind( + RING_GRINDING_LABEL, + security.grinding_bits(7 + usize::from(ood_claim.is_some())), + ); let batching_point = sample_challenges(transcript); let mut reduced = prepared_claims.reduce_dense(&batching_point); if let Some(claim) = ood_claim { @@ -248,6 +259,13 @@ fn verify_mle( if !ring_switch.target_matches(&claims, target) { return Err(VerifyError::VerificationFailed); } + transcript + .grind( + RING_GRINDING_LABEL, + pcs.security_level() + .grinding_bits(7 + usize::from(ood_claim.is_some())), + ) + .map_err(|_| VerifyError::MalformedProof)?; let batching_point = sample_challenges(transcript); ring_switch.reduce_succinct(&claims, &batching_point) }; diff --git a/crates/pcs/src/opening/tests.rs b/crates/pcs/src/opening/tests.rs index d09392e8..8fd63ca3 100644 --- a/crates/pcs/src/opening/tests.rs +++ b/crates/pcs/src/opening/tests.rs @@ -7,7 +7,7 @@ use proptest::prelude::*; use transcript::{build_prover, build_verifier}; use super::*; -use crate::{HashKind, LigeritoProfile}; +use crate::SecurityLevel; const M: usize = 22; const SINGLETON: usize = (1 << 21) | (1 << 7) | 0b101_0101; @@ -16,7 +16,6 @@ const INSTANCE: &[u8] = b"singleton"; struct Fixture { pcs: Pcs, - root: Root, witness: Vec, claim: LinearClaim, } @@ -25,12 +24,9 @@ fn fixture() -> &'static Fixture { static FIXTURE: OnceLock = OnceLock::new(); FIXTURE.get_or_init(|| { let shape = Shape::new(7, M - 7).unwrap(); - let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape, SecurityLevel::Bits100).unwrap(); let mut witness = vec![F128::ZERO; pcs.packed_len()]; witness[SINGLETON / 128].hi = 1 << (SINGLETON % 128 - 64); - let (root, _) = pcs - .commit(&witness, &mut build_prover(SESSION, INSTANCE)) - .unwrap(); let rows = (0..shape.rows()) .map(|row| F128::from(row as u64 + 2)) .collect::>(); @@ -41,7 +37,6 @@ fn fixture() -> &'static Fixture { let claim = LinearClaim::from_shape(&shape, rows, columns, target).unwrap(); Fixture { pcs, - root, witness, claim, } @@ -52,8 +47,8 @@ fn fixture() -> &'static Fixture { fn inner_product_proof_composes_sumcheck_with_a_bound_mle_opening() { let fixture = fixture(); let mut prover = build_prover(SESSION, INSTANCE); - let (_, data) = fixture.pcs.commit(&fixture.witness, &mut prover).unwrap(); - bind_inner_product_statement(&fixture.pcs, &fixture.root.0, &fixture.claim, &mut prover); + let (root, data) = fixture.pcs.commit(&fixture.witness, &mut prover).unwrap(); + bind_inner_product_statement(&fixture.pcs, &root.0, &fixture.claim, &mut prover); prove( &fixture.pcs, &data, @@ -69,13 +64,10 @@ fn inner_product_proof_composes_sumcheck_with_a_bound_mle_opening() { // Independent composition checks stage order and binding of the derived MLE claim. let mut verifier = build_verifier(SESSION, INSTANCE, &proof); - let commitment = fixture - .pcs - .receive_commitment(fixture.root, &mut verifier) - .unwrap(); - bind_inner_product_statement(&fixture.pcs, &fixture.root.0, &fixture.claim, &mut verifier); + let commitment = fixture.pcs.receive_commitment(root, &mut verifier).unwrap(); + bind_inner_product_statement(&fixture.pcs, &root.0, &fixture.claim, &mut verifier); verifier.public_message(SUMCHECK_LABEL); - let reduced = verify_post_gkr(&fixture.claim, &mut verifier).unwrap(); + let reduced = verify_post_gkr(&fixture.claim, SecurityLevel::Bits100, &mut verifier).unwrap(); verify( &fixture.pcs, &commitment, @@ -129,9 +121,9 @@ proptest! { } #[test] -fn opening_leaves_matching_transcripts_for_following_protocols() { +fn opening_reuses_commitment_state_and_leaves_matching_transcripts() { let fixture = fixture(); - for query in [ + let queries = [ OpeningQuery::Mle { point: vec![F128::ZERO; M], target: F128::ZERO, @@ -139,36 +131,84 @@ fn opening_leaves_matching_transcripts_for_following_protocols() { OpeningQuery::InnerProduct { claim: fixture.claim.clone(), }, - ] { + ]; + for security in [SecurityLevel::Bits100, SecurityLevel::Bits128] { + let pcs = Pcs::new(&Shape::new(7, M - 7).unwrap(), security).unwrap(); let mut prover = build_prover(SESSION, INSTANCE); - let (_, data) = fixture.pcs.commit(&fixture.witness, &mut prover).unwrap(); - prove( - &fixture.pcs, - &data, - fixture.witness.clone(), - &query, - StatementBinding::Bind, - &mut prover, - ) - .unwrap(); + let (root, data) = pcs.commit(&fixture.witness, &mut prover).unwrap(); + assert_eq!( + data.commitment().ood.is_some(), + security == SecurityLevel::Bits100 + ); + for query in &queries { + prove( + &pcs, + &data, + fixture.witness.clone(), + query, + StatementBinding::Bind, + &mut prover, + ) + .unwrap(); + } let expected = prover.verifier_message::(); let proof = prover.finish(); let mut verifier = build_verifier(SESSION, INSTANCE, &proof); - let commitment = fixture - .pcs - .receive_commitment(fixture.root, &mut verifier) + let commitment = pcs.receive_commitment(root, &mut verifier).unwrap(); + assert_eq!(commitment.ood.is_some(), security == SecurityLevel::Bits100); + for query in &queries { + verify( + &pcs, + &commitment, + query, + StatementBinding::Bind, + &mut verifier, + ) .unwrap(); + } + assert_eq!(verifier.verifier_message::(), expected); + verifier.check_eof().unwrap(); + } +} + +#[test] +fn opening_rejects_missing_ood_commitment_state() { + let fixture = fixture(); + let pcs = &fixture.pcs; + let mut prover = build_prover(SESSION, INSTANCE); + let (root, mut data) = pcs.commit(&fixture.witness, &mut prover).unwrap(); + let proof = prover.finish(); + + let query = OpeningQuery::Mle { + point: vec![F128::ZERO; M], + target: F128::ZERO, + }; + let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + let mut commitment = pcs.receive_commitment(root, &mut verifier).unwrap(); + commitment.ood = None; + assert_eq!( verify( - &fixture.pcs, + pcs, &commitment, &query, StatementBinding::Bind, - &mut verifier, - ) - .unwrap(); - assert_eq!(verifier.verifier_message::(), expected); - verifier.check_eof().unwrap(); - } + &mut verifier + ), + Err(VerifyError::VerificationFailed), + ); + data.commitment.ood = None; + let mut prover = build_prover(SESSION, INSTANCE); + assert_eq!( + prove( + pcs, + &data, + fixture.witness.clone(), + &query, + StatementBinding::Bind, + &mut prover, + ), + Err(ProveError::ProverDataMismatch), + ); } #[test] @@ -176,7 +216,7 @@ fn zero_weight_factor_still_requires_the_correct_pcs_witness_evaluation() { let fixture = fixture(); let pcs = &fixture.pcs; let shape = Shape::new(7, M - 7).unwrap(); - assert_eq!(pcs.ood_grinding_bits(), Some(0)); + assert_eq!(pcs.security_level(), SecurityLevel::Bits100); for zero_rows in [true, false] { let mut rows = fixture.claim.row_weights().to_vec(); diff --git a/crates/pcs/src/pow.rs b/crates/pcs/src/pow.rs deleted file mode 100644 index c0f2f890..00000000 --- a/crates/pcs/src/pow.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Nonce search and validation shared by PCS grinding rounds. -//! -//! A positive difficulty requires that many leading zero bits in -//! `BLAKE3(POW_HASH_TAG || seed || nonce.to_le_bytes())`. At zero difficulty, -//! only nonce zero is accepted. Transcript framing belongs to each calling round. - -const POW_HASH_TAG: &[u8] = b"bitz-pcs-pow-v1"; - -/// Returns the first valid nonce in ascending order. -// todo: parallel pow? use potentially spongefish? -pub(crate) fn find(seed: &[u8; 16], bits: u32) -> u64 { - if bits == 0 { - return 0; - } - let mut nonce = 0u64; - loop { - if valid(seed, nonce, bits) { - return nonce; - } - nonce = nonce.checked_add(1).expect("proof-of-work nonce exhausted"); - } -} - -/// Checks the hash difficulty, or the canonical zero nonce at zero difficulty. -pub(crate) fn valid(seed: &[u8; 16], nonce: u64, bits: u32) -> bool { - if bits == 0 { - return nonce == 0; - } - let mut hasher = blake3::Hasher::new(); - hasher.update(POW_HASH_TAG); - hasher.update(seed); - hasher.update(&nonce.to_le_bytes()); - let digest = hasher.finalize(); - leading_zero_bits(digest.as_bytes()) >= bits -} - -fn leading_zero_bits(bytes: &[u8]) -> u32 { - let mut total = 0; - for byte in bytes { - let zeros = byte.leading_zeros(); - total += zeros; - if zeros != 8 { - break; - } - } - total -} diff --git a/crates/pcs/src/profiles.rs b/crates/pcs/src/profiles.rs index 1050dbf3..79636393 100644 --- a/crates/pcs/src/profiles.rs +++ b/crates/pcs/src/profiles.rs @@ -1,144 +1,170 @@ -//! Security profiles for the commitment and its recursive opening. +//! Calculates PCS parameters from the padded witness size and security target. +//! +//! Input `m` is log2(padded witness bits). Packing gives `2^(m - 7)` field elements. +//! We use GF(2^128), 128 witness bits per element, and BLAKE3 for Merkle commitments and PoW. +//! +//! Each level groups binary folds. The first level uses four folds; later levels use up to three. +//! Each fold halves the message size. We stop at five variables: 32 field elements. +//! Successive levels use code rates 1/2, 1/4, 1/8, and so on. +//! +//! The 100-bit profile uses Johnson list decoding, OOD checks, and no query grinding. +//! The 128-bit profile uses unique decoding, no OOD checks, and query grinding. +//! Flock supplies folding, query, and multilinear OOD estimates. We add our combined error costs. +//! +//! These targets concern classical challenge blocks. They assume uniform transcript challenges and classical PoW costs. +//! Sources: BitZ Remark A.2; Flock Appendix C.3; BCHKS25 Corollary 1.4. -use flock_core::hash::HashKind; use flock_core::pcs::LOG_PACKING; use flock_core::pcs::ligerito::{ - FinalBlockConfig, GrindingStep, LigeritoLevelConfig, LigeritoProfile, LigeritoSecurityConfig, - SoundnessRegime, embedded_security_config, + FinalBlockConfig, GrindingStep, LigeritoLevelConfig, LigeritoSecurityConfig, SoundnessRegime, }; -use crate::ConfigError; +use crate::{ConfigError, SecurityLevel}; -const FAST_INITIAL_K: usize = 4; -const FAST_RECURSIVE_K: usize = 3; -const FAST_MAX_FINAL_LOG_N: usize = 5; -const FAST_SECURITY_BITS: usize = 100; -const FAST_QUERY_GRINDING_BITS: usize = 16; +const INITIAL_K: usize = 4; +const RECURSIVE_K: usize = 3; +const FINAL_LOG_N: usize = 5; const JOHNSON_ETA: f64 = 0.02; -/// Uses 16-lane rows for Fast and keeps Flock's embedded Slim and Secure profiles. -/// Every level uses the commitment's Merkle hash. -/// `CheckedLigerito` validates the result when it constructs the prover and verifier configurations. +/// Builds the fold schedule and configures each level for the selected target. pub(crate) fn security_config( m: usize, - profile: LigeritoProfile, - merkle_hash: HashKind, + security_level: SecurityLevel, ) -> Result { - let mut security = match profile { - LigeritoProfile::Fast => fast_security_config(m)?, - LigeritoProfile::Slim | LigeritoProfile::Secure => { - let toml = embedded_security_config(m, profile).ok_or(ConfigError::Invalid( - "no security config for this size and profile", - ))?; - LigeritoSecurityConfig::from_toml_str(toml) - .map_err(|_| ConfigError::Invalid("security config"))? - } - }; - security.hash = match merkle_hash { - HashKind::Sha256 => "sha256", - HashKind::Blake3 => "blake3", - } - .into(); - Ok(security) -} - -/// Derives initial OOD grinding from the level-zero collision bound. -/// -/// With `rho = 2^-log_inv_rate`, let `list_size = 1 / (2 * eta * sqrt(rho))`, -/// `pairs = max(list_size * (list_size - 1) / 2, 1)`, and -/// `degree = max(2^packed_vars - 1, 1)`. The unground bound is -/// `128 - log2(pairs) - log2(degree)` bits; grinding covers its rounded-up -/// deficit against the target. Unique-decoding profiles return `None`. -pub(crate) fn ood_grinding_bits( - security: &LigeritoSecurityConfig, - packed_vars: usize, -) -> Option { - let level = security.levels.first()?; - let eta = match level.regime { - SoundnessRegime::JohnsonOod => level.eta?, - SoundnessRegime::Udr => return None, - }; - let rho = (-(level.log_inv_rate as f64)).exp2(); - let list_size = 1.0 / (2.0 * eta * rho.sqrt()); - let pairs = (list_size * (list_size - 1.0) / 2.0).max(1.0); - let degree = ((packed_vars as f64).exp2() - 1.0).max(1.0); - let collision_bits = 128.0 - pairs.log2() - degree.log2(); - let deficit = security.target_security_bits as f64 - collision_bits; - let grinding_bits = deficit.ceil().max(0.0) as u32; - Some(grinding_bits) -} - -/// Reproduces the reference prover's k = 4 profile with 16-bit query grinding. -/// Flock's `derive_profile` fixes k = 6 and zero query grinding for Fast. -fn fast_security_config(m: usize) -> Result { - if !(22..=35).contains(&m) { - return Err(ConfigError::Invalid( - "no security config for this size and profile", - )); + if !(20..=35).contains(&m) { + return Err(ConfigError::Invalid("unsupported PCS size")); } - let log_n = m - LOG_PACKING; - let mut log_msg_cols = log_n - FAST_INITIAL_K; + let mut remaining = log_n; let mut levels = Vec::new(); - loop { - let first = levels.is_empty(); - let k = if first { - FAST_INITIAL_K + + while remaining > FINAL_LOG_N { + // 1. Choose folds. Leave five variables for the explicit final message. + let folds = if levels.is_empty() { + INITIAL_K } else { - FAST_RECURSIVE_K - }; - let mut level = LigeritoLevelConfig { - log_inv_rate: LigeritoProfile::Fast.log_inv_rate() + levels.len(), - log_msg_cols, - log_num_interleaved: k, - k_recursive: k, - regime: SoundnessRegime::JohnsonOod, - eta: Some(JOHNSON_ETA), - proximity_loss: None, - // One query lets Flock calculate the security contribution per query. - queries: 1, - grinding_bits: FAST_QUERY_GRINDING_BITS, - fold_grinding_bits: 0, - ood_samples: usize::from(!first), - target_security_bits: FAST_SECURITY_BITS, - expected_eps_pg_bits: 0.0, - expected_eps_query_bits: 0.0, - expected_eps_ood_bits: None, + RECURSIVE_K.min(remaining - FINAL_LOG_N) }; - let (proximity_bits, per_query_bits) = level.paper_predicted_bits(); - level.queries = ((FAST_SECURITY_BITS - FAST_QUERY_GRINDING_BITS) as f64 / per_query_bits) - .ceil() as usize; - level.fold_grinding_bits = - (FAST_SECURITY_BITS as f64 - proximity_bits).ceil().max(0.0) as usize; - level.expected_eps_pg_bits = round_bits(proximity_bits); - level.expected_eps_query_bits = round_bits(level.queries as f64 * per_query_bits); - level.expected_eps_ood_bits = level.paper_predicted_ood_bits().map(round_bits); - levels.push(level); - - if log_msg_cols <= FAST_MAX_FINAL_LOG_N { - break; - } - log_msg_cols -= FAST_RECURSIVE_K; + remaining -= folds; + + // 2. log_inv_rate = log2(encoded columns / message columns). + // Completed levels start at zero; adding one gives rates 1/2, 1/4, 1/8, ... + let log_inv_rate = levels.len() + 1; + levels.push(configure_level( + remaining, + folds, + log_inv_rate, + security_level, + )?); } + // 3. Assemble the completed levels and the explicit final message. Ok(LigeritoSecurityConfig { m, log_n, - initial_k: FAST_INITIAL_K, - target_security_bits: FAST_SECURITY_BITS, - analysis_version: "johnson_ood_row_union_over_bchks25_thm_4_6".into(), + initial_k: INITIAL_K, + target_security_bits: security_level.bits() as usize, + analysis_version: match security_level { + SecurityLevel::Bits100 => "bitz_johnson_combined_blocks_v2", + SecurityLevel::Bits128 => "bitz_udr_combined_blocks_v1", + } + .into(), field: "f128".into(), - hash: "sha256".into(), + hash: "blake3".into(), grinding_step: GrindingStep::PostCommitPreQueries, levels, final_block: FinalBlockConfig { - yr_log_n: log_msg_cols, + yr_log_n: remaining, }, }) } -fn round_bits(bits: f64) -> f64 { - (10.0 * bits).round() / 10.0 +/// Builds one complete level, including the estimates that Flock requires for validation. +fn configure_level( + log_msg_cols: usize, + folds: usize, + log_inv_rate: usize, + security_level: SecurityLevel, +) -> Result { + // 1. Set the shape and decoding policy. One query measures the contribution per query. + let johnson = security_level == SecurityLevel::Bits100; + let mut level = LigeritoLevelConfig { + log_msg_cols, + log_inv_rate, + log_num_interleaved: folds, + k_recursive: folds, + regime: if johnson { + SoundnessRegime::JohnsonOod + } else { + SoundnessRegime::Udr + }, + eta: johnson.then_some(JOHNSON_ETA), + // Zero is the fixed backend policy for unique decoding. + proximity_loss: (!johnson).then_some(0.0), + // Later Johnson levels use one OOD sample. The commitment performs the initial check. + ood_samples: usize::from(johnson && log_inv_rate > 1), + target_security_bits: security_level.bits() as usize, + // Steps 2 and 3 replace these starting values. + queries: 1, + grinding_bits: 0, + fold_grinding_bits: 0, + expected_eps_pg_bits: 0.0, + expected_eps_query_bits: 0.0, + expected_eps_ood_bits: None, + }; + + // 2. Select queries and grinding for the requested target. + match security_level { + SecurityLevel::Bits100 => configure_100(&mut level), + SecurityLevel::Bits128 => configure_128(&mut level), + } + if level.queries > 1usize << (log_msg_cols + log_inv_rate) { + return Err(ConfigError::Invalid("queries exceed codeword length")); + } + + // 3. Store Flock's validation estimates using the final query count. + (level.expected_eps_pg_bits, level.expected_eps_query_bits) = level.paper_predicted_bits(); + level.expected_eps_ood_bits = level.paper_predicted_ood_bits(); + Ok(level) +} + +/// Selects the 100-bit parameters with Johnson list decoding. +/// Tests check the fixed OOD and batching bounds for every supported size. +fn configure_100(level: &mut LigeritoLevelConfig) { + // 1. Reuse Flock's folding and per-query estimates. The fold estimate already includes the row multiplier. + let (fold_bits, per_query_bits) = level.paper_predicted_bits(); + let fold_coefficient = (128.0 - fold_bits).exp2(); + let rate = 2f64.powi(-(level.log_inv_rate as i32)); + let list_size = 1.0 / (2.0 * JOHNSON_ETA * rate.sqrt()); + + // 2. b = max(0, ceil(log2(fold_coefficient + 2^k_recursive * list_size)) - 28), for b >= k_recursive. + // Derived from Flock Appendix C.3 (paper_predicted_bits), plus our quadratic sumcheck error. + let coefficient = fold_coefficient + 2f64.powi(level.k_recursive as i32) * list_size; + level.fold_grinding_bits = (coefficient.log2().ceil() - 28.0).max(0.0) as usize; + + // 3. q = ceil(100 / per_query_bits). Supported profiles leave enough error budget for batching. + // See Flock paper_predicted_bits and johnson_all_sizes_cover_combined_blocks. + level.queries = (100.0 / per_query_bits).ceil() as usize; + level.grinding_bits = 0; +} + +/// Selects the 128-bit parameters with unique decoding and no OOD checks. +/// Tests check the theorem range and combined bounds for every supported size. +fn configure_128(level: &mut LigeritoLevelConfig) { + // 1. Reuse Flock's folding and per-query estimates. + let (fold_bits, per_query_bits) = level.paper_predicted_bits(); + let fold_coefficient = (128.0 - fold_bits).exp2(); + + // 2. b = ceil(log2(fold_coefficient + 2)). Each fold uses the same grinding. + // Flock Appendix C.3 (BCHKS25 Corollary 1.4), plus our quadratic sumcheck error. + level.fold_grinding_bits = (fold_coefficient + 2.0).log2().ceil() as usize; + + // 3. q = ceil(128 / per_query_bits). See Flock paper_predicted_bits. + level.queries = (128.0 / per_query_bits).ceil() as usize; + + // 4. 8 < 2^128 * query_miss + ceil(log2(q)) + final_claim <= 11 < 2^4. + // All supported sizes need four bits; see udr_all_supported_sizes_cover_combined_errors. + level.grinding_bits = 4; } #[cfg(test)] @@ -146,76 +172,161 @@ mod tests { use super::*; #[test] - fn fast_profiles_match_original_configs() { - // BLAKE3 fingerprints of Flock's canonical serialization of PR #66's original m22..m35 TOMLs. - let fingerprints = [ - "ea2a8a8e069a402f46cac3b621a0d5ec4fa422e410a59cd402ce0875e6335ae6", - "b7f47c3046888625127fb03f7995693915b40c6381718aa42049bea727311170", - "f452a419d857364970ce991c5abcc5eedb34f826a52ca8cd79f6209133f39a70", - "63a6a55488bae28b0af93fc904f655c96f131ccb08b9798ed261fc08654864fe", - "36537633d668dfe7617eea1d27ad9cbf412b52d7dc0f3ef826371c683bba2b38", - "aa7f5f62d683154cbde220298b541edd6938471eed5fed5b0fd9eb5d901674bc", - "94d33bc4dd03edea9dcb3543921d32ef9c5ac613299de6672e95b23cb3d231be", - "640972ec7c9c7a8243f98cae8812a297229adcd431386300f29bf99391fbc1d5", - "53cf053efb6bb3f4c17623991c26a2b313244ee953ad61a81760814c3361b72b", - "916129a4c90b1732d581783279059eb549bf272ecd05494072d0240af7cea75e", - "001a4d0bef2bcad7be395b39e55e60d4ec25bdf5268a7c3bf5db2267d86060a8", - "97c7ff6015cc19d373d6417ac8107ac20c83ce62858cab16939330d79249fadf", - "8ab7b908ad3d93fc34abd63ba5700ef47565f9720087a16b54354253529b6be9", - "40a19c7d4d41f3f47c92dda8c2961f8ebdbd1b555a13e8a04a6e481162354697", - ]; - for (m, expected) in (22..=35).zip(fingerprints) { - for (hash, hash_name) in [(HashKind::Sha256, "sha256"), (HashKind::Blake3, "blake3")] { - let mut security = security_config(m, LigeritoProfile::Fast, hash).unwrap(); - assert_eq!(security.hash, hash_name); - security.to_prover_verifier_configs().unwrap(); - security.hash = "sha256".into(); - let canonical = security.to_toml_string().unwrap(); - assert_eq!( - blake3::hash(canonical.as_bytes()).to_hex().as_str(), - expected, - "m={m}" + fn udr_m22_matches_audited_parameters() { + let config = security_config(22, SecurityLevel::Bits128).unwrap(); + assert_eq!(config.initial_k, 4); + assert_eq!(config.final_block.yr_log_n, 5); + assert_eq!(config.hash, "blake3"); + assert_eq!(config.target_security_bits, 128); + for (index, level) in config.levels.iter().enumerate() { + assert_eq!(level.queries, [311, 192, 161][index]); + assert_eq!(level.fold_grinding_bits, [10, 9, 7][index]); + assert_eq!(level.grinding_bits, 4); + assert_eq!(level.ood_samples, 0); + } + } + + #[test] + fn udr_all_supported_sizes_cover_combined_errors() { + for m in 20..=35 { + let config = security_config(m, SecurityLevel::Bits128).unwrap(); + assert_eq!(config.hash, "blake3"); + config.to_prover_verifier_configs().unwrap(); + for (index, level) in config.levels.iter().enumerate() { + // Reconstruct the published formulas independently of the bound helpers. + let n = 2f64.powi((level.log_msg_cols + level.log_inv_rate) as i32); + let rho = 2f64.powi(-(level.log_inv_rate as i32)); + let delta = 1.0 - rho; + let gamma = delta / 2.0 - 3.0 / (delta * n); + // BCHKS25 Corollary 1.4 requires delta^2 * n >= 18. + assert!(delta * delta * n >= 18.0); + assert!(gamma >= delta / 3.0 && gamma < delta / 2.0); + let fold_error = |grinding| { + let beta = f64::from(index > 0 && grinding == 0); + (gamma * n + 3.0 + beta) * 2f64.powi(-128 - grinding as i32) + }; + assert!(fold_error(level.fold_grinding_bits) <= 2f64.powi(-128)); + if level.fold_grinding_bits > 0 { + assert!(fold_error(level.fold_grinding_bits - 1) > 2f64.powi(-128)); + } + let final_beta = usize::from(index + 1 == config.levels.len()); + let alpha = level.queries.next_power_of_two().ilog2() as usize; + let miss = (1.0 - gamma).powi(level.queries as i32); + assert!(miss <= 2f64.powi(-128)); + assert!((1.0 - gamma).powi(level.queries as i32 - 1) > 2f64.powi(-128)); + let query = (miss + (alpha + final_beta) as f64 * 2f64.powi(-128)) + * 2f64.powi(-(level.grinding_bits as i32)); + assert!(query <= 2f64.powi(-128), "m={m}, level={index}"); + if level.grinding_bits > 0 { + assert!(query * 2.0 > 2f64.powi(-128)); + } + assert!(level.queries <= n as usize); + assert!(level.fold_grinding_bits <= 32 && level.grinding_bits <= 32); + assert!( + level.fold_grinding_bits >= level.k_recursive, + "every fold must retain its native grinding hook" ); } } } #[test] - fn other_profiles_keep_embedded_configs() { - for m in 22..=35 { - for profile in [LigeritoProfile::Slim, LigeritoProfile::Secure] { - let toml = embedded_security_config(m, profile).unwrap(); - let mut expected = LigeritoSecurityConfig::from_toml_str(toml).unwrap(); - for (hash, hash_name) in - [(HashKind::Sha256, "sha256"), (HashKind::Blake3, "blake3")] - { - let security = security_config(m, profile, hash).unwrap(); - security.to_prover_verifier_configs().unwrap(); - expected.hash = hash_name.into(); - assert_eq!( - security.to_toml_string().unwrap(), - expected.to_toml_string().unwrap() - ); + fn johnson_queries_replace_query_grinding() { + for (m, folds) in [(20, [7, 4, 1]), (22, [9, 6, 3])] { + let config = security_config(m, SecurityLevel::Bits100).unwrap(); + assert_eq!(config.target_security_bits, 100); + for (index, level) in config.levels.iter().enumerate() { + assert_eq!(level.queries, [218, 106, 71][index]); + assert_eq!(level.fold_grinding_bits, folds[index]); + assert_eq!(level.grinding_bits, 0); + assert_eq!(level.ood_samples, usize::from(index > 0)); + } + } + } + + #[test] + fn johnson_all_sizes_cover_combined_blocks() { + for m in 20..=35 { + let config = security_config(m, SecurityLevel::Bits100).unwrap(); + config.to_prover_verifier_configs().unwrap(); + for (index, level) in config.levels.iter().enumerate() { + // Reconstruct the published formulas independently of the bound helpers. + let rho = 2f64.powi(-(level.log_inv_rate as i32)); + let sqrt_rho = rho.sqrt(); + let eta = level.eta.unwrap(); + let list = 1.0 / (2.0 * eta * sqrt_rho); + let gamma = 1.0 - sqrt_rho - eta; + let half = (sqrt_rho / (2.0 * eta)).ceil().max(3.0) + 0.5; + let n = 2f64.powi((level.log_msg_cols + level.log_inv_rate) as i32); + let base = (2.0 * half.powi(5) + 3.0 * half * gamma * rho) / (3.0 * rho.powf(1.5)) + * n + + half / sqrt_rho; + let k = level.k_recursive; + let folds_fit = |grinding: usize| { + (0..k).all(|round| { + let row_union = 2f64.powi((k - 1 - round) as i32); + let effective = grinding.saturating_sub(round); + let beta = f64::from(index > 0 && round == 0 && effective == 0); + base * row_union + (2.0 + beta) * list <= 2f64.powi(28 + effective as i32) + }) + }; + assert!(folds_fit(level.fold_grinding_bits)); + if level.fold_grinding_bits > 0 { + assert!(!folds_fit(level.fold_grinding_bits - 1)); } + let next_list = config.levels.get(index + 1).map_or(1.0, |next| { + 1.0 / (2.0 * next.eta.unwrap() * 2f64.powi(-(next.log_inv_rate as i32)).sqrt()) + }); + let alpha = level.queries.next_power_of_two().ilog2(); + let query = (sqrt_rho + eta).powi(level.queries as i32) + + f64::from(alpha + 1) * next_list * 2f64.powi(-128); + assert!(query <= 2f64.powi(-100)); + let previous_query = (sqrt_rho + eta).powi(level.queries as i32 - 1) + + f64::from((level.queries - 1).next_power_of_two().ilog2() + 1) + * next_list + * 2f64.powi(-128); + assert!(previous_query > 2f64.powi(-100)); + let mu = (level.log_msg_cols + level.log_num_interleaved) as f64; + // Initial and recursive OOD points use independent coordinates, with total degree at most mu. + let ood = list * list * mu * 0.5; + assert!(ood <= 2f64.powi(28)); + assert!((if index == 0 { 8.0 } else { 1.0 }) * list <= 2f64.powi(28)); + assert!(level.queries <= n as usize); + assert_eq!(level.grinding_bits, 0); + assert_eq!(level.ood_samples, usize::from(index > 0)); + assert!(level.fold_grinding_bits <= 32); + assert!(level.fold_grinding_bits >= k); } } } #[test] - fn fast_profile_rejects_unsupported_sizes() { - for m in [0, 21, 36, usize::MAX] { - assert!(security_config(m, LigeritoProfile::Fast, HashKind::Blake3).is_err()); + fn both_profiles_keep_a_five_variable_residual() { + for security in [SecurityLevel::Bits100, SecurityLevel::Bits128] { + for m in 20..=35 { + let config = security_config(m, security).unwrap(); + assert_eq!(config.levels[0].k_recursive, 4); + assert_eq!(config.final_block.yr_log_n, 5); + let total_folds: usize = config.levels.iter().map(|level| level.k_recursive).sum(); + assert_eq!(total_folds + 5, m - 7); + for (index, level) in config.levels.iter().enumerate() { + assert_eq!(level.log_inv_rate, index + 1); + assert_eq!(level.log_num_interleaved, level.k_recursive); + } + if m <= 21 { + assert_eq!(config.levels[1].k_recursive, 3); + assert_eq!(config.levels[2].k_recursive, m - 19); + } + } } } #[test] - fn ood_round_parameters_match_the_level_zero_bound() { - let security = security_config(22, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); - assert_eq!(ood_grinding_bits(&security, 15), Some(0)); - - let mut unique = security; - unique.levels[0].regime = SoundnessRegime::Udr; - unique.levels[0].eta = None; - assert_eq!(ood_grinding_bits(&unique, 15), None); + fn both_profiles_reject_unsupported_sizes() { + for security in [SecurityLevel::Bits100, SecurityLevel::Bits128] { + for m in [0, 19, 36, usize::MAX] { + assert!(security_config(m, security).is_err()); + } + } } } diff --git a/crates/pcs/tests/round_trip.rs b/crates/pcs/tests/round_trip.rs index 5e4b3d46..acfe8c43 100644 --- a/crates/pcs/tests/round_trip.rs +++ b/crates/pcs/tests/round_trip.rs @@ -4,8 +4,8 @@ use common::{LinearClaim, Shape}; use field::F128; use num_traits::ConstZero; use pcs::{ - CommitScheme, Commitment, HashKind, LigeritoProfile, OpeningQuery, Pcs, ProveError, Root, - StatementBinding, VerifyError, + CommitScheme, Commitment, OpeningQuery, Pcs, ProveError, Root, SecurityLevel, StatementBinding, + VerifyError, }; use transcript::{Proof, PublicTranscript, VerifierState, build_prover, build_verifier}; @@ -43,8 +43,8 @@ struct RealFixture { } impl RealFixture { - fn build(profile: LigeritoProfile) -> Self { - let pcs = Pcs::new(&shape(), profile, HashKind::Blake3).unwrap(); + fn build(security: SecurityLevel) -> Self { + let pcs = Pcs::new(&shape(), security).unwrap(); // One nonzero bit gives the expected MLE value a simple independent formula. let mut packed_witness = vec![F128::ZERO; pcs.packed_len()]; let packed_index = SINGLETON / 128; @@ -85,7 +85,7 @@ impl RealFixture { fn fixture() -> &'static RealFixture { static FIXTURE: OnceLock = OnceLock::new(); - FIXTURE.get_or_init(|| RealFixture::build(LigeritoProfile::Fast)) + FIXTURE.get_or_init(|| RealFixture::build(SecurityLevel::Bits100)) } fn singleton_target(point: &[F128], index: usize) -> F128 { @@ -104,6 +104,45 @@ fn singleton_target(point: &[F128], index: usize) -> F128 { }) } +#[test] +fn dynamic_shapes_open_nonzero_witnesses_across_ladder_shapes() { + use pcs::SecurityLevel::{Bits100, Bits128}; + // These sizes exercise final folds of one, two, and three variables. + // Size 23 also adds a recursive level. + for m in 20..=23 { + let shape = Shape::new(7, m - 7).unwrap(); + for level in [Bits100, Bits128] { + let pcs = Pcs::new(&shape, level).unwrap(); + let index = (1usize << m) - 1; + let mut witness = vec![F128::ZERO; pcs.packed_len()]; + witness[index / 128].hi = 1 << 63; + let point: Vec<_> = (0..m).map(|i| F128::from(i as u64 + 2)).collect(); + let query = OpeningQuery::Mle { + target: singleton_target(&point, index), + point, + }; + let mut prover = build_prover(SESSION, b"dynamic"); + let (root, data) = pcs.commit(&witness, &mut prover).unwrap(); + + pcs.prove_lin(&data, witness, &query, StatementBinding::Bind, &mut prover) + .unwrap(); + let continuation = prover.verifier_message::(); + let proof = prover.finish(); + let mut verifier = build_verifier(SESSION, b"dynamic", &proof); + verify_opening(&pcs, &root, &query, StatementBinding::Bind, &mut verifier).unwrap(); + assert_eq!(verifier.verifier_message::(), continuation); + verifier.check_eof().unwrap(); + + let mut truncated = proof.clone(); + truncated.narg_string.pop(); + let mut verifier = build_verifier(SESSION, b"dynamic", &truncated); + assert!( + verify_opening(&pcs, &root, &query, StatementBinding::Bind, &mut verifier).is_err() + ); + } + } +} + fn factor_weight(index: usize) -> F128 { let index = index as u64; F128::new( @@ -155,9 +194,9 @@ struct InnerProductFixture { } impl InnerProductFixture { - fn build(profile: LigeritoProfile) -> Self { + fn build(security: SecurityLevel) -> Self { let shape = inner_product_shape(); - let pcs = Pcs::new(&shape, profile, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape, security).unwrap(); let witness = inner_product_witness(pcs.packed_len()); let target = inner_product_target(&shape); assert_ne!(target, F128::ZERO); @@ -193,16 +232,13 @@ impl InnerProductFixture { query: &OpeningQuery, proof: &'proof Proof, binding: StatementBinding, - ) -> (Commitment, VerifierState<'proof>) { + ) -> Result<(Commitment, VerifierState<'proof>), VerifyError> { let mut verifier = build_verifier(SESSION, INNER_PRODUCT_INSTANCE, proof); - let received = self - .pcs - .receive_commitment(*commitment, &mut verifier) - .unwrap(); + let received = self.pcs.receive_commitment(*commitment, &mut verifier)?; if binding == StatementBinding::AlreadyBound { bind_outer_inner_product_statement(&mut verifier, &self.pcs, commitment, query); } - (received, verifier) + Ok((received, verifier)) } fn verify( @@ -212,22 +248,20 @@ impl InnerProductFixture { proof: &Proof, binding: StatementBinding, ) -> Result<(), VerifyError> { - let (received, mut verifier) = self.verifier(commitment, query, proof, binding); + let (received, mut verifier) = self.verifier(commitment, query, proof, binding)?; self.pcs .verify_lin(&received, query, binding, &mut verifier) } } -fn inner_product_fixture(profile: LigeritoProfile) -> &'static InnerProductFixture { - static FAST: OnceLock = OnceLock::new(); - static SLIM: OnceLock = OnceLock::new(); - static SECURE: OnceLock = OnceLock::new(); - let fixture = match profile { - LigeritoProfile::Fast => &FAST, - LigeritoProfile::Slim => &SLIM, - LigeritoProfile::Secure => &SECURE, +fn inner_product_fixture(security: SecurityLevel) -> &'static InnerProductFixture { + static BITS100: OnceLock = OnceLock::new(); + static BITS128: OnceLock = OnceLock::new(); + let fixture = match security { + SecurityLevel::Bits100 => &BITS100, + SecurityLevel::Bits128 => &BITS128, }; - fixture.get_or_init(|| InnerProductFixture::build(profile)) + fixture.get_or_init(|| InnerProductFixture::build(security)) } fn bind_outer_inner_product_statement( @@ -282,7 +316,7 @@ fn real_pcs_opening_round_trip_succeeds() { #[test] fn real_pcs_ood_round_batches_into_opening() { - let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let mut packed_witness = vec![F128::ZERO; pcs.packed_len()]; packed_witness[SINGLETON / 128] = F128::new(0, 1 << (SINGLETON % 128 - 64)); let point = vec![F128::from(2u64); M]; @@ -317,7 +351,7 @@ fn ood_round_trip(pcs: &impl CommitScheme, packed_witness: Vec, query: Ope #[test] fn real_pcs_ood_round_rejects_a_changed_evaluation() { - let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; let query = OpeningQuery::Mle { point: vec![F128::from(2u64); M], @@ -345,20 +379,18 @@ fn real_pcs_ood_round_rejects_a_changed_evaluation() { } #[test] -fn factored_inner_product_round_trip_succeeds_for_all_profiles_and_bindings() { - for profile in [ - LigeritoProfile::Fast, - LigeritoProfile::Slim, - LigeritoProfile::Secure, - ] { - let fixture = inner_product_fixture(profile); +fn factored_inner_product_round_trip_succeeds_for_both_security_levels_and_bindings() { + for security in [SecurityLevel::Bits100, SecurityLevel::Bits128] { + let fixture = inner_product_fixture(security); for binding in [StatementBinding::Bind, StatementBinding::AlreadyBound] { - let (received, mut verifier) = fixture.verifier( - &fixture.commitment, - &fixture.query, - fixture.proof(binding), - binding, - ); + let (received, mut verifier) = fixture + .verifier( + &fixture.commitment, + &fixture.query, + fixture.proof(binding), + binding, + ) + .unwrap(); fixture .pcs .verify_lin(&received, &fixture.query, binding, &mut verifier) @@ -371,7 +403,7 @@ fn factored_inner_product_round_trip_succeeds_for_all_profiles_and_bindings() { #[test] fn factored_inner_product_prover_rejects_a_false_target() { let shape = inner_product_shape(); - let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape, SecurityLevel::Bits100).unwrap(); let witness = inner_product_witness(pcs.packed_len()); let query = factored_query(&shape, inner_product_target(&shape) + F128::from(1u64)); @@ -390,7 +422,7 @@ fn factored_inner_product_prover_rejects_a_false_target() { #[test] fn factored_inner_product_rejects_statement_mutations() { - let fixture = inner_product_fixture(LigeritoProfile::Fast); + let fixture = inner_product_fixture(SecurityLevel::Bits100); let OpeningQuery::InnerProduct { claim } = &fixture.query else { unreachable!(); }; @@ -432,7 +464,7 @@ fn factored_inner_product_rejects_statement_mutations() { #[test] fn factored_inner_product_requires_complete_transcript_consumption() { - let fixture = inner_product_fixture(LigeritoProfile::Fast); + let fixture = inner_product_fixture(SecurityLevel::Bits100); for binding in [StatementBinding::Bind, StatementBinding::AlreadyBound] { for append_hint in [false, true] { let mut proof = fixture.proof(binding).clone(); @@ -441,8 +473,9 @@ fn factored_inner_product_requires_complete_transcript_consumption() { } else { proof.narg_string.push(0); } - let (received, mut verifier) = - fixture.verifier(&fixture.commitment, &fixture.query, &proof, binding); + let (received, mut verifier) = fixture + .verifier(&fixture.commitment, &fixture.query, &proof, binding) + .unwrap(); fixture .pcs .verify_lin(&received, &fixture.query, binding, &mut verifier) @@ -454,7 +487,7 @@ fn factored_inner_product_requires_complete_transcript_consumption() { #[test] fn factored_inner_product_rejects_wrong_weight_lengths() { - let pcs = Pcs::new(&shape(), LigeritoProfile::Secure, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits128).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; let mut prover = build_prover(SESSION, b"wrong-inner-product-weight-count"); let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); @@ -489,7 +522,7 @@ fn factored_inner_product_rejects_wrong_weight_lengths() { #[test] fn factored_inner_product_rejects_invalid_prover_inputs_before_sumcheck() { let shape = inner_product_shape(); - let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape, SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; let mut prover = build_prover(SESSION, b"inner-product-wrong-packed-length"); let (_, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); @@ -508,7 +541,7 @@ fn factored_inner_product_rejects_invalid_prover_inputs_before_sumcheck() { Err(ProveError::PackedWitnessLengthMismatch), ); - let other = Pcs::new(&shape, LigeritoProfile::Slim, HashKind::Blake3).unwrap(); + let other = Pcs::new(&shape, SecurityLevel::Bits128).unwrap(); let mut prover = build_prover(SESSION, b"inner-product-mismatched-parameters"); assert_eq!( other.prove_lin( @@ -523,8 +556,8 @@ fn factored_inner_product_rejects_invalid_prover_inputs_before_sumcheck() { } #[test] -fn slim_profile_opening_round_trip_exercises_pow() { - let fixture = RealFixture::build(LigeritoProfile::Slim); +fn bits128_opening_round_trip_exercises_pow() { + let fixture = RealFixture::build(SecurityLevel::Bits128); let mut verifier = build_verifier(SESSION, INSTANCE, &fixture.proof); verify_opening( @@ -540,7 +573,7 @@ fn slim_profile_opening_round_trip_exercises_pow() { #[test] fn real_pcs_accepts_an_already_bound_statement() { - let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; let query = OpeningQuery::Mle { point: vec![F128::from(2u64); M], @@ -590,7 +623,7 @@ fn real_pcs_accepts_an_already_bound_statement() { #[test] fn real_pcs_rejects_point_length_mismatches() { - let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; let mut prover = build_prover(SESSION, b"wrong-prover-point"); let (_, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); @@ -629,7 +662,7 @@ fn real_pcs_rejects_point_length_mismatches() { #[test] fn real_pcs_rejects_packed_witness_length_mismatches_during_opening() { - let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let mut packed_witness = vec![F128::ZERO; pcs.packed_len()]; let mut prover = build_prover(SESSION, b"wrong-packed-length"); let (_, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); @@ -653,11 +686,11 @@ fn real_pcs_rejects_packed_witness_length_mismatches_during_opening() { #[test] fn real_pcs_rejects_mismatched_prover_parameters() { - let source = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let source = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; source.packed_len()]; let mut prover = build_prover(SESSION, b"mismatched-parameters"); let (_, data) = source.commit(&packed_witness, &mut prover).unwrap(); - let other = Pcs::new(&shape(), LigeritoProfile::Slim, HashKind::Blake3).unwrap(); + let other = Pcs::new(&shape(), SecurityLevel::Bits128).unwrap(); let query = OpeningQuery::Mle { point: vec![F128::from(2u64); M], target: F128::from(0u64), @@ -678,23 +711,19 @@ fn real_pcs_rejects_mismatched_prover_parameters() { #[test] fn real_pcs_rejects_commitment_state_from_different_parameters() { let fixture = fixture(); - for (profile, hash) in [ - (LigeritoProfile::Slim, HashKind::Blake3), - (LigeritoProfile::Fast, HashKind::Sha256), - ] { - let other = Pcs::new(&shape(), profile, hash).unwrap(); + for (m, security) in [(M - 1, SecurityLevel::Bits100), (M, SecurityLevel::Bits128)] { + let other = Pcs::new(&Shape::new(7, m - 7).unwrap(), security).unwrap(); + let query = OpeningQuery::Mle { + point: vec![F128::ZERO; m], + target: F128::ZERO, + }; let mut verifier = build_verifier(SESSION, INSTANCE, &fixture.proof); let commitment = fixture .pcs .receive_commitment(fixture.commitment, &mut verifier) .unwrap(); assert_eq!( - other.verify_lin( - &commitment, - &fixture.query, - StatementBinding::Bind, - &mut verifier - ), + other.verify_lin(&commitment, &query, StatementBinding::Bind, &mut verifier,), Err(VerifyError::VerificationFailed), ); } @@ -702,7 +731,7 @@ fn real_pcs_rejects_commitment_state_from_different_parameters() { #[test] fn real_pcs_prover_rejects_a_false_evaluation_without_consuming_prover_data() { - let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; let mut prover = build_prover(SESSION, b"false-evaluation"); let (_, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); @@ -727,7 +756,7 @@ fn real_pcs_prover_rejects_a_false_evaluation_without_consuming_prover_data() { #[test] fn real_pcs_rejects_an_opening_for_a_different_packed_witness() { - let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape(), SecurityLevel::Bits100).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; let mut prover = build_prover(SESSION, b"different-packed-witness"); let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); diff --git a/crates/post_gkr/src/lib.rs b/crates/post_gkr/src/lib.rs index 7a9ff452..81a81a17 100644 --- a/crates/post_gkr/src/lib.rs +++ b/crates/post_gkr/src/lib.rs @@ -21,12 +21,12 @@ //! the tables the prover folds to have one entry per two bits. The weights //! are never written out in full: their folded table is the folded row //! factor tensored with the column factor. Proof: `2m + 1` elements, `rho` -//! low coordinate first ([`OpeningQuery::Mle`]). +//! low coordinate first ([`OpeningQuery::Mle`]). Each positive grind adds an eight-byte nonce. //! //! # Transcript //! -//! Each round's coefficients are written and absorbed before its challenge -//! is squeezed; the closing evaluation follows the last round. The claim is +//! Each round absorbs its coefficients, checks any grinding, then samples its challenge. +//! The closing evaluation follows the last round. The claim is //! not re-absorbed here: the opening scheme binds its statement before it //! calls the sumcheck. The round count derives from the claim and is not //! absorbed either. @@ -37,7 +37,7 @@ mod test_util; use crate::sumcheck::{Pair, RoundMessage}; use common::shape::PACK_BITS; -use common::{LinearClaim, OpeningQuery}; +use common::{LinearClaim, OpeningQuery, SecurityLevel}; use field::F128; use num_traits::{ConstOne, ConstZero}; #[cfg(feature = "parallel")] @@ -67,6 +67,9 @@ pub enum VerifyError { /// The bits one packed element carries. const ELEMENT_BITS: usize = 1 << PACK_BITS; +/// Each quadratic round has error at most `2 / |F128|` before grinding. +const GRINDING_LABEL: &[u8] = b"bitz/pcs/sumcheck/v1"; + /// The even bit positions of a packed element, `0b01010101...` const EVEN: u128 = 0x55555555555555555555555555555555; @@ -82,6 +85,7 @@ const ELEMENTS_PER_TASK: usize = workload_size::() / ELEMENT_BITS; pub fn prove( claim: &LinearClaim, packed: &[F128], + security: SecurityLevel, transcript: &mut ProverState, ) -> Result { prove_factors( @@ -89,6 +93,7 @@ pub fn prove( claim.column_weights(), claim.target(), packed, + security, transcript, ) } @@ -99,12 +104,14 @@ pub fn prove( #[tracing::instrument(name = "Verify inner-product sumcheck", skip_all)] pub fn verify( claim: &LinearClaim, + security: SecurityLevel, transcript: &mut VerifierState<'_>, ) -> Result { verify_factors( claim.row_weights(), claim.column_weights(), claim.target(), + security, transcript, ) } @@ -116,6 +123,7 @@ fn prove_factors( columns: &[F128], target: F128, packed: &[F128], + security: SecurityLevel, transcript: &mut ProverState, ) -> Result { let factors = Factors { rows, columns }; @@ -128,10 +136,11 @@ fn prove_factors( let message = factors.first_message(packed); transcript.prover_message(&message); + transcript.grind(GRINDING_LABEL, security.grinding_bits(2)); let challenge: F128 = transcript.verifier_message(); let running = advance(target, message, challenge); let mut pair = Pair::new(factors.fold(challenge), bind_first(packed, challenge)); - let (rest, target) = sumcheck::prove(&mut pair, running, transcript); + let (rest, target) = sumcheck::prove(&mut pair, running, security, transcript); let mut point = vec![challenge]; point.extend(rest); Ok(OpeningQuery::Mle { point, target }) @@ -142,6 +151,7 @@ fn verify_factors( rows: &[F128], columns: &[F128], target: F128, + security: SecurityLevel, transcript: &mut VerifierState<'_>, ) -> Result { let log_rows = rows.len().trailing_zeros() as usize; @@ -151,7 +161,7 @@ fn verify_factors( poly::f128::evaluate(rows, &point[..log_rows]) * poly::f128::evaluate(columns, &point[log_rows..]) }; - let (point, target) = sumcheck::verify(rounds, target, weight, transcript)?; + let (point, target) = sumcheck::verify(rounds, target, weight, security, transcript)?; Ok(OpeningQuery::Mle { point, target }) } @@ -283,6 +293,7 @@ fn advance(claim: F128, [a0, a2]: RoundMessage, challenge: F128) -> F128 { #[cfg(test)] mod tests { + use common::SecurityLevel::{Bits100, Bits128}; use common::Shape; use poly::DenseMultilinearExtension; use transcript::{Proof, build_prover, build_verifier}; @@ -299,22 +310,33 @@ mod tests { (point, *target) } - fn reduced(leaf: &Leaf) -> (OpeningQuery, Proof) { + fn reduced(leaf: &Leaf, security: SecurityLevel) -> (OpeningQuery, Proof) { let mut prover = build_prover("post_gkr-tests", "reduce"); let sent = prove_factors( &leaf.rows, &leaf.columns, leaf.target, &leaf.packed, + security, &mut prover, ) .unwrap(); (sent, prover.finish()) } - fn verified(leaf: &Leaf, proof: &Proof) -> Result { + fn verified( + leaf: &Leaf, + proof: &Proof, + security: SecurityLevel, + ) -> Result { let mut verifier = build_verifier("post_gkr-tests", "reduce", proof); - let received = verify_factors(&leaf.rows, &leaf.columns, leaf.target, &mut verifier)?; + let received = verify_factors( + &leaf.rows, + &leaf.columns, + leaf.target, + security, + &mut verifier, + )?; assert!(verifier.check_eof().is_ok()); Ok(received) } @@ -336,11 +358,11 @@ mod tests { fn the_two_sides_agree_on_a_true_claim() { for (log_rows, log_columns, seed) in [(8, 2, 63), (10, 0, 64)] { let leaf = Leaf::random(log_rows, log_columns, seed); - let (sent, proof) = reduced(&leaf); + let (sent, proof) = reduced(&leaf, Bits100); assert_eq!(proof.narg_string.len(), (2 * 10 + 1) * 16); assert!(proof.hints.is_empty()); - let received = verified(&leaf, &proof).unwrap(); + let received = verified(&leaf, &proof, Bits100).unwrap(); assert_eq!(sent, received); let (point, target) = mle(&received); assert_eq!(point.len(), 10); @@ -364,10 +386,10 @@ mod tests { ) .unwrap(); let mut prover = build_prover("post_gkr-tests", "reduce"); - let sent = prove(&claim, &leaf.packed, &mut prover).unwrap(); + let sent = prove(&claim, &leaf.packed, Bits100, &mut prover).unwrap(); let proof = prover.finish(); let mut verifier = build_verifier("post_gkr-tests", "reduce", &proof); - assert_eq!(verify(&claim, &mut verifier), Ok(sent.clone())); + assert_eq!(verify(&claim, Bits100, &mut verifier), Ok(sent.clone())); let (point, target) = mle(&sent); assert_eq!(leaf.evaluate(point), target); } @@ -390,7 +412,7 @@ mod tests { let mut generic = Pair::new(weights.clone(), written_out.clone()); let mut prover = build_prover("post_gkr-tests", "reduce"); - let (point, _) = sumcheck::prove(&mut generic, leaf.target, &mut prover); + let (point, _) = sumcheck::prove(&mut generic, leaf.target, Bits100, &mut prover); let proof = prover.finish(); let message = factors.first_message(&leaf.packed); assert_eq!( @@ -412,29 +434,78 @@ mod tests { #[test] fn a_reduction_altered_in_transit_is_caught() { let mut leaf = Leaf::random(7, 1, 66); - let (_, proof) = reduced(&leaf); + let (_, proof) = reduced(&leaf, Bits100); let records = 2 * 8 + 1; assert_eq!(proof.narg_string.len(), records * 16); for record in 0..records { let mut altered = proof.clone(); altered.narg_string[record * 16] ^= 1; assert_eq!( - verified(&leaf, &altered), + verified(&leaf, &altered, Bits100), Err(VerifyError::EvaluationMismatch), "record {record}" ); } let mut short = proof.clone(); short.narg_string.truncate(proof.narg_string.len() - 16); - assert_eq!(verified(&leaf, &short), Err(VerifyError::MalformedProof)); + assert_eq!( + verified(&leaf, &short, Bits100), + Err(VerifyError::MalformedProof) + ); leaf.target += F128::ONE; assert_eq!( - verified(&leaf, &proof), + verified(&leaf, &proof, Bits100), Err(VerifyError::EvaluationMismatch) ); } + #[test] + fn security_targets_cover_every_quadratic_round() { + let leaf = Leaf::random(7, 1, 68); + let rounds = 8; + for security in [Bits100, Bits128] { + let (sent, proof) = reduced(&leaf, security); + let nonce_bytes = usize::from(security == Bits128) * 8; + assert_eq!(proof.narg_string.len(), rounds * (32 + nonce_bytes) + 16); + assert_eq!(verified(&leaf, &proof, security), Ok(sent.clone())); + let (point, target) = mle(&sent); + assert_eq!(leaf.evaluate(point), target); + + let other = match security { + Bits100 => Bits128, + Bits128 => Bits100, + }; + assert!(verified(&leaf, &proof, other).is_err()); + + if nonce_bytes != 0 { + for round in 0..rounds { + let mut altered = proof.clone(); + let nonce = round * 40 + 32; + altered.narg_string[nonce] ^= 1; + assert!( + verified(&leaf, &altered, security).is_err(), + "round {round}" + ); + let mut missing = proof.clone(); + missing.narg_string.drain(nonce..nonce + 8); + assert!( + verified(&leaf, &missing, security).is_err(), + "round {round}" + ); + } + } + for length in 0..proof.narg_string.len() { + let mut short = proof.clone(); + short.narg_string.truncate(length); + assert!( + verified(&leaf, &short, security).is_err(), + "length {length}" + ); + } + } + } + #[test] fn the_reduction_refuses_what_it_cannot_prove() { let leaf = Leaf::random(7, 1, 56); @@ -445,6 +516,7 @@ mod tests { &leaf.columns, leaf.target + random(&mut rng(57)), &leaf.packed, + Bits100, &mut transcript ), Err(ProveError::ClaimDoesNotHold) @@ -455,6 +527,7 @@ mod tests { &leaf.columns, leaf.target, &leaf.packed[1..], + Bits100, &mut transcript ), Err(ProveError::WitnessLengthMismatch) diff --git a/crates/post_gkr/src/sumcheck.rs b/crates/post_gkr/src/sumcheck.rs index 79b24d4e..9b60b807 100644 --- a/crates/post_gkr/src/sumcheck.rs +++ b/crates/post_gkr/src/sumcheck.rs @@ -13,10 +13,12 @@ //! `h = p(0) + p(1)` fixes `a_1 = h + a_2` and only `(a_0, a_2)` is sent //! ([`RoundMessage`]). Both sides continue with `h' = p(rho)` and the prover //! folds its tables at `rho`. There is no per-round check: a wrong message -//! in any round surfaces in the closing check. Soundness error at most -//! `2n / |E|` for `n` rounds, plus the probability that `MLE[W](rho) = 0`. +//! in any round surfaces in the closing check or the commitment opening. +//! Each challenge has error at most `2 / |F128|`, reduced by its grinding factor. +//! The opening authenticates the closing evaluation, including when its weight is zero. -use crate::VerifyError; +use crate::{GRINDING_LABEL, VerifyError}; +use common::SecurityLevel; use field::{F128, Wide256}; #[cfg(feature = "parallel")] use poly::parallel::workload_size; @@ -96,10 +98,11 @@ impl Pair { pub(crate) fn prove( pair: &mut Pair, claim: F128, + security: SecurityLevel, transcript: &mut ProverState, ) -> (Vec, F128) { let rounds = pair.weights.len().trailing_zeros() as usize; - let (point, running) = prove_rounds(pair, rounds, claim, transcript); + let (point, running) = prove_rounds(pair, rounds, claim, security, transcript); let evaluation = prove_evaluation(pair, running, transcript); (point, evaluation) } @@ -110,9 +113,10 @@ pub(crate) fn verify( rounds: usize, claim: F128, weight: impl FnOnce(&[F128]) -> F128, + security: SecurityLevel, transcript: &mut VerifierState<'_>, ) -> Result<(Vec, F128), VerifyError> { - let (point, running) = verify_rounds(rounds, claim, transcript)?; + let (point, running) = verify_rounds(rounds, claim, security, transcript)?; let evaluation = verify_evaluation(weight(&point), running, transcript)?; Ok((point, evaluation)) } @@ -123,12 +127,14 @@ fn prove_rounds( pair: &mut Pair, rounds: usize, mut claim: F128, + security: SecurityLevel, transcript: &mut ProverState, ) -> (Vec, F128) { let mut point = Vec::with_capacity(rounds); for _ in 0..rounds { let message = pair.message(); transcript.prover_message(&message); + transcript.grind(GRINDING_LABEL, security.grinding_bits(2)); let challenge: F128 = transcript.verifier_message(); claim = super::advance(claim, message, challenge); point.push(challenge); @@ -142,6 +148,7 @@ fn prove_rounds( fn verify_rounds( rounds: usize, mut claim: F128, + security: SecurityLevel, transcript: &mut VerifierState<'_>, ) -> Result<(Vec, F128), VerifyError> { let mut point = Vec::with_capacity(rounds); @@ -149,6 +156,9 @@ fn verify_rounds( let message: RoundMessage = transcript .prover_message() .map_err(|_| VerifyError::MalformedProof)?; + transcript + .grind(GRINDING_LABEL, security.grinding_bits(2)) + .map_err(|_| VerifyError::MalformedProof)?; let challenge: F128 = transcript.verifier_message(); claim = super::advance(claim, message, challenge); point.push(challenge); @@ -206,6 +216,7 @@ fn coefficients_serial(weights: &[F128], values: &[F128]) -> (Wide256, Wide256) #[cfg(test)] mod tests { + use common::SecurityLevel::Bits100; use num_traits::{ConstOne, ConstZero}; use poly::DenseMultilinearExtension; use poly::eq_table; @@ -255,7 +266,7 @@ mod tests { let claim = inner_product(&weights, &values); let mut prover = build_prover("post_gkr-tests", "sumcheck"); - let (point, running) = prove_rounds(&mut pair, 6, claim, &mut prover); + let (point, running) = prove_rounds(&mut pair, 6, claim, Bits100, &mut prover); let evaluation = prove_evaluation(&pair, running, &mut prover); let proof = prover.finish(); assert_eq!(proof.narg_string.len(), (2 * 6 + 1) * 16); @@ -271,7 +282,7 @@ mod tests { assert_eq!(evaluate(&weights, &point), extension(&weights)); let mut verifier = build_verifier("post_gkr-tests", "sumcheck", &proof); - let (same_point, same_running) = verify_rounds(6, claim, &mut verifier).unwrap(); + let (same_point, same_running) = verify_rounds(6, claim, Bits100, &mut verifier).unwrap(); assert_eq!(same_point, point); assert_eq!(same_running, running); assert_eq!( @@ -283,7 +294,7 @@ mod tests { // The same records against a claim one off: the gap survives every // round and the closing check catches it. let mut verifier = build_verifier("post_gkr-tests", "sumcheck", &proof); - let (_, running) = verify_rounds(6, claim + F128::ONE, &mut verifier).unwrap(); + let (_, running) = verify_rounds(6, claim + F128::ONE, Bits100, &mut verifier).unwrap(); assert_eq!( verify_evaluation(evaluate(&weights, &point), running, &mut verifier), Err(VerifyError::EvaluationMismatch) @@ -312,12 +323,12 @@ mod tests { let mut pair = pair(3, &mut rng(10)); let claim = inner_product(&pair.weights, &pair.values); let mut prover = build_prover("post_gkr-tests", "sumcheck"); - prove_rounds(&mut pair, 3, claim, &mut prover); + prove_rounds(&mut pair, 3, claim, Bits100, &mut prover); let mut proof = prover.finish(); proof.narg_string.truncate(proof.narg_string.len() - 16); let mut verifier = build_verifier("post_gkr-tests", "sumcheck", &proof); assert_eq!( - verify_rounds(3, claim, &mut verifier).err(), + verify_rounds(3, claim, Bits100, &mut verifier).err(), Some(VerifyError::MalformedProof) ); } diff --git a/crates/prover/benches/prover.rs b/crates/prover/benches/prover.rs index 6c70a9f4..26c8745a 100644 --- a/crates/prover/benches/prover.rs +++ b/crates/prover/benches/prover.rs @@ -28,11 +28,7 @@ fn random_table(shape: Shape) -> BitTable<'static> { #[divan::bench] fn gkr(bencher: Bencher) { - // `Shape::new(log_rows, log_columns)` rejects fewer than `2^22` committed - // bits total (`MIN_LOG_BITS`) and a row width under 7 (`PACK_BITS`), so - // the two args must sum to at least 22. That floor isn't a security bound - // computed here -- it's the range flock-core ships precomputed Ligerito - // configs for (see the comment on `MIN_LOG_BITS`). + // Use a table within the supported commitment range. let shape = Shape::new(10, 15).unwrap(); let table = random_table(shape); @@ -55,8 +51,13 @@ fn gkr(bencher: Bencher) { .with_inputs(|| transcript::build_prover("gkr", instance)) .bench_values(|mut transcript| { black_box( - prover::gkr_reduce(&mut transcript, black_box(&fold), black_box(&table)) - .expect("benchmark fold matches the table shape"), + prover::gkr_reduce( + &mut transcript, + black_box(&fold), + black_box(&table), + common::SecurityLevel::Bits100, + ) + .expect("benchmark fold matches the table shape"), ) }); } diff --git a/crates/prover/examples/profile.rs b/crates/prover/examples/profile.rs index b2e97402..5b02dd23 100644 --- a/crates/prover/examples/profile.rs +++ b/crates/prover/examples/profile.rs @@ -51,7 +51,12 @@ fn main() { #[inline(never)] fn gkr_wrapper(mut transcript: ProverState, fold: &Fold, table: BitTable<'_>) { black_box( - prover::gkr_reduce(&mut transcript, black_box(fold), black_box(&table)) - .expect("profiling fold matches the table shape"), + prover::gkr_reduce( + &mut transcript, + black_box(fold), + black_box(&table), + common::SecurityLevel::Bits100, + ) + .expect("profiling fold matches the table shape"), ); } diff --git a/crates/prover/src/fold.rs b/crates/prover/src/fold.rs index 8c272fe5..3934e690 100644 --- a/crates/prover/src/fold.rs +++ b/crates/prover/src/fold.rs @@ -1,6 +1,8 @@ //! The fold round: send the column folds, then take the challenge. -use common::{BitTable, Fold, FoldError, LinearClaim, column_images, fold_columns, row_images}; +use common::{ + BitTable, Fold, FoldError, LinearClaim, SecurityLevel, column_images, fold_columns, row_images, +}; use crate::BitZProver; use transcript::ProverState; @@ -37,6 +39,7 @@ impl BitZProver { claim: &LinearClaim>, table: &BitTable<'_>, transcript: &mut ProverState, + security: SecurityLevel, ) -> Result { // The weights are sized by the configured row count while the bits are // read at the table's. Disagreement is a panic, a silently wrong fold, or @@ -56,6 +59,10 @@ impl BitZProver { let images = column_images(self.comb(), &folds); let row_images = row_images(self.comb(), &exponents); + transcript.grind( + b"bitz/columns/v1", + security.grinding_bits(shape.log_columns()), + ); let zeta = (0..shape.log_columns()) .map(|_| transcript.verifier_message()) .collect(); diff --git a/crates/prover/src/prove.rs b/crates/prover/src/prove.rs index 49fc2789..e59e760b 100644 --- a/crates/prover/src/prove.rs +++ b/crates/prover/src/prove.rs @@ -1,8 +1,8 @@ //! `ProveBitZ`. use common::{ - BitTable, ClaimError, LinearClaim, OpeningQuery, TableError, VirtualMap, VirtualMapError, - VirtualStatement, + BitTable, ClaimError, LinearClaim, OpeningQuery, SecurityLevel, TableError, VirtualMap, + VirtualMapError, VirtualStatement, }; use field::{F128, Fq}; use pcs::{CommitScheme, Pcs, ProveError as OpeningProveError, ProverData, StatementBinding}; @@ -13,7 +13,7 @@ use crate::{BitZProver, SendError, reduce::gkr_reduce}; /// A proof the prover cannot produce. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProveError { - /// The setup or PCS bit count differs from the virtual parameters. + /// The setup or PCS bit count differs from the statement parameters. ParameterMismatch, /// The reduced claim cannot be transposed onto the committed bits. VirtualMap(VirtualMapError), @@ -58,6 +58,9 @@ impl BitZProver { packed: Vec, transcript: &mut ProverState, ) -> Result<(), ProveError> { + if pcs.bit_len() != 1 << self.params().shape().log_bits() { + return Err(ProveError::ParameterMismatch); + } let com = data.root(); let table = self.params().table(&packed).map_err(ProveError::Witness)?; @@ -65,14 +68,14 @@ impl BitZProver { // scheme, whose batched opening binds it only in its own statement mode -- // and that fires at step 6, long after the fold has squeezed. // - // The claim itself is not bound: neither `v^(1)`, `v^(2)` nor `mu` reaches - // the sponge here, only the parameters. They enter through the caller's - // own events. + // Bind the PCS policy and original claim before the fold challenges. transcript.public_message(&com.0); transcript.public_message(self.params()); + transcript.public_message(pcs); + transcript.public_message(claim); // Steps 3 and 4: integer column folds, then GKR to a factored bit claim. - let query = self.fold_and_reduce(claim, &table, transcript)?; + let query = self.fold_and_reduce(claim, &table, transcript, pcs.security_level())?; // Step 6: inner-product sumcheck, ring switching, and commitment opening. // Bind the derived query and PCS parameters before the opening challenges. @@ -122,10 +125,11 @@ impl BitZProver { transcript.public_message(params); transcript.public_message(&statement.map().digest()); transcript.public_message(claim); + transcript.public_message(pcs); // Steps 3 and 4: fold the virtual columns and reduce them through GKR. // Modulus reduction is absent; the parameters must already be admissible. - let query = self.fold_and_reduce(claim, &table, transcript)?; + let query = self.fold_and_reduce(claim, &table, transcript, pcs.security_level())?; // Transpose the reduced claim from h to f, since the PCS commits to f. let query = statement @@ -151,16 +155,17 @@ impl BitZProver { claim: &LinearClaim>, table: &BitTable<'_>, transcript: &mut ProverState, + security: SecurityLevel, ) -> Result { // Step 2 is absent: Q is fixed, and BitZParams::new checks its fold bound. // Step 3: fold each column into an integer exponent. let fold = self - .send_fold(claim, table, transcript) + .send_fold(claim, table, transcript, security) .map_err(ProveError::Fold)?; // Step 4: GKR reduces the batched column products to a factored bit claim. // Step 5 needs no separate batching: fold.zeta already batches the columns. - gkr_reduce(transcript, &fold, table).map_err(ProveError::Reduction) + gkr_reduce(transcript, &fold, table, security).map_err(ProveError::Reduction) } } diff --git a/crates/prover/src/reduce.rs b/crates/prover/src/reduce.rs index 4364cf26..e2d9d13f 100644 --- a/crates/prover/src/reduce.rs +++ b/crates/prover/src/reduce.rs @@ -5,7 +5,9 @@ //! `sum(row, column) u1[row] * u2[column] * table.bit(column, row)`. //! The caller must discharge this claim through the commitment opening. -use common::{BitTable, ClaimError, Fold, LinearClaim, OpeningQuery, TransposeError}; +use common::{ + BitTable, ClaimError, Fold, LinearClaim, OpeningQuery, SecurityLevel, TransposeError, +}; use field::F128; use gkr::{GrandProductCircuit, gpgkr_prove}; use num_traits::ConstOne; @@ -56,12 +58,18 @@ pub fn gkr_reduce( transcript: &mut ProverState, fold: &Fold, table: &BitTable, + security: SecurityLevel, ) -> Result { let circuit = init_circuit(table, fold); let (_last_value, witnesses) = circuit.batched_eval(table.shape().columns()); - let (mut point, claim) = - gpgkr_prove(transcript, table.shape().log_bits(), &fold.zeta, witnesses); + let (mut point, claim) = gpgkr_prove( + transcript, + table.shape().log_bits(), + &fold.zeta, + witnesses, + security, + ); // The multilinear extension of the constant-one table is one at every point. let inner_product_claim = claim - F128::ONE; @@ -144,7 +152,7 @@ mod order_check_ai_test { let fold = Fold::new(&shape, folds, images, row_images, zeta).unwrap(); let mut prover = transcript::build_prover("order-check", &F128::ZERO); - let query = gkr_reduce(&mut prover, &fold, &table).unwrap(); + let query = gkr_reduce(&mut prover, &fold, &table, SecurityLevel::Bits100).unwrap(); check_query(&query, &table); } @@ -178,7 +186,7 @@ mod order_check_ai_test { .unwrap(); let mut prover = transcript::build_prover("narrow-table", &F128::ZERO); - let query = gkr_reduce(&mut prover, &fold, &table).unwrap(); + let query = gkr_reduce(&mut prover, &fold, &table, SecurityLevel::Bits100).unwrap(); check_query(&query, &table); } } @@ -203,7 +211,7 @@ mod order_check_ai_test { let fold = Fold::new(&shape, folds, images, row_images, zeta).unwrap(); let mut prover = transcript::build_prover("order-check-b", &F128::ZERO); - let query = gkr_reduce(&mut prover, &fold, &table).unwrap(); + let query = gkr_reduce(&mut prover, &fold, &table, SecurityLevel::Bits100).unwrap(); check_query(&query, &table); } diff --git a/crates/tests/examples/dump_bitz.rs b/crates/tests/examples/dump_bitz.rs index e873a5ed..feb2c608 100644 --- a/crates/tests/examples/dump_bitz.rs +++ b/crates/tests/examples/dump_bitz.rs @@ -44,7 +44,6 @@ fn main() -> Result<(), Box> { println!("prove: {:.1?}", started.elapsed()); let started = std::time::Instant::now(); instance - .verifier .verify( &instance.claim, &instance.pcs, diff --git a/crates/tests/examples/dump_commit.rs b/crates/tests/examples/dump_commit.rs index 742d0464..76b3e3cb 100644 --- a/crates/tests/examples/dump_commit.rs +++ b/crates/tests/examples/dump_commit.rs @@ -5,7 +5,7 @@ mod support; use common::Shape; -use pcs::{HashKind, LigeritoProfile, Pcs}; +use pcs::Pcs; use rand_chacha::ChaCha8Rng; use rand_core::SeedableRng; use support::{hex, write_binary, write_witness}; @@ -39,7 +39,7 @@ fn main() -> Result<(), Box> { let shape = Shape::new(t, s).map_err(|error| format!("invalid shape: {error:?}"))?; let mut rng = ChaCha8Rng::seed_from_u64(seed); let packed = packed_witness(shape, &mut rng); - let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3) + let pcs = Pcs::new(&shape, common::SecurityLevel::Bits100) .map_err(|error| format!("PCS configuration failed: {error:?}"))?; let (root, _data) = pcs .commit(&packed, &mut prover_transcript()) @@ -47,7 +47,7 @@ fn main() -> Result<(), Box> { write_binary(out, |output| write_witness(output, &packed))?; let root = hex(&root.0); println!( - "their side: t={t} s={s} m={} packed_len={} profile=Fast hash=blake3 root={root}", + "their side: t={t} s={s} m={} packed_len={} security=100 hash=blake3 root={root}", shape.log_bits(), packed.len() ); diff --git a/crates/tests/examples/verify_bitz.rs b/crates/tests/examples/verify_bitz.rs index de6f74f1..2aea93f5 100644 --- a/crates/tests/examples/verify_bitz.rs +++ b/crates/tests/examples/verify_bitz.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Box> { }; let instance = Instance::honest(shape, seed); let started = std::time::Instant::now(); - let result = instance.verifier.verify( + let result = instance.verify( &instance.claim, &instance.pcs, instance.com, diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 4da32491..9ea559c0 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -9,10 +9,10 @@ //! The fixtures live here rather than under `tests/` so they compile once //! rather than once per test binary. -use common::{BitTable, BitZParams, LinearClaim, Root, Shape}; +use common::{BitTable, BitZParams, LinearClaim, Root, SecurityLevel, Shape}; use crypto_primitives::LiftElement; use field::{F128, Fq, gf128::smallest_generator}; -use pcs::{HashKind, LigeritoProfile, Pcs, ProverData}; +use pcs::{Pcs, ProverData}; use rand_chacha::ChaCha8Rng; use rand_core::{Rng, SeedableRng}; use transcript::{Proof, ProverState, VerifierState, build_prover, build_verifier}; @@ -101,8 +101,13 @@ pub struct Instance { } impl Instance { - /// [`HonestClaim::new`] under the `Fast` profile, with a setup per role. + /// Commits [`HonestClaim::new`] under the 100-bit policy, with a setup per role. pub fn honest(shape: Shape, seed: u64) -> Self { + Self::with_security(shape, seed, SecurityLevel::Bits100) + } + + /// Builds and commits an honest instance at the requested security level. + pub fn with_security(shape: Shape, seed: u64, security: SecurityLevel) -> Self { let mut rng = ChaCha8Rng::seed_from_u64(seed); let HonestClaim { params, @@ -110,7 +115,7 @@ impl Instance { packed, } = HonestClaim::new(shape, &mut rng); - let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&shape, security).unwrap(); let mut transcript = prover_transcript(); let (com, data) = pcs.commit(&packed, &mut transcript).unwrap(); @@ -127,6 +132,20 @@ impl Instance { } } + /// Receives the commitment before the BitZ proof draws witness-dependent challenges. + pub fn verify( + &self, + claim: &LinearClaim>, + pcs: &Pcs, + root: Root, + mut transcript: VerifierState<'_>, + ) -> Result<(), verifier::VerifyError> { + let commitment = pcs + .receive_commitment(root, &mut transcript) + .map_err(verifier::VerifyError::Opening)?; + self.verifier.verify(claim, pcs, &commitment, transcript) + } + pub fn table(&self) -> BitTable<'_> { self.params.table(&self.packed).unwrap() } diff --git a/crates/tests/tests/fold.rs b/crates/tests/tests/fold.rs index 2d2e9c15..5d786c05 100644 --- a/crates/tests/tests/fold.rs +++ b/crates/tests/tests/fold.rs @@ -1,6 +1,6 @@ //! The fold round, prover against verifier. -use common::{BitZParams, FoldError, LinearClaim}; +use common::{BitZParams, FoldError, LinearClaim, SecurityLevel::Bits100}; use field::{F128, Fq, gf128::smallest_generator}; use num_traits::{ConstOne, ConstZero}; use prover::{BitZProver, SendError}; @@ -15,7 +15,7 @@ fn prove(instance: &Instance) -> (common::Fold, Proof) { let mut transcript = prover_transcript(); let round = instance .prover - .send_fold(&instance.claim, &instance.table(), &mut transcript) + .send_fold(&instance.claim, &instance.table(), &mut transcript, Bits100) .unwrap(); (round, transcript.finish()) } @@ -31,7 +31,7 @@ fn forge(folds: &[u128]) -> Proof { } #[test] -fn the_two_sides_agree_on_every_shape_the_profile_admits() { +fn the_two_sides_agree_on_the_test_shapes() { for shape in [narrow_shape(), wide_shape()] { let instance = Instance::honest(shape, 7); let (sent, proof) = prove(&instance); @@ -39,7 +39,7 @@ fn the_two_sides_agree_on_every_shape_the_profile_admits() { let mut transcript = verifier_transcript(&proof); let received = instance .verifier - .receive_fold(&instance.claim, &mut transcript) + .receive_fold(&instance.claim, &mut transcript, Bits100) .expect("honest proof"); assert_eq!(sent, received, "t = {}", shape.log_rows()); @@ -110,17 +110,23 @@ fn a_fold_at_the_bound_is_accepted_and_one_past_it_is_not() { .unwrap(); let mut transcript = prover_transcript(); - let round = prover.send_fold(&claim, &table, &mut transcript).unwrap(); + let round = prover + .send_fold(&claim, &table, &mut transcript, Bits100) + .unwrap(); assert!(round.folds.iter().all(|&value| value == fold)); let proof = transcript.finish(); let mut transcript = verifier_transcript(&proof); - assert!(verifier.receive_fold(&claim, &mut transcript).is_ok()); + assert!( + verifier + .receive_fold(&claim, &mut transcript, Bits100) + .is_ok() + ); let over = forge(&vec![fold + 1; shape.columns()]); let mut transcript = verifier_transcript(&over); assert_eq!( - verifier.receive_fold(&claim, &mut transcript), + verifier.receive_fold(&claim, &mut transcript, Bits100), Err(ReceiveError::FoldOutOfRange) ); } @@ -138,7 +144,7 @@ fn the_range_check_fires_before_the_reconstruction() { assert_eq!( instance .verifier - .receive_fold(&instance.claim, &mut transcript), + .receive_fold(&instance.claim, &mut transcript, Bits100), Err(ReceiveError::FoldOutOfRange) ); // The same folds also fail the reconstruction, so the assertion above is @@ -158,7 +164,9 @@ fn folds_that_do_not_reconstruct_the_target_are_rejected() { let retargeted = instance.with_target(instance.claim.target() + Fq::ONE); let mut transcript = verifier_transcript(&proof); assert_eq!( - instance.verifier.receive_fold(&retargeted, &mut transcript), + instance + .verifier + .receive_fold(&retargeted, &mut transcript, Bits100), Err(ReceiveError::TargetMismatch) ); } @@ -172,7 +180,7 @@ fn a_witness_of_a_different_shape_is_refused_before_anything_is_written() { assert_eq!( instance .prover - .send_fold(&instance.claim, &other.table(), &mut transcript), + .send_fold(&instance.claim, &other.table(), &mut transcript, Bits100), Err(SendError::ShapeMismatch) ); assert!(transcript.finish().narg_string.is_empty()); @@ -189,7 +197,7 @@ fn a_truncated_proof_is_refused_rather_than_read_past() { assert_eq!( instance .verifier - .receive_fold(&instance.claim, &mut transcript), + .receive_fold(&instance.claim, &mut transcript, Bits100), Err(ReceiveError::MalformedProof) ); } @@ -211,14 +219,18 @@ fn an_all_zero_witness_folds_to_zero_and_still_round_trips() { let table = params.table(&packed).unwrap(); let mut transcript = prover_transcript(); - let round = prover.send_fold(&claim, &table, &mut transcript).unwrap(); + let round = prover + .send_fold(&claim, &table, &mut transcript, Bits100) + .unwrap(); assert!(round.folds.iter().all(|&fold| fold == 0)); assert!(round.images.iter().all(|&image| image == F128::ONE)); let proof = transcript.finish(); let mut transcript = verifier_transcript(&proof); assert_eq!( - verifier.receive_fold(&claim, &mut transcript).unwrap(), + verifier + .receive_fold(&claim, &mut transcript, Bits100) + .unwrap(), round ); } diff --git a/crates/tests/tests/host.rs b/crates/tests/tests/host.rs index 39ed8616..6dce0d1b 100644 --- a/crates/tests/tests/host.rs +++ b/crates/tests/tests/host.rs @@ -42,7 +42,6 @@ fn a_proof_survives_the_round_trip_through_bytes() { // Verification against the decoded proof, with the claim supplied // the way a caller supplies it on both sides. instance - .verifier .verify( &instance.claim, &instance.pcs, @@ -71,7 +70,6 @@ fn a_tampered_fold_is_left_for_the_verifier_to_catch() { let transcript = verifier_transcript(&proof); assert!( instance - .verifier .verify(&instance.claim, &instance.pcs, instance.com, transcript) .is_err(), "a tampered fold must not verify" diff --git a/crates/tests/tests/large.rs b/crates/tests/tests/large.rs index 0f94b66c..8f814dd6 100644 --- a/crates/tests/tests/large.rs +++ b/crates/tests/tests/large.rs @@ -7,7 +7,7 @@ //! //! This test is designed to test what's possible in under a minute. -use common::LinearClaim; +use common::{LinearClaim, SecurityLevel::Bits100}; use field::Fq; use num_traits::ConstOne; use prover::BitZProver; @@ -25,7 +25,7 @@ fn the_fold_round_trips_on_the_large_shape() { let mut transcript = prover_transcript(); let sent = prover - .send_fold(&honest.claim, &honest.table(), &mut transcript) + .send_fold(&honest.claim, &honest.table(), &mut transcript, Bits100) .unwrap(); let proof = transcript.finish(); assert_eq!(proof.narg_string.len(), 16 * shape.columns()); @@ -33,7 +33,7 @@ fn the_fold_round_trips_on_the_large_shape() { let mut transcript = verifier_transcript(&proof); let received = verifier - .receive_fold(&honest.claim, &mut transcript) + .receive_fold(&honest.claim, &mut transcript, Bits100) .expect("honest proof"); assert_eq!(sent, received); assert_eq!(received.row_images.len(), shape.rows()); @@ -47,7 +47,7 @@ fn the_fold_round_trips_on_the_large_shape() { } let over = transcript.finish(); assert_eq!( - verifier.receive_fold(&honest.claim, &mut verifier_transcript(&over)), + verifier.receive_fold(&honest.claim, &mut verifier_transcript(&over), Bits100), Err(ReceiveError::FoldOutOfRange) ); @@ -60,7 +60,7 @@ fn the_fold_round_trips_on_the_large_shape() { ) .unwrap(); assert_eq!( - verifier.receive_fold(&retargeted, &mut verifier_transcript(&proof)), + verifier.receive_fold(&retargeted, &mut verifier_transcript(&proof), Bits100), Err(ReceiveError::TargetMismatch) ); } diff --git a/crates/tests/tests/prove.rs b/crates/tests/tests/prove.rs index 07cc90b5..36e0a831 100644 --- a/crates/tests/tests/prove.rs +++ b/crates/tests/tests/prove.rs @@ -1,11 +1,11 @@ //! The top-level prove and verify, through the real opening. -use common::{Root, TableError}; +use common::{Root, SecurityLevel, Shape, TableError}; use field::{F128, Fq}; use num_traits::{ConstOne, ConstZero}; -use pcs::{HashKind, LigeritoProfile, Pcs, VerifyError as PcsVerifyError}; +use pcs::{Pcs, VerifyError as PcsVerifyError}; use prover::ProveError; -use tests::{Instance, narrow_shape, verifier_transcript, wide_shape}; +use tests::{Instance, narrow_shape, prover_transcript, verifier_transcript, wide_shape}; use transcript::Proof; use verifier::{ReceiveError, VerifyError}; @@ -31,7 +31,6 @@ fn an_honest_proof_verifies_on_both_floor_shapes() { let proof = prove(&mut instance); instance - .verifier .verify( &instance.claim, &instance.pcs, @@ -49,7 +48,7 @@ fn a_proof_replayed_under_a_different_commitment_is_refused() { // Binding a different root changes the fold batching point, so GKR rejects. assert_eq!( - instance.verifier.verify( + instance.verify( &instance.claim, &instance.pcs, Root([0xffu8; 32]), @@ -69,7 +68,7 @@ fn the_statement_is_bound_before_the_first_challenge() { // binding backs up rather than replaces. let retargeted = instance.with_target(instance.claim.target() + Fq::ONE); assert_eq!( - instance.verifier.verify( + instance.verify( &retargeted, &instance.pcs, instance.com, @@ -86,7 +85,7 @@ fn a_proof_with_trailing_bytes_is_refused() { proof.hints.push(0); assert_eq!( - instance.verifier.verify( + instance.verify( &instance.claim, &instance.pcs, instance.com, @@ -119,7 +118,7 @@ fn an_opening_against_another_commitment_is_refused() { let proof = transcript.finish(); assert_eq!( - proved.verifier.verify( + proved.verify( &proved.claim, &proved.pcs, committed.com, @@ -138,39 +137,36 @@ fn a_tampered_opening_proof_is_refused() { let middle = proof.hints.len() / 2; proof.hints[middle] ^= 0xff; - assert_eq!( - instance.verifier.verify( + assert!(matches!( + instance.verify( &instance.claim, &instance.pcs, instance.com, verifier_transcript(&proof) ), - Err(VerifyError::Opening(PcsVerifyError::VerificationFailed)) - ); + Err(VerifyError::Opening( + PcsVerifyError::MalformedProof | PcsVerifyError::VerificationFailed + )) + )); } #[test] -fn a_proof_verified_under_a_different_profile_is_refused() { - // OOD binds PCS parameters before the first fold challenge, so a different - // profile changes the fold transcript and GKR rejects. +fn a_proof_verified_under_a_different_security_target_is_refused() { + // PCS parameters enter the transcript before the first fold challenge. let mut instance = Instance::honest(narrow_shape(), 38); - let slim = Pcs::new( - instance.params.shape(), - LigeritoProfile::Slim, - HashKind::Blake3, - ) - .unwrap(); + let other_pcs = Pcs::new(instance.params.shape(), SecurityLevel::Bits128).unwrap(); let proof = prove(&mut instance); - assert!(matches!( - instance.verifier.verify( - &instance.claim, - &slim, - instance.com, - verifier_transcript(&proof) - ), - Err(VerifyError::Reduction(_)) - )); + assert!( + instance + .verify( + &instance.claim, + &other_pcs, + instance.com, + verifier_transcript(&proof), + ) + .is_err() + ); } #[test] @@ -199,3 +195,52 @@ fn a_witness_of_the_wrong_length_is_refused_without_changing_the_commitment_tran assert_eq!(verifier.verifier_message::(), next_challenge); verifier.check_eof().unwrap(); } + +#[test] +fn the_128_bit_profile_verifies_and_rejects_replay_or_tampering() { + let mut instance = Instance::with_security(narrow_shape(), 40, SecurityLevel::Bits128); + let mut proof = prove(&mut instance); + let verify = |claim, pcs, proof: &Proof| { + instance.verify(claim, pcs, instance.com, verifier_transcript(proof)) + }; + verify(&instance.claim, &instance.pcs, &proof).unwrap(); + + let other = Pcs::new(instance.params.shape(), SecurityLevel::Bits100).unwrap(); + assert!(verify(&instance.claim, &other, &proof).is_err()); + let retargeted = instance.with_target(instance.claim.target() + Fq::ONE); + assert_eq!( + verify(&retargeted, &instance.pcs, &proof), + Err(VerifyError::Fold(ReceiveError::TargetMismatch)) + ); + + // The first nonce follows the column folds. + proof.narg_string[16 * instance.params.shape().columns()] ^= 0xff; + assert!(verify(&instance.claim, &instance.pcs, &proof).is_err()); +} + +#[test] +fn a_mismatched_direct_commitment_size_is_rejected_before_transcript_mutation() { + let instance = Instance::honest(narrow_shape(), 41); + let pcs = Pcs::new(&Shape::new(7, 16).unwrap(), SecurityLevel::Bits128).unwrap(); + let mut transcript = prover_transcript(); + assert_eq!( + instance.prover.prove( + &instance.claim, + &pcs, + &instance.data, + instance.packed.clone(), + &mut transcript, + ), + Err(ProveError::ParameterMismatch) + ); + assert_eq!(transcript.finish(), Proof::default()); + assert_eq!( + instance.verify( + &instance.claim, + &pcs, + instance.com, + verifier_transcript(&Proof::default()), + ), + Err(VerifyError::ParameterMismatch) + ); +} diff --git a/crates/tests/tests/virtual_prove.rs b/crates/tests/tests/virtual_prove.rs index 452637ae..f6afbdcb 100644 --- a/crates/tests/tests/virtual_prove.rs +++ b/crates/tests/tests/virtual_prove.rs @@ -6,12 +6,12 @@ use circuit::{ witgen::{PackedWitness, Witgen}, }; use common::{ - BitZParams, LinearClaim, Root, Shape, TableError, TransposedWeights, VirtualMap, + BitZParams, LinearClaim, Root, SecurityLevel, Shape, TableError, TransposedWeights, VirtualMap, VirtualMapError, VirtualStatement, }; use field::{F128, Fq, gf128::smallest_generator}; use num_traits::{ConstOne, ConstZero}; -use pcs::{HashKind, LigeritoProfile, Pcs, ProverData}; +use pcs::{Pcs, ProverData}; use prover::{BitZProver, ProveError, VirtualWitness}; use tests::{Q, WINDOW, prover_transcript, verifier_transcript}; use transcript::{Proof, ProverState}; @@ -59,8 +59,14 @@ struct Instance { impl Instance { fn new() -> Self { - let claim_shape = Shape::new(7, 15).unwrap(); - let committed_shape = Shape::new(8, 14).unwrap(); + Self::with_shapes( + Shape::new(7, 15).unwrap(), + Shape::new(8, 14).unwrap(), + SecurityLevel::Bits100, + ) + } + + fn with_shapes(claim_shape: Shape, committed_shape: Shape, security: SecurityLevel) -> Self { let params = BitZParams::::new(claim_shape, smallest_generator()).unwrap(); let claim = LinearClaim::new( ¶ms, @@ -75,7 +81,7 @@ impl Instance { virtual_bits[0] = F128::from(3u64); virtual_bits[1] = F128::from(2u64); - let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&committed_shape, security).unwrap(); let mut transcript = prover_transcript(); let (root, data) = pcs.commit(&committed_bits, &mut transcript).unwrap(); Self { @@ -118,11 +124,16 @@ impl Instance { root: Root, proof: &Proof, ) -> Result<(), VerifyError> { + let mut transcript = verifier_transcript(proof); + let commitment = self + .pcs + .receive_commitment(root, &mut transcript) + .map_err(VerifyError::Opening)?; BitZVerifier::new(self.params, WINDOW).verify_virtual( statement, &self.pcs, - root, - verifier_transcript(proof), + &commitment, + transcript, ) } } @@ -136,6 +147,36 @@ fn virtual_inner_product_opens_the_committed_bits() { .unwrap(); } +#[test] +fn explicit_security_targets_support_different_virtual_and_committed_sizes() { + for level in [SecurityLevel::Bits100, SecurityLevel::Bits128] { + let mut instance = Instance::with_shapes( + Shape::new(7, 14).unwrap(), + Shape::new(8, 12).unwrap(), + level, + ); + assert_eq!(instance.params.shape().log_bits(), 21); + assert_eq!(instance.committed_shape.log_bits(), 20); + let proof = instance.prove(); + instance + .verify(&instance.statement(), instance.root, &proof) + .unwrap(); + + let changed_map = VirtualStatement::new( + instance.params, + instance.committed_shape, + &Map(8), + &instance.claim, + ) + .unwrap(); + assert!( + instance + .verify(&changed_map, instance.root, &proof) + .is_err() + ); + } +} + #[test] fn changed_virtual_statements_are_rejected() { let mut instance = Instance::new(); @@ -284,7 +325,7 @@ fn virtual_witness_lengths_and_setup_must_match_the_statement() { BitZVerifier::new(params, WINDOW).verify_virtual( &statement, &instance.pcs, - instance.root, + instance.data.commitment(), verifier_transcript(&Proof::default()), ), Err(VerifyError::ParameterMismatch) @@ -365,7 +406,7 @@ fn sha256_virtual_inner_product_opens_the_committed_bits() { .sum(); let claim = LinearClaim::new(¶ms, rows, columns, target).unwrap(); let statement = VirtualStatement::new(params, committed_shape, &map, &claim).unwrap(); - let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let pcs = Pcs::new(&committed_shape, common::SecurityLevel::Bits100).unwrap(); let mut transcript = prover_transcript(); let (root, data) = pcs.commit(&committed_bits, &mut transcript).unwrap(); BitZProver::new(params, WINDOW) @@ -380,12 +421,10 @@ fn sha256_virtual_inner_product_opens_the_committed_bits() { &mut transcript, ) .unwrap(); + let proof = transcript.finish(); + let mut transcript = verifier_transcript(&proof); + let commitment = pcs.receive_commitment(root, &mut transcript).unwrap(); BitZVerifier::new(params, WINDOW) - .verify_virtual( - &statement, - &pcs, - root, - verifier_transcript(&transcript.finish()), - ) + .verify_virtual(&statement, &pcs, &commitment, transcript) .unwrap(); } diff --git a/crates/transcript/Cargo.toml b/crates/transcript/Cargo.toml index ab3f2373..a3ad0bed 100644 --- a/crates/transcript/Cargo.toml +++ b/crates/transcript/Cargo.toml @@ -6,5 +6,6 @@ rust-version.workspace = true license.workspace = true [dependencies] +blake3 = { workspace = true } field = { workspace = true, features = ["spongefish"] } spongefish = { workspace = true } diff --git a/crates/transcript/src/lib.rs b/crates/transcript/src/lib.rs index eb9b981d..72c639f1 100644 --- a/crates/transcript/src/lib.rs +++ b/crates/transcript/src/lib.rs @@ -29,6 +29,7 @@ mod bytes; mod challenge; mod domain; +pub mod pow; mod proof; mod prover; mod verifier; diff --git a/crates/transcript/src/pow.rs b/crates/transcript/src/pow.rs new file mode 100644 index 00000000..5142db76 --- /dev/null +++ b/crates/transcript/src/pow.rs @@ -0,0 +1,134 @@ +//! Shared nonce search and explicit transcript grinding boundaries. + +use crate::PublicTranscript; + +const POW_HASH_TAG: &[u8] = b"bitz-pcs-pow-v1"; +const POW_TRANSCRIPT_TAG: &[u8] = b"bitz-transcript-pow-v1"; + +/// The largest supported grinding difficulty. +pub const MAX_GRINDING_BITS: u32 = 32; + +pub(crate) fn absorb_header(transcript: &mut impl PublicTranscript, label: &[u8], bits: u32) { + transcript.public_message(POW_TRANSCRIPT_TAG); + transcript.public_message(&(label.len() as u64)); + transcript.public_message(label); + transcript.public_message(&bits); +} + +/// Returns the first valid nonce in ascending order. +/// +/// The caller supplies transcript framing. Difficulties above 32 panic. +pub fn find(seed: &[u8; 16], bits: u32) -> u64 { + assert!( + bits <= MAX_GRINDING_BITS, + "grinding difficulty exceeds 32 bits" + ); + let mut nonce = 0u64; + loop { + if valid(seed, nonce, bits) { + return nonce; + } + nonce = nonce.checked_add(1).expect("proof-of-work nonce exhausted"); + } +} + +/// Checks the hash difficulty. Zero difficulty accepts only nonce zero. +/// +/// Difficulties above 32 return false. +pub fn valid(seed: &[u8; 16], nonce: u64, bits: u32) -> bool { + if bits > MAX_GRINDING_BITS { + return false; + } + if bits == 0 { + return nonce == 0; + } + let mut hasher = blake3::Hasher::new(); + hasher.update(POW_HASH_TAG); + hasher.update(seed); + hasher.update(&nonce.to_le_bytes()); + let digest = hasher.finalize(); + // The 32-bit cap makes the first four hash bytes sufficient. + u32::from_be_bytes(digest.as_bytes()[..4].try_into().unwrap()).leading_zeros() >= bits +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{build_prover, build_verifier}; + use field::F128; + + const SESSION: &[u8] = b"grinding-test"; + const INSTANCE: &[u8] = b"instance"; + const LABEL: &[u8] = b"test/cubic/v1"; + + #[test] + fn zero_grinding_leaves_the_transcript_unchanged() { + let mut unmodified = build_prover(SESSION, INSTANCE); + let expected = unmodified.verifier_message::(); + let unmodified_proof = unmodified.finish(); + + let mut prover = build_prover(SESSION, INSTANCE); + prover.grind(LABEL, 0); + assert_eq!(prover.verifier_message::(), expected); + let proof = prover.finish(); + assert_eq!(proof.narg_string, unmodified_proof.narg_string); + + let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + verifier.grind(LABEL, 0).unwrap(); + assert_eq!(verifier.verifier_message::(), expected); + verifier.check_eof().unwrap(); + assert_eq!(find(&[0; 16], 0), 0); + assert!(valid(&[0; 16], 0, 0)); + assert!(!valid(&[0; 16], 1, 0)); + } + + #[test] + fn grinding_replays_and_rejects_an_invalid_nonce() { + const BITS: u32 = 8; + let mut prover = build_prover(SESSION, INSTANCE); + prover.grind(LABEL, BITS); + let expected = prover.verifier_message::(); + let mut proof = prover.finish(); + assert_eq!(proof.narg_string.len(), 8); + + let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + verifier.grind(LABEL, BITS).unwrap(); + assert_eq!(verifier.verifier_message::(), expected); + verifier.check_eof().unwrap(); + + let mut seed_transcript = build_prover(SESSION, INSTANCE); + absorb_header(&mut seed_transcript, LABEL, BITS); + let seed = seed_transcript.verifier_message::().to_bytes(); + let invalid = (0..).find(|&nonce| !valid(&seed, nonce, BITS)).unwrap(); + proof.narg_string.copy_from_slice(&invalid.to_le_bytes()); + let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + assert!(verifier.grind(LABEL, BITS).is_err()); + } + + #[test] + fn grinding_binds_label_difficulty_and_history() { + fn seed(label: &[u8], bits: u32, message: &[u8]) -> F128 { + let mut prover = build_prover(SESSION, INSTANCE); + prover.public_message(message); + absorb_header(&mut prover, label, bits); + prover.verifier_message() + } + let expected = seed(LABEL, 8, b"first"); + assert_ne!(seed(b"test/affine/v1", 8, b"first"), expected); + assert_ne!(seed(LABEL, 9, b"first"), expected); + assert_ne!(seed(LABEL, 8, b"second"), expected); + } + + #[test] + fn verifier_rejects_excessive_difficulty_and_truncated_nonces() { + let mut prover = build_prover(SESSION, INSTANCE); + prover.grind(LABEL, 2); + let mut proof = prover.finish(); + let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + assert!(verifier.grind(LABEL, MAX_GRINDING_BITS + 1).is_err()); + assert!(!valid(&[0; 16], 0, MAX_GRINDING_BITS + 1)); + proof.narg_string.pop(); + let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + assert!(verifier.grind(LABEL, 2).is_err()); + } +} diff --git a/crates/transcript/src/prover.rs b/crates/transcript/src/prover.rs index 9b1035fe..70a38c70 100644 --- a/crates/transcript/src/prover.rs +++ b/crates/transcript/src/prover.rs @@ -23,6 +23,23 @@ impl PublicTranscript for ProverState { } impl ProverState { + /// Adds a labeled grinding boundary before the next challenge block. + /// + /// Zero difficulty leaves the transcript unchanged. Difficulties above 32 panic. + pub fn grind(&mut self, label: &[u8], bits: u32) { + assert!( + bits <= crate::pow::MAX_GRINDING_BITS, + "grinding difficulty exceeds 32 bits" + ); + if bits == 0 { + return; + } + crate::pow::absorb_header(self, label, bits); + let seed = self.verifier_message::().to_bytes(); + let nonce = crate::pow::find(&seed, bits); + self.prover_message(&nonce.to_le_bytes()); + } + /// Absorbs a message both parties already know; nothing is written. pub fn public_message + ?Sized>(&mut self, message: &T) { self.inner.public_message(message); diff --git a/crates/transcript/src/verifier.rs b/crates/transcript/src/verifier.rs index 0ba3e23f..be160551 100644 --- a/crates/transcript/src/verifier.rs +++ b/crates/transcript/src/verifier.rs @@ -23,6 +23,24 @@ impl PublicTranscript for VerifierState<'_> { } impl VerifierState<'_> { + /// Checks a labeled grinding boundary before the next challenge block. + /// + /// Zero difficulty leaves the transcript unchanged. Difficulties above 32 fail. + pub fn grind(&mut self, label: &[u8], bits: u32) -> VerificationResult<()> { + if bits > crate::pow::MAX_GRINDING_BITS { + return Err(VerificationError); + } + if bits == 0 { + return Ok(()); + } + crate::pow::absorb_header(self, label, bits); + let seed = self.verifier_message::().to_bytes(); + let nonce = u64::from_le_bytes(self.prover_message::<[u8; 8]>()?); + crate::pow::valid(&seed, nonce, bits) + .then_some(()) + .ok_or(VerificationError) + } + /// Absorbs a message both parties already know; nothing is read. pub fn public_message + ?Sized>(&mut self, message: &T) { self.inner.public_message(message); diff --git a/crates/transcript/tests/hints.rs b/crates/transcript/tests/hints.rs index 081ff1cd..af5db4fc 100644 --- a/crates/transcript/tests/hints.rs +++ b/crates/transcript/tests/hints.rs @@ -111,13 +111,13 @@ fn bounded_prover_message_bytes_preserve_the_transcript() { let challenge = prover.verifier_message::(); let proof = prover.finish(); - let mut legacy = build_prover(SESSION, INSTANCE); - legacy.prover_message(&(bytes.len() as u32)); + let mut reference = build_prover(SESSION, INSTANCE); + reference.prover_message(&(bytes.len() as u32)); for &byte in bytes { - legacy.prover_message(&[byte]); + reference.prover_message(&[byte]); } - assert_eq!(legacy.verifier_message::(), challenge); - assert_eq!(legacy.finish().narg_string, proof.narg_string); + assert_eq!(reference.verifier_message::(), challenge); + assert_eq!(reference.finish().narg_string, proof.narg_string); let mut verifier = build_verifier(SESSION, INSTANCE, &proof); assert_eq!(verifier.prover_message_bytes::<11>().unwrap(), bytes); diff --git a/crates/verifier/src/fold.rs b/crates/verifier/src/fold.rs index 4c23e0b9..a3e2ae48 100644 --- a/crates/verifier/src/fold.rs +++ b/crates/verifier/src/fold.rs @@ -1,7 +1,7 @@ //! The fold round: read the column folds, check them, then take the //! challenge. -use common::{Fold, FoldError, LinearClaim, column_images, reconstruct, row_images}; +use common::{Fold, FoldError, LinearClaim, SecurityLevel, column_images, reconstruct, row_images}; use crate::BitZVerifier; use transcript::VerifierState; @@ -38,6 +38,7 @@ impl BitZVerifier { &self, claim: &LinearClaim>, transcript: &mut VerifierState<'_>, + security: SecurityLevel, ) -> Result { let shape = self.params().shape(); @@ -59,6 +60,12 @@ impl BitZVerifier { let images = column_images(self.comb(), &folds); let row_images = row_images(self.comb(), &claim.row_exponents()); + transcript + .grind( + b"bitz/columns/v1", + security.grinding_bits(shape.log_columns()), + ) + .map_err(|_| ReceiveError::MalformedProof)?; let zeta = (0..shape.log_columns()) .map(|_| transcript.verifier_message()) .collect(); diff --git a/crates/verifier/src/reduce.rs b/crates/verifier/src/reduce.rs index eec8a599..d7081ef7 100644 --- a/crates/verifier/src/reduce.rs +++ b/crates/verifier/src/reduce.rs @@ -7,7 +7,7 @@ //! column coordinates first; `alfa_b` contains the remaining row coordinates. //! The caller must verify the returned claim against the commitment. -use common::{ClaimError, Fold, LinearClaim, OpeningQuery, Shape}; +use common::{ClaimError, Fold, LinearClaim, OpeningQuery, SecurityLevel, Shape}; use field::F128; use num_traits::ConstOne; use transcript::VerifierState; @@ -25,12 +25,13 @@ pub(crate) fn gkr_reduce( transcript: &mut VerifierState, fold: &Fold, shape: &Shape, + security: SecurityLevel, ) -> Result { // Each layer halves the row count, leaving one product per column. let r1 = fold.row_images.len().max(1).ilog2(); let (point, mle_leaf_claim) = - gkr::gpgkr_verify(transcript, fold.e0, &fold.zeta, r1).ok_or(ReduceError::GKR)?; + gkr::gpgkr_verify(transcript, fold.e0, &fold.zeta, r1, security).ok_or(ReduceError::GKR)?; let r2 = point.len() - r1 as usize; // Columns occupy the low index bits of the GKR leaf table. @@ -119,8 +120,13 @@ mod round_trip_ai_test { let fold = Fold::new(&shape, folds, top_layer, row_images.clone(), zeta.clone()).unwrap(); let mut prover = transcript::build_prover("verifier-round-trip", &F128::ZERO); - let (mut point, claim) = - gpgkr_prove(&mut prover, table.shape().log_bits(), &zeta, witnesses); + let (mut point, claim) = gpgkr_prove( + &mut prover, + table.shape().log_bits(), + &zeta, + witnesses, + SecurityLevel::Bits100, + ); let proof = prover.finish(); // Derive the expected factors from the prover's terminal point. @@ -135,7 +141,7 @@ mod round_trip_ai_test { let expected_inner_product_claim = claim - F128::ONE; let mut verifier = transcript::build_verifier("verifier-round-trip", &F128::ZERO, &proof); - let query = gkr_reduce(&mut verifier, &fold, &shape).unwrap(); + let query = gkr_reduce(&mut verifier, &fold, &shape, SecurityLevel::Bits100).unwrap(); verifier.check_eof().unwrap(); let expected = LinearClaim::from_shape( &shape, diff --git a/crates/verifier/src/verify.rs b/crates/verifier/src/verify.rs index 0510f1bb..0e944f15 100644 --- a/crates/verifier/src/verify.rs +++ b/crates/verifier/src/verify.rs @@ -1,6 +1,8 @@ //! `VerifyBitZ`. -use common::{LinearClaim, OpeningQuery, Root, VirtualMap, VirtualMapError, VirtualStatement}; +use common::{ + LinearClaim, OpeningQuery, SecurityLevel, VirtualMap, VirtualMapError, VirtualStatement, +}; use field::Fq; use pcs::{CommitScheme, Commitment, Pcs, StatementBinding, VerifyError as OpeningVerifyError}; use transcript::VerifierState; @@ -10,7 +12,7 @@ use crate::{BitZVerifier, ReceiveError, ReduceError, reduce::gkr_reduce}; /// A proof the verifier rejects. #[derive(Debug, Clone, PartialEq, Eq)] pub enum VerifyError { - /// The setup or PCS bit count differs from the virtual parameters. + /// The setup or PCS bit count differs from the statement parameters. ParameterMismatch, /// The reduced claim cannot be transposed onto the committed bits. VirtualMap(VirtualMapError), @@ -25,39 +27,6 @@ pub enum VerifyError { } impl BitZVerifier { - /// Receives the commitment's OOD claim and verifies the virtual BitZ proof. - pub fn verify_virtual( - &self, - statement: &VirtualStatement<'_, Q, impl VirtualMap>, - pcs: &Pcs, - root: Root, - mut transcript: VerifierState<'_>, - ) -> Result<(), VerifyError> { - if self.params() != statement.params().claim() - || pcs.bit_len() != 1 << statement.params().committed_shape().log_bits() - { - return Err(VerifyError::ParameterMismatch); - } - let commitment = pcs - .receive_commitment(root, &mut transcript) - .map_err(VerifyError::Opening)?; - self.verify_virtual_with_commitment(statement, pcs, &commitment, transcript) - } - - /// Receives the commitment's OOD claim and verifies the BitZ proof. - pub fn verify( - &self, - claim: &LinearClaim>, - pcs: &Pcs, - root: Root, - mut transcript: VerifierState<'_>, - ) -> Result<(), VerifyError> { - let commitment = pcs - .receive_commitment(root, &mut transcript) - .map_err(VerifyError::Opening)?; - self.verify_with_commitment(claim, pcs, &commitment, transcript) - } - /// Verifies a claim on `h = M (1 || f)` against the commitment to `f`. /// /// Build the setup from `statement.params().claim()` and match the commitment's @@ -65,12 +34,11 @@ impl BitZVerifier { /// padded virtual bits. Supply the public circuit's map; its digest must cover /// its shape and entries. /// - /// Continue the transcript that produced `commitment` through - /// [`Pcs::receive_commitment`], using the prover's public-input events. + /// Call `Pcs::receive_commitment` before witness-dependent challenges and continue its transcript. /// This method binds the inputs in [`VirtualStatement`], transposes the reduced /// claim, verifies the PCS opening, and rejects trailing proof or hint bytes. #[tracing::instrument(name = "Verify virtual BitZ", skip_all)] - pub fn verify_virtual_with_commitment( + pub fn verify_virtual( &self, statement: &VirtualStatement<'_, Q, impl VirtualMap>, pcs: &Pcs, @@ -89,7 +57,8 @@ impl BitZVerifier { transcript.public_message(params); transcript.public_message(&statement.map().digest()); transcript.public_message(claim); - let query = self.fold_and_reduce(claim, &mut transcript)?; + transcript.public_message(pcs); + let query = self.fold_and_reduce(claim, &mut transcript, pcs.security_level())?; let query = statement .transpose_query(query) .map_err(VerifyError::VirtualMap)?; @@ -102,25 +71,30 @@ impl BitZVerifier { /// Replays the proof of the caller's linear claim about the committed bits. /// - /// `pcs` must be the scheme the commitment was made under. Continue the - /// transcript used by [`Pcs::receive_commitment`]; this consumes it and checks EOF. + /// Call `Pcs::receive_commitment` before witness-dependent challenges and continue its transcript. + /// `pcs` must match the commitment parameters. This method consumes the transcript and checks EOF. #[tracing::instrument(name = "Verify BitZ", skip_all)] - pub fn verify_with_commitment( + pub fn verify( &self, claim: &LinearClaim>, pcs: &Pcs, commitment: &Commitment, mut transcript: VerifierState<'_>, ) -> Result<(), VerifyError> { + if pcs.bit_len() != 1 << self.params().shape().log_bits() { + return Err(VerifyError::ParameterMismatch); + } // Step 1: the admissibility and precondition checks have already run -- // the shape gates in Shape::new, the modulus in Fq's own const assertions, // the generator's order in BitZParams::new and the weight counts in // LinearClaim::new. What is left is binding, before any challenge. transcript.public_message(&commitment.root().0); transcript.public_message(self.params()); + transcript.public_message(pcs); + transcript.public_message(claim); // Steps 3 and 4: check integer folds and replay GKR to obtain a bit claim. - let query = self.fold_and_reduce(claim, &mut transcript)?; + let query = self.fold_and_reduce(claim, &mut transcript, pcs.security_level())?; // Step 6: verify the inner-product sumcheck, ring switch, and opening. // Acceptance requires authenticating GKR's terminal claim against the commitment. @@ -142,16 +116,18 @@ impl BitZVerifier { &self, claim: &LinearClaim>, transcript: &mut VerifierState<'_>, + security: SecurityLevel, ) -> Result { // Step 2 is absent: Q is fixed, and BitZParams::new checks its fold bound. // Step 3: read the folds, range-check them, reconstruct against mu. let fold = self - .receive_fold(claim, transcript) + .receive_fold(claim, transcript, security) .map_err(VerifyError::Fold)?; // Step 4: replay GKR from fold.e0 at fold.zeta to obtain the bit claim. // Step 5 needs no separate batching: fold.zeta already batches the columns. - gkr_reduce(transcript, &fold, self.params().shape()).map_err(VerifyError::Reduction) + gkr_reduce(transcript, &fold, self.params().shape(), security) + .map_err(VerifyError::Reduction) } } diff --git a/single_threaded.patch b/single_threaded.patch index 012e19f1..1f318a5d 100644 --- a/single_threaded.patch +++ b/single_threaded.patch @@ -19,10 +19,11 @@ index 89a1bf0..bb364ff 100644 .for_each(|(((l_lo, r_lo), &l_hi), &r_hi)| { // Recalculating delta rather than writing it in the top half of the array in the previous loop to save on writes. // No benchmarking has been done to check the difference. -@@ -161,16 +161,16 @@ fn reduce_round( +@@ -161,17 +161,17 @@ fn reduce_round( hi_r: &[Field], eq: &[Field], ) -> (Wide256, Wide256) { + // chunking reduces variance in benchmarking - lo_l.par_chunks_exact(2) - .zip(lo_r.par_chunks_exact(2)) - .zip(hi_l.par_chunks_exact(2)) diff --git a/tooling/cli/README.md b/tooling/cli/README.md index bfbe3a44..1c285468 100644 --- a/tooling/cli/README.md +++ b/tooling/cli/README.md @@ -6,6 +6,12 @@ Run an end-to-end proof with fresh random inputs: cargo run -p bitz-cli --release -- circuit-e2e --circuit sha256-chain --num-blocks 8 --threads 1 ``` +Select the PCS round budget with `--pcs-security-bits 100` or `128`; the default is `100`. +The implementation derives all internal parameters from the padded witness size. +The `100` target uses list decoding and an initial out-of-domain (OOD) check. +The `128` target uses unique decoding and omits that check. +Spartan still uses `Q100`; this option does not set Spartan security. + `--circuit` selects one of these compiled-in adapters: - `sha256-compression`: one raw block; optional `--initial-state` accepts eight words as 64 hexadecimal digits. diff --git a/tooling/cli/benches/circuits.rs b/tooling/cli/benches/circuits.rs index 13fec1c1..514d080e 100644 --- a/tooling/cli/benches/circuits.rs +++ b/tooling/cli/benches/circuits.rs @@ -6,6 +6,7 @@ use bitz_cli::{ end_to_end::CircuitProofSystem, }; use divan::Bencher; +use pcs::SecurityLevel; fn main() { divan::main(); @@ -19,21 +20,28 @@ fn instance(circuit: BuiltinCircuit) -> (CircuitInstance, Vec) { fn setup(circuit: BuiltinCircuit) -> (CircuitProofSystem, Vec) { let (statement, inputs) = instance(circuit); - (CircuitProofSystem::new(statement).unwrap(), inputs) + ( + CircuitProofSystem::new(statement, SecurityLevel::Bits100).unwrap(), + inputs, + ) } #[divan::bench(args = BuiltinCircuit::ALL)] fn end_to_end(bencher: Bencher, circuit: BuiltinCircuit) { bencher .with_inputs(|| instance(circuit)) - .bench_local_values(|(statement, inputs)| benchmark::run(statement, &inputs).unwrap()); + .bench_local_values(|(statement, inputs)| { + benchmark::run(statement, &inputs, SecurityLevel::Bits100).unwrap() + }); } #[divan::bench(args = BuiltinCircuit::ALL)] fn circuit_setup(bencher: Bencher, circuit: BuiltinCircuit) { bencher .with_inputs(|| CircuitInstance::random(circuit, None, None).unwrap()) - .bench_local_values(|statement| CircuitProofSystem::new(statement).unwrap()); + .bench_local_values(|statement| { + CircuitProofSystem::new(statement, SecurityLevel::Bits100).unwrap() + }); } #[divan::bench(args = BuiltinCircuit::ALL)] diff --git a/tooling/cli/src/benchmark.rs b/tooling/cli/src/benchmark.rs index cbd8f6a9..adf764fe 100644 --- a/tooling/cli/src/benchmark.rs +++ b/tooling/cli/src/benchmark.rs @@ -1,6 +1,7 @@ //! Shared execution and timing for circuit proof benchmarks. use crate::end_to_end::{CircuitProofSystem, CircuitStatement, CircuitStats, Error}; +use pcs::SecurityLevel; use std::{ fmt, time::{Duration, Instant}, @@ -18,13 +19,22 @@ pub struct Timings { /// Runs setup, witness generation, commitment, proving, and verification. /// Callers generate inputs and initialize worker threads before calling this. -pub fn run(statement: S, inputs: &[bool]) -> Result { +pub fn run( + statement: S, + inputs: &[bool], + pcs_security: SecurityLevel, +) -> Result { let started = Instant::now(); - let prepared = CircuitProofSystem::new(statement)?; + let prepared = CircuitProofSystem::new(statement, pcs_security)?; let setup = started.elapsed(); let circuit = prepared.stats(); tracing::info!( opening_path = ?circuit.opening_path, + pcs_round_target_bits = circuit.pcs_security.bits(), + pcs_decoding = match circuit.pcs_security { + SecurityLevel::Bits100 => "list", + SecurityLevel::Bits128 => "unique", + }, constraints = circuit.constraints, assignment_bits = circuit.assignment_bits, committed_bits = circuit.committed_bits, @@ -57,8 +67,9 @@ impl fmt::Display for Timings { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "opening_path={:?} constraints={} assignment_bits={} committed_bits={} padded_committed_bits={} setup_ms={:.3} witness_ms={:.3} commit_ms={:.3} prove_ms={:.3} total_prove_ms={:.3} verify_ms={:.3}", + "opening_path={:?} pcs_round_target_bits={} constraints={} assignment_bits={} committed_bits={} padded_committed_bits={} setup_ms={:.3} witness_ms={:.3} commit_ms={:.3} prove_ms={:.3} total_prove_ms={:.3} verify_ms={:.3}", self.circuit.opening_path, + self.circuit.pcs_security.bits(), self.circuit.constraints, self.circuit.assignment_bits, self.circuit.committed_bits, diff --git a/tooling/cli/src/cmd/circuit_e2e.rs b/tooling/cli/src/cmd/circuit_e2e.rs index c56ad04b..82ff3457 100644 --- a/tooling/cli/src/cmd/circuit_e2e.rs +++ b/tooling/cli/src/cmd/circuit_e2e.rs @@ -6,6 +6,7 @@ use { benchmark, circuits::{BuiltinCircuit, CircuitInstance}, }, + pcs::SecurityLevel, }; /// Prove generated circuit constraints over Q100 and verify the proof. @@ -28,6 +29,14 @@ pub struct Args { /// positive Rayon worker count (default available parallelism) #[argh(option)] threads: Option, + + /// classical PCS round budget: 100 (default) or 128; Spartan remains over Q100 + #[argh( + option, + default = "SecurityLevel::Bits100", + from_str_fn(parse_pcs_security) + )] + pcs_security_bits: SecurityLevel, } impl Command for Args { @@ -46,22 +55,30 @@ impl Command for Args { circuit = %self.circuit, threads = rayon::current_num_threads(), field = "Q100", - pcs = "Fast", + pcs_round_target_bits = self.pcs_security_bits.bits(), hash = "Blake3", ).entered(); let statement = tracing::info_span!("generate_inputs").in_scope(|| { CircuitInstance::random(self.circuit, self.num_blocks, self.initial_state) })?; let inputs = statement.inputs.clone(); - let timings = benchmark::run(statement, &inputs)?; + let timings = benchmark::run(statement, &inputs, self.pcs_security_bits)?; tracing::info!("Proof verified successfully"); - println!("circuit={} threads={} field=Q100 pcs=Fast hash=Blake3 relation=Q100-r1cs constraints_verified=true", self.circuit, rayon::current_num_threads()); + println!("circuit={} threads={} field=Q100 pcs_round_target_bits={} security_model=classical-pcs-round-budget relation=Q100-r1cs constraints_verified=true", self.circuit, rayon::current_num_threads(), self.pcs_security_bits.bits()); println!("{timings}"); Ok(()) }) } } +fn parse_pcs_security(value: &str) -> Result { + match value { + "100" => Ok(SecurityLevel::Bits100), + "128" => Ok(SecurityLevel::Bits128), + _ => Err("expected a PCS round budget of 100 or 128 bits".into()), + } +} + fn parse_state(hex: &str) -> Result<[u32; 8], String> { if hex.len() != 64 || !hex.bytes().all(|c| c.is_ascii_hexdigit()) { return Err("expected 64 hexadecimal digits".into()); @@ -72,3 +89,33 @@ fn parse_state(hex: &str) -> Result<[u32; 8], String> { } Ok(words) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pcs_budget_defaults_to_100_and_accepts_only_supported_values() { + let args = Args::from_args(&["circuit-e2e"], &["--circuit", "sha256-compression"]).unwrap(); + assert_eq!(args.pcs_security_bits, SecurityLevel::Bits100); + for (value, expected) in [ + ("100", SecurityLevel::Bits100), + ("128", SecurityLevel::Bits128), + ] { + let args = Args::from_args( + &["circuit-e2e"], + &[ + "--circuit", + "sha256-compression", + "--pcs-security-bits", + value, + ], + ) + .unwrap(); + assert_eq!(args.pcs_security_bits, expected); + } + for value in ["0", "99", "120", "129", "invalid"] { + assert!(parse_pcs_security(value).is_err()); + } + } +} diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index 9e4c9d96..d326e49d 100644 --- a/tooling/cli/src/end_to_end.rs +++ b/tooling/cli/src/end_to_end.rs @@ -12,7 +12,7 @@ use common::{ }; use field::{F128, FqDefault, Q100, gf128::smallest_generator}; use num_traits::{ConstOne, ConstZero}; -use pcs::{CommitScheme, HashKind, LigeritoProfile, Pcs, ProverData, StatementBinding}; +use pcs::{CommitScheme, Pcs, ProverData, SecurityLevel, StatementBinding}; use poly::{DenseMultilinearExtension, ScaledMleEvaluationClaim}; use prover::{BitZProver, VirtualWitness}; use transcript::{ProverState, PublicTranscript, build_prover, build_verifier}; @@ -50,8 +50,8 @@ pub enum Error { Spartan(spartan::SpartanError), #[error("commitment failed: {0:?}")] Commit(pcs::CommitError), - #[error("OOD commitment binding verification failed: {0:?}")] - OodVerify(pcs::VerifyError), + #[error("commitment verification failed: {0:?}")] + CommitmentVerify(pcs::VerifyError), #[error("constant-one opening failed: {0:?}")] ConstantProve(pcs::ProveError), #[error("constant-one verification failed: {0:?}")] @@ -72,6 +72,8 @@ pub enum OpeningPath { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CircuitStats { pub opening_path: OpeningPath, + /// Classical PCS round budget; this does not describe Spartan security. + pub pcs_security: SecurityLevel, pub constraints: usize, pub assignment_bits: usize, /// Meaningful committed witness bits before PCS zero padding. @@ -98,7 +100,8 @@ pub struct Witness { products: R1csProductMles, } -/// Commitment data and the transcript that sampled its OOD claim. +/// Retains the commitment and transcript for the remaining proof stages. +/// The transcript includes the initial OOD check when the selected policy requires it. pub struct CommittedWitness { data: ProverData, transcript: ProverState, @@ -112,8 +115,9 @@ pub struct Proof { } impl CircuitProofSystem { + /// Selects the classical PCS round budget. Spartan still uses Q100. #[tracing::instrument(name = "setup", skip_all)] - pub fn new(statement: S) -> Result { + pub fn new(statement: S, pcs_security: SecurityLevel) -> Result { let mut constraints = ConstraintGenerator::new(statement.input_bits()); let inputs: Vec<_> = (0..statement.input_bits()) .map(|i| constraints.input(i)) @@ -147,7 +151,7 @@ impl CircuitProofSystem { }; let params = BitZParams::new(claim_shape, smallest_generator()) .map_err(|_| Error::Configuration("inadmissible BitZ parameters"))?; - let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3) + let pcs = Pcs::new(&committed_shape, pcs_security) .map_err(|_| Error::Configuration("unsupported PCS shape"))?; Ok(Self { statement, @@ -163,6 +167,7 @@ impl CircuitProofSystem { pub fn stats(&self) -> CircuitStats { CircuitStats { opening_path: self.opening_path, + pcs_security: self.pcs.security_level(), constraints: self.matrices.matrices().a.row_count(), assignment_bits: self.map.h_len(), committed_bits: match self.opening_path { @@ -209,8 +214,8 @@ impl CircuitProofSystem { }) } - /// Commits and sends the initial OOD evaluation before any PIOP challenge. - /// The returned state retains both PCS data and the transcript for proving. + /// Commits and runs the selected policy's initial checks before any PIOP challenge. + /// The returned state retains the transcript, codeword, and Merkle tree for proving. #[tracing::instrument(name = "commit", skip_all)] pub fn commit(&self, witness: &Witness) -> Result { let mut transcript = build_prover(SESSION, self.statement.domain()); @@ -284,7 +289,7 @@ impl CircuitProofSystem { let commitment = self .pcs .receive_commitment(proof.root, &mut transcript) - .map_err(Error::OodVerify)?; + .map_err(Error::CommitmentVerify)?; self.bind(&mut transcript, proof.root); if self.opening_path == OpeningPath::Direct { self.pcs @@ -301,19 +306,12 @@ impl CircuitProofSystem { let claim = opening_claim(&self.params, &terminal)?; let verifier = BitZVerifier::new(self.params, WINDOW); match self.opening_path { - OpeningPath::Direct => { - verifier.verify_with_commitment(&claim, &self.pcs, &commitment, transcript) - } + OpeningPath::Direct => verifier.verify(&claim, &self.pcs, &commitment, transcript), OpeningPath::Virtual => { let statement = VirtualStatement::new(self.params, self.committed_shape, &self.map, &claim) .map_err(|_| Error::Configuration("invalid virtual statement"))?; - verifier.verify_virtual_with_commitment( - &statement, - &self.pcs, - &commitment, - transcript, - ) + verifier.verify_virtual(&statement, &self.pcs, &commitment, transcript) } } .map_err(Error::Verify) @@ -434,7 +432,7 @@ mod tests { #[test] fn commitment_sends_ood_before_proving() { - let system = CircuitProofSystem::new(IdentityBit).unwrap(); + let system = CircuitProofSystem::new(IdentityBit, SecurityLevel::Bits100).unwrap(); let witness = system.witness(&[true]).unwrap(); let committed = system.commit(&witness).unwrap(); let proof = committed.transcript.finish(); @@ -450,11 +448,18 @@ mod tests { #[test] fn direct_opening_requires_constant_one_on_both_sides() { - let mut system = CircuitProofSystem::new(IdentityBit).unwrap(); + let mut system = CircuitProofSystem::new(IdentityBit, SecurityLevel::Bits100).unwrap(); let witness = system.witness(&[true]).unwrap(); let data = system.commit(&witness).unwrap(); let mut proof = system.prove(witness, data).unwrap(); system.verify(&proof).unwrap(); + // The initial OOD value must arrive before constant checks or Spartan challenges. + let mut truncated_commitment = proof.clone(); + truncated_commitment.opening.narg_string.truncate(15); + assert!(matches!( + system.verify(&truncated_commitment), + Err(Error::CommitmentVerify(_)) + )); system.opening_path = OpeningPath::Virtual; assert!(system.verify(&proof).is_err()); system.opening_path = OpeningPath::Direct; diff --git a/tooling/cli/tests/circuits.rs b/tooling/cli/tests/circuits.rs index bb0f5153..dc1f93e9 100644 --- a/tooling/cli/tests/circuits.rs +++ b/tooling/cli/tests/circuits.rs @@ -8,6 +8,7 @@ use circuit::{ }; use num_bigint::BigInt; use num_traits::{Signed, ToPrimitive}; +use pcs::SecurityLevel; #[test] fn sha_constraint_residuals_cannot_wrap_modulo_q100() { @@ -51,7 +52,7 @@ fn supported_sha_circuits_prove_and_verify() { for circuit in BuiltinCircuit::ALL { let statement = CircuitInstance::random(circuit, None, None).unwrap(); let inputs = statement.inputs.clone(); - let system = CircuitProofSystem::new(statement).unwrap(); + let system = CircuitProofSystem::new(statement, SecurityLevel::Bits100).unwrap(); let witness = system.witness(&inputs).unwrap(); let data = system.commit(&witness).unwrap(); let proof = system.prove(witness, data).unwrap(); @@ -74,17 +75,17 @@ fn sha_compression_matches_abc_and_binds_public_values() { inputs: inputs.clone(), output: bits(&ABC_DIGEST), }; - let system = CircuitProofSystem::new(statement.clone()).unwrap(); + let system = CircuitProofSystem::new(statement.clone(), SecurityLevel::Bits100).unwrap(); let witness = system.witness(&inputs).unwrap(); let data = system.commit(&witness).unwrap(); let proof = system.prove(witness, data).unwrap(); - CircuitProofSystem::new(statement.clone()) + CircuitProofSystem::new(statement.clone(), SecurityLevel::Bits100) .unwrap() .verify(&proof) .unwrap(); let mut changed = statement.clone(); changed.output[0] ^= true; - let wrong_output = CircuitProofSystem::new(changed).unwrap(); + let wrong_output = CircuitProofSystem::new(changed, SecurityLevel::Bits100).unwrap(); assert!(wrong_output.verify(&proof).is_err()); assert!(matches!( wrong_output.witness(&inputs), @@ -93,7 +94,7 @@ fn sha_compression_matches_abc_and_binds_public_values() { let mut changed = statement; changed.inputs[0] ^= true; assert!( - CircuitProofSystem::new(changed) + CircuitProofSystem::new(changed, SecurityLevel::Bits100) .unwrap() .verify(&proof) .is_err() @@ -110,7 +111,7 @@ fn variable_lengths_custom_state_and_invalid_dimensions() { ] { let statement = CircuitInstance::random(circuit, count, state).unwrap(); let inputs = statement.inputs.clone(); - let system = CircuitProofSystem::new(statement).unwrap(); + let system = CircuitProofSystem::new(statement, SecurityLevel::Bits100).unwrap(); system.witness(&inputs).unwrap(); } for (circuit, count) in [ @@ -127,5 +128,5 @@ fn variable_lengths_custom_state_and_invalid_dimensions() { let mut malformed = CircuitInstance::random(BuiltinCircuit::Sha256Compression, None, None).unwrap(); malformed.inputs.pop(); - assert!(CircuitProofSystem::new(malformed).is_err()); + assert!(CircuitProofSystem::new(malformed, SecurityLevel::Bits100).is_err()); } diff --git a/tooling/cli/tests/end_to_end.rs b/tooling/cli/tests/end_to_end.rs index 857f7b80..68d68705 100644 --- a/tooling/cli/tests/end_to_end.rs +++ b/tooling/cli/tests/end_to_end.rs @@ -1,12 +1,12 @@ use bitz_cli::end_to_end::{CircuitProofSystem, CircuitStatement, Error, OpeningPath, Proof}; use circuit::Circuit; -use pcs::VerifyError; +use pcs::{SecurityLevel, VerifyError}; fn rejects_changed_or_missing_ood( system: &CircuitProofSystem, proof: &Proof, ) { - // These Fast-profile fixtures have zero initial grinding bits, so the first + // These 100-bit fixtures have zero initial grinding bits, so the first // 16 transcript bytes encode the OOD evaluation. let mut changed = proof.clone(); changed.opening.narg_string[0] ^= 1; @@ -20,18 +20,18 @@ fn rejects_changed_or_missing_ood( truncated.opening.narg_string.truncate(15); assert!(matches!( system.verify(&truncated), - Err(Error::OodVerify(VerifyError::MalformedProof)) + Err(Error::CommitmentVerify(VerifyError::MalformedProof)) )); } -struct PublicBit; +struct PublicBit(bool); impl CircuitStatement for PublicBit { fn domain(&self) -> &'static [u8] { b"test/public-bit/v1" } fn public_bytes(&self) -> Vec { - vec![1] + vec![u8::from(self.0)] } fn input_bits(&self) -> usize { 1 @@ -39,20 +39,22 @@ impl CircuitStatement for PublicBit { fn synthesize(&self, cs: &mut CS, inputs: &[CS::Bool]) -> Result<(), Error> { let bit = cs.bitz::<1>(inputs[0].clone()); let one = CS::Z::<1>::from(CS::Coefficient::<1>::from(1u64)); - cs.assert_r1c::<1>(one.clone(), bit, one); + let expected = CS::Z::<1>::from(CS::Coefficient::<1>::from(u64::from(self.0))); + cs.assert_r1c::<1>(one, bit, expected); Ok(()) } } #[test] fn generic_driver_accepts_a_non_sha_circuit() { - let prepared = CircuitProofSystem::new(PublicBit).unwrap(); + let prepared = CircuitProofSystem::new(PublicBit(true), SecurityLevel::Bits100).unwrap(); assert_eq!(prepared.stats().opening_path, OpeningPath::Direct); + assert_eq!(prepared.stats().pcs_security, SecurityLevel::Bits100); assert_eq!(prepared.stats().committed_bits, 2); let witness = prepared.witness(&[true]).unwrap(); let data = prepared.commit(&witness).unwrap(); let proof = prepared.prove(witness, data).unwrap(); - CircuitProofSystem::new(PublicBit) + CircuitProofSystem::new(PublicBit(true), SecurityLevel::Bits100) .unwrap() .verify(&proof) .unwrap(); @@ -83,12 +85,14 @@ fn generic_driver_accepts_a_non_sha_circuit() { #[test] fn benchmark_runs_a_generic_circuit_and_propagates_failure() { - let timings = bitz_cli::benchmark::run(PublicBit, &[true]).unwrap(); + let timings = + bitz_cli::benchmark::run(PublicBit(true), &[true], SecurityLevel::Bits100).unwrap(); let output = timings.to_string(); assert!(output.contains("total_prove_ms=")); assert!(output.contains("verify_ms=")); + assert!(output.contains("pcs_round_target_bits=100")); assert!(matches!( - bitz_cli::benchmark::run(PublicBit, &[false]), + bitz_cli::benchmark::run(PublicBit(true), &[false], SecurityLevel::Bits100), Err(Error::Unsatisfied) )); } @@ -118,14 +122,14 @@ impl CircuitStatement for PublicXor { #[test] fn nonidentity_map_uses_virtual_opening_and_checks_xor_relation() { - let system = CircuitProofSystem::new(PublicXor).unwrap(); + let system = CircuitProofSystem::new(PublicXor, SecurityLevel::Bits100).unwrap(); assert_eq!(system.stats().opening_path, OpeningPath::Virtual); assert_eq!(system.stats().assignment_bits, 3); assert_eq!(system.stats().committed_bits, 2); let witness = system.witness(&[true, false]).unwrap(); let data = system.commit(&witness).unwrap(); let proof = system.prove(witness, data).unwrap(); - CircuitProofSystem::new(PublicXor) + CircuitProofSystem::new(PublicXor, SecurityLevel::Bits100) .unwrap() .verify(&proof) .unwrap(); @@ -140,6 +144,37 @@ fn nonidentity_map_uses_virtual_opening_and_checks_xor_relation() { let mut changed = proof; changed.opening.narg_string.push(0); assert!(system.verify(&changed).is_err()); - let timings = bitz_cli::benchmark::run(PublicXor, &[true, false]).unwrap(); - assert_eq!(timings.circuit.opening_path, OpeningPath::Virtual); + for security in [SecurityLevel::Bits100, SecurityLevel::Bits128] { + let timings = bitz_cli::benchmark::run(PublicXor, &[true, false], security).unwrap(); + assert_eq!(timings.circuit.opening_path, OpeningPath::Virtual); + assert_eq!(timings.circuit.pcs_security, security); + assert!( + timings + .to_string() + .contains(&format!("pcs_round_target_bits={}", security.bits())) + ); + } +} + +#[test] +fn explicit_128_budget_binds_the_policy_and_public_statement() { + let system = CircuitProofSystem::new(PublicBit(true), SecurityLevel::Bits128).unwrap(); + assert_eq!(system.stats().pcs_security, SecurityLevel::Bits128); + let witness = system.witness(&[true]).unwrap(); + let data = system.commit(&witness).unwrap(); + let proof = system.prove(witness, data).unwrap(); + system.verify(&proof).unwrap(); + + assert!( + CircuitProofSystem::new(PublicBit(true), SecurityLevel::Bits100) + .unwrap() + .verify(&proof) + .is_err() + ); + assert!( + CircuitProofSystem::new(PublicBit(false), SecurityLevel::Bits128) + .unwrap() + .verify(&proof) + .is_err() + ); }