Skip to content

Commit 54452b6

Browse files
agustinmistanbacqueytbagrel1
committed
Tweak VotesWithSameTarget to also check of duplicates
In addition to enforcing that all votes used to forge a certificate have the same target (election ID and candidate), we should also conservatively also check for duplicates to avoid any bug that would silently allow for potentially equivocating votes to slip into a certificate. This commit tweaks the definition of VotesWithSameTarget (the data structure expected by forgeCert) to addionally check for duplicates when constructing it. This is done by asking for an additional ordering predicate between votes, where equality signals duplicated (i.e. potentially equivocating) votes. In addition, we now provide an additional (unsafe) smart constructor that runs this test via compiler-enabled assertion. Which can be used in the case that the client code can guarantee the desired invariants by construction. Finally, it relaxes the smart constructor interface a bit to allow for easier testing. Co-authored-by: Nicolas BACQUEY <nicolas.bacquey@tweag.io> Co-authored-by: Thomas BAGREL <thomas.bagrel@tweag.io> Co-authored-by: Agustin Mista <agustin.mista@moduscreate.com>
1 parent 0bbe23f commit 54452b6

1 file changed

Lines changed: 157 additions & 45 deletions

File tree

  • ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee

ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/Class.hs

Lines changed: 157 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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)
2024
import Data.Containers.NonEmpty (HasNonEmpty (..))
21-
import Data.Either (partitionEithers)
25+
import Data.Either (isRight)
2226
import Data.Kind (Type)
2327
import Data.List.NonEmpty (NonEmpty (..))
28+
import qualified Data.List.NonEmpty as NonEmpty
29+
import Data.Proxy (Proxy (..))
2430
import 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
128134
getElectionIdFromVotes ::
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
135141
getVoteCandidateFromVotes ::
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.
144150
getRawVotes ::
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

Comments
 (0)