A (Relaxed) PCS by Merkle Trees and Low-Degree Tests

Chapter three dives into a first Succinct Argument of Circuit Satisfiability. It does so by introducing what it calls a Relaxed Polynomial Commitment Scheme that combines Merkle Trees and Low-Degree tests. Implementing a first Succinct Argument sounds like an interesting challenge, let's find out.

In Chapter 7 The Books gives the main definitions and the motivations for the new objects called Polynomial Commitment Schemes. Essentially what we want is a scheme where $\mathcal{P}$ would "commit" to the evaluations of some polynomial and its degree to be later tested by $\mathcal{V}$.

The Scheme

The Scheme itself as it is quoting The Book:

Let $\tilde{w} : \mathbb{F}^{\log n} \rightarrow \mathbb{F}$ be a ($\log n$)-variate multilinear polynomial over $\mathbb{F}$.

Let $s$ be the string consisting of all $|\mathbb{F}|^{\log n}$ evaluations of $\tilde{w}$.

One obtains a polynomial commitment scheme by applying a Merkle-tree based string commitment scheme to $s$ and then applying a low-degree test to $s$.

Let's try to decompose it.

Merkelizing the Evaluations

This is the easier part. Suppose for example that we have a function $\mathbb{F_5}^2$. The evaluation table for this function would look as follows:

0 00 10 2..5 45 5
s[0]s[1]s[2]..s[23]s[24]

And in order for $\mathcal{P}$ to commit to all the evaluations of this function it needs to create a Merkle Tree of $s$ and send $\mathcal{V}$ the root of this tree. Later when $\mathcal{V}$ asks $\mathcal{P}$ to reveal any of the evaluations, $\mathcal{P}$ replies with the evaluation + a proof of this evaluation's inclusion into the tree. Since merkelizing things is such a common theme in the blockchain space I am not going to go into it any further.

Running a Low-Degree Test

The explanation from The Book is a bit handwavy so it probably makes sense to look into [AS03] paper:

The low degree test is presented with $f:\mathbb{F}^m \rightarrow \mathbb{F}$ and an integer $d$. It is also presented a table that is meant to be "proof" that $f$ is degree $d$ polynomial. This table contains for each line in $\mathbb{F}^m$ a univariate degree $d$ polynomial that supposedly describes restriction of $f$ to that line. We will use the term $\textit{d-oracle}$ for any table that contains, for each line in $\mathbb{F}^m$ a univariate degree $d$ polynomial.

$$ \boxed{ \begin{aligned} &\text{\bf{The Low Degree Test:}}\cr &\text{Pick a random line } l \text{ in } \mathbb{F}^m\cr &\text{and read the univariate polynomial, say } p_l(t)\cr &\text{which the given \textit{d-oracle} contains for this line.}\cr &\text{Randomly pick a point $x$ on line $l$ and check}\cr &\text{whether $p_l$ correctly describes $f$ at $x$.}\cr &\text{If so, ACCEPT, else REJECT} \end{aligned} } $$

The Test from the paper [AS03] is not interactive, and assumes the oracle access to the whole $\textit{d-oracle}$ table. So here are a few ideas to take this description to the PCS protocol from The Book:

  1. At the beginning of the protocol $\mathcal{P}$ commits to the Merkle tree of the string of $f$ evaluations and to the claimed degree $d$ of $f$ by sending these values to $\mathcal{V}$.
  2. $\mathcal{V}$ picks a random line $l$ in $\mathbb{F}^m$ and sends it to $\mathcal{P}$.
  3. $\mathcal{P}$ replies with a univariate polynomial $p_l$ which it claims to be a restriction of $f$ to $l$.
  4. $\mathcal{V}$ generates a random $x \in \mathbb{F}$. Then it evaluates line $l$ on $x$ to obtain a point in $\mathbb{F}^m$. $\mathcal{V}$ sends this point to $\mathcal{P}$ and $\mathcal{P}$ replies with a corresponding value from $s$ and with the proof this evaluation's membership in the Merkle tree which was previously committed to by $\mathcal{P}$.
  5. $\mathcal{V}$ checks that $p_l(x)$ equals the received value from $s$ and that this value belongs to the Merkle tree.

Implementation

Restricting a polynomial to a line has been described and implemented in a previous post on GKR. What we need is the machinery to merkelize the evaluations string $s$.

All Values of $\mathbb{F}$

First we need to be able to evaluate our function $f$ on all values in $\mathbb{F}^m$. It would be great to introduce a helper trait that would allow the code to iterate over all possible values of $\mathbb{F}$:

/// Iterate over all possible values of a finite field.
pub trait IF: Field {
    type Values: IntoIterator<Item = Self>;

    fn all_values() -> Self::Values;
}

Now in order to turn all values in $\mathbb{F}$ into all values in $\mathbb{F}^m$ we need to build all permutations of values in $\mathbb{F}$ of length $m$. Since building such permutations is not the topic of this post the readily available code can be used to extend the above trait with a helper function:

pub trait IF: Field {
    type Values: IntoIterator<Item = Self>;

    fn all_values() -> Self::Values;

    fn all_multidimentional_values(m: usize) -> Vec<Vec<Self>> {
        let mut res: Vec<_> =
            permutations::permutations(&Self::all_values().into_iter().collect::<Vec<_>>(), m)
                .collect();
        res.sort();
        res
    }
}

Prover

$\mathcal{P}$ is initialized with some polynomial $f$, our implementation as usual is going to be quite generic. Here, it is being generic over a field $\mathbb{F}$, the $f$ and the parameters of the Merkle tree:

pub struct Prover<F: Field, M: MultilinearExtension<F>, P: Config<Leaf = F>> {
    tree: MerkleTree<P>,
    values_convenience_map: HashMap<Vec<F>, usize>,
    poly: M,
    values: Vec<F>,
}

The implementation allows any MerkleTree configuration to be used with only restriction that the leafs of the tree have to be elements in $\mathbb{F}$. Also Prover holds a convenience map from $\mathbb{F}^m$ to indices in the evaluations string $s$.

Now to create a new $\mathcal{P}$ and populate all the fields in the above struct a fairly involved logic has to be implemented:

impl<F: IF, M: MultilinearExtension<F>, P: Config<Leaf = F>> Prover<F, M, P> {
    /// Create a new Prover.
    pub fn new(
        poly: M,
        leaf_chr_params: <<P as Config>::LeafHash as CRHScheme>::Parameters,
        two_to_one_params: <<P as Config>::TwoToOneHash as TwoToOneCRHScheme>::Parameters,
    ) -> Result<Self> {
        let all_values = F::all_multidimentional_values(poly.num_vars());
        // Populate s
        let all_poly_values: Result<Vec<_>> = all_values
            .iter()
            .map(|value| poly.evaluate(value).ok_or(Error::PolyEvalDimMismatch))
            .collect();

        let all_poly_values = all_poly_values?;

        // Length of array for `MerkleTree` has to be a power of two, so
        // extend s with zeros up to that length.
        let all_values_len = all_poly_values.len();
        let values: Vec<_> = all_poly_values
            .iter()
            .cloned()
            .chain((all_values_len..all_values_len.next_power_of_two()).map(|_| F::zero()))
            .collect();

        // Build a convenience indexing map
        let values_convenience_map = all_values
            .iter()
            .enumerate()
            .map(|(i, value)| (value.clone(), i))
            .collect();

        // Build a MerkleTree.
        let tree: MerkleTree<P> =
            MerkleTree::new(&leaf_chr_params, &two_to_one_params, values.clone())?;

        Ok(Self {
            tree,
            poly,
            values_convenience_map,
            values,
        })
    }

}

To complete the implementation of $\mathcal{P}$ three functions have to be added:

    /// Get the merkle root.
    pub fn merkle_root(&self) -> P::InnerDigest {
        self.tree.root()
    }

    /// Restrict to line.
    pub fn poly_restriction_to_line(&self, b: &[F], c: &[F]) -> univariate::SparsePolynomial<F> {
        restrict_poly(b, c, &self.poly)
    }

    /// Challenge
    pub fn challenge(&self, point: Vec<F>) -> Result<(Path<P>, F)> {
        let point_index = self.values_convenience_map.get(&point).unwrap();
        Ok((
            self.tree.generate_proof(*point_index)?,
            self.values[*point_index],
        ))
    }

First one returns the root of the Merkle tree of $s$ $\mathcal{P}$ commits to at the beginning of the protocol.

The second one restricts committed $f$ to a random line $x$ picked by $\mathcal{V}$.

The third one answers $\mathcal{V}$'s request to revel $s$ value at a given point along with the proof of this value's membership in the committed Merkle tree.

Verifier

The $\mathcal{V}$ is going to hold quite a lot of data:

/// The Verifier in the Relaxed PCS protocol.
pub struct Verifier<F: Field, P: Config<Leaf = F>> {
    x: F,
    degree: usize,
    challenge_point: Vec<F>,
    line: Vec<univariate::SparsePolynomial<F>>,
    num_vars: usize,
    prover_univariate: Option<univariate::SparsePolynomial<F>>,
    merkle_root: P::InnerDigest,
    leaf_chr_params: LeafParam<P>,
    two_to_one_params: TwoToOneParam<P>,
}

When a new $\mathcal{V}$ is created it has to be initialized with the values of degree $d$ and the root of the Merkle Tree of $s$ $\mathcal{P}$ has committed to:

