1- from dataclasses import dataclass , field
1+ from dataclasses import dataclass , field , replace
22from datetime import datetime
33from distutils import util
44from enum import Enum
5- from distutils import util
65from typing import Any , List , Optional , Protocol , runtime_checkable , Sequence
76
87from .chaum_pedersen import (
@@ -32,7 +31,7 @@ def _list_eq(
3231 )
3332
3433
35- @dataclass
34+ @dataclass ( eq = True , unsafe_hash = True )
3635class ExtendedData (object ):
3736 """
3837 ExtendedData represents any arbitrary data expressible as a string with a length.
@@ -43,18 +42,8 @@ class ExtendedData(object):
4342 value : str
4443 length : int
4544
46- def __eq__ (self , other : Any ) -> bool :
47- return (
48- isinstance (other , ExtendedData )
49- and self .value == other .value
50- and self .length == other .length
51- )
5245
53- def __ne__ (self , other : Any ) -> bool :
54- return not self .__eq__ (other )
55-
56-
57- @dataclass
46+ @dataclass (unsafe_hash = True )
5847class PlaintextBallotSelection (ElectionObjectBase ):
5948 """
6049 A BallotSelection represents an individual selection on a ballot.
@@ -128,6 +117,7 @@ def to_int(self) -> int:
128117 def __eq__ (self , other : Any ) -> bool :
129118 return (
130119 isinstance (other , PlaintextBallotSelection )
120+ and self .object_id == other .object_id
131121 and self .vote == other .vote
132122 and self .is_placeholder_selection == other .is_placeholder_selection
133123 and self .extended_data == other .extended_data
@@ -152,7 +142,7 @@ class CiphertextSelection(Protocol):
152142 """The encrypted representation of the selection"""
153143
154144
155- @dataclass
145+ @dataclass ( eq = True , unsafe_hash = True )
156146class CiphertextBallotSelection (
157147 ElectionObjectBase , CiphertextSelection , CryptoHashCheckable
158148):
@@ -312,7 +302,7 @@ def make_ciphertext_ballot_selection(
312302 )
313303
314304
315- @dataclass
305+ @dataclass ( unsafe_hash = True )
316306class PlaintextBallotContest (ElectionObjectBase ):
317307 """
318308 A PlaintextBallotContest represents the selections made by a voter for a specific ContestDescription
@@ -388,7 +378,7 @@ def __ne__(self, other: Any) -> bool:
388378 return not self .__eq__ (other )
389379
390380
391- @dataclass
381+ @dataclass ( unsafe_hash = True )
392382class CiphertextBallotContest (ElectionObjectBase , CryptoHashCheckable ):
393383 """
394384 A CiphertextBallotContest represents the selections made by a voter for a specific ContestDescription
@@ -423,6 +413,20 @@ class CiphertextBallotContest(ElectionObjectBase, CryptoHashCheckable):
423413 available selections for the contest, and that the proof was generated with the nonce
424414 """
425415
416+ def __eq__ (self , other : Any ) -> bool :
417+ return (
418+ isinstance (other , CiphertextBallotContest )
419+ and self .object_id == other .object_id
420+ and _list_eq (self .ballot_selections , other .ballot_selections )
421+ and self .description_hash == other .description_hash
422+ and self .crypto_hash == other .crypto_hash
423+ and self .nonce == other .nonce
424+ and self .proof == other .proof
425+ )
426+
427+ def __ne__ (self , other : Any ) -> bool :
428+ return not self .__eq__ (other )
429+
426430 def aggregate_nonce (self ) -> Optional [ElementModQ ]:
427431 """
428432 :return: an aggregate nonce for the contest composed of the nonces of the selections
@@ -578,7 +582,7 @@ def make_ciphertext_ballot_contest(
578582 )
579583
580584
581- @dataclass
585+ @dataclass ( unsafe_hash = True )
582586class PlaintextBallot (ElectionObjectBase ):
583587 """
584588 A PlaintextBallot represents a voters selections for a given ballot and ballot style
@@ -616,14 +620,16 @@ def __ne__(self, other: Any) -> bool:
616620 return not self .__eq__ (other )
617621
618622
619- @dataclass
623+ @dataclass ( unsafe_hash = True )
620624class CiphertextBallot (ElectionObjectBase , CryptoHashCheckable ):
621625 """
622- A CiphertextBallot represents a voters encrypted selections for a given ballot and ballot style
626+ A CiphertextBallot represents a voters encrypted selections for a given ballot and ballot style.
623627
624628 When a ballot is in it's complete, encrypted state, the `nonce` is the master nonce
625629 from which all other nonces can be derived to encrypt the ballot. Allong with the `nonce`
626630 fields on `Ballotcontest` and `BallotSelection`, this value is sensitive.
631+
632+ Don't make this directly. Use `make_ciphertext_ballot` instead.
627633 :field object_id: A unique Ballot ID that is relevant to the external system
628634 """
629635
@@ -639,22 +645,34 @@ class CiphertextBallot(ElectionObjectBase, CryptoHashCheckable):
639645 contests : List [CiphertextBallotContest ]
640646 """List of contests for this ballot"""
641647
642- tracking_hash : Optional [ElementModQ ] = field ( init = False )
648+ tracking_hash : Optional [ElementModQ ]
643649 """Unique ballot tracking hash for this ballot"""
644650
645- timestamp : int = field ( init = False )
651+ timestamp : int
646652 """Timestamp at which the ballot encryption is generated in tick"""
647653
648- crypto_hash : ElementModQ = field ( init = False )
654+ crypto_hash : ElementModQ
649655 """The hash of the encrypted ballot representation"""
650656
651- nonce : Optional [ElementModQ ] = field ( default = None )
657+ nonce : Optional [ElementModQ ]
652658 """The nonce used to encrypt this ballot. Sensitive & should be treated as a secret"""
653659
654- def __post_init__ (self ) -> None :
655- self .crypto_hash = self .crypto_hash_with (self .description_hash )
656- self .timestamp = to_ticks (datetime .utcnow ())
657- self .generate_tracking (self .previous_tracking_hash )
660+ def __eq__ (self , other : Any ) -> bool :
661+ return (
662+ isinstance (other , CiphertextBallot )
663+ and self .object_id == other .object_id
664+ and self .ballot_style == other .ballot_style
665+ and self .description_hash == other .description_hash
666+ and self .previous_tracking_hash == other .previous_tracking_hash
667+ and _list_eq (self .contests , other .contests )
668+ and self .tracking_hash == other .tracking_hash
669+ and self .timestamp == other .timestamp
670+ and self .crypto_hash == other .crypto_hash
671+ and self .nonce == other .nonce
672+ )
673+
674+ def __ne__ (self , other : Any ) -> bool :
675+ return not self .__eq__ (other )
658676
659677 @staticmethod
660678 def nonce_seed (
@@ -680,16 +698,6 @@ def hashed_ballot_nonce(self) -> Optional[ElementModQ]:
680698
681699 return self .nonce_seed (self .description_hash , self .object_id , self .nonce )
682700
683- def generate_tracking (self , seed_hash : ElementModQ ) -> None :
684- """
685- Generate a tracking hash from given hash and existing ballot hash
686- :param seed_hash: Seed hash whether starting or previous
687- """
688- self .previous_tracking_hash = seed_hash
689- self .tracking_hash = get_rotating_tracker_hash (
690- self .previous_tracking_hash , self .timestamp , self .crypto_hash
691- )
692-
693701 def get_tracker_code (self ) -> Optional [str ]:
694702 """
695703 Get a tracker hash as a code in friendly readable words for sharing
@@ -787,51 +795,154 @@ class BallotBoxState(Enum):
787795 """
788796
789797
790- @dataclass
798+ @dataclass ( unsafe_hash = True )
791799class CiphertextAcceptedBallot (CiphertextBallot ):
792800 """
793- a `CiphertextAcceptedBallot` represents a ballot that is accepted for inclusion in election results.
794- an accepted ballot is or is about to be either cast or spoiled.
801+ A `CiphertextAcceptedBallot` represents a ballot that is accepted for inclusion in election results.
802+ An accepted ballot is or is about to be either cast or spoiled.
795803 The state supports the `BallotBoxState.UNKNOWN` enumeration to indicate that this object is mutable
796804 and has not yet been explicitly assigned a specific state.
797805
798- note, additionally, this ballot includes all proofs but no nonces
806+ Note, additionally, this ballot includes all proofs but no nonces.
807+
808+ Do not make this class directly. Use `make_ciphertext_accepted_ballot` or `from_ciphertext_ballot` instead.
799809 """
800810
801- tracking_hash : Optional [ElementModQ ] = None
802- timestamp : int = 0
803- state : BallotBoxState = field (default = BallotBoxState .UNKNOWN )
811+ state : BallotBoxState
812+
813+ def __eq__ (self , other : Any ) -> bool :
814+ return (
815+ isinstance (other , CiphertextAcceptedBallot )
816+ and super .__eq__ (self , other )
817+ and self .state == other .state
818+ )
819+
820+ def __ne__ (self , other : Any ) -> bool :
821+ return not self .__eq__ (other )
822+
823+
824+ def make_ciphertext_ballot (
825+ object_id : str ,
826+ ballot_style : str ,
827+ description_hash : ElementModQ ,
828+ previous_tracking_hash : Optional [ElementModQ ],
829+ contests : List [CiphertextBallotContest ],
830+ nonce : Optional [ElementModQ ] = None ,
831+ timestamp : Optional [int ] = None ,
832+ tracking_hash : Optional [ElementModQ ] = None ,
833+ ) -> CiphertextBallot :
834+ """
835+ Makes a `CiphertextBallot`, initially in the state where it's neither been cast nor spoiled.
836+
837+ :param object_id: the object_id of this specific ballot
838+ :param ballot_style: The `object_id` of the `BallotStyle` in the `Election` Manifest
839+ :param description_hash: Hash of the election metadata
840+ :param crypto_base_hash: Hash of the cryptographic election context
841+ :param contests: List of contests for this ballot
842+ :param timestamp: Timestamp at which the ballot encryption is generated in tick
843+ :param previous_tracking_hash: Previous tracking hash or seed hash
844+ :param nonce: optional nonce used as part of the encryption process
845+ """
846+
847+ if len (contests ) == 0 :
848+ log_warning (f"ciphertext ballot with no contests" )
849+
850+ contest_hashes = [contest .crypto_hash for contest in contests ]
851+ contest_hash = hash_elems (object_id , description_hash , * contest_hashes )
852+
853+ timestamp = to_ticks (datetime .utcnow ()) if timestamp is None else timestamp
854+ if previous_tracking_hash is None :
855+ previous_tracking_hash = description_hash
856+ if tracking_hash is None :
857+ tracking_hash = get_rotating_tracker_hash (
858+ previous_tracking_hash , timestamp , contest_hash
859+ )
860+
861+ return CiphertextBallot (
862+ object_id = object_id ,
863+ ballot_style = ballot_style ,
864+ description_hash = description_hash ,
865+ previous_tracking_hash = previous_tracking_hash ,
866+ contests = contests ,
867+ tracking_hash = tracking_hash ,
868+ timestamp = timestamp ,
869+ nonce = nonce ,
870+ crypto_hash = contest_hash ,
871+ )
872+
873+
874+ def make_ciphertext_accepted_ballot (
875+ object_id : str ,
876+ ballot_style : str ,
877+ description_hash : ElementModQ ,
878+ previous_tracking_hash : Optional [ElementModQ ],
879+ contests : List [CiphertextBallotContest ],
880+ tracking_hash : Optional [ElementModQ ],
881+ timestamp : Optional [int ] = None ,
882+ state : BallotBoxState = BallotBoxState .UNKNOWN ,
883+ ) -> CiphertextAcceptedBallot :
804884 """
805- the state of the ballot
885+ Makes a `CiphertextAcceptedBallot`, ensuring that no nonces are part of the contests.
886+
887+ :param object_id: the object_id of this specific ballot
888+ :param ballot_style: The `object_id` of the `BallotStyle` in the `Election` Manifest
889+ :param description_hash: Hash of the election metadata
890+ :param previous_tracking_hash: Previous tracking hash or seed hash
891+ :param contests: List of contests for this ballot
892+ :param timestamp: Timestamp at which the ballot encryption is generated in tick
893+ :param state: ballot box state
806894 """
807895
808- def __post_init__ ( self ,) -> None :
809- super (). __post_init__ ( )
896+ if len ( contests ) == 0 :
897+ log_warning ( f"ciphertext ballot with no contests" )
810898
811- # HACK: ISSUE: #45: Accepted ballots should not have a nonce assoiciated with them
899+ contest_hashes = [contest .crypto_hash for contest in contests ]
900+ contest_hash = hash_elems (object_id , description_hash , * contest_hashes )
812901
813- for contest in self .contests :
814- for selection in contest .ballot_selections :
815- selection .nonce = None
816- contest .nonce = None
902+ timestamp = to_ticks (datetime .utcnow ()) if timestamp is None else timestamp
903+ if previous_tracking_hash is None :
904+ previous_tracking_hash = description_hash
905+ if tracking_hash is None :
906+ tracking_hash = get_rotating_tracker_hash (
907+ previous_tracking_hash , timestamp , contest_hash
908+ )
817909
818- self .nonce = None
910+ # copy the contests and selections, removing all nonces
911+ new_contests : List [CiphertextBallotContest ] = []
912+ for contest in contests :
913+ new_selections = [
914+ replace (selection , nonce = None ) for selection in contest .ballot_selections
915+ ]
916+ new_contest = replace (contest , nonce = None , ballot_selections = new_selections )
917+ new_contests .append (new_contest )
918+
919+ return CiphertextAcceptedBallot (
920+ object_id = object_id ,
921+ ballot_style = ballot_style ,
922+ description_hash = description_hash ,
923+ previous_tracking_hash = previous_tracking_hash ,
924+ contests = new_contests ,
925+ tracking_hash = tracking_hash ,
926+ timestamp = timestamp ,
927+ crypto_hash = contest_hash ,
928+ nonce = None ,
929+ state = state ,
930+ )
819931
820932
821933def from_ciphertext_ballot (
822- ballot : CiphertextBallot , state : BallotBoxState
934+ ballot : CiphertextBallot , state : BallotBoxState = BallotBoxState . UNKNOWN
823935) -> CiphertextAcceptedBallot :
824936 """
825- Convert a `CiphertextBallot` into a `CiphertextAcceptedBallot` with the correct state
937+ Convert a `CiphertextBallot` into a `CiphertextAcceptedBallot`, with all nonces removed.
826938 """
827-
828- return CiphertextAcceptedBallot (
939+ return make_ciphertext_accepted_ballot (
829940 object_id = ballot .object_id ,
830941 ballot_style = ballot .ballot_style ,
831942 description_hash = ballot .description_hash ,
832- previous_tracking_hash = ballot .previous_tracking_hash ,
833943 contests = ballot .contests ,
834- tracking_hash = ballot .tracking_hash ,
835944 timestamp = ballot .timestamp ,
945+ previous_tracking_hash = ballot .previous_tracking_hash ,
946+ tracking_hash = ballot .tracking_hash ,
836947 state = state ,
837948 )
0 commit comments