Bob designed a private payment protocol based on summation of polynomial evaluations. A new note is issued with a secret
polynomial
In order to enforce fair bahavior, it should be possible for all participants of the protocol to check the validity of all transactions. This is done by requiring every note to attach the domain
The sum-check protocol for univariate polynomials is used to give an efficient proof of the relation
We will review the basic constructions and facts that enable an efficient proof in the univariate sum-check protocol. For full details see section 10.3.1 of Thaler's book. For any subset
It is a polynomial of degree
This fact give us an efficient way to prove that a polynomial
The univariate sum-check protocol is based on a similar idea. In the case
Fact 1: If
See Lemma 10.2. in Thaler's book for a proof. The proof relies on the special structure of multiplicative subgroups of finite fields.
Given Fact 1, we can give a simple proof of a polynomial
In this callenge, we are given a polynomial
To see how to exploit this weekness, let's see how the univariate sum-check protocol is implemented in our particular problem. The challege uses the arkworks, in particular the ark_poly and ark_poly_commit crates which contain primitives and implementations for working with polynomials over finite fields and polynomial commitments.
The protocol begins by specifying a (multiplicative) domain using the GeneralEvaluationDomain
type from the ark_poly crate.
let domain_size = 16;
let domain = GeneralEvaluationDomain::new(domain_size).unwrap();
let max_degree = 30;
This challenge uses the Marlin implementation of the KZG polynomial commitment scheme which enables us to enforce degree bounds.
First, the protocol sets up the public keys for the prover and verifier:
let mut rng = test_rng();
let srs = PC::setup(max_degree, None, &mut rng).unwrap();
let (ck, vk) = PC::trim(&srs, max_degree, 1, Some(&[domain_size - 2])).unwrap();
We are then given a specific polynomial by the challenge
let coeffs = vec![
F::from(123312u64),
F::from(124151231u64),
F::from(1190283019u64),
F::from(19312315u64),
F::from(312423151u64),
F::from(61298741u64),
F::from(132151231u64),
F::from(1321512314u64),
F::from(721315123151u64),
F::from(783749123u64),
F::from(2135123151u64),
F::from(312512314u64),
F::from(23194890182314u64),
F::from(321514231512u64),
F::from(321451231512u64),
F::from(823897129831u64),
F::from(908241231u64),
F::from(9837249823u64),
F::from(982398741823u64),
F::from(3891748912u64),
F::from(21389749812u64),
F::from(891724876431u64),
F::from(213145213u64),
F::from(32897498123u64),
F::from(3219851289231u64),
F::from(2184718923u64),
F::from(31245123131431u64),
F::from(36712398759812u64),
F::from(8724876123u64),
F::from(89783927412u64),
F::from(8723498123u64),
];
let f = DensePolynomial::from_coefficients_slice(&coeffs);
We can even check that the sum is really not zero:
let mut real_sum = F::zero();
for h in domain.elements() {
real_sum += f.evaluate(&h);
}
assert_ne!(real_sum, F::zero());
The false statement is made by generating a commitment to the given domain
is zero.
let sum = F::zero();
let f = LabeledPolynomial::new("f".into(), f.clone(), None, Some(1));
let (f_commitment, f_rand) = PC::commit(&ck, &[f.clone()], Some(&mut rng)).unwrap();
let statement = Statement {
domain,
f: f_commitment[0].commitment().clone(),
sum,
};
let proof = prove::<F, PC, FS, StdRng>(&ck, &statement, &f, &f_rand[0], &mut rng).unwrap();
The output of the prove method is an instance of the Proof
struct, which is defined as follows:
pub struct Proof<F: Field, PC: PolynomialCommitment<F, DensePolynomial<F>>> {
pub f_opening: F,
pub s: PC::Commitment,
pub s_opening: F,
pub g: PC::Commitment,
pub g_opening: F,
pub h: PC::Commitment,
pub h_opening: F,
pub pc_proof: PC::BatchProof,
}
The statement is accepted if the verifier accepts the proof.
let res = verify::<F, PC, FS, StdRng>(&vk, &statement, &proof, &mut rng);
assert_eq!(true, res.is_ok());
Our goal is to implement a prover that convinces the verifier of the false statement is true without revealing the actual sum given by real_sum
which can be used to deanonimize us.
As we said before, this is done by making an appropriate choice of the polyonomial
The prover and verifier are using the Fiat-Shamir transform in order to enable a non-interactive protocol. Essentially, the random challenges are going to be produced from the prover's own custom input so they cannot be controlled by the prover in any way. The verifier produces a Fiat-Shamir random generator:
let mut fs_rng = FS::initialize(&to_bytes![&PROTOCOL_NAME, statement].unwrap());
fs_rng.absorb(&to_bytes![proof.s, proof.h, proof.g].unwrap());
let f = LabeledCommitment::new("f".into(), statement.f.clone(), None);
let s = LabeledCommitment::new("s".into(), proof.s.clone(), None);
let h = LabeledCommitment::new("h".into(), proof.h.clone(), None);
let g = LabeledCommitment::new(
"g".into(),
proof.g.clone(),
Some(statement.domain.size() - 2),
);
We can then use the Fiat-Shamir generator to get the openning challenge QuerySet
to encode the oppening challege for several named polynomials.
let xi = F::rand(&mut fs_rng);
let opening_challenge = F::rand(&mut fs_rng);
let point_label = String::from("xi");
let query_set = QuerySet::from([
("f".into(), (point_label.clone(), xi)),
("h".into(), (point_label.clone(), xi)),
("g".into(), (point_label.clone(), xi)),
("s".into(), (point_label, xi)),
]);
We can then check if the given commitments match the claimed openning values using the batch_check
method on a the polynomial commitment.
let res = PC::batch_check(
vk,
&[f, s, h, g],
&query_set,
&evaluations,
&proof.pc_proof,
opening_challenge,
rng,
).unwrap();
assert!(res)
Now that we verified the openning values, we can test the required relation
let card_inverse = statement.domain.size_as_field_element().inverse().unwrap();
let lhs = proof.s_opening + proof.f_opening;
let rhs = {
let x_gx = xi * proof.g_opening;
let zh_eval = statement.domain.evaluate_vanishing_polynomial(xi);
x_gx + proof.h_opening * zh_eval + statement.sum * card_inverse
};
assert_eq!(lhs, rhs)
The verifier accepts when
As we said, the vulnerability of the verifier comes from the fact that we are given complete freedom to choose the polynomial
Remark: the easiest choice is to set
The verifier checks a proof for the sum of the polynomial
let seed = b"GEOMETRY-SUMCHECK, double spend attack";
let mut s_rng = FS::initialize(&to_bytes![&seed, statement].unwrap());
We can then use s_rng
to produce random values for
let mut evaluations = Vec::new();
let mut sum = F::zero();
for h in statement.domain.elements() {
let random = F::rand(&mut s_rng);
sum = sum + random;
evaluations.push(-f.evaluate(&h) + random);
}
evaluations[0] = evaluations[0] - sum;
The polynomial Evaluations
.
let evals = Evaluations::from_vec_and_domain(evaluations, statement.domain);
This instance can then be used to get the interpoleted polynomial using the interpolate
method.
let s = evals.interpolate();
let p = f.polynomial().clone() + s.clone();
We have defined
All that's left is to produce
let (h, r) = p.divide_by_vanishing_poly(statement.domain).unwrap();
let x = DensePolynomial::from_coefficients_vec(vec![F::zero(), F::one()]);
let g = r.div(&x);
We now have all the ingridients to make our fictitious proof.
Now that we have the polynomials, all we need to do is to produce commitments, opennings, and package them into the Proof data structure. First, we will make labeled polynomials and then use them to make labeled commitments. This makes it easy for batch openning and quarying.
let g = LabeledPolynomial::new("g".into(), g.clone(), Some(statement.domain.size() - 2), Some(1));
let h = LabeledPolynomial::new("h".into(), h.clone(), None, Some(1));
let s = LabeledPolynomial::new("s".into(), s.clone(), None, Some(1));
let (g_commitment, g_rand) = PC::commit(&ck, &[g.clone()], Some(rng)).unwrap();
let (h_commitment, h_rand) = PC::commit(&ck, &[h.clone()], Some(rng)).unwrap();
let (s_commitment, s_rand) = PC::commit(&ck, &[s.clone()], Some(rng)).unwrap();
let f_c = LabeledCommitment::new("f".into(), statement.f.clone(), None);
let s_c = LabeledCommitment::new("s".into(), s_commitment[0].commitment().clone(), None);
let h_c = LabeledCommitment::new("h".into(), h_commitment[0].commitment().clone(), None);
let g_c = LabeledCommitment::new(
"g".into(),
g_commitment[0].commitment().clone(),
Some(statement.domain.size() - 2),
);
To produce the opennings of the commitments, we follow the Fiat-Shamir protocol to make a random challenge, as the verifier does.
let mut fs_rng = FS::initialize(&to_bytes![&PROTOCOL_NAME, statement].unwrap());
fs_rng.absorb(&to_bytes![
s_c.commitment().clone(),
h_c.commitment().clone(),
g_c.commitment().clone()].unwrap());
We can now use the generator fs_rng
to produce the opening challenge and quary set, as the verifier does.
let xi = F::rand(&mut fs_rng);
let opening_challenge = F::rand(&mut fs_rng);
let point_label = String::from("xi");
let query_set = QuerySet::from([
("f".into(), (point_label.clone(), xi)),
("h".into(), (point_label.clone(), xi)),
("g".into(), (point_label.clone(), xi)),
("s".into(), (point_label, xi)),
]);
We can now collect the opennings of our commitments by evaluating our polynomials on the quary set and produce a proof for these opennings using the bath_open
method.
let evaluations = evaluate_query_set(
[f, &h, &g, &s],
&query_set,
);
let pc_proof = PC::batch_open(
ck,
[f, &h, &g, &s],
[&f_c, &h_c, &g_c, &s_c],
&query_set,
opening_challenge,
[f_rand, &h_rand[0], &g_rand[0], &s_rand[0]],
Some(rng),
).unwrap();
Finally, we can return an instance of Proof containing all the data we produced
Ok(Proof{
f_opening : evaluations[&("f".into(), xi)],
s : s_c.commitment().clone(),
s_opening : evaluations[&("s".into(), xi)],
g : g_c.commitment().clone(),
g_opening :evaluations[&("g".into(), xi)],
h: h_c.commitment().clone(),
h_opening : evaluations[&("h".into(), xi)],
pc_proof : pc_proof,
})
and we are done!
Footnotes
-
The protocol works in the same way for the slightly more general multiplicative cosets, but it's not necessary for this review. ↩