Client-Side Verification

Every verification check runs in the viewer's browser. The server provides an initial content lookup but is not involved in any trust-bearing verification step.

What Happens When You Open a Link

When someone shares a RootLens link (e.g., rootlens.io/p/abc123), the abc123 part is a short ID that identifies the published content. Here is what the browser does, step by step:

  1. Short ID → content_hash
    The server resolves the short ID to a content_hash. This is a convenience lookup, not a trust-bearing step.
  2. content_hash → cNFT candidates
    The indexer cache returns candidate cNFT IDs. These are NOT trusted — every candidate is re-verified on-chain.
  3. cNFT ID → on-chain verification
    For each candidate, the browser queries the Solana blockchain directly. It checks collection membership, content_hash match, and fetches signed_json from the cNFT's URI.
  4. signed_json → browser verification
    TEE signature check, collection membership, content binding, processor-specific checks, and perceptual hash recomputation.

Steps 3 and 4 do not contact the RootLens server

Common Checks (All cNFTs)

Every cNFT — Core and Extension alike — goes through the same three common checks:

1. Collection

The cNFT's collection address must match the official collection defined in the on-chain GlobalConfig. The browser fetches GlobalConfig directly from Solana RPC — never from the RootLens server, never hardcoded.

2. TEE Signature

The signed_json's { payload, attributes } is canonicalized using JCS, prepended with a domain tag (title-protocol-v1), and verified against the tee_pubkey using the TEE's signature algorithm (currently Ed25519).

This is the most critical check. The TEE signature is what makes the proof unforgeable — the private key exists only inside TEE hardware and cannot be extracted by anyone. The browser re-verifies this signature entirely via the Web Crypto API, with no external service involved.

3. Content Binding

payload.content_hash must equal the content_hash used to find the cNFT. This prevents a signed_json from one content being reused for another.

Core-Specific Checks

4. Provenance

The C2PA provenance graph must contain at least one node. An empty graph means no provenance information was recorded.

5. Originality

The browser queries all Core cNFTs with the same content_hash and checks whether this one is the earliest. If someone else registers the same content later, the original registration takes precedence.

Extension-Specific Checks

6. WASM Trusted

The wasm_hash in the payload must be registered in the on-chain GlobalConfig under trusted_wasm_modules. This ensures the TEE ran a known, unmodified verification binary.

7. Cert Verified (cert-* only)

The payload's verified field must be true. This indicates that the TEE's WASM successfully verified the C2PA certificate chain against the appropriate Root CA public key.

8. PDQ Match (image-pdq only)

The browser recomputes the PDQ hash from the displayed image using a pure TypeScript implementation. The Hamming distance to the on-chain hash must be ≤ 31 out of 256 bits (Meta ThreatExchange's recommended similarity threshold).

This proves the image you are looking at is the same image that was verified by the TEE — independent of any server.

9. vPDQ Match (video-vpdq only)

The browser extracts keyframes via WebCodecs + mp4box.js, computes PDQ for each, and compares against on-chain hashes. The current client implementation requires ≥ 80% of keyframes to match within the threshold (a client-side heuristic, not a protocol-defined parameter).

Overall Verdict

The overall result is verified only if every check across all processors passes. A single failure in any check sets the overall result to failed.

Attack Scenarios

AttackDefense
Inject fake asset_id into indexerDAS collection + content_hash check rejects it
Tamper with signed_json off-chainTEE signature verification detects it
Swap the displayed image for a different onePDQ recomputation fails (Hamming distance exceeds threshold)
Inject fake GlobalConfig collection addressGlobalConfig is fetched from Solana RPC, not from any server
Use a backdoored WASM binary for verificationwasm_hash not registered in on-chain GlobalConfig → fails WASM Trusted check
Compromise the RootLens serverServer does not participate in trust-bearing verification steps

Developer Console

When viewing a content page, open the browser's developer console to see the full verification log. Every check is logged with pass/fail status, data sources, Solana addresses, and a summary line confirming that the RootLens server was not consulted.