net-rs: spec-faithful WFA+LS voting integration
Splits CommitteeSelection::WfaLs into per-epoch persistent voters and
per-EB non-persistent voters, each backed by stake-weighted lotteries.
Vote bodies carry no explicit weight; aggregators derive weight from
external state, mirroring CIP-0164.
WfaLs now has { persistent_voters, non_persistent_voters } (defaults
480 + 120, matching sim-rs e30087cdf). Per-startup wFA committee is
allocated identically by every node from the stake registry + a
shared seed (genesis_time_unix), so each node knows its own seat
count and every other pool's without communication.
Per-EB NPV: each pool computes a deterministic eligibility signature
from (voter_id, eb_hash, eb_slot) — modeling a CIP-0164 VRF output —
and seeds a per-stake-unit Bernoulli lottery from it. The signature
is what travels on the wire; the count of wins is reconstructed
independently by every aggregator from the signature plus the
voter's ledger-resolved stake.
A pool may emit up to two bodies per EB: one PV (if it holds ≥1 seat
in the persistent committee) and one NPV (if it won ≥1 lottery trial).
Quorum threshold is now weight-based:
Σ weight ≥ quorum_weight_fraction × expected_committee_size
where weight is committee[voter_id] for PV and count_npv_wins(...) for
NPV. expected_committee_size = Σ committee_seats + n_npv (e.g. 600
under defaults). EveryoneVotes / StakeCentile keep simpler unit-weight
semantics with no NPV path.
VoteDecision enum and decide_vote method removed; replaced by per-mode
committee construction at startup plus signature-driven NPV at vote
time. Telemetry: voted_stake → voted_weight on LeiosQuorumReached and
LeiosElectionExpired.
Cluster-verified: WfaLs at 25-node uniform stake produces 19 PV seats
per node (480/25), expected committee 600, quorum at 450 reached
reliably; RbCertifiedEb fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>