Skip to content
Snippets Groups Projects
Commit 85fba9da authored by E__Man's Association's avatar E__Man's Association
Browse files

Impl: (phases query + verify) + added benches

parent 0e14468f
No related branches found
No related tags found
No related merge requests found
......@@ -6,21 +6,29 @@ edition = "2021"
[lib]
[dependencies]
ark-ff = "0.4.2"
ark-poly = "0.4.2"
ark-serialize = "0.4.2"
rs_merkle = "1.4.2"
blake3 = { version = "1.5.1", optional = true }
sha3 = { version = "0.10.8", optional = true }
ark-ff = { version = "0.4.2", default-features = false }
ark-poly = { version = "0.4.2", default-features = false }
ark-serialize = { version = "0.4.2", default-features = false }
rs_merkle = { version = "1.4.2", default-features = false }
blake3 = { version = "1.5.1", default-features = false, optional = true }
sha3 = { version = "0.10.8", default-features = false, optional = true }
[dev-dependencies]
winter-fri = "0.9"
winter-utils = "0.9"
winter-rand-utils = "0.9"
winter-math = "0.9"
rand = "0.8.5"
criterion = "0.5.1"
fri_test_utils = { path = "test_utils" }
[features]
sha3 = ["dep:sha3"]
sha3_asm = ["sha3", "sha3/asm"]
blake3 = ["dep:blake3"]
default = ["sha3", "blake3"]
[[bench]]
name = "fri"
harness = false
required-features = ["blake3"]
\ No newline at end of file
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use fri::{algorithms::Blake3, build_proof, commit_polynomial, rng::FriChallenger};
use fri_test_utils::{Fq, BLOWUP_FACTOR, DOMAIN_SIZE, NUM_QUERIES, POLY_COEFFS_LEN};
use rand::{thread_rng, Rng};
fn bench_commit(c: &mut Criterion) {
let mut rng = thread_rng();
c.bench_function("commit", |b| {
b.iter_batched(
|| (0..POLY_COEFFS_LEN).map(|_| rng.gen()).collect(),
|poly| {
let rng = FriChallenger::<Blake3>::default();
let _c = commit_polynomial::<4, Fq, Blake3, _>(poly, rng, BLOWUP_FACTOR, 4);
},
BatchSize::SmallInput,
)
});
}
fn bench_query(c: &mut Criterion) {
let mut rng = thread_rng();
c.bench_function("query", |b| {
b.iter_batched(
|| {
let poly = (0..POLY_COEFFS_LEN).map(|_| rng.gen()).collect();
let mut rng = FriChallenger::<Blake3>::default();
let commitments =
commit_polynomial::<4, Fq, Blake3, _>(poly, &mut rng, BLOWUP_FACTOR, 4);
(rng, commitments)
},
|(rng, commitments)| {
let _c = build_proof(commitments, rng, NUM_QUERIES);
},
BatchSize::SmallInput,
)
});
}
fn bench_verify(c: &mut Criterion) {
let mut rng = thread_rng();
c.bench_function("verify", |b| {
b.iter_batched(
|| {
let poly = (0..POLY_COEFFS_LEN).map(|_| rng.gen()).collect();
let mut rng = FriChallenger::<Blake3>::default();
let commitments =
commit_polynomial::<4, Fq, Blake3, _>(poly, &mut rng, BLOWUP_FACTOR, 4);
build_proof(commitments, &mut rng, NUM_QUERIES)
},
|proof| {
let rng = FriChallenger::<Blake3>::default();
let _c = proof
.verify::<4, _>(rng, NUM_QUERIES, POLY_COEFFS_LEN, DOMAIN_SIZE)
.unwrap();
},
BatchSize::SmallInput,
)
});
}
criterion_group!(benches, bench_commit, bench_query, bench_verify);
criterion_main!(benches);
File moved
//! This module contains implementations of trait [`rs_merkle::Hasher`] for other algorithms
//! than [`Sha2-256`](`rs_merkle::algorithms::Sha256`) and [`Sha2-384`](`rs_merkle::algorithms::Sha384`),
//! which are already provided by [`rs_merkle`].
//!
//! These implementations are opt-in and only provided if the corresponding features are enabled.
#[cfg(feature = "blake3")]
pub mod blake3;
#[cfg(feature = "blake3")]
pub use blake3::Blake3;
#[cfg(feature = "sha3")]
pub mod sha3;
#[cfg(feature = "sha3")]
pub use sha3::{Sha3_256, Sha3_512};
File moved
use std::{borrow::Cow, ops::Index};
//!
use std::{
borrow::{Borrow, Cow},
ops::{Deref, Index},
};
use ark_ff::FftField;
use ark_poly::{
......@@ -7,67 +12,201 @@ use ark_poly::{
use crate::AssertPowerOfTwo;
/// An owned view over folded evaluations.
/// `N` is the folding factor. It should be a power of two.
///
/// It implements [`Deref`] to [`FoldedEvaluationsSlice`], meaning that all the methods
/// on [`FoldedEvaluationsSlice`] are available on `FoldedEvaluations` as well.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct FoldedEvaluations<const N: usize, F>(Vec<F>);
impl<const N: usize, F> Index<usize> for FoldedEvaluations<N, F> {
type Output = [F; N];
fn index(&self, index: usize) -> &Self::Output {
// TODO check if `unwrap_unchecked` is faster
(&self.0[index * N..(index + 1) * N]).try_into().unwrap()
}
}
impl<const N: usize, F> FoldedEvaluations<N, F> {
/// Folds `evaluations` of a polynomial, in such a way that the `N` evaluations necessary
/// to compute the `i`th evaluation in the next FRI layer are gathered into cell `i`.
///
/// This allocates a new vector.
///
/// `evaluations` is the evaluations of the polynomial on the `n`^th roots of unity, where `n` is the
/// len of `evaluations`. If `w` is `F::get_root_of_unity(n).unwrap()`, then `evaluations[i]` is the
/// evaluation at `w^i`.
///
/// See [https://eprint.iacr.org/2022/1216.pdf].
///
/// # Panics
/// This may either panic or have unspecified behaviour if `N` is not a power of two or
/// `evaluations.len()` is not a multiple of `N`.
pub fn new(evaluations: &[F]) -> Self
where
F: Clone,
{
let bound = evaluations.len().div_ceil(N);
Self::check_slice(evaluations);
let bound = evaluations.len() / N;
let mut folded = Vec::with_capacity(evaluations.len());
for i in 0..bound {
folded.extend(evaluations.iter().skip(i).step_by(bound).cloned());
}
Self::from_flat_evaluations(folded)
Self::from_flat_evaluations_unchecked(folded)
}
/// Returns the underlying, flattened vector.
///
/// It contains evaluations such that the items necessary to compute the `i`th evaluation
/// in the next FRI layer are at `N*i..N*(i+1)`.
///
/// This vector can **not** be used as-is as the input of [`crate::utils::to_polynomial`] or other functions
/// that expect ordered evaluations over the roots of unity.
#[inline]
pub fn into_flat_evaluations(self) -> Vec<F> {
self.0
}
/// Wraps a flatten vector of folded evaluations.
///
/// `evaluations` should be the return value of [`Self::into_flat_evaluations`].
///
/// # Panics
/// This may either panic or have unspecified behaviour if `N` is not a power of two or
/// `evaluations.len()` is not a multiple of `N`.
///
/// When debug assertions are disabled, this is currently equivalent to
/// [`Self::from_flat_evaluations_unchecked`], but this may change in the future.
#[inline]
pub fn from_flat_evaluations(evaluations: Vec<F>) -> Self {
assert!(
Self::check_slice(&evaluations);
Self(evaluations)
}
/// Same as [`Self::from_flat_evaluations`], but does not panic.
///
/// `Self::from_flat_evaluations_unchecked(folded_evaluations.into_flat_evaluations())` is a no-op.
///
/// It is incorrect to call this method if `N` is not a power of two or
/// `evaluations.len()` is not a multiple of `N`, but this won't cause undefined behaviour.
#[inline]
pub fn from_flat_evaluations_unchecked(evaluations: Vec<F>) -> Self {
Self(evaluations)
}
/// When debug assertions are enabled, panics if `evaluations.len() % N != 0`.
/// When debug assertions are disabled, this is a no-op.
#[inline]
fn check_slice(evaluations: &[F]) {
debug_assert!(
evaluations.len() % N == 0,
"Domain size must be a multiple of `N`"
);
debug_assert!(
evaluations.len().is_power_of_two(),
"Number of evaluations must be a power of two"
);
Self(evaluations)
}
}
/* #region delegates to `FoldedEvaluationsSlice` */
impl<const N: usize, F> Deref for FoldedEvaluations<N, F> {
type Target = FoldedEvaluationsSlice<N, F>;
#[inline]
pub unsafe fn from_flat_evaluations_unchecked(evaluations: Vec<F>) -> Self {
Self(evaluations)
fn deref(&self) -> &Self::Target {
FoldedEvaluationsSlice::from_flat_evaluations_unchecked(&self.0)
}
}
impl<const N: usize, F> Borrow<FoldedEvaluationsSlice<N, F>> for FoldedEvaluations<N, F> {
#[inline]
fn borrow(&self) -> &FoldedEvaluationsSlice<N, F> {
self
}
}
impl<const N: usize, F> AsRef<FoldedEvaluationsSlice<N, F>> for FoldedEvaluations<N, F> {
#[inline]
fn as_ref(&self) -> &FoldedEvaluationsSlice<N, F> {
self
}
}
impl<const N: usize, F, R: ?Sized> AsRef<R> for FoldedEvaluations<N, F>
where
FoldedEvaluationsSlice<N, F>: AsRef<R>,
{
#[inline]
fn as_ref(&self) -> &R {
(**self).as_ref()
}
}
impl<'a, const N: usize, F> IntoIterator for &'a FoldedEvaluations<N, F> {
type Item = <&'a FoldedEvaluationsSlice<N, F> as IntoIterator>::Item;
type IntoIter = <&'a FoldedEvaluationsSlice<N, F> as IntoIterator>::IntoIter;
#[inline]
fn into_iter(self) -> Self::IntoIter {
(**self).into_iter()
}
}
/* #endregion */
/// A borrowed view over folded evaluations.
/// `N` is the folding factor. It should be a power of two.
///
/// This is an unsized type and must be used behind a pointer like `&`.
///
/// `folded_evaluations[i]` yields a slice of size `N` containing the evaluations necessary to
/// compute the `i`th evaluations in the FRI next layer.
#[derive(PartialEq, Eq, Debug)]
#[repr(transparent)]
pub struct FoldedEvaluationsSlice<const N: usize, F>([F]);
impl<const N: usize, F> Index<usize> for FoldedEvaluationsSlice<N, F> {
type Output = [F; N];
#[inline]
fn index(&self, index: usize) -> &Self::Output {
(&self.0[index * N..(index + 1) * N]).try_into().unwrap()
}
}
impl<const N: usize, F> FoldedEvaluationsSlice<N, F> {
/// Wraps a flattened slice of folded evaluations.
/// See [`FoldedEvaluations::from_flat_evaluations`].
///
/// # Panics
/// This may either panic or have unspecified behaviour if `N` is not a power of two or
/// `evaluations.len()` is not a multiple of `N`.
#[inline]
pub fn from_flat_evaluations<'a>(evaluations: &'a [F]) -> &'a Self {
FoldedEvaluations::<N, _>::check_slice(evaluations);
Self::from_flat_evaluations_unchecked(evaluations)
}
/// Same as [`Self::from_flat_evaluations`], but does not panic.
#[inline]
pub fn from_flat_evaluations_unchecked<'a>(evaluations: &'a [F]) -> &'a Self {
// SAFETY: `Self` and `[F]` have the same layout.
// See [https://rust-lang.github.io/unsafe-code-guidelines/layout/structs-and-tuples.html#single-field-structs]
unsafe { &*(evaluations as *const [F] as *const Self) }
}
/// Yields the underlying slice. See [`FoldedEvaluations::into_flat_evaluations`].
#[inline]
pub fn as_flat_evaluations(&self) -> &[F] {
&self.0
}
/// Returns the size of the domain the evaluations were initially computed on.
#[inline]
pub fn domain_size(&self) -> usize {
self.0.len()
}
/// Returns the size of the folded domain. This corresponds to the number of leaves in the Merkle tree.
#[inline]
pub fn folded_len(&self) -> usize {
self.0.len() / N
}
}
impl<const N: usize, F> AsRef<[[F; N]]> for FoldedEvaluations<N, F> {
impl<const N: usize, F> AsRef<[[F; N]]> for FoldedEvaluationsSlice<N, F> {
#[inline]
fn as_ref(&self) -> &[[F; N]] {
// SAFETY: size_of::<[F; N]>() == N * size_of::<F>().
//
// `self.folded_len()` rounds towards 0, so this is safe even if `self.domain_size()` is not
// a multiple of `N` (but some items will be missing from the resulting slice).
unsafe { core::slice::from_raw_parts(self.0.as_ptr() as *const [F; N], self.folded_len()) }
}
}
impl<'a, const N: usize, F> IntoIterator for &'a FoldedEvaluations<N, F> {
impl<'a, const N: usize, F> IntoIterator for &'a FoldedEvaluationsSlice<N, F> {
type Item = &'a [F; N];
type IntoIter = core::slice::Iter<'a, [F; N]>;
......@@ -80,9 +219,9 @@ impl<'a, const N: usize, F> IntoIterator for &'a FoldedEvaluations<N, F> {
/// Reduces the polynomial by factor `N` using FRI algorithm.
///
/// - `N` is the reduction factor. It must be a power of two. Typical values include 2, 4 and 8.
/// - `evaluations` is the evaluations of the polynomial on the `n`^th roots of unity, where `n` is the
/// len of `evaluations`. `n` must be a power of two strictly greater than the degree-bound of the polynomial.
/// If `w` is `F::get_root_of_unity(n).unwrap()`, `evaluations[i]` is the evaluation at `w^i`.
/// - `evaluations` is the folded evaluations of the polynomial on the `n`^th roots of unity, where `n` is the
/// len of `evaluations`. `n` must be a power of two greater than the degree-bound of the polynomial.
/// See [`FoldedEvaluations::new`].
/// - `alpha` is the "challenge" used to reduce the polynomial.
/// - `domain`, if provided, is the pre-computed evaluation domain of size `N`.
///
......@@ -99,7 +238,6 @@ impl<'a, const N: usize, F> IntoIterator for &'a FoldedEvaluations<N, F> {
///
/// # Credits
/// This is partly based on equation (4) from [https://eprint.iacr.org/2022/1216.pdf].
#[must_use]
pub fn reduce_polynomial<const N: usize, F: FftField>(
evaluations: &FoldedEvaluations<N, F>,
alpha: F,
......@@ -125,19 +263,51 @@ pub fn reduce_polynomial<const N: usize, F: FftField>(
buffer.extend_from_slice(batch);
domain.ifft_in_place(&mut buffer);
let mut factor = F::ONE;
for coeff in &mut buffer {
// FIXME: rust-analyzer fails to infer type of `coeff` on VS Code
*(coeff as &mut F) *= factor;
factor *= offset;
}
offset *= root_inv;
let poly = DensePolynomial { coeffs: buffer };
new_evaluations.push(poly.evaluate(&alpha));
new_evaluations.push(poly.evaluate(&(alpha * offset)));
offset *= root_inv;
buffer = poly.coeffs;
buffer.clear();
}
new_evaluations
}
/// Folds positions. This discards duplicates. This has time complexity `O(l^2)`, where `l`
/// is the len of `positions`.
///
/// - `positions` is a list of positions in a domain of size `n`.
/// The domain should have the following form: `(w^0, ..., w^n)`
/// - `folded_domain_size` is the size of the domain after folding.
///
/// `n` must be a multiple of `folded_domain_size`. Let `n = N * folded_domain_size`.
/// If a position corresponds to `x` in the initial domain, the associated folded position will
/// correspond to `x^N` in the folded domain.
///
/// # Example
/// ```rust
/// use fri::folding::fold_positions;
///
/// // Domain (1, w, ..., w^7), where w^8 = 1
/// let positions = vec![7, 2, 4, 4, 1, 3];
///
/// // Folding factor = 2
/// // Folded domain (1, w^2, w^4, w^6), of size 4
/// let folded_positions = fold_positions(&positions, 4);
///
/// // Position `7` corresponds to `w^7` in the initial domain.
/// // `(w^7)^2 = w^14 = w^6`, which is at position `3` in the folded domain.
/// assert_eq!(folded_positions, vec![3, 2, 0, 1]);
/// ```
pub fn fold_positions(positions: &[usize], folded_domain_size: usize) -> Vec<usize> {
let mask = folded_domain_size - 1;
let mut new_positions = vec![];
for &position in positions {
let pos = position & mask;
if !new_positions.contains(&pos) {
new_positions.push(pos);
}
}
new_positions
}
use std::iter::zip;
use ark_ff::FftField;
use ark_poly::{EvaluationDomain, GeneralEvaluationDomain};
use ark_serialize::CanonicalSerialize;
use prover::FriLayer;
use ark_poly::{
univariate::DensePolynomial, EvaluationDomain, GeneralEvaluationDomain, Polynomial,
};
use folding::{fold_positions, reduce_polynomial, FoldedEvaluationsSlice};
use prover::{FriCommitments, FriLayer};
use rng::ReseedableRng;
use rs_merkle::{Hasher, MerkleProof, MerkleTree};
mod folding;
pub use folding::{reduce_polynomial, FoldedEvaluations};
use rs_merkle::{Hasher, MerkleProof};
use utils::{to_evaluations, to_polynomial, AssertPowerOfTwo, HasherExt};
pub mod algorithms;
pub mod folding;
pub mod prover;
pub use prover::FriCommitments;
pub mod rng;
#[cfg(feature = "blake3")]
pub mod blake3;
#[cfg(feature = "sha3")]
pub mod sha3;
pub mod utils;
#[cfg(test)]
pub mod tests;
struct AssertPowerOfTwo<const N: usize>;
mod tests;
impl<const N: usize> AssertPowerOfTwo<N> {
const OK: () = assert!(N.is_power_of_two(), "`N` must be a power of two");
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum VerifyError {
BadFoldingFactor(usize),
BadDomainSize(usize),
BadDegreeBound(usize),
DegreeTruncation { depth: usize },
CommitmentMismatch { depth: usize },
InvalidFolding { depth: usize },
WrongNumberOfEvaluations { depth: usize },
InvalidRemainderDegree,
InvalidRemainder,
}
trait HasherExt: Hasher {
fn hash_item<S: CanonicalSerialize>(value: &S) -> Self::Hash;
fn hash_many<S: CanonicalSerialize>(values: &[S]) -> Vec<Self::Hash>;
}
impl<H: Hasher> HasherExt for H {
fn hash_item<S: CanonicalSerialize>(value: &S) -> Self::Hash {
let mut bytes = vec![];
value
.serialize_compressed(&mut bytes)
.expect("Serialization failed");
H::hash(&bytes)
}
fn hash_many<S: CanonicalSerialize>(values: &[S]) -> Vec<Self::Hash> {
let mut hashes: Vec<<H as Hasher>::Hash> = Vec::with_capacity(values.len());
let mut bytes = vec![];
for evaluation in values {
evaluation
.serialize_compressed(&mut bytes)
.expect("Serialization failed");
hashes.push(H::hash(&bytes));
bytes.clear();
}
hashes
}
// TODO: serialization!
/// A FRI proof.
pub struct FriProof<F, H: Hasher> {
layers: Vec<FriProofLayer<F, H>>,
remainder: Vec<F>,
}
trait MerkleTreeExt {
fn from_evaluations<S: CanonicalSerialize>(evaluations: &[S]) -> Self;
struct FriProofLayer<F, H: Hasher> {
proof: MerkleProof<H>,
/// TODO: store separately?
commitment: H::Hash,
/// Flattened vector of **folded** evaluations
evaluations: Vec<F>,
}
impl<H: Hasher> MerkleTreeExt for MerkleTree<H> {
fn from_evaluations<S: CanonicalSerialize>(evaluations: &[S]) -> Self {
let hashes = H::hash_many(evaluations);
Self::from_leaves(&hashes)
}
impl<F: Copy, H: Hasher> FriProofLayer<F, H> {
/// Returns `None` if the number of evaluations stored in this layer is inconsistent with folding factor `N`.
fn evaluations<const N: usize>(&self) -> Option<&FoldedEvaluationsSlice<N, F>> {
(self.evaluations.len() % N == 0).then(|| {
FoldedEvaluationsSlice::<N, _>::from_flat_evaluations_unchecked(&self.evaluations)
})
}
/// Extracts the actually-queried evaluations from the evaluations stored in the layer.
///
/// Computing the evaluations in the next layer requires to store `N` evaluations per position, but
/// only one (per position) is actually used to verify this particular layer.
///
/// `domain_size` must be a power of two, otherwise the result is unspecified.
///
/// Returns `None` if the number of evaluations in this layer is inconsistent with folding factor `N` or
/// if the evaluations are inconsistent with the requested positions.
fn queried_evaluations<const N: usize>(
&self,
positions: &[usize],
folded_positions: &[usize],
domain_size: usize,
) -> Option<Vec<F>> {
let mask = domain_size / N - 1;
let folded_evals = self.evaluations::<N>()?;
pub struct FriProofLayer<F, H: Hasher> {
proof: MerkleProof<H>,
evaluations: Vec<F>,
let mut vec = Vec::with_capacity(positions.len());
for pos in positions {
let index = folded_positions.iter().position(|&p| p == pos & mask)?;
let queried = folded_evals
.as_ref()
.get(index)?
.get(pos / (domain_size / N))?;
vec.push(*queried);
}
Some(vec)
}
pub struct FriProof<F, H: Hasher> {
layers: Vec<FriProofLayer<F, H>>,
remainder: Vec<F>,
}
/// Commits the polynomial according to FRI algorithm.
///
/// - `polynomial` is the list of coefficients of the polynomial
/// - `N` is the folding factor. It should be a power of two.
/// - `polynomial` is the list of coefficients of the polynomial.
/// - `rng` is the pseudo-random number generator to be used by the algorithm.
/// - The initial evaluation domain will have size at least `polynomial.len() * blowup_factor` (rounded up to
/// the next power of two).
/// - `remainder_degree` is the degree of the remainder polynomial in the last FRI layer.
/// It should be a power of `N`.
///
/// TODO write documentation
/// # Panics
/// This panics if `polynomial.len() * blowup_factor > 2^63` and if `F` does not contain a subgroup of the size
/// of the requested evaluation domain.
pub fn commit_polynomial<const N: usize, F, H, R>(
polynomial: Vec<F>,
mut rng: R,
......@@ -113,13 +132,25 @@ where
}
// Commit remainder directly
let poly = to_polynomial(evaluations, num_coeffs);
let poly = to_polynomial(evaluations, remainder_degree);
rng.reseed(H::hash_item(&poly));
prover.set_remainder(poly);
prover
}
pub fn prove<const N: usize, F, H, R>(
// TODO? create a function that combines `commit_polynomial` and `build_proof`?
/// Constructs a FRI proof by making queries to the `commitments`.
///
/// - `rng` *must* be in the same state as it was when `commit_polynomial` has returned.
/// - `num_queries` is the number of random points to query in the first layer; this number decreases
/// in the following layers as the evaluation domain is reduced.
///
/// `num_queries` should be large enough to provide reasonable security, but small compared to `domain_size` to
/// avoid duplicates.
///
/// More than `num_queries` will be stored in the proof (approximately `N` times more) because each layer must
/// also store the evaluations necessary to compute the evaluations in the next layer.
pub fn build_proof<const N: usize, F, H, R>(
commitments: FriCommitments<N, F, H>,
mut rng: R,
num_queries: usize,
......@@ -134,23 +165,24 @@ where
let mut layers = Vec::with_capacity(commitments.layers().len());
for layer in commitments.layers() {
let proof = layer.tree().proof(&positions);
let mut evaluations = Vec::with_capacity(num_queries);
domain_size /= N;
positions = fold_positions(&positions, domain_size);
// `layer.tree().proof(...)` requires sorted positions
let mut positions_sorted = positions.clone();
positions_sorted.sort_unstable();
let proof = layer.tree().proof(&positions_sorted);
let mut evaluations = Vec::with_capacity(N * positions.len());
for &pos in &positions {
evaluations.extend_from_slice(&layer.evaluations()[pos]);
}
layers.push(FriProofLayer { proof, evaluations });
domain_size /= N;
let mask = domain_size - 1;
let mut new_positions = Vec::with_capacity(domain_size);
for position in positions {
let pos = position & mask;
if !new_positions.contains(&pos) {
new_positions.push(pos);
}
}
positions = new_positions;
layers.push(FriProofLayer {
proof,
evaluations,
commitment: layer.tree().root().unwrap(),
});
}
FriProof {
layers,
......@@ -158,33 +190,132 @@ where
}
}
#[inline]
pub fn to_evaluations<F: FftField>(mut polynomial: Vec<F>, domain_size: usize) -> Vec<F> {
debug_assert!(
domain_size.is_power_of_two(),
"Domain size must be a power of two"
);
impl<F: FftField, H: Hasher> FriProof<F, H> {
/// Verify a FRI proof. This checks the proof was properly built from a polynomial with degree lower than
/// `degree_bound`.
///
/// - `rng` must be consistent with the random number generator used to generate the proof.
/// - `num_queries` must match the value used in [`build_proof`].
/// - `degree_bound` must be a power of two strictly greater than the degree of the polynomial.
/// If `polynomial.len()` was not already a power of two in [`commit_polynomial`], the next power
/// of two must be used.
/// - `domain_size` must be a power of two and multiple of `degree_bound` such that
/// `domain_size / degree_bound = blowup_factor > 1`.
///
/// # Errors
/// This returns an error if the proof is not valid or any of the parameters is not consistent with it.
pub fn verify<const N: usize, R: ReseedableRng<Seed = H::Hash>>(
&self,
mut rng: R,
num_queries: usize,
mut degree_bound: usize,
mut domain_size: usize,
) -> Result<(), VerifyError> {
// Initial checks
let _ = AssertPowerOfTwo::<N>::OK;
if !domain_size.is_power_of_two() || domain_size <= degree_bound {
return Err(VerifyError::BadDomainSize(domain_size));
}
if !degree_bound.is_power_of_two() {
return Err(VerifyError::BadDegreeBound(degree_bound));
}
let Some(domain) = GeneralEvaluationDomain::<F>::new(N) else {
return Err(VerifyError::BadFoldingFactor(N));
};
let Some(mut root) = F::get_root_of_unity(domain_size as u64) else {
return Err(VerifyError::BadDomainSize(domain_size));
};
// Preparation
let mut alphas = Vec::<F>::with_capacity(self.layers.len());
for layer in &self.layers {
rng.reseed(layer.commitment);
alphas.push(rng.draw_alpha());
}
rng.reseed(H::hash_item(&self.remainder));
let mut positions = rng.draw_positions(num_queries, domain_size);
// TODO provide as argument
let mut evaluations = self.layers[0]
.queried_evaluations::<N>(
&positions,
&fold_positions(&positions, domain_size / N),
domain_size,
)
.ok_or_else(|| VerifyError::WrongNumberOfEvaluations { depth: 0 })?;
// Check layers
for (depth, layer) in self.layers.iter().enumerate() {
let folded_positions = fold_positions(&positions, domain_size / N);
let layer_evaluations = layer
.evaluations::<N>()
.ok_or_else(|| VerifyError::WrongNumberOfEvaluations { depth })?;
let queried_evaluations = layer
.queried_evaluations::<N>(&positions, &folded_positions, domain_size)
.ok_or_else(|| VerifyError::WrongNumberOfEvaluations { depth })?;
if queried_evaluations != evaluations {
return Err(VerifyError::InvalidFolding { depth });
}
if folded_positions.len() != layer_evaluations.folded_len() {
return Err(VerifyError::WrongNumberOfEvaluations { depth });
}
evaluations.clear();
let mut buffer = Vec::with_capacity(N);
for (&pos, eval) in std::iter::zip(&folded_positions, layer_evaluations) {
buffer.extend_from_slice(eval);
domain.ifft_in_place(&mut buffer);
let poly = DensePolynomial { coeffs: buffer };
let offset = root.pow([(domain_size - pos % (domain_size / N)) as u64]);
evaluations.push(poly.evaluate(&(alphas[depth] * offset)));
buffer = poly.coeffs;
buffer.clear();
}
let domain = GeneralEvaluationDomain::<F>::new(domain_size).unwrap();
domain.fft_in_place(&mut polynomial);
polynomial
if domain_size % N != 0 || degree_bound % N != 0 {
return Err(VerifyError::DegreeTruncation { depth });
}
root = root.pow([N as u64]);
domain_size /= N;
degree_bound /= N;
#[inline]
pub fn to_polynomial<F: FftField>(mut evaluations: Vec<F>, degree_bound: usize) -> Vec<F> {
debug_assert!(
evaluations.len().is_power_of_two(),
"Domain size must be a power of two"
);
positions = folded_positions;
let domain = GeneralEvaluationDomain::<F>::new(evaluations.len()).unwrap();
domain.ifft_in_place(&mut evaluations);
if !layer.proof.verify(
layer.commitment,
&positions,
&H::hash_many(layer_evaluations.as_ref()),
domain_size,
) {
return Err(VerifyError::CommitmentMismatch { depth });
}
}
debug_assert!(
evaluations[degree_bound..].iter().all(|c| *c == F::ZERO),
"Degree of polynomial is not bound by {degree_bound}"
);
// Check remainder
if self.remainder.len() != degree_bound
&& self.remainder[degree_bound..]
.iter()
.any(|&coeff| coeff != F::ZERO)
{
return Err(VerifyError::InvalidRemainderDegree);
}
let remainder = DensePolynomial {
coeffs: self.remainder.clone(),
};
for (position, &evaluation) in zip(positions, &evaluations) {
if remainder.evaluate(&root.pow([position as u64])) != evaluation {
return Err(VerifyError::InvalidRemainder);
}
}
evaluations.truncate(degree_bound);
evaluations
Ok(())
}
}
......@@ -2,8 +2,9 @@ use ark_ff::FftField;
use ark_serialize::CanonicalSerialize;
use rs_merkle::{Hasher, MerkleTree};
use crate::{FoldedEvaluations, MerkleTreeExt};
use crate::{utils::MerkleTreeExt, folding::FoldedEvaluations};
#[derive(Clone)]
pub(crate) struct FriLayer<const N: usize, F, H: Hasher> {
tree: MerkleTree<H>,
evaluations: FoldedEvaluations<N, F>,
......@@ -26,14 +27,14 @@ impl<const N: usize, F, H: Hasher> FriLayer<N, F, H> {
}
}
#[must_use]
/// Result of a polynomial folding. Use [`fri::build_proof`] to get a FRI proof.
#[derive(Clone)]
pub struct FriCommitments<const N: usize, F, H: Hasher> {
layers: Vec<FriLayer<N, F, H>>,
remainder: Vec<F>,
}
impl<const N: usize, F: FftField, H: Hasher> FriCommitments<N, F, H> {
#[inline]
pub(crate) fn new(degree_bound: usize) -> Self {
let layers = Vec::with_capacity(degree_bound.ilog2().div_ceil(N.ilog2()) as usize);
Self {
......@@ -47,8 +48,8 @@ impl<const N: usize, F: FftField, H: Hasher> FriCommitments<N, F, H> {
pub(crate) fn remainder(self) -> Vec<F> {
self.remainder
}
#[inline]
pub(crate) fn commit_layer(&mut self, layer: FriLayer<N, F, H>) {
pub(crate) fn commit_layer(&mut self, layer: FriLayer<N, F, H>)
{
self.layers.push(layer);
}
pub(crate) fn set_remainder(&mut self, polynomial: Vec<F>) {
......
......@@ -3,32 +3,46 @@ use std::mem::size_of;
use ark_ff::Field;
use rs_merkle::Hasher;
/// A seed-based pseudo-random number generator.
pub trait ReseedableRng {
type Seed;
fn reset(&mut self);
/// Sets the seed to be used in future calls to [`Self::next_bytes`].
fn reseed(&mut self, seed: Self::Seed);
/// Generates a pseudo-random value using the provided closure. The size of the slice passed to the closure
/// is implementation-dependent.
///
/// This method should also update the state of the object, such that subsequent calls may produce
/// seemingly unrelated (and unpredictable) values.
fn next_bytes<T, F>(&mut self, f: F) -> T
where
F: FnOnce(&[u8]) -> T;
/// Draws a pseudo-random number from field `F`. This does not need to be overriden.
///
/// # Panics
/// This panics if this object is not able to draw a random value from this field.
fn draw_alpha<F: Field>(&mut self) -> F {
for _ in 0..1 {
if let Some(elt) = self.next_bytes(|bytes| F::from_random_bytes(bytes)) {
return elt;
// TODO? retry on error?
// This has never failed during tests, but this should be tested with other fields.
self.next_bytes(|bytes| F::from_random_bytes(bytes))
.expect("Failed to draw alpha")
}
}
panic!("Failed to draw alpha after 1 attempts");
}
fn draw_positions(&mut self, number: usize, domain_size: usize) -> Vec<usize> {
/// Draws a [`Vec`] of `count` positions, each of them being strictly less than `domain_size`.
/// This does not need to be overriden.
///
/// `domain_size` must be a power of two; otherwise, the result is unspecified
/// (the implementation may either return incorrect positions or panic).
fn draw_positions(&mut self, count: usize, domain_size: usize) -> Vec<usize> {
debug_assert!(
domain_size.is_power_of_two(),
"Domain size must be a power of two"
);
let mask = domain_size - 1;
let mut positions = Vec::with_capacity(number);
for _ in 0..number {
let mut positions = Vec::with_capacity(count);
for _ in 0..count {
let number = self.next_bytes(|bytes| {
usize::from_le_bytes(bytes[0..size_of::<usize>()].try_into().unwrap())
});
......@@ -38,6 +52,19 @@ pub trait ReseedableRng {
}
}
/// This struct is designed to be used as the pseudo-random number generator in the
/// non-interactive version of FRI.
///
/// # Example
/// ```ignore
/// use fri::{algorithms::Blake3, rng::{FriChallenger, ReseedableRng}};
///
/// let mut challenger = FriChallenger::<Blake3>::default();
///
/// // For each FRI layer:
/// challenger.reseed(/* FRI commitment */);
/// let alpha = challenger.draw_alpha();
/// ```
pub struct FriChallenger<H: Hasher> {
seed: H::Hash,
counter: usize,
......@@ -58,10 +85,6 @@ where
{
type Seed = H::Hash;
fn reset(&mut self) {
self.seed = H::hash(&[]);
self.counter = 0;
}
fn reseed(&mut self, seed: Self::Seed) {
self.seed = H::concat_and_hash(&self.seed, Some(&seed));
self.counter = 0;
......@@ -76,14 +99,40 @@ where
}
}
impl<H: Hasher> FriChallenger<H> {
/// Resets this object to its initial state.
///
/// # Example
/// ```rust
/// use rs_merkle::Hasher;
/// use fri::{algorithms::Blake3, rng::{FriChallenger, ReseedableRng}};
///
/// let mut challenger1 = FriChallenger::<Blake3>::default();
///
/// challenger1.reseed(Blake3::hash(&[5]));
/// let hash1 = challenger1.next_bytes(|bytes| bytes.to_vec());
///
/// challenger1.reset();
/// let hash2 = challenger1.next_bytes(|bytes| bytes.to_vec());
///
/// let mut challenger2 = FriChallenger::<Blake3>::default();
/// let hash3 = challenger2.next_bytes(|bytes| bytes.to_vec());
///
/// assert_ne!(hash1, hash2);
/// assert_eq!(hash2, hash3);
/// ```
pub fn reset(&mut self) {
self.seed = H::hash(&[]);
self.counter = 0;
}
}
impl<'a, R> ReseedableRng for &'a mut R
where
R: ReseedableRng + ?Sized,
{
type Seed = R::Seed;
fn reset(&mut self) {
(**self).reset();
}
fn reseed(&mut self, seed: Self::Seed) {
(**self).reseed(seed);
}
......
use ark_ff::{Fp128, MontBackend, MontConfig};
use fri_test_utils::{Fq, BLOWUP_FACTOR, DOMAIN_SIZE, NUMBER_OF_POLYNOMIALS, NUM_QUERIES, POLY_COEFFS_LEN};
use rand::{thread_rng, Rng};
use winter_math::{fields::f128::BaseElement, FieldElement, StarkField};
use winter_rand_utils::{rand_value, rand_vector};
use winter_utils::transpose_slice;
use crate::{to_evaluations, FoldedEvaluations};
const NUMBER_OF_POLYNOMIALS: usize = 10;
const POLY_COEFFS_LEN: usize = 2048;
const BLOWUP_FACTOR: usize = 4;
const DOMAIN_SIZE: usize = (POLY_COEFFS_LEN * BLOWUP_FACTOR).next_power_of_two();
/// Matches `BaseElement` from winterfell
#[derive(MontConfig)]
#[modulus = "340282366920938463463374557953744961537"]
#[generator = "3"]
pub struct Test;
/// A prime, fft-friendly field isomorph to [`winter_math::fields::f128::BaseElement`].
pub type Fq = Fp128<MontBackend<Test, 2>>;
use crate::{
algorithms::Blake3,
build_proof, commit_polynomial,
folding::FoldedEvaluations,
rng::FriChallenger,
utils::to_evaluations,
};
macro_rules! do_for_multiple_folding_factors {
($factor: ident = $($factors: literal),* => $action: block) => {
......@@ -36,7 +29,7 @@ fn test_reduction() {
for i in 0..NUMBER_OF_POLYNOMIALS {
println!("Testing on polynomial {i}");
let poly = rand_vector(DOMAIN_SIZE);
let poly = rand_vector(POLY_COEFFS_LEN);
let poly2 = convert_many(&poly);
do_for_multiple_folding_factors!(FACTOR = 2, 4, 8, 16 => {
......@@ -60,7 +53,33 @@ fn test_reduction() {
}
}
#[inline]
#[test]
fn test_prove_verify() {
let mut rng = thread_rng();
let poly: Vec<Fq> = (0..POLY_COEFFS_LEN).map(|_| rng.gen()).collect();
do_for_multiple_folding_factors!(FACTOR = 2, 4, 8, 16 => {
println!("--Folding factor={FACTOR}");
let mut rng = FriChallenger::<Blake3>::default();
let commitments = commit_polynomial::<FACTOR, _, Blake3, _>(poly.clone(), &mut rng, BLOWUP_FACTOR, FACTOR);
let proof = build_proof(commitments, &mut rng, NUM_QUERIES);
rng.reset();
proof.verify::<FACTOR, _>(&mut rng, NUM_QUERIES, POLY_COEFFS_LEN, DOMAIN_SIZE).unwrap();
assert!(proof.verify::<{FACTOR*2}, _>(&mut rng, NUM_QUERIES, POLY_COEFFS_LEN, DOMAIN_SIZE).is_err());
assert!(proof.verify::<{FACTOR/2}, _>(&mut rng, NUM_QUERIES, POLY_COEFFS_LEN, DOMAIN_SIZE).is_err());
assert!(proof.verify::<FACTOR, _>(&mut rng, NUM_QUERIES, POLY_COEFFS_LEN, DOMAIN_SIZE * 2).is_err());
assert!(proof.verify::<FACTOR, _>(&mut rng, NUM_QUERIES, POLY_COEFFS_LEN, DOMAIN_SIZE / 2).is_err());
assert!(proof.verify::<FACTOR, _>(&mut rng, NUM_QUERIES, POLY_COEFFS_LEN * 2, DOMAIN_SIZE).is_err());
assert!(proof.verify::<FACTOR, _>(&mut rng, NUM_QUERIES, POLY_COEFFS_LEN / 2, DOMAIN_SIZE).is_err());
});
}
fn prepare_winterfell_poly<F: StarkField>(mut poly: Vec<F>) -> Vec<F> {
use winter_math::fft::{evaluate_poly, get_twiddles};
poly.resize(DOMAIN_SIZE, F::ZERO);
......@@ -69,12 +88,10 @@ fn prepare_winterfell_poly<F: StarkField>(mut poly: Vec<F>) -> Vec<F> {
poly
}
#[inline]
pub fn convert(value: &BaseElement) -> Fq {
fn convert(value: &BaseElement) -> Fq {
Fq::from(value.as_int())
}
#[inline]
pub fn convert_many(values: &[BaseElement]) -> Vec<Fq> {
fn convert_many(values: &[BaseElement]) -> Vec<Fq> {
values.iter().map(convert).collect()
}
use ark_ff::FftField;
use ark_poly::{EvaluationDomain, GeneralEvaluationDomain};
use rs_merkle::{Hasher, MerkleTree};
use ark_serialize::CanonicalSerialize;
/// Compile-time check that `N` is a power of two.
///
/// # Example
/// ```ignore
/// // OK:
/// let _ = AssertPowerOfTwo::<16>::OK;
///
/// // This does not compile:
/// let _ = AssertPowerOfTwo::<7>::OK;
/// ```
pub(crate) struct AssertPowerOfTwo<const N: usize>;
impl<const N: usize> AssertPowerOfTwo<N> {
pub const OK: () = assert!(N.is_power_of_two(), "`N` must be a power of two");
}
pub trait HasherExt: Hasher {
/// Uses the implementation of [`CanonicalSerialize`] to convert `value` into bytes then return the
/// hash value of those bytes.
///
/// `buffer` is used to store the serialized bytes. Its content when the function returns is unspecified.
/// If it is not empty initially, it will be cleared first.
fn hash_item_with<S: CanonicalSerialize>(value: &S, buffer: &mut Vec<u8>) -> Self::Hash;
/// Uses the implementation of [`CanonicalSerialize`] to convert `value` into bytes then return the
/// hash value of those bytes.
///
/// This allocates a new temporary vector to store the serialized bytes.
fn hash_item<S: CanonicalSerialize>(value: &S) -> Self::Hash {
Self::hash_item_with(value, &mut Vec::with_capacity(value.compressed_size()))
}
/// Convenience function to hash a slice of values.
fn hash_many<S: CanonicalSerialize>(values: &[S]) -> Vec<Self::Hash> {
let mut hashes = Vec::with_capacity(values.len());
let mut bytes = Vec::with_capacity(values.get(0).map_or(0, S::compressed_size));
for evaluation in values {
hashes.push(Self::hash_item_with(evaluation, &mut bytes));
}
hashes
}
}
impl<H: Hasher> HasherExt for H {
fn hash_item_with<S: CanonicalSerialize>(value: &S, buffer: &mut Vec<u8>) -> Self::Hash {
buffer.clear();
value
.serialize_compressed(&mut *buffer)
.expect("Serialization failed");
H::hash(buffer)
}
}
pub(crate) trait MerkleTreeExt {
/// Hash the evaluations and create a Merkle tree using the hashes as the leaves.
fn from_evaluations<S: CanonicalSerialize>(evaluations: &[S]) -> Self;
}
impl<H: Hasher> MerkleTreeExt for MerkleTree<H> {
fn from_evaluations<S: CanonicalSerialize>(evaluations: &[S]) -> Self {
let hashes = H::hash_many(evaluations);
Self::from_leaves(&hashes)
}
}
/// Converts `polynomial` from coefficient form to evaluations over roots of unity.
///
/// `domain_size` must be a power of two and strictly greater than the degree of the polynomial.
///
/// # Example
/// ```ignore
/// let polynomial = /* vec of coeffs */;
/// let evaluations = to_evaluations(polynomial, domain_size);
/// let dense_poly = DensePolynomial::from_coefficients_vec(polynomial.clone());
///
/// let w = F::get_root_of_unity(domain_size).unwrap();
///
/// assert_eq!(evaluations[0], dense_poly.evaluate(&w));
///
/// let interpolated = to_polynomial(evaluations, polynomial.len());
///
/// assert_eq!(polynomial, interpolated);
/// ```
#[inline]
pub fn to_evaluations<F: FftField>(mut polynomial: Vec<F>, domain_size: usize) -> Vec<F> {
debug_assert!(
domain_size.is_power_of_two(),
"Domain size must be a power of two"
);
let domain = GeneralEvaluationDomain::<F>::new(domain_size).unwrap();
domain.fft_in_place(&mut polynomial);
polynomial
}
/// Interpolates the coefficient form from the evaluations over the roots of unity.
/// This is the counterpart of [`to_evaluations`].
///
/// `degree_bound` must be strictly greater than the degree of the polynomial. Otherwise, this
/// may either panic or truncate higher degree coefficients.
#[inline]
pub fn to_polynomial<F: FftField>(mut evaluations: Vec<F>, degree_bound: usize) -> Vec<F> {
debug_assert!(
evaluations.len().is_power_of_two(),
"Domain size must be a power of two"
);
let domain = GeneralEvaluationDomain::<F>::new(evaluations.len()).unwrap();
domain.ifft_in_place(&mut evaluations);
debug_assert!(
evaluations[degree_bound..].iter().all(|c| *c == F::ZERO),
"Degree of polynomial is not bound by {degree_bound}"
);
evaluations.truncate(degree_bound);
evaluations
}
\ No newline at end of file
[package]
name = "fri_test_utils"
version = "0.1.0"
edition = "2021"
[lib]
[dependencies]
ark-ff = { version = "0.4.2", default-features = false }
\ No newline at end of file
//! Code shared between tests and benches
use ark_ff::{Fp128, MontBackend, MontConfig};
pub const NUMBER_OF_POLYNOMIALS: usize = 10;
pub const POLY_COEFFS_LEN: usize = 2048;
pub const BLOWUP_FACTOR: usize = 4;
pub const NUM_QUERIES: usize = 32;
pub const DOMAIN_SIZE: usize = (POLY_COEFFS_LEN * BLOWUP_FACTOR).next_power_of_two();
/// Matches `BaseElement` from winterfell
#[derive(MontConfig)]
#[modulus = "340282366920938463463374557953744961537"]
#[generator = "3"]
pub struct Test;
/// A prime, fft-friendly field isomorph to [`winter_math::fields::f128::BaseElement`].
pub type Fq = Fp128<MontBackend<Test, 2>>;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment