feat: add event-driven communication between identity-service and mpc-service
Replace synchronous HTTP polling with Kafka event-driven model for MPC operations:
- Add MPC event consumer service in mpc-service for keygen/signing requests
- Add keygen-requested and signing-requested event handlers
- Add MPC event consumer in identity-service for completion events
- Extend mpc-client.service with async event-driven methods
- Support backward compatibility via MPC_USE_EVENT_DRIVEN env var
Topics: mpc.KeygenRequested, mpc.SigningRequested, mpc.KeygenCompleted,
mpc.SigningCompleted, mpc.SessionFailed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
17fd663fe3
commit
c459387c42
|
|
@ -3,20 +3,38 @@
|
||||||
*
|
*
|
||||||
* 与 mpc-service (NestJS) 通信的客户端服务
|
* 与 mpc-service (NestJS) 通信的客户端服务
|
||||||
*
|
*
|
||||||
|
* 支持两种模式:
|
||||||
|
* 1. 同步模式 (legacy): 直接 HTTP 调用 + 轮询等待结果
|
||||||
|
* 2. 事件驱动模式 (推荐): 发布 Kafka 事件,异步接收结果
|
||||||
|
*
|
||||||
* 调用路径 (DDD 分领域):
|
* 调用路径 (DDD 分领域):
|
||||||
* identity-service (身份域) → mpc-service (MPC域/NestJS) → mpc-system (Go/TSS实现)
|
* identity-service (身份域) → mpc-service (MPC域/NestJS) → mpc-system (Go/TSS实现)
|
||||||
*
|
*
|
||||||
* 业务流程:
|
* 事件驱动流程:
|
||||||
* 1. identity-service 调用 mpc-service 的 keygen API
|
* 1. identity-service 发布 mpc.KeygenRequested 事件
|
||||||
* 2. mpc-service 协调 mpc-system 完成 TSS keygen
|
* 2. mpc-service 消费事件,协调 mpc-system 完成 TSS keygen
|
||||||
* 3. 返回公钥和 delegate share (用户分片) 给 identity-service
|
* 3. mpc-service 发布 mpc.KeygenCompleted 事件
|
||||||
|
* 4. identity-service 消费事件,更新用户钱包地址
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import { HttpService } from '@nestjs/axios';
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { createHash, randomUUID } from 'crypto';
|
import { createHash, randomUUID } from 'crypto';
|
||||||
|
import { EventPublisherService, IDENTITY_TOPICS } from '../../kafka/event-publisher.service';
|
||||||
|
import {
|
||||||
|
MpcEventConsumerService,
|
||||||
|
KeygenCompletedPayload,
|
||||||
|
SigningCompletedPayload,
|
||||||
|
SessionFailedPayload,
|
||||||
|
} from '../../kafka/mpc-event-consumer.service';
|
||||||
|
|
||||||
|
// MPC Request Topics (发布到 mpc-service)
|
||||||
|
export const MPC_REQUEST_TOPICS = {
|
||||||
|
KEYGEN_REQUESTED: 'mpc.KeygenRequested',
|
||||||
|
SIGNING_REQUESTED: 'mpc.SigningRequested',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export interface KeygenRequest {
|
export interface KeygenRequest {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
@ -51,23 +69,285 @@ export interface SigningResult {
|
||||||
messageHash: string;
|
messageHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 异步请求接口 (事件驱动模式)
|
||||||
|
export interface AsyncKeygenRequest {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
threshold: number;
|
||||||
|
totalParties: number;
|
||||||
|
requireDelegate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncKeygenResponse {
|
||||||
|
sessionId: string;
|
||||||
|
status: 'pending' | 'processing';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncSigningRequest {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
messageHash: string;
|
||||||
|
userShare?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncSigningResponse {
|
||||||
|
sessionId: string;
|
||||||
|
status: 'pending' | 'processing';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结果回调类型
|
||||||
|
export type KeygenResultCallback = (result: KeygenResult | null, error?: string) => Promise<void>;
|
||||||
|
export type SigningResultCallback = (result: SigningResult | null, error?: string) => Promise<void>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MpcClientService {
|
export class MpcClientService implements OnModuleInit {
|
||||||
private readonly logger = new Logger(MpcClientService.name);
|
private readonly logger = new Logger(MpcClientService.name);
|
||||||
private readonly mpcServiceUrl: string; // mpc-service (NestJS) URL
|
private readonly mpcServiceUrl: string; // mpc-service (NestJS) URL
|
||||||
private readonly mpcMode: string;
|
private readonly mpcMode: string;
|
||||||
|
private readonly useEventDriven: boolean;
|
||||||
private readonly pollIntervalMs = 2000;
|
private readonly pollIntervalMs = 2000;
|
||||||
private readonly maxPollAttempts = 150; // 5 minutes max
|
private readonly maxPollAttempts = 150; // 5 minutes max
|
||||||
|
|
||||||
|
// 待处理的 keygen/signing 请求回调
|
||||||
|
private pendingKeygenCallbacks: Map<string, KeygenResultCallback> = new Map();
|
||||||
|
private pendingSigningCallbacks: Map<string, SigningResultCallback> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private readonly eventPublisher: EventPublisherService,
|
||||||
|
private readonly mpcEventConsumer: MpcEventConsumerService,
|
||||||
) {
|
) {
|
||||||
// 连接 mpc-service (NestJS)
|
// 连接 mpc-service (NestJS)
|
||||||
this.mpcServiceUrl = this.configService.get<string>('MPC_SERVICE_URL', 'http://localhost:3001');
|
this.mpcServiceUrl = this.configService.get<string>('MPC_SERVICE_URL', 'http://localhost:3001');
|
||||||
this.mpcMode = this.configService.get<string>('MPC_MODE', 'local');
|
this.mpcMode = this.configService.get<string>('MPC_MODE', 'local');
|
||||||
|
this.useEventDriven = this.configService.get<string>('MPC_USE_EVENT_DRIVEN', 'true') === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
// 注册 MPC 事件处理器
|
||||||
|
this.mpcEventConsumer.onKeygenCompleted(this.handleKeygenCompleted.bind(this));
|
||||||
|
this.mpcEventConsumer.onSigningCompleted(this.handleSigningCompleted.bind(this));
|
||||||
|
this.mpcEventConsumer.onSessionFailed(this.handleSessionFailed.bind(this));
|
||||||
|
this.logger.log('MPC event handlers registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// 事件驱动模式 - 异步 API
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步发起 keygen 请求 (事件驱动)
|
||||||
|
* 立即返回 sessionId,结果通过回调处理
|
||||||
|
*/
|
||||||
|
async requestKeygenAsync(
|
||||||
|
request: AsyncKeygenRequest,
|
||||||
|
callback?: KeygenResultCallback,
|
||||||
|
): Promise<AsyncKeygenResponse> {
|
||||||
|
const sessionId = this.generateSessionId();
|
||||||
|
|
||||||
|
this.logger.log(`Requesting async keygen: userId=${request.userId}, sessionId=${sessionId}`);
|
||||||
|
|
||||||
|
// 如果是本地模式,直接执行并回调
|
||||||
|
if (this.mpcMode === 'local') {
|
||||||
|
this.executeLocalKeygenWithCallback(sessionId, request, callback);
|
||||||
|
return { sessionId, status: 'processing' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册回调
|
||||||
|
if (callback) {
|
||||||
|
this.pendingKeygenCallbacks.set(sessionId, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发布 keygen 请求事件
|
||||||
|
await this.eventPublisher.publish(MPC_REQUEST_TOPICS.KEYGEN_REQUESTED, {
|
||||||
|
eventId: sessionId,
|
||||||
|
eventType: 'KeygenRequested',
|
||||||
|
occurredAt: new Date().toISOString(),
|
||||||
|
aggregateId: request.userId,
|
||||||
|
aggregateType: 'UserAccount',
|
||||||
|
payload: {
|
||||||
|
sessionId,
|
||||||
|
userId: request.userId,
|
||||||
|
username: request.username,
|
||||||
|
threshold: request.threshold,
|
||||||
|
totalParties: request.totalParties,
|
||||||
|
requireDelegate: request.requireDelegate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Keygen request published: sessionId=${sessionId}`);
|
||||||
|
return { sessionId, status: 'pending' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步发起 signing 请求 (事件驱动)
|
||||||
|
*/
|
||||||
|
async requestSigningAsync(
|
||||||
|
request: AsyncSigningRequest,
|
||||||
|
callback?: SigningResultCallback,
|
||||||
|
): Promise<AsyncSigningResponse> {
|
||||||
|
const sessionId = this.generateSessionId();
|
||||||
|
|
||||||
|
this.logger.log(`Requesting async signing: userId=${request.userId}, sessionId=${sessionId}`);
|
||||||
|
|
||||||
|
// 如果是本地模式,直接执行并回调
|
||||||
|
if (this.mpcMode === 'local') {
|
||||||
|
this.executeLocalSigningWithCallback(sessionId, request, callback);
|
||||||
|
return { sessionId, status: 'processing' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册回调
|
||||||
|
if (callback) {
|
||||||
|
this.pendingSigningCallbacks.set(sessionId, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发布 signing 请求事件
|
||||||
|
await this.eventPublisher.publish(MPC_REQUEST_TOPICS.SIGNING_REQUESTED, {
|
||||||
|
eventId: sessionId,
|
||||||
|
eventType: 'SigningRequested',
|
||||||
|
occurredAt: new Date().toISOString(),
|
||||||
|
aggregateId: request.userId,
|
||||||
|
aggregateType: 'UserAccount',
|
||||||
|
payload: {
|
||||||
|
sessionId,
|
||||||
|
userId: request.userId,
|
||||||
|
username: request.username,
|
||||||
|
messageHash: request.messageHash,
|
||||||
|
userShare: request.userShare,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Signing request published: sessionId=${sessionId}`);
|
||||||
|
return { sessionId, status: 'pending' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// 事件处理器 - 处理 MPC 完成事件
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
private async handleKeygenCompleted(payload: KeygenCompletedPayload): Promise<void> {
|
||||||
|
const sessionId = payload.sessionId;
|
||||||
|
const callback = this.pendingKeygenCallbacks.get(sessionId);
|
||||||
|
|
||||||
|
this.logger.log(`Keygen completed event received: sessionId=${sessionId}`);
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
try {
|
||||||
|
const result: KeygenResult = {
|
||||||
|
sessionId,
|
||||||
|
publicKey: payload.publicKey,
|
||||||
|
delegateShare: payload.extraPayload?.delegateShare || {
|
||||||
|
partyId: payload.partyId,
|
||||||
|
partyIndex: 0,
|
||||||
|
encryptedShare: '',
|
||||||
|
},
|
||||||
|
serverParties: payload.extraPayload?.serverParties || [],
|
||||||
|
};
|
||||||
|
await callback(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Keygen callback error: sessionId=${sessionId}`, error);
|
||||||
|
} finally {
|
||||||
|
this.pendingKeygenCallbacks.delete(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSigningCompleted(payload: SigningCompletedPayload): Promise<void> {
|
||||||
|
const sessionId = payload.sessionId;
|
||||||
|
const callback = this.pendingSigningCallbacks.get(sessionId);
|
||||||
|
|
||||||
|
this.logger.log(`Signing completed event received: sessionId=${sessionId}`);
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
try {
|
||||||
|
const result: SigningResult = {
|
||||||
|
sessionId,
|
||||||
|
signature: payload.signature,
|
||||||
|
messageHash: payload.messageHash,
|
||||||
|
};
|
||||||
|
await callback(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Signing callback error: sessionId=${sessionId}`, error);
|
||||||
|
} finally {
|
||||||
|
this.pendingSigningCallbacks.delete(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSessionFailed(payload: SessionFailedPayload): Promise<void> {
|
||||||
|
const sessionId = payload.sessionId;
|
||||||
|
const sessionType = payload.sessionType;
|
||||||
|
|
||||||
|
this.logger.warn(`Session failed event received: sessionId=${sessionId}, type=${sessionType}`);
|
||||||
|
|
||||||
|
if (sessionType === 'keygen') {
|
||||||
|
const callback = this.pendingKeygenCallbacks.get(sessionId);
|
||||||
|
if (callback) {
|
||||||
|
await callback(null, payload.errorMessage);
|
||||||
|
this.pendingKeygenCallbacks.delete(sessionId);
|
||||||
|
}
|
||||||
|
} else if (sessionType === 'sign') {
|
||||||
|
const callback = this.pendingSigningCallbacks.get(sessionId);
|
||||||
|
if (callback) {
|
||||||
|
await callback(null, payload.errorMessage);
|
||||||
|
this.pendingSigningCallbacks.delete(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// 本地模式辅助方法
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
private async executeLocalKeygenWithCallback(
|
||||||
|
sessionId: string,
|
||||||
|
request: AsyncKeygenRequest,
|
||||||
|
callback?: KeygenResultCallback,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result = await this.executeLocalKeygen({
|
||||||
|
sessionId,
|
||||||
|
username: request.username,
|
||||||
|
threshold: request.threshold,
|
||||||
|
totalParties: request.totalParties,
|
||||||
|
requireDelegate: request.requireDelegate,
|
||||||
|
});
|
||||||
|
if (callback) {
|
||||||
|
await callback(result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (callback) {
|
||||||
|
await callback(null, error instanceof Error ? error.message : 'Unknown error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeLocalSigningWithCallback(
|
||||||
|
sessionId: string,
|
||||||
|
request: AsyncSigningRequest,
|
||||||
|
callback?: SigningResultCallback,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result = await this.executeLocalSigning({
|
||||||
|
username: request.username,
|
||||||
|
messageHash: request.messageHash,
|
||||||
|
userShare: request.userShare,
|
||||||
|
});
|
||||||
|
if (callback) {
|
||||||
|
await callback(result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (callback) {
|
||||||
|
await callback(null, error instanceof Error ? error.message : 'Unknown error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// 同步模式 (Legacy) - 保留兼容性
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成新的会话ID (必须是纯 UUID 格式)
|
* 生成新的会话ID (必须是纯 UUID 格式)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { MpcWalletService } from './mpc-wallet.service';
|
import { MpcWalletService } from './mpc-wallet.service';
|
||||||
import { MpcClientService } from './mpc-client.service';
|
import { MpcClientService } from './mpc-client.service';
|
||||||
|
import { KafkaModule } from '../../kafka/kafka.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -9,6 +10,7 @@ import { MpcClientService } from './mpc-client.service';
|
||||||
timeout: 300000, // MPC 操作可能需要较长时间
|
timeout: 300000, // MPC 操作可能需要较长时间
|
||||||
maxRedirects: 5,
|
maxRedirects: 5,
|
||||||
}),
|
}),
|
||||||
|
KafkaModule, // 用于事件驱动模式
|
||||||
],
|
],
|
||||||
providers: [MpcWalletService, MpcClientService],
|
providers: [MpcWalletService, MpcClientService],
|
||||||
exports: [MpcWalletService, MpcClientService],
|
exports: [MpcWalletService, MpcClientService],
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ export * from './event-publisher.service';
|
||||||
export * from './event-consumer.controller';
|
export * from './event-consumer.controller';
|
||||||
export * from './dead-letter.service';
|
export * from './dead-letter.service';
|
||||||
export * from './event-retry.service';
|
export * from './event-retry.service';
|
||||||
|
export * from './mpc-event-consumer.service';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { EventPublisherService } from './event-publisher.service';
|
import { EventPublisherService } from './event-publisher.service';
|
||||||
|
import { MpcEventConsumerService } from './mpc-event-consumer.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [EventPublisherService],
|
providers: [
|
||||||
exports: [EventPublisherService],
|
EventPublisherService,
|
||||||
|
MpcEventConsumerService,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
EventPublisherService,
|
||||||
|
MpcEventConsumerService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class KafkaModule {}
|
export class KafkaModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
/**
|
||||||
|
* MPC Event Consumer Service
|
||||||
|
*
|
||||||
|
* Consumes MPC keygen/signing completion events from mpc-service via Kafka.
|
||||||
|
* Updates user wallet addresses when keygen completes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs';
|
||||||
|
|
||||||
|
// MPC Event Topics
|
||||||
|
export const MPC_TOPICS = {
|
||||||
|
KEYGEN_COMPLETED: 'mpc.KeygenCompleted',
|
||||||
|
SIGNING_COMPLETED: 'mpc.SigningCompleted',
|
||||||
|
SESSION_FAILED: 'mpc.SessionFailed',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface KeygenCompletedPayload {
|
||||||
|
sessionId: string;
|
||||||
|
partyId: string;
|
||||||
|
publicKey: string;
|
||||||
|
shareId: string;
|
||||||
|
threshold: string;
|
||||||
|
extraPayload?: {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
delegateShare?: {
|
||||||
|
partyId: string;
|
||||||
|
partyIndex: number;
|
||||||
|
encryptedShare: string;
|
||||||
|
};
|
||||||
|
serverParties?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SigningCompletedPayload {
|
||||||
|
sessionId: string;
|
||||||
|
partyId: string;
|
||||||
|
messageHash: string;
|
||||||
|
signature: string;
|
||||||
|
publicKey: string;
|
||||||
|
extraPayload?: {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
mpcSessionId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionFailedPayload {
|
||||||
|
sessionId: string;
|
||||||
|
partyId: string;
|
||||||
|
sessionType: string;
|
||||||
|
errorMessage: string;
|
||||||
|
errorCode?: string;
|
||||||
|
extraPayload?: {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MpcEventHandler<T> = (payload: T) => Promise<void>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MpcEventConsumerService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(MpcEventConsumerService.name);
|
||||||
|
private kafka: Kafka;
|
||||||
|
private consumer: Consumer;
|
||||||
|
private isConnected = false;
|
||||||
|
|
||||||
|
private keygenCompletedHandler?: MpcEventHandler<KeygenCompletedPayload>;
|
||||||
|
private signingCompletedHandler?: MpcEventHandler<SigningCompletedPayload>;
|
||||||
|
private sessionFailedHandler?: MpcEventHandler<SessionFailedPayload>;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const brokers = this.configService.get<string>('KAFKA_BROKERS')?.split(',') || ['localhost:9092'];
|
||||||
|
const clientId = this.configService.get<string>('KAFKA_CLIENT_ID') || 'identity-service';
|
||||||
|
const groupId = 'identity-service-mpc-events';
|
||||||
|
|
||||||
|
this.kafka = new Kafka({
|
||||||
|
clientId,
|
||||||
|
brokers,
|
||||||
|
logLevel: logLevel.WARN,
|
||||||
|
retry: {
|
||||||
|
initialRetryTime: 100,
|
||||||
|
retries: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.consumer = this.kafka.consumer({
|
||||||
|
groupId,
|
||||||
|
sessionTimeout: 30000,
|
||||||
|
heartbeatInterval: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.consumer.connect();
|
||||||
|
this.isConnected = true;
|
||||||
|
this.logger.log('MPC Event Kafka consumer connected');
|
||||||
|
|
||||||
|
// Subscribe to MPC topics
|
||||||
|
await this.consumer.subscribe({ topics: Object.values(MPC_TOPICS), fromBeginning: false });
|
||||||
|
this.logger.log(`Subscribed to MPC topics: ${Object.values(MPC_TOPICS).join(', ')}`);
|
||||||
|
|
||||||
|
// Start consuming
|
||||||
|
await this.startConsuming();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to connect MPC Event Kafka consumer', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
if (this.isConnected) {
|
||||||
|
await this.consumer.disconnect();
|
||||||
|
this.logger.log('MPC Event Kafka consumer disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register handler for keygen completed events
|
||||||
|
*/
|
||||||
|
onKeygenCompleted(handler: MpcEventHandler<KeygenCompletedPayload>): void {
|
||||||
|
this.keygenCompletedHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register handler for signing completed events
|
||||||
|
*/
|
||||||
|
onSigningCompleted(handler: MpcEventHandler<SigningCompletedPayload>): void {
|
||||||
|
this.signingCompletedHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register handler for session failed events
|
||||||
|
*/
|
||||||
|
onSessionFailed(handler: MpcEventHandler<SessionFailedPayload>): void {
|
||||||
|
this.sessionFailedHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startConsuming(): Promise<void> {
|
||||||
|
await this.consumer.run({
|
||||||
|
eachMessage: async ({ topic, partition, message }: EachMessagePayload) => {
|
||||||
|
try {
|
||||||
|
const value = message.value?.toString();
|
||||||
|
if (!value) {
|
||||||
|
this.logger.warn('Empty message received');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
const payload = parsed.payload || parsed;
|
||||||
|
|
||||||
|
this.logger.debug(`Received MPC event from ${topic}: ${JSON.stringify(payload)}`);
|
||||||
|
|
||||||
|
switch (topic) {
|
||||||
|
case MPC_TOPICS.KEYGEN_COMPLETED:
|
||||||
|
if (this.keygenCompletedHandler) {
|
||||||
|
await this.keygenCompletedHandler(payload as KeygenCompletedPayload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MPC_TOPICS.SIGNING_COMPLETED:
|
||||||
|
if (this.signingCompletedHandler) {
|
||||||
|
await this.signingCompletedHandler(payload as SigningCompletedPayload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MPC_TOPICS.SESSION_FAILED:
|
||||||
|
if (this.sessionFailedHandler) {
|
||||||
|
await this.sessionFailedHandler(payload as SessionFailedPayload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.logger.warn(`Unknown MPC topic: ${topic}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error processing MPC event from ${topic}`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log('Started consuming MPC events');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Application Module
|
* Application Module
|
||||||
*
|
*
|
||||||
* mpc-service 作为网关,只需要 MPCCoordinatorService 转发请求到 mpc-system
|
* mpc-service 作为网关,处理来自其他服务的事件请求
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
@ -10,6 +10,11 @@ import { InfrastructureModule } from '../infrastructure/infrastructure.module';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { MPCCoordinatorService } from './services/mpc-coordinator.service';
|
import { MPCCoordinatorService } from './services/mpc-coordinator.service';
|
||||||
|
import { EventConsumerStarterService } from './services/event-consumer-starter.service';
|
||||||
|
|
||||||
|
// Event Handlers
|
||||||
|
import { KeygenRequestedHandler } from './event-handlers/keygen-requested.handler';
|
||||||
|
import { SigningRequestedHandler } from './event-handlers/signing-requested.handler';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -19,6 +24,11 @@ import { MPCCoordinatorService } from './services/mpc-coordinator.service';
|
||||||
providers: [
|
providers: [
|
||||||
// Application Services
|
// Application Services
|
||||||
MPCCoordinatorService,
|
MPCCoordinatorService,
|
||||||
|
EventConsumerStarterService, // 启动 Kafka 消费者
|
||||||
|
|
||||||
|
// Event Handlers (Kafka consumers)
|
||||||
|
KeygenRequestedHandler,
|
||||||
|
SigningRequestedHandler,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
MPCCoordinatorService,
|
MPCCoordinatorService,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Event Handlers Module Exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './keygen-requested.handler';
|
||||||
|
export * from './signing-requested.handler';
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
/**
|
||||||
|
* KeygenRequested Event Handler
|
||||||
|
*
|
||||||
|
* Handles keygen requests from identity-service via Kafka.
|
||||||
|
* Processes the keygen and publishes completion/failure events.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { MPCCoordinatorService } from '../services/mpc-coordinator.service';
|
||||||
|
import { EventPublisherService } from '../../infrastructure/messaging/kafka/event-publisher.service';
|
||||||
|
import {
|
||||||
|
EventConsumerService,
|
||||||
|
MPC_CONSUME_TOPICS,
|
||||||
|
KeygenRequestedPayload,
|
||||||
|
} from '../../infrastructure/messaging/kafka/event-consumer.service';
|
||||||
|
import { KeygenCompletedEvent } from '../../domain/events/keygen-completed.event';
|
||||||
|
import { SessionFailedEvent } from '../../domain/events/session-failed.event';
|
||||||
|
import { SessionType } from '../../domain/enums';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KeygenRequestedHandler implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(KeygenRequestedHandler.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly eventConsumer: EventConsumerService,
|
||||||
|
private readonly eventPublisher: EventPublisherService,
|
||||||
|
private readonly mpcCoordinator: MPCCoordinatorService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.eventConsumer.subscribe(
|
||||||
|
MPC_CONSUME_TOPICS.KEYGEN_REQUESTED,
|
||||||
|
this.handleMessage.bind(this),
|
||||||
|
);
|
||||||
|
this.logger.log(`Subscribed to ${MPC_CONSUME_TOPICS.KEYGEN_REQUESTED}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMessage(topic: string, payload: Record<string, unknown>): Promise<void> {
|
||||||
|
const data = payload as unknown as KeygenRequestedPayload;
|
||||||
|
const { sessionId, userId, username, threshold, totalParties, requireDelegate } = data;
|
||||||
|
|
||||||
|
this.logger.log(`Processing keygen request: userId=${userId}, username=${username}, sessionId=${sessionId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Create keygen session via mpc-system
|
||||||
|
const createResult = await this.mpcCoordinator.createKeygenSession({
|
||||||
|
username,
|
||||||
|
thresholdN: totalParties,
|
||||||
|
thresholdT: threshold,
|
||||||
|
requireDelegate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mpcSessionId = createResult.sessionId;
|
||||||
|
this.logger.log(`Keygen session created in mpc-system: ${mpcSessionId}`);
|
||||||
|
|
||||||
|
// Step 2: Poll for completion (with max retries)
|
||||||
|
const result = await this.pollKeygenCompletion(mpcSessionId, 150, 2000);
|
||||||
|
|
||||||
|
if (result.status === 'completed' && result.publicKey) {
|
||||||
|
// Cache public key
|
||||||
|
await this.mpcCoordinator.savePublicKeyCache(username, result.publicKey);
|
||||||
|
|
||||||
|
// Save delegate share if exists
|
||||||
|
if (result.delegateShare) {
|
||||||
|
await this.mpcCoordinator.saveDelegateShare({
|
||||||
|
username,
|
||||||
|
partyId: result.delegateShare.partyId,
|
||||||
|
partyIndex: result.delegateShare.partyIndex,
|
||||||
|
encryptedShare: result.delegateShare.encryptedShare,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish success event
|
||||||
|
const completedEvent = new KeygenCompletedEvent(
|
||||||
|
sessionId, // Original session ID from identity-service
|
||||||
|
result.delegateShare?.partyId || '',
|
||||||
|
result.publicKey,
|
||||||
|
mpcSessionId,
|
||||||
|
`${threshold}-of-${totalParties}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add extra payload for identity-service
|
||||||
|
(completedEvent as any).extraPayload = {
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
delegateShare: result.delegateShare,
|
||||||
|
serverParties: [], // mpc-system manages this
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.eventPublisher.publishWithRetry(completedEvent);
|
||||||
|
this.logger.log(`Keygen completed: userId=${userId}, publicKey=${result.publicKey}`);
|
||||||
|
} else {
|
||||||
|
// Publish failure event
|
||||||
|
const failedEvent = new SessionFailedEvent(
|
||||||
|
sessionId,
|
||||||
|
'', // partyId
|
||||||
|
SessionType.KEYGEN,
|
||||||
|
`Keygen failed with status: ${result.status}`,
|
||||||
|
);
|
||||||
|
(failedEvent as any).extraPayload = { userId, username };
|
||||||
|
|
||||||
|
await this.eventPublisher.publishWithRetry(failedEvent);
|
||||||
|
this.logger.warn(`Keygen failed: userId=${userId}, status=${result.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Keygen processing error: userId=${userId}`, error);
|
||||||
|
|
||||||
|
// Publish failure event
|
||||||
|
const failedEvent = new SessionFailedEvent(
|
||||||
|
sessionId,
|
||||||
|
'', // partyId
|
||||||
|
SessionType.KEYGEN,
|
||||||
|
error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
);
|
||||||
|
(failedEvent as any).extraPayload = { userId, username };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.eventPublisher.publishWithRetry(failedEvent);
|
||||||
|
} catch (publishError) {
|
||||||
|
this.logger.error('Failed to publish failure event', publishError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pollKeygenCompletion(
|
||||||
|
sessionId: string,
|
||||||
|
maxAttempts: number,
|
||||||
|
intervalMs: number,
|
||||||
|
): Promise<{
|
||||||
|
status: string;
|
||||||
|
publicKey?: string;
|
||||||
|
delegateShare?: {
|
||||||
|
partyId: string;
|
||||||
|
partyIndex: number;
|
||||||
|
encryptedShare: string;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
const status = await this.mpcCoordinator.getKeygenStatus(sessionId);
|
||||||
|
|
||||||
|
if (status.status === 'completed') {
|
||||||
|
return {
|
||||||
|
status: 'completed',
|
||||||
|
publicKey: status.publicKey,
|
||||||
|
delegateShare: status.delegateShare,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.status === 'failed' || status.status === 'expired') {
|
||||||
|
return { status: status.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sleep(intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 'timeout' };
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
/**
|
||||||
|
* SigningRequested Event Handler
|
||||||
|
*
|
||||||
|
* Handles signing requests from identity-service via Kafka.
|
||||||
|
* Processes the signing and publishes completion/failure events.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { MPCCoordinatorService } from '../services/mpc-coordinator.service';
|
||||||
|
import { EventPublisherService } from '../../infrastructure/messaging/kafka/event-publisher.service';
|
||||||
|
import {
|
||||||
|
EventConsumerService,
|
||||||
|
MPC_CONSUME_TOPICS,
|
||||||
|
SigningRequestedPayload,
|
||||||
|
} from '../../infrastructure/messaging/kafka/event-consumer.service';
|
||||||
|
import { SigningCompletedEvent } from '../../domain/events/signing-completed.event';
|
||||||
|
import { SessionFailedEvent } from '../../domain/events/session-failed.event';
|
||||||
|
import { SessionType } from '../../domain/enums';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SigningRequestedHandler implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(SigningRequestedHandler.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly eventConsumer: EventConsumerService,
|
||||||
|
private readonly eventPublisher: EventPublisherService,
|
||||||
|
private readonly mpcCoordinator: MPCCoordinatorService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.eventConsumer.subscribe(
|
||||||
|
MPC_CONSUME_TOPICS.SIGNING_REQUESTED,
|
||||||
|
this.handleMessage.bind(this),
|
||||||
|
);
|
||||||
|
this.logger.log(`Subscribed to ${MPC_CONSUME_TOPICS.SIGNING_REQUESTED}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMessage(topic: string, payload: Record<string, unknown>): Promise<void> {
|
||||||
|
const data = payload as unknown as SigningRequestedPayload;
|
||||||
|
const { sessionId, userId, username, messageHash, userShare } = data;
|
||||||
|
|
||||||
|
this.logger.log(`Processing signing request: userId=${userId}, username=${username}, sessionId=${sessionId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Create signing session via mpc-system
|
||||||
|
const createResult = await this.mpcCoordinator.createSigningSession({
|
||||||
|
username,
|
||||||
|
messageHash,
|
||||||
|
userShare,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mpcSessionId = createResult.sessionId;
|
||||||
|
this.logger.log(`Signing session created in mpc-system: ${mpcSessionId}`);
|
||||||
|
|
||||||
|
// Step 2: Poll for completion (with max retries)
|
||||||
|
const result = await this.pollSigningCompletion(mpcSessionId, 150, 2000);
|
||||||
|
|
||||||
|
if (result.status === 'completed' && result.signature) {
|
||||||
|
// Publish success event
|
||||||
|
const completedEvent = new SigningCompletedEvent(
|
||||||
|
sessionId, // Original session ID from identity-service
|
||||||
|
'', // partyId
|
||||||
|
messageHash,
|
||||||
|
result.signature,
|
||||||
|
'', // publicKey - not needed for signing result
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add extra payload for identity-service
|
||||||
|
(completedEvent as any).extraPayload = {
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
mpcSessionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.eventPublisher.publishWithRetry(completedEvent);
|
||||||
|
this.logger.log(`Signing completed: userId=${userId}, signature=${result.signature.substring(0, 16)}...`);
|
||||||
|
} else {
|
||||||
|
// Publish failure event
|
||||||
|
const failedEvent = new SessionFailedEvent(
|
||||||
|
sessionId,
|
||||||
|
'', // partyId
|
||||||
|
SessionType.SIGN,
|
||||||
|
`Signing failed with status: ${result.status}`,
|
||||||
|
);
|
||||||
|
(failedEvent as any).extraPayload = { userId, username };
|
||||||
|
|
||||||
|
await this.eventPublisher.publishWithRetry(failedEvent);
|
||||||
|
this.logger.warn(`Signing failed: userId=${userId}, status=${result.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Signing processing error: userId=${userId}`, error);
|
||||||
|
|
||||||
|
// Publish failure event
|
||||||
|
const failedEvent = new SessionFailedEvent(
|
||||||
|
sessionId,
|
||||||
|
'', // partyId
|
||||||
|
SessionType.SIGN,
|
||||||
|
error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
);
|
||||||
|
(failedEvent as any).extraPayload = { userId, username };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.eventPublisher.publishWithRetry(failedEvent);
|
||||||
|
} catch (publishError) {
|
||||||
|
this.logger.error('Failed to publish failure event', publishError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pollSigningCompletion(
|
||||||
|
sessionId: string,
|
||||||
|
maxAttempts: number,
|
||||||
|
intervalMs: number,
|
||||||
|
): Promise<{
|
||||||
|
status: string;
|
||||||
|
signature?: string;
|
||||||
|
}> {
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
const status = await this.mpcCoordinator.getSigningStatus(sessionId);
|
||||||
|
|
||||||
|
if (status.status === 'completed') {
|
||||||
|
return {
|
||||||
|
status: 'completed',
|
||||||
|
signature: status.signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.status === 'failed' || status.status === 'expired') {
|
||||||
|
return { status: status.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sleep(intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 'timeout' };
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Event Consumer Starter Service
|
||||||
|
*
|
||||||
|
* Starts Kafka consumers after all handlers are registered.
|
||||||
|
* This ensures handlers are registered before consuming starts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
|
import { EventConsumerService } from '../../infrastructure/messaging/kafka/event-consumer.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EventConsumerStarterService implements OnApplicationBootstrap {
|
||||||
|
private readonly logger = new Logger(EventConsumerStarterService.name);
|
||||||
|
|
||||||
|
constructor(private readonly eventConsumer: EventConsumerService) {}
|
||||||
|
|
||||||
|
async onApplicationBootstrap() {
|
||||||
|
try {
|
||||||
|
await this.eventConsumer.startConsuming();
|
||||||
|
this.logger.log('MPC event consumers started successfully');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to start MPC event consumers', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './mpc-coordinator.service';
|
export * from './mpc-coordinator.service';
|
||||||
|
export * from './event-consumer-starter.service';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* Infrastructure Module
|
* Infrastructure Module
|
||||||
*
|
*
|
||||||
* mpc-service 作为网关,只需要 PrismaService 用于缓存公钥和 delegate share
|
* mpc-service 作为网关,需要:
|
||||||
|
* - PrismaService 用于缓存公钥和 delegate share
|
||||||
|
* - Kafka 事件发布和消费
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
|
@ -10,15 +12,25 @@ import { ConfigModule } from '@nestjs/config';
|
||||||
// Persistence
|
// Persistence
|
||||||
import { PrismaService } from './persistence/prisma/prisma.service';
|
import { PrismaService } from './persistence/prisma/prisma.service';
|
||||||
|
|
||||||
|
// Kafka Messaging
|
||||||
|
import { EventPublisherService } from './messaging/kafka/event-publisher.service';
|
||||||
|
import { EventConsumerService } from './messaging/kafka/event-consumer.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
providers: [
|
providers: [
|
||||||
// Prisma (用于缓存公钥和 delegate share)
|
// Prisma (用于缓存公钥和 delegate share)
|
||||||
PrismaService,
|
PrismaService,
|
||||||
|
|
||||||
|
// Kafka (事件发布和消费)
|
||||||
|
EventPublisherService,
|
||||||
|
EventConsumerService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
PrismaService,
|
PrismaService,
|
||||||
|
EventPublisherService,
|
||||||
|
EventConsumerService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class InfrastructureModule {}
|
export class InfrastructureModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
/**
|
||||||
|
* Event Consumer Service
|
||||||
|
*
|
||||||
|
* Consumes domain events from Kafka for async processing.
|
||||||
|
* Handles keygen and signing requests from identity-service.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs';
|
||||||
|
|
||||||
|
// Kafka Topics for consuming
|
||||||
|
export const MPC_CONSUME_TOPICS = {
|
||||||
|
KEYGEN_REQUESTED: 'mpc.KeygenRequested',
|
||||||
|
SIGNING_REQUESTED: 'mpc.SigningRequested',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface KeygenRequestedPayload {
|
||||||
|
sessionId: string;
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
threshold: number;
|
||||||
|
totalParties: number;
|
||||||
|
requireDelegate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SigningRequestedPayload {
|
||||||
|
sessionId: string;
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
messageHash: string;
|
||||||
|
userShare?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageHandler = (topic: string, payload: Record<string, unknown>) => Promise<void>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EventConsumerService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(EventConsumerService.name);
|
||||||
|
private kafka: Kafka;
|
||||||
|
private consumer: Consumer;
|
||||||
|
private isConnected = false;
|
||||||
|
private handlers: Map<string, MessageHandler> = new Map();
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const brokers = this.configService.get<string>('KAFKA_BROKERS')?.split(',') || ['localhost:9092'];
|
||||||
|
const clientId = this.configService.get<string>('KAFKA_CLIENT_ID') || 'mpc-service';
|
||||||
|
const groupId = this.configService.get<string>('KAFKA_GROUP_ID') || 'mpc-service-group';
|
||||||
|
|
||||||
|
this.kafka = new Kafka({
|
||||||
|
clientId,
|
||||||
|
brokers,
|
||||||
|
logLevel: logLevel.WARN,
|
||||||
|
retry: {
|
||||||
|
initialRetryTime: 100,
|
||||||
|
retries: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.consumer = this.kafka.consumer({
|
||||||
|
groupId,
|
||||||
|
sessionTimeout: 30000,
|
||||||
|
heartbeatInterval: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.consumer.connect();
|
||||||
|
this.isConnected = true;
|
||||||
|
this.logger.log('Kafka consumer connected');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to connect Kafka consumer', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
if (this.isConnected) {
|
||||||
|
await this.consumer.disconnect();
|
||||||
|
this.logger.log('Kafka consumer disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to a topic with a handler
|
||||||
|
*/
|
||||||
|
async subscribe(topic: string, handler: MessageHandler): Promise<void> {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.logger.warn('Kafka not connected, cannot subscribe');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlers.set(topic, handler);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.consumer.subscribe({ topic, fromBeginning: false });
|
||||||
|
this.logger.log(`Subscribed to topic: ${topic}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to subscribe to topic: ${topic}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start consuming messages
|
||||||
|
*/
|
||||||
|
async startConsuming(): Promise<void> {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.logger.warn('Kafka not connected, cannot start consuming');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.consumer.run({
|
||||||
|
eachMessage: async ({ topic, partition, message }: EachMessagePayload) => {
|
||||||
|
const handler = this.handlers.get(topic);
|
||||||
|
if (!handler) {
|
||||||
|
this.logger.warn(`No handler for topic: ${topic}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = message.value?.toString();
|
||||||
|
if (!value) {
|
||||||
|
this.logger.warn('Empty message received');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
this.logger.debug(`Received message from ${topic}: ${JSON.stringify(parsed)}`);
|
||||||
|
|
||||||
|
await handler(topic, parsed.payload || parsed);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error processing message from ${topic}`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log('Started consuming messages');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './event-publisher.service';
|
export * from './event-publisher.service';
|
||||||
|
export * from './event-consumer.service';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue