
fhevm JS SDK 冻结上下文迁移以 FhevmClientFrozenContext 统一版本解析的工程实践【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm导读本文围绕 fhevm 仓库中 sdk/js-sdk/FROZEN_CONTEXT_MIGRATION_PLAN.md 这一内部迁移规划文档深入解析 fhevm JS SDK 中一项关键架构改造将FhevmClientFrozenContext客户端冻结上下文确立为 protocol / PubKey-CRS / TFHE / TKMS / host-contract 等所有已解析版本信息的唯一事实来源single source of truth并逐步删除历史遗留的按版本分路 memo 机制。读完本文你将理解 fhevm JS SDK 为什么需要一个“冻结上下文”、它如何保证多版本链上解析的一致性、迁移所涉及的消费方图谱init 函数、asFhevmWith*守卫、KMS shares 拉取、公共 getter以及如何通过六个步骤分阶段安全落地、最终用tsc --noEmit验证的完整流程。对于任何需要维护“多版本并存、单次捕获”语义的 SDK 架构这也是一份可复用的重构方法论参考。一、背景为什么需要“冻结上下文”fhevm JS SDK 在链上解析版本信息时天然面临一个一致性问题一个 FHEVM 操作通常依赖多组版本数据——原始 host-contract 版本ACL、KMSVerifier、InputVerifier、ProtocolConfig 的链上getVersion()结果由它们派生出的 protocol 版本、PubKey/CRS 版本以及 TFHE加密模块与 TKMS密钥管理模块的 WASM 模块版本。在引入冻结上下文之前这些版本“从多个地方读取、生命周期各不相同”protocol / tfhe / tkms 版本在客户端上每个客户端解析一次并冻结而 host-contract 的getVersion()读取是TTL 缓存的——这意味着它们可能在同一客户端内漂移出同步状态。例如一次 KMSVerifier 读取的缓存刷新可能晚于已加载 WASM 所依据的 protocol 版本导致加密/解密操作基于一套“混合时刻”的版本基线执行。这正是 sdk/js-sdk/src/core/types/fhevmClientFrozenContext-p.ts 中注释所描述的核心痛点。FhevmClientFrozenContext正是为此而生把需要的版本子集一次解析、立即冻结然后通过context参数贯穿所有内部函数而不是逐个传递TfheVersion/TkmsVersion/ protocol-version 参数让所有分支都基于同一份自洽快照决策。从迁移规划文档的角度看目标非常明确让FhevmClientFrozenContext成为所有已解析版本protocol、PubKey/CRS、TFHE、TKMS、host-contract 版本的唯一事实来源并删除当前会导致漂移的、按版本分路的平行 memo 机制。二、目标态模型End-state model迁移完成后的目标态由三个核心能力构成文档与源码相互印证1.resolveFhevmClientFrozenContext(fhevm)单次完成完整版本基线的解析该函数一次性解析客户端的完整版本基线已实现于 sdk/js-sdk/src/core/frozenContext/resolveFhevmClientFrozenContext-p.ts。从源码看它在一个批次内完成从链配置读取 ACL、InputVerifier、KMSVerifier以及 v0.14 部署才存在的 ProtocolConfig的地址通过executeWithBatching将多个getHostContractVersion调用合并为一个 RPC 批次受fhevm.options.batchRpcCalls控制用protocolContextFromAclVersion纯派生得到 protocol PubKey/CRS用hyperWasmResolveTfheModuleVersion/hyperWasmResolveTkmsModuleVersion查表得到 TFHE、TKMS 模块版本最后通过createFhevmClientFrozenContext组装成不可变上下文。其中有一处值得注意的设计决策所有链上读取故意不固定到某个区块号。依据是——所有协议合约ACL、KMSVerifier、InputVerifier、ProtocolConfig 等在同一笔原子升级交易中整体升级getVersion()会同步跳变因此一次批量读取要么全部观察到升级前版本、要么全部观察到升级后版本永远不会出现“一半一半”的混合状态。单次捕获完整基线正是让版本保持自洽的关键也正因如此可以完全跳过区块号固定及其重组织reorg暴露面。2.ensureFrozenContext(fhevm)一次性解析、并发去重、落盘缓存该函数实现于 sdk/js-sdk/src/core/frozenContext/ensureFrozenContext-p.ts是resolveFhevmClientFrozenContext的幂等封装也是整个迁移的核心同步点先查已存数据getFrozenContext(fhevm)已存在则直接返回extend()后的重复调用是无操作并发去重将解析动作挂在客户端持有的单一 in-flight promisegetFrozenContextPromise/setFrozenContextPromise上所有并发调用方共享同一次解析。由于init()会用Promise.all并发扇出多个_init*函数_initBase/_initEncrypt/_initDecrypt无法同步读到已解析上下文——第一个到达者启动唯一解析其余await同一 promise这就保证了所有 tier 都从同一份快照分支成功后落盘setFrozenContext将结果存为客户端上的数据后续读取变为同步失败可重试临时 promise 在解析结束无论成败后被清除一次瞬时 RPC 失败不会污染后续尝试——因为冻结上下文解析是纯链上读取、没有不可重置的副作用这与一次性 WASM 模块启动不同。所有状态都保存在客户端实例上getFrozenContext/getFrozenContextPromise从不放模块作用域——因此状态与客户端生命周期绑定可在服务端组件中按请求安全使用。文档将ensureFrozenContext定位为懒加载initTfheModule的“冻结上下文版”——幂等、点用即取、出错可重试。3._init*函数退化为 eager 预取迁移后_initBase/_initEncrypt/_initDecrypt不再承担“解析并 memo 版本”的职责变成急切的预取没有任何东西依赖它们必须先运行action 也可以惰性解析。当前源码已经反映了这一最终形态// _initBase —— sdk/js-sdk/src/core/clients/decorators/base.ts async function _initBase(fhevm: FhevmBaseFhevmChain): Promisevoid { // Resolve the frozen version basis once and cache it on the client. await ensureFrozenContext(fhevm); }// _initEncrypt —— sdk/js-sdk/src/core/clients/decorators/encrypt-p.ts export async function _initEncrypt(fhevm: FhevmBaseFhevmChain): Promisevoid { const f asFhevmClientWith(fhevm, encrypt); const frozen await ensureFrozenContext(f); await Promise.all([ // Prefetch the global FheEncryptionKey in bytes format fetchFheEncryptionKeyBytes(f, { fhevmContext: cloneFhevmClientFrozenContext(frozen) }), f.runtime.encrypt.initTfheModule({ tfheVersion: frozen.tfheVersion }), ]); }// _initDecrypt —— sdk/js-sdk/src/core/clients/decorators/decrypt-p.ts export async function _initDecrypt(fhevm: FhevmBaseFhevmChain): Promisevoid { const f asFhevmClientWith(fhevm, decrypt); const frozen await ensureFrozenContext(f); await f.runtime.decrypt.initTkmsModule({ tkmsVersion: frozen.tkmsVersion }); }注意_initEncrypt中一个细节fetchFheEncryptionKeyBytes传入的是cloneFhevmClientFrozenContext(frozen)的深拷贝而不是 live 实例——这保证预取动作在整个异步期间看到的是稳定版本视图不受客户端后续可能的上下文替换影响详见 sdk/js-sdk/src/core/runtime/CoreFhevm-p.ts 中initPublicAction的注释公共 action 的固定三步序言就是“懒幂等 init → 读取冻结上下文 → 返回深拷贝”。最终态中所有版本读取都走冻结上下文——不再有getResolved*/setResolved*不再有#protocolVersion/#tfheVersion/#tkmsVersion字段。三、什么保持不变do NOT touch迁移规划文档明确划出了禁止触碰的三块内容理解它们有助于把握迁移边界globalFheEncryptionKeyCache位于 sdk/js-sdk/src/core/key/FheEncryptionKeyCache-p.ts——存放 pub key / CRS 字节约 50MB重按 id / relayer 寻址与版本一致性正交不属于本次迁移范围cachedTfhe/TkmsModulePromiseByVersion模块初始化的 promise 缓存——保留只是改为从冻结上下文喂入版本号纯派生函数protocolContextFromAclVersion、pubKeyCrsVersionFromProtocolVersion位于 sdk/js-sdk/src/core/runtime/ProtocolVersionResolver-p.ts——冻结上下文解析器自身就在使用它们。四、消费者图谱迁移的真正作用面版本状态并非只被 init 函数读取文档指出实际有四个消费集群_initBase/_initEncrypt/_initDecrypt——原先通过ensureResolvedProtocolVersion/resolveFhevmTfheVersion/resolveFhevmTkmsVersionsetResolved*读写版本asFhevmWithTfheVersion/asFhevmWithTkmsVersionCoreFhevm-p.ts——内部读取getResolved*被7 个 action 文件使用加密侧encryptValue、encryptValues、generateZkProof解密侧decryptValue、decryptValues、decryptValuesFromPairs、generateTransportKeyPairfetchKmsSigncryptedSharesV1-p.ts/V2-p.ts——直接调用resolveFhevmTkmsVersion(context)不经过 init 函数。在 sdk/js-sdk/src/core/kms/fetchKmsSigncryptedSharesV1-p.ts 中可以看到迁移完成后的形态函数签名携带fhevmContext: FhevmClientFrozenContext直接const tkmsVersion fhevmContext.tkmsVersion;取值公共 getterclient.protocolVersion/tfheVersion/tkmsVersion——原先读取#…Version字段测试与asFhevmWith*都依赖它们。这四类消费者必须同步迁移否则会出现部分读取走冻结上下文、部分读取走旧字段的半迁移状态。冻结上下文的两个重要不变量在 sdk/js-sdk/src/core/types/fhevmClientFrozenContext-p.ts 中类型定义明确了两条必须长期维持的设计约束是任何基于此上下文扩展代码的硬性前提公共 action 绝不能调用另一个公共 action冻结上下文在某个公共以fhevm为首参的action 的入口处恰好解析一次只通过context参数下传给内部接收context的helper。正因为不存在“action 调 action”的嵌套才能保证上下文只构建一次、永不重建无需任何重入/复用机制。如果某个公共 action 需要复用另一个的行为应当把共享逻辑抽成接收context的内部 helper由两者共同调用。唯一例外是host链发现 tierresolveFhevmConfig会扇出到兄弟 host action但 host action 在链解析之前运行、从不携带冻结上下文因此该不变量不受影响。范围刻意收窄冻结上下文只持有版本基线准不可变、仅在合约升级时变化刻意不持有按区块可变的状态KMS context id / epoch、签名者集合、ACL 授权。可变状态生命周期不同、在使用处实时解析。把两类状态分开正是该快照能够无视 reorg 的原因。部分解析partial resolution与快速失败一个上下文可能只携带某操作所需的那部分版本——加密路径解析tfheVersion及其派生的 protocol/ACL 版本但从不解析tkmsVersion公共解密路径则相反。只解析需要的子集能让链上getVersion()读取次数最少。因此普通访问器protocolVersion、tfheVersion、hostContractVersion(name)、pubKeyCrsVersion、tkmsVersion、protocolContext在版本未解析时直接 throw让作用域误配立即失败而不是带着缺失值静默运行探测用has*谓词hasProtocolVersion、hasTfheVersion、hasTkmsVersion、hasHostContractVersion(name)、hasProtocolContext等或try*访问器tryTfheVersion等则不抛错、返回undefined。实现层面FhevmClientFrozenContextImplsdk/js-sdk/src/core/frozenContext/fhevmClientFrozenContext-p.ts通过以下手段保证不可变性私有构造 token只能通过createFhevmClientFrozenContext构造构造时对 host-contract 版本做防御性拷贝并Object.freeze防止调用方后续改动污染快照实例本身也Object.freeze类与原型均被Object.freeze阻止原型污染与外部子类化带 unique symbol brandfhevmClientFrozenContextBrand防止结构相似普通对象冒充血统提供cloneFhevmClientFrozenContext深拷贝经toJSON()导出全量状态后重建保证克隆与源零引用共享并配套isFhevmClientFrozenContext/assertIsFhevmClientFrozenContext运行时校验。五、迁移步骤六步渐进、最后统一编译迁移规划文档给出了一套**刻意设计为“中间态破损”**的分步策略核心纪律是步骤 1–5 之间不要编译只在步骤 6 编译。中间态是故意损坏的——一旦步骤 1 停止调用setResolvedTfheVersion公共.tfheVersion在步骤 5 重新指向之前会读到从未被赋值的字段。Step 1 — 将所有读写方迁移到冻结上下文1a. Init 函数src/core/clients/decorators/base.ts、encrypt-p.ts、decrypt-p.ts——即前文展示的三段代码。导入层面删除ensureResolvedProtocolVersion/resolveFhevmTfheVersion/resolveFhevmTkmsVersion与setResolvedTfheVersion/setResolvedTkmsVersion新增ensureFrozenContext保留asFhevmClientWith、fetchFheEncryptionKeyBytes。1b.asFhevmWith*CoreFhevm-p.ts约 453–478 行——把内部守卫从getResolved*Version(f) undefined重新指向getFrozenContext(f)/getFrozenContext(f)?.hasTfheVersion等。7 个 action 调用点保持不变——这正是“先改守卫、不改调用方”的兼容性技巧。1c. KMS shares——fetchKmsSigncryptedSharesV1-p.ts、V2-p.ts中把resolveFhevmTkmsVersion(context)替换为(await ensureFrozenContext(context)).tkmsVersion。从当前源码看V1 已经完成迁移改为从fhevmContext直接取tkmsVersion可作为落地样例对照。Step 2 — 验证零残留调用方用文档给出的两条 grep 命令扫描确认旧机制只剩定义本身、再无调用者grep -rn ensureResolvedProtocolVersion\|resolveFhevmTfheVersion\|resolveFhevmTkmsVersion\|resolveFhevmProtocolVersion src --include*.ts | grep -v \.test\.ts grep -rn getResolvedProtocolVersion\|getResolvedTfheVersion\|getResolvedTkmsVersion\|setResolvedProtocolVersion\|setResolvedTfheVersion\|setResolvedTkmsVersion src --include*.ts | grep -v \.test\.ts两条命令都必须只返回定义本身它们即将被删除。以当前仓库状态核对旧版getResolved*/setResolved*符号仅在 sdk/js-sdk/src/core/runtime/CoreFhevm-p.ts 中以注释形式残存冻结上下文相关符号GET_FROZEN_CONTEXT/SET_FROZEN_CONTEXT/GET_FROZEN_CONTEXT_PROMISE/SET_FROZEN_CONTEXT_PROMISE已全面接管。Step 3 — 删除旧机制CoreFhevm-p.ts删除导出的getResolvedProtocolVersion、getResolvedTfheVersion、getResolvedTkmsVersion、setResolvedProtocolVersion、setResolvedTfheVersion、setResolvedTkmsVersionresolveFhevmVersions-p.ts删除ensureResolvedProtocolVersion、resolveFhevmProtocolVersion、_resolveFhevmProtocolContext、resolveFhevmTfheVersion、resolveFhevmTkmsVersion若无其他内容整个文件一并删除保留它们曾使用的纯派生函数存于ProtocolVersionResolver-p.ts。Step 4 — 删除 symbol 机制CoreFhevm-p.ts中删除符号GET_PROTOCOL_VERSION、SET_PROTOCOL_VERSION、GET_TFHE_VERSION、SET_TFHE_VERSION、GET_TKMS_VERSION、SET_TKMS_VERSION对应的类内Object.defineProperties条目以及Get/SetProtocol/Tfhe/TkmsVersionFn类型别名。保留*_FROZEN_CONTEXT与*_FROZEN_CONTEXT_PROMISE机制。当前源码中这些旧符号已经以注释形式挂起见CoreFhevm-p.ts的// [GET_PROTOCOL_VERSION]: {...}区块并明确标注了旧字段各自的“防冲突守卫”逻辑protocol 版本不可重复设置为不同值等可作为删除时核对行为差异的参考。Step 5 — 重新指向公共 getter 并删除字段CoreFhevm-p.tsprotocolVersion/tfheVersion/tkmsVersiongetter 改为从#frozenContext读取当上下文或对应版本缺失时抛出清晰的await client.ready提示。保留公共属性本身——client.protocolVersion在测试中有断言。当前源码中 getter 已实现为若#frozenContext undefined则抛Fhevm context has not been resolved. Await client.ready before.否则返回#frozenContext.protocolVersion等删除字段#protocolVersion、#tfheVersion、#tkmsVersion及其构造函数初始化器。Step 6 — 编译 验证npx tsc -p src/tsconfig.json --noEmit编译通过后运行一个在client.ready之后读取client.protocolVersion/getFrozenContext的定向 FHE 测试文档示例viem-common/clientBase.tests.ts确认公共 getter 与内部上下文读数一致。六、已知边界与未来方向迁移规划文档在 Notes 部分如实记录了两个边界不夸大也不回避init 期间失败的污染问题ensureFrozenContext的重试能力只通过“点用即取的懒调用 /extend()/ 直接调用”可达——若失败发生在init 过程中仍会像任何 init 失败一样污染#readyPromise因为 init 函数把可重试的 RPC 与一次性 WASM 启动捆绑在一起。文档明确标注本次迁移范围之外未来增强后续可以让 action 在使用点await ensureFrozenContext(fhevm)真正惰性 可重试取代当前同步的asFhevmWith*——但本次迁移不需要refresh()通过setFrozenContext做原子整体基线替换是一个独立、稍后的功能不在本次范围。七、迁移后的架构图景综合文档与源码迁移完成后的版本数据流可以概括为一条单向链路init() / 公共 action 首次调用 │ ▼ ensureFrozenContext(fhevm) ← 幂等、并发去重客户端持有的 promise │ ▼ resolveFhevmClientFrozenContext() ← 单批 RPC 读取 ACL/InputVerifier/KMSVerifier/ProtocolConfig │ 纯派生 protocol/PubKey-CRS/TFHE/TKMS不加载 WASM ▼ FhevmClientFrozenContext不可变快照落盘 #frozenContext │ ├──► _initEncrypt/_initDecrypt → initTfheModule / initTkmsModule喂入模块版本 ├──► 7 个 actionencryptValue / decryptValue / generateZkProof 等 ├──► fetchKmsSigncryptedSharesV1/V2 → fhevmContext.tkmsVersion └──► 公共 getter protocolVersion / tfheVersion / tkmsVersion值得一提的是 sdk/js-sdk/src/core/runtime/CoreFhevm-p.ts 中新增的initPublicAction它作为每个公共 API action 的标准序言固定执行三步——await f.ready懒幂等 init、读取getFrozenContext(f)存储的上下文缺失即视为内部不变量被破坏、cloneFhevmClientFrozenContext返回深拷贝。这与ensureFrozenContext形成了“解析一次、多次安全消费”的完整闭环解析端负责并发去重与落盘消费端负责深拷贝隔离二者配合让版本快照在任意调用顺序下都保持一致。结语FROZEN_CONTEXT_MIGRATION_PLAN.md是一份“小而锐”的内部重构蓝图它以冻结上下文为锚点通过“单次捕获、不可变快照、点用即取”三项机制彻底消除了 fhevm JS SDK 中多版本 memo 平行机制带来的漂移风险而它对“迁移作用面”四个消费集群、“中间态故意破损、最后统一编译”的纪律以及对“什么保持不变”与“已知边界”的诚实记录使其不仅是 fhevm 的内部文档更是一份值得任何多版本状态 SDK 团队借鉴的分阶段重构方法论。感兴趣的读者可以继续阅读 sdk/js-sdk/README.md 了解 SDK 整体能力或深入 sdk/js-sdk/src/core/frozenContext/ 目录逐行研读冻结上下文的三个核心实现文件。【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考