ZK Proofs — ZKBoo and ZKBoo+

Introduction of ZK proofs with Graph Isomorphisms.

July 14, 2025 · cryptography, zkml, zkproof

Goal: We want to convince someone that we know something without revealing what exactly we know!

As it usally happens we ask help to 🙍🏻‍♀️ Alice and 🙎🏻‍♂️ Bob. Alice is the prover and Bob is the verifier.

Let’s consider as an example the graph isomorphism problem.

Problem: Given two graphs G1 and G2, with the same number of vertices, we say that the graphs are isomorphic if there is a relabelling of the vertices of one graph, which produces
the second graph. The relabelling is called φ.

So φ is a function that transforms G1 into G2.

beginequation varPhi : G₁ longrightarrow G₂ endequation

This is a hard computational problem, basically the only way to find it out is to try all the permutation of vertices and see if we can get G2.

Example:

Here, the isomorphism is given as a permutation in cycle notation:

Phi = (1, 2, 4, 3)

This means:

Suppose Alice know the isomorphism φ that transforms G1 into G2.

φ is the prover’s private input that Alice doesn’t want to reveal. But Alice want to convince Bob about the fact that she really knows φ. This is where zero knowledge proofs can help us.

Alice takes the graph G2 and applies a secret random permutation ψ to the vertices of G2 to
produce another isomorphic graph H.

This new isomorphism can be ψ = (1, 2). So now we get:

Peggy now publishes H as a commitment, she of course knows all the following secret graph
isomorphisms:

Phi : G₁ longrightarrow G₂ Psi : G₂ longrightarrow H Psi circ Phi = Phi · Psi : G₁ longrightarrow H

Now Bob gives Alice a challenge! He chooses randomly G1 or G2, and asks for the isomorphism between H and G_i (where i is 1 or 2).

So Alice will give bob either χ = ψ or χ = Φ· ψ. But non of these 2 solutions to the challenge will reveal the secret φ.

Foe example, if Bob chooses G1 then Alice publishes
Phi · ψ = (1, 2, 4, 3) · (1, 2) = (2, 4, 3).

Bob now can verify that (2, 4, 3) actually converts G1 into H. So he starts thinking that Alice knows for real some secret φ.

Bob is still not sure though, because Alice can cheat. How?

Suppose Alice does not know φ, in other words, she doesn’t know the secret isomorphism between G1 and G2. She wants to convince Bob she does, anyway.

What can she do? ❓

But if Victor asks b=1, she must produce φ⋅ψ, an isomorphism G1→H.

But without knowing φ she cannot compute φ⋅ψ.

→ She’s stuck and cannot respond correctly if b=1 is chosen. So she has 50% of probability to cheat succesfully.

Note that randomness is essential because:

So if Alice is cheating she will only be able to respond to Bob correctly 50%
of the time
. So we can repeat the protocol over and over to lower the probability that Alice is cheating.

If Alice used the same H repeatedly, Bob could ask b=1 in one round and b=2 in another, and get both φ *ψ and ψ.

From these two, he could compute: (φ⋅ψ)*(ψ^-1) and learn φ, which she is supposed to keep secret. Thus, Alice must generate a fresh H each round to prevent Bob from learning anything.

The protocol properties we want to have in zk proofs are:

1️⃣ Completeness: If Alice knows φ, she always convinces Bob.

2️⃣ Soundness: If Alice does not know φ, she can only guess, so she has at most a 50% chance of cheating successfully in one round. Repeating the protocol k times makes her chance of cheating successfully drop to (1/2)^k, which becomes negligible.

This is it fir the introduction of zkproofs. Now let’s see two real ZKProof protocols named ZKBoo and ZKBoo+

ZKBoo and ZKBoo+

We can also define a zkproof as a way for a Prover to convince a Verifier that they know a secret x that satisfies a publicly known computation f(x) = y .

For example Alice want to prove hat she knows x= ( a,b,d) such that the following are satisfied:

a · b = c c + d = e

