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 }