From 7804ac7108efda11a79434dfcf3f0e913790ccd4 Mon Sep 17 00:00:00 2001 From: Linus Gasser Date: Wed, 28 Feb 2024 10:02:00 +0100 Subject: [PATCH] Putting ballots in separate memory This PR separates the ballots from the forms. This allows to run adding a new ballot much faster. When there are more than 100 ballots, this gets very important: adding a 1000th ballot can take 1s, a 10'000th ballot 10s. Using this PR, up to the 10'000th ballot it only takes 100ms. --- contracts/evoting/controller/action.go | 12 +- contracts/evoting/evoting.go | 16 +- contracts/evoting/json/forms.go | 103 ++++------ contracts/evoting/json/mod.go | 1 + contracts/evoting/json/suffragia.go | 97 ++++++++++ contracts/evoting/mod_test.go | 23 +-- contracts/evoting/types/election.go | 183 ++++++++++-------- contracts/evoting/types/suffragia.go | 119 ++++++++++++ go.mod | 2 +- go.sum | 4 +- integration/performance_test.go | 35 +++- integration/transaction.go | 6 +- internal/testing/fake/election.go | 21 +- proxy/election.go | 9 +- services/dkg/pedersen/handler_test.go | 10 +- services/dkg/pedersen/mod_test.go | 17 +- services/shuffle/neff/handler.go | 6 +- services/shuffle/neff/handler_test.go | 46 +++-- services/shuffle/neff/mod.go | 6 +- services/shuffle/neff/mod_test.go | 8 +- web/frontend/src/pages/form/GroupedResult.tsx | 12 +- .../src/pages/form/components/ProgressBar.tsx | 30 +-- .../src/pages/form/components/RankResult.tsx | 2 +- .../pages/form/components/SelectResult.tsx | 36 ++-- .../src/pages/form/components/TextResult.tsx | 2 +- .../form/components/utils/countResult.ts | 37 ++-- web/frontend/src/types/form.ts | 7 +- web/frontend/tests/ballot.spec.ts | 91 ++++----- .../tests/json/evoting/forms/canceled.json | 53 +---- .../tests/json/evoting/forms/closed.json | 53 +---- .../tests/json/evoting/forms/combined.json | 103 +--------- .../tests/json/evoting/forms/created.json | 48 +---- .../tests/json/evoting/forms/decrypted.json | 53 +---- .../tests/json/evoting/forms/open.json | 52 +---- .../tests/json/evoting/forms/openNoVotes.json | 60 ++++++ .../tests/json/evoting/forms/shuffled.json | 53 +---- 36 files changed, 698 insertions(+), 718 deletions(-) create mode 100644 contracts/evoting/json/suffragia.go create mode 100644 contracts/evoting/types/suffragia.go create mode 100644 web/frontend/tests/json/evoting/forms/openNoVotes.json diff --git a/contracts/evoting/controller/action.go b/contracts/evoting/controller/action.go index b0b2a69b6..c675cabe3 100644 --- a/contracts/evoting/controller/action.go +++ b/contracts/evoting/controller/action.go @@ -425,7 +425,11 @@ func (a *scenarioTestAction) Execute(ctx node.Context) error { return xerrors.Errorf(getFormErr, err) } - encryptedBallots := form.Suffragia.Ciphervotes + suff, err := form.Suffragia(serdecontext, service.GetStore()) + if err != nil { + return xerrors.Errorf(getFormErr, err) + } + encryptedBallots := suff.Ciphervotes dela.Logger.Info().Msg("Length encrypted ballots: " + strconv.Itoa(len(encryptedBallots))) dela.Logger.Info().Msgf("Ballot of user1: %s", encryptedBallots[0]) dela.Logger.Info().Msgf("Ballot of user2: %s", encryptedBallots[1]) @@ -485,7 +489,11 @@ func (a *scenarioTestAction) Execute(ctx node.Context) error { logFormStatus(form) dela.Logger.Info().Msg("Number of shuffled ballots : " + strconv.Itoa(len(form.ShuffleInstances))) - dela.Logger.Info().Msg("Number of encrypted ballots : " + strconv.Itoa(len(form.Suffragia.Ciphervotes))) + suff, err = form.Suffragia(serdecontext, service.GetStore()) + if err != nil { + return xerrors.Errorf(getFormErr, err) + } + dela.Logger.Info().Msg("Number of encrypted ballots : " + strconv.Itoa(len(suff.Ciphervotes))) // ###################################### REQUEST PUBLIC SHARES ############ diff --git a/contracts/evoting/evoting.go b/contracts/evoting/evoting.go index 481d9dd5d..7d3fd1d7b 100644 --- a/contracts/evoting/evoting.go +++ b/contracts/evoting/evoting.go @@ -91,7 +91,6 @@ func (e evotingCommand) createForm(snap store.Snapshot, step execution.Step) err Status: types.Initial, // Pubkey is set by the opening command BallotSize: tx.Configuration.MaxBallotSize(), - Suffragia: types.Suffragia{}, PubsharesUnits: units, ShuffleInstances: []types.ShuffleInstance{}, DecryptedBallots: []types.Ballot{}, @@ -231,7 +230,10 @@ func (e evotingCommand) castVote(snap store.Snapshot, step execution.Step) error len(tx.Ballot), form.ChunksPerBallot()) } - form.Suffragia.CastVote(tx.UserID, tx.Ballot) + err = form.CastVote(e.context, snap, tx.UserID, tx.Ballot) + if err != nil { + return xerrors.Errorf("couldn't cast vote: %v", err) + } formBuf, err := form.Serialize(e.context) if err != nil { @@ -243,7 +245,7 @@ func (e evotingCommand) castVote(snap store.Snapshot, step execution.Step) error return xerrors.Errorf("failed to set value: %v", err) } - PromFormBallots.WithLabelValues(form.FormID).Set(float64(len(form.Suffragia.Ciphervotes))) + PromFormBallots.WithLabelValues(form.FormID).Set(float64(form.BallotCount)) return nil } @@ -362,7 +364,11 @@ func (e evotingCommand) shuffleBallots(snap store.Snapshot, step execution.Step) var ciphervotes []types.Ciphervote if tx.Round == 0 { - ciphervotes = form.Suffragia.Ciphervotes + suff, err := form.Suffragia(e.context, snap) + if err != nil { + return xerrors.Errorf("couldn't get ballots: %v", err) + } + ciphervotes = suff.Ciphervotes } else { // get the form's last shuffled ballots lastIndex := len(form.ShuffleInstances) - 1 @@ -470,7 +476,7 @@ func (e evotingCommand) closeForm(snap store.Snapshot, step execution.Step) erro return xerrors.Errorf("the form is not open, current status: %d", form.Status) } - if len(form.Suffragia.Ciphervotes) <= 1 { + if form.BallotCount <= 1 { return xerrors.Errorf("at least two ballots are required") } diff --git a/contracts/evoting/json/forms.go b/contracts/evoting/json/forms.go index 40e99e8b0..2640cfd80 100644 --- a/contracts/evoting/json/forms.go +++ b/contracts/evoting/json/forms.go @@ -1,6 +1,7 @@ package json import ( + "encoding/hex" "encoding/json" "github.com/c4dt/d-voting/contracts/evoting/types" @@ -35,9 +36,14 @@ func (formFormat) Encode(ctx serde.Context, message serde.Message) ([]byte, erro } } - suffragia, err := encodeSuffragia(ctx, m.Suffragia) - if err != nil { - return nil, xerrors.Errorf("failed to encode suffragia: %v", err) + suffragias := make([]string, len(m.SuffragiaIDs)) + for i, suf := range m.SuffragiaIDs { + suffragias[i] = hex.EncodeToString(suf) + } + + suffragiaHashes := make([]string, len(m.SuffragiaHashes)) + for i, sufH := range m.SuffragiaHashes { + suffragiaHashes[i] = hex.EncodeToString(sufH) } shuffleInstances, err := encodeShuffleInstances(ctx, m.ShuffleInstances) @@ -62,7 +68,9 @@ func (formFormat) Encode(ctx serde.Context, message serde.Message) ([]byte, erro Status: uint16(m.Status), Pubkey: pubkey, BallotSize: m.BallotSize, - Suffragia: suffragia, + Suffragias: suffragias, + SuffragiaHashes: suffragiaHashes, + BallotCount: m.BallotCount, ShuffleInstances: shuffleInstances, ShuffleThreshold: m.ShuffleThreshold, PubsharesUnits: pubsharesUnits, @@ -100,9 +108,20 @@ func (formFormat) Decode(ctx serde.Context, data []byte) (serde.Message, error) } } - suffragia, err := decodeSuffragia(ctx, formJSON.Suffragia) - if err != nil { - return nil, xerrors.Errorf("failed to decode suffragia: %v", err) + suffragias := make([][]byte, len(formJSON.Suffragias)) + for i, suff := range formJSON.Suffragias { + suffragias[i], err = hex.DecodeString(suff) + if err != nil { + return nil, xerrors.Errorf("failed to decode suffragia-address: %v", err) + } + } + + suffragiaHashes := make([][]byte, len(formJSON.SuffragiaHashes)) + for i, suffH := range formJSON.SuffragiaHashes { + suffragiaHashes[i], err = hex.DecodeString(suffH) + if err != nil { + return nil, xerrors.Errorf("failed to decode suffragia-hash: %v", err) + } } shuffleInstances, err := decodeShuffleInstances(ctx, formJSON.ShuffleInstances) @@ -132,7 +151,9 @@ func (formFormat) Decode(ctx serde.Context, data []byte) (serde.Message, error) Status: types.Status(formJSON.Status), Pubkey: pubKey, BallotSize: formJSON.BallotSize, - Suffragia: suffragia, + SuffragiaIDs: suffragias, + SuffragiaHashes: suffragiaHashes, + BallotCount: formJSON.BallotCount, ShuffleInstances: shuffleInstances, ShuffleThreshold: formJSON.ShuffleThreshold, PubsharesUnits: pubSharesSubmissions, @@ -157,7 +178,15 @@ type FormJSON struct { // to pad smaller ballots such that all ballots cast have the same size BallotSize int - Suffragia SuffragiaJSON + // Suffragias are the hex-encoded addresses of the Suffragia storages. + Suffragias []string + + // BallotCount represents the total number of ballots cast. + BallotCount uint32 + + // SuffragiaHashes are the hex-encoded sha256-hashes of the ballots + // in every Suffragia. + SuffragiaHashes []string // ShuffleInstances is all the shuffles, along with their proof and identity // of shuffler. @@ -179,62 +208,6 @@ type FormJSON struct { RosterBuf []byte } -// SuffragiaJSON defines the JSON representation of a suffragia. -type SuffragiaJSON struct { - UserIDs []string - Ciphervotes []json.RawMessage -} - -func encodeSuffragia(ctx serde.Context, suffragia types.Suffragia) (SuffragiaJSON, error) { - ciphervotes := make([]json.RawMessage, len(suffragia.Ciphervotes)) - - for i, ciphervote := range suffragia.Ciphervotes { - buff, err := ciphervote.Serialize(ctx) - if err != nil { - return SuffragiaJSON{}, xerrors.Errorf("failed to serialize ciphervote: %v", err) - } - - ciphervotes[i] = buff - } - return SuffragiaJSON{ - UserIDs: suffragia.UserIDs, - Ciphervotes: ciphervotes, - }, nil -} - -func decodeSuffragia(ctx serde.Context, suffragiaJSON SuffragiaJSON) (types.Suffragia, error) { - var res types.Suffragia - fac := ctx.GetFactory(types.CiphervoteKey{}) - - factory, ok := fac.(types.CiphervoteFactory) - if !ok { - return res, xerrors.Errorf("invalid ciphervote factory: '%T'", fac) - } - - ciphervotes := make([]types.Ciphervote, len(suffragiaJSON.Ciphervotes)) - - for i, ciphervoteJSON := range suffragiaJSON.Ciphervotes { - msg, err := factory.Deserialize(ctx, ciphervoteJSON) - if err != nil { - return res, xerrors.Errorf("failed to deserialize ciphervote json: %v", err) - } - - ciphervote, ok := msg.(types.Ciphervote) - if !ok { - return res, xerrors.Errorf("wrong type: '%T'", msg) - } - - ciphervotes[i] = ciphervote - } - - res = types.Suffragia{ - UserIDs: suffragiaJSON.UserIDs, - Ciphervotes: ciphervotes, - } - - return res, nil -} - // ShuffleInstanceJSON defines the JSON representation of a shuffle instance type ShuffleInstanceJSON struct { // ShuffledBallots contains the list of shuffled ciphertext for this round diff --git a/contracts/evoting/json/mod.go b/contracts/evoting/json/mod.go index 900c1e5ce..c9845338a 100644 --- a/contracts/evoting/json/mod.go +++ b/contracts/evoting/json/mod.go @@ -9,6 +9,7 @@ import ( func init() { types.RegisterFormFormat(serde.FormatJSON, formFormat{}) + types.RegisterSuffragiaFormat(serde.FormatJSON, suffragiaFormat{}) types.RegisterCiphervoteFormat(serde.FormatJSON, ciphervoteFormat{}) types.RegisterTransactionFormat(serde.FormatJSON, transactionFormat{}) } diff --git a/contracts/evoting/json/suffragia.go b/contracts/evoting/json/suffragia.go new file mode 100644 index 000000000..424c83910 --- /dev/null +++ b/contracts/evoting/json/suffragia.go @@ -0,0 +1,97 @@ +package json + +import ( + "encoding/json" + + "github.com/c4dt/d-voting/contracts/evoting/types" + "go.dedis.ch/dela/serde" + "golang.org/x/xerrors" +) + +type suffragiaFormat struct{} + +func (suffragiaFormat) Encode(ctx serde.Context, msg serde.Message) ([]byte, error) { + switch m := msg.(type) { + case types.Suffragia: + sJson, err := encodeSuffragia(ctx, m) + if err != nil { + return nil, xerrors.Errorf("couldn't encode suffragia: %v", err) + } + + buff, err := ctx.Marshal(&sJson) + if err != nil { + return nil, xerrors.Errorf("failed to marshal form: %v", err) + } + + return buff, nil + default: + return nil, xerrors.Errorf("Unknown format: %T", msg) + } +} + +func (suffragiaFormat) Decode(ctx serde.Context, data []byte) (serde.Message, error) { + var sJson SuffragiaJSON + + err := ctx.Unmarshal(data, &sJson) + if err != nil { + return nil, xerrors.Errorf("failed to unmarshal form: %v", err) + } + + return decodeSuffragia(ctx, sJson) +} + +// SuffragiaJSON defines the JSON representation of a suffragia. +type SuffragiaJSON struct { + UserIDs []string + Ciphervotes []json.RawMessage +} + +func encodeSuffragia(ctx serde.Context, suffragia types.Suffragia) (SuffragiaJSON, error) { + ciphervotes := make([]json.RawMessage, len(suffragia.Ciphervotes)) + + for i, ciphervote := range suffragia.Ciphervotes { + buff, err := ciphervote.Serialize(ctx) + if err != nil { + return SuffragiaJSON{}, xerrors.Errorf("failed to serialize ciphervote: %v", err) + } + + ciphervotes[i] = buff + } + return SuffragiaJSON{ + UserIDs: suffragia.UserIDs, + Ciphervotes: ciphervotes, + }, nil +} + +func decodeSuffragia(ctx serde.Context, suffragiaJSON SuffragiaJSON) (types.Suffragia, error) { + var res types.Suffragia + fac := ctx.GetFactory(types.CiphervoteKey{}) + + factory, ok := fac.(types.CiphervoteFactory) + if !ok { + return res, xerrors.Errorf("invalid ciphervote factory: '%T'", fac) + } + + ciphervotes := make([]types.Ciphervote, len(suffragiaJSON.Ciphervotes)) + + for i, ciphervoteJSON := range suffragiaJSON.Ciphervotes { + msg, err := factory.Deserialize(ctx, ciphervoteJSON) + if err != nil { + return res, xerrors.Errorf("failed to deserialize ciphervote json: %v", err) + } + + ciphervote, ok := msg.(types.Ciphervote) + if !ok { + return res, xerrors.Errorf("wrong type: '%T'", msg) + } + + ciphervotes[i] = ciphervote + } + + res = types.Suffragia{ + UserIDs: suffragiaJSON.UserIDs, + Ciphervotes: ciphervotes, + } + + return res, nil +} diff --git a/contracts/evoting/mod_test.go b/contracts/evoting/mod_test.go index f070dab5e..435a541d8 100644 --- a/contracts/evoting/mod_test.go +++ b/contracts/evoting/mod_test.go @@ -295,11 +295,13 @@ func TestCommand_CastVote(t *testing.T) { form, ok := message.(types.Form) require.True(t, ok) - require.Len(t, form.Suffragia.Ciphervotes, 1) - require.True(t, castVote.Ballot.Equal(form.Suffragia.Ciphervotes[0])) + require.Len(t, form.BallotCount, 1) + suff, err := form.Suffragia(ctx, snap) + require.NoError(t, err) + require.True(t, castVote.Ballot.Equal(suff.Ciphervotes[0])) - require.Equal(t, castVote.UserID, form.Suffragia.UserIDs[0]) - require.Equal(t, float64(len(form.Suffragia.Ciphervotes)), testutil.ToFloat64(PromFormBallots)) + require.Equal(t, castVote.UserID, suff.UserIDs[0]) + require.Equal(t, float64(form.BallotCount), testutil.ToFloat64(PromFormBallots)) } func TestCommand_CloseForm(t *testing.T) { @@ -364,8 +366,8 @@ func TestCommand_CloseForm(t *testing.T) { err = cmd.closeForm(snap, makeStep(t, FormArg, string(data))) require.EqualError(t, err, "at least two ballots are required") - dummyForm.Suffragia.CastVote("dummyUser1", types.Ciphervote{}) - dummyForm.Suffragia.CastVote("dummyUser2", types.Ciphervote{}) + require.NoError(t, dummyForm.CastVote(ctx, snap, "dummyUser1", types.Ciphervote{})) + require.NoError(t, dummyForm.CastVote(ctx, snap, "dummyUser2", types.Ciphervote{})) formBuf, err = dummyForm.Serialize(ctx) require.NoError(t, err) @@ -697,7 +699,6 @@ func TestCommand_ShuffleBallotsFormatErrors(t *testing.T) { form.Pubkey = pubKey shuffleBallots.Round = 0 form.ShuffleInstances = make([]types.ShuffleInstance, 0) - form.Suffragia.Ciphervotes = make([]types.Ciphervote, 0) data, err = shuffleBallots.Serialize(ctx) require.NoError(t, err) @@ -713,9 +714,9 @@ func TestCommand_ShuffleBallotsFormatErrors(t *testing.T) { // > With only one shuffled ballot the shuffling can't happen - form.Suffragia.CastVote("user1", types.Ciphervote{ + require.NoError(t, form.CastVote(ctx, snap, "user1", types.Ciphervote{ types.EGPair{K: suite.Point(), C: suite.Point()}, - }) + })) data, err = shuffleBallots.Serialize(ctx) require.NoError(t, err) @@ -1118,7 +1119,6 @@ func initFormAndContract() (types.Form, Contract) { FormID: fakeFormID, Status: 0, Pubkey: nil, - Suffragia: types.Suffragia{}, ShuffleInstances: make([]types.ShuffleInstance, 0), DecryptedBallots: nil, ShuffleThreshold: 0, @@ -1156,12 +1156,13 @@ func initGoodShuffleBallot(t *testing.T, k int) (types.Form, types.ShuffleBallot shuffleBallots.Round = 0 form.ShuffleInstances = make([]types.ShuffleInstance, 0) + snap := fake.InMemorySnapshot{} for i := 0; i < k; i++ { ballot := types.Ciphervote{types.EGPair{ K: Ks[i], C: Cs[i], }} - form.Suffragia.CastVote(fmt.Sprintf("user%d", i), ballot) + form.CastVote(ctx, &snap, fmt.Sprintf("user%d", i), ballot) } // Valid Signature of shuffle diff --git a/contracts/evoting/types/election.go b/contracts/evoting/types/election.go index 52caa6c79..fda807c88 100644 --- a/contracts/evoting/types/election.go +++ b/contracts/evoting/types/election.go @@ -1,10 +1,15 @@ package types import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" "io" "go.dedis.ch/dela/core/ordering/cosipbft/authority" ctypes "go.dedis.ch/dela/core/ordering/cosipbft/types" + "go.dedis.ch/dela/core/store" "go.dedis.ch/dela/serde" "go.dedis.ch/dela/serde/registry" "go.dedis.ch/kyber/v3" @@ -21,7 +26,6 @@ type ID string type Status uint16 const ( - // DecryptedBallots = 4 // Initial is when the form has just been created Initial Status = 0 // Open is when the form is open, i.e. it fetched the public key @@ -38,6 +42,13 @@ const ( Canceled Status = 6 ) +// BallotsPerBlock to improve performance, so that (de)serializing only touches +// 100 ballots at a time. +var BallotsPerBlock = uint32(100) + +// TestCastBallots: if true, automatically fills every block with ballots. +var TestCastBallots = false + // formFormat contains the supported formats for the form. Right now // only JSON is supported. var formFormat = registry.NewSimpleRegistry() @@ -67,8 +78,19 @@ type Form struct { // to pad smaller ballots such that all ballots cast have the same size BallotSize int - // Suffragia is a map from User ID to their encrypted ballot - Suffragia Suffragia + // SuffragiaIDs holds a slice of IDs to slices of SuffragiaIDs. + // This is to optimize the time it takes to (De)serialize a Form. + SuffragiaIDs [][]byte + + // BallotCount is the total number of ballots cast, including double + // ballots. + BallotCount uint32 + + // SuffragiaHashes holds a slice of hashes to all SuffragiaIDs. + // LG: not really sure if this is needed. In case a Form has also to be + // proven to be correct outside the nodes, the hashes are definitely + // needed. + SuffragiaHashes [][]byte // ShuffleInstances is all the shuffles, along with their proof and identity // of shuffler. @@ -146,6 +168,87 @@ func (e *Form) ChunksPerBallot() int { return e.BallotSize/29 + 1 } +// CastVote stores the new vote in the memory. +func (s *Form) CastVote(ctx serde.Context, st store.Snapshot, userID string, ciphervote Ciphervote) error { + var suff Suffragia + var blockID []byte + if s.BallotCount%BallotsPerBlock == 0 { + // Need to create a random ID for storing the ballots. + // H( formID | ballotcount ) + // should be random enough, even if it's previsible. + id, err := hex.DecodeString(s.FormID) + if err != nil { + return xerrors.Errorf("couldn't decode formID: %v", err) + } + h := sha256.New() + h.Write(id) + binary.LittleEndian.PutUint32(id, s.BallotCount) + blockID = h.Sum(id[0:4])[:32] + err = st.Set(blockID, []byte{}) + if err != nil { + return xerrors.Errorf("couldn't store new ballot block: %v", err) + } + s.SuffragiaIDs = append(s.SuffragiaIDs, blockID) + s.SuffragiaHashes = append(s.SuffragiaHashes, []byte{}) + } else { + blockID = s.SuffragiaIDs[len(s.SuffragiaIDs)-1] + buf, err := st.Get(blockID) + if err != nil { + return xerrors.Errorf("couldn't get ballots block: %v", err) + } + format := suffragiaFormat.Get(ctx.GetFormat()) + ctx = serde.WithFactory(ctx, CiphervoteKey{}, CiphervoteFactory{}) + msg, err := format.Decode(ctx, buf) + if err != nil { + return xerrors.Errorf("couldn't unmarshal ballots block in cast: %v", err) + } + suff = msg.(Suffragia) + } + + suff.CastVote(userID, ciphervote) + if TestCastBallots { + for i := uint32(1); i < BallotsPerBlock; i++ { + suff.CastVote(fmt.Sprintf("%s-%d", userID, i), ciphervote) + } + s.BallotCount += BallotsPerBlock - 1 + } + buf, err := suff.Serialize(ctx) + if err != nil { + return xerrors.Errorf("couldn't marshal ballots block: %v", err) + } + err = st.Set(blockID, buf) + if err != nil { + xerrors.Errorf("couldn't set new ballots block: %v", err) + } + s.BallotCount += 1 + return nil +} + +// Suffragia returns all ballots from the storage. This should only +// be called rarely, as it might take a long time. +// It overwrites ballots cast by the same user and keeps only +// the latest ballot. +func (s *Form) Suffragia(ctx serde.Context, rd store.Readable) (Suffragia, error) { + var suff Suffragia + for _, id := range s.SuffragiaIDs { + buf, err := rd.Get(id) + if err != nil { + return suff, xerrors.Errorf("couldn't get ballot block: %v", err) + } + format := suffragiaFormat.Get(ctx.GetFormat()) + ctx = serde.WithFactory(ctx, CiphervoteKey{}, CiphervoteFactory{}) + msg, err := format.Decode(ctx, buf) + if err != nil { + return suff, xerrors.Errorf("couldn't unmarshal ballots block in cast: %v", err) + } + suffTmp := msg.(Suffragia) + for i, uid := range suffTmp.UserIDs { + suff.CastVote(uid, suffTmp.Ciphervotes[i]) + } + } + return suff, nil +} + // RandomVector is a slice of kyber.Scalar (encoded) which is used to prove // and verify the proof of a shuffle type RandomVector [][]byte @@ -239,80 +342,6 @@ func (c *Configuration) IsValid() bool { return true } -type Suffragia struct { - UserIDs []string - Ciphervotes []Ciphervote -} - -// CastVote adds a new vote and its associated user or updates a user's vote. -func (s *Suffragia) CastVote(userID string, ciphervote Ciphervote) { - for i, u := range s.UserIDs { - if u == userID { - s.Ciphervotes[i] = ciphervote - return - } - } - - s.UserIDs = append(s.UserIDs, userID) - s.Ciphervotes = append(s.Ciphervotes, ciphervote.Copy()) -} - -// CiphervotesFromPairs transforms two parallel lists of EGPoints to a list of -// Ciphervotes. -func CiphervotesFromPairs(X, Y [][]kyber.Point) ([]Ciphervote, error) { - if len(X) != len(Y) { - return nil, xerrors.Errorf("X and Y must have same length: %d != %d", - len(X), len(Y)) - } - - if len(X) == 0 { - return nil, xerrors.Errorf("ElGamal pairs are empty") - } - - NQ := len(X) // sequence size - k := len(X[0]) // number of votes - res := make([]Ciphervote, k) - - for i := 0; i < k; i++ { - x := make([]kyber.Point, NQ) - y := make([]kyber.Point, NQ) - - for j := 0; j < NQ; j++ { - x[j] = X[j][i] - y[j] = Y[j][i] - } - - ciphervote, err := ciphervoteFromPairs(x, y) - if err != nil { - return nil, xerrors.Errorf("failed to init from ElGamal pairs: %v", err) - } - - res[i] = ciphervote - } - - return res, nil -} - -// ciphervoteFromPairs transforms two parallel lists of EGPoints to a list of -// ElGamal pairs. -func ciphervoteFromPairs(ks []kyber.Point, cs []kyber.Point) (Ciphervote, error) { - if len(ks) != len(cs) { - return Ciphervote{}, xerrors.Errorf("ks and cs must have same length: %d != %d", - len(ks), len(cs)) - } - - res := make(Ciphervote, len(ks)) - - for i := range ks { - res[i] = EGPair{ - K: ks[i], - C: cs[i], - } - } - - return res, nil -} - // Pubshare represents a public share. type Pubshare kyber.Point diff --git a/contracts/evoting/types/suffragia.go b/contracts/evoting/types/suffragia.go new file mode 100644 index 000000000..c8a507440 --- /dev/null +++ b/contracts/evoting/types/suffragia.go @@ -0,0 +1,119 @@ +package types + +import ( + "crypto/sha256" + + "go.dedis.ch/dela/serde" + "go.dedis.ch/dela/serde/registry" + "go.dedis.ch/kyber/v3" + "golang.org/x/xerrors" +) + +// suffragiaFormat contains the supported formats for the form. Right now +// only JSON is supported. +var suffragiaFormat = registry.NewSimpleRegistry() + +// RegisterSuffragiaFormat registers the engine for the provided format +func RegisterSuffragiaFormat(format serde.Format, engine serde.FormatEngine) { + suffragiaFormat.Register(format, engine) +} + +type Suffragia struct { + UserIDs []string + Ciphervotes []Ciphervote +} + +// Serialize implements the serde.Message +func (s Suffragia) Serialize(ctx serde.Context) ([]byte, error) { + format := suffragiaFormat.Get(ctx.GetFormat()) + + data, err := format.Encode(ctx, s) + if err != nil { + return nil, xerrors.Errorf("failed to encode form: %v", err) + } + + return data, nil +} + +// CastVote adds a new vote and its associated user or updates a user's vote. +func (s *Suffragia) CastVote(userID string, ciphervote Ciphervote) { + for i, u := range s.UserIDs { + if u == userID { + s.Ciphervotes[i] = ciphervote + return + } + } + + s.UserIDs = append(s.UserIDs, userID) + s.Ciphervotes = append(s.Ciphervotes, ciphervote.Copy()) +} + +// Hash returns the hash of this list of ballots. +func (s *Suffragia) Hash(ctx serde.Context) ([]byte, error) { + h := sha256.New() + for i, u := range s.UserIDs { + h.Write([]byte(u)) + buf, err := s.Ciphervotes[i].Serialize(ctx) + if err != nil { + return nil, xerrors.Errorf("couldn't serialize ciphervote: %v", err) + } + h.Write(buf) + } + return h.Sum(nil), nil +} + +// CiphervotesFromPairs transforms two parallel lists of EGPoints to a list of +// Ciphervotes. +func CiphervotesFromPairs(X, Y [][]kyber.Point) ([]Ciphervote, error) { + if len(X) != len(Y) { + return nil, xerrors.Errorf("X and Y must have same length: %d != %d", + len(X), len(Y)) + } + + if len(X) == 0 { + return nil, xerrors.Errorf("ElGamal pairs are empty") + } + + NQ := len(X) // sequence size + k := len(X[0]) // number of votes + res := make([]Ciphervote, k) + + for i := 0; i < k; i++ { + x := make([]kyber.Point, NQ) + y := make([]kyber.Point, NQ) + + for j := 0; j < NQ; j++ { + x[j] = X[j][i] + y[j] = Y[j][i] + } + + ciphervote, err := ciphervoteFromPairs(x, y) + if err != nil { + return nil, xerrors.Errorf("failed to init from ElGamal pairs: %v", err) + } + + res[i] = ciphervote + } + + return res, nil +} + +// ciphervoteFromPairs transforms two parallel lists of EGPoints to a list of +// ElGamal pairs. +func ciphervoteFromPairs(ks []kyber.Point, cs []kyber.Point) (Ciphervote, error) { + if len(ks) != len(cs) { + return Ciphervote{}, xerrors.Errorf("ks and cs must have same length: %d != %d", + len(ks), len(cs)) + } + + res := make(Ciphervote, len(ks)) + + for i := range ks { + res[i] = EGPair{ + K: ks[i], + C: cs[i], + } + } + + return res, nil +} diff --git a/go.mod b/go.mod index ad9c4034c..51ba6fcfd 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( ) replace ( - go.dedis.ch/dela => github.com/c4dt/dela v0.0.0-20231208150034-8c94bd32e18a + go.dedis.ch/dela => github.com/c4dt/dela v0.0.0-20240125143006-d9bfc5ad2f59 go.dedis.ch/dela-apps => github.com/c4dt/dela-apps v0.0.0-20231121155105-f3a8a6f4b3b8 ) diff --git a/go.sum b/go.sum index ae3aa5eab..b718b9980 100644 --- a/go.sum +++ b/go.sum @@ -399,8 +399,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/c4dt/dela v0.0.0-20231208150034-8c94bd32e18a h1:MU7JHdKZssVjSeQttHPPa4fUMwqEO/xBQpdQ15H/AUo= -github.com/c4dt/dela v0.0.0-20231208150034-8c94bd32e18a/go.mod h1:Oh/WK8JMO0POQg7nR3u436u+HwsZwPqPzDWAasgmFAU= +github.com/c4dt/dela v0.0.0-20240125143006-d9bfc5ad2f59 h1:mYFBc395DeUmimRIgB/Xk+AGXfuDcYWADM/qvocBkW4= +github.com/c4dt/dela v0.0.0-20240125143006-d9bfc5ad2f59/go.mod h1:Oh/WK8JMO0POQg7nR3u436u+HwsZwPqPzDWAasgmFAU= github.com/c4dt/dela-apps v0.0.0-20231121155105-f3a8a6f4b3b8 h1:ELho4tnVG7lM3c2I42Q5IGNyuk/2FQCterA2zVQGvms= github.com/c4dt/dela-apps v0.0.0-20231121155105-f3a8a6f4b3b8/go.mod h1:Rky9YH7R02zSOirr2BhhdJs/9VH4+rxqkQxHU3UTQRA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/integration/performance_test.go b/integration/performance_test.go index 7a4233e55..220418e8b 100644 --- a/integration/performance_test.go +++ b/integration/performance_test.go @@ -21,11 +21,32 @@ import ( "golang.org/x/xerrors" ) -// Check the shuffled votes versus the cast votes and a few nodes +// Check the shuffled votes versus the cast votes and a few nodes. +// One transaction contains one vote. func BenchmarkIntegration_CustomVotesScenario(b *testing.B) { + customVotesScenario(b, false) +} + +// Check the shuffled votes versus the cast votes and a few nodes. +// One transasction contains many votes +func BenchmarkIntegration_CustomVotesScenario_StuffBallots(b *testing.B) { + customVotesScenario(b, true) +} + +func customVotesScenario(b *testing.B, stuffing bool) { numNodes := 3 numVotes := 200 + transactions := 200 numChunksPerBallot := 3 + if stuffing { + // Fill every block of ballots with bogus votes to test performance. + types.TestCastBallots = true + defer func() { + types.TestCastBallots = false + }() + numVotes = 10000 + transactions = numVotes / int(types.BallotsPerBlock) + } adminID := "I am an admin" @@ -53,6 +74,8 @@ func BenchmarkIntegration_CustomVotesScenario(b *testing.B) { require.NoError(b, err) } + fmt.Println("Creating form") + // ##### CREATE FORM ##### formID, err := createFormNChunks(m, types.Title{En: "Three votes form", Fr: "", De: ""}, adminID, numChunksPerBallot) require.NoError(b, err) @@ -74,7 +97,7 @@ func BenchmarkIntegration_CustomVotesScenario(b *testing.B) { require.NoError(b, err) b.ResetTimer() - castedVotes, err := castVotesNChunks(m, actor, form, numVotes) + castedVotes, err := castVotesNChunks(m, actor, form, transactions) require.NoError(b, err) durationCasting := b.Elapsed() b.Logf("Casting %d votes took %v", numVotes, durationCasting) @@ -146,11 +169,13 @@ func BenchmarkIntegration_CustomVotesScenario(b *testing.B) { b.Logf("Submitting shares took: %v", durationPubShares) b.Logf("Decryption took: %v", durationDecrypt) - require.Len(b, form.DecryptedBallots, len(castedVotes)) + require.Len(b, form.DecryptedBallots, len(castedVotes)*int(types.BallotsPerBlock)) - for _, ballot := range form.DecryptedBallots { + // There will be a lot of supplementary ballots, but at least the ones that were + // cast by the test should be present. + for _, casted := range castedVotes { ok := false - for _, casted := range castedVotes { + for _, ballot := range form.DecryptedBallots { if ballot.Equal(casted) { ok = true break diff --git a/integration/transaction.go b/integration/transaction.go index 50549c269..b8e388e05 100644 --- a/integration/transaction.go +++ b/integration/transaction.go @@ -15,6 +15,7 @@ import ( "github.com/c4dt/d-voting/contracts/evoting" "github.com/c4dt/d-voting/proxy/txnmanager" "github.com/stretchr/testify/require" + "go.dedis.ch/dela" "go.dedis.ch/dela/contracts/access" "go.dedis.ch/dela/core/execution/native" "go.dedis.ch/dela/core/ordering" @@ -103,6 +104,7 @@ func pollTxnInclusion(maxPollCount int, interPollWait time.Duration, proxyAddr, // For integrationTest func (m txManager) addAndWait(args ...txn.Arg) ([]byte, error) { for i := 0; i < m.retry; i++ { + dela.Logger.Info().Msgf("Adding and waiting for tx to succeed: %d", i) sentTxn, err := m.m.Make(args...) if err != nil { return nil, xerrors.Errorf("failed to Make: %v", err) @@ -119,8 +121,6 @@ func (m txManager) addAndWait(args ...txn.Arg) ([]byte, error) { continue } - time.Sleep(time.Second) - sentTxnID := sentTxn.GetID() accepted := isAccepted(events, sentTxnID) @@ -134,6 +134,8 @@ func (m txManager) addAndWait(args ...txn.Arg) ([]byte, error) { } cancel() + + time.Sleep(time.Millisecond * (1 << i)) } return nil, xerrors.Errorf("transaction not included after timeout: %v", args) diff --git a/internal/testing/fake/election.go b/internal/testing/fake/election.go index 5481a3f58..7b53d4764 100644 --- a/internal/testing/fake/election.go +++ b/internal/testing/fake/election.go @@ -4,14 +4,17 @@ import ( "strconv" "github.com/c4dt/d-voting/contracts/evoting/types" + "go.dedis.ch/dela/core/store" + "go.dedis.ch/dela/serde" "go.dedis.ch/kyber/v3" "go.dedis.ch/kyber/v3/suites" "go.dedis.ch/kyber/v3/util/random" + "golang.org/x/xerrors" ) var suite = suites.MustFind("Ed25519") -func NewForm(formID string) types.Form { +func NewForm(ctx serde.Context, snapshot store.Snapshot, formID string) (types.Form, error) { k := 3 Ks, Cs, pubKey := NewKCPointsMarshalled(k) @@ -23,12 +26,9 @@ func NewForm(formID string) types.Form { De: "", }, }, - FormID: formID, - Status: types.Closed, - Pubkey: pubKey, - Suffragia: types.Suffragia{ - Ciphervotes: []types.Ciphervote{}, - }, + FormID: formID, + Status: types.Closed, + Pubkey: pubKey, ShuffleInstances: []types.ShuffleInstance{}, DecryptedBallots: nil, ShuffleThreshold: 1, @@ -39,10 +39,13 @@ func NewForm(formID string) types.Form { K: Ks[i], C: Cs[i], } - form.Suffragia.CastVote("dummyUser"+strconv.Itoa(i), types.Ciphervote{ballot}) + err := form.CastVote(ctx, snapshot, "dummyUser"+strconv.Itoa(i), types.Ciphervote{ballot}) + if err != nil { + return form, xerrors.Errorf("couldn't cast vote: %v", err) + } } - return form + return form, nil } func NewKCPointsMarshalled(k int) ([]kyber.Point, []kyber.Point, kyber.Point) { diff --git a/proxy/election.go b/proxy/election.go index 8743ae439..017982747 100644 --- a/proxy/election.go +++ b/proxy/election.go @@ -432,6 +432,13 @@ func (h *form) Form(w http.ResponseWriter, r *http.Request) { roster = append(roster, iter.GetNext().String()) } + suff, err := form.Suffragia(h.context, h.orderingSvc.GetStore()) + if err != nil { + http.Error(w, "couldn't get ballots: "+err.Error(), + http.StatusInternalServerError) + return + } + response := ptypes.GetFormResponse{ FormID: string(form.FormID), Configuration: form.Configuration, @@ -441,7 +448,7 @@ func (h *form) Form(w http.ResponseWriter, r *http.Request) { Roster: roster, ChunksPerBallot: form.ChunksPerBallot(), BallotSize: form.BallotSize, - Voters: form.Suffragia.UserIDs, + Voters: suff.UserIDs, } txnmanager.SendResponse(w, response) diff --git a/services/dkg/pedersen/handler_test.go b/services/dkg/pedersen/handler_test.go index fe17edba9..52627d0ff 100644 --- a/services/dkg/pedersen/handler_test.go +++ b/services/dkg/pedersen/handler_test.go @@ -64,7 +64,6 @@ func TestHandler_Stream(t *testing.T) { Status: formTypes.ShuffledBallots, Pubkey: nil, BallotSize: 0, - Suffragia: formTypes.Suffragia{}, ShuffleInstances: make([]formTypes.ShuffleInstance, 1), ShuffleThreshold: 0, PubsharesUnits: units, @@ -295,7 +294,6 @@ func TestHandler_HandlerDecryptRequest(t *testing.T) { Status: formTypes.ShuffledBallots, Pubkey: nil, BallotSize: 0, - Suffragia: formTypes.Suffragia{}, ShuffleInstances: make([]formTypes.ShuffleInstance, 1), ShuffleThreshold: 1, PubsharesUnits: units, @@ -356,16 +354,18 @@ func TestHandler_HandlerDecryptRequest(t *testing.T) { Ks, Cs, _ := fakeKCPoints(k, message, suite.Point()) + snap := fake.NewSnapshot() for i := 0; i < k; i++ { ballot := formTypes.Ciphervote{formTypes.EGPair{ K: Ks[i], C: Cs[i], }} - form.Suffragia.CastVote("dummyUser"+strconv.Itoa(i), ballot) + form.CastVote(service.Context, snap, "dummyUser"+strconv.Itoa(i), ballot) } - shuffledBallots := form.Suffragia.Ciphervotes - shuffleInstance := formTypes.ShuffleInstance{ShuffledBallots: shuffledBallots} + shuffledBallots, err := form.Suffragia(service.Context, snap) + require.NoError(t, err) + shuffleInstance := formTypes.ShuffleInstance{ShuffledBallots: shuffledBallots.Ciphervotes} form.ShuffleInstances = append(form.ShuffleInstances, shuffleInstance) Forms[formIDHex] = form diff --git a/services/dkg/pedersen/mod_test.go b/services/dkg/pedersen/mod_test.go index 5375ad82d..c89d2a1dc 100644 --- a/services/dkg/pedersen/mod_test.go +++ b/services/dkg/pedersen/mod_test.go @@ -198,10 +198,13 @@ func TestPedersen_SyncDB(t *testing.T) { formID2 := "deadbeef52" // Start some forms + snap := fake.NewSnapshot() context := fake.NewContext() - form1 := fake.NewForm(formID1) + form1, err := fake.NewForm(context, snap, formID1) + require.NoError(t, err) service := fake.NewService(formID1, form1, context) - form2 := fake.NewForm(formID2) + form2, err := fake.NewForm(context, snap, formID2) + require.NoError(t, err) service.Forms[formID2] = form2 pool := fake.Pool{} manager := fake.Manager{} @@ -498,7 +501,9 @@ func TestPedersen_Scenario(t *testing.T) { roster := authority.FromAuthority(fake.NewAuthorityFromMino(fake.NewSigner, minos...)) - form := fake.NewForm(formID) + st := fake.InMemorySnapshot{} + form, err := fake.NewForm(serdecontext, &st, formID) + require.NoError(t, err) form.Roster = roster service := fake.NewService(formID, form, serdecontext) @@ -537,10 +542,12 @@ func TestPedersen_Scenario(t *testing.T) { K: Ks[i], C: Cs[i], }} - form.Suffragia.CastVote("dummyUser"+strconv.Itoa(i), ballot) + require.NoError(t, form.CastVote(serdecontext, &st, "dummyUser"+strconv.Itoa(i), ballot)) } - shuffledBallots := form.Suffragia.Ciphervotes + suff, err := form.Suffragia(serdecontext, &st) + require.NoError(t, err) + shuffledBallots := suff.Ciphervotes shuffleInstance := etypes.ShuffleInstance{ShuffledBallots: shuffledBallots} form.ShuffleInstances = append(form.ShuffleInstances, shuffleInstance) diff --git a/services/shuffle/neff/handler.go b/services/shuffle/neff/handler.go index 5d5ba7dce..4722d9608 100644 --- a/services/shuffle/neff/handler.go +++ b/services/shuffle/neff/handler.go @@ -260,7 +260,11 @@ func (h *Handler) getShuffledBallots(form *etypes.Form) ([]etypes.Ciphervote, var ciphervotes []etypes.Ciphervote if round == 0 { - ciphervotes = form.Suffragia.Ciphervotes + suff, err := form.Suffragia(h.context, h.service.GetStore()) + if err != nil { + return nil, nil, xerrors.Errorf("couldn't get ballots: %v", err) + } + ciphervotes = suff.Ciphervotes } else { ciphervotes = form.ShuffleInstances[round-1].ShuffledBallots } diff --git a/services/shuffle/neff/handler_test.go b/services/shuffle/neff/handler_test.go index ec68adb27..e884ae9a7 100644 --- a/services/shuffle/neff/handler_test.go +++ b/services/shuffle/neff/handler_test.go @@ -5,6 +5,8 @@ import ( "strconv" "testing" + "go.dedis.ch/dela/core/store" + "go.dedis.ch/dela/serde" "go.dedis.ch/dela/serde/json" "github.com/c4dt/d-voting/services/shuffle/neff/types" @@ -96,7 +98,6 @@ func TestHandler_StartShuffle(t *testing.T) { FormID: dummyID, Status: 0, Pubkey: nil, - Suffragia: etypes.Suffragia{}, ShuffleInstances: []etypes.ShuffleInstance{}, DecryptedBallots: nil, ShuffleThreshold: 1, @@ -115,28 +116,30 @@ func TestHandler_StartShuffle(t *testing.T) { // Wrong formatted ballots: form.Status = etypes.Closed - deleteUserFromSuffragia := func(suff *etypes.Suffragia, userID string) bool { - for i, u := range suff.UserIDs { - if u == userID { - suff.UserIDs = append(suff.UserIDs[:i], suff.UserIDs[i+1:]...) - suff.Ciphervotes = append(suff.Ciphervotes[:i], suff.Ciphervotes[i+1:]...) - return true - } - } - - return false - } - - deleteUserFromSuffragia(&form.Suffragia, "fakeUser") + // TODO: think how to re-enable this test + //deleteUserFromSuffragia := func(suff *etypes.Suffragia, userID string) bool { + // for i, u := range suff.UserIDs { + // if u == userID { + // suff.UserIDs = append(suff.UserIDs[:i], suff.UserIDs[i+1:]...) + // suff.Ciphervotes = append(suff.Ciphervotes[:i], suff.Ciphervotes[i+1:]...) + // return true + // } + // } + // + // return false + //} + // + //deleteUserFromSuffragia(&form.Suffragia, "fakeUser") // Valid Ballots, bad form.PubKey + snap := fake.NewSnapshot() for i := 0; i < k; i++ { ballot := etypes.Ciphervote{etypes.EGPair{ K: Ks[i], C: Cs[i], }, } - form.Suffragia.CastVote("dummyUser"+strconv.Itoa(i), ballot) + form.CastVote(service.Context, snap, "dummyUser"+strconv.Itoa(i), ballot) } service = updateService(form, dummyID) @@ -204,7 +207,9 @@ func TestHandler_StartShuffle(t *testing.T) { require.NoError(t, err) // Shuffle already started: - shuffledBallots := append([]etypes.Ciphervote{}, form.Suffragia.Ciphervotes...) + ciphervotes, err := form.Suffragia(service.Context, snap) + require.NoError(t, err) + shuffledBallots := append([]etypes.Ciphervote{}, ciphervotes.Ciphervotes...) form.ShuffleInstances = append(form.ShuffleInstances, etypes.ShuffleInstance{ShuffledBallots: shuffledBallots}) @@ -239,7 +244,9 @@ func updateService(form etypes.Form, dummyID string) fake.Service { func initValidHandler(dummyID string) Handler { handler := Handler{} - form := initFakeForm(dummyID) + ctx := json.NewContext() + snap := fake.NewSnapshot() + form := initFakeForm(ctx, snap, dummyID) Forms := make(map[string]etypes.Form) Forms[dummyID] = form @@ -263,14 +270,13 @@ func initValidHandler(dummyID string) Handler { return handler } -func initFakeForm(formID string) etypes.Form { +func initFakeForm(ctx serde.Context, snap store.Snapshot, formID string) etypes.Form { k := 3 KsMarshalled, CsMarshalled, pubKey := fakeKCPoints(k) form := etypes.Form{ FormID: formID, Status: etypes.Closed, Pubkey: pubKey, - Suffragia: etypes.Suffragia{}, ShuffleInstances: []etypes.ShuffleInstance{}, DecryptedBallots: nil, ShuffleThreshold: 1, @@ -284,7 +290,7 @@ func initFakeForm(formID string) etypes.Form { C: CsMarshalled[i], }, } - form.Suffragia.CastVote("dummyUser"+strconv.Itoa(i), ballot) + form.CastVote(ctx, snap, "dummyUser"+strconv.Itoa(i), ballot) } return form } diff --git a/services/shuffle/neff/mod.go b/services/shuffle/neff/mod.go index 58bf61235..fe23b4a1d 100644 --- a/services/shuffle/neff/mod.go +++ b/services/shuffle/neff/mod.go @@ -157,7 +157,7 @@ func (a *Actor) waitAndCheckShuffling(formID string, rosterLen int) error { var form etypes.Form var err error - for i := 0; i < rosterLen*10; i++ { + for i := 0; ; i++ { form, err = getForm(a.formFac, a.context, formID, a.service) if err != nil { return xerrors.Errorf("failed to get form: %v", err) @@ -175,6 +175,10 @@ func (a *Actor) waitAndCheckShuffling(formID string, rosterLen int) error { dela.Logger.Info().Msgf("waiting a while before checking form: %d", i) sleepTime := rosterLen / 2 time.Sleep(time.Duration(sleepTime) * time.Second) + if i >= form.ShuffleThreshold*((int)(form.BallotCount)/16+1) { + break + } + dela.Logger.Info().Msgf("WaitingRounds is : %d", form.ShuffleThreshold*((int)(form.BallotCount)/10+1)) } return xerrors.Errorf("threshold of shuffling not reached: %d < %d", diff --git a/services/shuffle/neff/mod_test.go b/services/shuffle/neff/mod_test.go index 1d7cef573..e8e854eb4 100644 --- a/services/shuffle/neff/mod_test.go +++ b/services/shuffle/neff/mod_test.go @@ -52,10 +52,14 @@ func TestNeffShuffle_Shuffle(t *testing.T) { rosterLen := 2 roster := authority.FromAuthority(fake.NewAuthority(rosterLen, fake.NewSigner)) - form := fake.NewForm(formID) + st := fake.InMemorySnapshot{} + form, err := fake.NewForm(serdecontext, &st, formID) + require.NoError(t, err) form.Roster = roster - shuffledBallots := append([]etypes.Ciphervote{}, form.Suffragia.Ciphervotes...) + suff, err := form.Suffragia(serdecontext, &st) + require.NoError(t, err) + shuffledBallots := append([]etypes.Ciphervote{}, suff.Ciphervotes...) form.ShuffleInstances = append(form.ShuffleInstances, etypes.ShuffleInstance{ShuffledBallots: shuffledBallots}) form.ShuffleThreshold = 1 diff --git a/web/frontend/src/pages/form/GroupedResult.tsx b/web/frontend/src/pages/form/GroupedResult.tsx index 5608a54c2..bb077bfd4 100644 --- a/web/frontend/src/pages/form/GroupedResult.tsx +++ b/web/frontend/src/pages/form/GroupedResult.tsx @@ -124,15 +124,9 @@ const GroupedResult: FC = ({ rankResult, selectResult, textR const select = element as SelectQuestion; if (selectResult.has(id)) { - res = countSelectResult(selectResult.get(id)) - .map(([, totalCount], index) => { - return { - Candidate: select.Choices[index], - TotalCount: totalCount, - NumberOfBallots: selectResult.get(id).length, // number of combined ballots for this election - }; - }) - .sort((x, y) => y.TotalCount - x.TotalCount); + res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => { + return { Candidate: select.Choices[index], Percentage: `${percent}%` }; + }); dataToDownload.push({ Title: element.Title.En, Results: res }); } break; diff --git a/web/frontend/src/pages/form/components/ProgressBar.tsx b/web/frontend/src/pages/form/components/ProgressBar.tsx index 0d1e6f94f..4fc8e1790 100644 --- a/web/frontend/src/pages/form/components/ProgressBar.tsx +++ b/web/frontend/src/pages/form/components/ProgressBar.tsx @@ -5,14 +5,7 @@ type ProgressBarProps = { children: string; }; -type SelectProgressBarProps = { - percent: string; - totalCount: number; - numberOfBallots: number; - isBest: boolean; -}; - -export const ProgressBar: FC = ({ isBest, children }) => { +const ProgressBar: FC = ({ isBest, children }) => { return (
@@ -28,23 +21,4 @@ export const ProgressBar: FC = ({ isBest, children }) => { ); }; -export const SelectProgressBar: FC = ({ - percent, - totalCount, - numberOfBallots, - isBest, -}) => { - return ( -
-
-
- -
{`${totalCount}/${numberOfBallots}`}
-
-
- ); -}; +export default ProgressBar; diff --git a/web/frontend/src/pages/form/components/RankResult.tsx b/web/frontend/src/pages/form/components/RankResult.tsx index a3a07d168..c00b594d4 100644 --- a/web/frontend/src/pages/form/components/RankResult.tsx +++ b/web/frontend/src/pages/form/components/RankResult.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; import { RankQuestion } from 'types/configuration'; -import { ProgressBar } from './ProgressBar'; +import ProgressBar from './ProgressBar'; import { countRankResult } from './utils/countResult'; import { default as i18n } from 'i18next'; diff --git a/web/frontend/src/pages/form/components/SelectResult.tsx b/web/frontend/src/pages/form/components/SelectResult.tsx index 1dc49ffc2..8d2204e1c 100644 --- a/web/frontend/src/pages/form/components/SelectResult.tsx +++ b/web/frontend/src/pages/form/components/SelectResult.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; import { SelectQuestion } from 'types/configuration'; -import { SelectProgressBar } from './ProgressBar'; +import ProgressBar from './ProgressBar'; import { countSelectResult } from './utils/countResult'; import { default as i18n } from 'i18next'; @@ -11,33 +11,23 @@ type SelectResultProps = { // Display the results of a select question. const SelectResult: FC = ({ select, selectResult }) => { - const sortedResults = countSelectResult(selectResult) - .map((result, index) => { - const tempResult: [string, number, number] = [...result, index]; - return tempResult; - }) - .sort((x, y) => y[1] - x[1]); - const maxCount = sortedResults[0][1]; + const { resultsInPercent, maxIndices } = countSelectResult(selectResult); const displayResults = () => { - return sortedResults.map(([percent, totalCount, origIndex], index) => { + return resultsInPercent.map((percent, index) => { + const isBest = maxIndices.includes(index); + return (
- { - (select.ChoicesMap.has(i18n.language) - ? select.ChoicesMap.get(i18n.language) - : select.ChoicesMap.get('en'))[origIndex] - } + {i18n.language === 'en' && select.ChoicesMap.get('en')[index]} + {i18n.language === 'fr' && select.ChoicesMap.get('fr')[index]} + {i18n.language === 'de' && select.ChoicesMap.get('de')[index]} :
- + {percent}
); }); @@ -66,11 +56,9 @@ export const IndividualSelectResult: FC = ({ select, selectRe
{displayChoices(result, index)}
- { - (select.ChoicesMap.has(i18n.language) - ? select.ChoicesMap.get(i18n.language) - : select.ChoicesMap.get('en'))[index] - } + {i18n.language === 'en' && select.ChoicesMap.get('en')[index]} + {i18n.language === 'fr' && select.ChoicesMap.get('fr')[index]} + {i18n.language === 'de' && select.ChoicesMap.get('de')[index]}
diff --git a/web/frontend/src/pages/form/components/TextResult.tsx b/web/frontend/src/pages/form/components/TextResult.tsx index 059f4d127..4a010331e 100644 --- a/web/frontend/src/pages/form/components/TextResult.tsx +++ b/web/frontend/src/pages/form/components/TextResult.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; import { TextQuestion } from 'types/configuration'; -import { ProgressBar } from './ProgressBar'; +import ProgressBar from './ProgressBar'; import { countTextResult } from './utils/countResult'; import { default as i18n } from 'i18next'; diff --git a/web/frontend/src/pages/form/components/utils/countResult.ts b/web/frontend/src/pages/form/components/utils/countResult.ts index f2803bd65..c0bf7537b 100644 --- a/web/frontend/src/pages/form/components/utils/countResult.ts +++ b/web/frontend/src/pages/form/components/utils/countResult.ts @@ -42,20 +42,31 @@ const countRankResult = (rankResult: number[][], rank: RankQuestion) => { // percentage of the total number of votes and which candidate(s) in the // select.Choices has the most votes const countSelectResult = (selectResult: number[][]) => { - const results: [string, number][] = []; - - selectResult - .reduce( - (tally, currBallot) => tally.map((currCount, index) => currCount + currBallot[index]), - new Array(selectResult[0].length).fill(0) - ) - .forEach((totalCount) => { - results.push([ - (Math.round((totalCount / selectResult.length) * 100 * 100) / 100).toFixed(2).toString(), - totalCount, - ]); + const resultsInPercent: string[] = []; + const maxIndices: number[] = []; + let max = 0; + + const results = selectResult.reduce((a, b) => { + return a.map((value, index) => { + const current = value + b[index]; + + if (current >= max) { + max = current; + } + return current; }); - return results; + }, new Array(selectResult[0].length).fill(0)); + + results.forEach((count, index) => { + if (count === max) { + maxIndices.push(index); + } + + const percentage = (count / selectResult.length) * 100; + const roundedPercentage = (Math.round(percentage * 100) / 100).toFixed(2); + resultsInPercent.push(roundedPercentage); + }); + return { resultsInPercent, maxIndices }; }; // Count the number of votes for each candidate and returns the counts and the diff --git a/web/frontend/src/types/form.ts b/web/frontend/src/types/form.ts index 71d5be18a..6ea5a80dd 100644 --- a/web/frontend/src/types/form.ts +++ b/web/frontend/src/types/form.ts @@ -80,12 +80,7 @@ type TextResults = Map; interface DownloadedResults { Title: string; - Results?: { - Candidate: string; - Percent?: string; - TotalCount?: number; - NumberOfBallots?: number; - }[]; + Results?: { Candidate: string; Percentage: string }[]; } interface BallotResults { BallotNumber: number; diff --git a/web/frontend/tests/ballot.spec.ts b/web/frontend/tests/ballot.spec.ts index 47bc5f84f..256678de5 100644 --- a/web/frontend/tests/ballot.spec.ts +++ b/web/frontend/tests/ballot.spec.ts @@ -60,61 +60,45 @@ test('Assert ballot is displayed properly', async ({ page }) => { // TODO integrate localisation i18n.changeLanguage('en'); // force 'en' for this test await expect(content.locator('xpath=./div/div[3]/h3')).toContainText(Form.Configuration.Title.En); - for (const [index, scaffold] of Form.Configuration.Scaffold.entries()) { - await expect(content.locator(`xpath=./div/div[3]/div/div[${index + 1}]/h3`)).toContainText( - scaffold.Title.En - ); - const select = scaffold.Selects.at(0); - await expect( - content.locator(`xpath=./div/div[3]/div/div[${index + 1}]/div/div/div/div[1]/div[1]/h3`) - ).toContainText(select.Title.En); - await expect( - page.getByText(i18n.t('selectBetween', { minSelect: select.MinN, maxSelect: select.MaxN })) - ).toBeVisible(); - for (const choice of select.Choices.map((x) => JSON.parse(x))) { - await expect(page.getByRole('checkbox', { name: choice.en })).toBeVisible(); - } + const scaffold = Form.Configuration.Scaffold.at(0); + await expect(content.locator('xpath=./div/div[3]/div/div/h3')).toContainText(scaffold.Title.En); + const select = scaffold.Selects.at(0); + await expect( + content.locator('xpath=./div/div[3]/div/div/div/div/div/div[1]/div/h3') + ).toContainText(select.Title.En); + await expect( + page.getByText(i18n.t('selectBetween', { minSelect: select.MinN, maxSelect: select.MaxN })) + ).toBeVisible(); + for (const choice of select.Choices.map((x) => JSON.parse(x))) { + await expect(page.getByRole('checkbox', { name: choice.en })).toBeVisible(); } i18n.changeLanguage(); // unset language for the other tests }); test('Assert minimum/maximum number of choices are handled correctly', async ({ page }) => { - const content = await page.getByTestId('content'); const castVoteButton = await page.getByRole('button', { name: i18n.t('castVote') }); - for (const [index, scaffold] of Form.Configuration.Scaffold.entries()) { - const select = scaffold.Selects.at(0); - await test.step( - `Assert minimum number of choices (${select.MinN}) are handled correctly`, - async () => { - await castVoteButton.click(); - await expect( - content.locator(`xpath=./div/div[3]/div/div[${index + 1}]`).getByText( - i18n.t('minSelectError', { - min: select.MinN, - singularPlural: i18n.t('singularAnswer'), - }) - ) - ).toBeVisible(); - } - ); - await test.step( - `Assert maximum number of choices (${select.MaxN}) are handled correctly`, - async () => { - for (const choice of select.Choices.map((x) => JSON.parse(x))) { - await page.getByRole('checkbox', { name: choice.en }).setChecked(true); - } - await castVoteButton.click(); - await expect( - content.locator(`xpath=./div/div[3]/div/div[${index + 1}]`).getByText( - i18n.t('maxSelectError', { - max: select.MaxN, - singularPlural: i18n.t('singularAnswer'), - }) - ) - ).toBeVisible(); + const select = Form.Configuration.Scaffold.at(0).Selects.at(0); + await test.step( + `Assert minimum number of choices (${select.MinN}) are handled correctly`, + async () => { + await castVoteButton.click(); + await expect( + page.getByText( + i18n.t('minSelectError', { min: select.MinN, singularPlural: i18n.t('singularAnswer') }) + ) + ).toBeVisible(); + } + ); + await test.step( + `Assert maximum number of choices (${select.MaxN}) are handled correctly`, + async () => { + for (const choice of select.Choices.map((x) => JSON.parse(x))) { + await page.getByRole('checkbox', { name: choice.en }).setChecked(true); } - ); - } + await castVoteButton.click(); + await expect(page.getByText(i18n.t('maxSelectError', { max: select.MaxN }))).toBeVisible(); + } + ); }); test('Assert that correct number of choices are accepted', async ({ page, baseURL }) => { @@ -125,11 +109,9 @@ test('Assert that correct number of choices are accepted', async ({ page, baseUR request.url() === `${baseURL}/api/evoting/forms/${FORMID}/vote` && request.method() === 'POST' && body.UserID === null && - body.Ballot.length === 2 && + body.Ballot.length === 1 && body.Ballot.at(0).K.length === 32 && - body.Ballot.at(0).C.length === 32 && - body.Ballot.at(1).K.length === 32 && - body.Ballot.at(1).C.length === 32 + body.Ballot.at(0).C.length === 32 ); }); await page @@ -137,10 +119,5 @@ test('Assert that correct number of choices are accepted', async ({ page, baseUR name: JSON.parse(Form.Configuration.Scaffold.at(0).Selects.at(0).Choices.at(0)).en, }) .setChecked(true); - await page - .getByRole('checkbox', { - name: JSON.parse(Form.Configuration.Scaffold.at(1).Selects.at(0).Choices.at(0)).en, - }) - .setChecked(true); await page.getByRole('button', { name: i18n.t('castVote') }).click(); }); diff --git a/web/frontend/tests/json/evoting/forms/canceled.json b/web/frontend/tests/json/evoting/forms/canceled.json index 7c65a04c6..908e8885c 100644 --- a/web/frontend/tests/json/evoting/forms/canceled.json +++ b/web/frontend/tests/json/evoting/forms/canceled.json @@ -3,8 +3,8 @@ "Configuration": { "Title": { "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" + "Fr": "", + "De": "" }, "Scaffold": [ { @@ -42,43 +42,6 @@ ], "Ranks": [], "Texts": [] - }, - { - "ID": "1NqhDffw", - "Title": { - "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" - }, - "Order": [ - "riJFjw0q" - ], - "Subjects": [], - "Selects": [ - { - "ID": "riJFjw0q", - "Title": { - "En": "CMYK", - "Fr": "CMJN", - "De": "CMYK" - }, - "MaxN": 3, - "MinN": 1, - "Choices": [ - "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", - "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", - "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", - "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}" - ], - "Hint": { - "En": "", - "Fr": "", - "De": "" - } - } - ], - "Ranks": [], - "Texts": [] } ] }, @@ -91,13 +54,11 @@ "grpc://dela-worker-2:2000", "grpc://dela-worker-3:2000" ], - "ChunksPerBallot": 2, - "BallotSize": 48, + "ChunksPerBallot": 1, + "BallotSize": 23, "Voters": [ - "brcLwsgGcU", - "JThb56JvGF", - "zXcZU5QNwn", - "bWxTfeq4t5" + "oUItDdhhEE", + "WZyqP1gssL", + "K7ZNvumBVc" ] } - diff --git a/web/frontend/tests/json/evoting/forms/closed.json b/web/frontend/tests/json/evoting/forms/closed.json index 7c65a04c6..908e8885c 100644 --- a/web/frontend/tests/json/evoting/forms/closed.json +++ b/web/frontend/tests/json/evoting/forms/closed.json @@ -3,8 +3,8 @@ "Configuration": { "Title": { "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" + "Fr": "", + "De": "" }, "Scaffold": [ { @@ -42,43 +42,6 @@ ], "Ranks": [], "Texts": [] - }, - { - "ID": "1NqhDffw", - "Title": { - "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" - }, - "Order": [ - "riJFjw0q" - ], - "Subjects": [], - "Selects": [ - { - "ID": "riJFjw0q", - "Title": { - "En": "CMYK", - "Fr": "CMJN", - "De": "CMYK" - }, - "MaxN": 3, - "MinN": 1, - "Choices": [ - "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", - "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", - "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", - "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}" - ], - "Hint": { - "En": "", - "Fr": "", - "De": "" - } - } - ], - "Ranks": [], - "Texts": [] } ] }, @@ -91,13 +54,11 @@ "grpc://dela-worker-2:2000", "grpc://dela-worker-3:2000" ], - "ChunksPerBallot": 2, - "BallotSize": 48, + "ChunksPerBallot": 1, + "BallotSize": 23, "Voters": [ - "brcLwsgGcU", - "JThb56JvGF", - "zXcZU5QNwn", - "bWxTfeq4t5" + "oUItDdhhEE", + "WZyqP1gssL", + "K7ZNvumBVc" ] } - diff --git a/web/frontend/tests/json/evoting/forms/combined.json b/web/frontend/tests/json/evoting/forms/combined.json index d3edf82b1..e02139e73 100644 --- a/web/frontend/tests/json/evoting/forms/combined.json +++ b/web/frontend/tests/json/evoting/forms/combined.json @@ -3,8 +3,8 @@ "Configuration": { "Title": { "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" + "Fr": "", + "De": "" }, "Scaffold": [ { @@ -42,43 +42,6 @@ ], "Ranks": [], "Texts": [] - }, - { - "ID": "1NqhDffw", - "Title": { - "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" - }, - "Order": [ - "riJFjw0q" - ], - "Subjects": [], - "Selects": [ - { - "ID": "riJFjw0q", - "Title": { - "En": "CMYK", - "Fr": "CMJN", - "De": "CMYK" - }, - "MaxN": 3, - "MinN": 1, - "Choices": [ - "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", - "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", - "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", - "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}" - ], - "Hint": { - "En": "", - "Fr": "", - "De": "" - } - } - ], - "Ranks": [], - "Texts": [] } ] }, @@ -87,20 +50,13 @@ "Result": [ { "SelectResultIDs": [ - "CLgNiLbC", - "riJFjw0q" + "CLgNiLbC" ], "SelectResult": [ [ true, false, false - ], - [ - true, - true, - false, - false ] ], "RankResultIDs": [], @@ -110,43 +66,13 @@ }, { "SelectResultIDs": [ - "CLgNiLbC", - "riJFjw0q" + "CLgNiLbC" ], "SelectResult": [ [ false, - true, - true - ], - [ - true, - false, - false, - true - ] - ], - "RankResultIDs": [], - "RankResult": [], - "TextResultIDs": [], - "TextResult": [] - }, - { - "SelectResultIDs": [ - "CLgNiLbC", - "riJFjw0q" - ], - "SelectResult": [ - [ false, - true, true - ], - [ - false, - true, - false, - false ] ], "RankResultIDs": [], @@ -156,17 +82,10 @@ }, { "SelectResultIDs": [ - "CLgNiLbC", - "riJFjw0q" + "CLgNiLbC" ], "SelectResult": [ [ - false, - false, - true - ], - [ - false, false, true, false @@ -184,13 +103,11 @@ "grpc://dela-worker-2:2000", "grpc://dela-worker-3:2000" ], - "ChunksPerBallot": 2, - "BallotSize": 48, + "ChunksPerBallot": 1, + "BallotSize": 23, "Voters": [ - "brcLwsgGcU", - "JThb56JvGF", - "zXcZU5QNwn", - "bWxTfeq4t5" + "oUItDdhhEE", + "WZyqP1gssL", + "K7ZNvumBVc" ] } - diff --git a/web/frontend/tests/json/evoting/forms/created.json b/web/frontend/tests/json/evoting/forms/created.json index 36c765007..37b9366a5 100644 --- a/web/frontend/tests/json/evoting/forms/created.json +++ b/web/frontend/tests/json/evoting/forms/created.json @@ -3,8 +3,8 @@ "Configuration": { "Title": { "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" + "Fr": "", + "De": "" }, "Scaffold": [ { @@ -42,43 +42,6 @@ ], "Ranks": [], "Texts": [] - }, - { - "ID": "1NqhDffw", - "Title": { - "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" - }, - "Order": [ - "riJFjw0q" - ], - "Subjects": [], - "Selects": [ - { - "ID": "riJFjw0q", - "Title": { - "En": "CMYK", - "Fr": "CMJN", - "De": "CMYK" - }, - "MaxN": 3, - "MinN": 1, - "Choices": [ - "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", - "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", - "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", - "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}" - ], - "Hint": { - "En": "", - "Fr": "", - "De": "" - } - } - ], - "Ranks": [], - "Texts": [] } ] }, @@ -91,8 +54,7 @@ "grpc://dela-worker-2:2000", "grpc://dela-worker-3:2000" ], - "ChunksPerBallot": 2, - "BallotSize": 48, - "Voters": [] + "ChunksPerBallot": 1, + "BallotSize": 23, + "Voters": null } - diff --git a/web/frontend/tests/json/evoting/forms/decrypted.json b/web/frontend/tests/json/evoting/forms/decrypted.json index f38b100c4..35ab94a45 100644 --- a/web/frontend/tests/json/evoting/forms/decrypted.json +++ b/web/frontend/tests/json/evoting/forms/decrypted.json @@ -3,8 +3,8 @@ "Configuration": { "Title": { "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" + "Fr": "", + "De": "" }, "Scaffold": [ { @@ -42,43 +42,6 @@ ], "Ranks": [], "Texts": [] - }, - { - "ID": "1NqhDffw", - "Title": { - "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" - }, - "Order": [ - "riJFjw0q" - ], - "Subjects": [], - "Selects": [ - { - "ID": "riJFjw0q", - "Title": { - "En": "CMYK", - "Fr": "CMJN", - "De": "CMYK" - }, - "MaxN": 3, - "MinN": 1, - "Choices": [ - "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", - "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", - "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", - "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}" - ], - "Hint": { - "En": "", - "Fr": "", - "De": "" - } - } - ], - "Ranks": [], - "Texts": [] } ] }, @@ -91,13 +54,11 @@ "grpc://dela-worker-2:2000", "grpc://dela-worker-3:2000" ], - "ChunksPerBallot": 2, - "BallotSize": 48, + "ChunksPerBallot": 1, + "BallotSize": 23, "Voters": [ - "brcLwsgGcU", - "JThb56JvGF", - "zXcZU5QNwn", - "bWxTfeq4t5" + "oUItDdhhEE", + "WZyqP1gssL", + "K7ZNvumBVc" ] } - diff --git a/web/frontend/tests/json/evoting/forms/open.json b/web/frontend/tests/json/evoting/forms/open.json index b1caae8be..5dc33b69d 100644 --- a/web/frontend/tests/json/evoting/forms/open.json +++ b/web/frontend/tests/json/evoting/forms/open.json @@ -3,8 +3,8 @@ "Configuration": { "Title": { "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" + "Fr": "", + "De": "" }, "Scaffold": [ { @@ -42,43 +42,6 @@ ], "Ranks": [], "Texts": [] - }, - { - "ID": "1NqhDffw", - "Title": { - "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" - }, - "Order": [ - "riJFjw0q" - ], - "Subjects": [], - "Selects": [ - { - "ID": "riJFjw0q", - "Title": { - "En": "CMYK", - "Fr": "CMJN", - "De": "CMYK" - }, - "MaxN": 3, - "MinN": 1, - "Choices": [ - "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", - "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", - "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", - "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}" - ], - "Hint": { - "En": "", - "Fr": "", - "De": "" - } - } - ], - "Ranks": [], - "Texts": [] } ] }, @@ -91,12 +54,11 @@ "grpc://dela-worker-2:2000", "grpc://dela-worker-3:2000" ], - "ChunksPerBallot": 2, - "BallotSize": 48, + "ChunksPerBallot": 1, + "BallotSize": 23, "Voters": [ - "brcLwsgGcU", - "JThb56JvGF", - "zXcZU5QNwn", - "bWxTfeq4t5" + "oUItDdhhEE", + "WZyqP1gssL", + "K7ZNvumBVc" ] } diff --git a/web/frontend/tests/json/evoting/forms/openNoVotes.json b/web/frontend/tests/json/evoting/forms/openNoVotes.json new file mode 100644 index 000000000..52b8427e2 --- /dev/null +++ b/web/frontend/tests/json/evoting/forms/openNoVotes.json @@ -0,0 +1,60 @@ +{ + "FormID": "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4", + "Configuration": { + "Title": { + "En": "Colours", + "Fr": "", + "De": "" + }, + "Scaffold": [ + { + "ID": "yOakwFnR", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "CLgNiLbC" + ], + "Subjects": [], + "Selects": [ + { + "ID": "CLgNiLbC", + "Title": { + "En": "RGB", + "Fr": "RGB", + "De": "RGB" + }, + "MaxN": 2, + "MinN": 1, + "Choices": [ + "{\"en\":\"Red\",\"fr\":\"Rouge\",\"de\":\"Rot\"}", + "{\"en\":\"Green\",\"fr\":\"Vert\",\"de\":\"GrĂ¼n\"}", + "{\"en\":\"Blue\",\"fr\":\"Bleu\",\"de\":\"Blau\"}" + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + } + ] + }, + "Status": 1, + "Pubkey": "612fdc867be1a5faccf16e5aed946880005840ee04aaa382fe181a2b46dc9a24", + "Result": [], + "Roster": [ + "grpc://dela-worker-0:2000", + "grpc://dela-worker-1:2000", + "grpc://dela-worker-2:2000", + "grpc://dela-worker-3:2000" + ], + "ChunksPerBallot": 1, + "BallotSize": 23, + "Voters": null +} diff --git a/web/frontend/tests/json/evoting/forms/shuffled.json b/web/frontend/tests/json/evoting/forms/shuffled.json index 695d4db36..68cf96c69 100644 --- a/web/frontend/tests/json/evoting/forms/shuffled.json +++ b/web/frontend/tests/json/evoting/forms/shuffled.json @@ -3,8 +3,8 @@ "Configuration": { "Title": { "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" + "Fr": "", + "De": "" }, "Scaffold": [ { @@ -42,43 +42,6 @@ ], "Ranks": [], "Texts": [] - }, - { - "ID": "1NqhDffw", - "Title": { - "En": "Colours", - "Fr": "Couleurs", - "De": "Farben" - }, - "Order": [ - "riJFjw0q" - ], - "Subjects": [], - "Selects": [ - { - "ID": "riJFjw0q", - "Title": { - "En": "CMYK", - "Fr": "CMJN", - "De": "CMYK" - }, - "MaxN": 3, - "MinN": 1, - "Choices": [ - "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", - "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", - "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", - "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}" - ], - "Hint": { - "En": "", - "Fr": "", - "De": "" - } - } - ], - "Ranks": [], - "Texts": [] } ] }, @@ -91,13 +54,11 @@ "grpc://dela-worker-2:2000", "grpc://dela-worker-3:2000" ], - "ChunksPerBallot": 2, - "BallotSize": 48, + "ChunksPerBallot": 1, + "BallotSize": 23, "Voters": [ - "brcLwsgGcU", - "JThb56JvGF", - "zXcZU5QNwn", - "bWxTfeq4t5" + "oUItDdhhEE", + "WZyqP1gssL", + "K7ZNvumBVc" ] } -