Skip to content

feat(round): sign score reports with the validator's hotkey - #232

Merged
ai-hpc merged 1 commit into
mainfrom
ai-hpc/validator-report-signing
Sep 4, 2026
Merged

ai-hpc merged 1 commit into
mainfrom
ai-hpc/validator-report-signing

Conversation

@ai-hpc

@ai-hpc ai-hpc commented Sep 4, 2026

Copy link
Copy Markdown
Member

Pairs with cathedral-cybergym-backend (validator auth). Together they close the hole where post_results accepted any non-empty hotkey string — and that report is the payout.

What the signature covers

The content, not just the sender. Signing only the hotkey and round would let anyone who observed one report replay its signature over different scores. Rows are canonicalised (sorted, normalised decimals), so wire order cannot change what was signed, and the round is bound so last round's signature cannot be replayed into this one.

cybergym:v2:results:5V:3:[["m1","100",true],["m2","50",false]]

The duplication is deliberate

results_message / weights_message exist in both repos because they cannot import each other on the payout path. The agreement is pinned as a byte literal in tests on both sides: if the rendering drifts here without drifting there, every honest report is rejected as a forgery — and a failing test is how that should surface, not a silent loss of every score in a round.

The seed

The entrypoint loads a bittensor wallet by name, or a hex seed from the environment. It goes straight into the keypair and is never returned, printed, or put in an error message — a validator seed in a log is a stolen validator. Signing as one hotkey while claiming another is refused at startup, not at the compose block. An unconfigured signer warns that an enforcing backend will refuse its reports.

Verification

109 v2 round tests pass; ruff check and ruff format --check clean over cathedral_thin. Unsigned remains a working mode for a local dry run — the client sends no signature field rather than fabricating one.

A score report is the payout input: the server averages the validators' verdicts and the composed
weights follow that average. The backend now authenticates those reports, so this is the other
half — the validator proves the report came from its hotkey.

The signature covers the CONTENT, not just the sender. Signing only the hotkey and round would let
anyone who observed one report replay its signature over different scores. The rows are sorted
into a canonical form, so the wire order cannot change what was signed.

`results_message` / `weights_message` are duplicated from the backend deliberately: the two repos
cannot import each other on the payout path, so the agreement is pinned as a byte literal in the
tests on BOTH sides. If this rendering drifts here without drifting there, every honest report
starts being rejected as a forgery — a test failing is how that should be found.

The entrypoint loads a bittensor wallet by name, or a hex seed from the environment. The seed goes
straight into the keypair and is never returned, printed, or included in an error message: a
validator seed in a log is a stolen validator. Signing as one hotkey while claiming another is
refused at startup rather than at the compose block, and an unconfigured signer warns loudly that
an enforcing backend will refuse its reports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ai-hpc

ai-hpc commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Self-review before merge (owner-directed; posted for post-merge audit)

All checks pass: Python 3.11, 3.12, publisher suite, integration lane, SEV-SNP contract, release, PEM-key gate.

What I verified beyond the tests:

  • The signature covers every miner verdict and the round, so it cannot be replayed over different scores or into another round. Canonicalised, so wire order cannot change what was signed.
  • Client and backend build byte-identical messages — pinned as a literal in tests on both sides, because the repos cannot import each other on the payout path. That test failing is how a drift should surface, rather than every honest report being silently rejected as a forgery.
  • The seed goes straight into the keypair: never returned, never printed, never in an error message.
  • A hotkey mismatch between the loaded key and CYBERGYM_VALIDATOR_HOTKEY refuses at startup, not at the compose block.
  • Unsigned remains a working dry-run mode and sends no signature field rather than fabricating one.

Process note: I merged #226/#229/#230 this morning without reading their checks and left main red all day. I read the checks on this one.

@ai-hpc
ai-hpc merged commit a64b690 into main Sep 4, 2026
9 checks passed
@ai-hpc
ai-hpc deleted the ai-hpc/validator-report-signing branch September 4, 2026 16:46
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.

1 participant