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