Problem
PR #2020 made UniqueVotesWithSameTarget a properly opaque carrier whose construction checks for duplicates, replacing the silent-dedup hazard at the type level (option (a) of the original forgeCert dedup concern).
The remaining concern is at the caller side: uniqueness is parameterized by the cmpVotes :: Vote crypto committee -> Vote crypto committee -> Ordering function the caller supplies, so the duplicate-rejection guarantee is only as strong as that comparator.
For WFALS and EveryoneVotes the underlying hazard is two votes sharing a SeatIndex but differing in signature / VRF output:
WFALS.implForgeCert (WFALS.hs:456) calls NEMap.fromAscList voters while signatures are aggregated separately.
If two voters entries share a SeatIndex, the cert's seat-index map has fewer entries than the signature aggregate, and downstream verifyCert fails with the opaque "number of keys and signatures do not match" error.
EveryoneVotes.implForgeCert (EveryoneVotes.hs:265) has the same shape via NESet.fromList voters.
The Peras integration site that builds UniqueVotesWithSameTarget from inbound votes must therefore choose a cmpVotes that returns EQ for any two votes that would collide at the seat-index level (or, more conservatively, on underlying voter identity).
Proposed fix
- Locate the call site that builds
UniqueVotesWithSameTarget for the Peras voting committee.
- Confirm
cmpVotes orders EQ on shared SeatIndex for both persistent and non-persistent variants of WFALS and EveryoneVotes Vote constructors.
- Add a property test that generates votes with seeded seat-index collisions and asserts
ensureUniqueVotesWithSameTarget returns Left (DuplicateVotes …) for them.
References
Problem
PR #2020 made
UniqueVotesWithSameTargeta properly opaque carrier whose construction checks for duplicates, replacing the silent-dedup hazard at the type level (option (a) of the originalforgeCertdedup concern).The remaining concern is at the caller side: uniqueness is parameterized by the
cmpVotes :: Vote crypto committee -> Vote crypto committee -> Orderingfunction the caller supplies, so the duplicate-rejection guarantee is only as strong as that comparator.For
WFALSandEveryoneVotesthe underlying hazard is two votes sharing aSeatIndexbut differing in signature / VRF output:WFALS.implForgeCert(WFALS.hs:456) callsNEMap.fromAscList voterswhile signatures are aggregated separately.If two
votersentries share aSeatIndex, the cert's seat-index map has fewer entries than the signature aggregate, and downstreamverifyCertfails with the opaque "number of keys and signatures do not match" error.EveryoneVotes.implForgeCert(EveryoneVotes.hs:265) has the same shape viaNESet.fromList voters.The Peras integration site that builds
UniqueVotesWithSameTargetfrom inbound votes must therefore choose acmpVotesthat returnsEQfor any two votes that would collide at the seat-index level (or, more conservatively, on underlying voter identity).Proposed fix
UniqueVotesWithSameTargetfor the Peras voting committee.cmpVotesordersEQon sharedSeatIndexfor both persistent and non-persistent variants ofWFALSandEveryoneVotesVoteconstructors.ensureUniqueVotesWithSameTargetreturnsLeft (DuplicateVotes …)for them.References