fhEVM SDK 用户解密 API 设计详解:从userDecrypt到FhevmWalletClient的三层封装
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
fhEVM 的 JavaScript SDK(sdk/js-sdk)在其既有底层userDecrypt函数之上,设计了一套面向用户解密场景的三层便利 API。本文以设计草案 sdk/js-sdk/notes/DRAFT-API-v1.md 为核心,结合仓库中真实的类型定义、底层实现与模块源码,系统讲解FhevmAccount(身份)、FhevmUserDecryptionPermit(授权)与FhevmWalletClient(绑定身份的解密客户端)三个新抽象的设计动机、内部实现与端到端用法。读完本文,你将掌握如何在fhevmClient之上组装"密钥自行托管、授权按次签发、解密随调随传"的完整用户解密链路。
三层架构:为什么在userDecrypt之上再做封装
草案开篇即明确了整体分层,底层能力保持不变,上层逐步收敛心智负担:
Layer 0 (unchanged): userDecrypt(fhevmClient, rawEIP712Params) Layer 1 (new): FhevmAccount, FhevmUserDecryptionPermit Layer 2 (new): FhevmWalletClient- Layer 0(原样保留):面向高级用法的裸
userDecrypt函数,调用方需要自行组装 EIP-712 参数、持有原始 KMS 私钥、逐次手工签名授权,要求对协议细节有完整把握; - Layer 1:把"你是谁"(
FhevmAccount)与"你能解什么"(FhevmUserDecryptionPermit)两个独立概念实体化,让身份与授权可被类型系统约束、可被工厂函数校验、可被复用; - Layer 2:把"身份 + 客户端"绑定成
FhevmWalletClient,日常解密只需"传 permit + 传 handle 对",调用方不再接触任何 KMS 私钥与 EIP-712 细节。
草案同时强调,这些新层只是"convenience concepts"(便利抽象),它们最终全部委托给 Layer 0 的既有函数,不会在协议层引入任何新机制,因此对现有网络协议与后端 relayer 完全透明。
Layer 1 之FhevmAccount:FHE 世界中的身份
设计约束
FhevmAccount回答"在 FHE 世界中你是谁"这一问题:它把用户的以太坊地址与其 KMS 私钥配对成一个不可变对象。草案为其定义了四条硬性约束:
- KMS 私钥绝不自动生成——私钥必须由用户自己创建并显式传入,SDK 不负责托管或派生;
- 不可变——内部使用私有字段(
#fields),仅通过只读 getter 暴露; kmsPrivateKey不出现在公开类型上——消费方代码只能读account.userAddress,永远无法触达私钥;- 只能通过 SDK 的工厂函数创建——工厂负责输入校验,杜绝手工拼装出非法对象。
公开类型与私有实现
公开类型定义在core/types下(草案写作 core/types/fhevmAccount.ts,当前仓库的core/types目录按能力拆分为 fhevmClient.ts、coreFhevmClient.ts 等文件,读者可在 sdk/js-sdk/src/core/types 下找到对应声明):
type FhevmAccount = { readonly userAddress: ChecksummedAddress; };kmsPrivateKey被有意从公开类型中移除。私有实现FhevmAccountImpl则沿用仓库中TkmsPrivateEncKeyMlKem512Impl的模式——Symbol 键 + token 校验的静态访问器(草案路径为core/actions/decrypt/user/FhevmAccount-p.ts):
const FHEVM_ACCOUNT_TOKEN = Symbol("FhevmAccount.token"); const GET_KMS_PRIVATE_KEY = Symbol("FhevmAccount.getKmsPrivateKey"); class FhevmAccountImpl implements FhevmAccount { readonly #kmsPrivateKey: TkmsPrivateKey; readonly #userAddress: ChecksummedAddress; constructor(parameters: { readonly kmsPrivateKey: TkmsPrivateKey; readonly userAddress: ChecksummedAddress; }) { this.#kmsPrivateKey = parameters.kmsPrivateKey; this.#userAddress = parameters.userAddress; } public get userAddress(): ChecksummedAddress { return this.#userAddress; } // Symbol-keyed — invisible to consumers, accessible only to SDK internals public static GET_KMS_PRIVATE_KEY: TkmsPrivateKey { if (token !== FHEVM_ACCOUNT_TOKEN) { throw new Error("Unauthorized"); } if (!(account instanceof FhevmAccountImpl)) { throw new Error("Unauthorized"); } return account.#kmsPrivateKey; } }这种"Symbol 键 + token 双重校验"手法并非孤例:仓库中TkmsPrivateEncKeyMlKem512Impl同样通过私有 token(见 core/modules/decrypt/module/api-p.ts 中的PRIVATE_TKMS_LIB_TOKEN)隔离 WASM 原生对象。SDK 内部代码(如userDecrypt、FhevmWalletClient)通过下述方式取回私钥:
const kmsPrivateKey = FhevmAccountImplGET_KMS_PRIVATE_KEY;外部调用方既看不到这个 Symbol,也没有 token,因而在类型层面与运行层面都无法读取私钥。
工厂函数与类型守卫
工厂与守卫放在FhevmAccount.ts(草案路径core/actions/decrypt/user/FhevmAccount.ts),完全遵循isDecryptionPermit的instanceof守卫模式:
import { FhevmAccountImpl } from "./FhevmAccount-p.js"; import { assertIsChecksummedAddress } from "../../../base/address.js"; import { isTkmsPrivateKey } from "../../../modules/tkms/module.js"; // new export needed // Type guard — guarantees value is a valid FhevmAccount created by the SDK function isFhevmAccount(value: unknown): value is FhevmAccount { return value instanceof FhevmAccountImpl; } // Factory — validates inputs, wraps into immutable FhevmAccountImpl function createFhevmAccount(parameters: { readonly kmsPrivateKey: TkmsPrivateKey; readonly userAddress: string; }): FhevmAccount { assertIsChecksummedAddress(parameters.userAddress, {}); if (!isTkmsPrivateKey(parameters.kmsPrivateKey)) { throw new Error("Invalid kmsPrivateKey: not a valid TkmsPrivateKey created by the SDK"); } return new FhevmAccountImpl({ kmsPrivateKey: parameters.kmsPrivateKey, userAddress: parameters.userAddress, // already validated as ChecksummedAddress }); }要点:
createFhevmAccount接受kmsPrivateKey参数(因为私钥必须由用户提供),但创建完成后FhevmAccount不再暴露它;- 地址走
assertIsChecksummedAddress校验(仓库中对应 core/base/address.ts 一类的地址断言工具),私钥走新的isTkmsPrivateKey守卫; - 它不依赖
fhevmClient,是纯数据包装 + 输入校验,这也意味着它可以在没有网络、没有链上下文的环境中构造。
TkmsPrivateKey与新增守卫isTkmsPrivateKey
TkmsPrivateKey是一个带品牌(brand)标记的声明类型,真实载体是私有类TkmsPrivateEncKeyMlKem512Impl。仓库中 core/types/tkms-p.ts 的声明为:
export declare const TkmsPrivateKeyBrand: unique symbol; export type TkmsPrivateKey = { readonly [TkmsPrivateKeyBrand]: never; readonly tkmsVersion: TkmsVersion; free(): void; };TkmsPrivateKey实例由 WASM 密钥生成路径产出:底层generateTkmsPrivateKey调用kmsLib.ml_kem_pke_keygen()并包装成TkmsPrivateEncKeyMlKem512Impl(见 core/modules/decrypt/module/api-p.ts 中generateTkmsPrivateKey的实现),密钥体系基于ML-KEM-512后量子加密算法。由于instanceof无法作用在带品牌的对象上,草案建议在core/modules/tkms/module.ts新增导出,沿用isDecryptionPermit的同一模式:
// in core/modules/tkms/module.ts export function isTkmsPrivateKey(value: unknown): value is TkmsPrivateKey { return value instanceof TkmsPrivateEncKeyMlKem512Impl; }Layer 1 之FhevmUserDecryptionPermit:你可以解什么
授权语义
FhevmUserDecryptionPermit回答"你能解密什么":它是一份已签名的 EIP-712 消息,授权该账户对应的 KMS 公钥发起解密请求。其核心语义:
- 纯数据、无方法——只是一个类型化对象;
- 可复用——在有效性窗口内可跨多次解密调用重复使用;
- 作用域受限——限定到具体合约地址(最多 10 个);
- 限时——由
startTimestamp + durationDays决定(最长 365 天); - 绑定密钥——通过 permit 内嵌的公钥绑定到某一把 KMS 私钥。
type FhevmUserDecryptionPermit = { readonly eip712: KmsUserDecryptEIP712; readonly signature: Bytes65Hex; readonly signerAddress: ChecksummedAddress; };从仓库既有类型看,签名授权在协议演进中经历了 V1/V2 两个形态:core/types/signedDecryptionPermit.ts 定义了SignedDecryptionPermitV1(协议 v13 及以下,自签与委托是两种 EIP-712 形状)与SignedDecryptionPermitV2(协议 v14 及以上,统一为KmsUserDecryptEip712V2形状,signature放宽为BytesHex以承载 ERC-1271 签名 blob)。V2 消息体校验(core/kms/createKmsUserDecryptEip712V2.ts)要求userAddress、publicKey、allowedContracts(校验和地址数组)、startTimestamp、durationSeconds、extraData等字段齐全,且extraData必须携带 v2 或更新版本的编码。这些细节解释了草案中"permit 是自描述工件"的论断:permit 自身携带版本,路由时以 permit 版本为准,而非链上解析出的协议版本。
工厂一:用钱包签名
signFhevmUserDecryptionPermit接受WalletSigner作为参数,签名器永远不被存储在任何结构中:
async function signFhevmUserDecryptionPermit( signer: WalletSigner, fhevmClient: FhevmClient, params: { account: FhevmAccount; contractAddresses: readonly string[]; durationDays: number; startTimestamp?: number; // defaults to now extraData?: string; // defaults to "0x" }, ): Promise<FhevmUserDecryptionPermit>;内部执行三步:
- 通过
fhevmClient.tkms.getTkmsPublicKeyHex()从account.kmsPrivateKey派生publicKey; - 从
fhevmClient.chain提取chainId与verifyingContractAddressDecryption(即解密验证合约地址); - 在底层调用既有的
signDecryptionPermit(signer, fhevmClient, ...)。
仓库中的signDecryptionPermit动作(core/actions/base/signDecryptionPermit.ts)要求contractAddresses、startTimestamp、durationSeconds、signerAddress、signer(NativeSigner)与transportKeyPair(端到端传输密钥对),并返回SignedDecryptionPermit。可见新工厂只是把"KMS 公钥 + 链信息 + 账户私钥"自动装配进了既有签名流程。
工厂二:从原始组件恢复
当 permit 已在别处签名完毕时,可用createFhevmUserDecryptionPermit从原始组件恢复:
function createFhevmUserDecryptionPermit( fhevmClient: FhevmClient, params: { signerAddress: string; eip712: KmsUserDecryptEIP712; signature: Bytes65Hex; }, ): Promise<FhevmUserDecryptionPermit>;它在内部调用既有的createDecryptionPermit(fhevmClient, ...),校验签名有效性后才返回新对象,确保任何进入系统的高层 permit 都是真实可信的。
Layer 2 之FhevmWalletClient:绑定身份的解密入口
为什么不存储 permit
FhevmWalletClient是一个绑定了FhevmAccount的FhevmClient:
- 持有
FhevmClient与FhevmAccount的引用; - 不存储任何
WalletSigner; - permit 按次传入,不存储——因为:
- permit 是短暂的、限时的;
- 不同的 permit 可覆盖不同的合约集合;
- wallet client 的存活期可以比单个 permit 更长。
type FhevmWalletClient = { readonly account: FhevmAccount; userDecrypt(params: { permit: FhevmUserDecryptionPermit; handleContractPairs: ReadonlyArray<{ handle: FhevmHandle; contractAddress: ChecksummedAddress; }>; options?: RelayerFetchOptions; }): Promise<readonly DecryptedFhevmHandle[]>; };工厂函数非常轻量,仅做绑定:
function createFhevmWalletClient( fhevmClient: FhevmClient, params: { account: FhevmAccount }, ): FhevmWalletClient;userDecrypt如何映射到底层函数
FhevmWalletClient.userDecrypt不重复实现解密逻辑,而是把高层参数解构后转发给 Layer 0 的userDecrypt:
// Inside FhevmWalletClient.userDecrypt(params): return userDecrypt(this.#fhevmClient, { tkmsPrivateKey: this.#account.kmsPrivateKey, handleContractPairs: params.handleContractPairs, userDecryptEIP712Signer: params.permit.signerAddress, userDecryptEIP712Message: { contractAddresses: params.permit.eip712.message.contractAddresses, startTimestamp: params.permit.eip712.message.startTimestamp, durationDays: params.permit.eip712.message.durationDays, extraData: params.permit.eip712.message.extraData, }, userDecryptEIP712Signature: params.permit.signature, options: params.options, });注意this.#account.kmsPrivateKey一行——这正是FhevmAccountImpl的 Symbol 访问器在 SDK 内部的用武之地:上层无需也不应直接持有私钥,一切由封装代劳。
底层调用链印证
从仓库源码可以完整还原这条委托链(这也解释了为什么草案反复强调"既有函数原样保留"):
- 动作层:core/actions/decrypt/decryptValuesFromPairs.ts 对每一对
handle/contractAddress做地址断言与 checksum 规范化,组装ownerAddress后交给 KMS 层; - KMS 路由层:core/kms/decryptValuesFromPairs.ts 按permit 自身版本路由——
version === 1走fetchKmsSigncryptedSharesV1(对应 relayer 的v2/user-decrypt端点),否则走fetchKmsSigncryptedSharesV2(对应v3/user-decrypt端点);随后用transportKeyPair调用decryptKmsSigncryptedShares解出明文; - 数据流:relayer 返回的
KmsSigncryptedShares携带着经 KMS 后量子密钥签密的份额,SDK 本地使用传输密钥对解密重建TypedValue。
也就是说,三层 API 只是在"权限/身份"语义上做了收敛,核心的签密份额获取与本地重建路径完全复用既有实现,协议兼容性因此得到保证。
端到端用法:完整代码示例
草案给出的完整示例覆盖了从初始化到多 permit 切换的全部 6 个步骤:
import { createEthersFhevmClient } from "@fhevm/sdk/ethers"; import { addRelayer, addTkms, createFhevmAccount, signFhevmUserDecryptionPermit, createFhevmWalletClient, } from "@fhevm/sdk"; import { mainnet } from "@fhevm/sdk/chains"; // --- Setup (existing) --- const fhevmClient = createEthersFhevmClient({ chain: mainnet, provider }); addRelayer(fhevmClient); await addTkms(fhevmClient); // --- New API --- // 1. Generate key (user manages this) const kmsPrivateKey = fhevmClient.tkms.generateTkmsPrivateKey(); // 2. Create account (pure data) const account = createFhevmAccount({ kmsPrivateKey, userAddress: "0xAbC1234...", }); // 3. Sign a permit (walletSigner is an argument, not stored) const permit = await signFhevmUserDecryptionPermit(walletSigner, fhevmClient, { account, contractAddresses: ["0xDef5678..."], durationDays: 1, }); // 4. Create wallet client (binds fhevmClient + account) const walletClient = createFhevmWalletClient(fhevmClient, { account }); // 5. Decrypt — permit passed per-call const results = await walletClient.userDecrypt({ permit, handleContractPairs: [ { handle: h1, contractAddress: "0xDef5678..." }, { handle: h2, contractAddress: "0xDef5678..." }, ], }); // 6. Same wallet client, different permit (different contracts, different validity) const permit2 = await signFhevmUserDecryptionPermit(walletSigner, fhevmClient, { account, contractAddresses: ["0xGhi9012..."], durationDays: 7, }); const results2 = await walletClient.userDecrypt({ permit: permit2, handleContractPairs: [{ handle: h3, contractAddress: "0xGhi9012..." }], });关键要点逐条拆解:
- 第 1 步:私钥生成由用户主动发起(
generateTkmsPrivateKey),SDK 绝不自动生成——这是"用户自己管理 KMS 私钥"原则的直接体现; - 第 2 步:
createFhevmAccount是纯数据构造,此处私钥仅作为入参传入,随后即被封存于#kmsPrivateKey; - 第 3 步:签名器以参数形式传入,签名完成后不残留任何引用;
durationDays: 1表示 permit 有效期一天; - 第 4 步:wallet client 只绑定
fhevmClient + account,不绑定 permit、不绑定 signer; - 第 5/6 步:同一个 wallet client 可以配合不同 permit 使用——不同合约集合、不同有效期,实现"一个客户端、多份授权"的灵活模型。这也正是 permit 按次传入而非存储的根本原因。
底层函数速查表(原样保留)
便利层最终都委托给下表这些既有函数,高级场景需要裸 EIP-712 控制时仍可直接调用:
| 函数 | 文件 |
|---|---|
userDecrypt(fhevmClient, params) | core/actions/decrypt/user/userDecrypt.ts |
signDecryptionPermit(signer, fhevmClient, params) | core/actions/base/signDecryptionPermit.ts |
createDecryptionPermit(fhevmClient, params) | core/actions/decrypt/user/createDecryptionPermit.ts |
说明:草案中给出的部分文件路径(如core/actions/decrypt/user/目录)在当前仓库中尚未落地为最终实现(该 API 仍处于草稿设计阶段),上表将signDecryptionPermit映射到仓库中实际存在的 core/actions/base/signDecryptionPermit.ts;读者可结合 sdk/js-sdk/notes/ 目录下的 DRAFT-API-v2.md、DRAFT-API-FHEVM-ACCOUNT.md 等草案,跟踪该设计的后续演进。
设计小结
| 抽象 | 回答的问题 | 关键特征 | 底层复用 |
|---|---|---|---|
FhevmAccount | 你是谁 | 不可变、私钥不暴露、工厂校验、纯数据 | — |
FhevmUserDecryptionPermit | 你能解什么 | 限时(≤365 天)、限合约(≤10 个)、可复用、已签名 | signDecryptionPermit/createDecryptionPermit |
FhevmWalletClient | 日常怎么解 | 绑定 client+account、permit 按次传入、不存 signer | userDecrypt→decryptValuesFromPairs→fetchKmsSigncryptedSharesV1/V2+decryptKmsSigncryptedShares |
这套三层设计在安全边界(KMS 私钥通过 Symbol/token 隔离、signer 不落存储、permit 有有效期与合约范围)与易用性(每次调用只传 permit 与 handle 对)之间取得了平衡;而其"新层全部委托既有底层"的原则,保证了向后兼容与协议演进空间。
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考