第33章:EVE Vault 钱包概述——zkLogin 原理与设计
学习目标:理解 EVE Vault 是什么,它为什么使用 zkLogin 而不是传统私钥,以及 zkLogin 的完整密码学工作原理。
状态:源码导读。密码学细节以 EVE Vault 当前实现和 Sui zkLogin 机制为准,正文偏重架构理解。
最小调用链
FusionAuth/OAuth 登录 -> 回调拿 code -> 交换 token -> 派生 zkLogin 地址 -> 保存登录态 -> 钱包可签名
对应代码目录
1. EVE Vault 是什么?
EVE Vault 是 EVE Frontier 的专属 Chrome 浏览器扩展钱包,基于以下技术栈构建:
| 层 | 技术 | 用途 |
|---|---|---|
| 扩展框架 | WXT + Chrome MV3 | 跨浏览器扩展构建 |
| UI 框架 | React + TanStack Router | 弹窗和审批页面 |
| 状态管理 | Zustand + Chrome Storage | 持久化用户状态 |
| 区块链 | Sui Wallet Standard | dApp 发现与交互协议 |
| 身份认证 | EVE Frontier FusionAuth (OAuth) | EVE 游戏账户登录 |
| 地址派生 | Sui zkLogin + Enoki | 从 OAuth 身份派生链上地址 |
核心设计理念:玩家无需管理私钥——用 EVE Frontier 的游戏账户登录,自动获得 Sui 区块链地址。
这章最重要的不是记住一串密码学名词,而是先看清它到底在替谁解决什么问题:
- 对玩家:降低钱包门槛
- 对 Builder:降低 onboarding 成本
- 对产品:把“游戏身份”和“链上身份”尽量收拢成一条体验链
2. 为什么不用传统私钥?
普通 Sui 钱包的痛点:
❌ 玩家需要保管助记词(12-24个单词)
❌ 助记词泄露 = 资产全损
❌ 游戏账户与链上身份是两套独立系统
❌ 新用户的学习门槛极高
EVE Vault 的方案:
✅ 用 EVE Frontier 游戏账户(邮箱登录)直接对应链上地址
✅ 地址由零知识证明(zkLogin)确定性派生
✅ 即使 OAuth token 被盗,需要 ZK 证明才能签名
✅ 游戏账户 = 链上身份,用户体验无缝衔接
这里真正的产品意义
不是“更先进”,而是让大量原本不会装传统钱包的玩家也能进入链上交互。
对 EVE Builder 来说,这直接影响:
- 连接流程能有多短
- 首次使用的心理成本有多低
- 赞助交易能否把最后一层摩擦也削掉
3. zkLogin 原理精讲
3.1 核心概念
zkLogin 是 Sui 原生支持的一种签名方案,它将 OAuth 身份与区块链地址绑定:
【传统钱包】
私钥 k → 公钥 PK → 地址 A
签名 = ed25519_sign(k, tx)
【zkLogin 钱包】
JWT (OAuth token) + Ephemeral Key → ZK Proof → 签名
↑
这个证明证明了"我持有有效的 JWT 且 JWT 对应地址 A"
3.2 zkLogin 地址公式
zkLogin_address = hash(
iss, // JWT 颁发者(如 "https://auth.evefrontier.com")
sub, // 用户的唯一 ID(EVE 账户 ID)
aud, // OAuth 客户端 ID
user_salt, // Enoki 保存的用户盐值(防止 sub 泄漏关联链上身份)
)
关键安全性:攻击者即使知道你的 EVE 账户 ID,没有 user_salt 就无法算出你的链上地址。user_salt 由 Enoki(Mysten Labs 的 zkLogin 服务)负责保管。
zkLogin 最值得抓住的直觉
不是“我有一个长期私钥”,而是:
我用 OAuth 身份证明“我是谁”,再用临时密钥证明“这次操作是我授权的”。
这也是为什么 zkLogin 体系里会同时出现:
- JWT
- salt
- 临时密钥
- ZK proof
它们分别在证明不同的事。
3.3 临时密钥对(Ephemeral Key Pair)
zkLogin 使用一个临时密钥对来执行实际签名:
登录流程:
1. 生成临时 ed25519 密钥对(有效期 = Sui Epoch,约 24h)
2. 将临时公钥的 nonce 嵌入 OAuth 请求
3. OAuth server 在 JWT 中返回含 nonce 的 token
4. 用临时私钥签名交易
5. 提交 ZK 证明 + 临时签名 → Sui 验证
临时私钥存储在 EVE Vault 的 Keeper 安全容器中(见第34章)。
为什么一定要有临时密钥?
因为 zkLogin 并不是让 OAuth token 直接变成签名器。临时密钥的作用是把“登录态”和“具体签名动作”连接起来,同时把风险窗口限制在一个较短周期内。
3.4 ZK Proof 生成
// packages/shared/src/wallet/zkProof.ts
interface ZkProofParams {
jwtRandomness: string; // 随机盐,防止 nonce 推算
maxEpoch: string; // 临时密钥的最大有效 Epoch
ephemeralPublicKey: PublicKey; // 临时公钥(嵌入 JWT nonce)
idToken: string; // 从 FusionAuth 获取的 JWT
enokiApiKey: string; // Enoki 服务密钥
network?: string; // devnet | testnet | mainnet
}
ZK Proof 的生成步骤:
- 收集上述参数
- 调用 Sui ZK Prover 端点(Enoki 托管)
- 返回包含
proofPoints、issBase64Details、headerBase64的 ZK 证明
3.5 JWT Nonce 的构造
zkLogin 最关键的设计是把临时公钥“嵌入“ JWT 中,这通过 nonce 字段实现:
// nonce = poseidon_hash(ephemeral_public_key, max_epoch, randomness)
// 这一步在请求 OAuth 前完成
const nonce = generateNonce(ephemeralPublicKey, maxEpoch, randomness);
// OAuth URL 中传入 nonce
const authUrl = `${fusionAuthUrl}/oauth2/authorize?`
+ `client_id=${CLIENT_ID}`
+ `&response_type=code`
+ `&nonce=${nonce}` // ← FusionAuth 会把这个 nonce 放进 JWT
+ `&scope=openid+profile+email`;
FusionAuth 在返回的 JWT(id_token)中包含:
{
"iss": "https://auth.evefrontier.com",
"sub": "user-12345", ← EVE 账户唯一 ID
"aud": "your-client-id",
"nonce": "H5SmVjkG...", ← 含临时公钥信息
"exp": 1712345678
}
Sui 的 ZK 验证器通过检查 nonce 中嵌入的临时公钥,确认签名确实来自该临时密钥对。
3.6 zkLogin 地址的 TypeScript 计算
import { computeZkLoginAddress } from "@mysten/sui/zklogin";
// 从 Enoki API 获取 user_salt 和地址
const { address, salt } = await fetch("https://api.enoki.mystenlabs.com/v1/zklogin", {
method: "POST",
headers: { "Authorization": `Bearer ${ENOKI_API_KEY}` },
body: JSON.stringify({ jwt: idToken }),
}).then(r => r.json());
// 验证:本地计算地址(与 Enoki 返回的地址相同)
const localAddress = computeZkLoginAddress({
claimName: "sub",
claimValue: decodedJwt.sub, // EVE 账户 ID
iss: decodedJwt.iss,
aud: decodedJwt.aud,
userSalt: BigInt(salt),
});
console.assert(address === localAddress);
4. EVE Vault 的认证流程
用户点击 "Sign in with EVE Vault"
│
▼
生成临时 Ed25519 密钥对 + JWT Nonce
│
▼
打开 FusionAuth OAuth 页面(chrome.identity API)
│
▼
用户用 EVE Frontier 账户登录
│
▼
FusionAuth 返回 JWT(包含 nonce)
│
▼
调用 Enoki API → 获取 user_salt + zkLogin 地址
│
▼
调用 ZK Prover → 生成 ZK Proof
│
▼
Popup 显示 zkLogin 地址 + SUI 余额
│
▼
dApp 调用 wallet.connect() → 获取地址 → 可以发交易
这条流程里最关键的不是步骤多,而是职责分明
- FusionAuth 负责确认用户是谁
- Enoki 负责辅助 zkLogin 地址和盐值
- Prover 负责生成可验证证明
- Vault 负责把这些东西组织成钱包能力
只要你把每层职责分清,这套机制就不会显得神秘。
5. 多网络支持
EVE Vault 支持同时连接多个测试网,可随时切换:
// packages/shared/src/types/wallet.ts
export class EveVaultWallet implements Wallet {
#currentChain: SuiChain = SUI_TESTNET_CHAIN;
get chains(): Wallet["chains"] {
return [SUI_TESTNET_CHAIN, SUI_DEVNET_CHAIN] as `sui:${string}`[];
}
// ...
}
弹窗左下角的网络切换器让玩家在 Devnet(开发测试)和 Testnet(演示/Pre-launch)之间切换,切换后地址相同(因为派生公式不含网络参数),但查询的节点会切换。
6. EVE Vault vs 传统 Sui 钱包对比
| 特性 | Sui Wallet / OKX | EVE Vault |
|---|---|---|
| 需要助记词 | ✅ 是 | ❌ 否 |
| 基于 OAuth 登录 | ❌ 否 | ✅ 是(EVE 账户) |
| 私钥存储位置 | 用户本地 | 无私钥(zkLogin) |
| 地址确定性 | 取决于私钥 | JWT + salt 确定性派生 |
| 签名方案 | ed25519 / secp256k1 | zkLogin(ZK Proof + 临时签名) |
| 赞助交易 | 部分支持 | ✅ EVE Frontier 原生支持 |
| dApp discover | Wallet Standard | Wallet Standard + EVE 扩展特性 |
7. 安全模型
Keeper 机制
临时私钥不存储在 chrome.storage(可被 JS 读取),而是存在 Keeper(一个隔离的 hidden document)中:
┌─────────────────────────────────────────┐
│ Chrome Extension 沙箱 │
│ │
│ Background Service Worker │
│ ↕ chrome.runtime.sendMessage │
│ Keeper (hidden iframe/document) │
│ ← 临时私钥仅在此内存中 │
│ ← 不写入 chrome.storage │
│ ← 关闭浏览器即清除 │
└─────────────────────────────────────────┘
锁定机制
浏览器关闭或一段时间不操作后,Keeper 自动清除临时私钥(“锁定“状态)。重新解锁需要重新生成 ZK Proof(有缓存,通常几秒内完成)。
8. 对 Builder 的意义
作为 Builder,你的 dApp 用户将通过 EVE Vault 连接,以下是关键影响:
- 无需私钥管理 UX:用户直接用游戏账户连接,降低 onboarding 门槛
- 赞助交易原生支持:EVE Vault 实现了
sign_sponsored_transaction,Builder 可以替用户付 Gas - 地址稳定性:玩家的链上地址与其 EVE 账户绑定,不会因“换设备“而改变
- 多网络:开发时用 Devnet,上线用 Testnet,地址不变
本章小结
| 概念 | 要点 |
|---|---|
| zkLogin | 无私钥的零知识签名方案,基于 OAuth JWT |
user_salt | Enoki 保管,防止 OAuth ID 与链上地址关联 |
| 临时密钥对 | 每次 Epoch 重新生成,Keeper 安全容器存储 |
| ZK Proof | 向 Enoki 请求,证明“合法 JWT 持有者“ |
| FusionAuth | EVE Frontier 的 OAuth 身份提供商 |
下一章:EVE Vault 技术架构与开发部署 —— Chrome MV3 的 5 个脚本层、消息通信协议、以及如何在本地构建和加载扩展。