Franck Cassez, Head of Research at Mantle. — all views expressed are my own.
The zk-bug-tracker repository is an excellent resource to learn about bugs and vulneriabilities in ZK components. I was looking for a simple known bug to check whether, with some deductive verification techniques, I could identify it, fix it and formally prove correctness. The simplest bug I could find is the PSE & Scroll zkEVM: Missing Overflow Constraint bug. So I set out to implement the code and verify it with my favourite verification-friendly language: Dafny.
In a zk-proof system, a prover wants to convince a verifier that they have computed a function correctly. They both know the function, and in the example of the PSE & Scroll zkEVM: Missing Overflow Constraint this is the function Mod
defined by (the code is this article is written in Dafny):
The prover wants to convince the verifier that they have computed r
and that r == Mod(a, n)
for given a
and n
. Note that all the values are unsigned integers over 256 bits (u256
).
The verifier has a gadget to verify this claim in the form of a set of constraints. The constraints must have some predefined form (R1CS, QAP, AIR, etc) and represent what is usually called a circuit. In this instance, the gadget/circuit for verifying the Mod
function takes 4 input values k, n, r, a
and has one constraint[1] k * n + r == a [modulo 2^256]
.
Both the prover and the verifier know a
and n
. The protocol to verify the correctness of the computation is:
k, r
.k * n + r == a [modulo 2^256]
evaluates to true
.r == Mod(a, n)
;r == Mod(a, n)
the prover can supply a proof k, r
that is accepted by the verifier.How can we check whether the protocol is sound and/or complete?
We can specify the verifier's gadget with a Dafny predicate, a function that evaluates to eithertrue
or false
:
In this predicate,
TWO_256
is a constant equal to 2^256
,u256
is the set of unsigned integers over 256 bits (from 0
to TWO_256 - 1
),nat
is the type for mathematical unbounded natural numbers, and%
is the usual modulo operator on nat
.The predicate reads: "compute a * b + c
as an unbounded natural number, take the modulo TWO_256
and check that it is equal to d % TWO_256
." Note that d as nat % TWO_256
is d
as d
is less than TWO_256
(because it is a u256
).
Soundness and completeness are properties of the protocol and they can conveniently be specified as lemmas (theorems) in Dafny. Here is how the soundness property can be written:
The requires
is a precondition (assumption) of the lemma SoundnessProof
and the ensures
the postcondition (conclusion). As a result the lemma specifies the property: "If the verifier accepts then r == Mod(a, n)
".
Completeness is specified in a similar manner:
The CompletenessProof
lemma reads: "if r == Mod(a, n)
then there exists a value of k
such that the verifier accepts the proof". As a result if the prover has correctly computed r
they can supply a value of k
such that MulAddWordsGadget(k, n, r, a)
evaluates to true (the verifier accepts).
We can now try to verify the lemmas. By verify, I mean formally prove with the rules of logic (and the arithmetic properties of Mod
) that for all inputs that satisfy the preconditions, the postcondition is satisfied. The rules of logic and arithmetic properties are used by the Dafny verification engine to verify each step of a proof. Moreover, in Dafny, a proof is a program, so if you can write programs, you can write a proof.
Let's first try with a weaker version (I'll explain why later) of the SoundnessProof
lemma, where we assume that n > 0
(not zero) and we only want to ensure that Mod(r, n) == Mod(a, n)
rather than r == Mod(a, n)
:
The value of r
is actually 2^256 - 3
, and k * n + r % TWO_256
evaluates to 3
, same as a % TWO_256
so the verifier accepts. However Mod(r, n) == 1
but Mod(a, n) == 0
so the prover did not compute the correct value. The prover managed to supply k, r
that verifies but they have not computed the correct value of r
.
The problem has already been identified in this issue-996. The reason is that the MulAddWordsGadget
gadget is under constrained. Note that the counter example provided in issue-996 is different to the one provided by Dafny.
The fix is to add a constraint to ensure that no overflow occurs in the computation of (a as nat * b as nat + c as nat)
. Let's add another gadget (a predicate) that is true if and if the computation overflows:
The (weak) soundness proof now becomes:
Lemma1
, Dafny automatically verifies SoundnessProofWeak
.
Now let's try to verify the original lemma SoundnessProof
.
k, n, r, a
again and we perform some simple computation (e.g. with Python) to understand the counter-example better:
The issue here (I assume it has already been identified) is that we do NOT require r < n
. So the Mod
gadget is still under constrained. And this is the reason why I started with a weaker version of the soundness proof.
requires n > 0
(line 3) with requires r < n
fixes the problem and this time Dafny verifies lemma SoundnessProof
. Remember that verifies here means for all inputs the lemma holds.
There is one case with have not yet dealt with: n == 0
. In this case we have to add the constraint r == 0
to the gadget (I assume this has also been identified before). So overall, here is the final version of the lemma:
Mod
requires 3 constraints (and circuits) given by lines 3–5 in the SoundnessProofWithZero
lemma.
Now let's check completeness. Here is the specification (as above) augmented with a proof, using a simple lemma ModuloLemma2
that states that: if two numbers a
and b
are equal modulo n
then their difference is a multiple of n
.
CompletenessProof
lemma is valid.
What about the case n == 0
? In that case we cannot prove completeness. And indeed, the prover cannot build a proof except if a == 0.
Assume n == 0
and a != 0
. Then Mod(a, n) == 0
. So the prover should provide a proof of the form k, 0
. In that case the verifier computesk * n + r == k * 0 + 0 = 0
but a % TWO_256 != 0
and the verifier rejects the proof.
The formal proof of incompleteness can also be specified and verified with Dafny:
The previous lemma states that for any value of k
, MulAddWordsGadget(k, n, r, a)
evaluates to false
, implying that the prover cannot provide any k
to build an accepting proof (although they computed r
correctly as per line 3). I also assume that this is known fact.
The example above demosntrates that deductive formal verification is practical and applicable to proving zk-circuits. I did not have to write any proof code, only to import already proved arithmetic lemmas. The counter-examples were synthesised automatically.
The result above is a machine-checkable proof of soundness and completeness for the Mod
gadget. To the best of my knowledge this is the first proof of this type for this gadget. Unfortunately I have not found any formal proof bounty, and as a matter of fact you can get rewards for finding bugs, but not for proving the absence of bugs