EVMFS.app
How it works
- Build your site — Create or import files in the workspace below. Any static site works: HTML, CSS, JS, images, fonts.
- Upload to EVMFS — Files are zipped client-side (DEFLATE), split into 20 KB chunks, and each chunk stored on-chain via
Block.sol. The sequence is registered inEVMFS.sol. - Get a CID — On-chain rolling SHA-256 produces a 32-byte Content ID — your permanent, tamper-proof address on Ethereum.
- Register a name — Bind your CID to
<name>.evmfs.appviaEVMFSRegistry.sol. Tiered pricing: 7+ chars = 0.015 ETH. - Zero-server serving — Visitors open
<name>.evmfs.app. A Service Worker fetches all chunks from chain, verifies the CID hash, decompresses the ZIP, caches files in IndexedDB, and intercepts every request — no servers, forever.
Gateway & Service Worker architecture
Builds, zips, uploads to EVMFS, and optionally registers an app name. Uses our deployed contracts on Sepolia.
ETH_PRIVATE_KEY=0xYOUR_KEY \ BLOCK_ADDR=0x4571D19Ada5Ec6853bd7Bef5fBe9F0f94238419c \ EVMFS_ADDR=0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A \ node scripts/evmfs-build-and-deploy.mjs \ --network sepolia \ --skip-deploy \ --register-app --app-name myapp
Flags
--skip-deploy | Use existing contracts (default for most users) |
--skip-build | Reuse existing dist/ |
--skip-ipfs | Upload to EVMFS only, skip Pinata pin |
--ipfs-only | Pin only (dist-evmfs/ must exist) |
--loader-only | Regenerate loader HTML and re-pin |
--force-statics | Re-upload framework JS to IPFS |
--register-app | Register app name in EVMFSRegistry after upload |
--update-cid | Update CID for an existing registered name |
--app-name <n> | App name for registry (required with register/update) |
--registry-addr | Override EVMFSRegistry address |
What is EVMFS
A two-contract system — Block.sol (chunk store) and EVMFS.sol (file registry) — that turns any EVM chain into a permanent content-addressed file store.
Files are split into fixed-size chunks, each stored in its own Block. The sequence of blocks is registered as a File, identified by a deterministic 32-byte CID. No gateways, no off-chain relay, no expiry.
Architecture
Virtual filesystem & ZIP layout
EVMFS stores a single ZIP per CID — not individual files. Every folder and file lives inside that ZIP as standard directory entries. The gateway decompresses it and maps each path to an IndexedDB key.
Any static site works: plain HTML, React/Vite builds, frameworks, WASM binaries. As long as it can be served from a flat file tree, it can live on EVMFS.
Service Worker lifecycle
self.skipWaiting() activates immediately.LOAD_SITE message: fetch blocks from chain, verify CID, decompress ZIP, store all files.Response with correct MIME + CSP headers.eth_call JSON-RPC with manually-encoded calldata. Zero external dependencies in the SW means zero supply-chain risk.
CID computation
A 32-byte value computed by rolling SHA-256 as chunks are uploaded on-chain. Uses the EVM’s native sha256() precompile.
SHA-256 is hardware-accelerated via crypto.subtle.digest in browsers, so clients can recompute the CID locally after download and verify against the on-chain value — trustless integrity even against a malicious RPC node.
Contracts
| Contract | Address | Network |
|---|---|---|
| Block.sol | 0x4571D19Ada5Ec6853bd7Bef5fBe9F0f94238419c | Sepolia |
| EVMFS.sol | 0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A | Sepolia |
| EVMFSRegistry.sol | 0x1DC0fe8Ad09F4FB6f36c5DEC81f4975fb3a85999 | Sepolia |
| EVMFSSubscription.sol | 0x21BfAAfC8eb807Cf695F3D4e3c99eA6D43bE8600 | Sepolia |
| Block.sol | 0x32DB8E8Eeb4A8Fec859fDAC5A6222608D847DB7F | Taiko Hoodi |
| EVMFS.sol | 0x36bF7216C9F23dc9a2433B75B7be7971d3f78b47 | Taiko Hoodi |
| EVMFSRegistry.sol | 0xc4aca772da000649003951Fd3E9FF65b5001C008 | Taiko Hoodi |
| EVMFSSubscription.sol | 0xA427B3A109d95013c7990E08654FAaE722afe8f3 | Taiko Hoodi |
IPFS framework assets
Permanent, content-addressed. Served via <CID>.ipfs.dweb.link subdomains.
| Asset | IPFS CID |
|---|---|
ethers.umd.min.js v6 | bafybeifhgcw2r5cmvnniy3mv3vxh7c4ar6i3oam4vfyi5zc6csfwnnre2u |
evmfs-crypto.js | bafkreib3mqg77ixf42t6nzz4p2mocqk2fnv22rhrw2zx34epencepfnduu |
evmfs-web-loader.js | bafkreieizmmxatd63zg6i2ven46hyxd7sze3iglrtd33tjp3zu7h4svzye |
evmfs-zip-processor.js | bafkreih36uv3yimcjqua5ddjvsehsvzf3dvzwbhgfux6qjjevcmqakf7xy |
jszip.min.js | bafkreifmy7sbivnia5s3l7m4p3q3qb4knulaxo6kivnov2cu3zs4sr6vty |
Protocol reference
Two contracts, one CID. Everything you need to integrate EVMFS at the protocol level.
Single-chunk upload
Files under ~20 KB — one createFile(data, true) call.
import { ethers } from "ethers"; const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const ABI = [ "event FileFinalized(uint256 indexed fileId, bytes cid)", "function createFile(bytes calldata data, bool end) returns (uint256 fileId)", ]; const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com"); const signer = new ethers.Wallet(process.env.ETH_PRIVATE_KEY, provider); const evmfs = new ethers.Contract(EVMFS, ABI, signer); const content = new TextEncoder().encode("Hello, EVMFS!"); const tx = await evmfs.createFile(content, true); const rec = await tx.wait(); const log = rec.logs .map(l => { try { return evmfs.interface.parseLog(l); } catch {} }) .find(e => e?.name === "FileFinalized"); const cid = ethers.hexlify(log.args.cid); // cid → "0x<64 hex chars>" — permanent on Sepolia
import { ethers, type ContractTransactionResponse } from "ethers"; const EVMFS: string = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const ABI: string[] = [ "event FileFinalized(uint256 indexed fileId, bytes cid)", "function createFile(bytes calldata data, bool end) returns (uint256 fileId)", ]; const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com"); const signer = new ethers.Wallet(process.env.ETH_PRIVATE_KEY!, provider); const evmfs = new ethers.Contract(EVMFS, ABI, signer); const content: Uint8Array = new TextEncoder().encode("Hello, EVMFS!"); const tx: ContractTransactionResponse = await evmfs.createFile(content, true); const rec = await tx.wait(); const log = rec!.logs .map((l: any) => { try { return evmfs.interface.parseLog(l); } catch {} }) .find((e: any) => e?.name === "FileFinalized"); const cid: string = ethers.hexlify(log!.args.cid); // cid → "0x<64 hex chars>" — permanent on Sepolia
<!-- ethers v6 UMD --> <script src="https://bafybeifhgcw2r5cmvnniy3mv3vxh7c4ar6i3oam4vfyi5zc6csfwnnre2u.ipfs.dweb.link"></script> <script> (async () => { const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const ABI = [ "event FileFinalized(uint256 indexed fileId, bytes cid)", "function createFile(bytes calldata data, bool end) returns (uint256 fileId)", ]; const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const evmfs = new ethers.Contract(EVMFS, ABI, signer); const content = new TextEncoder().encode("Hello, EVMFS!"); const tx = await evmfs.createFile(content, true); const rec = await tx.wait(); const log = rec.logs .map(l => { try { return evmfs.interface.parseLog(l); } catch {} }) .find(e => e?.name === "FileFinalized"); console.log("CID:", ethers.hexlify(log.args.cid)); })(); </script>
<script type="module"> import { ethers } from "https://esm.sh/ethers@6"; const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const ABI = [ "event FileFinalized(uint256 indexed fileId, bytes cid)", "function createFile(bytes calldata data, bool end) returns (uint256 fileId)", ]; const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const evmfs = new ethers.Contract(EVMFS, ABI, signer); const content = new TextEncoder().encode("Hello, EVMFS!"); const tx = await evmfs.createFile(content, true); const rec = await tx.wait(); const log = rec.logs .map(l => { try { return evmfs.interface.parseLog(l); } catch {} }) .find(e => e?.name === "FileFinalized"); console.log("CID:", ethers.hexlify(log.args.cid)); </script>
Multi-chunk upload
Files larger than 20 KB — split, upload first chunk with createFile, append the rest with upload.
import { ethers } from "ethers"; import { readFileSync } from "fs"; import { createHash } from "crypto"; const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const CHUNK = 20 * 1024; const ABI = [ "event FileCreated(uint256 indexed fileId, address indexed owner)", "event FileFinalized(uint256 indexed fileId, bytes cid)", "function createFile(bytes calldata data, bool end) returns (uint256)", "function upload(uint256 fileId, bytes calldata data, bool end) returns (uint256)", ]; function computeCID(chunks) { let h = createHash("sha256").update(chunks[0]).digest(); for (let i = 1; i < chunks.length; i++) h = createHash("sha256").update(Buffer.concat([h, chunks[i]])).digest(); return "0x" + h.toString("hex"); } const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com"); const signer = new ethers.Wallet(process.env.ETH_PRIVATE_KEY, provider); const evmfs = new ethers.Contract(EVMFS, ABI, signer); const raw = readFileSync("./my-file.zip"); const chunks = []; for (let o = 0; o < raw.length; o += CHUNK) chunks.push(raw.subarray(o, o + CHUNK)); console.log("Expected CID:", computeCID(chunks)); let nonce = await provider.getTransactionCount(signer.address, "latest"); const r = await (await evmfs.createFile(chunks[0], chunks.length === 1, { nonce: nonce++ })).wait(); const fileId = evmfs.interface.parseLog(r.logs[0]).args.fileId; for (let i = 1; i < chunks.length; i++) await (await evmfs.upload(fileId, chunks[i], i === chunks.length - 1, { nonce: nonce++ })).wait();
import { ethers } from "ethers"; import { readFileSync } from "fs"; import { createHash } from "crypto"; const EVMFS: string = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const CHUNK: number = 20 * 1024; const ABI: string[] = [ "event FileCreated(uint256 indexed fileId, address indexed owner)", "event FileFinalized(uint256 indexed fileId, bytes cid)", "function createFile(bytes calldata data, bool end) returns (uint256)", "function upload(uint256 fileId, bytes calldata data, bool end) returns (uint256)", ]; function computeCID(chunks: Buffer[]): string { let h = createHash("sha256").update(chunks[0]).digest(); for (let i = 1; i < chunks.length; i++) h = createHash("sha256").update(Buffer.concat([h, chunks[i]])).digest(); return "0x" + h.toString("hex"); } const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com"); const signer = new ethers.Wallet(process.env.ETH_PRIVATE_KEY!, provider); const evmfs = new ethers.Contract(EVMFS, ABI, signer); const raw = readFileSync("./my-file.zip"); const chunks: Buffer[] = []; for (let o = 0; o < raw.length; o += CHUNK) chunks.push(raw.subarray(o, o + CHUNK)); console.log("Expected CID:", computeCID(chunks)); let nonce = await provider.getTransactionCount(signer.address, "latest"); const r = await (await evmfs.createFile(chunks[0], chunks.length === 1, { nonce: nonce++ })).wait(); const fileId: bigint = evmfs.interface.parseLog(r!.logs[0]).args.fileId; for (let i = 1; i < chunks.length; i++) await (await evmfs.upload(fileId, chunks[i], i === chunks.length - 1, { nonce: nonce++ })).wait();
<script src="https://bafybeifhgcw2r5cmvnniy3mv3vxh7c4ar6i3oam4vfyi5zc6csfwnnre2u.ipfs.dweb.link"></script> <input type="file" id="file" /> <script> const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const CHUNK = 20 * 1024; const ABI = [ "event FileCreated(uint256 indexed fileId, address indexed owner)", "event FileFinalized(uint256 indexed fileId, bytes cid)", "function createFile(bytes calldata data, bool end) returns (uint256)", "function upload(uint256 fileId, bytes calldata data, bool end) returns (uint256)", ]; document.getElementById("file").addEventListener("change", async (e) => { const raw = new Uint8Array(await e.target.files[0].arrayBuffer()); const chunks = []; for (let o = 0; o < raw.length; o += CHUNK) chunks.push(raw.subarray(o, o + CHUNK)); const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const evmfs = new ethers.Contract(EVMFS, ABI, signer); const r = await (await evmfs.createFile(chunks[0], chunks.length === 1)).wait(); const fileId = evmfs.interface.parseLog(r.logs[0]).args.fileId; for (let i = 1; i < chunks.length; i++) await (await evmfs.upload(fileId, chunks[i], i === chunks.length - 1)).wait(); console.log("Upload complete, fileId:", fileId.toString()); }); </script>
<input type="file" id="file" /> <script type="module"> import { ethers } from "https://esm.sh/ethers@6"; const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const CHUNK = 20 * 1024; const ABI = [ "event FileCreated(uint256 indexed fileId, address indexed owner)", "event FileFinalized(uint256 indexed fileId, bytes cid)", "function createFile(bytes calldata data, bool end) returns (uint256)", "function upload(uint256 fileId, bytes calldata data, bool end) returns (uint256)", ]; document.getElementById("file").addEventListener("change", async (e) => { const raw = new Uint8Array(await e.target.files[0].arrayBuffer()); const chunks = []; for (let o = 0; o < raw.length; o += CHUNK) chunks.push(raw.subarray(o, o + CHUNK)); const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const evmfs = new ethers.Contract(EVMFS, ABI, signer); const r = await (await evmfs.createFile(chunks[0], chunks.length === 1)).wait(); const fileId = evmfs.interface.parseLog(r.logs[0]).args.fileId; for (let i = 1; i < chunks.length; i++) await (await evmfs.upload(fileId, chunks[i], i === chunks.length - 1)).wait(); console.log("Upload complete, fileId:", fileId.toString()); }); </script>
Read a file by CID
Read-only — no signer needed. Any public RPC node works.
import { ethers } from "ethers"; const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const CID = "0x<your-cid-here>"; const ABI = [ "function getFileByCID(bytes calldata cid) view returns (tuple(uint256 id, uint8 status, bytes cid, uint256[] blockIds))", "function getFileBlocks(uint256 fileId) view returns (tuple(uint256 id, bytes data)[])", ]; const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com"); const evmfs = new ethers.Contract(EVMFS, ABI, provider); const meta = await evmfs.getFileByCID(CID); const blocks = await evmfs.getFileBlocks(meta.id); // Reassemble const chunks = blocks.map(b => ethers.getBytes(b.data)); const total = chunks.reduce((s, c) => s + c.length, 0); const buf = new Uint8Array(total); let off = 0; for (const c of chunks) { buf.set(c, off); off += c.length; } console.log("Downloaded", total, "bytes");
import { ethers } from "ethers"; const EVMFS: string = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const CID: string = "0x<your-cid-here>"; const ABI: string[] = [ "function getFileByCID(bytes calldata cid) view returns (tuple(uint256 id, uint8 status, bytes cid, uint256[] blockIds))", "function getFileBlocks(uint256 fileId) view returns (tuple(uint256 id, bytes data)[])", ]; const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com"); const evmfs = new ethers.Contract(EVMFS, ABI, provider); const meta = await evmfs.getFileByCID(CID); const blocks = await evmfs.getFileBlocks(meta.id); // Reassemble const chunks: Uint8Array[] = blocks.map((b: any) => ethers.getBytes(b.data)); const total: number = chunks.reduce((s, c) => s + c.length, 0); const buf = new Uint8Array(total); let off = 0; for (const c of chunks) { buf.set(c, off); off += c.length; } console.log("Downloaded", total, "bytes");
<script src="https://bafybeifhgcw2r5cmvnniy3mv3vxh7c4ar6i3oam4vfyi5zc6csfwnnre2u.ipfs.dweb.link"></script> <script> (async () => { const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const CID = "0x<your-cid-here>"; const ABI = [ "function getFileByCID(bytes calldata cid) view returns (tuple(uint256 id, uint8 status, bytes cid, uint256[] blockIds))", "function getFileBlocks(uint256 fileId) view returns (tuple(uint256 id, bytes data)[])", ]; const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com"); const evmfs = new ethers.Contract(EVMFS, ABI, provider); const meta = await evmfs.getFileByCID(CID); const blocks = await evmfs.getFileBlocks(meta.id); const chunks = blocks.map(b => ethers.getBytes(b.data)); const total = chunks.reduce((s, c) => s + c.length, 0); const buf = new Uint8Array(total); let off = 0; for (const c of chunks) { buf.set(c, off); off += c.length; } console.log("Downloaded", total, "bytes"); })(); </script>
<script type="module"> import { ethers } from "https://esm.sh/ethers@6"; const EVMFS = "0xDb0CBFd5ceFB148f606Ae3b61B4d944e430F2f2A"; const CID = "0x<your-cid-here>"; const ABI = [ "function getFileByCID(bytes calldata cid) view returns (tuple(uint256 id, uint8 status, bytes cid, uint256[] blockIds))", "function getFileBlocks(uint256 fileId) view returns (tuple(uint256 id, bytes data)[])", ]; const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com"); const evmfs = new ethers.Contract(EVMFS, ABI, provider); const meta = await evmfs.getFileByCID(CID); const blocks = await evmfs.getFileBlocks(meta.id); const chunks = blocks.map(b => ethers.getBytes(b.data)); const total = chunks.reduce((s, c) => s + c.length, 0); const buf = new Uint8Array(total); let off = 0; for (const c of chunks) { buf.set(c, off); off += c.length; } console.log("Downloaded", total, "bytes"); </script>
Verify CID
Recompute the rolling SHA-256 client-side and compare — trustless integrity check.
import { createHash } from "crypto"; function verifyCID(chunks, expected) { let h = createHash("sha256").update(chunks[0]).digest(); for (let i = 1; i < chunks.length; i++) h = createHash("sha256").update(Buffer.concat([h, chunks[i]])).digest(); const computed = "0x" + h.toString("hex"); return computed.toLowerCase() === expected.toLowerCase(); } // chunks = array of Buffer/Uint8Array from getFileBlocks console.log(verifyCID(chunks, CID) ? "CID verified" : "CID MISMATCH");
import { createHash } from "crypto"; function verifyCID(chunks: Buffer[], expected: string): boolean { let h = createHash("sha256").update(chunks[0]).digest(); for (let i = 1; i < chunks.length; i++) h = createHash("sha256").update(Buffer.concat([h, chunks[i]])).digest(); const computed: string = "0x" + h.toString("hex"); return computed.toLowerCase() === expected.toLowerCase(); } // chunks = array of Buffer from getFileBlocks console.log(verifyCID(chunks, CID) ? "CID verified" : "CID MISMATCH");
async function verifyCID(chunks, expected) { let h = new Uint8Array(await crypto.subtle.digest("SHA-256", chunks[0])); for (let i = 1; i < chunks.length; i++) { const joined = new Uint8Array(h.length + chunks[i].length); joined.set(h); joined.set(chunks[i], h.length); h = new Uint8Array(await crypto.subtle.digest("SHA-256", joined)); } const computed = "0x" + [...h].map(x => x.toString(16).padStart(2,"0")).join(""); return computed.toLowerCase() === expected.toLowerCase(); } // chunks = array of Uint8Array from getFileBlocks console.log(await verifyCID(chunks, CID) ? "CID verified" : "CID MISMATCH");
// SubtleCrypto is built-in — no import needed. async function verifyCID(chunks, expected) { let h = new Uint8Array(await crypto.subtle.digest("SHA-256", chunks[0])); for (let i = 1; i < chunks.length; i++) { const joined = new Uint8Array(h.length + chunks[i].length); joined.set(h); joined.set(chunks[i], h.length); h = new Uint8Array(await crypto.subtle.digest("SHA-256", joined)); } const computed = "0x" + [...h].map(x => x.toString(16).padStart(2,"0")).join(""); return computed.toLowerCase() === expected.toLowerCase(); } // chunks = array of Uint8Array from getFileBlocks console.log(await verifyCID(chunks, CID) ? "CID verified" : "CID MISMATCH");
Subscription Plans
EVMFS uploads require a small per-block premium (0.00005 ETH) unless you hold an active subscription NFT. Plans are ERC-1155 tokens β fully on-chain, no admin keys, no middlemen.
Monthly
Yearly
Lifetime
How it works
- With subscription: createFile() and upload() cost only gas (no premium).
- Without subscription: each block write costs 0.00005 ETH, forwarded to the protocol beneficiary.
- Subscriptions stack: buying two monthly plans gives you 60 days.
- ERC-1155 tokens are minted as proof of purchase and remain in your wallet permanently.
- addExistingBlock(), createFileEmpty(), and extendFile() are always free (no new blocks created).
- Fully permissionless: no admin keys, no pause function. ETH goes directly to the immutable beneficiary.