rwadurian/backend/mpc-system/docs/06-tss-protocol.md

14 KiB
Raw Blame History

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 注意事项

  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
  • 建议: 加密备份分片到安全存储