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

67 KiB
Raw Blame History

MPC Service Context - 完整技术规范

RWA榴莲树系统的MPC Server Party服务 - 作为分布式签名的服务器参与方

目录

  1. 服务概述
  2. 目录结构
  3. 领域模型设计
  4. 应用层设计
  5. 基础设施层
  6. API接口
  7. 数据库设计
  8. 配置文件
  9. Docker部署
  10. 测试规范

1. 服务概述

1.1 服务定位

mpc-service 是RWA系统中的一个Context作为MPC分布式签名的Server Party

  • 🔐 持有服务器端的Share1/3 或 1/5
  • 🤝 作为对等参与方参与MPC协议Keygen/Signing
  • 🔧 提供签名服务给其他ContextWallet、Admin等
  • 🚫 不协调会话由MPC系统的Session Coordinator负责

1.2 职责边界

职责 说明
Share管理 安全存储和管理服务器端的Share
MPC参与 运行tss-lib参与Keygen/Signing
对外服务 提供gRPC/REST API给其他Context
会话创建 由Wallet/Admin Service创建
会话协调 由MPC Session Coordinator负责
业务逻辑 只提供签名能力,不管业务

1.3 集成关系

┌──────────────────── RWA系统 ────────────────────┐
│                                                  │
│  ┌──────────┐      ┌──────────┐                │
│  │ Wallet   │      │  Admin   │                │
│  │ Service  │      │ Service  │                │
│  └────┬─────┘      └────┬─────┘                │
│       │                 │                       │
│       │  调用MPC服务    │                       │
│       └────────┬────────┘                       │
│                │                                 │
│         ┌──────▼──────┐                         │
│         │     MPC     │◄─── 新增的Context       │
│         │   Service   │                         │
│         │             │                         │
│         │  持有Share  │                         │
│         └──────┬──────┘                         │
│                │                                 │
└────────────────┼─────────────────────────────────┘
                 │
                 │ 调用外部MPC系统
                 ▼
┌────────────────────────────────────────┐
│        MPC基础设施独立              │
│  • Session Coordinator                 │
│  • Message Router                      │
└────────────────────────────────────────┘

2. 目录结构

2.1 完整目录树