without revealing the secrets (a,b,d).

If we assume that we work only with boolean variables, we can model the computation f() as a boolean circuit.

Instead of restricting variables to just two possible values (0 or 1), we can allow them to take on more values, up to some maximum. To do this, we use a mathematical structure called a field, with a chosen prime number p All arithmetic operations are then performed modulo p (i.e., we work with numbers from 0 to p−1). This technique is known as ZKBoo+.

In order to understand how the ZKBoo protocol works we need to clarify some concepts, let’s start with secret sharing.

Secret Sharing

We can split a variable x in 3 (or more) parts x1,x2,x3 such that:

x = x₁ + x₂ + x₃ ±odp

So if we share with somebody part of our secret e.g (x2 and x3) we will still not reveal the secret x.

Multi-Party Computation in the head (MPC)

Pretend that the three parts of a secret variable are held by three different parties.

Each actor (or party) only knows its own share of each value. So if our computations include variables a,b,c,d,e,r the actor i will have access to a_i, b_i, c_i, d_i, e_i and r_i.

The party can perform the computation locally, only on the shares he knows

We differentiate between addition and XOR based if we use ZKBoo or ZKBoo+

In order to support multiplication we need to be a bit smart.

Part of our original computation was :

a · b = c

So let’s split these variables.

a = a₁ + a₂ + a₃ b = b₁ + b₂ + b₃ c = c₁ + c₂ + c₃

Now each party is able to do its own computation

extActor 1: quad a₁ · b₁ = c₁ extActor 2: quad a₂ · b₂ = c₂ extActor 3: quad a₃ · b₃ = c₃

But we want that the following equation still holds:

a + b = (a₁ b₁) + (a₂ b₂) + (a₃ b₃)

But this is not true now because

a + b =(a₁ + a₂ + a₃) + (b₁ + b₂ + b₃) =a₁ b₁ + a₁ b₂ + a₁ b₃ + dots + a₃ b₁ + a₃ b₂ + a₃ b₃ eq c₁ + c₂ + c₃ =(a₁ b₁) + (a₂ b₂) + (a₃ b₃)

Crap! This is because in multiplications we have cross terms! So the trick is to split the cross terms and define the following:

c₁ = a₁ b₁ + a₁ b₂ + a₂ b₁ c₂ = a₂ b₂ + a₂ b₃ + a₃ b₂ c₃ = a₃ b₃ + a₁ b₃ + a₃ b₁

Now it holds that :

a · b =(a₁ + a₂ + a₃) + (b₁ + b₂ + b₃) =c₁ + c₂ + c₃

Commitments

A commitment allows us to promise to some value without revealing it. A commitment is like a sealed envelope:

A good commitment is:

A simple commit is the hash of some value.

com = SHA256(m || r)

Meaning that we can commit a message m, by adding to it some random noise by concatenating and the “crypting” the message with a hash function.

The verifier can see the hash message, and if he gets later the information m,r he can check if he obtain the same hash, so if the commitment was truthful.

Example:

m = “marcello”

r = “123”

SHA256(”marcello123”) —> “41ADf3gf34sd2”

The prover uses “41ADf3gf34sd2” as a committment. When the verifier will get “marcello” and “123” he can check weather SHA256(”marcello123”) really results in “41ADf3gf34sd2”.

Sigma Protocol

Many ZK proofs systems follow the sigma protocol, also called interactive protocol. In this case we have two parties interacting exchanging messages in three different steps.

The three messages are:

  1. A commitment
  2. A challenge
  3. A response to the challenge

This Sigma protocol is like a primitive ZKProof. It allow the prover to convince a verifier that some statement is true, without revealing the secret info.

Basically what happens is that the prover send to the verifier concatenates the variables of the original computation, so he commits to these variables.

The the verifies says something

If you really know the secret variables that solve the equations, show me some part of these secrets variables, so only some part of the secret sharing

The prover than shows these parts of the secret variable.

Now the verifier starts believing that the prover really knows the secret variable, but to be more convinced he once to do another round, and then another and so on. N times!

