cNFT Structure
All cNFTs share the same base structure. The processor type determines their role.
1 Content = 1 Core + N Extensions
All cNFTs share the same fundamental structure. The only difference is their protocol role (Core or Extension) and which processor ran inside the TEE. Each content item produces one Core cNFT and multiple Extension cNFTs.
- content_hash
- core-c2pacore-c2pa / Provenance graph
- cert-*Certificate chain verification result
- image-pdq256-bit PDQ hash
- video-vpdqPer-frame PDQ hashes
What Goes On-Chain
The cNFT metadata on Solana follows the Metaplex Bubblegum standard. Only minimal identification is stored on-chain — the actual verification data lives in the off-chain signed_json.
| Field | Description |
|---|---|
name | "Title #" + first 8 chars of content_hash |
symbol | TITLE |
uri | URI pointing to the signed_json (off-chain storage) |
collection | Core or Extension collection (defined in GlobalConfig) |
creators | Content owner's wallet address |
All data needed for verification (content_hash, provenance graph, WASM output, etc.) is in the signed_json at the URI, protected by the TEE signature.
Core cNFT (core-c2pa)
The Core cNFT records the C2PA provenance graph — the chain of signatures showing who created the content and how it was modified.
signed_json Payload (Off-Chain)
| Field | Type | Description |
|---|---|---|
content_hash | string | SHA-256 of the active manifest signature |
content_type | string | MIME type (e.g., image/jpeg) |
creator_wallet | string | Content owner's Solana wallet address |
nodes[] | array | Provenance graph nodes. Each has {id, type} where type is "final" or "ingredient" |
links[] | array | Relationships between nodes. Each has {source, target, role} (e.g., "audio", "image") |
tsa_timestamp | number? | RFC 3161 timestamp (Unix seconds), only if C2PA TSA exists |
tsa_pubkey_hash | string? | SHA-256 hash of TSA certificate (only if TSA exists) |
tsa_token_data | string? | Base64-encoded RFC 3161 token (only if TSA exists) |
Browser Verification Checks
- Collection — Belongs to GlobalConfig's core_collection_mint
- TEE Signature — Hardware-isolated key signature (currently Ed25519) over JCS-canonicalized payload
- Content Binding — payload.content_hash matches the query
- Provenance — At least one node exists in the C2PA graph
- Originality — Earliest Core cNFT for this content_hash
Extension cNFT (Shared Structure)
All Extension cNFTs share the same base structure, with WASM execution results flattened into the payload.
signed_json Payload (Off-Chain)
| Field | Type | Description |
|---|---|---|
content_hash | string | Same content_hash as the Core cNFT |
content_type | string | MIME type |
creator_wallet | string | Content owner's wallet address |
extension_id | string | Extension ID (e.g., image-pdq, cert-rootlens) |
wasm_source | string | URI to the WASM binary |
wasm_hash | string | SHA-256 of the WASM binary (verifiable against GlobalConfig) |
extension_input_hash | string? | SHA-256 of extension inputs (only if WASM uses auxiliary inputs) |
| WASM output fields | WASM execution results are flattened into the payload. The output fields are deterministic per processor and documented in each cNFT section below. |
Extension Verification Checks (Shared)
- Collection — Belongs to GlobalConfig's ext_collection_mint
- TEE Signature — Hardware-isolated key signature (currently Ed25519) over JCS-canonicalized payload
- Content Binding — payload.content_hash matches the query
- WASM Trusted — wasm_hash is registered in GlobalConfig
cert-* cNFT
Records the result of verifying the C2PA certificate chain against the corresponding manufacturer's Root CA. The extension_id indicates which Root CA was used:
cert-google— Verified against Google's Root CA public keycert-sony— Verified against Sony's Root CA public keycert-leica— Verified against Leica's Root CA public keycert-rootlens— Verified against RootLens's Root CA public key
The WASM output contains: verified (boolean result), certs_der (array of Base64-encoded DER certificates), root_ca (name of the Root CA used), and root_spki (hex-encoded Root CA public key info).
image-pdq cNFT
A 256-bit perceptual hash computed using the PDQ algorithm (Meta ThreatExchange compatible). A visual fingerprint that survives re-encoding and minor resizing.
WASM output: pdqhash (64-char hex), quality (integer 0-100, gradient energy metric), algorithm ("pdq"), bits (256).
PDQ Algorithm
PDQ is a 256-bit perceptual hash algorithm developed by Meta (formerly Facebook) for the ThreatExchange project. The algorithm works as follows: the input image is converted to grayscale using BT.601 coefficients, smoothed with a Jarosz filter (box×2 = tent), and downsampled to 64×64. A 2D DCT (Discrete Cosine Transform) is applied to extract the top-left 16×16 low-frequency coefficients (256 values, excluding DC). The Torben algorithm computes the median, and each coefficient is quantized to 1 if above the median, 0 otherwise, producing a 256-bit hash. Re-encoding and minor resizing produce similar hashes; substantially different images produce substantially different ones.
Verification (Browser)
The browser recomputes the PDQ hash from the displayed image using a pure TypeScript implementation, then compares using Hamming distance (the number of bit positions where the two hashes differ). The threshold of 31 (out of 256 bits) follows Meta ThreatExchange's recommended similarity threshold.
video-vpdq cNFT
Computes PDQ hashes per keyframe (I-frame). Two filters apply: only frames with quality ≥ 50 (gradient energy metric) are included, and near-duplicate frames (Hamming distance ≤ 10 from the previous frame) are pruned.
WASM output: frames array (each with pdqhash, quality, keyframe index), frame_count (number of output frames), algorithm ("vpdq-keyframe").
Verification (Browser)
The browser extracts keyframes via WebCodecs + mp4box.js, recomputes PDQ, and matches. The current client implementation requires ≥ 80% of on-chain keyframes to match within the Hamming distance threshold (a client-side heuristic, not a protocol-defined parameter).