523 lines
12 KiB
Markdown
523 lines
12 KiB
Markdown
# 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**:
|
||
|
||
```yaml
|
||
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):
|
||
|
||
```yaml
|
||
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`):
|
||
|
||
```json
|
||
{
|
||
"session_type": "keygen",
|
||
"threshold_n": 5,
|
||
"threshold_t": 3,
|
||
"expires_in": 600,
|
||
|
||
"party_composition": {
|
||
"persistent_count": 3,
|
||
"delegate_count": 2
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response**:
|
||
|
||
```json
|
||
{
|
||
"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**:
|
||
|
||
```go
|
||
// 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**:
|
||
|
||
```go
|
||
// 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):
|
||
|
||
```go
|
||
// 如果是 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`):
|
||
|
||
```bash
|
||
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>"
|
||
```
|
||
|
||
**成功响应**:
|
||
|
||
```json
|
||
{
|
||
"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"
|
||
}
|
||
```
|
||
|
||
**错误响应** (已被获取或过期):
|
||
|
||
```json
|
||
{
|
||
"error": "Share not found or already retrieved",
|
||
"note": "Shares can only be retrieved once and expire after 15 minutes"
|
||
}
|
||
```
|
||
|
||
**Forbidden** (非 delegate party):
|
||
|
||
```json
|
||
{
|
||
"error": "This endpoint is only available for delegate parties",
|
||
"role": "persistent"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 安全特性
|
||
|
||
### 1. 内存缓存(非持久化)
|
||
|
||
```go
|
||
// share_cache.go
|
||
type ShareCache struct {
|
||
entries map[string]*ShareCacheEntry // session_id -> entry
|
||
mu sync.RWMutex
|
||
ttl time.Duration // 15 分钟
|
||
}
|
||
```
|
||
|
||
**特点**:
|
||
- ✅ 只存在内存中(不写入磁盘)
|
||
- ✅ 服务重启自动清空
|
||
- ✅ 15 分钟 TTL 自动过期
|
||
- ✅ 后台定时清理过期数据
|
||
|
||
### 2. 一次性获取
|
||
|
||
```go
|
||
// 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 检查
|
||
|
||
```go
|
||
// 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
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 测试指南
|
||
|
||
### 端到端测试流程
|
||
|
||
```bash
|
||
# 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-3(1 个用户) |
|
||
| **ZenGo** | 1 | 1 | 2-of-2(1 个用户) |
|
||
| **Coinbase** | 3 | 0 | 3-of-5(全托管) |
|
||
| **我们的系统** | **灵活配置** | **灵活配置** | 任意组合 |
|
||
|
||
---
|
||
|
||
## Kubernetes 完整部署示例
|
||
|
||
```yaml
|
||
# 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 获取失败
|
||
|
||
```bash
|
||
# 错误: 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
|
||
|
||
```bash
|
||
# 错误: 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)
|