This is the general concept of the sigma protocol, we will go more in depth now by showing how this is implemented in the ZKBoo protocol.

We have all the ingredients for the ZKBoo!

Recall that the original set of equations for which we know the secrets variables (a, b, d) is:

a · b = c c + d = e

After the secret sharing each of this variable is split in three parts. Plus we will have also some random variable r split in r1, r2, and r3. I will explain soon why this is needed.

The prover can now commit to these 3 sets of variables (one set for each imaginary actor of the MPC).

Prover:Commitment: {com1, com2, com3} where com_k = hash(a_k, b_k, c_k, d_k, e_k, r_k), k = {1,2,3}

Verifier now challenges the prover asking him to reveal 2 columns. What columns? You can imagine the set of variables arranged in a table.

VariableParty 1 (k=1)Party 2 (k=2)Party 3 (k=3)
aa1a2a3
bb1b2b3
cc1c2c3
dd1d2d3
ee1e2e3

If the Prover shows 2 columns, the verifier can’t still infer the original variable since you need all the 3 part of the secrets.

We have one problem though. We defined c_k in a tricky way, so the actual value are:

VariableParty 1 (k=1)Party 2 (k=2)Party 3 (k=3)
aa1a2a3
bb1b2b3
ca1b1 + a1b2 + a2b1a2b2 + a2b3 + a3b2a3b3 + a1b3 + a3b1
dd1d2d3
ee1e2e3
rr1r2r3

In this case when we reveal:

We don’t want to show information about the columns that are not requested!

That is why we add some randomness, so that the it is more difficult for the verifier to infer any information out of it.

So the way we actually want to define c_k is:

c₁ = a₁ b₁ + a₁ b₂ + a₂ b₁ + r₁ - r₂ c₂ = a₂ b₂ + a₂ b₃ + a₃ b₂ + r₂ - r₃ c₃ = a₃ b₃ + a₁ b₃ + a₃ b₁ + r₃ - r₁

Note that when we add them up the random terms cancel out, so it still holds that c = c1 + c2 + c3.

But what columns the verifier asks to the prover to show? He has 3 possibilities : (1,2), (2,3) , (1,3).

So he randomly chooses 2 columns (i,j), and the verifier shows those columns.

The verifier now does:

  1. Consistency Check:

quad 1. quad c_i = a_i b_i + a_i b_j + a_j b_i + r_i - r_j quad 2. quad c_j = a_j b_j + a_j b_k + a_k b_j + r_j - r_k quad ( extcannot be checked: no k ext variables) quad 3. quad c_i + d_i = e_i quad 4. quad c_j + d_j = e_j

  1. Recompute the hashes of the commitment and double check the hold

Improve Soundness

As we have seen for the graph isomorphism at the beginning, the verifier has the chance of cheating equal to 1/3, if he can guess which columns will be requested by the verifier.
Thats why wee need to repeat several rounds of the protocol, and in each round the verifier will produce a different set of secret sharing variable, in order to don’t leak any secret info.

With n rounds, the probability of the verifier to cheat is (1/3)^N which becomes negligible!

Flat - Shamir Transform

We still have one problem though. With N rounds the messages that need to be exchanged are a lot! Can we summarise it entirely in one single message?

We said at the beginning, that is really important the randomness introduced by the verifier in asking randomly two columns (i,j).

But maybe the prover can simulate this randomness itself, so he doesn’t need to ask the prover.

The way to do this is to use an hash function.

So the steps of the algorithm become:

  1. Prover commits for all rounds
  2. Prover computes challenge for each round:
  3. challenge_k = SHA256(all previous commitments, pbl info, k ) mod 3
  4. Then, for each round k, the prover reveals the two columns according to challenge_k and sends all the responses to the verifier in a single package.
  5. The verifier simply recomputes the challenges using the same hash function and verifies the responses against the commitments and the public info.

Implementing ZKBoo

The following Python code is a demonstration of a Zero-Knowledge Proof (ZKP) using the ZKBoo protocol principles.

It shows how a prover can convince a verifier that they know secret values a,b,da, b, da,b,d such that a⋅b+d=ea cdot b + d = ea⋅b+d=e,

without revealing the secrets themselves.

The code implements:

It simulates a single round of the protocol and clearly logs all the steps to illustrate how the prover and verifier interact —

while guaranteeing that the verifier learns nothing about the secret values beyond the fact that the prover knows them.

import random, hashlib

p = 101  # Prime field modulus

class FiniteField:
    """Simple finite field implementation for GF(p)"""
    def __init__(self, modulus):
        self.p = modulus
    
    def random_element(self):
        """Generate a random field element"""
        return random.randint(0, self.p - 1)
    
    def add(self, a, b):
        """Addition in the field"""
        return (a + b) % self.p
    
    def sub(self, a, b):
        """Subtraction in the field"""
        return (a - b) % self.p
    
    def mul(self, a, b):
        """Multiplication in the field"""
        return (a * b) % self.p

F = FiniteField(p)

def secret_share(v):
    """Split 'v' into 3 random shares mod p."""
    s1, s2 = F.random_element(), F.random_element()
    s3 = (v - s1 - s2) % p
    print(f"Secret sharing {v}:")
    print(f"   s1 = {s1}, s2 = {s2}, s3 = {s3}")
    print(f"   Check: ({s1} + {s2} + {s3}) mod {p} = {(s1 + s2 + s3) % p} = {v}")
    return [s1, s2, s3]

def commit(vals):
    """Hash tuple of values to produce a commitment."""
    commitment = hashlib.sha256(",".join(map(str, vals)).encode()).hexdigest()
    print(f"Creating commitment for {vals}")
    print(f"   Commitment: {commitment[:16]}...")
    return commitment

def multiply_shares(a, b):
    """Compute c_i for a single gate a*b=c with offsets r_i."""
    print(f"\
MULTIPLICATION GATE:")
    print(f"   Input a shares: {a}")
    print(f"   Input b shares: {b}")
    print(f"   We need to compute c = a × b")
    print(f"   Expected result: sum(a) × sum(b) = {sum(a) % p} × {sum(b) % p} = {(sum(a) * sum(b)) % p}")
    
    r = [F.random_element() for _ in range(3)]
    print(f"   Random offsets r: {r}")
    
    print(f"   Computing c shares using ZKBoo multiplication formula:")
    print(f"   c[i] = a[i]×b[i] + a[i]×b[i+1] + a[i+1]×b[i] + r[i] - r[i+1]")
    
    c = []
    for i in range(3):
        j = (i + 1) % 3
        term1 = a[i] * b[i]
        term2 = a[i] * b[j]
        term3 = a[j] * b[i]
        term4 = r[i] - r[j]
        c_i = (term1 + term2 + term3 + term4) % p
        c.append(c_i)
        print(f"   c[{i}] = {a[i]}×{b[i]} + {a[i]}×{b[j]} + {a[j]}×{b[i]} + ({r[i]} - {r[j]})")
        print(f"       = {term1} + {term2} + {term3} + {term4} = {c_i}")
    
    print(f"   Result c shares: {c}")
    print(f"   Verification: sum(c) = {sum(c) % p} = {(sum(a) * sum(b)) % p}")
    
    assert sum(c) % p == (sum(a) * sum(b)) % p
    return c, r

