# Redactable Decision Receipts — commitment scheme `summit-decrec-merkle-v1`

**Status:** implemented, opt-in per deployment (`RECEIPT_COMMITMENT=merkle`).
**Applies to:** receipts carrying `commitment.scheme = "summit-decrec-merkle-v1"`.
Receipts without a `commitment` field are v1 and are covered by
[`DECISION_RECEIPT_V1.md`](DECISION_RECEIPT_V1.md) unchanged, permanently.

This document exists so you can verify a receipt **without trusting us and without
using our code**. If any statement here disagrees with our implementation, this
document is the specification and the implementation is wrong.

---

## 1. What problem this solves

A v1 `receipt_hash` is a single SHA-256 over the canonical form of the whole
receipt body. That is an all-or-nothing commitment: hiding one field changes the
hash, so a receipt containing an operator's identity must be served **whole or
not at all**. Served not-at-all, a reader can confirm only that the ledger pins
*some* body they never see.

A Merkle commitment over per-field leaves lets a publisher replace individual
fields with their leaf digests. Everything else stays readable and the root still
reproduces, so **you verify exactly the bytes you were handed.**

What this does **not** give you: the redacted values, or any assurance about what
they were. A redaction is visible, counted, and located — never silent.

## 2. The commitment covers the CORE

Before hashing, the publisher removes exactly two top-level fields:

- `attestations`
- `chain`

Both are metadata written *after* the receipt is committed to (signatures, and
chain links that are re-stamped atomically at append time). **A verifier MUST
strip both before recomputing.** Leaving them in yields a root that never
matches. All paths below are relative to this stripped core.

## 3. Leaves

The core is flattened to leaves at primitive granularity, in canonical order.

| value | leaf |
|---|---|
| string, number, boolean, null | `kind = "primitive"`, `value` as-is |
| `{}` | `kind = "empty-object"`, `value = null` |
| `[]` | `kind = "empty-array"`, `value = null` |
| object | recurse over keys sorted by UTF-16 code unit |
| array | recurse over indices in order |
| absent / `undefined`-valued key | **omitted**, exactly as `JSON.stringify` does |
| `undefined` array element | committed as `null`, exactly as `JSON.stringify` does |

A **path** is an array of segments: object keys as strings, array indices as
numbers. Paths are never dotted strings — `["a","b"]` must not be confusable with
a key literally named `"a.b"`.

Empty containers get distinct kinds so `{"a":{}}`, `{"a":[]}` and `{"a":null}`
cannot collide.

**Undefined handling is not cosmetic.** The tree and the canonical form must agree
about what a receipt *is*, or a receipt's root and its canonicalization describe
different documents. JSON has no `undefined`, so a key whose value is `undefined`
is omitted and an `undefined` array element becomes `null` — matching
`JSON.stringify` exactly. A consequence, inherited from JSON rather than
introduced here: `{"a":1,"b":undefined}` and `{"a":1}` commit identically.

**Leaf digest:**

```
leaf_hash = SHA256( 0x00 || JCS([path, kind, value]) )
```

`JCS(...)` is RFC 8785 canonical JSON (sorted keys, ECMAScript number
formatting, well-formed escaping).

## 4. Tree

Leaf digests are folded pairwise, **in leaf order**:

```
node_hash = SHA256( 0x01 || left_bytes || right_bytes )
```

where `*_bytes` are the raw 32 bytes, not hex text.

If a level has odd length the trailing node is **promoted unchanged to the next
level**. It MUST NOT be duplicated and paired with itself — that is
CVE-2012-2459, and it makes `[a,b,c]` and `[a,b,c,c]` produce the same root.

## 5. Root

```
receipt_hash = SHA256( 0x02 || JCS(["summit-decrec-merkle-v1", leaf_count]) || tree_root_bytes )
```

The leaf **count** is bound in. Together with promotion this removes the
structural ambiguity that duplication reintroduces. The scheme string is bound in
so a future revision cannot produce a root that validates under these rules.

The three distinct prefixes (`0x00` leaf, `0x01` node, `0x02` root) are domain
separation. In this encoding leaf and node preimages are already disjoint — one
is UTF-8 JSON, the other is raw digest bytes, and canonical JSON escapes control
characters — so the prefixes are defence against a future change to leaf
encoding rather than load-bearing today. Implement them anyway.

## 6. Redacted documents

A redacted receipt is served as:

```json
{
  "receipt": { ...body with markers... },
  "redaction": {
    "scheme": "summit-decrec-merkle-v1",
    "redacted_paths": [["subject","agent"]]
  }
}
```

Each redacted value is replaced by a **marker**:

```json
{ "__redacted__": "<64 hex chars — the leaf digest>" }
```

### The rule that makes this safe

**A marker is honoured ONLY at a path listed in `redacted_paths`.** A
marker-shaped object anywhere else is ordinary data and is hashed as such.

Without this rule, any value that merely *looked* like a marker could substitute
an arbitrary digest into the tree — forgery wearing the costume of redaction.

Only **leaf** paths may be redacted. A path covering multiple leaves MUST be
rejected: one digest cannot honestly stand for several values.

## 7. Verification

1. Take `receipt`, strip `attestations` and `chain`.
2. Walk it in canonical order. At each declared redacted path, use the carried
   digest. Everywhere else compute the leaf digest per §3.
3. Fold per §4, finalize per §5.
4. Compare against `ledger_entry.receipt_hash` for that `receipt_id`, in
   **constant time**.

A mismatch means the document was altered — including any change to a *visible*
field. Redaction hides values; it does not loosen the binding on anything else.

### Verify it against our endpoint

```bash
curl -sX POST https://decrec.summitcognitive.ai/v1/verify \
  -H 'Content-Type: application/json' \
  -d '{"receipt": <body>, "redaction": <redaction>}'
```

`/v1/verify` accepts a bare receipt (as always) or this envelope. Checking your
own implementation against ours is encouraged; **disagreement is a bug report we
want.**

## 8. Scope and honest limits

- **Forward-only.** Receipts issued before this scheme was enabled remain v1 and
  are not redactable. Their hashes are already published, chained and signed;
  re-deriving them under a new rule would rewrite history. Identity-bearing v1
  receipts are still withheld from anonymous callers.
- **Redaction is not deletion.** The leaf digest is a commitment to the removed
  value. Low-entropy values (an email address) are recoverable by dictionary
  search against that digest by design of hashing, not of this scheme. Treat a
  redacted leaf as *withheld from casual view*, **not** as cryptographically
  private.
- **Metadata is outside the commitment.** Identity appearing in `attestations` or
  `chain` cannot be redacted this way; such receipts are withheld whole instead.
