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 { SmsService } from '@/infrastructure/external/sms/sms.service';
import { MpcClientService, MpcWalletService } from '@/infrastructure/external/mpc';
import { BackupClientService, MpcShareStorageService } from '@/infrastructure/external/backup';
import { WalletGeneratorServiceImpl } from '@/infrastructure/external/blockchain/wallet-generator.service.impl';
// Shared
@ -56,8 +55,6 @@ import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
SmsService,
MpcClientService,
MpcWalletService,
BackupClientService,
MpcShareStorageService,
// WalletGeneratorService 抽象类由 WalletGeneratorServiceImpl 实现
WalletGeneratorServiceImpl,
{ provide: WalletGeneratorService, useExisting: WalletGeneratorServiceImpl },
@ -71,8 +68,6 @@ import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
SmsService,
MpcClientService,
MpcWalletService,
BackupClientService,
MpcShareStorageService,
WalletGeneratorService,
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 { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
import { MpcWalletService } from '@/infrastructure/external/mpc';
import { BackupClientService } from '@/infrastructure/external/backup';
describe('UserApplicationService - Referral APIs', () => {
let service: UserApplicationService;
@ -131,10 +130,6 @@ describe('UserApplicationService - Referral APIs', () => {
generateMpcWallet: jest.fn(),
};
const mockBackupClientService = {
storeBackupShare: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UserApplicationService,
@ -182,10 +177,6 @@ describe('UserApplicationService - Referral APIs', () => {
provide: MpcWalletService,
useValue: mockMpcWalletService,
},
{
provide: BackupClientService,
useValue: mockBackupClientService,
},
],
}).compile();

View File

@ -15,7 +15,6 @@ import { RedisService } from '@/infrastructure/redis/redis.service';
import { SmsService } from '@/infrastructure/external/sms/sms.service';
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
import { MpcWalletService } from '@/infrastructure/external/mpc';
import { BackupClientService } from '@/infrastructure/external/backup';
import { ApplicationError } from '@/shared/exceptions/domain.exception';
import { generateRandomIdentity } from '@/shared/utils';
import {
@ -43,7 +42,6 @@ export class UserApplicationService {
private readonly validatorService: UserValidatorService,
private readonly walletGenerator: WalletGeneratorService,
private readonly mpcWalletService: MpcWalletService,
private readonly backupClient: BackupClientService,
private readonly tokenService: TokenService,
private readonly redisService: RedisService,
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 { BlockchainQueryService } from './external/blockchain/blockchain-query.service';
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 { WalletGeneratorService } from '@/domain/services/wallet-generator.service';
@ -44,8 +43,6 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
BlockchainQueryService,
MpcClientService,
MpcWalletService,
BackupClientService,
MpcShareStorageService,
],
exports: [
PrismaService,
@ -64,8 +61,6 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
BlockchainQueryService,
MpcClientService,
MpcWalletService,
BackupClientService,
MpcShareStorageService,
],
})
export class InfrastructureModule {}

View File

@ -13,6 +13,7 @@ import {
MPC_CONSUME_TOPICS,
KeygenRequestedPayload,
} from '../../infrastructure/messaging/kafka/event-consumer.service';
import { BackupClientService } from '../../infrastructure/external/backup';
import { KeygenStartedEvent } from '../../domain/events/keygen-started.event';
import { KeygenCompletedEvent } from '../../domain/events/keygen-completed.event';
import { SessionFailedEvent } from '../../domain/events/session-failed.event';
@ -26,6 +27,7 @@ export class KeygenRequestedHandler implements OnModuleInit {
private readonly eventConsumer: EventConsumerService,
private readonly eventPublisher: EventPublisherService,
private readonly mpcCoordinator: MPCCoordinatorService,
private readonly backupClient: BackupClientService,
) {}
async onModuleInit() {
@ -72,14 +74,31 @@ export class KeygenRequestedHandler implements OnModuleInit {
// Cache public key
await this.mpcCoordinator.savePublicKeyCache(username, result.publicKey);
// Save delegate share if exists
// Save delegate share to backup-service if exists
if (result.delegateShare) {
// 1. 保存到本地缓存(用于签名时快速访问)
await this.mpcCoordinator.saveDelegateShare({
username,
partyId: result.delegateShare.partyId,
partyIndex: result.delegateShare.partyIndex,
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

View File

@ -1,192 +1,155 @@
/**
* Backup Client Service
*
* backup-service
* MPC Backup Share (Party 2)
*
* 安全要求: backup-service identity-service
*/
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import * as jwt from 'jsonwebtoken';
export interface StoreBackupShareParams {
userId: string;
accountSequence: number;
publicKey: string;
encryptedShareData: string;
}
export interface RetrieveBackupShareParams {
userId: string;
publicKey: string;
recoveryToken: string;
deviceId?: string;
}
export interface BackupShareResult {
encryptedShareData: string;
partyIndex: number;
publicKey: string;
}
@Injectable()
export class BackupClientService {
private readonly logger = new Logger(BackupClientService.name);
private readonly backupServiceUrl: string;
private readonly serviceJwtSecret: string;
private readonly enabled: boolean;
constructor(
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.enabled = this.configService.get<string>('BACKUP_SERVICE_ENABLED', 'false') === 'true';
}
/**
* backup-service
*/
isEnabled(): boolean {
return this.enabled && !!this.serviceJwtSecret;
}
/**
* backup-service
*/
async storeBackupShare(params: StoreBackupShareParams): Promise<void> {
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 {
const serviceToken = this.generateServiceToken();
await firstValueFrom(
this.httpService.post(
`${this.backupServiceUrl}/backup-share/store`,
{
userId: params.userId,
accountSequence: params.accountSequence,
publicKey: params.publicKey,
encryptedShareData: params.encryptedShareData,
},
{
headers: {
'Content-Type': 'application/json',
'X-Service-Token': serviceToken,
},
timeout: 30000, // 30秒超时
},
),
);
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);
// 不抛出异常,允许账户创建继续
// 可以通过补偿任务稍后重试
}
}
/**
* backup-service ()
*/
async retrieveBackupShare(params: RetrieveBackupShareParams): Promise<BackupShareResult | null> {
if (!this.isEnabled()) {
this.logger.warn('Backup service is disabled');
return null;
}
this.logger.log(`Retrieving backup share for user: ${params.userId}`);
try {
const serviceToken = this.generateServiceToken();
const response = await firstValueFrom(
this.httpService.post<BackupShareResult>(
`${this.backupServiceUrl}/backup-share/retrieve`,
{
userId: params.userId,
publicKey: params.publicKey,
recoveryToken: params.recoveryToken,
deviceId: params.deviceId,
},
{
headers: {
'Content-Type': 'application/json',
'X-Service-Token': serviceToken,
},
timeout: 30000,
},
),
);
this.logger.log(`Backup share retrieved successfully for user: ${params.userId}`);
return response.data;
} catch (error) {
this.logger.error(`Failed to retrieve backup share for user: ${params.userId}`, error);
throw new Error(`Failed to retrieve backup share: ${error.message}`);
}
}
/**
* ()
*/
async revokeBackupShare(userId: string, publicKey: string, reason: string): Promise<void> {
if (!this.isEnabled()) {
this.logger.warn('Backup service is disabled');
return;
}
this.logger.log(`Revoking backup share for user: ${userId}, reason: ${reason}`);
try {
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' },
);
}
}
/**
* Backup Client Service
*
* mpc-service backup-service / delegate share
* backup-service MPC
*/
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import * as jwt from 'jsonwebtoken';
export interface StoreBackupShareParams {
userId: string;
username: string;
publicKey: string;
partyId: string;
partyIndex: number;
encryptedShare: string;
}
export interface RetrieveBackupShareParams {
userId: string;
publicKey: string;
}
export interface BackupShareResult {
encryptedShareData: string;
partyIndex: number;
publicKey: string;
}
@Injectable()
export class BackupClientService {
private readonly logger = new Logger(BackupClientService.name);
private readonly backupServiceUrl: string;
private readonly serviceJwtSecret: string;
private readonly enabled: boolean;
constructor(
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.enabled = this.configService.get<string>('BACKUP_SERVICE_ENABLED', 'true') === '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 {
return this.enabled && !!this.serviceJwtSecret;
}
/**
* delegate share backup-service
*/
async storeBackupShare(params: StoreBackupShareParams): Promise<void> {
if (!this.isEnabled()) {
this.logger.warn('Backup service is disabled, skipping backup share storage');
return;
}
this.logger.log(`Storing backup share: userId=${params.userId}, username=${params.username}`);
try {
const serviceToken = this.generateServiceToken();
await firstValueFrom(
this.httpService.post(
`${this.backupServiceUrl}/backup-share/store`,
{
userId: params.userId,
username: params.username,
publicKey: params.publicKey,
partyId: params.partyId,
partyIndex: params.partyIndex,
encryptedShareData: params.encryptedShare,
},
{
headers: {
'Content-Type': 'application/json',
'X-Service-Token': serviceToken,
},
timeout: 30000,
},
),
);
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 delegate share ()
*/
async retrieveBackupShare(params: RetrieveBackupShareParams): Promise<BackupShareResult | null> {
if (!this.isEnabled()) {
this.logger.warn('Backup service is disabled');
return null;
}
this.logger.log(`Retrieving backup share: userId=${params.userId}`);
try {
const serviceToken = this.generateServiceToken();
const response = await firstValueFrom(
this.httpService.post<BackupShareResult>(
`${this.backupServiceUrl}/backup-share/retrieve`,
{
userId: params.userId,
publicKey: params.publicKey,
},
{
headers: {
'Content-Type': 'application/json',
'X-Service-Token': serviceToken,
},
timeout: 30000,
},
),
);
this.logger.log(`Backup share retrieved: userId=${params.userId}`);
return response.data;
} catch (error) {
this.logger.error(`Failed to retrieve backup share: userId=${params.userId}`, error);
return null;
}
}
/**
* JWT
*/
private generateServiceToken(): string {
return jwt.sign(
{
service: 'mpc-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 :
* - PrismaService delegate share
* - Kafka
* - BackupClientService delegate share backup-service
*/
import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios';
// Persistence
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 { EventConsumerService } from './messaging/kafka/event-consumer.service';
// External Services
import { BackupClientService } from './external/backup';
@Global()
@Module({
imports: [ConfigModule],
imports: [
ConfigModule,
HttpModule.register({
timeout: 30000,
maxRedirects: 5,
}),
],
providers: [
// Prisma (用于缓存公钥和 delegate share)
PrismaService,
@ -26,11 +37,15 @@ import { EventConsumerService } from './messaging/kafka/event-consumer.service';
// Kafka (事件发布和消费)
EventPublisherService,
EventConsumerService,
// External Services
BackupClientService,
],
exports: [
PrismaService,
EventPublisherService,
EventConsumerService,
BackupClientService,
],
})
export class InfrastructureModule {}