def zkboo_prover(a, b, d):
    """Generate shares for a,b,d and compute c=a*b, e=c+d with random offsets."""
    print(f"\
PROVER PHASE:")
    print(f"   Secret values: a={a}, b={b}, d={d}")
    print(f"   Statement to prove: a×b + d = {a}×{b} + {d} = {a*b + d}")
    
    print(f"\
STEP 1: Secret sharing inputs")
    a_sh = secret_share(a)
    b_sh = secret_share(b)
    d_sh = secret_share(d)
    
    print(f"\
STEP 2: Computing multiplication c = a × b")
    c_sh, r_sh = multiply_shares(a_sh, b_sh)
    
    print(f"\
STEP 3: Computing addition e = c + d")
    e_sh = [(c_sh[i] + d_sh[i]) % p for i in range(3)]
    print(f"   c shares: {c_sh}")
    print(f"   d shares: {d_sh}")
    print(f"   e shares: {e_sh}")
    print(f"   Verification: sum(e) = {sum(e_sh) % p} = sum(c) + sum(d) = {sum(c_sh) % p} + {sum(d_sh) % p} = {(sum(c_sh) + sum(d_sh)) % p}")
    
    print(f"\
STEP 4: Creating commitments")
    commits = []
    for i in range(3):
        vals = (a_sh[i], b_sh[i], c_sh[i], d_sh[i], e_sh[i], r_sh[i])
        print(f"   Share {i}: a={a_sh[i]}, b={b_sh[i]}, c={c_sh[i]}, d={d_sh[i]}, e={e_sh[i]}, r={r_sh[i]}")
        commits.append(commit(vals))
    
    return a_sh, b_sh, c_sh, d_sh, e_sh, commits, r_sh

def zkboo_verifier_challenge():
    """Pick two random shares to reveal."""
    challenge = random.sample(range(3), 2)
    print(f"\
VERIFIER CHALLENGE:")
    print(f"   Randomly chosen shares to reveal: {challenge}")
    print(f"   Hidden share: {[i for i in range(3) if i not in challenge][0]}")
    print(f"   Why only 2 shares? Because 2 shares reveal nothing about the secret!")
    return challenge

def zkboo_prover_response(ch, a, b, c, d, e, r):
    """Reveal the requested two shares with all data."""
    print(f"\
PROVER RESPONSE:")
    print(f"   Revealing shares {ch[0]} and {ch[1]}:")
    response = [{"a": a[i], "b": b[i], "c": c[i], "d": d[i],
                 "e": e[i], "r": r[i]} for i in ch]
    
    for i, share_idx in enumerate(ch):
        share = response[i]
        print(f"   Share {share_idx}: a={share['a']}, b={share['b']}, c={share['c']}, d={share['d']}, e={share['e']}, r={share['r']}")
    
    return response

def zkboo_verify(ch, resp, commits):
    """Check commitments and verify correctness of revealed shares."""
    print(f"\
VERIFICATION PHASE:")
    
    print(f"   CHECK 1: Commitment verification")
    for i in range(2):
        share_data = (resp[i]["a"], resp[i]["b"], resp[i]["c"], 
                     resp[i]["d"], resp[i]["e"], resp[i]["r"])
        computed_commit = commit(share_data)
        original_commit = commits[ch[i]]
        match = computed_commit == original_commit
        print(f"     Share {ch[i]}: {'OK' if match else 'FAIL'} Commitment matches")
        if not match:
            print(f"     Commitment verification failed!")
            return False
    
    print(f"   CHECK 2: Multiplication consistency")
    def check_c(sh_i, sh_j):
        """Ensure multiplication consistency of revealed shares."""
        lhs = (sh_i["a"] * sh_i["b"] + sh_i["a"] * sh_j["b"] +
               sh_j["a"] * sh_i["b"] + (sh_i["r"] - sh_j["r"])) % p
        rhs = sh_i["c"]
        print(f"     Formula: {sh_i['a']}×{sh_i['b']} + {sh_i['a']}×{sh_j['b']} + {sh_j['a']}×{sh_i['b']} + ({sh_i['r']} - {sh_j['r']}) = {lhs}")
        print(f"     Should equal c = {rhs}: {'OK' if lhs == rhs else 'FAIL'}")
        return lhs == rhs

    if (ch[0] + 1) % 3 == ch[1]:
        print(f"     Checking adjacent shares {ch[0]} and {ch[1]}...")
        if not check_c(resp[0], resp[1]):
            print(f"     Multiplication verification failed!")
            return False
    elif (ch[1] + 1) % 3 == ch[0]:
        print(f"     Checking adjacent shares {ch[1]} and {ch[0]}...")
        if not check_c(resp[1], resp[0]):
            print(f"     Multiplication verification failed!")
            return False
    else:
        print(f"     Shares {ch[0]} and {ch[1]} are not adjacent - no multiplication check needed")
    
    print(f"   CHECK 3: Addition consistency (e = c + d)")
    for i, share in enumerate(resp):
        expected_e = (share["c"] + share["d"]) % p
        actual_e = share["e"]
        match = expected_e == actual_e
        print(f"     Share {ch[i]}: c + d = {share['c']} + {share['d']} = {expected_e} = e = {actual_e}: {'OK' if match else 'FAIL'}")
        if not match:
            print(f"     Addition verification failed!")
            return False
    
    print(f"   All checks passed. Proof is VALID.")
    return True

