Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hint registry #183

Merged
merged 6 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (id ID) String() string {

// NewProverOption returns a default ProverOption with given options applied
func NewProverOption(opts ...func(opt *ProverOption) error) (ProverOption, error) {
opt := ProverOption{LoggerOut: os.Stdout}
opt := ProverOption{LoggerOut: os.Stdout, HintFunctions: hint.GetAll()}
for _, option := range opts {
if err := option(&opt); err != nil {
return ProverOption{}, err
Expand Down
70 changes: 70 additions & 0 deletions backend/hint/builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package hint

import (
"errors"
"math/big"
"sync"

"github.com/consensys/gnark-crypto/ecc"
)

var initBuiltinOnce sync.Once

func init() {
initBuiltinOnce.Do(func() {
IsZero = builtinIsZero
Register(IsZero)
IthBit = builtinIthBit
Register(IthBit)
})
}

// The package provides the following built-in hint functions. All built-in hint
// functions are registered in the registry.
var (
// IsZero computes the value 1 - a^(modulus-1) for the single input a. This
// corresponds to checking if a == 0 (for which the function returns 1) or a
// != 0 (for which the function returns 0).
IsZero Function

// IthBit returns the i-tb bit the input. The function expects exactly two
// integer inputs i and n, takes the little-endian bit representation of n and
// returns its i-th bit.
IthBit Function
)

func builtinIsZero(curveID ecc.ID, inputs []*big.Int, result *big.Int) error {
if len(inputs) != 1 {
return errors.New("IsZero expects one input")
}

// get fr modulus
q := curveID.Info().Fr.Modulus()

// save input
result.Set(inputs[0])

// reuse input to compute q - 1
qMinusOne := inputs[0].SetUint64(1)
qMinusOne.Sub(q, qMinusOne)

// result = 1 - input**(q-1)
result.Exp(result, qMinusOne, q)
inputs[0].SetUint64(1)
result.Sub(inputs[0], result).Mod(result, q)

return nil
}

func builtinIthBit(_ ecc.ID, inputs []*big.Int, result *big.Int) error {
if len(inputs) != 2 {
return errors.New("ithBit expects 2 inputs; inputs[0] == value, inputs[1] == bit position")
}
if !inputs[1].IsUint64() {
result.SetUint64(0)
return nil
}

result.SetUint64(uint64(inputs[0].Bit(int(inputs[1].Uint64()))))
return nil
}
120 changes: 73 additions & 47 deletions backend/hint/hint.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,70 @@
/*
Package hint allows to define computations outside of a circuit.

Usually, it is expected that computations in circuits are performed on
variables. However, in some cases defining the computations in circuits may be
complicated or computationally expensive. By using hints, the computations are
performed outside of the circuit on integers (compared to the frontend.Variable
values inside the circuits) and the result of a hint function is assigned to a
newly created variable in a circuit.

As the computations are perfomed outside of the circuit, then the correctness of
the result is not guaranteed. This also means that the result of a hint function
is unconstrained by default, leading to failure while composing circuit proof.
Thus, it is the circuit developer responsibility to verify the correctness hint
result by adding necessary constraints in the circuit.

As an example, lets say the hint function computes a factorization of a
semiprime n:

p, q <- hint(n) st. p * q = n

into primes p and q. Then, the circuit developer needs to assert in the circuit
that p*q indeed equals to n:

n == p * q.

However, if the hint function is incorrectly defined (e.g. in the previous
example, it returns 1 and n instead of p and q), then the assertion may still
hold, but the constructed proof is semantically invalid. Thus, the user
constructing the proof must be extremely cautious when using hints.

Using hint functions in circuits

To use a hint function in a circuit, the developer first needs to define a hint
function hintFn according to the Function type. Then, in a circuit, the
developer applies the hint function with frontend.API.NewHint(hintFn, vars...),
where vars are the variables the hint function will be applied to (and
correspond to the argument inputs in the Function type) which returns a new
unconstrained variable. The returned variable must be constrained using
frontend.API.Assert[.*] methods.

As explained, the hints are essentially black boxes from the circuit point of
view and thus the defined hints in circuits are not used when constructing a
proof. To allow the particular hint functions to be used during proof
construction, the user needs to supply a backend.ProverOption indicating the
enabled hints. Such options can be optained by a call to
backend.WithHints(hintFns...), where hintFns are the corresponding hint
functions.

Using hint functions in gadgets

Similar considerations apply for hint functions used in gadgets as in
user-defined circuits. However, listing all hint functions used in a particular
gadget for constructing backend.ProverOption puts high overhead for the user to
enable all necessary hints.

For that, this package also provides a registry of trusted hint functions. When
a gadget registers a hint function, then it is automatically enabled during
proof computation and the prover does not need to provide a corresponding
proving option.

In the init() method of the gadget, call the method Register(hintFn) method on
the hint function hintFn to register a hint function in the package registry.
*/
package hint

import (
"errors"
"hash/fnv"
"math/big"
"reflect"
Expand All @@ -10,57 +73,20 @@ import (
"github.com/consensys/gnark-crypto/ecc"
)

// ID is a unique identifier for an hint function. It is set by the package in
// is used for lookup.
type ID uint32

// Function defines how a hint is computed from the inputs. The hint value is
// stored in result. If the hint is computable, then the functio must return a
// nil error and non-nil error otherwise.
type Function func(curveID ecc.ID, inputs []*big.Int, result *big.Int) error

// UUID returns a unique ID for a hint function name
func UUID(f Function) ID {
name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
// UUID returns a unique ID for the hint function. Multiple calls using same
// function return the same ID.
func UUID(hintFn Function) ID {
name := runtime.FuncForPC(reflect.ValueOf(hintFn).Pointer()).Name()
h := fnv.New32a()
_, _ = h.Write([]byte(name))
return ID(h.Sum32())
}

// IthBit expects len(inputs) == 2
// inputs[0] == a
// inputs[1] == n
// returns bit number n of a
func IthBit(_ ecc.ID, inputs []*big.Int, result *big.Int) error {
if len(inputs) != 2 {
return errors.New("ithBit expects 2 inputs; inputs[0] == value, inputs[1] == bit position")
}
if !inputs[1].IsUint64() {
result.SetUint64(0)
return nil
}

result.SetUint64(uint64(inputs[0].Bit(int(inputs[1].Uint64()))))
return nil
}

// IsZero expects len(inputs) == 1
// inputs[0] == a
// returns m = 1 - a^(modulus-1)
func IsZero(curveID ecc.ID, inputs []*big.Int, result *big.Int) error {
if len(inputs) != 1 {
return errors.New("IsZero expects one input")
}

// get fr modulus
q := curveID.Info().Fr.Modulus()

// save input
result.Set(inputs[0])

// reuse input to compute q - 1
qMinusOne := inputs[0].SetUint64(1)
qMinusOne.Sub(q, qMinusOne)

// result = 1 - input**(q-1)
result.Exp(result, qMinusOne, q)
inputs[0].SetUint64(1)
result.Sub(inputs[0], result).Mod(result, q)

return nil
}
33 changes: 33 additions & 0 deletions backend/hint/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package hint

import (
"fmt"
"sync"
)

var registry = make(map[ID]Function)
var registryM sync.RWMutex

// Register registers a hint function in the global registry. All registered
// hint functions can be retrieved with a call to GetAll(). It is an error to
// register a single function twice and results in a panic.
func Register(hintFn Function) {
registryM.Lock()
defer registryM.Unlock()
key := UUID(hintFn)
if _, ok := registry[key]; ok {
panic(fmt.Sprintf("function %d registered twice", key))
}
registry[key] = hintFn
}

// GetAll returns all registered hint functions.
func GetAll() []Function {
registryM.RLock()
defer registryM.RUnlock()
ret := make([]Function, 0, len(registry))
for _, v := range registry {
ret = append(ret, v)
}
return ret
}
17 changes: 10 additions & 7 deletions frontend/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,17 @@ type API interface {
// whose value will be resolved at runtime when computed by the solver
Println(a ...interface{})

// NewHint initialize a Variable whose value will be evaluated using the provided hint function at run time
// NewHint initializes an internal variable whose value will be evaluated
// using the provided hint function at run time from the inputs. Inputs must
// be either variables or convertible to *big.Int.
//
// hint function is provided at proof creation time and must match the hintID
// inputs must be either variables or convertible to big int
// /!\ warning /!\
// this doesn't add any constraint to the newly created wire
// from the backend point of view, it's equivalent to a user-supplied witness
// except, the solver is going to assign it a value, not the caller
// The hint function is provided at the proof creation time and is not
// embedded into the circuit. From the backend point of view, the variable
// returned by the hint function is equivalent to the user-supplied witness,
// but its actual value is assigned by the solver, not the caller.
//
// No new constraints are added to the newly created wire and must be added
// manually in the circuit. Failing to do so leads to solver failure.
NewHint(f hint.Function, inputs ...interface{}) Variable

// Tag creates a tag at a given place in a circuit. The state of the tag may contain informations needed to
Expand Down
19 changes: 11 additions & 8 deletions frontend/cs.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,17 @@ func newConstraintSystem(curveID ecc.ID, initialCapacity ...int) constraintSyste
return cs
}

// NewHint initialize a variable whose value will be evaluated in the Prover by the constraint
// solver using the provided hint function
// hint function is provided at proof creation time and must match the hintID
// inputs must be either variables or convertible to big int
// /!\ warning /!\
// this doesn't add any constraint to the newly created wire
// from the backend point of view, it's equivalent to a user-supplied witness
// except, the solver is going to assign it a value, not the caller
// NewHint initializes an internal variable whose value will be evaluated using
// the provided hint function at run time from the inputs. Inputs must be either
// variables or convertible to *big.Int.
//
// The hint function is provided at the proof creation time and is not embedded
// into the circuit. From the backend point of view, the variable returned by
// the hint function is equivalent to the user-supplied witness, but its actual
// value is assigned by the solver, not the caller.
//
// No new constraints are added to the newly created wire and must be added
// manually in the circuit. Failing to do so leads to solver failure.
func (cs *constraintSystem) NewHint(f hint.Function, inputs ...interface{}) Variable {
// create resulting wire
r := cs.newInternalVariable()
Expand Down
5 changes: 1 addition & 4 deletions internal/backend/bls12-377/cs/solution.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions internal/backend/bls12-381/cs/solution.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions internal/backend/bls24-315/cs/solution.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions internal/backend/bn254/cs/solution.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions internal/backend/bw6-761/cs/solution.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@ func newSolution(nbWires int, hintFunctions []hint.Function, coefficients []fr.E
values: make([]fr.Element, nbWires),
coefficients: coefficients,
solved: make([]bool, nbWires),
mHintsFunctions: make(map[hint.ID]hint.Function, len(hintFunctions) + 2),
mHintsFunctions: make(map[hint.ID]hint.Function, len(hintFunctions)),
}

s.mHintsFunctions[hint.UUID(hint.IsZero)] = hint.IsZero
s.mHintsFunctions[hint.UUID(hint.IthBit)] = hint.IthBit

for i := 0; i < len(hintFunctions);i++ {
id := hint.UUID(hintFunctions[i])
Expand Down