iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

prover.rs (27376B)


      1 use std::sync::atomic::{AtomicBool, Ordering};
      2 
      3 use kyn_vdf::{Form, KynVdfError, get_b};
      4 use num_bigint::{BigInt, BigUint};
      5 use num_traits::{One, ToPrimitive};
      6 
      7 use super::{VdfProgressPhase, limb_arithmetic};
      8 
      9 // Keep peak prover memory bounded. Larger parameter sets retain the old
     10 // constant-memory algorithm instead of attempting an attacker-sized allocation.
     11 const MAX_CHECKPOINTS: u64 = 262_144;
     12 const MAX_BUCKETS: u64 = 65_536;
     13 const INVALID_BUCKET: usize = usize::MAX;
     14 
     15 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     16 struct ProofParameters {
     17     k: u32,
     18     l: u64,
     19     checkpoint_count: u64,
     20     bucket_count: u64,
     21 }
     22 
     23 #[derive(Clone, Copy)]
     24 struct ClassGroup<'a> {
     25     discriminant: &'a BigInt,
     26     threshold: &'a BigInt,
     27 }
     28 
     29 struct CheckpointProofInput<'a> {
     30     generator: &'a Form,
     31     output: &'a Form,
     32     checkpoints: &'a [limb_arithmetic::LimbForm],
     33     rounds: u64,
     34     parameters: ProofParameters,
     35 }
     36 
     37 #[cfg(test)]
     38 pub(super) fn prove(
     39     discriminant: &BigInt,
     40     generator: &Form,
     41     threshold: &BigInt,
     42     rounds: u64,
     43     progress: impl FnMut(VdfProgressPhase, u64),
     44 ) -> Result<(Form, Form), KynVdfError> {
     45     let cancelled = AtomicBool::new(false);
     46     prove_cancellable(
     47         discriminant,
     48         generator,
     49         threshold,
     50         rounds,
     51         progress,
     52         &cancelled,
     53     )?
     54     .ok_or_else(|| arithmetic_error("non-cancellable VDF was cancelled"))
     55 }
     56 
     57 pub(super) fn prove_cancellable(
     58     discriminant: &BigInt,
     59     generator: &Form,
     60     threshold: &BigInt,
     61     rounds: u64,
     62     progress: impl FnMut(VdfProgressPhase, u64),
     63     cancelled: &AtomicBool,
     64 ) -> Result<Option<(Form, Form)>, KynVdfError> {
     65     let group = ClassGroup {
     66         discriminant,
     67         threshold,
     68     };
     69     let parameters = ProofParameters::for_rounds(rounds);
     70     if parameters.checkpoint_count > MAX_CHECKPOINTS || parameters.bucket_count > MAX_BUCKETS {
     71         return prove_constant_memory_cancellable(
     72             discriminant,
     73             generator,
     74             threshold,
     75             rounds,
     76             progress,
     77             cancelled,
     78         );
     79     }
     80 
     81     prove_checkpointed_cancellable(group, generator, rounds, parameters, progress, cancelled)
     82 }
     83 
     84 fn prove_checkpointed_cancellable(
     85     group: ClassGroup<'_>,
     86     generator: &Form,
     87     rounds: u64,
     88     parameters: ProofParameters,
     89     mut progress: impl FnMut(VdfProgressPhase, u64),
     90     cancelled: &AtomicBool,
     91 ) -> Result<Option<(Form, Form)>, KynVdfError> {
     92     let checkpoint_capacity = usize::try_from(parameters.checkpoint_count)
     93         .map_err(|_| arithmetic_error("checkpoint count does not fit in memory"))?;
     94     let checkpoint_stride = u64::from(parameters.k)
     95         .checked_mul(parameters.l)
     96         .ok_or_else(|| arithmetic_error("checkpoint stride overflow"))?;
     97     let limb_discriminant = limb_arithmetic::to_limb(group.discriminant);
     98     let limb_threshold = limb_arithmetic::to_limb(group.threshold);
     99     let mut checkpoints = Vec::with_capacity(checkpoint_capacity);
    100     let mut output = limb_arithmetic::LimbForm::from_form(generator);
    101     let mut output_scratch = limb_arithmetic::LimbFormScratch::default();
    102     for completed_rounds in 1..=rounds {
    103         if cancelled.load(Ordering::Relaxed) {
    104             return Ok(None);
    105         }
    106         if (completed_rounds - 1) % checkpoint_stride == 0 {
    107             checkpoints.push(output.clone());
    108         }
    109         output = output.nudupl_reduce_with_scratch(
    110             &limb_discriminant,
    111             &limb_threshold,
    112             &mut output_scratch,
    113         );
    114         progress(VdfProgressPhase::Output, completed_rounds);
    115     }
    116 
    117     let output = output.into_form();
    118     debug_assert_eq!(checkpoints.len(), checkpoint_capacity);
    119     let Some(proof) = generate_checkpoint_proof_cancellable(
    120         group,
    121         CheckpointProofInput {
    122             generator,
    123             output: &output,
    124             checkpoints: &checkpoints,
    125             rounds,
    126             parameters,
    127         },
    128         &mut progress,
    129         cancelled,
    130     )?
    131     else {
    132         return Ok(None);
    133     };
    134     Ok(Some((output, proof)))
    135 }
    136 
    137 fn generate_checkpoint_proof_cancellable(
    138     group: ClassGroup<'_>,
    139     input: CheckpointProofInput<'_>,
    140     progress: &mut impl FnMut(VdfProgressPhase, u64),
    141     cancelled: &AtomicBool,
    142 ) -> Result<Option<Form>, KynVdfError> {
    143     let CheckpointProofInput {
    144         generator,
    145         output,
    146         checkpoints,
    147         rounds,
    148         parameters,
    149     } = input;
    150     let challenge = get_b(group.discriminant, generator, output)?;
    151     let bucket_count = usize::try_from(parameters.bucket_count)
    152         .map_err(|_| arithmetic_error("bucket count does not fit in memory"))?;
    153     let k0 = parameters.k - parameters.k / 2;
    154     let k1 = parameters.k / 2;
    155     let row_count = 1_u64 << k1;
    156     let column_count = 1_u64 << k0;
    157     let work_per_pass = parameters
    158         .checkpoint_count
    159         .checked_add(row_count)
    160         .and_then(|value| value.checked_add(column_count))
    161         .ok_or_else(|| arithmetic_error("proof progress calculation overflow"))?;
    162     let total_work = parameters
    163         .l
    164         .checked_mul(work_per_pass)
    165         .ok_or_else(|| arithmetic_error("proof progress calculation overflow"))?;
    166     let mut completed_work = 0_u64;
    167     let limb_discriminant = limb_arithmetic::to_limb(group.discriminant);
    168     let limb_threshold = limb_arithmetic::to_limb(group.threshold);
    169     let mut proof = limb_arithmetic::LimbForm::identity(&limb_discriminant);
    170     let mut proof_scratch = limb_arithmetic::LimbFormScratch::default();
    171     let block_step_exponent = u64::from(parameters.k)
    172         .checked_mul(parameters.l)
    173         .ok_or_else(|| arithmetic_error("proof block step overflow"))?;
    174     let block_step = BigUint::from(2_u8).modpow(&BigUint::from(block_step_exponent), &challenge);
    175 
    176     for j in (0..parameters.l).rev() {
    177         if cancelled.load(Ordering::Relaxed) {
    178             return Ok(None);
    179         }
    180         proof = proof.fast_pow_u64_with_scratch(
    181             1_u64 << parameters.k,
    182             &limb_discriminant,
    183             &limb_threshold,
    184             &mut proof_scratch,
    185         );
    186 
    187         let checkpoint_blocks =
    188             get_blocks_for_pass(j, parameters, rounds, &challenge, &block_step)?;
    189         let mut buckets: Vec<Option<limb_arithmetic::LimbForm>> = vec![None; bucket_count];
    190         for (i, checkpoint) in checkpoints.iter().enumerate() {
    191             if cancelled.load(Ordering::Relaxed) {
    192                 return Ok(None);
    193             }
    194             let bucket = checkpoint_blocks[i];
    195             if bucket != INVALID_BUCKET {
    196                 let bucket_form = buckets[bucket].take();
    197                 buckets[bucket] = Some(match bucket_form {
    198                     Some(bucket_form) => bucket_form.compose_unreduced(
    199                         checkpoint,
    200                         &limb_discriminant,
    201                         &limb_threshold,
    202                         &mut proof_scratch,
    203                     ),
    204                     None => checkpoint.clone(),
    205                 });
    206             }
    207 
    208             completed_work += 1;
    209             report_proof_progress(progress, rounds, completed_work, total_work);
    210         }
    211 
    212         for b1 in 0..row_count {
    213             if cancelled.load(Ordering::Relaxed) {
    214                 return Ok(None);
    215             }
    216             let row_start = b1 << k0;
    217             let mut aggregate: Option<limb_arithmetic::LimbForm> = None;
    218             for b0 in 0..column_count {
    219                 if let Some(bucket) = &buckets[(row_start + b0) as usize] {
    220                     aggregate = Some(match aggregate {
    221                         Some(aggregate) => aggregate.compose_unreduced(
    222                             bucket,
    223                             &limb_discriminant,
    224                             &limb_threshold,
    225                             &mut proof_scratch,
    226                         ),
    227                         None => bucket.clone(),
    228                     });
    229                 }
    230             }
    231             if row_start != 0 {
    232                 if let Some(aggregate) = aggregate {
    233                     let aggregate = aggregate.fast_pow_u64_with_scratch(
    234                         row_start,
    235                         &limb_discriminant,
    236                         &limb_threshold,
    237                         &mut proof_scratch,
    238                     );
    239                     proof = proof.compose_unreduced(
    240                         &aggregate,
    241                         &limb_discriminant,
    242                         &limb_threshold,
    243                         &mut proof_scratch,
    244                     );
    245                 }
    246             }
    247             completed_work += 1;
    248             report_proof_progress(progress, rounds, completed_work, total_work);
    249         }
    250 
    251         for b0 in 0..column_count {
    252             if cancelled.load(Ordering::Relaxed) {
    253                 return Ok(None);
    254             }
    255             let mut aggregate: Option<limb_arithmetic::LimbForm> = None;
    256             for b1 in 0..row_count {
    257                 if let Some(bucket) = &buckets[((b1 << k0) + b0) as usize] {
    258                     aggregate = Some(match aggregate {
    259                         Some(aggregate) => aggregate.compose_unreduced(
    260                             bucket,
    261                             &limb_discriminant,
    262                             &limb_threshold,
    263                             &mut proof_scratch,
    264                         ),
    265                         None => bucket.clone(),
    266                     });
    267                 }
    268             }
    269             if b0 != 0 {
    270                 if let Some(aggregate) = aggregate {
    271                     let aggregate = aggregate.fast_pow_u64_with_scratch(
    272                         b0,
    273                         &limb_discriminant,
    274                         &limb_threshold,
    275                         &mut proof_scratch,
    276                     );
    277                     proof = proof.compose_unreduced(
    278                         &aggregate,
    279                         &limb_discriminant,
    280                         &limb_threshold,
    281                         &mut proof_scratch,
    282                     );
    283                 }
    284             }
    285             completed_work += 1;
    286             report_proof_progress(progress, rounds, completed_work, total_work);
    287         }
    288     }
    289 
    290     proof.reduce();
    291     progress(VdfProgressPhase::Proof, rounds);
    292     Ok(Some(proof.into_form()))
    293 }
    294 
    295 fn get_blocks_for_pass(
    296     j: u64,
    297     parameters: ProofParameters,
    298     rounds: u64,
    299     challenge: &BigUint,
    300     step: &BigUint,
    301 ) -> Result<Vec<usize>, KynVdfError> {
    302     let checkpoint_count = usize::try_from(parameters.checkpoint_count)
    303         .map_err(|_| arithmetic_error("checkpoint count does not fit in memory"))?;
    304     let mut blocks = vec![INVALID_BUCKET; checkpoint_count];
    305     if checkpoint_count == 0 {
    306         return Ok(blocks);
    307     }
    308 
    309     let mut index = checkpoint_count - 1;
    310     loop {
    311         let position = checkpoint_position(index, j, parameters.l)?;
    312         if rounds >= block_end(position, parameters.k)? {
    313             break;
    314         }
    315         if index == 0 {
    316             return Ok(blocks);
    317         }
    318         index -= 1;
    319     }
    320 
    321     let position = checkpoint_position(index, j, parameters.l)?;
    322     let exponent = rounds
    323         .checked_sub(block_end(position, parameters.k)?)
    324         .ok_or_else(|| arithmetic_error("proof block exceeds iteration count"))?;
    325     let mut residue = BigUint::from(2_u8).modpow(&BigUint::from(exponent), challenge);
    326     loop {
    327         blocks[index] = block_from_residue(&residue, parameters.k, challenge)?;
    328         if index == 0 {
    329             break;
    330         }
    331         index -= 1;
    332         residue = (residue * step) % challenge;
    333     }
    334 
    335     Ok(blocks)
    336 }
    337 
    338 fn report_proof_progress(
    339     progress: &mut impl FnMut(VdfProgressPhase, u64),
    340     rounds: u64,
    341     completed: u64,
    342     total: u64,
    343 ) {
    344     let proof_rounds = completed.saturating_mul(rounds) / total.max(1);
    345     progress(VdfProgressPhase::Proof, proof_rounds);
    346 }
    347 
    348 fn checkpoint_position(index: usize, j: u64, l: u64) -> Result<u64, KynVdfError> {
    349     u64::try_from(index)
    350         .map_err(|_| arithmetic_error("checkpoint index does not fit in u64"))?
    351         .checked_mul(l)
    352         .and_then(|value| value.checked_add(j))
    353         .ok_or_else(|| arithmetic_error("checkpoint position overflow"))
    354 }
    355 
    356 fn block_end(position: u64, k: u32) -> Result<u64, KynVdfError> {
    357     u64::from(k)
    358         .checked_mul(
    359             position
    360                 .checked_add(1)
    361                 .ok_or_else(|| arithmetic_error("checkpoint position overflow"))?,
    362         )
    363         .ok_or_else(|| arithmetic_error("checkpoint block overflow"))
    364 }
    365 
    366 fn block_from_residue(
    367     residue: &BigUint,
    368     k: u32,
    369     challenge: &BigUint,
    370 ) -> Result<usize, KynVdfError> {
    371     let block = (residue << k) / challenge;
    372     block
    373         .to_usize()
    374         .filter(|value| *value < (1_usize << k))
    375         .ok_or_else(|| arithmetic_error("proof block does not fit in its bucket range"))
    376 }
    377 
    378 fn prove_constant_memory_cancellable(
    379     discriminant: &BigInt,
    380     generator: &Form,
    381     threshold: &BigInt,
    382     rounds: u64,
    383     mut progress: impl FnMut(VdfProgressPhase, u64),
    384     cancelled: &AtomicBool,
    385 ) -> Result<Option<(Form, Form)>, KynVdfError> {
    386     let limb_discriminant = limb_arithmetic::to_limb(discriminant);
    387     let limb_threshold = limb_arithmetic::to_limb(threshold);
    388     let mut output = limb_arithmetic::LimbForm::from_form(generator);
    389     let mut output_scratch = limb_arithmetic::LimbFormScratch::default();
    390     for completed_rounds in 1..=rounds {
    391         if cancelled.load(Ordering::Relaxed) {
    392             return Ok(None);
    393         }
    394         output = output.nudupl_reduce_with_scratch(
    395             &limb_discriminant,
    396             &limb_threshold,
    397             &mut output_scratch,
    398         );
    399         progress(VdfProgressPhase::Output, completed_rounds);
    400     }
    401     let output = output.into_form();
    402 
    403     let challenge = get_b(discriminant, generator, &output)?;
    404     let generator_limb = limb_arithmetic::LimbForm::from_form(generator);
    405     let mut proof = limb_arithmetic::LimbForm::identity(&limb_discriminant);
    406     let mut proof_scratch = limb_arithmetic::LimbFormScratch::default();
    407     let mut remainder = BigUint::one() % &challenge;
    408     for completed_rounds in 1..=rounds {
    409         if cancelled.load(Ordering::Relaxed) {
    410             return Ok(None);
    411         }
    412         let doubled = &remainder << 1_usize;
    413         let carry = doubled >= challenge;
    414         proof = proof.nudupl_reduce_with_scratch(
    415             &limb_discriminant,
    416             &limb_threshold,
    417             &mut proof_scratch,
    418         );
    419         if carry {
    420             proof = proof.nucomp_reduce_with_scratch(
    421                 &generator_limb,
    422                 &limb_discriminant,
    423                 &limb_threshold,
    424                 &mut proof_scratch,
    425             );
    426         }
    427         remainder = doubled % &challenge;
    428         progress(VdfProgressPhase::Proof, completed_rounds);
    429     }
    430 
    431     Ok(Some((output, proof.into_form())))
    432 }
    433 
    434 #[cfg(test)]
    435 fn prove_checkpointed(
    436     group: ClassGroup<'_>,
    437     generator: &Form,
    438     rounds: u64,
    439     parameters: ProofParameters,
    440     progress: impl FnMut(VdfProgressPhase, u64),
    441 ) -> Result<(Form, Form), KynVdfError> {
    442     let cancelled = AtomicBool::new(false);
    443     prove_checkpointed_cancellable(group, generator, rounds, parameters, progress, &cancelled)?
    444         .ok_or_else(|| arithmetic_error("non-cancellable VDF was cancelled"))
    445 }
    446 
    447 #[cfg(test)]
    448 fn generate_checkpoint_proof(
    449     group: ClassGroup<'_>,
    450     generator: &Form,
    451     output: &Form,
    452     checkpoints: &[limb_arithmetic::LimbForm],
    453     rounds: u64,
    454     parameters: ProofParameters,
    455     progress: &mut impl FnMut(VdfProgressPhase, u64),
    456 ) -> Result<Form, KynVdfError> {
    457     let cancelled = AtomicBool::new(false);
    458     generate_checkpoint_proof_cancellable(
    459         group,
    460         CheckpointProofInput {
    461             generator,
    462             output,
    463             checkpoints,
    464             rounds,
    465             parameters,
    466         },
    467         progress,
    468         &cancelled,
    469     )?
    470     .ok_or_else(|| arithmetic_error("non-cancellable VDF was cancelled"))
    471 }
    472 
    473 #[cfg(test)]
    474 fn prove_constant_memory(
    475     discriminant: &BigInt,
    476     generator: &Form,
    477     threshold: &BigInt,
    478     rounds: u64,
    479     progress: impl FnMut(VdfProgressPhase, u64),
    480 ) -> Result<(Form, Form), KynVdfError> {
    481     let cancelled = AtomicBool::new(false);
    482     prove_constant_memory_cancellable(
    483         discriminant,
    484         generator,
    485         threshold,
    486         rounds,
    487         progress,
    488         &cancelled,
    489     )?
    490     .ok_or_else(|| arithmetic_error("non-cancellable VDF was cancelled"))
    491 }
    492 
    493 fn arithmetic_error(message: &str) -> KynVdfError {
    494     KynVdfError::ArithmeticError(message.to_owned())
    495 }
    496 
    497 impl ProofParameters {
    498     #[allow(clippy::approx_constant)] // Match Chia's published parameter heuristic.
    499     fn for_rounds(rounds: u64) -> Self {
    500         let log_memory = 23.253_496_66_f64;
    501         let log_rounds = (rounds as f64).log2();
    502         let l = if log_rounds - log_memory > 0.000_001 {
    503             2_f64.powf(log_memory - 20.0).ceil() as u64
    504         } else {
    505             1
    506         };
    507         let intermediate = rounds as f64 * 0.693_147_1 / (2.0 * l as f64);
    508         let mut k = if intermediate <= 1.0 {
    509             1
    510         } else {
    511             (intermediate.ln() - intermediate.ln().ln() + 0.25)
    512                 .round()
    513                 .max(1.0) as u32
    514         };
    515         if rounds >= 100_000 {
    516             k = k.max(10);
    517         }
    518         Self {
    519             k,
    520             l,
    521             checkpoint_count: rounds.div_ceil(u64::from(k).saturating_mul(l).max(1)),
    522             bucket_count: 1_u64.checked_shl(k).unwrap_or(u64::MAX),
    523         }
    524         .fit_checkpoint_budget(rounds, MAX_CHECKPOINTS)
    525     }
    526 
    527     // k and l only select the checkpoint prover's time-memory tradeoff; they do
    528     // not affect the resulting Wesolowski proof. Increasing l lets large, honest
    529     // protocol workloads stay on the checkpoint path without raising the memory
    530     // cap or falling back to a second round-sized sequential pass.
    531     fn fit_checkpoint_budget(mut self, rounds: u64, max_checkpoints: u64) -> Self {
    532         if max_checkpoints == 0 || self.checkpoint_count <= max_checkpoints {
    533             return self;
    534         }
    535 
    536         let checkpoint_budget_stride = u64::from(self.k).saturating_mul(max_checkpoints).max(1);
    537         self.l = self.l.max(rounds.div_ceil(checkpoint_budget_stride));
    538         let checkpoint_stride = u64::from(self.k).saturating_mul(self.l).max(1);
    539         self.checkpoint_count = rounds.div_ceil(checkpoint_stride);
    540         self
    541     }
    542 }
    543 
    544 #[cfg(test)]
    545 mod tests {
    546     use std::time::Instant;
    547 
    548     use kyn_vdf::{Form, create_discriminant, isqrt_fourth};
    549     use num_traits::Signed;
    550 
    551     use super::{
    552         ClassGroup, ProofParameters, VdfProgressPhase, prove, prove_checkpointed,
    553         prove_constant_memory,
    554     };
    555 
    556     #[test]
    557     fn parameters_match_chia_reference_values() {
    558         for (rounds, k, l, checkpoints, buckets) in [
    559             (1, 1, 1, 1, 2),
    560             (100, 3, 1, 34, 8),
    561             (300, 3, 1, 100, 8),
    562             (100_000, 10, 1, 10_000, 1_024),
    563             (1_000_000, 10, 1, 100_000, 1_024),
    564             (1_500_000, 11, 1, 136_364, 2_048),
    565             (20_000_000, 11, 10, 181_819, 2_048),
    566         ] {
    567             assert_eq!(
    568                 ProofParameters::for_rounds(rounds),
    569                 ProofParameters {
    570                     k,
    571                     l,
    572                     checkpoint_count: checkpoints,
    573                     bucket_count: buckets,
    574                 }
    575             );
    576         }
    577     }
    578 
    579     #[test]
    580     fn network_default_rounds_fit_the_checkpoint_memory_budget() {
    581         let rounds = u64::from(crate::app::DEFAULT_VDF_ROUNDS);
    582         let parameters = ProofParameters::for_rounds(rounds);
    583 
    584         assert_eq!(
    585             parameters,
    586             ProofParameters {
    587                 k: 12,
    588                 l: 22,
    589                 checkpoint_count: 253_788,
    590                 bucket_count: 4_096,
    591             }
    592         );
    593         assert!(parameters.checkpoint_count <= super::MAX_CHECKPOINTS);
    594     }
    595 
    596     #[test]
    597     fn checkpoint_proof_matches_constant_memory_proof() {
    598         let discriminant = create_discriminant(b"iuna-vdf-checkpoint-differential", 1024).unwrap();
    599         let generator = Form::generator(&discriminant).unwrap();
    600         let threshold = isqrt_fourth(&discriminant.abs());
    601 
    602         for rounds in [1, 2, 16, 100, 300, 1_001] {
    603             let checkpoint =
    604                 prove(&discriminant, &generator, &threshold, rounds, |_, _| {}).unwrap();
    605             let constant_memory =
    606                 prove_constant_memory(&discriminant, &generator, &threshold, rounds, |_, _| {})
    607                     .unwrap();
    608 
    609             assert_eq!(checkpoint, constant_memory, "rounds={rounds}");
    610         }
    611     }
    612 
    613     #[test]
    614     fn multi_pass_checkpoint_proof_matches_constant_memory_and_reports_monotonic_progress() {
    615         let rounds = 301_u64;
    616         let discriminant = create_discriminant(b"iuna-vdf-checkpoint-multi-pass", 1024).unwrap();
    617         let generator = Form::generator(&discriminant).unwrap();
    618         let threshold = isqrt_fourth(&discriminant.abs());
    619         let group = ClassGroup {
    620             discriminant: &discriminant,
    621             threshold: &threshold,
    622         };
    623         let parameters = ProofParameters {
    624             k: 3,
    625             l: 2,
    626             checkpoint_count: rounds.div_ceil(6),
    627             bucket_count: 8,
    628         };
    629         let mut proof_progress = Vec::new();
    630 
    631         let checkpoint =
    632             prove_checkpointed(group, &generator, rounds, parameters, |phase, completed| {
    633                 if phase == VdfProgressPhase::Proof {
    634                     proof_progress.push(completed);
    635                 }
    636             })
    637             .unwrap();
    638         let constant_memory =
    639             prove_constant_memory(&discriminant, &generator, &threshold, rounds, |_, _| {})
    640                 .unwrap();
    641 
    642         assert_eq!(checkpoint, constant_memory);
    643         assert!(proof_progress.windows(2).all(|pair| pair[0] <= pair[1]));
    644         assert_eq!(proof_progress.last(), Some(&rounds));
    645     }
    646 
    647     #[test]
    648     fn memory_fitted_checkpoint_proof_matches_constant_memory_proof() {
    649         let rounds = 301_u64;
    650         let discriminant = create_discriminant(b"iuna-vdf-checkpoint-memory-fit", 1024).unwrap();
    651         let generator = Form::generator(&discriminant).unwrap();
    652         let threshold = isqrt_fourth(&discriminant.abs());
    653         let group = ClassGroup {
    654             discriminant: &discriminant,
    655             threshold: &threshold,
    656         };
    657         let parameters = ProofParameters::for_rounds(rounds).fit_checkpoint_budget(rounds, 20);
    658 
    659         assert_eq!(parameters.k, 3);
    660         assert_eq!(parameters.l, 6);
    661         assert_eq!(parameters.checkpoint_count, 17);
    662 
    663         let checkpoint =
    664             prove_checkpointed(group, &generator, rounds, parameters, |_, _| {}).unwrap();
    665         let constant_memory =
    666             prove_constant_memory(&discriminant, &generator, &threshold, rounds, |_, _| {})
    667                 .unwrap();
    668 
    669         assert_eq!(checkpoint, constant_memory);
    670     }
    671 
    672     #[test]
    673     #[ignore = "manual VDF prover benchmark"]
    674     fn benchmark_checkpoint_prover_against_constant_memory() {
    675         let rounds = 100_000_u64;
    676         let discriminant = create_discriminant(b"iuna-vdf-prover-benchmark", 1024).unwrap();
    677         let generator = Form::generator(&discriminant).unwrap();
    678         let threshold = isqrt_fourth(&discriminant.abs());
    679 
    680         let started = Instant::now();
    681         let checkpoint = prove(&discriminant, &generator, &threshold, rounds, |_, _| {}).unwrap();
    682         let checkpoint_elapsed = started.elapsed();
    683         let started = Instant::now();
    684         let constant_memory =
    685             prove_constant_memory(&discriminant, &generator, &threshold, rounds, |_, _| {})
    686                 .unwrap();
    687         let constant_memory_elapsed = started.elapsed();
    688 
    689         assert_eq!(checkpoint, constant_memory);
    690         eprintln!(
    691             "rounds={rounds} checkpoint={checkpoint_elapsed:?} constant_memory={constant_memory_elapsed:?} speedup={:.2}x",
    692             constant_memory_elapsed.as_secs_f64() / checkpoint_elapsed.as_secs_f64()
    693         );
    694     }
    695 
    696     #[test]
    697     #[ignore = "manual VDF phase benchmark"]
    698     fn benchmark_checkpoint_prover_phases() {
    699         let rounds = 100_000_u64;
    700         let discriminant = create_discriminant(b"iuna-vdf-prover-benchmark", 1024).unwrap();
    701         let generator = Form::generator(&discriminant).unwrap();
    702         let threshold = isqrt_fourth(&discriminant.abs());
    703         let group = ClassGroup {
    704             discriminant: &discriminant,
    705             threshold: &threshold,
    706         };
    707         let parameters = ProofParameters::for_rounds(rounds);
    708         let checkpoint_stride = u64::from(parameters.k) * parameters.l;
    709         let mut checkpoints = Vec::with_capacity(parameters.checkpoint_count as usize);
    710         let limb_discriminant = crate::domain::vdf::limb_arithmetic::to_limb(&discriminant);
    711         let limb_threshold = crate::domain::vdf::limb_arithmetic::to_limb(&threshold);
    712         let mut output = crate::domain::vdf::limb_arithmetic::LimbForm::from_form(&generator);
    713         let mut output_scratch = crate::domain::vdf::limb_arithmetic::LimbFormScratch::default();
    714 
    715         let started = Instant::now();
    716         for completed_rounds in 1..=rounds {
    717             if (completed_rounds - 1) % checkpoint_stride == 0 {
    718                 checkpoints.push(output.clone());
    719             }
    720             output = output.nudupl_reduce_with_scratch(
    721                 &limb_discriminant,
    722                 &limb_threshold,
    723                 &mut output_scratch,
    724             );
    725         }
    726         let output = output.into_form();
    727         let output_elapsed = started.elapsed();
    728 
    729         let started = Instant::now();
    730         let proof = super::generate_checkpoint_proof(
    731             group,
    732             &generator,
    733             &output,
    734             &checkpoints,
    735             rounds,
    736             parameters,
    737             &mut |_, _| {},
    738         )
    739         .unwrap();
    740         let proof_elapsed = started.elapsed();
    741 
    742         assert_eq!(
    743             super::prove(&discriminant, &generator, &threshold, rounds, |_, _| {}).unwrap(),
    744             (output, proof)
    745         );
    746         eprintln!(
    747             "rounds={rounds} output={output_elapsed:?} proof={proof_elapsed:?} total={:?}",
    748             output_elapsed + proof_elapsed
    749         );
    750     }
    751     #[test]
    752     #[ignore = "manual VDF square phase benchmark"]
    753     fn benchmark_output_square_phases() {
    754         let rounds = 100_000;
    755         let discriminant = create_discriminant(b"iuna-vdf-prover-benchmark", 1024).unwrap();
    756         let threshold = isqrt_fourth(&discriminant.abs());
    757         let mut output = Form::generator(&discriminant).unwrap();
    758         let mut nudupl_elapsed = std::time::Duration::ZERO;
    759         let mut reduce_elapsed = std::time::Duration::ZERO;
    760 
    761         for _ in 0..rounds {
    762             let started = Instant::now();
    763             output =
    764                 crate::domain::vdf::arithmetic::nudupl_owned(output, &discriminant, &threshold);
    765             nudupl_elapsed += started.elapsed();
    766 
    767             let started = Instant::now();
    768             crate::domain::vdf::reducer::reduce(&mut output);
    769             reduce_elapsed += started.elapsed();
    770         }
    771 
    772         assert!(output.is_reduced());
    773         eprintln!(
    774             "rounds={rounds} nudupl={nudupl_elapsed:?} reduce={reduce_elapsed:?} total={:?}",
    775             nudupl_elapsed + reduce_elapsed
    776         );
    777     }
    778 }