rwadurian/backend/services/mpc-service/docs/DEVELOPMENT.md

770 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MPC Party Service 开发指南
## 环境准备
### 系统要求
- **Node.js**: >= 18.x
- **npm**: >= 9.x
- **MySQL**: >= 8.0
- **Redis**: >= 6.x
- **Docker**: >= 20.x (可选,用于容器化开发)
### 安装步骤
#### 1. 克隆项目
```bash
cd backend/services/mpc-service
```
#### 2. 安装依赖
```bash
npm install
```
#### 3. 配置环境变量
复制环境变量模板并修改:
```bash
cp .env.example .env
```
编辑 `.env` 文件:
```env
# 应用配置
NODE_ENV=development
APP_PORT=3006
API_PREFIX=api/v1
# 数据库配置
DATABASE_URL="mysql://user:password@localhost:3306/mpc_service"
# Redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=
# JWT 配置
JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters
JWT_ACCESS_EXPIRES_IN=2h
JWT_REFRESH_EXPIRES_IN=7d
# MPC 配置
MPC_PARTY_ID=party-server-1
MPC_COORDINATOR_URL=http://localhost:50051
MPC_MESSAGE_ROUTER_WS_URL=ws://localhost:50052
# 加密配置
SHARE_MASTER_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
# Kafka 配置 (可选)
KAFKA_ENABLED=false
KAFKA_BROKERS=localhost:9092
KAFKA_CLIENT_ID=mpc-party-service
```
#### 4. 初始化数据库
```bash
# 生成 Prisma Client
npx prisma generate
# 运行数据库迁移
npx prisma migrate dev
# (可选) 查看数据库
npx prisma studio
```
#### 5. 启动开发服务器
```bash
npm run start:dev
```
服务将在 `http://localhost:3006` 启动。
---
## 开发工作流
### 项目脚本
```json
{
"scripts": {
// 开发
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
// 构建
"build": "nest build",
"prebuild": "rimraf dist",
// 测试
"test": "jest --config ./tests/jest.config.js",
"test:unit": "jest --config ./tests/jest-unit.config.js",
"test:integration": "jest --config ./tests/jest-integration.config.js",
"test:e2e": "jest --config ./tests/jest-e2e.config.js",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
// 代码质量
"lint": "eslint \"{src,tests}/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
// 数据库
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio"
}
}
```
### 开发流程
1. **创建功能分支**
```bash
git checkout -b feature/your-feature-name
```
2. **编写代码**
- 遵循项目代码规范
- 编写相应的测试
3. **运行测试**
```bash
npm run test
npm run lint
```
4. **提交代码**
```bash
git add .
git commit -m "feat: add your feature description"
```
5. **创建 Pull Request**
---
## 代码规范
### 目录命名
- 使用 kebab-case`party-share`, `mpc-party`
- 模块目录包含 `index.ts` 导出
### 文件命名
- 实体:`party-share.entity.ts`
- 服务:`share-encryption.domain-service.ts`
- 控制器:`mpc-party.controller.ts`
- DTO`participate-keygen.dto.ts`
- 测试:`party-share.entity.spec.ts`
### TypeScript 规范
```typescript
// 使用接口定义数据结构
interface CreatePartyShareProps {
partyId: PartyId;
sessionId: SessionId;
shareType: PartyShareType;
shareData: ShareData;
publicKey: PublicKey;
threshold: Threshold;
}
// 使用枚举定义常量
enum PartyShareStatus {
ACTIVE = 'active',
ROTATED = 'rotated',
REVOKED = 'revoked',
}
// 使用类型别名简化复杂类型
type ShareFilters = {
partyId?: string;
status?: PartyShareStatus;
shareType?: PartyShareType;
};
// 使用 readonly 保护不可变属性
class PartyShare {
private readonly _id: ShareId;
private readonly _createdAt: Date;
}
// 使用 private 前缀
private readonly _domainEvents: DomainEvent[] = [];
// 使用 getter 暴露属性
get id(): ShareId {
return this._id;
}
```
### 错误处理
```typescript
// 领域层错误
export class DomainError extends Error {
constructor(message: string) {
super(message);
this.name = 'DomainError';
}
}
// 应用层错误
export class ApplicationError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly details?: any,
) {
super(message);
this.name = 'ApplicationError';
}
}
// 使用错误
if (!share) {
throw new ApplicationError('Share not found', 'SHARE_NOT_FOUND');
}
```
### 日志规范
```typescript
import { Logger } from '@nestjs/common';
@Injectable()
export class MyService {
private readonly logger = new Logger(MyService.name);
async doSomething() {
this.logger.log('Starting operation');
this.logger.debug(`Processing with params: ${JSON.stringify(params)}`);
this.logger.warn('Potential issue detected');
this.logger.error('Operation failed', error.stack);
}
}
```
---
## 添加新功能指南
### 1. 添加新的 API 端点
#### Step 1: 创建 DTO
```typescript
// src/api/dto/request/new-feature.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class NewFeatureDto {
@ApiProperty({ description: 'Feature parameter' })
@IsString()
@IsNotEmpty()
param: string;
}
```
#### Step 2: 创建 Command/Query
```typescript
// src/application/commands/new-feature/new-feature.command.ts
export class NewFeatureCommand {
constructor(
public readonly param: string,
) {}
}
```
#### Step 3: 创建 Handler
```typescript
// src/application/commands/new-feature/new-feature.handler.ts
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { Injectable } from '@nestjs/common';
import { NewFeatureCommand } from './new-feature.command';
@Injectable()
@CommandHandler(NewFeatureCommand)
export class NewFeatureHandler implements ICommandHandler<NewFeatureCommand> {
async execute(command: NewFeatureCommand): Promise<any> {
// 实现业务逻辑
}
}
```
#### Step 4: 更新 Application Service
```typescript
// src/application/services/mpc-party-application.service.ts
async newFeature(params: NewFeatureParams): Promise<ResultDto> {
const command = new NewFeatureCommand(params.param);
return this.commandBus.execute(command);
}
```
#### Step 5: 添加 Controller 端点
```typescript
// src/api/controllers/mpc-party.controller.ts
@Post('new-feature')
@ApiOperation({ summary: '新功能' })
@ApiResponse({ status: 200, description: 'Success' })
async newFeature(@Body() dto: NewFeatureDto) {
return this.mpcPartyService.newFeature(dto);
}
```
#### Step 6: 编写测试
```typescript
// tests/unit/application/new-feature.handler.spec.ts
describe('NewFeatureHandler', () => {
// ... 单元测试
});
// tests/integration/new-feature.spec.ts
describe('NewFeature (Integration)', () => {
// ... 集成测试
});
```
### 2. 添加新的领域实体
#### Step 1: 定义实体
```typescript
// src/domain/entities/new-entity.entity.ts
import { DomainEvent } from '../events';
export class NewEntity {
private readonly _id: EntityId;
private readonly _domainEvents: DomainEvent[] = [];
private constructor(props: NewEntityProps) {
this._id = props.id;
}
static create(props: CreateNewEntityProps): NewEntity {
const entity = new NewEntity({
id: EntityId.generate(),
...props,
});
entity.addDomainEvent(new NewEntityCreatedEvent(entity.id.value));
return entity;
}
static reconstruct(props: NewEntityProps): NewEntity {
return new NewEntity(props);
}
// Getters
get id(): EntityId {
return this._id;
}
get domainEvents(): DomainEvent[] {
return [...this._domainEvents];
}
// Business methods
doSomething(): void {
// 业务逻辑
this.addDomainEvent(new SomethingDoneEvent(this._id.value));
}
// Private methods
private addDomainEvent(event: DomainEvent): void {
this._domainEvents.push(event);
}
clearDomainEvents(): void {
this._domainEvents.length = 0;
}
}
```
#### Step 2: 定义值对象
```typescript
// src/domain/value-objects/entity-id.ts
export class EntityId {
private constructor(private readonly _value: string) {}
static create(value: string): EntityId {
if (!this.isValid(value)) {
throw new DomainError('Invalid EntityId format');
}
return new EntityId(value);
}
static generate(): EntityId {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 15);
return new EntityId(`entity_${timestamp}_${random}`);
}
private static isValid(value: string): boolean {
return /^entity_\d+_[a-z0-9]+$/.test(value);
}
get value(): string {
return this._value;
}
equals(other: EntityId): boolean {
return this._value === other._value;
}
}
```
#### Step 3: 定义仓储接口
```typescript
// src/domain/repositories/new-entity.repository.interface.ts
import { NewEntity } from '../entities/new-entity.entity';
import { EntityId } from '../value-objects';
export const NEW_ENTITY_REPOSITORY = Symbol('NEW_ENTITY_REPOSITORY');
export interface NewEntityRepository {
save(entity: NewEntity): Promise<void>;
findById(id: EntityId): Promise<NewEntity | null>;
findMany(filters?: any): Promise<NewEntity[]>;
delete(id: EntityId): Promise<void>;
}
```
#### Step 4: 实现仓储
```typescript
// src/infrastructure/persistence/repositories/new-entity.repository.impl.ts
@Injectable()
export class NewEntityRepositoryImpl implements NewEntityRepository {
constructor(
private readonly prisma: PrismaService,
private readonly mapper: NewEntityMapper,
) {}
async save(entity: NewEntity): Promise<void> {
const data = this.mapper.toPersistence(entity);
await this.prisma.newEntity.create({ data });
}
async findById(id: EntityId): Promise<NewEntity | null> {
const record = await this.prisma.newEntity.findUnique({
where: { id: id.value },
});
return record ? this.mapper.toDomain(record) : null;
}
}
```
### 3. 添加新的外部服务集成
```typescript
// src/infrastructure/external/new-service/new-service.client.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class NewServiceClient {
private readonly logger = new Logger(NewServiceClient.name);
private readonly baseUrl: string;
constructor(private readonly configService: ConfigService) {
this.baseUrl = this.configService.get<string>('NEW_SERVICE_URL');
}
async callExternalService(params: any): Promise<any> {
this.logger.log(`Calling external service: ${JSON.stringify(params)}`);
try {
// 实现外部服务调用
const response = await fetch(`${this.baseUrl}/endpoint`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`External service error: ${response.status}`);
}
return response.json();
} catch (error) {
this.logger.error(`External service failed: ${error.message}`, error.stack);
throw error;
}
}
}
```
---
## 调试技巧
### 1. VSCode 调试配置
`.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug MPC Service",
"runtimeArgs": [
"-r",
"ts-node/register",
"-r",
"tsconfig-paths/register"
],
"args": ["${workspaceFolder}/src/main.ts"],
"cwd": "${workspaceFolder}",
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "Debug Tests",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"--config",
"${workspaceFolder}/tests/jest.config.js",
"--runInBand",
"--testPathPattern",
"${file}"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
```
### 2. 日志调试
```typescript
// 启用详细日志
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
});
// 在代码中添加调试日志
this.logger.debug(`Debug info: ${JSON.stringify(data)}`);
this.logger.verbose(`Verbose details: ${details}`);
```
### 3. 数据库调试
```bash
# 查看 Prisma 生成的 SQL
DEBUG=prisma:query npm run start:dev
# 使用 Prisma Studio 查看数据
npx prisma studio
```
### 4. API 调试
- 使用 Swagger UI: `http://localhost:3006/api/docs`
- 使用 Postman 或 Insomnia
- 查看请求/响应日志
---
## 性能优化
### 1. 数据库优化
```typescript
// 使用索引
@@index([partyId, status])
@@index([publicKey])
// 使用 select 限制字段
const share = await this.prisma.partyShare.findUnique({
where: { id },
select: {
id: true,
status: true,
publicKey: true,
// 不选择大字段如 shareData
},
});
// 使用分页
const shares = await this.prisma.partyShare.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
});
```
### 2. 缓存优化
```typescript
// 使用 Redis 缓存
@Injectable()
export class CachedShareService {
constructor(private readonly redis: Redis) {}
async getShareInfo(shareId: string): Promise<ShareInfo> {
const cacheKey = `share:${shareId}`;
// 尝试从缓存获取
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 从数据库获取
const share = await this.repository.findById(shareId);
// 存入缓存
await this.redis.setex(cacheKey, 3600, JSON.stringify(share));
return share;
}
}
```
### 3. 异步处理
```typescript
// 使用异步端点处理长时间操作
@Post('keygen/participate')
@HttpCode(HttpStatus.ACCEPTED)
async participateInKeygen(@Body() dto: ParticipateKeygenDto) {
// 立即返回,后台异步处理
this.mpcPartyService.participateInKeygen(dto).catch(error => {
this.logger.error(`Keygen failed: ${error.message}`);
});
return {
message: 'Keygen participation started',
sessionId: dto.sessionId,
};
}
```
---
## 常见问题解决
### 1. Prisma 相关
**问题**: Prisma Client 未生成
```bash
npx prisma generate
```
**问题**: 数据库连接失败
检查 `DATABASE_URL` 环境变量格式:
```
mysql://user:password@host:port/database
```
### 2. 测试相关
**问题**: 测试超时
```javascript
// 增加超时时间
jest.setTimeout(60000);
```
**问题**: 模拟不生效
确保模拟在正确的位置:
```typescript
beforeEach(() => {
jest.clearAllMocks();
mockService.method.mockResolvedValue(expectedValue);
});
```
### 3. TypeScript 相关
**问题**: 路径别名不工作
检查 `tsconfig.json`:
```json
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}
```
**问题**: 类型错误
```bash
# 重新生成类型
npm run build
npx prisma generate
```
### 4. 运行时相关
**问题**: 端口已被占用
```bash
# 查找占用端口的进程
lsof -i :3006
# 或在 Windows 上
netstat -ano | findstr :3006
# 终止进程
kill <PID>
```
**问题**: 内存不足
```bash
# 增加 Node.js 内存限制
NODE_OPTIONS=--max-old-space-size=4096 npm run start:dev
```
---
## 代码审查清单
在提交 PR 前,请检查:
- [ ] 代码遵循项目规范
- [ ] 所有测试通过
- [ ] 新功能有相应的测试
- [ ] 更新了相关文档
- [ ] 没有硬编码的敏感信息
- [ ] 日志级别适当
- [ ] 错误处理完善
- [ ] 没有引入新的安全漏洞