11{-# LANGUAGE FlexibleContexts #-}
2- {-# LANGUAGE LambdaCase #-}
32{-# LANGUAGE MultiParamTypeClasses #-}
3+ {-# LANGUAGE ScopedTypeVariables #-}
4+ {-# LANGUAGE TypeApplications #-}
45{-# LANGUAGE TypeFamilies #-}
56
67-- | Generic interface used by implementations of voting committees.
@@ -9,18 +10,23 @@ module Ouroboros.Consensus.Committee.Class
910 CryptoSupportsVotingCommittee (.. )
1011
1112 -- * Votes with same target
12- , VotesWithSameTarget
13+ , UniqueVotesWithSameTarget
1314 , getElectionIdFromVotes
1415 , getVoteCandidateFromVotes
1516 , getRawVotes
16- , VotesWithSameTargetError (.. )
17- , ensureSameTarget
17+ , UniqueVotesWithSameTargetError (.. )
18+ , ensureUniqueVotesWithSameTarget
19+ , unsafeUniqueVotesWithSameTarget
20+ , checkUniqueVotesWithSameTarget -- for testing purposes only
1821 ) where
1922
23+ import Control.Exception (assert )
2024import Data.Containers.NonEmpty (HasNonEmpty (.. ))
21- import Data.Either (partitionEithers )
25+ import Data.Either (isRight )
2226import Data.Kind (Type )
2327import Data.List.NonEmpty (NonEmpty (.. ))
28+ import qualified Data.List.NonEmpty as NonEmpty
29+ import Data.Proxy (Proxy (.. ))
2430import Ouroboros.Consensus.Committee.Crypto
2531 ( CryptoSupportsVoteSigning
2632 , ElectionId
@@ -102,7 +108,7 @@ class
102108
103109 -- | Forge a certificate attesting the winner of a given election
104110 forgeCert ::
105- VotesWithSameTarget crypto committee ->
111+ UniqueVotesWithSameTarget crypto committee ->
106112 Either
107113 (VotingCommitteeError crypto committee )
108114 (Cert crypto committee )
@@ -117,74 +123,180 @@ class
117123
118124-- * Votes with same target
119125
120- -- | Collection of votes all targeting the same election and candidate
121- data VotesWithSameTarget crypto committee
122- = VotesWithSameTarget
126+ -- | Collection of unique votes all targeting the same election and candidate
127+ data UniqueVotesWithSameTarget crypto committee
128+ = UniqueVotesWithSameTarget
123129 (ElectionId crypto )
124130 (VoteCandidate crypto )
125131 (NE [Vote crypto committee ])
126132
127133-- | Get the election identifier targeted by a collection of votes
128134getElectionIdFromVotes ::
129- VotesWithSameTarget crypto committee ->
135+ UniqueVotesWithSameTarget crypto committee ->
130136 ElectionId crypto
131- getElectionIdFromVotes (VotesWithSameTarget electionId _ _) =
137+ getElectionIdFromVotes (UniqueVotesWithSameTarget electionId _ _) =
132138 electionId
133139
134140-- | Get the vote candidate targeted by a collection of votes
135141getVoteCandidateFromVotes ::
136- VotesWithSameTarget crypto committee ->
142+ UniqueVotesWithSameTarget crypto committee ->
137143 VoteCandidate crypto
138- getVoteCandidateFromVotes (VotesWithSameTarget _ candidate _) =
144+ getVoteCandidateFromVotes (UniqueVotesWithSameTarget _ candidate _) =
139145 candidate
140146
141147-- | Get the raw votes from a collection of votes with the same target.
142148--
143149-- NOTE: this returns votes in ascending seat index order.
144150getRawVotes ::
145- VotesWithSameTarget crypto committee ->
151+ UniqueVotesWithSameTarget crypto committee ->
146152 NE [Vote crypto committee ]
147- getRawVotes (VotesWithSameTarget _ _ votes) =
153+ getRawVotes (UniqueVotesWithSameTarget _ _ votes) =
148154 votes
149155
150156-- | Errors when votes do not all target the same election and candidate
151- data VotesWithSameTargetError crypto committee
152- = EmptyVotes
157+ data UniqueVotesWithSameTargetError vote
158+ = DuplicateVotes
159+ -- A cluster of votes equal under the supplied ordering, i.e.,
160+ -- either true duplicates or equivocating votes (same id, different
161+ -- target).
162+ (NE [vote ])
153163 | TargetMismatch
154- -- First vote and the rest of the votes that match its target
155- (NE [Vote crypto committee ])
164+ -- First vote (under the supplied ordering) whose target is treated
165+ -- as the canonical target for the comparison
166+ vote
156167 -- Votes that do not match the target of the first vote
157- (NE [Vote crypto committee ])
168+ (NE [vote ])
158169
159- -- | Check that a list of votes all target the same election and candidate
160- ensureSameTarget ::
170+ -- | Check that a non-empty list of votes all target the same election and
171+ -- candidate and there are no duplicates.
172+ --
173+ -- NOTE: duplicates are reported in preference to target mismatches.
174+ ensureUniqueVotesWithSameTarget ::
175+ forall crypto committee .
161176 ( Eq (ElectionId crypto )
162177 , Eq (VoteCandidate crypto )
163178 ) =>
179+ -- | How to project the target from an abstract vote
164180 (Vote crypto committee -> (ElectionId crypto , VoteCandidate crypto )) ->
165- [Vote crypto committee ] ->
181+ -- | How to compare votes by ID, where EQ means that two votes have the same
182+ -- ID and are either total duplicates, or are equivocating (i.e., they have
183+ -- the same ID but a different target)
184+ (Vote crypto committee -> Vote crypto committee -> Ordering ) ->
185+ -- | Collection of votes to check
186+ NE [Vote crypto committee ] ->
166187 Either
167- (VotesWithSameTargetError crypto committee )
168- (VotesWithSameTarget crypto committee )
169- ensureSameTarget getTarget = \ case
170- [] ->
171- Left EmptyVotes
172- (firstVote : nextVotes) -> do
173- case partitionEithers (fmap matchesTarget nextVotes) of
174- ([] , matchingVotes) ->
175- Right $
176- VotesWithSameTarget
188+ (UniqueVotesWithSameTargetError (Vote crypto committee ))
189+ (UniqueVotesWithSameTarget crypto committee )
190+ ensureUniqueVotesWithSameTarget getTarget cmpVotes votes =
191+ fmap
192+ ( const
193+ ( UniqueVotesWithSameTarget
177194 electionId
178195 candidate
179- (firstVote :| matchingVotes)
180- (firstMismatchingVote : nextMismatchingVotes, matchingVotes) ->
181- Left $
182- TargetMismatch
183- (firstVote :| matchingVotes)
184- (firstMismatchingVote :| nextMismatchingVotes)
185- where
186- target@ (electionId, candidate) =
187- getTarget firstVote
188- matchesTarget v'
189- | getTarget v' /= target = Left v'
190- | otherwise = Right v'
196+ (firstVote :| nextVotes)
197+ )
198+ )
199+ $ checkUniqueVotesWithSameTarget
200+ (Proxy @ crypto )
201+ getTarget
202+ cmpVotes
203+ votes
204+ where
205+ firstVote :| nextVotes = votes
206+ (electionId, candidate) = getTarget firstVote
207+
208+ -- | Same as 'ensureUniqueVotesWithSameTarget' but turns the invariant
209+ -- checks into assertions.
210+ --
211+ -- WARNING: asserts become a no-op if the code is compiled with optimizations,
212+ -- thus this function should only be used in production when the caller can
213+ -- guarantee that the input votes satisfy the contract.
214+ unsafeUniqueVotesWithSameTarget ::
215+ forall crypto committee .
216+ ( Eq (ElectionId crypto )
217+ , Eq (VoteCandidate crypto )
218+ ) =>
219+ -- | How to project the target from an abstract vote
220+ (Vote crypto committee -> (ElectionId crypto , VoteCandidate crypto )) ->
221+ -- | How to compare votes by ID, where EQ means that two votes have the same
222+ -- ID and are either total duplicates, or are equivocating (i.e., they have
223+ -- the same ID but a different target)
224+ (Vote crypto committee -> Vote crypto committee -> Ordering ) ->
225+ -- | Collection of votes to check
226+ NE [Vote crypto committee ] ->
227+ UniqueVotesWithSameTarget crypto committee
228+ unsafeUniqueVotesWithSameTarget getTarget cmpVotes votes =
229+ assert
230+ ( isRight
231+ ( checkUniqueVotesWithSameTarget
232+ (Proxy @ crypto )
233+ getTarget
234+ cmpVotes
235+ votes
236+ )
237+ )
238+ $ UniqueVotesWithSameTarget
239+ electionId
240+ candidate
241+ (firstVote :| nextVotes)
242+ where
243+ firstVote :| nextVotes = votes
244+ (electionId, candidate) = getTarget firstVote
245+
246+ -- | Validate that a non-empty collection of votes is well-formed for
247+ -- certificate forging: all votes target the same election and candidate
248+ -- (per @getTarget@), and no two votes are equal under @cmpVotes@.
249+ --
250+ -- Equality (@EQ@) is treated as evidence of a duplicate or equivocating vote
251+ -- and is reported via 'DuplicateVotes' in preference to any 'TargetMismatch'.
252+ --
253+ -- NOTE: this is exposed for testing; production code should use
254+ -- 'ensureUniqueVotesWithSameTarget' or 'unsafeUniqueVotesWithSameTarget'.
255+ checkUniqueVotesWithSameTarget ::
256+ ( Eq (ElectionId crypto )
257+ , Eq (VoteCandidate crypto )
258+ ) =>
259+ Proxy crypto ->
260+ -- | How to project the target an abstract vote
261+ (vote -> (ElectionId crypto , VoteCandidate crypto )) ->
262+ -- | How to compare votes by ID, where EQ means that two votes have the same
263+ -- ID and are either total duplicates, or are equivocating (i.e., they have
264+ -- the same ID but a different target)
265+ (vote -> vote -> Ordering ) ->
266+ -- | Collection of votes to check
267+ NE [vote ] ->
268+ Either (UniqueVotesWithSameTargetError vote ) ()
269+ checkUniqueVotesWithSameTarget _ getTarget cmpVotes votes =
270+ go firstVote nextVotes
271+ where
272+ -- Sort the votes so checking for duplicates can be done in the same pass as
273+ -- checking for target mismatches
274+ firstVote :| nextVotes =
275+ NonEmpty. sortBy cmpVotes votes
276+
277+ -- Check that all votes have the same target and there are no duplicates. The
278+ -- first argument is the last vote we have checked, passed sequentially to the
279+ -- next step to check against duplicates (the input must be sorted for this).
280+ go _ [] =
281+ Right ()
282+ go v (v' : vs)
283+ | isDuplicateOf v v' = do
284+ -- NOTE: duplicates are contiguous because the input is sorted by
285+ -- @cmpVotes@, thus we can stop at the first non-duplicate vote.
286+ let duplicateVotes = v :| (v' : takeWhile (isDuplicateOf v) vs)
287+ Left (DuplicateVotes duplicateVotes)
288+ | doesNotMatchFirstVoteTarget v' = do
289+ -- NOTE: mismatches are /not/ necessarily contiguous, thus we cannot
290+ -- stop at the first matching vote.
291+ let mismatchingVotes = v' :| filter doesNotMatchFirstVoteTarget vs
292+ Left (TargetMismatch firstVote mismatchingVotes)
293+ | otherwise =
294+ go v' vs
295+
296+ -- Check if a vote does not match the target of the first vote
297+ doesNotMatchFirstVoteTarget v =
298+ getTarget v /= getTarget firstVote
299+
300+ -- Check if two votes are duplicates of each other acording to @cmpVotes@
301+ isDuplicateOf v v' =
302+ cmpVotes v v' == EQ
0 commit comments