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

added sudoku example for prover and verifier #1282

Merged
merged 5 commits into from
Oct 3, 2024
Merged
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
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!
}