2285 lines
67 KiB
Markdown
2285 lines
67 KiB
Markdown
# 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)
|
||
- 🔧 **提供签名服务**给其他Context(Wallet、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 Cases(Commands/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
|