{
  "name": "nukez-evidence-v2",
  "schema_version": "2.0",
  "title": "Nukez Evidence V2: Signed Commitment Envelope, On-Chain Anchor, and Evidence Bundle",
  "status": "normative",
  "machine_readable_url": "https://docs.nukez.xyz/specs/nukez-evidence-v2.json",
  "depends_on": {
    "merkle_spec": "https://docs.nukez.xyz/specs/nukez-merkle-v1.json",
    "note": "The Merkle root that the envelope commits to is computed exactly as nukez-merkle-v1 defines it. This document does not change that algorithm."
  },
  "scope": {
    "summary": "This document describes the version-two evidence that the gateway issues for a content attestation: the signed commitment envelope, the compact statement written on Solana, the provider-neutral anchor object, the separate verification facts, the Solana Attestation Service schema, and the portable evidence bundle.",
    "additive": "Everything here is additive. No version-one field changed meaning, and no version-one field was removed. Records that were anchored in the Switchboard era keep their original fields and their original meaning.",
    "what_the_anchor_proves": "The anchor proves that the gateway's published key committed this exact snapshot on Solana at this slot. With the attestation service account, it also proves that the current commitment is readable on-chain at a predictable address.",
    "what_the_anchor_does_not_prove": "The anchor does not prove that a third party inspected the files. Do not describe a solana_memo_v2 or solana_sas_v1 anchor as oracle-verified, and never label it as Switchboard."
  },
  "identifiers": {
    "proof_id": {
      "format": "32 lowercase hex characters (16 random bytes)",
      "meaning": "The public identifier of the proof for one receipt. It is random and public. It is never the receipt id, because the receipt id doubles as a download handle.",
      "stability": "A receipt keeps the same proof_id across all of its snapshots."
    },
    "manifest_digest": {
      "format": "sha256:<64 lowercase hex>",
      "formula": "'sha256:' + SHA256(canonical_bytes(rows)).hexdigest()",
      "rows": "One object per file entry, sorted by filename: {\"filename\": string, \"size_bytes\": decimal string, \"content_hash\": 64 lowercase hex without the 'sha256:' prefix}."
    },
    "snapshot_id": {
      "format": "64 lowercase hex characters",
      "formula": "SHA256(canonical_bytes({\"domain\": \"nukez/snapshot/v2\", \"proof_id\": proof_id, \"merkle_root\": <64 hex without prefix>, \"manifest_digest\": <64 hex without prefix>, \"file_count\": integer})).hexdigest()",
      "meaning": "The deterministic identity of one immutable content snapshot. The same proof_id and the same content always give the same snapshot_id."
    },
    "issuer_key_id": {
      "format": "'ed25519:' followed by the first 16 hex characters of the signing public key",
      "unsigned_value": "unsigned"
    }
  },
  "canonicalization": {
    "name": "RFC 8785 (JSON Canonicalization Scheme), strict subset",
    "rules": [
      "Object keys must be strings. Members are emitted sorted by the UTF-16 code units of the key (RFC 8785 section 3.2.3), with no whitespace.",
      "Strings are emitted exactly as ECMAScript JSON.stringify emits them. Non-ASCII characters are written literally as UTF-8. Control characters use the short escapes (\\b \\t \\n \\f \\r) or lowercase \\u00xx. Lone surrogates are rejected.",
      "Integers are allowed only when their magnitude is at most 2^53 - 1. Larger values must be carried as decimal strings.",
      "Floats are rejected outright. Amounts that need precision are carried as decimal strings.",
      "true, false, null, and arrays are supported."
    ],
    "output": "canonical_bytes(value) is the UTF-8 encoding of the canonical text.",
    "version_one_note": "The version-one canonical JSON that produces result_hash and receipt hashes is a different serialization. It is unchanged, and it is not used by the envelope."
  },
  "envelope": {
    "shape": {
      "body": "object (see body_fields)",
      "statement_digest": "64 lowercase hex characters",
      "signature": "128 lowercase hex characters (a 64-byte Ed25519 signature), or null when unsigned",
      "issuer_pubkey": "64 lowercase hex characters (a 32-byte Ed25519 public key), or null when unsigned",
      "sig_alg": "ed25519 | none"
    },
    "body_fields": {
      "schema": "The constant string \"nukez/evidence/v2\".",
      "policy_version": "The constant string \"1\".",
      "proof_id": "See identifiers.proof_id.",
      "snapshot_id": "See identifiers.snapshot_id.",
      "sequence": "Integer. The number of this statement for the receipt. The first statement is 1, and each new statement adds one.",
      "prev_statement_digest": "String or null. The statement_digest of the previous envelope for the same receipt, or null for the first one.",
      "merkle_algorithm": "The constant string \"nukez-merkle-v1\".",
      "merkle_root": "sha256:<64 lowercase hex>. The Merkle root of the snapshot.",
      "file_count": "Integer. The number of file entries in the snapshot, which is the number of Merkle leaves.",
      "manifest_file_count": "Integer. The number of entries the manifest listed when the snapshot was taken. When it is greater than file_count, some listed files were missing from storage and were left out, and the envelope says so instead of hiding it.",
      "total_bytes": "Decimal string. The total size of the files in the snapshot.",
      "manifest_digest": "See identifiers.manifest_digest.",
      "check_mode": "recorded_hashes | bytes_hashed. recorded_hashes means the content hashes recorded at upload time were trusted. bytes_hashed means the bytes were downloaded and hashed in the run that produced the snapshot.",
      "issued_at": "UTC timestamp in the form YYYY-MM-DDTHH:MM:SSZ.",
      "issued_at_unix": "Integer. The same instant as issued_at, in Unix seconds.",
      "issuer_key_id": "See identifiers.issuer_key_id.",
      "nonce": "32 lowercase hex characters (16 random bytes)."
    },
    "statement_digest": {
      "formula": "SHA256(canonical_bytes(body))",
      "encoding": "64 lowercase hex characters, with no 'sha256:' prefix"
    },
    "signature": {
      "algorithm": "Ed25519",
      "signed_message": "The 25 ASCII bytes of the tag NUKEZ_CONTENT_ENVELOPE_V2, then one zero byte (0x00), then the 32 raw bytes of the statement digest. The message is 58 bytes long.",
      "domain_tag": "NUKEZ_CONTENT_ENVELOPE_V2",
      "signer": "The gateway statement-signing key. It is the same Ed25519 key that signs receipts and version-one attestations. GET /v1/keys lists it.",
      "encoding": "Lowercase hex. The signature is 128 characters, and the public key is 64 characters.",
      "unsigned_envelopes": "When sig_alg is \"none\", signature and issuer_pubkey are null. Verifiers MUST NOT treat such an envelope as signed. The production gateway refuses to issue an unsigned envelope.",
      "key_pinning": "A verifier should take the public key from a list that it pinned out of band. Verifying against envelope.issuer_pubkey, or against a key list fetched from the same gateway that produced the evidence, proves self-consistency only."
    },
    "verification_steps": [
      "1. Compute canonical_bytes(envelope.body) and hash it with SHA-256. The hex digest MUST equal envelope.statement_digest.",
      "2. Build the message: the ASCII bytes of NUKEZ_CONTENT_ENVELOPE_V2, one zero byte, and the 32 digest bytes.",
      "3. Verify envelope.signature over that message with a pinned Nukez Ed25519 public key.",
      "4. Confirm that envelope.body.merkle_root equals the Merkle root you recomputed with nukez-merkle-v1, and that envelope.body.file_count equals the number of leaves you used."
    ]
  },
  "onchain_payload": {
    "schema_tag": "nukez/anchor/v2",
    "carrier": "The data of one SPL Memo instruction (program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr). The memo instruction lists the transaction signer as a signer account, so the Memo program refuses to run unless that key signed.",
    "encoding": "canonical_bytes(payload), as defined under canonicalization.",
    "fields": {
      "s": "The constant string \"nukez/anchor/v2\".",
      "pid": "envelope.body.proof_id",
      "sid": "envelope.body.snapshot_id",
      "dg": "envelope.statement_digest",
      "root": "envelope.body.merkle_root without the 'sha256:' prefix",
      "n": "envelope.body.file_count",
      "seq": "envelope.body.sequence",
      "t": "envelope.body.issued_at_unix",
      "k": "envelope.body.issuer_key_id"
    },
    "privacy": "The payload never carries the receipt id, a filename, or any access handle.",
    "transaction_signer": "The gateway publication key, which is a Solana keypair. Proof responses report it as anchor.signer. The statement-signing key that signs the envelope is identified inside the payload by k, and the payload binds to the signed envelope through dg."
  },
  "anchor_object": {
    "presence": "Returned next to every proof. It is null when the record has no on-chain anchor.",
    "types": {
      "solana_memo_v2": "A Solana transaction that carries the compact statement in an SPL Memo, signed by the gateway publication key.",
      "solana_sas_v1": "The same memo transaction, which also writes a Solana Attestation Service account for the receipt.",
      "switchboard_v2": "A historical anchor from the Switchboard era. It keeps its original meaning."
    },
    "fields": {
      "type": "solana_sas_v1 | solana_memo_v2 | switchboard_v2",
      "chain": "CAIP-2 network identifier, for example solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
      "tx": "The Solana transaction signature, base58.",
      "slot": "Integer. The slot of the transaction.",
      "block_time": "Integer Unix seconds, or null.",
      "signer": "The gateway publication public key, base58.",
      "fee_lamports": "Integer. The cost of the transaction.",
      "commitment": "confirmed | finalized. The commitment level the gateway observed when it recorded the anchor.",
      "finalized": "Boolean. True when the gateway observed the transaction as finalized.",
      "statement_digest": "64 lowercase hex. Equal to envelope.statement_digest.",
      "snapshot_id": "64 lowercase hex. Equal to envelope.body.snapshot_id.",
      "payload": "The object written on-chain. See onchain_payload.",
      "envelope_version": "2 for solana_memo_v2 and solana_sas_v1. 1 for switchboard_v2.",
      "sas": "Present only for solana_sas_v1. See anchor_object.sas_fields."
    },
    "sas_fields": {
      "program": "The Solana Attestation Service program id.",
      "credential": "The credential account address, base58.",
      "schema": "The schema account address, base58.",
      "nonce": "The nonce used to derive the attestation address, base58.",
      "attestation": "The attestation account address, base58.",
      "mode": "latest | per_snapshot",
      "replaced_previous": "Boolean. True when the transaction closed an earlier attestation account for the receipt before it created this one."
    },
    "historical_shape": {
      "type": "switchboard_v2",
      "chain": "CAIP-2 network identifier",
      "tx": "The Switchboard-era transaction signature.",
      "slot": "Integer.",
      "account": "The Switchboard feed public key.",
      "envelope_version": 1
    },
    "reader_rules": [
      "The legacy fields switchboard_slot, switchboard_tx, and switchboard_feed are populated only for historical Switchboard anchors.",
      "A reader MUST take the transaction from anchor.tx first, and fall back to switchboard_tx only when the anchor object is absent.",
      "A reader MUST NOT label a solana_sas_v1 or solana_memo_v2 anchor as Switchboard."
    ],
    "appears_on": [
      "GET and POST /v1/storage/verify",
      "GET /v1/retrieve",
      "GET /v1/storage/merkle-proof",
      "GET /v1/storage/recompute-verify",
      "GET /v1/attest-code",
      "GET /v1/self-verify",
      "GET /v1/storage/evidence-bundle",
      "The stored attestation object of version-two records",
      "push_result.anchor and push_result.anchor_type on POST /v1/storage/attest and POST /v1/attest/push",
      "The owner portal listing reports on_chain (boolean) and anchor_type instead of the full object."
    ]
  },
  "verification_facts": {
    "values": [
      "passed",
      "failed",
      "not_checked",
      "unavailable"
    ],
    "principle": "Each fact answers one question. The facts are never folded into a single verified flag. Producing them does not contact a chain and does not read file bytes again, so a fact that would need either is reported as not_checked instead of being implied.",
    "facts": {
      "commitment_computed": "passed when the record has a Merkle root. failed when a record exists without one, which includes a pending record. not_checked when there is no record.",
      "signature_valid": "For a version-two record, the envelope digest and signature were checked against the gateway's own signing key, and the result is passed or failed. For a version-one record, manifest_signature was checked over the bare Merkle root, and the result is passed, or unavailable when the check could not be completed. not_checked when the record is unsigned or has no Merkle root.",
      "anchor_submitted": "passed when an anchor transaction is recorded or the record is in the pushing state.",
      "anchor_confirmed": "passed when an anchor transaction is recorded and the record status is complete, or the record is a legacy record with no status. failed when the anchor object records an error. Otherwise not_checked. Readers that want to say \"anchored on-chain\" MUST use this fact.",
      "anchor_finalized": "passed when the recorded anchor was observed as finalized. Otherwise not_checked.",
      "payment_verified": "Always not_checked in this object. Payment is verified by the receipt endpoints.",
      "bytes_checked": "passed when check_mode is bytes_hashed. Otherwise not_checked.",
      "witness": "Always not_checked. No third-party witness takes part."
    },
    "legacy_verified_flag": "The boolean verified on /v1/storage/verify is unchanged. It means that an attestation record exists and is not pending. It does not say that the record is anchored or that any bytes were read again.",
    "appears_on": [
      "GET and POST /v1/storage/verify",
      "GET /v1/retrieve",
      "GET /v1/storage/evidence-bundle"
    ]
  },
  "stored_attestation_fields_v2": {
    "proof_id": "See identifiers.proof_id.",
    "snapshot_id": "See identifiers.snapshot_id.",
    "evidence_seq": "Integer. Equal to envelope.body.sequence.",
    "envelope": "The signed envelope. See envelope.",
    "anchor": "The anchor object. Present only when that transaction published the record's current snapshot.",
    "check_mode": "recorded_hashes | bytes_hashed",
    "manifest_file_count": "Integer.",
    "files_skipped": "Integer. manifest_file_count minus the number of files in the snapshot, never below zero.",
    "last_anchor_error": "Present after a failed anchor attempt: {error, retryable, ambiguous, tx_signature, at}. It is removed when a later anchor succeeds."
  },
  "solana_attestation_service": {
    "program": "22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG",
    "credential_name": "Nukez",
    "schema_name": "nukez.content.v2",
    "schema_version": 1,
    "schema_description": "Nukez content commitment: public proof id, immutable snapshot id, statement digest of the signed evidence envelope, and Merkle root.",
    "schema_fields": [
      {
        "name": "proof_id",
        "type": "bytes",
        "sas_type_code": 13,
        "length": "16 bytes"
      },
      {
        "name": "snapshot_id",
        "type": "bytes",
        "sas_type_code": 13,
        "length": "32 bytes"
      },
      {
        "name": "statement_digest",
        "type": "bytes",
        "sas_type_code": 13,
        "length": "32 bytes"
      },
      {
        "name": "merkle_root",
        "type": "bytes",
        "sas_type_code": 13,
        "length": "32 bytes"
      },
      {
        "name": "file_count",
        "type": "u32",
        "sas_type_code": 2
      },
      {
        "name": "sequence",
        "type": "u32",
        "sas_type_code": 2
      },
      {
        "name": "issued_at",
        "type": "i64",
        "sas_type_code": 8
      },
      {
        "name": "issuer_key_id",
        "type": "string",
        "sas_type_code": 12
      }
    ],
    "schema_layout_bytes": [
      13,
      13,
      13,
      13,
      2,
      2,
      8,
      12
    ],
    "data_encoding": {
      "bytes_and_string": "A little-endian u32 length, followed by the bytes. Strings are UTF-8.",
      "u32": "4 bytes, little-endian.",
      "i64": "8 bytes, little-endian, signed.",
      "field_order": "The fields are concatenated in the order of schema_fields.",
      "sources": {
        "proof_id": "bytes.fromhex(envelope.body.proof_id)",
        "snapshot_id": "bytes.fromhex(envelope.body.snapshot_id)",
        "statement_digest": "bytes.fromhex(envelope.statement_digest)",
        "merkle_root": "bytes.fromhex(envelope.body.merkle_root without the 'sha256:' prefix)",
        "file_count": "envelope.body.file_count",
        "sequence": "envelope.body.sequence",
        "issued_at": "envelope.body.issued_at_unix",
        "issuer_key_id": "envelope.body.issuer_key_id"
      }
    },
    "address_derivations": {
      "note": "Every address is a program-derived address under the program above. Seeds are listed in order.",
      "credential": [
        "utf8('credential')",
        "authority public key (32 bytes)",
        "utf8(credential_name)"
      ],
      "schema": [
        "utf8('schema')",
        "credential address (32 bytes)",
        "utf8(schema_name)",
        "one byte holding schema_version (0x01)"
      ],
      "attestation": [
        "utf8('attestation')",
        "credential address (32 bytes)",
        "schema address (32 bytes)",
        "nonce (32 bytes)"
      ],
      "event_authority": [
        "utf8('__event_authority')"
      ],
      "nonce": {
        "formula": "SHA256(utf8(material)), used as a 32-byte public key",
        "material_latest_mode": "'nukez/sas-nonce/v1|' + proof_id",
        "material_per_snapshot_mode": "'nukez/sas-nonce/v1|' + proof_id + '|' + snapshot_id"
      }
    },
    "account_modes": {
      "latest": "The default. The receipt has exactly one attestation account, which holds the current commitment. The program has create and close for attestations and no update, so the gateway closes the account and creates it again for each new snapshot. The complete history lives in the memo transactions, which are permanent.",
      "per_snapshot": "Each snapshot keeps its own attestation account. When the account for a snapshot already exists, the memo alone anchors it again."
    },
    "create_attestation_instruction": {
      "discriminator": 6,
      "data": "One discriminator byte, then the 32-byte nonce, then the attestation data as a length-prefixed byte vector, then the expiry as a little-endian i64.",
      "expiry": "0 means no expiry. When the gateway sets a lifetime, the expiry is issued_at_unix plus that lifetime. An expiry bounds how long the statement should be relied on. It is not evidence that the bytes were checked again at that time."
    },
    "close_attestation_instruction": {
      "discriminator": 7
    },
    "attestation_account_layout": [
      "1 byte: the account discriminator, which is 2 for an attestation",
      "32 bytes: nonce",
      "32 bytes: credential address",
      "32 bytes: schema address",
      "4 bytes: little-endian u32 length of the data",
      "data: the attestation data, encoded as described under data_encoding",
      "32 bytes: signer public key",
      "8 bytes: little-endian i64 expiry",
      "The reference decoder reads no fields after the expiry."
    ],
    "reader_check": "Read the account at anchor.sas.attestation. Its owner MUST be the program above. Decode the data and compare proof_id, snapshot_id, statement_digest, and merkle_root with the envelope. In latest mode, an account that holds the same proof_id and a different snapshot means that a newer snapshot replaced it, which is expected. The memo transaction remains the permanent record."
  },
  "evidence_bundle": {
    "endpoint": "GET /v1/storage/evidence-bundle?receipt_id={receipt_id}",
    "auth": "None. The endpoint is open in the same way as /v1/storage/verify, and it is receipt-scoped: the caller must hold the receipt_id.",
    "cache_control": "no-store",
    "errors": {
      "400": "missing_receipt_id",
      "404": "receipt_not_found",
      "409": "attestation_not_available. No computed attestation exists for this receipt yet."
    },
    "fields": {
      "schema": "The constant string \"nukez/evidence-bundle/v2\".",
      "bundle_version": "2 when the record carries a signed envelope. 1 for a record from the Switchboard era, which is exported with its original signature and its original meaning.",
      "receipt_id": "The receipt the bundle was requested for.",
      "proof_id": "See identifiers.proof_id. Null for version-one records.",
      "network": "CAIP-2 network identifier.",
      "algorithms": "{merkle, envelope}. merkle restates nukez-merkle-v1. envelope restates the canonicalization, statement digest, and signature rules of this document, and it is null for bundle_version 1.",
      "commitment": "{merkle_root, file_count, manifest_file_count, total_bytes, check_mode, attested_at}",
      "files": "A list of {filename, size_bytes (integer), content_hash (64 hex without the 'sha256:' prefix)}.",
      "envelope": "The signed envelope, or null for bundle_version 1.",
      "v1_signature": "Null for bundle_version 2. Otherwise {manifest_signature, signed_message, result_hash}, where the signed message is the UTF-8 encoding of the Merkle root hex without the 'sha256:' prefix.",
      "anchor": "The anchor object, or null.",
      "verification": "The verification facts object.",
      "signing_keys": "The same key entries that GET /v1/keys returns.",
      "how_to_verify": "A list of five steps in plain language."
    },
    "how_to_verify": [
      "1. Recompute the Merkle root from files with nukez-merkle-v1. It must equal commitment.merkle_root.",
      "2. Hash your own copy of each file with SHA-256 and compare it with files[].content_hash.",
      "3. For bundle_version 2, canonicalize envelope.body, hash it with SHA-256, compare the result with envelope.statement_digest, and verify envelope.signature against a pinned Nukez key. For bundle_version 1, verify v1_signature against the same key.",
      "4. Fetch anchor.tx from a Solana RPC endpoint that you choose. The transaction must have succeeded, it must be signed by anchor.signer, and its memo must carry the same statement digest and Merkle root. This step is online by nature. A saved transaction alone does not prove finality.",
      "5. When anchor.sas.attestation is present, read that account and compare its statement digest, snapshot id, and Merkle root with the envelope."
    ]
  },
  "signing_keys": {
    "endpoints": [
      "GET /v1/keys",
      "GET /.well-known/nukez-signing-keys.json"
    ],
    "auth": "None.",
    "cache_control": "public, max-age=300",
    "shape": {
      "schema": "The constant string \"nukez/signing-keys/v1\".",
      "keys": "A list of {key_id, alg, public_key_hex, use, status}.",
      "note": "A plain-language reminder to pin the keys out of band."
    },
    "key_fields": {
      "key_id": "See identifiers.issuer_key_id.",
      "alg": "ed25519",
      "public_key_hex": "64 lowercase hex characters.",
      "use": [
        "receipt",
        "attestation",
        "evidence-envelope"
      ],
      "status": "active"
    },
    "pinning": "Pin these keys out of band. A key list served by the gateway that also produced the evidence proves self-consistency only."
  },
  "reference_verifier": {
    "command": "nukez-verify bundle.json [--pin <ed25519 public key hex>]... [--rpc URL] [--files DIR]",
    "exit_status": "0 only when no fact failed.",
    "offline_facts": {
      "merkle_root_matches": "The file list in the bundle hashes to the committed root.",
      "files_match": "Your local copies hash to the listed content hashes.",
      "statement_digest_matches": "The envelope body hashes to its statement digest.",
      "envelope_binds_commitment": "The envelope's Merkle root and file count equal the bundle's commitment and file list.",
      "signature_valid": "The envelope signature, or the version-one root signature, verifies.",
      "key_pinned": "The key that was used is one that you supplied, not one that the bundle supplied."
    },
    "online_facts": {
      "anchor_tx_succeeded": "The anchor transaction exists and did not fail.",
      "anchor_signer_matches": "The transaction was signed by the key that the bundle names.",
      "anchor_payload_matches": "The memo carries this statement digest and this Merkle root.",
      "anchor_slot_matches": "The recorded slot is the transaction's own slot.",
      "anchor_finalized": "The cluster reports the transaction as finalized.",
      "registry_matches": "The Solana Attestation Service account holds the same statement."
    },
    "online_note": "The online facts need a Solana RPC endpoint that you choose, and they trust that endpoint. A saved transaction is not an offline proof of finality, so the online facts are reported as not_checked when no endpoint is given."
  }
}
