Skip to content

Test a pool ranked out of the Leios voting committee - #3711

Merged
mkoura merged 2 commits into
masterfrom
leios_committee_rank
Sep 25, 2026
Merged

mkoura merged 2 commits into
masterfrom
leios_committee_rank

Conversation

@mkoura

@mkoura mkoura commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

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 - neither the ranking nor what happens to
a pool that misses out was covered by anything.

TestLeiosCommitteeRank

Starts 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:

  • finds the pools the committee left out;
  • checks they were left out on rank alone - they have a registered BLS key, and no
    seated pool ranks behind them under (-stakeSet, poolId), the key
    selectLeiosCommittee sorts on;
  • searches the pool logs for the rest of the epoch the committee was read in, and
    checks that the pools without a seat 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.

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_POOLS and could never reach the default
0.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 as
leiosCommitteeSize: 2, leiosQuorumStakeThreshold: 0.5:

pool AnnouncementAccepted Voted Certified NotVoted
pool1 (seated) 22 21 21 1 ChainTipDoesNotAnnounce
pool2 (seated) 22 21 21 1 ChainTipDoesNotAnnounce
pool3 (ranked out) 22 0 21 21 NotOnCommittee

Two things this settled. The reason a seatless pool reports is NotOnCommittee, not
SignerNotInCommittee - worth pinning down, because LeiosSeat carries only a weight
and 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_key read bksRegisteredIn from one cluster pool and put every other
pool 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 - 1 and that none is at expire_epoch, which needs
expire_epoch <= min(reg) + max_key_age and >= max(reg) + max_key_age respectively,
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=true
stamps everything epoch 0), but the kind of thing that only bites on a slow machine.

Shared code

  • leios.NOT_VOTED_MSGS groups the reasons a pool can give for being unable to vote.
  • 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.

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.

@mkoura
mkoura requested a review from saratomaz as a code owner September 25, 2026 09:52
@mkoura
mkoura requested a lite review from Copilot and removed request for saratomaz September 25, 2026 09:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 Medium severity

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.

Comment thread cardano_node_tests/tests/test_leios_blocks.py
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
mkoura force-pushed the leios_committee_rank branch from 7f73a31 to c809281 Compare September 25, 2026 10:05
@mkoura
mkoura merged commit 8ca4015 into master Sep 25, 2026
3 checks passed
@mkoura
mkoura deleted the leios_committee_rank branch September 25, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants