This repository represents an implementation of Groth16 ZK Snark construction for any R1CS. To generate/verify a proof, you only need to provide your problem statement ecoded as R1CS in r1cs.json
and a valid witness in witness.json
. The usage is as follows:
go build &&
./r1cs-zk-go setup # Run the trusted setup. The pk and vk are saved into json files
./r1cs-zk-go prove # generating a proof using 'pk.json' for 'r1cs.json' and 'witness.json'. The proof is saved into 'proof.json'
./r1cs-zk-go verify # reads the proof from 'proof.json' and verify it using 'vk.json'
The construction was built incrementally, in 4 steps. The reasoning behind each step and its commit code is explained below. The last commit is the final construction.
Example and reasoning are inspired from my journey reading ZK-Book
Implementation of step 1 is at commit 4aa7ad5
It is so brilliant how far you can go in building zero-knowledge with just ECC points and pairings. This repository is an ongoing implementation of ZK construction and it supports any problem statement that can be written as a set of constraints (circuit). You only need to adapt the circuit matrices (main.go
).
The code in main.go is a (not so) zk code that proves the following statement:
I know a number
x
which is a solution for$x^3 + 5x + 5 = 155$
We start first by building our arithmetic circuit in an R1CS format:
To make our life easier, we convert this to the following R1CS to have it in the form
Our witness vector is
So, the matrix representation for the R1CS is the following:
The naive approach is to send the vector
So, instead, we encrypt the vector
Given the hardness of the discrete log problem, a verifier cannot, for example, determine
So, the matrix representation for the R1CS becomes:
After performing the Hadamard product, we get:
Notice that we haven't encrypted the witness
This comes from the bilinearity property, more specifically:
So, the prover needs to encrypt
The correct solution is
Now, the prover sends the above 6 points to the verifier and the latter calculates and checks the pairing result. More specifically, the verifier checks that:
$e(5G1, 5G2) == e(25G1, G2)$ $e(5G1, 155G2) == e(125G1, G2)$
If both the above verifications pass, then the prover has sent a valid proof and he knows the number
- The construction is not zero-knowledge secure. A verifier, by doing guesses, can infer the witness
$a$ by simply multiplying his guess with$G_1$ and$G_2$ and comparing the result with the points sent by the prover. - Definitely, the construction is not succinct. The verifier needs to do two pairings in our example to check the proof. We want to have a
$O(1)$ verification complexity
In the previous step, we established a foundation for our proving system. However, a significant issue remains: the construction is not yet succinct. Specifically, if we have
To address this, let's reconsider what the verifier is checking:
Therefore, for the
This works because The group of vectors under addition in a finite field is homomorphic to the group of polynomials under addition in a finite field.
Naturally, the degrees of
Our proof system therefore becomes:
However, there is an issue: the polynomial
We must constrain the polynomial
Our proof system thus becomes:
The verifier then simply checks that:
Thanks to the Schwartz-Zippel Lemma, this ensures with very high probability that the two polynomials are equal and thus, the underlying vectors interpolated are equal!.
However, this requires the verifier to trust that the prover is evaluating the polynomials correctly. If the prover knows the point
Evaluating polynomials simply involves multiplying coefficients by the evaluation point:
We can generate a secret value
Since we require multiplication, we also generate another SRS where
This works because the polynomial
Therefore, the final output of the Trusted Setup Ceremony is:
Now, the prover can efficiently compute
The prover publishes
With this, we have achieved succinctness! Regardless of the number of constraints, the prover sends only three points, and the verifier needs to verify only a single pairing!
The implementation of this step is available at the commit 3dc8997
- Given that the verifier checks only the pairing of three points, the prover could send arbitrary points
$A$ ,$B$ ,$C$ that satisfy the pairing, and the verifier has no guarantee that these points were derived from the generated QAP. - Our proof system does not yet support public inputs, so the verifier has no means of injecting public inputs.
A critical missing component in our construction is ensuring that the prover must derive the points
To enforce this requirement, we must connect the QAP (the polynomials
Expanding, we obtain:
The rightmost boxed term is equivalent to the right side of the QAP equation. Thus, our new, expanded QAP equation is:
If we encode
Here,
However, the prover cannot compute
The trusted setup can generate
Our trusted setup is now updated to produce and return:
The prover then computes
The verifier checks that:
Now, the prover is compelled to derive valid points from the QAP, since the discrete logs of
The implementation of this step is available at the commt ebdf27e
- Our proof system still does not yet support public inputs, so the verifier has no means of injecting public inputs.
By convention, we assume that the public portion of the witness vector consists of the first
The prover computes:
Note that only computation of
The verifier then computes the sum corresponding to the public portion:
And verifies:
However, nothing prevents the prover from improperly reusing the public terms
TThe prover could maliciously choose the public portion of the witness as
This equation would still verify successfully, but the witness may not actually satisfy the original constraints. Therefore, we must ensure the prover cannot use public computation terms
To address this, the trusted setup introduces two scalars
Our updated trusted setup outputs:
This ensures that public and private computations are separated at the algebraic level (having different denominators), making it impossible for the prover to interchange them.
The prover’s computations remain as:
The verifier’s check now incorporates pairings with
Where
Implementation of this step is available at the HEAD commit
Our ZK construction is almost complete. One problem remains: the scheme is not yet truly zero-knowledge. If an attacker can guess the witness vector—possible when the set of valid inputs is small—they could confirm their guess by comparing their generated proof with the actual proof.