|
| 1 | +from dataclasses import dataclass |
| 2 | +from typing import Dict, List |
| 3 | + |
| 4 | + |
| 5 | +from .ballot import ( |
| 6 | + SubmittedBallot, |
| 7 | + ExtendedData, |
| 8 | + PlaintextBallot, |
| 9 | + PlaintextBallotContest, |
| 10 | + PlaintextBallotSelection, |
| 11 | + create_ballot_hash, |
| 12 | +) |
| 13 | +from .ballot_box import BallotBoxState |
| 14 | +from .election import CiphertextElectionContext |
| 15 | +from .encrypt import encrypt_ballot_contests |
| 16 | +from .group import ElementModQ |
| 17 | +from .manifest import ( |
| 18 | + ContestDescriptionWithPlaceholders, |
| 19 | + InternalManifest, |
| 20 | +) |
| 21 | +from .utils import get_optional |
| 22 | + |
| 23 | +YES_VOTE = 1 |
| 24 | +NO_VOTE = 0 |
| 25 | + |
| 26 | + |
| 27 | +@dataclass |
| 28 | +class CompactPlaintextBallot: |
| 29 | + """A compact plaintext representation of ballot minimized for data size""" |
| 30 | + |
| 31 | + object_id: str |
| 32 | + style_id: str |
| 33 | + selections: List[bool] |
| 34 | + extended_data: Dict[int, ExtendedData] |
| 35 | + |
| 36 | + |
| 37 | +@dataclass |
| 38 | +class CompactSubmittedBallot: |
| 39 | + """A compact submitted ballot minimized for data size""" |
| 40 | + |
| 41 | + compact_plaintext_ballot: CompactPlaintextBallot |
| 42 | + timestamp: int |
| 43 | + ballot_nonce: ElementModQ |
| 44 | + previous_code: ElementModQ |
| 45 | + code: ElementModQ |
| 46 | + ballot_box_state: BallotBoxState |
| 47 | + |
| 48 | + |
| 49 | +def compress_plaintext_ballot(ballot: PlaintextBallot) -> CompactPlaintextBallot: |
| 50 | + """Compress a plaintext ballot into a compact plaintext ballot""" |
| 51 | + selections = _get_compact_selections(ballot) |
| 52 | + extended_data = _get_compact_extended_data(ballot) |
| 53 | + |
| 54 | + return CompactPlaintextBallot( |
| 55 | + ballot.object_id, ballot.style_id, selections, extended_data |
| 56 | + ) |
| 57 | + |
| 58 | + |
| 59 | +def compress_submitted_ballot( |
| 60 | + ballot: SubmittedBallot, |
| 61 | + plaintext_ballot: PlaintextBallot, |
| 62 | + ballot_nonce: ElementModQ, |
| 63 | +) -> CompactSubmittedBallot: |
| 64 | + """Compress a submitted ballot into a compact submitted ballot""" |
| 65 | + return CompactSubmittedBallot( |
| 66 | + compress_plaintext_ballot(plaintext_ballot), |
| 67 | + ballot.timestamp, |
| 68 | + ballot_nonce, |
| 69 | + ballot.previous_code, |
| 70 | + get_optional(ballot.code), |
| 71 | + ballot.state, |
| 72 | + ) |
| 73 | + |
| 74 | + |
| 75 | +def expand_compact_submitted_ballot( |
| 76 | + compact_ballot: CompactSubmittedBallot, |
| 77 | + internal_manifest: InternalManifest, |
| 78 | + context: CiphertextElectionContext, |
| 79 | +) -> SubmittedBallot: |
| 80 | + """ |
| 81 | + Expand a compact submitted ballot using context and |
| 82 | + the election manifest into a submitted ballot |
| 83 | + """ |
| 84 | + # Expand ballot and encrypt & hash contests |
| 85 | + plaintext_ballot = expand_compact_plaintext_ballot( |
| 86 | + compact_ballot.compact_plaintext_ballot, internal_manifest |
| 87 | + ) |
| 88 | + contests = get_optional( |
| 89 | + encrypt_ballot_contests( |
| 90 | + plaintext_ballot, internal_manifest, context, compact_ballot.ballot_nonce |
| 91 | + ) |
| 92 | + ) |
| 93 | + crypto_hash = create_ballot_hash( |
| 94 | + plaintext_ballot.object_id, internal_manifest.manifest_hash, contests |
| 95 | + ) |
| 96 | + |
| 97 | + return SubmittedBallot( |
| 98 | + plaintext_ballot.object_id, |
| 99 | + plaintext_ballot.style_id, |
| 100 | + internal_manifest.manifest_hash, |
| 101 | + compact_ballot.previous_code, |
| 102 | + contests, |
| 103 | + compact_ballot.code, |
| 104 | + compact_ballot.timestamp, |
| 105 | + crypto_hash, |
| 106 | + compact_ballot.ballot_nonce, |
| 107 | + compact_ballot.ballot_box_state, |
| 108 | + ) |
| 109 | + |
| 110 | + |
| 111 | +def expand_compact_plaintext_ballot( |
| 112 | + compact_ballot: CompactPlaintextBallot, internal_manifest: InternalManifest |
| 113 | +) -> PlaintextBallot: |
| 114 | + """Expand a compact plaintext ballot into the original plaintext ballot""" |
| 115 | + return PlaintextBallot( |
| 116 | + compact_ballot.object_id, |
| 117 | + compact_ballot.style_id, |
| 118 | + _get_plaintext_contests(compact_ballot, internal_manifest), |
| 119 | + ) |
| 120 | + |
| 121 | + |
| 122 | +def _get_compact_selections(ballot: PlaintextBallot) -> List[bool]: |
| 123 | + selections = [] |
| 124 | + for contest in ballot.contests: |
| 125 | + for selection in contest.ballot_selections: |
| 126 | + selections.append(selection.vote == YES_VOTE) |
| 127 | + return selections |
| 128 | + |
| 129 | + |
| 130 | +def _get_compact_extended_data(ballot: PlaintextBallot) -> Dict[int, ExtendedData]: |
| 131 | + extended_data = {} |
| 132 | + index = 0 |
| 133 | + for contest in ballot.contests: |
| 134 | + for selection in contest.ballot_selections: |
| 135 | + index += 1 |
| 136 | + if selection.extended_data: |
| 137 | + extended_data[index] = selection.extended_data |
| 138 | + return extended_data |
| 139 | + |
| 140 | + |
| 141 | +def _get_plaintext_contests( |
| 142 | + compact_ballot: CompactPlaintextBallot, internal_manifest: InternalManifest |
| 143 | +) -> List[PlaintextBallotContest]: |
| 144 | + """Get ballot contests from compact plaintext ballot""" |
| 145 | + index = 0 |
| 146 | + ballot_style_contests = _get_ballot_style_contests( |
| 147 | + compact_ballot.style_id, internal_manifest |
| 148 | + ) |
| 149 | + |
| 150 | + contests: List[PlaintextBallotContest] = [] |
| 151 | + for manifest_contest in sorted( |
| 152 | + internal_manifest.contests, key=lambda c: c.sequence_order |
| 153 | + ): |
| 154 | + contest_in_style = ( |
| 155 | + ballot_style_contests.get(manifest_contest.object_id) is not None |
| 156 | + ) |
| 157 | + |
| 158 | + # Iterate through selections. If contest not in style, mark placeholder |
| 159 | + selections: List[PlaintextBallotSelection] = [] |
| 160 | + for selection in sorted( |
| 161 | + manifest_contest.ballot_selections, key=lambda s: s.sequence_order |
| 162 | + ): |
| 163 | + selections.append( |
| 164 | + PlaintextBallotSelection( |
| 165 | + selection.object_id, |
| 166 | + YES_VOTE if compact_ballot.selections[index] else NO_VOTE, |
| 167 | + not contest_in_style, |
| 168 | + compact_ballot.extended_data.get(index), |
| 169 | + ) |
| 170 | + ) |
| 171 | + index += 1 |
| 172 | + |
| 173 | + contests.append(PlaintextBallotContest(manifest_contest.object_id, selections)) |
| 174 | + return contests |
| 175 | + |
| 176 | + |
| 177 | +def _get_ballot_style_contests( |
| 178 | + ballot_style_id: str, internal_manifest: InternalManifest |
| 179 | +) -> Dict[str, ContestDescriptionWithPlaceholders]: |
| 180 | + ballot_style_contests = internal_manifest.get_contests_for(ballot_style_id) |
| 181 | + return {contest.object_id: contest for contest in ballot_style_contests} |
0 commit comments