MPC 钱包安全
Fystack 使用 多方计算(MPC) 消除私钥管理中的单点故障。任何一方都不会持有完整的私钥。密钥分片分布在多个节点上,签名则通过门限密码学协同计算得出。
本文介绍 Fystack 的 MPC 实现如何通过分布式信任、安全通信和可验证计算来保护钱包密钥。

节点职责
每个节点:
- 仅在本地存储 自己的加密分片。
- 通过 Ed25519 验证 和 ECDH 保护的 P2P 通信参与门限签名。
- 即使被攻破,也无法重建或导出完整密钥。
属性 | 说明 |
|---|---|
机密性 | 任何单个节点或运维人员都无法访问完整私钥。 |
完整性 | 所有节点间消息均通过 Ed25519 或 Cloud KMS 签名进行认证。 |
韧性 | 支持 n 个节点、门限为 t 的配置,最多可容忍 (t-1) 个节点故障。 |
静态加密 | 密钥分片在本地以 AES 加密存储。 |
传输加密 | 通过 NATS TLS 以及 P2P 通道上的 ECDH 加密层强制实施。 |
1. 概述
多方计算让多个参与方能够共同执行密码学运算,而任何一方都无法访问完整私钥。Fystack 实现了门限签名方案,必须有最少数量(门限)的参与方协作,才能生成有效签名。
组件 | 说明 |
|---|---|
MPC 节点 | 至少由 3 个节点组成一个集群。 |
门限(t) | 即使最多 (t-1) 个节点被攻破,钱包依然安全。 |
密钥分片 | 私钥在数学上被拆分为多个分片。 |
分片加密 | 每个分片均使用 AES 加密(BadgerDB 默认加密)。 |
消息安全 | 双层保护:NATS TLS 加上用于 P2P 加密的 ECDH 密钥交换。 |
消息认证 | Apex 使用签名消息(Ed25519 或 Cloud KMS)与 MPC 集群通信。 |
主要优势
- 无单点失陷: 单个节点被攻破不会暴露私钥
- 分布式信任: 必须由多个独立参与方协作
- 对区块链透明: 输出的签名是标准的 ECDSA/EdDSA 签名
- 动态门限: 可根据交易金额调整签名要求
- 审计记录: 每个签名会话都有日志记录且可追溯
2. 分布式密钥生成(DKG)
Fystack 使用 分布式密钥生成 创建钱包密钥,任何一方都不会持有完整私钥。
DKG 流程
阶段 1:承诺
- 每个参与方生成一个 t-1 次的随机多项式(t 为门限)
- 各参与方广播其多项式的承诺
- 使用 Feldman VSS(可验证秘密共享)验证承诺
阶段 2:分片分发
- 每个参与方在特定点上计算其多项式的值
- 通过加密通道将分片安全地发送给对应的参与方
- 每个参与方都会从其他每个参与方各收到一个分片
阶段 3:验证
- 各参与方根据广播的承诺验证收到的分片
- 识别无效分片并提出投诉
- 如果检测到过多无效分片,协议将中止
阶段 4:密钥构建
- 每个参与方组合有效分片,形成自己的密钥分片
- 根据承诺计算出公钥
- 任何一方都不知道完整私钥
DKG 配置
mpc_threshold: 2 # Number of MPC nodes required to sign
mpc_threshold 表示 n-of-t 方案中的门限 t。在有 n 个活跃 MPC 节点时,系统强制要求 2 ≤ t ≤ n,在最多 (t - 1) 个节点故障或离线的情况下仍可生成签名。例如,n = 3 且 mpc_threshold: 2 时构成一个 2-of-3 集群:任意两个经过认证的节点即可签名,但攻破单个节点不足以恢复私钥。
对于 GG18/GG20 风格的鲁棒 MPC 协议,我们还建议 t > n / 2,以保证拜占庭容错:即使 f = n - t 个节点存在恶意、离线或合谋,诚实节点仍占多数,可以阻止伪造签名或私钥重建。
安全特性
- 鲁棒性: 最多可容忍 n-t 个恶意参与方
- 可验证性: 所有参与方都可以验证分片的正确性
- 隐私性: 不会泄露任何关于私钥的信息
- 非交互性: 完成初始设置后无需再次执行 DKG
3. 门限签名
门限签名允许(满足门限的)部分参与方协同生成有效签名,而无需重建私钥。
签名协议(ECDSA)
第 1 轮:承诺阶段
生成承诺
// Each signer generates commitment
const k_i = randomScalar()
const gamma_i = randomScalar()
const commitment_i = {
K_i: k_i * G, // Ephemeral public key
Gamma_i: gamma_i * G, // Blinding factor
}
broadcastCommitment(commitment_i)
第 2 轮:生成部分签名
部分签名
// After receiving all commitments
const R = combineCommitments(allCommitments)
const r = R.x // x-coordinate of combined point
// Each party computes partial signature
const s_i = k_i + r * keyShare_i * hash(message)
broadcastPartialSignature(s_i)
第 3 轮:签名聚合
组合签名
// Combine partial signatures
const s = lagrangeInterpolation(partialSignatures)
const signature = { r, s }
// Verify signature
const valid = ecdsaVerify(signature, publicKey, message)
签名工作流程
步骤 | 操作 | 安全检查 |
|---|---|---|
| 使用交易详情创建签名会话 | 验证会话 ID 的唯一性 |
| 选择满足门限数量的参与方 | 验证参与方在线且已通过认证 |
| 交换临时密钥的承诺 | 校验承诺的格式和新鲜度 |
| 每个参与方计算并分享部分签名 | 验证部分签名的有效性 |
| 将部分签名组合成最终签名 | 使用公钥验证最终签名 |
| 将已签名的交易提交到区块链 | 确认交易已被接受 |
4. 网络安全
NATS 传输(TLS)
MPC 节点通过 config.yaml 中声明的共享 NATS 总线交换流量。生产部署需要 TLS endpoint 以及锁定的客户端证书:
NATS TLS 配置
nats:
url: tls://nats.example.com:4222
username: ""
password: ""
tls:
client_cert: "/etc/mpcium/certs/client-cert.pem"
client_key: "/etc/mpcium/certs/client-key.pem"
ca_cert: "/etc/mpcium/certs/rootCA.pem"
如果没有上述 tls:// URL 和 nats.tls.* 路径,deployments/systemd/setup-config.sh 脚本会拒绝在生产环境中运行,从而确保每个节点都通过经过认证的 TLS 隧道与 NATS 通信。
签名消息总线
在发布 TSS 载荷之前,节点会使用其 Ed25519 身份对其进行签名(参见 pkg/identity/fileStore):
签署 TSS 消息
func (s *fileStore) SignMessage(msg *types.TssMessage) ([]byte, error) {
msgBytes, err := msg.MarshalForSigning()
if err != nil {
return nil, fmt.Errorf("failed to marshal message for signing: %w", err)
}
signature := ed25519.Sign(s.privateKey, msgBytes)
return signature, nil
}
每个节点在接受消息前都会验证签名,以防止伪造的对等方或重放数据:
验证 TSS 消息
func (s *fileStore) VerifyMessage(msg *types.TssMessage) error {
if msg.Signature == nil {
return fmt.Errorf("message has no signature")
}
publicKey, err := s.GetPublicKey(partyIDToNodeID(msg.From))
if err != nil {
return fmt.Errorf("failed to get sender's public key: %w", err)
}
msgBytes, err := msg.MarshalForSigning()
if err != nil {
return fmt.Errorf("failed to marshal message for verification: %w", err)
}
if !ed25519.Verify(publicKey, msgBytes, msg.Signature) {
return fmt.Errorf("invalid signature")
}
return nil
}
点对点加密
节点通过专用的 NATS 主题(pkg/mpc/key_exchange_session.go)执行 ECDH 交换,为每个对等方派生对称密钥。这些密钥用于对每个载荷进行 AES-GCM 加密:
加密 TSS 载荷
func (s *fileStore) EncryptMessage(plaintext []byte, peerID string) ([]byte, error) {
key, err := s.GetSymmetricKey(peerID)
if err != nil {
return nil, err
}
if key == nil {
return nil, fmt.Errorf("no symmetric key for peer %s", peerID)
}
return encryption.EncryptAESGCMWithNonceEmbed(plaintext, key)
}
ECDH 握手流程
ECDH 会话(pkg/mpc/key_exchange_session.go)构建在 NATS 总线之上:
- 每个节点生成一个临时 X25519 密钥对,并订阅共享的
ecdh:exchange主题。 - 节点广播其公钥和时间戳,并使用其 Ed25519 身份进行签名(
SignEcdhMessage)。 - 监听方验证签名,使用自己的私钥执行
ECDH(),并通过 HKDF-SHA256 派生对称密钥。 - 派生出的 32 字节密钥通过
identityStore.SetSymmetricKey按对等方缓存,并由上述 AES-GCM 辅助函数复用。
HKDF 密钥派生
func (e *ecdhSession) deriveSymmetricKey(secret []byte, peerID string) []byte {
info := deriveConsistentInfo(e.nodeID, peerID) // stable ordering of IDs
hkdfReader := hkdf.New(sha256.New, secret, nil, info)
key := make([]byte, 32) // AES-256 key
if _, err := hkdfReader.Read(key); err != nil {
e.errCh <- err
return nil
}
return key
}
如果对等方断开连接,RemovePeer 会丢弃缓存的对称密钥,因此重新连接时需要重新握手,从而确保通道密钥的临时性。
网络隔离
控制措施 | 实现方式 |
|---|---|
私有网络 | MPC 节点通过私有 VPC/VPN 通信 |
防火墙规则 | 仅开放 MPC 端口,并限制为已知节点 IP |
速率限制 | 限制每个节点的最大消息速率,以防范 DoS |
连接监控 | 记录所有连接并监控异常 |
5. 存储安全
密钥分片持久化存储在 BadgerDB 中。除非提供加密密钥(badger_password),否则 MPC 服务会拒绝打开数据库,该密钥会通过 WithEncryptionKey(...)(pkg/kvstore/badger.go)传入 Badger 的 AES-256 加密。备份沿用相同的加密格式,因此没有密码时,无论是运行中的数据库文件还是快照归档都无法读取。
Badger 加密
opts := badger.DefaultOptions(config.DBPath).
WithEncryptionKey(config.EncryptionKey).
WithCompression(options.ZSTD)
db, err := badger.Open(opts)
6. 总结
Fystack 的 MPC 实现通过分布式信任、门限密码学和安全的多方协议消除了单点故障。通过将私钥拆分为分片并要求协同签名,我们确保任何单一环节被攻破都不会导致资产损失。
关键安全特性:
- 分布式密钥生成,无单一信任点
- 门限签名,需要多个参与方协作
- 加密通信,采用 TLS 以及基于 ECDH 派生密钥的 AES-GCM 载荷加密
- BadgerDB 静态加密,使用 AES-256 及运维人员提供的密码
- 可验证计算,用于检测恶意参与方