def test_zkboo_single_round():
    """Test a single-round reveal for a,b,d = 3,4,5."""
    print("=" * 70)
    print("ZKBOO ZERO-KNOWLEDGE PROOF DEMONSTRATION")
    print("=" * 70)
    print("Goal: Prove knowledge of a, b, d such that a×b + d = e")
    print("WITHOUT revealing the actual values of a, b, d!\
")
    
    a, b, d = 3, 4, 5
    print(f"Secret statement: {a} × {b} + {d} = {a*b + d}")
    print(f"   (The verifier will NOT learn these values)")
    
    a_sh, b_sh, c_sh, d_sh, e_sh, commits, r_sh = zkboo_prover(a, b, d)
    
    ch = zkboo_verifier_challenge()
    
    resp = zkboo_prover_response(ch, a_sh, b_sh, c_sh, d_sh, e_sh, r_sh)
    
    result = zkboo_verify(ch, resp, commits)
    
    print(f"\
FINAL RESULT:")
    if result:
        print("Zero-knowledge proof ACCEPTED.")
        print("   The prover has convinced the verifier they know values a, b, d")
        print(f"   such that a×b + d = {a*b + d}, without revealing the actual values.")
    else:
        print("Zero-knowledge proof REJECTED.")
        print("   The prover failed to convince the verifier.")
    
    print("=" * 70)
    assert result, "ZKBoo single-round failed"

test_zkboo_single_round()

Implement multiple rounds

def test_zkboo_multiple_rounds(num_rounds=10):
    print("=" * 70)
    print(f"ZKBOO ZERO-KNOWLEDGE PROOF WITH {num_rounds} ROUNDS")
    print("=" * 70)
    print("Goal: Prove knowledge of a, b, d such that a×b + d = e")
    print("WITHOUT revealing the actual values of a, b, d!\
")
    
    a, b, d = 3, 4, 5
    print(f"Secret statement: {a} × {b} + {d} = {a*b + d}")
    print(f"   (The verifier will NOT learn these values)\
")
    
    all_passed = True
    
    for round_num in range(1, num_rounds + 1):
        print(f"\
\
=== ROUND {round_num} ===\
")
        
        # Prover generates proof
        a_sh, b_sh, c_sh, d_sh, e_sh, commits, r_sh = zkboo_prover(a, b, d)
        
        # Verifier sends challenge
        ch = zkboo_verifier_challenge()
        
        # Prover responds
        resp = zkboo_prover_response(ch, a_sh, b_sh, c_sh, d_sh, e_sh, r_sh)
        
        # Verifier checks
        result = zkboo_verify(ch, resp, commits)
        
        if not result:
            all_passed = False
            print(f"Round {round_num} FAILED")
            break
        else:
            print(f"Round {round_num} PASSED")
    
    print("\
 FINAL RESULT:")
    if all_passed:
        print(f"All {num_rounds} rounds passed. Proof accepted.")
    else:
        print(f"Proof failed at round {round_num}.")
    
    print("=" * 70)

# Run with desired number of rounds
test_zkboo_multiple_rounds(10)