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

feat: add printf support for circuit debugging #1431

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
9 changes: 5 additions & 4 deletions constraint/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import (
// to represent string values (in logs or debug info) where a value is not known at compile time
// (which is the case for variables that need to be resolved in the R1CS)
type LogEntry struct {
Caller string
Format string
ToResolve []LinearExpression // TODO @gbotrel we could store here a struct with a flag that says if we expand or evaluate the expression
Stack []int
Caller string
Format string
ToResolve []LinearExpression // TODO @gbotrel we could store here a struct with a flag that says if we expand or evaluate the expression
Stack []int
FormatSpecifiers []string // Format specifiers for each value in ToResolve
}

func (l *LogEntry) WriteVariable(le LinearExpression, sbb *strings.Builder) {
Expand Down
3 changes: 3 additions & 0 deletions constraint/r1cs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ type R1CS interface {

// GetR1CIterator returns an R1CIterator to iterate on the R1C constraints of the system.
GetR1CIterator() R1CIterator

// GetLogs returns all log entries
GetLogs() []LogEntry
}

// R1CIterator facilitates iterating through R1C constraints.
Expand Down
31 changes: 31 additions & 0 deletions examples/printf/printf_example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package examples

import (
"github.com/consensys/gnark/frontend"
)

// PrintfCircuit demonstrates the usage of Printf for debugging circuit variables
type PrintfCircuit struct {
X, Y frontend.Variable `gnark:"x,public"`
Z frontend.Variable `gnark:"z,secret"`
}

// Define implements the circuit logic using Printf for debugging
func (circuit *PrintfCircuit) Define(api frontend.API) error {
// Basic arithmetic with debug output
sum := api.Add(circuit.X, circuit.Y)
api.Printf("Sum of %d and %d is %d", circuit.X, circuit.Y, sum)

// Show different number formats
api.Printf("X in different formats: dec=%d hex=%x bin=%b", circuit.X, circuit.X, circuit.X)

// Debug intermediate calculations
product := api.Mul(sum, circuit.Z)
api.Printf("Product of sum(%d) and secret Z is %d", sum, product)

// Verify constraints with debug output
isLessOrEqual := api.IsZero(api.Sub(api.Mul(circuit.X, circuit.Y), product))
api.Printf("Is X*Y <= sum*Z? %d", isLessOrEqual)

return nil
}
18 changes: 18 additions & 0 deletions examples/printf/printf_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package examples

import (
"testing"

"github.com/consensys/gnark/test"
)

func TestPrintfExample(t *testing.T) {
assert := test.NewAssert(t)

var circuit PrintfCircuit
assert.ProverSucceeded(&circuit, &PrintfCircuit{
X: 2,
Y: 3,
Z: 4,
})
}
9 changes: 9 additions & 0 deletions frontend/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ type API interface {
// whose value will be resolved at runtime when computed by the solver
Println(a ...Variable)

// Printf behaves like fmt.Printf but accepts cd.Variable as parameter
// whose value will be resolved at runtime when computed by the solver.
// Supported format specifiers:
// %v - default format
// %d - decimal integer
// %x - hexadecimal
// %b - binary
Printf(format string, a ...Variable)

// Compiler returns the compiler object for advanced circuit development
Compiler() Compiler

Expand Down
54 changes: 54 additions & 0 deletions frontend/cs/r1cs/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -824,3 +824,57 @@ func (builder *builder) wireIDsToVars(wireIDs ...[]int) []frontend.Variable {
func (builder *builder) SetGkrInfo(info constraint.GkrInfo) error {
return builder.cs.AddGkr(info)
}

// Printf enables circuit debugging and behaves like fmt.Printf()
// Format specifiers:
// %v - default format
// %d - decimal integer
// %x - hexadecimal
// %b - binary
func (builder *builder) Printf(format string, a ...frontend.Variable) {
var log constraint.LogEntry

// prefix log line with file.go:line
if _, file, line, ok := runtime.Caller(1); ok {
log.Caller = fmt.Sprintf("%s:%d", filepath.Base(file), line)
}

var sbb strings.Builder
argIndex := 0

for i := 0; i < len(format); {
if i+1 < len(format) && format[i] == '%' {
if argIndex >= len(a) {
panic("Printf: not enough arguments for format string")
}

switch format[i+1] {
case 'v', 'd', 'x', 'b':
if v, ok := a[argIndex].(expr.LinearExpression); ok {
assertIsSet(v)
sbb.WriteString("%s")
// Store format specifier with the value
log.ToResolve = append(log.ToResolve, builder.getLinearExpression(v))
log.FormatSpecifiers = append(log.FormatSpecifiers, string(format[i+1]))
} else {
builder.printArg(&log, &sbb, a[argIndex])
}
argIndex++
i += 2
case '%':
sbb.WriteByte('%')
i += 2
default:
sbb.WriteByte(format[i])
i++
}
} else {
sbb.WriteByte(format[i])
i++
}
}

// set format string to be used with fmt.Sprintf
log.Format = sbb.String()
builder.cs.AddLog(log)
}
53 changes: 53 additions & 0 deletions frontend/cs/r1cs/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package r1cs

import (
"testing"

"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/schema"
"github.com/consensys/gnark/internal/tinyfield"
)

func TestPrintf(t *testing.T) {
var circuit struct {
X, Y frontend.Variable
}

builder := newBuilder(tinyfield.Modulus(), frontend.CompileConfig{})

// Create LeafInfo for variables
xInfo := schema.LeafInfo{
FullName: func() string { return "X" },
Visibility: schema.Public,
}
yInfo := schema.LeafInfo{
FullName: func() string { return "Y" },
Visibility: schema.Public,
}

circuit.X = builder.PublicVariable(xInfo)
circuit.Y = builder.PublicVariable(yInfo)

// Test different format specifiers
builder.Printf("X in different formats: dec=%d hex=%x bin=%b default=%v", circuit.X, circuit.X, circuit.X, circuit.X)

// Test multiple variables
builder.Printf("X=%d Y=%d sum=%d", circuit.X, circuit.Y, builder.Add(circuit.X, circuit.Y))

// Test escaping %%
builder.Printf("100%% sure that X=%d", circuit.X)

// Get logs from constraint system
logs := builder.cs.GetLogs()
if len(logs) != 3 {
t.Errorf("expected 3 log entries, got %d", len(logs))
}

// Verify format specifiers are stored correctly
if len(logs[0].FormatSpecifiers) != 4 {
t.Errorf("expected 4 format specifiers, got %d", len(logs[0].FormatSpecifiers))
}
if logs[0].FormatSpecifiers[0] != "d" {
t.Errorf("expected 'd' format specifier, got %s", logs[0].FormatSpecifiers[0])
}
}
53 changes: 53 additions & 0 deletions frontend/cs/scs/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,3 +684,56 @@ func (*builder) FrontendType() frontendtype.Type {
func (builder *builder) SetGkrInfo(info constraint.GkrInfo) error {
return builder.cs.AddGkr(info)
}

// Printf enables circuit debugging and behaves like fmt.Printf()
// Format specifiers:
// %v - default format
// %d - decimal integer
// %x - hexadecimal
// %b - binary
func (builder *builder) Printf(format string, a ...frontend.Variable) {
var log constraint.LogEntry

// prefix log line with file.go:line
if _, file, line, ok := runtime.Caller(1); ok {
log.Caller = fmt.Sprintf("%s:%d", filepath.Base(file), line)
}

var sbb strings.Builder
argIndex := 0

for i := 0; i < len(format); {
if i+1 < len(format) && format[i] == '%' {
if argIndex >= len(a) {
panic("Printf: not enough arguments for format string")
}

switch format[i+1] {
case 'v', 'd', 'x', 'b':
if v, ok := a[argIndex].(expr.Term); ok {
sbb.WriteString("%s")
// Store format specifier with the value
log.ToResolve = append(log.ToResolve, constraint.LinearExpression{builder.cs.MakeTerm(v.Coeff, v.VID)})
log.FormatSpecifiers = append(log.FormatSpecifiers, string(format[i+1]))
} else {
builder.printArg(&log, &sbb, a[argIndex])
}
argIndex++
i += 2
case '%':
sbb.WriteByte('%')
i += 2
default:
sbb.WriteByte(format[i])
i++
}
} else {
sbb.WriteByte(format[i])
i++
}
}

// set format string to be used with fmt.Sprintf
log.Format = sbb.String()
builder.cs.AddLog(log)
}
46 changes: 46 additions & 0 deletions frontend/cs/scs/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/scs"
"github.com/consensys/gnark/frontend/schema"
"github.com/consensys/gnark/internal/tinyfield"
"github.com/consensys/gnark/test"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -290,3 +292,47 @@ func TestRegressionOr(t *testing.T) {
}
}
}

func TestPrintf(t *testing.T) {
var circuit struct {
X, Y frontend.Variable
}

builder := newBuilder(tinyfield.Modulus(), frontend.CompileConfig{})

// Create LeafInfo for variables
xInfo := schema.LeafInfo{
FullName: func() string { return "X" },
Visibility: schema.Public,
}
yInfo := schema.LeafInfo{
FullName: func() string { return "Y" },
Visibility: schema.Public,
}

circuit.X = builder.PublicVariable(xInfo)
circuit.Y = builder.PublicVariable(yInfo)

// Test different format specifiers
builder.Printf("X in different formats: dec=%d hex=%x bin=%b default=%v", circuit.X, circuit.X, circuit.X, circuit.X)

// Test multiple variables
builder.Printf("X=%d Y=%d sum=%d", circuit.X, circuit.Y, builder.Add(circuit.X, circuit.Y))

// Test escaping %%
builder.Printf("100%% sure that X=%d", circuit.X)

// Get logs from constraint system
logs := builder.cs.GetLogs()
if len(logs) != 3 {
t.Errorf("expected 3 log entries, got %d", len(logs))
}

// Verify format specifiers are stored correctly
if len(logs[0].FormatSpecifiers) != 4 {
t.Errorf("expected 4 format specifiers, got %d", len(logs[0].FormatSpecifiers))
}
if logs[0].FormatSpecifiers[0] != "d" {
t.Errorf("expected 'd' format specifier, got %s", logs[0].FormatSpecifiers[0])
}
}