# MPC 分布式签名系统 - TSS 协议详解 ## 1. 概述 本系统使用 **门限签名方案 (Threshold Signature Scheme, TSS)** 实现分布式密钥管理和签名。基于 [bnb-chain/tss-lib](https://github.com/bnb-chain/tss-lib) 库,采用 GG20 协议。 ### 1.1 核心概念 | 术语 | 定义 | |------|------| | t-of-n | t+1 个参与方中的任意组合可以签名,需要 n 个参与方共同生成密钥 | | DKG | 分布式密钥生成 (Distributed Key Generation) | | TSS | 门限签名方案 (Threshold Signature Scheme) | | Party | MPC 协议中的参与方 | | Share | 密钥分片,每个 Party 持有一份 | ### 1.2 安全属性 - **无单点故障**: 私钥从未以完整形式存在 - **门限安全**: 需要 t+1 个分片才能签名 - **抗合谋**: t 个恶意方无法伪造签名 - **可审计**: 每次签名可追踪参与方 ## 2. 阈值参数说明 ### 2.1 tss-lib 参数约定 在 tss-lib 中,`threshold` 参数定义如下: - `threshold = t` 表示需要 **t+1** 个签名者 - 例如: `threshold=1` 需要 2 个签名者 ### 2.2 常见阈值方案 | 方案 | tss-lib threshold | 总参与方 (n) | 签名者数 (t+1) | 应用场景 | |------|-------------------|-------------|---------------|---------| | 2-of-3 | 1 | 3 | 2 | 个人钱包 + 设备 + 恢复 | | 3-of-5 | 2 | 5 | 3 | 企业多签 | | 4-of-7 | 3 | 7 | 4 | 机构托管 | | 5-of-9 | 4 | 9 | 5 | 大型组织 | ### 2.3 阈值选择建议 ``` 安全性 vs 可用性权衡: 高安全性 ◄────────────────────────► 高可用性 5-of-9 4-of-7 3-of-5 2-of-3 建议: - 个人用户: 2-of-3 (设备 + 服务器 + 恢复) - 小型企业: 3-of-5 (3 管理员 + 1 服务器 + 1 恢复) - 大型企业: 4-of-7 或更高 ``` ## 3. 密钥生成协议 (Keygen) ### 3.1 协议流程 ``` Round 1: 承诺分发 ┌────────────┐ ┌────────────┐ ┌────────────┐ │ Party 0 │ │ Party 1 │ │ Party 2 │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ 生成随机多项式 │ │ │ 计算承诺 Ci │ │ │ │ │ │◄─────────────────┼──────────────────┤ 广播承诺 ├──────────────────►◄─────────────────┤ │ │ │ Round 2: 秘密分享 │ │ │ │ 计算 Shamir 分片│ │ │ 发送 share_ij │ │ │ │ │ │──────────────────► │ 点对点 │ ◄──────────────────│ ◄──────────────────│ │ │ │──────────────────► │ │ │ Round 3: 验证与聚合 │ │ │ │ 验证收到的分片 │ │ │ 计算最终密钥分片 │ │ │ 计算公钥 PK │ │ │ │ │ ▼ ▼ ▼ Share_0 Share_1 Share_2 │ │ │ └──────────────────┼──────────────────┘ │ 公钥 PK (相同) ``` ### 3.2 代码实现 ```go // pkg/tss/keygen.go func RunLocalKeygen(threshold, totalParties int) ([]*LocalKeygenResult, error) { // 验证参数 if threshold < 1 || threshold > totalParties { return nil, ErrInvalidThreshold } // 创建 Party IDs partyIDs := make([]*tss.PartyID, totalParties) for i := 0; i < totalParties; i++ { partyIDs[i] = tss.NewPartyID( fmt.Sprintf("party-%d", i), fmt.Sprintf("party-%d", i), big.NewInt(int64(i+1)), ) } sortedPartyIDs := tss.SortPartyIDs(partyIDs) peerCtx := tss.NewPeerContext(sortedPartyIDs) // 创建各方的通道和 Party 实例 outChs := make([]chan tss.Message, totalParties) endChs := make([]chan *keygen.LocalPartySaveData, totalParties) parties := make([]tss.Party, totalParties) for i := 0; i < totalParties; i++ { outChs[i] = make(chan tss.Message, totalParties*10) endChs[i] = make(chan *keygen.LocalPartySaveData, 1) params := tss.NewParameters( tss.S256(), // secp256k1 曲线 peerCtx, sortedPartyIDs[i], totalParties, threshold, ) parties[i] = keygen.NewLocalParty(params, outChs[i], endChs[i]) } // 启动所有 Party for i := 0; i < totalParties; i++ { go parties[i].Start() } // 消息路由 go routeMessages(parties, outChs, sortedPartyIDs) // 收集结果 results := make([]*LocalKeygenResult, totalParties) for i := 0; i < totalParties; i++ { saveData := <-endChs[i] results[i] = &LocalKeygenResult{ SaveData: saveData, PublicKey: saveData.ECDSAPub.ToECDSAPubKey(), PartyIndex: i, } } return results, nil } ``` ### 3.3 SaveData 结构 每个 Party 保存的数据: ```go type LocalPartySaveData struct { // 本方的私钥分片 (xi) Xi *big.Int // 所有方的公钥分片 (Xi = xi * G) BigXj []*crypto.ECPoint // 组公钥 ECDSAPub *crypto.ECPoint // Paillier 密钥对 (用于同态加密) PaillierSK *paillier.PrivateKey PaillierPKs []*paillier.PublicKey // 其他预计算数据... } ``` ## 4. 签名协议 (Signing) ### 4.1 协议流程 ``` 签名协议 (GG20 - 6 轮): Round 1: 承诺生成 ┌────────────┐ ┌────────────┐ │ Party 0 │ │ Party 1 │ └─────┬──────┘ └─────┬──────┘ │ │ │ 生成随机 ki │ │ 计算 γi = ki*G │ │ 广播 C(γi) │ │ │ │◄────────────────►│ │ │ Round 2: Paillier 加密 │ │ │ 加密 ki │ │ MtA 协议开始 │ │ │ │◄────────────────►│ │ │ Round 3: MtA 响应 │ │ │ 计算乘法三元组 │ │ │ │◄────────────────►│ │ │ Round 4: Delta 分享 │ │ │ 计算 δi │ │ 广播 │ │ │ │◄────────────────►│ │ │ Round 5: 重构与验证 │ │ │ 重构 δ = Σδi │ │ 计算 R = δ^-1*Γ │ │ 计算 r = Rx │ │ │ │◄────────────────►│ │ │ Round 6: 签名聚合 │ │ │ 计算 si = ... │ │ 广播 si │ │ │ │◄────────────────►│ │ │ ▼ ▼ 最终签名 (r, s) ``` ### 4.2 代码实现 ```go // pkg/tss/signing.go func RunLocalSigning( threshold int, keygenResults []*LocalKeygenResult, messageHash []byte, ) (*LocalSigningResult, error) { signerCount := len(keygenResults) if signerCount < threshold+1 { return nil, ErrInvalidSignerCount } // 创建 Party IDs (必须使用原始索引) partyIDs := make([]*tss.PartyID, signerCount) for i, result := range keygenResults { idx := result.PartyIndex partyIDs[i] = tss.NewPartyID( fmt.Sprintf("party-%d", idx), fmt.Sprintf("party-%d", idx), big.NewInt(int64(idx+1)), ) } sortedPartyIDs := tss.SortPartyIDs(partyIDs) peerCtx := tss.NewPeerContext(sortedPartyIDs) // 转换消息哈希 msgHash := new(big.Int).SetBytes(messageHash) // 创建签名方 outChs := make([]chan tss.Message, signerCount) endChs := make([]chan *common.SignatureData, signerCount) parties := make([]tss.Party, signerCount) for i := 0; i < signerCount; i++ { outChs[i] = make(chan tss.Message, signerCount*10) endChs[i] = make(chan *common.SignatureData, 1) params := tss.NewParameters(tss.S256(), peerCtx, sortedPartyIDs[i], signerCount, threshold) parties[i] = signing.NewLocalParty(msgHash, params, *keygenResults[i].SaveData, outChs[i], endChs[i]) } // 启动并路由消息 for i := 0; i < signerCount; i++ { go parties[i].Start() } go routeSignMessages(parties, outChs, sortedPartyIDs) // 收集签名结果 signData := <-endChs[0] return &LocalSigningResult{ R: new(big.Int).SetBytes(signData.R), S: new(big.Int).SetBytes(signData.S), RecoveryID: int(signData.SignatureRecovery[0]), }, nil } ``` ### 4.3 签名验证 ```go // 验证签名 import "crypto/ecdsa" func VerifySignature(publicKey *ecdsa.PublicKey, messageHash []byte, r, s *big.Int) bool { return ecdsa.Verify(publicKey, messageHash, r, s) } // 示例 message := []byte("Hello MPC!") hash := sha256.Sum256(message) valid := ecdsa.Verify(publicKey, hash[:], signResult.R, signResult.S) ``` ## 5. 消息路由 ### 5.1 消息类型 | 类型 | 说明 | 方向 | |------|------|------| | Broadcast | 发送给所有其他方 | 1 → n-1 | | P2P | 点对点消息 | 1 → 1 | ### 5.2 消息结构 ```go type MPCMessage struct { SessionID string // 会话 ID FromParty string // 发送方 ToParties []string // 接收方 (空=广播) Round int // 协议轮次 Payload []byte // 加密的协议消息 IsBroadcast bool // 是否广播 Timestamp int64 } ``` ### 5.3 消息路由实现 ```go func routeMessages( parties []tss.Party, outChs []chan tss.Message, sortedPartyIDs []*tss.PartyID, ) { signerCount := len(parties) for idx := 0; idx < signerCount; idx++ { go func(i int) { for msg := range outChs[i] { if msg.IsBroadcast() { // 广播给所有其他方 for j := 0; j < signerCount; j++ { if j != i { updateParty(parties[j], msg) } } } else { // 点对点发送 for _, dest := range msg.GetTo() { for j := 0; j < signerCount; j++ { if sortedPartyIDs[j].Id == dest.Id { updateParty(parties[j], msg) break } } } } } }(idx) } } ``` ## 6. 子集签名 (Subset Signing) ### 6.1 原理 在 t-of-n 方案中,任意 t+1 个 Party 的子集都可以生成有效签名。关键是使用原始的 Party 索引。 ### 6.2 示例: 2-of-3 的所有组合 ```go // 3 方生成密钥 keygenResults, _ := tss.RunLocalKeygen(1, 3) // threshold=1, n=3 // 任意 2 方可签名: // 组合 1: Party 0 + Party 1 signers1 := []*tss.LocalKeygenResult{keygenResults[0], keygenResults[1]} sig1, _ := tss.RunLocalSigning(1, signers1, messageHash) // 组合 2: Party 0 + Party 2 signers2 := []*tss.LocalKeygenResult{keygenResults[0], keygenResults[2]} sig2, _ := tss.RunLocalSigning(1, signers2, messageHash) // 组合 3: Party 1 + Party 2 signers3 := []*tss.LocalKeygenResult{keygenResults[1], keygenResults[2]} sig3, _ := tss.RunLocalSigning(1, signers3, messageHash) // 所有签名都对同一公钥有效! ecdsa.Verify(publicKey, messageHash, sig1.R, sig1.S) // true ecdsa.Verify(publicKey, messageHash, sig2.R, sig2.S) // true ecdsa.Verify(publicKey, messageHash, sig3.R, sig3.S) // true ``` ### 6.3 注意事项 1. **Party 索引必须一致**: 签名时使用 keygen 时的原始索引 2. **不能混用不同 keygen 的分片**: 每个账户对应唯一的一组分片 3. **阈值验证**: 签名者数量 >= threshold + 1 ## 7. 性能考虑 ### 7.1 测试基准 | 操作 | 2-of-3 | 3-of-5 | 4-of-7 | |------|--------|--------|--------| | Keygen | ~93s | ~198s | ~221s | | Signing | ~80s | ~120s | ~150s | ### 7.2 优化建议 1. **预计算**: 部分 Keygen 数据可预计算 2. **并行执行**: 多个签名请求可并行处理 3. **消息压缩**: 大消息进行压缩传输 4. **连接池**: 复用 Party 间的连接 ## 8. 故障恢复 ### 8.1 Keygen 失败 如果 Keygen 过程中某个 Party 离线: - 协议超时失败 - 需要全部重新开始 - 建议设置合理的超时时间 ### 8.2 Signing 失败 如果签名过程中 Party 离线: - 当前签名失败 - 可以选择其他 Party 子集重试 - 密钥分片不受影响 ### 8.3 密钥分片丢失 如果某个 Party 的分片丢失: - 如果丢失数量 < n - t: 仍可签名 - 如果丢失数量 >= n - t: 无法签名,需要重新 Keygen - 建议: 加密备份分片到安全存储