67 KiB
67 KiB
MPC Service Context - 完整技术规范
RWA榴莲树系统的MPC Server Party服务 - 作为分布式签名的服务器参与方
目录
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(密钥分片实体)
// 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 值对象
// 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;
}
}
// 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];
}
}
// 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}`;
}
}
// 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'),
);
}
}
// 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 领域服务
// 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 领域事件
// 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,
) {}
}
// 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,
) {}
}
// 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
// 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',
) {}
}
// 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
// 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,
) {}
}
// 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(读操作)
// src/application/queries/get-share-info/get-share-info.query.ts
export class GetShareInfoQuery {
constructor(
public readonly shareId: string,
) {}
}
// 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 数据库实现
// 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;
}
// 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));
}
}
// 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系统客户端
// 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}`);
}
}
}
// 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封装
// 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控制器
// 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定义
// 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';
}
// 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
-- 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 环境配置
// 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"
}
}
// 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
{
"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
# 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集成
# 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 单元测试
// 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 集成测试
// 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();
});
});
总结
✅ 完整性检查
- ✅ 六边形架构:清晰的Presentation、Application、Domain、Infrastructure分层
- ✅ DDD设计:完整的实体、值对象、聚合根、领域服务
- ✅ CQRS模式:Commands和Queries分离
- ✅ 领域事件:ShareCreated、KeygenCompleted、SigningCompleted
- ✅ 依赖倒置:Domain层无外部依赖,通过接口与Infrastructure交互
- ✅ TypeScript/NestJS:符合您现有技术栈
- ✅ 完整的目录结构:仿照identity-service的结构
- ✅ 数据库设计:MySQL schema完整定义
- ✅ API接口:REST + Swagger文档
- ✅ 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