Skip to content

Commit

Permalink
feat: lighter stack trace by default for circuits, more verbose when …
Browse files Browse the repository at this point in the history
…-tags=debug provided
  • Loading branch information
gbotrel committed Nov 11, 2021
1 parent d8c60fa commit b7d28fc
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 82 deletions.
70 changes: 70 additions & 0 deletions debug/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package debug

import (
"path/filepath"
"runtime"
"strconv"
"strings"
)

var light = true

func Stack() string {
var sbb strings.Builder
WriteStack(&sbb)
return sbb.String()
}

func WriteStack(sbb *strings.Builder) {
// derived from: https://golang.org/pkg/runtime/#example_Frames
// we stop when func name == Define as it is where the gnark circuit code should start

// Ask runtime.Callers for up to 10 pcs
pc := make([]uintptr, 10)
n := runtime.Callers(3, pc)
if n == 0 {
// No pcs available. Stop now.
// This can happen if the first argument to runtime.Callers is large.
return
}
pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
frames := runtime.CallersFrames(pc)
// Loop to get frames.
// A fixed number of pcs can expand to an indefinite number of Frames.
for {
frame, more := frames.Next()
fe := strings.Split(frame.Function, "/")
function := fe[len(fe)-1]
file := frame.File

if light {
if strings.Contains(function, "runtime.gopanic") {
continue
}
if strings.Contains(function, "frontend.(*constraintSystem)") {
continue
}
if strings.Contains(frame.File, "test/engine.go") {
continue
}
if strings.Contains(frame.File, "gnark/frontend") {
continue
}
file = filepath.Base(file)
}

sbb.WriteString(function)
sbb.WriteByte('\n')
sbb.WriteByte('\t')
sbb.WriteString(file)
sbb.WriteByte(':')
sbb.WriteString(strconv.Itoa(frame.Line))
sbb.WriteByte('\n')
if !more {
break
}
if strings.HasSuffix(function, "Define") {
break
}
}
}
8 changes: 8 additions & 0 deletions debug/debug_full.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build debug
// +build debug

package debug

func init() {
light = false
}
11 changes: 3 additions & 8 deletions frontend/cs_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ limitations under the License.
package frontend

