refactor: move backup-service client from identity-service to mpc-service

Architecture change: delegate share storage is now handled by mpc-service.
- identity-service no longer calls backup-service directly
- mpc-service calls backup-service after keygen completion
- This follows proper domain boundaries (MPC domain handles share storage)

Flow:
1. identity-service publishes mpc.KeygenRequested
2. mpc-service calls mpc-system for keygen
3. mpc-service stores delegate share to backup-service
4. mpc-service publishes mpc.KeygenCompleted
5. identity-service updates user wallet address

🤖 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 22:56:35 -08:00
parent f4f0466616
commit 383a9540a0
10 changed files with 192 additions and 306 deletions

View File

@ -32,7 +32,6 @@ import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.se
import { MpcEventConsumerService } from '@/infrastructure/kafka/mpc-event-consumer.service'; import { MpcEventConsumerService } from '@/infrastructure/kafka/mpc-event-consumer.service';
import { SmsService } from '@/infrastructure/external/sms/sms.service'; import { SmsService } from '@/infrastructure/external/sms/sms.service';
import { MpcClientService, MpcWalletService } from '@/infrastructure/external/mpc'; import { MpcClientService, MpcWalletService } from '@/infrastructure/external/mpc';
import { BackupClientService, MpcShareStorageService } from '@/infrastructure/external/backup';
import { WalletGeneratorServiceImpl } from '@/infrastructure/external/blockchain/wallet-generator.service.impl'; import { WalletGeneratorServiceImpl } from '@/infrastructure/external/blockchain/wallet-generator.service.impl';
// Shared // Shared
@ -56,8 +55,6 @@ import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
SmsService, SmsService,
MpcClientService, MpcClientService,
MpcWalletService, MpcWalletService,
BackupClientService,
MpcShareStorageService,
// WalletGeneratorService 抽象类由 WalletGeneratorServiceImpl 实现 // WalletGeneratorService 抽象类由 WalletGeneratorServiceImpl 实现
WalletGeneratorServiceImpl, WalletGeneratorServiceImpl,
{ provide: WalletGeneratorService, useExisting: WalletGeneratorServiceImpl }, { provide: WalletGeneratorService, useExisting: WalletGeneratorServiceImpl },
@ -71,8 +68,6 @@ import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
SmsService, SmsService,
MpcClientService, MpcClientService,
MpcWalletService, MpcWalletService,
BackupClientService,
MpcShareStorageService,
WalletGeneratorService, WalletGeneratorService,
MPC_KEY_SHARE_REPOSITORY, MPC_KEY_SHARE_REPOSITORY,
], ],

View File

@ -13,7 +13,6 @@ import { RedisService } from '@/infrastructure/redis/redis.service';
import { SmsService } from '@/infrastructure/external/sms/sms.service'; import { SmsService } from '@/infrastructure/external/sms/sms.service';
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
import { MpcWalletService } from '@/infrastructure/external/mpc'; import { MpcWalletService } from '@/infrastructure/external/mpc';
import { BackupClientService } from '@/infrastructure/external/backup';
describe('UserApplicationService - Referral APIs', () => { describe('UserApplicationService - Referral APIs', () => {
let service: UserApplicationService; let service: UserApplicationService;
@ -131,10 +130,6 @@ describe('UserApplicationService - Referral APIs', () => {
generateMpcWallet: jest.fn(), generateMpcWallet: jest.fn(),
}; };
const mockBackupClientService = {
storeBackupShare: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
UserApplicationService, UserApplicationService,
@ -182,10 +177,6 @@ describe('UserApplicationService - Referral APIs', () => {
provide: MpcWalletService, provide: MpcWalletService,
useValue: mockMpcWalletService, useValue: mockMpcWalletService,
}, },
{
provide: BackupClientService,
useValue: mockBackupClientService,
},
], ],
}).compile(); }).compile();

View File

