rwadurian/backend/mpc-system/DELEGATE_PARTY_GUIDE.md

12 KiB
Raw Permalink Blame History

Delegate Party Implementation Guide

概述

Delegate Party 是一种特殊类型的 Server-Party用于生成用户端 key shares 并返回给用户,而不保存到服务器数据库中。

这是 混合托管Hybrid Custody 模式的核心实现,类似于 Fireblocks 和 ZenGo 的架构。


架构设计

三种 Party Role

Role 存储位置 用途 示例
persistent 服务器数据库 永久存储 share 托管钱包、企业密钥
delegate 不保存,返回给用户 用户自己持有 share 移动钱包、个人密钥
temporary 临时(不实现) 一次性操作 临时签名

MPC Session 组合示例

Keygen Session (3-of-5)
├── Persistent Party 1 → Database (share_1) ✅ 服务器托管
├── Persistent Party 2 → Database (share_2) ✅ 服务器托管
├── Persistent Party 3 → Database (share_3) ✅ 服务器托管
├── Delegate Party 1   → User Device (share_4) 📱 用户自持
└── Delegate Party 2   → User Device (share_5) 📱 用户自持

签名时需要任意 3 个 shares
- 方案 A: 3 个服务器 shares完全托管
- 方案 B: 2 个服务器 + 1 个用户(用户参与)
- 方案 C: 1 个服务器 + 2 个用户(用户主导)

完整工作流程

1. 部署 Delegate Parties

Kubernetes Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mpc-delegate-party
  namespace: mpc-system
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mpc-server-party
      party-role: delegate
  template:
    metadata:
      labels:
        app: mpc-server-party
        party-role: delegate  # ← Party Discovery 使用
    spec:
      containers:
      - name: delegate-party
        image: rwadurian/mpc-server-party:latest
        env:
        - name: PARTY_ROLE
          value: "delegate"  # ← 关键环境变量
        - name: MESSAGE_ROUTER_ADDR
          value: "mpc-message-router:9092"
        - name: SESSION_COORDINATOR_ADDR
          value: "mpc-session-coordinator:9091"
        ports:
        - containerPort: 8080
          name: http

Docker Compose (Development):

services:
  delegate-party-1:
    image: rwadurian/mpc-server-party:latest
    container_name: delegate-party-1
    environment:
      PARTY_ROLE: "delegate"
      MESSAGE_ROUTER_ADDR: "mpc-message-router:9092"
      SESSION_COORDINATOR_ADDR: "mpc-session-coordinator:9091"
    ports:
      - "8081:8080"

2. 创建 Session指定 Party Composition

API Request (POST /api/v1/sessions/create):

{
  "session_type": "keygen",
  "threshold_n": 5,
  "threshold_t": 3,
  "expires_in": 600,

  "party_composition": {
    "persistent_count": 3,
    "delegate_count": 2
  }
}

Response:

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "join_tokens": {
    "mpc-server-party-1": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "mpc-server-party-2": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "mpc-server-party-3": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "mpc-delegate-party-0": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "mpc-delegate-party-1": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  },
  "expires_at": "2024-01-01T12:10:00Z"
}

3. Keygen 协议执行

Session Coordinator 发布 SessionEvent:

1. Session Coordinator
   └─ PublishSessionCreated() → Message Router

2. Message Router 广播到所有 selected parties:
   ├─ → mpc-server-party-1 (persistent)
   ├─ → mpc-server-party-2 (persistent)
   ├─ → mpc-server-party-3 (persistent)
   ├─ → mpc-delegate-party-0 (delegate)
   └─ → mpc-delegate-party-1 (delegate)

3. All Parties 收到 SessionEvent:
   ├─ 提取自己的 JoinToken
   ├─ 调用 JoinSession()
   └─ 执行 GG20 Keygen 协议

4. Keygen 完成后的处理

Persistent Parties:

// participate_keygen.go:165-173
case "persistent":
    // 保存 share 到数据库
    if err := uc.keyShareRepo.Save(ctx, keyShare); err != nil {
        return nil, ErrShareSaveFailed
    }
    logger.Info("Share saved to database (persistent party)",
        zap.String("party_id", input.PartyID),
        zap.String("session_id", input.SessionID.String()))

