fix: 防止钱包生成中状态下重复触发MPC keygen
问题: - 前端在钱包状态为"generating"时仍然调用retryWalletGeneration - 后端identity-service没有检查生成中状态 - mpc-service没有幂等保护,可能导致同一用户多次keygen 修复: 1. 前端 wallet_status_provider.dart: - 只在"failed"状态下才触发重试 - "generating"状态只更新UI,继续轮询等待 2. 后端 identity-service user-application.service.ts: - retryWalletGeneration添加Redis状态检查 - pending/generating/deriving状态下跳过重试 - 只有failed或无状态时才触发重试 3. 后端 mpc-service keygen-requested.handler.ts: - 使用分布式锁防止同一用户重复keygen - 锁TTL为5分钟,覆盖整个keygen过程 - 无法获取锁时跳过请求 🤖 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
5b731498de
commit
10b25e222e
|
|
@ -1078,6 +1078,11 @@ export class UserApplicationService {
|
|||
*
|
||||
* 用户可以通过此方法手动触发钱包生成重试
|
||||
* 幂等操作:重新发布 UserAccountCreatedEvent
|
||||
*
|
||||
* 保护机制:
|
||||
* - 钱包已完成时,直接返回不重试
|
||||
* - 钱包正在生成中(generating/deriving/pending),直接返回不重试
|
||||
* - 只有失败或无状态时才触发重试
|
||||
*/
|
||||
async retryWalletGeneration(userId: string): Promise<void> {
|
||||
this.logger.log(
|
||||
|
|
@ -1106,7 +1111,37 @@ export class UserApplicationService {
|
|||
return; // 钱包已完成,无需重试
|
||||
}
|
||||
|
||||
// 3. 重新触发钱包生成流程
|
||||
// 3. 检查 Redis 中的 keygen 状态,防止重复触发
|
||||
const redisKey = `keygen:status:${userId}`;
|
||||
const currentStatusData = await this.redisService.get(redisKey);
|
||||
|
||||
if (currentStatusData) {
|
||||
try {
|
||||
const parsed = JSON.parse(currentStatusData);
|
||||
const status = parsed.status;
|
||||
|
||||
// 如果状态是 pending/generating/deriving,说明正在生成中,不触发重试
|
||||
if (status === 'pending' || status === 'generating' || status === 'deriving') {
|
||||
this.logger.log(
|
||||
`[WALLET-RETRY] Wallet generation in progress (status=${status}) for user: ${userId}, skip retry`,
|
||||
);
|
||||
return; // 正在生成中,无需重试
|
||||
}
|
||||
|
||||
// 如果状态是 completed,但数据库没有完整地址,说明是过期状态,继续重试
|
||||
// 如果状态是 failed,允许重试
|
||||
this.logger.log(
|
||||
`[WALLET-RETRY] Current status: ${status}, will trigger retry`,
|
||||
);
|
||||
} catch (e) {
|
||||
// JSON 解析失败,继续执行重试
|
||||
this.logger.warn(
|
||||
`[WALLET-RETRY] Failed to parse status data for user: ${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 重新触发钱包生成流程
|
||||
const event = account.createWalletGenerationEvent();
|
||||
await this.eventPublisher.publish(event);
|
||||
|
||||
|
|
@ -1114,7 +1149,7 @@ export class UserApplicationService {
|
|||
`[WALLET-RETRY] Wallet generation retry triggered for user: ${userId}`,
|
||||
);
|
||||
|
||||
// 4. 更新 Redis 状态为 pending(等待重新生成)
|
||||
// 5. 更新 Redis 状态为 pending(等待重新生成)
|
||||
const statusData = {
|
||||
status: 'pending',
|
||||
userId,
|
||||
|
|
@ -1122,7 +1157,7 @@ export class UserApplicationService {
|
|||
};
|
||||
|
||||
await this.redisService.set(
|
||||
`keygen:status:${userId}`,
|
||||
redisKey,
|
||||
JSON.stringify(statusData),
|
||||
60 * 60 * 24, // 24 小时
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@
|
|||
*
|
||||
* Handles keygen requests from identity-service via Kafka.
|
||||
* Processes the keygen and publishes completion/failure events.
|
||||
*
|
||||
* Idempotency Protection:
|
||||
* - Uses distributed lock to prevent duplicate keygen for the same user
|
||||
* - Lock is held for the entire keygen process (up to 5 minutes)
|
||||
* - If lock cannot be acquired, the request is skipped (another instance is processing)
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
|
|
@ -19,6 +24,10 @@ import { KeygenStartedEvent } from '../../domain/events/keygen-started.event';
|
|||
import { KeygenCompletedEvent } from '../../domain/events/keygen-completed.event';
|
||||
import { SessionFailedEvent } from '../../domain/events/session-failed.event';
|
||||
import { SessionType } from '../../domain/enums';
|
||||
import { DistributedLockService } from '../../infrastructure/redis/lock/distributed-lock.service';
|
||||
|
||||
// Lock TTL for keygen process: 5 minutes (keygen may take a while)
|
||||
const KEYGEN_LOCK_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
@Injectable()
|
||||
export class KeygenRequestedHandler implements OnModuleInit {
|
||||
|
|
@ -30,6 +39,7 @@ export class KeygenRequestedHandler implements OnModuleInit {
|
|||
private readonly mpcCoordinator: MPCCoordinatorService,
|
||||
private readonly backupClient: BackupClientService,
|
||||
private readonly blockchainClient: BlockchainClientService,
|
||||
private readonly distributedLock: DistributedLockService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
|
|
@ -52,6 +62,24 @@ export class KeygenRequestedHandler implements OnModuleInit {
|
|||
this.logger.log(`[HANDLE] userId=${userId}, username=${username}`);
|
||||
this.logger.log(`[HANDLE] threshold=${threshold}, totalParties=${totalParties}, requireDelegate=${requireDelegate}`);
|
||||
|
||||
// Idempotency check: Try to acquire lock for this user's keygen
|
||||
// If lock is already held, another keygen is in progress for this user
|
||||
const lockKey = `keygen:user:${userId}`;
|
||||
const lock = await this.distributedLock.acquire(lockKey, {
|
||||
ttl: KEYGEN_LOCK_TTL_MS,
|
||||
retryCount: 1, // Only try once, don't wait
|
||||
retryDelay: 0,
|
||||
});
|
||||
|
||||
if (!lock) {
|
||||
this.logger.warn(
|
||||
`[HANDLE] Keygen already in progress for user: ${userId}, skipping duplicate request`,
|
||||
);
|
||||
return; // Skip this request, another instance is handling it
|
||||
}
|
||||
|
||||
this.logger.log(`[HANDLE] Acquired keygen lock for user: ${userId}`);
|
||||
|
||||
try {
|
||||
// Step 1: Create keygen session via mpc-system
|
||||
const createResult = await this.mpcCoordinator.createKeygenSession({
|
||||
|
|
@ -170,6 +198,10 @@ export class KeygenRequestedHandler implements OnModuleInit {
|
|||
} catch (publishError) {
|
||||
this.logger.error('Failed to publish failure event', publishError);
|
||||
}
|
||||
} finally {
|
||||
// Always release the lock when done (success or failure)
|
||||
await lock.release();
|
||||
this.logger.log(`[HANDLE] Released keygen lock for user: ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,9 +132,9 @@ class WalletStatusNotifier extends StateNotifier<WalletStatusState> {
|
|||
);
|
||||
|
||||
stopPolling();
|
||||
} else {
|
||||
// 非 ready 状态,自动触发重试生成(API 是幂等的)
|
||||
debugPrint('[WalletStatusProvider] Wallet not ready, triggering retry...');
|
||||
} else if (walletInfo.isFailed) {
|
||||
// 只有失败状态才触发重试,生成中状态不重试
|
||||
debugPrint('[WalletStatusProvider] Wallet generation failed, triggering retry...');
|
||||
|
||||
try {
|
||||
await _accountService.retryWalletGeneration();
|
||||
|
|
@ -144,6 +144,14 @@ class WalletStatusNotifier extends StateNotifier<WalletStatusState> {
|
|||
// 继续轮询,下次再试
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
status: WalletGenerationStatus.generating,
|
||||
lastChecked: DateTime.now(),
|
||||
);
|
||||
} else {
|
||||
// 生成中状态,只更新状态,继续轮询等待完成
|
||||
debugPrint('[WalletStatusProvider] Wallet is generating, waiting...');
|
||||
|
||||
state = state.copyWith(
|
||||
status: WalletGenerationStatus.generating,
|
||||
lastChecked: DateTime.now(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue