What problems can be delegated to a server or collection of servers and verified by a client with an efficient proof of correctness? This is an important question that may have just been resolved, but in this essay we'll focus on an earlier result in this direction that surpised many computer scientists. The result lies at the heart of important theoretical advances as well as practical systems for delegating computation, it's called the Sumcheck Protocol. To explore the details of the protocol we'll focus on a concrete problem, namely #SAT.
Let's formalize one class of methods for computation delegation, in particular interactive proof systems. A
-
$\forall x \in L$ ,$Pr[V(x, t, r) = 1] \ge c$ -
$\forall x \notin L$ and every prover stategy$P', Pr[V(x, t', r) = 1] \leq s$
The complexity class
#SAT is a # $P$-complete problem so specifying an interactive proof system for it would mean that #
We’ll model the boolean expressions as a recursive data structure:
#[derive(Clone)]
pub enum Expr {
Terminal(usize),
Not(Box<Expr>),
And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>),
}
Evaluating an expression takes advantage of the recursive structure:
impl Expr {
pub fn evaluate(&self, vals: &[bool]) -> bool {
match self {
Expr::Terminal(t) => vals[*t],
Expr::And(ref a, ref b) => a.evaluate(vals) && b.evaluate(vals),
Expr::Or(ref a, ref b) => a.evaluate(vals) || b.evaluate(vals),
Expr::Not(ref a) => !a.evaluate(vals),
}
}
}
One of the main insights from this protocol is the utility of shifting the problem from boolean functions to polynomials over a finite field
To evaluate the polynomial at a point we can use:
impl Expr {
...
// This is equivalent to evaluating the arithmetized polynomial at at point.
// Doing it this way allows us to avoid calculating all the terms in the
// multivariate polynomial resulting from the arithmetization process.
fn sharp_sat_arithmetization_evaluate_full(&self, vals: &[Fq]) -> Fq {
let one = Fq::new(BigInt::new([1, 0]));
match self {
Expr::Terminal(t) => vals[*t],
// AND(x, y) -> x * y
Expr::And(a, b) => {
a.sharp_sat_arithmetization_evaluate_full(vals)
* b.sharp_sat_arithmetization_evaluate_full(vals)
}
// OR(x, y) -> 1 – (1 – x) * (1 – y)
Expr::Or(a, b) => {
one - (one - a.sharp_sat_arithmetization_evaluate_full(vals))
* (one - b.sharp_sat_arithmetization_evaluate_full(vals))
}
// NOT(y) -> 1 – y
Expr::Not(a) => one - a.sharp_sat_arithmetization_evaluate_full(vals),
}
}
}
Let's construct an interactive proof system for #SAT. The Prover and Verifier take the boolean function
In the
In the
Finally,
If
The code follows the above description fairly closely:
fn sumcheck_protocol(
prover: &mut dyn SumcheckProver,
verifier: &mut dyn SumcheckVerifier,
claimed_sum: Fq,
num_vars: usize,
) -> bool {
let mut prior_poly = UniSparsePolynomial::from_coefficients_slice(&[(0, claimed_sum)]);
// round_num starts at 0 instead of 1
for round_num in 0..num_vars {
// Prover calculates the ith_polynomial
let prover_message = prover.ith_poly_message(round_num);
// Verifier ensures that the partial sum is valid
if !verifier.check_prover_message_validity(round_num, &prior_poly, &prover_message) {
return false;
}
// generate random field element
let cur_rand_el = verifier.generate_and_store_random_field_element(round_num);
prover.recieve_rand_element(round_num, cur_rand_el);
// final check
if round_num == num_vars - 1 {
if prover_message.evaluate(&cur_rand_el) != verifier.poly_eval() {
return false;
}
} else {
prior_poly = prover_message;
}
}
true
}
I chose to use trait objects to keep the protocol abstracted from our particular problem's details. Rather, the #SAT details are saved for the HonestSharpSATSumcheckProver
and SharpSATSumcheckVerifier
types.
struct HonestSharpSATSumcheckProver {
bool_form: Expr,
vals: Vec<Fq>,
num_vars: usize,
}
impl SumcheckProver for HonestSharpSATSumcheckProver {
// free_var starts at the 0th variable
fn ith_poly_message(&mut self, free_var: usize) -> UniSparsePolynomial<Fq> {
// Initialize the result polynomial
let mut accumulator_poly =
UniSparsePolynomial::from_coefficients_slice(&[(0, Fq::new(BigInt::zero()))]);
let mut cur_var = 0;
// sum over the n - (free_var + 1) dimensional boolean hypercube
for i in 0..2u32.pow((self.num_vars - (free_var + 1)) as u32) {
// Example format result when n = 5: 3 -> String::from("00011")
if free_var + 1 < self.num_vars {
let bits = format!("{i:0n$b}", n = self.num_vars - (free_var + 1));
for c in bits.chars() {
let field_val: Fq;
if c == '0' {
field_val = Fq::new(BigInt::zero());
} else if c == '1' {
field_val = Fq::new(BigInt::one());
} else {
panic!("this shouldnt happen")
};
self.vals[cur_var + free_var + 1] = field_val;
cur_var += 1;
}
}
accumulator_poly = accumulator_poly
+ self
.bool_form
.sharp_sat_arithmetization_uni(free_var, &self.vals);
cur_var = 0;
}
accumulator_poly
}
fn recieve_rand_element(&mut self, round_num: usize, rand_elem: Fq) {
self.vals.insert(round_num, rand_elem);
}
}
struct SharpSATSumcheckVerifier {
rng: ThreadRng,
bool_form: Expr,
vals: Vec<Fq>,
}
impl SumcheckVerifier for SharpSATSumcheckVerifier {
// This is only used for the final check to calculate
// p(r_1, ..., r_n)
fn poly_eval(&self) -> Fq {
self.bool_form
.sharp_sat_arithmetization_evaluate_full(&self.vals)
}
// This method is used every round to check the univariate polynomial that was
// sent this round
fn check_prover_message_validity(
&self,
round_num: usize,
prior_poly: &UniSparsePolynomial<Fq>,
prover_message: &UniSparsePolynomial<Fq>,
) -> bool {
let field_zero = Fq::new(BigInt::zero());
// This is p_i(0) + p_i(1)
let prover_partial_sum = prover_message.evaluate(&field_zero) +
prover_message.evaluate(&Fq::new(BigInt::one()));
let prior_evaluation: Fq;
if round_num == 0 {
// In the initital round the polynomial is a constant
prior_evaluation = prior_poly.evaluate(&field_zero);
} else {
match self.vals.get(&(round_num - 1)) {
Some(val) => {
// in the ith round this is p_{i-1}(r_{i-1})
prior_evaluation = prior_poly.evaluate(val);
},
None => panic!("No value for the index"),
}
}
// check that p_{i-1}(r_{i-1}) = p_i(0) + p_i(1)
prior_evaluation == prover_partial_sum
}
// generate and store a random field element r_i in F_p where
// p = 170141183460469231731687303715884105727
fn generate_and_store_random_field_element(&mut self, round_num: usize) -> Fq {
let rand_field: Fq = self.rng.gen();
self.vals[round_num] = rand_field;
rand_field
}
}
We never formally specify the multivariate polynomial, because it would have many terms for large
During initialization the prover sends
Completeness is satisfied, since if the correct
These statements obviously don't pass as proofs, but due to length constraints I'll leave the proofs to the pros.
We've constructed an interactive proof system for the #SAT problem. One can pick the size of the field so that the system has a very small soundness error. This shows that #