Skip to content

ledger: four anchoring defects — unretried failures at a quiet tip, a tip-read race publishing a false tamper signal, 1MB file truncation, short_tail blind spots #9489

Description

@JSONbored

Summary

Four defects in the decision-ledger anchoring feature that shipped 2026-07-27 (#9413/#9274). Each one degrades the property the feature exists to provide — an external, tamper-evident anchor a skeptic can verify — and two of them produce signals that read as evidence of tampering when nothing was tampered with.

1. A failed anchor at a quiet tip is never retried

src/review/ledger-anchor-scheduler.ts:39-42 decides tipUnchanged by comparing the rowHash against the last attempt regardless of its status (src/review/ledger-anchor-persistence.ts:161-167).

Scenario: Rekor returns 429 at seq N; the ledger then goes quiet (a weekend); every hourly tick returns unchanged; the tip therefore has no valid external anchor indefinitely — exactly the unanchored window the feature exists to bound.

It is also backend-blind: git succeeding at seq N masks Rekor's failure at seq N, because there is no per-backend watermark.

  • Retry when the last attempt for that backend at this rowHash was failed, with backoff.
  • Track the watermark per backend.
  • Test: rekor fails, ledger stays quiet ⇒ the next tick retries rekor.

2. A tip read race publishes a signed, unretractable false tamper signal

src/review/decision-record.ts:346-354 issues the COUNT(*) and the tip row as two separate queries under Promise.all — no snapshot isolation. A concurrent appendDecisionLedger landing between them yields totalCount = seq + 1 paired with the old rowHash.

Per the payload documentation (src/review/ledger-anchor.ts:29-31), that seq/totalCount pairing is precisely how a verifier detects truncation-and-rechaining. So the race publishes a signed, Rekor-anchored, unretractable false tamper signal.

  • Read the tip and count in one statement (subselect) or a DB.batch.
  • Test: concurrent append during the tip read ⇒ the anchored payload is internally consistent.

3. The git backend truncates anchors.jsonl past 1 MB

src/review/ledger-anchor-git.ts:66-75: for a file between 1 MB and 100 MB, GitHub's Contents API returns content: "" with encoding: "none". The code checks typeof data.content === "string", which "" satisfies, so existingContent becomes "" and the subsequent PUT rewrites the file to a single line — recording status: "ok".

At roughly 300 B per line and hourly cadence, that is about 4–5 months out. History survives in the git commits, but the file a skeptic is told to read shrinks — indistinguishable from the tampering signal the module's own header teaches them to look for. The whole-file read-modify-write is also O(n²) in bytes over time.

  • Refuse (or rotate) when data.content === "" && data.size > 0; consider the Git Data API instead of Contents.
  • Test: simulate the empty-content/non-zero-size response ⇒ no truncating write occurs.

4. short_tail is transiently false-positive and permanently blind to interior orphans

src/review/decision-record.ts:487-493: a verification running between the record INSERT and the ledger append — or after a permanently failed append (:225-236) — reports short_tail, i.e. "tampering", on a public endpoint.

Conversely, once any newer row chains, an unchained record's created_at falls behind lastVerifiedCreatedAt and that orphan becomes invisible forever, because the completeness reconciliation only ever examines the tail.

  • Distinguish "append in flight" from "record missing from the chain" (a short grace window on recency, or an explicit in-flight marker).
  • Give completeness reconciliation a path that can find interior orphans, not only the tail.
  • Test: verify during an in-flight append ⇒ no tamper signal; an interior orphan is detected.

Minor

loadPublicLedgerAnchors's keyset cursor is created_at < ? with ORDER BY created_at DESC, id DESC (src/review/ledger-anchor-persistence.ts:106-127) — ties on created_at at a page boundary are skipped. Use a composite (created_at, id) cursor.

Verified solid (do not re-audit)

The signing and verification crypto in src/review/ledger-anchor.ts is sound: canonical serialization shared by sign and verify, fail-closed key parsing, ambiguity-safe currentAnchorKey, and parseRekorResponse degrading to a recorded failure. One documented nuance: nothing checks payload.at against a key's [notBefore, notAfter] window at verification — weak protection anyway given an attacker-chosen at, reasonable to leave as-is.

Metadata

Metadata

Assignees

Labels

gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.maintainer-onlyOwner-only work — yields no Gittensor points.orbGittensory Orb related - maintainer self-hosting analytics.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions