news 2026/9/13 11:22:32

fhevm js-sdk 架构解析:可组合运行时(FhevmRuntime)与可扩展客户端(FhevmClient)的设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fhevm js-sdk 架构解析:可组合运行时(FhevmRuntime)与可扩展客户端(FhevmClient)的设计与实现

fhevm js-sdk 架构解析:可组合运行时(FhevmRuntime)与可扩展客户端(FhevmClient)的设计与实现

【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm

导读

fhevm js-sdk 是 fhEVM 全栈框架的前端 JavaScript SDK,负责在链下完成 TFHE 密文的加密、解密、签名许可生成与密钥管理。本文以其架构设计文档 sdk/js-sdk/notes/ARCHITECTURE.md 为骨架,讲解 SDK 的两大核心抽象——可组合的运行时FhevmRuntime与可扩展的客户端FhevmClient,并对照仓库源码(运行时实现、模块工厂、初始化链路)逐条印证其设计原则。读完本文,你将理解 SDK 为何能做到"零配置可用、按需加载 WASM、惰性幂等初始化、顺序无关的可链式配置",并能正确使用createFhevmClient/createFhevmEncryptClient/createFhevmDecryptClient三种工厂,以及extend()init()ready等生命周期 API。

SDK 设计原则:一份可验证的契约清单

架构文档开篇即列出约四十条设计原则,它们是理解后续所有代码实现的"验收标准"。将其归纳为几个主题,并与源码一一对应:

  • 顺序无关与可链式配置(Order-independent API / Configuration is chainable and order-independent):配置项通过withXXX(...)形式链式设置,且withPublicKeyfetchPublicKey(当前实现为fetchFheEncryptionKeyBytes)的调用顺序不影响最终结果;配置在调用时解析(resolve config at call time),而非创建时捕获(Extensions must not capture config at creation time)。
  • 惰性、幂等、共享的初始化(Lazy, Idempotent, Shared)init()无论被手动调用还是被内部首次调用触发,永远返回同一个 Promise;多个并发调用共享同一次初始化。对应源码见 CoreFhevm-p.ts 中的init/ready实现。
  • 创建必须纯净(Creation must be pure):构造客户端不执行任何异步操作、不加载 WASM、不发 RPC,"no async at construction",一切延后到首次使用。
  • 可树摇(Treeshackable):加密与解密是两个相互独立的模块,各自绑定一个独立的 WASM 模块(TFHE 与 TKMS),未使用的模块绝不加载(SDK constraints条目),以此避免不必要的网络与内存开销。
  • 可组合、可扩展(Composable Extensions / Composable runtime modules):能力以"模块"为单位挂载到运行时,以"动作组(actions)"为单位挂载到客户端,扩展物可复用、可自由组合。
  • 错误可见性:配置缺失或错误时抛出清晰错误信息(Throw clear error messages),运行时执行校验(Validation is performed at runtime),绝不静默误用(no silent misuse)。
  • TypeScript 不做过度约束(Avoid over-constraining Typescript):类型只描述能力边界,不限制扩展组合方式。
  • 多运行时共存:生产运行时与 mock 运行时可同时存在;多个客户端可共享同一个运行时;模块可以是 JS 运行时内的单例(如 WASM 模块)。

这些原则并非停留在文档层面,下文将从源码结构上逐一给出实现证据。

架构总览:runtime 与 clients 的两层模型

架构文档将 SDK 分为两层:

  • runtime(FhevmRuntime):可组合的运行时。一个运行时由一组模块(module)构成,模块可动态添加;多个运行时可共享同一模块实例;某些模块在 JS 运行时内是全局唯一的(例如 WASM 模块)。运行时的创建与扩展都必须是纯净的(不产生副作用)。每个模块可能有 CPU 密集的初始化步骤,初始化遵循"幂等、惰性或手动"。
  • clients(FhevmClient):每个客户端拥有一个运行时,一个运行时可被多个客户端共享。客户端本质上是"运行时 + 一组用于启用特定功能的附加参数"。普通 SDK 使用者操作的是客户端而非运行时——运行时始终是内部组件(Runtime should remain an internal component)。客户端通过extend(...)扩展新函数,通过withXXX(...)设置配置参数。

这一分层在类型定义中体现得十分清晰:FhevmBase持有runtimechainclientoptions四个只读字段(见 coreFhevmClient.ts),而运行时接口FhevmRuntime只暴露ethereumrelayer两个基础模块、uidconfig以及按模块名重载的extend(factory)(见 coreFhevmRuntime.ts)。

// runtime 的类型骨架(简化) interface FhevmRuntime { readonly ethereum: EthereumModule; // 链上合约读取模块 readonly relayer: RelayerModule; // 中继器 HTTP 模块 readonly uid: string; readonly config: FhevmRuntimeConfig; extend(factory: DecryptModuleFactory): this & { readonly decrypt: DecryptModule }; extend(factory: EncryptModuleFactory): this & { readonly encrypt: EncryptModule }; }

注意extend返回类型是this & { readonly decrypt: ... }——这正是 TypeScript 层面实现"组合式扩展、且不破坏原有类型"的机制:每次扩展都会在类型上累加模块能力,而无需在构造时声明全部模块(避免构造函数爆炸,no constructor explosion)。

三种工厂函数与部分客户端

架构文档给出了三类客户端的构建方式:全量客户端、仅解密客户端、仅加密客户端。当前仓库中 ethers 与 viem 两个适配层各有一套同名工厂,位于 sdk/js-sdk/src/ethers/clients 与 sdk/js-sdk/src/viem/clients。

全量客户端:createFhevmClient

// full client (chain, provider, encrypt module, decrypt module) const fhevmFull = createFhevmClient({ chain, provider });

其实现正是"基础客户端 + 解密动作组 + 加密动作组"的两次extend(见 ethers/clients/createFhevmClient.ts):

export function createFhevmClient<chain extends FhevmChain, provider extends EthersT.ContractRunner>(parameters: { readonly provider: provider; readonly chain: chain; readonly options?: FhevmOptions | undefined; }): FhevmClient<chain, WithAll, provider> { const c = createFhevmBaseClient(parameters); return c.extend(decryptActions).extend(encryptActions); }

createFhevmBaseClient(见 ethers/clients/createFhevmBaseClient.ts)先通过createCoreFhevm创建裸客户端,再extend(baseActions)挂载基础动作组。也就是说,任何客户端都必然包含 base 层extend只会在其之上继续叠加能力。

部分客户端:按需加载 WASM 的关键

// partial decrypt client (chain, provider, decrypt module, no encrypt module) const fhevmDecrypt = createFhevmDecryptClient({ chain, provider }); // partial encrypt client (chain, provider, encrypt module, no decrypt module) const fhevmEncrypt = createFhevmEncryptClient({ chain, provider }); // create with optional publicKeyBytes, const fhevmEncrypt = createFhevmEncryptClient({ chain, provider, publicKeyBytes, });

viem 版加密工厂见 viem/clients/createFhevmEncryptClient.ts,结构相同:createFhevmBaseClient(parameters)后仅extend(encryptActions)。这样创建的加密客户端不会加载 TKMS 解密 WASM,解密客户端不会加载 TFHE 加密 WASM——这是"两个模块、两个 WASM、按需加载"原则的直接落地。

说明:publicKeyBytes只是架构笔记中描述的预期 API 形态。当前实现中,加密公钥通过createFhevmEncryptClientoptions.fheEncryptionKey传入,或由客户端从 relayer 拉取(fetchFheEncryptionKeyBytes,见 base.ts)。文档中"publicKeyBytes can be fetched independently"的描述与当前fetchFheEncryptionKeyBytes的设计一致——公钥可独立于客户端获取。

部分客户端到全量客户端:extend 的升级通道

架构文档强调:"Given clientA and clientB it should always be possible to extend clientA and/or clientB so that clientA == clientB"(给定任意两个客户端,总能通过扩展使它们的能力相等),并且"当部分客户端被创建后,SDK 应允许将其扩展为全量客户端"。

