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

16 KiB
Raw Blame History

MPC Party Service 开发指南

环境准备

系统要求

  • Node.js: >= 18.x
  • npm: >= 9.x
  • MySQL: >= 8.0
  • Redis: >= 6.x
  • Docker: >= 20.x (可选,用于容器化开发)

安装步骤

1. 克隆项目

cd backend/services/mpc-service

2. 安装依赖

npm install

3. 配置环境变量

复制环境变量模板并修改:

cp .env.example .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. 初始化数据库

# 生成 Prisma Client
npx prisma generate

# 运行数据库迁移
npx prisma migrate dev

# (可选) 查看数据库
npx prisma studio

5. 启动开发服务器

npm run start:dev

服务将在 http://localhost:3006 启动。


开发工作流

项目脚本

{
  "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. 创建功能分支

    git checkout -b feature/your-feature-name
    
  2. 编写代码

    • 遵循项目代码规范
    • 编写相应的测试
  3. 运行测试

    npm run test
    npm run lint
    
  4. 提交代码

    git add .
    git commit -m "feat: add your feature description"
    
  5. 创建 Pull Request


代码规范

目录命名

  • 使用 kebab-caseparty-share, mpc-party
  • 模块目录包含 index.ts 导出

文件命名

  • 实体:party-share.entity.ts
  • 服务:share-encryption.domain-service.ts
  • 控制器:mpc-party.controller.ts
  • DTOparticipate-keygen.dto.ts
  • 测试:party-share.entity.spec.ts

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;
}

错误处理

// 领域层错误
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');
}

日志规范

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

// 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

// src/application/commands/new-feature/new-feature.command.ts
export class NewFeatureCommand {
  constructor(
    public readonly param: string,
  ) {}
}

Step 3: 创建 Handler

// 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

// 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 端点

// 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: 编写测试

// tests/unit/application/new-feature.handler.spec.ts
describe('NewFeatureHandler', () => {
  // ... 单元测试
});

// tests/integration/new-feature.spec.ts
describe('NewFeature (Integration)', () => {
  // ... 集成测试
});

2. 添加新的领域实体

Step 1: 定义实体

// 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: 定义值对象

// 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: 定义仓储接口

// 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: 实现仓储

// 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. 添加新的外部服务集成

// 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:

{
  "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. 日志调试

// 启用详细日志
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. 数据库调试

# 查看 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. 数据库优化

// 使用索引
@@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. 缓存优化

// 使用 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. 异步处理

// 使用异步端点处理长时间操作
@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 未生成

npx prisma generate

问题: 数据库连接失败

检查 DATABASE_URL 环境变量格式:

mysql://user:password@host:port/database

2. 测试相关

问题: 测试超时

// 增加超时时间
jest.setTimeout(60000);

问题: 模拟不生效

确保模拟在正确的位置:

beforeEach(() => {
  jest.clearAllMocks();
  mockService.method.mockResolvedValue(expectedValue);
});

3. TypeScript 相关

问题: 路径别名不工作

检查 tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

问题: 类型错误

# 重新生成类型
npm run build
npx prisma generate

4. 运行时相关

问题: 端口已被占用

# 查找占用端口的进程
lsof -i :3006
# 或在 Windows 上
netstat -ano | findstr :3006

# 终止进程
kill <PID>

问题: 内存不足

# 增加 Node.js 内存限制
NODE_OPTIONS=--max-old-space-size=4096 npm run start:dev

代码审查清单

在提交 PR 前,请检查:

  • 代码遵循项目规范
  • 所有测试通过
  • 新功能有相应的测试
  • 更新了相关文档
  • 没有硬编码的敏感信息
  • 日志级别适当
  • 错误处理完善
  • 没有引入新的安全漏洞