770 lines
16 KiB
Markdown
770 lines
16 KiB
Markdown
# 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 前,请检查:
|
||
|
||
- [ ] 代码遵循项目规范
|
||
- [ ] 所有测试通过
|
||
- [ ] 新功能有相应的测试
|
||
- [ ] 更新了相关文档
|
||
- [ ] 没有硬编码的敏感信息
|
||
- [ ] 日志级别适当
|
||
- [ ] 错误处理完善
|
||
- [ ] 没有引入新的安全漏洞
|