docs: update blockchain-service guide with address derivation responsibilities

- Add public key → address derivation as primary responsibility
- Add AddressDerivationAdapter for EVM/Cosmos address derivation
- Add WalletAddressCreated event definition
- Add MPC event consumption (mpc.KeygenCompleted)
- Add MpcKeygenCompletedHandler for processing keygen events
- Add section 17: MPC integration event flow with diagrams
- Update document version to 1.2.0

🤖 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 20:02:50 -08:00
parent 23043d5d79
commit 6150617c14
1 changed files with 410 additions and 5 deletions

View File

@ -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<void> {
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 事件*