github.com/consensys/gnark@v0.11.0/constraint/core.go (about)

     1  package constraint
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"strconv"
     7  	"sync"
     8  
     9  	"github.com/blang/semver/v4"
    10  	"github.com/consensys/gnark"
    11  	"github.com/consensys/gnark-crypto/ecc"
    12  	"github.com/consensys/gnark/constraint/solver"
    13  	"github.com/consensys/gnark/debug"
    14  	"github.com/consensys/gnark/internal/tinyfield"
    15  	"github.com/consensys/gnark/internal/utils"
    16  	"github.com/consensys/gnark/logger"
    17  	"github.com/consensys/gnark/profile"
    18  )
    19  
    20  type SystemType uint16
    21  
    22  const (
    23  	SystemUnknown SystemType = iota
    24  	SystemR1CS
    25  	SystemSparseR1CS
    26  )
    27  
    28  // PackedInstruction is the lowest element of a constraint system. It stores just enough data to
    29  // reconstruct a constraint of any shape or a hint at solving time.
    30  type PackedInstruction struct {
    31  	// BlueprintID maps this instruction to a blueprint
    32  	BlueprintID BlueprintID
    33  
    34  	// ConstraintOffset stores the starting constraint ID of this instruction.
    35  	// Might not be strictly necessary; but speeds up solver for instructions that represents
    36  	// multiple constraints.
    37  	ConstraintOffset uint32
    38  
    39  	// WireOffset stores the starting internal wire ID of this instruction. Blueprints may use this
    40  	// and refer to output wires by their offset.
    41  	// For example, if a blueprint declared 5 outputs, the first output wire will be WireOffset,
    42  	// the last one WireOffset+4.
    43  	WireOffset uint32
    44  
    45  	// The constraint system stores a single []uint32 calldata slice. StartCallData
    46  	// points to the starting index in the mentioned slice. This avoid storing a slice per
    47  	// instruction (3 * uint64 in memory).
    48  	StartCallData uint64
    49  }
    50  
    51  // Unpack returns the instruction corresponding to the packed instruction.
    52  func (pi PackedInstruction) Unpack(cs *System) Instruction {
    53  
    54  	blueprint := cs.Blueprints[pi.BlueprintID]
    55  	cSize := blueprint.CalldataSize()
    56  	if cSize < 0 {
    57  		// by convention, we store nbInputs < 0 for non-static input length.
    58  		cSize = int(cs.CallData[pi.StartCallData])
    59  	}
    60  
    61  	return Instruction{
    62  		ConstraintOffset: pi.ConstraintOffset,
    63  		WireOffset:       pi.WireOffset,
    64  		Calldata:         cs.CallData[pi.StartCallData : pi.StartCallData+uint64(cSize)],
    65  	}
    66  }
    67  
    68  // Instruction is the lowest element of a constraint system. It stores all the data needed to
    69  // reconstruct a constraint of any shape or a hint at solving time.
    70  type Instruction struct {
    71  	ConstraintOffset uint32
    72  	WireOffset       uint32
    73  	Calldata         []uint32
    74  }
    75  
    76  // System contains core elements for a constraint System
    77  type System struct {
    78  	// serialization header
    79  	GnarkVersion string
    80  	ScalarField  string
    81  
    82  	Type SystemType
    83  
    84  	Instructions []PackedInstruction `cbor:"-"`
    85  	Blueprints   []Blueprint
    86  	CallData     []uint32 `cbor:"-"`
    87  
    88  	// can be != than len(instructions)
    89  	NbConstraints int
    90  
    91  	// number of internal wires
    92  	NbInternalVariables int
    93  
    94  	// input wires names
    95  	Public, Secret []string
    96  
    97  	// logs (added with system.Println, resolved when solver sets a value to a wire)
    98  	Logs []LogEntry
    99  
   100  	// debug info contains stack trace (including line number) of a call to a system.API that
   101  	// results in an unsolved constraint
   102  	DebugInfo   []LogEntry
   103  	SymbolTable debug.SymbolTable
   104  	// maps constraint id to debugInfo id
   105  	// several constraints may point to the same debug info
   106  	MDebug map[int]int
   107  
   108  	// maps hintID to hint string identifier
   109  	MHintsDependencies map[solver.HintID]string
   110  
   111  	// each level contains independent constraints and can be parallelized
   112  	// it is guaranteed that all dependencies for constraints in a level l are solved
   113  	// in previous levels
   114  	// TODO @gbotrel these are currently updated after we add a constraint.
   115  	// but in case the object is built from a serialized representation
   116  	// we need to init the level builder lbWireLevel from the existing constraints.
   117  	Levels [][]uint32 `cbor:"-"`
   118  
   119  	// scalar field
   120  	q      *big.Int `cbor:"-"`
   121  	bitLen int      `cbor:"-"`
   122  
   123  	// level builder
   124  	lbWireLevel []Level `cbor:"-"` // at which level we solve a wire. init at -1.
   125  
   126  	CommitmentInfo Commitments
   127  	GkrInfo        GkrInfo
   128  
   129  	genericHint BlueprintID
   130  }
   131  
   132  // NewSystem initialize the common structure among constraint system
   133  func NewSystem(scalarField *big.Int, capacity int, t SystemType) System {
   134  	system := System{
   135  		Type:               t,
   136  		SymbolTable:        debug.NewSymbolTable(),
   137  		MDebug:             map[int]int{},
   138  		GnarkVersion:       gnark.Version.String(),
   139  		ScalarField:        scalarField.Text(16),
   140  		MHintsDependencies: make(map[solver.HintID]string),
   141  		q:                  new(big.Int).Set(scalarField),
   142  		bitLen:             scalarField.BitLen(),
   143  		Instructions:       make([]PackedInstruction, 0, capacity),
   144  		CallData:           make([]uint32, 0, capacity*8),
   145  		lbWireLevel:        make([]Level, 0, capacity),
   146  		Levels:             make([][]uint32, 0, capacity/2),
   147  		CommitmentInfo:     NewCommitments(t),
   148  	}
   149  
   150  	system.genericHint = system.AddBlueprint(&BlueprintGenericHint{})
   151  	return system
   152  }
   153  
   154  // GetNbInstructions returns the number of instructions in the system
   155  func (system *System) GetNbInstructions() int {
   156  	return len(system.Instructions)
   157  }
   158  
   159  // GetInstruction returns the instruction at index id
   160  func (system *System) GetInstruction(id int) Instruction {
   161  	return system.Instructions[id].Unpack(system)
   162  }
   163  
   164  // AddBlueprint adds a blueprint to the system and returns its ID
   165  func (system *System) AddBlueprint(b Blueprint) BlueprintID {
   166  	system.Blueprints = append(system.Blueprints, b)
   167  	return BlueprintID(len(system.Blueprints) - 1)
   168  }
   169  
   170  func (system *System) GetNbSecretVariables() int {
   171  	return len(system.Secret)
   172  }
   173  func (system *System) GetNbPublicVariables() int {
   174  	return len(system.Public)
   175  }
   176  func (system *System) GetNbInternalVariables() int {
   177  	return system.NbInternalVariables
   178  }
   179  
   180  // CheckSerializationHeader parses the scalar field and gnark version headers
   181  //
   182  // This is meant to be use at the deserialization step, and will error for illegal values
   183  func (system *System) CheckSerializationHeader() error {
   184  	// check gnark version
   185  	binaryVersion := gnark.Version
   186  	objectVersion, err := semver.Parse(system.GnarkVersion)
   187  	if err != nil {
   188  		return fmt.Errorf("when parsing gnark version: %w", err)
   189  	}
   190  
   191  	if binaryVersion.Compare(objectVersion) != 0 {
   192  		log := logger.Logger()
   193  		log.Warn().Str("binary", binaryVersion.String()).Str("object", objectVersion.String()).Msg("gnark version (binary) mismatch with constraint system. there are no guarantees on compatibility")
   194  	}
   195  
   196  	// TODO @gbotrel maintain version changes and compare versions properly
   197  	// (ie if major didn't change,we shouldn't have a compatibility issue)
   198  
   199  	scalarField := new(big.Int)
   200  	_, ok := scalarField.SetString(system.ScalarField, 16)
   201  	if !ok {
   202  		return fmt.Errorf("when parsing serialized modulus: %s", system.ScalarField)
   203  	}
   204  	curveID := utils.FieldToCurve(scalarField)
   205  	if curveID == ecc.UNKNOWN && scalarField.Cmp(tinyfield.Modulus()) != 0 {
   206  		return fmt.Errorf("unsupported scalar field %s", scalarField.Text(16))
   207  	}
   208  	system.q = new(big.Int).Set(scalarField)
   209  	system.bitLen = system.q.BitLen()
   210  	return nil
   211  }
   212  
   213  // GetNbVariables return number of internal, secret and public variables
   214  func (system *System) GetNbVariables() (internal, secret, public int) {
   215  	return system.NbInternalVariables, system.GetNbSecretVariables(), system.GetNbPublicVariables()
   216  }
   217  
   218  func (system *System) Field() *big.Int {
   219  	return new(big.Int).Set(system.q)
   220  }
   221  
   222  // bitLen returns the number of bits needed to represent a fr.Element
   223  func (system *System) FieldBitLen() int {
   224  	return system.bitLen
   225  }
   226  
   227  func (system *System) AddInternalVariable() (idx int) {
   228  	idx = system.NbInternalVariables + system.GetNbPublicVariables() + system.GetNbSecretVariables()
   229  	system.NbInternalVariables++
   230  	// also grow the level slice
   231  	system.lbWireLevel = append(system.lbWireLevel, LevelUnset)
   232  	if debug.Debug && len(system.lbWireLevel) != system.NbInternalVariables {
   233  		panic("internal error")
   234  	}
   235  	return idx
   236  }
   237  
   238  func (system *System) AddPublicVariable(name string) (idx int) {
   239  	idx = system.GetNbPublicVariables()
   240  	system.Public = append(system.Public, name)
   241  	return idx
   242  }
   243  
   244  func (system *System) AddSecretVariable(name string) (idx int) {
   245  	idx = system.GetNbSecretVariables() + system.GetNbPublicVariables()
   246  	system.Secret = append(system.Secret, name)
   247  	return idx
   248  }
   249  
   250  func (system *System) AddSolverHint(f solver.Hint, id solver.HintID, input []LinearExpression, nbOutput int) (internalVariables []int, err error) {
   251  	if nbOutput <= 0 {
   252  		return nil, fmt.Errorf("hint function must return at least one output")
   253  	}
   254  
   255  	var name string
   256  	if id == solver.GetHintID(f) {
   257  		name = solver.GetHintName(f)
   258  	} else {
   259  		name = strconv.Itoa(int(id))
   260  	}
   261  
   262  	// register the hint as dependency
   263  	if registeredName, ok := system.MHintsDependencies[id]; ok {
   264  		// hint already registered, let's ensure string registeredName matches
   265  		if registeredName != name {
   266  			return nil, fmt.Errorf("hint dependency registration failed; %s previously register with same UUID as %s", name, registeredName)
   267  		}
   268  	} else {
   269  		system.MHintsDependencies[id] = name
   270  	}
   271  
   272  	// prepare wires
   273  	internalVariables = make([]int, nbOutput)
   274  	for i := 0; i < len(internalVariables); i++ {
   275  		internalVariables[i] = system.AddInternalVariable()
   276  	}
   277  
   278  	// associate these wires with the solver hint
   279  	hm := HintMapping{
   280  		HintID: id,
   281  		Inputs: input,
   282  		OutputRange: struct {
   283  			Start uint32
   284  			End   uint32
   285  		}{
   286  			uint32(internalVariables[0]),
   287  			uint32(internalVariables[len(internalVariables)-1]) + 1,
   288  		},
   289  	}
   290  
   291  	blueprint := system.Blueprints[system.genericHint]
   292  
   293  	// get []uint32 from the pool
   294  	calldata := getBuffer()
   295  
   296  	blueprint.(BlueprintHint).CompressHint(hm, calldata)
   297  
   298  	system.AddInstruction(system.genericHint, *calldata)
   299  
   300  	// return []uint32 to the pool
   301  	putBuffer(calldata)
   302  
   303  	return
   304  }
   305  
   306  func (system *System) AddCommitment(c Commitment) error {
   307  	switch v := c.(type) {
   308  	case Groth16Commitment:
   309  		system.CommitmentInfo = append(system.CommitmentInfo.(Groth16Commitments), v)
   310  	case PlonkCommitment:
   311  		system.CommitmentInfo = append(system.CommitmentInfo.(PlonkCommitments), v)
   312  	default:
   313  		return fmt.Errorf("unknown commitment type %T", v)
   314  	}
   315  	return nil
   316  }
   317  
   318  func (system *System) AddLog(l LogEntry) {
   319  	system.Logs = append(system.Logs, l)
   320  }
   321  
   322  func (system *System) AttachDebugInfo(debugInfo DebugInfo, constraintID []int) {
   323  	system.DebugInfo = append(system.DebugInfo, LogEntry(debugInfo))
   324  	id := len(system.DebugInfo) - 1
   325  	for _, cID := range constraintID {
   326  		system.MDebug[cID] = id
   327  	}
   328  }
   329  
   330  // VariableToString implements Resolver
   331  func (system *System) VariableToString(vID int) string {
   332  	nbPublic := system.GetNbPublicVariables()
   333  	nbSecret := system.GetNbSecretVariables()
   334  
   335  	if vID < nbPublic {
   336  		return system.Public[vID]
   337  	}
   338  	vID -= nbPublic
   339  	if vID < nbSecret {
   340  		return system.Secret[vID]
   341  	}
   342  	vID -= nbSecret
   343  	return fmt.Sprintf("v%d", vID) // TODO @gbotrel  vs strconv.Itoa.
   344  }
   345  
   346  func (cs *System) AddR1C(c R1C, bID BlueprintID) int {
   347  	profile.RecordConstraint()
   348  
   349  	blueprint := cs.Blueprints[bID]
   350  
   351  	// get a []uint32 from a pool
   352  	calldata := getBuffer()
   353  
   354  	// compress the R1C into a []uint32 and add the instruction
   355  	blueprint.(BlueprintR1C).CompressR1C(&c, calldata)
   356  	cs.AddInstruction(bID, *calldata)
   357  
   358  	// release the []uint32 to the pool
   359  	putBuffer(calldata)
   360  
   361  	return cs.NbConstraints - 1
   362  }
   363  
   364  func (cs *System) AddSparseR1C(c SparseR1C, bID BlueprintID) int {
   365  	profile.RecordConstraint()
   366  
   367  	blueprint := cs.Blueprints[bID]
   368  
   369  	// get a []uint32 from a pool
   370  	calldata := getBuffer()
   371  
   372  	// compress the SparceR1C into a []uint32 and add the instruction
   373  	blueprint.(BlueprintSparseR1C).CompressSparseR1C(&c, calldata)
   374  
   375  	cs.AddInstruction(bID, *calldata)
   376  
   377  	// release the []uint32 to the pool
   378  	putBuffer(calldata)
   379  
   380  	return cs.NbConstraints - 1
   381  }
   382  
   383  func (cs *System) AddInstruction(bID BlueprintID, calldata []uint32) []uint32 {
   384  	// set the offsets
   385  	pi := PackedInstruction{
   386  		StartCallData:    uint64(len(cs.CallData)),
   387  		ConstraintOffset: uint32(cs.NbConstraints),
   388  		WireOffset:       uint32(cs.NbInternalVariables + cs.GetNbPublicVariables() + cs.GetNbSecretVariables()),
   389  		BlueprintID:      bID,
   390  	}
   391  
   392  	// append the call data
   393  	cs.CallData = append(cs.CallData, calldata...)
   394  
   395  	// update the total number of constraints
   396  	blueprint := cs.Blueprints[pi.BlueprintID]
   397  	cs.NbConstraints += blueprint.NbConstraints()
   398  
   399  	// add the output wires
   400  	inst := pi.Unpack(cs)
   401  	nbOutputs := blueprint.NbOutputs(inst)
   402  	var wires []uint32
   403  	for i := 0; i < nbOutputs; i++ {
   404  		wires = append(wires, uint32(cs.AddInternalVariable()))
   405  	}
   406  
   407  	// add the instruction
   408  	cs.Instructions = append(cs.Instructions, pi)
   409  
   410  	// update the instruction dependency tree
   411  	level := blueprint.UpdateInstructionTree(inst, cs)
   412  	iID := uint32(len(cs.Instructions) - 1)
   413  
   414  	// we can't skip levels, so appending is fine.
   415  	if int(level) >= len(cs.Levels) {
   416  		cs.Levels = append(cs.Levels, []uint32{iID})
   417  	} else {
   418  		cs.Levels[level] = append(cs.Levels[level], iID)
   419  	}
   420  
   421  	return wires
   422  }
   423  
   424  // GetNbConstraints returns the number of constraints
   425  func (cs *System) GetNbConstraints() int {
   426  	return cs.NbConstraints
   427  }
   428  
   429  func (cs *System) CheckUnconstrainedWires() error {
   430  	// TODO @gbotrel
   431  	return nil
   432  }
   433  
   434  func (cs *System) GetR1CIterator() R1CIterator {
   435  	return R1CIterator{cs: cs}
   436  }
   437  
   438  func (cs *System) GetSparseR1CIterator() SparseR1CIterator {
   439  	return SparseR1CIterator{cs: cs}
   440  }
   441  
   442  func (cs *System) GetCommitments() Commitments {
   443  	return cs.CommitmentInfo
   444  }
   445  
   446  // bufPool is a pool of buffers used by getBuffer and putBuffer.
   447  // It is used to avoid allocating buffers for each constraint.
   448  var bufPool = sync.Pool{
   449  	New: func() interface{} {
   450  		r := make([]uint32, 0, 20)
   451  		return &r
   452  	},
   453  }
   454  
   455  // getBuffer returns a buffer of at least the given size.
   456  // The buffer is taken from the pool if it is large enough,
   457  // otherwise a new buffer is allocated.
   458  // Caller must call putBuffer when done with the buffer.
   459  func getBuffer() *[]uint32 {
   460  	to := bufPool.Get().(*[]uint32)
   461  	*to = (*to)[:0]
   462  	return to
   463  }
   464  
   465  // putBuffer returns a buffer to the pool.
   466  func putBuffer(buf *[]uint32) {
   467  	if buf == nil {
   468  		panic("invalid entry in putBuffer")
   469  	}
   470  	bufPool.Put(buf)
   471  }
   472  
   473  func (system *System) AddGkr(gkr GkrInfo) error {
   474  	if system.GkrInfo.Is() {
   475  		return fmt.Errorf("currently only one GKR sub-circuit per SNARK is supported")
   476  	}
   477  
   478  	system.GkrInfo = gkr
   479  	return nil
   480  }