# Hello FHEVM:私密计数器(完整教程) **Published by:** [pangdong](https://paragraph.com/@pangdong/) **Published on:** 2025-09-19 **URL:** https://paragraph.com/@pangdong/hello-fhevm ## Content 用最小可用的 Demo,带你从零实现:密文加法 + 明文解密展示。 技术栈:Solidity + Hardhat + React/Vite + Zama FHEVM SDK。 目标链:Sepolia 测试网。0. 你将得到什么?一个可以接受加密输入(+1)的合约 PrivateCounter一个前端 DApp:先连接任意 EIP-1193 钱包(MetaMask/OKX/Bitget 等均可)使用 Zama Relayer 注册加密输入,调用合约的 add()通过合约发起解密 requestReveal(),等待回调后读取 totalPlain一键部署到 Vercel 的网站链接1. 准备环境Node.js ≥ 18(推荐 20+)pnpm(也可用 npm/yarn)npm i -g pnpm 一个可用的 Sepolia RPC(公共节点即可)推荐先测试延迟/可用性:# 任选其一可用即可 for u in https://ethereum-sepolia.publicnode.com https://rpc.sepolia.org; do printf "%-38s" "$u" r=$(curl -s -m 6 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' "$u") bn=$(echo "$r" | sed -n 's/.*"result":"\([^"]*\)".*/\1/p'); if [ -n "$bn" ]; then echo " OK block=$bn"; else echo " FAIL resp=$(echo "$r" | cut -c1-80)"; fi done 钱包(浏览器扩展),建议 MetaMask2. 初始化 Hardhat 项目mkdir -p hello-fhevm && cd hello-fhevm pnpm init -y mkdir hardhat && cd hardhat pnpm add -D hardhat typescript ts-node @types/node @nomicfoundation/hardhat-toolbox \ @fhevm/solidity @fhevm/hardhat-plugin dotenv pnpm dlx hardhat # 选“Create a TypeScript project” 生成的 hardhat.config.ts 替换为(适配 FHEVM 插件并禁用它在非本地链的 remapping):// hardhat/hardhat.config.ts import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; import * as dotenv from "dotenv"; dotenv.config(); const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || "https://ethereum-sepolia.publicnode.com"; const PRIVATE_KEY = (process.env.PRIVATE_KEY || "").replace(/^"|"$/g, ""); const config: HardhatUserConfig = { solidity: { version: "0.8.24", settings: { viaIR: false, optimizer: { enabled: true, runs: 200 } }, }, networks: { sepolia: { url: SEPOLIA_RPC_URL, accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], }, }, }; export default config; 说明:FHEVM 的 @fhevm/hardhat-plugin 只用于本地模拟开发,本教程直接用 Zama 提供的链上 KMS/Relayer,不需要在编译阶段做 remapping,所以上面的配置不启用插件扩展。.env(放在 hardhat/ 目录下):SEPOLIA_RPC_URL=https://ethereum-sepolia.publicnode.com PRIVATE_KEY=0x你的私钥 3. 编写合约 PrivateCounter.sol// hardhat/contracts/PrivateCounter.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /** * 私密计数器: * - add(): 接收加密的 euint32,累计到密文 total * - requestReveal(): 发起一次解密请求(走 Zama KMS) * - onReveal(): KMS 回调,验签 & 更新明文 totalPlain */ contract PrivateCounter is SepoliaConfig { euint32 private total; uint32 public totalPlain; bool private pending; uint256 private lastReqId; constructor() { total = FHE.asEuint32(0); FHE.allowThis(total); } function add( externalEuint32 encryptedDelta, bytes calldata inputProof ) external { euint32 delta = FHE.fromExternal(encryptedDelta, inputProof); total = FHE.add(total, delta); FHE.allowThis(total); } function requestReveal() external { require(!pending, "Decrypting in progress"); bytes32; // 某些版本是 toBytes(total);新版本为 toBytes32(total) handles[0] = FHE.toBytes32(total); lastReqId = FHE.requestDecryption(handles, this.onReveal.selector); pending = true; } function onReveal( uint256 requestId, bytes memory cleartexts, bytes memory decryptionProof ) external returns (bool) { require(requestId == lastReqId, "Invalid request id"); FHE.checkSignatures(requestId, cleartexts, decryptionProof); (uint32 value) = abi.decode(cleartexts, (uint32)); totalPlain = value; pending = false; return true; } } 编译:pnpm hardhat clean && pnpm hardhat compile 4. 部署脚本scripts/deploy.ts:import { ethers } from "hardhat"; async function main() { const [deployer] = await ethers.getSigners(); console.log("Deployer:", deployer.address); const Factory = await ethers.getContractFactory("PrivateCounter"); const c = await Factory.deploy(); await c.waitForDeployment(); console.log("✅ PrivateCounter deployed at:", await c.getAddress()); } main().catch((e)=>{ console.error(e); process.exit(1); }); 部署到 Sepolia:pnpm hardhat run scripts/deploy.ts --network sepolia 记下输出的合约地址,后面前端要用。文中示例: 0x9F8069282814a1177C1f6b8D7d8f7cC11A6635545.(可选)链上验证此 Demo 不强制验证。若你需要,在 Etherscan 开个 API Key 后,使用 @nomicfoundation/hardhat-verify 即可。6. 前端工程(Vite + React)在 hello-fhevm/ 下创建 frontend/:cd ../ pnpm create vite@latest frontend -- --template react-ts cd frontend pnpm add ethers 6.1 环境变量.env(部署时在 Vercel 里配置同名变量):VITE_COUNTER_ADDRESS=0x你的合约地址 VITE_RPC_URL=https://ethereum-sepolia.publicnode.com # (可选,若你的网络环境对某些域名有限制) # VITE_RELAYER_URL=https://relayer.testnet.zama.cloud # VITE_GATEWAY_URL=https://gateway.testnet.zama.cloud 6.2 类型声明(用于从 CDN 动态导入 SDK)mkdir -p src/types src/types/zama-relayer-cdn.d.ts:declare module "https://cdn.zama.ai/relayer-sdk-js/0.2.0/relayer-sdk-js.js" { export function initSDK(): Promise<void>; export function createInstance(cfg: any): Promise<any>; export const SepoliaConfig: any; } 确保 tsconfig.json 包含 src 与 src/types/**/*.d.ts(Vite 默认模板基本 OK):{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "allowJs": true }, "include": ["src", "src/types/**/*.d.ts"] } 6.3 核心页面 src/App.tsx这版逻辑是:先连接钱包(任何 EIP-1193 钱包都可以) → 切到 Sepolia → initSDK() → createInstance({...SepoliaConfig, network: provider, chainId: 11155111})。import { useRef, useState } from "react"; import { ethers } from "ethers"; /** PrivateCounter ABI */ const ABI = [ "function add(bytes32 encryptedDelta, bytes inputProof)", "function requestReveal()", "function totalPlain() view returns (uint32)", ] as const; const COUNTER_ADDR = (import.meta.env.VITE_COUNTER_ADDRESS as `0x${string}`) || "0x9F8069282814a1177C1f6b8D7d8f7cC11A663554"; function pickProvider(): any { const eth = (window as any).ethereum; if (!eth) return null; if (Array.isArray(eth?.providers)) { const anyProv = eth.providers.find((p: any) => typeof p?.request === "function"); if (anyProv) return anyProv; } return eth; } const RPC_FALLBACK = (import.meta.env.VITE_RPC_URL as string) || "https://ethereum-sepolia.publicnode.com"; const RELAYER_CANDIDATES = [ import.meta.env.VITE_RELAYER_URL as string, "https://relayer.testnet.zama.cloud", "https://relayer.fhevm.zama.ai", "https://relayer.zama.ai", "https://devnet.zama.ai/relayer", ].filter(Boolean); const GATEWAY_CANDIDATES = [ import.meta.env.VITE_GATEWAY_URL as string, "https://gateway.testnet.zama.cloud", "https://gateway.fhevm.zama.ai", ].filter(Boolean); export default function App() { const [status, setStatus] = useState("尚未连接钱包"); const [addr, setAddr] = useState<string | null>(null); const [plain, setPlain] = useState<number | null>(null); const [chainId, setChainId] = useState<number | null>(null); const [usingRelayer, setUsingRelayer] = useState<string | null>(null); const [usingGateway, setUsingGateway] = useState<string | null>(null); const providerRef = useRef<any>(null); const fheRef = useRef<any>(null); async function connectAndInit() { try { const eth = pickProvider(); if (!eth) throw new Error("未检测到钱包。请安装或启用一个 EIP-1193 钱包扩展(如 MetaMask)。"); providerRef.current = eth; setStatus("请求账户授权…"); const accounts: string[] = await eth.request({ method: "eth_requestAccounts" }); if (!accounts?.length) throw new Error("未授权任何账户。"); setAddr(accounts[0]); const hexId: string = await eth.request({ method: "eth_chainId" }); let id = parseInt(hexId, 16); if (id !== 11155111) { setStatus("切换到 Sepolia…"); try { await eth.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0xaa36a7" }] }); id = 11155111; } catch (e: any) { if (e?.code === 4902) { await eth.request({ method: "wallet_addEthereumChain", params: [{ chainId: "0xaa36a7", chainName: "Sepolia", nativeCurrency: { name: "Sepolia ETH", symbol: "ETH", decimals: 18 }, rpcUrls: [RPC_FALLBACK], blockExplorerUrls: ["https://sepolia.etherscan.io/"] }] }); id = 11155111; } else { throw e; } } } setChainId(id); setStatus("加载 FHEVM SDK…"); const mod = await import( /* @vite-ignore */ "https://cdn.zama.ai/relayer-sdk-js/0.2.0/relayer-sdk-js.js" ); const { initSDK, createInstance, SepoliaConfig } = mod as any; await initSDK(); let lastErr: any = null; for (const r of RELAYER_CANDIDATES) { for (const g of GATEWAY_CANDIDATES) { try { setStatus(`创建 FHE 实例…(尝试 ${r} / ${g})`); const cfg = { ...SepoliaConfig, relayerUrl: r, gatewayUrl: g, network: providerRef.current, chainId: 11155111, }; const inst = await createInstance(cfg); if (typeof inst.init === "function") await inst.init(); fheRef.current = inst; setUsingRelayer(r); setUsingGateway(g); setStatus("✅ 已连接 & SDK 就绪"); return; } catch (e: any) { lastErr = e; } } } throw new Error(`所有 relayer/gateway 组合均失败。最后错误:${lastErr?.message || lastErr}`); } catch (e: any) { console.error(e); setStatus("❌ 连接/初始化失败: " + (e?.message || e)); } } async function getSignerAndContract() { const eth = providerRef.current || pickProvider(); if (!eth) throw new Error("未检测到钱包。"); const browserProvider = new ethers.BrowserProvider(eth); await browserProvider.send("eth_requestAccounts", []); const signer = await browserProvider.getSigner(); const contract = new ethers.Contract(COUNTER_ADDR, ABI, signer); return { signer, contract, addr: await signer.getAddress() }; } async function handleAddOne() { try { if (!fheRef.current) throw new Error("SDK 未就绪,请先连接钱包。"); setStatus("注册加密输入…"); const { addr, contract } = await getSignerAndContract(); const buf = fheRef.current.createEncryptedInput(COUNTER_ADDR, addr); buf.add32(1n); const cipher = await buf.encrypt(); setStatus("发送交易 add(+1)…"); const tx = await contract.add(cipher.handles[0], cipher.inputProof); await tx.wait(); setStatus("✅ 已提交 +1"); } catch (e: any) { console.error(e); setStatus("❌ 失败: " + (e?.message || e)); } } async function handleReveal() { try { if (!fheRef.current) throw new Error("SDK 未就绪,请先连接钱包。"); setStatus("请求解密…"); const { contract } = await getSignerAndContract(); const tx = await contract.requestReveal(); await tx.wait(); setStatus("等待回调(~30s)…"); await new Promise((r) => setTimeout(r, 30_000)); const v: bigint = await contract.totalPlain(); setPlain(Number(v)); setStatus("✅ 完成"); } catch (e: any) { console.error(e); setStatus("❌ 失败: " + (e?.message || e)); } } return ( <div style={{ maxWidth: 880, margin: "48px auto", fontFamily: "system-ui" }}> <h1>Hello FHEVM: 私密计数器</h1> <p>合约地址:<code>{COUNTER_ADDR}</code></p> {(usingRelayer || usingGateway) && ( <p style={{ fontSize: 12, opacity: .7 }}> relayer: <code>{usingRelayer}</code> · gateway: <code>{usingGateway}</code> </p> )} <div style={{ display: "flex", gap: 12, marginTop: 16, flexWrap: "wrap" }}> {!addr ? ( <button onClick={connectAndInit} style={{ background: "#e5f0ff" }}> 🔌 连接钱包并初始化 </button> ) : ( <> <button onClick={handleAddOne}>➕ 加 1(加密提交)</button> <button onClick={handleReveal}>🔓 解密总数</button> <button onClick={connectAndInit} style={{ background: "#f3f4f6" }}> ♻️ 重新连接/重新初始化 </button> </> )} </div> <p style={{ marginTop: 16 }}> 状态:{status} {addr && <> | 账户:<code>{addr}</code></>} {chainId !== null && <> | ChainId:<code>{chainId}</code></>} </p> <p>明文总数:{plain === null ? "(未解密)" : plain}</p> </div> ); } 开发预览:pnpm i pnpm dev # 打开提示的本地域名即可 7. 部署到 VercelNew Project → 选择你的仓库 → Root Directory = frontendBuild Command:pnpm buildOutput Directory:distInstall Command:pnpm installEnvironment Variables:VITE_COUNTER_ADDRESS=0x你的合约地址VITE_RPC_URL=https://ethereum-sepolia.publicnode.com(可选)VITE_RELAYER_URL、VITE_GATEWAY_URL点 Deploy,完成后打开域名。8. 使用步骤打开你的 Vercel 域名(https)浏览器钱包解锁点击页面上的 “🔌 连接钱包并初始化” → 同意授权/切换到 Sepolia点击 “➕ 加 1(加密提交)”,确认交易点击 “🔓 解密总数”,状态会显示“等待回调(~30s)…”,随后明文更新9. 常见错误速查Invalid JSON-RPC response received: invalid project id → 你用了 Infura/Alchemy 但没带 Project ID。换公共节点或换成自己的完整 RPC URL。KMS contract address is not valid or empty / Impossible to fetch public key: wrong relayer url. → 当前 Relayer/Gateway 与链/KMS 不匹配或不可达。 → 试试教程里前端的候选端点重试逻辑,或换浏览器/网络(关闭广告拦截、公司代理、无痕模式)。wallet must has at least one account / ACTION_REJECTED → 钱包没给站点授权、或被其它钱包扩展“抢占”。 → 先只启用 MetaMask,删除 Connected sites 中的站点记录 → 刷新 → 重新连接。Relayer didn't response correctly. REQUEST FAILED RESPONSE → 多半是浏览器/网络拦截了 *.zama.ai / *.zama.cloud。 → 换正常网络 + Chrome 正常窗口,或在 Vercel 上设置自定义 VITE_RELAYER_URL / VITE_GATEWAY_URL。10. 项目结构参考hello-fhevm/ ├─ hardhat/ │ ├─ contracts/PrivateCounter.sol │ ├─ scripts/deploy.ts │ ├─ hardhat.config.ts │ └─ .env └─ frontend/ ├─ src/App.tsx ├─ src/types/zama-relayer-cdn.d.ts ├─ .env └─ ... ## Publication Information - [pangdong](https://paragraph.com/@pangdong/): Publication homepage - [All Posts](https://paragraph.com/@pangdong/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@pangdong): Subscribe to updates - [Twitter](https://twitter.com/shexiaodong): Follow on Twitter