Skip to main content

snarkvm_synthesizer_coinbase/
lib.rs

1// Copyright (C) 2019-2023 Aleo Systems Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7// http://www.apache.org/licenses/LICENSE-2.0
8
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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    /// The prover contains the coinbase puzzle proving key.
53    Prover(Arc<CoinbaseProvingKey<N>>),
54    /// The verifier contains the coinbase puzzle verifying key.
55    Verifier(Arc<CoinbaseVerifyingKey<N>>),
56}
57
58impl<N: Network> CoinbasePuzzle<N> {
59    /// Initializes a new `SRS` for the coinbase puzzle.
60    #[cfg(any(test, feature = "setup"))]
61    pub fn setup(config: PuzzleConfig) -> Result<SRS<N::PairingCurve>> {
62        // The SRS must support committing to the product of two degree `n` polynomials.
63        // Thus, the SRS must support committing to a polynomial of degree `2n - 1`.
64        let total_degree = (2 * config.degree - 1).try_into()?;
65        let srs = KZG10::load_srs(total_degree)?;
66        Ok(srs)
67    }
68
69    /// Load the coinbase puzzle proving and verifying keys.
70    pub fn load() -> Result<Self> {
71        let max_degree = N::COINBASE_PUZZLE_DEGREE;
72        // Load the universal SRS.
73        let universal_srs = UniversalSRS::<N>::load()?;
74        // Trim the universal SRS to the maximum degree.
75        Self::trim(&*universal_srs, PuzzleConfig { degree: max_degree })
76    }
77
78    pub fn trim(srs: &SRS<N::PairingCurve>, config: PuzzleConfig) -> Result<Self> {
79        // As above, we must support committing to the product of two degree `n` polynomials.
80        // Thus, the SRS must support committing to a polynomial of degree `2n - 1`.
81        // Since the upper bound to `srs.powers_of_beta_g` takes as input the number
82        // of coefficients. The degree of the product has `2n - 1` coefficients.
83        //
84        // Hence, we request the powers of beta for the interval [0, 2n].
85        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(), // We don't use gamma_g later on since we are not hiding.
94            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    /// Returns a prover solution to the coinbase puzzle.
112    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        // Retrieve the coinbase proving key.
120        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        // Check that the minimum target is met.
140        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    /// Returns a coinbase solution for the given epoch challenge and prover solutions.
166    ///
167    /// # Note
168    /// This method does *not* check that the prover solutions are valid.
169    pub fn accumulate_unchecked(
170        &self,
171        epoch_challenge: &EpochChallenge<N>,
172        prover_solutions: &[ProverSolution<N>],
173    ) -> Result<CoinbaseSolution<N>> {
174        // Ensure there exists prover solutions.
175        if prover_solutions.is_empty() {
176            bail!("Cannot accumulate an empty list of prover solutions.");
177        }
178
179        // Ensure the number of prover solutions does not exceed `MAX_PROVER_SOLUTIONS`.
180        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        // Retrieve the coinbase proving key.
189        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        // Compute the challenge points.
206        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        // Pop the last challenge as the accumulator challenge point.
210        let accumulator_point = match challenges.pop() {
211            Some(point) => point,
212            None => bail!("Missing the accumulator challenge point"),
213        };
214
215        // Accumulate the prover polynomial.
216        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        // Compute the accumulator polynomial.
232        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        // Compute the coinbase proof.
242        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        // Ensure the coinbase proof is non-hiding.
251        if proof.is_hiding() {
252            bail!("The coinbase proof must be non-hiding");
253        }
254
255        // Return the accumulated proof.
256        Ok(CoinbaseSolution::new(partial_solutions, proof))
257    }
258
259    /// Returns `true` if the coinbase solution is valid.
260    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        // Ensure the coinbase solution is not empty.
268        if coinbase_solution.is_empty() {
269            bail!("The coinbase solution does not contain any partial solutions");
270        }
271
272        // Ensure the number of partial solutions does not exceed `MAX_PROVER_SOLUTIONS`.
273        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        // Ensure the coinbase proof is non-hiding.
282        if coinbase_solution.proof().is_hiding() {
283            bail!("The coinbase proof must be non-hiding");
284        }
285
286        // Ensure the coinbase proof meets the required coinbase target.
287        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        // Ensure the puzzle commitments are unique.
292        if has_duplicates(coinbase_solution.puzzle_commitments()) {
293            bail!("The coinbase solution contains duplicate puzzle commitments");
294        }
295
296        // Compute the prover polynomials.
297        let prover_polynomials = cfg_iter!(coinbase_solution.partial_solutions())
298            // Ensure that each of the prover solutions meets the required proof target.
299            .map(|solution| match solution.to_target()? >= proof_target {
300                // Compute the prover polynomial.
301                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        // Compute the challenge points.
307        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        // Pop the last challenge point as the accumulator challenge point.
315        let accumulator_point = match challenge_points.pop() {
316            Some(point) => point,
317            None => bail!("Missing the accumulator challenge point"),
318        };
319
320        // Compute the accumulator evaluation.
321        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        // Compute the accumulator commitment.
333        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        // Retrieve the coinbase verifying key.
340        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        // Return the verification result.
346        Ok(KZG10::check(
347            coinbase_verifying_key,
348            &accumulator_commitment,
349            accumulator_point,
350            accumulator_evaluation,
351            coinbase_solution.proof(),
352        )?)
353    }
354
355    /// Returns the coinbase proving key.
356    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    /// Returns the coinbase verifying key.
364    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    /// Checks that the degree for the epoch and prover polynomial is within bounds,
374    /// and returns the evaluation domain for the product polynomial.
375    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    /// Returns the prover polynomial for the coinbase puzzle.
390    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}