snarkvm_synthesizer_coinbase/
lib.rs1#![forbid(unsafe_code)]
16#![allow(clippy::too_many_arguments)]
17#![warn(clippy::cast_possible_truncation)]
18
19mod helpers;
20pub use helpers::*;
21
22mod hash;
23use hash::*;
24
25#[cfg(test)]
26mod tests;
27
28use console::{
29 account::Address,
30 prelude::{anyhow, bail, cfg_iter, ensure, has_duplicates, Network, Result, ToBytes},
31 program::cfg_into_iter,
32};
33use snarkvm_algorithms::{
34 fft::{DensePolynomial, EvaluationDomain},
35 msm::VariableBase,
36 polycommit::kzg10::{KZGCommitment, UniversalParams as SRS, KZG10},
37};
38use snarkvm_curves::PairingEngine;
39use snarkvm_fields::{PrimeField, Zero};
40use snarkvm_synthesizer_snark::UniversalSRS;
41use snarkvm_utilities::cfg_zip_fold;
42
43use std::sync::Arc;
44
45#[cfg(feature = "serial")]
46use itertools::Itertools;
47#[cfg(not(feature = "serial"))]
48use rayon::prelude::*;
49
50#[derive(Clone)]
51pub enum CoinbasePuzzle<N: Network> {
52 Prover(Arc<CoinbaseProvingKey<N>>),
54 Verifier(Arc<CoinbaseVerifyingKey<N>>),
56}
57
58impl<N: Network> CoinbasePuzzle<N> {
59 #[cfg(any(test, feature = "setup"))]
61 pub fn setup(config: PuzzleConfig) -> Result<SRS<N::PairingCurve>> {
62 let total_degree = (2 * config.degree - 1).try_into()?;
65 let srs = KZG10::load_srs(total_degree)?;
66 Ok(srs)
67 }
68
69 pub fn load() -> Result<Self> {
71 let max_degree = N::COINBASE_PUZZLE_DEGREE;
72 let universal_srs = UniversalSRS::<N>::load()?;
74 Self::trim(&*universal_srs, PuzzleConfig { degree: max_degree })
76 }
77
78 pub fn trim(srs: &SRS<N::PairingCurve>, config: PuzzleConfig) -> Result<Self> {
79 let product_domain = Self::product_domain(config.degree)?;
86
87 let lagrange_basis_at_beta_g = srs.lagrange_basis(product_domain)?;
88 let fft_precomputation = product_domain.precompute_fft();
89 let product_domain_elements = product_domain.elements().collect();
90
91 let vk = CoinbaseVerifyingKey::<N> {
92 g: srs.power_of_beta_g(0)?,
93 gamma_g: <N::PairingCurve as PairingEngine>::G1Affine::zero(), h: srs.h,
95 beta_h: srs.beta_h(),
96 prepared_h: srs.prepared_h.clone(),
97 prepared_beta_h: srs.prepared_beta_h.clone(),
98 };
99
100 let pk = CoinbaseProvingKey {
101 product_domain,
102 product_domain_elements,
103 lagrange_basis_at_beta_g,
104 fft_precomputation,
105 verifying_key: vk,
106 };
107
108 Ok(Self::Prover(Arc::new(pk)))
109 }
110
111 pub fn prove(
113 &self,
114 epoch_challenge: &EpochChallenge<N>,
115 address: Address<N>,
116 nonce: u64,
117 minimum_proof_target: Option<u64>,
118 ) -> Result<ProverSolution<N>> {
119 let pk = match self {
121 Self::Prover(coinbase_proving_key) => coinbase_proving_key,
122 Self::Verifier(_) => bail!("Cannot prove the coinbase puzzle with a verifier"),
123 };
124
125 let polynomial = Self::prover_polynomial(epoch_challenge, address, nonce)?;
126
127 let product_evaluations = {
128 let polynomial_evaluations = pk.product_domain.in_order_fft_with_pc(&polynomial, &pk.fft_precomputation);
129 let product_evaluations = pk.product_domain.mul_polynomials_in_evaluation_domain(
130 polynomial_evaluations,
131 &epoch_challenge.epoch_polynomial_evaluations().evaluations,
132 );
133 product_evaluations
134 };
135 let (commitment, _rand) = KZG10::commit_lagrange(&pk.lagrange_basis(), &product_evaluations, None, None)?;
136
137 let partial_solution = PartialSolution::new(address, nonce, commitment);
138
139 if let Some(minimum_target) = minimum_proof_target {
141 let proof_target = partial_solution.to_target()?;
142 ensure!(
143 proof_target >= minimum_target,
144 "Prover solution was below the necessary proof target ({proof_target} < {minimum_target})"
145 );
146 }
147
148 let point = hash_commitment(&commitment)?;
149 let product_eval_at_point = polynomial.evaluate(point) * epoch_challenge.epoch_polynomial().evaluate(point);
150
151 let proof = KZG10::open_lagrange(
152 &pk.lagrange_basis(),
153 pk.product_domain_elements(),
154 &product_evaluations,
155 point,
156 product_eval_at_point,
157 )?;
158 ensure!(!proof.is_hiding(), "The prover solution must contain a non-hiding proof");
159
160 debug_assert!(KZG10::check(&pk.verifying_key, &commitment, point, product_eval_at_point, &proof)?);
161
162 Ok(ProverSolution::new(partial_solution, proof))
163 }
164
165 pub fn accumulate_unchecked(
170 &self,
171 epoch_challenge: &EpochChallenge<N>,
172 prover_solutions: &[ProverSolution<N>],
173 ) -> Result<CoinbaseSolution<N>> {
174 if prover_solutions.is_empty() {
176 bail!("Cannot accumulate an empty list of prover solutions.");
177 }
178
179 if prover_solutions.len() > N::MAX_PROVER_SOLUTIONS {
181 bail!(
182 "Cannot accumulate beyond {} prover solutions, found {}.",
183 N::MAX_PROVER_SOLUTIONS,
184 prover_solutions.len()
185 );
186 }
187
188 let pk = match self {
190 Self::Prover(coinbase_proving_key) => coinbase_proving_key,
191 Self::Verifier(_) => bail!("Cannot accumulate the coinbase puzzle with a verifier"),
192 };
193 ensure!(!has_duplicates(prover_solutions), "Cannot accumulate duplicate prover solutions");
194
195 let (prover_polynomials, partial_solutions): (Vec<_>, Vec<_>) = cfg_iter!(prover_solutions)
196 .filter_map(|solution| {
197 if solution.proof().is_hiding() {
198 return None;
199 }
200 let polynomial = solution.to_prover_polynomial(epoch_challenge).ok()?;
201 Some((polynomial, PartialSolution::new(solution.address(), solution.nonce(), solution.commitment())))
202 })
203 .unzip();
204
205 let mut challenges = hash_commitments(partial_solutions.iter().map(|solution| *solution.commitment()))?;
207 ensure!(challenges.len() == partial_solutions.len() + 1, "Invalid number of challenge points");
208
209 let accumulator_point = match challenges.pop() {
211 Some(point) => point,
212 None => bail!("Missing the accumulator challenge point"),
213 };
214
215 let zero = DensePolynomial::zero;
217 let accumulated_prover_polynomial = cfg_zip_fold!(
218 cfg_into_iter!(prover_polynomials),
219 challenges,
220 zero,
221 |mut accumulator, (mut prover_polynomial, challenge)| {
222 prover_polynomial *= challenge;
223 accumulator += &prover_polynomial;
224 accumulator
225 },
226 DensePolynomial<_>
227 );
228 let product_eval_at_challenge_point = accumulated_prover_polynomial.evaluate(accumulator_point)
229 * epoch_challenge.epoch_polynomial().evaluate(accumulator_point);
230
231 let product_evals = {
233 let accumulated_polynomial_evaluations =
234 pk.product_domain.in_order_fft_with_pc(&accumulated_prover_polynomial.coeffs, &pk.fft_precomputation);
235 pk.product_domain.mul_polynomials_in_evaluation_domain(
236 accumulated_polynomial_evaluations,
237 &epoch_challenge.epoch_polynomial_evaluations().evaluations,
238 )
239 };
240
241 let proof = KZG10::open_lagrange(
243 &pk.lagrange_basis(),
244 pk.product_domain_elements(),
245 &product_evals,
246 accumulator_point,
247 product_eval_at_challenge_point,
248 )?;
249
250 if proof.is_hiding() {
252 bail!("The coinbase proof must be non-hiding");
253 }
254
255 Ok(CoinbaseSolution::new(partial_solutions, proof))
257 }
258
259 pub fn verify(
261 &self,
262 coinbase_solution: &CoinbaseSolution<N>,
263 epoch_challenge: &EpochChallenge<N>,
264 coinbase_target: u64,
265 proof_target: u64,
266 ) -> Result<bool> {
267 if coinbase_solution.is_empty() {
269 bail!("The coinbase solution does not contain any partial solutions");
270 }
271
272 if coinbase_solution.len() > N::MAX_PROVER_SOLUTIONS {
274 bail!(
275 "The coinbase solution exceeds the allowed number of partial solutions. ({} > {})",
276 coinbase_solution.len(),
277 N::MAX_PROVER_SOLUTIONS
278 );
279 }
280
281 if coinbase_solution.proof().is_hiding() {
283 bail!("The coinbase proof must be non-hiding");
284 }
285
286 if coinbase_solution.to_cumulative_proof_target()? < coinbase_target as u128 {
288 bail!("The coinbase proof does not meet the coinbase target");
289 }
290
291 if has_duplicates(coinbase_solution.puzzle_commitments()) {
293 bail!("The coinbase solution contains duplicate puzzle commitments");
294 }
295
296 let prover_polynomials = cfg_iter!(coinbase_solution.partial_solutions())
298 .map(|solution| match solution.to_target()? >= proof_target {
300 true => solution.to_prover_polynomial(epoch_challenge),
302 false => bail!("Prover puzzle does not meet the proof target requirements."),
303 })
304 .collect::<Result<Vec<_>>>()?;
305
306 let mut challenge_points =
308 hash_commitments(coinbase_solution.partial_solutions().iter().map(|solution| *solution.commitment()))?;
309 ensure!(
310 challenge_points.len() == coinbase_solution.partial_solutions().len() + 1,
311 "Invalid number of challenge points"
312 );
313
314 let accumulator_point = match challenge_points.pop() {
316 Some(point) => point,
317 None => bail!("Missing the accumulator challenge point"),
318 };
319
320 let mut accumulator_evaluation = cfg_zip_fold!(
322 cfg_iter!(prover_polynomials),
323 &challenge_points,
324 <N::PairingCurve as PairingEngine>::Fr::zero,
325 |accumulator, (prover_polynomial, challenge_point)| {
326 accumulator + (prover_polynomial.evaluate(accumulator_point) * challenge_point)
327 },
328 _
329 );
330 accumulator_evaluation *= &epoch_challenge.epoch_polynomial().evaluate(accumulator_point);
331
332 let commitments: Vec<_> =
334 cfg_iter!(coinbase_solution.partial_solutions()).map(|solution| solution.commitment().0).collect();
335 let fs_challenges = challenge_points.into_iter().map(|f| f.to_bigint()).collect::<Vec<_>>();
336 let accumulator_commitment =
337 KZGCommitment::<N::PairingCurve>(VariableBase::msm(&commitments, &fs_challenges).into());
338
339 let coinbase_verifying_key = match self {
341 Self::Prover(coinbase_proving_key) => &coinbase_proving_key.verifying_key,
342 Self::Verifier(coinbase_verifying_key) => coinbase_verifying_key,
343 };
344
345 Ok(KZG10::check(
347 coinbase_verifying_key,
348 &accumulator_commitment,
349 accumulator_point,
350 accumulator_evaluation,
351 coinbase_solution.proof(),
352 )?)
353 }
354
355 pub fn coinbase_proving_key(&self) -> Result<&CoinbaseProvingKey<N>> {
357 match self {
358 Self::Prover(coinbase_proving_key) => Ok(coinbase_proving_key),
359 Self::Verifier(_) => bail!("Cannot fetch the coinbase proving key with a verifier"),
360 }
361 }
362
363 pub fn coinbase_verifying_key(&self) -> &CoinbaseVerifyingKey<N> {
365 match self {
366 Self::Prover(coinbase_proving_key) => &coinbase_proving_key.verifying_key,
367 Self::Verifier(coinbase_verifying_key) => coinbase_verifying_key,
368 }
369 }
370}
371
372impl<N: Network> CoinbasePuzzle<N> {
373 pub(crate) fn product_domain(degree: u32) -> Result<EvaluationDomain<N::Field>> {
376 ensure!(degree != 0, "Degree cannot be zero");
377 let num_coefficients = degree.checked_add(1).ok_or_else(|| anyhow!("Degree is too large"))?;
378 let product_num_coefficients = num_coefficients
379 .checked_mul(2)
380 .and_then(|t| t.checked_sub(1))
381 .ok_or_else(|| anyhow!("Degree is too large"))?;
382 assert_eq!(product_num_coefficients, 2 * degree + 1);
383 let product_domain =
384 EvaluationDomain::new(product_num_coefficients.try_into()?).ok_or_else(|| anyhow!("Invalid degree"))?;
385 assert_eq!(product_domain.size(), (product_num_coefficients as usize).checked_next_power_of_two().unwrap());
386 Ok(product_domain)
387 }
388
389 fn prover_polynomial(
391 epoch_challenge: &EpochChallenge<N>,
392 address: Address<N>,
393 nonce: u64,
394 ) -> Result<DensePolynomial<<N::PairingCurve as PairingEngine>::Fr>> {
395 let input = {
396 let mut bytes = [0u8; 76];
397 bytes[..4].copy_from_slice(&epoch_challenge.epoch_number().to_bytes_le()?);
398 bytes[4..36].copy_from_slice(&epoch_challenge.epoch_block_hash().to_bytes_le()?);
399 bytes[36..68].copy_from_slice(&address.to_bytes_le()?);
400 bytes[68..].copy_from_slice(&nonce.to_le_bytes());
401 bytes
402 };
403 Ok(hash_to_polynomial::<<N::PairingCurve as PairingEngine>::Fr>(&input, epoch_challenge.degree()))
404 }
405}