The Saiyans have landed on planet earth. Our great defenders Krillin, Piccolo, Tien and Gohan have to hold on till Goku arrives on the scene.
Vegeta and Nappa have scouters that indicate our heroes power levels and sadly we are not doing too well.
Somehow, Gohan has raised his power level to
p_4(X) = 9000
, but it is not good enough. Piccolop_3(X)
can help but he is still regenerating, and Krillinp_2(X)
and Tienp_1(X)
are in bad shape. The total power of the team is computed asP = p_1(X) * 0 + p_2(X) * 0 + p_3(X) * 0 + p_4(X)
At the current moment, the X is equal to
42
.Suddenly Gohan, and Piccolo recieve a message from Bulma that the scouters verify the sensed power level of individual enemies using KZG and for multiple enemies with batched KZG method. Vegeta knows for sure that the power level of Gohan is
p_4(X) = 9000
, so he will know if we change that. If only the team had a way to trick their opponents to believe that their total power level isP > 9000
- then the enemies will surely flee.To run
cargo run --releaseVerification Vegeta is running the scouters from
52.7.211.188:8000
.
- By Vladimir
- By Ayush Shukla
By Ayush Shukla
The key to solving this challenge is understanding how batching works in KZG commitments. Looking into the code reveals two important functions, open_batch
and verify_batch
. The crucial insight comes from realizing that the upsilon
parameter has to be random. Otherwise, it can be used to make the verification ignore some of the polynomials.
fn open_batch(
&self,
x: &FieldElement<F>,
ys: &[FieldElement<F>],
polynomials: &[Polynomial<FieldElement<F>>],
upsilon: &FieldElement<F>,
) -> Self::Commitment {
let acc_polynomial = polynomials
.iter()
.rev()
.fold(Polynomial::zero(), |acc, polynomial| {
acc * upsilon.to_owned() + polynomial
});
let acc_y = ys
.iter()
.rev()
.fold(FieldElement::zero(), |acc, y| acc * upsilon.to_owned() + y);
self.open(x, &acc_y, &acc_polynomial)
}
fn verify_batch(
&self,
x: &FieldElement<F>,
ys: &[FieldElement<F>],
p_commitments: &[Self::Commitment],
proof: &Self::Commitment,
upsilon: &FieldElement<F>,
) -> bool {
let acc_commitment =
p_commitments
.iter()
.rev()
.fold(P::G1Point::neutral_element(), |acc, point| {
acc.operate_with_self(upsilon.to_owned().representative())
.operate_with(point)
});
let acc_y = ys
.iter()
.rev()
.fold(FieldElement::zero(), |acc, y| acc * upsilon.to_owned() + y);
self.verify(x, &acc_y, &acc_commitment, proof)
}
In the code, u
(upsilon
) can be selected by the prover. By hardcoding it to 0
instead of a random value, only p1
is checked. Now since p4
isn't checked, you can keep the polynomial the same so that Vegeta can verify the commitment to it, and confirm Gohan's power level. But you can change the y4
value, which would never be verified in the batch_verify
function. As a result, the sum of power levels appears to be over 9000, fooling the villains.
By Vladimir
-
$G_1$ ,$G_2$ - multiplicative group generators on BLS12-381 -
$[x]_1$ is a point $xG_1$ as opposed to $[x]_2$ which is $xG_2$. It can be thought as encryption of$x$ that is additively homomorphic:$[x]_1$ +$[y]_1$ =$[x+y]_1$ , but you can't get$x$ from$[x]_1$ or$[x]_2$ , and you can't get$[x]_1$ from$[x]_2$ either
The prover is trying to assure the Verifier that a set of polynomials have certain evaluations at some point. Below we look into more details how batching is performed and how it can be broken.
An individual proof for KZG10 is calculated as following:
where
-
$p_i$ is an individual polynomial in the set -
$\tau$ is a secret point from$SRS$ -
$X$ is a point where the evaluation is being proven -
$y_i$ is an individual evaluation being proven
The fragile part of this protocol lies of course in the batching mechanism. Batch proof is just a proof for a new polynomial that is composed from individual polynomials:
So the Prover sends the following:
- batch proof
$\pi_{batch}$ - individual commitments
$p(\tau)$ - individual evaluations
$y_i$ - challenge
$\upsilon$
And the verifier checks that $$ e(\pi_{batch}, [\tau-X]_2) = e([P(\tau)-Y]_1,[1]_2)\tag {2}$$
The challenge requires us to change an individual component
The suggested protocol could be fine if only it is interactive whereas
Since in this particular case
$$Y = \upsilon^3y_3+\upsilon^2y_2 + \upsilony_1 + y_0 = \upsilon'^3(y_3+1)+\upsilon'^2y_2 + \upsilon'y_1 + y_0$$ $$\upsilon^3y_3+\upsilon^2y_2 + \upsilony_1 = \upsilon'^3(y_3+1)+\upsilon'^2*y_2 + \upsilon'*y_1$$
In general this is a cubic equation with respect to
Alternatively we could leave
$$Y' = \upsilon^3*(y_3+1)+\upsilon^2y_2 + \upsilony_1 + y_0 = \upsilon^3y_3+\upsilon^3+\upsilon^2y_2 + \upsilon*y_1 + y_0 = Y + \upsilon^3$$
So the only thing that we need to do is to make one of other polynomials contribution to evaluation to be equal exactly
So that gives us exactly what we wanter if we pass a fake
The solution for
let u = FieldElement::zero();
and now we can create a new set of evaluations, that includes a fake value
let y4_fake = y4.clone() + FieldElement::one();
let ys_fake = [y1.clone(), y2.clone(), y3.clone(), y4_fake.clone()];
let total_power = u32::from_str_radix(&ys_fake[3].to_string()[2..], 16).unwrap();
println!("total power: {}", total_power);
assert!(total_power > 9000);
assert!(kzg.verify_batch(&x, &ys_fake, &ps_c, &proof, &FieldElement::zero()));
Now we have Gohan power 9001 that still statisfies batch proof check
For alternative solution we change coefficient for an individual polynomial:
// Sample random u
let u = FieldElement::from(rand::random::<u64>());
let p1_coeffs = [u.pow(3 as u32)];
This is the case for y1
variable and ys[0]
element in evaluations array). Of course for
let ys_fake = [FieldElement::zero(), y2, y3, y4 + FieldElement::one()];
And you're good to go!
let total_power = u32::from_str_radix(&ys_fake[3].to_string()[2..], 16).unwrap();
println!("total power: {}", total_power);
assert!(total_power > 9000);
assert!(kzg.verify_batch(&x, &ys_fake, &ps_c, &proof, &u));
You will see that y4
variable, y[3]
element in evaluations ) is greater than 9000 and the batch proof is again successfully checked.
This challenge was comparatively easy if you get familiar with KZG. From the very first sight batch proof seems suspicious. Furthermore vulnerabilities that are caused by bad Fiat-Shamir are very common, so this should be one of the first things to check. To fix this protocol we would have to either check individual proofs, which defeats the purpose of batching, or we need to make sure, that the Prover can't mess with the challenge, because otherwise everything can be just solved backwards for any specific result the Prover chooses.