Skip to content

Commit

Permalink
added sudoku example for prover and verifier (#1282)
Browse files Browse the repository at this point in the history
* added sudoku example for prover and verifier

* chore: rename package

* test: refactor the example to be package doc

* docs: add package documentation

* chore: remove README

---------

Co-authored-by: Ivo Kubjas <[email protected]>
  • Loading branch information
2pir2 and ivokub authored Oct 3, 2024
1 parent 42dcb0c commit 382258a
Show file tree
Hide file tree
Showing 2 changed files with 268 additions and 0 deletions.
14 changes: 14 additions & 0 deletions examples/sudoku/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Package sudoku implements a Sudoku circuit using gnark.
//
// [Sudoku] is a popular puzzle to fill a 9x9 grid with digits so that each
// column, each row, and each of the nine 3x3 sub-grids that compose the grid
// contain all of the digits from 1 to 9. This package provides a circuit that
// verifies a solution to a Sudoku puzzle.
//
// See the included full example on how to define the circuit, run the setup,
// generate proof and verify the proof. This example also demonstrates how to
// serialize and deserialize the values produced during setup and proof
// generation.
//
// [Sudoku]: https://en.wikipedia.org/wiki/Sudoku
package sudoku
254 changes: 254 additions & 0 deletions examples/sudoku/sudoku_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package sudoku

import (
"bytes"
"encoding/json"
"fmt"
"io"

"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/groth16"
groth16_bn254 "github.com/consensys/gnark/backend/groth16/bn254"
cs_bn254 "github.com/consensys/gnark/constraint/bn254"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/r1cs"
)

// SudokuCircuit represents a Sudoku circuit. It contains two grids: the
// challenge and solution grids (named Challenge and Solution respectively). The
// challenge grid is public, while the solution grid is private.
type SudokuCircuit struct {
Challenge SudokuGrid `gnark:"Challenge,public"`
Solution SudokuGrid `gnark:"Solution,secret"`
}

// SudokuGrid represents a 9x9 Sudoku grid in-circuit.
type SudokuGrid [9][9]frontend.Variable

// Define defines the constraints of the Sudoku circuit.
func (circuit *SudokuCircuit) Define(api frontend.API) error {
// Constraint 1: Each cell value in the CompleteGrid must be between 1 and 9
for i := 0; i < 9; i++ {
for j := 0; j < 9; j++ {
api.AssertIsLessOrEqual(circuit.Solution[i][j], 9)
api.AssertIsLessOrEqual(1, circuit.Solution[i][j])
}
}

// Constraint 2: Each row in the CompleteGrid must contain unique values
for i := 0; i < 9; i++ {
for j := 0; j < 9; j++ {
for k := j + 1; k < 9; k++ {
api.AssertIsDifferent(circuit.Solution[i][j], circuit.Solution[i][k])
}
}
}

// Constraint 3: Each column in the CompleteGrid must contain unique values
for j := 0; j < 9; j++ {
for i := 0; i < 9; i++ {
for k := i + 1; k < 9; k++ {
api.AssertIsDifferent(circuit.Solution[i][j], circuit.Solution[k][j])
}
}
}

// Constraint 4: Each 3x3 sub-grid in the CompleteGrid must contain unique values
for boxRow := 0; boxRow < 3; boxRow++ {
for boxCol := 0; boxCol < 3; boxCol++ {
for i := 0; i < 9; i++ {
for j := i + 1; j < 9; j++ {
row1 := boxRow*3 + i/3
col1 := boxCol*3 + i%3
row2 := boxRow*3 + j/3
col2 := boxCol*3 + j%3
api.AssertIsDifferent(circuit.Solution[row1][col1], circuit.Solution[row2][col2])
}
}
}
}

// Constraint 5: The values in the IncompleteGrid must match the CompleteGrid where provided
for i := 0; i < 9; i++ {
for j := 0; j < 9; j++ {
isCellGiven := api.IsZero(circuit.Challenge[i][j])
api.AssertIsEqual(api.Select(isCellGiven, circuit.Solution[i][j], circuit.Challenge[i][j]), circuit.Solution[i][j])
}
}

return nil
}

// SudokuSerialization represents a Sudoku witness out-circuit. Used for serialization.
type SudokuSerialization struct {
Grid [9][9]int `json:"grid"`
}

// NewSudokuGrid creates a new Sudoku grid from the serialized grid.
func NewSudokuGrid(serialized SudokuSerialization) SudokuGrid {
var grid SudokuGrid
for i := 0; i < 9; i++ {
for j := 0; j < 9; j++ {
grid[i][j] = frontend.Variable(serialized.Grid[i][j])
}
}
return grid
}

// setup performs the setup phase of the Sudoku circuit
func setup(ccsWriter, pkWriter, vkWriter io.Writer) error {
// compile the circuit
ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &SudokuCircuit{})
if err != nil {
return fmt.Errorf("failed to compile circuit: %v", err)
}
// perform the setup. NB! In practice use MPC. This is currently UNSAFE
// approach.
pk, vk, err := groth16.Setup(ccs)
if err != nil {
return fmt.Errorf("failed to setup circuit: %v", err)
}
// serialize the circuit, proving key and verifying key
_, err = ccs.WriteTo(ccsWriter)
if err != nil {
return fmt.Errorf("failed to write constraint system: %v", err)
}
_, err = pk.WriteTo(pkWriter)
if err != nil {
return fmt.Errorf("failed to write proving key: %v", err)
}
_, err = vk.WriteTo(vkWriter)
if err != nil {
return fmt.Errorf("failed to write verifying key: %v", err)
}
return nil
}

