# Nukez Merkle V1 — Attestation & Verification Spec

**Status**: Normative. This is the canonical specification third parties should cite when independently verifying Nukez on-chain attestations.

**Spec name**: `nukez-merkle-v1`
**Schema version**: `1.0`
**Machine-readable form**: [/specs/nukez-merkle-v1.json](/specs/nukez-merkle-v1.json)

---

## 1. Spec identity

| Field | Value |
|---|---|
| `name` | `nukez-merkle-v1` |
| `schema_version` | `1.0` |
| `hash` | SHA-256 |
| `encoding` | UTF-8 |
| `digest_format` | lowercase hex |

---

## 2. File entry schema

Every file participating in a Merkle tree is described by:

```json
{
  "filename": "string",
  "size_bytes": "integer >= 0",
  "content_hash": "sha256:<64 hex>  OR  <64 hex>"
}
```

`content_hash` MAY appear with or without the `sha256:` prefix on the wire. All algorithms below normalize to the bare 64-character lowercase hex form before hashing.

---

## 3. Content hash normalization

```
If content_hash starts with "sha256:", strip that prefix before Merkle leaf hashing.
For byte-level file verification, SHA-256 the raw downloaded bytes and compare to
content_hash after normalizing both sides to the same prefix format.
```

---

## 4. Leaf formula

```python
content_hash = content_hash.removeprefix("sha256:")
leaf_material = f"{filename}:{size_bytes}:{content_hash}"
leaf_hash = sha256(leaf_material.encode("utf-8")).hexdigest()
```

`leaf_hash` is a bare 64-character lowercase hex string.

---

## 5. Ordering

```
Sort file entries lexicographically by filename before computing leaves.
```

Sorting is by raw Unicode codepoint order over UTF-8 strings.

---

## 6. Parent formula

```python
parent_hash = sha256((left_hex + right_hex).encode("utf-8")).hexdigest()
```

Concatenation is over the **hex strings** (not raw bytes), then the UTF-8 encoding of that concatenation is fed to SHA-256.

---

## 7. Tree construction

```
Build bottom-up.
Pair nodes left-to-right.
If a level has an odd number of nodes, duplicate the final node.
Repeat until one root remains.

A single-file tree root is that file's leaf hash.
Empty file lists are INVALID for Nukez attestations.
```

The duplication rule is intentional and matches the on-chain anchor implementation. Verifiers MUST implement it identically.

---

## 8. Root format

```
Published roots are represented as "sha256:<64 lowercase hex>".
Internal tree math uses bare 64-character lowercase hex strings.
```

Conversions:
- Bare → published: prepend `sha256:`
- Published → bare: strip `sha256:` prefix

---

## 9. Attestation object

```json
{
  "receipt_id": "string",
  "locker_id": "string",
  "result_hash": "sha256:<64 hex>",
  "merkle_root": "sha256:<64 hex>",
  "manifest_signature": "string",
  "file_count": "integer",
  "total_bytes": "integer",
  "files": ["sorted file entries"],
  "attestation_status": "pending | computed | pushing | complete",
  "attested_at": "ISO-8601 UTC timestamp",
  "schema_version": "1.0",
  "switchboard_slot": "integer | null",
  "switchboard_tx": "string | null",
  "switchboard_feed": "string | null"
}
```

The three `switchboard_*` fields are populated only for historical Switchboard anchors. Records anchored today carry a provider-neutral `anchor` object instead, together with `proof_id`, `snapshot_id`, `evidence_seq`, `envelope`, `check_mode`, `manifest_file_count`, `files_skipped`, and `last_anchor_error`. These fields are additive, and they are specified in `/specs/nukez-evidence-v2.json`. None of them changes how the Merkle root, the `result_hash`, or the `manifest_signature` is computed.

The authoritative cryptographic values are `result_hash`, `merkle_root`, `files`, and the on-chain anchors. Keep the two hashes distinct: `merkle_root` is the field that carries the Ed25519 `manifest_signature` and the value written into the on-chain SPL Memo, while `result_hash` is a separate digest over the canonicalized manifest summary, and it is the value from which `att_code` derives.

The `attestation_status` lifecycle runs `pending`, then `computed`, then `pushing`, then `complete`. For pollers, a truthy `merkle_root` is the completion signal.

