-
1 Install Go: Make sure you have Go installed. You can download it from here
-
2 Initialize a Go Module: Create a new directory for your project and initialize a Go module
mkdir gnark_plonk_circuit
cd gnark_plonk_circuit
go mod init gnark_plonk_circuit
- 3 Install Gnark: Add the library to your project
go get github.com/consensys/gnark@v0.12.0
import (
"fmt"
"log"
"os"
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/plonk"
cs "github.com/consensys/gnark/constraint/bn254"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/test/unsafekzg"
"github.com/consensys/gnark/frontend/cs/scs"
)
Here's what each package is used for:
fmt: Standard Go library for formatted input/output.
log: Standard Go library for event logging.
os: Standard Go library for interacting with the operating system.
path/filepath: Standard Go library for portable file path manipulation.
github.com/consensys/gnark-crypto/ecc: Provides cryptographic operations over elliptic curves.
github.com/consensys/gnark/backend/plonk Gnark backend for the PLONK proving system.
github.com/consensys/gnark/constraint/bn254: Provides types and functions to work with constraint systems
specifically for the BN254 curve.
github.com/consensys/gnark/frontend: Provides the API for defining constraints and creating witness data.
github.com/consensys/gnark/test/unsafekzg: Gnark testing utilities for KZG commitments.
github.com/consensys/gnark/frontend/cs/scs: Gnark frontend for the SCS (Sparse Constraint System) builder.
The circuit structure is defined in this case using the equation
// CubicCircuit defines a simple circuit
// x**3 + x + 5 == y
type CubicCircuit struct {
X frontend.Variable `gnark:"x"`
Y frontend.Variable `gnark:",public"`
}
Here
CubicCircuitstruct contains the variables X and Y
X is a secret input, annotated as 'gnark:"x"'
Y is a public input, annotated as 'gnark:",public"'
Establish constraints that the circuit must satisfy. Here you define the logic that relates inputs to outputs, encapsulating the computation:
// Define declares the circuit constraints
// x**3 + x + 5 == y
func (circuit *CubicCircuit) Define(api frontend.API) error {
x3 := api.Mul(circuit.X, circuit.X, circuit.X)
api.AssertIsEqual(circuit.Y, api.Add(x3, circuit.X, 5))
return nil
}
The Define method specifies the constraints for the circuit.
x3 := api.Mul(circuit.X, circuit.X, circuit.X) computes X**3
api.AssertIsEqual(circuit.Y, api.Add(x3, circuit.X, 5) asserts that X**3 + X + 5 == Y
There are other options that we might use like ÀssertDifferent AssertIsLessOrEqual
Detail the steps to compile the circuit, generate a witness, create a proof, and verify it:
we need to specify the directory where the proof, verification key and the public key will be saved
outputDir := "gnark_plonk_circuit/"To compile the circuit, we do
var circuit CubicCircuit
// Compile the circuit using scs.NewBuilder
ccs, err := frontend.Compile(ecc.BN254.ScalarField(), scs.NewBuilder, &circuit)
if err != nil {
panic("circuit compilation error")
}
where
The frontend.Compile function compiles the circuit using the SCS
constraint system.
ecc.BN254.ScalarField() specifies the scalar field, in this case for the BN254 curve.
scs.NewBuilder is used to build the sparse constraint system.
We generate the SRS (Structured Reference String)
// Generate the SRS and its Lagrange interpolation
r1cs := ccs.(*cs.SparseR1CS)
srs, srsLagrangeInterpolation, err := unsafekzg.NewSRS(r1cs)
if err != nil {
panic("KZG setup error")
}
r1cs := ccs.(*cs.SparseR1CS) converts the compiled circuit to a sparse R1CS(Rank-1 Constraint Systems) format
required by the SRS generation.
unsafekzg.NewSRS generates the structured reference string (SRS) and its Lagrange interpolation.
Next, we need to set up PLONK
pk, vk, _ := plonk.Setup(ccs, srs, srsLagrangeInterpolation)
plonk.Setup initializes the PLONK proving system with the constraint system, SRS, and its Lagrange interpolation.
This generates the proving key pk and verification key vk
Then the Witness is created
assignment := CubicCircuit{X: 3, Y: 35}
fullWitness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())
if err != nil {
log.Fatal(err)
}
publicWitness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField(), frontend.PublicOnly())
if err != nil {
log.Fatal(err)
}
An assignment to the circuit variables is created: X = 3 and Y = 35.
frontend.NewWitness creates the full witness including all variables.
frontend.NewWitness with frontend.PublicOnly() creates the public witness including only the public
variables.
Generate the Proof:
proof, err := plonk.Prove(ccs, pk, fullWitness)
if err != nil {
panic("PLONK proof generation error")
}
plonk.Prove generates a proof using the compiled circuit, proving key and full witness
Then to Verify
// Verify the proof
err = plonk.Verify(proof, vk, publicWitness)
if err != nil {
panic("PLONK proof not verified")
}
plonk.Verify verifies the proof using the compiled circuit, proving key and full witness
Finally, we have to serialize and save outputs
// Open files for writing the proof, the verification key, and the public witness
proofFile, err := os.Create(outputDir + "plonk.proof")
if err != nil {
panic(err)
}
vkFile, err := os.Create( "plonk.vk")
if err != nil {
panic(err)
}
witnessFile, err := os.Create( "plonk_pub_input.pub")
if err != nil {
panic(err)
}
defer proofFile.Close()
defer vkFile.Close()
defer witnessFile.Close()
// Write the proof to the file
_, err = proof.WriteTo(proofFile)
if err != nil {
panic("could not serialize proof into file")
}
// Write the verification key to the file
_, err = vk.WriteTo(vkFile)
if err != nil {
panic("could not serialize verification key into file")
}
// Write the public witness to the file
_, err = publicWitness.WriteTo(witnessFile)
if err != nil {
panic("could not serialize proof into file")
}
fmt.Println("Proof written into plonk.proof")
fmt.Println("Verification key written into plonk.vk")
fmt.Println("Public witness written into plonk_pub_input.pub")
}
Files are created for the proof, verification key, and public witness.
The proof, verification key, and public witness are written to these files.
This ensures that the proof and related data are saved for later use or verification.
The complete code is:
package main
import (
"fmt"
"log"
"os"
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/plonk"
cs "github.com/consensys/gnark/constraint/bn254"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/test/unsafekzg"
"github.com/consensys/gnark/frontend/cs/scs"
)
// CubicCircuit defines a simple circuit
// x**3 + x + 5 == y
type CubicCircuit struct {
// struct tags on a variable is optional
// default uses variable name and secret visibility.
X frontend.Variable `gnark:"x"`
Y frontend.Variable `gnark:",public"`
}
// Define declares the circuit constraints
// x**3 + x + 5 == y
func (circuit *CubicCircuit) Define(api frontend.API) error {
x3 := api.Mul(circuit.X, circuit.X, circuit.X)
api.AssertIsEqual(circuit.Y, api.Add(x3, circuit.X, 5))
return nil
}
func main() {
var circuit CubicCircuit
// use scs.NewBuilder instead of r1cs.NewBuilder (groth16)
ccs, err := frontend.Compile(ecc.BN254.ScalarField(), scs.NewBuilder, &circuit)
if err != nil {
panic("circuit compilation error")
}
// use unsafekzg.NewSRS to generate the SRS and the Lagrange interpolation of the SRS
// Setup prepares the public data associated to a circuit + public inputs.
// The kzg SRS must be provided in canonical and lagrange form.
// For test purposes, see test/unsafekzg package. With an existing SRS generated through MPC in canonical form,
r1cs := ccs.(*cs.SparseR1CS)
srs, srsLagrangeInterpolation, err := unsafekzg.NewSRS(r1cs)
// srs, err := test.NewKZGSRS(r1cs)
if err != nil {
panic("KZG setup error")
}
// add srsLagrangeInterpolation to the Setup function
pk, vk, _ := plonk.Setup(ccs, srs, srsLagrangeInterpolation)
assignment := CubicCircuit{X: 3, Y: 35}
fullWitness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())
if err != nil {
log.Fatal(err)
}
publicWitness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField(), frontend.PublicOnly())
if err != nil {
log.Fatal(err)
}
// This proof should be serialized for testing in the operator
proof, err := plonk.Prove(ccs, pk, fullWitness)
if err != nil {
panic("PLONK proof generation error")
}
// The proof is verified before writing it into a file to make sure it is valid.
err = plonk.Verify(proof, vk, publicWitness)
if err != nil {
panic("PLONK proof not verified")
}
// Open files for writing the proof, the verification key and the public witness
proofFile, err := os.Create("plonk.proof")
if err != nil {
panic(err)
}
vkFile, err := os.Create("plonk.vk")
if err != nil {
panic(err)
}
witnessFile, err := os.Create( "plonk_pub_input.pub")
if err != nil {
panic(err)
}
defer proofFile.Close()
defer vkFile.Close()
defer witnessFile.Close()
_, err = proof.WriteTo(proofFile)
if err != nil {
panic("could not serialize proof into file")
}
_, err = vk.WriteTo(vkFile)
if err != nil {
panic("could not serialize verification key into file")
}
_, err = publicWitness.WriteTo(witnessFile)
if err != nil {
panic("could not serialize proof into file")
}
fmt.Println("Proof written into plonk.proof")
fmt.Println("Verification key written into plonk.vk")
fmt.Println("Public witness written into plonk_pub_input.pub")
}