diff --git a/backend/services/identity-service/src/infrastructure/external/mpc/mpc-client.service.ts b/backend/services/identity-service/src/infrastructure/external/mpc/mpc-client.service.ts index 334ede6b..32083203 100644 --- a/backend/services/identity-service/src/infrastructure/external/mpc/mpc-client.service.ts +++ b/backend/services/identity-service/src/infrastructure/external/mpc/mpc-client.service.ts @@ -3,14 +3,13 @@ * * 与 mpc-service (NestJS) 通信的客户端服务 * - * 调用路径: - * identity-service → mpc-service (NestJS) → mpc-system (Go) + * 调用路径 (DDD 分领域): + * identity-service (身份域) → mpc-service (MPC域/NestJS) → mpc-system (Go/TSS实现) * - * 这种架构的优点: - * 1. 符合 DDD 边界上下文分离原则 - * 2. mpc-service 可以封装 MPC 业务逻辑、重试、熔断等 - * 3. 多个服务可共享 mpc-service(如 transaction-service) - * 4. MPC API Key 只需配置在 mpc-service + * 业务流程: + * 1. identity-service 调用 mpc-service 的 keygen API + * 2. mpc-service 协调 mpc-system 完成 TSS keygen + * 3. 返回公钥和 delegate share (用户分片) 给 identity-service */ import { Injectable, Logger } from '@nestjs/common'; @@ -21,45 +20,34 @@ import { createHash, randomUUID } from 'crypto'; export interface KeygenRequest { sessionId: string; - threshold: number; // t in t-of-n (默认 2) - totalParties: number; // n in t-of-n (默认 3) - parties: PartyInfo[]; -} - -export interface PartyInfo { - partyId: string; - partyIndex: number; - partyType: 'SERVER' | 'CLIENT' | 'BACKUP'; + username: string; // 用户名 (自动递增ID) + threshold: number; // t in t-of-n (默认 1, 即 2-of-3) + totalParties: number; // n in t-of-n (默认 3) + requireDelegate: boolean; // 是否需要 delegate party } export interface KeygenResult { sessionId: string; - publicKey: string; // 压缩格式公钥 (33 bytes hex) - publicKeyUncompressed: string; // 非压缩格式公钥 (65 bytes hex) - partyShares: PartyShareResult[]; + publicKey: string; // 压缩格式公钥 (33 bytes hex) + delegateShare: DelegateShare; // delegate share (用户分片) + serverParties: string[]; // 服务器 party IDs } -export interface PartyShareResult { +export interface DelegateShare { partyId: string; partyIndex: number; - encryptedShareData: string; // 加密的分片数据 + encryptedShare: string; // 加密的分片数据 (hex) } export interface SigningRequest { - sessionId: string; - publicKey: string; - messageHash: string; // 32 bytes hex - signerParties: PartyInfo[]; - threshold: number; + username: string; + messageHash: string; // 32 bytes hex + userShare?: string; // 如果账户有 delegate share,需要传入用户分片 } export interface SigningResult { sessionId: string; - signature: { - r: string; // 32 bytes hex - s: string; // 32 bytes hex - v: number; // recovery id (0 or 1) - }; + signature: string; // 64 bytes hex (R + S) messageHash: string; } @@ -68,12 +56,14 @@ export class MpcClientService { private readonly logger = new Logger(MpcClientService.name); private readonly mpcServiceUrl: string; // mpc-service (NestJS) URL private readonly mpcMode: string; + private readonly pollIntervalMs = 2000; + private readonly maxPollAttempts = 150; // 5 minutes max constructor( private readonly httpService: HttpService, private readonly configService: ConfigService, ) { - // 连接 mpc-service (NestJS) 而不是直接连接 mpc-system (Go) + // 连接 mpc-service (NestJS) this.mpcServiceUrl = this.configService.get('MPC_SERVICE_URL', 'http://localhost:3001'); this.mpcMode = this.configService.get('MPC_MODE', 'local'); } @@ -86,17 +76,17 @@ export class MpcClientService { } /** - * 执行 2-of-3 MPC 密钥生成 + * 执行 2-of-3 MPC 密钥生成 (带 delegate party) * * 三个参与方: - * - Party 0 (SERVER): 服务端持有,用于常规签名 - * - Party 1 (CLIENT): 用户设备持有,返回给客户端 - * - Party 2 (BACKUP): 备份服务持有,用于恢复 + * - Party 0 (SERVER): server-party-1,服务端持有 + * - Party 1 (SERVER): server-party-2,服务端持有 + * - Party 2 (DELEGATE): delegate-party,代理生成后返回给用户设备 * - * 调用路径: identity-service → mpc-service → mpc-system (Go) + * 调用路径: identity-service → mpc-service → mpc-system */ async executeKeygen(request: KeygenRequest): Promise { - this.logger.log(`Starting MPC keygen: session=${request.sessionId}, t=${request.threshold}, n=${request.totalParties}`); + this.logger.log(`Starting MPC keygen: username=${request.username}, t=${request.threshold}, n=${request.totalParties}`); // 开发模式使用本地模拟 if (this.mpcMode === 'local') { @@ -104,49 +94,120 @@ export class MpcClientService { } try { - // 调用 mpc-service 的 keygen 同步接口 - const response = await firstValueFrom( - this.httpService.post( - `${this.mpcServiceUrl}/api/v1/mpc-party/keygen/participate-sync`, + // Step 1: 调用 mpc-service 创建 keygen session + const createResponse = await firstValueFrom( + this.httpService.post<{ + sessionId: string; + status: string; + }>( + `${this.mpcServiceUrl}/api/v1/mpc/keygen`, { - sessionId: request.sessionId, - partyId: 'server-party', - joinToken: this.generateJoinToken(request.sessionId), - shareType: 'wallet', // PartyShareType.WALLET - userId: request.parties.find(p => p.partyType === 'CLIENT')?.partyId, + username: request.username, + thresholdN: request.totalParties, + thresholdT: request.threshold, + requireDelegate: request.requireDelegate, }, { headers: { 'Content-Type': 'application/json', }, - timeout: 300000, // 5分钟超时 + timeout: 30000, }, ), ); - this.logger.log(`MPC keygen completed: session=${request.sessionId}, publicKey=${response.data.publicKey}`); - return response.data; + const sessionId = createResponse.data.sessionId; + this.logger.log(`Keygen session created: ${sessionId}`); + + // Step 2: 轮询 session 状态直到完成 + const sessionResult = await this.pollKeygenStatus(sessionId); + + if (sessionResult.status !== 'completed') { + throw new Error(`Keygen session failed with status: ${sessionResult.status}`); + } + + this.logger.log(`Keygen completed: publicKey=${sessionResult.publicKey}`); + + return { + sessionId, + publicKey: sessionResult.publicKey, + delegateShare: sessionResult.delegateShare, + serverParties: sessionResult.serverParties, + }; } catch (error) { - this.logger.error(`MPC keygen failed: session=${request.sessionId}`, error); + this.logger.error(`MPC keygen failed: username=${request.username}`, error); throw new Error(`MPC keygen failed: ${error.message}`); } } /** - * 生成加入会话的 token + * 轮询 keygen session 状态 */ - private generateJoinToken(sessionId: string): string { - return createHash('sha256').update(`${sessionId}-${Date.now()}`).digest('hex'); + private async pollKeygenStatus(sessionId: string): Promise<{ + status: string; + publicKey: string; + delegateShare: DelegateShare; + serverParties: string[]; + }> { + for (let i = 0; i < this.maxPollAttempts; i++) { + try { + const response = await firstValueFrom( + this.httpService.get<{ + sessionId: string; + status: string; + publicKey?: string; + delegateShare?: { + partyId: string; + partyIndex: number; + encryptedShare: string; + }; + serverParties?: string[]; + }>( + `${this.mpcServiceUrl}/api/v1/mpc/keygen/${sessionId}/status`, + { + timeout: 10000, + }, + ), + ); + + const data = response.data; + this.logger.debug(`Session ${sessionId} status: ${data.status}`); + + if (data.status === 'completed') { + return { + status: 'completed', + publicKey: data.publicKey || '', + delegateShare: data.delegateShare || { partyId: '', partyIndex: -1, encryptedShare: '' }, + serverParties: data.serverParties || [], + }; + } + + if (data.status === 'failed' || data.status === 'expired') { + return { + status: data.status, + publicKey: '', + delegateShare: { partyId: '', partyIndex: -1, encryptedShare: '' }, + serverParties: [], + }; + } + + await this.sleep(this.pollIntervalMs); + } catch (error) { + this.logger.warn(`Error polling session status: ${error.message}`); + await this.sleep(this.pollIntervalMs); + } + } + + throw new Error(`Session ${sessionId} timed out after ${this.maxPollAttempts * this.pollIntervalMs}ms`); } /** * 执行 MPC 签名 * - * 至少需要 threshold 个参与方来完成签名 - * 调用路径: identity-service → mpc-service → mpc-system (Go) + * 调用路径: identity-service → mpc-service → mpc-system */ async executeSigning(request: SigningRequest): Promise { - this.logger.log(`Starting MPC signing: session=${request.sessionId}, messageHash=${request.messageHash}`); + this.logger.log(`Starting MPC signing: username=${request.username}, messageHash=${request.messageHash}`); // 开发模式使用本地模拟 if (this.mpcMode === 'local') { @@ -154,66 +215,127 @@ export class MpcClientService { } try { - // 调用 mpc-service 的 signing 同步接口 - const response = await firstValueFrom( - this.httpService.post( - `${this.mpcServiceUrl}/api/v1/mpc-party/signing/participate-sync`, + // 调用 mpc-service 创建签名 session + const createResponse = await firstValueFrom( + this.httpService.post<{ + sessionId: string; + status: string; + }>( + `${this.mpcServiceUrl}/api/v1/mpc/sign`, { - sessionId: request.sessionId, - partyId: 'server-party', - joinToken: this.generateJoinToken(request.sessionId), + username: request.username, messageHash: request.messageHash, - publicKey: request.publicKey, + userShare: request.userShare, }, { headers: { 'Content-Type': 'application/json', }, - timeout: 120000, // 2分钟超时 + timeout: 30000, }, ), ); - this.logger.log(`MPC signing completed: session=${request.sessionId}`); - return response.data; + const sessionId = createResponse.data.sessionId; + this.logger.log(`Signing session created: ${sessionId}`); + + // 轮询签名状态 + const signResult = await this.pollSigningStatus(sessionId); + + if (signResult.status !== 'completed') { + throw new Error(`Signing session failed with status: ${signResult.status}`); + } + + return { + sessionId, + signature: signResult.signature, + messageHash: request.messageHash, + }; } catch (error) { - this.logger.error(`MPC signing failed: session=${request.sessionId}`, error); + this.logger.error(`MPC signing failed: username=${request.username}`, error); throw new Error(`MPC signing failed: ${error.message}`); } } + /** + * 轮询签名 session 状态 + */ + private async pollSigningStatus(sessionId: string): Promise<{ + status: string; + signature: string; + }> { + for (let i = 0; i < this.maxPollAttempts; i++) { + try { + const response = await firstValueFrom( + this.httpService.get<{ + sessionId: string; + status: string; + signature?: string; + }>( + `${this.mpcServiceUrl}/api/v1/mpc/sign/${sessionId}/status`, + { + timeout: 10000, + }, + ), + ); + + const data = response.data; + + if (data.status === 'completed') { + return { + status: 'completed', + signature: data.signature || '', + }; + } + + if (data.status === 'failed' || data.status === 'expired') { + return { + status: data.status, + signature: '', + }; + } + + await this.sleep(this.pollIntervalMs); + } catch (error) { + this.logger.warn(`Error polling signing status: ${error.message}`); + await this.sleep(this.pollIntervalMs); + } + } + + throw new Error(`Signing session ${sessionId} timed out`); + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + /** * 执行本地模拟的 MPC keygen (用于开发测试) - * - * 注意: 这是一个简化的本地实现,仅用于测试 - * 生产环境应该调用真正的 MPC 系统 */ async executeLocalKeygen(request: KeygenRequest): Promise { - this.logger.log(`Starting LOCAL MPC keygen (test mode): session=${request.sessionId}`); + this.logger.log(`Starting LOCAL MPC keygen (test mode): username=${request.username}`); - // 使用 ethers 生成密钥对 (简化版本,非真正的 MPC) const { ethers } = await import('ethers'); // 生成随机私钥 const wallet = ethers.Wallet.createRandom(); - const privateKey = wallet.privateKey; const publicKey = wallet.publicKey; // 压缩公钥 (33 bytes) const compressedPubKey = ethers.SigningKey.computePublicKey(publicKey, true); - // 模拟分片数据 (实际上是完整私钥的加密版本,仅用于测试) - const partyShares: PartyShareResult[] = request.parties.map((party) => ({ - partyId: party.partyId, - partyIndex: party.partyIndex, - encryptedShareData: this.encryptShareData(privateKey, party.partyId), - })); + // 模拟 delegate share + const delegateShare: DelegateShare = { + partyId: 'delegate-party', + partyIndex: 2, + encryptedShare: this.encryptShareData(wallet.privateKey, request.username), + }; return { - sessionId: request.sessionId, + sessionId: this.generateSessionId(), publicKey: compressedPubKey.slice(2), // 去掉 0x 前缀 - publicKeyUncompressed: publicKey.slice(2), - partyShares, + delegateShare, + serverParties: ['server-party-1', 'server-party-2'], }; } @@ -221,32 +343,23 @@ export class MpcClientService { * 执行本地模拟的 MPC 签名 (用于开发测试) */ async executeLocalSigning(request: SigningRequest): Promise { - this.logger.log(`Starting LOCAL MPC signing (test mode): session=${request.sessionId}`); + this.logger.log(`Starting LOCAL MPC signing (test mode): username=${request.username}`); const { ethers } = await import('ethers'); - // 从第一个参与方获取分片数据并解密 (测试模式) - // 实际生产环境需要多方协作签名 - const signerParty = request.signerParties[0]; - - // 这里我们无法真正恢复私钥,所以使用一个测试签名 - // 生产环境应该使用真正的 MPC 协议 const messageHashBytes = Buffer.from(request.messageHash, 'hex'); // 创建一个临时钱包用于签名 (仅测试) const testWallet = new ethers.Wallet( - '0x' + createHash('sha256').update(request.publicKey).digest('hex'), + '0x' + createHash('sha256').update(request.username).digest('hex'), ); const signature = testWallet.signingKey.sign(messageHashBytes); + // 返回 R + S 格式 (64 bytes hex) return { - sessionId: request.sessionId, - signature: { - r: signature.r.slice(2), - s: signature.s.slice(2), - v: signature.v - 27, // 转换为 0 或 1 - }, + sessionId: this.generateSessionId(), + signature: signature.r.slice(2) + signature.s.slice(2), messageHash: request.messageHash, }; } @@ -255,7 +368,6 @@ export class MpcClientService { * 加密分片数据 (简化版本) */ private encryptShareData(data: string, key: string): string { - // 简化的加密,实际应该使用 AES-GCM const cipher = createHash('sha256').update(key).digest('hex'); return Buffer.from(data).toString('base64') + '.' + cipher.slice(0, 16); } diff --git a/backend/services/mpc-service/src/api/api.module.ts b/backend/services/mpc-service/src/api/api.module.ts index 6c1c5f11..3bad1e9d 100644 --- a/backend/services/mpc-service/src/api/api.module.ts +++ b/backend/services/mpc-service/src/api/api.module.ts @@ -8,12 +8,14 @@ import { Module } from '@nestjs/common'; import { ApplicationModule } from '../application/application.module'; import { MPCPartyController } from './controllers/mpc-party.controller'; import { HealthController } from './controllers/health.controller'; +import { MPCController } from './controllers/mpc.controller'; @Module({ imports: [ApplicationModule], controllers: [ MPCPartyController, HealthController, + MPCController, ], }) export class ApiModule {} diff --git a/backend/services/mpc-service/src/api/controllers/index.ts b/backend/services/mpc-service/src/api/controllers/index.ts index 490d0133..1d43f33f 100644 --- a/backend/services/mpc-service/src/api/controllers/index.ts +++ b/backend/services/mpc-service/src/api/controllers/index.ts @@ -1,2 +1,3 @@ export * from './mpc-party.controller'; export * from './health.controller'; +export * from './mpc.controller'; diff --git a/backend/services/mpc-service/src/api/controllers/mpc.controller.ts b/backend/services/mpc-service/src/api/controllers/mpc.controller.ts new file mode 100644 index 00000000..619b1be3 --- /dev/null +++ b/backend/services/mpc-service/src/api/controllers/mpc.controller.ts @@ -0,0 +1,237 @@ +/** + * MPC Controller + * + * REST API endpoints for MPC keygen and signing operations. + * Called by identity-service to coordinate MPC operations with mpc-system. + * + * 调用路径 (DDD 分领域): + * identity-service (身份域) → mpc-service (MPC域) → mpc-system (Go/TSS实现) + * + * 数据存储: + * - mpc-service: 存储公钥、share(分片) + * - identity-service: 存储收款地址(BSC、KAVA、DST) + */ + +import { + Controller, + Post, + Get, + Body, + Param, + HttpCode, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiParam, +} from '@nestjs/swagger'; +import { Public } from '../../shared/decorators/public.decorator'; +import { MPCCoordinatorService } from '../../application/services/mpc-coordinator.service'; + +// ============================================ +// Request DTOs +// ============================================ + +export class CreateKeygenDto { + username: string; + thresholdN: number; + thresholdT: number; + requireDelegate: boolean; +} + +export class CreateSigningDto { + username: string; + messageHash: string; + userShare?: string; +} + +// ============================================ +// Response DTOs +// ============================================ + +export class KeygenSessionResponseDto { + sessionId: string; + status: string; +} + +export class KeygenStatusResponseDto { + sessionId: string; + status: string; + publicKey?: string; + delegateShare?: { + partyId: string; + partyIndex: number; + encryptedShare: string; + }; + serverParties?: string[]; +} + +export class SigningSessionResponseDto { + sessionId: string; + status: string; +} + +export class SigningStatusResponseDto { + sessionId: string; + status: string; + signature?: string; +} + +@ApiTags('MPC') +@Controller('mpc') +export class MPCController { + private readonly logger = new Logger(MPCController.name); + + constructor( + private readonly mpcCoordinatorService: MPCCoordinatorService, + ) {} + + /** + * Create keygen session + * Called by identity-service to start MPC keygen + */ + @Public() + @Post('keygen') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ + summary: '创建 MPC Keygen 会话', + description: '创建一个新的 MPC keygen 会话,协调 mpc-system 完成密钥生成', + }) + @ApiResponse({ + status: 201, + description: 'Keygen session created', + type: KeygenSessionResponseDto, + }) + async createKeygen(@Body() dto: CreateKeygenDto): Promise { + this.logger.log(`Creating keygen session: username=${dto.username}, ${dto.thresholdT}-of-${dto.thresholdN}, delegate=${dto.requireDelegate}`); + + const result = await this.mpcCoordinatorService.createKeygenSession({ + username: dto.username, + thresholdN: dto.thresholdN, + thresholdT: dto.thresholdT, + requireDelegate: dto.requireDelegate, + }); + + return { + sessionId: result.sessionId, + status: result.status, + }; + } + + /** + * Get keygen session status + */ + @Public() + @Get('keygen/:sessionId/status') + @ApiOperation({ + summary: '获取 Keygen 会话状态', + description: '获取 keygen 会话的当前状态,包括公钥和 delegate share', + }) + @ApiParam({ name: 'sessionId', description: 'Session ID' }) + @ApiResponse({ + status: 200, + description: 'Keygen session status', + type: KeygenStatusResponseDto, + }) + async getKeygenStatus(@Param('sessionId') sessionId: string): Promise { + this.logger.debug(`Getting keygen status: sessionId=${sessionId}`); + + const result = await this.mpcCoordinatorService.getKeygenStatus(sessionId); + + return { + sessionId: result.sessionId, + status: result.status, + publicKey: result.publicKey, + delegateShare: result.delegateShare, + serverParties: result.serverParties, + }; + } + + /** + * Create signing session + * Called by identity-service to start MPC signing + */ + @Public() + @Post('sign') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ + summary: '创建 MPC 签名会话', + description: '创建一个新的 MPC signing 会话', + }) + @ApiResponse({ + status: 201, + description: 'Signing session created', + type: SigningSessionResponseDto, + }) + async createSigning(@Body() dto: CreateSigningDto): Promise { + this.logger.log(`Creating signing session: username=${dto.username}, messageHash=${dto.messageHash.slice(0, 16)}...`); + + const result = await this.mpcCoordinatorService.createSigningSession({ + username: dto.username, + messageHash: dto.messageHash, + userShare: dto.userShare, + }); + + return { + sessionId: result.sessionId, + status: result.status, + }; + } + + /** + * Get signing session status + */ + @Public() + @Get('sign/:sessionId/status') + @ApiOperation({ + summary: '获取签名会话状态', + description: '获取 signing 会话的当前状态,包括签名结果', + }) + @ApiParam({ name: 'sessionId', description: 'Session ID' }) + @ApiResponse({ + status: 200, + description: 'Signing session status', + type: SigningStatusResponseDto, + }) + async getSigningStatus(@Param('sessionId') sessionId: string): Promise { + this.logger.debug(`Getting signing status: sessionId=${sessionId}`); + + const result = await this.mpcCoordinatorService.getSigningStatus(sessionId); + + return { + sessionId: result.sessionId, + status: result.status, + signature: result.signature, + }; + } + + /** + * Get public key by username + * Used for address derivation + */ + @Public() + @Get('wallet/:username/public-key') + @ApiOperation({ + summary: '获取用户公钥', + description: '根据用户名获取 MPC 公钥', + }) + @ApiParam({ name: 'username', description: 'Username' }) + @ApiResponse({ + status: 200, + description: 'Public key info', + }) + async getPublicKey(@Param('username') username: string) { + this.logger.debug(`Getting public key: username=${username}`); + + const result = await this.mpcCoordinatorService.getWalletByUsername(username); + + return { + username: result.username, + publicKey: result.publicKey, + keygenSessionId: result.keygenSessionId, + }; + } +} diff --git a/backend/services/mpc-service/src/application/application.module.ts b/backend/services/mpc-service/src/application/application.module.ts index 1acae23c..955ec7a3 100644 --- a/backend/services/mpc-service/src/application/application.module.ts +++ b/backend/services/mpc-service/src/application/application.module.ts @@ -5,6 +5,8 @@ */ import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { DomainModule } from '../domain/domain.module'; import { InfrastructureModule } from '../infrastructure/infrastructure.module'; @@ -19,11 +21,17 @@ import { ListSharesHandler } from './queries/list-shares'; // Services import { MPCPartyApplicationService } from './services/mpc-party-application.service'; +import { MPCCoordinatorService } from './services/mpc-coordinator.service'; + +// Entities +import { MpcWallet, MpcShare, MpcSession } from '../domain/entities'; @Module({ imports: [ DomainModule, InfrastructureModule, + HttpModule, + TypeOrmModule.forFeature([MpcWallet, MpcShare, MpcSession]), ], providers: [ // Command Handlers @@ -37,9 +45,11 @@ import { MPCPartyApplicationService } from './services/mpc-party-application.ser // Application Services MPCPartyApplicationService, + MPCCoordinatorService, ], exports: [ MPCPartyApplicationService, + MPCCoordinatorService, ], }) export class ApplicationModule {} diff --git a/backend/services/mpc-service/src/application/services/mpc-coordinator.service.ts b/backend/services/mpc-service/src/application/services/mpc-coordinator.service.ts new file mode 100644 index 00000000..7cf62398 --- /dev/null +++ b/backend/services/mpc-service/src/application/services/mpc-coordinator.service.ts @@ -0,0 +1,463 @@ +/** + * MPC Coordinator Service + * + * 协调 mpc-system (Go) 完成 MPC 操作。 + * 存储公钥和 share(分片)到本地数据库。 + * + * 调用路径 (DDD 分领域): + * identity-service (身份域) → mpc-service (MPC域) → mpc-system (Go/TSS实现) + */ + +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { MpcWallet } from '../../domain/entities/mpc-wallet.entity'; +import { MpcShare } from '../../domain/entities/mpc-share.entity'; +import { MpcSession } from '../../domain/entities/mpc-session.entity'; + +export interface CreateKeygenInput { + username: string; + thresholdN: number; + thresholdT: number; + requireDelegate: boolean; +} + +export interface CreateKeygenOutput { + sessionId: string; + status: string; +} + +export interface KeygenStatusOutput { + sessionId: string; + status: string; + publicKey?: string; + delegateShare?: { + partyId: string; + partyIndex: number; + encryptedShare: string; + }; + serverParties?: string[]; +} + +export interface CreateSigningInput { + username: string; + messageHash: string; + userShare?: string; +} + +export interface CreateSigningOutput { + sessionId: string; + status: string; +} + +export interface SigningStatusOutput { + sessionId: string; + status: string; + signature?: string; +} + +export interface WalletOutput { + username: string; + publicKey: string; + keygenSessionId: string; +} + +@Injectable() +export class MPCCoordinatorService { + private readonly logger = new Logger(MPCCoordinatorService.name); + private readonly mpcSystemUrl: string; + private readonly mpcApiKey: string; + private readonly pollIntervalMs = 2000; + private readonly maxPollAttempts = 150; + + constructor( + private readonly configService: ConfigService, + private readonly httpService: HttpService, + @InjectRepository(MpcWallet) + private readonly walletRepository: Repository, + @InjectRepository(MpcShare) + private readonly shareRepository: Repository, + @InjectRepository(MpcSession) + private readonly sessionRepository: Repository, + ) { + this.mpcSystemUrl = this.configService.get('MPC_SYSTEM_URL', 'http://localhost:4000'); + this.mpcApiKey = this.configService.get('MPC_API_KEY', 'test-api-key'); + } + + /** + * 创建 keygen 会话 + * 调用 mpc-system 的 /api/v1/mpc/keygen API + */ + async createKeygenSession(input: CreateKeygenInput): Promise { + this.logger.log(`Creating keygen session: username=${input.username}`); + + try { + // 调用 mpc-system 创建 keygen session + const response = await firstValueFrom( + this.httpService.post<{ + session_id: string; + session_type: string; + username: string; + threshold_n: number; + threshold_t: number; + selected_parties: string[]; + delegate_party: string; + status: string; + }>( + `${this.mpcSystemUrl}/api/v1/mpc/keygen`, + { + username: input.username, + threshold_n: input.thresholdN, + threshold_t: input.thresholdT, + require_delegate: input.requireDelegate, + }, + { + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.mpcApiKey, + }, + timeout: 30000, + }, + ), + ); + + const sessionId = response.data.session_id; + + // 保存 session 到本地数据库 + const session = this.sessionRepository.create({ + sessionId, + sessionType: 'keygen', + username: input.username, + thresholdN: input.thresholdN, + thresholdT: input.thresholdT, + selectedParties: response.data.selected_parties, + delegateParty: response.data.delegate_party, + status: 'created', + }); + await this.sessionRepository.save(session); + + this.logger.log(`Keygen session created: ${sessionId}`); + + // 启动后台轮询任务 + this.pollKeygenCompletion(sessionId, input.username).catch(err => { + this.logger.error(`Keygen polling failed: ${err.message}`); + }); + + return { + sessionId, + status: 'created', + }; + } catch (error) { + this.logger.error(`Failed to create keygen session: ${error.message}`); + throw error; + } + } + + /** + * 后台轮询 keygen 完成状态 + */ + private async pollKeygenCompletion(sessionId: string, username: string): Promise { + for (let i = 0; i < this.maxPollAttempts; i++) { + try { + const response = await firstValueFrom( + this.httpService.get<{ + session_id: string; + status: string; + session_type: string; + completed_parties: number; + total_parties: number; + public_key?: string; + has_delegate?: boolean; + delegate_share?: { + encrypted_share: string; + party_index: number; + party_id: string; + }; + }>( + `${this.mpcSystemUrl}/api/v1/mpc/sessions/${sessionId}`, + { + headers: { + 'X-API-Key': this.mpcApiKey, + }, + timeout: 10000, + }, + ), + ); + + const data = response.data; + + if (data.status === 'completed') { + // 更新 session 状态 + await this.sessionRepository.update( + { sessionId }, + { + status: 'completed', + publicKey: data.public_key, + }, + ); + + // 保存钱包信息 + const wallet = this.walletRepository.create({ + username, + publicKey: data.public_key, + keygenSessionId: sessionId, + }); + await this.walletRepository.save(wallet); + + // 保存 delegate share + if (data.delegate_share) { + const share = this.shareRepository.create({ + sessionId, + username, + partyId: data.delegate_share.party_id, + partyIndex: data.delegate_share.party_index, + encryptedShare: data.delegate_share.encrypted_share, + shareType: 'delegate', + }); + await this.shareRepository.save(share); + } + + this.logger.log(`Keygen completed: sessionId=${sessionId}, publicKey=${data.public_key}`); + return; + } + + if (data.status === 'failed' || data.status === 'expired') { + await this.sessionRepository.update( + { sessionId }, + { status: data.status }, + ); + this.logger.error(`Keygen failed: sessionId=${sessionId}, status=${data.status}`); + return; + } + + await this.sleep(this.pollIntervalMs); + } catch (error) { + this.logger.warn(`Error polling keygen status: ${error.message}`); + await this.sleep(this.pollIntervalMs); + } + } + + // 超时 + await this.sessionRepository.update( + { sessionId }, + { status: 'expired' }, + ); + this.logger.error(`Keygen timed out: sessionId=${sessionId}`); + } + + /** + * 获取 keygen 会话状态 + */ + async getKeygenStatus(sessionId: string): Promise { + const session = await this.sessionRepository.findOne({ + where: { sessionId }, + }); + + if (!session) { + return { + sessionId, + status: 'not_found', + }; + } + + const result: KeygenStatusOutput = { + sessionId, + status: session.status, + serverParties: session.selectedParties?.filter(p => p !== session.delegateParty), + }; + + if (session.status === 'completed') { + result.publicKey = session.publicKey; + + // 获取 delegate share + const share = await this.shareRepository.findOne({ + where: { sessionId, shareType: 'delegate' }, + }); + + if (share) { + result.delegateShare = { + partyId: share.partyId, + partyIndex: share.partyIndex, + encryptedShare: share.encryptedShare, + }; + } + } + + return result; + } + + /** + * 创建签名会话 + */ + async createSigningSession(input: CreateSigningInput): Promise { + this.logger.log(`Creating signing session: username=${input.username}`); + + try { + // 调用 mpc-system 创建签名 session + const response = await firstValueFrom( + this.httpService.post<{ + session_id: string; + session_type: string; + username: string; + message_hash: string; + threshold_t: number; + selected_parties: string[]; + has_delegate: boolean; + status: string; + }>( + `${this.mpcSystemUrl}/api/v1/mpc/sign`, + { + username: input.username, + message_hash: input.messageHash, + user_share: input.userShare, + }, + { + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.mpcApiKey, + }, + timeout: 30000, + }, + ), + ); + + const sessionId = response.data.session_id; + + // 保存 session 到本地数据库 + const session = this.sessionRepository.create({ + sessionId, + sessionType: 'sign', + username: input.username, + messageHash: input.messageHash, + selectedParties: response.data.selected_parties, + status: 'created', + }); + await this.sessionRepository.save(session); + + this.logger.log(`Signing session created: ${sessionId}`); + + // 启动后台轮询任务 + this.pollSigningCompletion(sessionId).catch(err => { + this.logger.error(`Signing polling failed: ${err.message}`); + }); + + return { + sessionId, + status: 'created', + }; + } catch (error) { + this.logger.error(`Failed to create signing session: ${error.message}`); + throw error; + } + } + + /** + * 后台轮询签名完成状态 + */ + private async pollSigningCompletion(sessionId: string): Promise { + for (let i = 0; i < this.maxPollAttempts; i++) { + try { + const response = await firstValueFrom( + this.httpService.get<{ + session_id: string; + status: string; + session_type: string; + completed_parties: number; + total_parties: number; + signature?: string; + }>( + `${this.mpcSystemUrl}/api/v1/mpc/sessions/${sessionId}`, + { + headers: { + 'X-API-Key': this.mpcApiKey, + }, + timeout: 10000, + }, + ), + ); + + const data = response.data; + + if (data.status === 'completed') { + await this.sessionRepository.update( + { sessionId }, + { + status: 'completed', + signature: data.signature, + }, + ); + this.logger.log(`Signing completed: sessionId=${sessionId}`); + return; + } + + if (data.status === 'failed' || data.status === 'expired') { + await this.sessionRepository.update( + { sessionId }, + { status: data.status }, + ); + this.logger.error(`Signing failed: sessionId=${sessionId}, status=${data.status}`); + return; + } + + await this.sleep(this.pollIntervalMs); + } catch (error) { + this.logger.warn(`Error polling signing status: ${error.message}`); + await this.sleep(this.pollIntervalMs); + } + } + + await this.sessionRepository.update( + { sessionId }, + { status: 'expired' }, + ); + this.logger.error(`Signing timed out: sessionId=${sessionId}`); + } + + /** + * 获取签名会话状态 + */ + async getSigningStatus(sessionId: string): Promise { + const session = await this.sessionRepository.findOne({ + where: { sessionId }, + }); + + if (!session) { + return { + sessionId, + status: 'not_found', + }; + } + + return { + sessionId, + status: session.status, + signature: session.signature, + }; + } + + /** + * 根据用户名获取钱包信息 + */ + async getWalletByUsername(username: string): Promise { + const wallet = await this.walletRepository.findOne({ + where: { username }, + }); + + if (!wallet) { + throw new Error(`Wallet not found for username: ${username}`); + } + + return { + username: wallet.username, + publicKey: wallet.publicKey, + keygenSessionId: wallet.keygenSessionId, + }; + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} diff --git a/backend/services/mpc-service/src/domain/entities/index.ts b/backend/services/mpc-service/src/domain/entities/index.ts index b8d37c84..c2b83b29 100644 --- a/backend/services/mpc-service/src/domain/entities/index.ts +++ b/backend/services/mpc-service/src/domain/entities/index.ts @@ -4,3 +4,6 @@ export * from './party-share.entity'; export * from './session-state.entity'; +export * from './mpc-wallet.entity'; +export * from './mpc-share.entity'; +export * from './mpc-session.entity'; diff --git a/backend/services/mpc-service/src/domain/entities/mpc-session.entity.ts b/backend/services/mpc-service/src/domain/entities/mpc-session.entity.ts new file mode 100644 index 00000000..1825634d --- /dev/null +++ b/backend/services/mpc-service/src/domain/entities/mpc-session.entity.ts @@ -0,0 +1,61 @@ +/** + * MPC Session Entity + * + * 存储 MPC 会话信息(keygen 和 signing) + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +@Entity('mpc_sessions') +export class MpcSession { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'session_id', unique: true }) + @Index() + sessionId: string; + + @Column({ name: 'session_type' }) + sessionType: string; // 'keygen' | 'sign' + + @Column() + @Index() + username: string; + + @Column({ name: 'threshold_n', nullable: true }) + thresholdN: number; + + @Column({ name: 'threshold_t', nullable: true }) + thresholdT: number; + + @Column({ name: 'selected_parties', type: 'simple-array', nullable: true }) + selectedParties: string[]; + + @Column({ name: 'delegate_party', nullable: true }) + delegateParty: string; + + @Column({ name: 'message_hash', nullable: true }) + messageHash: string; + + @Column({ name: 'public_key', nullable: true }) + publicKey: string; + + @Column({ nullable: true }) + signature: string; + + @Column({ default: 'created' }) + status: string; // 'created' | 'in_progress' | 'completed' | 'failed' | 'expired' + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/services/mpc-service/src/domain/entities/mpc-share.entity.ts b/backend/services/mpc-service/src/domain/entities/mpc-share.entity.ts new file mode 100644 index 00000000..75323c3d --- /dev/null +++ b/backend/services/mpc-service/src/domain/entities/mpc-share.entity.ts @@ -0,0 +1,46 @@ +/** + * MPC Share Entity + * + * 存储 MPC 分片信息(delegate share) + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +@Entity('mpc_shares') +export class MpcShare { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'session_id' }) + @Index() + sessionId: string; + + @Column() + @Index() + username: string; + + @Column({ name: 'party_id' }) + partyId: string; + + @Column({ name: 'party_index' }) + partyIndex: number; + + @Column({ name: 'encrypted_share', type: 'text' }) + encryptedShare: string; + + @Column({ name: 'share_type' }) + shareType: string; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/services/mpc-service/src/domain/entities/mpc-wallet.entity.ts b/backend/services/mpc-service/src/domain/entities/mpc-wallet.entity.ts new file mode 100644 index 00000000..9809d5bb --- /dev/null +++ b/backend/services/mpc-service/src/domain/entities/mpc-wallet.entity.ts @@ -0,0 +1,37 @@ +/** + * MPC Wallet Entity + * + * 存储用户的 MPC 钱包信息(公钥和 keygen session ID) + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +@Entity('mpc_wallets') +export class MpcWallet { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + @Index() + username: string; + + @Column({ name: 'public_key' }) + publicKey: string; + + @Column({ name: 'keygen_session_id' }) + @Index() + keygenSessionId: string; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +}