gnark: Your Guide to Write zkSNARKs in Go
zkSNARKs have important applications for blockchains, especially for scalability and privacy. It is an acronym for zero-knowledge succinct non-interactive argument of knowledge and refers to a family of cryptographic protocols that allows a party to prove to another that it holds some information, without disclosing that information, in a very short amount of time.
The zero-knowledge research space is booming with advancements in programmability (verifying execution trace of virtual machines inside a SNARK), and performance (new schemes, better composability, hardware-implementations).
Virtual machines in SNARKs, like the zkEVM our colleagues are working on, offer great advances towards programmability. We can see these zkVMs as a single zkSNARK circuit capable of verifying executions of many different programs.
Specialized circuits – one zkSNARK circuit per program – still make a lot of sense where performance is critical (for example; to verify the inner proof of a zkVM execution on the blockchain) or at the protocol level (bridges, light clients, privacy-preserving protocols). gnark’s goal is to enable efficient specialized circuits.
In this post, we will learn more about gnark and walk through some on-going work our research teams are doing.
A Tour of gnark
gnark circuits are written in plain Go and can be integrated in complex solutions like any other Go module. Serialization, web-services, logging, are all but a couple of lines of code away.
gnark’s philosophy is simple; no domain-specific language (DSL), one codebase without external dependencies which integrates and performs well on server, browser, and mobile. gnark ensures a smooth learning curve, a mature tool chain, and blazingly fast compile and proving times. Go enables all this and a battle-tested developer environment, which includes dependency management, unit tests, benchmarking, IDE integration. These features are stable, free, and used and maintained by hundreds of thousands of users.
Source: gnark Playground
gnark supports several elliptic curves and multiple backends (Groth16, PlonK w/ KZG or FRI), with more to come. In particular, we are looking at recursion, emulated arithmetic, GKR (faster hash verification) and new field-agnostic proof systems.
Let’s walk through an example. We are going to prove that a BLS signature is valid, inside a SNARK.
To do so, we use bls12-377/bw6-761 forming a 2-chain. The BLS signature is defined over bls12-377 base field, which happens to be bw6-761 scalar field; in other words, we are going to instantiate a SNARK over bw6-761 (the outer curve) and prove a statement defined on bls12-377 (the inner curve). This recursion technique can be used to verify SNARKs inside of a SNARK very efficiently.
This leverages on our “in-circuit” pairing implementation in 11.5k constraints, which is, to the best of our knowledge, the most efficient to date.
First we define the circuit:
Then we define the constraints (using gnark/std/algebra).
And compile the constraint system:
gnark helps identify performance bottlenecks by outputting pprof compatible data. Here we can see the graph of the constraints generated by the example circuit.
Finally we run the setup/prove/verify algorithms:
Alternatively, gnark exposes a test engine, which doesn’t build a constraint system but simply executes the circuit Define method. It makes it extremely convenient to debug, fuzz-test or cross-test against “out-of-circuit” implementation.
gnark is Blazingly Fast
On low-level primitives like the pairing or the field multiplication, gnark-crypto outperforms most libraries out there. It translates well up-the-stack – gnark compiles gigantic circuits in seconds, and its solver (aka witness generation), prover and verifier are significantly faster than circom/rapidsnark or arkworks. For protocols like zk-Rollups, this translates directly in throughput on the network.
Groth16 prover time, 65k and 8M constraints (AWS hpc6a instance)
Groth16 verifier time (AWS hpc6a instance)
gnark-crypto provides components of independent interest, ranging from field arithmetic to polynomial commitment schemes (e.g. KZG). The image below gives an overview of the packages.
All primitives in gnark-crypto can be used independently from gnark.