fix(ledger): say WHY the anchor-key list is empty, not just that it is - #9835
Conversation
`GET /v1/public/decision-ledger/anchor-key` answered
`{"keys":[],"currentKeyId":null}` to six different causes: the secret unset,
unparseable JSON, a non-array, every entry failing validation, all keys expired,
and an ambiguous rotation. parseAnchorPublicKeys returns [] from four separate
paths with no diagnostic, and currentAnchorKey folds two more together by
returning null both when NO key is current and when more than one is.
The consequence is concrete rather than cosmetic. #9719 provisioned the anchor
keypair and was closed, but whether that took effect on the hosted deployment
could not be determined -- from outside OR by the operator -- because a typo in
the secret is byte-identical to an unset secret. An empty response reads as a
healthy empty state.
This is the same defect PublicAnchorStatus was introduced to fix for the sibling
anchors listing ("never configured", "no ledger to anchor" and "healthy but has
not run yet" all rendering as {"anchors":[]}), never applied here. It could not
be answered by that status either: publicAnchorStatus checks tipSeq === 0 before
hasSigningKey, so on an empty ledger it reports empty_ledger whether or not a key
is configured -- deliberate, since it mirrors the scheduler's own guard order,
and left unchanged.
diagnoseAnchorPublicKeys is additive and pure. parseAnchorPublicKeys keeps its
exact signature and behaviour (the scheduler depends on it, asserted), and
`keys`/`currentKeyId` keep their shape for existing consumers. droppedEntries is
reported even when the status is ok, so one typo'd key among several is visible
instead of silently discarded into a healthy-looking response.
The raw value is NEVER echoed back under any status. This endpoint is
unauthenticated, and an operator who mis-pastes
LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY into the public var would otherwise have the
private half published by the very diagnostic meant to help them.
Closes #9834
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-29 13:14:52 UTC
Review summary Nits — 6 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. Scroll preview
A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loopover-ui | b975521 | Commit Preview URL Branch Preview URL |
Jul 29 2026, 12:56 PM |
Bundle ReportChanges will increase total bundle size by 844 bytes (0.01%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: loopover-uiAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9835 +/- ##
==========================================
- Coverage 91.59% 90.76% -0.83%
==========================================
Files 915 915
Lines 112692 112704 +12
Branches 27078 27082 +4
==========================================
- Hits 103220 102297 -923
- Misses 8144 9314 +1170
+ Partials 1328 1093 -235
Flags with carried forward coverage won't be shown. Click here to find out more.
|


Closes #9834
What I was actually trying to find out
You asked me to investigate why
/v1/public/decision-ledger/anchor-keyreturns nothing on loopover.ai before touching the docs. The answer is that it cannot be determined — and that is the bug.Six causes, one response. Verified by running the parser over each:
{"keys":[],"currentKeyId":null}{"keys":[],"currentKeyId":null}{"keys":[],"currentKeyId":null}keyidvskeyId){"keys":[],"currentKeyId":null}currentKeyId: nullcurrentKeyId: nullparseAnchorPublicKeysreturns[]from four separate paths with no diagnostic;currentAnchorKeyfolds the last two together, returningnullboth when no key is current and when several are — which are different operator problems (publish a successor vs. retire one of two).So #9719 is unverifiable. It provisioned the anchor keypair and was closed, but whether that took effect on the hosted Worker is invisible — not just to an anonymous reader, but to the operator, because a typo in the secret looks exactly like an unset secret.
The existing status field can't answer it either.
publicAnchorStatuscheckstipSeq === 0beforehasSigningKey, so with an empty ledger it reportsempty_ledgerregardless:That ordering is deliberate — its comment says it mirrors
runScheduledLedgerAnchor's own so a reader never gets "a second opinion" — so it is left unchanged.The precedent this restores
PublicAnchorStatusexists in the same file for exactly this reason. Its own doc:Applied to
/anchors; never applied to/anchor-key.Design notes worth reviewing
parseAnchorPublicKeyskeeps its exact signature and behaviour — the scheduler's guard order depends on it, and there's an invariant test asserting the two agree across every input.keysandcurrentKeyIdkeep their shape for existing consumers.droppedEntriesis reported even when status isok, so one typo'd key among three is visible rather than silently discarded into a healthy-looking response.unconfigured, notno_valid_entries. "Configured to publish nothing" is the same actionable state as never setting it, and deliberately distinct from "entries were present and every one was rejected".LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEYinto the publicLOOPOVER_LEDGER_ANCHOR_KEYSvar would otherwise have the private half published by the diagnostic meant to help them. There are two tests for this, with the probe assembled from fragments so neither file contains a credential-shaped literal.Two existing assertions were pinning the ambiguity
public-anchor-key-route.test.tshad a test titled "…distinguishable from a key that exists" assertingtoEqual({ keys: [], currentKeyId: null }). The empty half was true; the distinguishable half was not. Both that and the malformed-config case are updated, with the correction noted inline rather than quietly rewritten.Validation
tsc --noEmit·ui:openapi:check·dead-source-files:check·import-specifiers:check·docs:drift-check— clean. OpenAPI description updated andapps/loopover-ui/public/openapi.jsonregenerated in the same commit. 28 tests pass across the anchor-key and decision-ledger route suites, 27 in the unit suite.Coverage: every branch and statement I added is covered — verified against the coverage JSON restricted to the added line range, not just the whole-file percentage. The file's remaining uncovered lines (294–295) are pre-existing and untouched.
Noted, not fixed here
contract:api-schemas:checkfails onmaintoday — a pending regeneration addingignoredCheckRuns, left over from #9813 alongside the three problems #9829 already fixed. I confirmed it reproduces with my changes stashed, so it is not from this PR; keeping it out to stay focused, and doing it separately.