§ proof · verification guide
The Canonical Attestation & Verification Guide
Each Nukez receipt is a gateway-signed, content-addressed JSON record cryptographically bound to the purchasing keypair. A receipt serves as a durable authorization token and as proof of title. External verification is enabled by anchoring each attestation on Solana. Current anchors are published by the gateway's own key. Historical anchors were published through the Switchboard attestation pipeline, and they keep their original meaning.
§ the four levels
The Verification Gates
Each level proves a specific thing. You don't need to do them all — any single one that passes gives you a specific guarantee.
Did the bytes change?
Hash the file you downloaded. Compare to the manifest's content_hash. Match means byte-identical to what was stored.
Did the set of files change?
Rebuild the merkle tree from the manifest entries. Compare to the stored root. Match means no add, remove, modify, or reorder since attestation.
Did the gateway sign it?
Verify the Ed25519 attestation signature over the merkle root using the gateway's public key. Match means the attestation is authentic.
Did Solana see the same root?
Read the SPL Memo in the anchor transaction on Solana. Compare the on-chain Merkle root to the receipt's. A match means the root was committed at a specific slot. Historical anchors also have a Switchboard feed.
§ level 1 · file integrity
Did the bytes change?
After downloading a file from a Nukez locker, compute its SHA-256 hash locally and compare against the content_hash recorded in the locker manifest. Be clear about where the bytes come from: the verifier holds the receipt (or the verification bundle built from it) and fetches the file through the receipt-scoped endpoints, such as GET /v1/r/{receipt_id}/f/{filename}. This check is permissionless in the sense that no Nukez account is needed — possession of the receipt is what grants read access. It is not anonymous access to arbitrary lockers.
import hashlib, urllib.request
# The verifier holds the receipt (or its verification bundle). Fetch the
# bytes through the receipt-scoped file proxy — no Nukez account needed.
url = f"https://api.nukez.xyz/v1/r/{receipt_id}/f/{filename}"
file_bytes = urllib.request.urlopen(url).read()
# Compute SHA-256
local_hash = "sha256:" + hashlib.sha256(file_bytes).hexdigest()
# Compare against the manifest entry from the verification bundle
assert local_hash == manifest_entry["content_hash"]Match — the file you hold is byte-identical to what was stored. Mismatch — the content has been modified since storage.
§ level 2 · manifest + merkle integrity
Did the set of files change?
The manifest lists every file with filename, size, and content hash. The merkle root is computed deterministically from these entries — any change to any file produces a different root. The normative algorithm and test vectors live in the Merkle V1 spec.
Merkle leaf
leaf = SHA256("{filename}:{size_bytes}:{content_hash}")content_hash is the raw hex digest in the leaf computation (no sha256: prefix).
Merkle tree (bottom-up)
- Sort all leaves alphabetically by filename.
- Compute SHA-256 of each leaf string.
- Pair leaves left-to-right; if odd, the last leaf pairs with itself.
- Parent = SHA-256(left_hex + right_hex) — concatenate the hex strings, then hash.
- Repeat until one root remains.
import hashlib
def build_merkle_root(file_entries):
"""
file_entries: list of dicts with 'filename', 'size_bytes', 'content_hash'
content_hash values should be raw hex (no 'sha256:' prefix)
"""
if not file_entries:
raise ValueError("empty file lists are invalid for Nukez attestations")
sorted_entries = sorted(file_entries, key=lambda e: e["filename"])
# Compute leaf hashes
leaves = []
for entry in sorted_entries:
raw_hash = entry["content_hash"].replace("sha256:", "")
leaf_data = f"{entry['filename']}:{entry['size_bytes']}:{raw_hash}"
leaf_hash = hashlib.sha256(leaf_data.encode("utf-8")).hexdigest()
leaves.append(leaf_hash)
# Build tree bottom-up
level = leaves
while len(level) > 1:
next_level = []
for i in range(0, len(level), 2):
left = level[i]
right = level[i + 1] if i + 1 < len(level) else level[i]
combined = hashlib.sha256((left + right).encode("utf-8")).hexdigest()
next_level.append(combined)
level = next_level
return level[0]Compare your computed root against the merkle_root in the attestation. Match means no file has been added, removed, modified, or reordered since attestation.
Skip the loop: recompute-verify
Rather than rebuilding the tree yourself, you can ask the gateway to do it. This is a convenience, not a trust dependency — you can always recompute locally. The endpoint requires a payer-signed envelope with the locker:read operation, because it makes the gateway re-download file bytes from storage; byte retrieval is only ever triggered by a request the payer keypair (or an authorized operator) signed. The recompute itself is strictly read-only: it happens in memory and never overwrites the stored attestation. In PyNukez, client.recompute_verify(receipt_id) builds and signs the envelope for you.
recompute-verify is different from /v1/storage/verify. Plain /verify is a cheap, cached snapshot: it trusts the manifest's recorded content hashes and reports the last attested Merkle root. The recompute-verify endpoint does real byte-level work — it re-downloads every file from storage, re-hashes the bytes, rebuilds the tree, and compares to the anchored root. Use /verify for quick status checks; reach for recompute-verify when you need to prove the actual stored bytes still match what was attested. Cost scales with locker size — that's the intended trade-off.
# Signed request — ops ["locker:read"], envelope bound to this GET,
# its query string, and the empty-body hash; the envelope receipt_id
# must match the query receipt_id.
GET /v1/storage/recompute-verify?receipt_id={receipt_id}
X-Nukez-Envelope: <base64url canonical envelope>
X-Nukez-Signature: <signature over the envelope bytes>
# Or, in PyNukez (signs internally):
# result = client.recompute_verify(receipt_id)
# Response
{
"match": true,
"receipt_id": "8239fc15efc46042",
"locker_id": "locker_62e358731c6f",
"computed": "sha256:6a0c80f5c0a3...",
"stored": "sha256:6a0c80f5c0a3...",
"file_count": 1,
"recompute_ms": 1040
}If match is false, either the locker contents changed since attestation, or the stored attestation was tampered with. Either way — you know.
§ level 3 · attestation signature
Did the gateway sign it?
Two Ed25519 signatures cover the proof objects, and both verify against the same key. The attestation carries manifest_signature, signed over the bare merkle root (the 64-character hex digest without its sha256: prefix, UTF-8 encoded). The receipt carries receipt_sig, signed over the canonical JSON of the receipt plus its manifest. The verifying key is receipt_signer_pubkey, returned by GET /v1/receipts/{receipt_id}. All of these are hex-encoded: the public key is 64 hex characters and each signature is 128 hex characters.
from nacl.signing import VerifyKey
import binascii
attestation = get_attestation(receipt_id)
# receipt_signer_pubkey comes from GET /v1/receipts/{receipt_id} —
# hex, 64 characters. A robust verifier PINS this key out of band
# (see the caveat below) instead of trusting the fetched copy.
pubkey = pinned_receipt_signer_pubkey
sig = attestation["manifest_signature"] # hex, 128 characters
# The signed payload is the bare merkle_root hex string, UTF-8 encoded.
signed_payload = attestation["merkle_root"].removeprefix("sha256:").encode("utf-8")
vk = VerifyKey(binascii.unhexlify(pubkey))
vk.verify(signed_payload, binascii.unhexlify(sig)) # raises if forgedRecords anchored today also carry a version-two evidence envelope. The gateway hashes the canonical JSON of envelope.body (RFC 8785) with SHA-256, and the result is the statement digest. The signature covers the ASCII bytes NUKEZ_CONTENT_ENVELOPE_V2, then one zero byte, then the 32 bytes of the statement digest. The verifying key is the same gateway key, and the gateway lists it at GET /v1/keys. Pin it out of band, as the caveat below explains.
import hashlib # The envelope comes from GET /v1/storage/evidence-bundle?receipt_id=... envelope = bundle["envelope"] # canonicalize() is RFC 8785 canonical JSON. The envelope uses no floats. digest = hashlib.sha256(canonicalize(envelope["body"])).digest() assert digest.hex() == envelope["statement_digest"] message = b"NUKEZ_CONTENT_ENVELOPE_V2\x00" + digest vk.verify(message, binascii.unhexlify(envelope["signature"])) # raises if forged
Combined with Level 2 — which binds the manifest to the merkle root — this transitively binds the manifest to the signer. Any modification to the manifest produces a different root, and the signature no longer covers it.
The key-pinning caveat. A signature check is only as independent as your copy of the public key. If you fetch receipt_signer_pubkey from the same gateway response you are verifying, a tampering gateway could serve a forged object together with a key that matches it. A robust verifier therefore pins the receipt signer key out of band — recorded once from a trusted exchange and reused thereafter. Do not substitute the key from /v1/short-url/verify-key: that endpoint serves a different key (the short-URL signer), and it will not verify receipt or attestation signatures. The on-chain anchor in Level 4 is the complementary defense, because a forged root will not match the root committed on Solana.
§ level 4 · on-chain attestation
Did Solana see the same root?
Nukez anchors every attestation on Solana. How the anchor was written depends on when the record was anchored, and every proof response names the kind in anchor.type. Current anchors have the type solana_sas_v1 or solana_memo_v2. Historical anchors have the type switchboard_v2. They were never rewritten, and they keep their original meaning.
Current anchors: a signed memo and an attestation account
The gateway first signs the version-two evidence envelope described in Level 3. It then sends one Solana transaction, signed by its publication key, which the response reports as anchor.signer. The transaction carries an SPL Memo with a compact statement: the public proof identifier, the snapshot identifier, the statement digest, and the Merkle root. The memo never carries the receipt ID, a filename, or any access handle.
{
"s": "nukez/anchor/v2",
"pid": "<proof_id, 32 hex characters>",
"sid": "<snapshot_id, 64 hex characters>",
"dg": "<statement digest, 64 hex characters>",
"root": "<merkle root, 64 hex characters, no sha256: prefix>",
"n": <file count>,
"seq": <sequence number of this statement>,
"t": <issued_at as unix seconds>,
"k": "ed25519:<first 16 hex characters of the signing key>"
}When the anchor type is solana_sas_v1, the same transaction also writes a Solana Attestation Service account for the receipt. The response reports its address as anchor.sas.attestation. The account holds the receipt's current commitment: the proof identifier, the snapshot identifier, the statement digest, and the Merkle root. In the default mode a later snapshot replaces the account, so the memo transactions remain the permanent history.
What this anchor proves: the gateway's published key committed this exact snapshot on Solana at this slot, and, with the attestation account, the current commitment is readable on-chain at a predictable address. It does not prove that a third party inspected the files.
Historical anchors: a Switchboard feed and a memo
Records with the anchor type switchboard_v2 were anchored through two complementary mechanisms, described in the next two sections exactly as they were published. Either is sufficient on its own. Together they give you both a numerical comparison and a permanent log entry.
Switchboard oracle feed
The attestation code (att_code) is pushed to a Switchboard PullFeed account on Solana. It is an integer of at most nine digits, derived from the result_hash (never from merkle_root) — so anyone with the verification bundle can recompute it and compare.
def att_code_from_hash(result_hash):
"""First 12 hex chars (48 bits) as int, clamped to 9 digits."""
h = result_hash.removeprefix("sha256:")
return int(h[:12], 16) % 1_000_000_000The oracle's Ed25519 signature over the quote is verified by Solana's Ed25519 precompile — no trust in the oracle itself is needed.
SPL Memo
The full attestation metadata is written as an SPL Memo in the same transaction — permanently readable in the Solana transaction log via any block explorer.
{
"schema": "nukez/attestation/v1",
"receipt_id": "8239fc15efc46042",
"merkle_root": "sha256:abc123...",
"file_count": 42,
"attested_at": "2026-02-11T..."
}Verifying on-chain
- Get the transaction signature from
anchor.txin the verify response. Use the legacyswitchboard_txfield only when the response has noanchorobject. - Look it up:
https://explorer.solana.com/tx/{tx_signature}. - Find the “Program Log: Memo” entry.
- For a current anchor, confirm that the memo's
dgandrootvalues equal the statement digest and the Merkle root that you verified. For a historical anchor, confirm that the memo contains the expectedreceipt_idandmerkle_root. - For a
solana_sas_v1anchor, openanchor.sas.attestationin the explorer and compare the statement digest, the snapshot identifier, and the Merkle root held in that account.
The evidence bundle and nukez-verify
GET /v1/storage/evidence-bundle returns everything an independent verifier needs in one document: the file list, the commitment, the signed envelope, the anchor, the separate verification facts, and the gateway's signing keys. It needs no authentication. Save the bundle and check it whenever you like with the nukez-verify command. The command reports each fact separately as passed, failed, not_checked, or unavailable, and it never folds them into a single verified flag.
curl -sS "https://api.nukez.xyz/v1/storage/evidence-bundle?receipt_id=$RECEIPT_ID" > bundle.json # Offline checks: the Merkle root, the statement digest, and the signature. # Pin the Nukez signing key yourself. GET /v1/keys lists the current keys. nukez-verify bundle.json --pin <ed25519 public key hex> # Add your own copies of the files and a Solana RPC endpoint that you choose. nukez-verify bundle.json --pin <ed25519 public key hex> --files ./my-files --rpc <your rpc url>
A key list fetched from the same gateway that produced the bundle proves self-consistency only, and the command says so when you give it no pinned key. The on-chain checks are online by nature, because a saved transaction alone does not prove finality.
§ tamper evidence
Every attack vector. Detected.
Every modification produces a different merkle root. There is no way to modify content and preserve the root — the math does not allow it.
| Attack | What changes | Detection |
|---|---|---|
| Modify a file's content | Content hash changes → leaf changes → root changes | Root mismatch vs. on-chain attestation |
| Delete a file | Leaf count drops → tree structure changes | Root mismatch |
| Add a file | Leaf count increases → new root | Root mismatch |
| Swap a file (same name, different content) | Content hash changes → leaf changes | Root mismatch |
| Reorder files | Leaves are sorted by filename — order is canonical | Not possible by construction |
| Forge the manifest | Merkle root changes; receipt signature covers the root | Root mismatch or signature verification fails |
§ extending the chain
Verification further down.
No shared keys. No shared accounts.
Agent A stores data and gets a receipt_id. Agent A passes it to Agent B. Agent B verifies locally using only the receipt ID: it fetches the verification bundle, downloads the bytes through the receipt-scoped endpoints, and recomputes the hashes and the root. Agent B now has independent cryptographic proof of what Agent A stored, when, and that it hasn't been tampered with. The server-side recompute-verify audit is separate — it requires an envelope signed by the payer keypair or an authorized operator, so it is a tool for the locker owner rather than for an arbitrary receipt holder.
Verifying the verifier.
The gateway hashes its own running source against an on-chain attestation. If the gateway's code has been modified after deployment, the verification fails.
GET /v1/self-verify
Supported signatures
Both algorithms produce the same receipt structure, the same verification path, and the same attestation chain. Neither is primary — both are first-class.
| Algorithm | Chain | Key format | Use case |
|---|---|---|---|
| Ed25519 | Solana | Base58 public key | Solana-native wallets and agents |
| secp256k1 | EVM / Monad | Ethereum address (0x…) | EVM wallets, Monad chain payments |
§ cheat sheet
Five questions. Five answers.
| Question | How to check | What it proves |
|---|---|---|
| Is my downloaded file authentic? | SHA-256 locally; compare to manifest content_hash | File is byte-identical to the stored version |
| Is the manifest intact? | Rebuild merkle tree from file entries; compare to merkle_root | No files added, removed, or modified since attestation |
| Was the receipt forged? | Ed25519 verification against receipt_signer_pubkey (hex, 64 chars) — pinned out of band | Receipt was produced by the holder of the pinned signer key, unaltered |
| Is the attestation on-chain? | Look up tx_signature on Solana Explorer; read SPL Memo | Merkle root was committed to Solana at a specific slot |
| Can I verify without trusting Nukez? | Yes, with one caveat: pin receipt_signer_pubkey out of band. The hash and merkle math use public data and standard cryptography; the on-chain root cross-check catches a forged bundle even without the pin | The entire point — provided the signer key is pinned |
§ next
Verify a real receipt.
Paste a receipt ID into the public verifier to inspect the proof checks in one place — or jump to the integration docs to wire verification into your stack.
