Compiler hints
Instead of computing every value inside a circuit, it is sometimes more efficient to compute a value outside the circuit and verify only the properties required for soundness. gnark calls these off-circuit computations hints. During solving, the prover obtains hint outputs from a hint function and then uses those outputs in constraints.
For example, the circuit API can decompose an integer into bits directly:
bits := api.ToBinary(circuit.Value, nBits)
This is preferable to hand-rolling a decomposition with a hint.
Implement a hint
A hint is a Go function of type solver.Hint:
func bitsHint(field *big.Int, inputs []*big.Int, outputs []*big.Int) error {
if len(inputs) != 1 {
return fmt.Errorf("unexpected hint arity")
}
a := inputs[0]
for i := range outputs {
outputs[i].SetUint64(uint64(a.Bit(i)))
}
return nil
}
Use api.Compiler().NewHint in the circuit to request outputs, then constrain them:
b, err := api.Compiler().NewHint(bitsHint, nBits, circuit.Value)
if err != nil {
return err
}
var weightedSum frontend.Variable
for i, bit := range b {
api.AssertIsBoolean(bit)
weightedSum = api.Add(weightedSum, api.Mul(bit, 1<<i))
}
api.AssertIsEqual(weightedSum, circuit.Value)
Register hints
The backend solver can execute a hint only if the function is available to it. Pass it explicitly when proving:
proof, err := groth16.Prove(ccs, pk, witness,
backend.WithSolverOptions(solver.WithHints(bitsHint)),
)
The corresponding verifier option is backend.WithSolverOptions(solver.WithHints(bitsHint)). To replace an already registered hint, use solver.OverrideHint.
Gadgets in gnark/std register their required hints automatically when the package is imported. If a serialized constraint system is loaded by a process that does not import the gadget package, call the gadget's registration function (for example, std.RegisterHints()) during initialization.
For the built-in hint registry, see the constraint/solver package documentation.