Skip to main content

ZK Proofs Development

Build privacy-preserving applications using RP1's gnark-based ZK proof system.

Overview

RP1 uses gnark with Groth16 proving system on the BN254 curve:

ComponentImplementation
Librarygnark v0.9.x
Proving SystemGroth16
CurveBN254 (alt_bn128)
HashMiMC
CommitmentPedersen

Circuit Development

Setup

# Install gnark
go get github.com/consensys/gnark@latest
go get github.com/consensys/gnark-crypto@latest

Basic Circuit

package circuits

import (
"github.com/consensys/gnark/frontend"
)

// Define a simple circuit
type MyCircuit struct {
// Public inputs (verifier knows these)
PublicInput frontend.Variable `gnark:",public"`

// Private inputs (prover knows these)
SecretInput frontend.Variable
}

// Define implements the circuit constraints
func (circuit *MyCircuit) Define(api frontend.API) error {
// Prove: PublicInput = SecretInput * SecretInput
result := api.Mul(circuit.SecretInput, circuit.SecretInput)
api.AssertIsEqual(circuit.PublicInput, result)
return nil
}

Compile and Generate Keys

package main

import (
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/r1cs"
"github.com/consensys/gnark-crypto/ecc"
)

func main() {
// Compile circuit
var circuit MyCircuit
ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit)
if err != nil {
panic(err)
}

// Generate proving and verifying keys
pk, vk, err := groth16.Setup(ccs)
if err != nil {
panic(err)
}

// Save keys
// ...
}

Generate and Verify Proof

func generateProof() {
// Create witness (actual values)
assignment := MyCircuit{
PublicInput: 25, // 5 * 5 = 25
SecretInput: 5,
}

witness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())
if err != nil {
panic(err)
}

// Generate proof
proof, err := groth16.Prove(ccs, pk, witness)
if err != nil {
panic(err)
}

// Verify proof
publicWitness, _ := witness.Public()
err = groth16.Verify(proof, vk, publicWitness)
if err != nil {
panic("proof verification failed")
}
}

RP1 Privacy Circuits

Shield Circuit

type ShieldCircuit struct {
// Public
Amount frontend.Variable `gnark:",public"`
Commitment frontend.Variable `gnark:",public"`

// Private
Blinding frontend.Variable
}

func (c *ShieldCircuit) Define(api frontend.API) error {
mimc, _ := mimc.NewMiMC(api)

// Commitment = MiMC(amount, blinding)
mimc.Write(c.Amount)
mimc.Write(c.Blinding)
computedCommitment := mimc.Sum()

api.AssertIsEqual(c.Commitment, computedCommitment)
return nil
}

Transfer Circuit

type TransferCircuit struct {
// Public
OldRoot frontend.Variable `gnark:",public"`
NewRoot frontend.Variable `gnark:",public"`
Nullifier frontend.Variable `gnark:",public"`

// Private
OldAmount frontend.Variable
NewAmount frontend.Variable
Secret frontend.Variable
PathIndices []frontend.Variable
Siblings []frontend.Variable
}

func (c *TransferCircuit) Define(api frontend.API) error {
mimc, _ := mimc.NewMiMC(api)

// 1. Verify nullifier = hash(secret, pathIndices)
mimc.Reset()
mimc.Write(c.Secret)
for _, idx := range c.PathIndices {
mimc.Write(idx)
}
computedNullifier := mimc.Sum()
api.AssertIsEqual(c.Nullifier, computedNullifier)

// 2. Verify Merkle inclusion proof
// ... merkle proof verification

// 3. Verify amounts balance
api.AssertIsEqual(c.OldAmount, c.NewAmount)

return nil
}

MiMC Hash

RP1 uses MiMC for ZK-friendly hashing:

import "github.com/consensys/gnark/std/hash/mimc"

func hashInCircuit(api frontend.API, inputs ...frontend.Variable) frontend.Variable {
h, _ := mimc.NewMiMC(api)
for _, input := range inputs {
h.Write(input)
}
return h.Sum()
}

Off-chain MiMC

import (
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
"github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc"
)

func hashOffChain(inputs ...[]byte) []byte {
h := mimc.NewMiMC()
for _, input := range inputs {
h.Write(input)
}
return h.Sum(nil)
}

Merkle Tree

type MerkleProof struct {
Leaf frontend.Variable
Root frontend.Variable `gnark:",public"`
Path []frontend.Variable
Indices []frontend.Variable
}

func VerifyMerkleProof(api frontend.API, proof MerkleProof) {
mimc, _ := mimc.NewMiMC(api)

current := proof.Leaf
for i, sibling := range proof.Path {
// If index is 0, current is left child
// If index is 1, current is right child
left := api.Select(proof.Indices[i], sibling, current)
right := api.Select(proof.Indices[i], current, sibling)

mimc.Reset()
mimc.Write(left)
mimc.Write(right)
current = mimc.Sum()
}

api.AssertIsEqual(current, proof.Root)
}

Integration with RP1

Submit ZK Proof Transaction

import { RP1Client, ZKProof } from '@rp1/sdk';

const client = new RP1Client('https://rpc.rp.one');

// Generate proof off-chain
const proof = await generateProof(secretInput, publicInput);

// Submit to chain
const tx = await client.submitZKProof({
circuitType: 'shield',
proof: proof.serialize(),
publicInputs: [publicInput],
});

Verify Proof On-chain

// In keeper
func (k Keeper) VerifyProof(ctx context.Context, proofBytes []byte, publicInputs []string) error {
// Deserialize proof
proof := groth16.NewProof(ecc.BN254)
if _, err := proof.ReadFrom(bytes.NewReader(proofBytes)); err != nil {
return err
}

// Convert public inputs
var inputs []fr.Element
for _, input := range publicInputs {
var elem fr.Element
elem.SetString(input)
inputs = append(inputs, elem)
}

// Verify
if err := groth16.Verify(proof, k.verifyingKey, inputs); err != nil {
return types.ErrInvalidProof
}

return nil
}

Performance

OperationTime (typical)
Shield proof generation500ms
Transfer proof generation1-2s
Proof verification2ms
Proof size256 bytes

Testing

func TestShieldCircuit(t *testing.T) {
// Create circuit
var circuit ShieldCircuit

// Create assignment
assignment := ShieldCircuit{
Amount: 100,
Blinding: 12345,
Commitment: computeCommitment(100, 12345),
}

// Test compilation
assert := test.NewAssert(t)
assert.ProverSucceeded(&circuit, &assignment, test.WithCurves(ecc.BN254))
}

Best Practices

  1. Minimize constraints: Fewer constraints = faster proofs
  2. Use efficient gadgets: Leverage gnark's optimized standard library
  3. Batch operations: Amortize setup costs across multiple proofs
  4. Cache proving keys: Keys are large, generate once
  5. Test thoroughly: Circuit bugs are hard to debug

Resources

Next Steps