Basic Usage

This page outlines walks through the basic usage of zksk library, leveraging only built-in primitives.

Notation and Syntax

The computations are done within cyclic groups induced by elliptic curves. When we use mathematical expressions, we write points in those groups in uppercase, and scalar numbers in lowercase. We write the group operation additively, i.e., \(+\) denotes the group operation.

In code, however, we follow the Python conventions and write both groups and group elements in lowercase.

We use Camenisch-Stadler notation [CS97] for zero-knowledge proofs. A proof of knowledge of a secret integer \(x\) such that \(Y = x G\) for some group element \(G\) is denoted as follows:

\[PK \{ x : Y = x G \}\]

Tip

We use petlib library for working with elliptic curves. We strongly advise you to take a look at petlib’s documentation page.

Overview

This library enables zero-knowledge proofs that are composed of the following blocks:

  • Discrete Logarithm representation. A proof of knowledge of secret integers \(x_i\), such that \(Y\) can be written as \(Y = \sum_i x_i G_i\):

    \[PK \{ (x_0, x_1, ..., x_n): Y = x_0 G_0 + x_1 G_1 + ... + x_n G_n \}\]
  • AND, conjunctions of other proofs.

  • OR, disjunctions of other proofs.

  • Your own custom proof primitive.

Apart from discrete-logarithm representations, we include other useful primitives in the library distribution.

The library supports three different modes of using zero-knowledge proofs:

  • Interactive proof. You can get the messages that need to be transmitted between a prover and a verifier, along with functionality to verify those messages.

  • Non-interactive proof through Fiat-Shamir heuristic.

  • Simulation.

Defining a Simple Proof Statement

In this example, we define a proof for the following statement:

\[PK \{ (x_1, x_2): Y = x_0 G_1 + x_1 G_1 \}\]

Tip

The next steps use petlib’s big number and elliptic curve syntax (petlib.bn, petlib.ec). We encourage you to get familiar with them on petlib’s documentation page.

First, we set up the group elements: elliptic curve points \(G_i\), and the secrets \(x_i\).

g0 = group.hash_to_point(b"g0")
g1 = group.hash_to_point(b"g1")

# Preparing the secrets.
# In practice, they probably should be big integers (petlib.bn.Bn)
x0 = Secret()
x1 = Secret()

Then, we can define a proof statement like this:

# First, compute the value, "left-hand side".
y = 4 * g0 + 42 * g1

# Next, create the proof statement.
stmt = DLRep(y, x0 * g0 + x1 * g1)

See the next section for more details about secrets and expressions that can be specified in zksk.

Executing the Proofs

zksk supports both intractive and non-interactive proof modes. To execute the interactive proof protocol, we are going to instantiate a zksk.base.Verifier and a zksk.base.Prover. In a realistic setup, one would not get both from the same proof object (typically the prover and the verifier sides are not running on the same machine).

prover = stmt.get_prover({x0: 4, x1: 42})
verifier = stmt.get_verifier()

The prover and verifier are going to interact using a sigma protocol, ending with the verifier accepting or rejecting the proof.

challenge = verifier.send_challenge(commitment)
response = prover.compute_response(challenge)
assert verifier.verify(response)

Once you have defined your proof along with the values of the secrets, you can call the zksk.base.Prover.prove() method to get a non-interactive ZK proof (NIZK):

nizk = stmt.prove({x0: 4, x1: 42})

The returned zksk.base.NIZK object embeds a challenge, a list of responses, a hash of the proof statement and a precommitment, if any.

To verify a NIZK, the verifier needs reconstruct the same statement and call zksk.base.Verifier.verify():

stmt.verify(nizk)

Secrets and Expressions

A zksk.expr.Secret objects represent the secret integer in a zero-knowledge proof. A secret has a name and a value, but both can be empty. It can be constructed without any arguments:

x = Secret()

In this case, a name is assigned automatically.

To provide a value, construct a secret like this:

x = Secret(value=42)

If a secret contains a value, the proof will be able to use it, otherwise a prover will wait for a dictionary that maps secrets to values. The following two equivalent snippets illustrate this:

# If secret come with values, prover will get them.
x = Secret(value=4, name="x")
y = x.value * g
stmt = DLRep(y, x * g)
prover = stmt.get_prover()
# Otherwise, a prover needs to have the values as a dictionary.
x = Secret(name="x")
value = 4
stmt = DLRep(value * g, x * g)
prover = stmt.get_prover({x: value})

The names of secrets do not matter, they are only used for debugging purposes. All secrets can be left unnamed.

Multiplying secrets by elliptic curve points, as in x * G, produces expressions. Expressions can also be added together. In general, an expression has the following form:

\[x_0 G_0 + x_1 G_1 + ... + x_n G_n\]

For example:

x = Secret()
y = Secret()
g = group.hash_to_point(b"one")
h = group.hash_to_point(b"two")
expr = x * g + y * h

expr here represents \(x G + y H\).

If secrets have their values set, you can also evaluate an expression using zksk.expr.Expression.eval() method:

x = Secret(value=5)
expr = x * g
expr.eval()

This can simplify the redundant definition of a proof above:

stmt = DLRep(expr.eval(), expr)

Composing Proofs with AND

In this example, we show how to build an “and”-composition of two discrete-logarithm proofs:

\[PK \{ (x_0, x_1, x_2): \underbrace{Y_0 = x_0 G_0 + x_1 G_1}_{\text{First statement}} \land \underbrace{Y_1 = x_0 G_2 + x_2 G_3}_{\text{Second statement}} \}\]