// Convert partial client to full client const fhevmFull = fhevmEncrypt.extend(decryptActions);

其类型层面等价于FhevmEncryptClient<WithEncrypt>FhevmClient<WithAll>,完全符合extend的"类型累加"语义。decryptActions是一个接收客户端为参数、返回一组闭包捕获客户端的函数(Function groups usually depends on modules that must be extended to the client runtime to run properly),例如解密动作组依赖decryptModule;扩展后客户端必须重新初始化,因为底层新增的 decrypt 模块需要初始化(after client extend, the client must be initialized again)。

这一点在源码中有明确的强制约束:extendCoreFhevm(见 CoreFhevm-p.ts)要求 actionsFactory 返回的runtime必须与客户端的 runtime 是同一个实例(否则抛错),并且把扩展携带的init函数注册进#initFns集合、同时将#readyPromise置为undefined——强制下一次调用必须重新走一遍初始化。

extend:唯一合法的能力扩展通道

运行时层面的extend实现位于 CoreFhevmRuntime-p.ts,其机制可以概括为"占位符(placeholder)单次填充 + 工厂引用幂等":

  1. 构造运行时(CoreFhevmRuntimeImpl)时,#encrypt#decrypt都是空对象占位符,对应模块槽位(slot)注册在一个Map中(L146-L149)。
  2. createExtendFn(L32-L79)调用模块工厂moduleFactory(runtime),工厂必须恰好返回一个键(如'encrypt'),SDK 据此查找对应槽位:
    • 同一工厂引用再次 extend → 幂等 no-opfactories.has(moduleFactory)直接返回自身);
    • 槽位已被不同工厂填充 → 抛错Already extended: <moduleName>(不允许二次扩展同一模块);
    • 未知模块名 → 抛错Unknown module: <moduleName>
  3. 填充后的占位符被Object.freeze,运行时实例也在构造末尾Object.freeze(this)(L157),并冻结类与原型(L187-L188)——运行时不变量不可被外部篡改。
  4. 对外校验通过instanceof加私有 token 完成:createFhevmRuntime需要调用方持有 owner token,assertIsFhevmRuntime/verifyFhevmRuntime保证传入的是真实 SDK 运行时(L200-L233)。

模块工厂的真实形态可在加密模块看到:encryptModule: EncryptModuleFactory = (runtime) => Object.freeze({ encrypt: Object.freeze({ initTfheModule, getTfheModuleInfo, parseTFHEProvenCompactCiphertextList, buildWithProofPacked, serialize/deserialize 密钥与 CRS }) })(见 modules/encrypt/module/index.ts);解密模块则暴露initTkmsModulegetTkmsModuleInfodecryptAndReconstruct、TKMS 私钥的生成/序列化/校验等(见 modules/decrypt/module/index.ts)。

客户端层面的extend则由extendCoreFhevm实现:把 actions 中每个函数通过Object.defineProperty(不可写、不可配置)挂到客户端实例上,并跳过已存在的键(if (key in client) continue),从而避免动作组之间的命名冲突。

init / ready:惰性、幂等、共享的初始化链路

客户端生命周期 API

架构文档给出了完整的生命周期调用方式:

// returns a promise (eq to { return init(); }) await fhevmEncrypt.ready; // manual init call (fetch key if needed) await fhevmEncrypt.init();

源码中(CoreFhevm-p.ts)initready的实现印证了"幂等、共享、惰性"三条原则:

init: { value: (): Promise<void> => { this.#readyPromise ??= Promise.all([...this.#initFns].map((fn) => fn(this))).then(() => {}); return this.#readyPromise; }, }, ready: { get: (): Promise<void> => this.init(), },
  • 惰性:构造时不执行任何初始化,#readyPromise初始为undefined
  • 幂等??=保证无论init()被调用多少次,都返回同一个 Promise
  • 共享:并发调用者await的是同一个 in-flight Promise,天然去重;
  • 可预测:每次extend会注册新的 init 函数并清空#readyPromise,从而保证扩展后的模块一定被初始化。

initPublicAction:每个公开动作的标准前奏

架构文档强调"fhevmClient初始化是可选的吗?——若不调用,首次调用时自动执行(at first call)"。这一"首次使用即初始化"由 CoreFhevm-p.ts 的initPublicAction统一保证,所有公开 API 动作(加密、解密、签名等)第一步都调用它

  1. await fhevm.ready——触发惰性、共享的初始化;
  2. 读取初始化期间解析并缓存在客户端上的 frozen context(版本快照);缺失即视为内部不变量被违反,抛出明确错误;
  3. 返回一份深拷贝的 frozen context(cloneFhevmClientFrozenContext),使动作在整个异步执行期间持有稳定的版本视图,不受后续上下文刷新的影响。

frozen context:一次性解析的版本快照

初始化期间,SDK 需要从链上解析协议版本、PubKey/CRS 版本、TFHE/TKMS 模块版本与各宿主合约版本,打包成不可变的FhevmClientFrozenContext(见 fhevmClientFrozenContext-p.ts)。其解析只执行一次并做并发去重:ensureFrozenContext(见 ensureFrozenContext-p.ts)在客户端实例上维护"已解析的数据 + 进行中的单一 Promise",多个 init 函数并发到达时共享同一个解析 Promise,成功后将数据落盘为同步可读状态;解析过程纯链上读取、无可重置副作用,因此临时 RPC 失败不会污染后续重试。

不同路径只解析自己需要的版本子集:加密路径需要tfheVersion与协议/ACL 版本,解密路径则是tkmsVersion与 KMSVerifier 版本(见 fhevmClientFrozenContext-p.ts 的类型注释),从而把链上getVersion()调用次数降到最低。客户端protocolVersiontfheVersiontkmsVersion等 getter 在 frozen context 未解析时抛出'Fhevm context has not been resolved. Await client.ready before.'(L40)。

各层的 init 职责

  • base 层_initBase仅解析 frozen context(base.ts);
  • encrypt 层_initEncrypt并行执行"拉取约 50MB 的全局 FHE 加密公钥(fetchFheEncryptionKeyBytes)+ 初始化 TFHE WASM 模块(initTfheModule)"(见 encrypt-p.ts);
  • decrypt 层_initDecrypt类似地初始化 TKMS WASM 模块。

这也解释了文档中"任何对withPublicKeyfetchPublicKey的调用在init()之后应抛出错误"的设计意图:配置必须在调用时解析、在初始化前完成固化,初始化后变更配置会破坏已建立的版本/密钥快照一致性。

WASM 模块的按需加载、单例约束与线程配置

两个模块、两个 WASM、一个运行时独占

架构文档明确要求:"encryptModule 和 decryptModule 是两个独立模块,分别与两个不同的 WASM 模块交互(各一个),必须避免在不需要时加载某个 WASM 模块,因此 SDK 采用扩展原则(extension principle)"。仓库的 wasm 资产目录印证了这一点:

  • sdk/js-sdk/src/wasm/tfhe 存放多个版本的 TFHE WASM(如 v1.5.3、v1.6.0-dev、v1.6.2),含tfhe_bg.wasm、worker 脚本与 base64 内嵌版本;
  • sdk/js-sdk/src/wasm/tkms 存放多个版本的 TKMS WASM(如 v0.13.10、v0.13.20-0、v0.14.0-1)。

每个模块的初始化都按版本缓存整个初始化 Promise(cachedTfheModulePromiseByVersion/cachedTkmsModulePromiseByVersion),且每个版本的 WASM 模块在同一时刻只能被一个运行时独占initTfheModule/initTkmsModule会检查ownerUidByVersion,若该版本已被其他运行时的uid占用则抛错Encrypt WASM module is already owned by runtime '...' and cannot be shared with runtime '...'(见 modules/encrypt/module/init-p.ts)。这与文档"有些模块在 JS 运行时内是全局唯一的"完全对应——WASM 实例及其 worker 池无法安全地在多个运行时间共享。

资产加载、SHA 校验与单线程降级

TFHE 模块的初始化(modules/encrypt/module/init-p.ts)定义了完整的资产解析与降级策略:

  • 资产 URL 解析:提供locateFile时按每个资产独立解析(返回URL走 URL 加载,返回null/undefined走内嵌 base64);未提供时,Node 端自动推导file://URL 并做磁盘存在性检查(任一缺失则整体回退 base64,兼容 Turbopack 等打包器搬移场景),浏览器端直接使用内嵌 base64;
  • WASM 编译:有 URL 则isomorphicCompileVerifiedWasm(SHA-256 校验后编译),否则从内嵌 base64 编译;
  • worker 加载模式wasmAssetLoadMode支持autoembedded-base64verified-blobprecheck-direct-urltrusted-direct-url五种(定义见 wasmAssets.ts),其中verified-blob提供真正的完整性保证(校验后以 Blob worker 执行),precheck-direct-url仅是"预检失败即快速报错"而非完整性校验,trusted-direct-url完全信任运行时加载;
  • 线程singleThreadnumberOfThreads控制线程池;检测到不支持 SharedArrayBuffer(缺少 COOP/COEP 头)或无 worker 来源时自动降级单线程,显式 URL 模式(_requiresAssetUrl)若无 worker URL 则直接抛错而非静默降级(L104-L106, L293-L330)。

这些实现细节共同支撑了"初始化惰性、幂等、可预测"的文档承诺:初始化失败会被缓存为 rejected Promise(不重试半初始化状态,避免二次错误如 'Already started',见 L553-L560 注释)。

链配置与零配置默认路径

SDK 内置了四条链定义(chains/index.ts):mainnetsepoliapolygonpolygonAmoy(另有localTestnet定义文件)。以 sepolia 为例(chains/definitions/sepolia.ts),每条链携带 fhEVM 相关宿主合约地址(ACL、InputVerifier、KMSVerifier、ProtocolConfig)、relayer URL(如https://relayer.testnet.zama.org)以及网关侧合约(Decryption、InputVerification)。加密所需的全局公钥正来源于 relayer 服务,这回答了架构文档中的问题:"Problem: how to get the publicKeyBytes? —— publicKeyBytes can be fetched independently"。

"零配置必须可用(Zero config must work)"的路径是:createFhevmClient({ chain, provider })→ 首次调用任意动作 → 惰性 init 自动完成 frozen context 解析与模块初始化 → 从链定义中的 relayerUrl 拉取公钥。而"显式初始化(Lazy init or Explicitly init must be supported)"则为高级用户提供两条途径:init()手动预热(用于预加载、避免延迟尖峰、SSR/受控环境,见设计原则"Power-user explicit init (this is useful for preloading, avoiding latency spikes, SSR/controlled environments)"),以及通过options预注入公钥等配置。

客户端配置FhevmOptions(见 coreFhevmClient.ts)包含:batchRpcCalls(RPC 批量调用,默认 false)、fheEncryptionKey(预置加密公钥,可避免后续 50MB 拉取)、moduleVersions(模块版本覆盖)。运行时配置FhevmRuntimeConfig(见 coreFhevmRuntime.ts)包含locateFilewasmAssetLoadModemoduleVersionsloggersingleThreadnumberOfThreadsauth。配置对象在创建时被防御性拷贝并冻结(resolveOptions返回Object.freeze结果,见 CoreFhevm-p.ts),与"扩展不得在创建时捕获配置、配置应在调用时解析"的原则一致。

典型动作:加密与解密的最小调用路径

作为设计落地的实例,看两个代表性动作(均以initPublicAction开头,印证"每个公开动作自动触发惰性初始化"):

加密单个值encryptValue(actions/encrypt/encryptValue.ts):校验value类型与地址 →await initPublicAction(fhevm)触发初始化并取得版本快照 → 调用coprocessor/encrypt生成密文 → 返回{ encryptedValue, inputProof }。批量版本encryptValues结构相同,返回encryptedValues数组(见 actions/encrypt/encryptValues.ts)。

解密单个值decryptValue(actions/decrypt/decryptValue.ts):将encryptedValue归一化为 fhEVM handle,与合约地址、密文所有者地址组成pairs,配合transportKeyPairsignedPermit交给 TKMS 解密(decryptValuesFromPairs),返回带类型的明文。配套的generateTransportKeyPair用于生成端到端传输密钥对(见 actions/decrypt/generateTransportKeyPair.ts)。

基础动作组(base.ts)还提供decryptPublicValue(s)(读取已公开的密文明文)、decryptPublicValuesWithSignatures(明文 + 可上链校验的签名证明)、signLegacyDecryptionPermit/signUnifiedDecryptionPermit(V1/V2 解密许可签名,后者需协议 API v0.14.0+ 且链上支持 unified extraData v2)、传输密钥对与许可的序列化/反序列化等。

小结:一张图理解 SDK 的生命周期

可以把整个设计收敛为一条主线:

  1. 创建(纯函数、无副作用):createFhevmBaseClientcreateCoreFhevm构造不可变核心,extend(baseActions)挂基础能力;加密/解密能力由extend(encryptActions/decryptActions)按需叠加,运行时同步填充对应模块占位符;
  2. 首次使用(惰性自动初始化):任意公开动作调用initPublicActionready返回共享的单一 Promise → init 函数集并行执行:解析并冻结 frozen context 版本快照、拉取加密公钥(加密路径)、初始化 TFHE/TKMS WASM 模块(含 worker 池);
  3. 扩展(随时允许):extend()注册新 init 函数并作废#readyPromise,下次使用自动补齐新模块初始化;
  4. 使用:动作从深拷贝的 frozen context 读取稳定版本视图,完成加密、解密、签名、公钥管理等操作。

文档中"SDK design: initialization + dependency orchestration problem"的结论在此闭环:默认路径全惰性自动,显式控制权留给高级用户。这也是 fhevm js-sdk 在"零配置可用"与"面向功率用户的显式控制"之间取得平衡的完整答案。若需深入代码,建议按以下顺序阅读:CoreFhevmRuntime-p.ts(运行时与模块槽位)→ CoreFhevm-p.ts(客户端生命周期与动作前奏)→ ethers/clients(三种工厂)→ modules/encrypt/module/init-p.ts(WASM 加载与降级细节)。

【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 11:22:19

Python景区数据分析与可视化系统开发实战

1. 项目概述与核心价值 全国景区数据分析与可视化系统是一个典型的Python数据工程实战项目&#xff0c;它通过爬取、清洗和分析全国景区数据&#xff0c;最终以交互式可视化形式呈现分析结果。这个项目特别适合以下几类人群&#xff1a; 计算机相关专业学生作为毕业设计参考 …

作者头像 李华
网站建设 2026/9/13 11:22:06

OpenCV C++ 实现 LBP 人脸识别全链路解析

简介&#xff1a;本资源是一套基于LBP算法、OpenCV与C实现的完整人脸识别系统&#xff0c;面向计算机视觉初学者及C图像处理学习者&#xff0c;解决人脸检测、特征提取与匹配识别等核心问题。压缩包共266个文件&#xff0c;含208张JPG格式人脸图像样本、19个SQLite人脸库文件&a…

作者头像 李华
网站建设 2026/9/13 11:21:12

AI教材写作:低查重率技术实现与高效工具链

1. AI教材写作的核心挑战与解决方案在高等教育和职业培训领域&#xff0c;教材编写一直是项耗时耗力的系统工程。传统方式下&#xff0c;编写一本20万字左右的专业教材&#xff0c;通常需要3-5位专家耗时6-12个月。而AI技术的介入&#xff0c;正在彻底改变这个工作流程。1.1 查…

作者头像 李华
网站建设 2026/9/13 11:20:52

FastAPI内存字典应用与线程安全实践

1. 内存字典在FastAPI中的核心价值当我们需要在FastAPI应用中处理临时状态数据时&#xff0c;内存字典往往是最直接有效的解决方案。不同于传统数据库方案&#xff0c;内存字典将数据完全保存在RAM中&#xff0c;这使得它的读写速度可以达到微秒级别。我在实际项目中发现&#…

作者头像 李华