diff --git a/contracts/evoting/types/ballots.go b/contracts/evoting/types/ballots.go index 2d7d10932..770af3698 100644 --- a/contracts/evoting/types/ballots.go +++ b/contracts/evoting/types/ballots.go @@ -333,30 +333,47 @@ func (s *Subject) MaxEncodedSize() int { //TODO : optimise by computing max size according to number of choices and maxN for _, rank := range s.Ranks { - size += len(rank.GetID() + "::") - size += len(rank.ID) - // at most 3 bytes (128) + ',' per choice + size += len(rank.GetID()) + // the ID arrives Base64-encoded, but rank.ID is decoded + // we need the size of the Base64-encoded string + size += len(base64.StdEncoding.EncodeToString([]byte(rank.ID))) + + // ':' separators ('id:id:choice') + size += 2 + + // 4 bytes per choice (choice and separating comma/newline) size += len(rank.Choices) * 4 } for _, selection := range s.Selects { - size += len(selection.GetID() + "::") - size += len(selection.ID) - // 1 bytes (0/1) + ',' per choice + size += len(selection.GetID()) + // the ID arrives Base64-encoded, but selection.ID is decoded + // we need the size of the Base64-encoded string + size += len(base64.StdEncoding.EncodeToString([]byte(selection.ID))) + + // ':' separators ('id:id:choice') + size += 2 + + // 2 bytes per choice (0/1 and separating comma/newline) size += len(selection.Choices) * 2 } for _, text := range s.Texts { - size += len(text.GetID() + "::") - size += len(text.ID) + size += len(text.GetID()) + // the ID arrives Base64-encoded, but text.ID is decoded + // we need the size of the Base64-encoded string + size += len(base64.StdEncoding.EncodeToString([]byte(text.ID))) + + // ':' separators ('id:id:choice') + size += 2 - // at most 4 bytes per character + ',' per answer + // 4 bytes per character and 1 byte for separating comma/newline maxTextPerAnswer := 4*int(text.MaxLength) + 1 size += maxTextPerAnswer*int(text.MaxN) + int(math.Max(float64(len(text.Choices)-int(text.MaxN)), 0)) } - // Last line has 2 '\n' + // additional '\n' on last line if size != 0 { size++ } diff --git a/contracts/evoting/types/ballots_test.go b/contracts/evoting/types/ballots_test.go index 9b292f739..f47bddfda 100644 --- a/contracts/evoting/types/ballots_test.go +++ b/contracts/evoting/types/ballots_test.go @@ -314,13 +314,13 @@ func TestSubject_MaxEncodedSize(t *testing.T) { }}, Selects: []Select{{ - ID: encodedQuestionID(1), + ID: decodedQuestionID(1), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 3, MinN: 0, Choices: make([]Choice, 3), }, { - ID: encodedQuestionID(2), + ID: decodedQuestionID(2), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 5, MinN: 0, @@ -328,7 +328,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { }}, Ranks: []Rank{{ - ID: encodedQuestionID(3), + ID: decodedQuestionID(3), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 4, MinN: 0, @@ -336,7 +336,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { }}, Texts: []Text{{ - ID: encodedQuestionID(4), + ID: decodedQuestionID(4), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 2, MinN: 0, @@ -344,7 +344,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { Regex: "", Choices: make([]Choice, 2), }, { - ID: encodedQuestionID(5), + ID: decodedQuestionID(5), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 1, MinN: 0, diff --git a/docs/ballot_encoding.md b/docs/ballot_encoding.md index 84b105122..6e1f69e47 100644 --- a/docs/ballot_encoding.md +++ b/docs/ballot_encoding.md @@ -13,7 +13,7 @@ The answers to questions are encoded in the following way, with one question per TYPE = "select"|"text"|"rank" SEP = ":" -ID = 3 bytes, encoded in base64 +ID = 8 bytes UUID encoded in base64 = 12 bytes ANSWERS = [","]* ANSWER = || SELECT_ANSWER = "0"|"1" @@ -39,11 +39,11 @@ For the following questions : A possible encoding of an answer would be (by string concatenation): ``` -"select:3fb2:0,0,0,1,0\n" + +"select:base64(D0Da4H6o):0,0,0,1,0\n" + -"rank:19c7:0,1,2\n" + +"rank:base64(19c7cd13):0,1,2\n" + -"text:cd13:base64("Noémien"),base64("Pierluca")\n" +"text:base64(wSfBs25a):base64("Noémien"),base64("Pierluca")\n" ``` ## Size of the ballot @@ -53,15 +53,15 @@ voting process, it is important that all encrypted ballots have the same size. T the form has an attribute called "BallotSize" which is the size that all ballots should have before they're encrypted. Smaller ballots should therefore be padded in order to reach this size. To denote the end of the ballot and the start of the padding, -we use an empty line (\n\n). For a ballot size of 117, our ballot from the previous example +we use an empty line (\n\n). For a ballot size of 144, our ballot from the previous example would then become: ``` -"select:3fb2:0,0,0,1,0\n" + +"select:base64(D0Da4H6o):0,0,0,1,0\n" + -"rank:19c7:0,1,2\n" + +"rank:base64(19c7cd13):0,1,2\n" + -"text:cd13:base64("Noémien"),base64("Pierluca")\n\n" + +"text:base64(wSfBs25a):base64("Noémien"),base64("Pierluca")\n\n" + "ndtTx5uxmvnllH1T7NgLORuUWbN" ``` @@ -70,4 +70,4 @@ would then become: The encoded ballot must then be divided into chunks of 29 or less bytes since the maximum size supported by the kyber library for the encryption is of 29 bytes. -For the previous example we would then have 5 chunks, the first 4 would contain 29 bytes, while the last chunk would contain a single byte. +For the previous example we would then have 5 chunks, the first 4 would contain 29 bytes, while the last chunk would contain 28 bytes. diff --git a/web/frontend/src/pages/ballot/components/VoteEncode.tsx b/web/frontend/src/pages/ballot/components/VoteEncode.tsx index 85cd60a61..3a7f9c30b 100644 --- a/web/frontend/src/pages/ballot/components/VoteEncode.tsx +++ b/web/frontend/src/pages/ballot/components/VoteEncode.tsx @@ -38,7 +38,7 @@ export function voteEncode( encodedBallot += '\n'; - const encodedBallotSize = Buffer.byteLength(encodedBallot); + let encodedBallotSize = Buffer.byteLength(encodedBallot); // add padding if necessary until encodedBallot.length == ballotSize if (encodedBallotSize < ballotSize) { @@ -46,9 +46,18 @@ export function voteEncode( encodedBallot += padding(); } + encodedBallotSize = Buffer.byteLength(encodedBallot); + const chunkSize = 29; + const maxEncodedBallotSize = chunkSize * chunksPerBallot; const ballotChunks: string[] = []; + if (encodedBallotSize > maxEncodedBallotSize) { + throw new Error( + `actual encoded ballot size ${encodedBallotSize} is bigger than maximum ballot size ${maxEncodedBallotSize}` + ); + } + // divide into chunksPerBallot chunks, where 1 character === 1 byte for (let i = 0; i < chunksPerBallot; i += 1) { const start = i * chunkSize;