Client-Side Verification for On-Chain Frontends
We spent a decade hardening smart contracts. The web pages that drive them are still served on trust. Here's how EthStorage and Colibri close that gap — in the browser, without running a full node.
TL;DR
- The problem. The frontend — the HTML and JavaScript that builds the transaction you sign — usually lives on a centralized server, where it can be swapped out without anyone noticing. The $1.5B Bybit hack was exactly this: flawless contracts, a tampered web page.
- The idea. web3:// serves a frontend straight from on-chain smart contracts — no central host to compromise.
- Two gaps remain. Storing a whole site on Ethereum L1 is far too expensive; and because browsers don't speak
web3://yet, users reach these sites through a gateway — a trusted server again, with no way to check what it returned. - EthStorage closes the cost gap: the bytes live in an L2 network that inherits L1 security, with only the blob's versioned hash committed on Ethereum — at a small fraction of L1 cost.
- This post closes the verification gap: a prototype built around Colibri — a stateless client whose proofs are generated by a server and verified on the client — that independently checks a gateway's bytes against the on-chain commitment, fast enough (~0.45× the download time) for a "Verifying → Verified" badge in a browser or wallet, taking a few seconds before users signing a sensitive transaction.
- Code: github.com/ethstorage/web3-url-verifier
Smart contract security is a mature discipline. We audit, we fuzz, we formally verify, and then we make the code immutable. But users don't touch contracts. They touch a website — the HTML and JavaScript that reads the chain, shows a balance, builds a transaction, and asks them to sign it. That website is almost always served the old-fashioned way: from an S3 bucket, a CDN, a server someone controls. It is the least-verified, most-trusted component in the entire stack, and it sits directly between the user and their keys.
In February 2025, more than $1.5 billion was drained from Bybit. No smart contract was broken. Attackers had modified the Safe{Wallet} frontend JavaScript — served from an AWS S3 bucket — so that when Bybit's signers reviewed what looked like a routine transfer, the transaction they actually approved handed control of the wallet to the attackers. The contracts were flawless. The web page was the exploit.
This is the part of the stack we keep leaving unguarded. The fix is to stop serving the frontend from a centralized untrusted server at all.
Putting the frontend on-chain
The cleanest way to do that is to serve the frontend from the chain itself. web3:// (ERC-4804 / ERC-6860) is a URL scheme that addresses on-chain resources the way https:// addresses servers: the page and its assets are read straight from smart contracts, with no central host to quietly compromise — and anyone can check what they received against what the chain holds.
Two things stand between that idea and real users. The first is cost: Ethereum L1 storage is far too expensive to hold an entire website. The second is verification: browsers don't speak web3:// natively yet, so in practice users reach these sites through a gateway that translates web3:// to https:// and hands back the bytes. That puts an untrusted server right back in the path — if the gateway lies, injects, or is compromised, the user is back in Bybit territory, except now they believe they're on a trustless site.
EthStorage closes the cost gap. It's a Layer 2 storage network that inherits Ethereum's security while storing data at a small fraction of L1 prices. The file bytes — your HTML, JS, CSS, and images — live in the EthStorage network, and Ethereum keeps only a compact commitment to each blob: its versioned hash. Putting a full frontend on-chain becomes affordable.
This post is about the second gap — verification — and the question is narrow and practical: can a user — or their browser, or their wallet — independently verify that the bytes a gateway served are exactly the bytes committed on-chain, without trusting the gateway and without running a full node?
The answer is yes. Getting there rests on one piece of standard Ethereum infrastructure — the light client — so we'll start there, then show what we built.
A 60-second primer on light clients
When you ask an RPC node for something — an account balance, a contract call result — it just hands you back the answer, with no way to tell whether it's the real one. You're simply trusting the node to have told you the truth. A light client changes that. Without running a full node yourself, you run a lightweight client that takes the answer an untrusted RPC gave you, fetches a cryptographic proof for it, and checks that proof against the chain — so you end up trusting math, not the node. The proof rests on two facts baked into Ethereum's design:
- State is committed. Every block header contains a state root — the root of a Merkle-Patricia trie (MPT) over all accounts and storage. Given the state root, a short Merkle proof shows that a specific value really is in the state. Change the value or the proof by even a single byte, and it no longer hashes to that state root.
- Headers are signed. On the beacon chain, a randomly chosen sync committee of 512 validators BLS-signs the headers it sees. If more than two-thirds sign a header, it's overwhelmingly likely to be canonical — and thanks to BLS aggregation, you check that with a single signature verification. The committee rotates roughly every 27 hours, so a client only has to track that small, slowly-changing piece of state.
Put them together, and a verifiable answer arrives as one bundle: the value, a Merkle proof up to the state root, the block header, and a sync-committee signature over that header. A client holding the current sync committee can check that whole bundle with a couple of cryptographic operations — no full node, no chain sync. Tamper with any part and verification fails.
Colibri: a stateless client with a server-side prover
Verifying an answer this way is the standard light-client recipe, and it leaves one practical question: assembling that bundle is real work. Gathering the block header, the sync-committee signature, and a Merkle proof for every piece of state a page touches — then verifying all of it — is a lot to do inside a browser. Colibri, the stateless Ethereum client we built the prototype around (from corpus-core, developed with Ethereum Foundation support), answers this by splitting the light client into a prover and a verifier:

