Try   HackMD

ZK-Hack Puzzle #4 Writeup

Intro

This week the 4th puzzle of the ZK-Hack event was released.
The event includes really cool workshops and puzzles with the goal of teaching more people about the world of Zero-Knowledge.
I (@shalev0s) solved the challenges together with Matan and Elichai.
We also write writeups for every challenge available here: puzzle 1, puzzle 2, puzzle 3.

Hidden in Plain Sight

Sometimes you can say too much

We are also told to read about KZG polynomial commitments.

Then we get to the challenge itself.

One-thousand accounts participate in a shielded pool which hides the
recipients and other data in each transaction between parties in the pool. The
*recipient* is a 256-bit account address which is hidden by blinding a KZG-like
polynomial commitment to the address. The sender of the transaction chooses two
secret *blinding factors* known only to them by which the polynomial commitment
is blinded. Included with the commitment are two openings used to verify the
commitment. You intercept a transaction by observing the shielded pool. Armed
with the blinded commitment and two openings from the intercepted transaction as
well as the public data (the trusted setup for the KZG commitment scheme, a list
of all one-thousand account addresses participating in the pool, and the two
random challenges used to compute the openings) can you deanonymize the
recipient address?

Blinding Scheme
---------------
The 256-bit recipient address is split into a vector of 32
bytes, and each byte (as a BLS12-381 scalar) becomes a coefficient of a
degree-31 polynomial P. There is an evaluation domain H = {1, ω, ω^2, ...,
ω^(n-1)} with $ω^n = 1$ and a vanishing polynomial Z_H(x) = x^n -1 which
evaluates to 0 on each element of the evaluation domain. The sender of the
transaction chooses two secret blinding factors b_0 and b_1 and computes the
blinded polynomial Q(x) = P(x) + (b_0 + b_1x) • Z_H(x).

Polynomial Commitment
---------------------

The KZG-like polynomial commitment scheme uses a public
trusted setup S={g, g•s, ..., g•s^(n+1)}$ where g is the generator point of the
BLS12-381 elliptic curve and s is a secret scalar. For the polynomial Q(x) = c_0
+ c_1x+c_2x^2 + ... + c_(n+1)x^(n+1) the commitment com(Q) = c_0•g + c_1•g•s +
c_2•g•s^2 + ... + c_(n+1)•g•s^(n+1).

Openings
--------
Openings of the polynomial are required in order to verify the
polynomial commitment. These are simple evaluations of the polynomial at random
challenges which are public. For instance, for challenges z_1 and z_2, the
openings are Q(z_1) and Q(z_2).

Setup

Let's take a closer look at our setup.

We are given the 256-bit addresses of a 1000 accounts of some network.
For every transaction in the network, the address of the recipient of the transaction is hidden using a blinded commitment.
We are given the blinded commitment of some transaction.

Our goal is to figure out who the recipient is out of all the possible accounts.
We are given:

  • The blinded commitment of the transaction
  • Two commitment openings for the blinded commitment
  • All 1000 account addresses
  • Some public trusted setup
    S
    needed for the commitment

Now let's dive into the blinded commitment scheme:

Blinding Scheme

(Based on the given code)
First, we set an evaluation domain of size

n:=1024 to be:
H={0i<n | ωi}
s.t.
ωn=1
, (roots of unity)

The account address is split into 32 bytes -

a0,,a31.
We then look at the tuples:
(ω0,a0),,(ω31,a31),(ω32,0),,(ωn1,0)

As
n
evaluations of a polynomial.
Using the Inverse FFT algorithm, we compute a polynomial
P(x)
of degree
<n
s.t. its evaluations at
ωi
are
ai
(or
0
for
i32
).

We also define a vanishing polynomial

ZH(x)=xn1.
Let the transaction creator pick to random values
b0,b1RFr
.
Define the blinded polynomial as:
Q(x):=P(x)+(b0+b1x)ZH(x)

Note that since
ZH(ωi)=0
for all
i
, then
Q(x)
evaluates to the same values as
P(x)
for all
ωi
.
Also, note that
deg(Q(x))=n+1
.

Polynomial Commitment

We assume a trusted public setup

S={0in+1 | siG}.
Where
G
is a generator of the BLS12-381 elliptic curve and
s
is a secret scalar not known to anyone.
For
Q(x)=i=0n+1qixi
, the commitment is:
comm(Q(x))=i=0n+1(qisi)G=(i=0n+1qisi)G=Q(s)G

