Skip to content

Commit

Permalink
docs: add input packing example (Consensys#1311)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivokub authored Nov 4, 2024
1 parent 47ae846 commit c975bac
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 0 deletions.
19 changes: 19 additions & 0 deletions examples/inputpacking/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Package inputpacking illustrates input packing for reducing public input.

// Usually in a SNARK circuit there are public and private inputs. The public
// inputs are known to the prover and verifier, while the private inputs are
// known only to the prover. To verify the proof, the verifier needs to provide
// the public inputs as an input to the verification algorithm.
//
// However, there are several drawbacks to this approach:
// 1. The public inputs may not be of a convenient format -- this happens for example when using the non-native arithmetic where we work on limbs.
// 2. The verifier work depends on the number of public inputs -- this is a problem in case of a recursive SNARK verifier, making the recursion more expensive.
// 3. The public input needs to be provided as a calldata to the Solidity verifier, which is expensive.
//
// An alternative approach however is to provide only a hash of the public
// inputs to the verifier. This way, if the verifier computes the hash of the
// inputs on its own, it can be sure that the inputs are correct and we can
// mitigate the issues.
//
// This examples how to use this approach for both native and non-native inputs. We use MiMC hash function.
package inputpacking
199 changes: 199 additions & 0 deletions examples/inputpacking/inputpacking_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package inputpacking

import (
"crypto/rand"
"fmt"
"math/big"

fp_bn254 "github.com/consensys/gnark-crypto/ecc/bn254/fp"
fr_bn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr"
cmimc "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc"

"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/r1cs"
"github.com/consensys/gnark/std/hash/mimc"
"github.com/consensys/gnark/std/math/emulated"
"github.com/consensys/gnark/std/math/emulated/emparams"
)

func inCircuitComputation(api frontend.API, input1, input2 frontend.Variable, expected frontend.Variable) {
res := api.Mul(input1, input2)
api.AssertIsEqual(res, expected)
}

func inCircuitComputationEmulated(api frontend.API, input1, input2 emulated.Element[emulated.BN254Fp], expected emulated.Element[emulated.BN254Fp]) error {
f, err := emulated.NewField[emulated.BN254Fp](api)
if err != nil {
return err
}
res := f.Mul(&input1, &input2)
f.AssertIsEqual(res, &expected)
return nil
}

// UnpackedCircuit represents a circuit where all public inputs are given as is
type UnpackedCircuit struct {
Input1, Input2 frontend.Variable `gnark:",public"`
EmulatedInput1, EmulatedInput2 emulated.Element[emulated.BN254Fp] `gnark:",public"`
Output frontend.Variable `gnark:",private"`
EmulatedOutput emulated.Element[emulated.BN254Fp] `gnark:",private"`
}

func (circuit *UnpackedCircuit) Define(api frontend.API) error {
inCircuitComputation(api, circuit.Input1, circuit.Input2, circuit.Output)
return inCircuitComputationEmulated(api, circuit.EmulatedInput1, circuit.EmulatedInput2, circuit.EmulatedOutput)
}

// PackedCircuit represents a circuit where all public inputs are given as private instead and we provide a hash of them as the only public input.
type PackedCircuit struct {
PublicHash frontend.Variable

Input1, Input2 frontend.Variable `gnark:",private"`
EmulatedInput1, EmulatedInput2 emulated.Element[emulated.BN254Fp] `gnark:",private"`
Output frontend.Variable `gnark:",private"`
EmulatedOutput emulated.Element[emulated.BN254Fp] `gnark:",private"`
}

func (circuit *PackedCircuit) Define(api frontend.API) error {
h, err := mimc.NewMiMC(api)
if err != nil {
return err
}
h.Write(circuit.Input1)
h.Write(circuit.Input2)
h.Write(circuit.EmulatedInput1.Limbs...)
h.Write(circuit.EmulatedInput2.Limbs...)
dgst := h.Sum()
api.AssertIsEqual(dgst, circuit.PublicHash)

inCircuitComputation(api, circuit.Input1, circuit.Input2, circuit.Output)
return inCircuitComputationEmulated(api, circuit.EmulatedInput1, circuit.EmulatedInput2, circuit.EmulatedOutput)
}

func Example() {
modulusNative := ecc.BN254.ScalarField()
modulusEmulated := ecc.BN254.BaseField()

// declare inputs
input1, err := rand.Int(rand.Reader, modulusNative)
if err != nil {
panic(err)
}
input2, err := rand.Int(rand.Reader, modulusNative)
if err != nil {
panic(err)
}
emulatedInput1, err := rand.Int(rand.Reader, modulusEmulated)
if err != nil {
panic(err)
}
emulatedInput2, err := rand.Int(rand.Reader, modulusEmulated)
if err != nil {
panic(err)
}
output := new(big.Int).Mul(input1, input2)
output.Mod(output, modulusNative)
emulatedOutput := new(big.Int).Mul(emulatedInput1, emulatedInput2)
emulatedOutput.Mod(emulatedOutput, modulusEmulated)

// first we run the circuit where public inputs are not packed
assignment := &UnpackedCircuit{
Input1: input1,
Input2: input2,
EmulatedInput1: emulated.ValueOf[emparams.BN254Fp](emulatedInput1),
EmulatedInput2: emulated.ValueOf[emparams.BN254Fp](emulatedInput2),
Output: output,
EmulatedOutput: emulated.ValueOf[emparams.BN254Fp](emulatedOutput),
}
privWit, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField())
if err != nil {
panic(err)
}
publicWit, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField(), frontend.PublicOnly())
if err != nil {
panic(err)
}

ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &UnpackedCircuit{})
if err != nil {
panic(err)
}
pk, vk, err := groth16.Setup(ccs)
if err != nil {
panic(err)
}
proof, err := groth16.Prove(ccs, pk, privWit)
if err != nil {
panic(err)
}
err = groth16.Verify(proof, vk, publicWit)
if err != nil {
panic(err)
}