impl<F: Field, P: Config<Leaf = F>> Verifier<F, P> {
    /// Create a new Verifier.
    pub fn new(
        num_vars: usize,
        degree: usize,
        merkle_root: P::InnerDigest,
        leaf_chr_params: <<P as Config>::LeafHash as CRHScheme>::Parameters,
        two_to_one_params: <<P as Config>::TwoToOneHash as TwoToOneCRHScheme>::Parameters,
    ) -> Self {

Then it picks the random line in $\mathbb{F}^m$ to send it to the $\mathcal{P}$. To do so $\mathcal{V}$ generates two random points in $\mathbb{F}^m$ and uses the function line implemented earlier for the GKR protocol:

    pub fn random_line<R: Rng>(&mut self, rng: &mut R) -> (Vec<F>, Vec<F>) {
        let b: Vec<F> = (0..self.num_vars).map(|_| F::rand(rng)).collect();
        let c: Vec<F> = (0..self.num_vars).map(|_| F::rand(rng)).collect();
        self.line = line(&b, &c);
        (b, c)
    }

When $\mathcal{P}$ has sent a univariate polynomial $p_l$ claimed to be a restriction of $f$ to the line generated earlier, $\mathcal{V}$ checks its degree and saves to evaluate it later:

    pub fn committed_univariate(&mut self, p: univariate::SparsePolynomial<F>) -> Result<()> {
        if p.degree() > self.degree {
            return Err(Error::DegreeMismatch);
        }
        self.prover_univariate = Some(p);
        Ok(())
    }

Then $\mathcal{V}$ needs to pick a random point along the picked line $l$ and send it to $\mathcal{P}$ querying the corresponding value in $s$ along with the proof:

    /// Challenge the prover at some point.
    pub fn challenge_prover<R: Rng>(&mut self, rng: &mut R) -> Vec<F> {
        self.x = F::rand(rng);
        self.challenge_point = self
            .line
            .iter()
            .map(|poly| poly.evaluate(&self.x))
            .collect();
        self.challenge_point.clone()
    }

Finally the $\mathcal{P}$'s reply has to be verified against the earlier committed root of Merkle Tree of $f$ and also $\mathcal{V}$ needs to check that value from $s$ $\mathcal{P}$ has replied with equals $p_l(x)$:

    /// Verify the prover's reply.
    pub fn verify_prover_reply(&self, path: Path<P>, leaf: F) -> Result<()> {
        path.verify(
            &self.leaf_chr_params,
            &self.two_to_one_params,
            &self.merkle_root,
            leaf,
        )?;

        let eval = self
            .prover_univariate
            .as_ref()
            .ok_or(Error::NoProverPoly)?
            .evaluate(&self.x);
        if leaf != eval {
            return Err(Error::EvalMismatch(
                format!("{:?}", leaf),
                format!("{:?}", eval),
            ));
        }
        Ok(())
    }

Testing

The testing code configures the Merkle Trees the same way testing code in ark-crypto-primitives does with the only exception is that the implementation of Config is generic over field $\mathbb{F}$ and uses a wrapper for a hasher to go from this generic field type to bytes for hash convenience:

    type LeafH = pedersen::CRH<JubJub, Window4x256>;
    struct CHROverField<F> {
        __f: PhantomData<F>,
    }

    impl<F: Field> CRHScheme for CHROverField<F> {
        type Input = F;

        type Output = <LeafH as CRHScheme>::Output;

        type Parameters = <LeafH as CRHScheme>::Parameters;

        fn setup<R: Rng>(
            r: &mut R,
        ) -> std::result::Result<Self::Parameters, ark_crypto_primitives::Error> {
            LeafH::setup(r)
        }

        fn evaluate<T: Borrow<Self::Input>>(
            parameters: &Self::Parameters,
            input: T,
        ) -> std::result::Result<Self::Output, ark_crypto_primitives::Error> {
            let bytes = to_uncompressed_bytes!(input).map_err(|_| Box::new(Error::ToBytesError))?;
            LeafH::evaluate(parameters, bytes.as_ref())
        }
    }

This way in the config for the merkle tree the associated Leaf type can be our field and not the [u8] slice:

    struct JubJubMerkleTreeParamsFp5;

    impl Config for JubJubMerkleTreeParamsFp5 {
        type Leaf = Fp5;

        type LeafDigest = <LeafH as CRHScheme>::Output;
        type LeafInnerDigestConverter = ByteDigestConverter<Self::LeafDigest>;
        type InnerDigest = <CompressH as TwoToOneCRHScheme>::Output;

        type LeafHash = CHROverField<Fp5>;
        type TwoToOneHash = CompressH;
    }

Conclusion

In this a first Succinct Argument from The Book has been implemented. The implementation is available at b73f65d. As always the implementation has been made extremely generic over everything and turned out quite good I believe. Thank you for reading and stay tuned for the future posts!


Reference List.

YouTube: Log Degree Testing - Alessandro Chiesa

Robust Characterizations of Polynomials with Applications to Program Testing

Improved Low-Degree testing and its applications