- Author: Nicolas IOOSS
- Date: 2022-12-12
- Puzzle: https://zkhack.dev/zkhackIII/puzzleT3.html, https://github.com/ZK-Hack/puzzle-bigger-is-better
The GitHub repository contains this introduction.
Bob was catching up on the latest in zkSNARK research, and came across the Vampire paper. In that paper, he found a reference to an inner-product commitment scheme, which allows committing to a vector and later proving that its inner-product with another (public) vector is equal to a claimed value. Bob was intrigued by this scheme, and decided to implement it in Rust.
Bob was delighted with the performance of the resulting implementation, and so decided to deploy it. The scheme requires a universal Powers-of-Tau-type trusted setup, and so Bob generated a SRS using an MPC ceremony.
Things were going smoothly for a while, but then Bob received an anonymous email that contained a full break of the scheme! Unfortunately for Bob, the email didn't contain any details about the break. Can you help Bob figure out the issue, and fix his scheme?
This puzzle is about a Zero-Knowledge protocol which proves that the inner-product of a secret vector with a public one is equal to a claimed value.
This protocol is implemented in a ILV
trait in src/algorithms.rs
(named after the authors Izabachène, M., Libert, B., Vergnaud, D. of the paper Block-wise P-Signatures and Non-Interactive Anonymous Credentials with Efficient Attributes).
This file also includes a test named test_ilv
which shows how the functions can be used.
- This test starts by randomly generating two vectors of 512 elements
let mut rng = ark_std::test_rng();
let dim = 512;
let a = (0..dim).map(|_| Fr::rand(&mut rng)).collect::<Vec<_>>();
let b = (0..dim).map(|_| Fr::rand(&mut rng)).collect::<Vec<_>>();
- Then it loads a Structured common Reference String (SRS) from a binary file
ck.srs
.
let ck = CommitmentKey::<Bls12_381>::deserialize_unchecked(crate::SRS).unwrap();
- This SRS contains a Commitment Key (CK), which structure is defined in
src/data_structures.rs
.
#[derive(CanonicalSerialize, CanonicalDeserialize, PartialEq, Eq)]
pub struct CommitmentKey<E: PairingEngine> {
/// The powers [beta^0 * G, beta^1 * G, ..., beta^n * G]
pub powers_of_beta_g_first: Vec<E::G1Affine>,
/// The powers [beta^{n + 2} * G, ..., beta^{2n} * G]
pub powers_of_beta_g_second: Vec<E::G1Affine>,
/// The powers [beta^0 * H, beta^1 * H, ..., beta^n * H]
pub powers_of_beta_h: Vec<E::G2Affine>,
}
- This Commitment Key is used to commit the first vector and to generate a proof.
let cm = ILV::commit(&ck, &a);
let proof = ILV::open(&ck, &a, &b);
- The test then verifies that the proof is valid.
let inner_product = a.iter().zip(b.iter()).map(|(&a, b)| a * b).sum::<Fr>();
assert!(ILV::verify(&ck, &cm, &b, inner_product, &proof));
Function main
creates an Attack
object and calls assert_attack_works
from src/attack.rs
which calls the same ILV::commit
and ILV::verify
functions.
But assert_attack_works
has two major differences with test_ilv
:
- It generates vector
b
from a deterministic random number generator which takes the commitment ofa
as input. This looks like a Fiat–Shamir heuristic to prevent the prover from cheating in a non-interactive setup. - It also compares a field named
claimed_inner_product
with the actual inner-product ofa
andb
and fails if they are equal.
So the puzzle consists in crafting a valid proof which states that the inner-product of vectors a
and b
is some value different from the actual one.
To understand how to solve it, we first need to go into the details of the proving system.
Let's introduce some mathematical notations.
The puzzle is about the inner-product of two vectors of the same dimension
The puzzle uses these generic notations when defining the algorithm of the protocol (for example E::Fr
in src/algorithms.rs
is the type which represents an element of src/main.rs
, implemented in Rust's crate ark_bls12_381
.
The inner-product of
To commit ILV::commit
performs two steps:
- Convert the elements
$a_i$ to big integers with methodFr::into_repr
. - Combine these integers with some points from the Commitment Key with function
VariableBaseMSM::multi_scalar_mul
. Even though this function is not documented inarkworks
0.3.0, we can guess that it performs a Multi-Scalar Multiplication between points and integers.
What does the Commitment Key contain?
Its definition in src/data_structures.rs
contains some helpful comments:
-
ck.powers_of_beta_g_first
contains$(\beta^0 G, \beta^1 G..., \beta^n G)$ in$\mathbb{G}_1$ -
ck.powers_of_beta_g_second
contains$(\beta^{n+2} G, \beta^{n+3} G..., \beta^{2n} G)$ in$\mathbb{G}_1$ -
ck.powers_of_beta_h
contains$(\beta^0 H, \beta^1 H..., \beta^n H)$ in$\mathbb{G}_2$
The
Back to the algorithm, function ILV::commit
therefore computes:
Function ILV::open
is more complex.
- It starts by computing the inner-product of the two given vectors:
- It then creates a polynomial of degree
$n + 1$ from$a$ :
- It also creates a polynomial from
$b$ , reversing the coefficients:
- It multiplies both polynomials:
(with
- It checks that the coefficient of
$X^{n + 1}$ in$p$ is the inner-product. Indeed this coefficient is:
- It extracts the coefficients of
$p$ in a vector$(p_0, p_1..., p_{2n}) \in \mathbb{F}_r^{2n + 1}$ - It computes a proof by combining all these coefficients with the Commitment Key:
In short, ILV::open
computes a polynomial commitment of the product
To verify such a proof using the Commitment Key, ILV::verify
starts by computing a commitment of the polynomial
It then computes three pairings, using the claimed value of the inner-product
ILV::verify
accepts the proof if and only if
With the prover implemented in ILV::open
, the computed pairings can be expanded:
So
This equation verifies that the claimed inner-product of vectors
The algorithm only uses several points from the Commitment Key: ck.srs
can be displayed in Rust:
println!(
"ck.powers_of_beta_g_first 0..n => n+1 items = {}",
ck.powers_of_beta_g_first.len()
);
println!(
"ck.powers_of_beta_g_second n+2..2n => n-1 items = {}",
ck.powers_of_beta_g_second.len()
);
println!("ck.powers_of_beta_h 0..n => n+1 items = {}", ck.powers_of_beta_h.len());
This displays:
ck.powers_of_beta_g_first 0..n => n+1 items = 514
ck.powers_of_beta_g_second n+2..2n => n-1 items = 511
ck.powers_of_beta_h 0..n => n+1 items = 513
As ck.powers_of_beta_g_first
!
We can check that this is indeed ck.powers_of_beta_g_first
verify the pairing relation
for i in 0..ck.powers_of_beta_g_first.len() - 1 {
assert_eq!(
E::pairing(ck.powers_of_beta_g_first[i], ck.powers_of_beta_h[1]),
E::pairing(ck.powers_of_beta_g_first[i + 1], ck.powers_of_beta_h[0])
);
}
From the understanding of the equation that the verifier checks, it seems interesting to add this point to the proof.
Let's define a modified proof point using a scalar
Function ILV::verify
checks that
Can we use this to modify the claimed inner-product?
As it is uses in
$$e'_1 e'3 = e_2 \Leftrightarrow e_1 \left(\left[\delta \beta^{n + 1}\right] e(G, H)\right) e(Claimed'{a \cdot b} \beta^n G, \beta H) = e_1 e_3$$
$$\Leftrightarrow \left[(\delta + Claimed'{a \cdot b}) \beta^{n+1}\right] e(G, H) = \left[Claimed{a \cdot b} \beta^{n+1}\right] e(G, H)$$
Therefore, we can claim any value for the inner-product of
Here is an implementation of this attack in function attack
in src/main.rs
.
pub fn attack<E: ark_ec::PairingEngine>(ck: &CommitmentKey<E>, dim: usize) -> attack::Attack<E> {
use crate::algorithms::ILV;
use ark_ec::AffineCurve;
use ark_std::UniformRand;
// Craft a valid inner-product proof for a random vector a
let mut rng = ark_std::test_rng();
let a = (0..dim).map(|_| E::Fr::rand(&mut rng)).collect::<Vec<_>>();
let cm = ILV::commit(ck, &a);
let b = attack::hash(cm, dim);
let proof = ILV::open(&ck, &a, &b);
let inner_product = a.iter().zip(b.iter()).map(|(&a, b)| a * b).sum::<E::Fr>();
assert!(ILV::verify(&ck, &cm, &b, inner_product, &proof));
// Craft the attack to claim a random value as the inner product
let attack_claim = E::Fr::rand(&mut rng);
assert_ne!(attack_claim, inner_product);
let delta: E::Fr = inner_product - attack_claim;
let attack_proof = Proof(proof.0 + ck.powers_of_beta_g_first[dim + 1].mul(delta).into());
// Verify the attack
assert!(ILV::verify(&ck, &cm, &b, attack_claim, &attack_proof));
println!(
"Created an attack with {} instead of {}",
attack_claim, inner_product
);
attack::Attack {
a,
commitment: cm,
claimed_inner_product: attack_claim,
proof: attack_proof,
}
}
In the end, this inner-product protocol was broken because the Structured common Reference String of the Trusted Setup contained an additional point,