Skip to content

fix(validators): read absence from a fresh scan as a confirmed zero - #9334

Merged
JSONbored merged 1 commit into
mainfrom
fix/nominator-count-confirmed-zero
Aug 3, 2026
Merged

fix(validators): read absence from a fresh scan as a confirmed zero#9334
JSONbored merged 1 commit into
mainfrom
fix/nominator-count-confirmed-zero

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

nominator_count was null on 471 of the 1,028 permitted validators even after the lane got a live producer — and that null was overstating our ignorance.

The producer emits a row only for hotkeys it saw holding stake in SubtensorModule::Alpha, and it has never once recorded a zero: measured 2026-08-03, WHERE nominator_count = 0 returns nothing across 112,550 rows. So "no row" was doing double duty for "no nominators" and "not scanned".

Its pass over Alpha is exhaustive. A permitted validator missing from a completed pass genuinely has zero distinct coldkeys staked to it — that's an answer, not a gap.

Why this is now safe, and wasn't before

This was filed as an open question precisely because serving 0 is only correct against a scan you can trust. That precondition is now met: the lane has a live producer (metagraphed-infra#243/#267), a truncation guard that refuses to publish a partial pass, and a staleness watchdog (#9311).

So the zero-fill is gated on the scan being inside the same window the watchdog alarms on — imported rather than restated, so the two can never disagree about what "fresh" means. Against a stale table, absence stays null.

Three cases deliberately fill nothing:

  • an empty table — the cutover state, where inventing zeros would publish a wrong number for the whole network at once
  • a missing scan stamp (Number(null) is the epoch, so it fails the freshness check)
  • an unparseable stamp

Shape of the change

The read becomes a LEFT JOIN from the permitted set rather than an inner read of the counts table — the unmatched rows are exactly what the fill is about, so they have to survive the query to be seen at all.

fillConfirmedZeros lives in src/validator-nominator-summary.ts, beside the formatter it composes with, because two of its guards (an empty batch, an unusable stamp) cannot be produced by the real queryhotkey is TEXT NOT NULL and the stamp is a scalar subselect. A guard reachable only from a unit test still has to be reached by one, rather than ignored.

No schema shape change: the field was already z.int().min(0).nullable(). Both descriptions now state the rule, since null and 0 no longer mean what a caller would have assumed.

Validation

npm run lint / typecheck / validate / validate:contract-drift   # all clean
npx vitest run tests/validator-nominator-summary.test.ts \
  tests/data-api-neurons-d1.test.ts                             # 67 passed
npx vitest run <5 related suites>                               # 190 passed

Patch coverage by diff intersection: 0 uncovered lines, 0 uncovered branches across both changed source files.

Closes #9314
Part of #9146

nominator_count was null on 471 of the 1,028 permitted validators even after
the lane got a live producer, and that null was overstating our ignorance. The
producer emits a row only for hotkeys it saw holding stake in
SubtensorModule::Alpha and has never once recorded a zero -- measured
2026-08-03, `WHERE nominator_count = 0` returns nothing across 112,550 rows --
so "no row" was doing double duty for "no nominators" and "not scanned".

Its pass over Alpha is EXHAUSTIVE. A permitted validator missing from a
completed pass genuinely has zero distinct coldkeys staked to it, and that is
an answer, not a gap. Serving null for it left every caller to guess at a
distinction we can actually make.

FRESHNESS IS WHAT MAKES THE INFERENCE SAFE. Against a stale or truncated table
the same absence means "we have not looked recently", and 0 would be a
confident wrong number -- the failure this family's own doctrine calls worse
than an absent one. So the zero-fill is gated on the scan being inside the same
window the staleness watchdog alarms on, imported rather than restated so the
two can never disagree about what fresh means. An empty table, a missing scan
stamp and an unparseable one all fill nothing, which keeps the cutover state
(no scan has ever run) reading as unknown rather than as a network-wide zero.

The read becomes a LEFT JOIN from the permitted set rather than an inner read
of the counts table: the unmatched rows are exactly what the fill is about, so
they have to survive the query to be seen.

fillConfirmedZeros lives in src/validator-nominator-summary.ts, beside the
formatter it composes with, because two of its guards -- an empty batch and an
unusable scan stamp -- cannot be produced by the real query (hotkey is TEXT NOT
NULL, the stamp is a scalar subselect). A guard that can only be reached from a
unit test still has to be reached by one.

Both field descriptions now state the rule, since null and 0 no longer mean
what a caller would have assumed.

Closes #9314
Part of #9146
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-data-api 4d8a696 Aug 03 2026, 10:44 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-registry-sync-api 4d8a696 Aug 03 2026, 10:43 PM

@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored JSONbored self-assigned this Aug 3, 2026
@JSONbored
JSONbored merged commit c4c3a73 into main Aug 3, 2026
7 checks passed
@JSONbored
JSONbored deleted the fix/nominator-count-confirmed-zero branch August 3, 2026 22:50
JSONbored added a commit that referenced this pull request Aug 3, 2026
#9276 added a serving-Worker overlay filling nominator_count from the frozen
lakehouse mirror. It was the bridge for the period when D1 had no data, and
that period is over: migration 0012 gave the field a live table, #243/#267
gave it a live producer, and #9334 made the tier answer COMPLETELY -- absence
from a fresh scan reads as a confirmed zero, so nominator_count is now non-null
on 1,028 of 1,028 validators.

The overlay could only ever fire on a null count, because
validatorHotkeysNeedingCount collects exactly those and returns early when
there are none. There are none left to fill.

It was not simply dead, which is why this is worth doing rather than leaving.
The one situation that still reached it was a partial failure -- the counts
query throwing while the neurons query succeeded -- and there it served ~557
counts stamped 2026-08-02 from a mirror nothing refreshes, values that age
indefinitely. That is the failure the retired module's own header warned about:
a degraded count is worse than no count, because a card cannot tell its reader
which one it got. Null is the honest answer in that case.

Removed at all eight call sites across the three surfaces that shared it --
REST (workers/request-handlers/entities.ts), GraphQL (src/graphql.ts) and MCP
(src/mcp-server.ts) -- plus the module and its suite. Each site was a
multi-line expression wrapper, so this is unwrapping rather than deletion; the
2,877 tests across those three suites are what makes that safe to assert.

Closes #9337
Part of #9146
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.

nominator_count cannot distinguish "no nominators" from "not scanned" — decide whether a fresh exhaustive scan licenses serving 0

1 participant