diff --git a/backend/services/blockchain-service/DEVELOPMENT_GUIDE.md b/backend/services/blockchain-service/DEVELOPMENT_GUIDE.md index dbbdfd1b..401f57ec 100644 --- a/backend/services/blockchain-service/DEVELOPMENT_GUIDE.md +++ b/backend/services/blockchain-service/DEVELOPMENT_GUIDE.md @@ -6,12 +6,46 @@ blockchain-service 是 RWA 榴莲女皇平台的区块链基础设施服务,负责: +- **公钥→地址派生**:从 MPC 公钥派生多链钱包地址 (EVM/Cosmos) - **链上事件监听**:监听 ERC20 Transfer 事件,检测用户充值 - **充值入账触发**:检测到充值后通知 wallet-service 入账 - **余额查询**:查询链上 USDT/原生代币余额 - **交易广播**:提交签名后的交易到链上 - **地址管理**:管理平台充值地址池 +### 1.4 与其他服务的关系 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ identity-service│ │ mpc-service │ │blockchain-service│ +├─────────────────┤ ├─────────────────┤ ├─────────────────┤ +│ - 用户账户 │ │ - 密钥分片生成 │ │ - 公钥→地址派生 │ +│ - 设备绑定 │ │ - 签名协调 │ │ - 充值检测 │ +│ - KYC验证 │ │ - 分片存储 │ │ - 交易广播 │ +│ - 身份认证 │ │ - 阈值策略 │ │ - 余额查询 │ +└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ + │ │ │ + │ MpcKeygenRequested │ KeygenCompleted │ + │──────────────────────>│ (with publicKey) │ + │ │──────────────────────>│ + │ │ │ derive address + │ WalletAddressCreated │ + │<──────────────────────────────────────────────│ + │ (存储 userId ↔ walletAddress 关联) │ + │ │ │ + │ wallet-service │ + │ │ │ + │ WalletAddressCreated │ + │<──────────────────────────────────────────────│ + │ (存储钱包地址、管理余额) │ +``` + +**职责边界原则**: +- **identity-service**:只关心用户身份,不处理区块链技术细节 +- **mpc-service**:只关心 MPC 协议,生成公钥后发布事件 +- **blockchain-service**:封装所有区块链技术,包括地址派生、链交互 +- **wallet-service**:管理用户钱包的业务逻辑,不直接与链交互 + ### 1.2 技术栈 | 组件 | 技术选型 | @@ -119,7 +153,8 @@ blockchain-service/ │ │ │ ├── deposit-detection.service.ts │ │ │ ├── balance-query.service.ts │ │ │ ├── transaction-broadcast.service.ts -│ │ │ └── address-registry.service.ts +│ │ │ ├── address-registry.service.ts +│ │ │ └── address-derivation.service.ts # 公钥→地址派生 │ │ ├── commands/ │ │ │ ├── register-address/ │ │ │ │ ├── register-address.command.ts @@ -155,6 +190,7 @@ blockchain-service/ │ │ │ ├── deposit-detected.event.ts │ │ │ ├── deposit-confirmed.event.ts │ │ │ ├── transaction-broadcasted.event.ts +│ │ │ ├── wallet-address-created.event.ts # 地址派生完成事件 │ │ │ └── index.ts │ │ ├── repositories/ │ │ │ ├── deposit-transaction.repository.interface.ts @@ -182,6 +218,7 @@ blockchain-service/ │ │ │ ├── event-listener.service.ts │ │ │ ├── block-scanner.service.ts │ │ │ ├── transaction-sender.service.ts +│ │ │ ├── address-derivation.adapter.ts # 地址派生适配器 │ │ │ └── blockchain.module.ts │ │ ├── persistence/ │ │ │ ├── prisma/ @@ -1137,6 +1174,51 @@ export { TxHash } from './tx-hash.vo'; ### 4.4 领域事件 +### 4.6 WalletAddressCreated 事件 + +```typescript +// src/domain/events/wallet-address-created.event.ts + +import { DomainEvent } from './domain-event.base'; + +export interface WalletAddressCreatedPayload { + userId: string; + username: string; + publicKey: string; // 压缩公钥 (33 bytes hex) + addresses: Array<{ + chainType: string; // BSC, KAVA, DST + address: string; // 派生的地址 + addressType: string; // EVM 或 COSMOS + }>; + mpcSessionId: string; // MPC 会话 ID + delegateShare?: { + partyId: string; + partyIndex: number; + encryptedShare: string; + }; +} + +export class WalletAddressCreatedEvent extends DomainEvent { + constructor(public readonly payload: WalletAddressCreatedPayload) { + super(); + } + + get eventType(): string { + return 'WalletAddressCreated'; + } + + get aggregateId(): string { + return this.payload.userId; + } + + get aggregateType(): string { + return 'WalletAddress'; + } +} +``` + +--- + ```typescript // src/domain/events/deposit-detected.event.ts @@ -1484,7 +1566,127 @@ export class EventListenerService implements OnModuleInit, OnModuleDestroy { } ``` -### 5.3 区块扫描器(补扫服务) +### 5.3 地址派生适配器 + +```typescript +// src/infrastructure/blockchain/address-derivation.adapter.ts + +import { Injectable, Logger } from '@nestjs/common'; +import { createHash } from 'crypto'; +import { bech32 } from 'bech32'; +import { ethers } from 'ethers'; +import * as secp256k1 from 'secp256k1'; + +export enum AddressType { + EVM = 'EVM', + COSMOS = 'COSMOS', +} + +export interface DerivedAddress { + chainType: string; + address: string; + addressType: AddressType; +} + +/** + * 地址派生适配器 + * + * 从压缩的 secp256k1 公钥派生多链钱包地址 + * - EVM 链 (BSC, KAVA EVM): 使用 keccak256(uncompressed_pubkey[1:])[-20:] + * - Cosmos 链 (KAVA, DST): 使用 bech32(ripemd160(sha256(compressed_pubkey))) + */ +@Injectable() +export class AddressDerivationAdapter { + private readonly logger = new Logger(AddressDerivationAdapter.name); + + /** + * 从压缩公钥派生所有支持链的地址 + * @param compressedPubKeyHex 33 字节压缩公钥 (hex 格式,不含 0x 前缀) + */ + deriveAllAddresses(compressedPubKeyHex: string): DerivedAddress[] { + const pubKeyBytes = Buffer.from(compressedPubKeyHex, 'hex'); + + if (pubKeyBytes.length !== 33) { + throw new Error(`Invalid compressed public key length: ${pubKeyBytes.length}, expected 33`); + } + + const addresses: DerivedAddress[] = []; + + // EVM 地址 (BSC, KAVA EVM) + const evmAddress = this.deriveEvmAddress(pubKeyBytes); + addresses.push({ chainType: 'BSC', address: evmAddress, addressType: AddressType.EVM }); + addresses.push({ chainType: 'KAVA', address: evmAddress, addressType: AddressType.EVM }); + + // Cosmos 地址 (KAVA Native 使用 kava 前缀,DST 使用 dst 前缀) + const kavaCosmosAddress = this.deriveCosmosAddress(pubKeyBytes, 'kava'); + const dstAddress = this.deriveCosmosAddress(pubKeyBytes, 'dst'); + addresses.push({ chainType: 'KAVA_COSMOS', address: kavaCosmosAddress, addressType: AddressType.COSMOS }); + addresses.push({ chainType: 'DST', address: dstAddress, addressType: AddressType.COSMOS }); + + this.logger.log(`Derived ${addresses.length} addresses from public key`); + return addresses; + } + + /** + * 派生 EVM 地址 + * 1. 解压公钥 (33 bytes → 65 bytes) + * 2. 取非压缩公钥去掉前缀 (64 bytes) + * 3. keccak256 哈希后取后 20 bytes + */ + private deriveEvmAddress(compressedPubKey: Buffer): string { + // 使用 secp256k1 解压公钥 + const uncompressedPubKey = Buffer.from( + secp256k1.publicKeyConvert(compressedPubKey, false) + ); + + // 去掉 0x04 前缀,取 64 bytes + const pubKeyWithoutPrefix = uncompressedPubKey.slice(1); + + // keccak256 哈希后取后 20 bytes + const hash = ethers.keccak256(pubKeyWithoutPrefix); + const address = '0x' + hash.slice(-40); + + return ethers.getAddress(address); // 返回 checksum 格式 + } + + /** + * 派生 Cosmos 地址 + * 1. SHA256(compressed_pubkey) + * 2. RIPEMD160(sha256_result) + * 3. bech32 编码 + */ + private deriveCosmosAddress(compressedPubKey: Buffer, prefix: string): string { + // SHA256 + const sha256Hash = createHash('sha256').update(compressedPubKey).digest(); + + // RIPEMD160 + const ripemd160Hash = createHash('ripemd160').update(sha256Hash).digest(); + + // Bech32 编码 + const words = bech32.toWords(ripemd160Hash); + return bech32.encode(prefix, words); + } + + /** + * 验证地址格式 + */ + validateAddress(chainType: string, address: string): boolean { + switch (chainType) { + case 'BSC': + case 'KAVA': + return ethers.isAddress(address); + case 'KAVA_COSMOS': + return /^kava1[a-z0-9]{38}$/.test(address); + case 'DST': + return /^dst1[a-z0-9]{38}$/.test(address); + default: + return false; + } + } +} +``` + +### 5.4 区块扫描器(补扫服务) ```typescript // src/infrastructure/blockchain/block-scanner.service.ts @@ -1986,16 +2188,126 @@ export class BalanceQueryService { // src/infrastructure/kafka/event-publisher.service.ts export const BLOCKCHAIN_TOPICS = { + // 地址派生完成事件 (发给 identity-service, wallet-service) + WALLET_ADDRESS_CREATED: 'blockchain.WalletAddressCreated', + + // 充值相关事件 DEPOSIT_DETECTED: 'blockchain.DepositDetected', DEPOSIT_CONFIRMED: 'blockchain.DepositConfirmed', + + // 交易相关事件 TRANSACTION_BROADCASTED: 'blockchain.TransactionBroadcasted', TRANSACTION_CONFIRMED: 'blockchain.TransactionConfirmed', + + // 区块扫描事件 BLOCK_SCANNED: 'blockchain.BlockScanned', } as const; ``` ### 7.2 消费的事件 +```typescript +// src/infrastructure/kafka/mpc-event-consumer.service.ts + +// 消费 MPC 服务发布的事件 +export const MPC_CONSUME_TOPICS = { + KEYGEN_COMPLETED: 'mpc.KeygenCompleted', + SESSION_FAILED: 'mpc.SessionFailed', +} as const; + +export interface KeygenCompletedPayload { + sessionId: string; + partyId: string; + publicKey: string; // 33 bytes 压缩公钥 (hex) + shareId: string; + threshold: string; // "2-of-3" + extraPayload?: { + userId: string; + username: string; + delegateShare?: { + partyId: string; + partyIndex: number; + encryptedShare: string; + }; + serverParties?: string[]; + }; +} +``` + +### 7.3 MPC Keygen 完成事件处理 + +```typescript +// src/application/event-handlers/mpc-keygen-completed.handler.ts + +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { MpcEventConsumerService, KeygenCompletedPayload } from '@/infrastructure/kafka/mpc-event-consumer.service'; +import { AddressDerivationAdapter } from '@/infrastructure/blockchain/address-derivation.adapter'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { WalletAddressCreatedEvent } from '@/domain/events'; + +@Injectable() +export class MpcKeygenCompletedHandler implements OnModuleInit { + private readonly logger = new Logger(MpcKeygenCompletedHandler.name); + + constructor( + private readonly mpcEventConsumer: MpcEventConsumerService, + private readonly addressDerivation: AddressDerivationAdapter, + private readonly eventPublisher: EventPublisherService, + ) {} + + async onModuleInit() { + this.mpcEventConsumer.onKeygenCompleted(this.handleKeygenCompleted.bind(this)); + this.logger.log('Registered KeygenCompleted event handler'); + } + + private async handleKeygenCompleted(payload: KeygenCompletedPayload): Promise { + const userId = payload.extraPayload?.userId; + const username = payload.extraPayload?.username; + const publicKey = payload.publicKey; + + if (!userId || !username || !publicKey) { + this.logger.error('Missing required fields in KeygenCompleted payload'); + return; + } + + this.logger.log(`[DERIVE] Processing keygen completion for userId=${userId}`); + this.logger.log(`[DERIVE] PublicKey: ${publicKey.substring(0, 20)}...`); + + try { + // 从公钥派生所有链的地址 + const addresses = this.addressDerivation.deriveAllAddresses(publicKey); + + this.logger.log(`[DERIVE] Derived ${addresses.length} addresses:`); + addresses.forEach(addr => { + this.logger.log(`[DERIVE] ${addr.chainType}: ${addr.address}`); + }); + + // 发布 WalletAddressCreated 事件 + const event = new WalletAddressCreatedEvent({ + userId, + username, + publicKey, + addresses: addresses.map(a => ({ + chainType: a.chainType, + address: a.address, + addressType: a.addressType, + })), + mpcSessionId: payload.sessionId, + delegateShare: payload.extraPayload?.delegateShare, + }); + + await this.eventPublisher.publish(event); + this.logger.log(`[DERIVE] Published WalletAddressCreated event for userId=${userId}`); + + } catch (error) { + this.logger.error(`[DERIVE] Failed to derive addresses: ${error.message}`, error.stack); + } + } +} +``` + +### 7.4 消费的 Identity/Wallet 事件 + ```typescript // src/infrastructure/kafka/event-consumer.controller.ts @@ -2380,6 +2692,99 @@ constructor( --- -*文档版本: 1.1.0* -*最后更新: 2025-12-03* -*变更说明: 添加模块化设计、聚合根基类、完整值对象定义、架构兼容性说明* +## 17. MPC 集成事件流 + +### 17.1 完整事件流程图 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ MPC 钱包创建事件流 │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ [1] 用户创建账户 │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ identity-service│ │ +│ │ │ ────────────────────────────────────────┐ │ +│ │ 发布事件: │ │ │ +│ │ mpc.KeygenRequested │ │ +│ └─────────────────┘ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ [2] MPC 密钥生成 │ mpc-service │ │ +│ │ │ │ +│ ┌────────────────────────────────────────────│ 消费事件: │ │ +│ │ │ mpc.KeygenRequested │ +│ │ │ │ │ +│ │ [3] 生成公钥 │ 发布事件: │ │ +│ │ │ │ mpc.KeygenStarted │ +│ │ │ │ mpc.KeygenCompleted │ +│ │ ▼ └─────────────────┘ │ +│ │ publicKey: 33 bytes │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ blockchain-service │ │ +│ │ │ │ +│ │ [4] 消费: mpc.KeygenCompleted │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ AddressDerivationAdapter │ │ │ +│ │ │ │ │ │ +│ │ │ publicKey ──┬──> deriveEvmAddress() ──> 0x1234... │ │ │ +│ │ │ │ (BSC, KAVA) │ │ │ +│ │ │ │ │ │ │ +│ │ │ └──> deriveCosmosAddress() ──> kava1... │ │ │ +│ │ │ dst1... │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ [5] 发布: blockchain.WalletAddressCreated │ │ +│ │ │ │ │ +│ └───────┼──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ├──────────────────────────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ identity-service│ │ wallet-service │ │ +│ │ │ │ │ │ +│ │ [6] 存储关联: │ │ [7] 存储钱包: │ │ +│ │ userId ↔ address│ │ 地址、余额管理 │ │ +│ └─────────────────┘ └─────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### 17.2 事件 Topic 汇总 + +| Topic | 发布者 | 消费者 | 描述 | +|-------|--------|--------|------| +| `mpc.KeygenRequested` | identity-service | mpc-service | 请求生成 MPC 密钥 | +| `mpc.KeygenStarted` | mpc-service | identity-service | 密钥生成开始 | +| `mpc.KeygenCompleted` | mpc-service | blockchain-service | 密钥生成完成,包含公钥 | +| `mpc.SessionFailed` | mpc-service | identity-service, blockchain-service | 会话失败 | +| `blockchain.WalletAddressCreated` | blockchain-service | identity-service, wallet-service | 地址派生完成 | + +### 17.3 关键设计决策 + +1. **职责分离**: + - mpc-service 只负责 MPC 协议,不了解区块链地址格式 + - blockchain-service 封装所有区块链技术细节 + - identity-service 不直接处理公钥→地址转换 + +2. **多链扩展性**: + - 新增链类型只需在 `AddressDerivationAdapter` 添加派生逻辑 + - 事件格式保持不变,下游服务无需修改 + +3. **事件溯源**: + - 所有状态变更通过事件传递 + - 支持事件重放和故障恢复 + +--- + +*文档版本: 1.2.0* +*最后更新: 2025-12-06* +*变更说明: 添加公钥→地址派生职责、MPC 事件集成、AddressDerivationAdapter、WalletAddressCreated 事件*