# Veritally hash-chain specification

This is the exact, independently-verifiable rule Veritally uses to make its
ledger tamper-evident. An auditor can recompute the whole chain from the raw
rows with nothing but a SHA-256 implementation. If a single field of a single
historical entry changes, every hash from that point forward stops matching.

## The rule

```
hash(entry) = SHA-256( prev_hash + "|" + canonical_json(body) )
```

- **`prev_hash`** is the `hash` of the previous entry in the same tenant's chain.
- The **first** entry in a tenant's chain uses the fixed genesis constant
  `"GENESIS"` as its `prev_hash`.
- **`canonical_json(body)`** is `JSON.stringify(body)` over the body object with
  its keys in the exact order below (no whitespace, no reordering).
- The chain is **per tenant**. Two tenants never share a chain.
- The same rule is used for the `ledger` and for `adjustments`.

## The body

For a **ledger** entry the body is, in this key order:

```json
{
  "seq": 11,
  "ts": "2026-06-01",
  "source": "llm",
  "model": "gpt-4o",
  "agent": "research-agent",
  "principal": "burak@veritally.ai",
  "policy": "R&D",
  "amount": 12.5,
  "lines": [
    { "gl": "6000 · AI Services", "debit": 12.5, "credit": 0 },
    { "gl": "2000 · AI Spend Clearing", "debit": 0, "credit": 12.5 }
  ],
  "attributed": true
}
```

- `seq` is the ledger row's sequence number (the value stored in the row, used
  verbatim in the hash — not a recomputed 1..n index).
- `amount` is rounded to cents (`Math.round(x*100)/100`).
- `lines` is always the double-entry pair: debit the expense GL, credit
  `2000 · AI Spend Clearing`, both equal to `amount`.
- The `hash` and `prev_hash` fields are **not** part of the body.

For an **adjustment** the body is (only the fields the adjustments table
persists, so the chain re-walks from stored rows):

```json
{
  "type": "adjustment",
  "ts": "2026-06-20",
  "agent": "research-agent",
  "amount": 5,
  "lines": [
    { "gl": "6000 · AI Services", "debit": 5, "credit": 0 },
    { "gl": "6900 · Unallocated AI", "debit": 0, "credit": 5 }
  ]
}
```

An adjustment reclassifies spend from `old_gl` (credited, the second line) to
`new_gl` (debited, the first line) as a **new** chained entry. History is never
edited in place. The adjustments chain uses the same `prev|json` rule with its
own genesis, and `GET /v1/verify` walks it too (see below).

## Verifying independently (reference implementation)

> **Byte encoding matters.** The entry string is hashed as a **Latin-1 byte
> sequence** (each code unit is one byte) — this is the normal case, since GL
> labels use the "·" separator (U+00B7), which is the single byte `0xB7`. The one
> exception: if the string contains any character **above U+00FF** (e.g. an emoji
> or CJK character in a free-text field), the whole string is **UTF-8** encoded
> first, then hashed as bytes. So a correct verifier uses Latin-1 unless a
> character is out of Latin-1 range, in which case it uses UTF-8. The helper below
> picks the right one automatically.

```js
import { createHash } from "node:crypto";
// Latin-1 for the normal (≤ U+00FF) case; UTF-8 when any character is out of range.
const wide = (s) => { for (const ch of s) if (ch.codePointAt(0) > 0xFF) return true; return false; };
const sha256 = (s) => createHash("sha256").update(s, wide(s) ? "utf8" : "latin1").digest("hex");

function verify(entries, genesis = "GENESIS") {
  let prev = genesis;
  for (const e of entries) {          // entries in ascending seq order
    const body = { seq: e.seq, ts: e.ts, source: e.source, model: e.model,
      agent: e.agent, principal: e.principal, policy: e.policy,
      amount: e.amount, lines: e.lines, attributed: e.attributed };
    const h = sha256(prev + "|" + JSON.stringify(body));
    if (h !== e.hash || e.prev_hash !== prev) return { valid: false, first_broken_seq: e.seq };
    prev = h;
  }
  return { valid: true, first_broken_seq: null };
}
```

> Note: Veritally's engine ships its own dependency-free SHA-256 (`engine.sha256`)
> so the browser demo and the Worker produce byte-identical hashes. It is a
> standard SHA-256 over the byte sequence described above (Latin-1, or UTF-8 for
> out-of-range characters); any conformant SHA-256 — like the `node:crypto`
> helper above, or an auditor's own tool using the same encoding rule —
> reproduces it exactly.

## The public proof

`GET /v1/verify` (and the `verify_ledger` MCP tool) re-walk **both** the ledger
chain and the adjustments chain and return
`{ valid, entries_checked, first_broken_seq, adjustments: { valid, entries_checked, first_broken_seq } }`.
`valid` is true only if both chains reconcile; each `first_broken_seq` is the
sequence number of the first entry whose hash or `prev_hash` does not reconcile —
`null` when that chain is intact. This endpoint needs no key on the demo tenant;
point an auditor at it.
