diff --git a/docs/0_Configure_Election.md b/docs/0_Configure_Election.md index 0645fa636..cd79ab86e 100644 --- a/docs/0_Configure_Election.md +++ b/docs/0_Configure_Election.md @@ -2,40 +2,41 @@ An election in ElectionGuard is defined as a set of metadata and cryptographic artifacts necessary to encrypt, conduct, tally, decrypt, and verify an election. The Data format used for election metadata is based on the [NIST Election Common Standard Data Specification](https://www.nist.gov/itl/voting/interoperability) but includes some modifications to support the end-to-end cryptography of ElectionGuard. -Election metadata is described in a specific format parseable into an `ElectionDescription` and it's validity is checked to ensure that it is of an appropriate structure to conduct an End-to-End Verified ElectionGuard Election. ElectionGuard only verifies the components of the election metadata that are necessary to encrypt and decrypt the election. Some components of the election metadata are not checked for structural validity, but are used when generating a hash representation of the `Election Description`. +Election metadata is described in a specific format parseable into an `Manifest` and it's validity is checked to ensure that it is of an appropriate structure to conduct an End-to-End Verified ElectionGuard Election. ElectionGuard only verifies the components of the election metadata that are necessary to encrypt and decrypt the election. Some components of the election metadata are not checked for structural validity, but are used when generating a hash representation of the `Manifest`. -From an `ElectionDescription` we derive an `InternalElectionDescription` that includes a subset of the elements from the `ElectionDescription` required to verify ballots are correct. Additionally a `CiphertextElectionContext` is created during the [Key Ceremony](/1_Key_Ceremony.md) that includes the cryptographic artifacts necessary for encrypting ballots. +From an `Manifest` we derive an `InternalManifest` that includes a subset of the elements from the `Manifest` required to verify ballots are correct. Additionally a `CiphertextElectionContext` is created during the [Key Ceremony](/1_Key_Ceremony.md) that includes the cryptographic artifacts necessary for encrypting ballots. ## Glossary - **Election Manifest** The election metadata in json format that is parsed into an Election Description - **Election Description** The election metadata that describes the structure and type of the election, including geopolitical units, contests, candidates, and ballot styles, etc. -- **Internal Election Description** The subset of the `ElectionDescription` required by ElectionGuard to validate ballots are correctly associated with an election. This component mutates the state of the Election Description. +- **Internal Election Description** The subset of the `Manifest` required by ElectionGuard to validate ballots are correctly associated with an election. This component mutates the state of the Election Description. - **Ciphertext Election Context** The cryptographic context of an election that is configured during the `Key Ceremony` -- **Description Hash** a Hash representation of the original ElectionDescription. +- **Description Hash** a Hash representation of the original Manifest. ## Process -1. Define an election according to the `ElectionDescription` requirements. +1. Define an election according to the `Manifest` requirements. 2. Use the [NIST Common Standard Data Specification](https://www.nist.gov/itl/voting/interoperability) as a guide, but note the differences in [election.py](https://github.com/microsoft/electionguard-python/tree/main/src/electionguard.election.py) and the provided [sample manifest](https://github.com/microsoft/electionguard-python/tree/main/data/election_manifest_simple.json). -3. Parse the `ElectionDescription` into the application. +3. Parse the `Manifest` into the application. 4. Define the encryption parameters necessary for conducting an election (see `Key Ceremony`). 5. Create the Pubic Key either from a single secret, or from the Key Ceremony. -6. Build the `InternalElectionDescription` and `CiphertextElectionContext` from the `ElectionDescription` and `ElGamalKeyPair.public_key`. +6. Build the `InternalManifest` and `CiphertextElectionContext` from the `Manifest` and `ElGamalKeyPair.public_key`. ## Usage Example ```python import os -from electionguard.election import ElectionDescription, InternalElectionDescription, CiphertextElectionContext +from electionguard.election import CiphertextElectionContext from electionguard.election_builder import ElectionBuilder from electionguard.elgamal import ElGamalKeyPair, elgamal_keypair_from_secret +from electionguard.manifest import Manifest, InternalManifest # Open an election manifest file with open(os.path.join(some_path, "election-manifest.json"), "r") as manifest: string_representation = manifest.read() - election_description = ElectionDescription.from_json(string_representation) + election_description = Manifest.from_json(string_representation) # Create an election builder instance, and configure it for a single public-private keypair. # in a real election, you would configure this for a group of guardians. See Key Ceremony for more information. diff --git a/src/electionguard/ballot_box.py b/src/electionguard/ballot_box.py index 345f836d2..b1c38cacf 100644 --- a/src/electionguard/ballot_box.py +++ b/src/electionguard/ballot_box.py @@ -7,11 +7,11 @@ SubmittedBallot, from_ciphertext_ballot, ) +from .ballot_validator import ballot_is_valid_for_election from .data_store import DataStore - -from .election import CiphertextElectionContext, InternalElectionDescription +from .election import CiphertextElectionContext from .logs import log_warning -from .ballot_validator import ballot_is_valid_for_election +from .manifest import InternalManifest @dataclass @@ -20,7 +20,7 @@ class BallotBox: A stateful convenience wrapper to cache election data """ - _metadata: InternalElectionDescription = field() + _internal_manifest: InternalManifest = field() _encryption: CiphertextElectionContext = field() _store: DataStore = field(default_factory=lambda: DataStore()) @@ -29,7 +29,11 @@ def cast(self, ballot: CiphertextBallot) -> Optional[SubmittedBallot]: Cast a specific encrypted `CiphertextBallot` """ return accept_ballot( - ballot, BallotBoxState.CAST, self._metadata, self._encryption, self._store + ballot, + BallotBoxState.CAST, + self._internal_manifest, + self._encryption, + self._store, ) def spoil(self, ballot: CiphertextBallot) -> Optional[SubmittedBallot]: @@ -39,7 +43,7 @@ def spoil(self, ballot: CiphertextBallot) -> Optional[SubmittedBallot]: return accept_ballot( ballot, BallotBoxState.SPOILED, - self._metadata, + self._internal_manifest, self._encryption, self._store, ) @@ -48,17 +52,17 @@ def spoil(self, ballot: CiphertextBallot) -> Optional[SubmittedBallot]: def accept_ballot( ballot: CiphertextBallot, state: BallotBoxState, - metadata: InternalElectionDescription, + internal_manifest: InternalManifest, context: CiphertextElectionContext, store: DataStore, ) -> Optional[SubmittedBallot]: """ Submit a ballot within the context of a specified election and against an existing data store - Verified that the ballot is valid for the election `metadata` and `context` and + Verified that the ballot is valid for the election `internal_manifest` and `context` and that the ballot has not already been cast or spoiled. :return: a `SubmittedBallot` or `None` if there was an error """ - if not ballot_is_valid_for_election(ballot, metadata, context): + if not ballot_is_valid_for_election(ballot, internal_manifest, context): return None existing_ballot = store.get(ballot.object_id) diff --git a/src/electionguard/ballot_validator.py b/src/electionguard/ballot_validator.py index 918b74327..93eb47950 100644 --- a/src/electionguard/ballot_validator.py +++ b/src/electionguard/ballot_validator.py @@ -1,28 +1,27 @@ from .ballot import CiphertextBallot, CiphertextBallotContest, CiphertextBallotSelection -from .election import ( - CiphertextElectionContext, +from .election import CiphertextElectionContext +from .logs import log_warning +from .manifest import ( ContestDescriptionWithPlaceholders, - InternalElectionDescription, + InternalManifest, SelectionDescription, ) -from .logs import log_warning - def ballot_is_valid_for_election( ballot: CiphertextBallot, - metadata: InternalElectionDescription, + internal_manifest: InternalManifest, context: CiphertextElectionContext, ) -> bool: """ Determine if a ballot is valid for a given election """ - if not ballot_is_valid_for_style(ballot, metadata): + if not ballot_is_valid_for_style(ballot, internal_manifest): return False if not ballot.is_valid_encryption( - metadata.description_hash, + internal_manifest.manifest_hash, context.elgamal_public_key, context.crypto_extended_base_hash, ): @@ -87,15 +86,15 @@ def contest_is_valid_for_style( def ballot_is_valid_for_style( - ballot: CiphertextBallot, metadata: InternalElectionDescription + ballot: CiphertextBallot, internal_manifest: InternalManifest ) -> bool: """ Determine if ballot is valid for ballot style :param ballot: Ballot - :param metadata: Internal election description + :param internal_manifest: Internal election description :return: Is valid """ - descriptions = metadata.get_contests_for(ballot.style_id) + descriptions = internal_manifest.get_contests_for(ballot.style_id) for description in descriptions: use_contest = None diff --git a/src/electionguard/decrypt_with_secrets.py b/src/electionguard/decrypt_with_secrets.py index 298a97ca9..df2cdd526 100644 --- a/src/electionguard/decrypt_with_secrets.py +++ b/src/electionguard/decrypt_with_secrets.py @@ -8,13 +8,13 @@ PlaintextBallotContest, PlaintextBallotSelection, ) -from .election import ( - InternalElectionDescription, +from .group import ElementModP, ElementModQ +from .logs import log_warning +from .manifest import ( + InternalManifest, ContestDescriptionWithPlaceholders, SelectionDescription, ) -from .group import ElementModP, ElementModQ -from .logs import log_warning from .nonces import Nonces from .utils import get_optional @@ -238,7 +238,7 @@ def decrypt_contest_with_nonce( def decrypt_ballot_with_secret( ballot: CiphertextBallot, - election_metadata: InternalElectionDescription, + internal_manifest: InternalManifest, crypto_extended_base_hash: ElementModQ, public_key: ElementModP, secret_key: ElementModQ, @@ -249,7 +249,7 @@ def decrypt_ballot_with_secret( Decrypt the specified `CiphertextBallot` within the context of the specified election. :param ballot: the ballot to decrypt - :param election_metadata: the qualified election metadata that includes placeholder selections + :param internal_manifest: the qualified election metadata that includes placeholder selections :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param public_key: the public key for the election (K) :param secret_key: the known secret key used to generate the public key for this election @@ -258,14 +258,14 @@ def decrypt_ballot_with_secret( """ if not suppress_validity_check and not ballot.is_valid_encryption( - election_metadata.description_hash, public_key, crypto_extended_base_hash + internal_manifest.manifest_hash, public_key, crypto_extended_base_hash ): return None plaintext_contests: List[PlaintextBallotContest] = list() for contest in ballot.contests: - description = election_metadata.contest_for(contest.object_id) + description = internal_manifest.contest_for(contest.object_id) plaintext_contest = decrypt_contest_with_secret( contest, get_optional(description), @@ -288,7 +288,7 @@ def decrypt_ballot_with_secret( def decrypt_ballot_with_nonce( ballot: CiphertextBallot, - election_metadata: InternalElectionDescription, + internal_manifest: InternalManifest, crypto_extended_base_hash: ElementModQ, public_key: ElementModP, nonce: Optional[ElementModQ] = None, @@ -299,7 +299,7 @@ def decrypt_ballot_with_nonce( Decrypt the specified `CiphertextBallot` within the context of the specified election. :param ballot: the ballot to decrypt - :param election_metadata: the qualified election metadata that includes placeholder selections + :param internal_manifest: the qualified election metadata that includes placeholder selections :param crypto_extended_base_hash: the extended base hash code (𝑄') for the election :param public_key: the public key for the election (K) :param nonce: the optional master ballot nonce that was either seeded to, or gernated by the encryption function @@ -308,7 +308,7 @@ def decrypt_ballot_with_nonce( """ if not suppress_validity_check and not ballot.is_valid_encryption( - election_metadata.description_hash, public_key, crypto_extended_base_hash + internal_manifest.manifest_hash, public_key, crypto_extended_base_hash ): return None @@ -318,7 +318,7 @@ def decrypt_ballot_with_nonce( nonce_seed = ballot.hashed_ballot_nonce() else: nonce_seed = CiphertextBallot.nonce_seed( - election_metadata.description_hash, ballot.object_id, nonce + internal_manifest.manifest_hash, ballot.object_id, nonce ) if nonce_seed is None: @@ -330,7 +330,7 @@ def decrypt_ballot_with_nonce( plaintext_contests: List[PlaintextBallotContest] = list() for contest in ballot.contests: - description = election_metadata.contest_for(contest.object_id) + description = internal_manifest.contest_for(contest.object_id) plaintext_contest = decrypt_contest_with_nonce( contest, get_optional(description), diff --git a/src/electionguard/decryption_mediator.py b/src/electionguard/decryption_mediator.py index 643c9305c..90c2447c8 100644 --- a/src/electionguard/decryption_mediator.py +++ b/src/electionguard/decryption_mediator.py @@ -14,10 +14,7 @@ ) from .decryption_share import DecryptionShare, CompensatedDecryptionShare from .decrypt_with_shares import decrypt_ballots, decrypt_tally -from .election import ( - CiphertextElectionContext, - InternalElectionDescription, -) +from .election import CiphertextElectionContext from .election_polynomial import compute_lagrange_coefficient from .group import ElementModP, ElementModQ from .guardian import Guardian @@ -45,7 +42,6 @@ class DecryptionMediator: to form a decrypted representation of an election tally """ - _metadata: InternalElectionDescription _encryption: CiphertextElectionContext # Tally to Decrypt diff --git a/src/electionguard/election.py b/src/electionguard/election.py index 1a1f6c119..b111f4566 100644 --- a/src/electionguard/election.py +++ b/src/electionguard/election.py @@ -1,813 +1,8 @@ -from dataclasses import dataclass, field, InitVar -from datetime import datetime -from enum import Enum, unique -from typing import cast, List, Optional, Set, Any +from dataclasses import dataclass -from .ballot import _list_eq -from .election_object_base import ElectionObjectBase from .group import Q, P, R, G, ElementModQ, ElementModP -from .hash import CryptoHashable, hash_elems -from .logs import log_warning +from .hash import hash_elems from .serializable import Serializable -from .utils import get_optional, to_ticks - - -@unique -class ElectionType(Enum): - """ - enumerations for the `ElectionReport` entity - see: https://developers.google.com/elections-data/reference/election-type - """ - - unknown = 0 - general = 1 - partisan_primary_closed = 2 - partisan_primary_open = 3 - primary = 4 - runoff = 5 - special = 6 - other = 7 - - -@unique -class ReportingUnitType(Enum): - """ - Enumeration for the type of geopolitical unit - see: https://developers.google.com/elections-data/reference/reporting-unit-type - """ - - unknown = 0 - ballot_batch = 1 - ballot_style_area = 2 - borough = 3 - city = 4 - city_council = 5 - combined_precinct = 6 - congressional = 7 - country = 8 - county = 9 - county_council = 10 - drop_box = 11 - judicial = 12 - municipality = 13 - polling_place = 14 - precinct = 15 - school = 16 - special = 17 - split_precinct = 18 - state = 19 - state_house = 20 - state_senate = 21 - township = 22 - utility = 23 - village = 24 - vote_center = 25 - ward = 26 - water = 27 - other = 28 - - -@unique -class VoteVariationType(Enum): - """ - Enumeration for contest algorithm or rules in the `Contest` entity - see: https://developers.google.com/elections-data/reference/vote-variation - """ - - unknown = 0 - one_of_m = 1 - approval = 2 - borda = 3 - cumulative = 4 - majority = 5 - n_of_m = 6 - plurality = 7 - proportional = 8 - range = 9 - rcv = 10 - super_majority = 11 - other = 12 - - -@dataclass(eq=True, unsafe_hash=True) -class AnnotatedString(Serializable, CryptoHashable): - """ - Use this as a type for character strings. - See: https://developers.google.com/elections-data/reference/annotated-string - """ - - annotation: str = field(default="") - value: str = field(default="") - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems(self.annotation, self.value) - - -@dataclass(eq=True, unsafe_hash=True) -class Language(Serializable, CryptoHashable): - """ - The ISO-639 language - see: https://en.wikipedia.org/wiki/ISO_639 - """ - - value: str - language: str = field(default="en") - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems(self.value, self.language) - - -@dataclass(eq=True, unsafe_hash=True) -class InternationalizedText(Serializable, CryptoHashable): - """ - Data entity used to represent multi-national text. Use when text on a ballot contains multi-national text. - See: https://developers.google.com/elections-data/reference/internationalized-text - """ - - text: List[Language] = field(default_factory=lambda: []) - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems(self.text) - - -@dataclass(eq=True, unsafe_hash=True) -class ContactInformation(Serializable, CryptoHashable): - """ - For defining contact information about objects such as persons, boards of authorities, and organizations. - See: https://developers.google.com/elections-data/reference/contact-information - """ - - address_line: Optional[List[str]] = field(default=None) - email: Optional[List[AnnotatedString]] = field(default=None) - phone: Optional[List[AnnotatedString]] = field(default=None) - name: Optional[str] = field(default=None) - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems(self.name, self.address_line, self.email, self.phone) - - -@dataclass(eq=True, unsafe_hash=True) -class GeopoliticalUnit(ElectionObjectBase, CryptoHashable): - """ - Use this entity for defining geopolitical units such as cities, districts, jurisdictions, or precincts, - for the purpose of associating contests, offices, vote counts, or other information with the geographies. - See: https://developers.google.com/elections-data/reference/gp-unit - """ - - name: str - type: ReportingUnitType - contact_information: Optional[ContactInformation] = field(default=None) - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems( - self.object_id, self.name, str(self.type.name), self.contact_information - ) - - -@dataclass(eq=True, unsafe_hash=True) -class BallotStyle(ElectionObjectBase, CryptoHashable): - """ - A BallotStyle works as a key to uniquely specify a set of contests. See also `ContestDescription`. - """ - - geopolitical_unit_ids: Optional[List[str]] = field(default=None) - party_ids: Optional[List[str]] = field(default=None) - image_uri: Optional[str] = field(default=None) - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems( - self.object_id, self.geopolitical_unit_ids, self.party_ids, self.image_uri - ) - - -@dataclass(eq=True, unsafe_hash=True) -class Party(ElectionObjectBase, CryptoHashable): - """ - Use this entity to describe a political party that can then be referenced from other entities. - See: https://developers.google.com/elections-data/reference/party - """ - - name: InternationalizedText = field(default=InternationalizedText()) - abbreviation: Optional[str] = field(default=None) - color: Optional[str] = field(default=None) - logo_uri: Optional[str] = field(default=None) - - def get_party_id(self) -> str: - """ - Returns the object identifier associated with the Party. - """ - return self.object_id - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems( - self.object_id, - self.name, - self.abbreviation, - self.color, - self.logo_uri, - ) - - -@dataclass(eq=True, unsafe_hash=True) -class Candidate(ElectionObjectBase, CryptoHashable): - """ - Entity describing information about a candidate in a contest. - See: https://developers.google.com/elections-data/reference/candidate - Note: The ElectionGuard Data Spec deviates from the NIST model in that - selections for any contest type are considered a "candidate". - for instance, on a yes-no referendum contest, two `candidate` objects - would be included in the model to represent the `affirmative` and `negative` - selections for the contest. See the wiki, readme's, and tests in this repo for more info - """ - - name: InternationalizedText = field(default=InternationalizedText()) - party_id: Optional[str] = field(default=None) - image_uri: Optional[str] = field(default=None) - is_write_in: Optional[bool] = field(default=None) - - def get_candidate_id(self) -> str: - """ - Given a `Candidate`, returns a "candidate ID", which is used in other ElectionGuard structures. - """ - return self.object_id - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems(self.object_id, self.name, self.party_id, self.image_uri) - - -@dataclass(eq=True, unsafe_hash=True) -class SelectionDescription(ElectionObjectBase, CryptoHashable): - """ - Data entity for the ballot selections in a contest, - for example linking candidates and parties to their vote counts. - See: https://developers.google.com/elections-data/reference/ballot-selection - Note: The ElectionGuard Data Spec deviates from the NIST model in that - there is no difference for different types of selections. - The ElectionGuard Data Spec deviates from the NIST model in that - `sequence_order` is a required field since it is used for ordering selections - in a contest to ensure various encryption primitives are deterministic. - For a given election, the sequence of selections displayed to a user may be different - however that information is not captured by default when encrypting a specific ballot. - """ - - candidate_id: str - sequence_order: int - """ - Used for ordering selections in a contest to ensure various encryption primitives are deterministic. - The sequence order must be unique and should be representative of how the contests are represnted - on a "master" ballot in an external system. The sequence order is not required to be in the order - in which they are displayed to a voter. Any acceptable range of integer values may be provided. - """ - - def crypto_hash(self) -> ElementModQ: - """ - A hash representation of the object - """ - return hash_elems(self.object_id, self.sequence_order, self.candidate_id) - - -# pylint: disable=too-many-instance-attributes -@dataclass(unsafe_hash=True) -class ContestDescription(ElectionObjectBase, CryptoHashable): - """ - Use this data entity for describing a contest and linking the contest - to the associated candidates and parties. - See: https://developers.google.com/elections-data/reference/contest - Note: The ElectionGuard Data Spec deviates from the NIST model in that - `sequence_order` is a required field since it is used for ordering selections - in a contest to ensure various encryption primitives are deterministic. - For a given election, the sequence of contests displayed to a user may be different - however that information is not captured by default when encrypting a specific ballot. - """ - - electoral_district_id: str - sequence_order: int - """ - Used for ordering contests in a ballot to ensure various encryption primitives are deterministic. - The sequence order must be unique and should be representative of how the contests are represnted - on a "master" ballot in an external system. The sequence order is not required to be in the order - in which they are displayed to a voter. Any acceptable range of integer values may be provided. - """ - - vote_variation: VoteVariationType - - # Number of candidates that are elected in the contest ("n" of n-of-m). - # Note: a referendum is considered a specific case of 1-of-m in ElectionGuard - number_elected: int - - # Maximum number of votes/write-ins per voter in this contest. Used in cumulative voting - # to indicate how many total votes a voter can spread around. In n-of-m elections, this will - # be None. - votes_allowed: Optional[int] - - # Name of the contest, not necessarily as it appears on the ballot. - name: str - - # For associating a ballot selection for the contest, i.e., a candidate, a ballot measure. - ballot_selections: List[SelectionDescription] = field(default_factory=lambda: []) - - # Title of the contest as it appears on the ballot. - ballot_title: Optional[InternationalizedText] = field(default=None) - - # Subtitle of the contest as it appears on the ballot. - ballot_subtitle: Optional[InternationalizedText] = field(default=None) - - def __eq__(self, other: Any) -> bool: - return ( - isinstance(other, ContestDescription) - and self.electoral_district_id == other.electoral_district_id - and self.sequence_order == other.sequence_order - and self.vote_variation == other.vote_variation - and self.number_elected == other.number_elected - and self.votes_allowed == other.votes_allowed - and self.name == other.name - and _list_eq(self.ballot_selections, other.ballot_selections) - and self.ballot_title == other.ballot_title - and self.ballot_subtitle == other.ballot_subtitle - ) - - def crypto_hash(self) -> ElementModQ: - """ - Given a ContestDescription, deterministically derives a "hash" of that contest, - suitable for use in ElectionGuard's "base hash" values, and for validating that - either a plaintext or encrypted voted context and its corresponding contest - description match up. - """ - # remove any placeholders from the hash mechanism - return hash_elems( - self.object_id, - self.sequence_order, - self.electoral_district_id, - str(self.vote_variation.name), - self.ballot_title, - self.ballot_subtitle, - self.name, - self.number_elected, - self.votes_allowed, - self.ballot_selections, - ) - - def is_valid(self) -> bool: - """ - Check the validity of the contest object by verifying its data - """ - contest_has_valid_number_elected = self.number_elected <= len( - self.ballot_selections - ) - contest_has_valid_votes_allowed = ( - self.votes_allowed is None or self.number_elected <= self.votes_allowed - ) - - # verify the candidate_ids, selection object_ids, and sequence_ids are unique - candidate_ids: Set[str] = set() - selection_ids: Set[str] = set() - sequence_ids: Set[int] = set() - selection_count = 0 - expected_selection_count = len(self.ballot_selections) - - for selection in self.ballot_selections: - selection_count += 1 - # validate the object_id - if selection.object_id not in selection_ids: - selection_ids.add(selection.object_id) - # validate the sequence_order - if selection.sequence_order not in sequence_ids: - sequence_ids.add(selection.sequence_order) - # validate the candidate id - if selection.candidate_id not in candidate_ids: - candidate_ids.add(selection.candidate_id) - - selections_have_valid_candidate_ids = ( - len(candidate_ids) == expected_selection_count - ) - selections_have_valid_selection_ids = ( - len(selection_ids) == expected_selection_count - ) - selections_have_valid_sequence_ids = ( - len(sequence_ids) == expected_selection_count - ) - - success = ( - contest_has_valid_number_elected - and contest_has_valid_votes_allowed - and selections_have_valid_candidate_ids - and selections_have_valid_selection_ids - and selections_have_valid_sequence_ids - ) - - if not success: - log_warning( - "Contest %s failed validation check: %s", - self.object_id, - str( - { - "contest_has_valid_number_elected": contest_has_valid_number_elected, - "contest_has_valid_votes_allowed": contest_has_valid_votes_allowed, - "selections_have_valid_candidate_ids": selections_have_valid_candidate_ids, - "selections_have_valid_selection_ids": selections_have_valid_selection_ids, - "selections_have_valid_sequence_ids": selections_have_valid_sequence_ids, - } - ), - ) - - return success - - -@dataclass(eq=True, unsafe_hash=True) -class CandidateContestDescription(ContestDescription): - """ - Use this entity to describe a contest that involves selecting one or more candidates. - See: https://developers.google.com/elections-data/reference/contest - Note: The ElectionGuard Data Spec deviates from the NIST model in that - this subclass is used purely for convenience - """ - - primary_party_ids: List[str] = field(default_factory=lambda: []) - - -@dataclass(eq=True, unsafe_hash=True) -class ReferendumContestDescription(ContestDescription): - """ - Use this entity to describe a contest that involves selecting exactly one 'candidate'. - See: https://developers.google.com/elections-data/reference/contest - Note: The ElectionGuard Data Spec deviates from the NIST model in that - this subclass is used purely for convenience - """ - - -@dataclass(eq=True, unsafe_hash=True) -class ContestDescriptionWithPlaceholders(ContestDescription): - """ - ContestDescriptionWithPlaceholders is a `ContestDescription` with ElectionGuard `placeholder_selections`. - (The ElectionGuard spec requires for n-of-m elections that there be *exactly* n counters that are one - with the rest zero, so if a voter deliberately undervotes, one or more of the placeholder counters will - become one. This allows the `ConstantChaumPedersenProof` to verify correctly for undervoted contests.) - """ - - placeholder_selections: List[SelectionDescription] = field( - default_factory=lambda: [] - ) - - def is_valid(self) -> bool: - """ - Checks is contest description is valid - :return: true if valid - """ - contest_description_validates = super().is_valid() - return ( - contest_description_validates - and len(self.placeholder_selections) == self.number_elected - ) - - def is_placeholder(self, selection: SelectionDescription) -> bool: - """ - Checks is contest description is placeholder - :return: true if placeholder - """ - return selection in self.placeholder_selections - - def selection_for(self, selection_id: str) -> Optional[SelectionDescription]: - """ - Gets the description for a particular id - :param selection_id: Id of Selection - :return: description - """ - matching_selections = list( - filter(lambda i: i.object_id == selection_id, self.ballot_selections) - ) - - if any(matching_selections): - return matching_selections[0] - - matching_placeholders = list( - filter(lambda i: i.object_id == selection_id, self.placeholder_selections) - ) - - if any(matching_placeholders): - return matching_placeholders[0] - return None - - -# pylint: disable=too-many-instance-attributes -@dataclass(unsafe_hash=True) -class ElectionDescription(Serializable, CryptoHashable): - """ - Use this entity for defining the structure of the election and associated - information such as candidates, contests, and vote counts. This class is - based on the NIST Election Common Standard Data Specification. Some deviations - from the standard exist. - - This structure is considered an immutable input object and should not be changed - through the course of an election, as it's hash representation is the basis for all - other hash representations within an ElectionGuard election context. - - See: https://developers.google.com/elections-data/reference/election - """ - - election_scope_id: str - spec_version: str - type: ElectionType - start_date: datetime - end_date: datetime - geopolitical_units: List[GeopoliticalUnit] - parties: List[Party] - candidates: List[Candidate] - contests: List[ContestDescription] - ballot_styles: List[BallotStyle] - name: Optional[InternationalizedText] = field(default=None) - contact_information: Optional[ContactInformation] = field(default=None) - - def __eq__(self, other: Any) -> bool: - return ( - isinstance(other, ElectionDescription) - and self.election_scope_id == other.election_scope_id - and self.type == other.type - and self.start_date == other.start_date - and self.end_date == other.end_date - and _list_eq(self.geopolitical_units, other.geopolitical_units) - and _list_eq(self.parties, other.parties) - and _list_eq(self.candidates, other.candidates) - and _list_eq(self.contests, other.contests) - and _list_eq(self.ballot_styles, other.ballot_styles) - and self.name == other.name - and self.contact_information == other.contact_information - ) - - def crypto_hash(self) -> ElementModQ: - """ - Returns a hash of the metadata components of the election - """ - - return hash_elems( - self.election_scope_id, - str(self.type.name), - to_ticks(self.start_date), - to_ticks(self.end_date), - self.name, - self.contact_information, - self.geopolitical_units, - self.parties, - self.contests, - self.ballot_styles, - ) - - def is_valid(self) -> bool: - """ - Verifies the dataset to ensure it is well-formed - """ - gp_unit_ids: Set[str] = set() - ballot_style_ids: Set[str] = set() - party_ids: Set[str] = set() - candidate_ids: Set[str] = set() - contest_ids: Set[str] = set() - - # Validate GP Units - for gp_unit in self.geopolitical_units: - if gp_unit.object_id not in gp_unit_ids: - gp_unit_ids.add(gp_unit.object_id) - - # fail if there are duplicates - geopolitical_units_valid = len(gp_unit_ids) == len(self.geopolitical_units) - - # Validate Ballot Styles - ballot_styles_have_valid_gp_unit_ids = True - - for style in self.ballot_styles: - if style.object_id not in ballot_style_ids: - ballot_style_ids.add(style.object_id) - if style.geopolitical_unit_ids is None: - ballot_styles_have_valid_gp_unit_ids = False - break - # validate associated gp unit ids - for gp_unit_id in style.geopolitical_unit_ids: - ballot_styles_have_valid_gp_unit_ids = ( - ballot_styles_have_valid_gp_unit_ids and gp_unit_id in gp_unit_ids - ) - - ballot_styles_valid = ( - len(ballot_style_ids) == len(self.ballot_styles) - and ballot_styles_have_valid_gp_unit_ids - ) - - # Validate Parties - for party in self.parties: - if party.object_id not in party_ids: - party_ids.add(party.object_id) - - parties_valid = len(party_ids) == len(self.parties) - - # Validate Candidates - candidates_have_valid_party_ids = True - - for candidate in self.candidates: - if candidate.object_id not in candidate_ids: - candidate_ids.add(candidate.object_id) - # validate the associated party id - candidates_have_valid_party_ids = candidates_have_valid_party_ids and ( - candidate.party_id is None or candidate.party_id in party_ids - ) - - candidates_have_valid_length = len(candidate_ids) == len(self.candidates) - candidates_valid = ( - candidates_have_valid_length and candidates_have_valid_party_ids - ) - - # Validate Contests - contests_validate_their_properties = True - contests_have_valid_electoral_district_id = True - candidate_contests_have_valid_party_ids = True - - contest_sequence_ids: Set[int] = set() - - for contest in self.contests: - - contests_validate_their_properties = ( - contests_validate_their_properties and contest.is_valid() - ) - - if contest.object_id not in contest_ids: - contest_ids.add(contest.object_id) - - # validate the sequence order - if contest.sequence_order not in contest_sequence_ids: - contest_sequence_ids.add(contest.sequence_order) - - # validate the associated gp unit id - contests_have_valid_electoral_district_id = ( - contests_have_valid_electoral_district_id - and contest.electoral_district_id in gp_unit_ids - ) - - if isinstance(contest, CandidateContestDescription): - candidate_contest = cast(CandidateContestDescription, contest) - if candidate_contest.primary_party_ids is not None: - for primary_party_id in candidate_contest.primary_party_ids: - # validate the party ids - candidate_contests_have_valid_party_ids = ( - candidate_contests_have_valid_party_ids - and primary_party_id in party_ids - ) - - # TODO: ISSUE #55: verify that the contest sequence order set is in the proper order - - contests_have_valid_object_ids = len(contest_ids) == len(self.contests) - contests_have_valid_sequence_ids = len(contest_sequence_ids) == len( - self.contests - ) - contests_valid = ( - contests_have_valid_object_ids - and contests_have_valid_sequence_ids - and contests_validate_their_properties - and contests_have_valid_electoral_district_id - and candidate_contests_have_valid_party_ids - ) - - success = ( - geopolitical_units_valid - and ballot_styles_valid - and parties_valid - and candidates_valid - and contests_valid - ) - - if not success: - log_warning( - "Election failed validation check: is_valid: %s", - str( - { - "geopolitical_units_valid": geopolitical_units_valid, - "ballot_styles_valid": ballot_styles_valid, - "ballot_styles_have_valid_gp_unit_ids": ballot_styles_have_valid_gp_unit_ids, - "parties_valid": parties_valid, - "candidates_valid": candidates_valid, - "candidates_have_valid_length": candidates_have_valid_length, - "candidates_have_valid_party_ids": candidates_have_valid_party_ids, - "contests_valid": contests_valid, - "contests_have_valid_object_ids": contests_have_valid_object_ids, - "contests_have_valid_sequence_ids": contests_have_valid_sequence_ids, - "contests_validate_their_properties": contests_validate_their_properties, - "contests_have_valid_electoral_district_id": contests_have_valid_electoral_district_id, - "candidate_contests_have_valid_party_ids": candidate_contests_have_valid_party_ids, - } - ), - ) - return success - - -@dataclass(eq=True, unsafe_hash=True) -class InternalElectionDescription: - """ - `InternalElectionDescription` is a subset of the `ElectionDescription` structure that specifies - the components that ElectionGuard uses for conducting an election. The key component is the - `contests` collection, which applies placeholder selections to the `ElectionDescription` contests - """ - - description: InitVar[ElectionDescription] = None - - geopolitical_units: List[GeopoliticalUnit] = field(init=False) - - contests: List[ContestDescriptionWithPlaceholders] = field(init=False) - - ballot_styles: List[BallotStyle] = field(init=False) - - description_hash: ElementModQ = field(init=False) - - def __post_init__(self, description: ElectionDescription) -> None: - object.__setattr__(self, "description_hash", description.crypto_hash()) - object.__setattr__(self, "geopolitical_units", description.geopolitical_units) - object.__setattr__(self, "ballot_styles", description.ballot_styles) - object.__setattr__( - self, "contests", self._generate_contests_with_placeholders(description) - ) - - def contest_for( - self, contest_id: str - ) -> Optional[ContestDescriptionWithPlaceholders]: - """ - Get contest by id - :param contest_id: Contest id - :return: Contest description or none - """ - matching_contests = list( - filter(lambda i: i.object_id == contest_id, self.contests) - ) - - if any(matching_contests): - return matching_contests[0] - return None - - # SUGGEST should return Optional - def get_ballot_style(self, ballot_style_id: str) -> BallotStyle: - """ - Get a ballot style for a specified ballot_style_id - """ - style = list( - filter(lambda i: i.object_id == ballot_style_id, self.ballot_styles) - )[0] - return style - - def get_contests_for( - self, ballot_style_id: str - ) -> List[ContestDescriptionWithPlaceholders]: - """ - Get contests for a ballot style - :param ballot_style_id: ballot style id - :return: contest descriptions - """ - style = self.get_ballot_style(ballot_style_id) - if style.geopolitical_unit_ids is None: - return list() - # pylint: disable=unnecessary-comprehension - gp_unit_ids = [gp_unit_id for gp_unit_id in style.geopolitical_unit_ids] - contests = list( - filter(lambda i: i.electoral_district_id in gp_unit_ids, self.contests) - ) - return contests - - @staticmethod - def _generate_contests_with_placeholders( - description: ElectionDescription, - ) -> List[ContestDescriptionWithPlaceholders]: - """ - For each contest, append the `number_elected` number - of placeholder selections to the end of the contest collection - """ - contests: List[ContestDescriptionWithPlaceholders] = list() - for contest in description.contests: - placeholder_selections = generate_placeholder_selections_from( - contest, contest.number_elected - ) - contests.append( - contest_description_with_placeholders_from( - contest, placeholder_selections - ) - ) - - return contests @dataclass(eq=True, unsafe_hash=True) @@ -915,77 +110,3 @@ def make_ciphertext_election_context( crypto_base_hash=crypto_base_hash, crypto_extended_base_hash=crypto_extended_base_hash, ) - - -def contest_description_with_placeholders_from( - description: ContestDescription, placeholders: List[SelectionDescription] -) -> ContestDescriptionWithPlaceholders: - """ - Generates a placeholder selection description - :param description: contest description - :param placeholders: list of placeholder descriptions of selections - :return: a SelectionDescription or None - """ - return ContestDescriptionWithPlaceholders( - object_id=description.object_id, - electoral_district_id=description.electoral_district_id, - sequence_order=description.sequence_order, - vote_variation=description.vote_variation, - number_elected=description.number_elected, - votes_allowed=description.votes_allowed, - name=description.name, - ballot_selections=description.ballot_selections, - ballot_title=description.ballot_title, - ballot_subtitle=description.ballot_subtitle, - placeholder_selections=placeholders, - ) - - -def generate_placeholder_selection_from( - contest: ContestDescription, use_sequence_id: Optional[int] = None -) -> Optional[SelectionDescription]: - """ - Generates a placeholder selection description that is unique so it can be hashed - - :param use_sequence_id: an optional integer unique to the contest identifying this selection's place in the contest - :return: a SelectionDescription or None - """ - sequence_ids = [selection.sequence_order for selection in contest.ballot_selections] - if use_sequence_id is None: - # if no sequence order is specified, take the max - use_sequence_id = max(*sequence_ids) + 1 - elif use_sequence_id in sequence_ids: - log_warning( - f"mismatched placeholder selection {use_sequence_id} already exists" - ) - return None - - placeholder_object_id = f"{contest.object_id}-{use_sequence_id}" - return SelectionDescription( - f"{placeholder_object_id}-placeholder", - f"{placeholder_object_id}-candidate", - use_sequence_id, - ) - - -def generate_placeholder_selections_from( - contest: ContestDescription, count: int -) -> List[SelectionDescription]: - """ - Generates the specified number of placeholder selections in - ascending sequence order from the max selection sequence orderf - - :param contest: ContestDescription for input - :param count: optionally specify a number of placeholders to generate - :return: a collection of `SelectionDescription` objects, which may be empty - """ - max_sequence_order = max( - [selection.sequence_order for selection in contest.ballot_selections] - ) - selections: List[SelectionDescription] = list() - for i in range(count): - sequence_order = max_sequence_order + 1 + i - selections.append( - get_optional(generate_placeholder_selection_from(contest, sequence_order)) - ) - return selections diff --git a/src/electionguard/election_builder.py b/src/electionguard/election_builder.py index 528e3d1c4..d7317a2de 100644 --- a/src/electionguard/election_builder.py +++ b/src/electionguard/election_builder.py @@ -3,13 +3,9 @@ from dataclasses import dataclass, field from typing import Optional, Tuple -from .election import ( - CiphertextElectionContext, - ElectionDescription, - InternalElectionDescription, - make_ciphertext_election_context, -) +from .election import CiphertextElectionContext, make_ciphertext_election_context from .group import ElementModP, ElementModQ +from .manifest import Manifest, InternalManifest from .utils import get_optional @@ -30,16 +26,16 @@ class ElectionBuilder: The quorum of guardians necessary to decrypt an election. Must be less than `number_of_guardians` """ - description: ElectionDescription + manifest: Manifest - internal_description: InternalElectionDescription = field(init=False) + internal_manifest: InternalManifest = field(init=False) elgamal_public_key: Optional[ElementModP] = field(default=None) commitment_hash: Optional[ElementModQ] = field(default=None) def __post_init__(self) -> None: - self.internal_description = InternalElectionDescription(self.description) + self.internal_manifest = InternalManifest(self.manifest) def set_public_key(self, elgamal_public_key: ElementModP) -> ElectionBuilder: """ @@ -61,24 +57,24 @@ def set_commitment_hash(self, commitment_hash: ElementModQ) -> ElectionBuilder: def build( self, - ) -> Optional[Tuple[InternalElectionDescription, CiphertextElectionContext]]: + ) -> Optional[Tuple[InternalManifest, CiphertextElectionContext]]: """ Build election - :return: election description and context or none + :return: election manifest and context or none """ - if not self.description.is_valid(): + if not self.manifest.is_valid(): return None if self.elgamal_public_key is None: return None return ( - self.internal_description, + self.internal_manifest, make_ciphertext_election_context( self.number_of_guardians, self.quorum, get_optional(self.elgamal_public_key), get_optional(self.commitment_hash), - self.description.crypto_hash(), + self.manifest.crypto_hash(), ), ) diff --git a/src/electionguard/encrypt.py b/src/electionguard/encrypt.py index 5c65b7d3c..f3058a3b7 100644 --- a/src/electionguard/encrypt.py +++ b/src/electionguard/encrypt.py @@ -13,16 +13,16 @@ make_ciphertext_ballot, ) -from .election import ( - CiphertextElectionContext, - InternalElectionDescription, +from .election import CiphertextElectionContext +from .elgamal import elgamal_encrypt +from .group import ElementModP, ElementModQ, rand_q +from .logs import log_warning +from .manifest import ( + InternalManifest, ContestDescription, ContestDescriptionWithPlaceholders, SelectionDescription, ) -from .elgamal import elgamal_encrypt -from .group import ElementModP, ElementModQ, rand_q -from .logs import log_warning from .nonces import Nonces from .serializable import Serializable from .tracker import get_hash_for_device @@ -56,17 +56,17 @@ class EncryptionMediator: It composes Elections and Ballots. """ - _metadata: InternalElectionDescription + _internal_manifest: InternalManifest _encryption: CiphertextElectionContext _seed_hash: ElementModQ def __init__( self, - election_metadata: InternalElectionDescription, + internal_manifest: InternalManifest, context: CiphertextElectionContext, encryption_device: EncryptionDevice, ): - self._metadata = election_metadata + self._internal_manifest = internal_manifest self._encryption = context self._seed_hash = encryption_device.get_hash() @@ -75,7 +75,7 @@ def encrypt(self, ballot: PlaintextBallot) -> Optional[CiphertextBallot]: Encrypt the specified ballot using the cached election context. """ encrypted_ballot = encrypt_ballot( - ballot, self._metadata, self._encryption, self._seed_hash + ballot, self._internal_manifest, self._encryption, self._seed_hash ) if encrypted_ballot is not None and encrypted_ballot.tracking_hash is not None: self._seed_hash = encrypted_ballot.tracking_hash @@ -376,7 +376,7 @@ def encrypt_contest( def encrypt_ballot( ballot: PlaintextBallot, - election_metadata: InternalElectionDescription, + internal_manifest: InternalManifest, context: CiphertextElectionContext, seed_hash: ElementModQ, nonce: Optional[ElementModQ] = None, @@ -393,7 +393,7 @@ def encrypt_ballot( It will fill missing contests with `False` selections and generate `placeholder` selections that are marked `True`. :param ballot: the ballot in the valid input form - :param election_metadata: the `InternalElectionDescription` which defines this ballot's structure + :param internal_manifest: the `InternalManifest` which defines this ballot's structure :param context: all the cryptographic context for the election :param seed_hash: Hash from previous ballot or starting hash from device :param nonce: an optional `int` used to seed the `Nonce` generated for this contest @@ -402,7 +402,7 @@ def encrypt_ballot( """ # Determine the relevant range of contests for this ballot style - style = election_metadata.get_ballot_style(ballot.style_id) + style = internal_manifest.get_ballot_style(ballot.style_id) # Validate Input if not ballot.is_valid(style.object_id): @@ -415,7 +415,7 @@ def encrypt_ballot( # Include a representation of the election and the external Id in the nonce's used # to derive other nonce values on the ballot nonce_seed = CiphertextBallot.nonce_seed( - election_metadata.description_hash, + internal_manifest.manifest_hash, ballot.object_id, random_master_nonce, ) @@ -423,7 +423,7 @@ def encrypt_ballot( encrypted_contests: List[CiphertextBallotContest] = list() # only iterate on contests for this specific ballot style - for description in election_metadata.get_contests_for(ballot.style_id): + for description in internal_manifest.get_contests_for(ballot.style_id): use_contest = None for contest in ballot.contests: if contest.object_id == description.object_id: @@ -449,7 +449,7 @@ def encrypt_ballot( encrypted_ballot = make_ciphertext_ballot( ballot.object_id, ballot.style_id, - election_metadata.description_hash, + internal_manifest.manifest_hash, seed_hash, encrypted_contests, random_master_nonce, @@ -463,7 +463,7 @@ def encrypt_ballot( # Verify the proofs if encrypted_ballot.is_valid_encryption( - election_metadata.description_hash, + internal_manifest.manifest_hash, context.elgamal_public_key, context.crypto_extended_base_hash, ): diff --git a/src/electionguard/manifest.py b/src/electionguard/manifest.py new file mode 100644 index 000000000..b3c92c6b5 --- /dev/null +++ b/src/electionguard/manifest.py @@ -0,0 +1,883 @@ +from dataclasses import dataclass, field, InitVar +from datetime import datetime +from enum import Enum, unique +from typing import cast, List, Optional, Set, Any + +from .ballot import _list_eq +from .election_object_base import ElectionObjectBase +from .group import ElementModQ +from .hash import CryptoHashable, hash_elems +from .logs import log_warning +from .serializable import Serializable +from .utils import get_optional, to_ticks + + +@unique +class ElectionType(Enum): + """ + enumerations for the `ElectionReport` entity + see: https://developers.google.com/elections-data/reference/election-type + """ + + unknown = 0 + general = 1 + partisan_primary_closed = 2 + partisan_primary_open = 3 + primary = 4 + runoff = 5 + special = 6 + other = 7 + + +@unique +class ReportingUnitType(Enum): + """ + Enumeration for the type of geopolitical unit + see: https://developers.google.com/elections-data/reference/reporting-unit-type + """ + + unknown = 0 + ballot_batch = 1 + ballot_style_area = 2 + borough = 3 + city = 4 + city_council = 5 + combined_precinct = 6 + congressional = 7 + country = 8 + county = 9 + county_council = 10 + drop_box = 11 + judicial = 12 + municipality = 13 + polling_place = 14 + precinct = 15 + school = 16 + special = 17 + split_precinct = 18 + state = 19 + state_house = 20 + state_senate = 21 + township = 22 + utility = 23 + village = 24 + vote_center = 25 + ward = 26 + water = 27 + other = 28 + + +@unique +class VoteVariationType(Enum): + """ + Enumeration for contest algorithm or rules in the `Contest` entity + see: https://developers.google.com/elections-data/reference/vote-variation + """ + + unknown = 0 + one_of_m = 1 + approval = 2 + borda = 3 + cumulative = 4 + majority = 5 + n_of_m = 6 + plurality = 7 + proportional = 8 + range = 9 + rcv = 10 + super_majority = 11 + other = 12 + + +@dataclass(eq=True, unsafe_hash=True) +class AnnotatedString(Serializable, CryptoHashable): + """ + Use this as a type for character strings. + See: https://developers.google.com/elections-data/reference/annotated-string + """ + + annotation: str = field(default="") + value: str = field(default="") + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems(self.annotation, self.value) + + +@dataclass(eq=True, unsafe_hash=True) +class Language(Serializable, CryptoHashable): + """ + The ISO-639 language + see: https://en.wikipedia.org/wiki/ISO_639 + """ + + value: str + language: str = field(default="en") + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems(self.value, self.language) + + +@dataclass(eq=True, unsafe_hash=True) +class InternationalizedText(Serializable, CryptoHashable): + """ + Data entity used to represent multi-national text. Use when text on a ballot contains multi-national text. + See: https://developers.google.com/elections-data/reference/internationalized-text + """ + + text: List[Language] = field(default_factory=lambda: []) + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems(self.text) + + +@dataclass(eq=True, unsafe_hash=True) +class ContactInformation(Serializable, CryptoHashable): + """ + For defining contact information about objects such as persons, boards of authorities, and organizations. + See: https://developers.google.com/elections-data/reference/contact-information + """ + + address_line: Optional[List[str]] = field(default=None) + email: Optional[List[AnnotatedString]] = field(default=None) + phone: Optional[List[AnnotatedString]] = field(default=None) + name: Optional[str] = field(default=None) + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems(self.name, self.address_line, self.email, self.phone) + + +@dataclass(eq=True, unsafe_hash=True) +class GeopoliticalUnit(ElectionObjectBase, CryptoHashable): + """ + Use this entity for defining geopolitical units such as cities, districts, jurisdictions, or precincts, + for the purpose of associating contests, offices, vote counts, or other information with the geographies. + See: https://developers.google.com/elections-data/reference/gp-unit + """ + + name: str + type: ReportingUnitType + contact_information: Optional[ContactInformation] = field(default=None) + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems( + self.object_id, self.name, str(self.type.name), self.contact_information + ) + + +@dataclass(eq=True, unsafe_hash=True) +class BallotStyle(ElectionObjectBase, CryptoHashable): + """ + A BallotStyle works as a key to uniquely specify a set of contests. See also `ContestDescription`. + """ + + geopolitical_unit_ids: Optional[List[str]] = field(default=None) + party_ids: Optional[List[str]] = field(default=None) + image_uri: Optional[str] = field(default=None) + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems( + self.object_id, self.geopolitical_unit_ids, self.party_ids, self.image_uri + ) + + +@dataclass(eq=True, unsafe_hash=True) +class Party(ElectionObjectBase, CryptoHashable): + """ + Use this entity to describe a political party that can then be referenced from other entities. + See: https://developers.google.com/elections-data/reference/party + """ + + name: InternationalizedText = field(default=InternationalizedText()) + abbreviation: Optional[str] = field(default=None) + color: Optional[str] = field(default=None) + logo_uri: Optional[str] = field(default=None) + + def get_party_id(self) -> str: + """ + Returns the object identifier associated with the Party. + """ + return self.object_id + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems( + self.object_id, + self.name, + self.abbreviation, + self.color, + self.logo_uri, + ) + + +@dataclass(eq=True, unsafe_hash=True) +class Candidate(ElectionObjectBase, CryptoHashable): + """ + Entity describing information about a candidate in a contest. + See: https://developers.google.com/elections-data/reference/candidate + Note: The ElectionGuard Data Spec deviates from the NIST model in that + selections for any contest type are considered a "candidate". + for instance, on a yes-no referendum contest, two `candidate` objects + would be included in the model to represent the `affirmative` and `negative` + selections for the contest. See the wiki, readme's, and tests in this repo for more info + """ + + name: InternationalizedText = field(default=InternationalizedText()) + party_id: Optional[str] = field(default=None) + image_uri: Optional[str] = field(default=None) + is_write_in: Optional[bool] = field(default=None) + + def get_candidate_id(self) -> str: + """ + Given a `Candidate`, returns a "candidate ID", which is used in other ElectionGuard structures. + """ + return self.object_id + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems(self.object_id, self.name, self.party_id, self.image_uri) + + +@dataclass(eq=True, unsafe_hash=True) +class SelectionDescription(ElectionObjectBase, CryptoHashable): + """ + Data entity for the ballot selections in a contest, + for example linking candidates and parties to their vote counts. + See: https://developers.google.com/elections-data/reference/ballot-selection + Note: The ElectionGuard Data Spec deviates from the NIST model in that + there is no difference for different types of selections. + The ElectionGuard Data Spec deviates from the NIST model in that + `sequence_order` is a required field since it is used for ordering selections + in a contest to ensure various encryption primitives are deterministic. + For a given election, the sequence of selections displayed to a user may be different + however that information is not captured by default when encrypting a specific ballot. + """ + + candidate_id: str + sequence_order: int + """ + Used for ordering selections in a contest to ensure various encryption primitives are deterministic. + The sequence order must be unique and should be representative of how the contests are represnted + on a "master" ballot in an external system. The sequence order is not required to be in the order + in which they are displayed to a voter. Any acceptable range of integer values may be provided. + """ + + def crypto_hash(self) -> ElementModQ: + """ + A hash representation of the object + """ + return hash_elems(self.object_id, self.sequence_order, self.candidate_id) + + +# pylint: disable=too-many-instance-attributes +@dataclass(unsafe_hash=True) +class ContestDescription(ElectionObjectBase, CryptoHashable): + """ + Use this data entity for describing a contest and linking the contest + to the associated candidates and parties. + See: https://developers.google.com/elections-data/reference/contest + Note: The ElectionGuard Data Spec deviates from the NIST model in that + `sequence_order` is a required field since it is used for ordering selections + in a contest to ensure various encryption primitives are deterministic. + For a given election, the sequence of contests displayed to a user may be different + however that information is not captured by default when encrypting a specific ballot. + """ + + electoral_district_id: str + sequence_order: int + """ + Used for ordering contests in a ballot to ensure various encryption primitives are deterministic. + The sequence order must be unique and should be representative of how the contests are represnted + on a "master" ballot in an external system. The sequence order is not required to be in the order + in which they are displayed to a voter. Any acceptable range of integer values may be provided. + """ + + vote_variation: VoteVariationType + + # Number of candidates that are elected in the contest ("n" of n-of-m). + # Note: a referendum is considered a specific case of 1-of-m in ElectionGuard + number_elected: int + + # Maximum number of votes/write-ins per voter in this contest. Used in cumulative voting + # to indicate how many total votes a voter can spread around. In n-of-m elections, this will + # be None. + votes_allowed: Optional[int] + + # Name of the contest, not necessarily as it appears on the ballot. + name: str + + # For associating a ballot selection for the contest, i.e., a candidate, a ballot measure. + ballot_selections: List[SelectionDescription] = field(default_factory=lambda: []) + + # Title of the contest as it appears on the ballot. + ballot_title: Optional[InternationalizedText] = field(default=None) + + # Subtitle of the contest as it appears on the ballot. + ballot_subtitle: Optional[InternationalizedText] = field(default=None) + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, ContestDescription) + and self.electoral_district_id == other.electoral_district_id + and self.sequence_order == other.sequence_order + and self.vote_variation == other.vote_variation + and self.number_elected == other.number_elected + and self.votes_allowed == other.votes_allowed + and self.name == other.name + and _list_eq(self.ballot_selections, other.ballot_selections) + and self.ballot_title == other.ballot_title + and self.ballot_subtitle == other.ballot_subtitle + ) + + def crypto_hash(self) -> ElementModQ: + """ + Given a ContestDescription, deterministically derives a "hash" of that contest, + suitable for use in ElectionGuard's "base hash" values, and for validating that + either a plaintext or encrypted voted context and its corresponding contest + description match up. + """ + # remove any placeholders from the hash mechanism + return hash_elems( + self.object_id, + self.sequence_order, + self.electoral_district_id, + str(self.vote_variation.name), + self.ballot_title, + self.ballot_subtitle, + self.name, + self.number_elected, + self.votes_allowed, + self.ballot_selections, + ) + + def is_valid(self) -> bool: + """ + Check the validity of the contest object by verifying its data + """ + contest_has_valid_number_elected = self.number_elected <= len( + self.ballot_selections + ) + contest_has_valid_votes_allowed = ( + self.votes_allowed is None or self.number_elected <= self.votes_allowed + ) + + # verify the candidate_ids, selection object_ids, and sequence_ids are unique + candidate_ids: Set[str] = set() + selection_ids: Set[str] = set() + sequence_ids: Set[int] = set() + selection_count = 0 + expected_selection_count = len(self.ballot_selections) + + for selection in self.ballot_selections: + selection_count += 1 + # validate the object_id + if selection.object_id not in selection_ids: + selection_ids.add(selection.object_id) + # validate the sequence_order + if selection.sequence_order not in sequence_ids: + sequence_ids.add(selection.sequence_order) + # validate the candidate id + if selection.candidate_id not in candidate_ids: + candidate_ids.add(selection.candidate_id) + + selections_have_valid_candidate_ids = ( + len(candidate_ids) == expected_selection_count + ) + selections_have_valid_selection_ids = ( + len(selection_ids) == expected_selection_count + ) + selections_have_valid_sequence_ids = ( + len(sequence_ids) == expected_selection_count + ) + + success = ( + contest_has_valid_number_elected + and contest_has_valid_votes_allowed + and selections_have_valid_candidate_ids + and selections_have_valid_selection_ids + and selections_have_valid_sequence_ids + ) + + if not success: + log_warning( + "Contest %s failed validation check: %s", + self.object_id, + str( + { + "contest_has_valid_number_elected": contest_has_valid_number_elected, + "contest_has_valid_votes_allowed": contest_has_valid_votes_allowed, + "selections_have_valid_candidate_ids": selections_have_valid_candidate_ids, + "selections_have_valid_selection_ids": selections_have_valid_selection_ids, + "selections_have_valid_sequence_ids": selections_have_valid_sequence_ids, + } + ), + ) + + return success + + +@dataclass(eq=True, unsafe_hash=True) +class CandidateContestDescription(ContestDescription): + """ + Use this entity to describe a contest that involves selecting one or more candidates. + See: https://developers.google.com/elections-data/reference/contest + Note: The ElectionGuard Data Spec deviates from the NIST model in that + this subclass is used purely for convenience + """ + + primary_party_ids: List[str] = field(default_factory=lambda: []) + + +@dataclass(eq=True, unsafe_hash=True) +class ReferendumContestDescription(ContestDescription): + """ + Use this entity to describe a contest that involves selecting exactly one 'candidate'. + See: https://developers.google.com/elections-data/reference/contest + Note: The ElectionGuard Data Spec deviates from the NIST model in that + this subclass is used purely for convenience + """ + + +@dataclass(eq=True, unsafe_hash=True) +class ContestDescriptionWithPlaceholders(ContestDescription): + """ + ContestDescriptionWithPlaceholders is a `ContestDescription` with ElectionGuard `placeholder_selections`. + (The ElectionGuard spec requires for n-of-m elections that there be *exactly* n counters that are one + with the rest zero, so if a voter deliberately undervotes, one or more of the placeholder counters will + become one. This allows the `ConstantChaumPedersenProof` to verify correctly for undervoted contests.) + """ + + placeholder_selections: List[SelectionDescription] = field( + default_factory=lambda: [] + ) + + def is_valid(self) -> bool: + """ + Checks is contest description is valid + :return: true if valid + """ + contest_description_validates = super().is_valid() + return ( + contest_description_validates + and len(self.placeholder_selections) == self.number_elected + ) + + def is_placeholder(self, selection: SelectionDescription) -> bool: + """ + Checks is contest description is placeholder + :return: true if placeholder + """ + return selection in self.placeholder_selections + + def selection_for(self, selection_id: str) -> Optional[SelectionDescription]: + """ + Gets the description for a particular id + :param selection_id: Id of Selection + :return: description + """ + matching_selections = list( + filter(lambda i: i.object_id == selection_id, self.ballot_selections) + ) + + if any(matching_selections): + return matching_selections[0] + + matching_placeholders = list( + filter(lambda i: i.object_id == selection_id, self.placeholder_selections) + ) + + if any(matching_placeholders): + return matching_placeholders[0] + return None + + +# pylint: disable=too-many-instance-attributes +@dataclass(unsafe_hash=True) +class Manifest(Serializable, CryptoHashable): + """ + Use this entity for defining the structure of the election and associated + information such as candidates, contests, and vote counts. This class is + based on the NIST Election Common Standard Data Specification. Some deviations + from the standard exist. + + This structure is considered an immutable input object and should not be changed + through the course of an election, as it's hash representation is the basis for all + other hash representations within an ElectionGuard election context. + + See: https://developers.google.com/elections-data/reference/election + """ + + election_scope_id: str + spec_version: str + type: ElectionType + start_date: datetime + end_date: datetime + geopolitical_units: List[GeopoliticalUnit] + parties: List[Party] + candidates: List[Candidate] + contests: List[ContestDescription] + ballot_styles: List[BallotStyle] + name: Optional[InternationalizedText] = field(default=None) + contact_information: Optional[ContactInformation] = field(default=None) + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, Manifest) + and self.election_scope_id == other.election_scope_id + and self.type == other.type + and self.start_date == other.start_date + and self.end_date == other.end_date + and _list_eq(self.geopolitical_units, other.geopolitical_units) + and _list_eq(self.parties, other.parties) + and _list_eq(self.candidates, other.candidates) + and _list_eq(self.contests, other.contests) + and _list_eq(self.ballot_styles, other.ballot_styles) + and self.name == other.name + and self.contact_information == other.contact_information + ) + + def crypto_hash(self) -> ElementModQ: + """ + Returns a hash of the metadata components of the election + """ + + return hash_elems( + self.election_scope_id, + str(self.type.name), + to_ticks(self.start_date), + to_ticks(self.end_date), + self.name, + self.contact_information, + self.geopolitical_units, + self.parties, + self.contests, + self.ballot_styles, + ) + + def is_valid(self) -> bool: + """ + Verifies the dataset to ensure it is well-formed + """ + gp_unit_ids: Set[str] = set() + ballot_style_ids: Set[str] = set() + party_ids: Set[str] = set() + candidate_ids: Set[str] = set() + contest_ids: Set[str] = set() + + # Validate GP Units + for gp_unit in self.geopolitical_units: + if gp_unit.object_id not in gp_unit_ids: + gp_unit_ids.add(gp_unit.object_id) + + # fail if there are duplicates + geopolitical_units_valid = len(gp_unit_ids) == len(self.geopolitical_units) + + # Validate Ballot Styles + ballot_styles_have_valid_gp_unit_ids = True + + for style in self.ballot_styles: + if style.object_id not in ballot_style_ids: + ballot_style_ids.add(style.object_id) + if style.geopolitical_unit_ids is None: + ballot_styles_have_valid_gp_unit_ids = False + break + # validate associated gp unit ids + for gp_unit_id in style.geopolitical_unit_ids: + ballot_styles_have_valid_gp_unit_ids = ( + ballot_styles_have_valid_gp_unit_ids and gp_unit_id in gp_unit_ids + ) + + ballot_styles_valid = ( + len(ballot_style_ids) == len(self.ballot_styles) + and ballot_styles_have_valid_gp_unit_ids + ) + + # Validate Parties + for party in self.parties: + if party.object_id not in party_ids: + party_ids.add(party.object_id) + + parties_valid = len(party_ids) == len(self.parties) + + # Validate Candidates + candidates_have_valid_party_ids = True + + for candidate in self.candidates: + if candidate.object_id not in candidate_ids: + candidate_ids.add(candidate.object_id) + # validate the associated party id + candidates_have_valid_party_ids = candidates_have_valid_party_ids and ( + candidate.party_id is None or candidate.party_id in party_ids + ) + + candidates_have_valid_length = len(candidate_ids) == len(self.candidates) + candidates_valid = ( + candidates_have_valid_length and candidates_have_valid_party_ids + ) + + # Validate Contests + contests_validate_their_properties = True + contests_have_valid_electoral_district_id = True + candidate_contests_have_valid_party_ids = True + + contest_sequence_ids: Set[int] = set() + + for contest in self.contests: + + contests_validate_their_properties = ( + contests_validate_their_properties and contest.is_valid() + ) + + if contest.object_id not in contest_ids: + contest_ids.add(contest.object_id) + + # validate the sequence order + if contest.sequence_order not in contest_sequence_ids: + contest_sequence_ids.add(contest.sequence_order) + + # validate the associated gp unit id + contests_have_valid_electoral_district_id = ( + contests_have_valid_electoral_district_id + and contest.electoral_district_id in gp_unit_ids + ) + + if isinstance(contest, CandidateContestDescription): + candidate_contest = cast(CandidateContestDescription, contest) + if candidate_contest.primary_party_ids is not None: + for primary_party_id in candidate_contest.primary_party_ids: + # validate the party ids + candidate_contests_have_valid_party_ids = ( + candidate_contests_have_valid_party_ids + and primary_party_id in party_ids + ) + + # TODO: ISSUE #55: verify that the contest sequence order set is in the proper order + + contests_have_valid_object_ids = len(contest_ids) == len(self.contests) + contests_have_valid_sequence_ids = len(contest_sequence_ids) == len( + self.contests + ) + contests_valid = ( + contests_have_valid_object_ids + and contests_have_valid_sequence_ids + and contests_validate_their_properties + and contests_have_valid_electoral_district_id + and candidate_contests_have_valid_party_ids + ) + + success = ( + geopolitical_units_valid + and ballot_styles_valid + and parties_valid + and candidates_valid + and contests_valid + ) + + if not success: + log_warning( + "Election failed validation check: is_valid: %s", + str( + { + "geopolitical_units_valid": geopolitical_units_valid, + "ballot_styles_valid": ballot_styles_valid, + "ballot_styles_have_valid_gp_unit_ids": ballot_styles_have_valid_gp_unit_ids, + "parties_valid": parties_valid, + "candidates_valid": candidates_valid, + "candidates_have_valid_length": candidates_have_valid_length, + "candidates_have_valid_party_ids": candidates_have_valid_party_ids, + "contests_valid": contests_valid, + "contests_have_valid_object_ids": contests_have_valid_object_ids, + "contests_have_valid_sequence_ids": contests_have_valid_sequence_ids, + "contests_validate_their_properties": contests_validate_their_properties, + "contests_have_valid_electoral_district_id": contests_have_valid_electoral_district_id, + "candidate_contests_have_valid_party_ids": candidate_contests_have_valid_party_ids, + } + ), + ) + return success + + +@dataclass(eq=True, unsafe_hash=True) +class InternalManifest: + """ + `InternalManifest` is a subset of the `Manifest` structure that specifies + the components that ElectionGuard uses for conducting an election. The key component is the + `contests` collection, which applies placeholder selections to the `Manifest` contests + """ + + manifest: InitVar[Manifest] = None + + geopolitical_units: List[GeopoliticalUnit] = field(init=False) + + contests: List[ContestDescriptionWithPlaceholders] = field(init=False) + + ballot_styles: List[BallotStyle] = field(init=False) + + manifest_hash: ElementModQ = field(init=False) + + def __post_init__(self, manifest: Manifest) -> None: + object.__setattr__(self, "manifest_hash", manifest.crypto_hash()) + object.__setattr__(self, "geopolitical_units", manifest.geopolitical_units) + object.__setattr__(self, "ballot_styles", manifest.ballot_styles) + object.__setattr__( + self, "contests", self._generate_contests_with_placeholders(manifest) + ) + + def contest_for( + self, contest_id: str + ) -> Optional[ContestDescriptionWithPlaceholders]: + """ + Get contest by id + :param contest_id: Contest id + :return: Contest description or none + """ + matching_contests = list( + filter(lambda i: i.object_id == contest_id, self.contests) + ) + + if any(matching_contests): + return matching_contests[0] + return None + + def get_ballot_style(self, ballot_style_id: str) -> BallotStyle: + """ + Get a ballot style for a specified ballot_style_id + """ + style = list( + filter(lambda i: i.object_id == ballot_style_id, self.ballot_styles) + )[0] + return style + + def get_contests_for( + self, ballot_style_id: str + ) -> List[ContestDescriptionWithPlaceholders]: + """ + Get contests for a ballot style + :param ballot_style_id: ballot style id + :return: contest descriptions + """ + style = self.get_ballot_style(ballot_style_id) + if style.geopolitical_unit_ids is None: + return list() + # pylint: disable=unnecessary-comprehension + gp_unit_ids = [gp_unit_id for gp_unit_id in style.geopolitical_unit_ids] + contests = list( + filter(lambda i: i.electoral_district_id in gp_unit_ids, self.contests) + ) + return contests + + @staticmethod + def _generate_contests_with_placeholders( + manifest: Manifest, + ) -> List[ContestDescriptionWithPlaceholders]: + """ + For each contest, append the `number_elected` number + of placeholder selections to the end of the contest collection + """ + contests: List[ContestDescriptionWithPlaceholders] = list() + for contest in manifest.contests: + placeholder_selections = generate_placeholder_selections_from( + contest, contest.number_elected + ) + contests.append( + contest_description_with_placeholders_from( + contest, placeholder_selections + ) + ) + + return contests + + +def contest_description_with_placeholders_from( + description: ContestDescription, placeholders: List[SelectionDescription] +) -> ContestDescriptionWithPlaceholders: + """ + Generates a placeholder selection description + :param description: contest description + :param placeholders: list of placeholder descriptions of selections + :return: a SelectionDescription or None + """ + return ContestDescriptionWithPlaceholders( + object_id=description.object_id, + electoral_district_id=description.electoral_district_id, + sequence_order=description.sequence_order, + vote_variation=description.vote_variation, + number_elected=description.number_elected, + votes_allowed=description.votes_allowed, + name=description.name, + ballot_selections=description.ballot_selections, + ballot_title=description.ballot_title, + ballot_subtitle=description.ballot_subtitle, + placeholder_selections=placeholders, + ) + + +def generate_placeholder_selection_from( + contest: ContestDescription, use_sequence_id: Optional[int] = None +) -> Optional[SelectionDescription]: + """ + Generates a placeholder selection description that is unique so it can be hashed + + :param use_sequence_id: an optional integer unique to the contest identifying this selection's place in the contest + :return: a SelectionDescription or None + """ + sequence_ids = [selection.sequence_order for selection in contest.ballot_selections] + if use_sequence_id is None: + # if no sequence order is specified, take the max + use_sequence_id = max(*sequence_ids) + 1 + elif use_sequence_id in sequence_ids: + log_warning( + f"mismatched placeholder selection {use_sequence_id} already exists" + ) + return None + + placeholder_object_id = f"{contest.object_id}-{use_sequence_id}" + return SelectionDescription( + f"{placeholder_object_id}-placeholder", + f"{placeholder_object_id}-candidate", + use_sequence_id, + ) + + +def generate_placeholder_selections_from( + contest: ContestDescription, count: int +) -> List[SelectionDescription]: + """ + Generates the specified number of placeholder selections in + ascending sequence order from the max selection sequence orderf + + :param contest: ContestDescription for input + :param count: optionally specify a number of placeholders to generate + :return: a collection of `SelectionDescription` objects, which may be empty + """ + max_sequence_order = max( + [selection.sequence_order for selection in contest.ballot_selections] + ) + selections: List[SelectionDescription] = list() + for i in range(count): + sequence_order = max_sequence_order + 1 + i + selections.append( + get_optional(generate_placeholder_selection_from(contest, sequence_order)) + ) + return selections diff --git a/src/electionguard/publish.py b/src/electionguard/publish.py index 297e91775..49ba8cffa 100644 --- a/src/electionguard/publish.py +++ b/src/electionguard/publish.py @@ -3,14 +3,15 @@ from .ballot import PlaintextBallot, CiphertextBallot, SubmittedBallot from .guardian import Guardian -from .election import CiphertextElectionContext, ElectionConstants, ElectionDescription +from .election import CiphertextElectionContext, ElectionConstants from .encrypt import EncryptionDevice from .key_ceremony import CoefficientValidationSet +from .manifest import Manifest from .tally import PlaintextTally, PublishedCiphertextTally from .utils import make_directory RESULTS_DIR = "results" -DESCRIPTION_FILE_NAME = "description" +MANIFEST_FILE_NAME = "manifest" CONTEXT_FILE_NAME = "context" CONSTANTS_FILE_NAME = "constants" ENCRYPTED_TALLY_FILE_NAME = "encrypted_tally" @@ -25,7 +26,7 @@ # TODO #148 Revert PlaintextTally to PublishedPlaintextTally after moving spoiled info def publish( - description: ElectionDescription, + manifest: Manifest, context: CiphertextElectionContext, constants: ElectionConstants, devices: Iterable[EncryptionDevice], @@ -44,7 +45,7 @@ def publish( spoiled_directory = path.join(results_directory, "spoiled_ballots") make_directory(results_directory) - description.to_json_file(DESCRIPTION_FILE_NAME, results_directory) + manifest.to_json_file(MANIFEST_FILE_NAME, results_directory) context.to_json_file(CONTEXT_FILE_NAME, results_directory) constants.to_json_file(CONSTANTS_FILE_NAME, results_directory) diff --git a/src/electionguard/tally.py b/src/electionguard/tally.py index abd75a0c0..5d40c5401 100644 --- a/src/electionguard/tally.py +++ b/src/electionguard/tally.py @@ -12,11 +12,12 @@ from .data_store import DataStore from .ballot_validator import ballot_is_valid_for_election from .decryption_share import CiphertextDecryptionSelection -from .election import CiphertextElectionContext, InternalElectionDescription +from .election import CiphertextElectionContext from .election_object_base import ElectionObjectBase from .elgamal import ElGamalCiphertext, elgamal_add from .group import ElementModQ, ONE_MOD_P, ElementModP from .logs import log_warning +from .manifest import InternalManifest from .scheduler import Scheduler from .types import BALLOT_ID, CONTEST_ID, SELECTION_ID @@ -188,7 +189,7 @@ class CiphertextTally(ElectionObjectBase, Container, Sized): A `CiphertextTally` accepts cast and spoiled ballots and accumulates a tally on the cast ballots """ - _metadata: InternalElectionDescription + _internal_manifest: InternalManifest _encryption: CiphertextElectionContext # A local cache of ballots id's that have already been cast @@ -205,7 +206,7 @@ def __post_init__(self) -> None: object.__setattr__(self, "_cast_ballot_ids", set()) object.__setattr__(self, "_spoiled_ballot_ids", set()) object.__setattr__( - self, "contests", self._build_tally_collection(self._metadata) + self, "contests", self._build_tally_collection(self._internal_manifest) ) def __len__(self) -> int: @@ -237,7 +238,9 @@ def append( log_warning(f"append cannot add {ballot.object_id} that is already tallied") return False - if not ballot_is_valid_for_election(ballot, self._metadata, self._encryption): + if not ballot_is_valid_for_election( + ballot, self._internal_manifest, self._encryption + ): return False if ballot.state == BallotBoxState.CAST: @@ -264,7 +267,7 @@ def batch_append( # get the value of the dict ballot_value = ballot[1] if not self.__contains__(ballot) and ballot_is_valid_for_election( - ballot_value, self._metadata, self._encryption + ballot_value, self._internal_manifest, self._encryption ): if ballot_value.state == BallotBoxState.CAST: @@ -352,14 +355,14 @@ def _add_spoiled(self, ballot: SubmittedBallot) -> bool: @staticmethod def _build_tally_collection( - description: InternalElectionDescription, + internal_manifest: InternalManifest, ) -> Dict[CONTEST_ID, CiphertextTallyContest]: """ - Build the object graph for the tally from the InternalElectionDescription + Build the object graph for the tally from the InternalManifest """ cast_collection: Dict[str, CiphertextTallyContest] = {} - for contest in description.contests: + for contest in internal_manifest.contests: # build a collection of valid selections for the contest description # note: we explicitly ignore the Placeholder Selections. contest_selections: Dict[str, CiphertextTallySelection] = {} @@ -430,7 +433,7 @@ def tally_ballot( def tally_ballots( store: DataStore, - metadata: InternalElectionDescription, + internal_manifest: InternalManifest, context: CiphertextElectionContext, ) -> Optional[CiphertextTally]: """ @@ -438,7 +441,9 @@ def tally_ballots( :return: a CiphertextTally or None if there is an error """ # TODO: ISSUE #14: unique Id for the tally - tally: CiphertextTally = CiphertextTally("election-results", metadata, context) + tally: CiphertextTally = CiphertextTally( + "election-results", internal_manifest, context + ) if tally.batch_append(store): return tally return None diff --git a/src/electionguardtest/ballot_factory.py b/src/electionguardtest/ballot_factory.py index 57a29ef2a..7e31567be 100644 --- a/src/electionguardtest/ballot_factory.py +++ b/src/electionguardtest/ballot_factory.py @@ -19,14 +19,13 @@ PlaintextBallotContest, PlaintextBallotSelection, ) - -from electionguard.election import ( +from electionguard.encrypt import selection_from +from electionguard.manifest import ( ContestDescription, SelectionDescription, - InternalElectionDescription, + InternalManifest, ) -from electionguard.encrypt import selection_from _T = TypeVar("_T") _DrawType = Callable[[SearchStrategy[_T]], _T] @@ -90,7 +89,7 @@ def get_random_contest_from( def get_fake_ballot( self, - election: InternalElectionDescription, + internal_manifest: InternalManifest, ballot_id: str = None, with_trues=True, ) -> PlaintextBallot: @@ -102,29 +101,31 @@ def get_fake_ballot( ballot_id = "some-unique-ballot-id-123" contests: List[PlaintextBallotContest] = [] - for contest in election.get_contests_for(election.ballot_styles[0].object_id): + for contest in internal_manifest.get_contests_for( + internal_manifest.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 + ballot_id, internal_manifest.ballot_styles[0].object_id, contests ) return fake_ballot def generate_fake_plaintext_ballots_for_election( - self, election: InternalElectionDescription, number_of_ballots: int + self, internal_manifest: InternalManifest, 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] + style_index = randint(0, len(internal_manifest.ballot_styles) - 1) + ballot_style = internal_manifest.ballot_styles[style_index] ballot_id = f"ballot-{uuid.uuid1()}" contests: List[PlaintextBallotContest] = [] - for contest in election.get_contests_for(ballot_style.object_id): + for contest in internal_manifest.get_contests_for(ballot_style.object_id): contests.append( self.get_random_contest_from(contest, Random(), with_trues=True) ) diff --git a/src/electionguardtest/election.py b/src/electionguardtest/election.py index 1b82ff991..2d4dbbb76 100644 --- a/src/electionguardtest/election.py +++ b/src/electionguardtest/election.py @@ -18,6 +18,12 @@ from electionguard.ballot import PlaintextBallotContest, PlaintextBallot from electionguard.election import ( + CiphertextElectionContext, + make_ciphertext_election_context, +) +from electionguard.encrypt import selection_from +from electionguard.group import ElementModQ +from electionguard.manifest import ( Candidate, ElectionType, ReportingUnitType, @@ -31,17 +37,15 @@ Party, CandidateContestDescription, ReferendumContestDescription, - ElectionDescription, - InternalElectionDescription, - CiphertextElectionContext, SelectionDescription, ContestDescription, - make_ciphertext_election_context, + Manifest, + InternalManifest, ) -from electionguard.encrypt import selection_from -from electionguard.group import ElementModQ -from electionguardtest.group import elements_mod_q_no_zero + from electionguardtest.elgamal import elgamal_keypairs +from electionguardtest.group import elements_mod_q_no_zero + _T = TypeVar("_T") _DrawType = Callable[[SearchStrategy[_T]], _T] @@ -506,7 +510,7 @@ def election_descriptions( draw: _DrawType, max_num_parties: int = 3, max_num_contests: int = 3 ): """ - Generates an `ElectionDescription` -- the top-level object describing an election. + Generates a `Manifest` -- the top-level object describing an election. :param draw: Hidden argument, used by Hypothesis. :param max_num_parties: The largest number of parties that will be generated (default: 3) :param max_num_contests: The largest number of contests that will be generated (default: 3) @@ -539,7 +543,7 @@ def election_descriptions( start_date = draw(datetimes()) end_date = start_date - return ElectionDescription( + return Manifest( election_scope_id=draw(emails()), spec_version="v0.95", type=ElectionType.general, # good enough for now @@ -557,35 +561,37 @@ def election_descriptions( @composite def plaintext_voted_ballots( - draw: _DrawType, metadata: InternalElectionDescription, count: int = 1 + draw: _DrawType, internal_manifest: InternalManifest, count: int = 1 ): """ Given """ if count == 1: - return draw(plaintext_voted_ballot(metadata)) + return draw(plaintext_voted_ballot(internal_manifest)) ballots: List[PlaintextBallot] = [] for _i in range(count): - ballots.append(draw(plaintext_voted_ballot(metadata))) + ballots.append(draw(plaintext_voted_ballot(internal_manifest))) return ballots @composite -def plaintext_voted_ballot(draw: _DrawType, metadata: InternalElectionDescription): +def plaintext_voted_ballot(draw: _DrawType, internal_manifest: InternalManifest): """ - Given an `InternalElectionDescription` object, generates an arbitrary `PlaintextBallot` with the + Given an `InternalManifest` object, generates an arbitrary `PlaintextBallot` with the choices made randomly. :param draw: Hidden argument, used by Hypothesis. - :param metadata: Any `InternalElectionDescription` + :param internal_manifest: Any `InternalManifest` """ - num_ballot_styles = len(metadata.ballot_styles) + num_ballot_styles = len(internal_manifest.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))] + ballot_style = internal_manifest.ballot_styles[ + draw(integers(0, num_ballot_styles - 1)) + ] - contests = metadata.get_contests_for(ballot_style.object_id) + contests = internal_manifest.get_contests_for(ballot_style.object_id) assert len(contests) > 0, "invalid ballot style with no contests in it" voted_contests: List[PlaintextBallotContest] = [] @@ -620,14 +626,14 @@ def plaintext_voted_ballot(draw: _DrawType, metadata: InternalElectionDescriptio @composite -def ciphertext_elections(draw: _DrawType, election_description: ElectionDescription): +def ciphertext_elections(draw: _DrawType, manifest: Manifest): """ Generates a `CiphertextElectionContext` with a single public-private key pair as the encryption context. In a real election, the key ceremony would be used to generate a shared public key. :param draw: Hidden argument, used by Hypothesis. - :param election_description: An `ElectionDescription` object, with + :param manifest: An `Manifest` object, with which the `CiphertextElectionContext` will be associated :return: a tuple of a `CiphertextElectionContext` and the secret key associated with it """ @@ -640,15 +646,15 @@ def ciphertext_elections(draw: _DrawType, election_description: ElectionDescript quorum=1, elgamal_public_key=public_key, commitment_hash=commitment_hash, - description_hash=election_description.crypto_hash(), + description_hash=manifest.crypto_hash(), ), ) return ciphertext_election_with_secret ELECTIONS_AND_BALLOTS_TUPLE_TYPE = Tuple[ - ElectionDescription, - InternalElectionDescription, + Manifest, + InternalManifest, List[PlaintextBallot], ElementModQ, CiphertextElectionContext, @@ -664,23 +670,22 @@ def elections_and_ballots(draw: _DrawType, num_ballots: int = 3): :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, + :reeturn: a tuple of: an `InternalManifest`, 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) + manifest = draw(election_descriptions()) + internal_manifest = InternalManifest(manifest) ballots = [ - draw(plaintext_voted_ballots(internal_election_description)) - for _ in range(num_ballots) + draw(plaintext_voted_ballots(internal_manifest)) for _ in range(num_ballots) ] - secret_key, context = draw(ciphertext_elections(election_description)) + secret_key, context = draw(ciphertext_elections(manifest)) mock_election: ELECTIONS_AND_BALLOTS_TUPLE_TYPE = ( - election_description, - internal_election_description, + manifest, + internal_manifest, ballots, secret_key, context, diff --git a/src/electionguardtest/election_factory.py b/src/electionguardtest/election_factory.py index c120866e9..6eb910862 100644 --- a/src/electionguardtest/election_factory.py +++ b/src/electionguardtest/election_factory.py @@ -12,14 +12,18 @@ ) from electionguard.ballot import PlaintextBallot - -from electionguard.election import ( +from electionguard.election import CiphertextElectionContext, ElectionConstants +from electionguard.election_builder import ElectionBuilder +from electionguard.encrypt import contest_from +from electionguard.group import ElementModP, TWO_MOD_Q +from electionguard.guardian import Guardian +from electionguard.key_ceremony import CoefficientValidationSet +from electionguard.key_ceremony_mediator import KeyCeremonyMediator +from electionguard.manifest import ( BallotStyle, - CiphertextElectionContext, - ElectionConstants, - ElectionDescription, + Manifest, ElectionType, - InternalElectionDescription, + InternalManifest, generate_placeholder_selections_from, GeopoliticalUnit, Candidate, @@ -32,15 +36,6 @@ CandidateContestDescription, ReferendumContestDescription, ) - -from electionguard.election_builder import ElectionBuilder - -from electionguard.encrypt import contest_from - -from electionguard.group import ElementModP, TWO_MOD_Q -from electionguard.guardian import Guardian -from electionguard.key_ceremony import CoefficientValidationSet -from electionguard.key_ceremony_mediator import KeyCeremonyMediator from electionguard.utils import get_optional _T = TypeVar("_T") @@ -56,8 +51,8 @@ class AllPublicElectionData: """All public data for election""" - description: ElectionDescription - metadata: InternalElectionDescription + manifest: Manifest + internal_manifest: InternalManifest context: CiphertextElectionContext constants: ElectionConstants guardians: List[CoefficientValidationSet] @@ -75,28 +70,28 @@ class ElectionFactory: simple_election_manifest_file_name = "election_manifest_simple.json" - def get_simple_election_from_file(self) -> ElectionDescription: - return self._get_election_from_file(self.simple_election_manifest_file_name) + def get_simple_manifest_from_file(self) -> Manifest: + return self._get_manifest_from_file(self.simple_election_manifest_file_name) @staticmethod - def get_hamilton_election_from_file() -> ElectionDescription: + def get_hamilton_manifest_from_file() -> Manifest: with open( os.path.join(data, "hamilton-county", "election_manifest.json"), "r" ) as subject: result = subject.read() - target = ElectionDescription.from_json(result) + target = Manifest.from_json(result) return target - def get_hamilton_election_with_encryption_context( + def get_hamilton_manifest_with_encryption_context( self, ) -> Tuple[AllPublicElectionData, AllPrivateElectionData]: guardians: List[Guardian] = [] coefficient_validation_sets: List[CoefficientValidationSet] = [] # Configure the election builder - description = self.get_hamilton_election_from_file() - builder = ElectionBuilder(NUMBER_OF_GUARDIANS, QUORUM, description) + manifest = self.get_hamilton_manifest_from_file() + builder = ElectionBuilder(NUMBER_OF_GUARDIANS, QUORUM, manifest) # Setup Guardians for i in range(NUMBER_OF_GUARDIANS): @@ -127,20 +122,24 @@ def get_hamilton_election_with_encryption_context( builder.set_public_key(get_optional(joint_key).joint_public_key) builder.set_commitment_hash(get_optional(joint_key).commitment_hash) - metadata, context = get_optional(builder.build()) + internal_manifest, context = get_optional(builder.build()) constants = ElectionConstants() return ( AllPublicElectionData( - description, metadata, context, constants, coefficient_validation_sets + manifest, + internal_manifest, + context, + constants, + coefficient_validation_sets, ), AllPrivateElectionData(guardians), ) @staticmethod - def get_fake_election() -> ElectionDescription: + def get_fake_manifest() -> Manifest: """ - Get a single Fake Election object that is manually constructed with default values + Get a single fake manifest object that is manually constructed with default values """ fake_ballot_style = BallotStyle("some-ballot-style-id") @@ -194,7 +193,7 @@ def get_fake_election() -> ElectionDescription: fake_candidate_ballot_selections, ) - fake_election = ElectionDescription( + fake_manifest = Manifest( spec_version="v0.95", election_scope_id="some-scope-id", type=ElectionType.unknown, @@ -217,48 +216,46 @@ def get_fake_election() -> ElectionDescription: ballot_styles=[fake_ballot_style], ) - return fake_election + return fake_manifest @staticmethod def get_fake_ciphertext_election( - description: ElectionDescription, elgamal_public_key: ElementModP - ) -> Tuple[InternalElectionDescription, CiphertextElectionContext]: - builder = ElectionBuilder( - number_of_guardians=1, quorum=1, description=description - ) + manifest: Manifest, elgamal_public_key: ElementModP + ) -> Tuple[InternalManifest, CiphertextElectionContext]: + builder = ElectionBuilder(number_of_guardians=1, quorum=1, manifest=manifest) builder.set_public_key(elgamal_public_key) builder.set_commitment_hash(TWO_MOD_Q) - metadata, election = get_optional(builder.build()) - return metadata, election + internal_manifest, context = get_optional(builder.build()) + return internal_manifest, context # TODO: Move to ballot Factory? def get_fake_ballot( - self, election: ElectionDescription = None, ballot_id: str = None + self, manifest: Manifest = None, ballot_id: str = None ) -> PlaintextBallot: """ Get a single Fake Ballot object that is manually constructed with default vaules """ - if election is None: - election = self.get_fake_election() + if manifest is None: + manifest = self.get_fake_manifest() if ballot_id is None: ballot_id = "some-unique-ballot-id-123" fake_ballot = PlaintextBallot( ballot_id, - election.ballot_styles[0].object_id, - [contest_from(election.contests[0]), contest_from(election.contests[1])], + manifest.ballot_styles[0].object_id, + [contest_from(manifest.contests[0]), contest_from(manifest.contests[1])], ) return fake_ballot @staticmethod - def _get_election_from_file(filename: str) -> ElectionDescription: + def _get_manifest_from_file(filename: str) -> Manifest: with open(os.path.join(data, filename), "r") as subject: result = subject.read() - target = ElectionDescription.from_json(result) + manifest = Manifest.from_json(result) - return target + return manifest @composite diff --git a/src/electionguardtest/sample_generator.py b/src/electionguardtest/sample_generator.py index f93003f73..6895317be 100644 --- a/src/electionguardtest/sample_generator.py +++ b/src/electionguardtest/sample_generator.py @@ -56,16 +56,16 @@ def generate( # Configure the election ( - public_data, + manifest, private_data, - ) = self.election_factory.get_hamilton_election_with_encryption_context() + ) = self.election_factory.get_hamilton_manifest_with_encryption_context() plaintext_ballots = ( self.ballot_factory.generate_fake_plaintext_ballots_for_election( - public_data.metadata, number_of_ballots + manifest.internal_manifest, number_of_ballots ) ) self.encrypter = EncryptionMediator( - public_data.metadata, public_data.context, self.encryption_device + manifest.internal_manifest, manifest.context, self.encryption_device ) # Encrypt some ballots @@ -76,7 +76,9 @@ def generate( ) ballot_store = DataStore() - ballot_box = BallotBox(public_data.metadata, public_data.context, ballot_store) + ballot_box = BallotBox( + manifest.internal_manifest, manifest.context, ballot_store + ) # Randomly cast/spoil the ballots submitted_ballots: List[SubmittedBallot] = [] @@ -88,12 +90,12 @@ def generate( # Tally ciphertext_tally = get_optional( - tally_ballots(ballot_store, public_data.metadata, public_data.context) + tally_ballots(ballot_store, manifest.internal_manifest, manifest.context) ) # Decrypt decrypter = DecryptionMediator( - public_data.metadata, public_data.context, ciphertext_tally + manifest.internal_manifest, manifest.context, ciphertext_tally ) for i, guardian in enumerate(private_data.guardians): @@ -105,15 +107,15 @@ def generate( # Publish publish( - public_data.description, - public_data.context, - public_data.constants, + manifest.manifest, + manifest.context, + manifest.constants, [self.encryption_device], submitted_ballots, plaintext_spoiled_ballots.values(), ciphertext_tally.publish(), plaintext_tally, - public_data.guardians, + manifest.guardians, ) publish_private_data( diff --git a/tests/integration/test_end_to_end_election.py b/tests/integration/test_end_to_end_election.py index 34acf66d2..8e6a2aef0 100644 --- a/tests/integration/test_end_to_end_election.py +++ b/tests/integration/test_end_to_end_election.py @@ -15,11 +15,10 @@ # Step 0 - Configure Election from electionguard.election import ( ElectionConstants, - ElectionDescription, - InternalElectionDescription, CiphertextElectionContext, ) from electionguard.election_builder import ElectionBuilder +from electionguard.manifest import Manifest, InternalManifest # Step 1 - Key Ceremony from electionguard.guardian import Guardian @@ -52,14 +51,14 @@ # Step 5 - Publish and Verify from electionguard.publish import ( publish, - DEVICE_PREFIX, + BALLOT_PREFIX, COEFFICIENT_PREFIX, CONSTANTS_FILE_NAME, - DESCRIPTION_FILE_NAME, CONTEXT_FILE_NAME, + DEVICE_PREFIX, ENCRYPTED_TALLY_FILE_NAME, + MANIFEST_FILE_NAME, TALLY_FILE_NAME, - BALLOT_PREFIX, ) RESULTS_DIR = "test-results" @@ -81,9 +80,9 @@ class TestEndToEndElection(TestCase): REMOVE_OUTPUT = False # Step 0 - Configure Election - description: ElectionDescription + manifest: Manifest election_builder: ElectionBuilder - metadata: InternalElectionDescription + internal_manifest: InternalManifest context: CiphertextElectionContext constants: ElectionConstants @@ -121,34 +120,34 @@ def test_end_to_end_election(self) -> None: def step_0_configure_election(self) -> None: """ - To conduct an election, load an `ElectionDescription` file + To conduct an election, load an `Manifest` file """ # Load a pre-configured Election Description # TODO: replace with complex election - self.description = ElectionFactory().get_simple_election_from_file() + self.manifest = ElectionFactory().get_simple_manifest_from_file() print( f""" {'-'*40}\n # Election Summary: - # Scope: {self.description.election_scope_id} - # Geopolitical Units: {len(self.description.geopolitical_units)} - # Parties: {len(self.description.parties)} - # Candidates: {len(self.description.candidates)} - # Contests: {len(self.description.contests)} - # Ballot Styles: {len(self.description.ballot_styles)}\n + # Scope: {self.manifest.election_scope_id} + # Geopolitical Units: {len(self.manifest.geopolitical_units)} + # Parties: {len(self.manifest.parties)} + # Candidates: {len(self.manifest.candidates)} + # Contests: {len(self.manifest.contests)} + # Ballot Styles: {len(self.manifest.ballot_styles)}\n {'-'*40}\n """ ) self._assert_message( - ElectionDescription.is_valid.__qualname__, + Manifest.is_valid.__qualname__, "Verify that the input election meta-data is well-formed", - self.description.is_valid(), + self.manifest.is_valid(), ) # Create an Election Builder self.election_builder = ElectionBuilder( - self.NUMBER_OF_GUARDIANS, self.QUORUM, self.description + self.NUMBER_OF_GUARDIANS, self.QUORUM, self.manifest ) self._assert_message( ElectionBuilder.__qualname__, @@ -237,7 +236,9 @@ def step_1_key_ceremony(self) -> None: self.election_builder.set_commitment_hash( get_optional(joint_key).commitment_hash ) - self.metadata, self.context = get_optional(self.election_builder.build()) + self.internal_manifest, self.context = get_optional( + self.election_builder.build() + ) self.constants = ElectionConstants() # Move on to encrypting ballots @@ -249,7 +250,9 @@ def step_2_encrypt_votes(self) -> None: # Configure the Encryption Device self.device = EncryptionDevice("polling-place-one") - self.encrypter = EncryptionMediator(self.metadata, self.context, self.device) + self.encrypter = EncryptionMediator( + self.internal_manifest, self.context, self.device + ) self._assert_message( EncryptionDevice.__qualname__, f"Ready to encrypt at location: {self.device.location}", @@ -283,7 +286,9 @@ def step_3_cast_and_spoil(self) -> None: # Configure the Ballot Box self.ballot_store = DataStore() - self.ballot_box = BallotBox(self.metadata, self.context, self.ballot_store) + self.ballot_box = BallotBox( + self.internal_manifest, self.context, self.ballot_store + ) # Randomly cast or spoil the ballots for ballot in self.ciphertext_ballots: @@ -307,7 +312,7 @@ def step_4_decrypt_tally(self) -> None: # Generate a Homomorphically Accumulated Tally of the ballots self.ciphertext_tally = get_optional( - tally_ballots(self.ballot_store, self.metadata, self.context) + tally_ballots(self.ballot_store, self.internal_manifest, self.context) ) self.ciphertext_ballots = get_ballots(self.ballot_store, BallotBoxState.SPOILED) self._assert_message( @@ -322,7 +327,9 @@ def step_4_decrypt_tally(self) -> None: # Configure the Decryption self.decrypter = DecryptionMediator( - self.metadata, self.context, self.ciphertext_tally, self.ciphertext_ballots + self.context, + self.ciphertext_tally, + self.ciphertext_ballots, ) # Announce each guardian as present @@ -370,7 +377,7 @@ def compare_results(self) -> None: # Create a representation of each contest's tally selection_ids = [ selection.object_id - for contest in self.metadata.contests + for contest in self.manifest.contests for selection in contest.ballot_selections ] expected_plaintext_tally: Dict[str, int] = {key: 0 for key in selection_ids} @@ -432,7 +439,7 @@ def publish_results(self) -> None: Publish results/artifacts of the election """ publish( - self.description, + self.manifest, self.context, self.constants, [self.device], @@ -453,10 +460,8 @@ def verify_results(self) -> None: """Verify results of election""" # Deserialize - description_from_file = ElectionDescription.from_json_file( - DESCRIPTION_FILE_NAME, RESULTS_DIR - ) - self.assertEqual(self.description, description_from_file) + manifest_from_file = Manifest.from_json_file(MANIFEST_FILE_NAME, RESULTS_DIR) + self.assertEqual(self.manifest, manifest_from_file) context_from_file = CiphertextElectionContext.from_json_file( CONTEXT_FILE_NAME, RESULTS_DIR diff --git a/tests/integration/test_hamilton_county_election.py b/tests/integration/test_hamilton_county_election.py index 740e91147..fb9bc6939 100644 --- a/tests/integration/test_hamilton_county_election.py +++ b/tests/integration/test_hamilton_county_election.py @@ -12,10 +12,10 @@ class TestHamiltonCountyElection(unittest.TestCase): Demonstrates a non-trivial example using realistic input data """ - def test_election_description_is_valid(self) -> None: + def test_manifest_is_valid(self) -> None: # Act - subject = election_factory.get_hamilton_election_from_file() + subject = election_factory.get_hamilton_manifest_from_file() # Assert self.assertTrue(subject.is_valid()) diff --git a/tests/property/test_decrypt_with_secrets.py b/tests/property/test_decrypt_with_secrets.py index b88981712..17e385e7a 100644 --- a/tests/property/test_decrypt_with_secrets.py +++ b/tests/property/test_decrypt_with_secrets.py @@ -8,8 +8,6 @@ from hypothesis import given, settings from hypothesis.strategies import integers -import electionguardtest.ballot_factory as BallotFactory -import electionguardtest.election_factory as ElectionFactory from electionguard.chaum_pedersen import DisjunctiveChaumPedersenProof from electionguard.decrypt_with_secrets import ( decrypt_selection_with_secret, @@ -19,12 +17,6 @@ decrypt_ballot_with_nonce, decrypt_ballot_with_secret, ) -from electionguard.election import ( - ContestDescription, - SelectionDescription, - generate_placeholder_selections_from, - contest_description_with_placeholders_from, -) from electionguard.elgamal import ElGamalKeyPair, ElGamalCiphertext from electionguard.encrypt import ( encrypt_contest, @@ -39,9 +31,19 @@ mult_p, int_to_q_unchecked, ) +from electionguard.manifest import ( + ContestDescription, + SelectionDescription, + generate_placeholder_selections_from, + contest_description_with_placeholders_from, +) + +import electionguardtest.ballot_factory as BallotFactory +import electionguardtest.election_factory as ElectionFactory from electionguardtest.elgamal import elgamal_keypairs from electionguardtest.group import elements_mod_q_no_zero + election_factory = ElectionFactory.ElectionFactory() ballot_factory = BallotFactory.BallotFactory() @@ -503,7 +505,7 @@ def test_decrypt_ballot_valid_input_succeeds(self, keypair: ElGamalKeyPair): # TODO: Hypothesis test instead # Arrange - election = election_factory.get_simple_election_from_file() + election = election_factory.get_simple_manifest_from_file() metadata, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) @@ -667,7 +669,7 @@ def test_decrypt_ballot_valid_input_missing_nonce_fails( ): # Arrange - election = election_factory.get_simple_election_from_file() + election = election_factory.get_simple_manifest_from_file() metadata, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) diff --git a/tests/property/test_encrypt.py b/tests/property/test_encrypt.py index 68ee3e27d..ba8f1cf27 100644 --- a/tests/property/test_encrypt.py +++ b/tests/property/test_encrypt.py @@ -17,14 +17,6 @@ make_constant_chaum_pedersen, make_disjunctive_chaum_pedersen, ) -from electionguard.election import ( - ContestDescription, - ContestDescriptionWithPlaceholders, - contest_description_with_placeholders_from, - generate_placeholder_selections_from, - SelectionDescription, - VoteVariationType, -) from electionguard.elgamal import ( ElGamalKeyPair, elgamal_keypair_from_secret, @@ -49,6 +41,15 @@ TWO_MOD_P, mult_p, ) +from electionguard.manifest import ( + ContestDescription, + ContestDescriptionWithPlaceholders, + contest_description_with_placeholders_from, + generate_placeholder_selections_from, + SelectionDescription, + VoteVariationType, +) + from electionguardtest.elgamal import elgamal_keypairs from electionguardtest.group import elements_mod_q_no_zero @@ -545,21 +546,21 @@ def test_encrypt_ballot_simple_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) - election = election_factory.get_fake_election() - metadata, context = election_factory.get_fake_ciphertext_election( - election, keypair.public_key + manifest = election_factory.get_fake_manifest() + internal_manifest, context = election_factory.get_fake_ciphertext_election( + manifest, keypair.public_key ) nonce_seed = TWO_MOD_Q # TODO: Ballot Factory - subject = election_factory.get_fake_ballot(metadata) - self.assertTrue(subject.is_valid(metadata.ballot_styles[0].object_id)) + subject = election_factory.get_fake_ballot(internal_manifest) + self.assertTrue(subject.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act - result = encrypt_ballot(subject, metadata, context, SEED_HASH) + result = encrypt_ballot(subject, internal_manifest, context, SEED_HASH) tracker_code = result.get_tracker_code() result_from_seed = encrypt_ballot( - subject, metadata, context, SEED_HASH, nonce_seed + subject, internal_manifest, context, SEED_HASH, nonce_seed ) # Assert @@ -569,14 +570,14 @@ def test_encrypt_ballot_simple_succeeds(self): self.assertIsNotNone(result_from_seed) self.assertTrue( result.is_valid_encryption( - metadata.description_hash, + internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) ) self.assertTrue( result_from_seed.is_valid_encryption( - metadata.description_hash, + internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) @@ -585,16 +586,16 @@ def test_encrypt_ballot_simple_succeeds(self): def test_encrypt_ballot_with_stateful_composer_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) - election = election_factory.get_fake_election() - metadata, context = election_factory.get_fake_ciphertext_election( - election, keypair.public_key + manifest = election_factory.get_fake_manifest() + internal_manifest, context = election_factory.get_fake_ciphertext_election( + manifest, keypair.public_key ) - data = election_factory.get_fake_ballot(metadata) - self.assertTrue(data.is_valid(metadata.ballot_styles[0].object_id)) + data = election_factory.get_fake_ballot(internal_manifest) + self.assertTrue(data.is_valid(internal_manifest.ballot_styles[0].object_id)) device = EncryptionDevice("Location") - subject = EncryptionMediator(metadata, context, device) + subject = EncryptionMediator(internal_manifest, context, device) # Act result = subject.encrypt(data) @@ -603,7 +604,7 @@ def test_encrypt_ballot_with_stateful_composer_succeeds(self): self.assertIsNotNone(result) self.assertTrue( result.is_valid_encryption( - metadata.description_hash, + internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) @@ -612,16 +613,16 @@ def test_encrypt_ballot_with_stateful_composer_succeeds(self): def test_encrypt_simple_ballot_from_files_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) - election = election_factory.get_simple_election_from_file() - metadata, context = election_factory.get_fake_ciphertext_election( - election, keypair.public_key + manifest = election_factory.get_simple_manifest_from_file() + internal_manifest, context = election_factory.get_fake_ciphertext_election( + manifest, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() - self.assertTrue(data.is_valid(metadata.ballot_styles[0].object_id)) + self.assertTrue(data.is_valid(internal_manifest.ballot_styles[0].object_id)) device = EncryptionDevice("Location") - subject = EncryptionMediator(metadata, context, device) + subject = EncryptionMediator(internal_manifest, context, device) # Act result = subject.encrypt(data) @@ -631,7 +632,7 @@ def test_encrypt_simple_ballot_from_files_succeeds(self): self.assertEqual(data.object_id, result.object_id) self.assertTrue( result.is_valid_encryption( - metadata.description_hash, + internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) @@ -653,22 +654,22 @@ def test_encrypt_ballot_with_derivative_nonces_regenerates_valid_proofs( # TODO: Hypothesis test instead # Arrange - election = election_factory.get_simple_election_from_file() - metadata, context = election_factory.get_fake_ciphertext_election( - election, keypair.public_key + manifest = election_factory.get_simple_manifest_from_file() + internal_manifest, context = election_factory.get_fake_ciphertext_election( + manifest, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() - self.assertTrue(data.is_valid(metadata.ballot_styles[0].object_id)) + self.assertTrue(data.is_valid(internal_manifest.ballot_styles[0].object_id)) device = EncryptionDevice("Location") - subject = EncryptionMediator(metadata, context, device) + subject = EncryptionMediator(internal_manifest, context, device) # Act result = subject.encrypt(data) self.assertTrue( result.is_valid_encryption( - metadata.description_hash, + internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ) @@ -679,7 +680,8 @@ def test_encrypt_ballot_with_derivative_nonces_regenerates_valid_proofs( # Find the contest description contest_description = list( filter( - lambda i, c=contest: i.object_id == c.object_id, metadata.contests + lambda i, c=contest: i.object_id == c.object_id, + internal_manifest.contests, ) )[0] diff --git a/tests/property/test_encrypt_hypotheses.py b/tests/property/test_encrypt_hypotheses.py index a2ec46996..65fdb10b6 100644 --- a/tests/property/test_encrypt_hypotheses.py +++ b/tests/property/test_encrypt_hypotheses.py @@ -7,10 +7,10 @@ from electionguard.ballot import CiphertextBallot from electionguard.decrypt_with_secrets import decrypt_ballot_with_secret -from electionguard.election import ElectionDescription from electionguard.elgamal import ElGamalCiphertext, elgamal_encrypt, elgamal_add from electionguard.encrypt import encrypt_ballot, EncryptionDevice from electionguard.group import ElementModQ +from electionguard.manifest import Manifest from electionguard.nonces import Nonces from electionguardtest.election import ( election_descriptions, @@ -33,13 +33,13 @@ class TestElections(unittest.TestCase): max_examples=10, ) @given(election_descriptions()) - def test_generators_yield_valid_output(self, ed: ElectionDescription): + def test_generators_yield_valid_output(self, manifest: Manifest): """ Tests that our Hypothesis election strategies generate "valid" output, also exercises the full stack of `is_valid` methods. """ - self.assertTrue(ed.is_valid()) + self.assertTrue(manifest.is_valid()) @settings( deadline=timedelta(milliseconds=10000), @@ -64,15 +64,21 @@ def test_accumulation_encryption_decryption( encryption context. It also manually verifies that homomorphic accumulation works as expected. """ # Arrange - _election_description, metadata, ballots, secret_key, context = everything + ( + _election_description, + internal_manifest, + ballots, + secret_key, + context, + ) = everything # Tally the plaintext ballots for comparison later plaintext_tallies = accumulate_plaintext_ballots(ballots) num_ballots = len(ballots) - num_contests = len(metadata.contests) + num_contests = len(internal_manifest.contests) zero_nonce, *nonces = Nonces(nonce)[: num_ballots + 1] self.assertEqual(len(nonces), num_ballots) - self.assertTrue(len(metadata.contests) > 0) + self.assertTrue(len(internal_manifest.contests) > 0) # Generate a valid encryption of zero encrypted_zero = elgamal_encrypt(0, zero_nonce, context.elgamal_public_key) @@ -83,7 +89,7 @@ def test_accumulation_encryption_decryption( # encrypt each ballot for i in range(num_ballots): encrypted_ballot = encrypt_ballot( - ballots[i], metadata, context, SEED_HASH, nonces[i] + ballots[i], internal_manifest, context, SEED_HASH, nonces[i] ) encrypted_ballots.append(encrypted_ballot) @@ -94,7 +100,7 @@ def test_accumulation_encryption_decryption( # decrypt the ballot with secret and verify it matches the plaintext decrypted_ballot = decrypt_ballot_with_secret( ballot=encrypted_ballot, - election_metadata=metadata, + internal_manifest=internal_manifest, crypto_extended_base_hash=context.crypto_extended_base_hash, public_key=context.elgamal_public_key, secret_key=secret_key, @@ -115,7 +121,7 @@ def test_accumulation_encryption_decryption( # loop through the contest descriptions and verify # the decrypted tallies match the plaintext tallies - for contest in metadata.contests: + for contest in internal_manifest.contests: # Sanity check the generated data self.assertTrue(len(contest.ballot_selections) > 0) self.assertTrue(len(contest.placeholder_selections) > 0) diff --git a/tests/test_decryption_mediator.py b/tests/test_decryption_mediator.py index 80230f0cb..c4b3febf4 100644 --- a/tests/test_decryption_mediator.py +++ b/tests/test_decryption_mediator.py @@ -28,14 +28,10 @@ ) from electionguard.decryption_mediator import DecryptionMediator from electionguard.decryption_share import create_ciphertext_decryption_selection -from electionguard.election import ( - CiphertextElectionContext, - InternalElectionDescription, -) +from electionguard.election import CiphertextElectionContext from electionguard.election_builder import ElectionBuilder from electionguard.election_polynomial import compute_lagrange_coefficient from electionguard.encrypt import EncryptionDevice, EncryptionMediator, encrypt_ballot - from electionguard.group import ( int_to_q_unchecked, mult_p, @@ -44,6 +40,7 @@ from electionguard.guardian import Guardian from electionguard.key_ceremony import CeremonyDetails from electionguard.key_ceremony_mediator import KeyCeremonyMediator +from electionguard.manifest import InternalManifest from electionguard.tally import ( CiphertextTally, PlaintextTally, @@ -100,46 +97,50 @@ def setUp(self): self.assertIsNotNone(self.joint_public_key) # setup the election - self.election = election_factory.get_fake_election() - builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, self.election) + manifest = election_factory.get_fake_manifest() + builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, manifest) self.assertIsNone(builder.build()) # Can't build without the public key builder.set_public_key(self.joint_public_key.joint_public_key) builder.set_commitment_hash(self.joint_public_key.commitment_hash) - self.metadata, self.context = get_optional(builder.build()) + self.internal_manifest, self.context = get_optional(builder.build()) self.encryption_device = EncryptionDevice("location") self.ballot_marking_device = EncryptionMediator( - self.metadata, self.context, self.encryption_device + self.internal_manifest, self.context, self.encryption_device ) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( - self.metadata, "some-unique-ballot-id-cast" + self.internal_manifest, "some-unique-ballot-id-cast" ) self.more_fake_ballots = [] for i in range(10): self.more_fake_ballots.append( ballot_factory.get_fake_ballot( - self.metadata, f"some-unique-ballot-id-cast{i}" + self.internal_manifest, f"some-unique-ballot-id-cast{i}" ) ) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( - self.metadata, "some-unique-ballot-id-spoiled" + self.internal_manifest, "some-unique-ballot-id-spoiled" ) self.more_fake_spoiled_ballots = [] for i in range(2): self.more_fake_spoiled_ballots.append( ballot_factory.get_fake_ballot( - self.metadata, f"some-unique-ballot-id-spoiled{i}" + self.internal_manifest, f"some-unique-ballot-id-spoiled{i}" ) ) self.assertTrue( - self.fake_cast_ballot.is_valid(self.metadata.ballot_styles[0].object_id) + self.fake_cast_ballot.is_valid( + self.internal_manifest.ballot_styles[0].object_id + ) ) self.assertTrue( - self.fake_spoiled_ballot.is_valid(self.metadata.ballot_styles[0].object_id) + self.fake_spoiled_ballot.is_valid( + self.internal_manifest.ballot_styles[0].object_id + ) ) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] + self.more_fake_ballots @@ -149,7 +150,7 @@ def setUp(self): # that were not made on any ballots selection_ids = { selection.object_id - for contest in self.metadata.contests + for contest in self.internal_manifest.contests for selection in contest.ballot_selections } @@ -171,7 +172,7 @@ def setUp(self): self.assertIsNotNone(self.encrypted_fake_spoiled_ballot) self.assertTrue( self.encrypted_fake_cast_ballot.is_valid_encryption( - self.metadata.description_hash, + self.internal_manifest.manifest_hash, self.joint_public_key.joint_public_key, self.context.crypto_extended_base_hash, ) @@ -192,7 +193,7 @@ def setUp(self): # configure the ballot box ballot_store = DataStore() - ballot_box = BallotBox(self.metadata, self.context, ballot_store) + ballot_box = BallotBox(self.internal_manifest, self.context, ballot_store) ballot_box.cast(self.encrypted_fake_cast_ballot) ballot_box.spoil(self.encrypted_fake_spoiled_ballot) @@ -204,7 +205,9 @@ def setUp(self): ballot_box.spoil(fake_ballot) # generate encrypted tally - self.ciphertext_tally = tally_ballots(ballot_store, self.metadata, self.context) + self.ciphertext_tally = tally_ballots( + ballot_store, self.internal_manifest, self.context + ) self.ciphertext_ballots = get_ballots(ballot_store, BallotBoxState.SPOILED) def tearDown(self): @@ -213,7 +216,9 @@ def tearDown(self): def test_announce(self): # Arrange subject = DecryptionMediator( - self.metadata, self.context, self.ciphertext_tally, self.ciphertext_ballots + self.context, + self.ciphertext_tally, + self.ciphertext_ballots, ) # act @@ -620,9 +625,7 @@ def test_decrypt_spoiled_ballots_all_guardians_present(self): def test_get_plaintext_tally_all_guardians_present_simple(self): # Arrange - decrypter = DecryptionMediator( - self.metadata, self.context, self.ciphertext_tally, {} - ) + decrypter = DecryptionMediator(self.context, self.ciphertext_tally, {}) # act for guardian in self.guardians: @@ -646,7 +649,9 @@ def test_get_plaintext_tally_compensate_missing_guardian_simple(self): # Arrange decrypter = DecryptionMediator( - self.metadata, self.context, self.ciphertext_tally, self.ciphertext_ballots + self.context, + self.ciphertext_tally, + self.ciphertext_ballots, ) # Act @@ -677,22 +682,22 @@ def test_get_plaintext_tally_all_guardians_present( # Arrange description = data.draw(election_descriptions(parties, contests)) builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, description) - metadata, context = ( + internal_manifest, context = ( builder.set_public_key(self.joint_public_key.joint_public_key) .set_commitment_hash(self.joint_public_key.commitment_hash) .build() ) plaintext_ballots: List[PlaintextBallot] = data.draw( - plaintext_voted_ballots(metadata, randrange(3, 6)) + plaintext_voted_ballots(internal_manifest, randrange(3, 6)) ) plaintext_tallies = accumulate_plaintext_ballots(plaintext_ballots) encrypted_tally = self._generate_encrypted_tally( - metadata, context, plaintext_ballots + internal_manifest, context, plaintext_ballots ) - decrypter = DecryptionMediator(metadata, context, encrypted_tally, {}) + decrypter = DecryptionMediator(context, encrypted_tally, {}) # act for guardian in self.guardians: @@ -709,7 +714,7 @@ def test_get_plaintext_tally_all_guardians_present( def _generate_encrypted_tally( self, - metadata: InternalElectionDescription, + internal_manifest: InternalManifest, context: CiphertextElectionContext, ballots: List[PlaintextBallot], ) -> CiphertextTally: @@ -718,7 +723,7 @@ def _generate_encrypted_tally( store = DataStore() for ballot in ballots: encrypted_ballot = encrypt_ballot( - ballot, metadata, context, int_to_q_unchecked(1) + ballot, internal_manifest, context, int_to_q_unchecked(1) ) self.assertIsNotNone(encrypted_ballot) # add to the ballot store @@ -727,7 +732,7 @@ def _generate_encrypted_tally( from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST), ) - tally = tally_ballots(store, metadata, context) + tally = tally_ballots(store, internal_manifest, context) self.assertIsNotNone(tally) return get_optional(tally) diff --git a/tests/unit/test_ballot_box.py b/tests/unit/test_ballot_box.py index 1edbd7660..6585b5d73 100644 --- a/tests/unit/test_ballot_box.py +++ b/tests/unit/test_ballot_box.py @@ -27,18 +27,18 @@ class TestBallotBox(TestCase): def test_ballot_box_cast_ballot(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) - election = election_factory.get_fake_election() - metadata, context = election_factory.get_fake_ciphertext_election( - election, keypair.public_key + manifest = election_factory.get_fake_manifest() + internal_manifest, context = election_factory.get_fake_ciphertext_election( + manifest, keypair.public_key ) store = DataStore() - source = election_factory.get_fake_ballot(metadata) - self.assertTrue(source.is_valid(metadata.ballot_styles[0].object_id)) + source = election_factory.get_fake_ballot(internal_manifest) + self.assertTrue(source.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act - data = encrypt_ballot(source, metadata, context, SEED_HASH) - self.assertTrue(ballot_is_valid_for_election(data, metadata, context)) - subject = BallotBox(metadata, context, store) + data = encrypt_ballot(source, internal_manifest, context, SEED_HASH) + self.assertTrue(ballot_is_valid_for_election(data, internal_manifest, context)) + subject = BallotBox(internal_manifest, context, store) result = subject.cast(data) # Assert @@ -54,17 +54,17 @@ def test_ballot_box_cast_ballot(self): def test_ballot_box_spoil_ballot(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) - election = election_factory.get_fake_election() - metadata, context = election_factory.get_fake_ciphertext_election( - election, keypair.public_key + manifest = election_factory.get_fake_manifest() + internal_manifest, context = election_factory.get_fake_ciphertext_election( + manifest, keypair.public_key ) store = DataStore() - source = election_factory.get_fake_ballot(metadata) - self.assertTrue(source.is_valid(metadata.ballot_styles[0].object_id)) + source = election_factory.get_fake_ballot(internal_manifest) + self.assertTrue(source.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act - data = encrypt_ballot(source, metadata, context, SEED_HASH) - subject = BallotBox(metadata, context, store) + data = encrypt_ballot(source, internal_manifest, context, SEED_HASH) + subject = BallotBox(internal_manifest, context, store) result = subject.spoil(data) # Assert @@ -80,17 +80,19 @@ def test_ballot_box_spoil_ballot(self): def test_cast_ballot(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) - election = election_factory.get_fake_election() - metadata, context = election_factory.get_fake_ciphertext_election( - election, keypair.public_key + manifest = election_factory.get_fake_manifest() + internal_manifest, context = election_factory.get_fake_ciphertext_election( + manifest, keypair.public_key ) store = DataStore() - source = election_factory.get_fake_ballot(metadata) - self.assertTrue(source.is_valid(metadata.ballot_styles[0].object_id)) + source = election_factory.get_fake_ballot(internal_manifest) + self.assertTrue(source.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act - data = encrypt_ballot(source, metadata, context, SEED_HASH) - result = accept_ballot(data, BallotBoxState.CAST, metadata, context, store) + data = encrypt_ballot(source, internal_manifest, context, SEED_HASH) + result = accept_ballot( + data, BallotBoxState.CAST, internal_manifest, context, store + ) # Assert expected = store.get(source.object_id) @@ -100,26 +102,30 @@ def test_cast_ballot(self): # Test failure modes self.assertIsNone( - accept_ballot(data, BallotBoxState.CAST, metadata, context, store) + accept_ballot(data, BallotBoxState.CAST, internal_manifest, context, store) ) # cannot cast again self.assertIsNone( - accept_ballot(data, BallotBoxState.SPOILED, metadata, context, store) + accept_ballot( + data, BallotBoxState.SPOILED, internal_manifest, context, store + ) ) # cannot cspoil a ballot already cast def test_spoil_ballot(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) - election = election_factory.get_fake_election() - metadata, context = election_factory.get_fake_ciphertext_election( - election, keypair.public_key + manifest = election_factory.get_fake_manifest() + internal_manifest, context = election_factory.get_fake_ciphertext_election( + manifest, keypair.public_key ) store = DataStore() - source = election_factory.get_fake_ballot(metadata) - self.assertTrue(source.is_valid(metadata.ballot_styles[0].object_id)) + source = election_factory.get_fake_ballot(internal_manifest) + self.assertTrue(source.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act - data = encrypt_ballot(source, metadata, context, SEED_HASH) - result = accept_ballot(data, BallotBoxState.SPOILED, metadata, context, store) + data = encrypt_ballot(source, internal_manifest, context, SEED_HASH) + result = accept_ballot( + data, BallotBoxState.SPOILED, internal_manifest, context, store + ) # Assert expected = store.get(source.object_id) @@ -129,8 +135,10 @@ def test_spoil_ballot(self): # Test failure modes self.assertIsNone( - accept_ballot(data, BallotBoxState.SPOILED, metadata, context, store) + accept_ballot( + data, BallotBoxState.SPOILED, internal_manifest, context, store + ) ) # cannot spoil again self.assertIsNone( - accept_ballot(data, BallotBoxState.CAST, metadata, context, store) + accept_ballot(data, BallotBoxState.CAST, internal_manifest, context, store) ) # cannot cast a ballot already spoiled diff --git a/tests/unit/test_election.py b/tests/unit/test_manifest.py similarity index 80% rename from tests/unit/test_election.py rename to tests/unit/test_manifest.py index d6b1481c3..b2c936da8 100644 --- a/tests/unit/test_election.py +++ b/tests/unit/test_manifest.py @@ -1,10 +1,10 @@ import unittest from datetime import datetime -from electionguard.election import ( +from electionguard.manifest import ( ContestDescriptionWithPlaceholders, - ElectionDescription, - InternalElectionDescription, + Manifest, + InternalManifest, SelectionDescription, VoteVariationType, ) @@ -17,50 +17,50 @@ ballot_factory = BallotFactory.BallotFactory() -class TestElection(unittest.TestCase): - """Election tests""" +class TestManifest(unittest.TestCase): + """Manifest tests""" - def test_simple_election_is_valid(self): + def test_simple_manifest_is_valid(self): # Act - subject = election_factory.get_simple_election_from_file() + subject = election_factory.get_simple_manifest_from_file() # Assert self.assertIsNotNone(subject.election_scope_id) self.assertEqual(subject.election_scope_id, "jefferson-county-primary") self.assertTrue(subject.is_valid()) - def test_simple_election_can_serialize(self): + def test_simple_manifest_can_serialize(self): # Arrange - subject = election_factory.get_simple_election_from_file() + subject = election_factory.get_simple_manifest_from_file() intermediate = subject.to_json() # Act - result = ElectionDescription.from_json(intermediate) + result = Manifest.from_json(intermediate) # Assert self.assertIsNotNone(result.election_scope_id) self.assertEqual(result.election_scope_id, "jefferson-county-primary") - def test_election_has_deterministic_hash(self): + def test_manifest_has_deterministic_hash(self): # Act - subject1 = election_factory.get_simple_election_from_file() - subject2 = election_factory.get_simple_election_from_file() + subject1 = election_factory.get_simple_manifest_from_file() + subject2 = election_factory.get_simple_manifest_from_file() # Assert self.assertEqual(subject1.crypto_hash(), subject2.crypto_hash()) - def test_election_hash_is_consistent_regardless_of_format(self): + def test_manifest_hash_is_consistent_regardless_of_format(self): # Act - subject1 = election_factory.get_simple_election_from_file() + subject1 = election_factory.get_simple_manifest_from_file() subject1.start_date = read_json('"2020-03-01T08:00:00-05:00"', datetime) - subject2 = election_factory.get_simple_election_from_file() + subject2 = election_factory.get_simple_manifest_from_file() subject2.start_date = read_json('"2020-03-01T13:00:00-00:00"', datetime) - subject3 = election_factory.get_simple_election_from_file() + subject3 = election_factory.get_simple_manifest_from_file() subject3.start_date = read_json('"2020-03-01T13:00:00.000-00:00"', datetime) subjects = [subject1, subject2, subject3] @@ -70,12 +70,12 @@ def test_election_hash_is_consistent_regardless_of_format(self): for other_hash in hashes[1:]: self.assertEqual(hashes[0], other_hash) - def test_election_from_file_generates_consistent_internal_description_contest_hashes( + def test_manifest_from_file_generates_consistent_internal_description_contest_hashes( self, ): # Arrange - comparator = election_factory.get_simple_election_from_file() - subject = InternalElectionDescription(comparator) + comparator = election_factory.get_simple_manifest_from_file() + subject = InternalManifest(comparator) self.assertEqual(len(comparator.contests), len(subject.contests)) diff --git a/tests/unit/test_publish.py b/tests/unit/test_publish.py index 08314deac..31943da61 100644 --- a/tests/unit/test_publish.py +++ b/tests/unit/test_publish.py @@ -7,15 +7,11 @@ PlaintextBallot, make_ciphertext_ballot, ) -from electionguard.election import ( - ElectionType, - ElectionConstants, - ElectionDescription, - make_ciphertext_election_context, -) +from electionguard.election import ElectionConstants, make_ciphertext_election_context from electionguard.group import ONE_MOD_Q, ONE_MOD_P, int_to_q_unchecked from electionguard.guardian import Guardian from electionguard.key_ceremony import CoefficientValidationSet +from electionguard.manifest import ElectionType, Manifest from electionguard.publish import publish, publish_private_data, RESULTS_DIR from electionguard.tally import ( CiphertextTally, @@ -29,9 +25,7 @@ class TestPublish(TestCase): def test_publish(self) -> None: # Arrange now = datetime.now(timezone.utc) - description = ElectionDescription( - "", ElectionType.unknown, now, now, [], [], [], [], [], [] - ) + manifest = Manifest("", ElectionType.unknown, now, now, [], [], [], [], [], []) context = make_ciphertext_election_context( 1, 1, ONE_MOD_P, ONE_MOD_Q, ONE_MOD_Q ) @@ -41,11 +35,11 @@ def test_publish(self) -> None: encrypted_ballots = [] spoiled_ballots = [] plaintext_tally = PlaintextTally("", []) - ciphertext_tally = CiphertextTally("", description, context) + ciphertext_tally = CiphertextTally("", manifest, context) # Act publish( - description, + manifest, context, constants, devices, diff --git a/tests/unit/test_schema.py b/tests/unit/test_schema.py index 95ca36f01..523072215 100644 --- a/tests/unit/test_schema.py +++ b/tests/unit/test_schema.py @@ -13,23 +13,23 @@ def test_election_description_schema(self): """Test schema validation for election description""" # Arrange - simple_election = ( - election_factory.get_simple_election_from_file().to_json_object() + simple_manifest = ( + election_factory.get_simple_manifest_from_file().to_json_object() ) - hamilton_election = ( - election_factory.get_hamilton_election_from_file().to_json_object() + hamilton_manifest = ( + election_factory.get_hamilton_manifest_from_file().to_json_object() ) # Act election_description_schema = get_election_description_schema() - simple_election_validates = validate_json_schema( - simple_election, election_description_schema + simple_manifest_validates = validate_json_schema( + simple_manifest, election_description_schema ) - hamilton_election_validates = validate_json_schema( - hamilton_election, election_description_schema + hamilton_manifest_validates = validate_json_schema( + hamilton_manifest, election_description_schema ) # Assert self.assertIsNotNone(election_description_schema) - self.assertTrue(simple_election_validates) - self.assertTrue(hamilton_election_validates) + self.assertTrue(simple_manifest_validates) + self.assertTrue(hamilton_manifest_validates)