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

Ciphertexts #6

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
19 changes: 10 additions & 9 deletions circuits/statetransition/circuit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/vocdoni/gnark-crypto-primitives/elgamal"
"github.com/vocdoni/gnark-crypto-primitives/utils"
"github.com/vocdoni/vocdoni-z-sandbox/state"
"github.com/vocdoni/vocdoni-z-sandbox/util"
)

var HashFn = utils.MiMCHasher
Expand Down Expand Up @@ -96,7 +97,7 @@ func (circuit Circuit) VerifyMerkleProofs(api frontend.API, hFn utils.Hasher) {

func (circuit Circuit) VerifyMerkleTransitions(api frontend.API, hFn utils.Hasher) {
// verify chain of tree transitions, order here is fundamental.
api.Println("tree transition starts with RootHashBefore:", prettyHex(circuit.RootHashBefore))
api.Println("tree transition starts with RootHashBefore:", util.PrettyHex(circuit.RootHashBefore))
root := circuit.RootHashBefore
for i := range circuit.Ballot {
root = circuit.Ballot[i].Verify(api, hFn, root)
Expand All @@ -106,32 +107,32 @@ func (circuit Circuit) VerifyMerkleTransitions(api frontend.API, hFn utils.Hashe
}
root = circuit.ResultsAdd.Verify(api, hFn, root)
root = circuit.ResultsSub.Verify(api, hFn, root)
api.Println("and final root is", prettyHex(root), "should be equal to RootHashAfter", prettyHex(circuit.RootHashAfter))
api.Println("and final root is", util.PrettyHex(root), "should be equal to RootHashAfter", util.PrettyHex(circuit.RootHashAfter))
api.AssertIsEqual(root, circuit.RootHashAfter)
}

// VerifyBallots counts the ballots using homomorphic encrpytion
func (circuit Circuit) VerifyBallots(api frontend.API) {
ballotSum, overwrittenSum, zero := elgamal.NewCiphertext(), elgamal.NewCiphertext(), elgamal.NewCiphertext()
ballotSum, overwrittenSum, zero := elgamal.NewCiphertexts(), elgamal.NewCiphertexts(), elgamal.NewCiphertexts()
var ballotCount, overwrittenCount frontend.Variable = 0, 0

for _, b := range circuit.Ballot {
// TODO: check that Hash(NewCiphertext) matches b.NewValue
// and Hash(OldCiphertext) matches b.OldValue
ballotSum.Add(api, ballotSum,
elgamal.NewCiphertext().Select(api, b.IsInsertOrUpdate(api), &b.NewCiphertext, zero))
elgamal.NewCiphertexts().Select(api, b.IsInsertOrUpdate(api), &b.NewCiphertexts, zero))

overwrittenSum.Add(api, overwrittenSum,
elgamal.NewCiphertext().Select(api, b.IsUpdate(api), &b.OldCiphertext, zero))
elgamal.NewCiphertexts().Select(api, b.IsUpdate(api), &b.OldCiphertexts, zero))

ballotCount = api.Add(ballotCount, api.Select(b.IsInsertOrUpdate(api), 1, 0))
overwrittenCount = api.Add(overwrittenCount, api.Select(b.IsUpdate(api), 1, 0))
}

circuit.ResultsAdd.NewCiphertext.AssertIsEqual(api,
circuit.ResultsAdd.OldCiphertext.Add(api, &circuit.ResultsAdd.OldCiphertext, ballotSum))
circuit.ResultsSub.NewCiphertext.AssertIsEqual(api,
circuit.ResultsSub.OldCiphertext.Add(api, &circuit.ResultsSub.OldCiphertext, overwrittenSum))
circuit.ResultsAdd.NewCiphertexts.AssertIsEqual(api,
circuit.ResultsAdd.OldCiphertexts.Add(api, &circuit.ResultsAdd.OldCiphertexts, ballotSum))
circuit.ResultsSub.NewCiphertexts.AssertIsEqual(api,
circuit.ResultsSub.OldCiphertexts.Add(api, &circuit.ResultsSub.OldCiphertexts, overwrittenSum))
api.AssertIsEqual(circuit.NumNewVotes, ballotCount)
api.AssertIsEqual(circuit.NumOverwrites, overwrittenCount)
}
65 changes: 35 additions & 30 deletions circuits/statetransition/circuit_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package statetransition_test

