Test a pool ranked out of the Leios voting committee - #3711
Merged
Merged
Conversation
mkoura
requested
a lite review from Copilot
and removed request for
saratomaz
September 25, 2026 09:53
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved test-evidence validation gaps remain, and the new helper lacks unit coverage.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Adds Leios committee-ranking and out-of-committee voting coverage, shared helpers, and stronger BLS registration-epoch validation.
Changes:
- Adds reduced-committee ranking and voting tests.
- Adds shared Leios and pool-ID helpers.
- Validates BLS registration epochs across pools.
| File | Summary |
|---|---|
cardano_node_tests/utils/helpers.py |
Adds pool ID normalization. |
cardano_node_tests/tests/test_leios_blocks.py |
Adds committee ranking and voting checks. |
cardano_node_tests/tests/test_bls_rotation.py |
Validates consistent BLS registration epochs. |
cardano_node_tests/tests/leios.py |
Adds shared Leios constants and genesis skip logic. |
cardano_node_tests/tests/bls.py |
Uses shared pool ID normalization. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The Leios voting committee seats the `leiosCommitteeSize` pools with the most stake, largest first, ties broken by ascending pool id. A local testnet has three pools and room for 900, so every pool is always seated and neither the ranking nor what happens to a pool that misses out is covered by anything. Add `TestLeiosCommitteeRank`, which starts a cluster instance whose Dijkstra genesis gives the committee fewer seats than the cluster has pools. The quorum is lowered along with it: a seat is weighted by the pool's share of the active stake, not of the committee, so a committee that doesn't hold every pool cannot reach the default 0.75. The test finds the pools the committee left out, checks that they were left out on rank alone - they have a registered BLS key, and no seated pool ranks behind them - and then checks against the pool logs that they answer every EB announcement with `NotOnCommittee` and cast no vote, while the seated pools vote, decline for no reason of their own, and still add up to a certificate. Along the way: * `leios.NOT_VOTED_MSGS` groups the reasons a pool can give for being unable to vote, for a check that has to notice a pool declining without caring which of them it was. * `leios.skip_if_no_ebs_in_genesis` takes the genesis instead of a cluster instance, so a test that starts a cluster of its own can be ruled out before paying for the startup. * `helpers.get_pool_id_hex` replaces the inline Bech32 conversion in `bls.get_committee_seat`.
`test_expired_bls_key` reads `bksRegisteredIn` from one cluster pool and puts every other pool on the schedule that follows from it. That holds only while the pools really did register their keys in the same epoch, and nothing checked it - a comment said so. The assumption is load bearing. The test checks that every pool that didn't rotate is still voting in `expire_epoch - 1` and that none of them is in `expire_epoch`, and those two are contradictory as soon as the stamps differ: the first needs `expire_epoch` at or below the earliest expiry, the second at or above the latest. Read the stamp of every pool and assert they agree, so a cluster that registered the pools across an epoch boundary says so instead of failing as a pool that kept voting too long.
mkoura
force-pushed
the
leios_committee_rank
branch
from
September 25, 2026 10:05
7f73a31 to
c809281
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

The Leios voting committee seats the
leiosCommitteeSizepools with the most stake,largest first, ties broken by ascending pool id. A local testnet has three pools and
room for 900, so every pool is always seated - neither the ranking nor what happens to
a pool that misses out was covered by anything.
TestLeiosCommitteeRankStarts a cluster instance whose Dijkstra genesis gives the committee fewer seats than
the cluster has pools, which is what makes the ranking observable. It then:
seated pool ranks behind them under
(-stakeSet, poolId), the keyselectLeiosCommitteesorts on;checks that the pools without a seat answer every EB announcement with
NotOnCommitteeand cast no vote, while the seated pools vote, decline for no reasonof their own, and still add up to a certificate.
The quorum is lowered along with the committee. A seat is weighted by the pool's share
of the active stake, not of the committee, so a committee that doesn't hold every
pool tops out at
SMALL_COMMITTEE_SIZE / NUM_POOLSand could never reach the default0.75. That ratio is the bound to respect when changing the committee size, and it is
noted next to the constant.
Evidence
From a full testrun on
leios_fast, with the genesis coming out asleiosCommitteeSize: 2, leiosQuorumStakeThreshold: 0.5:ChainTipDoesNotAnnounceChainTipDoesNotAnnounceNotOnCommitteeTwo things this settled. The reason a seatless pool reports is
NotOnCommittee, notSignerNotInCommittee- worth pinning down, becauseLeiosSeatcarries only a weightand a vkey with no pool id, so it is not obvious from the ledger source which one the
node would give. And the lowered quorum holds up: 21 certificates off 22 announcements,
with seat weights near 1/3 each, means both seated pools voted every time, since one
alone (0.333) is under the 0.5 threshold.
Second commit
test_expired_bls_keyreadbksRegisteredInfrom one cluster pool and put every otherpool on the schedule that followed from it, with a comment standing in for a check. The
assumption is load bearing - the test asserts every non-rotated pool is still voting at
expire_epoch - 1and that none is atexpire_epoch, which needsexpire_epoch <= min(reg) + max_key_ageand>= max(reg) + max_key_agerespectively,so it only holds when the stamps are identical. Now read from every pool and asserted,
so a cluster that registered pools across an epoch boundary reports that instead of
failing as a pool that kept voting too long. Latent today (
POOLS_IN_GENESIS=truestamps everything epoch 0), but the kind of thing that only bites on a slow machine.
Shared code
leios.NOT_VOTED_MSGSgroups the reasons a pool can give for being unable to vote.leios.skip_if_no_ebs_in_genesistakes the genesis instead of a cluster instance, soa test that starts a cluster of its own can be ruled out before paying for the
startup.
helpers.get_pool_id_hexreplaces the inline Bech32 conversion inbls.get_committee_seat.Two notes for whoever reviews:
The test can't be exercised against a dev cluster - .source.dev sets FORBID_RESTART=true, and cluster_getter rejects a custom scriptsdir under it, the same wall test_expired_bls_key hits. It needs a full testrun, which is where the table above comes from.
The plural handling (out_pool_names, cut_off_rank = min(...)) is currently unreachable, since SMALL_COMMITTEE_SIZE = NUM_POOLS - 1 always leaves exactly one pool out. It's there so the test doesn't encode an arithmetic identity that breaks the moment the committee size is chosen independently of the pool count.