#![forbid(unsafe_code)]
#![allow(clippy::too_many_arguments)]
#![warn(clippy::cast_possible_truncation)]
mod helpers;
pub use helpers::*;
mod hash;
use hash::*;
#[cfg(test)]
mod tests;
use console::{
account::Address,
prelude::{anyhow, bail, cfg_iter, ensure, has_duplicates, Network, Result, ToBytes},
};
use snarkvm_algorithms::{
fft::{DensePolynomial, EvaluationDomain},
polycommit::kzg10::{UniversalParams as SRS, KZG10},
};
use snarkvm_curves::PairingEngine;
use snarkvm_fields::Zero;
use snarkvm_synthesizer_snark::UniversalSRS;
use aleo_std::prelude::*;
use std::sync::Arc;
#[cfg(not(feature = "serial"))]
use rayon::prelude::*;
#[derive(Clone)]
pub enum CoinbasePuzzle<N: Network> {
Prover(Arc<CoinbaseProvingKey<N>>),
Verifier(Arc<CoinbaseVerifyingKey<N>>),
}
impl<N: Network> CoinbasePuzzle<N> {
#[cfg(any(test, feature = "setup"))]
pub fn setup(config: PuzzleConfig) -> Result<SRS<N::PairingCurve>> {
let total_degree = (2 * config.degree - 1).try_into()?;
let srs = KZG10::load_srs(total_degree)?;
Ok(srs)
}
pub fn load() -> Result<Self> {
let max_degree = N::COINBASE_PUZZLE_DEGREE;
let universal_srs = UniversalSRS::<N>::load()?;
Self::trim(&*universal_srs, PuzzleConfig { degree: max_degree })
}
pub fn trim(srs: &SRS<N::PairingCurve>, config: PuzzleConfig) -> Result<Self> {
let product_domain = Self::product_domain(config.degree)?;
let lagrange_basis_at_beta_g = srs.lagrange_basis(product_domain)?;
let fft_precomputation = product_domain.precompute_fft();
let product_domain_elements = product_domain.elements().collect();
let vk = CoinbaseVerifyingKey::<N> {
g: srs.power_of_beta_g(0)?,
gamma_g: <N::PairingCurve as PairingEngine>::G1Affine::zero(), h: srs.h,
beta_h: srs.beta_h(),
prepared_h: srs.prepared_h.clone(),
prepared_beta_h: srs.prepared_beta_h.clone(),
};
let pk = CoinbaseProvingKey {
product_domain,
product_domain_elements,
lagrange_basis_at_beta_g,
fft_precomputation,
verifying_key: vk,
};
Ok(Self::Prover(Arc::new(pk)))
}
pub fn prove(
&self,
epoch_challenge: &EpochChallenge<N>,
address: Address<N>,
nonce: u64,
minimum_proof_target: Option<u64>,
) -> Result<ProverSolution<N>> {
let pk = match self {
Self::Prover(coinbase_proving_key) => coinbase_proving_key,
Self::Verifier(_) => bail!("Cannot prove the coinbase puzzle with a verifier"),
};
let polynomial = Self::prover_polynomial(epoch_challenge, address, nonce)?;
let product_evaluations = {
let polynomial_evaluations = pk.product_domain.in_order_fft_with_pc(&polynomial, &pk.fft_precomputation);
pk.product_domain.mul_polynomials_in_evaluation_domain(
polynomial_evaluations,
&epoch_challenge.epoch_polynomial_evaluations().evaluations,
)?
};
let (commitment, _rand) = KZG10::commit_lagrange(&pk.lagrange_basis(), &product_evaluations, None, None)?;
let partial_solution = PartialSolution::new(address, nonce, commitment);
if let Some(minimum_target) = minimum_proof_target {
let proof_target = partial_solution.to_target()?;
ensure!(
proof_target >= minimum_target,
"Prover solution was below the necessary proof target ({proof_target} < {minimum_target})"
);
}
let point = hash_commitment(&commitment)?;
let product_eval_at_point = polynomial.evaluate(point) * epoch_challenge.epoch_polynomial().evaluate(point);
let proof = KZG10::open_lagrange(
&pk.lagrange_basis(),
pk.product_domain_elements(),
&product_evaluations,
point,
product_eval_at_point,
)?;
ensure!(!proof.is_hiding(), "The prover solution must contain a non-hiding proof");
debug_assert!(KZG10::check(&pk.verifying_key, &commitment, point, product_eval_at_point, &proof)?);
Ok(ProverSolution::new(partial_solution, proof))
}
pub fn check_solutions(
&self,
solutions: &CoinbaseSolution<N>,
epoch_challenge: &EpochChallenge<N>,
proof_target: u64,
) -> Result<()> {
let timer = timer!("CoinbasePuzzle::verify");
ensure!(!solutions.is_empty(), "There are no solutions to verify for the coinbase puzzle");
if solutions.len() > N::MAX_SOLUTIONS {
bail!(
"The solutions exceed the allowed number of partial solutions. ({} > {})",
solutions.len(),
N::MAX_SOLUTIONS
);
}
if has_duplicates(solutions.puzzle_commitments()) {
bail!("The solutions contain duplicate puzzle commitments");
}
lap!(timer, "Perform initial checks");
if !cfg_iter!(solutions).all(|(_, solution)| {
solution.verify(self.coinbase_verifying_key(), epoch_challenge, proof_target).unwrap_or(false)
}) {
bail!("The solutions contain an invalid prover solution");
}
finish!(timer, "Verify each solution");
Ok(())
}
pub fn coinbase_proving_key(&self) -> Result<&CoinbaseProvingKey<N>> {
match self {
Self::Prover(coinbase_proving_key) => Ok(coinbase_proving_key),
Self::Verifier(_) => bail!("Cannot fetch the coinbase proving key with a verifier"),
}
}
pub fn coinbase_verifying_key(&self) -> &CoinbaseVerifyingKey<N> {
match self {
Self::Prover(coinbase_proving_key) => &coinbase_proving_key.verifying_key,
Self::Verifier(coinbase_verifying_key) => coinbase_verifying_key,
}
}
}
impl<N: Network> CoinbasePuzzle<N> {
pub(crate) fn product_domain(degree: u32) -> Result<EvaluationDomain<N::Field>> {
ensure!(degree != 0, "Degree cannot be zero");
let num_coefficients = degree.checked_add(1).ok_or_else(|| anyhow!("Degree is too large"))?;
let product_num_coefficients = num_coefficients
.checked_mul(2)
.and_then(|t| t.checked_sub(1))
.ok_or_else(|| anyhow!("Degree is too large"))?;
assert_eq!(product_num_coefficients, 2 * degree + 1);
let product_domain =
EvaluationDomain::new(product_num_coefficients.try_into()?).ok_or_else(|| anyhow!("Invalid degree"))?;
assert_eq!(product_domain.size(), (product_num_coefficients as usize).checked_next_power_of_two().unwrap());
Ok(product_domain)
}
fn prover_polynomial(
epoch_challenge: &EpochChallenge<N>,
address: Address<N>,
nonce: u64,
) -> Result<DensePolynomial<<N::PairingCurve as PairingEngine>::Fr>> {
let input = {
let mut bytes = [0u8; 76];
epoch_challenge.epoch_number().write_le(&mut bytes[..4])?;
epoch_challenge.epoch_block_hash().write_le(&mut bytes[4..36])?;
address.write_le(&mut bytes[36..68])?;
nonce.write_le(&mut bytes[68..])?;
bytes
};
Ok(hash_to_polynomial::<<N::PairingCurve as PairingEngine>::Fr>(&input, epoch_challenge.degree()))
}
}