A server — the prover — does the expensive part: collecting the header, the BLS signature, and all the Merkle proofs, and assembling them into one compact, self-contained proof. The client runs only the verifier. It is almost stateless, holding nothing but the current sync committee, and it checks the bundle locally in WebAssembly. One request out, one proof in.
The key point: offloading proof generation to a server costs you nothing in trust. The server can't forge a BLS signature or a Merkle proof. A dishonest prover produces a bundle that simply fails to verify. You move the work, not the trust. And because Colibri is written in C and compiles to a small WASM module (with bindings for JS, Swift, Kotlin, and more), the verifier drops cleanly into a browser extension, a wallet, or a phone — exactly where we need it.
What we built
Our prototype does what a verifying browser extension would do. It fetches a web3:// site through a public gateway exactly as a normal browser would — discovering and downloading the page and all its sub-resources — and then independently verifies every byte against Ethereum.
The design has one organizing principle: separate the data path from the verification path.

The data path can be anything — gateway, RPC, CDN, cache — and is assumed hostile. The verification path uses Colibri to obtain cryptographically verified on-chain state. The two meet only at a local comparison on the client. The user never has to figure out who cheated; they only need to know whether the final bytes match the chain.
A web3:// site contains two kinds of resources, so we verify two ways.
Contract-call results (Ethereum state). Some URLs resolve to an on-chain method call — an NFT rendered entirely on-chain, say, via render(78, 0). We send the same eth_call through Colibri's prover, get back a verified result, and compare it byte-for-byte to what the gateway served. Same call, two paths, one comparison.
EthStorage files (HTML, CSS, JS, images). This is the more interesting case. The file bytes live in the EthStorage network; on Ethereum, the EthStorage contract records only the versioned hash of each blob — the same KZG-commitment-derived hash that EIP-4844 uses for blobs:

So we verify a file in three steps:

We ask Colibri for the on-chain versioned hash — a verified eth_call that reads it from the EthStorage contract — then take the bytes the gateway served, re-run the exact same blob encoding and KZG commitment locally, derive the versioned hash ourselves, and compare. Because the versioned hash is a binding commitment, changing a single byte — or injecting a single <script> tag — changes the hash and the check fails.
That's the whole trick: we never ask the gateway to be honest. We only ask whether what it gave us hashes to what the chain committed.
Performance
The question that decides whether any of this is real: is verification fast enough to sit in front of a user? We ran six real, deployed targets — the EthStorage website, the Safe wallet frontend, and Vitalik's blog — its homepage plus two image-heavy posts, one from 2022 and one from 2024 — plus an on-chain-rendered NFT.

For ordinary static sites — HTML, JS, CSS, images served from EthStorage — verification is consistently cheaper than download. Across the five file-based sites, it averaged about 0.45× the download time, and it runs concurrently with everything else: checking a page costs roughly half of what it costs to fetch it.
The on-chain NFT is the one outlier, and it's worth understanding why. Its output isn't a stored file — it's assembled at call time from state scattered across several L1 contracts, so verifying it means proving far more on-chain state than there are bytes to download (hence 2.9×, though still only 2.9 seconds). The EthStorage files are fast for the opposite reason: their bytes live in the L2 storage network, and Ethereum records each file as just its blob hashes — one 32-byte hash per 128 KB chunk. The on-chain proof only has to cover that short list of hashes, never the file's contents.
One honest caveat: the public Colibri prover caches aggressively, so these are warm-cache figures averaged over several runs; cold-cache latency will be somewhat higher.
This is a UX feature, not a blocker
Verification costs a few seconds — but those seconds never sit between the user and the page. It doesn't have to block rendering: the page loads at normal gateway speed, and a small indicator resolves a moment later, before the user does anything sensitive like signing a transaction.

It's the trustless analog of the TLS padlock: a quiet signal that what you're looking at is what was actually committed on-chain.
And we have proof the signal works, because it caught something live. The gateway we tested through injects a small <script> into some HTML pages to rewrite web3:// links into https:// ones — benign in intent, but it changes the bytes. Our check flagged it every single time, and only on the affected HTML; the JavaScript, CSS, and images verified clean. That's the entire thesis in miniature: you don't need to know who changed the bytes, or why. You only need to know they no longer match the chain.
Where this goes
Today the prototype is a standalone program. The natural next forms are obvious:
- A browser extension that watches the active tab, verifies its resources in the background, and surfaces a Verifying / Verified / Tampered badge — the trustless padlock.
- Built into a wallet. Before you sign, the wallet already knows whether the dapp frontend in front of you matches its on-chain source. The Bybit failure mode becomes detectable at signing time, by the very tool holding your keys.
- Eventually, native
web3://support in browsers makes the gateway optional altogether — but the verifier is still what turns "the gateway says so" into "the chain says so."
We hardened the contracts a long time ago. EthStorage puts the frontend on-chain so it can be hardened too, and Colibri lets every user check it — from a browser, in a few seconds, without trusting anyone in between.
The prototype, including the performance harness and the poisoning experiments, is open source: github.com/ethstorage/web3-url-verifier.
Don't trust. Verify. The frontend included.
To learn more about EthStorage and connect with our community, visit our community channels:
- Website: https://ethstorage.io/
- Twitter: https://x.com/EthStorage
- Discord: https://discord.com/invite/xhCwaMp7ps
- Telegram: https://t.me/ethstorage