@ -15,7 +15,6 @@ import { RedisService } from '@/infrastructure/redis/redis.service';
import { SmsService } from '@/infrastructure/external/sms/sms.service'; import { SmsService } from '@/infrastructure/external/sms/sms.service';
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
import { MpcWalletService } from '@/infrastructure/external/mpc'; import { MpcWalletService } from '@/infrastructure/external/mpc';
import { BackupClientService } from '@/infrastructure/external/backup';
import { ApplicationError } from '@/shared/exceptions/domain.exception'; import { ApplicationError } from '@/shared/exceptions/domain.exception';
import { generateRandomIdentity } from '@/shared/utils'; import { generateRandomIdentity } from '@/shared/utils';
import { import {
@ -43,7 +42,6 @@ export class UserApplicationService {
private readonly validatorService: UserValidatorService, private readonly validatorService: UserValidatorService,
private readonly walletGenerator: WalletGeneratorService, private readonly walletGenerator: WalletGeneratorService,
private readonly mpcWalletService: MpcWalletService, private readonly mpcWalletService: MpcWalletService,
private readonly backupClient: BackupClientService,
private readonly tokenService: TokenService, private readonly tokenService: TokenService,
private readonly redisService: RedisService, private readonly redisService: RedisService,
private readonly smsService: SmsService, private readonly smsService: SmsService,

View File

@ -1,2 +0,0 @@
export * from './backup-client.service';
export * from './mpc-share-storage.service';

View File

@ -1,89 +0,0 @@
/**
* MPC Share Storage Service
*
* MPC
*/
import { Injectable, Logger } from '@nestjs/common';
import { BackupClientService } from './backup-client.service';
export interface MpcStoreBackupShareParams {
userId: string;
shareData: string;
publicKey: string;
accountSequence?: number;
}
export interface MpcRetrieveBackupShareParams {
userId: string;
publicKey: string;
recoveryToken: string;
deviceId?: string;
}
export interface MpcBackupShareData {
encryptedShareData: string;
partyIndex: number;
publicKey: string;
}
@Injectable()
export class MpcShareStorageService {
private readonly logger = new Logger(MpcShareStorageService.name);
constructor(private readonly backupClient: BackupClientService) {}
/**
*
*
* @param params
*/
async storeBackupShare(params: MpcStoreBackupShareParams): Promise<void> {
this.logger.log(`Storing backup share for user=${params.userId}`);
await this.backupClient.storeBackupShare({
userId: params.userId,
accountSequence: params.accountSequence || 0,
publicKey: params.publicKey,
encryptedShareData: params.shareData,
});
}
/**
* ()
*
* @param params
* @returns null
*/
async retrieveBackupShare(
params: MpcRetrieveBackupShareParams,
): Promise<MpcBackupShareData | null> {
this.logger.log(`Retrieving backup share for user=${params.userId}`);
return this.backupClient.retrieveBackupShare(params);
}
/**
*
*
* @param userId ID
* @param publicKey MPC
* @param reason
*/
async revokeBackupShare(
userId: string,
publicKey: string,
reason: string,
): Promise<void> {
this.logger.log(`Revoking backup share for user=${userId}`);
await this.backupClient.revokeBackupShare(userId, publicKey, reason);
}
/**
*
*/
isEnabled(): boolean {
return this.backupClient.isEnabled();
}
}

View File

@ -11,7 +11,6 @@ import { SmsService } from './external/sms/sms.service';
import { WalletGeneratorServiceImpl } from './external/blockchain/wallet-generator.service.impl'; import { WalletGeneratorServiceImpl } from './external/blockchain/wallet-generator.service.impl';
import { BlockchainQueryService } from './external/blockchain/blockchain-query.service'; import { BlockchainQueryService } from './external/blockchain/blockchain-query.service';
import { MpcClientService, MpcWalletService } from './external/mpc'; import { MpcClientService, MpcWalletService } from './external/mpc';
import { BackupClientService, MpcShareStorageService } from './external/backup';
import { MPC_KEY_SHARE_REPOSITORY } from '@/domain/repositories/mpc-key-share.repository.interface'; import { MPC_KEY_SHARE_REPOSITORY } from '@/domain/repositories/mpc-key-share.repository.interface';
import { WalletGeneratorService } from '@/domain/services/wallet-generator.service'; import { WalletGeneratorService } from '@/domain/services/wallet-generator.service';
@ -44,8 +43,6 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
BlockchainQueryService, BlockchainQueryService,
MpcClientService, MpcClientService,
MpcWalletService, MpcWalletService,
BackupClientService,
MpcShareStorageService,
], ],
exports: [ exports: [
PrismaService, PrismaService,
@ -64,8 +61,6 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
BlockchainQueryService, BlockchainQueryService,
MpcClientService, MpcClientService,
MpcWalletService, MpcWalletService,
BackupClientService,
MpcShareStorageService,
], ],
}) })
export class InfrastructureModule {} export class InfrastructureModule {}

