fix(validators): read absence from a fresh scan as a confirmed zero - #9334
Merged
Conversation
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
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-data-api | 4d8a696 | Aug 03 2026, 10:44 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-registry-sync-api | 4d8a696 | Aug 03 2026, 10:43 PM |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
This was referenced Aug 3, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
nominator_countwas 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 = 0returns 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
0is 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:
Number(null)is the epoch, so it fails the freshness check)Shape of the change
The read becomes a
LEFT JOINfrom 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.fillConfirmedZeroslives insrc/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 query —hotkeyisTEXT NOT NULLand 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, sincenulland0no longer mean what a caller would have assumed.Validation
Patch coverage by diff intersection: 0 uncovered lines, 0 uncovered branches across both changed source files.
Closes #9314
Part of #9146