Skip to content

[Peras 23] wFA^LS and EveryoneVotes voting committee implementations#1975

Open
agustinmista wants to merge 5 commits intomainfrom
peras/wfals-everyonevotes-implementations
Open

[Peras 23] wFA^LS and EveryoneVotes voting committee implementations#1975
agustinmista wants to merge 5 commits intomainfrom
peras/wfals-everyonevotes-implementations

Conversation

@agustinmista
Copy link
Copy Markdown
Contributor

@agustinmista agustinmista commented Apr 13, 2026

This PR implements two concrete voting committee strategies:

  1. Weighted Fait-Accompli with Local Sortition (WFALS), and
  2. A simpler one where every pool with positive stake can vote (EveryoneVotes)

While Peras will use WFALS at the beginning, EveryoneVotes serves as a baseline for future benchmarks, as well as a starting point for Leios shall they end up opting for a simpler approach.

NOTES:

  • WFALS depends on two separate sub-componenents:

    • WFA: the pure weighted Fait-Accompli seat split algorithm, which relies on an "extended stake distribution" ExtWFAStakeDistr (a sorted array of pools with cumulative stake). The latter can be precomputed at the beginning of an epoch and shared across multiple voting committees (e.g. shared between Leios and Peras).
    • LS: the Local Sortition check for non-persistent voters, mapping their VRF outputs into seats sampled through a Poisson distribution.
  • For simplicity, EveryoneVotes reuses the same ExtWFAStakeDistr as WFALS. This way, if Leios is instantiated with EveryoneVotes, it could still use the same precomputed data as Peras.

  • Both voting committee implementations defined their own abstract vote and certificate type and it's up to the clients to marshal between these types and their concrete ones (possibly using the same underlying representation and serialization routines)

@agustinmista agustinmista self-assigned this Apr 13, 2026
@agustinmista agustinmista marked this pull request as ready for review April 13, 2026 12:00
@dnadales dnadales moved this to 🗓️ Next up in Consensus Team Backlog Apr 13, 2026
@dnadales dnadales self-assigned this Apr 13, 2026
@agustinmista agustinmista force-pushed the peras/wfals-everyonevotes-implementations branch from 4fcfdf3 to 19f3766 Compare April 14, 2026 07:48
@agustinmista agustinmista force-pushed the peras/generic-voting-committee-api branch from d6a1575 to 8ead4f4 Compare April 14, 2026 07:48
@agustinmista agustinmista force-pushed the peras/wfals-everyonevotes-implementations branch from 19f3766 to 9c10f37 Compare April 14, 2026 14:04
@agustinmista agustinmista force-pushed the peras/generic-voting-committee-api branch from 8ead4f4 to 7381729 Compare April 14, 2026 14:04
@agustinmista agustinmista force-pushed the peras/wfals-everyonevotes-implementations branch from 9c10f37 to 9bd7727 Compare April 15, 2026 12:44
@agustinmista agustinmista force-pushed the peras/generic-voting-committee-api branch from 7381729 to 047702c Compare April 15, 2026 12:44
@tbagrel1 tbagrel1 force-pushed the peras/generic-voting-committee-api branch from 047702c to c5b05a6 Compare April 20, 2026 07:37
Base automatically changed from peras/generic-voting-committee-api to main April 20, 2026 08:47
@qnikst qnikst linked an issue Apr 21, 2026 that may be closed by this pull request
agustinmista and others added 3 commits April 21, 2026 11:48
This commit implements the deterministic core of the weighted
Fait-Accompli algorithm using a precomputed extended stake distribution,
shareable across multiple voting committees running on the same epoch.

The implementation includes a tiebreaker mechanism to allow altering the
order of pools with the same stake when the threshold index between
persistent and non-persistent voters would land between them. This can
later be instantiated to allow for a fair split across epochs.

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>
This commit implements the local sortition fallback scheme needed by
wFA^LS to allocate non-persistent voters. Each non-persistent voter
provides a VRF output that gets normalized and compared against the
output of a numerically-stable stake-weighted Poisson distribution.

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>
This commit defined the weighted Fait-Accompli with local soritition
voting scheme (WFALS) using the separate WFA and LS components. This
includes the definition of both persistent and non-pesistent abstract
votes and abstract certificates.