View File

@ -13,6 +13,7 @@ import {
MPC_CONSUME_TOPICS, MPC_CONSUME_TOPICS,
KeygenRequestedPayload, KeygenRequestedPayload,
} from '../../infrastructure/messaging/kafka/event-consumer.service'; } from '../../infrastructure/messaging/kafka/event-consumer.service';
import { BackupClientService } from '../../infrastructure/external/backup';
import { KeygenStartedEvent } from '../../domain/events/keygen-started.event'; import { KeygenStartedEvent } from '../../domain/events/keygen-started.event';
import { KeygenCompletedEvent } from '../../domain/events/keygen-completed.event'; import { KeygenCompletedEvent } from '../../domain/events/keygen-completed.event';
import { SessionFailedEvent } from '../../domain/events/session-failed.event'; import { SessionFailedEvent } from '../../domain/events/session-failed.event';
@ -26,6 +27,7 @@ export class KeygenRequestedHandler implements OnModuleInit {
private readonly eventConsumer: EventConsumerService, private readonly eventConsumer: EventConsumerService,
private readonly eventPublisher: EventPublisherService, private readonly eventPublisher: EventPublisherService,
private readonly mpcCoordinator: MPCCoordinatorService, private readonly mpcCoordinator: MPCCoordinatorService,
private readonly backupClient: BackupClientService,
) {} ) {}
async onModuleInit() { async onModuleInit() {
@ -72,14 +74,31 @@ export class KeygenRequestedHandler implements OnModuleInit {
// Cache public key // Cache public key
await this.mpcCoordinator.savePublicKeyCache(username, result.publicKey); await this.mpcCoordinator.savePublicKeyCache(username, result.publicKey);
// Save delegate share if exists // Save delegate share to backup-service if exists
if (result.delegateShare) { if (result.delegateShare) {
// 1. 保存到本地缓存(用于签名时快速访问)
await this.mpcCoordinator.saveDelegateShare({ await this.mpcCoordinator.saveDelegateShare({
username, username,
partyId: result.delegateShare.partyId, partyId: result.delegateShare.partyId,
partyIndex: result.delegateShare.partyIndex, partyIndex: result.delegateShare.partyIndex,
encryptedShare: result.delegateShare.encryptedShare, encryptedShare: result.delegateShare.encryptedShare,
}); });
// 2. 存储到 backup-service用于灾备恢复
try {
await this.backupClient.storeBackupShare({
userId,
username,
publicKey: result.publicKey,
partyId: result.delegateShare.partyId,
partyIndex: result.delegateShare.partyIndex,
encryptedShare: result.delegateShare.encryptedShare,
});
this.logger.log(`Delegate share stored to backup-service: userId=${userId}`);
} catch (backupError) {
// 备份失败不阻塞主流程,但记录错误
this.logger.error(`Failed to store delegate share to backup-service: userId=${userId}`, backupError);
}
} }
// Publish success event // Publish success event

View File

@ -1,192 +1,155 @@
/** /**
* Backup Client Service * Backup Client Service
* *
* backup-service * mpc-service backup-service / delegate share
* MPC Backup Share (Party 2) * backup-service MPC
* */
* 安全要求: backup-service identity-service
*/ import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs';
import { ConfigService } from '@nestjs/config'; import * as jwt from 'jsonwebtoken';
import { firstValueFrom } from 'rxjs';
import * as jwt from 'jsonwebtoken'; export interface StoreBackupShareParams {
userId: string;
export interface StoreBackupShareParams { username: string;
userId: string; publicKey: string;
accountSequence: number; partyId: string;
publicKey: string; partyIndex: number;
encryptedShareData: string; encryptedShare: string;
} }
export interface RetrieveBackupShareParams { export interface RetrieveBackupShareParams {
userId: string; userId: string;
publicKey: string; publicKey: string;
recoveryToken: string; }
deviceId?: string;
} export interface BackupShareResult {
encryptedShareData: string;
export interface BackupShareResult { partyIndex: number;
encryptedShareData: string; publicKey: string;
partyIndex: number; }
publicKey: string;
} @Injectable()
export class BackupClientService {
@Injectable() private readonly logger = new Logger(BackupClientService.name);
export class BackupClientService { private readonly backupServiceUrl: string;
private readonly logger = new Logger(BackupClientService.name); private readonly serviceJwtSecret: string;
private readonly backupServiceUrl: string; private readonly enabled: boolean;
private readonly serviceJwtSecret: string;
private readonly enabled: boolean; constructor(
private readonly httpService: HttpService,
constructor( private readonly configService: ConfigService,
private readonly httpService: HttpService, ) {
private readonly configService: ConfigService, this.backupServiceUrl = this.configService.get<string>('BACKUP_SERVICE_URL', 'http://localhost:3002');
) { this.serviceJwtSecret = this.configService.get<string>('SERVICE_JWT_SECRET', '');
this.backupServiceUrl = this.configService.get<string>('BACKUP_SERVICE_URL', 'http://localhost:3002'); this.enabled = this.configService.get<string>('BACKUP_SERVICE_ENABLED', 'true') === 'true';
this.serviceJwtSecret = this.configService.get<string>('SERVICE_JWT_SECRET', '');
this.enabled = this.configService.get<string>('BACKUP_SERVICE_ENABLED', 'false') === 'true'; this.logger.log(`[INIT] BackupClientService initialized`);
} this.logger.log(`[INIT] URL: ${this.backupServiceUrl}`);
this.logger.log(`[INIT] Enabled: ${this.enabled}`);
/** }
* backup-service
*/ /**
isEnabled(): boolean { * backup-service
return this.enabled && !!this.serviceJwtSecret; */
} isEnabled(): boolean {
return this.enabled && !!this.serviceJwtSecret;
/** }
* backup-service
*/ /**
async storeBackupShare(params: StoreBackupShareParams): Promise<void> { * delegate share backup-service
if (!this.isEnabled()) { */
this.logger.warn('Backup service is disabled, skipping backup share storage'); async storeBackupShare(params: StoreBackupShareParams): Promise<void> {
return; if (!this.isEnabled()) {
} this.logger.warn('Backup service is disabled, skipping backup share storage');
return;
this.logger.log(`Storing backup share for user: ${params.userId}`); }
try { this.logger.log(`Storing backup share: userId=${params.userId}, username=${params.username}`);
const serviceToken = this.generateServiceToken();
try {
await firstValueFrom( const serviceToken = this.generateServiceToken();
this.httpService.post(
`${this.backupServiceUrl}/backup-share/store`, await firstValueFrom(
{ this.httpService.post(
userId: params.userId, `${this.backupServiceUrl}/backup-share/store`,
accountSequence: params.accountSequence, {
publicKey: params.publicKey, userId: params.userId,
encryptedShareData: params.encryptedShareData, username: params.username,
}, publicKey: params.publicKey,
{ partyId: params.partyId,
headers: { partyIndex: params.partyIndex,
'Content-Type': 'application/json', encryptedShareData: params.encryptedShare,
'X-Service-Token': serviceToken, },
}, {
timeout: 30000, // 30秒超时 headers: {
}, 'Content-Type': 'application/json',
), 'X-Service-Token': serviceToken,
); },
timeout: 30000,
this.logger.log(`Backup share stored successfully for user: ${params.userId}`); },
} catch (error) { ),
this.logger.error(`Failed to store backup share for user: ${params.userId}`, error); );
// 不抛出异常,允许账户创建继续
// 可以通过补偿任务稍后重试 this.logger.log(`Backup share stored successfully: userId=${params.userId}`);
} } catch (error) {
} this.logger.error(`Failed to store backup share: userId=${params.userId}`, error);
throw error; // 抛出异常,让调用方处理
/** }
* backup-service () }
*/
async retrieveBackupShare(params: RetrieveBackupShareParams): Promise<BackupShareResult | null> { /**
if (!this.isEnabled()) { * backup-service delegate share ()
this.logger.warn('Backup service is disabled'); */
return null; async retrieveBackupShare(params: RetrieveBackupShareParams): Promise<BackupShareResult | null> {
} if (!this.isEnabled()) {
this.logger.warn('Backup service is disabled');
this.logger.log(`Retrieving backup share for user: ${params.userId}`); return null;
}
try {
const serviceToken = this.generateServiceToken(); this.logger.log(`Retrieving backup share: userId=${params.userId}`);
const response = await firstValueFrom( try {
this.httpService.post<BackupShareResult>( const serviceToken = this.generateServiceToken();
`${this.backupServiceUrl}/backup-share/retrieve`,
{ const response = await firstValueFrom(
userId: params.userId, this.httpService.post<BackupShareResult>(
publicKey: params.publicKey, `${this.backupServiceUrl}/backup-share/retrieve`,
recoveryToken: params.recoveryToken, {
deviceId: params.deviceId, userId: params.userId,
}, publicKey: params.publicKey,
{ },
headers: { {
'Content-Type': 'application/json', headers: {
'X-Service-Token': serviceToken, 'Content-Type': 'application/json',
}, 'X-Service-Token': serviceToken,
timeout: 30000, },
}, timeout: 30000,
), },
); ),
);
this.logger.log(`Backup share retrieved successfully for user: ${params.userId}`);
return response.data; this.logger.log(`Backup share retrieved: userId=${params.userId}`);
} catch (error) { return response.data;
this.logger.error(`Failed to retrieve backup share for user: ${params.userId}`, error); } catch (error) {
throw new Error(`Failed to retrieve backup share: ${error.message}`); this.logger.error(`Failed to retrieve backup share: userId=${params.userId}`, error);
} return null;
} }
}
/**
* () /**
*/ * JWT
async revokeBackupShare(userId: string, publicKey: string, reason: string): Promise<void> { */
if (!this.isEnabled()) { private generateServiceToken(): string {
this.logger.warn('Backup service is disabled'); return jwt.sign(
return; {
} service: 'mpc-service',
iat: Math.floor(Date.now() / 1000),
this.logger.log(`Revoking backup share for user: ${userId}, reason: ${reason}`); },
this.serviceJwtSecret,
try { { expiresIn: '5m' },
const serviceToken = this.generateServiceToken(); );
}
await firstValueFrom( }
this.httpService.post(
`${this.backupServiceUrl}/backup-share/revoke`,
{
userId,
publicKey,
reason,
},
{
headers: {
'Content-Type': 'application/json',
'X-Service-Token': serviceToken,
},
timeout: 30000,
},
),
);
this.logger.log(`Backup share revoked successfully for user: ${userId}`);
} catch (error) {
this.logger.error(`Failed to revoke backup share for user: ${userId}`, error);
}
}
/**
* JWT
*/
private generateServiceToken(): string {
return jwt.sign(
{
service: 'identity-service',
iat: Math.floor(Date.now() / 1000),
},
this.serviceJwtSecret,
{ expiresIn: '5m' },
);
}
}

View File

@ -0,0 +1 @@
export { BackupClientService } from './backup-client.service';

View File

@ -4,10 +4,12 @@
* mpc-service : * mpc-service :
* - PrismaService delegate share * - PrismaService delegate share
* - Kafka * - Kafka
* - BackupClientService delegate share backup-service
*/ */
import { Global, Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios';
// Persistence // Persistence
import { PrismaService } from './persistence/prisma/prisma.service'; import { PrismaService } from './persistence/prisma/prisma.service';
@ -16,9 +18,18 @@ import { PrismaService } from './persistence/prisma/prisma.service';
import { EventPublisherService } from './messaging/kafka/event-publisher.service'; import { EventPublisherService } from './messaging/kafka/event-publisher.service';
import { EventConsumerService } from './messaging/kafka/event-consumer.service'; import { EventConsumerService } from './messaging/kafka/event-consumer.service';
// External Services
import { BackupClientService } from './external/backup';
@Global() @Global()
@Module({ @Module({
imports: [ConfigModule], imports: [
ConfigModule,
HttpModule.register({
timeout: 30000,
maxRedirects: 5,
}),
],
providers: [ providers: [
// Prisma (用于缓存公钥和 delegate share) // Prisma (用于缓存公钥和 delegate share)
PrismaService, PrismaService,
@ -26,11 +37,15 @@ import { EventConsumerService } from './messaging/kafka/event-consumer.service';
// Kafka (事件发布和消费) // Kafka (事件发布和消费)
EventPublisherService, EventPublisherService,
EventConsumerService, EventConsumerService,
// External Services
BackupClientService,
], ],
exports: [ exports: [
PrismaService, PrismaService,
EventPublisherService, EventPublisherService,
EventConsumerService, EventConsumerService,
BackupClientService,
], ],
}) })
export class InfrastructureModule {} export class InfrastructureModule {}