CLI Election Record Export (#590)
* Parameterize output path, add election record step * Starting to gather info to publish an election record * Expose the encryption device, it's required for election record * Retrieve all the inputs for election record publishing * Create election record * Refactor election record and private data into separate directories * Refactor to remove export_private_data * Use output_file parameter for outputting election record * Fix linting errors * PR Feedback
This commit is contained in:
Родитель
4dfdb1a036
Коммит
b7c8782e81
3
Makefile
3
Makefile
|
@ -252,3 +252,6 @@ release-notes:
|
|||
echo -en "\n" >> release_notes.md
|
||||
echo "## Issues" >> release_notes.md
|
||||
curl "${GITHUB_API_URL}/${GITHUB_REPOSITORY}/issues?milestone=${MILESTONE_NUM}&state=all" | jq '.[].title' | while read i; do echo "[$i]($MILESTONE_URL)" >> release_notes.md; done
|
||||
|
||||
simple-election:
|
||||
poetry run eg e2e --guardian-count=2 --quorum=2 --manifest=data/election_manifest_simple.json --ballots=data/plaintext_ballots_simple.json --spoil-id=25a7111b-4334-425a-87c1-f7a49f42b3a2 --output-file="./election_record.zip"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
eg e2e --guardian-count=2 --quorum=2 --manifest=data/election_manifest_simple.json --ballots=data/plaintext_ballots_simple.json --spoil-id=25a7111b-4334-425a-87c1-f7a49f42b3a2
|
|
@ -7,9 +7,11 @@ from electionguard_cli.cli_models import (
|
|||
BuildElectionResults,
|
||||
E2eDecryptResults,
|
||||
E2eInputs,
|
||||
E2eSubmitResults,
|
||||
e2e_build_election_results,
|
||||
e2e_decrypt_results,
|
||||
e2e_inputs,
|
||||
e2e_submit_results,
|
||||
)
|
||||
from electionguard_cli.commands import (
|
||||
e2e,
|
||||
|
@ -21,6 +23,7 @@ from electionguard_cli.e2e_steps import (
|
|||
DecryptStep,
|
||||
E2eStepBase,
|
||||
ElectionBuilderStep,
|
||||
ElectionRecordStep,
|
||||
InputRetrievalStep,
|
||||
KeyCeremonyStep,
|
||||
PrintResultsStep,
|
||||
|
@ -28,6 +31,7 @@ from electionguard_cli.e2e_steps import (
|
|||
decrypt_step,
|
||||
e2e_step_base,
|
||||
election_builder_step,
|
||||
election_record_step,
|
||||
input_retrieval_step,
|
||||
key_ceremony_step,
|
||||
print_results_step,
|
||||
|
@ -43,7 +47,9 @@ __all__ = [
|
|||
"E2eDecryptResults",
|
||||
"E2eInputs",
|
||||
"E2eStepBase",
|
||||
"E2eSubmitResults",
|
||||
"ElectionBuilderStep",
|
||||
"ElectionRecordStep",
|
||||
"InputRetrievalStep",
|
||||
"KeyCeremonyStep",
|
||||
"PrintResultsStep",
|
||||
|
@ -59,7 +65,9 @@ __all__ = [
|
|||
"e2e_inputs",
|
||||
"e2e_step_base",
|
||||
"e2e_steps",
|
||||
"e2e_submit_results",
|
||||
"election_builder_step",
|
||||
"election_record_step",
|
||||
"hello",
|
||||
"hello_command",
|
||||
"input_retrieval_step",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from electionguard_cli.cli_models import e2e_build_election_results
|
||||
from electionguard_cli.cli_models import e2e_decrypt_results
|
||||
from electionguard_cli.cli_models import e2e_inputs
|
||||
from electionguard_cli.cli_models import e2e_submit_results
|
||||
|
||||
from electionguard_cli.cli_models.e2e_build_election_results import (
|
||||
BuildElectionResults,
|
||||
|
@ -11,12 +12,17 @@ from electionguard_cli.cli_models.e2e_decrypt_results import (
|
|||
from electionguard_cli.cli_models.e2e_inputs import (
|
||||
E2eInputs,
|
||||
)
|
||||
from electionguard_cli.cli_models.e2e_submit_results import (
|
||||
E2eSubmitResults,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BuildElectionResults",
|
||||
"E2eDecryptResults",
|
||||
"E2eInputs",
|
||||
"E2eSubmitResults",
|
||||
"e2e_build_election_results",
|
||||
"e2e_decrypt_results",
|
||||
"e2e_inputs",
|
||||
"e2e_submit_results",
|
||||
]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Dict
|
||||
from electionguard.tally import PlaintextTally
|
||||
from electionguard.tally import CiphertextTally, PlaintextTally
|
||||
from electionguard.type import BallotId
|
||||
from electionguard.election_polynomial import LagrangeCoefficientsRecord
|
||||
|
||||
|
||||
class E2eDecryptResults:
|
||||
|
@ -10,9 +11,15 @@ class E2eDecryptResults:
|
|||
self,
|
||||
plaintext_tally: PlaintextTally,
|
||||
plaintext_spoiled_ballots: Dict[BallotId, PlaintextTally],
|
||||
ciphertext_tally: CiphertextTally,
|
||||
lagrange_coefficients: LagrangeCoefficientsRecord,
|
||||
):
|
||||
self.plaintext_tally = plaintext_tally
|
||||
self.plaintext_spoiled_ballots = plaintext_spoiled_ballots
|
||||
self.ciphertext_tally = ciphertext_tally
|
||||
self.lagrange_coefficients = lagrange_coefficients
|
||||
|
||||
plaintext_tally: PlaintextTally
|
||||
plaintext_spoiled_ballots: Dict[BallotId, PlaintextTally]
|
||||
ciphertext_tally: CiphertextTally
|
||||
lagrange_coefficients: LagrangeCoefficientsRecord
|
||||
|
|
|
@ -15,6 +15,7 @@ class E2eInputs:
|
|||
manifest: Manifest,
|
||||
ballots: List[PlaintextBallot],
|
||||
spoil_id: str,
|
||||
output_file: str,
|
||||
):
|
||||
self.guardian_count = guardian_count
|
||||
self.quorum = quorum
|
||||
|
@ -22,6 +23,7 @@ class E2eInputs:
|
|||
self.manifest = manifest
|
||||
self.ballots = ballots
|
||||
self.spoil_id = spoil_id
|
||||
self.output_file = output_file
|
||||
|
||||
guardian_count: int
|
||||
quorum: int
|
||||
|
@ -29,3 +31,4 @@ class E2eInputs:
|
|||
manifest: Manifest
|
||||
ballots: List[PlaintextBallot]
|
||||
spoil_id: str
|
||||
output_file: str
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from typing import List
|
||||
from electionguard.ballot import CiphertextBallot
|
||||
from electionguard.data_store import DataStore
|
||||
from electionguard.encrypt import EncryptionDevice
|
||||
|
||||
|
||||
class E2eSubmitResults:
|
||||
"""Responsible for holding the results of submitting votes in an election."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_store: DataStore,
|
||||
device: EncryptionDevice,
|
||||
ciphertext_ballots: List[CiphertextBallot],
|
||||
):
|
||||
self.data_store = data_store
|
||||
self.device = device
|
||||
self.ciphertext_ballots = ciphertext_ballots
|
||||
|
||||
data_store: DataStore
|
||||
device: EncryptionDevice
|
||||
ciphertext_ballots: List[CiphertextBallot]
|
|
@ -7,6 +7,7 @@ from ..e2e_steps import (
|
|||
DecryptStep,
|
||||
PrintResultsStep,
|
||||
InputRetrievalStep,
|
||||
ElectionRecordStep,
|
||||
)
|
||||
|
||||
|
||||
|
@ -38,23 +39,31 @@ from ..e2e_steps import (
|
|||
@click.option(
|
||||
"--spoil-id",
|
||||
prompt="Object-id of ballot to spoil",
|
||||
help="The object-id of a ballot within the ballots file to spoil",
|
||||
help="The object-id of a ballot within the ballots file to spoil.",
|
||||
type=click.STRING,
|
||||
default=None,
|
||||
prompt_required=False,
|
||||
)
|
||||
@click.option(
|
||||
"--output-file",
|
||||
help="A file name for saving an output election record (e.g. './election.zip')."
|
||||
+ " If no value provided then an election record will not be generated.",
|
||||
type=click.Path(exists=False),
|
||||
default=None,
|
||||
)
|
||||
def e2e(
|
||||
guardian_count: int,
|
||||
quorum: int,
|
||||
manifest: TextIOWrapper,
|
||||
ballots: TextIOWrapper,
|
||||
spoil_id: str,
|
||||
output_file: str,
|
||||
) -> None:
|
||||
"""Runs through an end-to-end election."""
|
||||
|
||||
# get user inputs
|
||||
election_inputs = InputRetrievalStep().get_inputs(
|
||||
guardian_count, quorum, manifest, ballots, spoil_id
|
||||
guardian_count, quorum, manifest, ballots, spoil_id, output_file
|
||||
)
|
||||
|
||||
# perform election
|
||||
|
@ -62,12 +71,17 @@ def e2e(
|
|||
build_election_results = ElectionBuilderStep().build_election(
|
||||
election_inputs, joint_key
|
||||
)
|
||||
ballot_store = SubmitVotesStep().submit_votes(
|
||||
submit_results = SubmitVotesStep().submit_votes(
|
||||
election_inputs, build_election_results
|
||||
)
|
||||
decrypt_results = DecryptStep().decrypt_tally(
|
||||
ballot_store, election_inputs.guardians, build_election_results
|
||||
submit_results.data_store, election_inputs.guardians, build_election_results
|
||||
)
|
||||
|
||||
# print results
|
||||
PrintResultsStep().print_election_results(election_inputs, decrypt_results)
|
||||
|
||||
# publish election record
|
||||
ElectionRecordStep().run(
|
||||
election_inputs, build_election_results, submit_results, decrypt_results
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from electionguard_cli.e2e_steps import decrypt_step
|
||||
from electionguard_cli.e2e_steps import e2e_step_base
|
||||
from electionguard_cli.e2e_steps import election_builder_step
|
||||
from electionguard_cli.e2e_steps import election_record_step
|
||||
from electionguard_cli.e2e_steps import input_retrieval_step
|
||||
from electionguard_cli.e2e_steps import key_ceremony_step
|
||||
from electionguard_cli.e2e_steps import print_results_step
|
||||
|
@ -15,6 +16,9 @@ from electionguard_cli.e2e_steps.e2e_step_base import (
|
|||
from electionguard_cli.e2e_steps.election_builder_step import (
|
||||
ElectionBuilderStep,
|
||||
)
|
||||
from electionguard_cli.e2e_steps.election_record_step import (
|
||||
ElectionRecordStep,
|
||||
)
|
||||
from electionguard_cli.e2e_steps.input_retrieval_step import (
|
||||
InputRetrievalStep,
|
||||
)
|
||||
|
@ -32,6 +36,7 @@ __all__ = [
|
|||
"DecryptStep",
|
||||
"E2eStepBase",
|
||||
"ElectionBuilderStep",
|
||||
"ElectionRecordStep",
|
||||
"InputRetrievalStep",
|
||||
"KeyCeremonyStep",
|
||||
"PrintResultsStep",
|
||||
|
@ -39,6 +44,7 @@ __all__ = [
|
|||
"decrypt_step",
|
||||
"e2e_step_base",
|
||||
"election_builder_step",
|
||||
"election_record_step",
|
||||
"input_retrieval_step",
|
||||
"key_ceremony_step",
|
||||
"print_results_step",
|
||||
|
|
|
@ -18,14 +18,15 @@ from .e2e_step_base import E2eStepBase
|
|||
class DecryptStep(E2eStepBase):
|
||||
"""Responsible for decrypting a tally and/or cast ballots"""
|
||||
|
||||
def _print_lagrange_coefficients(
|
||||
def _get_lagrange_coefficients(
|
||||
self, decryption_mediator: DecryptionMediator
|
||||
) -> None:
|
||||
) -> LagrangeCoefficientsRecord:
|
||||
lagrange_coefficients = LagrangeCoefficientsRecord(
|
||||
decryption_mediator.get_lagrange_coefficients()
|
||||
)
|
||||
coefficient_count = len(lagrange_coefficients.coefficients)
|
||||
self.print_value("Lagrange coefficients retrieved", coefficient_count)
|
||||
return lagrange_coefficients
|
||||
|
||||
def decrypt_tally(
|
||||
self,
|
||||
|
@ -35,33 +36,40 @@ class DecryptStep(E2eStepBase):
|
|||
) -> E2eDecryptResults:
|
||||
self.print_header("Decrypting tally")
|
||||
|
||||
tally = _get_tally(ballot_store, build_election_results)
|
||||
ciphertext_tally = _get_tally(ballot_store, build_election_results)
|
||||
spoiled_ballots = _get_spoiled_ballots(ballot_store)
|
||||
decryption_mediator = _get_decryption_mediator(build_election_results)
|
||||
context = build_election_results.context
|
||||
|
||||
self.print_value("Cast ballots", tally.cast())
|
||||
self.print_value("Spoiled ballots", tally.spoiled())
|
||||
self.print_value("Total ballots", len(tally))
|
||||
self.print_value("Cast ballots", ciphertext_tally.cast())
|
||||
self.print_value("Spoiled ballots", ciphertext_tally.spoiled())
|
||||
self.print_value("Total ballots", len(ciphertext_tally))
|
||||
|
||||
count = 0
|
||||
for guardian in guardians:
|
||||
guardian_key = guardian.share_key()
|
||||
tally_share = _compute_tally_share(guardian, tally, context)
|
||||
tally_share = _compute_tally_share(guardian, ciphertext_tally, context)
|
||||
ballot_shares = guardian.compute_ballot_shares(spoiled_ballots, context)
|
||||
decryption_mediator.announce(guardian_key, tally_share, ballot_shares)
|
||||
count += 1
|
||||
click.echo(f"Guardian Present: {guardian.id}")
|
||||
|
||||
self._print_lagrange_coefficients(decryption_mediator)
|
||||
lagrange_coefficients = self._get_lagrange_coefficients(decryption_mediator)
|
||||
|
||||
plaintext_tally = get_optional(decryption_mediator.get_plaintext_tally(tally))
|
||||
plaintext_tally = get_optional(
|
||||
decryption_mediator.get_plaintext_tally(ciphertext_tally)
|
||||
)
|
||||
|
||||
plaintext_spoiled_ballots = get_optional(
|
||||
decryption_mediator.get_plaintext_ballots(spoiled_ballots)
|
||||
)
|
||||
|
||||
return E2eDecryptResults(plaintext_tally, plaintext_spoiled_ballots)
|
||||
return E2eDecryptResults(
|
||||
plaintext_tally,
|
||||
plaintext_spoiled_ballots,
|
||||
ciphertext_tally,
|
||||
lagrange_coefficients,
|
||||
)
|
||||
|
||||
|
||||
def _compute_tally_share(
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
from shutil import make_archive
|
||||
from os.path import splitext
|
||||
from tempfile import TemporaryDirectory
|
||||
from click import echo
|
||||
from electionguard_cli.cli_models import BuildElectionResults, E2eSubmitResults
|
||||
from electionguard_cli.cli_models.e2e_decrypt_results import E2eDecryptResults
|
||||
from electionguard_cli.cli_models.e2e_inputs import E2eInputs
|
||||
from electionguard_cli.e2e_steps.e2e_step_base import E2eStepBase
|
||||
from electionguard.constants import get_constants
|
||||
|
||||
from electionguard_tools.helpers.export import export_record
|
||||
|
||||
|
||||
class ElectionRecordStep(E2eStepBase):
|
||||
"""Responsible for publishing an election record after an election has completed."""
|
||||
|
||||
_COMPRESSION_FORMAT = "zip"
|
||||
|
||||
def run(
|
||||
self,
|
||||
election_inputs: E2eInputs,
|
||||
build_election_results: BuildElectionResults,
|
||||
submit_results: E2eSubmitResults,
|
||||
decrypt_results: E2eDecryptResults,
|
||||
) -> None:
|
||||
|
||||
self.print_header("Election Record")
|
||||
guardian_records = [
|
||||
guardian.publish() for guardian in election_inputs.guardians
|
||||
]
|
||||
constants = get_constants()
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
export_record(
|
||||
election_inputs.manifest,
|
||||
build_election_results.context,
|
||||
constants,
|
||||
[submit_results.device],
|
||||
submit_results.data_store.all(),
|
||||
decrypt_results.plaintext_spoiled_ballots.values(),
|
||||
decrypt_results.ciphertext_tally.publish(),
|
||||
decrypt_results.plaintext_tally,
|
||||
guardian_records,
|
||||
decrypt_results.lagrange_coefficients,
|
||||
election_record_directory=temp_dir,
|
||||
)
|
||||
file_name = splitext(election_inputs.output_file)[0]
|
||||
make_archive(file_name, self._COMPRESSION_FORMAT, temp_dir)
|
||||
echo(
|
||||
f"Successfully exported election record to '{election_inputs.output_file}'"
|
||||
)
|
|
@ -20,6 +20,7 @@ class InputRetrievalStep(E2eStepBase):
|
|||
manifest_file: TextIOWrapper,
|
||||
ballots_file: TextIOWrapper,
|
||||
spoil_id: str,
|
||||
output_file: str,
|
||||
) -> E2eInputs:
|
||||
self.print_header("Retrieving Inputs")
|
||||
guardians = InputRetrievalStep._get_guardians(guardian_count, quorum)
|
||||
|
@ -27,7 +28,9 @@ class InputRetrievalStep(E2eStepBase):
|
|||
ballots = InputRetrievalStep._get_ballots(ballots_file)
|
||||
self.print_value("Guardians", guardian_count)
|
||||
self.print_value("Quorum", quorum)
|
||||
return E2eInputs(guardian_count, quorum, guardians, manifest, ballots, spoil_id)
|
||||
return E2eInputs(
|
||||
guardian_count, quorum, guardians, manifest, ballots, spoil_id, output_file
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_ballots(ballots_file: TextIOWrapper) -> List[PlaintextBallot]:
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from typing import List
|
||||
from typing import List, Tuple
|
||||
import click
|
||||
|
||||
from electionguard.data_store import DataStore
|
||||
from electionguard.ballot_box import BallotBox
|
||||
from electionguard.encrypt import EncryptionMediator
|
||||
from electionguard.encrypt import EncryptionDevice, EncryptionMediator
|
||||
from electionguard.election import CiphertextElectionContext
|
||||
from electionguard.manifest import InternalManifest
|
||||
from electionguard.utils import get_optional
|
||||
|
@ -15,7 +15,7 @@ from electionguard_tools.factories.election_factory import (
|
|||
ElectionFactory,
|
||||
)
|
||||
|
||||
from ..cli_models import E2eInputs, BuildElectionResults
|
||||
from ..cli_models import E2eInputs, BuildElectionResults, E2eSubmitResults
|
||||
from .e2e_step_base import E2eStepBase
|
||||
|
||||
|
||||
|
@ -24,39 +24,41 @@ class SubmitVotesStep(E2eStepBase):
|
|||
|
||||
def submit_votes(
|
||||
self, e2e_inputs: E2eInputs, build_election_results: BuildElectionResults
|
||||
) -> DataStore:
|
||||
) -> E2eSubmitResults:
|
||||
ballots = e2e_inputs.ballots
|
||||
internal_manifest = build_election_results.internal_manifest
|
||||
context = build_election_results.context
|
||||
ballot_store: DataStore = DataStore()
|
||||
ciphertext_ballots = self._encrypt_votes(ballots, internal_manifest, context)
|
||||
(ciphertext_ballots, device) = self._encrypt_votes(
|
||||
ballots, internal_manifest, context
|
||||
)
|
||||
SubmitVotesStep._cast_and_spoil(
|
||||
ballot_store, internal_manifest, context, ciphertext_ballots, e2e_inputs
|
||||
)
|
||||
return ballot_store
|
||||
return E2eSubmitResults(ballot_store, device, ciphertext_ballots)
|
||||
|
||||
def _get_encrypter(
|
||||
self,
|
||||
internal_manifest: InternalManifest,
|
||||
context: CiphertextElectionContext,
|
||||
) -> EncryptionMediator:
|
||||
) -> Tuple[EncryptionMediator, EncryptionDevice]:
|
||||
device = ElectionFactory.get_encryption_device()
|
||||
self.print_value("Device location", device.location)
|
||||
encrypter = EncryptionMediator(internal_manifest, context, device)
|
||||
return encrypter
|
||||
return (encrypter, device)
|
||||
|
||||
def _encrypt_votes(
|
||||
self,
|
||||
plaintext_ballots: List[PlaintextBallot],
|
||||
internal_manifest: InternalManifest,
|
||||
context: CiphertextElectionContext,
|
||||
) -> List[CiphertextBallot]:
|
||||
) -> Tuple[List[CiphertextBallot], EncryptionDevice]:
|
||||
self.print_value("Ballots to encrypt", len(plaintext_ballots))
|
||||
encrypter = self._get_encrypter(internal_manifest, context)
|
||||
(encrypter, device) = self._get_encrypter(internal_manifest, context)
|
||||
encrypted_ballots = SubmitVotesStep._encrypt_ballots(
|
||||
plaintext_ballots, encrypter
|
||||
)
|
||||
return encrypted_ballots
|
||||
return (encrypted_ballots, device)
|
||||
|
||||
@staticmethod
|
||||
def _encrypt_ballots(
|
||||
|
|
Загрузка…
Ссылка в новой задаче