Skip to content

feat(validator): ask the miner for attestation, not the operator - #121

Open
wallscaler wants to merge 1 commit into
mainfrom
feat/direct-miner-attestation
Open

wallscaler wants to merge 1 commit into
mainfrom
feat/direct-miner-attestation

Conversation

@wallscaler

Copy link
Copy Markdown
Contributor

First code toward #120. Makes it possible for a validator that is not Cathedral to obtain proof of miner work without asking Cathedral for it.

The problem this starts fixing

Every validator today fetches one signed weight vector from Cathedral's endpoint and checks its signature. Two consequences:

  • Cathedral is the only party that can verify anything. Everyone else checks a signature and copies a number.
  • Cathedral's uptime is everyone's dependency. api.cathedral.computer returned 502 for several hours on 2026-08-12 when a TLS certificate expired, and every validator following the feed stalled with nothing to write.

What made this small

The miner side already exists. MinerClient.collect_evidence(nonce) is in cathedral-compute today, and its own docstring calls it "the validator's miner protocol". Miners have always been able to answer a validator directly against a 32-byte challenge. Only Cathedral's epoch loop ever asked.

So this is validator-side only. No miner protocol change.

What this adds

discover(metagraph) miner addresses from their on-chain axons
nonce_for(...) the challenge this validator issues to this miner this epoch
collect(...) evidence over the miner's own TLS channel
collect_all(...) returns evidence and failures separately

Three properties of the challenge, each load-bearing:

  • Unique per validator. Two validators challenging the same miner in the same epoch issue different nonces, so a miner cannot answer one with the proof it gave another. Same reason Targon derives its nonce from the validator's own hotkey (nonce.NewNonce(attester.Hotkey.Address), internal/validator/callbacks/cvms.go).
  • Epoch-bound. A miner cannot precompute answers for epochs it has not reached.
  • Publicly reproducible. All three inputs are public, so a third party can recompute the challenge a validator was obliged to issue and check that it did. That makes verification work auditable rather than asserted, which is close to the only lever Bittensor offers against a lazy validator.

Two deliberate constraints, both enforced by tests

Discovery reads the chain and takes nothing else. test_discovery_reads_the_chain_and_nothing_else asserts the signature is exactly {metagraph}. An operator-held address book would reintroduce the dependency being removed: whoever serves the list decides who gets verified, and therefore who earns.

No fallback, ever. Every failure ends in "this miner earns nothing", never "use something else". test_nothing_in_this_module_can_reach_a_signed_feed asserts the module cannot even reference weights/next, publisher_url or api.cathedral. A fallback appearing here later is exactly what would turn one endpoint's outage back into everybody's.

Verified against Finney

discoverable miners on SN39 right now: 1
  uid 204  5G3qVaXzKMPDm5...  https://31.22.104.49:8080
challenge for catminer this epoch: 57293bd1efcf035253b9f02ccf6a9f6d... (32 bytes)

That result is itself the finding. Only 1 of 256 neurons publishes an axon, and it is the burn hotkey. Miners must publish an address before any validator can reach them. That is the standard Bittensor mechanism and a miner-side action, not a permission Cathedral grants, but it does mean this path reaches nobody until miners serve axons.

Scope

Collection and discovery. Nothing here scores or writes weights. Wiring it into the audit path is #120.

Not addressed here, and tracked in #120:

  • Per-machine attestation at fleet scale. Targon skips ~95% of attestation opportunities (ATTEST_RATE default .95, internal/validator/setup/setup.go:97) and caches verified nodes within an interval, with the comment "helps reduce stress on cvm nodes from number of pings". That amortization is what makes N validators x M miners x K machines tractable and should be copied before this runs at scale.
  • A penalty that outlasts the epoch. Detection is worth little if a caught miner only forfeits the epoch it was caught in.

Tests

12, all passing, ruff clean, no em dashes. They pin the properties rather than the implementation: that a miner receives this validator's challenge verbatim, that one dead miner does not stop the others, and that an empty answer is a failure rather than an empty pass.

Today every validator fetches one signed weight vector from Cathedral's endpoint
and checks its signature. Cathedral is therefore the only party that can verify
anything, and its uptime is everyone else's dependency: that endpoint returned
502 for several hours on 2026-08-12 and every following validator stalled with
nothing to write.

Nothing in the miner protocol required that. MinerClient.collect_evidence(nonce)
already exists in cathedral-compute and its own docstring calls it "the
validator's miner protocol". Miners have always been able to answer a validator
directly. Only Cathedral's epoch loop ever asked.

This adds the asking. discover() reads miner addresses from their on-chain axons,
nonce_for() issues a challenge, and collect() obtains evidence over the miner's
own TLS channel. Cathedral is not in that path, so Cathedral being down is not an
outage.

Three properties of the challenge, each load-bearing:

  unique per validator   two validators challenging one miner in the same epoch
                         issue different nonces, so a miner cannot answer one
                         with the proof it gave another
  epoch-bound            a miner cannot precompute answers for epochs it has
                         not reached
  publicly reproducible  all three inputs are public, so anyone can recompute
                         the challenge a validator was obliged to issue and
                         check that it did

That last one matters beyond replay: it makes verification work auditable rather
than asserted, which is the only lever Bittensor offers against a lazy validator.

Discovery reads the chain and takes no other argument, enforced by a test. An
operator-held address book would reintroduce exactly the dependency being
removed, because whoever serves the list decides who gets verified and therefore
who earns.

Every failure ends in "this miner earns nothing", never in "use something else".
A test asserts the module cannot reference a signed feed at all, because a
fallback appearing here later is precisely what would turn one endpoint's outage
back into everybody's.

Scope is collection and discovery. Nothing here scores or writes; wiring is
issue #120. Verified against Finney: discovery returns the 1 of 256 neurons
currently publishing an axon, which is itself the finding that miners must
publish one before any of this reaches them.

12 tests, ruff clean.
@cursor

cursor Bot commented Aug 14, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 17b3217188

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scaffold/direct_attest.py
Comment on lines +104 to +107
b"cathedral-direct-attest-v1",
validator_hotkey.encode("utf-8"),
miner_hotkey.encode("utf-8"),
str(int(epoch)).encode("ascii"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use an unpredictable per-request attestation nonce

Because the validator hotkey, miner hotkey, and future epoch numbers are all public, a miner can calculate every future nonce and pre-generate the corresponding evidence while it is in a valid state, then replay that evidence when challenged after its state has changed. Including a predictable epoch number prevents cross-epoch reuse but does not provide the promised anti-precomputation or freshness guarantee; use fresh randomness or an unpredictable finalized-chain value and record the issued nonce separately for auditing.

Useful? React with 👍 / 👎.

Comment thread scaffold/direct_attest.py
Comment on lines +155 to +156
return evidence

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject all empty evidence responses

When collect_evidence() returns an empty bundle such as [], {}, or b"" rather than None, this check treats the miner as successful and collect_all() places it in the evidence map. That contradicts the documented fail-closed behavior and makes an all-empty response set look like miners answered, so supported zero-length evidence representations should be classified as failures here.

Useful? React with 👍 / 👎.

Comment thread scaffold/direct_attest.py

@property
def url(self) -> str:
return f"https://{self.ip}:{self.port}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bracket IPv6 axon addresses in URLs

When a miner publishes an IPv6 axon address, this produces a URL such as https://2001:db8::1:8443, whose host and port cannot be parsed correctly because IPv6 literals in URLs require brackets. Format IPv6 endpoints as https://[2001:db8::1]:8443 so those miners remain reachable.

Useful? React with 👍 / 👎.

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