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:
| Component | Implementation |
|---|---|
| Library | gnark v0.9.x |
| Proving System | Groth16 |
| Curve | BN254 (alt_bn128) |
| Hash | MiMC |
| Commitment | Pedersen |
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
| Operation | Time (typical) |
|---|---|
| Shield proof generation | 500ms |
| Transfer proof generation | 1-2s |
| Proof verification | 2ms |
| Proof size | 256 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
- Minimize constraints: Fewer constraints = faster proofs
- Use efficient gadgets: Leverage gnark's optimized standard library
- Batch operations: Amortize setup costs across multiple proofs
- Cache proving keys: Keys are large, generate once
- Test thoroughly: Circuit bugs are hard to debug