Delegate Parties:

// participate_keygen.go:175-181
case "delegate":
    // 不保存到数据库,返回给用户
    shareForUser = encryptedShare
    logger.Info("Share NOT saved, will be returned to user (delegate party)",
        zap.String("party_id", input.PartyID),
        zap.String("session_id", input.SessionID.String()),
        zap.Int("share_size", len(shareForUser)))

存入内存缓存 (main.go:328-340):

// 如果是 delegate party将 share 存入缓存
if output.ShareForUser != nil && len(output.ShareForUser) > 0 {
    globalShareCache.Store(
        sessionID,
        req.PartyID,
        output.ShareForUser,
        output.PublicKey,
    )
    logger.Info("Share stored in cache for user retrieval (delegate party)")
}

5. 用户获取 Share

API Request (GET /api/v1/sessions/{session_id}/user-share):

curl -X GET \
  https://delegate-party-1.example.com/api/v1/sessions/550e8400-e29b-41d4-a716-446655440000/user-share \
  -H "Authorization: Bearer <optional_jwt_token>"

成功响应:

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "party_id": "mpc-delegate-party-0",
  "share": "0123456789abcdef...",  // hex-encoded encrypted share
  "public_key": "04a1b2c3d4e5f6...",  // hex-encoded public key
  "note": "This share has been deleted from memory and cannot be retrieved again"
}

错误响应 (已被获取或过期):

{
  "error": "Share not found or already retrieved",
  "note": "Shares can only be retrieved once and expire after 15 minutes"
}

Forbidden (非 delegate party):

{
  "error": "This endpoint is only available for delegate parties",
  "role": "persistent"
}

安全特性

1. 内存缓存(非持久化)

// share_cache.go
type ShareCache struct {
    entries map[string]*ShareCacheEntry  // session_id -> entry
    mu      sync.RWMutex
    ttl     time.Duration  // 15 分钟
}

特点

  • 只存在内存中(不写入磁盘)
  • 服务重启自动清空
  • 15 分钟 TTL 自动过期
  • 后台定时清理过期数据

2. 一次性获取

// GetAndDelete - 获取后立即删除
func (c *ShareCache) GetAndDelete(sessionID uuid.UUID) (*ShareCacheEntry, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()

    entry, exists := c.entries[sessionID.String()]
    if !exists {
        return nil, false
    }

    // 立即删除(原子操作)
    delete(c.entries, sessionID.String())

    return entry, true
}

特点

  • 只能获取一次
  • 获取后立即从内存删除
  • 原子操作(线程安全)

3. Role 检查

// main.go:454-461
// 检查 PARTY_ROLE 环境变量
partyRole := os.Getenv("PARTY_ROLE")
if partyRole != "delegate" {
    c.JSON(http.StatusForbidden, gin.H{
        "error": "This endpoint is only available for delegate parties",
    })
    return
}

测试指南

端到端测试流程

# 1. 创建 Session
curl -X POST http://localhost:9091/api/v1/sessions/create \
  -H "Content-Type: application/json" \
  -d '{
    "session_type": "keygen",
    "threshold_n": 5,
    "threshold_t": 3,
    "party_composition": {
      "persistent_count": 3,
      "delegate_count": 2
    }
  }'

# Response:
# {
#   "session_id": "550e8400-e29b-41d4-a716-446655440000",
#   "join_tokens": { ... }
# }

# 2. 等待 Keygen 完成(约 30-60 秒)
# Parties 会自动接收 SessionEvent 并执行 Keygen

# 3. 检查日志
docker logs mpc-delegate-party-0 | grep "Share stored in cache"

# 输出:
# INFO  Share stored in cache for user retrieval (delegate party)
#   session_id=550e8400-e29b-41d4-a716-446655440000
#   party_id=mpc-delegate-party-0
#   share_size=256

# 4. 用户获取 Share第一次
curl -X GET http://localhost:8081/api/v1/sessions/550e8400-e29b-41d4-a716-446655440000/user-share