As before, we initialize the points \(G_i\) and the secrets \(x_i\).

group = EcGroup()

# Create the base points on the curve.
g0 = group.generator()
g1 = group.hash_to_point(b"one")
g2 = group.hash_to_point(b"two")
g3 = group.hash_to_point(b"three")

# Preparing the secrets.
# In practice, they probably should be big integers (petlib.bn.Bn)
x0 = Secret()
x1 = Secret()
x2 = Secret()

Tip

If you need several group generators, as above, you can use the zksk.utils.groups.make_generators() function.

Then, we can create the “and”-proof like this:

# First, compute the values, "left-hand side".
y1 = 4 * g0 + 5 * g1
y2 = 4 * g2 + 7 * g3

# Next, create the proof statement.
stmt = DLRep(y1, x0 * g0 + x1 * g1) \
     & DLRep(y2, x0 * g2 + x2 * g3)

This syntax enables us to almost copy the mathematical expression of the proof in the Camenisch-Stadler notation.

We can also instantiate subproofs separately and pass them to the zksk.composition.AndProofStmt. In fact, the above is just a simplified way of writing the following:

stmt_1 = DLRep(y1, x0 * g0 + x1 * g1)
stmt_2 = DLRep(y2, x0 * g2 + x2 * g3)

equivalent_stmt = AndProofStmt(stmt_1, stmt_2)

The two ways to construct the AndProofStmt are equivalent and work with an arbitrary number of parameters.

Composing proofs takes into consideration the re-occuring secrets. The following are two not equivalent snippets:

x0 = Secret(4)
x1 = Secret(4)
stmt = DLRep(4 * g0, x0 * g0) & DLRep(4 * g1, x1 * g1)
# NOT the same as above. Note that x1_prime is used for both clauses.
x1_prime = Secret(4)
another_stmt = DLRep(4 * g0, x1_prime * g0) & DLRep(4 * g1, x1_prime * g1)

They are not equivalent as the second one will verify that the same zksk.expr.Secret object is used.

Executing the protocol is the same as in the previous example.

Composing Proofs with OR

In this example, we show how to build an “or”-composition of two discrete-logarithm proofs:

\[PK \{ (x_0, x_1): \underbrace{Y_0 = x_0 G_0}_{\text{First statement}} \lor \underbrace{Y_1 = x_1 G_1}_{\text{Second statement}} \}\]

Or-proofs are slightly more complicated than and-proofs.

A simple way to define this or-proof is as follows:

# First, compute the values, "left-hand side".
y0 = x0.value * g0
y1 = x1.value * g1

# Next, create the proof statement.
stmt = DLRep(y0, x0 * g0) | DLRep(y1, x1 * g1)

An or-proof works by simulating all subproofs but the one true subproof which will be actually proved. Before executing the protocol, you can explicitly define which subproofs will be simulated.

# Set the first clause as simulated.
stmt.subproofs[0].set_simulated()

This ensures that this subproof is not picked for the legitimate execution.

Note

When constructing an or-proof, orp = p1 | p2, the p1 and p2 are copied. After the or-proof is constructed, modifying the original objects does not change the composed proof.

Tip

You don’t need to provide all the secret values for the or-proof. The library will draw a random subproof to execute, but it will choose only among those for which you provided all secrets.

Equivalently, you can use zksk.composition.OrProofStmt:

# This is an equivalent way to define the proof statement above.
stmt_1 = DLRep(y0, x0 * g0)
stmt_2 = DLRep(y1, x1 * g1)
stmt_1.set_simulated()

equivalent_stmt = OrProofStmt(stmt_1, stmt_2)

The built-in primitives such as zksk.primitives.dlrep.DLRep accept a simulated parameter. You can use it to mark which subproof to simulate at its construction time:

# Another equivalent way.
stmt_1 = DLRep(y0, x0 * g0, simulated=True)
stmt_2 = DLRep(y1, x1 * g1)

equivalent_stmt = OrProofStmt(stmt_1, stmt_2)

Executing the protocol is the same as in the previous sections.

Composing AND and OR

You can have complex composition trees:

\[PK\{ (x_0, x_1, x_2): (Y_0 = x_0 G_0 \lor Y_1 = x_1 G_1) \land Y_2 = x_2 G_2 \}\]

Definining this statement amounts to the following:

y0 = x0.value * g0
y1 = x1.value * g1
y2 = x2.value * g2
stmt = (DLRep(y0, x0 * g0) | DLRep(y1, x1 * g1)) & DLRep(y2, x2 * g2)

Some Special Cases are not Allowed

The library cannot prevent all cases of flawed proofs, but it will try its best to detect issues. For that, it needs to impose certain limitations on the expressivity of the proofs.

  1. When reusing a secret \(x\) with two different group points \(G_0\), \(G_1\), the groups induced by \(G_0\) and \(G_1\) must have the same size.

  2. No secret should appear at the same time in and out of an or-proof. This proof will fail to instantiate:

    \[PK\{ (x_0, x_1): Y_0 = x_0 G_0 \land  (Y_1 = x_1 G_1 \lor Y_2 = x_0 G_2 ) \}\]
  3. You must use the same order in expressions when proving and verifying. Proving non-interactively \(PK\{(x_0, x_1): Y = x_0 G_0 + x_1 G_1 \}\), but verifying \(PK\{(x_0, x_1): Y = x_1 G_1 + x_0 G_0 \}\) will fail as proofs generate and compare order-sensitive identifiers.

CS97

J. Camenisch and M. Stadler, “Proof systems for general statements about discrete logarithms,” Tech. rep. 260, Mar. 1997