Skip to content

feat(orb): hash-chained append-only decision ledger with a public verify endpoint - #8843

Merged
JSONbored merged 1 commit into
mainfrom
feat/decision-ledger
Jul 26, 2026
Merged

feat(orb): hash-chained append-only decision ledger with a public verify endpoint#8843
JSONbored merged 1 commit into
mainfrom
feat/decision-ledger

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

What

A hash-chained, append-only ledger over the decision records, with a public, resumable verify endpoint. Advances #8837 — external anchoring (signed checkpoints / witness cosigning) stays deliberately deferred until tenants exist, per the epic's sequencing. Stacked on #8842 (chains the records that PR persists).

Why

#8842 makes each verdict legible; this makes the sequence tamper-evident. Nobody can delete, reorder, or silently rewrite decision history without breaking the chain at a point any observer can find — at ~1% of the cost of the TEE approach the research explicitly recommended against, and against a threat model we actually have.

The honest limit is documented at the migration and repeated here: a self-operated chain is tamper-evident, not tamper-proof — the operator can still rewrite wholesale. That gap is closed by witness cosigning when there are tenants to ask for it; it does not reduce the value against every other actor or against accidental corruption.

How

  • seq is explicit and contiguous, never autoincrement — a deleted row leaves a gap, and a gap is a detectable break rather than something the sequence papers over.
  • row_hash = SHA-256(prev_hash ‖ canonicalJson({seq, recordId, recordDigest, createdAt})); genesis links to 64 zeros. One definition shared by append and verify.
  • Every persistDecisionRecord write appends — including latest-finalize-wins rewrites of the same record id, so supersessions are visible history, not silent replacement.
  • Concurrent appends race on the primary key and retry with a re-read tip (bounded); exhaustion surfaces through the persist path's existing warn.
  • GET /v1/public/decision-ledger/verify?afterSeq=&limit= — verifies a window, reports the first break with its class (sequence_gap | predecessor_mismatch | row_hash_mismatch), returns a resume cursor, 409 on any break. Hashes and ids only — no record contents — so it is safe unauthenticated. Registered in the OpenAPI spec.

Includes a real bug found by its own tests: the resume-cursor probe used === null where D1 drivers return undefined for no-row — a missing prior row would have verified as clean. Now == null, with the arm pinned.

Verification

  • TSC clean; 14 decision-record/ledger tests green: chain linkage across persists, rewrite-appends-visible-history, resumable pagination, each corruption class reporting correctly (including checked-count-before-break), the concurrent-append race, retry exhaustion, and the missing-resume-cursor gap.
  • Changed-line coverage: 0 uncovered statements/branches. Schema-drift + OpenAPI regen included.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 26, 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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 1f269ad Commit Preview URL

Branch Preview URL
Jul 26 2026, 12:01 PM

@superagent-security

Copy link
Copy Markdown
Contributor

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

@JSONbored JSONbored self-assigned this Jul 26, 2026
@JSONbored
JSONbored force-pushed the feat/decision-record branch from dd85f5a to f237a05 Compare July 26, 2026 11:54
@JSONbored
JSONbored force-pushed the feat/decision-ledger branch from a94867c to c8f0327 Compare July 26, 2026 11:55
Base automatically changed from feat/decision-record to main July 26, 2026 11:57
…ify endpoint

decision_records makes each verdict legible; the chain makes the
SEQUENCE tamper-evident: nobody deletes, reorders, or rewrites history
without breaking it at a verifiable point. Advances #8837 (external
anchoring/witnesses remain, deliberately deferred until tenants exist).

- explicit contiguous seq (never autoincrement -- a gap IS a break),
  genesis prev of 64 zeros, row_hash = SHA-256(prev || canonical
  fields); every persist appends, including latest-finalize-wins
  rewrites -- supersessions are visible history
- concurrent appends race on the PK and retry with a re-read tip
- GET /v1/public/decision-ledger/verify: resumable window verification
  reporting the FIRST break by class (sequence_gap |
  predecessor_mismatch | row_hash_mismatch); hashes/ids only, safe
  unauthenticated; 409 on a break
- honest limit documented at the migration: self-operated chains are
  tamper-evident, not tamper-proof
@JSONbored
JSONbored force-pushed the feat/decision-ledger branch from c8f0327 to 1f269ad Compare July 26, 2026 11:58
@JSONbored
JSONbored merged commit 6f3215c into main Jul 26, 2026
5 checks passed
@JSONbored
JSONbored deleted the feat/decision-ledger branch July 26, 2026 12:06
@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 26, 2026
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
21834 2 21832 21
View the top 2 failed test(s) by shortest run time
test/unit/config-templates.test.ts > config/examples review templates (#1682) > loopover.full.yml body matches .loopover.yml.example from WHERE IT LIVES onward
Stack Traces | 0.0309s run time
AssertionError: expected '# WHERE IT LIVES (first match wins):\…' to be '# WHERE IT LIVES (first match wins):\…' // Object.is equality

- Expected
+ Received

@@ -91,13 +91,10 @@
  #   off      — the dimension is not evaluated.
  #   advisory — the finding is surfaced (comment/context) but never blocks.
  #   block    — the finding can become a hard `LoopOver Gate` blocker
  #              (always confirmed-contributor-gated).
  gate:
-   # Percent (0-20) of would-auto-close PRs randomly HELD for human adjudication instead of closing --
-   # the randomized holdout behind the unbiased close-precision estimate (#8831). 0/absent disables.
-   # closeAuditHoldoutPct: 5
    # Legacy check-run publish switch (#5355) — despite the name, this does NOT turn the deterministic
    # gate itself on or off. Gate evaluation, comments, labels, audit records, spend, and autonomous
    # merge/close all run identically whether this is true, false, or unset; the per-dimension modes
    # below are what actually configure gate evaluation. `enabled` is only a boolean shorthand for
    # `checkMode` below: true maps to `required`, false maps to `disabled`, and it cannot express

 ❯ test/unit/config-templates.test.ts:39:57
test/unit/selfhost-metrics.test.ts > DEFAULT_METRIC_META completeness (drift guard, 2026-07 fix) > every literal metric name emitted anywhere in src/ has a registered DEFAULT_METRIC_META entry
Stack Traces | 0.22s run time
AssertionError: expected [ Array(1) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   "loopover_close_audit_holdouts_total",
+ ]

 ❯ test/unit/selfhost-metrics.test.ts:385:21

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant