diff --git a/MULTI_ROUND_GRINDING_PLAN.md b/MULTI_ROUND_GRINDING_PLAN.md new file mode 100644 index 000000000..eb030f851 --- /dev/null +++ b/MULTI_ROUND_GRINDING_PLAN.md @@ -0,0 +1,610 @@ +# Multi-Round Grinding Implementation Plan + +## Overview + +Generalize the current single FRI query grinding to support grinding before any verifier challenge. This allows flexible distribution of proof-of-work across protocol rounds to optimize security vs. prover performance trade-offs. + +## Current State (Commit 1) + +**What we have:** +- ✅ `GrindingSchedule` struct in `air/src/proof/security.rs` with per-round fields (ali, deep, fri_batching, fri_first_intermediate, fri_query) +- ✅ `plan_grinding_schedule_ldr()` - Computes optimal schedule for list-decoding regime +- ✅ `plan_grinding_schedule_udr()` - Computes optimal schedule for unique-decoding regime +- ✅ `ProvenSecurity::compute_with_schedule()` - Security computation with per-round grinding +- ✅ Fixed DEEP epsilon calculation bug (uses `l` not `l²`) +- ✅ All security tests passing + +**Commit message:** +``` +Add multi-round grinding security estimator + +- Add GrindingSchedule struct with per-round grinding fields +- Implement plan_grinding_schedule_ldr/udr for optimal schedule computation +- Add ProvenSecurity::compute_with_schedule for per-round security analysis +- Fix DEEP epsilon calculation in planner helpers (use l not l²) +- Remove deprecated greedy planner in favor of direct planner +- Separate LDR and UDR planning into distinct functions + +This provides the security analysis foundation for multi-round grinding +but does not yet modify the prover/verifier protocol. + +Based on improved proximity gap bounds from ePrint 2025/2055. +``` + +## Future State (Commit 2) - Protocol Implementation + +### Current FRI Query Grinding Pattern + +**Key Components:** +1. **Storage**: `pow_nonce: u64` in ProverChannel and Proof +2. **Grinding Method**: `grind_query_seed()` in `prover/src/channel.rs` + - Searches for nonce where `check_leading_zeros(nonce) >= grinding_factor` + - Parallelizable using Rayon's `find_any()` +3. **Algorithm**: `hash(seed || nonce).trailing_zeros() >= grinding_factor` +4. **Usage**: Nonce reseeds public coin before drawing query positions +5. **Verification**: Verifier checks `check_leading_zeros(pow_nonce) >= grinding_factor` + +**Code Locations:** +- ProofOptions: `air/src/options.rs` (grinding_factor field) +- Prover grinding: `prover/src/channel.rs:169-184` (grind_query_seed) +- Verifier check: `verifier/src/lib.rs:273-279` +- Proof structure: `air/src/proof/mod.rs:52-72` (pow_nonce field) +- RandomCoin: `crypto/src/random/default.rs:139-146` (check_leading_zeros) + +### Generalization Strategy + +Every verifier challenge follows the same pattern: +1. Accumulate transcript (commitments from prover) +2. **[NEW]** Optionally grind to find valid nonce +3. Draw randomness from public coin (optionally reseeded with nonce) +4. Continue protocol + +### Implementation Steps + +#### Step 1: Data Structure Changes + +**File: `air/src/proof/mod.rs`** + +Add new struct for storing per-round nonces: +```rust +/// Proof-of-work nonces for multi-round grinding +#[derive(Clone, Debug, Default)] +pub struct GrindingNonces { + pub ali: Option, + pub deep: Option, + pub fri_batching: Option, + pub fri_first_intermediate: Option, + pub fri_query: Option, +} + +impl Serializable for GrindingNonces { + fn write_into(&self, target: &mut W) { + // Write each optional nonce + // Format: bool (present?) followed by u64 if present + write_option_u64(target, self.ali); + write_option_u64(target, self.deep); + write_option_u64(target, self.fri_batching); + write_option_u64(target, self.fri_first_intermediate); + write_option_u64(target, self.fri_query); + } +} + +impl Deserializable for GrindingNonces { + fn read_from(source: &mut R) -> Result { + Ok(Self { + ali: read_option_u64(source)?, + deep: read_option_u64(source)?, + fri_batching: read_option_u64(source)?, + fri_first_intermediate: read_option_u64(source)?, + fri_query: read_option_u64(source)?, + }) + } +} +``` + +Update Proof structure: +```rust +pub struct Proof { + pub context: Context, + pub num_unique_queries: u8, + pub commitments: Commitments, + pub trace_queries: Vec, + pub constraint_queries: Queries, + pub ood_frame: OodFrame, + pub fri_proof: FriProof, + + // Replace: pub pow_nonce: u64, + pub grinding_nonces: GrindingNonces, +} +``` + +**File: `air/src/options.rs`** + +Update ProofOptions to use GrindingSchedule: +```rust +pub struct ProofOptions { + num_queries: u8, + blowup_factor: u8, + // Remove: grinding_factor: u8, + grinding_schedule: GrindingSchedule, + field_extension: FieldExtension, + fri_folding_factor: u8, + fri_remainder_max_degree: u8, + batching_constraints: BatchingMethod, + batching_deep: BatchingMethod, + partition_options: PartitionOptions, +} +``` + +Add builder methods to GrindingSchedule: +```rust +impl GrindingSchedule { + /// No grinding at all + pub fn none() -> Self { + Self::default() + } + + /// Only grind before FRI query sampling (most common optimization) + pub fn query_only(bits: u32) -> Self { + Self { + fri_query: Some(bits), + ..Default::default() + } + } + + /// Uniform grinding across all rounds + pub fn uniform(bits: u32) -> Self { + Self { + ali: Some(bits), + deep: Some(bits), + fri_batching: Some(bits), + fri_first_intermediate: Some(bits), + fri_query: Some(bits), + } + } + + /// Compute schedule to reach target security level in LDR + pub fn for_target_ldr( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + target_bits: u32, + ) -> Self { + let (schedule, _achieved, _m) = plan_grinding_schedule_ldr( + options, base_field_bits, trace_domain_size, + collision_resistance, num_constraints, num_committed_polys, target_bits + ); + // Convert internal representation (0 = no grinding) to Option + Self { + ali: if schedule.ali > 0 { Some(schedule.ali) } else { None }, + deep: if schedule.deep > 0 { Some(schedule.deep) } else { None }, + fri_batching: if schedule.fri_batching > 0 { Some(schedule.fri_batching) } else { None }, + fri_first_intermediate: if schedule.fri_first_intermediate > 0 { Some(schedule.fri_first_intermediate) } else { None }, + fri_query: if schedule.fri_query > 0 { Some(schedule.fri_query) } else { None }, + } + } + + /// Compute schedule to reach target security level in UDR + pub fn for_target_udr(/* similar parameters */) -> Self { + // Similar implementation using plan_grinding_schedule_udr + } +} +``` + +#### Step 2: Prover Channel Changes + +**File: `prover/src/channel.rs`** + +Update ProverChannel: +```rust +pub struct ProverChannel { + // Existing fields... + + // Replace: pow_nonce: u64, + grinding_nonces: GrindingNonces, +} + +impl ProverChannel { + /// Generic grinding method that can be used for any round + fn grind_for_challenge(&mut self, grinding_bits: u32) -> u64 { + #[cfg(not(feature = "concurrent"))] + let nonce = (1..u64::MAX) + .find(|&nonce| self.public_coin.check_leading_zeros(nonce) >= grinding_bits) + .expect("nonce not found"); + + #[cfg(feature = "concurrent")] + let nonce = (1..u64::MAX) + .into_par_iter() + .find_any(|&nonce| self.public_coin.check_leading_zeros(nonce) >= grinding_bits) + .expect("nonce not found"); + + nonce + } + + /// Apply grinding before drawing ALI randomness + pub fn grind_ali_seed(&mut self) { + if let Some(bits) = self.context.options().grinding_schedule().ali { + let nonce = self.grind_for_challenge(bits); + self.grinding_nonces.ali = Some(nonce); + } + } + + /// Apply grinding before drawing DEEP randomness + pub fn grind_deep_seed(&mut self) { + if let Some(bits) = self.context.options().grinding_schedule().deep { + let nonce = self.grind_for_challenge(bits); + self.grinding_nonces.deep = Some(nonce); + } + } + + /// Apply grinding before drawing FRI batching randomness + pub fn grind_fri_batching_seed(&mut self) { + if let Some(bits) = self.context.options().grinding_schedule().fri_batching { + let nonce = self.grind_for_challenge(bits); + self.grinding_nonces.fri_batching = Some(nonce); + } + } + + /// Apply grinding before drawing FRI intermediate layer randomness + pub fn grind_fri_intermediate_seed(&mut self) { + if let Some(bits) = self.context.options().grinding_schedule().fri_first_intermediate { + let nonce = self.grind_for_challenge(bits); + self.grinding_nonces.fri_first_intermediate = Some(nonce); + } + } + + /// Apply grinding before drawing FRI query positions (existing method, modified) + pub fn grind_query_seed(&mut self) { + if let Some(bits) = self.context.options().grinding_schedule().fri_query { + let nonce = self.grind_for_challenge(bits); + self.grinding_nonces.fri_query = Some(nonce); + } + } + + /// Get all grinding nonces for inclusion in proof + pub fn grinding_nonces(&self) -> GrindingNonces { + self.grinding_nonces.clone() + } +} +``` + +Update RandomCoin methods to accept optional nonce: +```rust +// In crypto/src/random/default.rs or mod.rs +pub trait RandomCoin { + // Add new method that accepts optional nonce + fn draw_with_nonce(&mut self, nonce: Option) -> Result + where + E: FieldElement; + + fn draw_integers_with_nonce( + &mut self, + num_values: usize, + domain_size: usize, + nonce: Option, + ) -> Result, RandomCoinError>; +} + +// Implementation would reseed if nonce present, otherwise use current seed +``` + +#### Step 3: Prover Integration + +**File: `prover/src/lib.rs`** + +Add grinding calls at appropriate protocol points: + +```rust +// After constraint commitment, before ALI randomness draw +// Location: around line 300-350 +channel.commit_constraints(/*...*/); +channel.grind_ali_seed(); // NEW +let constraint_coeffs = channel.draw_constraint_coefficients(/*...*/); + +// After DEEP commitment, before DEEP randomness draw +// Location: around line 400-450 +channel.commit_deep(/*...*/); +channel.grind_deep_seed(); // NEW +let z = channel.draw_deep_challenge(); + +// After FRI commitment, before FRI batching randomness draw +// Location: around line 450-500 +channel.commit_fri_layer(/*...*/); +channel.grind_fri_batching_seed(); // NEW +let fri_alpha = channel.draw_fri_alpha(); + +// For FRI intermediate layers (may need loop modification) +// Location: FRI folding loop +channel.commit_fri_layer(/*...*/); +if is_first_intermediate { + channel.grind_fri_intermediate_seed(); // NEW +} +let folding_challenge = channel.draw_fri_folding_challenge(); + +// Existing query grinding (now uses schedule) +// Location: around line 446-462 +channel.grind_query_seed(); // MODIFIED to use schedule +let query_positions = channel.get_query_positions(); +``` + +Update proof construction: +```rust +// Location: around line 500+ +let proof = Proof::new( + context, + num_unique_queries, + commitments, + trace_queries, + constraint_queries, + ood_frame, + fri_proof, + channel.grinding_nonces(), // CHANGED from pow_nonce +); +``` + +#### Step 4: Verifier Integration + +**File: `verifier/src/lib.rs`** + +Add verification checks after each commitment: + +```rust +// After constraint commitment +let grinding_nonces = &proof.grinding_nonces; + +// ALI grinding check +if let Some(bits) = air.options().grinding_schedule().ali { + let nonce = grinding_nonces.ali + .ok_or(VerifierError::MissingAliGrindingNonce)?; + if public_coin.check_leading_zeros(nonce) < bits { + return Err(VerifierError::AliGrindingVerificationFailed); + } + // Reseed public coin with verified nonce + public_coin.reseed_with_int(nonce); +} + +// DEEP grinding check (similar pattern) +if let Some(bits) = air.options().grinding_schedule().deep { + let nonce = grinding_nonces.deep + .ok_or(VerifierError::MissingDeepGrindingNonce)?; + if public_coin.check_leading_zeros(nonce) < bits { + return Err(VerifierError::DeepGrindingVerificationFailed); + } + public_coin.reseed_with_int(nonce); +} + +// FRI batching grinding check +if let Some(bits) = air.options().grinding_schedule().fri_batching { + let nonce = grinding_nonces.fri_batching + .ok_or(VerifierError::MissingFriBatchingGrindingNonce)?; + if public_coin.check_leading_zeros(nonce) < bits { + return Err(VerifierError::FriBatchingGrindingVerificationFailed); + } + public_coin.reseed_with_int(nonce); +} + +// FRI intermediate grinding check +if let Some(bits) = air.options().grinding_schedule().fri_first_intermediate { + let nonce = grinding_nonces.fri_first_intermediate + .ok_or(VerifierError::MissingFriIntermediateGrindingNonce)?; + if public_coin.check_leading_zeros(nonce) < bits { + return Err(VerifierError::FriIntermediateGrindingVerificationFailed); + } + public_coin.reseed_with_int(nonce); +} + +// FRI query grinding check (replace existing check around line 273-279) +if let Some(bits) = air.options().grinding_schedule().fri_query { + let nonce = grinding_nonces.fri_query + .ok_or(VerifierError::MissingFriQueryGrindingNonce)?; + if public_coin.check_leading_zeros(nonce) < bits { + return Err(VerifierError::QuerySeedProofOfWorkVerificationFailed); + } + public_coin.reseed_with_int(nonce); +} +``` + +Update VerifierError enum: +```rust +pub enum VerifierError { + // ... existing variants + MissingAliGrindingNonce, + AliGrindingVerificationFailed, + MissingDeepGrindingNonce, + DeepGrindingVerificationFailed, + MissingFriBatchingGrindingNonce, + FriBatchingGrindingVerificationFailed, + MissingFriIntermediateGrindingNonce, + FriIntermediateGrindingVerificationFailed, + // QuerySeedProofOfWorkVerificationFailed already exists +} +``` + +#### Step 5: Testing + +**File: `prover/src/tests.rs` or new test file** + +Add comprehensive tests: + +```rust +#[test] +fn test_no_grinding() { + let schedule = GrindingSchedule::none(); + let proof = generate_test_proof_with_schedule(schedule); + assert!(verify_proof(proof).is_ok()); +} + +#[test] +fn test_query_only_grinding() { + let schedule = GrindingSchedule::query_only(20); + let proof = generate_test_proof_with_schedule(schedule); + assert!(proof.grinding_nonces.fri_query.is_some()); + assert!(proof.grinding_nonces.ali.is_none()); + assert!(verify_proof(proof).is_ok()); +} + +#[test] +fn test_uniform_grinding() { + let schedule = GrindingSchedule::uniform(16); + let proof = generate_test_proof_with_schedule(schedule); + assert!(proof.grinding_nonces.ali.is_some()); + assert!(proof.grinding_nonces.deep.is_some()); + assert!(proof.grinding_nonces.fri_batching.is_some()); + assert!(verify_proof(proof).is_ok()); +} + +#[test] +fn test_custom_grinding_schedule() { + let schedule = GrindingSchedule { + ali: Some(5), + deep: Some(10), + fri_batching: Some(15), + fri_first_intermediate: Some(20), + fri_query: Some(25), + }; + let proof = generate_test_proof_with_schedule(schedule); + + // Verify nonces are present + assert_eq!(proof.grinding_nonces.ali.unwrap().trailing_zeros(), 5); + assert_eq!(proof.grinding_nonces.deep.unwrap().trailing_zeros(), 10); + + assert!(verify_proof(proof).is_ok()); +} + +#[test] +fn test_optimal_ldr_schedule() { + let schedule = GrindingSchedule::for_target_ldr( + &options, 64, 1<<20, 128, 100, 200, 128 + ); + let proof = generate_test_proof_with_schedule(schedule); + assert!(verify_proof(proof).is_ok()); + + // Verify security level is achieved + let security = ProvenSecurity::compute_with_schedule( + &options, 64, 1<<20, 128, 100, 200, &schedule.to_internal() + ); + assert!(security.ldr_bits() >= 128); +} + +#[test] +fn test_verifier_rejects_missing_nonce() { + let schedule = GrindingSchedule::query_only(20); + let mut proof = generate_test_proof_with_schedule(schedule); + + // Remove the nonce + proof.grinding_nonces.fri_query = None; + + let result = verify_proof(proof); + assert!(matches!(result, Err(VerifierError::MissingFriQueryGrindingNonce))); +} + +#[test] +fn test_verifier_rejects_invalid_nonce() { + let schedule = GrindingSchedule::query_only(20); + let mut proof = generate_test_proof_with_schedule(schedule); + + // Replace with invalid nonce (not enough zeros) + proof.grinding_nonces.fri_query = Some(1); + + let result = verify_proof(proof); + assert!(matches!(result, Err(VerifierError::QuerySeedProofOfWorkVerificationFailed))); +} + +#[test] +fn test_serialization_roundtrip() { + let schedule = GrindingSchedule::uniform(16); + let proof = generate_test_proof_with_schedule(schedule); + + // Serialize + let mut bytes = Vec::new(); + proof.write_into(&mut bytes); + + // Deserialize + let deserialized = Proof::read_from(&mut &bytes[..]).unwrap(); + + assert_eq!(proof.grinding_nonces.ali, deserialized.grinding_nonces.ali); + assert_eq!(proof.grinding_nonces.deep, deserialized.grinding_nonces.deep); + // ... check all fields +} +``` + +#### Step 6: Documentation + +**File: `README.md` or new `docs/grinding.md`** + +Add documentation explaining: +- What multi-round grinding is and why it's useful +- How to use the builder methods (none, query_only, uniform) +- How to use the scheduler (for_target_ldr, for_target_udr) +- Performance implications of different schedules +- Security trade-offs + +Example usage patterns: +```rust +// Simple: no grinding +let options = ProofOptions::new( + 100, 8, GrindingSchedule::none(), /* ... */ +); + +// Common: only query grinding (backward compatible behavior) +let options = ProofOptions::new( + 100, 8, GrindingSchedule::query_only(20), /* ... */ +); + +// Advanced: optimal schedule for 128-bit security +let schedule = GrindingSchedule::for_target_ldr( + &base_options, 64, 1<<20, 128, 100, 200, 128 +); +let options = ProofOptions::new( + 100, 8, schedule, /* ... */ +); +``` + +### Migration Checklist + +- [ ] **Commit 1: Security Estimator** + - [ ] Verify all tests pass + - [ ] Review diff + - [ ] Commit with detailed message + +- [ ] **Commit 2: Protocol Implementation** + - [ ] Update data structures (GrindingNonces, Proof, ProofOptions) + - [ ] Add builder methods to GrindingSchedule + - [ ] Update ProverChannel with generic grinding + - [ ] Wire grinding into prover at each round + - [ ] Add verifier checks for all rounds + - [ ] Update serialization/deserialization + - [ ] Add comprehensive tests + - [ ] Update documentation + - [ ] Verify all tests pass + - [ ] Commit + +### Notes and Considerations + +1. **Nonce Reseeding**: Need to ensure public coin is reseeded with nonce (if present) before drawing randomness. This maintains the security proof that grinding adds entropy. + +2. **FRI Intermediate Layers**: The `fri_first_intermediate` field applies to the first intermediate FRI layer. Subsequent layers may use the same grinding or none - this needs careful consideration based on the security analysis. + +3. **Backward Compatibility**: Since we're not maintaining backward compatibility, all examples and tests in the codebase will need to be updated to use `GrindingSchedule`. + +4. **Performance Testing**: Should benchmark the overhead of grinding at different rounds to validate the cost model used by the scheduler. + +5. **Serialization Format**: The optional nonce serialization adds a bool per field. Consider if this overhead is acceptable or if we should use a more compact encoding (e.g., bitflags + variable-length nonce list). + +6. **Error Messages**: Need clear error messages when grinding verification fails, indicating which round failed. + +7. **Proof Size Impact**: Each `Option` adds 1 byte (flag) + up to 8 bytes (value) = 9 bytes max per round. With 5 rounds, that's 45 bytes maximum overhead compared to the current single nonce (8 bytes). This is acceptable but should be documented. + +## Success Criteria + +- [ ] All existing tests pass +- [ ] New multi-round grinding tests pass +- [ ] Proofs with different schedules verify correctly +- [ ] Verifier rejects invalid/missing nonces +- [ ] Serialization round-trips correctly +- [ ] Documentation is clear and complete +- [ ] Security estimator integration works correctly +- [ ] Performance is acceptable (grinding overhead measured) diff --git a/air/src/lib.rs b/air/src/lib.rs index 9d43daa90..e31a010bd 100644 --- a/air/src/lib.rs +++ b/air/src/lib.rs @@ -32,6 +32,9 @@ #[macro_use] extern crate alloc; +#[cfg(feature = "std")] +extern crate std; + pub mod proof; mod errors; diff --git a/air/src/proof/mod.rs b/air/src/proof/mod.rs index 6f1cd6f4a..28ce44db6 100644 --- a/air/src/proof/mod.rs +++ b/air/src/proof/mod.rs @@ -10,7 +10,6 @@ use alloc::vec::Vec; use crypto::{Hasher, MerkleTree}; use fri::FriProof; use math::FieldElement; -use security::{ConjecturedSecurity, ProvenSecurity}; use utils::{ByteReader, Deserializable, DeserializationError, Serializable, SliceReader}; use crate::{options::BatchingMethod, ProofOptions, TraceInfo}; @@ -27,7 +26,12 @@ pub use queries::Queries; mod ood_frame; pub use ood_frame::{merge_ood_evaluations, OodFrame, QuotientOodFrame, TraceOodFrame}; -mod security; +pub mod security; +pub use security::{ + find_min_grinding_ldr, find_min_grinding_udr, find_min_queries_ldr, find_min_queries_udr, + plan_grinding_schedule_ldr, plan_grinding_schedule_udr, summarize_ldr, summarize_udr, + ConjecturedSecurity, GrindingSchedule, ProvenSecurity, RoundSecurity, SecuritySummary, +}; mod table; pub use table::Table; @@ -93,8 +97,8 @@ impl Proof { /// /// This is the conjecture on the security of the Toy problem (Conjecture 1) /// in https://eprint.iacr.org/2021/582. - pub fn conjectured_security(&self) -> ConjecturedSecurity { - ConjecturedSecurity::compute( + pub fn conjectured_security(&self) -> security::ConjecturedSecurity { + security::ConjecturedSecurity::compute( self.context.options(), self.context.num_modulus_bits(), H::COLLISION_RESISTANCE, @@ -104,7 +108,7 @@ impl Proof { /// /// Usually, the number of queries needed for provable security is 2x - 3x higher than /// the number of queries needed for conjectured security at the same security level. - pub fn proven_security(&self) -> ProvenSecurity { + pub fn proven_security(&self) -> security::ProvenSecurity { // note that we need the count of the total number of constraints in the protocol as // the soundness error, in the case of algebraic batching, depends on the this number. let num_constraints = self.context.num_constraints(); @@ -116,7 +120,7 @@ impl Proof { let num_trace_polys = self.context.trace_info().width(); let num_constraint_composition_polys = self.options().blowup_factor(); let num_committed_polys = num_trace_polys + num_constraint_composition_polys; - ProvenSecurity::compute( + security::ProvenSecurity::compute( self.context.options(), self.context.num_modulus_bits(), self.trace_info().length(), diff --git a/air/src/proof/security.rs b/air/src/proof/security.rs deleted file mode 100644 index edeaca64b..000000000 --- a/air/src/proof/security.rs +++ /dev/null @@ -1,1391 +0,0 @@ -// Copyright (c) Facebook, Inc. and its affiliates. -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. - -//! Contains helper structs and methods to estimate the security of STARK proofs. - -use core::cmp; - -use crate::{BatchingMethod, ProofOptions}; - -// CONSTANTS -// ================================================================================================ - -const GRINDING_CONTRIBUTION_FLOOR: u32 = 80; -const MAX_PROXIMITY_PARAMETER: u64 = 1000; - -// CONJECTURED SECURITY -// ================================================================================================ - -/// Security estimate (in bits) of the protocol under Conjecture 1 in [1]. -/// -/// [1]: https://eprint.iacr.org/2021/582 -pub struct ConjecturedSecurity(u32); - -impl ConjecturedSecurity { - /// Computes the security level (in bits) of the protocol using Eq. (19) in [1]. - /// - /// [1]: https://eprint.iacr.org/2021/582 - pub fn compute( - options: &ProofOptions, - base_field_bits: u32, - collision_resistance: u32, - ) -> Self { - // compute max security we can get for a given field size - let field_security = base_field_bits * options.field_extension().degree(); - - // compute security we get by executing multiple query rounds - let security_per_query = options.blowup_factor().ilog2(); - let mut query_security = security_per_query * options.num_queries() as u32; - - // include grinding factor contributions only for proofs adequate security - if query_security >= GRINDING_CONTRIBUTION_FLOOR { - query_security += options.grinding_factor(); - } - - Self(cmp::min(cmp::min(field_security, query_security) - 1, collision_resistance)) - } - - /// Returns the conjectured security level (in bits). - pub fn bits(&self) -> u32 { - self.0 - } - - /// Returns whether or not the conjectured security level is greater than or equal to the the - /// specified security level in bits. - pub fn is_at_least(&self, bits: u32) -> bool { - self.0 >= bits - } -} - -// PROVEN SECURITY -// ================================================================================================ - -/// Proven security estimate (in bits), in list-decoding and unique decoding regimes, of the -/// protocol. -pub struct ProvenSecurity { - unique_decoding: u32, - list_decoding: u32, -} - -impl ProvenSecurity { - /// Computes the proven security level (in bits) of the protocol using Theorem 2 and Theorem 3 - /// in [1]. - /// - /// [1]: https://eprint.iacr.org/2024/1553 - pub fn compute( - options: &ProofOptions, - base_field_bits: u32, - trace_domain_size: usize, - collision_resistance: u32, - num_constraints: usize, - num_committed_polys: usize, - ) -> Self { - let unique_decoding = cmp::min( - proven_security_protocol_unique_decoding( - options, - base_field_bits, - trace_domain_size, - num_constraints, - num_committed_polys, - ), - collision_resistance as u64, - ) as u32; - - // determine the interval to which the which the optimal `m` belongs - let m_min: usize = 3; - let m_max = compute_upper_m(trace_domain_size); - - // search for optimal `m` i.e., the one at which we maximize the number of security bits - let m_optimal = (m_min as u32..m_max as u32) - .max_by_key(|&a| { - proven_security_protocol_for_given_proximity_parameter( - options, - base_field_bits, - trace_domain_size, - a as usize, - num_constraints, - num_committed_polys, - ) - }) - .expect( - "Should not fail since m_max is larger than m_min for all trace sizes of length greater than 4", - ); - - let list_decoding = cmp::min( - proven_security_protocol_for_given_proximity_parameter( - options, - base_field_bits, - trace_domain_size, - m_optimal as usize, - num_constraints, - num_committed_polys, - ), - collision_resistance as u64, - ) as u32; - - Self { unique_decoding, list_decoding } - } - - /// Returns the proven security level (in bits) in the list decoding regime. - pub fn ldr_bits(&self) -> u32 { - self.list_decoding - } - - /// Returns the proven security level (in bits) in the unique decoding regime. - pub fn udr_bits(&self) -> u32 { - self.unique_decoding - } - - /// Returns whether or not the proven security level is greater than or equal to the the - /// specified security level in bits. - pub fn is_at_least(&self, bits: u32) -> bool { - self.list_decoding >= bits || self.unique_decoding >= bits - } -} - -/// Computes proven security level for the specified proof parameters for a fixed value of the -/// proximity parameter m in the list-decoding regime. -fn proven_security_protocol_for_given_proximity_parameter( - options: &ProofOptions, - base_field_bits: u32, - trace_domain_size: usize, - m: usize, - num_constraints: usize, - num_committed_polys: usize, -) -> u64 { - let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; - let num_fri_queries = options.num_queries() as f64; - let m = m as f64; - let rho = 1.0 / options.blowup_factor() as f64; - let alpha = (1.0 + 0.5 / m) * sqrt(rho); - // we use the blowup factor in order to bound the max degree - let max_deg = options.blowup_factor() as f64 + 1.0; - let lde_domain_size = (trace_domain_size * options.blowup_factor()) as f64; - let trace_domain_size = trace_domain_size as f64; - let num_openings = 2.0; - - // we apply Theorem 2 in https://eprint.iacr.org/2024/1553, which is based on Theorem 8 in - // https://eprint.iacr.org/2022/1216.pdf and Theorem 5 in https://eprint.iacr.org/2021/582 - // Note that the range of m needs to be restricted in order to ensure that eta, the slackness - // factor to the distance bound, is greater than 0. - // Determining the range of m is the responsibility of the calling function. - let mut epsilons_bits_neg = vec![]; - - // list size - let l = m / (rho - (2.0 * m / lde_domain_size)); - - // ALI related soundness error. If algebraic/curve batching is used for batching the constraints - // then there is a loss of log2(C - 1) where C is the total number of constraints. - let batching_factor = match options.constraint_batching_method() { - BatchingMethod::Linear => 1.0, - BatchingMethod::Algebraic | BatchingMethod::Horner => num_constraints as f64 - 1.0, - }; - let epsilon_1_bits_neg = -log2(l) - log2(batching_factor) + extension_field_bits; - epsilons_bits_neg.push(epsilon_1_bits_neg); - - // DEEP related soundness error. Note that this uses that the denominator |F| - |D ∪ H| - // can be approximated by |F| for all practical domain sizes. We also use the blow-up factor - // as an upper bound for the maximal constraint degree. - let epsilon_2_bits_neg = -log2( - l * l * (max_deg * (trace_domain_size + num_openings - 1.0) + (trace_domain_size - 1.0)), - ) + extension_field_bits; - epsilons_bits_neg.push(epsilon_2_bits_neg); - - // compute FRI commit-phase (i.e., pre-query) soundness error. - // This considers only the first term given in eq. 7 in https://eprint.iacr.org/2022/1216.pdf, - // i.e. (m + 0.5)^7 * n^2 * (N - 1) / (3 * q * rho^1.5) as all other terms are negligible in - // comparison. N is the number of batched polynomials. - let batching_factor = match options.deep_poly_batching_method() { - BatchingMethod::Linear => 1.0, - BatchingMethod::Algebraic | BatchingMethod::Horner => num_committed_polys as f64 - 1.0, - }; - let epsilon_3_bits_neg = extension_field_bits - - log2( - (powf(m + 0.5, 7.0) / (3.0 * powf(rho, 1.5))) - * powf(lde_domain_size, 2.0) - * batching_factor, - ); - epsilons_bits_neg.push(epsilon_3_bits_neg); - - // epsilon_i for i in [3..(k-1)], where k is number of rounds, are also negligible - - // compute FRI query-phase soundness error - let epsilon_k_bits_neg = options.grinding_factor() as f64 - log2(powf(alpha, num_fri_queries)); - epsilons_bits_neg.push(epsilon_k_bits_neg); - - // return the round-by-round (RbR) soundness error - epsilons_bits_neg.into_iter().fold(f64::INFINITY, |a, b| a.min(b)) as u64 -} - -/// Computes proven security level for the specified proof parameters in the unique-decoding regime. -fn proven_security_protocol_unique_decoding( - options: &ProofOptions, - base_field_bits: u32, - trace_domain_size: usize, - num_constraints: usize, - num_committed_polys: usize, -) -> u64 { - let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; - let num_fri_queries = options.num_queries() as f64; - let lde_domain_size = (trace_domain_size * options.blowup_factor()) as f64; - let trace_domain_size = trace_domain_size as f64; - let num_openings = 2.0; - let rho_plus = (trace_domain_size + num_openings) / lde_domain_size; - let alpha = (1.0 + rho_plus) * 0.5; - // we use the blowup factor in order to bound the max degree - let max_deg = options.blowup_factor() as f64 + 1.0; - - // we apply Theorem 3 in https://eprint.iacr.org/2024/1553 - let mut epsilons_bits_neg = vec![]; - - // ALI related soundness error. If algebraic/curve batching is used for batching the constraints - // then there is a loss of log2(C - 1) where C is the total number of constraints. - let batching_factor = match options.constraint_batching_method() { - BatchingMethod::Linear => 1.0, - BatchingMethod::Algebraic | BatchingMethod::Horner => num_constraints as f64 - 1.0, - }; - let epsilon_1_bits_neg = -log2(batching_factor) + extension_field_bits; - epsilons_bits_neg.push(epsilon_1_bits_neg); - - // DEEP related soundness error. Note that this uses that the denominator |F| - |D ∪ H| - // can be approximated by |F| for all practical domain sizes. We also use the blow-up factor - // as an upper bound for the maximal constraint degree - let epsilon_2_bits_neg = - -log2(max_deg * (trace_domain_size + num_openings - 1.0) + (trace_domain_size - 1.0)) - + extension_field_bits; - epsilons_bits_neg.push(epsilon_2_bits_neg); - - // compute FRI commit-phase (i.e., pre-query) soundness error. Note that there is no soundness - // degradation in the case of linear batching while there is a degradation in the order - // of log2(N - 1) in the case of algebraic batching, where N is the number of polynomials - // being batched. - let batching_factor = match options.deep_poly_batching_method() { - BatchingMethod::Linear => 1.0, - BatchingMethod::Algebraic | BatchingMethod::Horner => num_committed_polys as f64 - 1.0, - }; - let epsilon_3_bits_neg = extension_field_bits - log2(lde_domain_size * batching_factor); - epsilons_bits_neg.push(epsilon_3_bits_neg); - - // epsilon_i for i in [3..(k-1)], where k is number of rounds - let folding_factor = options.to_fri_options().folding_factor() as f64; - let num_fri_layers = options.to_fri_options().num_fri_layers(lde_domain_size as usize); - let epsilon_i_min_bits_neg = (0..num_fri_layers) - .map(|_| extension_field_bits - log2((folding_factor - 1.0) * (lde_domain_size + 1.0))) - .fold(f64::INFINITY, |a, b| a.min(b)); - epsilons_bits_neg.push(epsilon_i_min_bits_neg); - - // compute FRI query-phase soundness error - let epsilon_k_bits_neg = options.grinding_factor() as f64 - log2(powf(alpha, num_fri_queries)); - epsilons_bits_neg.push(epsilon_k_bits_neg); - - // return the round-by-round (RbR) soundness error - epsilons_bits_neg.into_iter().fold(f64::INFINITY, |a, b| a.min(b)) as u64 -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Computes the largest proximity parameter m such that eta is greater than 0 in the proof of -/// Theorem 1 in https://eprint.iacr.org/2021/582. See Theorem 2 in https://eprint.iacr.org/2024/1553 -/// and its proof for more on this point. -/// -/// The bound on m in Theorem 2 in https://eprint.iacr.org/2024/1553 is sufficient but we can use -/// the following to compute a better bound. -fn compute_upper_m(h: usize) -> f64 { - let h = h as f64; - let ratio = (h + 2.0) / h; - let m_max = ceil(1.0 / (2.0 * (sqrt(ratio) - 1.0))); - assert!(m_max >= h / 2.0, "the bound in the theorem should be tighter"); - - // We cap the range to 1000 as the optimal m value will be in the lower range of [m_min, m_max] - // since increasing m too much will lead to a deterioration in the FRI commit soundness making - // any benefit gained in the FRI query soundess mute. - cmp::min(m_max as u64, MAX_PROXIMITY_PARAMETER) as f64 -} - -#[cfg(feature = "std")] -pub fn log2(value: f64) -> f64 { - value.log2() -} - -#[cfg(not(feature = "std"))] -pub fn log2(value: f64) -> f64 { - libm::log2(value) -} - -#[cfg(feature = "std")] -pub fn sqrt(value: f64) -> f64 { - value.sqrt() -} - -#[cfg(not(feature = "std"))] -pub fn sqrt(value: f64) -> f64 { - libm::sqrt(value) -} - -#[cfg(feature = "std")] -pub fn powf(value: f64, exp: f64) -> f64 { - value.powf(exp) -} - -#[cfg(not(feature = "std"))] -pub fn powf(value: f64, exp: f64) -> f64 { - libm::pow(value, exp) -} - -#[cfg(feature = "std")] -pub fn ceil(value: f64) -> f64 { - value.ceil() -} - -#[cfg(not(feature = "std"))] -pub fn ceil(value: f64) -> f64 { - libm::ceil(value) -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use math::{fields::f64::BaseElement, StarkField}; - - use super::ProofOptions; - use crate::{proof::security::ProvenSecurity, BatchingMethod, FieldExtension}; - - #[test] - fn get_100_bits_security() { - let field_extension = FieldExtension::Quadratic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 2; - let fri_remainder_max_degree = 127; - let grinding_factor = 20; - let blowup_factor = 4; - let num_queries = 119; - let collision_resistance = 128; - let trace_length = 2_usize.pow(20); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(unique_decoding, 100); - assert_eq!(list_decoding, 69); - - // increasing the queries does not help the LDR case - let num_queries = 150; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 69); - - // increasing the extension degree does help and we then need fewer queries by virtue - // of being in LDR - let field_extension = FieldExtension::Cubic; - let num_queries = 81; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 100); - } - - #[test] - fn unique_decoding_folding_factor_effect() { - let field_extension = FieldExtension::Quadratic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 2; - let fri_remainder_max_degree = 7; - let grinding_factor = 16; - let blowup_factor = 8; - let num_queries = 123; - let collision_resistance = 128; - let trace_length = 2_usize.pow(8); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding, list_decoding: _ } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(unique_decoding, 116); - - let fri_folding_factor = 4; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding, list_decoding: _ } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(unique_decoding, 115); - } - - #[test] - fn unique_versus_list_decoding_rate_effect() { - let field_extension = FieldExtension::Quadratic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 2; - let fri_remainder_max_degree = 7; - let grinding_factor = 20; - let blowup_factor = 2; - let num_queries = 195; - let collision_resistance = 128; - let trace_length = 2_usize.pow(8); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding, list_decoding: _ } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(unique_decoding, 100); - - // when the rate is large, going to a larger extension field in order to make full use of - // being in the LDR might not always be justified - - // we increase the extension degree - let field_extension = FieldExtension::Cubic; - // and we reduce the number of required queries to reach the target level, but this is - // a relatively small, approximately 16%, reduction - let num_queries = 163; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 100); - - // reducing the rate further changes things - let field_extension = FieldExtension::Quadratic; - let blowup_factor = 4; - let num_queries = 119; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding, list_decoding: _ } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(unique_decoding, 100); - - // the improvement is now at approximately 32% - let field_extension = FieldExtension::Cubic; - let num_queries = 81; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 100); - } - - #[test] - fn get_96_bits_security() { - let field_extension = FieldExtension::Cubic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 127; - let grinding_factor = 20; - let blowup_factor = 4; - let num_queries = 80; - let collision_resistance = 128; - let trace_length = 2_usize.pow(18); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 99); - - // increasing the blowup factor should increase the bits of security gained per query - let blowup_factor = 8; - let num_queries = 53; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 99); - } - - #[test] - fn get_128_bits_security() { - let field_extension = FieldExtension::Cubic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 127; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 85; - let collision_resistance = 128; - let trace_length = 2_usize.pow(18); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 128); - - // increasing the blowup factor should increase the bits of security gained per query - let blowup_factor = 16; - let num_queries = 65; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 128); - } - - #[test] - fn extension_degree() { - let field_extension = FieldExtension::Quadratic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 127; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 85; - let collision_resistance = 128; - let trace_length = 2_usize.pow(18); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 70); - - // increasing the extension degree improves the FRI commit phase soundness error and permits - // reaching 128 bits security - let field_extension = FieldExtension::Cubic; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(list_decoding, 128); - } - - #[test] - fn trace_length() { - let field_extension = FieldExtension::Cubic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 127; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 80; - let collision_resistance = 128; - let trace_length = 2_usize.pow(20); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_1, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - let trace_length = 2_usize.pow(16); - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_2, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert!(security_1 < security_2); - } - - #[test] - fn num_fri_queries() { - let field_extension = FieldExtension::Cubic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 127; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 60; - let collision_resistance = 128; - let trace_length = 2_usize.pow(20); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_1, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - let num_queries = 80; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_2, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert!(security_1 < security_2); - } - - #[test] - fn blowup_factor() { - let field_extension = FieldExtension::Cubic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 127; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 30; - let collision_resistance = 128; - let trace_length = 2_usize.pow(20); - let num_committed_polys = 2; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_1, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - let blowup_factor = 16; - - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_2, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert!(security_1 < security_2); - } - - #[test] - fn deep_batching_method_udr() { - let field_extension = FieldExtension::Quadratic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 255; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 120; - let collision_resistance = 128; - let trace_length = 2_usize.pow(16); - let num_committed_polys = 1 << 1; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Algebraic, - ); - let ProvenSecurity { - unique_decoding: security_1, - list_decoding: _, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_1, 106); - - // when the FRI batching error is not largest when compared to the other soundness error - // terms, increasing the number of committed polynomials might not lead to a degradation - // in the round-by-round soundness of the protocol - let num_committed_polys = 1 << 2; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Algebraic, - ); - let ProvenSecurity { - unique_decoding: security_2, - list_decoding: _, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_2, 106); - - // but after a certain point, there will be a degradation - let num_committed_polys = 1 << 5; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Algebraic, - ); - let ProvenSecurity { - unique_decoding: security_2, - list_decoding: _, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_2, 104); - - // and this degradation is on the order of log2(N - 1) where N is the number of - // committed polynomials - let num_committed_polys = num_committed_polys << 3; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Algebraic, - ); - let ProvenSecurity { - unique_decoding: security_2, - list_decoding: _, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_2, 101); - } - - #[test] - fn deep_batching_method_ldr() { - let field_extension = FieldExtension::Cubic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 255; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 120; - let collision_resistance = 128; - let trace_length = 2_usize.pow(22); - let num_committed_polys = 1 << 1; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Algebraic, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_1, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_1, 126); - - // increasing the number of committed polynomials might lead to a degradation - // in the round-by-round soundness of the protocol on the order of log2(N - 1) where - // N is the number of committed polynomials. This happens when the FRI batching error - // is the largest among all errors - let num_committed_polys = 1 << 8; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Algebraic, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_2, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_2, 118); - } - - #[test] - fn constraints_batching_method_udr() { - let field_extension = FieldExtension::Quadratic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 2; - let fri_remainder_max_degree = 255; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 120; - let collision_resistance = 128; - let trace_length = 2_usize.pow(16); - let num_committed_polys = 1 << 1; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: security_1, - list_decoding: _, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_1, 108); - - // when the total number of constraints is on the order of the size of the LDE domain size - // there is no degradation in the soundness error when using algebraic/curve batching - // to batch constraints - let num_constraints = trace_length * blowup_factor; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Algebraic, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: security_2, - list_decoding: _, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_2, 108); - - // but after a certain point, there will be a degradation - let num_constraints = (trace_length * blowup_factor) << 2; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Algebraic, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: security_2, - list_decoding: _, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_2, 107); - - // and this degradation is on the order of log2(C - 1) where C is the total number of - // constraints - let num_constraints = num_constraints << 2; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Algebraic, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: security_2, - list_decoding: _, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_2, 105); - } - - #[test] - fn constraints_batching_method_ldr() { - let field_extension = FieldExtension::Cubic; - let base_field_bits = BaseElement::MODULUS_BITS; - let fri_folding_factor = 8; - let fri_remainder_max_degree = 255; - let grinding_factor = 20; - let blowup_factor = 8; - let num_queries = 120; - let collision_resistance = 128; - let trace_length = 2_usize.pow(22); - let num_committed_polys = 1 << 1; - let num_constraints = 100; - - let mut options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Linear, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_1, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_1, 126); - - // when the total number of constraints is on the order of the size of the LDE domain size - // square there is no degradation in the soundness error when using algebraic/curve batching - // to batch constraints - let num_constraints = (trace_length * blowup_factor).pow(2); - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Algebraic, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_2, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_2, 126); - - // and we have a good margin until we see any degradation in the soundness error - let num_constraints = num_constraints << 12; - options = ProofOptions::new( - num_queries, - blowup_factor, - grinding_factor, - field_extension, - fri_folding_factor as usize, - fri_remainder_max_degree as usize, - BatchingMethod::Algebraic, - BatchingMethod::Linear, - ); - let ProvenSecurity { - unique_decoding: _, - list_decoding: security_3, - } = ProvenSecurity::compute( - &options, - base_field_bits, - trace_length, - collision_resistance, - num_constraints, - num_committed_polys, - ); - - assert_eq!(security_3, 125); - } -} diff --git a/air/src/proof/security/conjectured.rs b/air/src/proof/security/conjectured.rs new file mode 100644 index 000000000..5778fbcda --- /dev/null +++ b/air/src/proof/security/conjectured.rs @@ -0,0 +1,52 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Conjectured security estimation based on Conjecture 1 in https://eprint.iacr.org/2021/582 + +use core::cmp; + +use super::GRINDING_CONTRIBUTION_FLOOR; +use crate::ProofOptions; + +/// Security estimate (in bits) of the protocol under Conjecture 1 in [1]. +/// +/// [1]: https://eprint.iacr.org/2021/582 +pub struct ConjecturedSecurity(u32); + +impl ConjecturedSecurity { + /// Computes the security level (in bits) of the protocol using Eq. (19) in [1]. + /// + /// [1]: https://eprint.iacr.org/2021/582 + pub fn compute( + options: &ProofOptions, + base_field_bits: u32, + collision_resistance: u32, + ) -> Self { + // compute max security we can get for a given field size + let field_security = base_field_bits * options.field_extension().degree(); + + // compute security we get by executing multiple query rounds + let security_per_query = options.blowup_factor().ilog2(); + let mut query_security = security_per_query * options.num_queries() as u32; + + // include grinding factor contributions only for proofs adequate security + if query_security >= GRINDING_CONTRIBUTION_FLOOR { + query_security += options.grinding_factor(); + } + + Self(cmp::min(cmp::min(field_security, query_security) - 1, collision_resistance)) + } + + /// Returns the conjectured security level (in bits). + pub fn bits(&self) -> u32 { + self.0 + } + + /// Returns whether or not the conjectured security level is greater than or equal to the the + /// specified security level in bits. + pub fn is_at_least(&self, bits: u32) -> bool { + self.0 >= bits + } +} diff --git a/air/src/proof/security/grinding.rs b/air/src/proof/security/grinding.rs new file mode 100644 index 000000000..2de6af591 --- /dev/null +++ b/air/src/proof/security/grinding.rs @@ -0,0 +1,747 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Grinding schedule planning for round-by-round soundness. +//! +//! This module provides functions for planning optimal grinding schedules to achieve target +//! security levels in both list-decoding regime (LDR) and unique-decoding regime (UDR). + +use alloc::{fmt, vec::Vec}; +use core::fmt::{Display, Formatter}; + +use super::{batching_factor, log2, powf, proven::compute_upper_m, sqrt}; +use crate::ProofOptions; + +// GRINDING SCHEDULE +// ================================================================================================ +// Represents per-round grinding bits to boost round-by-round soundness, as per the grinding lemma +// in the ethSTARK paper (IACR ePrint 2021/582). Each value adds that many bits to the +// corresponding round's epsilon in the round-by-round soundness vector. +// +// Rounds: +// +// - ε₁: ALI - randomness used to batch constraints +// - ε₂: DEEP - randomness for out-of-domain (OOD) challenges +// - ε₃: FRI batching - randomness to batch multiple polynomials for FRI +// - ε₄, ..., εₖ₋₁: FRI intermediate layers - one grinding per FRI folding challenge +// - εₖ: FRI query - randomness for the FRI query seed (TODO: this should override +// options.grinding_factor.) +// +// Note: With many FRI layers, total grinding cost increases as each intermediate layer must be +// boosted to the target. The optimizer tends to prefer smaller proximity parameter m to strengthen +// commit-phase rounds (ε₁-εₖ₋₁), accepting weaker query soundness (εₖ) which can be compensated +// with grinding at a single location. +#[derive(Clone, Debug, Default)] +pub struct GrindingSchedule { + pub ali: u32, + pub deep: u32, + pub fri_batching: u32, + /// Grinding for each FRI intermediate layer (ε₄, ..., εₖ₋₁). + /// Length equals the number of FRI folding layers. + pub fri_intermediate: Vec, + pub fri_query: u32, +} + +impl GrindingSchedule { + /// Returns the total grinding cost as log₂(number of hashes). + pub fn cost_log2(&self) -> f64 { + let cost = powf(2.0, self.ali as f64) + + powf(2.0, self.deep as f64) + + powf(2.0, self.fri_batching as f64) + + self.fri_intermediate.iter().map(|&b| powf(2.0, b as f64)).sum::() + + powf(2.0, self.fri_query as f64); + log2(cost) + } +} + +// SECURITY SUMMARY +// ================================================================================================ + +/// Summary of round-by-round security analysis for a grinding schedule. +/// +/// Contains baseline security bits (before grinding), grinding deltas, and final security +/// for each round in the protocol. Implements `Display` for human-readable output. +#[derive(Clone, Debug)] +pub struct SecuritySummary { + /// Target security level in bits. + pub target_bits: u32, + /// Proximity parameter m (only for LDR). + pub proximity_parameter: Option, + /// Baseline security bits per round (before grinding). + pub baseline: RoundSecurity, + /// Grinding bits applied per round. + pub grinding: GrindingSchedule, + /// Total grinding cost as log₂(number of hashes). + pub total_grinding_cost_log2: f64, +} + +/// Per-round baseline security bits (before grinding). +#[derive(Clone, Debug)] +pub struct RoundSecurity { + pub ali: f64, + pub deep: f64, + pub fri_batching: f64, + pub fri_intermediate: Vec, + pub fri_query: f64, +} + +impl Display for SecuritySummary { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "Security Summary")?; + writeln!(f, "================")?; + if let Some(m) = self.proximity_parameter { + writeln!(f, "Regime: LDR (m={})", m)?; + } else { + writeln!(f, "Regime: UDR")?; + } + writeln!(f, "Target: {} bits", self.target_bits)?; + writeln!(f)?; + writeln!(f, "{:20} {:>12} {:>10} {:>12}", "Round", "Baseline", "Grinding", "Final")?; + writeln!(f, "{:-<20} {:-<12} {:-<10} {:-<12}", "", "", "", "")?; + + // Helper to format a row + let row = + |f: &mut Formatter<'_>, name: &str, baseline: f64, grinding: u32| -> fmt::Result { + writeln!( + f, + "{:20} {:>12.2} {:>10} {:>12.2}", + name, + baseline, + grinding, + baseline + grinding as f64 + ) + }; + + row(f, "ε₁ (ALI)", self.baseline.ali, self.grinding.ali)?; + row(f, "ε₂ (DEEP)", self.baseline.deep, self.grinding.deep)?; + row(f, "ε₃ (FRI batching)", self.baseline.fri_batching, self.grinding.fri_batching)?; + + for (i, (&baseline, &grinding)) in self + .baseline + .fri_intermediate + .iter() + .zip(&self.grinding.fri_intermediate) + .enumerate() + { + let name = fmt::format(format_args!("ε₄₊{} (FRI layer {})", i, i)); + row(f, &name, baseline, grinding)?; + } + + row(f, "εₖ (Query)", self.baseline.fri_query, self.grinding.fri_query)?; + + writeln!(f)?; + writeln!(f, "Total grinding cost: 2^{:.2} hashes", self.total_grinding_cost_log2)?; + + Ok(()) + } +} + +/// Computes a grinding schedule to reach a target security level in the list-decoding regime. +/// +/// For each candidate proximity parameter m, this function computes the baseline security bits +/// for each round without grinding. It then calculates the grinding deltas needed to lift all +/// rounds to the target security level. +/// +/// The optimal m is chosen to minimize total expected prover work, measured as the log-sum-exp +/// of the grinding deltas: Σ 2^{delta_i} (in base 2). Ties are broken by L1 norm (Σ delta_i), +/// then by preferring smaller m. +/// +/// # Returns +/// +/// Returns the grinding schedule and the chosen proximity parameter `m`. +/// +/// # Panics +/// +/// Panics if `target_bits` exceeds the field size or collision resistance. +pub fn plan_grinding_schedule_ldr( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + target_bits: u32, +) -> (GrindingSchedule, u32) { + let field_cap = base_field_bits * options.field_extension().degree(); + let cap = field_cap.min(collision_resistance); + assert!( + target_bits <= cap, + "target_bits ({target_bits}) exceeds maximum achievable security ({cap})" + ); + + let lde_domain_size = trace_domain_size * options.blowup_factor(); + let num_fri_layers = options.to_fri_options().num_fri_layers(lde_domain_size); + + // Helper: compute per-round bits (no schedule, query grinding = 0) for a given m + fn round_bits_for_m( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + m: usize, + num_constraints: usize, + num_committed_polys: usize, + num_fri_layers: usize, + ) -> (f64, f64, f64, Vec, f64) { + let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; + let num_fri_queries = options.num_queries() as f64; + let m = m as f64; + let rho = 1.0 / options.blowup_factor() as f64; + let alpha = (1.0 + 0.5 / m) * sqrt(rho); + let max_deg = options.blowup_factor() as f64 + 1.0; + let lde_domain_size = (trace_domain_size * options.blowup_factor()) as f64; + let trace_domain_size = trace_domain_size as f64; + let num_openings = 2.0; + + // ALI + let l = m / (rho - (2.0 * m / lde_domain_size)); + let constraint_batching = + batching_factor(options.constraint_batching_method(), num_constraints); + let b1 = -log2(l) - log2(constraint_batching) + extension_field_bits; + + // DEEP + let b2 = -log2( + l * (max_deg * (trace_domain_size + num_openings - 1.0) + (trace_domain_size - 1.0)), + ) + extension_field_bits; + + // FRI batching base (without batching constant) + // Note: FRI layer errors do NOT depend on the batching constant. + // They only depend on the folding factor, domain size, and proximity parameter m. + let b3_no_batching = extension_field_bits + - log2((2.0 * powf(m + 0.5, 5.0) / (3.0 * powf(rho, 1.5))) * lde_domain_size); + + // FRI batching with batching constant (only affects ε₃) + let deep_batching = + batching_factor(options.deep_poly_batching_method(), num_committed_polys); + let b3 = b3_no_batching - log2(deep_batching); + + // ε₄, ..., εₖ₋₁: FRI intermediate layers (one per folding step) + let folding_factor = options.to_fri_options().folding_factor() as f64; + let mut fri_layer_bits = Vec::with_capacity(num_fri_layers); + let mut current_domain_size = lde_domain_size; + + for _ in 0..num_fri_layers { + current_domain_size /= folding_factor; + + // Two contributions to intermediate layer bound + let term_from_b3 = b3_no_batching; // Use version without batching constant + let term_from_n_over_q = extension_field_bits + - log2(folding_factor) + - log2(current_domain_size + 1.0) + - log2(2.0 * m + 1.0) + + 0.5 * log2(rho); + + let b_layer = term_from_b3.min(term_from_n_over_q); + fri_layer_bits.push(b_layer); + } + + // εₖ: Query + let bq = -log2(powf(alpha, num_fri_queries)); + (b1, b2, b3, fri_layer_bits, bq) + } + + // Search for optimal m by minimizing Σ 2^{delta_i} + let m_min: usize = 3; + let m_max = compute_upper_m(trace_domain_size).max(4.0) as usize; + let mut best_cost_log2 = f64::INFINITY; + let mut best_l1 = u64::MAX; + let mut best_m = m_min as u32; + let mut best_schedule = GrindingSchedule::default(); + + for m in m_min..m_max { + let (b1, b2, b3, fri_layer_bits, bq) = round_bits_for_m( + options, + base_field_bits, + trace_domain_size, + m, + num_constraints, + num_committed_polys, + num_fri_layers, + ); + + // Compute grinding deltas for all rounds + let t = target_bits as i64; + let d1 = (t - b1 as i64).max(0) as u32; + let d2 = (t - b2 as i64).max(0) as u32; + let d3 = (t - b3 as i64).max(0) as u32; + let d_fri_layers: Vec = + fri_layer_bits.iter().map(|&bits| (t - bits as i64).max(0) as u32).collect(); + let dq = (t - bq as i64).max(0) as u32; + + // Compute cost with per-layer FRI grinding + let log2_cost = log2( + powf(2.0, d1 as f64) + + powf(2.0, d2 as f64) + + powf(2.0, d3 as f64) + + d_fri_layers.iter().map(|&delta| powf(2.0, delta as f64)).sum::() + + powf(2.0, dq as f64), + ); + + let l1: u64 = d1 as u64 + + d2 as u64 + + d3 as u64 + + d_fri_layers.iter().map(|&delta| delta as u64).sum::() + + dq as u64; + + let better = (log2_cost < best_cost_log2) + || ((log2_cost - best_cost_log2).abs() < 1e-9 && l1 < best_l1) + || ((log2_cost - best_cost_log2).abs() < 1e-9 && l1 == best_l1 && best_m > m as u32); + + if better { + best_cost_log2 = log2_cost; + best_l1 = l1; + best_m = m as u32; + best_schedule = GrindingSchedule { + ali: d1, + deep: d2, + fri_batching: d3, + fri_intermediate: d_fri_layers.clone(), + fri_query: dq, + }; + } + } + + (best_schedule, best_m) +} + +/// Computes a grinding schedule to reach a target security level in the unique-decoding regime. +/// +/// This function computes the baseline security bits for each round without grinding, then +/// calculates the grinding deltas needed to lift all rounds to the target security level. +/// +/// # Panics +/// +/// Panics if `target_bits` exceeds the field size or collision resistance. +pub fn plan_grinding_schedule_udr( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + target_bits: u32, +) -> GrindingSchedule { + let field_cap = base_field_bits * options.field_extension().degree(); + let cap = field_cap.min(collision_resistance); + assert!( + target_bits <= cap, + "target_bits ({target_bits}) exceeds maximum achievable security ({cap})" + ); + + let lde_domain_size = trace_domain_size * options.blowup_factor(); + let num_fri_layers = options.to_fri_options().num_fri_layers(lde_domain_size); + + // Helper: compute per-round bits (no schedule, query grinding = 0) + fn round_bits( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + num_constraints: usize, + num_committed_polys: usize, + num_fri_layers: usize, + ) -> (f64, f64, f64, Vec, f64) { + let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; + let num_fri_queries = options.num_queries() as f64; + let lde_domain_size = (trace_domain_size * options.blowup_factor()) as f64; + let trace_domain_size = trace_domain_size as f64; + let num_openings = 2.0; + let rho_plus = (trace_domain_size + num_openings) / lde_domain_size; + let alpha = (1.0 + rho_plus) * 0.5; + let max_deg = options.blowup_factor() as f64 + 1.0; + + // ALI + let constraint_batching = + batching_factor(options.constraint_batching_method(), num_constraints); + let b1 = -log2(constraint_batching) + extension_field_bits; + + // DEEP + let b2 = + -log2(max_deg * (trace_domain_size + num_openings - 1.0) + (trace_domain_size - 1.0)) + + extension_field_bits; + + // FRI batching (UDR commit-like) + let deep_batching = + batching_factor(options.deep_poly_batching_method(), num_committed_polys); + let b3 = extension_field_bits - log2(lde_domain_size * deep_batching); + + // FRI intermediate layers (UDR analogue) + let folding_factor = options.to_fri_options().folding_factor() as f64; + let mut fri_layer_bits = Vec::with_capacity(num_fri_layers); + let mut current_domain_size = lde_domain_size; + + for _ in 0..num_fri_layers { + current_domain_size /= folding_factor; + let b_layer = + extension_field_bits - log2((folding_factor - 1.0) * (current_domain_size + 1.0)); + fri_layer_bits.push(b_layer); + } + + // Query + let bq = -log2(powf(alpha, num_fri_queries)); + (b1, b2, b3, fri_layer_bits, bq) + } + + let (b1, b2, b3, fri_layer_bits, bq) = round_bits( + options, + base_field_bits, + trace_domain_size, + num_constraints, + num_committed_polys, + num_fri_layers, + ); + + let t = target_bits as i64; + let d1 = (t - b1 as i64).max(0) as u32; + let d2 = (t - b2 as i64).max(0) as u32; + let d3 = (t - b3 as i64).max(0) as u32; + let d_fri_layers: Vec = + fri_layer_bits.iter().map(|&bits| (t - bits as i64).max(0) as u32).collect(); + let dq = (t - bq as i64).max(0) as u32; + + GrindingSchedule { + ali: d1, + deep: d2, + fri_batching: d3, + fri_intermediate: d_fri_layers, + fri_query: dq, + } +} + +/// Creates a security summary for a grinding schedule in the list-decoding regime. +/// +/// This recomputes the baseline security bits for the given proximity parameter `m` +/// and combines them with the grinding schedule to produce a complete summary. +#[allow(clippy::too_many_arguments)] +pub fn summarize_ldr( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + num_constraints: usize, + num_committed_polys: usize, + target_bits: u32, + schedule: &GrindingSchedule, + m: u32, +) -> SecuritySummary { + let lde_domain_size = trace_domain_size * options.blowup_factor(); + let num_fri_layers = options.to_fri_options().num_fri_layers(lde_domain_size); + + let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; + let num_fri_queries = options.num_queries() as f64; + let m_f64 = m as f64; + let rho = 1.0 / options.blowup_factor() as f64; + let alpha = (1.0 + 0.5 / m_f64) * sqrt(rho); + let max_deg = options.blowup_factor() as f64 + 1.0; + let lde_domain_size_f64 = lde_domain_size as f64; + let trace_domain_size_f64 = trace_domain_size as f64; + let num_openings = 2.0; + + // ALI + let l = m_f64 / (rho - (2.0 * m_f64 / lde_domain_size_f64)); + let constraint_batching = + batching_factor(options.constraint_batching_method(), num_constraints); + let b1 = -log2(l) - log2(constraint_batching) + extension_field_bits; + + // DEEP + let b2 = -log2( + l * (max_deg * (trace_domain_size_f64 + num_openings - 1.0) + + (trace_domain_size_f64 - 1.0)), + ) + extension_field_bits; + + // FRI batching + let b3_no_batching = extension_field_bits + - log2((2.0 * powf(m_f64 + 0.5, 5.0) / (3.0 * powf(rho, 1.5))) * lde_domain_size_f64); + let deep_batching = batching_factor(options.deep_poly_batching_method(), num_committed_polys); + let b3 = b3_no_batching - log2(deep_batching); + + // FRI intermediate layers + let folding_factor = options.to_fri_options().folding_factor() as f64; + let mut fri_layer_bits = Vec::with_capacity(num_fri_layers); + let mut current_domain_size = lde_domain_size_f64; + + for _ in 0..num_fri_layers { + current_domain_size /= folding_factor; + let term_from_b3 = b3_no_batching; + let term_from_n_over_q = extension_field_bits + - log2(folding_factor) + - log2(current_domain_size + 1.0) + - log2(2.0 * m_f64 + 1.0) + + 0.5 * log2(rho); + fri_layer_bits.push(term_from_b3.min(term_from_n_over_q)); + } + + // Query + let bq = -log2(powf(alpha, num_fri_queries)); + + SecuritySummary { + target_bits, + proximity_parameter: Some(m), + baseline: RoundSecurity { + ali: b1, + deep: b2, + fri_batching: b3, + fri_intermediate: fri_layer_bits, + fri_query: bq, + }, + grinding: schedule.clone(), + total_grinding_cost_log2: schedule.cost_log2(), + } +} + +/// Creates a security summary for a grinding schedule in the unique-decoding regime. +pub fn summarize_udr( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + num_constraints: usize, + num_committed_polys: usize, + target_bits: u32, + schedule: &GrindingSchedule, +) -> SecuritySummary { + let lde_domain_size = trace_domain_size * options.blowup_factor(); + let num_fri_layers = options.to_fri_options().num_fri_layers(lde_domain_size); + + let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; + let num_fri_queries = options.num_queries() as f64; + let lde_domain_size_f64 = lde_domain_size as f64; + let trace_domain_size_f64 = trace_domain_size as f64; + let num_openings = 2.0; + let rho_plus = (trace_domain_size_f64 + num_openings) / lde_domain_size_f64; + let alpha = (1.0 + rho_plus) * 0.5; + let max_deg = options.blowup_factor() as f64 + 1.0; + + // ALI + let constraint_batching = + batching_factor(options.constraint_batching_method(), num_constraints); + let b1 = -log2(constraint_batching) + extension_field_bits; + + // DEEP + let b2 = -log2( + max_deg * (trace_domain_size_f64 + num_openings - 1.0) + (trace_domain_size_f64 - 1.0), + ) + extension_field_bits; + + // FRI batching + let deep_batching = batching_factor(options.deep_poly_batching_method(), num_committed_polys); + let b3 = extension_field_bits - log2(lde_domain_size_f64 * deep_batching); + + // FRI intermediate layers + let folding_factor = options.to_fri_options().folding_factor() as f64; + let mut fri_layer_bits = Vec::with_capacity(num_fri_layers); + let mut current_domain_size = lde_domain_size_f64; + + for _ in 0..num_fri_layers { + current_domain_size /= folding_factor; + fri_layer_bits.push( + extension_field_bits - log2((folding_factor - 1.0) * (current_domain_size + 1.0)), + ); + } + + // Query + let bq = -log2(powf(alpha, num_fri_queries)); + + SecuritySummary { + target_bits, + proximity_parameter: None, + baseline: RoundSecurity { + ali: b1, + deep: b2, + fri_batching: b3, + fri_intermediate: fri_layer_bits, + fri_query: bq, + }, + grinding: schedule.clone(), + total_grinding_cost_log2: schedule.cost_log2(), + } +} + +// QUERY/GRINDING TRADE-OFF HELPERS +// ================================================================================================ + +/// Finds the minimum number of queries needed to achieve a target security level +/// with at most `max_grinding_log2` bits of grinding work in LDR. +/// +/// Returns `None` if no valid configuration exists within the search bounds. +/// +/// # Arguments +/// * `base_options` - Base proof options (num_queries will be varied) +/// * `max_grinding_log2` - Maximum allowed grinding cost as log₂(hashes) +/// * `target_bits` - Target security level in bits +/// +/// # Example +/// ```ignore +/// // Find minimum queries for 100-bit security with at most 2^20 grinding work +/// let min_queries = find_min_queries_ldr(&options, ..., 20.0, 100); +/// ``` +#[allow(clippy::too_many_arguments)] +pub fn find_min_queries_ldr( + base_options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + max_grinding_log2: f64, + target_bits: u32, +) -> Option { + // Binary search for minimum queries + let mut lo = 1usize; + let mut hi = 255usize; // max queries is u8 + let mut result = None; + + while lo <= hi { + let mid = (lo + hi) / 2; + + let options = ProofOptions::new( + mid, + base_options.blowup_factor(), + 0, + base_options.field_extension(), + base_options.to_fri_options().folding_factor(), + base_options.to_fri_options().remainder_max_degree(), + base_options.constraint_batching_method(), + base_options.deep_poly_batching_method(), + ); + + // Try to plan a grinding schedule + let plan_result = std::panic::catch_unwind(|| { + plan_grinding_schedule_ldr( + &options, + base_field_bits, + trace_domain_size, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ) + }); + + match plan_result { + Ok((schedule, _m)) if schedule.cost_log2() <= max_grinding_log2 => { + result = Some(mid); + hi = mid - 1; + }, + _ => { + lo = mid + 1; + }, + } + } + + result +} + +/// Finds the minimum grinding cost (as log₂ hashes) to achieve a target security level +/// with the given number of queries in LDR. +/// +/// Returns `None` if the target cannot be achieved (exceeds collision resistance). +#[allow(clippy::too_many_arguments)] +pub fn find_min_grinding_ldr( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + target_bits: u32, +) -> Option { + let result = std::panic::catch_unwind(|| { + plan_grinding_schedule_ldr( + options, + base_field_bits, + trace_domain_size, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ) + }); + + result.ok().map(|(schedule, _m)| schedule.cost_log2()) +} + +/// Finds the minimum number of queries needed to achieve a target security level +/// with at most `max_grinding_log2` bits of grinding work in UDR. +/// +/// Returns `None` if no valid configuration exists within the search bounds. +#[allow(clippy::too_many_arguments)] +pub fn find_min_queries_udr( + base_options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + max_grinding_log2: f64, + target_bits: u32, +) -> Option { + let mut lo = 1usize; + let mut hi = 255usize; + let mut result = None; + + while lo <= hi { + let mid = (lo + hi) / 2; + + let options = ProofOptions::new( + mid, + base_options.blowup_factor(), + 0, + base_options.field_extension(), + base_options.to_fri_options().folding_factor(), + base_options.to_fri_options().remainder_max_degree(), + base_options.constraint_batching_method(), + base_options.deep_poly_batching_method(), + ); + + let plan_result = std::panic::catch_unwind(|| { + plan_grinding_schedule_udr( + &options, + base_field_bits, + trace_domain_size, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ) + }); + + match plan_result { + Ok(schedule) if schedule.cost_log2() <= max_grinding_log2 => { + result = Some(mid); + hi = mid - 1; + }, + _ => { + lo = mid + 1; + }, + } + } + + result +} + +/// Finds the minimum grinding cost (as log₂ hashes) to achieve a target security level +/// with the given number of queries in UDR. +/// +/// Returns `None` if the target cannot be achieved (exceeds collision resistance). +#[allow(clippy::too_many_arguments)] +pub fn find_min_grinding_udr( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + target_bits: u32, +) -> Option { + let result = std::panic::catch_unwind(|| { + plan_grinding_schedule_udr( + options, + base_field_bits, + trace_domain_size, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ) + }); + + result.ok().map(|schedule| schedule.cost_log2()) +} diff --git a/air/src/proof/security/mod.rs b/air/src/proof/security/mod.rs new file mode 100644 index 000000000..7a6d7a55f --- /dev/null +++ b/air/src/proof/security/mod.rs @@ -0,0 +1,94 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Contains helper structs and methods to estimate the security of STARK proofs. +//! +//! This module provides security estimation in two regimes: +//! - **Conjectured security**: Based on Conjecture 1 in IACR ePrint 2021/582 +//! - **Proven security**: Based on round-by-round soundness analysis from IACR ePrint 2024/1553 +//! with Johnson-regime improvements from IACR ePrint 2025/2055 +//! +//! The module also provides grinding schedule planning for achieving target security levels +//! with the help of grinding while optimizing prover work. + +// Module declarations +mod conjectured; +mod grinding; +mod proven; + +#[cfg(test)] +mod tests; + +// Re-exports +pub use conjectured::ConjecturedSecurity; +pub use grinding::{ + find_min_grinding_ldr, find_min_grinding_udr, find_min_queries_ldr, find_min_queries_udr, + plan_grinding_schedule_ldr, plan_grinding_schedule_udr, summarize_ldr, summarize_udr, + GrindingSchedule, RoundSecurity, SecuritySummary, +}; +pub use proven::ProvenSecurity; + +// CONSTANTS +// ================================================================================================ + +pub(crate) const GRINDING_CONTRIBUTION_FLOOR: u32 = 80; +pub(crate) const MAX_PROXIMITY_PARAMETER: u64 = 1000; + +// MATH HELPER FUNCTIONS +// ================================================================================================ + +#[cfg(feature = "std")] +pub(crate) fn log2(value: f64) -> f64 { + value.log2() +} + +#[cfg(not(feature = "std"))] +pub(crate) fn log2(value: f64) -> f64 { + libm::log2(value) +} + +#[cfg(feature = "std")] +pub(crate) fn sqrt(value: f64) -> f64 { + value.sqrt() +} + +#[cfg(not(feature = "std"))] +pub(crate) fn sqrt(value: f64) -> f64 { + libm::sqrt(value) +} + +#[cfg(feature = "std")] +pub(crate) fn powf(value: f64, exp: f64) -> f64 { + value.powf(exp) +} + +#[cfg(not(feature = "std"))] +pub(crate) fn powf(value: f64, exp: f64) -> f64 { + libm::pow(value, exp) +} + +#[cfg(feature = "std")] +pub(crate) fn ceil(value: f64) -> f64 { + value.ceil() +} + +#[cfg(not(feature = "std"))] +pub(crate) fn ceil(value: f64) -> f64 { + libm::ceil(value) +} + +// BATCHING HELPER +// ================================================================================================ + +use crate::BatchingMethod; + +/// Returns the batching factor for the given batching method and count. +/// For linear batching, factor is 1.0. For algebraic/Horner batching, factor is (count - 1). +pub(crate) fn batching_factor(method: BatchingMethod, count: usize) -> f64 { + match method { + BatchingMethod::Linear => 1.0, + BatchingMethod::Algebraic | BatchingMethod::Horner => count as f64 - 1.0, + } +} diff --git a/air/src/proof/security/proven.rs b/air/src/proof/security/proven.rs new file mode 100644 index 000000000..2397dab24 --- /dev/null +++ b/air/src/proof/security/proven.rs @@ -0,0 +1,489 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Proven security estimation based on round-by-round soundness analysis. +//! +//! This module provides security estimates in both the list-decoding regime (LDR) and +//! unique-decoding regime (UDR), following the analysis in IACR ePrint 2024/1553 and +//! incorporating Johnson-regime proximity-gap improvements from IACR ePrint 2025/2055. + +use alloc::vec; +use core::cmp; + +use super::{batching_factor, ceil, log2, powf, sqrt, GrindingSchedule, MAX_PROXIMITY_PARAMETER}; +use crate::ProofOptions; + +/// Proven security estimate (in bits), in list-decoding and unique decoding regimes, of the +/// protocol. +pub struct ProvenSecurity { + pub(crate) unique_decoding: u32, + pub(crate) list_decoding: u32, +} + +impl ProvenSecurity { + /// Computes the proven security level (in bits) of the protocol using Theorem 2 and Theorem 3 + /// in [1]. + /// + /// [1]: https://eprint.iacr.org/2024/1553 + pub fn compute( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + ) -> Self { + let unique_decoding = cmp::min( + proven_security_protocol_unique_decoding( + options, + base_field_bits, + trace_domain_size, + num_constraints, + num_committed_polys, + ), + collision_resistance as u64, + ) as u32; + + // determine the interval to which the optimal `m` belongs + let m_min: usize = 3; + let m_max = compute_upper_m(trace_domain_size); + + // search for optimal `m` i.e., the one at which we maximize the number of security bits + let m_optimal = (m_min as u32..m_max as u32) + .max_by_key(|&a| { + proven_security_protocol_for_given_proximity_parameter( + options, + base_field_bits, + trace_domain_size, + a as usize, + num_constraints, + num_committed_polys, + ) + }) + .expect( + "Should not fail since m_max is larger than m_min for all trace sizes of length greater than 4", + ); + + let list_decoding = cmp::min( + proven_security_protocol_for_given_proximity_parameter( + options, + base_field_bits, + trace_domain_size, + m_optimal as usize, + num_constraints, + num_committed_polys, + ), + collision_resistance as u64, + ) as u32; + + Self { unique_decoding, list_decoding } + } + + /// Computes the proven security level (in bits) using a per-round grinding schedule. + /// + /// # Note + /// + /// TODO: `schedule.fri_query` overrides `options.grinding_factor` to avoid double-counting + /// grinding contributions, should be simplified once we remove options.grinding_factor. + #[allow(clippy::too_many_arguments)] + pub fn compute_with_schedule( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + collision_resistance: u32, + num_constraints: usize, + num_committed_polys: usize, + schedule: &GrindingSchedule, + ) -> Self { + let unique_decoding = cmp::min( + proven_security_protocol_unique_decoding_with_schedule( + options, + base_field_bits, + trace_domain_size, + num_constraints, + num_committed_polys, + schedule, + ), + collision_resistance as u64, + ) as u32; + + // determine the interval to which the optimal `m` belongs + let m_min: usize = 3; + let m_max = compute_upper_m(trace_domain_size); + + // search for optimal `m` + let m_optimal = (m_min as u32..m_max as u32) + .max_by_key(|&a| { + proven_security_protocol_for_given_proximity_parameter_with_schedule( + options, + base_field_bits, + trace_domain_size, + a as usize, + num_constraints, + num_committed_polys, + schedule, + ) + }) + .expect("m_max > m_min for valid trace sizes"); + + let list_decoding = cmp::min( + proven_security_protocol_for_given_proximity_parameter_with_schedule( + options, + base_field_bits, + trace_domain_size, + m_optimal as usize, + num_constraints, + num_committed_polys, + schedule, + ), + collision_resistance as u64, + ) as u32; + + Self { unique_decoding, list_decoding } + } + + /// Returns the proven security level (in bits) in the list decoding regime. + pub fn ldr_bits(&self) -> u32 { + self.list_decoding + } + + /// Returns the proven security level (in bits) in the unique decoding regime. + pub fn udr_bits(&self) -> u32 { + self.unique_decoding + } + + /// Returns whether or not the proven security level is greater than or equal to the the + /// specified security level in bits. + pub fn is_at_least(&self, bits: u32) -> bool { + self.list_decoding >= bits || self.unique_decoding >= bits + } +} + +/// Computes proven security level for the specified proof parameters for a fixed value of the +/// proximity parameter m in the list-decoding regime. +fn proven_security_protocol_for_given_proximity_parameter( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + m: usize, + num_constraints: usize, + num_committed_polys: usize, +) -> u64 { + let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; + let num_fri_queries = options.num_queries() as f64; + let m = m as f64; + let rho = 1.0 / options.blowup_factor() as f64; + let alpha = (1.0 + 0.5 / m) * sqrt(rho); + // we use the blowup factor in order to bound the max degree + let max_deg = options.blowup_factor() as f64 + 1.0; + let lde_domain_size = (trace_domain_size * options.blowup_factor()) as f64; + let trace_domain_size = trace_domain_size as f64; + let num_openings = 2.0; + + // We follow the round-by-round (RbR) composition from prior analyses and incorporate + // the Johnson-regime proximity-gap improvements: + // - For ALI/DEEP and query-phase we keep the structure as in prior work (e.g., 2024/1553 and + // 2022/1216), + // - For the FRI commit-phase in LDR we use the improved Johnson-regime bound from IACR ePrint + // 2025/2055 (Theorem 4.2) which tightens the dominant term and reduces the scaling in n. + // Note: the range of m must ensure a positive slackness (η > 0 / valid Johnson gap τ > 0); + // the caller (search over m) is responsible for selecting admissible values. + let mut epsilons_bits_neg = vec![]; + + // list size + let l = m / (rho - (2.0 * m / lde_domain_size)); + + // ALI related soundness error. If algebraic/curve batching is used for batching the constraints + // then there is a loss of log2(C - 1) where C is the total number of constraints. + let constraint_batching = + batching_factor(options.constraint_batching_method(), num_constraints); + let epsilon_1_bits_neg = -log2(l) - log2(constraint_batching) + extension_field_bits; + epsilons_bits_neg.push(epsilon_1_bits_neg); + + // DEEP related soundness error. Note that this uses that the denominator |F| - |D ∪ H| + // can be approximated by |F| for all practical domain sizes. We also use the blow-up factor + // as an upper bound for the maximal constraint degree. + let epsilon_2_bits_neg = + -log2(l * (max_deg * (trace_domain_size + num_openings - 1.0) + (trace_domain_size - 1.0))) + + extension_field_bits; + epsilons_bits_neg.push(epsilon_2_bits_neg); + + // compute FRI commit-phase (i.e., pre-query) soundness error. + // Johnson-regime improvement (IACR ePrint 2025/2055): dominant term scales as + // 2 * (m + 1/2)^5 / (3 * ρ^{3/2}) * n * (N - 1), + // replacing the older (m + 1/2)^7 * n^2 /(3 * ρ^{3/2}) dependence. Here n is the LDE domain size, + // and N is the number of batched polynomials (captured by deep_batching below). + let deep_batching = batching_factor(options.deep_poly_batching_method(), num_committed_polys); + // LDR commit-phase improvement (per IACR ePrint 2025/2055 proximity-gap results): + // - Replace n^2 with n in the pre-query term (O(n) “exceptions” instead of O(n^2)) in LDR + // (e.g., Theorem 4.2 in IACR ePrint 2025/2055). + // - Replace (m + 0.5)^7 with (m + 0.5)^5 (dominant term exponent tightened by the refined analysis). + // Johnson-gap parameterization (Theorem 4.2 in IACR ePrint 2025/2055 and Theorem 5.1 in IACR ePrint 2020/654): + // Let J(δ) = 1 - sqrt(ρ) and τ = J(δ) - γ. Then + // m = max( sqrt(ρ) / (2 * τ), 3 ). + // Our estimator searches over m directly, so it captures this regime without explicitly computing γ. + // Lower-order term (∝ (m + 0.5) * γ * ρ) is omitted since its effect is negligible: + // using γ ≤ J(δ) = 1 - sqrt(ρ), the ratio of lower-order to dominant term satisfies + // R ≤ (3/2) * (J(δ) * ρ) / (m + 0.5)^4 = (3/2) * (ρ * (1 - sqrt(ρ))) / (m + 0.5)^4. + // Numerical example (conservative): m = 3 ⇒ m + 0.5 = 3.5, ρ = 1/2 ⇒ J(δ) ≈ 0.2929. + // Then R ≤ ~0.00146 ⇒ Δ_bits = log2(1 + R) ≈ 0.002 bits. For larger m or smaller ρ this only decreases. + + // FRI batching base (without batching constant) + // This is used for FRI layer computations. We denote this as ε₃′ (epsilon_3_no_batching). + let epsilon_3_no_batching = extension_field_bits + - log2((2.0 * powf(m + 0.5, 5.0) / (3.0 * powf(rho, 1.5))) * lde_domain_size); + + // FRI batching with batching constant: -log₂(ε₃) = -log₂(ε₃′) - log₂(deep_batching) + // (only the ε₃ round is affected by batching) + let epsilon_3_bits_neg = epsilon_3_no_batching - log2(deep_batching); + epsilons_bits_neg.push(epsilon_3_bits_neg); + + // ε_i for i ∈ [4..(k-1)] (intermediate FRI layers). Using Theorem 5 of IACR ePrint + // 2021/582 and Theorem 4.2 in IACR ePrint 2025/2055. Noting that t_ℓ are the FRI + // folding factors, we include for layer j ≥ 0 a contribution of the form + // ε_i ≈ ε₃′ · (∏_{r=0}^{i-1} 1/t_r) and an additive term ~ (n/q). + let folding_factor = options.to_fri_options().folding_factor() as f64; + // With fixed folding factor across layers, intermediate errors decrease with layer index. + // Thus, we bound the entire intermediate range by the first intermediate round (i = 4): + // from ε₃′ path: -log₂(ε₃′) where ε₃′ is ε₃ WITHOUT batching constant + // from (n/q) path: |𝔽| - log₂(t_ℓ) - log₂(n_ℓ + 1) - log₂(2m + 1) + ½log₂(ρ) + let term_from_e3 = epsilon_3_no_batching; + let term_from_n_over_q = extension_field_bits + - log2(folding_factor) + - log2(lde_domain_size + 1.0) + - log2(2.0 * m + 1.0) + + 0.5 * log2(rho); + let epsilon_i_min_bits_neg = term_from_e3.min(term_from_n_over_q); + epsilons_bits_neg.push(epsilon_i_min_bits_neg); + + // compute FRI query-phase soundness error + let epsilon_k_bits_neg = options.grinding_factor() as f64 - log2(powf(alpha, num_fri_queries)); + epsilons_bits_neg.push(epsilon_k_bits_neg); + + // return the round-by-round (RbR) soundness error + epsilons_bits_neg.into_iter().fold(f64::INFINITY, |a, b| a.min(b)) as u64 +} + +/// Computes proven security level in the list-decoding regime for a fixed proximity parameter m, +/// using a per-round grinding schedule. +#[allow(clippy::too_many_arguments)] +fn proven_security_protocol_for_given_proximity_parameter_with_schedule( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + m: usize, + num_constraints: usize, + num_committed_polys: usize, + schedule: &GrindingSchedule, +) -> u64 { + let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; + let num_fri_queries = options.num_queries() as f64; + let m = m as f64; + let rho = 1.0 / options.blowup_factor() as f64; + let alpha = (1.0 + 0.5 / m) * sqrt(rho); + let max_deg = options.blowup_factor() as f64 + 1.0; + let lde_domain_size = (trace_domain_size * options.blowup_factor()) as f64; + let trace_domain_size = trace_domain_size as f64; + let num_openings = 2.0; + + let mut epsilons_bits_neg = vec![]; + + // ALI + let l = m / (rho - (2.0 * m / lde_domain_size)); + let constraint_batching = + batching_factor(options.constraint_batching_method(), num_constraints); + let mut epsilon_1_bits_neg = -log2(l) - log2(constraint_batching) + extension_field_bits; + epsilon_1_bits_neg += schedule.ali as f64; + epsilons_bits_neg.push(epsilon_1_bits_neg); + + // DEEP + let mut epsilon_2_bits_neg = + -log2(l * (max_deg * (trace_domain_size + num_openings - 1.0) + (trace_domain_size - 1.0))) + + extension_field_bits; + epsilon_2_bits_neg += schedule.deep as f64; + epsilons_bits_neg.push(epsilon_2_bits_neg); + + // FRI batching base (without batching constant) + // FRI layers should NOT be affected by the batching constant + let epsilon_3_no_batching = extension_field_bits + - log2((2.0 * powf(m + 0.5, 5.0) / (3.0 * powf(rho, 1.5))) * lde_domain_size); + + // FRI batching with batching constant (only affects ε₃) + let deep_batching = batching_factor(options.deep_poly_batching_method(), num_committed_polys); + let mut epsilon_3_bits_neg = epsilon_3_no_batching - log2(deep_batching); + epsilon_3_bits_neg += schedule.fri_batching as f64; + epsilons_bits_neg.push(epsilon_3_bits_neg); + + // FRI intermediate layers (ε₄, ..., εₖ₋₁) + let folding_factor = options.to_fri_options().folding_factor() as f64; + let mut current_domain_size = lde_domain_size; + + for &layer_grinding in &schedule.fri_intermediate { + current_domain_size /= folding_factor; + + let mut epsilon_layer_bits_neg = extension_field_bits + .min( + // from ε3 WITHOUT batching constant (batching only affects ε3, not layers) + epsilon_3_no_batching, + ) + .min( + // additive path with constant C = (2m+1)/sqrt(ρ) + extension_field_bits + - log2(folding_factor) + - log2(current_domain_size + 1.0) + - log2(2.0 * m + 1.0) + + 0.5 * log2(rho), + ); + epsilon_layer_bits_neg += layer_grinding as f64; + epsilons_bits_neg.push(epsilon_layer_bits_neg); + } + + // FRI query (override options.grinding_factor) + let epsilon_k_bits_neg = schedule.fri_query as f64 - log2(powf(alpha, num_fri_queries)); + epsilons_bits_neg.push(epsilon_k_bits_neg); + + epsilons_bits_neg.into_iter().fold(f64::INFINITY, |a, b| a.min(b)) as u64 +} + +/// Computes proven security level in the unique-decoding regime using Theorem 3 in +/// https://eprint.iacr.org/2024/1553. +fn proven_security_protocol_unique_decoding( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + num_constraints: usize, + num_committed_polys: usize, +) -> u64 { + let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; + let num_fri_queries = options.num_queries() as f64; + let lde_domain_size = (trace_domain_size * options.blowup_factor()) as f64; + let trace_domain_size = trace_domain_size as f64; + let num_openings = 2.0; + let rho_plus = (trace_domain_size + num_openings) / lde_domain_size; + let alpha = (1.0 + rho_plus) * 0.5; + // we use the blowup factor in order to bound the max degree + let max_deg = options.blowup_factor() as f64 + 1.0; + + // we apply Theorem 3 in https://eprint.iacr.org/2024/1553 + let mut epsilons_bits_neg = vec![]; + + // ALI related soundness error. If algebraic/curve batching is used for batching the constraints + // then there is a loss of log2(C - 1) where C is the total number of constraints. + let constraint_batching = + batching_factor(options.constraint_batching_method(), num_constraints); + let epsilon_1_bits_neg = -log2(constraint_batching) + extension_field_bits; + epsilons_bits_neg.push(epsilon_1_bits_neg); + + // DEEP related soundness error. Note that this uses that the denominator |F| - |D ∪ H| + // can be approximated by |F| for all practical domain sizes. We also use the blow-up factor + // as an upper bound for the maximal constraint degree + let epsilon_2_bits_neg = + -log2(max_deg * (trace_domain_size + num_openings - 1.0) + (trace_domain_size - 1.0)) + + extension_field_bits; + epsilons_bits_neg.push(epsilon_2_bits_neg); + + // compute FRI commit-phase (i.e., pre-query) soundness error. Note that there is no soundness + // degradation in the case of linear batching while there is a degradation in the order + // of log2(N - 1) in the case of algebraic batching, where N is the number of polynomials + // being batched. + let deep_batching = batching_factor(options.deep_poly_batching_method(), num_committed_polys); + let epsilon_3_bits_neg = extension_field_bits - log2(lde_domain_size * deep_batching); + epsilons_bits_neg.push(epsilon_3_bits_neg); + + // ε_i for i ∈ [4..(k-1)] (intermediate FRI layers) + let folding_factor = options.to_fri_options().folding_factor() as f64; + let num_fri_layers = options.to_fri_options().num_fri_layers(lde_domain_size as usize); + let epsilon_i_min_bits_neg = (0..num_fri_layers) + .map(|_| extension_field_bits - log2((folding_factor - 1.0) * (lde_domain_size + 1.0))) + .fold(f64::INFINITY, |a, b| a.min(b)); + epsilons_bits_neg.push(epsilon_i_min_bits_neg); + + // compute FRI query-phase soundness error + let epsilon_k_bits_neg = options.grinding_factor() as f64 - log2(powf(alpha, num_fri_queries)); + epsilons_bits_neg.push(epsilon_k_bits_neg); + + // return the round-by-round (RbR) soundness error + epsilons_bits_neg.into_iter().fold(f64::INFINITY, |a, b| a.min(b)) as u64 +} + +/// Computes proven security level in the unique-decoding regime using a per-round grinding +/// schedule. +#[allow(clippy::too_many_arguments)] +fn proven_security_protocol_unique_decoding_with_schedule( + options: &ProofOptions, + base_field_bits: u32, + trace_domain_size: usize, + num_constraints: usize, + num_committed_polys: usize, + schedule: &GrindingSchedule, +) -> u64 { + let extension_field_bits = (base_field_bits * options.field_extension().degree()) as f64; + let num_fri_queries = options.num_queries() as f64; + let lde_domain_size = (trace_domain_size * options.blowup_factor()) as f64; + let trace_domain_size = trace_domain_size as f64; + let num_openings = 2.0; + let rho_plus = (trace_domain_size + num_openings) / lde_domain_size; + let alpha = (1.0 + rho_plus) * 0.5; + let max_deg = options.blowup_factor() as f64 + 1.0; + + let mut epsilons_bits_neg = vec![]; + + // ALI + let constraint_batching = + batching_factor(options.constraint_batching_method(), num_constraints); + let mut epsilon_1_bits_neg = -log2(constraint_batching) + extension_field_bits; + epsilon_1_bits_neg += schedule.ali as f64; + epsilons_bits_neg.push(epsilon_1_bits_neg); + + // DEEP + let mut epsilon_2_bits_neg = + -log2(max_deg * (trace_domain_size + num_openings - 1.0) + (trace_domain_size - 1.0)) + + extension_field_bits; + epsilon_2_bits_neg += schedule.deep as f64; + epsilons_bits_neg.push(epsilon_2_bits_neg); + + // FRI batching (commit-like term in UDR) + let deep_batching = batching_factor(options.deep_poly_batching_method(), num_committed_polys); + let mut epsilon_3_bits_neg = extension_field_bits - log2(lde_domain_size * deep_batching); + epsilon_3_bits_neg += schedule.fri_batching as f64; + epsilons_bits_neg.push(epsilon_3_bits_neg); + + // FRI intermediate layers (UDR analogue) + let folding_factor = options.to_fri_options().folding_factor() as f64; + let mut current_domain_size = lde_domain_size; + + for &layer_grinding in &schedule.fri_intermediate { + current_domain_size /= folding_factor; + let epsilon_i_min_bits_neg = + extension_field_bits - log2((folding_factor - 1.0) * (current_domain_size + 1.0)); + let mut epsilon_layer_bits_neg = epsilon_i_min_bits_neg; + epsilon_layer_bits_neg += layer_grinding as f64; + epsilons_bits_neg.push(epsilon_layer_bits_neg); + } + + // Query phase (override options.grinding_factor) + let epsilon_k_bits_neg = schedule.fri_query as f64 - log2(powf(alpha, num_fri_queries)); + epsilons_bits_neg.push(epsilon_k_bits_neg); + + epsilons_bits_neg.into_iter().fold(f64::INFINITY, |a, b| a.min(b)) as u64 +} + +/// Computes the largest proximity parameter m such that eta is greater than 0 in the proof of +/// Theorem 1 in https://eprint.iacr.org/2021/582. See Theorem 2 in https://eprint.iacr.org/2024/1553 +/// and its proof for more on this point. +/// +/// The bound on m in Theorem 2 in https://eprint.iacr.org/2024/1553 is sufficient but we can use +/// the following to compute a better bound. +pub(super) fn compute_upper_m(h: usize) -> f64 { + let h = h as f64; + let ratio = (h + 2.0) / h; + let m_max = ceil(1.0 / (2.0 * (sqrt(ratio) - 1.0))); + assert!(m_max >= h / 2.0, "the bound in the theorem should be tighter"); + + // We cap the range to 1000 as the optimal m value will be in the lower range of [m_min, m_max] + // since increasing m too much will lead to a deterioration in the FRI commit soundness making + // any benefit gained in the FRI query soundness moot. + cmp::min(m_max as u64, MAX_PROXIMITY_PARAMETER) as f64 +} diff --git a/air/src/proof/security/tests.rs b/air/src/proof/security/tests.rs new file mode 100644 index 000000000..3082ab96a --- /dev/null +++ b/air/src/proof/security/tests.rs @@ -0,0 +1,1359 @@ +use math::{fields::f64::BaseElement, StarkField}; + +use crate::{proof::security::ProvenSecurity, BatchingMethod, FieldExtension, ProofOptions}; + +#[test] +fn get_100_bits_security() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 2; + let fri_remainder_max_degree = 127; + let grinding_factor = 20; + let blowup_factor = 4; + let num_queries = 119; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(unique_decoding, 100); + assert_eq!(list_decoding, 94); + + // increasing the queries does not help the LDR case + let num_queries = 150; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 94); + + // increasing the extension degree does help and we then need fewer queries by virtue + // of being in LDR + let field_extension = FieldExtension::Cubic; + let num_queries = 81; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 100); +} + +#[test] +fn unique_decoding_folding_factor_effect() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 2; + let fri_remainder_max_degree = 7; + let grinding_factor = 16; + let blowup_factor = 8; + let num_queries = 123; + let collision_resistance = 128; + let trace_length = 2_usize.pow(8); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding, list_decoding: _ } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(unique_decoding, 116); + + let fri_folding_factor = 4; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding, list_decoding: _ } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(unique_decoding, 115); +} + +#[test] +fn unique_versus_list_decoding_rate_effect() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 2; + let fri_remainder_max_degree = 7; + let grinding_factor = 20; + let blowup_factor = 2; + let num_queries = 195; + let collision_resistance = 128; + let trace_length = 2_usize.pow(8); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding, list_decoding: _ } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(unique_decoding, 100); + + // when the rate is large, going to a larger extension field in order to make full use of + // being in the LDR might not always be justified + + // we increase the extension degree + let field_extension = FieldExtension::Cubic; + // and we reduce the number of required queries to reach the target level, but this is + // a relatively small, approximately 16%, reduction + let num_queries = 163; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 100); + + // reducing the rate further changes things + let field_extension = FieldExtension::Quadratic; + let blowup_factor = 4; + let num_queries = 119; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding, list_decoding: _ } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(unique_decoding, 100); + + // the improvement is now at approximately 32% + let field_extension = FieldExtension::Cubic; + let num_queries = 81; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 100); +} + +#[test] +fn get_96_bits_security() { + let field_extension = FieldExtension::Cubic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let grinding_factor = 20; + let blowup_factor = 4; + let num_queries = 80; + let collision_resistance = 128; + let trace_length = 2_usize.pow(18); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 99); + + // increasing the blowup factor should increase the bits of security gained per query + let blowup_factor = 8; + let num_queries = 53; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 99); +} + +#[test] +fn get_128_bits_security() { + let field_extension = FieldExtension::Cubic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 80; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 128); + + // increasing the blowup factor should increase the bits of security gained per query + let blowup_factor = 16; + let num_queries = 65; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 128); +} + +#[test] +fn grinding_schedule_quadratic_reaches_target() { + // Show that the grinding schedule planner correctly reaches the target security level + // in LDR for the quadratic extension case. + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let blowup_factor = 8; + let num_queries = 70; + let collision_resistance = 128; + let trace_length = 2_usize.pow(18); + let num_committed_polys = 128; + let num_constraints = 4048; + + let options = ProofOptions::new( + num_queries, + blowup_factor, + 0, // baseline grinding factor is ignored by schedule variant + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Horner, + BatchingMethod::Horner, + ); + + // Plan a schedule to reach 110 bits in LDR and verify it achieves the target. + let target_bits = 110; + let (schedule, _m) = super::plan_grinding_schedule_ldr( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ); + + let list_decoding = ProvenSecurity::compute_with_schedule( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + &schedule, + ) + .ldr_bits(); + + assert_eq!(list_decoding, target_bits); +} + +#[test] +fn grinding_schedule_quadratic_reaches_target_udr() { + // Show that the grinding schedule planner correctly reaches the target security level + // in UDR for the quadratic extension case. + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let blowup_factor = 8; + let num_queries = 110; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 128; + let num_constraints = 4048; + + let options = ProofOptions::new( + num_queries, + blowup_factor, + 0, // baseline grinding factor is ignored by schedule variant + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Horner, + BatchingMethod::Horner, + ); + + // Plan a schedule to reach 110 bits in UDR and verify it achieves the target. + let target_bits = 110; + let schedule = super::plan_grinding_schedule_udr( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ); + + let unique_decoding = ProvenSecurity::compute_with_schedule( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + &schedule, + ) + .udr_bits(); + + assert_eq!(unique_decoding, target_bits); +} + +#[test] +fn security_summary_display_100bits_ldr() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let blowup_factor = 8; + let num_queries = 60; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 128; + let num_constraints = 4048; + let target_bits = 100; + + let options = ProofOptions::new( + num_queries, + blowup_factor, + 0, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Horner, + BatchingMethod::Horner, + ); + + let (schedule, m) = super::plan_grinding_schedule_ldr( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ); + + let summary = super::summarize_ldr( + &options, + base_field_bits, + trace_length, + num_constraints, + num_committed_polys, + target_bits, + &schedule, + m, + ); + + std::println!("{summary}"); +} + +#[test] +fn security_summary_display_udr() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let blowup_factor = 8; + let num_queries = 110; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 128; + let num_constraints = 4048; + let target_bits = 110; + + let options = ProofOptions::new( + num_queries, + blowup_factor, + 0, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Horner, + BatchingMethod::Horner, + ); + + let schedule = super::plan_grinding_schedule_udr( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ); + + let summary = super::summarize_udr( + &options, + base_field_bits, + trace_length, + num_constraints, + num_committed_polys, + target_bits, + &schedule, + ); + + std::println!("{summary}"); +} + +#[test] +fn find_min_queries_ldr() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let blowup_factor = 8; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 128; + let num_constraints = 4048; + let target_bits = 100; + + let base_options = ProofOptions::new( + 1, // placeholder, will be varied + blowup_factor, + 0, + field_extension, + fri_folding_factor, + fri_remainder_max_degree, + BatchingMethod::Horner, + BatchingMethod::Horner, + ); + + // Find minimum queries for 2^20 grinding budget + let min_queries = super::find_min_queries_ldr( + &base_options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + 20.0, + target_bits, + ); + + assert!(min_queries.is_some()); + let q = min_queries.unwrap(); + std::println!("Min queries for 2^20 grinding budget: {q}"); + + // Verify the result: grinding cost should be <= 20 + let verify_options = ProofOptions::new( + q, + blowup_factor, + 0, + field_extension, + fri_folding_factor, + fri_remainder_max_degree, + BatchingMethod::Horner, + BatchingMethod::Horner, + ); + let grinding = super::find_min_grinding_ldr( + &verify_options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ); + assert!(grinding.is_some()); + assert!(grinding.unwrap() <= 20.0); + + // Sanity check: one fewer query should exceed the budget + if q > 1 { + let fewer_options = ProofOptions::new( + q - 1, + blowup_factor, + 0, + field_extension, + fri_folding_factor, + fri_remainder_max_degree, + BatchingMethod::Horner, + BatchingMethod::Horner, + ); + let grinding_fewer = super::find_min_grinding_ldr( + &fewer_options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + target_bits, + ); + if let Some(g) = grinding_fewer { + assert!(g > 20.0); + } + } +} + +#[test] +fn extension_degree() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 85; + let collision_resistance = 128; + let trace_length = 2_usize.pow(18); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 94); + + // increasing the extension degree improves the FRI commit phase soundness error and permits + // reaching 128 bits security + let field_extension = FieldExtension::Cubic; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { unique_decoding: _, list_decoding } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(list_decoding, 128); +} + +#[test] +fn trace_length() { + let field_extension = FieldExtension::Cubic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 80; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_1, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + let trace_length = 2_usize.pow(16); + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_2, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert!(security_1 <= security_2); +} + +#[test] +fn num_fri_queries() { + let field_extension = FieldExtension::Cubic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 60; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_1, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + let num_queries = 80; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_2, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert!(security_1 < security_2); +} + +#[test] +fn blowup_factor() { + let field_extension = FieldExtension::Cubic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 127; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 30; + let collision_resistance = 128; + let trace_length = 2_usize.pow(20); + let num_committed_polys = 2; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_1, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + let blowup_factor = 16; + + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_2, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert!(security_1 < security_2); +} + +#[test] +fn deep_batching_method_udr() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 255; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 120; + let collision_resistance = 128; + let trace_length = 2_usize.pow(16); + let num_committed_polys = 1 << 1; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Algebraic, + ); + let ProvenSecurity { + unique_decoding: security_1, + list_decoding: _, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_1, 106); + + // when the FRI batching error is not largest when compared to the other soundness error + // terms, increasing the number of committed polynomials might not lead to a degradation + // in the round-by-round soundness of the protocol + let num_committed_polys = 1 << 2; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Algebraic, + ); + let ProvenSecurity { + unique_decoding: security_2, + list_decoding: _, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_2, 106); + + // but after a certain point, there will be a degradation + let num_committed_polys = 1 << 5; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Algebraic, + ); + let ProvenSecurity { + unique_decoding: security_2, + list_decoding: _, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_2, 104); + + // and this degradation is on the order of log2(N - 1) where N is the number of + // committed polynomials + let num_committed_polys = num_committed_polys << 3; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Algebraic, + ); + let ProvenSecurity { + unique_decoding: security_2, + list_decoding: _, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_2, 101); +} + +#[test] +fn deep_batching_method_ldr() { + let field_extension = FieldExtension::Cubic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 255; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 120; + let collision_resistance = 128; + let trace_length = 2_usize.pow(22); + let num_committed_polys = 1 << 1; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Algebraic, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_1, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_1, 128); + + // increasing the number of committed polynomials might lead to a degradation + // in the round-by-round soundness of the protocol on the order of log2(N - 1) where + // N is the number of committed polynomials. This happens when the FRI batching error + // is the largest among all errors + let num_committed_polys = 1 << 8; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Algebraic, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_2, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + // with improved Johnson-regime bounds, degradation may occur only for very large N; + // ensure non-increase when increasing the number of committed polynomials + assert!(security_2 <= security_1); +} + +#[test] +fn constraints_batching_method_udr() { + let field_extension = FieldExtension::Quadratic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 2; + let fri_remainder_max_degree = 255; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 120; + let collision_resistance = 128; + let trace_length = 2_usize.pow(16); + let num_committed_polys = 1 << 1; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: security_1, + list_decoding: _, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_1, 108); + + // when the total number of constraints is on the order of the size of the LDE domain size + // there is no degradation in the soundness error when using algebraic/curve batching + // to batch constraints + let num_constraints = trace_length * blowup_factor; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Algebraic, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: security_2, + list_decoding: _, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_2, 108); + + // but after a certain point, there will be a degradation + let num_constraints = (trace_length * blowup_factor) << 2; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Algebraic, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: security_2, + list_decoding: _, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_2, 107); + + // and this degradation is on the order of log2(C - 1) where C is the total number of + // constraints + let num_constraints = num_constraints << 2; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Algebraic, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: security_2, + list_decoding: _, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_2, 105); +} + +#[test] +fn constraints_batching_method_ldr() { + let field_extension = FieldExtension::Cubic; + let base_field_bits = BaseElement::MODULUS_BITS; + let fri_folding_factor = 8; + let fri_remainder_max_degree = 255; + let grinding_factor = 20; + let blowup_factor = 8; + let num_queries = 120; + let collision_resistance = 128; + let trace_length = 2_usize.pow(22); + let num_committed_polys = 1 << 1; + let num_constraints = 100; + + let mut options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Linear, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_1, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_1, 128); + + // when the total number of constraints is on the order of the size of the LDE domain size + // square there is no degradation in the soundness error when using algebraic/curve batching + // to batch constraints + let num_constraints = (trace_length * blowup_factor).pow(2); + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Algebraic, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_2, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_2, 128); + + // and we have a good margin until we see any degradation in the soundness error + let num_constraints = num_constraints << 12; + options = ProofOptions::new( + num_queries, + blowup_factor, + grinding_factor, + field_extension, + fri_folding_factor as usize, + fri_remainder_max_degree as usize, + BatchingMethod::Algebraic, + BatchingMethod::Linear, + ); + let ProvenSecurity { + unique_decoding: _, + list_decoding: security_3, + } = ProvenSecurity::compute( + &options, + base_field_bits, + trace_length, + collision_resistance, + num_constraints, + num_committed_polys, + ); + + assert_eq!(security_3, 125); +} + +#[test] +fn ldr_lower_order_term_negligible() { + // Show the lower-order term (proportional to (m + 0.5) * γ * ρ) contributes negligibly + // to the commit-phase bound in the Johnson regime, using γ ≤ J(δ) = 1 - sqrt(ρ). + // The bit impact is Δ_bits = log2(1 + R) where R = (3/2) * (γ * ρ) / (m + 0.5)^4. + + // Conservative concrete example from the comment: m = 3, ρ = 1/2. + let m = 3.0; + let rho = 0.5; + let gamma = 1.0 - super::sqrt(rho); + let a = m + 0.5; + let r = 1.5 * (gamma * rho) / super::powf(a, 4.0); + let delta_bits = super::log2(1.0 + r); + assert!(delta_bits < 0.005); + + // Typical ranges: blowup in {2,4,8,16} ⇒ ρ ∈ {1/2,1/4,1/8,1/16}; + // m in {3,6,12,20}. The bound should stay well below 0.005 bits. + for blowup in [2_usize, 4, 8, 16] { + let rho = 1.0 / (blowup as f64); + let gamma = 1.0 - super::sqrt(rho); + for m in [3.0_f64, 6.0, 12.0, 20.0] { + let a = m + 0.5; + let r = 1.5 * (gamma * rho) / super::powf(a, 4.0); + let delta_bits = super::log2(1.0 + r); + assert!(delta_bits < 0.005); + } + } +}