Skip to content

api(ledger): /anchor-key collapses five distinct causes into one empty response #9834

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue.

Problem

GET /v1/public/decision-ledger/anchor-key returns {"keys":[],"currentKeyId":null} on loopover.ai today.
That response is indistinguishable across five different causes — verified by running
parseAnchorPublicKeys (src/review/ledger-anchor.ts) over each:

Cause Response
secret never set {"keys":[],"currentKeyId":null}
set to malformed JSON {"keys":[],"currentKeyId":null}
set to an object instead of an array {"keys":[],"currentKeyId":null}
set with a typo'd field (keyid vs keyId) {"keys":[],"currentKeyId":null}
set correctly but every key expired {"keys":[…],"currentKeyId":null}

parseAnchorPublicKeys returns [] from four separate paths (!raw, a thrown JSON.parse, a non-array,
and a .filter that silently drops every entry) with no diagnostic anywhere. currentAnchorKey then folds
two more states together: it returns null both when NO key is current and when MORE THAN ONE is — its own
doc comment documents the ambiguous-rotation case as deliberate fail-closed behaviour, but nothing reports
which of the two happened.

The consequence is concrete. #9719 provisioned the anchor keypair and was closed. Whether that
provisioning actually took effect on the hosted Worker cannot be determined — not from outside, and not by
the operator either, because a typo in the secret looks exactly like an unset secret.

The status field does not help. publicAnchorStatus checks tipSeq === 0 before hasSigningKey, so with an
empty ledger it reports empty_ledger whether or not a key is configured:

status with empty ledger + NO key : empty_ledger
status with empty ledger + key    : empty_ledger

That guard order is deliberate (its comment says it mirrors runScheduledLedgerAnchor's own so a reader
never gets "a second opinion") and should not change. The gap is that /anchor-key never got the treatment
its sibling did.

The precedent

PublicAnchorStatus exists in this same file for exactly this reason. Its doc comment:

Without this, "never configured", "no ledger to anchor", and "anchoring is healthy but has not run yet"
are all indistinguishable from outside — every one of them renders as {"anchors":[]}, which reads as a
healthy empty state.

That reasoning was applied to /anchors and not to /anchor-key, which still has the identical ambiguity.

Requirements

  • A PURE diagnosis function classifying the raw env value into a discriminated status, alongside the parsed
    keys. parseAnchorPublicKeys's existing signature and behaviour must not change — it has other callers
    (ledger-anchor-scheduler.ts, the route) and the scheduler's guard order must stay as-is.
  • Statuses must separate, at minimum: unconfigured, malformed (unparseable or non-array), every entry
    invalid, valid-but-none-current (expired), more-than-one-current (ambiguous rotation), and ok.
  • Entries dropped by validation must be COUNTED and surfaced even when the overall status is ok, so one
    typo'd key among three is visible rather than silently ignored.
  • The raw value must never be echoed back, in any status. An operator who mis-pastes
    LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY into the public LOOPOVER_LEDGER_ANCHOR_KEYS var would otherwise have
    the private half published by an unauthenticated endpoint. Only a classification may be returned.
  • keys and currentKeyId must keep their current shape and meaning — this is additive, not a rewrite.

Deliverables

  • Pure diagnosis function with a test per status, including both arms of the currentAnchorKey collapse
    (zero current keys vs. more than one).
  • A test asserting the raw value never appears in the response for a malformed input, using a
    private-key-shaped probe assembled from fragments (the convention forbidden-content.test.ts uses).
  • A test asserting droppedEntries is non-zero, and status still ok, for one valid + one typo'd entry.
  • Route returns the new field(s); keys/currentKeyId unchanged for existing consumers.
  • parseAnchorPublicKeys behaviour unchanged, asserted.
  • OpenAPI/contract regenerated and committed if this response is declared there.

Test Coverage Requirements

99%+ Codecov patch coverage, branch-counted, on src/**. Every status arm needs a test.

Expected Outcome

curl -s https://api.loopover.ai/v1/public/decision-ledger/anchor-key | jq .status answers "is anchoring
configured on this deployment, and if not, why" — for an anonymous reader and for the operator, without
publishing key material.

Links & Resources

src/review/ledger-anchor.ts (parseAnchorPublicKeys, currentAnchorKey, publicAnchorStatus),
src/api/routes.ts (the /v1/public/decision-ledger/anchor-key handler),
src/review/ledger-anchor-scheduler.ts. Related: #9719, #9271, #9267.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions