Skip to content

test: check the BLS proof of possession - #3708

Merged
mkoura merged 2 commits into
masterfrom
bls_pop_test
Sep 24, 2026
Merged

mkoura merged 2 commits into
masterfrom
bls_pop_test

Conversation

@mkoura

@mkoura mkoura commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

CIP-0164 makes two statements about the proof of possession that
accompanies a BLS key in a pool registration certificate, because BLS
aggregate signatures are otherwise open to rogue-key attacks. They land
differently, so there is a test for each.

"Mandatory and verified at registration" - not implemented. The
Dijkstra POOL rule reuses ShelleyPoolPredFailure unchanged and has no
BLS predicate failure at all, so a registration carrying a proof that
belongs to another key is accepted and the pair is stored verbatim.
test_mismatched_pop_is_not_verified pins that down, so the day the
ledger starts rejecting such a registration is a day a test says so,
rather than a silent change of behaviour.

"Only a key with a valid proof of possession may occupy a committee
seat or contribute to a certificate" - implemented
, one layer below the
ledger rules, in mkLeiosCommittee:

, seatVKey = do
    (vk, pop) <- mKeyPoP
    case verifyPossessionProofDSIGN vk pop of
      Left _err -> SNothing
      Right () -> SJust vk

test_mismatched_pop_is_not_seated checks the observable consequence:
the pool keeps its committee seat and the ledger keeps reporting the key
it registered, but the seat is not voting, so the key can neither vote
nor contribute to a certificate. An aged-out key looks identical from the
outside, so the test also asserts the key is well inside its maxKeyAge
lifetime and age cannot be the reason.

The proof cannot be generated on its own - the CLI derives it from the
signing key and emits it only inside a registration certificate - so the
mismatch is built by splicing the proof out of a second certificate into
the first, checking that the re-encoded certificate differs from the
original in nothing but the proof. Both tests share that setup in
register_pool_with_mismatched_pop.

BLS_ACTIVATION_EPOCHS, get_committee_seat and get_max_key_age move
from test_bls_rotation.py to bls.py, now that more than one test
module needs them (-53/+13 there, all call sites mechanical).

test_mismatched_pop_is_not_seated waits two epoch boundaries, so it is
marked long and, unlike its sibling, not testnets. It is marked
leios instead, because regression-leios.yaml is the only workflow
that routinely runs a Dijkstra cluster and it selects with
markexpr: testnets or leios - without the marker the test would be
selected nowhere.

test: keep pool names inside the 50 char limit

A pool name is limited to 50 characters, and a test ID eats most of that
on its own: the test function name plus the cluster ID plus a random
string comes to 44-52 characters for the tests in these two modules. The
pools are now named after a short random string, the way
test_pool_registration_conway_cert already does.

The derived names were the worse offenders. rotate_bls_key appended the
whole transaction name to a pool name that already contained the test ID,
so the ID appeared twice and the certificate of
test_drop_and_restore_bls_key was named with 95 characters. The
rotation now takes a short cert_suffix for the pool name, while
tx_name keeps naming the transaction, where nothing is constrained.

The longest name left is 18 characters. Nothing reads a pool name except
the generated file names, and none of these pools carry metadata, so the
limit was never enforced on them - they were over it all the same.

CIP-0164 makes two statements about the proof that accompanies a BLS key
in a pool registration certificate, and they land differently.

It "is mandatory and verified at registration" - that one is not
implemented. The Dijkstra `POOL` rule reuses `ShelleyPoolPredFailure`
unchanged and has no BLS predicate failure at all, so a registration
carrying a proof that belongs to another key is accepted and the pair is
stored verbatim. `test_mismatched_pop_is_not_verified` pins that down,
so that the day the ledger starts rejecting it is a day a test says so.

"Only a key with a valid proof of possession may occupy a committee seat
or contribute to a certificate" - that one is implemented, in
`mkLeiosCommittee`, which verifies the proof and admits the seat keyless
when it fails. `test_mismatched_pop_is_not_seated` checks it: the pool
keeps its seat and the ledger keeps reporting the key, but the seat is
not voting. An aged-out key looks the same from the outside, so the test
also checks the key is well inside its lifetime.

The proof cannot be generated on its own - the CLI derives it from the
signing key and emits it only inside a registration certificate - so the
mismatch is built by splicing the proof of a second certificate into the
first, shared by both tests in `register_pool_with_mismatched_pop`.

`BLS_ACTIVATION_EPOCHS`, `get_committee_seat` and `get_max_key_age` move
to `bls.py`, now that more than one test module needs them.
A pool name is limited to 50 characters, and a test ID eats most of that
on its own - it is the test function name plus the cluster ID plus a
random string, 44 to 52 characters for the tests in these two modules.
Name the pools after a short random string instead, the way
`test_pool_registration_conway_cert` already does.

The derived names were the worse offenders. `rotate_bls_key` appended
the whole transaction name to a pool name that already contained the
test ID, so the ID appeared twice and the certificate of
`test_drop_and_restore_bls_key` was named with 95 characters. The
rotation now takes a short `cert_suffix` for the pool name, while
`tx_name` keeps naming the transaction, where nothing is constrained.

The longest name left is 18 characters. Nothing reads a pool name except
the generated file names, and none of these pools carry metadata, so the
limit was never enforced on them - they were over it all the same.

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

Add a cluster-era guard for the Dijkstra-dependent test class.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Adds BLS proof-of-possession coverage and shortens generated pool names.

Changes:

  • Tests mismatched proof registration and committee non-voting behavior.
  • Centralizes shared BLS helpers.
  • Separates certificate suffixes from transaction names.
File Summary
cardano_node_tests/​tests/​test_bls_rotation.py Updates shared helper usage and pool naming.
cardano_node_tests/​tests/​test_bls_keys.py Adds mismatched proof-of-possession tests.
cardano_node_tests/​tests/​bls.py Centralizes BLS timing and committee helpers.

💡 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_bls_keys.py
@mkoura
mkoura merged commit e6d3b2d into master Sep 24, 2026
4 checks passed
@mkoura
mkoura deleted the bls_pop_test branch September 24, 2026 13:13
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