# Response:
# {
#   "session_id": "550e8400-e29b-41d4-a716-446655440000",
#   "party_id": "mpc-delegate-party-0",
#   "share": "0a1b2c3d...",
#   "public_key": "04a1b2c3...",
#   "note": "This share has been deleted from memory..."
# }

# 5. 再次尝试获取(应该失败)
curl -X GET http://localhost:8081/api/v1/sessions/550e8400-e29b-41d4-a716-446655440000/user-share

# Response:
# {
#   "error": "Share not found or already retrieved",
#   "note": "Shares can only be retrieved once..."
# }

# 6. 验证持久化 Party对比
curl -X GET http://localhost:8080/api/v1/shares/mpc-server-party-1

# Response (Persistent Party 有数据库记录):
# {
#   "party_id": "mpc-server-party-1",
#   "count": 1,
#   "shares": [...]
# }

对比总结

Persistent vs Delegate

特性 Persistent Party Delegate Party
存储位置 PostgreSQL 数据库 内存缓存(临时)
持久化 永久保存 不保存
获取方式 数据库查询(多次) HTTP API一次
安全性 服务器托管 用户自持
TTL 无限期 15 分钟
重启后 数据保留 数据丢失

国际标准对比

方案 Persistent Count Delegate Count 模式
Fireblocks 2 1 2-of-31 个用户)
ZenGo 1 1 2-of-21 个用户)
Coinbase 3 0 3-of-5全托管
我们的系统 灵活配置 灵活配置 任意组合

Kubernetes 完整部署示例

# k8s/delegate-party-deployment.yaml
apiVersion: v1
kind: Service
metadata:
  name: mpc-delegate-party
  namespace: mpc-system
spec:
  type: LoadBalancer  # 或 Ingress
  ports:
  - port: 443
    targetPort: 8080
    name: https
  selector:
    app: mpc-server-party
    party-role: delegate

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mpc-delegate-party
  namespace: mpc-system
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mpc-server-party
      party-role: delegate
  template:
    metadata:
      labels:
        app: mpc-server-party
        party-role: delegate
    spec:
      containers:
      - name: delegate-party
        image: rwadurian/mpc-server-party:latest
        env:
        - name: PARTY_ROLE
          value: "delegate"
        - name: MESSAGE_ROUTER_ADDR
          value: "mpc-message-router:9092"
        - name: SESSION_COORDINATOR_ADDR
          value: "mpc-session-coordinator:9091"
        - name: MPC_CRYPTO_MASTER_KEY
          valueFrom:
            secretKeyRef:
              name: mpc-secrets
              key: crypto-master-key
        ports:
        - containerPort: 8080
          name: http
        resources:
          requests:
            memory: "256Mi"
            cpu: "500m"
          limits:
            memory: "512Mi"
            cpu: "1000m"

故障排查

问题 1: Share 获取失败

# 错误: Share not found or already retrieved

# 可能原因:
1. Share 已经被获取过(一次性)
2. Share 已过期15 分钟)
3. Keygen 还未完成
4. PARTY_ROLE 不是 "delegate"

# 排查步骤:
# 检查缓存大小
curl http://localhost:8081/health
# 查看日志
docker logs mpc-delegate-party-0 --tail 100

问题 2: Endpoint 返回 403 Forbidden

# 错误: This endpoint is only available for delegate parties

# 原因: PARTY_ROLE 环境变量不是 "delegate"

# 解决:
kubectl set env deployment/mpc-delegate-party PARTY_ROLE=delegate

总结

已实现功能:

  1. Delegate Party Role 定义
  2. PartyComposition API 支持
  3. Share 不保存到数据库
  4. 内存缓存15 分钟 TTL
  5. HTTP API 一次性获取
  6. 自动清理机制

安全特性:

  1. 一次性获取
  2. 自动过期
  3. 重启清空
  4. Role 权限检查

符合国际标准:

  • Fireblocks: Party-Driven + Delegate Share
  • ZenGo: Hybrid Custody
  • ING Bank: Threshold Signature

文档版本: 1.0 最后更新: 2024-01-01 作者: Claude (Anthropic)