NOTE: it is the job of the low-level vote and certificate implementation
to provide the plumbing needed to convert between abstract and concrete
values, possibly allowing the same concrete definitions to work with
multiple voting commitee implementations.

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>
agustinmista and others added 2 commits April 21, 2026 11:48
This commit implements EveryoneVotes a simpler alternative to WFALS
where every voter with non-negative stake is entitled to vote. This
exists as a baseline to run benchmarks against later on.

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>
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>
@agustinmista agustinmista force-pushed the peras/wfals-everyonevotes-implementations branch from 9bd7727 to ddbf3ee Compare April 21, 2026 09:53
Array
SeatIndex
( PoolId -- Voter ID of this voter
, a -- Extra payload associated to this voter
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give an example here (eg pubkey I guess?)

(LedgerStake voterStake)
cumulativeStake

-- | Evaluate whether a voter with its give stake and relatile position in the
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- | Evaluate whether a voter with its give stake and relatile position in the
-- | Evaluate whether a voter with the given stake and relative position in the

(VotingCommitteeError crypto WFALS)
(VRFOutput crypto)
checkVRFOutput context electionId committee =
bimap InvalidVoteSignature id $ do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evalVRF errors come from the VRF layer (invalid proof, signing-key issues), not from vote-signature verification — reporting them as InvalidVoteSignature is misleading and also leaves the purpose-built LocalSortitionError String constructor (declared at L164) as dead code.

Suggest swapping to bimap LocalSortitionError id. Propagates to both call sites (implCheckShouldVote L281 and implVerifyVote L361) and makes the existing error constructor actually reachable.

WFALSCert
(getElectionIdFromVotes votes)
(getVoteCandidateFromVotes votes)
(NEMap.fromAscList voters)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEMap.fromAscList voters silently deduplicates entries with the same SeatIndex, while sconcat voteSignatures on the line below keeps every signature. If any two elements of voters share a seat index, the resulting cert has fewer seat entries than signatures; downstream, trivialVerifyAggregateVoteSignature (Committee/Crypto.hs L181–183) rejects it with an opaque "number of keys and signatures do not match" string.

The root cause sits at the class level: VotesWithSameTarget (Committee/Class.hs L119–123) guarantees shared election and candidate but makes no claim about voter distinctness, and ensureSameTarget does not enforce any. Byzantine voters can legitimately produce same-seat-index votes with different signatures / VRF outputs, so this is not purely a caller-hygiene issue.

Three options; worth choosing one:

  • (a) Strengthen VotesWithSameTarget to carry a distinctness guarantee on a per-instance voter-identity key (enforced in ensureSameTarget).
  • (b) Change forgeCert's return to Either (VotingCommitteeError …) (Cert …) so duplicates can be rejected with a dedicated error constructor.
  • (c) Document at the class level that forgeCert callers must pre-deduplicate — band-aid, weakest of the three.

EveryoneVotesCert
(getElectionIdFromVotes votes)
(getVoteCandidateFromVotes votes)
(NESet.fromList voters)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same class-level concern as noted on WFALS.hsNESet.fromList here silently dedupes seat indices while sconcat below retains every signature. The fix is structural, at VotesWithSameTarget in Committee/Class.hs.

(LedgerStake voterStake)
cumulativeStake

-- | Evaluate whether a voter with its give stake and relatile position in the
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment typos collected across both files:

  • WFA.hs L186: "with its give stake and relatile position" → given, relative
  • WFALS.hs L162: "committe" → committee
  • WFALS.hs L383: "NOTE: theres is" → there is

One-line fixes, grouped here for visibility.

import qualified Data.Array as Array
import Data.Bifunctor (Bifunctor (..))
import Data.Containers.NonEmpty (HasNonEmpty (..))
import Data.Data (Proxy (..))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import polish, bundled:

1. WFALS.hs L55import Data.Data (Proxy (..)): Data.Data re-exports Proxy for historical reasons, but the canonical location is Data.Proxy. EveryoneVotes.hs L36 already uses Data.Proxy in this PR, so switching here aligns the two modules.

2. EveryoneVotes.hs L25–26NonZero is imported twice:

import Cardano.Ledger.BaseTypes (HasZero (..), NonZero, Nonce)
import Cardano.Ledger.BaseTypes.NonZero (NonZero (..), nonZero)

Harmless but easy to trim: drop NonZero from the first line (the second brings it in with constructors already).

cumulativeStakeAndPools

((_totalStake, numPoolsWithPositiveStakeAcc), cumulativeStakeAndPools) =
List.mapAccumR
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapAccumR (right-to-left) is load-bearing here: seat 0's cumulative stake ends up equal to the total, and the last seat's cumulative equals its own stake. Flipping to mapAccumL would silently invert the cumulative semantics, corrupting isAbovePersistentSeatThreshold.

The direction is encoded in a single character (R vs L) which is easy to skim past. A brief comment at the call site would help:

-- mapAccumR (right-to-left) so seat 0's cumulative = total stake
-- and the last seat's cumulative = its own stake.

Follow-up worth considering: PR #1977's model already uses a phantom-typed Cumulative phase (WFALS/Model.hs L88, L451 — Stake Ledger Cumulative) that mechanically distinguishes left- from right-cumulative. Mirroring this on the real Cumulative a type in Committee/Types.hs would make the direction a compile-time guarantee rather than a comment-level contract. Larger refactor, but aligns real and model implementations.

Test gap: PR #1977 tests the direction on the model, and the real impl is pinned to the model via modelConformsToRealImplementation — so direction regressions are caught transitively. A direct property on the real implementation (cumulative[0] == sum of all stakes, cumulative[lastSeat] == stake at last seat) would make the invariant explicit at the implementation side, not just indirect via conformance. Small addition to WFALS/Tests.hs.

SeatIndex ->
-- | Current voter stake
LedgerStake ->
-- | Cumulated stake of voters with smaller or equal stake than the current one
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring precision nit: "Cumulated stake of voters with smaller or equal stake than the current one" is accurate when there are no ties. With ties, the tiebreaker splits equal-stake pools across seat indices, and the cumulative at seat i only includes the subset of equal-stake pools that come after seat i in tiebreaker order (plus the current voter) — not all equal-stake pools.

The mathematically clean characterization is just seat-index-based:

-- | Cumulated stake of voters at the current seat index or later in sorted order.
--   Equivalently: total stake of voters with stake ≤ this voter's, with ties
--   resolved by the tiebreaker.
Cumulative LedgerStake ->

Matters only at ties, but a reader building a mental model from the current wording may underestimate how the tiebreaker affects "equal stake" accounting.

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

-- | Local sortition used by non-persistent members of the voting committee
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module's header is a single line ("Local sortition used by non-persistent members of the voting committee") with no reference to the paper or CIP-0164. Compare with WFALS.hs L13–29, which cites the Fait-Accompli paper with DOI and eprint link.

Since LS.hs implements non-trivial numerical machinery (Poisson thresholding, Taylor expansion), a reader trying to verify the implementation against a spec currently has to go hunting. Two lines would close the gap:

-- | Local sortition for non-persistent voters.
--
-- Implements the @LS@ component of the wFA^LS scheme from the
-- Fait-Accompli Committee Selection paper
-- (https://eprint.iacr.org/2023/1273.pdf, §<section>).
--
-- Also specified in CIP-0164:
-- https://github.com/cardano-scaling/CIPs/blob/leios/CIP-0164/README.md

(Author should supply the correct paper section — I have not cross-referenced.)

@jasagredo jasagredo moved this from 🗓️ Next up to 👀 In review in Consensus Team Backlog Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

Template Vote and Cert Cryptography Voting Committee selection logic Implement committee selection schemes

3 participants