def elections_and_ballots(draw: _DrawType, num_ballots: int = 3): """ A convenience generator to generate all of the necessary components for simulating an election. Every ballot will match the same ballot style. Hypothesis doesn't let us declare a type hint on strategy return values, so you can use `ELECTIONS_AND_BALLOTS_TUPLE_TYPE`. :param draw: Hidden argument, used by Hypothesis. :param num_ballots: The number of ballots to generate (default: 3). :reeturn: a tuple of: an `InternalElectionDescription`, a list of plaintext ballots, an ElGamal secret key, and a `CiphertextElectionContext` """ assert num_ballots >= 0, "You're asking for a negative number of ballots?" election_description = draw(election_descriptions()) internal_election_description = InternalElectionDescription( election_description) ballots = [ draw(plaintext_voted_ballots(internal_election_description)) for _ in range(num_ballots) ] secret_key, context = draw(ciphertext_elections(election_description)) mock_election: ELECTIONS_AND_BALLOTS_TUPLE_TYPE = ( election_description, internal_election_description, ballots, secret_key, context, ) return mock_election
def encrypt_ballots(request: EncryptBallotsRequest = Body(...)) -> Any: """ Encrypt one or more ballots """ ballots = [ PlaintextBallot.from_json_object(ballot) for ballot in request.ballots ] description = InternalElectionDescription( ElectionDescription.from_json_object(request.description)) context = CiphertextElectionContext.from_json_object(request.context) seed_hash = read_json_object(request.seed_hash, ElementModQ) nonce: Optional[ElementModQ] = (read_json_object( request.nonce, ElementModQ) if request.nonce else None) encrypted_ballots = [] current_hash = seed_hash for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, description, context, current_hash, nonce) if not encrypted_ballot: raise HTTPException(status_code=500, detail="Ballot failed to encrypt") encrypted_ballots.append(encrypted_ballot) current_hash = get_optional(encrypted_ballot.tracking_hash) response = EncryptBallotsResponse( encrypted_ballots=[ ballot.to_json_object() for ballot in encrypted_ballots ], next_seed_hash=write_json_object(current_hash), ) return response
def get_fake_ballot( self, election: InternalElectionDescription, ballot_id: str = None, with_trues=True, ) -> PlaintextBallot: """ Get a single Fake Ballot object that is manually constructed with default vaules """ if ballot_id is None: ballot_id = "some-unique-ballot-id-123" contests: List[PlaintextBallotContest] = [] for contest in election.get_contests_for( election.ballot_styles[0].object_id): contests.append( self.get_random_contest_from(contest, Random(), with_trues=with_trues)) fake_ballot = PlaintextBallot(ballot_id, election.ballot_styles[0].object_id, contests) return fake_ballot
def test_eg_conversion(self, state: DominionBallotsAndContext, seed: ElementModQ) -> None: ied = InternalElectionDescription(state.ed) ballot_box = BallotBox(ied, state.cec) seed_hash = EncryptionDevice("Location").get_hash() nonces = Nonces(seed)[0:len(state.ballots)] for b, n in zip(state.ballots, nonces): eb = encrypt_ballot(b, ied, state.cec, seed_hash, n) self.assertIsNotNone(eb) pb = decrypt_ballot_with_secret( eb, ied, state.cec.crypto_extended_base_hash, state.cec.elgamal_public_key, state.secret_key, ) self.assertEqual(b, pb) self.assertGreater(len(eb.contests), 0) cast_result = ballot_box.cast(eb) self.assertIsNotNone(cast_result) tally = tally_ballots(ballot_box._store, ied, state.cec) self.assertIsNotNone(tally) results = decrypt_tally_with_secret(tally, state.secret_key) self.assertEqual(len(results.keys()), len(state.id_map.keys())) for obj_id in results.keys(): self.assertIn(obj_id, state.id_map) cvr_sum = int(state.dominion_cvrs.data[state.id_map[obj_id]].sum()) decryption = results[obj_id] self.assertEqual(cvr_sum, decryption)
def test_election_from_file_generates_consistent_internal_description_contest_hashes( self, ): # Arrange comparator = election_factory.get_simple_election_from_file() subject = InternalElectionDescription(comparator) self.assertEqual(len(comparator.contests), len(subject.contests)) for expected in comparator.contests: for actual in subject.contests: if expected.object_id == actual.object_id: self.assertEqual(expected.crypto_hash(), actual.crypto_hash())
def handle_ballot(request: AcceptBallotRequest, state: BallotBoxState) -> Any: ballot = CiphertextBallot.from_json_object(request.ballot) description = ElectionDescription.from_json_object(request.description) internal_description = InternalElectionDescription(description) context = CiphertextElectionContext.from_json_object(request.context) accepted_ballot = accept_ballot( ballot, state, internal_description, context, BallotStore(), ) return accepted_ballot
def _parse_tally_request( request: StartTallyRequest, ) -> Tuple[List[CiphertextAcceptedBallot], InternalElectionDescription, CiphertextElectionContext, ]: """ Deserialize common tally request values """ ballots = [ CiphertextAcceptedBallot.from_json_object(ballot) for ballot in request.ballots ] description = ElectionDescription.from_json_object(request.description) internal_description = InternalElectionDescription(description) context = CiphertextElectionContext.from_json_object(request.context) return (ballots, internal_description, context)
def plaintext_voted_ballot(draw: _DrawType, metadata: InternalElectionDescription): """ Given an `InternalElectionDescription` object, generates an arbitrary `PlaintextBallot` with the choices made randomly. :param draw: Hidden argument, used by Hypothesis. :param metadata: Any `InternalElectionDescription` """ num_ballot_styles = len(metadata.ballot_styles) assert num_ballot_styles > 0, "invalid election with no ballot styles" # pick a ballot style at random ballot_style = metadata.ballot_styles[draw( integers(0, num_ballot_styles - 1))] contests = metadata.get_contests_for(ballot_style.object_id) assert len(contests) > 0, "invalid ballot style with no contests in it" voted_contests: List[PlaintextBallotContest] = [] for contest in contests: assert contest.is_valid(), "every contest needs to be valid" n = contest.number_elected # we need exactly this many 1's, and the rest 0's ballot_selections = contest.ballot_selections assert len(ballot_selections) >= n random = Random(draw(integers())) random.shuffle(ballot_selections) cut_point = draw(integers(0, n)) yes_votes = ballot_selections[:cut_point] no_votes = ballot_selections[cut_point:] voted_selections = [ selection_from( description, is_placeholder=False, is_affirmative=True) for description in yes_votes ] + [ selection_from( description, is_placeholder=False, is_affirmative=False) for description in no_votes ] voted_contests.append( PlaintextBallotContest(contest.object_id, voted_selections)) return PlaintextBallot(str(draw(uuids())), ballot_style.object_id, voted_contests)
def decrypt_share( request: DecryptTallyShareRequest = Body(...), scheduler: Scheduler = Depends(get_scheduler), ) -> Any: """ Decrypt a single guardian's share of a tally """ description = InternalElectionDescription( ElectionDescription.from_json_object(request.description) ) context = CiphertextElectionContext.from_json_object(request.context) guardian = convert_guardian(request.guardian) tally = convert_tally(request.encrypted_tally, description, context) share = compute_decryption_share(guardian, tally, context, scheduler) return write_json_object(share)
def generate_fake_plaintext_ballots_for_election( self, election: InternalElectionDescription, number_of_ballots: int ) -> List[PlaintextBallot]: ballots: List[PlaintextBallot] = [] for _i in range(number_of_ballots): style_index = randint(0, len(election.ballot_styles) - 1) ballot_style = election.ballot_styles[style_index] ballot_id = f"ballot-{uuid.uuid1()}" contests: List[PlaintextBallotContest] = [] for contest in election.get_contests_for(ballot_style.object_id): contests.append( self.get_random_contest_from(contest, Random(), with_trues=True) ) ballots.append(PlaintextBallot(ballot_id, ballot_style.object_id, contests)) return ballots
def decrypt_tally(request: DecryptTallyRequest = Body(...)) -> Any: """ Decrypt a tally from a collection of decrypted guardian shares """ description = InternalElectionDescription( ElectionDescription.from_json_object(request.description)) context = CiphertextElectionContext.from_json_object(request.context) tally = convert_tally(request.encrypted_tally, description, context) shares = { guardian_id: read_json_object(share, TallyDecryptionShare) for guardian_id, share in request.shares.items() } full_plaintext_tally = decrypt(tally, shares, context) if not full_plaintext_tally: raise HTTPException( status_code=500, detail="Unable to decrypt tally", ) published_plaintext_tally = publish_plaintext_tally(full_plaintext_tally) return published_plaintext_tally.to_json_object()
def ray_tally_everything( cvrs: DominionCSV, verbose: bool = True, use_progressbar: bool = True, date: Optional[datetime] = None, seed_hash: Optional[ElementModQ] = None, master_nonce: Optional[ElementModQ] = None, secret_key: Optional[ElementModQ] = None, root_dir: Optional[str] = None, ) -> "RayTallyEverythingResults": """ This top-level function takes a collection of Dominion CVRs and produces everything that we might want for arlo-e2e: a list of encrypted ballots, their encrypted and decrypted tally, and proofs of the correctness of the whole thing. The election `secret_key` is an optional parameter. If absent, a random keypair is generated and used. Similarly, if a `seed_hash` or `master_nonce` is not provided, random ones are generated and used. For parallelism, Ray is used. Make sure you've called `ray.init()` or `ray_localhost_init()` before calling this. If `root_dir` is specified, then the tally is written out to the specified directory, and the resulting `RayTallyEverythingResults` object will support the methods that allow those ballots to be read back in again. Conversely, if `root_dir` is `None`, then nothing is written to disk, and the result will not have access to individual ballots. """ rows, cols = cvrs.data.shape ray_wait_for_workers(min_workers=2) if date is None: date = datetime.now() if root_dir is not None: mkdir_helper(root_dir, num_retries=NUM_WRITE_RETRIES) r_manifest_aggregator = ManifestAggregatorActor.remote( root_dir) # type: ignore else: r_manifest_aggregator = None r_root_dir = ray.put(root_dir) start_time = timer() # Performance note: by using to_election_description_ray rather than to_election_description, we're # only getting back a list of dictionaries rather than a list of PlaintextBallots. We're pushing that # work out into the nodes, where it will run in parallel. The BallotPlaintextFactory wraps up all # the (immutable) state necessary to convert from these dicts to PlaintextBallots and is meant to # be sent to every node in the cluster. ed, bpf, ballot_dicts, id_map = cvrs.to_election_description_ray(date=date) setup_time = timer() num_ballots = len(ballot_dicts) assert num_ballots > 0, "can't have zero ballots!" log_and_print( f"ElectionGuard setup time: {setup_time - start_time: .3f} sec, {num_ballots / (setup_time - start_time):.3f} ballots/sec" ) keypair = (elgamal_keypair_random() if secret_key is None else elgamal_keypair_from_secret(secret_key)) assert keypair is not None, "unexpected failure with keypair computation" secret_key, public_key = keypair cec = make_ciphertext_election_context( number_of_guardians=1, quorum=1, elgamal_public_key=public_key, description_hash=ed.crypto_hash(), ) r_cec = ray.put(cec) ied = InternalElectionDescription(ed) r_ied = ray.put(ied) if seed_hash is None: seed_hash = rand_q() r_seed_hash = ray.put(seed_hash) r_keypair = ray.put(keypair) r_ballot_plaintext_factory = ray.put(bpf) if master_nonce is None: master_nonce = rand_q() nonces = Nonces(master_nonce) r_nonces = ray.put(nonces) nonce_indices = range(num_ballots) inputs = list(zip(ballot_dicts, nonce_indices)) batches = shard_list_uniform(inputs, BATCH_SIZE) num_batches = len(batches) log_and_print( f"Launching Ray.io remote encryption! (number of batches: {num_batches})" ) start_time = timer() progressbar = (ProgressBar({ "Ballots": num_ballots, "Tallies": num_ballots, "Iterations": 0, "Batch": 0, }) if use_progressbar else None) progressbar_actor = progressbar.actor if progressbar is not None else None batch_tallies: List[ObjectRef] = [] for batch in batches: if progressbar_actor: progressbar_actor.update_completed.remote("Batch", 1) num_ballots_in_batch = len(batch) sharded_inputs = shard_list_uniform(batch, BALLOTS_PER_SHARD) num_shards = len(sharded_inputs) partial_tally_refs = [ r_encrypt_and_write.remote( r_ied, r_cec, r_seed_hash, r_root_dir, r_manifest_aggregator, progressbar_actor, r_ballot_plaintext_factory, r_nonces, right_tuple_list(shard), *(left_tuple_list(shard)), ) for shard in sharded_inputs ] # log_and_print("Remote tallying.") btally = ray_tally_ballots(partial_tally_refs, BALLOTS_PER_SHARD, progressbar) batch_tallies.append(btally) # Each batch ultimately yields one partial tally; we add these up here at the # very end. If we have a million ballots and have batches of 10k ballots, this # would mean we'd have only 100 partial tallies. So, what's here works just fine. # If we wanted, we could certainly burn some scalar time and keep a running, # singular, partial tally. It's probably more important to push onward to the # next batch, so we can do as much work in parallel as possible. if len(batch_tallies) > 1: tally = ray.get(ray_tally_ballots(batch_tallies, 10, progressbar)) else: tally = ray.get(batch_tallies[0]) if progressbar: progressbar.close() assert tally is not None, "tally failed!" log_and_print("Tally decryption.") decrypted_tally: DECRYPT_TALLY_OUTPUT_TYPE = ray_decrypt_tally( tally, r_cec, r_keypair, seed_hash) log_and_print("Validating tally.") # Sanity-checking logic: make sure we don't have any unexpected keys, and that the decrypted totals # match up with the columns in the original plaintext data. tally_keys = set(decrypted_tally.keys()) expected_keys = set(id_map.keys()) assert tally_keys.issubset( expected_keys ), f"bad tally keys (actual keys: {sorted(tally_keys)}, expected keys: {sorted(expected_keys)})" for obj_id in decrypted_tally.keys(): cvr_sum = int(cvrs.data[id_map[obj_id]].sum()) decryption, proof = decrypted_tally[obj_id] assert cvr_sum == decryption, f"decryption failed for {obj_id}" final_manifest: Optional[Manifest] = None if root_dir is not None: final_manifest = ray.get(r_manifest_aggregator.result.remote()) assert isinstance( final_manifest, Manifest), "type error: bad result from manifest aggregation" # Assemble the data structure that we're returning. Having nonces in the ciphertext makes these # structures sensitive for writing out to disk, but otherwise they're ready to go. log_and_print("Constructing results.") reported_tally: Dict[str, SelectionInfo] = { k: SelectionInfo( object_id=k, encrypted_tally=tally[k], # we need to forcibly convert mpz to int here to make serialization work properly decrypted_tally=int(decrypted_tally[k][0]), proof=decrypted_tally[k][1], ) for k in tally.keys() } tabulate_time = timer() log_and_print( f"Encryption and tabulation: {rows} ballots, {rows / (tabulate_time - start_time): .3f} ballot/sec", verbose, ) return RayTallyEverythingResults( metadata=cvrs.metadata, cvr_metadata=cvrs.dataframe_without_selections(), election_description=ed, num_ballots=rows, manifest=final_manifest, tally=SelectionTally(reported_tally), context=cec, )
def fast_tally_everything( cvrs: DominionCSV, pool: Optional[Pool] = None, verbose: bool = True, date: Optional[datetime] = None, seed_hash: Optional[ElementModQ] = None, master_nonce: Optional[ElementModQ] = None, secret_key: Optional[ElementModQ] = None, use_progressbar: bool = True, ) -> FastTallyEverythingResults: """ This top-level function takes a collection of Dominion CVRs and produces everything that we might want for arlo-e2e: a list of encrypted ballots, their encrypted and decrypted tally, and proofs of the correctness of the whole thing. The election `secret_key` is an optional parameter. If absent, a random keypair is generated and used. Similarly, if a `seed_hash` or `master_nonce` is not provided, random ones are generated and used. For parallelism, a `multiprocessing.pool.Pool` may be provided, and should result in significant speedups on multicore computers. If absent, the computation will proceed sequentially. """ rows, cols = cvrs.data.shape if date is None: date = datetime.now() parse_time = timer() log_and_print(f"Rows: {rows}, cols: {cols}", verbose) ed, ballots, id_map = cvrs.to_election_description(date=date) assert len(ballots) > 0, "can't have zero ballots!" keypair = (elgamal_keypair_random() if secret_key is None else elgamal_keypair_from_secret(secret_key)) assert keypair is not None, "unexpected failure with keypair computation" secret_key, public_key = keypair # This computation exists only to cause side-effects in the DLog engine, so the lame nonce is not an issue. assert len(ballots) == get_optional( elgamal_encrypt(m=len(ballots), nonce=int_to_q_unchecked(3), public_key=public_key)).decrypt( secret_key), "got wrong ElGamal decryption!" dlog_prime_time = timer() log_and_print( f"DLog prime time (n={len(ballots)}): {dlog_prime_time - parse_time: .3f} sec", verbose, ) cec = make_ciphertext_election_context( number_of_guardians=1, quorum=1, elgamal_public_key=public_key, description_hash=ed.crypto_hash(), ) ied = InternalElectionDescription(ed) # REVIEW THIS: is this cryptographically sound? Is the seed_hash properly a secret? Should # it go in the output? The nonces are clearly secret. If you know them, you can decrypt. if seed_hash is None: seed_hash = rand_q() if master_nonce is None: master_nonce = rand_q() nonces: List[ElementModQ] = Nonces(master_nonce)[0:len(ballots)] # even if verbose is false, we still want to see the progress bar for the encryption cballots = fast_encrypt_ballots(ballots, ied, cec, seed_hash, nonces, pool, use_progressbar=use_progressbar) eg_encrypt_time = timer() log_and_print( f"Encryption time: {eg_encrypt_time - dlog_prime_time: .3f} sec", verbose) log_and_print( f"Encryption rate: {rows / (eg_encrypt_time - dlog_prime_time): .3f} ballot/sec", verbose, ) tally: TALLY_TYPE = fast_tally_ballots(cballots, pool) eg_tabulate_time = timer() log_and_print( f"Tabulation time: {eg_tabulate_time - eg_encrypt_time: .3f} sec", verbose) log_and_print( f"Tabulation rate: {rows / (eg_tabulate_time - eg_encrypt_time): .3f} ballot/sec", verbose, ) log_and_print( f"Encryption and tabulation: {rows} ballots / {eg_tabulate_time - dlog_prime_time: .3f} sec = {rows / (eg_tabulate_time - dlog_prime_time): .3f} ballot/sec", verbose, ) assert tally is not None, "tally failed!" if verbose: # pragma: no cover print("Decryption & Proofs: ") decrypted_tally: DECRYPT_TALLY_OUTPUT_TYPE = fast_decrypt_tally( tally, cec, keypair, seed_hash, pool, verbose) eg_decryption_time = timer() log_and_print( f"Decryption time: {eg_decryption_time - eg_tabulate_time: .3f} sec", verbose) log_and_print( f"Decryption rate: {len(decrypted_tally.keys()) / (eg_decryption_time - eg_tabulate_time): .3f} selection/sec", verbose, ) # Sanity-checking logic: make sure we don't have any unexpected keys, and that the decrypted totals # match up with the columns in the original plaintext data. for obj_id in decrypted_tally.keys(): assert obj_id in id_map, "object_id in results that we don't know about!" cvr_sum = int(cvrs.data[id_map[obj_id]].sum()) decryption, proof = decrypted_tally[obj_id] assert cvr_sum == decryption, f"decryption failed for {obj_id}" # Assemble the data structure that we're returning. Having nonces in the ciphertext makes these # structures sensitive for writing out to disk, but otherwise they're ready to go. reported_tally: Dict[str, SelectionInfo] = { k: SelectionInfo( object_id=k, encrypted_tally=tally[k], # we need to forcibly convert mpz to int here to make serialization work properly decrypted_tally=int(decrypted_tally[k][0]), proof=decrypted_tally[k][1], ) for k in tally.keys() } # strips the ballots of their nonces, which is important because those could allow for decryption accepted_ballots = [ciphertext_ballot_to_accepted(x) for x in cballots] return FastTallyEverythingResults( metadata=cvrs.metadata, cvr_metadata=cvrs.dataframe_without_selections(), election_description=ed, encrypted_ballot_memos={ ballot.object_id: make_memo_value(ballot) for ballot in accepted_ballots }, tally=SelectionTally(reported_tally), context=cec, )
def equivalent( self, other: "FastTallyEverythingResults", keys: ElGamalKeyPair, pool: Optional[Pool] = None, ) -> bool: """ The built-in equality checking (__eq__) will determine if two tally results are absolutely identical, but with the non-determinism built into ElGamal encryption, we need a somewhat more general equality checker that knows how to decrypt the ciphertexts first. Note that this method doesn't check the Chaum-Pedersen proofs, and assumes that the tally decryptions already present are correct. That makes this method much faster when used in a testing context, but more limited if used elsewhere. """ same_metadata = self.metadata == other.metadata my_ied = InternalElectionDescription(self.election_description) other_ied = InternalElectionDescription(other.election_description) same_ied = my_ied == other_ied my_cballots = self.encrypted_ballots other_cballots = other.encrypted_ballots wrapped_func = functools.partial( _equivalent_decrypt_helper, my_ied, self.context.crypto_extended_base_hash, keys.public_key, keys.secret_key, ) my_cballots_tqdm = tqdm(my_cballots, desc="Equivalent (1/2)") other_cballots_tqdm = tqdm(other_cballots, desc="Equivalent (2/2)") my_pballots: List[PlaintextBallot] = sorted( [wrapped_func(cballot) for cballot in my_cballots_tqdm] if not pool else pool.map(func=wrapped_func, iterable=my_cballots_tqdm), key=lambda x: x.object_id, ) other_pballots: List[PlaintextBallot] = sorted( [wrapped_func(cballot) for cballot in other_cballots_tqdm] if not pool else pool.map( func=wrapped_func, iterable=other_cballots_tqdm), key=lambda x: x.object_id, ) same_ballots = my_pballots == other_pballots my_decrypted_tallies = { k: self.tally.map[k].decrypted_tally for k in self.tally.map.keys() } other_decrypted_tallies = { k: other.tally.map[k].decrypted_tally for k in other.tally.map.keys() } same_tallies = my_decrypted_tallies == other_decrypted_tallies same_cvr_metadata = self.cvr_metadata.equals(other.cvr_metadata) success = (same_metadata and same_ballots and same_tallies and same_cvr_metadata and same_ied) return success
if results is None: print(f"Failed to load results from {tally_dir}") exit(1) for bid in ballot_ids: if bid not in results.metadata.ballot_id_to_ballot_type: print(f"Ballot id {bid} is not part of the tally") encrypted_ballots = [results.get_encrypted_ballot(bid) for bid in ballot_ids] if None in encrypted_ballots: print("Missing files on disk. Exiting.") exit(1) plaintext_ballots = [load_proven_ballot(bid, decrypted_dir) for bid in ballot_ids] ied = InternalElectionDescription(results.election_description) extended_base_hash = results.context.crypto_extended_base_hash for encrypted, plaintext in zip(encrypted_ballots, plaintext_ballots): if encrypted is None: # this would have been caught earlier, will never happen here continue bid = encrypted.object_id if bid in results.metadata.ballot_id_to_ballot_type: ballot_type = results.metadata.ballot_id_to_ballot_type[bid] else: print(f"Ballot: {bid}, Unknown ballot style!") continue if plaintext is None:
def test_end_to_end_publications(self, input: str, check_proofs: bool, keypair: ElGamalKeyPair) -> None: coverage.process_startup( ) # necessary for coverage testing to work in parallel self.removeTree( ) # if there's anything leftover from a prior run, get rid of it cvrs = read_dominion_csv(StringIO(input)) self.assertIsNotNone(cvrs) _, ballots, _ = cvrs.to_election_description() assert len(ballots) > 0, "can't have zero ballots!" results = fast_tally_everything(cvrs, self.pool, secret_key=keypair.secret_key, verbose=True) self.assertTrue(results.all_proofs_valid(self.pool)) # dump files out to disk write_fast_tally(results, TALLY_TESTING_DIR) log_and_print( "tally_testing written, proceeding to read it back in again") # now, read it back again! results2 = load_fast_tally( TALLY_TESTING_DIR, check_proofs=check_proofs, pool=self.pool, verbose=True, recheck_ballots_and_tallies=True, ) self.assertIsNotNone(results2) log_and_print("tally_testing got non-null result!") self.assertTrue( _list_eq(results.encrypted_ballots, results2.encrypted_ballots)) self.assertTrue(results.equivalent(results2, keypair, self.pool)) # Make sure there's an index.html file; throws an exception if it's missing self.assertIsNotNone(stat(path.join(TALLY_TESTING_DIR, "index.html"))) # And lastly, while we're here, we'll use all this machinery to exercise the ballot decryption # read/write facilities. ied = InternalElectionDescription(results.election_description) log_and_print("decrypting one more time") pballots = decrypt_ballots( ied, results.context.crypto_extended_base_hash, keypair, self.pool, results.encrypted_ballots, ) self.assertEqual(len(pballots), len(results.encrypted_ballots)) self.assertNotIn(None, pballots) # for speed, we're only going to do this for the first ballot, not all of them pballot = pballots[0] eballot = results.encrypted_ballots[0] bid = pballot.ballot.object_id self.assertTrue( verify_proven_ballot_proofs( results.context.crypto_extended_base_hash, keypair.public_key, eballot, pballot, )) write_proven_ballot(pballot, DECRYPTED_DIR) self.assertTrue(exists_proven_ballot(bid, DECRYPTED_DIR)) self.assertFalse(exists_proven_ballot(bid + "0", DECRYPTED_DIR)) self.assertEqual(pballot, load_proven_ballot(bid, DECRYPTED_DIR)) self.removeTree() # clean up our mess