Securing Zero-Knowledge Implementations

  • Petr Korolev | Co-founder @0xorio

  • Lev Menshchikov | Security Researcher @0xorio

Automatically translated by AI.

Introduction

This summary covers six common vulnerabilities, basic attacks and their prevention methods, as well as security practices used when writing secure code.

Under-Constrained and Non-Deterministic Circuits

The first and most basic concept is Under-Constrained Circuits. This occurs when you create a circuit — a fundamental building block in Zero-Knowledge (ZK) — and often users do not verify its parameters. It is important to ensure a setup in which you will have only one proof confirming the statement. If your circuit is under-constrained, there may be a possibility of generating multiple different proofs for the same circuit.

Here’s an example of basic code in Circom, the standard language for writing zk-SNARKs. This example simply checks that you have two numbers which, when multiplied, produce a given value.

You have a private input and these two numbers. In this case, it is possible to generate two different proofs because there are no constraints checking these values in the given context.

Several issues arise here:

  • First: Numbers can be both "+1" and "-1", and both options would be valid.
  • Second: Overflow possibility. In this case, we work with fields, and all multiplications are performed modulo P. For example, if we have a field of order 100 and value is 10, then if you provide a value of 110, it will wrap around according to the field order, and the result will again be 10.
  • Third: Possibility of guessing an additional valid value for the given circuit.

Therefore, Under-Constrained Circuits are the most basic thing to watch out for when writing code, and you should use various static analyzers.

Arithmetic Overflows and Underflows

Overflow and underflow are common issues in low-level programming languages. In some high-level languages like Solidity, safety measures such as the "SafeMath" library were introduced to prevent accidental overflows and to encourage secure practices.

In Circom, you are constantly working with fields where overflows frequently occur, which can lead to incorrect results or vulnerabilities such as double spend.

Roman Semenov, one of the creators of the first widely-used zk-SNARK application TornadoCash, found a huge vulnerability that allowed double spend.

This vulnerability affected many libraries such as miximus, ZoKrates, and snarksjs, which converted 256-bit Solidity numbers into 254-bit numbers for the bn254 field in circuits.

To avoid this, you must ensure that values do not exceed their maximum allowed size.

Mismatching Bit Length

In circuits, only basic operations such as multiplication, addition, and comparison are typically used. However, converting numbers for comparison can sometimes lead to incorrect results.

For example, in a comparison that checks whether one number is greater than another, both inputs are first converted into binary using the standard Num2Bits() function and then compared. If the inputs are 1 (001) and 9 (1001), they may be truncated to a certain bit length, resulting in an incorrect comparison: 9 < 1.

To avoid such errors, set a maximum bit length and convert each input with Num2Bits() to the same bit length before comparing them.

Unused Public Inputs Optimized Out

Another subtle issue is optimization. The compiler may remove unused public inputs (e.g., signal inThree).

To prevent this input from being removed, you can, for example, create a fake parameter that stores the square of its value. In this case, the compiler will keep the input.

Do Not Use Custom Cryptography

Serious vulnerabilities occur when developers create their own cryptography. This happened with the Fiat-Shamir hashing implementation called "Frozen Heart". While the basic algorithm was fine, porting it to ZK introduced problems because circuits provide non-interactive proofs. The hashing algorithm was implemented in such a way that the result could be predicted, making the hash predictable. zk-SNARK security relies on random values, but in this case, the value could be precomputed from the hash function.

To avoid this, refer to audited implementations on zkdocs.com, where the implementation principles are explained in detail.

Trusted Setup Mechanism

Trusted setup is the basic mechanism needed to generate public and private parameters for circuits, enabling the prover to later create a proof for the verifier that they know the secret polynomial. During the "mixing of all secrets", "toxic waste" is produced — data known to the people who organized the ceremony — which must be securely deleted. If it is not deleted, proofs could be forged in the future. In the past, setups had to be re-run for each update. Today, universal setups are used so the ceremony does not need to be repeated each time.

The problem is that the "toxic waste" that should have been deleted was recorded in publicly available transcripts. Initially, everyone thought these transcripts contained safe data unrelated to the toxic waste and could be published. Later, it turned out that some parameters were interdependent, and from the transcripts, one could reconstruct the toxic waste and compromise the system.

Tools and Tests

Below is a selection of different static analyzers, tools, and frameworks for writing tests.

Conclusion

To understand the differences between Solidity and Circom in zk-SNARKs, it is important to study vulnerabilities, math, and functions, and understand their application and context.

Read more

More Events