// prover performs the prover phase of the Sudoku circuit
func prover(ccsReader, challengeReader, pkReader io.Reader, proofWriter io.Writer) error {
// define the sudoku solution. This is private information known only to the
// prover. We use serialization to represent it.
serializedSolution := `{"grid":[[5,3,4,6,7,8,9,1,2],[6,7,2,1,9,5,3,4,8],[1,9,8,3,4,2,5,6,7],[8,5,9,7,6,1,4,2,3],[4,2,6,8,5,3,7,9,1],[7,1,3,9,2,4,8,5,6],[9,6,1,5,3,7,2,8,4],[2,8,7,4,1,9,6,3,5],[3,4,5,2,8,6,1,7,9]]}`
var nativeSolution SudokuSerialization
err := json.Unmarshal([]byte(serializedSolution), &nativeSolution)
if err != nil {
return fmt.Errorf("failed to unmarshal solution: %v", err)
}
// deserialize the circuit, challenge and proving key
var ccs cs_bn254.R1CS
_, err = ccs.ReadFrom(ccsReader)
if err != nil {
return fmt.Errorf("failed to read constraint system: %v", err)
}
var nativeChallenge SudokuSerialization
err = json.NewDecoder(challengeReader).Decode(&nativeChallenge)
if err != nil {
return fmt.Errorf("failed to read challenge: %v", err)
}
var pk groth16_bn254.ProvingKey
_, err = pk.ReadFrom(pkReader)
if err != nil {
return fmt.Errorf("failed to read proving key: %v", err)
}

// create the circuit assignments
assignment := &SudokuCircuit{
Challenge: NewSudokuGrid(nativeChallenge),
Solution: NewSudokuGrid(nativeSolution),
}
// create the witness
witness, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField())
if err != nil {
return fmt.Errorf("failed to create witness: %v", err)
}
// generate the proof
proof, err := groth16.Prove(&ccs, &pk, witness)
if err != nil {
return fmt.Errorf("failed to generate proof: %v", err)
}
// serialize the proof
_, err = proof.WriteTo(proofWriter)
if err != nil {
return fmt.Errorf("failed to write proof: %v", err)
}
return nil
}

// verifier performs the verifier phase of the Sudoku circuit
func verifier(challengeReader, vkReader, proofReader io.Reader) error {
// deserialize the challenge, verifying key and proof
var nativeChallenge SudokuSerialization
err := json.NewDecoder(challengeReader).Decode(&nativeChallenge)
if err != nil {
return fmt.Errorf("failed to read challenge: %v", err)
}
var vk groth16_bn254.VerifyingKey
_, err = vk.ReadFrom(vkReader)
if err != nil {
return fmt.Errorf("failed to read verifying key: %v", err)
}
var proof groth16_bn254.Proof
_, err = proof.ReadFrom(proofReader)
if err != nil {
return fmt.Errorf("failed to read proof: %v", err)
}
// create the circuit assignment
assignment := &SudokuCircuit{
Challenge: NewSudokuGrid(nativeChallenge),
}
// create the public witness
pubWit, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField(), frontend.PublicOnly())
if err != nil {
return fmt.Errorf("failed to create public witness: %v", err)
}
// verify the proof
err = groth16.Verify(&proof, &vk, pubWit)
if err != nil {
return fmt.Errorf("failed to verify proof: %v", err)
}
return nil
}

// This example demonstrates how to implement a Sudoku challenge verification
// circuit such that the solution stays private.
//
// This example also demonstrates how to serialize and deserialize the values
// produced during setup and proof generation.
func Example() {
// define the sudoku challenge. This is public information. We use
// serialization to represent it.
serializedChallengeBytes := `{"grid":[[5,3,0,0,7,0,0,0,0],[6,0,0,1,9,5,0,0,0],[0,9,8,0,0,0,0,6,0],[8,0,0,0,6,0,0,0,3],[4,0,0,8,0,3,0,0,1],[7,0,0,0,2,0,0,0,6],[0,6,0,0,0,0,2,8,0],[0,0,0,4,1,9,0,0,5],[0,0,0,0,8,0,0,7,9]]}`

var (
serializedCCS bytes.Buffer

serializedProvingKey bytes.Buffer
serializedVerifyingKey bytes.Buffer

serializedProof bytes.Buffer
)

// full example

// first we run the setup phase. This happens offline in a trusted or MPC setting
if err := setup(&serializedCCS, &serializedProvingKey, &serializedVerifyingKey); err != nil {
fmt.Println("failed to setup circuit:", err)
return
}

// then we run the prover phase. This happens online
serializedChallenge := bytes.NewBufferString(serializedChallengeBytes)
if err := prover(&serializedCCS, serializedChallenge, &serializedProvingKey, &serializedProof); err != nil {
fmt.Println("failed to prove circuit:", err)
return
}

// finally we run the verifier phase. This happens online
serializedChallenge = bytes.NewBufferString(serializedChallengeBytes)
if err := verifier(serializedChallenge, &serializedVerifyingKey, &serializedProof); err != nil {
fmt.Println("failed to verify circuit:", err)
return
}
fmt.Println("proof verified successfully!")
// Output: proof verified successfully!
}

0 comments on commit 382258a

Please sign in to comment.