import (
"fmt"
"math/big"
"runtime/debug"

"github.com/consensys/gnark/backend/hint"
"github.com/consensys/gnark/internal/backend/compiled"
Expand Down Expand Up @@ -152,8 +150,7 @@ func (cs *constraintSystem) Inverse(i1 interface{}) Variable {
if vars[0].isConstant() {
c := vars[0].constantValue(cs)
if c.IsUint64() && c.Uint64() == 0 {
stack := string(debug.Stack())
panic(fmt.Sprintf("inverse by constant(0):\n%s", stack))
panic("inverse by constant(0)")
}

c.ModInverse(c, cs.curveID.Info().Fr.Modulus())
Expand Down Expand Up @@ -189,8 +186,7 @@ func (cs *constraintSystem) Div(i1, i2 interface{}) Variable {
// v2 is constant
b2 := v2.constantValue(cs)
if b2.IsUint64() && b2.Uint64() == 0 {
stack := string(debug.Stack())
panic(fmt.Sprintf("div by constant(0):\n%s", stack))
panic("div by constant(0)")
}
q := cs.curveID.Info().Fr.Modulus()
b2.ModInverse(b2, q)
Expand Down Expand Up @@ -221,8 +217,7 @@ func (cs *constraintSystem) DivUnchecked(i1, i2 interface{}) Variable {
// v2 is constant
b2 := v2.constantValue(cs)
if b2.IsUint64() && b2.Uint64() == 0 {
stack := string(debug.Stack())
panic(fmt.Sprintf("div by constant(0):\n%s", stack))
panic("div by constant(0)")
}
q := cs.curveID.Info().Fr.Modulus()
b2.ModInverse(b2, q)
Expand Down
3 changes: 1 addition & 2 deletions frontend/cs_assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package frontend
import (
"fmt"
"math/big"
"runtime/debug"

"github.com/consensys/gnark/internal/backend/compiled"
)
Expand Down Expand Up @@ -52,7 +51,7 @@ func (cs *constraintSystem) AssertIsBoolean(i1 interface{}) {
if v.isConstant() {
c := v.constantValue(cs)
if !(c.IsUint64() && (c.Uint64() == 0 || c.Uint64() == 1)) {
panic(fmt.Sprintf("assertIsBoolean failed: constant(%s)\n%s", c.String(), string(debug.Stack())))
panic(fmt.Sprintf("assertIsBoolean failed: constant(%s)", c.String()))
}
}

Expand Down
11 changes: 6 additions & 5 deletions frontend/cs_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"strconv"
"strings"

"github.com/consensys/gnark/debug"
"github.com/consensys/gnark/internal/backend/compiled"
"github.com/consensys/gnark/internal/parser"
)
Expand Down Expand Up @@ -111,7 +112,7 @@ func printArg(log *compiled.LogEntry, sbb *strings.Builder, a interface{}) {
}

func (cs *constraintSystem) addDebugInfo(errName string, i ...interface{}) int {
var debug compiled.LogEntry
var l compiled.LogEntry

const minLogSize = 500
var sbb strings.Builder
Expand All @@ -126,7 +127,7 @@ func (cs *constraintSystem) addDebugInfo(errName string, i ...interface{}) int {
if len(v.linExp) > 1 {
sbb.WriteString("(")
}
debug.WriteLinearExpression(v.linExp, &sbb)
l.WriteLinearExpression(v.linExp, &sbb)
if len(v.linExp) > 1 {
sbb.WriteString(")")
}
Expand All @@ -136,15 +137,15 @@ func (cs *constraintSystem) addDebugInfo(errName string, i ...interface{}) int {
case int:
sbb.WriteString(strconv.Itoa(v))
case compiled.Term:
debug.WriteTerm(v, &sbb)
l.WriteTerm(v, &sbb)
default:
panic("unsupported log type")
}
}
sbb.WriteByte('\n')
debug.WriteStack(&sbb)
debug.Format = sbb.String()
l.Format = sbb.String()

cs.debugInfo = append(cs.debugInfo, debug)
cs.debugInfo = append(cs.debugInfo, l)
return len(cs.debugInfo) - 1
}
21 changes: 10 additions & 11 deletions frontend/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ import (
"fmt"
"reflect"

"github.com/consensys/gnark/debug"

"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend"
"github.com/consensys/gnark/internal/backend/compiled"
"github.com/consensys/gnark/internal/parser"
)

// errInputNotSet triggered when trying to access a variable that was not allocated
var errInputNotSet = errors.New("variable is not allocated")

// Compile will generate a CompiledConstraintSystem from the given circuit
//
// 1. it will first allocate the user inputs (see type Tag for more info)
Expand Down Expand Up @@ -91,14 +90,7 @@ func Compile(curveID ecc.ID, zkpID backend.ID, circuit Circuit, opts ...func(opt
// allocations by parsing the circuit's underlying structure, then
// it builds the constraint system using the Define method.
func buildCS(curveID ecc.ID, circuit Circuit, initialCapacity ...int) (cs constraintSystem, err error) {
// recover from panics to print user-friendlier messages
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
// TODO @gbotrel with debug buiild tag
// fmt.Println(string(debug.Stack()))
}
}()

// instantiate our constraint system
cs = newConstraintSystem(curveID, initialCapacity...)

Expand Down Expand Up @@ -133,6 +125,13 @@ func buildCS(curveID ecc.ID, circuit Circuit, initialCapacity ...int) (cs constr
return cs, err
}

// recover from panics to print user-friendlier messages
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v\n%s\n", r, debug.Stack())
}
}()

// call Define() to fill in the Constraints
if err := circuit.Define(curveID, &cs); err != nil {
return cs, err
Expand Down
23 changes: 12 additions & 11 deletions frontend/variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ limitations under the License.
package frontend

import (
"errors"
"fmt"
"math/big"
"strings"

"github.com/consensys/gnark/debug"

"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/internal/backend/compiled"

Expand All @@ -28,6 +31,9 @@ import (
fr_bw6761 "github.com/consensys/gnark-crypto/ecc/bw6-761/fr"
)

// errNoValue triggered when trying to access a variable that was not allocated
var errNoValue = errors.New("can't determine API input value")

// Variable of a circuit
// represents a Variable to a circuit, plus the linear combination leading to it.
// the linExp is always non empty, the PartialVariabl can be unset. It is set and allocated in the
Expand All @@ -46,16 +52,12 @@ type Variable struct {
// since a was not in the circuit struct it is not a secret variable
func (v *Variable) assertIsSet(cs *constraintSystem) {
if v.WitnessValue != nil {
var l compiled.LogEntry
var sbb strings.Builder
l.WriteStack(&sbb)
panic(fmt.Errorf("variable.WitnessValue is set. this is illegal in Define.\n%s", sbb.String()))
// note the compile already checks that, but, if inside a Define, a user mistakingly writes
// a.Assign(...) then this will detect it
panic("variable.WitnessValue is set. this is illegal in Define")
}
if len(v.linExp) == 0 {
var l compiled.LogEntry
var sbb strings.Builder
l.WriteStack(&sbb)
panic(fmt.Errorf("%w\n%s", errInputNotSet, sbb.String()))
panic(errNoValue)
}

}
Expand Down Expand Up @@ -84,10 +86,9 @@ func (v *Variable) constantValue(cs *constraintSystem) *big.Int {
// if it is not set this panics
func (v *Variable) GetWitnessValue(curveID ecc.ID) big.Int {
if v.WitnessValue == nil {
var l compiled.LogEntry
var sbb strings.Builder
l.WriteStack(&sbb)
panic(fmt.Errorf("%w\n%s", errInputNotSet, sbb.String()))
debug.WriteStack(&sbb)
panic(fmt.Errorf("%w\n%s", errNoValue, sbb.String()))
}

b := FromInterface(v.WitnessValue)
Expand Down
43 changes: 0 additions & 43 deletions internal/backend/compiled/log.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package compiled

import (
"runtime"
"strconv"
"strings"
)

Expand Down Expand Up @@ -45,44 +43,3 @@ func (l *LogEntry) WriteTerm(t Term, sbb *strings.Builder) {

l.ToResolve = append(l.ToResolve, t)
}

func (l *LogEntry) WriteStack(sbb *strings.Builder) {
// TODO @gbotrel should be a package level function somewhere (internal), not a method on log.
// derived from: https://golang.org/pkg/runtime/#example_Frames
// we stop when func name == Define as it is where the gnark circuit code should start

// Ask runtime.Callers for up to 10 pcs
pc := make([]uintptr, 10)
n := runtime.Callers(3, pc)
if n == 0 {
// No pcs available. Stop now.
// This can happen if the first argument to runtime.Callers is large.
return
}
pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
frames := runtime.CallersFrames(pc)
// Loop to get frames.
// A fixed number of pcs can expand to an indefinite number of Frames.
for {
frame, more := frames.Next()
fe := strings.Split(frame.Function, "/")
function := fe[len(fe)-1]
if strings.Contains(function, "frontend.(*ConstraintSystem)") {
continue
}

sbb.WriteString(function)
sbb.WriteByte('\n')
sbb.WriteByte('\t')
sbb.WriteString(frame.File)
sbb.WriteByte(':')
sbb.WriteString(strconv.Itoa(frame.Line))
sbb.WriteByte('\n')
if !more {
break
}
if strings.HasSuffix(function, "Define") {
break
}
}
}
4 changes: 2 additions & 2 deletions test/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import (
"math/big"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"

"github.com/consensys/gnark/debug"

"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend"
"github.com/consensys/gnark/backend/hint"
Expand All @@ -51,7 +52,6 @@ type engine struct {
//
// This is an experimental feature.
func IsSolved(circuit, witness frontend.Circuit, curveID ecc.ID, opts ...func(opt *backend.ProverOption) error) (err error) {

// apply options
opt, err := backend.NewProverOption(opts...)
if err != nil {
Expand Down

0 comments on commit b7d28fc

Please sign in to comment.