inputpacking

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 12, 2025 License: Apache-2.0 Imports: 0 Imported by: 0

Documentation

Overview

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.

Example
package main

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 main() {
	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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL