Skip to content

Commit d1644db

Browse files
authored
Bug #783 Chaum Pederson Bug (#784)
* get ballots with individual queries and with retry logic * more logging * More UI status updates when loading ballots * fix linting in mongo-init * Be more defensive while deserializing ballots * Better UI on decryption error * fix linting issues
1 parent 8285a1a commit d1644db

File tree

9 files changed

+115
-38
lines changed

9 files changed

+115
-38
lines changed

src/electionguard_db/mongo-init.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ db.createCollection("ballot_uploads");
55
db.createCollection("decryptions");
66
db.createCollection("db_deltas", { capped: true, size: 100000 });
77
db.db_deltas.insert({ type: "init" });
8-
db.ballot_uploads.createIndex({election_id: 1});
9-
db.ballot_uploads.createIndex({election_id: 1, object_id: 1});
10-
db.decryptions.createIndex({decryption_name: 1});
11-
db.decryptions.createIndex({election_id: 1});
12-
db.decryptions.createIndex({completed_at: 1});
13-
db.key_ceremonies.createIndex({completed_at: 1});
14-
db.key_ceremonies.createIndex({key_ceremony_name: 1});
8+
db.ballot_uploads.createIndex({ election_id: 1 });
9+
db.ballot_uploads.createIndex({ election_id: 1, object_id: 1 });
10+
db.decryptions.createIndex({ decryption_name: 1 });
11+
db.decryptions.createIndex({ election_id: 1 });
12+
db.decryptions.createIndex({ completed_at: 1 });
13+
db.key_ceremonies.createIndex({ completed_at: 1 });
14+
db.key_ceremonies.createIndex({ key_ceremony_name: 1 });

src/electionguard_gui/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
KeyCeremonyStateService,
9494
MODE_KEY,
9595
PORT_KEY,
96+
RetryException,
9697
ServiceBase,
9798
VersionService,
9899
announce_guardians,
@@ -140,7 +141,6 @@
140141
service_base,
141142
status_descriptions,
142143
to_ballot_share_raw,
143-
update_decrypt_status,
144144
verification_to_dict,
145145
version_service,
146146
)
@@ -194,6 +194,7 @@
194194
"MODE_KEY",
195195
"MainApp",
196196
"PORT_KEY",
197+
"RetryException",
197198
"ServiceBase",
198199
"UploadBallotsComponent",
199200
"VersionService",
@@ -273,7 +274,6 @@
273274
"start",
274275
"status_descriptions",
275276
"to_ballot_share_raw",
276-
"update_decrypt_status",
277277
"update_upload_status",
278278
"upload_ballots_component",
279279
"utc_to_str",

src/electionguard_gui/components/export_election_record_component.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ def export_election_record(
5959
manifest = election.get_manifest()
6060
constants = get_constants()
6161
encryption_devices = election.get_encryption_devices()
62-
submitted_ballots = self._ballot_upload_service.get_ballots(db, election.id)
62+
submitted_ballots = self._ballot_upload_service.get_ballots(
63+
db, election.id, lambda x: None
64+
)
6365
plaintext_tally = decryption.get_plaintext_tally()
6466
spoiled_ballots = decryption.get_plaintext_spoiled_ballots()
6567
lagrange_coefficients = decryption.get_lagrange_coefficients()

src/electionguard_gui/services/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
)
2525
from electionguard_gui.services.ballot_upload_service import (
2626
BallotUploadService,
27+
RetryException,
2728
)
2829
from electionguard_gui.services.configuration_service import (
2930
ConfigurationService,
@@ -58,7 +59,6 @@
5859
decryption_s2_announce_service,
5960
decryption_stage_base,
6061
get_tally,
61-
update_decrypt_status,
6262
)
6363
from electionguard_gui.services.directory_service import (
6464
DOCKER_MOUNT_DIR,
@@ -149,6 +149,7 @@
149149
"KeyCeremonyStateService",
150150
"MODE_KEY",
151151
"PORT_KEY",
152+
"RetryException",
152153
"ServiceBase",
153154
"VersionService",
154155
"announce_guardians",
@@ -196,7 +197,6 @@
196197
"service_base",
197198
"status_descriptions",
198199
"to_ballot_share_raw",
199-
"update_decrypt_status",
200200
"verification_to_dict",
201201
"version_service",
202202
]

src/electionguard_gui/services/ballot_upload_service.py

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from datetime import datetime
2+
from time import sleep
3+
from typing import Callable
24
from pymongo.database import Database
35
from electionguard.ballot import SubmittedBallot
46
from electionguard.serialize import from_raw
@@ -76,19 +78,81 @@ def any_ballot_exists(self, db: Database, election_id: str, object_id: str) -> b
7678
> 0
7779
)
7880

79-
def get_ballots(self, db: Database, election_id: str) -> list[SubmittedBallot]:
81+
def get_ballots(
82+
self, db: Database, election_id: str, report_status: Callable[[str], None]
83+
) -> list[SubmittedBallot]:
8084
self._log.debug(f"getting ballots for {election_id}")
81-
ballot_uploads = db.ballot_uploads.find(
82-
{"election_id": election_id, "file_contents": {"$exists": True}}
85+
ballot_uploads = list(
86+
db.ballot_uploads.find(
87+
{"election_id": election_id, "file_contents": {"$exists": True}},
88+
projection={"_id": 1, "file_contents": 0},
89+
)
8390
)
8491
ballots = []
85-
for ballot_obj in ballot_uploads:
86-
ballot_str = ballot_obj["file_contents"]
92+
total_ballots = len(ballot_uploads)
93+
ballot_num = 1
94+
for ballot_id_obj in ballot_uploads:
95+
ballot_id = ballot_id_obj["_id"]
96+
report_status(f"Loading ballot {ballot_num}/{total_ballots}")
8797
try:
88-
ballot = from_raw(SubmittedBallot, ballot_str)
98+
ballot = self.get_submitted_ballot_with_retry(db, ballot_id)
8999
ballots.append(ballot)
90100
# pylint: disable=broad-except
91101
except Exception as e:
92-
self._log.error("error deserializing ballot: {ballot_obj}", e)
102+
self._log.error(
103+
f"Error deserializing ballot {ballot_id}. "
104+
+ "Skipping ballot, but this may cause Chaum Pederson errors later.",
105+
e,
106+
)
93107
# per RC 8/15/22 log errors and continue processing even if it makes numbers incorrect
108+
ballot_num += 1
94109
return ballots
110+
111+
def get_submitted_ballot_with_retry(
112+
self, db: Database, ballot_upload_id: str
113+
) -> SubmittedBallot:
114+
retry_num = 0
115+
max_retries = 3
116+
while retry_num < max_retries:
117+
try:
118+
return self.get_submitted_ballot(db, ballot_upload_id)
119+
except RetryException:
120+
self._log.warn(
121+
f"retrying get ballot {ballot_upload_id} in {retry_num + 1} second(s). Retry #{retry_num + 1}"
122+
)
123+
# wait 1 second before retrying in case network was slow
124+
sleep(retry_num + 1)
125+
retry_num += 1
126+
raise Exception(
127+
f"Failed to get ballot {ballot_upload_id} after {max_retries} retries"
128+
)
129+
130+
def get_submitted_ballot(
131+
self, db: Database, ballot_upload_id: str
132+
) -> SubmittedBallot:
133+
self._log.trace(f"getting submitted ballot {ballot_upload_id}")
134+
ballot_obj = None
135+
try:
136+
ballot_obj = db.ballot_uploads.find_one(
137+
{"_id": ballot_upload_id}, projection={"file_contents": 1}
138+
)
139+
except Exception as e:
140+
self._log.error(f"mongo error getting ballot {ballot_upload_id}", e)
141+
raise RetryException from e
142+
if ballot_obj is None:
143+
raise Exception(f"Ballot {ballot_upload_id} not found")
144+
ballot_str = ballot_obj["file_contents"]
145+
# if ballot_str ends with a }
146+
if not ballot_str[-1] == "}":
147+
self._log.warn(f"ballot {ballot_upload_id} is missing a closing bracket")
148+
raise RetryException
149+
try:
150+
ballot = from_raw(SubmittedBallot, ballot_str)
151+
except Exception as e:
152+
self._log.error(f"error deserializing ballot {ballot_upload_id}", e)
153+
raise RetryException from e
154+
return ballot
155+
156+
157+
class RetryException(Exception):
158+
"""An exception to notify the caller to retry"""

src/electionguard_gui/services/decryption_stages/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
)
88
from electionguard_gui.services.decryption_stages.decryption_s2_announce_service import (
99
DecryptionS2AnnounceService,
10-
update_decrypt_status,
1110
)
1211
from electionguard_gui.services.decryption_stages.decryption_stage_base import (
1312
DecryptionStageBase,
@@ -22,5 +21,4 @@
2221
"decryption_s2_announce_service",
2322
"decryption_stage_base",
2423
"get_tally",
25-
"update_decrypt_status",
2624
]

src/electionguard_gui/services/decryption_stages/decryption_s1_join_service.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class DecryptionS1JoinService(DecryptionStageBase):
1313
"""Responsible for the 1st stage during a decryption were guardians join the decryption"""
1414

1515
def run(self, db: Database, decryption: DecryptionDto) -> None:
16-
update_decrypt_status("Starting tally")
16+
_update_decrypt_status("Starting tally")
1717
current_user_id = self._auth_service.get_required_user_id()
1818
self._log.info(f"S1: {current_user_id} decrypting {decryption.decryption_id}")
1919
election = self._election_service.get(db, decryption.election_id)
@@ -23,14 +23,19 @@ def run(self, db: Database, decryption: DecryptionDto) -> None:
2323
guardian = self._guardian_service.load_guardian_from_decryption(
2424
current_user_id, decryption
2525
)
26-
ballots = self._ballot_upload_service.get_ballots(db, election.id)
27-
update_decrypt_status("Calculating tally")
26+
ballots = self._ballot_upload_service.get_ballots(
27+
db, election.id, _update_decrypt_status
28+
)
29+
_update_decrypt_status("Calculating tally")
30+
self._log.debug(f"getting tally for {len(ballots)} ballots")
2831
ciphertext_tally = get_tally(manifest, context, ballots, False)
32+
self._log.debug("computing tally share")
2933
decryption_share = guardian.compute_tally_share(ciphertext_tally, context)
3034
if decryption_share is None:
3135
raise Exception("No decryption_shares found")
3236

33-
update_decrypt_status("Calculating spoiled ballots")
37+
_update_decrypt_status("Calculating spoiled ballots")
38+
self._log.debug("decrypting spoiled ballots")
3439
spoiled_ballots = [
3540
ballot for ballot in ballots if ballot.state == BallotBoxState.SPOILED
3641
]
@@ -39,7 +44,7 @@ def run(self, db: Database, decryption: DecryptionDto) -> None:
3944
raise Exception("No ballot shares found")
4045
guardian_key = guardian.share_key()
4146

42-
update_decrypt_status("Finalizing tally")
47+
_update_decrypt_status("Finalizing tally")
4348
self._decryption_service.append_guardian_joined(
4449
db,
4550
decryption.decryption_id,
@@ -48,9 +53,10 @@ def run(self, db: Database, decryption: DecryptionDto) -> None:
4853
ballot_shares,
4954
guardian_key,
5055
)
56+
self._log.debug("Completed tally")
5157
self._decryption_service.notify_changed(db, decryption.decryption_id)
5258

5359

54-
def update_decrypt_status(status: str) -> None:
60+
def _update_decrypt_status(status: str) -> None:
5561
# pylint: disable=no-member
5662
eel.update_decrypt_status(status)

src/electionguard_gui/services/decryption_stages/decryption_s2_announce_service.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def should_run(self, db: Database, decryption: DecryptionDto) -> bool:
2020
return is_admin and all_guardians_joined and not is_completed
2121

2222
def run(self, db: Database, decryption: DecryptionDto) -> None:
23-
update_decrypt_status("Starting tally")
23+
_update_decrypt_status("Starting tally")
2424
self._log.info(f"S2: Announcing decryption {decryption.decryption_id}")
2525
election = self._election_service.get(db, decryption.election_id)
2626
context = election.get_context()
@@ -33,7 +33,7 @@ def run(self, db: Database, decryption: DecryptionDto) -> None:
3333
share_count = len(decryption_shares)
3434
current_share = 1
3535
for decryption_share_dict in decryption_shares:
36-
update_decrypt_status(f"Calculating share {current_share}/{share_count}")
36+
_update_decrypt_status(f"Calculating share {current_share}/{share_count}")
3737
self._log.debug(f"announcing {decryption_share_dict.guardian_id}")
3838
guardian_sequence_number = election.get_guardian_sequence_order(
3939
decryption_share_dict.guardian_id
@@ -48,12 +48,15 @@ def run(self, db: Database, decryption: DecryptionDto) -> None:
4848
)
4949
current_share += 1
5050

51-
update_decrypt_status("Decrypting spoiled ballots")
5251
manifest = election.get_manifest()
53-
ballots = self._ballot_upload_service.get_ballots(db, election.id)
52+
ballots = self._ballot_upload_service.get_ballots(
53+
db, election.id, _update_decrypt_status
54+
)
5455
spoiled_ballots = [
5556
ballot for ballot in ballots if ballot.state == BallotBoxState.SPOILED
5657
]
58+
_update_decrypt_status("Calculating tally")
59+
self._log.debug(f"getting tally for {len(ballots)} ballots")
5760
ciphertext_tally = get_tally(manifest, context, ballots, False)
5861
self._log.debug("getting plaintext tally")
5962
plaintext_tally = decryption_mediator.get_plaintext_tally(
@@ -62,13 +65,14 @@ def run(self, db: Database, decryption: DecryptionDto) -> None:
6265
if plaintext_tally is None:
6366
raise Exception("No plaintext tally found")
6467
self._log.debug("getting plaintext spoiled ballots")
68+
_update_decrypt_status("Processing spoiled ballots")
6569
plaintext_spoiled_ballots = decryption_mediator.get_plaintext_ballots(
6670
spoiled_ballots, manifest
6771
)
6872
if plaintext_spoiled_ballots is None:
6973
raise Exception("No plaintext spoiled ballots found")
7074

71-
update_decrypt_status("Finalizing tally")
75+
_update_decrypt_status("Finalizing tally")
7276

7377
lagrange_coefficients = _get_lagrange_coefficients(decryption_mediator)
7478

@@ -91,6 +95,6 @@ def _get_lagrange_coefficients(
9195
return LagrangeCoefficientsRecord(decryption_mediator.get_lagrange_coefficients())
9296

9397

94-
def update_decrypt_status(status: str) -> None:
98+
def _update_decrypt_status(status: str) -> None:
9599
# pylint: disable=no-member
96100
eel.update_decrypt_status(status)

src/electionguard_gui/web/components/admin/view-decryption-admin-component.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export default {
3535
} else {
3636
console.error(result.message);
3737
this.error = true;
38+
this.loading = false;
39+
this.status = null;
40+
this.decryption = null;
3841
}
3942
},
4043
get_decryption: async function (is_refresh) {
@@ -45,17 +48,17 @@ export default {
4548
this.decryptionId,
4649
is_refresh
4750
)();
51+
console.log("get_decryption complete", result);
4852
this.error = !result.success;
49-
if (result.success) {
50-
this.decryption = result.result;
51-
}
53+
this.decryption = result.success ? result.result : null;
54+
} catch (error) {
55+
console.error(error);
5256
} finally {
5357
this.loading = false;
5458
this.status = null;
5559
}
5660
},
5761
updateDecryptStatus: function (status) {
58-
console.log("updateDecryptStatus", status);
5962
this.status = status;
6063
},
6164
},
@@ -75,7 +78,7 @@ export default {
7578
},
7679
template: /*html*/ `
7780
<div v-if="error">
78-
<p class="alert alert-danger" role="alert">An error occurred. Check the logs and try again.</p>
81+
<p class="alert alert-danger" role="alert">An error occurred. Check the logs and/or <a href="javascript:history.back()">try again</a>.</p>
7982
</div>
8083
<div v-if="decryption">
8184
<div class="text-end">

0 commit comments

Comments
 (0)