14 KiB
14 KiB
MPC 分布式签名系统 - TSS 协议详解
1. 概述
本系统使用 门限签名方案 (Threshold Signature Scheme, TSS) 实现分布式密钥管理和签名。基于 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 代码实现
// 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 保存的数据:
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 代码实现
// 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 签名验证
// 验证签名
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 消息结构
type MPCMessage struct {
SessionID string // 会话 ID
FromParty string // 发送方
ToParties []string // 接收方 (空=广播)
Round int // 协议轮次
Payload []byte // 加密的协议消息
IsBroadcast bool // 是否广播
Timestamp int64
}
5.3 消息路由实现
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 的所有组合
// 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 注意事项
- Party 索引必须一致: 签名时使用 keygen 时的原始索引
- 不能混用不同 keygen 的分片: 每个账户对应唯一的一组分片
- 阈值验证: 签名者数量 >= threshold + 1
7. 性能考虑
7.1 测试基准
| 操作 | 2-of-3 | 3-of-5 | 4-of-7 |
|---|---|---|---|
| Keygen | ~93s | ~198s | ~221s |
| Signing | ~80s | ~120s | ~150s |
7.2 优化建议
- 预计算: 部分 Keygen 数据可预计算
- 并行执行: 多个签名请求可并行处理
- 消息压缩: 大消息进行压缩传输
- 连接池: 复用 Party 间的连接
8. 故障恢复
8.1 Keygen 失败
如果 Keygen 过程中某个 Party 离线:
- 协议超时失败
- 需要全部重新开始
- 建议设置合理的超时时间
8.2 Signing 失败
如果签名过程中 Party 离线:
- 当前签名失败
- 可以选择其他 Party 子集重试
- 密钥分片不受影响
8.3 密钥分片丢失
如果某个 Party 的分片丢失:
- 如果丢失数量 < n - t: 仍可签名
- 如果丢失数量 >= n - t: 无法签名,需要重新 Keygen
- 建议: 加密备份分片到安全存储