import (
"encoding/hex"
"fmt"
"math/big"
"os"
"reflect"
"testing"

"github.com/consensys/gnark-crypto/ecc"
Expand All @@ -18,6 +16,7 @@ import (
"github.com/vocdoni/vocdoni-z-sandbox/circuits/statetransition"
"github.com/vocdoni/vocdoni-z-sandbox/crypto/elgamal"
"github.com/vocdoni/vocdoni-z-sandbox/state"
"github.com/vocdoni/vocdoni-z-sandbox/util"

"github.com/vocdoni/arbo"
"go.vocdoni.io/dvote/db/metadb"
Expand Down Expand Up @@ -99,39 +98,40 @@ func TestCircuitProve(t *testing.T) {
func debugLog(t *testing.T, witness *statetransition.Circuit) {
// js, _ := json.MarshalIndent(witness, "", " ")
// fmt.Printf("\n\n%s\n\n", js)
t.Log("public: RootHashBefore", prettyHex(witness.RootHashBefore))
t.Log("public: RootHashAfter", prettyHex(witness.RootHashAfter))
t.Log("public: NumVotes", prettyHex(witness.NumNewVotes))
t.Log("public: NumOverwrites", prettyHex(witness.NumOverwrites))
t.Log("public: RootHashBefore", util.PrettyHex(witness.RootHashBefore))
t.Log("public: RootHashAfter", util.PrettyHex(witness.RootHashAfter))
t.Log("public: NumVotes", util.PrettyHex(witness.NumNewVotes))
t.Log("public: NumOverwrites", util.PrettyHex(witness.NumOverwrites))
for name, mts := range map[string][statetransition.VoteBatchSize]state.MerkleTransition{
"Ballot": witness.Ballot,
"Commitment": witness.Commitment,
} {
for _, mt := range mts {
t.Log(name, "transitioned", "(root", util.PrettyHex(mt.OldRoot), "->", util.PrettyHex(mt.NewRoot), ")",
"value", mt.OldValue, "->", mt.NewValue,
)
for i := range mt.OldCiphertexts {
t.Log(name, i, "elgamal.C1.X", mt.OldCiphertexts[i].C1.X, "->", mt.NewCiphertexts[i].C1.X)
t.Log(name, i, "elgamal.C1.Y", mt.OldCiphertexts[i].C1.Y, "->", mt.NewCiphertexts[i].C1.Y)
t.Log(name, i, "elgamal.C2.X", mt.OldCiphertexts[i].C2.X, "->", mt.NewCiphertexts[i].C2.X)
t.Log(name, i, "elgamal.C2.Y", mt.OldCiphertexts[i].C2.Y, "->", mt.NewCiphertexts[i].C2.Y)
}
}
}

for name, mt := range map[string]state.MerkleTransition{
"ResultsAdd": witness.ResultsAdd,
"ResultsSub": witness.ResultsSub,
} {
t.Log(name, "transitioned", "(root", prettyHex(mt.OldRoot), "->", prettyHex(mt.NewRoot), ")",
t.Log(name, "transitioned", "(root", util.PrettyHex(mt.OldRoot), "->", util.PrettyHex(mt.NewRoot), ")",
"value", mt.OldValue, "->", mt.NewValue,
)
t.Log(name, "elgamal.C1.X", mt.OldCiphertext.C1.X, "->", mt.NewCiphertext.C1.X)
t.Log(name, "elgamal.C1.Y", mt.OldCiphertext.C1.Y, "->", mt.NewCiphertext.C1.Y)
t.Log(name, "elgamal.C2.X", mt.OldCiphertext.C2.X, "->", mt.NewCiphertext.C2.X)
t.Log(name, "elgamal.C2.Y", mt.OldCiphertext.C2.Y, "->", mt.NewCiphertext.C2.Y)
}
}

func prettyHex(v frontend.Variable) string {
type hasher interface {
HashCode() [16]byte
}
switch v := v.(type) {
case (*big.Int):
return hex.EncodeToString(arbo.BigIntToBytes(32, v)[:4])
case int:
return fmt.Sprintf("%d", v)
case []byte:
return fmt.Sprintf("%x", v[:4])
case hasher:
return fmt.Sprintf("%x", v.HashCode())
default:
return fmt.Sprintf("(%v)=%+v", reflect.TypeOf(v), v)
for i := range mt.OldCiphertexts {
t.Log(name, i, "elgamal.C1.X", mt.OldCiphertexts[i].C1.X, "->", mt.NewCiphertexts[i].C1.X)
t.Log(name, i, "elgamal.C1.Y", mt.OldCiphertexts[i].C1.Y, "->", mt.NewCiphertexts[i].C1.Y)
t.Log(name, i, "elgamal.C2.X", mt.OldCiphertexts[i].C2.X, "->", mt.NewCiphertexts[i].C2.X)
t.Log(name, i, "elgamal.C2.Y", mt.OldCiphertexts[i].C2.Y, "->", mt.NewCiphertexts[i].C2.Y)
}
}
}

Expand Down Expand Up @@ -266,7 +266,12 @@ func newMockVote(index, amount int64) *state.Vote {
panic(fmt.Errorf("error generating public key: %v", err))
}

ballot, err := elgamal.NewCiphertext(publicKey).Encrypt(big.NewInt(int64(amount)), publicKey, nil)
ballot, err := elgamal.NewCiphertexts(publicKey).Encrypt(
[elgamal.NumCiphertexts]*big.Int{
big.NewInt(int64(amount)),
big.NewInt(int64(amount + 1)),
},
publicKey, nil)
if err != nil {
panic(fmt.Errorf("error encrypting: %v", err))
}
Expand Down
29 changes: 0 additions & 29 deletions circuits/statetransition/util.go

This file was deleted.

8 changes: 2 additions & 6 deletions circuits/statetransition/witness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,15 @@ func GenerateWitnesses(o *state.State) (*statetransition.Circuit, error) {
}

// update ResultsAdd
witness.ResultsAdd.OldCiphertext = o.ResultsAdd.ToGnark()
witness.ResultsAdd.NewCiphertext = o.ResultsAdd.Add(o.ResultsAdd, o.BallotSum).ToGnark()
witness.ResultsAdd, err = o.MerkleTransitionFromAddOrUpdate(
state.KeyResultsAdd, o.ResultsAdd.Serialize())
state.KeyResultsAdd, o.ResultsAdd.Add(o.ResultsAdd, o.BallotSum).Serialize())
if err != nil {
return nil, fmt.Errorf("ResultsAdd: %w", err)
}

// update ResultsSub
witness.ResultsSub.OldCiphertext = o.ResultsSub.ToGnark()
witness.ResultsSub.NewCiphertext = o.ResultsSub.Add(o.ResultsSub, o.OverwriteSum).ToGnark()
witness.ResultsSub, err = o.MerkleTransitionFromAddOrUpdate(
state.KeyResultsSub, o.ResultsSub.Serialize())
state.KeyResultsSub, o.ResultsSub.Add(o.ResultsSub, o.OverwriteSum).Serialize())
if err != nil {
return nil, fmt.Errorf("ResultsSub: %w", err)
}
Expand Down
121 changes: 108 additions & 13 deletions crypto/elgamal/ciphertext.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,103 @@ import (
"github.com/vocdoni/vocdoni-z-sandbox/crypto/ecc/format"
)

// size in bytes needed to serialize an ecc.Point coord
const sizePointCoord = 32
// NumCiphertexts represents how many Ciphertexts are grouped
const NumCiphertexts = 2

// sizes in bytes needed to serialize Ciphertexts
const (
sizeCoord = 32
sizePoint = 2 * sizeCoord
SizeCiphertext = 2 * sizePoint
SizeCiphertexts = NumCiphertexts * SizeCiphertext
)

type Ciphertexts [NumCiphertexts]*Ciphertext

func NewCiphertexts(curve ecc.Point) *Ciphertexts {
cs := &Ciphertexts{}
for i := range cs {
cs[i] = NewCiphertext(curve)
}
return cs
}

// Encrypt encrypts a message using the public key provided as elliptic curve point.
// The randomness k can be provided or nil to generate a new one.
func (cs *Ciphertexts) Encrypt(message [NumCiphertexts]*big.Int, publicKey ecc.Point, k *big.Int) (*Ciphertexts, error) {
for i := range cs {
if _, err := cs[i].Encrypt(message[i], publicKey, k); err != nil {
return nil, err
}
}
return cs, nil
}

// Add adds two Ciphertexts and stores the result in the receiver, which is also returned.
func (cs *Ciphertexts) Add(x, y *Ciphertexts) *Ciphertexts {
for i := range cs {
cs[i].Add(x[i], y[i])
}
return cs
}

// Serialize returns a slice of len N*4*32 bytes,
// representing each Ciphertext C1.X, C1.Y, C2.X, C2.Y as little-endian,
// in reduced twisted edwards form.
func (cs *Ciphertexts) Serialize() []byte {
var buf bytes.Buffer
for _, z := range cs {
buf.Write(z.Serialize())
}
return buf.Bytes()
}

// Deserialize reconstructs a Ciphertexts from a slice of bytes.
// The input must be of len N*4*32 bytes (otherwise it returns an error),
// representing each Ciphertext C1.X, C1.Y, C2.X, C2.Y as little-endian,
// in reduced twisted edwards form.
func (cs *Ciphertexts) Deserialize(data []byte) error {
// Validate the input length
if len(data) != SizeCiphertexts {
return fmt.Errorf("invalid input length: got %d bytes, expected %d bytes", len(data), SizeCiphertexts)
}
for i := range cs {
err := cs[i].Deserialize(data[i*SizeCiphertext : (i+1)*SizeCiphertext])
if err != nil {
return err
}
}
return nil
}

// TODO: implement Marshal, Unmarshal, String for Ciphertexts
// // Marshal converts Ciphertexts to a byte slice.
// func (z *Ciphertexts) Marshal() ([]byte, error) {
// return json.Marshal(z)
// }

// // Unmarshal populates Ciphertexts from a byte slice.
// func (z *Ciphertexts) Unmarshal(data []byte) error {
// return json.Unmarshal(data, z)
// }

// // String returns a string representation of the Ciphertexts.
// func (z *Ciphertexts) String() string {
// if z == nil || z.C1 == nil || z.C2 == nil {
// return "{C1: nil, C2: nil}"
// }
// return fmt.Sprintf("{C1: %s, C2: %s}", z.C1.String(), z.C2.String())
// }

// ToGnark returns cs as the struct used by gnark,
// with the points in reduced twisted edwards format
func (cs *Ciphertexts) ToGnark() *gelgamal.Ciphertexts {
gcs := &gelgamal.Ciphertexts{}
for i := range cs {
gcs[i] = *cs[i].ToGnark()
}
return gcs
}

// Ciphertext represents an ElGamal encrypted message with homomorphic properties.
// It is a wrapper for convenience of the elGamal ciphersystem that encapsulates the two points of a ciphertext.
Expand Down Expand Up @@ -65,7 +160,7 @@ func (z *Ciphertext) Serialize() []byte {
c1x, c1y := format.FromTEtoRTE(z.C1.Point())
c2x, c2y := format.FromTEtoRTE(z.C2.Point())
for _, bi := range []*big.Int{c1x, c1y, c2x, c2y} {
buf.Write(arbo.BigIntToBytes(sizePointCoord, bi))
buf.Write(arbo.BigIntToBytes(sizeCoord, bi))
}
return buf.Bytes()
}
Expand All @@ -76,23 +171,23 @@ func (z *Ciphertext) Serialize() []byte {
// in reduced twisted edwards form.
func (z *Ciphertext) Deserialize(data []byte) error {
// Validate the input length
if len(data) != 4*sizePointCoord {
return fmt.Errorf("invalid input length: got %d bytes, expected %d bytes", len(data), 4*sizePointCoord)
if len(data) != SizeCiphertext {
return fmt.Errorf("invalid input length: got %d bytes, expected %d bytes", len(data), SizeCiphertext)
}

// Helper function to extract *big.Int from a 32-byte slice
// Helper function to extract *big.Int from a serialized slice
readBigInt := func(offset int) *big.Int {
return arbo.BytesToBigInt(data[offset : offset+sizePointCoord])
return arbo.BytesToBigInt(data[offset : offset+sizeCoord])
}
// Deserialize each field
// TODO: we wouldn't need the format conversion if SetPoint() accepts the correct format
z.C1 = z.C1.SetPoint(format.FromRTEtoTE(
readBigInt(0*sizePointCoord),
readBigInt(1*sizePointCoord),
readBigInt(0*sizeCoord),
readBigInt(1*sizeCoord),
))
z.C2 = z.C2.SetPoint(format.FromRTEtoTE(
readBigInt(2*sizePointCoord),
readBigInt(3*sizePointCoord),
readBigInt(2*sizeCoord),
readBigInt(3*sizeCoord),
))
return nil
}
Expand All @@ -117,11 +212,11 @@ func (z *Ciphertext) String() string {

// ToGnark returns z as the struct used by gnark,
// with the points in reduced twisted edwards format
func (z *Ciphertext) ToGnark() gelgamal.Ciphertext {
func (z *Ciphertext) ToGnark() *gelgamal.Ciphertext {
// TODO: we wouldn't need the format conversion if Point() returns the correct format
c1x, c1y := format.FromTEtoRTE(z.C1.Point())
c2x, c2y := format.FromTEtoRTE(z.C2.Point())
return gelgamal.Ciphertext{
return &gelgamal.Ciphertext{
C1: twistededwards.Point{X: c1x, Y: c1y},
C2: twistededwards.Point{X: c2x, Y: c2y},
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/rs/zerolog v1.33.0
github.com/vocdoni/arbo v0.0.0-20241216103934-e64315269b49
github.com/vocdoni/circom2gnark v1.0.1-0.20241118090531-f24bf0de0e2f
github.com/vocdoni/gnark-crypto-primitives v0.0.2-0.20241218124633-bd3f44d2cb73
github.com/vocdoni/gnark-crypto-primitives v0.0.2-0.20241220102053-ba1eee831862
go.vocdoni.io/dvote v1.10.2-0.20241024102542-c1ce6d744bc5
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ github.com/vocdoni/arbo v0.0.0-20241216103934-e64315269b49 h1:GMyepEuxLflqhdDHts
github.com/vocdoni/arbo v0.0.0-20241216103934-e64315269b49/go.mod h1:wXxPP+5vkT5t54lrKz6bCXKIyv8aRplKq8uCFb2wgy4=
github.com/vocdoni/circom2gnark v1.0.1-0.20241118090531-f24bf0de0e2f h1:iy2/GnPg5IdlkqslXUwGmqlqONDZSDnDu+1+h9LSDwM=
github.com/vocdoni/circom2gnark v1.0.1-0.20241118090531-f24bf0de0e2f/go.mod h1:A1WU0hL7rO9oZlvp82you2uCc4T3/ySi1UNW6N6hBJs=
github.com/vocdoni/gnark-crypto-primitives v0.0.2-0.20241218124633-bd3f44d2cb73 h1:pfmyx98qiu1KpKV/OnjL0UMZ2oH0CueozIOA2gxYnAk=
github.com/vocdoni/gnark-crypto-primitives v0.0.2-0.20241218124633-bd3f44d2cb73/go.mod h1:SWuNIPw2nWhnyLNWnLj2c/6U/51LCGrbx/nLM5E4Lig=
github.com/vocdoni/gnark-crypto-primitives v0.0.2-0.20241220102053-ba1eee831862 h1:Qu6Mv+jwFM0eSV/gYTZUYYImZsN4p3gTOSEPxqxPdM8=
github.com/vocdoni/gnark-crypto-primitives v0.0.2-0.20241220102053-ba1eee831862/go.mod h1:SWuNIPw2nWhnyLNWnLj2c/6U/51LCGrbx/nLM5E4Lig=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
Expand Down
Loading
Loading