diff --git a/backend/services/blockchain-service/prisma/schema.prisma b/backend/services/blockchain-service/prisma/schema.prisma index a30b3304..1eb905b4 100644 --- a/backend/services/blockchain-service/prisma/schema.prisma +++ b/backend/services/blockchain-service/prisma/schema.prisma @@ -147,6 +147,37 @@ model TransactionRequest { @@map("transaction_requests") } +// ============================================ +// 账户恢复助记词 +// 与账户序列号关联,用于账户恢复验证 +// ============================================ +model RecoveryMnemonic { + id BigInt @id @default(autoincrement()) + accountSequence Int @map("account_sequence") // 8位账户序列号 + publicKey String @map("public_key") @db.VarChar(130) // 关联的钱包公钥 + + // 助记词存储 (加密) + encryptedMnemonic String @map("encrypted_mnemonic") @db.Text // AES加密的助记词 + mnemonicHash String @map("mnemonic_hash") @db.VarChar(64) // SHA256哈希(用于验证) + + // 状态管理 + status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, REPLACED + isBackedUp Boolean @default(false) @map("is_backed_up") // 用户是否已备份 + + // 挂失/更换相关 + revokedAt DateTime? @map("revoked_at") + revokedReason String? @map("revoked_reason") @db.VarChar(200) + replacedById BigInt? @map("replaced_by_id") // 被哪个新助记词替代 + + createdAt DateTime @default(now()) @map("created_at") + + @@unique([accountSequence, status], name: "uk_account_active_mnemonic") // 一个账户只有一个ACTIVE助记词 + @@index([accountSequence], name: "idx_recovery_account") + @@index([publicKey], name: "idx_recovery_public_key") + @@index([status], name: "idx_recovery_status") + @@map("recovery_mnemonics") +} + // ============================================ // 区块链事件日志 (Append-Only 审计) // ============================================ diff --git a/backend/services/blockchain-service/src/api/controllers/internal.controller.ts b/backend/services/blockchain-service/src/api/controllers/internal.controller.ts index 4f2bf144..f22b4f5d 100644 --- a/backend/services/blockchain-service/src/api/controllers/internal.controller.ts +++ b/backend/services/blockchain-service/src/api/controllers/internal.controller.ts @@ -1,7 +1,8 @@ import { Controller, Post, Body, Get, Param } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AddressDerivationService } from '@/application/services/address-derivation.service'; -import { MnemonicDerivationAdapter, RecoveryMnemonicAdapter } from '@/infrastructure/blockchain'; +import { MnemonicVerificationService } from '@/application/services/mnemonic-verification.service'; +import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain'; import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto } from '../dto/request'; import { DeriveAddressResponseDto } from '../dto/response'; @@ -14,8 +15,8 @@ import { DeriveAddressResponseDto } from '../dto/response'; export class InternalController { constructor( private readonly addressDerivationService: AddressDerivationService, + private readonly mnemonicVerification: MnemonicVerificationService, private readonly mnemonicDerivation: MnemonicDerivationAdapter, - private readonly recoveryMnemonic: RecoveryMnemonicAdapter, ) {} @Post('derive-address') @@ -77,10 +78,13 @@ export class InternalController { } @Post('verify-mnemonic-hash') - @ApiOperation({ summary: '验证助记词哈希是否匹配' }) + @ApiOperation({ summary: '通过账户序列号验证助记词' }) @ApiResponse({ status: 200, description: '验证结果' }) async verifyMnemonicHash(@Body() dto: VerifyMnemonicHashDto) { - const result = this.recoveryMnemonic.verifyMnemonic(dto.mnemonic, dto.expectedHash); + const result = await this.mnemonicVerification.verifyMnemonicByAccount({ + accountSequence: dto.accountSequence, + mnemonic: dto.mnemonic, + }); return { valid: result.valid, message: result.message, diff --git a/backend/services/blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts b/backend/services/blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts index 37cf2f4c..3ed3e6ac 100644 --- a/backend/services/blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts +++ b/backend/services/blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts @@ -1,18 +1,18 @@ -import { IsString } from 'class-validator'; +import { IsString, IsInt } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class VerifyMnemonicHashDto { + @ApiProperty({ + description: '账户序列号 (8位数字)', + example: 10000001, + }) + @IsInt() + accountSequence: number; + @ApiProperty({ description: '助记词 (12个单词,空格分隔)', example: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', }) @IsString() mnemonic: string; - - @ApiProperty({ - description: '期望的助记词哈希', - example: 'a1b2c3d4e5f6...', - }) - @IsString() - expectedHash: string; } diff --git a/backend/services/blockchain-service/src/application/application.module.ts b/backend/services/blockchain-service/src/application/application.module.ts index ed307e08..c980b96f 100644 --- a/backend/services/blockchain-service/src/application/application.module.ts +++ b/backend/services/blockchain-service/src/application/application.module.ts @@ -1,7 +1,12 @@ import { Module } from '@nestjs/common'; import { InfrastructureModule } from '@/infrastructure/infrastructure.module'; import { DomainModule } from '@/domain/domain.module'; -import { AddressDerivationService, DepositDetectionService, BalanceQueryService } from './services'; +import { + AddressDerivationService, + DepositDetectionService, + BalanceQueryService, + MnemonicVerificationService, +} from './services'; import { MpcKeygenCompletedHandler } from './event-handlers'; @Module({ @@ -11,6 +16,7 @@ import { MpcKeygenCompletedHandler } from './event-handlers'; AddressDerivationService, DepositDetectionService, BalanceQueryService, + MnemonicVerificationService, // 事件处理器 MpcKeygenCompletedHandler, @@ -19,6 +25,7 @@ import { MpcKeygenCompletedHandler } from './event-handlers'; AddressDerivationService, DepositDetectionService, BalanceQueryService, + MnemonicVerificationService, MpcKeygenCompletedHandler, ], }) diff --git a/backend/services/blockchain-service/src/application/services/index.ts b/backend/services/blockchain-service/src/application/services/index.ts index a2954ad4..843861a1 100644 --- a/backend/services/blockchain-service/src/application/services/index.ts +++ b/backend/services/blockchain-service/src/application/services/index.ts @@ -1,3 +1,4 @@ export * from './address-derivation.service'; export * from './deposit-detection.service'; export * from './balance-query.service'; +export * from './mnemonic-verification.service'; diff --git a/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts b/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts new file mode 100644 index 00000000..717b85c8 --- /dev/null +++ b/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts @@ -0,0 +1,103 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; +import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter'; + +export interface VerifyMnemonicByAccountParams { + accountSequence: number; + mnemonic: string; +} + +export interface VerifyMnemonicResult { + valid: boolean; + message?: string; +} + +/** + * 助记词验证服务 + * 通过账户序列号查询存储的哈希并验证助记词 + */ +@Injectable() +export class MnemonicVerificationService { + private readonly logger = new Logger(MnemonicVerificationService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly recoveryMnemonic: RecoveryMnemonicAdapter, + ) {} + + /** + * 验证助记词是否匹配指定账户 + */ + async verifyMnemonicByAccount(params: VerifyMnemonicByAccountParams): Promise { + const { accountSequence, mnemonic } = params; + this.logger.log(`Verifying mnemonic for account ${accountSequence}`); + + // 1. 查询账户的 ACTIVE 助记词记录 + const recoveryRecord = await this.prisma.recoveryMnemonic.findFirst({ + where: { + accountSequence, + status: 'ACTIVE', + }, + }); + + if (!recoveryRecord) { + this.logger.warn(`No active recovery mnemonic found for account ${accountSequence}`); + return { + valid: false, + message: 'Account has no recovery mnemonic configured', + }; + } + + // 2. 使用 RecoveryMnemonicAdapter 验证哈希 + const result = this.recoveryMnemonic.verifyMnemonic(mnemonic, recoveryRecord.mnemonicHash); + + if (result.valid) { + this.logger.log(`Mnemonic verified successfully for account ${accountSequence}`); + } else { + this.logger.warn(`Mnemonic verification failed for account ${accountSequence}: ${result.message}`); + } + + return result; + } + + /** + * 保存助记词记录(创建账户时调用) + */ + async saveRecoveryMnemonic(params: { + accountSequence: number; + publicKey: string; + encryptedMnemonic: string; + mnemonicHash: string; + }): Promise { + this.logger.log(`Saving recovery mnemonic for account ${params.accountSequence}`); + + await this.prisma.recoveryMnemonic.create({ + data: { + accountSequence: params.accountSequence, + publicKey: params.publicKey, + encryptedMnemonic: params.encryptedMnemonic, + mnemonicHash: params.mnemonicHash, + status: 'ACTIVE', + isBackedUp: false, + }, + }); + + this.logger.log(`Recovery mnemonic saved for account ${params.accountSequence}`); + } + + /** + * 标记助记词已备份 + */ + async markAsBackedUp(accountSequence: number): Promise { + await this.prisma.recoveryMnemonic.updateMany({ + where: { + accountSequence, + status: 'ACTIVE', + }, + data: { + isBackedUp: true, + }, + }); + this.logger.log(`Recovery mnemonic marked as backed up for account ${accountSequence}`); + } +} diff --git a/backend/services/identity-service/src/application/commands/recover-by-mnemonic/recover-by-mnemonic.handler.ts b/backend/services/identity-service/src/application/commands/recover-by-mnemonic/recover-by-mnemonic.handler.ts index 233be383..40bc4bfb 100644 --- a/backend/services/identity-service/src/application/commands/recover-by-mnemonic/recover-by-mnemonic.handler.ts +++ b/backend/services/identity-service/src/application/commands/recover-by-mnemonic/recover-by-mnemonic.handler.ts @@ -4,7 +4,6 @@ import { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/reposit import { AccountSequence } from '@/domain/value-objects'; import { TokenService } from '@/application/services/token.service'; import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; -import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; import { BlockchainClientService } from '@/infrastructure/external/blockchain/blockchain-client.service'; import { ApplicationError } from '@/shared/exceptions/domain.exception'; import { RecoverAccountResult } from '../index'; @@ -16,7 +15,6 @@ export class RecoverByMnemonicHandler { constructor( @Inject(USER_ACCOUNT_REPOSITORY) private readonly userRepository: UserAccountRepository, - private readonly prisma: PrismaService, private readonly tokenService: TokenService, private readonly eventPublisher: EventPublisherService, private readonly blockchainClient: BlockchainClientService, @@ -28,29 +26,16 @@ export class RecoverByMnemonicHandler { if (!account) throw new ApplicationError('账户序列号不存在'); if (!account.isActive) throw new ApplicationError('账户已冻结或注销'); - // 从 recovery_mnemonics 表获取存储的助记词哈希 - const recoveryMnemonic = await this.prisma.recoveryMnemonic.findFirst({ - where: { - userId: account.userId.value, - status: 'ACTIVE', - }, - }); - - if (!recoveryMnemonic) { - this.logger.error(`No recovery mnemonic found for account ${command.accountSequence}`); - throw new ApplicationError('账户未设置恢复助记词'); - } - - // 调用 blockchain-service 验证助记词哈希 - this.logger.log(`Verifying mnemonic hash for account ${command.accountSequence}`); - const verifyResult = await this.blockchainClient.verifyMnemonicHash({ + // 调用 blockchain-service 验证助记词(blockchain-service 内部查询哈希并验证) + this.logger.log(`Verifying mnemonic for account ${command.accountSequence}`); + const verifyResult = await this.blockchainClient.verifyMnemonicByAccount({ + accountSequence: command.accountSequence, mnemonic: command.mnemonic, - expectedHash: recoveryMnemonic.mnemonicHash, }); if (!verifyResult.valid) { - this.logger.warn(`Mnemonic hash mismatch for account ${command.accountSequence}`); - throw new ApplicationError('助记词错误'); + this.logger.warn(`Mnemonic verification failed for account ${command.accountSequence}: ${verifyResult.message}`); + throw new ApplicationError(verifyResult.message || '助记词错误'); } this.logger.log(`Mnemonic verified successfully for account ${command.accountSequence}`); diff --git a/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts b/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts index 8b096e14..78118ebf 100644 --- a/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts +++ b/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts @@ -25,9 +25,9 @@ export interface VerifyMnemonicResult { mismatchedAddresses: string[]; } -export interface VerifyMnemonicHashParams { +export interface VerifyMnemonicByAccountParams { + accountSequence: number; mnemonic: string; - expectedHash: string; } export interface VerifyMnemonicHashResult { @@ -87,18 +87,18 @@ export class BlockchainClientService { } /** - * 验证助记词哈希是否匹配(用于账户恢复) + * 通过账户序列号验证助记词(用于账户恢复) */ - async verifyMnemonicHash(params: VerifyMnemonicHashParams): Promise { - this.logger.log(`Verifying mnemonic hash`); + async verifyMnemonicByAccount(params: VerifyMnemonicByAccountParams): Promise { + this.logger.log(`Verifying mnemonic for account ${params.accountSequence}`); try { const response = await firstValueFrom( this.httpService.post( `${this.blockchainServiceUrl}/internal/verify-mnemonic-hash`, { + accountSequence: params.accountSequence, mnemonic: params.mnemonic, - expectedHash: params.expectedHash, }, { headers: { 'Content-Type': 'application/json' }, @@ -107,10 +107,10 @@ export class BlockchainClientService { ), ); - this.logger.log(`Mnemonic hash verification result: valid=${response.data.valid}`); + this.logger.log(`Mnemonic verification result: valid=${response.data.valid}`); return response.data; } catch (error) { - this.logger.error('Failed to verify mnemonic hash', error); + this.logger.error('Failed to verify mnemonic', error); throw error; } }