Skip to content

test(validator): add coverage for registry and service modules#502

Merged
tcoratger merged 2 commits intoleanEthereum:mainfrom
dicethedev:test/validator-registry-and-service-coverage
Mar 30, 2026
Merged

test(validator): add coverage for registry and service modules#502
tcoratger merged 2 commits intoleanEthereum:mainfrom
dicethedev:test/validator-registry-and-service-coverage

Conversation

@dicethedev
Copy link
Copy Markdown
Contributor

Summary

Closes #493.

validator/registry.py and validator/service.py had 0% test coverage
despite being actively used production modules. This PR adds comprehensive
unit and integration tests for both.

What was tested and why

registry.py

  • ValidatorManifestEntry — field validation including YAML's integer
    coercion of 0x-prefixed hex strings, which silently breaks pubkey
    parsing without this test
  • ValidatorManifest.from_yaml_file() — direct YAML loading, previously
    untested even though it's the only entry point for key metadata
  • load_node_validator_mapping() — normal loading, empty file, single
    node cases
  • ValidatorRegistry — full lifecycle: add/get, __contains__,
    __len__, indices(), primary_index(), from_yaml() (happy path,
    unknown node, missing manifest entry, missing key file, corrupt key
    file for both attestation and proposal keys), and from_secret_keys()
  • ValidatorEntry — construction and immutability (frozen dataclass)

service.py

  • _sign_with_key — key advancement loop (no advance, one advance,
    multiple advances), registry persistence after advancement, and
    isolation between attestation and proposal keys. This is the most
    security-sensitive path: a bug here could exhaust OTS keys or reuse them
  • _sign_block / _sign_attestation — correct field population, correct
    key routing (proposal vs attestation), and ValueError for unknown
    validators
  • _maybe_produce_block — not-proposer early exit, no-head-state early
    exit, and AssertionError from the store being swallowed gracefully
  • _produce_attestations — block-wait polling loop (8 retries, early
    exit when block arrives), local gossip processing before publish,
    counter increments
  • run() — interval 0 routes to block production, interval ≥ 1 routes
    to attestation, empty registry skips all duties, duplicate slot
    prevention, and _attested_slots pruning to bound memory

Coverage achieved

File Before After
validator/registry.py 0% ≥90%
validator/service.py 0% 100%

Notes

  • @dataclass(slots=True) on ValidatorService prevents
    patch.object(instance, method) — all mocks patch the class and
    include self as the first parameter
  • Signature.zero() is used wherever Pydantic validation rejects
    MagicMock for signature fields
  • Async tests use pytest-asyncio with mode=auto already configured
    in pyproject.toml
  • Registry tests use tmp_path for temporary YAML files and mock key files.
  • Service tests mock SyncService, SlotClock, and Store, and use _zero_sig() for signature fields.
  • Coverage targets: ≥90% for registry, 100% for service (async loops fully exercised).
  • Edge cases tested: empty/malformed YAML, missing or corrupted key files, non-proposer block intervals, duplicate attestations, and OTS key exhaustion.
  • All quality checks pass: uvx tox -e all-checks (ruff, ty, codespell, mdformat).
  • Tests are safe: no real cryptography is performed.

@tcoratger tcoratger merged commit b5c6960 into leanEthereum:main Mar 30, 2026
13 checks passed
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.

test: add coverage for validator registry and service (both at 0%)

2 participants