services/
└── mpc-service/                     # Context 6: MPC Server Party
    ├── src/
    │   ├── api/                     # 表现层Presentation Layer
    │   │   ├── controllers/
    │   │   │   ├── mpc-party.controller.ts
    │   │   │   └── health.controller.ts
    │   │   ├── dto/
    │   │   │   ├── participate-keygen.dto.ts
    │   │   │   ├── participate-signing.dto.ts
    │   │   │   ├── share-info.dto.ts
    │   │   │   └── mpc-response.dto.ts
    │   │   └── validators/
    │   │       ├── session-id.validator.ts
    │   │       └── party-id.validator.ts
    │   │
    │   ├── application/             # 应用层Application Layer
    │   │   ├── commands/
    │   │   │   ├── participate-keygen/
    │   │   │   │   ├── participate-keygen.command.ts
    │   │   │   │   └── participate-keygen.handler.ts
    │   │   │   ├── participate-signing/
    │   │   │   │   ├── participate-signing.command.ts
    │   │   │   │   └── participate-signing.handler.ts
    │   │   │   └── rotate-share/
    │   │   │       ├── rotate-share.command.ts
    │   │   │       └── rotate-share.handler.ts
    │   │   ├── queries/
    │   │   │   ├── get-share-info/
    │   │   │   │   ├── get-share-info.query.ts
    │   │   │   │   └── get-share-info.handler.ts
    │   │   │   └── list-shares/
    │   │   │       ├── list-shares.query.ts
    │   │   │       └── list-shares.handler.ts
    │   │   └── services/
    │   │       ├── mpc-party-application.service.ts
    │   │       └── share-encryption.service.ts
    │   │
    │   ├── domain/                  # 领域层Domain Layer
    │   │   ├── aggregates/
    │   │   │   └── party-session/
    │   │   │       ├── party-session.aggregate.ts
    │   │   │       ├── party-session.factory.ts
    │   │   │       └── party-session.spec.ts
    │   │   ├── entities/
    │   │   │   ├── party-share.entity.ts
    │   │   │   ├── mnemonic-record.entity.ts
    │   │   │   └── share-backup.entity.ts
    │   │   ├── value-objects/
    │   │   │   ├── session-id.vo.ts
    │   │   │   ├── party-id.vo.ts
    │   │   │   ├── share-data.vo.ts
    │   │   │   ├── threshold.vo.ts
    │   │   │   └── public-key.vo.ts
    │   │   ├── events/
    │   │   │   ├── share-created.event.ts
    │   │   │   ├── keygen-completed.event.ts
    │   │   │   ├── signing-completed.event.ts
    │   │   │   └── share-rotated.event.ts
    │   │   ├── repositories/
    │   │   │   ├── party-share.repository.interface.ts
    │   │   │   └── session-state.repository.interface.ts
    │   │   └── services/
    │   │       ├── tss-lib.domain-service.ts
    │   │       ├── share-encryption.domain-service.ts
    │   │       └── key-derivation.domain-service.ts
    │   │
    │   └── infrastructure/          # 基础设施层Infrastructure Layer
    │       ├── persistence/
    │       │   ├── mysql/
    │       │   │   ├── entities/
    │       │   │   │   ├── party-share.entity.ts
    │       │   │   │   ├── session-state.entity.ts
    │       │   │   │   └── share-backup.entity.ts
    │       │   │   ├── mappers/
    │       │   │   │   ├── party-share.mapper.ts
    │       │   │   │   └── session-state.mapper.ts
    │       │   │   └── repositories/
    │       │   │       ├── party-share.repository.impl.ts
    │       │   │       └── session-state.repository.impl.ts
    │       │   └── redis/
    │       │       ├── cache/
    │       │       │   └── session-cache.service.ts
    │       │       └── lock/
    │       │           └── distributed-lock.service.ts
    │       ├── messaging/
    │       │   ├── kafka/
    │       │   │   ├── mpc-event.publisher.ts
    │       │   │   └── event-bus.ts
    │       │   └── rabbitmq/
    │       │       └── message-queue.service.ts
    │       ├── external/
    │       │   ├── mpc-system/
    │       │   │   ├── coordinator-client.ts
    │       │   │   ├── message-router-client.ts
    │       │   │   └── dto/
    │       │   │       ├── create-session.dto.ts
    │       │   │       └── mpc-message.dto.ts
    │       │   ├── tss-lib/
    │       │   │   ├── tss-wrapper.ts
    │       │   │   ├── keygen.service.ts
    │       │   │   └── signing.service.ts
    │       │   └── hsm/
    │       │       ├── hsm-client.ts
    │       │       └── hsm-config.ts
    │       └── crypto/
    │           ├── aes-encryption.service.ts
    │           ├── kdf.service.ts
    │           └── secure-random.service.ts
    │
    ├── tests/
    │   ├── unit/
    │   │   ├── domain/
    │   │   │   └── party-share.entity.spec.ts
    │   │   └── application/
    │   │       └── participate-keygen.handler.spec.ts
    │   ├── integration/
    │   │   ├── mpc-party.controller.spec.ts
    │   │   └── party-share.repository.spec.ts
    │   └── e2e/
    │       └── mpc-service.e2e-spec.ts
    │
    ├── database/
    │   └── migrations/
    │       ├── 001_create_party_shares_table.sql
    │       ├── 002_create_session_states_table.sql
    │       └── 003_create_share_backups_table.sql
    │
    ├── config/
    │   ├── development.json
    │   ├── production.json
    │   └── test.json
    │
    ├── Dockerfile
    ├── docker-compose.yml
    ├── package.json
    ├── tsconfig.json
    ├── .env.example
    └── README.md

2.2 目录说明

目录 职责 依赖方向
api/ HTTP/gRPC控制器、DTO、验证器 → application
application/ Use CasesCommands/Queries、应用服务 → domain
domain/ 核心业务逻辑、实体、值对象、领域服务 无外部依赖
infrastructure/ 数据库、外部服务、消息队列 → domain

3. 领域模型设计

3.1 核心实体

3.1.1 PartyShare密钥分片实体

// 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();
  });
});

总结

完整性检查

  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