// print the number of public inputs when we provide all public inputs. Note that we also count the commitment here.
fmt.Println("unpacked public variables:", ccs.GetNbPublicVariables())

// then we run the circuit where public inputs are packed
var buf [fr_bn254.Bytes]byte
var buf2 [fp_bn254.Bytes]byte
h := cmimc.NewMiMC()
input1.FillBytes(buf[:])
h.Write(buf[:])
input2.FillBytes(buf[:])
h.Write(buf[:])
emulatedInput1.FillBytes(buf2[:])
h.Write(buf2[24:32])
h.Write(buf2[16:24])
h.Write(buf2[8:16])
h.Write(buf2[0:8])
emulatedInput2.FillBytes(buf2[:])
h.Write(buf2[24:32])
h.Write(buf2[16:24])
h.Write(buf2[8:16])
h.Write(buf2[0:8])

dgst := h.Sum(nil)
phash := new(big.Int).SetBytes(dgst)

assignment2 := &PackedCircuit{
PublicHash: phash,
Input1: input1,
Input2: input2,
EmulatedInput1: emulated.ValueOf[emparams.BN254Fp](emulatedInput1),
EmulatedInput2: emulated.ValueOf[emparams.BN254Fp](emulatedInput2),
Output: output,
EmulatedOutput: emulated.ValueOf[emparams.BN254Fp](emulatedOutput),
}
privWit2, err := frontend.NewWitness(assignment2, ecc.BN254.ScalarField())
if err != nil {
panic(err)
}
publicWit2, err := frontend.NewWitness(assignment2, ecc.BN254.ScalarField(), frontend.PublicOnly())
if err != nil {
panic(err)
}

ccs2, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &PackedCircuit{})
if err != nil {
panic(err)
}
pk2, vk2, err := groth16.Setup(ccs2)
if err != nil {
panic(err)
}
proof2, err := groth16.Prove(ccs2, pk2, privWit2)
if err != nil {
panic(err)
}
err = groth16.Verify(proof2, vk2, publicWit2)
if err != nil {
panic(err)
}
// print the number of public inputs when we provide only the hash. Note that we also count the commitment here.
fmt.Println("packed public variables:", ccs2.GetNbPublicVariables())
// output: unpacked public variables: 11
// packed public variables: 1
}

0 comments on commit c975bac

Please sign in to comment.