feat: add MPC coordinator service and keygen/signing API

- Add MPCController with keygen and sign endpoints
- Add MPCCoordinatorService to coordinate with mpc-system
- Add MpcWallet, MpcShare, MpcSession entities for data storage
- Update identity-service mpc-client to call mpc-service

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-06 16:03:17 -08:00
parent 9fc41cfa53
commit d652f1d7a4
10 changed files with 1071 additions and 99 deletions

View File

@ -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)
username: string; // 用户名 (自动递增ID)
threshold: number; // t in t-of-n (默认 1, 即 2-of-3)
totalParties: number; // n in t-of-n (默认 3)
parties: PartyInfo[];
}
export interface PartyInfo {
partyId: string;
partyIndex: number;
partyType: 'SERVER' | 'CLIENT' | 'BACKUP';
requireDelegate: boolean; // 是否需要 delegate party
}
export interface KeygenResult {
sessionId: string;
publicKey: string; // 压缩格式公钥 (33 bytes hex)
publicKeyUncompressed: string; // 非压缩格式公钥 (65 bytes hex)
partyShares: PartyShareResult[];
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;
username: string;
messageHash: string; // 32 bytes hex
signerParties: PartyInfo[];
threshold: number;
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<string>('MPC_SERVICE_URL', 'http://localhost:3001');
this.mpcMode = this.configService.get<string>('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<KeygenResult> {
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<KeygenResult>(
`${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<SigningResult> {
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<SigningResult>(
`${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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* MPC keygen ()
*
* 注意: 这是一个简化的本地实现
* MPC
*/
async executeLocalKeygen(request: KeygenRequest): Promise<KeygenResult> {
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<SigningResult> {
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);
}

View File

@ -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 {}

View File

@ -1,2 +1,3 @@
export * from './mpc-party.controller';
export * from './health.controller';
export * from './mpc.controller';

View File

@ -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: 存储收款地址BSCKAVADST
*/
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<KeygenSessionResponseDto> {
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<KeygenStatusResponseDto> {
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<SigningSessionResponseDto> {
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<SigningStatusResponseDto> {
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,
};
}
}

View File

@ -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 {}

View File

@ -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<MpcWallet>,
@InjectRepository(MpcShare)
private readonly shareRepository: Repository<MpcShare>,
@InjectRepository(MpcSession)
private readonly sessionRepository: Repository<MpcSession>,
) {
this.mpcSystemUrl = this.configService.get<string>('MPC_SYSTEM_URL', 'http://localhost:4000');
this.mpcApiKey = this.configService.get<string>('MPC_API_KEY', 'test-api-key');
}
/**
* keygen
* mpc-system /api/v1/mpc/keygen API
*/
async createKeygenSession(input: CreateKeygenInput): Promise<CreateKeygenOutput> {
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<void> {
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<KeygenStatusOutput> {
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<CreateSigningOutput> {
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<void> {
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<SigningStatusOutput> {
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<WalletOutput> {
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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}