A routine green attestation is a presence-and-consistency proof: it verifies that the manifest's file entries exist in storage and that their recorded content hashes combine into the signed merkle root. It does not re-download and re-hash the stored bytes. The stricter byte-level check — re-reading every file's bytes and re-hashing them — is what `GET /v1/storage/recompute-verify` performs, and that endpoint requires a payer-signed envelope with the `locker:read` operation.

### 9.1 manifest_signature

`manifest_signature` is the gateway's own attestation that it computed the `merkle_root`. It complements the on-chain anchor: the anchor proves the root exists on Solana, the signature proves Nukez produced it. It is tamper-evidence at the gateway boundary, verifiable independently of the chain.

| Property | Value |
| --- | --- |
| Algorithm | Ed25519 |
| Signer | The gateway service key — the same key that signs `receipt_sig` |
| Signer public key | Published as `receipt_signer_pubkey` on `GET /v1/receipts/{receipt_id}` (64 lowercase hex chars) |
| Signed payload | The bare `merkle_root` with the `sha256:` prefix stripped, UTF-8 encoded — **not** the prefixed string and **not** the raw 32 bytes |
| Signature encoding | Lowercase hex, 128 chars (a 64-byte Ed25519 signature). Note: not base58, despite some older internal docs |
| Unsigned fallback | If the signing key is unavailable, the field is `unsigned:<first 32 hex of sha256(merkle_root)>`. Verifiers **MUST** reject any value starting with `unsigned:`. |

Verification:

```python
import binascii
from nacl.signing import VerifyKey

pub_hex = GET(f"/v1/receipts/{receipt_id}")["receipt_signer_pubkey"]
sig_hex = attestation["manifest_signature"]
assert not sig_hex.startswith("unsigned:"), "unsigned placeholder — reject"

payload = attestation["merkle_root"].removeprefix("sha256:").encode("utf-8")
VerifyKey(binascii.unhexlify(pub_hex)).verify(payload, binascii.unhexlify(sig_hex))
# raises nacl.exceptions.BadSignatureError if the signature does not verify
```

For full chain-of-custody, the same `merkle_root` should also appear in the SPL Memo of the on-chain anchor transaction, tying the gateway signature to the on-chain record. Take the transaction from `anchor.tx` first, and use `switchboard_tx` only when the response has no `anchor` object.

The same key also signs `receipt_sig`, the signature over the canonical receipt-plus-manifest payload, so verifying a receipt proof means checking both `manifest_signature` (over the bare merkle root string) and `receipt_sig` against `receipt_signer_pubkey`. The signatures are 128 hexadecimal characters (64-byte Ed25519 signatures) and the public key is 64 hexadecimal characters — none of these values are base58.

One honest caveat applies to any claim of trust-free verification: a robust verifier must pin `receipt_signer_pubkey` out of band — record it once from a context it trusts and compare on every later verification — because the key is served by the same gateway that produces the signatures, and a gateway that chose to tamper could serve both a forged object and a matching key. Do not substitute the key from `/v1/short-url/verify-key`: that endpoint serves a different key (the short-URL token signer), which does not verify `receipt_sig` or `manifest_signature`.

### 9.2 file_count binding (odd-node ambiguity)

The odd-node duplication rule (§ on tree construction) means two distinct file lists can in principle produce the same `merkle_root` — the classic CVE-2012-2459 ambiguity inherited from Bitcoin-style trees. Nukez closes this by binding the file count.

A verifier **MUST** confirm that the number of leaves it used to reconstruct the `merkle_root` equals the attested `file_count` (present in the attestation object and written into the on-chain SPL Memo). If the leaf count does not equal `file_count`, the verification **MUST** be rejected even if the reconstructed root matches. This prevents a forged file list that exploits odd-node duplication from validating against a legitimate root.

Filenames cannot contain `:` (server-side validation rejects it, along with `\`, `..`, and a leading `.` outside the system-prefix allowance), so the `filename:size_bytes:content_hash` leaf material is unambiguous. A third-party verifier reconstructing leaves should reject any filename containing `:` rather than attempt to parse it.

---

## 10. Result hash

`result_hash` is a deterministic digest over the locker's canonicalized manifest summary.

```python
manifest_summary = {
  "locker_id": locker_id,
  "files": sorted(
    [
      {
        "filename": file.filename,
        "size_bytes": file.size_bytes,
        "content_hash": file.content_hash
      }
      for file in files
    ],
    key=lambda e: e["filename"]
  )
}

canonical_json = json.dumps(
  manifest_summary,
  separators=(",", ":"),
  sort_keys=True,
  ensure_ascii=False
)

result_hash = "sha256:" + sha256(canonical_json.encode("utf-8")).hexdigest()
```

Canonical JSON rules:
- Keys sorted alphabetically at every level
- No whitespace between elements
- Separators `,` and `:` without spaces
- `ensure_ascii=False` (UTF-8 preserved as-is)

---

## 11. att_code (display / oracle value — NOT a security primitive)

> **Important**: `att_code` is **derived from `result_hash`**, not directly from `merkle_root`.
> `att_code` is **not the security primitive**. It is a compact oracle/display value
> suitable for UIs and on-chain oracle feeds where a full 256-bit hash is impractical.
> The authoritative values for verification are `result_hash`, `merkle_root`, the
> file entries, and the on-chain anchors.

```python
def att_code_from_hash(h):
    h = h.removeprefix("sha256:")
    return int(h[:12], 16) % 1_000_000_000
```

---

## 12. Inclusion proof schema

```json
{
  "receipt_id": "string",
  "filename": "string",
  "leaf_hash": "<64 hex>",
  "leaf_index": "integer",
  "merkle_root": "sha256:<64 hex>",
  "proof": [
    {
      "hash": "<64 hex>",
      "position": "left | right"
    }
  ],
  "tree_depth": "integer",
  "file_count": "integer",
  "file_entry": {
    "filename": "string",
    "size_bytes": "integer",
    "content_hash": "string"
  },
  "schema_version": "1.0"
}
```

---

## 13. Proof verification

```python
current = leaf_hash

for step in proof:
    sibling = step["hash"].removeprefix("sha256:")
    if step["position"] == "left":
        current = sha256((sibling + current).encode("utf-8")).hexdigest()
    else:
        current = sha256((current + sibling).encode("utf-8")).hexdigest()

assert "sha256:" + current == merkle_root
```

`position` describes where the **sibling** sits relative to `current`:
- `left` → sibling is on the left, so `sibling || current`
- `right` → sibling is on the right, so `current || sibling`

---

## 14. Public endpoints

```
GET  /v1/storage/verification-bundle?receipt_id={receipt_id}
GET  /v1/storage/evidence-bundle?receipt_id={receipt_id}
GET  /v1/keys
GET  /v1/storage/merkle-proof?receipt_id={receipt_id}&filename={filename}
GET  /v1/storage/verify?receipt_id={receipt_id}
POST /v1/storage/verify         body: {"receipt_id": "..."}
GET  /v1/attest-code?receipt_id={receipt_id}
GET  /v1/receipts/{receipt_id}
GET  /v1/receipts/{receipt_id}/verify
```

All of the endpoints listed above are unauthenticated, and `verification-bundle` returns a self-contained proof package suitable for offline verification. They are, however, receipt-scoped: a verifier must hold the `receipt_id` (or the verification bundle that contains it), and file bytes for byte-level checks come through the receipt-scoped file endpoints such as `GET /v1/r/{receipt_id}/f/{filename}`. Verification is therefore permissionless in the sense of requiring no Nukez account — it is not anonymous access to arbitrary lockers.

Two related endpoints are deliberately not public. `POST /v1/storage/attest` computes and anchors attestations and requires a payer-signed envelope with the `locker:attest` operation. `GET /v1/storage/recompute-verify` — the endpoint that re-downloads stored bytes and re-hashes them — requires a payer-signed envelope with the `locker:read` operation, and it is strictly read-only: it computes in memory and never overwrites the stored attestation.

---

## 15. On-chain anchors

Every proof response names the kind of anchor in `anchor.type`. A reader takes the transaction from `anchor.tx` first, and falls back to `switchboard_tx` only when there is no `anchor` object. A reader never labels a `solana_sas_v1` or `solana_memo_v2` anchor as Switchboard.

### Solana, published by the gateway (current anchors)

Anchor types: `solana_memo_v2` and `solana_sas_v1`.

The gateway signs a version-two evidence envelope with its statement-signing key, which `GET /v1/keys` lists. The envelope commits to the `merkle_root` defined by this spec, and the SHA-256 hash of its canonical body is called the statement digest. The gateway then sends one Solana transaction, signed by its publication key, which proof responses report as `anchor.signer`.

- **SPL Memo** instruction stores a compact statement:
  - `s` (the constant `nukez/anchor/v2`)
  - `pid` (the public proof id) and `sid` (the snapshot id)
  - `dg` (the statement digest of the signed envelope)
  - `root` (the `merkle_root` without its `sha256:` prefix)
  - `n` (the file count), `seq` (the statement sequence number), `t` (the issue time in Unix seconds), and `k` (the signing key id)
- **Solana Attestation Service account** (only for `solana_sas_v1`) holds the receipt's current commitment: the proof id, the snapshot id, the statement digest, and the Merkle root. Proof responses report its address as `anchor.sas.attestation`. In the default mode a later snapshot replaces the account, so the memo transactions remain the permanent history.

The memo never carries the receipt id, a filename, or any access handle. The `file_count` binding from section 9.2 still applies: `n` in the memo MUST equal the leaf count the verifier used.

What this anchor proves: the gateway's published key committed this exact snapshot on Solana at this slot, and, with the attestation service account, the current commitment is readable on-chain at a predictable address. It does not prove that a third party inspected the files.

The normative description of the envelope, the memo, the attestation service schema, and the portable evidence bundle is `/specs/nukez-evidence-v2.json`. `GET /v1/storage/evidence-bundle?receipt_id={receipt_id}` returns everything an independent verifier needs in one document, and the `nukez-verify` command checks a saved bundle and reports each fact separately.

### Solana / Switchboard (historical anchors)

Anchor type: `switchboard_v2`. Records that were anchored in the Switchboard era keep these anchors and their original meaning.

- **PullFeed** stores the oracle `att_code` (the compact display value, not authoritative).
- **SPL Memo** instruction stores the authoritative metadata:
  - `schema` (this spec name)
  - `receipt_id`
  - `merkle_root`
  - `file_count`
  - `attested_at`

The SPL Memo is the cryptographically meaningful anchor; PullFeed is a convenience for on-chain consumers that can only handle small numeric values.

### Monad

- `receiptId` on Monad is `bytes16(keccak256(receipt_id_string))`.
- `verifyMerkle(receiptId)` returns: `merkleRoot`, `solanaSlot`, `fileCount`, `attestedAt`.

The Monad anchor format above is specified for cross-chain verification, but the Monad bridge is not currently operational: it is unconfigured and inert in production, so new attestations are not being anchored on Monad at this time. The Solana anchor is the live anchor.

---

## 16. Test vector

Verifiers MUST reproduce these exact values to be considered conformant.

### Input

```json
{
  "files": [
    {
      "filename": "a.txt",
      "size_bytes": 3,
      "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    },
    {
      "filename": "b.txt",
      "size_bytes": 5,
      "content_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    },
    {
      "filename": "c.txt",
      "size_bytes": 7,
      "content_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
    }
  ]
}
```

### Expected merkle root

```
sha256:a80128f3298c7b6bf0b894576066d61a1e270d8bf4638d01ddd6d8e626f45528
```

### Inclusion proof for `b.txt`

```json
[
  {
    "hash": "91481cbebb6c2f6438ed263b130212193ef908a9864c2b9b77d511bd07072879",
    "position": "left"
  },
  {
    "hash": "539d42382ade0da0fe370b9f86b80739b31db6f06ac8a482ef1f7390251f6262",
    "position": "right"
  }
]
```

Verifying this proof against the leaf hash of `b.txt` MUST yield the expected merkle root above.

---

## 17. Conformance

An implementation is conformant if:
1. It produces the exact root from §16 for the test vector.
2. It produces the exact proof from §16 for `b.txt`.
3. It rejects empty file lists with a clear error.
4. It treats `att_code` as a display value, never as a security primitive.
5. It applies the leaf-duplication rule on odd-count tree levels.

---

## 18. References

- Machine-readable spec: [/specs/nukez-merkle-v1.json](/specs/nukez-merkle-v1.json)
- Discovery doc: [/.well-known/nukez.json](/.well-known/nukez.json)
- Endpoint reference: [/openapi.json](/openapi.json)
- Narrative walkthrough: [/docs/verify](/docs/verify) (frontend) and [/proof/verify](/proof/verify) (frontend, per-receipt)
