# Gwaalas Trust Chain — Canonical Hash Specification

**Version 1.** Served at `GET /api/v1/spec`. This document is authoritative: the Elixir
implementation (`Gwaalas.Trust.canonical_json/1`) and the reference Python verifier
(`priv/verifier/verify_trust.py`) both implement exactly what is written here. If any of the three
disagree, this document is the arbiter and the other two are bugs.

You do not need Gwaalas's cooperation to verify a chain. Everything required is below, and the
data comes from a public, unauthenticated endpoint.

---

## 1. What a chain is

Each batch has a `trust_code` (format `GW-XXXXXX`) and an ordered list of events. Every event is
hashed, and each event's hash covers the previous event's hash — so altering any event invalidates
that event and every event after it.

Fetch a chain:

```
GET /api/v1/verify/:trust_code
```

No authentication. Returns JSON with `trust_code`, `event_chain`, `validity`, `batch_details`,
and `specification`.

## 2. Event hash

For each event, in order:

```
hash = SHA256( prev_hash || canonical_json(event) )
```

- `||` is byte concatenation of the UTF-8 encodings.
- `prev_hash` is the 64-character lowercase hex string of the previous event's hash.
- The result is encoded as **64 lowercase hex characters**.

### Genesis rule

For event index 0 there is no previous event. Its `prev_hash` is the hash of the trust code itself:

```
prev_hash[0] = SHA256(trust_code)      # lowercase hex
```

`trust_code` is UTF-8 encoded, exactly as it appears in the response (e.g. `GW-YICNIO`).

### Chain linkage

For every event `n > 0`:

```
prev_hash[n] == hash[n-1]
```

A chain is valid when every event's recomputed hash equals its stored `hash` **and** every
event's `prev_hash` equals the preceding event's `hash`.

## 3. `canonical_json`

The hash input must be byte-identical for everyone, so the JSON encoding is fully specified — do
not use a general-purpose JSON serialiser's default output.

### 3.1 Excluded keys (root level only)

At the **root** of an event object, remove these keys before encoding:

| Key | Why |
|---|---|
| `hash` | It is the output; it cannot be part of its own input. |
| `prev_hash` | Concatenated separately, before the JSON. |
| `index` | Presentation only, added by the API. Not stored. |
| `attestation` | Presentation only, added by the API. Not stored. |

**Only at the root.** A nested map that happens to contain a key named `hash` keeps it.

`index` and `attestation` are derived by the API for convenience and are never part of stored
events, so excluding them reproduces the stored bytes exactly. Attestation is *not* unprotected by
this: the attesting device's identifier is stored at `data.key_id`, which is inside the hash.

### 3.2 Key ordering

Sort the keys of **every** object — root and nested, at every depth — ascending by the **UTF-8 byte
sequence** of the key. Not by locale, not by code point after normalisation, not by insertion order.

### 3.3 Encoding

- Objects: `{` + `"key":value` pairs joined by `,` + `}`. **No whitespace anywhere.**
- Arrays: `[` + elements joined by `,` + `]`. **Element order is preserved** — arrays are never
  sorted.
- Strings: standard JSON string escaping, emitting non-ASCII characters **raw as UTF-8**, not as
  `\uXXXX` escapes.
- Numbers, booleans, null: standard JSON.
- Values are encoded recursively by these same rules.

### 3.4 Worked example

Given the stored event:

```json
{"type":"tested","timestamp":"2026-07-21T15:30:45.357718Z",
 "data":{"snf_percent":"8.9","fat_percent":"4.8","density":"1.031"},
 "prev_hash":"ddee...","hash":"6ff8..."}
```

`canonical_json` produces (single line, no spaces, all keys sorted at both levels):

```
{"data":{"density":"1.031","fat_percent":"4.8","snf_percent":"8.9"},"timestamp":"2026-07-21T15:30:45.357718Z","type":"tested"}
```

and the hash is `SHA256("ddee..." || that_string)`.

## 4. Event types

`created`, `tested`, `can_sealed`, `handed_to_rider`, `delivery_outcome`, `delivered`,
`route_recorded`, `analyzer_reading`.

### 4.1 Seal Lifecycle Events

- **`can_sealed`**: Appended when a tamper-evident physical seal tag is attached to a milk container. `data` carries `"seal_code"` (e.g. `SEAL-98241A`).
- **`delivery_outcome`**: Appended at delivery handover. When a seal is attached, `data` carries `"seal_code_presented"`, `"seal_match"` (`true`, `false`, or `null`), and `"seal_status"` (`"confirmed"`, `"mismatched"`, or `"unchecked"`).

Treat this list as open — new types may be added, and a verifier must hash an unknown type exactly
the same way. **Never special-case a type when hashing.**

## 5. Timestamps

Two distinct fields, and the distinction matters:

| Field | Meaning |
|---|---|
| `timestamp` | Server time at append. This orders the chain and is the only trusted ordering. |
| `data.observed_at` | What the sending device *claims*. Data, never ordering. |

When `observed_at` precedes `timestamp` by more than 300 seconds, the source was offline and
uploaded late. Both are inside the hash.

## 6. Attestation

Each event in the API response carries an `attestation` object (added by the API, excluded from
hashing per §3.1):

- `{"source": "human"}` — entered by a person.
- `{"source": "device", "key_id": "..."}` — submitted by a named registered device, derived from
  the hash-covered `data.key_id`.

**Attestation raises confidence; it does not by itself mark a batch verified.** A device is trusted
no more than its seal.

## 7. What this proves, and what it does not

A verifying chain proves the recorded events have **not been altered since they were recorded**.

It does not prove any statement was true when recorded. A person can enter a false reading, and a
device can be miscalibrated or tampered with physically; the chain faithfully preserves either.
This is why the system is described as **tamper-evident**, never tamper-proof.

## 8. Reference implementation

`GET /verifier/verify_trust.py` — standalone Python 3, no third-party dependencies.

```bash
curl -O https://gwaalas.com/verifier/verify_trust.py
python3 verify_trust.py GW-XXXXXX --base-url https://gwaalas.com
```

Exit code `0` = verified, `1` = tampered. It is a convenience, not an authority: this document is
sufficient to write your own, and one you wrote yourself is worth more than one we gave you.
