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:
- Short ID → content_hashThe server resolves the short ID to a content_hash. This is a convenience lookup, not a trust-bearing step.
- content_hash → cNFT candidatesThe indexer cache returns candidate cNFT IDs. These are NOT trusted — every candidate is re-verified on-chain.
- cNFT ID → on-chain verificationFor 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.
- signed_json → browser verificationTEE 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
| Attack | Defense |
|---|---|
| Inject fake asset_id into indexer | DAS collection + content_hash check rejects it |
| Tamper with signed_json off-chain | TEE signature verification detects it |
| Swap the displayed image for a different one | PDQ recomputation fails (Hamming distance exceeds threshold) |
| Inject fake GlobalConfig collection address | GlobalConfig is fetched from Solana RPC, not from any server |
| Use a backdoored WASM binary for verification | wasm_hash not registered in on-chain GlobalConfig → fails WASM Trusted check |
| Compromise the RootLens server | Server 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.