rwadurian/backend/services/mpc-service/MPC-Service-Context-Complet...

2285 lines
67 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MPC Service Context - 完整技术规范
> RWA榴莲树系统的MPC Server Party服务 - 作为分布式签名的服务器参与方
## 目录
1. [服务概述](#1-服务概述)
2. [目录结构](#2-目录结构)
3. [领域模型设计](#3-领域模型设计)
4. [应用层设计](#4-应用层设计)
5. [基础设施层](#5-基础设施层)
6. [API接口](#6-api接口)
7. [数据库设计](#7-数据库设计)
8. [配置文件](#8-配置文件)
9. [Docker部署](#9-docker部署)
10. [测试规范](#10-测试规范)
---
## 1. 服务概述
### 1.1 服务定位
**mpc-service** 是RWA系统中的一个Context作为MPC分布式签名的**Server Party**
- 🔐 **持有服务器端的Share**1/3 或 1/5
- 🤝 **作为对等参与方**参与MPC协议Keygen/Signing
- 🔧 **提供签名服务**给其他ContextWallet、Admin等
- 🚫 **不协调会话**由MPC系统的Session Coordinator负责
### 1.2 职责边界
| 职责 | 说明 |
|------|------|
| ✅ Share管理 | 安全存储和管理服务器端的Share |
| ✅ MPC参与 | 运行tss-lib参与Keygen/Signing |
| ✅ 对外服务 | 提供gRPC/REST API给其他Context |
| ❌ 会话创建 | 由Wallet/Admin Service创建 |
| ❌ 会话协调 | 由MPC Session Coordinator负责 |
| ❌ 业务逻辑 | 只提供签名能力,不管业务 |
### 1.3 集成关系
```
┌──────────────────── RWA系统 ────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Wallet │ │ Admin │ │
│ │ Service │ │ Service │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ │ 调用MPC服务 │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ MPC │◄─── 新增的Context │
│ │ Service │ │
│ │ │ │
│ │ 持有Share │ │
│ └──────┬──────┘ │
│ │ │
└────────────────┼─────────────────────────────────┘
│ 调用外部MPC系统
┌────────────────────────────────────────┐
│ MPC基础设施独立
│ • Session Coordinator │
│ • Message Router │
└────────────────────────────────────────┘
```
---
## 2. 目录结构
### 2.1 完整目录树
```
services/
└── mpc-service/ # Context 6: MPC Server Party
├── src/
│ ├── api/ # 表现层Presentation Layer
│ │ ├── controllers/
│ │ │ ├── mpc-party.controller.ts
│ │ │ └── health.controller.ts
│ │ ├── dto/
│ │ │ ├── participate-keygen.dto.ts
│ │ │ ├── participate-signing.dto.ts
│ │ │ ├── share-info.dto.ts
│ │ │ └── mpc-response.dto.ts
│ │ └── validators/
│ │ ├── session-id.validator.ts
│ │ └── party-id.validator.ts
│ │
│ ├── application/ # 应用层Application Layer
│ │ ├── commands/
│ │ │ ├── participate-keygen/
│ │ │ │ ├── participate-keygen.command.ts
│ │ │ │ └── participate-keygen.handler.ts
│ │ │ ├── participate-signing/
│ │ │ │ ├── participate-signing.command.ts
│ │ │ │ └── participate-signing.handler.ts
│ │ │ └── rotate-share/
│ │ │ ├── rotate-share.command.ts
│ │ │ └── rotate-share.handler.ts
│ │ ├── queries/
│ │ │ ├── get-share-info/
│ │ │ │ ├── get-share-info.query.ts
│ │ │ │ └── get-share-info.handler.ts
│ │ │ └── list-shares/
│ │ │ ├── list-shares.query.ts
│ │ │ └── list-shares.handler.ts
│ │ └── services/
│ │ ├── mpc-party-application.service.ts
│ │ └── share-encryption.service.ts
│ │
│ ├── domain/ # 领域层Domain Layer
│ │ ├── aggregates/
│ │ │ └── party-session/
│ │ │ ├── party-session.aggregate.ts
│ │ │ ├── party-session.factory.ts
│ │ │ └── party-session.spec.ts
│ │ ├── entities/
│ │ │ ├── party-share.entity.ts
│ │ │ ├── mnemonic-record.entity.ts
│ │ │ └── share-backup.entity.ts
│ │ ├── value-objects/
│ │ │ ├── session-id.vo.ts
│ │ │ ├── party-id.vo.ts
│ │ │ ├── share-data.vo.ts
│ │ │ ├── threshold.vo.ts
│ │ │ └── public-key.vo.ts
│ │ ├── events/
│ │ │ ├── share-created.event.ts
│ │ │ ├── keygen-completed.event.ts
│ │ │ ├── signing-completed.event.ts
│ │ │ └── share-rotated.event.ts
│ │ ├── repositories/
│ │ │ ├── party-share.repository.interface.ts
│ │ │ └── session-state.repository.interface.ts
│ │ └── services/
│ │ ├── tss-lib.domain-service.ts
│ │ ├── share-encryption.domain-service.ts
│ │ └── key-derivation.domain-service.ts
│ │
│ └── infrastructure/ # 基础设施层Infrastructure Layer
│ ├── persistence/
│ │ ├── mysql/
│ │ │ ├── entities/
│ │ │ │ ├── party-share.entity.ts
│ │ │ │ ├── session-state.entity.ts
│ │ │ │ └── share-backup.entity.ts
│ │ │ ├── mappers/
│ │ │ │ ├── party-share.mapper.ts
│ │ │ │ └── session-state.mapper.ts
│ │ │ └── repositories/
│ │ │ ├── party-share.repository.impl.ts
│ │ │ └── session-state.repository.impl.ts
│ │ └── redis/
│ │ ├── cache/
│ │ │ └── session-cache.service.ts
│ │ └── lock/
│ │ └── distributed-lock.service.ts
│ ├── messaging/
│ │ ├── kafka/
│ │ │ ├── mpc-event.publisher.ts
│ │ │ └── event-bus.ts
│ │ └── rabbitmq/
│ │ └── message-queue.service.ts
│ ├── external/
│ │ ├── mpc-system/
│ │ │ ├── coordinator-client.ts
│ │ │ ├── message-router-client.ts
│ │ │ └── dto/
│ │ │ ├── create-session.dto.ts
│ │ │ └── mpc-message.dto.ts
│ │ ├── tss-lib/
│ │ │ ├── tss-wrapper.ts
│ │ │ ├── keygen.service.ts
│ │ │ └── signing.service.ts
│ │ └── hsm/
│ │ ├── hsm-client.ts
│ │ └── hsm-config.ts
│ └── crypto/
│ ├── aes-encryption.service.ts
│ ├── kdf.service.ts
│ └── secure-random.service.ts
├── tests/
│ ├── unit/
│ │ ├── domain/
│ │ │ └── party-share.entity.spec.ts
│ │ └── application/
│ │ └── participate-keygen.handler.spec.ts
│ ├── integration/
│ │ ├── mpc-party.controller.spec.ts
│ │ └── party-share.repository.spec.ts
│ └── e2e/
│ └── mpc-service.e2e-spec.ts
├── database/
│ └── migrations/
│ ├── 001_create_party_shares_table.sql
│ ├── 002_create_session_states_table.sql
│ └── 003_create_share_backups_table.sql
├── config/
│ ├── development.json
│ ├── production.json
│ └── test.json
├── Dockerfile
├── docker-compose.yml
├── package.json
├── tsconfig.json
├── .env.example
└── README.md
```
### 2.2 目录说明
| 目录 | 职责 | 依赖方向 |
|------|------|---------|
| **api/** | HTTP/gRPC控制器、DTO、验证器 | → application |
| **application/** | Use CasesCommands/Queries、应用服务 | → domain |
| **domain/** | 核心业务逻辑、实体、值对象、领域服务 | 无外部依赖 |
| **infrastructure/** | 数据库、外部服务、消息队列 | → domain |
---
## 3. 领域模型设计
### 3.1 核心实体
#### 3.1.1 PartyShare密钥分片实体
```typescript
// src/domain/entities/party-share.entity.ts
import { AggregateRoot } from '@nestjs/cqrs';
import { SessionId } from '../value-objects/session-id.vo';
import { PartyId } from '../value-objects/party-id.vo';
import { ShareData } from '../value-objects/share-data.vo';
import { Threshold } from '../value-objects/threshold.vo';
import { PublicKey } from '../value-objects/public-key.vo';
import { ShareCreatedEvent } from '../events/share-created.event';
import { ShareRotatedEvent } from '../events/share-rotated.event';
export enum PartyShareType {
WALLET = 'wallet', // 用户钱包
ADMIN = 'admin', // 管理员多签
RECOVERY = 'recovery', // 恢复密钥
}
export enum PartyShareStatus {
ACTIVE = 'active',
ROTATED = 'rotated', // 已轮换
REVOKED = 'revoked', // 已撤销
}
export class PartyShare extends AggregateRoot {
private readonly _id: string;
private readonly _partyId: PartyId;
private readonly _sessionId: SessionId;
private _shareType: PartyShareType;
private _shareData: ShareData; // 加密的Share数据
private _publicKey: PublicKey; // 群公钥
private _threshold: Threshold;
private _status: PartyShareStatus;
private readonly _createdAt: Date;
private _updatedAt: Date;
private _lastUsedAt?: Date;
constructor(
id: string,
partyId: PartyId,
sessionId: SessionId,
shareType: PartyShareType,
shareData: ShareData,
publicKey: PublicKey,
threshold: Threshold,
createdAt: Date = new Date(),
) {
super();
this._id = id;
this._partyId = partyId;
this._sessionId = sessionId;
this._shareType = shareType;
this._shareData = shareData;
this._publicKey = publicKey;
this._threshold = threshold;
this._status = PartyShareStatus.ACTIVE;
this._createdAt = createdAt;
this._updatedAt = createdAt;
}
// Getters
get id(): string { return this._id; }
get partyId(): PartyId { return this._partyId; }
get sessionId(): SessionId { return this._sessionId; }
get shareType(): PartyShareType { return this._shareType; }
get shareData(): ShareData { return this._shareData; }
get publicKey(): PublicKey { return this._publicKey; }
get threshold(): Threshold { return this._threshold; }
get status(): PartyShareStatus { return this._status; }
get createdAt(): Date { return this._createdAt; }
get lastUsedAt(): Date | undefined { return this._lastUsedAt; }
/**
* 记录Share使用
*/
markAsUsed(): void {
this._lastUsedAt = new Date();
this._updatedAt = new Date();
}
/**
* 轮换Share生成新的Share旧的标记为rotated
*/
rotate(newShareData: ShareData): void {
if (this._status !== PartyShareStatus.ACTIVE) {
throw new Error('Cannot rotate non-active share');
}
const oldShareId = this._id;
this._shareData = newShareData;
this._status = PartyShareStatus.ROTATED;
this._updatedAt = new Date();
// 发布领域事件
this.apply(new ShareRotatedEvent(
this._id,
oldShareId,
this._partyId.value,
new Date(),
));
}
/**
* 撤销Share
*/
revoke(reason: string): void {
if (this._status === PartyShareStatus.REVOKED) {
throw new Error('Share already revoked');
}
this._status = PartyShareStatus.REVOKED;
this._updatedAt = new Date();
}
/**
* 验证阈值
*/
validateThreshold(participantsCount: number): boolean {
return this._threshold.validate(participantsCount);
}
/**
* 创建Share的工厂方法
*/
static create(
partyId: PartyId,
sessionId: SessionId,
shareType: PartyShareType,
shareData: ShareData,
publicKey: PublicKey,
threshold: Threshold,
): PartyShare {
const id = this.generateId();
const share = new PartyShare(
id,
partyId,
sessionId,
shareType,
shareData,
publicKey,
threshold,
);
// 发布领域事件
share.apply(new ShareCreatedEvent(
id,
partyId.value,
sessionId.value,
shareType,
publicKey.toHex(),
new Date(),
));
return share;
}
private static generateId(): string {
return `share_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
```
#### 3.1.2 值对象
```typescript
// src/domain/value-objects/session-id.vo.ts
export class SessionId {
private readonly _value: string;
constructor(value: string) {
this.validate(value);
this._value = value;
}
get value(): string {
return this._value;
}
private validate(value: string): void {
if (!value || value.trim().length === 0) {
throw new Error('SessionId cannot be empty');
}
// UUID格式验证
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(value)) {
throw new Error('Invalid SessionId format');
}
}
equals(other: SessionId): boolean {
return this._value === other._value;
}
}
```
```typescript
// src/domain/value-objects/party-id.vo.ts
export class PartyId {
private readonly _value: string;
constructor(value: string) {
this.validate(value);
this._value = value;
}
get value(): string {
return this._value;
}
private validate(value: string): void {
if (!value || value.trim().length === 0) {
throw new Error('PartyId cannot be empty');
}
// 格式: {userId}-{type} 例如: user123-server
const partyIdRegex = /^[\w-]+-\w+$/;
if (!partyIdRegex.test(value)) {
throw new Error('Invalid PartyId format. Expected: {userId}-{type}');
}
}
equals(other: PartyId): boolean {
return this._value === other._value;
}
getUserId(): string {
return this._value.split('-')[0];
}
getType(): string {
return this._value.split('-')[1];
}
}
```
```typescript
// src/domain/value-objects/threshold.vo.ts
export class Threshold {
private readonly _n: number; // 总参与方数
private readonly _t: number; // 所需签名方数
constructor(n: number, t: number) {
this.validate(n, t);
this._n = n;
this._t = t;
}
get n(): number { return this._n; }
get t(): number { return this._t; }
private validate(n: number, t: number): void {
if (n <= 0 || t <= 0) {
throw new Error('Threshold values must be positive');
}
if (t > n) {
throw new Error('t cannot exceed n');
}
if (t < 2) {
throw new Error('t must be at least 2 for security');
}
}
validate(participantsCount: number): boolean {
return participantsCount >= this._t && participantsCount <= this._n;
}
toString(): string {
return `${this._t}-of-${this._n}`;
}
}
```
```typescript
// src/domain/value-objects/share-data.vo.ts
export class ShareData {
private readonly _encryptedData: Buffer;
private readonly _iv: Buffer; // 初始化向量
private readonly _authTag: Buffer; // 认证标签AES-GCM
constructor(encryptedData: Buffer, iv: Buffer, authTag: Buffer) {
this.validate(encryptedData, iv, authTag);
this._encryptedData = encryptedData;
this._iv = iv;
this._authTag = authTag;
}
get encryptedData(): Buffer { return this._encryptedData; }
get iv(): Buffer { return this._iv; }
get authTag(): Buffer { return this._authTag; }
private validate(data: Buffer, iv: Buffer, authTag: Buffer): void {
if (!data || data.length === 0) {
throw new Error('Encrypted data cannot be empty');
}
if (!iv || iv.length !== 12) { // GCM标准IV长度
throw new Error('IV must be 12 bytes');
}
if (!authTag || authTag.length !== 16) { // GCM标准authTag长度
throw new Error('AuthTag must be 16 bytes');
}
}
toJSON(): { data: string; iv: string; authTag: string } {
return {
data: this._encryptedData.toString('base64'),
iv: this._iv.toString('base64'),
authTag: this._authTag.toString('base64'),
};
}
static fromJSON(json: { data: string; iv: string; authTag: string }): ShareData {
return new ShareData(
Buffer.from(json.data, 'base64'),
Buffer.from(json.iv, 'base64'),
Buffer.from(json.authTag, 'base64'),
);
}
}
```
```typescript
// src/domain/value-objects/public-key.vo.ts
export class PublicKey {
private readonly _keyBytes: Buffer;
constructor(keyBytes: Buffer) {
this.validate(keyBytes);
this._keyBytes = keyBytes;
}
get bytes(): Buffer {
return this._keyBytes;
}
private validate(keyBytes: Buffer): void {
if (!keyBytes || keyBytes.length === 0) {
throw new Error('Public key cannot be empty');
}
// ECDSA公钥通常是33或65字节压缩或未压缩
if (keyBytes.length !== 33 && keyBytes.length !== 65) {
throw new Error('Invalid public key length');
}
}
toHex(): string {
return this._keyBytes.toString('hex');
}
toBase64(): string {
return this._keyBytes.toString('base64');
}
equals(other: PublicKey): boolean {
return this._keyBytes.equals(other._keyBytes);
}
static fromHex(hex: string): PublicKey {
return new PublicKey(Buffer.from(hex, 'hex'));
}
static fromBase64(base64: string): PublicKey {
return new PublicKey(Buffer.from(base64, 'base64'));
}
}
```
### 3.2 领域服务
```typescript
// src/domain/services/share-encryption.domain-service.ts
import { Injectable } from '@nestjs/common';
import { ShareData } from '../value-objects/share-data.vo';
import * as crypto from 'crypto';
/**
* Share加密领域服务
* 职责使用AES-256-GCM加密/解密Share数据
*/
@Injectable()
export class ShareEncryptionDomainService {
private readonly algorithm = 'aes-256-gcm';
private readonly keyLength = 32; // 256 bits
/**
* 加密Share数据
* @param rawShareData - 原始Share数据tss-lib的SaveData
* @param masterKey - 主密钥从HSM或环境变量获取
*/
encrypt(rawShareData: Buffer, masterKey: Buffer): ShareData {
this.validateMasterKey(masterKey);
// 生成随机IV
const iv = crypto.randomBytes(12); // GCM标准IV长度
// 创建加密器
const cipher = crypto.createCipheriv(this.algorithm, masterKey, iv);
// 加密数据
const encrypted = Buffer.concat([
cipher.update(rawShareData),
cipher.final(),
]);
// 获取认证标签
const authTag = cipher.getAuthTag();
return new ShareData(encrypted, iv, authTag);
}
/**
* 解密Share数据
*/
decrypt(shareData: ShareData, masterKey: Buffer): Buffer {
this.validateMasterKey(masterKey);
// 创建解密器
const decipher = crypto.createDecipheriv(
this.algorithm,
masterKey,
shareData.iv,
);
// 设置认证标签
decipher.setAuthTag(shareData.authTag);
// 解密数据
const decrypted = Buffer.concat([
decipher.update(shareData.encryptedData),
decipher.final(),
]);
return decrypted;
}
private validateMasterKey(key: Buffer): void {
if (!key || key.length !== this.keyLength) {
throw new Error(`Master key must be ${this.keyLength} bytes`);
}
}
/**
* 从密码派生密钥(用于开发/测试环境)
*/
deriveKeyFromPassword(password: string, salt: Buffer): Buffer {
return crypto.pbkdf2Sync(
password,
salt,
100000, // 迭代次数
this.keyLength,
'sha256',
);
}
}
```
### 3.3 领域事件
```typescript
// src/domain/events/share-created.event.ts
import { IEvent } from '@nestjs/cqrs';
export class ShareCreatedEvent implements IEvent {
constructor(
public readonly shareId: string,
public readonly partyId: string,
public readonly sessionId: string,
public readonly shareType: string,
public readonly publicKey: string,
public readonly occurredAt: Date,
) {}
}
```
```typescript
// src/domain/events/keygen-completed.event.ts
export class KeygenCompletedEvent implements IEvent {
constructor(
public readonly sessionId: string,
public readonly partyId: string,
public readonly publicKey: string,
public readonly shareId: string,
public readonly occurredAt: Date,
) {}
}
```
```typescript
// src/domain/events/signing-completed.event.ts
export class SigningCompletedEvent implements IEvent {
constructor(
public readonly sessionId: string,
public readonly partyId: string,
public readonly messageHash: string,
public readonly signature: string,
public readonly occurredAt: Date,
) {}
}
```
---
## 4. 应用层设计
### 4.1 Commands写操作
#### 4.1.1 ParticipateInKeygenCommand
```typescript
// src/application/commands/participate-keygen/participate-keygen.command.ts
export class ParticipateInKeygenCommand {
constructor(
public readonly sessionId: string,
public readonly partyId: string,
public readonly joinToken: string,
public readonly shareType: 'wallet' | 'admin' | 'recovery',
) {}
}
```
```typescript
// src/application/commands/participate-keygen/participate-keygen.handler.ts
import { CommandHandler, ICommandHandler, EventBus } from '@nestjs/cqrs';
import { ParticipateInKeygenCommand } from './participate-keygen.command';
import { PartyShareRepository } from '@/domain/repositories/party-share.repository.interface';
import { TssLibDomainService } from '@/domain/services/tss-lib.domain-service';
import { ShareEncryptionDomainService } from '@/domain/services/share-encryption.domain-service';
import { MPCCoordinatorClient } from '@/infrastructure/external/mpc-system/coordinator-client';
import { MPCMessageRouterClient } from '@/infrastructure/external/mpc-system/message-router-client';
import { PartyShare, PartyShareType } from '@/domain/entities/party-share.entity';
import { SessionId } from '@/domain/value-objects/session-id.vo';
import { PartyId } from '@/domain/value-objects/party-id.vo';
import { Threshold } from '@/domain/value-objects/threshold.vo';
import { PublicKey } from '@/domain/value-objects/public-key.vo';
import { KeygenCompletedEvent } from '@/domain/events/keygen-completed.event';
import { Logger } from '@nestjs/common';
@CommandHandler(ParticipateInKeygenCommand)
export class ParticipateInKeygenHandler implements ICommandHandler<ParticipateInKeygenCommand> {
private readonly logger = new Logger(ParticipateInKeygenHandler.name);
constructor(
private readonly partyShareRepo: PartyShareRepository,
private readonly tssLibService: TssLibDomainService,
private readonly encryptionService: ShareEncryptionDomainService,
private readonly coordinatorClient: MPCCoordinatorClient,
private readonly messageRouterClient: MPCMessageRouterClient,
private readonly eventBus: EventBus,
) {}
async execute(command: ParticipateInKeygenCommand): Promise<PartyShare> {
this.logger.log(`Starting Keygen participation for party: ${command.partyId}`);
// 1. 加入会话从MPC Coordinator获取会话信息
const sessionInfo = await this.coordinatorClient.joinSession({
sessionId: command.sessionId,
partyId: command.partyId,
joinToken: command.joinToken,
});
this.logger.log(`Joined session ${command.sessionId}, ${sessionInfo.participants.length} participants`);
// 2. 初始化TSS Party参数
const tssParty = await this.tssLibService.initializeKeygenParty(
command.partyId,
sessionInfo.participants,
sessionInfo.thresholdN,
sessionInfo.thresholdT,
);
// 3. 设置消息路由
const messageStream = await this.messageRouterClient.subscribeMessages(
command.sessionId,
command.partyId,
);
// 处理incoming消息
messageStream.on('message', (msg) => {
this.tssLibService.handleIncomingMessage(tssParty, msg);
});
// 处理outgoing消息
tssParty.on('outgoing', (msg) => {
this.messageRouterClient.sendMessage({
sessionId: command.sessionId,
fromParty: command.partyId,
toParties: msg.toParties,
roundNumber: msg.roundNumber,
payload: msg.payload,
});
});
// 4. 启动TSS Keygen协议
this.logger.log('Starting TSS Keygen protocol...');
const keygenResult = await this.tssLibService.runKeygen(tssParty);
this.logger.log('Keygen completed successfully');
// 5. 加密Share数据
const masterKey = await this.getMasterKey(); // 从HSM或配置获取
const encryptedShareData = this.encryptionService.encrypt(
keygenResult.shareData,
masterKey,
);
// 6. 创建PartyShare实体
const partyShare = PartyShare.create(
new PartyId(command.partyId),
new SessionId(command.sessionId),
command.shareType as PartyShareType,
encryptedShareData,
PublicKey.fromHex(keygenResult.publicKey),
new Threshold(sessionInfo.thresholdN, sessionInfo.thresholdT),
);
// 7. 持久化Share
await this.partyShareRepo.save(partyShare);
// 8. 通知Coordinator完成
await this.coordinatorClient.reportCompletion({
sessionId: command.sessionId,
partyId: command.partyId,
publicKey: keygenResult.publicKey,
});
// 9. 发布领域事件
this.eventBus.publish(new KeygenCompletedEvent(
command.sessionId,
command.partyId,
keygenResult.publicKey,
partyShare.id,
new Date(),
));
this.logger.log(`Keygen completed, ShareID: ${partyShare.id}`);
return partyShare;
}
private async getMasterKey(): Promise<Buffer> {
// 生产环境从HSM获取
// 开发环境:从环境变量获取
const keyHex = process.env.SHARE_MASTER_KEY;
if (!keyHex) {
throw new Error('SHARE_MASTER_KEY not configured');
}
return Buffer.from(keyHex, 'hex');
}
}
```
#### 4.1.2 ParticipateInSigningCommand
```typescript
// src/application/commands/participate-signing/participate-signing.command.ts
export class ParticipateInSigningCommand {
constructor(
public readonly sessionId: string,
public readonly partyId: string,
public readonly joinToken: string,
public readonly messageHash: string,
) {}
}
```
```typescript
// src/application/commands/participate-signing/participate-signing.handler.ts
@CommandHandler(ParticipateInSigningCommand)
export class ParticipateInSigningHandler implements ICommandHandler<ParticipateInSigningCommand> {
private readonly logger = new Logger(ParticipateInSigningHandler.name);
constructor(
private readonly partyShareRepo: PartyShareRepository,
private readonly tssLibService: TssLibDomainService,
private readonly encryptionService: ShareEncryptionDomainService,
private readonly coordinatorClient: MPCCoordinatorClient,
private readonly messageRouterClient: MPCMessageRouterClient,
private readonly eventBus: EventBus,
) {}
async execute(command: ParticipateInSigningCommand): Promise<{ signature: string }> {
this.logger.log(`Starting Signing participation for party: ${command.partyId}`);
// 1. 加入签名会话
const sessionInfo = await this.coordinatorClient.joinSession({
sessionId: command.sessionId,
partyId: command.partyId,
joinToken: command.joinToken,
});
// 2. 加载对应的Share根据公钥匹配
const partyShare = await this.partyShareRepo.findByPartyIdAndPublicKey(
command.partyId,
sessionInfo.publicKey,
);
if (!partyShare) {
throw new Error('Party share not found for this public key');
}
// 3. 解密Share数据
const masterKey = await this.getMasterKey();
const rawShareData = this.encryptionService.decrypt(
partyShare.shareData,
masterKey,
);
// 4. 初始化TSS Signing Party
const tssParty = await this.tssLibService.initializeSigningParty(
command.partyId,
sessionInfo.participants,
sessionInfo.thresholdN,
sessionInfo.thresholdT,
rawShareData,
command.messageHash,
);
// 5. 设置消息路由同Keygen
const messageStream = await this.messageRouterClient.subscribeMessages(
command.sessionId,
command.partyId,
);
messageStream.on('message', (msg) => {
this.tssLibService.handleIncomingMessage(tssParty, msg);
});
tssParty.on('outgoing', (msg) => {
this.messageRouterClient.sendMessage({
sessionId: command.sessionId,
fromParty: command.partyId,
toParties: msg.toParties,
roundNumber: msg.roundNumber,
payload: msg.payload,
});
});
// 6. 启动TSS Signing协议
this.logger.log('Starting TSS Signing protocol...');
const signingResult = await this.tssLibService.runSigning(tssParty);
this.logger.log('Signing completed successfully');
// 7. 更新Share使用时间
partyShare.markAsUsed();
await this.partyShareRepo.update(partyShare);
// 8. 通知Coordinator完成
await this.coordinatorClient.reportCompletion({
sessionId: command.sessionId,
partyId: command.partyId,
signature: signingResult.signature,
});
// 9. 发布领域事件
this.eventBus.publish(new SigningCompletedEvent(
command.sessionId,
command.partyId,
command.messageHash,
signingResult.signature,
new Date(),
));
return { signature: signingResult.signature };
}
private async getMasterKey(): Promise<Buffer> {
const keyHex = process.env.SHARE_MASTER_KEY;
if (!keyHex) {
throw new Error('SHARE_MASTER_KEY not configured');
}
return Buffer.from(keyHex, 'hex');
}
}
```
### 4.2 Queries读操作
```typescript
// src/application/queries/get-share-info/get-share-info.query.ts
export class GetShareInfoQuery {
constructor(
public readonly shareId: string,
) {}
}
```
```typescript
// src/application/queries/get-share-info/get-share-info.handler.ts
@QueryHandler(GetShareInfoQuery)
export class GetShareInfoHandler implements IQueryHandler<GetShareInfoQuery> {
constructor(
private readonly partyShareRepo: PartyShareRepository,
) {}
async execute(query: GetShareInfoQuery): Promise<ShareInfoDto> {
const share = await this.partyShareRepo.findById(query.shareId);
if (!share) {
throw new Error('Share not found');
}
return {
id: share.id,
partyId: share.partyId.value,
sessionId: share.sessionId.value,
shareType: share.shareType,
publicKey: share.publicKey.toHex(),
threshold: share.threshold.toString(),
status: share.status,
createdAt: share.createdAt,
lastUsedAt: share.lastUsedAt,
};
}
}
```
---
## 5. 基础设施层
### 5.1 数据库实现
```typescript
// src/infrastructure/persistence/mysql/entities/party-share.entity.ts
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('party_shares')
export class PartyShareEntity {
@PrimaryColumn({ type: 'varchar', length: 255 })
id: string;
@Column({ type: 'varchar', length: 255, name: 'party_id' })
partyId: string;
@Column({ type: 'varchar', length: 255, name: 'session_id' })
sessionId: string;
@Column({ type: 'varchar', length: 20, name: 'share_type' })
shareType: string;
@Column({ type: 'text', name: 'share_data' })
shareData: string; // JSON格式存储 {data, iv, authTag}
@Column({ type: 'text', name: 'public_key' })
publicKey: string; // Hex格式
@Column({ type: 'int', name: 'threshold_n' })
thresholdN: number;
@Column({ type: 'int', name: 'threshold_t' })
thresholdT: number;
@Column({ type: 'varchar', length: 20, default: 'active' })
status: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
@Column({ type: 'timestamp', nullable: true, name: 'last_used_at' })
lastUsedAt: Date | null;
}
```
```typescript
// src/infrastructure/persistence/mysql/repositories/party-share.repository.impl.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PartyShareRepository } from '@/domain/repositories/party-share.repository.interface';
import { PartyShare } from '@/domain/entities/party-share.entity';
import { PartyShareEntity } from '../entities/party-share.entity';
import { PartyShareMapper } from '../mappers/party-share.mapper';
@Injectable()
export class PartyShareRepositoryImpl implements PartyShareRepository {
constructor(
@InjectRepository(PartyShareEntity)
private readonly repository: Repository<PartyShareEntity>,
private readonly mapper: PartyShareMapper,
) {}
async save(partyShare: PartyShare): Promise<void> {
const entity = this.mapper.toEntity(partyShare);
await this.repository.save(entity);
}
async update(partyShare: PartyShare): Promise<void> {
const entity = this.mapper.toEntity(partyShare);
await this.repository.update(entity.id, entity);
}
async findById(id: string): Promise<PartyShare | null> {
const entity = await this.repository.findOne({ where: { id } });
return entity ? this.mapper.toDomain(entity) : null;
}
async findByPartyIdAndPublicKey(partyId: string, publicKey: string): Promise<PartyShare | null> {
const entity = await this.repository.findOne({
where: {
partyId,
publicKey,
status: 'active',
},
});
return entity ? this.mapper.toDomain(entity) : null;
}
async findBySessionId(sessionId: string): Promise<PartyShare[]> {
const entities = await this.repository.find({
where: { sessionId },
});
return entities.map(e => this.mapper.toDomain(e));
}
}
```
```typescript
// src/infrastructure/persistence/mysql/mappers/party-share.mapper.ts
import { Injectable } from '@nestjs/common';
import { PartyShare, PartyShareType, PartyShareStatus } from '@/domain/entities/party-share.entity';
import { PartyShareEntity } from '../entities/party-share.entity';
import { PartyId } from '@/domain/value-objects/party-id.vo';
import { SessionId } from '@/domain/value-objects/session-id.vo';
import { ShareData } from '@/domain/value-objects/share-data.vo';
import { PublicKey } from '@/domain/value-objects/public-key.vo';
import { Threshold } from '@/domain/value-objects/threshold.vo';
@Injectable()
export class PartyShareMapper {
toDomain(entity: PartyShareEntity): PartyShare {
const shareDataJson = JSON.parse(entity.shareData);
return new PartyShare(
entity.id,
new PartyId(entity.partyId),
new SessionId(entity.sessionId),
entity.shareType as PartyShareType,
ShareData.fromJSON(shareDataJson),
PublicKey.fromHex(entity.publicKey),
new Threshold(entity.thresholdN, entity.thresholdT),
entity.createdAt,
);
}
toEntity(domain: PartyShare): PartyShareEntity {
const entity = new PartyShareEntity();
entity.id = domain.id;
entity.partyId = domain.partyId.value;
entity.sessionId = domain.sessionId.value;
entity.shareType = domain.shareType;
entity.shareData = JSON.stringify(domain.shareData.toJSON());
entity.publicKey = domain.publicKey.toHex();
entity.thresholdN = domain.threshold.n;
entity.thresholdT = domain.threshold.t;
entity.status = domain.status;
entity.createdAt = domain.createdAt;
entity.lastUsedAt = domain.lastUsedAt || null;
return entity;
}
}
```
### 5.2 外部MPC系统客户端
```typescript
// src/infrastructure/external/mpc-system/coordinator-client.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
export interface JoinSessionRequest {
sessionId: string;
partyId: string;
joinToken: string;
}
export interface SessionInfo {
sessionId: string;
sessionType: 'keygen' | 'sign';
thresholdN: number;
thresholdT: number;
participants: Array<{ partyId: string; partyIndex: number }>;
publicKey?: string;
messageHash?: string;
}
@Injectable()
export class MPCCoordinatorClient {
private readonly logger = new Logger(MPCCoordinatorClient.name);
private readonly client: AxiosInstance;
constructor(private readonly configService: ConfigService) {
const baseURL = this.configService.get<string>('MPC_COORDINATOR_URL');
this.client = axios.create({
baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* 加入MPC会话
*/
async joinSession(request: JoinSessionRequest): Promise<SessionInfo> {
this.logger.log(`Joining session: ${request.sessionId}`);
try {
const response = await this.client.post('/sessions/join', {
session_id: request.sessionId,
party_id: request.partyId,
join_token: request.joinToken,
});
return {
sessionId: response.data.session_info.session_id,
sessionType: response.data.session_info.session_type,
thresholdN: response.data.session_info.threshold_n,
thresholdT: response.data.session_info.threshold_t,
participants: response.data.other_parties.map((p: any) => ({
partyId: p.party_id,
partyIndex: p.party_index,
})),
publicKey: response.data.session_info.public_key,
messageHash: response.data.session_info.message_hash,
};
} catch (error) {
this.logger.error('Failed to join session', error);
throw new Error(`Failed to join MPC session: ${error.message}`);
}
}
/**
* 上报完成状态
*/
async reportCompletion(data: {
sessionId: string;
partyId: string;
publicKey?: string;
signature?: string;
}): Promise<void> {
this.logger.log(`Reporting completion for session: ${data.sessionId}`);
try {
await this.client.post('/sessions/report-completion', {
session_id: data.sessionId,
party_id: data.partyId,
public_key: data.publicKey,
signature: data.signature,
});
} catch (error) {
this.logger.error('Failed to report completion', error);
throw new Error(`Failed to report completion: ${error.message}`);
}
}
/**
* 获取会话状态
*/
async getSessionStatus(sessionId: string): Promise<any> {
try {
const response = await this.client.get(`/sessions/${sessionId}/status`);
return response.data;
} catch (error) {
this.logger.error('Failed to get session status', error);
throw new Error(`Failed to get session status: ${error.message}`);
}
}
}
```
```typescript
// src/infrastructure/external/mpc-system/message-router-client.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import WebSocket from 'ws';
import { EventEmitter } from 'events';
export interface MPCMessage {
fromParty: string;
toParties?: string[];
roundNumber: number;
payload: Buffer;
}
@Injectable()
export class MPCMessageRouterClient {
private readonly logger = new Logger(MPCMessageRouterClient.name);
private readonly wsUrl: string;
constructor(private readonly configService: ConfigService) {
this.wsUrl = this.configService.get<string>('MPC_MESSAGE_ROUTER_WS_URL');
}
/**
* 订阅会话消息WebSocket流
*/
async subscribeMessages(
sessionId: string,
partyId: string,
): Promise<EventEmitter> {
this.logger.log(`Subscribing to messages for session: ${sessionId}, party: ${partyId}`);
const ws = new WebSocket(`${this.wsUrl}/sessions/${sessionId}/messages?party_id=${partyId}`);
const emitter = new EventEmitter();
ws.on('open', () => {
this.logger.log('WebSocket connected');
});
ws.on('message', (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
emitter.emit('message', {
fromParty: message.from_party,
roundNumber: message.round_number,
payload: Buffer.from(message.payload, 'base64'),
});
} catch (error) {
this.logger.error('Failed to parse message', error);
}
});
ws.on('error', (error) => {
this.logger.error('WebSocket error', error);
emitter.emit('error', error);
});
ws.on('close', () => {
this.logger.log('WebSocket closed');
emitter.emit('close');
});
return emitter;
}
/**
* 发送消息到其他Party
*/
async sendMessage(data: {
sessionId: string;
fromParty: string;
toParties?: string[];
roundNumber: number;
payload: Buffer;
}): Promise<void> {
// 使用HTTP POST发送消息或保持WebSocket发送
// 这里简化为HTTP
try {
await axios.post(`${this.wsUrl}/sessions/${data.sessionId}/messages`, {
from_party: data.fromParty,
to_parties: data.toParties,
round_number: data.roundNumber,
payload: data.payload.toString('base64'),
});
} catch (error) {
this.logger.error('Failed to send message', error);
throw error;
}
}
}
```
### 5.3 TSS-Lib封装
```typescript
// src/infrastructure/external/tss-lib/tss-wrapper.ts
import { Injectable, Logger } from '@nestjs/common';
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import * as path from 'path';
const execAsync = promisify(exec);
/**
* TSS-Lib包装器
* 通过调用Go编译的二进制文件来运行tss-lib
* 或使用Go Mobile绑定或gRPC服务
*/
@Injectable()
export class TssWrapper {
private readonly logger = new Logger(TssWrapper.name);
private readonly tssLibPath: string;
constructor() {
// TSS-Lib二进制文件路径
this.tssLibPath = process.env.TSS_LIB_PATH || '/opt/tss-lib/tss-keygen';
}
/**
* 运行Keygen
*/
async runKeygen(params: {
partyId: string;
partyIndex: number;
thresholdN: number;
thresholdT: number;
parties: Array<{ id: string; index: number }>;
messageChannel: string; // 消息交换通道文件路径或socket
}): Promise<{ shareData: Buffer; publicKey: string }> {
this.logger.log(`Running TSS Keygen for party ${params.partyId}`);
// 准备输入文件
const inputFile = await this.prepareKeygenInput(params);
const outputFile = `/tmp/keygen_output_${params.partyId}_${Date.now()}.json`;
try {
// 调用TSS-Lib二进制
const command = `${this.tssLibPath} keygen ` +
`--input ${inputFile} ` +
`--output ${outputFile} ` +
`--party-id ${params.partyId} ` +
`--party-index ${params.partyIndex} ` +
`--threshold-n ${params.thresholdN} ` +
`--threshold-t ${params.thresholdT}`;
const { stdout, stderr } = await execAsync(command, {
timeout: 300000, // 5分钟超时
});
if (stderr) {
this.logger.warn(`TSS-Lib stderr: ${stderr}`);
}
// 读取输出
const outputData = await fs.readFile(outputFile, 'utf-8');
const result = JSON.parse(outputData);
// 清理临时文件
await Promise.all([
fs.unlink(inputFile),
fs.unlink(outputFile),
]);
return {
shareData: Buffer.from(result.share_data, 'base64'),
publicKey: result.public_key,
};
} catch (error) {
this.logger.error('TSS Keygen failed', error);
throw new Error(`TSS Keygen failed: ${error.message}`);
}
}
/**
* 运行Signing
*/
async runSigning(params: {
partyId: string;
partyIndex: number;
thresholdN: number;
thresholdT: number;
parties: Array<{ id: string; index: number }>;
shareData: Buffer;
messageHash: string;
messageChannel: string;
}): Promise<{ signature: string }> {
this.logger.log(`Running TSS Signing for party ${params.partyId}`);
const inputFile = await this.prepareSigningInput(params);
const outputFile = `/tmp/signing_output_${params.partyId}_${Date.now()}.json`;
try {
const command = `${this.tssLibPath} sign ` +
`--input ${inputFile} ` +
`--output ${outputFile} ` +
`--party-id ${params.partyId} ` +
`--party-index ${params.partyIndex} ` +
`--message-hash ${params.messageHash}`;
const { stdout, stderr } = await execAsync(command, {
timeout: 180000, // 3分钟超时
});
const outputData = await fs.readFile(outputFile, 'utf-8');
const result = JSON.parse(outputData);
await Promise.all([
fs.unlink(inputFile),
fs.unlink(outputFile),
]);
return {
signature: result.signature,
};
} catch (error) {
this.logger.error('TSS Signing failed', error);
throw new Error(`TSS Signing failed: ${error.message}`);
}
}
private async prepareKeygenInput(params: any): Promise<string> {
const inputPath = `/tmp/keygen_input_${params.partyId}_${Date.now()}.json`;
await fs.writeFile(inputPath, JSON.stringify({
parties: params.parties,
threshold_n: params.thresholdN,
threshold_t: params.thresholdT,
message_channel: params.messageChannel,
}));
return inputPath;
}
private async prepareSigningInput(params: any): Promise<string> {
const inputPath = `/tmp/signing_input_${params.partyId}_${Date.now()}.json`;
await fs.writeFile(inputPath, JSON.stringify({
parties: params.parties,
threshold_n: params.thresholdN,
threshold_t: params.thresholdT,
share_data: params.shareData.toString('base64'),
message_hash: params.messageHash,
message_channel: params.messageChannel,
}));
return inputPath;
}
}
```
---
## 6. API接口
### 6.1 REST API控制器
```typescript
// src/api/controllers/mpc-party.controller.ts
import {
Controller,
Post,
Get,
Body,
Param,
HttpCode,
HttpStatus,
UseGuards,
Logger,
} from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { ParticipateInKeygenCommand } from '@/application/commands/participate-keygen/participate-keygen.command';
import { ParticipateInSigningCommand } from '@/application/commands/participate-signing/participate-signing.command';
import { GetShareInfoQuery } from '@/application/queries/get-share-info/get-share-info.query';
import { ParticipateKeygenDto } from '../dto/participate-keygen.dto';
import { ParticipateSigningDto } from '../dto/participate-signing.dto';
import { JwtAuthGuard } from '@/infrastructure/auth/jwt-auth.guard';
@ApiTags('MPC Party')
@Controller('mpc-party')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class MPCPartyController {
private readonly logger = new Logger(MPCPartyController.name);
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
/**
* 参与Keygen会话
*/
@Post('keygen/participate')
@HttpCode(HttpStatus.ACCEPTED)
@ApiOperation({ summary: 'Participate in MPC Keygen session' })
@ApiResponse({ status: 202, description: 'Keygen participation accepted' })
async participateInKeygen(@Body() dto: ParticipateKeygenDto) {
this.logger.log(`Received Keygen participation request for session: ${dto.sessionId}`);
const command = new ParticipateInKeygenCommand(
dto.sessionId,
dto.partyId,
dto.joinToken,
dto.shareType,
);
// 异步执行因为MPC协议可能需要几分钟
this.commandBus.execute(command).catch(error => {
this.logger.error('Keygen failed', error);
});
return {
message: 'Keygen participation started',
sessionId: dto.sessionId,
partyId: dto.partyId,
};
}
/**
* 参与Signing会话
*/
@Post('signing/participate')
@HttpCode(HttpStatus.ACCEPTED)
@ApiOperation({ summary: 'Participate in MPC Signing session' })
@ApiResponse({ status: 202, description: 'Signing participation accepted' })
async participateInSigning(@Body() dto: ParticipateSigningDto) {
this.logger.log(`Received Signing participation request for session: ${dto.sessionId}`);
const command = new ParticipateInSigningCommand(
dto.sessionId,
dto.partyId,
dto.joinToken,
dto.messageHash,
);
// 异步执行
this.commandBus.execute(command).catch(error => {
this.logger.error('Signing failed', error);
});
return {
message: 'Signing participation started',
sessionId: dto.sessionId,
partyId: dto.partyId,
};
}
/**
* 获取Share信息
*/
@Get('shares/:shareId')
@ApiOperation({ summary: 'Get share information' })
async getShareInfo(@Param('shareId') shareId: string) {
const query = new GetShareInfoQuery(shareId);
return await this.queryBus.execute(query);
}
/**
* 健康检查
*/
@Get('health')
@ApiOperation({ summary: 'Health check' })
health() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
service: 'mpc-party-service',
};
}
}
```
### 6.2 DTO定义
```typescript
// src/api/dto/participate-keygen.dto.ts
import { IsString, IsEnum, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ParticipateKeygenDto {
@ApiProperty({
description: 'MPC session ID',
example: '550e8400-e29b-41d4-a716-446655440000',
})
@IsString()
@IsNotEmpty()
sessionId: string;
@ApiProperty({
description: 'Party ID (format: {userId}-server)',
example: 'user123-server',
})
@IsString()
@IsNotEmpty()
partyId: string;
@ApiProperty({
description: 'Join token from session coordinator',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
@IsString()
@IsNotEmpty()
joinToken: string;
@ApiProperty({
description: 'Share type',
enum: ['wallet', 'admin', 'recovery'],
example: 'wallet',
})
@IsEnum(['wallet', 'admin', 'recovery'])
shareType: 'wallet' | 'admin' | 'recovery';
}
```
```typescript
// src/api/dto/participate-signing.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ParticipateSigningDto {
@ApiProperty({
description: 'MPC session ID',
example: '550e8400-e29b-41d4-a716-446655440000',
})
@IsString()
@IsNotEmpty()
sessionId: string;
@ApiProperty({
description: 'Party ID',
example: 'user123-server',
})
@IsString()
@IsNotEmpty()
partyId: string;
@ApiProperty({
description: 'Join token',
})
@IsString()
@IsNotEmpty()
joinToken: string;
@ApiProperty({
description: 'Message hash to sign (hex format)',
example: '0x1a2b3c4d...',
})
@IsString()
@IsNotEmpty()
messageHash: string;
}
```
---
## 7. 数据库设计
### 7.1 MySQL Schema
```sql
-- database/migrations/001_create_party_shares_table.sql
CREATE TABLE `party_shares` (
`id` VARCHAR(255) PRIMARY KEY,
`party_id` VARCHAR(255) NOT NULL,
`session_id` VARCHAR(255) NOT NULL,
`share_type` VARCHAR(20) NOT NULL,
`share_data` TEXT NOT NULL COMMENT 'Encrypted share data (JSON: {data, iv, authTag})',
`public_key` TEXT NOT NULL COMMENT 'Group public key (hex)',
`threshold_n` INT NOT NULL,
`threshold_t` INT NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'active',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`last_used_at` TIMESTAMP NULL,
INDEX `idx_party_id` (`party_id`),
INDEX `idx_session_id` (`session_id`),
INDEX `idx_public_key` (`public_key`(255)),
INDEX `idx_status` (`status`),
UNIQUE KEY `uk_party_session` (`party_id`, `session_id`),
CONSTRAINT `chk_share_type` CHECK (`share_type` IN ('wallet', 'admin', 'recovery')),
CONSTRAINT `chk_status` CHECK (`status` IN ('active', 'rotated', 'revoked')),
CONSTRAINT `chk_threshold` CHECK (`threshold_t` <= `threshold_n` AND `threshold_t` >= 2)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Session状态表用于跟踪正在进行的会话
CREATE TABLE `session_states` (
`id` VARCHAR(255) PRIMARY KEY,
`session_id` VARCHAR(255) NOT NULL UNIQUE,
`party_id` VARCHAR(255) NOT NULL,
`session_type` VARCHAR(20) NOT NULL,
`status` VARCHAR(20) NOT NULL,
`current_round` INT DEFAULT 0,
`error_message` TEXT NULL,
`started_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`completed_at` TIMESTAMP NULL,
INDEX `idx_session_id` (`session_id`),
INDEX `idx_party_id` (`party_id`),
INDEX `idx_status` (`status`),
CONSTRAINT `chk_session_type` CHECK (`session_type` IN ('keygen', 'sign')),
CONSTRAINT `chk_session_status` CHECK (`status` IN ('in_progress', 'completed', 'failed', 'timeout'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Share备份表用于灾难恢复
CREATE TABLE `share_backups` (
`id` VARCHAR(255) PRIMARY KEY,
`share_id` VARCHAR(255) NOT NULL,
`backup_data` TEXT NOT NULL COMMENT 'Encrypted backup',
`backup_type` VARCHAR(20) NOT NULL COMMENT 'manual, auto, recovery',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` VARCHAR(255) NULL,
INDEX `idx_share_id` (`share_id`),
INDEX `idx_created_at` (`created_at`),
FOREIGN KEY (`share_id`) REFERENCES `party_shares`(`id`) ON DELETE CASCADE,
CONSTRAINT `chk_backup_type` CHECK (`backup_type` IN ('manual', 'auto', 'recovery'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
---
## 8. 配置文件
### 8.1 环境配置
```json
// config/development.json
{
"server": {
"port": 3006,
"host": "0.0.0.0"
},
"database": {
"type": "mysql",
"host": "localhost",
"port": 3306,
"database": "rwa_mpc_party_db",
"username": "mpc_user",
"password": "password",
"synchronize": false,
"logging": true
},
"redis": {
"host": "localhost",
"port": 6379,
"db": 5
},
"mpc": {
"coordinatorUrl": "http://localhost:50051",
"messageRouterWsUrl": "ws://localhost:50052",
"shareMasterKey": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"shareStorageType": "postgres"
},
"kafka": {
"brokers": ["localhost:9092"],
"clientId": "mpc-party-service",
"groupId": "mpc-party-group"
},
"security": {
"jwtSecret": "your-jwt-secret-here",
"jwtExpiration": "24h"
}
}
```
```json
// config/production.json
{
"server": {
"port": 3006,
"host": "0.0.0.0"
},
"database": {
"type": "mysql",
"host": "${DB_HOST}",
"port": 3306,
"database": "rwa_mpc_party_db",
"username": "${DB_USER}",
"password": "${DB_PASSWORD}",
"synchronize": false,
"logging": false,
"ssl": true
},
"redis": {
"host": "${REDIS_HOST}",
"port": 6379,
"db": 5,
"password": "${REDIS_PASSWORD}"
},
"mpc": {
"coordinatorUrl": "${MPC_COORDINATOR_URL}",
"messageRouterWsUrl": "${MPC_MESSAGE_ROUTER_WS_URL}",
"shareMasterKey": "${SHARE_MASTER_KEY}",
"shareStorageType": "hsm"
},
"hsm": {
"type": "aws-cloudhsm",
"endpoint": "${HSM_ENDPOINT}",
"credentials": "${HSM_CREDENTIALS}"
},
"kafka": {
"brokers": ["${KAFKA_BROKER_1}", "${KAFKA_BROKER_2}"],
"clientId": "mpc-party-service",
"groupId": "mpc-party-group",
"ssl": true
},
"security": {
"jwtSecret": "${JWT_SECRET}",
"jwtExpiration": "24h"
},
"monitoring": {
"prometheus": {
"enabled": true,
"port": 9090
},
"jaeger": {
"enabled": true,
"endpoint": "${JAEGER_ENDPOINT}"
}
}
}
```
### 8.2 Package.json
```json
{
"name": "mpc-party-service",
"version": "1.0.0",
"description": "MPC Server Party Service for RWA Durian System",
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
"start:prod": "node dist/main.js",
"lint": "eslint src --ext .ts",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migrate": "typeorm migration:run",
"migrate:revert": "typeorm migration:revert"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/cqrs": "^10.0.0",
"@nestjs/config": "^3.0.0",
"@nestjs/typeorm": "^10.0.0",
"@nestjs/swagger": "^7.0.0",
"typeorm": "^0.3.17",
"mysql2": "^3.6.0",
"redis": "^4.6.0",
"kafkajs": "^2.2.4",
"axios": "^1.5.0",
"ws": "^8.14.0",
"class-validator": "^0.14.0",
"class-transformer": "^0.5.1",
"uuid": "^9.0.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/jest": "^29.5.0",
"@types/ws": "^8.5.0",
"typescript": "^5.2.0",
"ts-node-dev": "^2.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.50.0"
}
}
```
---
## 9. Docker部署
### 9.1 Dockerfile
```dockerfile
# services/mpc-service/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY src ./src
# Build TypeScript
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --only=production
# Copy built files
COPY --from=builder /app/dist ./dist
# Copy config files
COPY config ./config
# Expose port
EXPOSE 3006
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
CMD node -e "require('http').get('http://localhost:3006/mpc-party/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start service
CMD ["node", "dist/main.js"]
```
### 9.2 Docker Compose集成
```yaml
# docker-compose.yml添加到现有配置
services:
# ========== 新增MPC Party Service ==========
mpc-party-service:
build:
context: ./services/mpc-service
dockerfile: Dockerfile
container_name: rwa-mpc-party
ports:
- "3006:3006"
environment:
NODE_ENV: development
DB_HOST: mysql
DB_USER: mpc_user
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
REDIS_PASSWORD: ${REDIS_PASSWORD}
MPC_COORDINATOR_URL: http://mpc-session-coordinator:50051
MPC_MESSAGE_ROUTER_WS_URL: ws://mpc-message-router:50052
SHARE_MASTER_KEY: ${SHARE_MASTER_KEY}
KAFKA_BROKER_1: kafka:9092
JWT_SECRET: ${JWT_SECRET}
depends_on:
- mysql
- redis
- kafka
- mpc-session-coordinator
- mpc-message-router
networks:
- rwa-network
restart: unless-stopped
volumes:
- ./services/mpc-service/logs:/app/logs
```
---
## 10. 测试规范
### 10.1 单元测试
```typescript
// tests/unit/domain/party-share.entity.spec.ts
import { PartyShare, PartyShareType } from '@/domain/entities/party-share.entity';
import { PartyId } from '@/domain/value-objects/party-id.vo';
import { SessionId } from '@/domain/value-objects/session-id.vo';
import { ShareData } from '@/domain/value-objects/share-data.vo';
import { PublicKey } from '@/domain/value-objects/public-key.vo';
import { Threshold } from '@/domain/value-objects/threshold.vo';
describe('PartyShare Entity', () => {
let partyShare: PartyShare;
beforeEach(() => {
const shareData = new ShareData(
Buffer.from('encrypted-data'),
Buffer.from('123456789012', 'utf-8'), // 12 bytes
Buffer.from('1234567890123456', 'utf-8'), // 16 bytes
);
partyShare = PartyShare.create(
new PartyId('user123-server'),
new SessionId('550e8400-e29b-41d4-a716-446655440000'),
PartyShareType.WALLET,
shareData,
PublicKey.fromHex('03' + '0'.repeat(64)),
new Threshold(3, 2),
);
});
it('should create a valid party share', () => {
expect(partyShare.id).toBeDefined();
expect(partyShare.partyId.value).toBe('user123-server');
expect(partyShare.shareType).toBe(PartyShareType.WALLET);
expect(partyShare.threshold.toString()).toBe('2-of-3');
});
it('should mark share as used', () => {
expect(partyShare.lastUsedAt).toBeUndefined();
partyShare.markAsUsed();
expect(partyShare.lastUsedAt).toBeDefined();
expect(partyShare.lastUsedAt).toBeInstanceOf(Date);
});
it('should throw error when rotating non-active share', () => {
partyShare.revoke('test reason');
const newShareData = new ShareData(
Buffer.from('new-encrypted-data'),
Buffer.from('123456789012', 'utf-8'),
Buffer.from('1234567890123456', 'utf-8'),
);
expect(() => partyShare.rotate(newShareData)).toThrow();
});
it('should validate threshold correctly', () => {
expect(partyShare.validateThreshold(2)).toBe(true);
expect(partyShare.validateThreshold(3)).toBe(true);
expect(partyShare.validateThreshold(1)).toBe(false);
expect(partyShare.validateThreshold(4)).toBe(false);
});
});
```
### 10.2 集成测试
```typescript
// tests/integration/mpc-party.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { MPCPartyController } from '@/api/controllers/mpc-party.controller';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
describe('MPCPartyController (Integration)', () => {
let app: INestApplication;
let commandBus: CommandBus;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
controllers: [MPCPartyController],
providers: [
{
provide: CommandBus,
useValue: {
execute: jest.fn(),
},
},
{
provide: QueryBus,
useValue: {
execute: jest.fn(),
},
},
],
}).compile();
app = moduleFixture.createNestApplication();
commandBus = moduleFixture.get<CommandBus>(CommandBus);
await app.init();
});
describe('POST /mpc-party/keygen/participate', () => {
it('should accept keygen participation request', async () => {
const dto = {
sessionId: '550e8400-e29b-41d4-a716-446655440000',
partyId: 'user123-server',
joinToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
shareType: 'wallet',
};
const response = await request(app.getHttpServer())
.post('/mpc-party/keygen/participate')
.send(dto)
.expect(202);
expect(response.body).toHaveProperty('message');
expect(response.body.sessionId).toBe(dto.sessionId);
});
});
afterAll(async () => {
await app.close();
});
});
```
---
## 总结
### ✅ 完整性检查
1.**六边形架构**清晰的Presentation、Application、Domain、Infrastructure分层
2.**DDD设计**:完整的实体、值对象、聚合根、领域服务
3.**CQRS模式**Commands和Queries分离
4.**领域事件**ShareCreated、KeygenCompleted、SigningCompleted
5.**依赖倒置**Domain层无外部依赖通过接口与Infrastructure交互
6.**TypeScript/NestJS**:符合您现有技术栈
7.**完整的目录结构**仿照identity-service的结构
8.**数据库设计**MySQL schema完整定义
9.**API接口**REST + Swagger文档
10.**Docker部署**完整的Dockerfile和docker-compose集成
### 📋 Claude Code开发检查清单
- [ ] 创建目录结构
- [ ] 实现领域模型Entities, Value Objects
- [ ] 实现应用层Commands, Queries, Handlers
- [ ] 实现基础设施层Repositories, External Clients
- [ ] 实现API控制器和DTO
- [ ] 配置数据库连接和迁移
- [ ] 配置环境变量
- [ ] 编写单元测试
- [ ] 编写集成测试
- [ ] 构建Docker镜像
- [ ] 集成到现有系统
---
**版本**: 1.0
**最后更新**: 2024-11-27
**技术栈**: TypeScript + NestJS + MySQL + Redis + Kafka
**架构**: DDD + Hexagonal + CQRS