This is the blinded commitment for an account.
It is given with two openings:
(z1,Q(z1)),(z2,Q(z2))
.

Solution

Openings

Let's break down our openings:

Q(z1)=P(z1)+(b0+b1z1)ZH(z1)Q(z2)=P(z2)+(b0+b1z2)ZH(z2)

Note, that for every account

a we can compute
Pa(x)
, the
P(x)
polymoial for that account.
For the rest of the solution we will always assume we are running the same computation for all accounts.

Since, we can compute

Pa(x) we can also compute
Pa(z1),Pa(z2)
.
We also know
ZH(x)
, so we can compute
ZH(z1),ZH(z2)
.
So we have:
Q(z1)P(z1)ZH(z1)=b0+b1z1Q(z2)P(z2)ZH(z2)=b0+b1z2

Note that we now have two linear equations with two unknowns

b0,b1.
We can easily compute them.

Reconstructing
Q(x)

Looking at the

Q(x) coeffients (for every polynomial denote its coeffients with the corresponding small letter):
Q(x)=Pa(x)+(b0+b1x)ZH(x)q0+q1x++qnxn+qn+1xn+1=(p0+p1x1++pn1xn1)+b0xn+b1xn+1b0b1x

Let's reorganize the coeffients a bit:
q0+q1x1++qnxn+qn+1xn+1=(p0b0)+(p1b1)x++pn1xn1+b0xn+b1xn+1

If 2 polymoials are equal, then so are their coeffients:

q0=p0b0q1=p1b12in1,  qi=piqn=b0qn+1=b1
All
pi,b0,b1
are known, therefore we can compute
Q(x)
.

All that's left to do is to compute

comm(Q(x)) and check if what we got is the same commitment as we have in the given transaction.
Since we know one of the accounts were used to create the given commitment, by iterating over all of them we must find one that satisfies the commitment.
The probability of the commitment being equal by chance is extremely low and therefore if the commitment is equal, we assume that the account used is indeed the recipient of the transaction.

Now In Code

fn main() { welcome(); puzzle(PUZZLE_DESCRIPTION); let (setup, accts, cha_1, cha_2, commt, opn_1, opn_2) = read_cha_from_file(); let mut solution_commitment = G1Affine::zero(); let number_of_accts = 1000usize; let domain: GeneralEvaluationDomain<Fr> = GeneralEvaluationDomain::new(number_of_accts + 2).unwrap(); // Solution: // To find the right account we iterated over all accts, we then saw that accts[535] solves it! for evals_p in accts.to_vec() { // We compute P_a(x) let p = DensePolynomial::from_coefficients_vec(domain.ifft(&evals_p)); // Evaluate it at z_1,z_2 let p_cha_1: Fr = p.evaluate(&cha_1); let p_cha_2: Fr = p.evaluate(&cha_2); // Here we just set up the 2 equations with 2 unknowns let e_cha_1 = opn_1 - p_cha_1; let e_cha_2 = opn_2 - p_cha_2; const N: u64 = 1024u64; // We compute b_0, b_1 let b_1 = ((e_cha_1 / (cha_1.pow(&[N]) - Fr::from(1))) - (e_cha_2 / (cha_2.pow(&[N]) - Fr::from(1)))) / (cha_1 - cha_2); let b_0 = (e_cha_1 / (cha_1.pow(&[N]) - Fr::from(1))) - (b_1 * cha_1); // We set Q(x) = P(x) since most of the coeffients are the same let mut q = vec![Fr::from(0); 1026]; p.coeffs .iter() .enumerate() .for_each(|(idx, f)| q[idx] = f.clone()); // We compute q_0,q_1,q_1024,q_1025 q[0] -= b_0; // q[0] = p[0] - b_0 q[1] -= b_1; // q[1] = p[1] - b_1 q[1024] = b_0.clone(); q[1025] = b_1.clone(); // We compute comm(Q(x)) and check if it's equal to the given commitment let dp = DensePolynomial::from_coefficients_vec(q); solution_commitment = kzg_commit(&dp, &setup); if (solution_commitment == commt) { break; } } // let solution_commitment = kzg_commit(&solution_blinded_acct, &setup); assert_eq!(solution_commitment, commt); }

We run our code and find that accts[535] passes the assertion!
accts[535] is the recipient of the given transaction.
Success!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →