feat: add MPC coordinator service and keygen/signing API
- Add MPCController with keygen and sign endpoints - Add MPCCoordinatorService to coordinate with mpc-system - Add MpcWallet, MpcShare, MpcSession entities for data storage - Update identity-service mpc-client to call mpc-service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9fc41cfa53
commit
d652f1d7a4
|
|
@ -3,14 +3,13 @@
|
||||||
*
|
*
|
||||||
* 与 mpc-service (NestJS) 通信的客户端服务
|
* 与 mpc-service (NestJS) 通信的客户端服务
|
||||||
*
|
*
|
||||||
* 调用路径:
|
* 调用路径 (DDD 分领域):
|
||||||
* identity-service → mpc-service (NestJS) → mpc-system (Go)
|
* identity-service (身份域) → mpc-service (MPC域/NestJS) → mpc-system (Go/TSS实现)
|
||||||
*
|
*
|
||||||
* 这种架构的优点:
|
* 业务流程:
|
||||||
* 1. 符合 DDD 边界上下文分离原则
|
* 1. identity-service 调用 mpc-service 的 keygen API
|
||||||
* 2. mpc-service 可以封装 MPC 业务逻辑、重试、熔断等
|
* 2. mpc-service 协调 mpc-system 完成 TSS keygen
|
||||||
* 3. 多个服务可共享 mpc-service(如 transaction-service)
|
* 3. 返回公钥和 delegate share (用户分片) 给 identity-service
|
||||||
* 4. MPC API Key 只需配置在 mpc-service
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
@ -21,45 +20,34 @@ import { createHash, randomUUID } from 'crypto';
|
||||||
|
|
||||||
export interface KeygenRequest {
|
export interface KeygenRequest {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
threshold: number; // t in t-of-n (默认 2)
|
username: string; // 用户名 (自动递增ID)
|
||||||
totalParties: number; // n in t-of-n (默认 3)
|
threshold: number; // t in t-of-n (默认 1, 即 2-of-3)
|
||||||
parties: PartyInfo[];
|
totalParties: number; // n in t-of-n (默认 3)
|
||||||
}
|
requireDelegate: boolean; // 是否需要 delegate party
|
||||||
|
|
||||||
export interface PartyInfo {
|
|
||||||
partyId: string;
|
|
||||||
partyIndex: number;
|
|
||||||
partyType: 'SERVER' | 'CLIENT' | 'BACKUP';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeygenResult {
|
export interface KeygenResult {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
publicKey: string; // 压缩格式公钥 (33 bytes hex)
|
publicKey: string; // 压缩格式公钥 (33 bytes hex)
|
||||||
publicKeyUncompressed: string; // 非压缩格式公钥 (65 bytes hex)
|
delegateShare: DelegateShare; // delegate share (用户分片)
|
||||||
partyShares: PartyShareResult[];
|
serverParties: string[]; // 服务器 party IDs
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PartyShareResult {
|
export interface DelegateShare {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
partyIndex: number;
|
partyIndex: number;
|
||||||
encryptedShareData: string; // 加密的分片数据
|
encryptedShare: string; // 加密的分片数据 (hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SigningRequest {
|
export interface SigningRequest {
|
||||||
sessionId: string;
|
username: string;
|
||||||
publicKey: string;
|
messageHash: string; // 32 bytes hex
|
||||||
messageHash: string; // 32 bytes hex
|
userShare?: string; // 如果账户有 delegate share,需要传入用户分片
|
||||||
signerParties: PartyInfo[];
|
|
||||||
threshold: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SigningResult {
|
export interface SigningResult {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
signature: {
|
signature: string; // 64 bytes hex (R + S)
|
||||||
r: string; // 32 bytes hex
|
|
||||||
s: string; // 32 bytes hex
|
|
||||||
v: number; // recovery id (0 or 1)
|
|
||||||
};
|
|
||||||
messageHash: string;
|
messageHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,12 +56,14 @@ export class MpcClientService {
|
||||||
private readonly logger = new Logger(MpcClientService.name);
|
private readonly logger = new Logger(MpcClientService.name);
|
||||||
private readonly mpcServiceUrl: string; // mpc-service (NestJS) URL
|
private readonly mpcServiceUrl: string; // mpc-service (NestJS) URL
|
||||||
private readonly mpcMode: string;
|
private readonly mpcMode: string;
|
||||||
|
private readonly pollIntervalMs = 2000;
|
||||||
|
private readonly maxPollAttempts = 150; // 5 minutes max
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
// 连接 mpc-service (NestJS) 而不是直接连接 mpc-system (Go)
|
// 连接 mpc-service (NestJS)
|
||||||
this.mpcServiceUrl = this.configService.get<string>('MPC_SERVICE_URL', 'http://localhost:3001');
|
this.mpcServiceUrl = this.configService.get<string>('MPC_SERVICE_URL', 'http://localhost:3001');
|
||||||
this.mpcMode = this.configService.get<string>('MPC_MODE', 'local');
|
this.mpcMode = this.configService.get<string>('MPC_MODE', 'local');
|
||||||
}
|
}
|
||||||
|
|
@ -86,17 +76,17 @@ export class MpcClientService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 2-of-3 MPC 密钥生成
|
* 执行 2-of-3 MPC 密钥生成 (带 delegate party)
|
||||||
*
|
*
|
||||||
* 三个参与方:
|
* 三个参与方:
|
||||||
* - Party 0 (SERVER): 服务端持有,用于常规签名
|
* - Party 0 (SERVER): server-party-1,服务端持有
|
||||||
* - Party 1 (CLIENT): 用户设备持有,返回给客户端
|
* - Party 1 (SERVER): server-party-2,服务端持有
|
||||||
* - Party 2 (BACKUP): 备份服务持有,用于恢复
|
* - Party 2 (DELEGATE): delegate-party,代理生成后返回给用户设备
|
||||||
*
|
*
|
||||||
* 调用路径: identity-service → mpc-service → mpc-system (Go)
|
* 调用路径: identity-service → mpc-service → mpc-system
|
||||||
*/
|
*/
|
||||||
async executeKeygen(request: KeygenRequest): Promise<KeygenResult> {
|
async executeKeygen(request: KeygenRequest): Promise<KeygenResult> {
|
||||||
this.logger.log(`Starting MPC keygen: session=${request.sessionId}, t=${request.threshold}, n=${request.totalParties}`);
|
this.logger.log(`Starting MPC keygen: username=${request.username}, t=${request.threshold}, n=${request.totalParties}`);
|
||||||
|
|
||||||
// 开发模式使用本地模拟
|
// 开发模式使用本地模拟
|
||||||
if (this.mpcMode === 'local') {
|
if (this.mpcMode === 'local') {
|
||||||
|
|
@ -104,49 +94,120 @@ export class MpcClientService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用 mpc-service 的 keygen 同步接口
|
// Step 1: 调用 mpc-service 创建 keygen session
|
||||||
const response = await firstValueFrom(
|
const createResponse = await firstValueFrom(
|
||||||
this.httpService.post<KeygenResult>(
|
this.httpService.post<{
|
||||||
`${this.mpcServiceUrl}/api/v1/mpc-party/keygen/participate-sync`,
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
}>(
|
||||||
|
`${this.mpcServiceUrl}/api/v1/mpc/keygen`,
|
||||||
{
|
{
|
||||||
sessionId: request.sessionId,
|
username: request.username,
|
||||||
partyId: 'server-party',
|
thresholdN: request.totalParties,
|
||||||
joinToken: this.generateJoinToken(request.sessionId),
|
thresholdT: request.threshold,
|
||||||
shareType: 'wallet', // PartyShareType.WALLET
|
requireDelegate: request.requireDelegate,
|
||||||
userId: request.parties.find(p => p.partyType === 'CLIENT')?.partyId,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: 300000, // 5分钟超时
|
timeout: 30000,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`MPC keygen completed: session=${request.sessionId}, publicKey=${response.data.publicKey}`);
|
const sessionId = createResponse.data.sessionId;
|
||||||
return response.data;
|
this.logger.log(`Keygen session created: ${sessionId}`);
|
||||||
|
|
||||||
|
// Step 2: 轮询 session 状态直到完成
|
||||||
|
const sessionResult = await this.pollKeygenStatus(sessionId);
|
||||||
|
|
||||||
|
if (sessionResult.status !== 'completed') {
|
||||||
|
throw new Error(`Keygen session failed with status: ${sessionResult.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Keygen completed: publicKey=${sessionResult.publicKey}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
publicKey: sessionResult.publicKey,
|
||||||
|
delegateShare: sessionResult.delegateShare,
|
||||||
|
serverParties: sessionResult.serverParties,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`MPC keygen failed: session=${request.sessionId}`, error);
|
this.logger.error(`MPC keygen failed: username=${request.username}`, error);
|
||||||
throw new Error(`MPC keygen failed: ${error.message}`);
|
throw new Error(`MPC keygen failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成加入会话的 token
|
* 轮询 keygen session 状态
|
||||||
*/
|
*/
|
||||||
private generateJoinToken(sessionId: string): string {
|
private async pollKeygenStatus(sessionId: string): Promise<{
|
||||||
return createHash('sha256').update(`${sessionId}-${Date.now()}`).digest('hex');
|
status: string;
|
||||||
|
publicKey: string;
|
||||||
|
delegateShare: DelegateShare;
|
||||||
|
serverParties: string[];
|
||||||
|
}> {
|
||||||
|
for (let i = 0; i < this.maxPollAttempts; i++) {
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get<{
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
publicKey?: string;
|
||||||
|
delegateShare?: {
|
||||||
|
partyId: string;
|
||||||
|
partyIndex: number;
|
||||||
|
encryptedShare: string;
|
||||||
|
};
|
||||||
|
serverParties?: string[];
|
||||||
|
}>(
|
||||||
|
`${this.mpcServiceUrl}/api/v1/mpc/keygen/${sessionId}/status`,
|
||||||
|
{
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
this.logger.debug(`Session ${sessionId} status: ${data.status}`);
|
||||||
|
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
return {
|
||||||
|
status: 'completed',
|
||||||
|
publicKey: data.publicKey || '',
|
||||||
|
delegateShare: data.delegateShare || { partyId: '', partyIndex: -1, encryptedShare: '' },
|
||||||
|
serverParties: data.serverParties || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'failed' || data.status === 'expired') {
|
||||||
|
return {
|
||||||
|
status: data.status,
|
||||||
|
publicKey: '',
|
||||||
|
delegateShare: { partyId: '', partyIndex: -1, encryptedShare: '' },
|
||||||
|
serverParties: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sleep(this.pollIntervalMs);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Error polling session status: ${error.message}`);
|
||||||
|
await this.sleep(this.pollIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Session ${sessionId} timed out after ${this.maxPollAttempts * this.pollIntervalMs}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 MPC 签名
|
* 执行 MPC 签名
|
||||||
*
|
*
|
||||||
* 至少需要 threshold 个参与方来完成签名
|
* 调用路径: identity-service → mpc-service → mpc-system
|
||||||
* 调用路径: identity-service → mpc-service → mpc-system (Go)
|
|
||||||
*/
|
*/
|
||||||
async executeSigning(request: SigningRequest): Promise<SigningResult> {
|
async executeSigning(request: SigningRequest): Promise<SigningResult> {
|
||||||
this.logger.log(`Starting MPC signing: session=${request.sessionId}, messageHash=${request.messageHash}`);
|
this.logger.log(`Starting MPC signing: username=${request.username}, messageHash=${request.messageHash}`);
|
||||||
|
|
||||||
// 开发模式使用本地模拟
|
// 开发模式使用本地模拟
|
||||||
if (this.mpcMode === 'local') {
|
if (this.mpcMode === 'local') {
|
||||||
|
|
@ -154,66 +215,127 @@ export class MpcClientService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用 mpc-service 的 signing 同步接口
|
// 调用 mpc-service 创建签名 session
|
||||||
const response = await firstValueFrom(
|
const createResponse = await firstValueFrom(
|
||||||
this.httpService.post<SigningResult>(
|
this.httpService.post<{
|
||||||
`${this.mpcServiceUrl}/api/v1/mpc-party/signing/participate-sync`,
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
}>(
|
||||||
|
`${this.mpcServiceUrl}/api/v1/mpc/sign`,
|
||||||
{
|
{
|
||||||
sessionId: request.sessionId,
|
username: request.username,
|
||||||
partyId: 'server-party',
|
|
||||||
joinToken: this.generateJoinToken(request.sessionId),
|
|
||||||
messageHash: request.messageHash,
|
messageHash: request.messageHash,
|
||||||
publicKey: request.publicKey,
|
userShare: request.userShare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: 120000, // 2分钟超时
|
timeout: 30000,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`MPC signing completed: session=${request.sessionId}`);
|
const sessionId = createResponse.data.sessionId;
|
||||||
return response.data;
|
this.logger.log(`Signing session created: ${sessionId}`);
|
||||||
|
|
||||||
|
// 轮询签名状态
|
||||||
|
const signResult = await this.pollSigningStatus(sessionId);
|
||||||
|
|
||||||
|
if (signResult.status !== 'completed') {
|
||||||
|
throw new Error(`Signing session failed with status: ${signResult.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
signature: signResult.signature,
|
||||||
|
messageHash: request.messageHash,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`MPC signing failed: session=${request.sessionId}`, error);
|
this.logger.error(`MPC signing failed: username=${request.username}`, error);
|
||||||
throw new Error(`MPC signing failed: ${error.message}`);
|
throw new Error(`MPC signing failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询签名 session 状态
|
||||||
|
*/
|
||||||
|
private async pollSigningStatus(sessionId: string): Promise<{
|
||||||
|
status: string;
|
||||||
|
signature: string;
|
||||||
|
}> {
|
||||||
|
for (let i = 0; i < this.maxPollAttempts; i++) {
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get<{
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
signature?: string;
|
||||||
|
}>(
|
||||||
|
`${this.mpcServiceUrl}/api/v1/mpc/sign/${sessionId}/status`,
|
||||||
|
{
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
return {
|
||||||
|
status: 'completed',
|
||||||
|
signature: data.signature || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'failed' || data.status === 'expired') {
|
||||||
|
return {
|
||||||
|
status: data.status,
|
||||||
|
signature: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sleep(this.pollIntervalMs);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Error polling signing status: ${error.message}`);
|
||||||
|
await this.sleep(this.pollIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Signing session ${sessionId} timed out`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行本地模拟的 MPC keygen (用于开发测试)
|
* 执行本地模拟的 MPC keygen (用于开发测试)
|
||||||
*
|
|
||||||
* 注意: 这是一个简化的本地实现,仅用于测试
|
|
||||||
* 生产环境应该调用真正的 MPC 系统
|
|
||||||
*/
|
*/
|
||||||
async executeLocalKeygen(request: KeygenRequest): Promise<KeygenResult> {
|
async executeLocalKeygen(request: KeygenRequest): Promise<KeygenResult> {
|
||||||
this.logger.log(`Starting LOCAL MPC keygen (test mode): session=${request.sessionId}`);
|
this.logger.log(`Starting LOCAL MPC keygen (test mode): username=${request.username}`);
|
||||||
|
|
||||||
// 使用 ethers 生成密钥对 (简化版本,非真正的 MPC)
|
|
||||||
const { ethers } = await import('ethers');
|
const { ethers } = await import('ethers');
|
||||||
|
|
||||||
// 生成随机私钥
|
// 生成随机私钥
|
||||||
const wallet = ethers.Wallet.createRandom();
|
const wallet = ethers.Wallet.createRandom();
|
||||||
const privateKey = wallet.privateKey;
|
|
||||||
const publicKey = wallet.publicKey;
|
const publicKey = wallet.publicKey;
|
||||||
|
|
||||||
// 压缩公钥 (33 bytes)
|
// 压缩公钥 (33 bytes)
|
||||||
const compressedPubKey = ethers.SigningKey.computePublicKey(publicKey, true);
|
const compressedPubKey = ethers.SigningKey.computePublicKey(publicKey, true);
|
||||||
|
|
||||||
// 模拟分片数据 (实际上是完整私钥的加密版本,仅用于测试)
|
// 模拟 delegate share
|
||||||
const partyShares: PartyShareResult[] = request.parties.map((party) => ({
|
const delegateShare: DelegateShare = {
|
||||||
partyId: party.partyId,
|
partyId: 'delegate-party',
|
||||||
partyIndex: party.partyIndex,
|
partyIndex: 2,
|
||||||
encryptedShareData: this.encryptShareData(privateKey, party.partyId),
|
encryptedShare: this.encryptShareData(wallet.privateKey, request.username),
|
||||||
}));
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: request.sessionId,
|
sessionId: this.generateSessionId(),
|
||||||
publicKey: compressedPubKey.slice(2), // 去掉 0x 前缀
|
publicKey: compressedPubKey.slice(2), // 去掉 0x 前缀
|
||||||
publicKeyUncompressed: publicKey.slice(2),
|
delegateShare,
|
||||||
partyShares,
|
serverParties: ['server-party-1', 'server-party-2'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,32 +343,23 @@ export class MpcClientService {
|
||||||
* 执行本地模拟的 MPC 签名 (用于开发测试)
|
* 执行本地模拟的 MPC 签名 (用于开发测试)
|
||||||
*/
|
*/
|
||||||
async executeLocalSigning(request: SigningRequest): Promise<SigningResult> {
|
async executeLocalSigning(request: SigningRequest): Promise<SigningResult> {
|
||||||
this.logger.log(`Starting LOCAL MPC signing (test mode): session=${request.sessionId}`);
|
this.logger.log(`Starting LOCAL MPC signing (test mode): username=${request.username}`);
|
||||||
|
|
||||||
const { ethers } = await import('ethers');
|
const { ethers } = await import('ethers');
|
||||||
|
|
||||||
// 从第一个参与方获取分片数据并解密 (测试模式)
|
|
||||||
// 实际生产环境需要多方协作签名
|
|
||||||
const signerParty = request.signerParties[0];
|
|
||||||
|
|
||||||
// 这里我们无法真正恢复私钥,所以使用一个测试签名
|
|
||||||
// 生产环境应该使用真正的 MPC 协议
|
|
||||||
const messageHashBytes = Buffer.from(request.messageHash, 'hex');
|
const messageHashBytes = Buffer.from(request.messageHash, 'hex');
|
||||||
|
|
||||||
// 创建一个临时钱包用于签名 (仅测试)
|
// 创建一个临时钱包用于签名 (仅测试)
|
||||||
const testWallet = new ethers.Wallet(
|
const testWallet = new ethers.Wallet(
|
||||||
'0x' + createHash('sha256').update(request.publicKey).digest('hex'),
|
'0x' + createHash('sha256').update(request.username).digest('hex'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const signature = testWallet.signingKey.sign(messageHashBytes);
|
const signature = testWallet.signingKey.sign(messageHashBytes);
|
||||||
|
|
||||||
|
// 返回 R + S 格式 (64 bytes hex)
|
||||||
return {
|
return {
|
||||||
sessionId: request.sessionId,
|
sessionId: this.generateSessionId(),
|
||||||
signature: {
|
signature: signature.r.slice(2) + signature.s.slice(2),
|
||||||
r: signature.r.slice(2),
|
|
||||||
s: signature.s.slice(2),
|
|
||||||
v: signature.v - 27, // 转换为 0 或 1
|
|
||||||
},
|
|
||||||
messageHash: request.messageHash,
|
messageHash: request.messageHash,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +368,6 @@ export class MpcClientService {
|
||||||
* 加密分片数据 (简化版本)
|
* 加密分片数据 (简化版本)
|
||||||
*/
|
*/
|
||||||
private encryptShareData(data: string, key: string): string {
|
private encryptShareData(data: string, key: string): string {
|
||||||
// 简化的加密,实际应该使用 AES-GCM
|
|
||||||
const cipher = createHash('sha256').update(key).digest('hex');
|
const cipher = createHash('sha256').update(key).digest('hex');
|
||||||
return Buffer.from(data).toString('base64') + '.' + cipher.slice(0, 16);
|
return Buffer.from(data).toString('base64') + '.' + cipher.slice(0, 16);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ import { Module } from '@nestjs/common';
|
||||||
import { ApplicationModule } from '../application/application.module';
|
import { ApplicationModule } from '../application/application.module';
|
||||||
import { MPCPartyController } from './controllers/mpc-party.controller';
|
import { MPCPartyController } from './controllers/mpc-party.controller';
|
||||||
import { HealthController } from './controllers/health.controller';
|
import { HealthController } from './controllers/health.controller';
|
||||||
|
import { MPCController } from './controllers/mpc.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ApplicationModule],
|
imports: [ApplicationModule],
|
||||||
controllers: [
|
controllers: [
|
||||||
MPCPartyController,
|
MPCPartyController,
|
||||||
HealthController,
|
HealthController,
|
||||||
|
MPCController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ApiModule {}
|
export class ApiModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './mpc-party.controller';
|
export * from './mpc-party.controller';
|
||||||
export * from './health.controller';
|
export * from './health.controller';
|
||||||
|
export * from './mpc.controller';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
/**
|
||||||
|
* MPC Controller
|
||||||
|
*
|
||||||
|
* REST API endpoints for MPC keygen and signing operations.
|
||||||
|
* Called by identity-service to coordinate MPC operations with mpc-system.
|
||||||
|
*
|
||||||
|
* 调用路径 (DDD 分领域):
|
||||||
|
* identity-service (身份域) → mpc-service (MPC域) → mpc-system (Go/TSS实现)
|
||||||
|
*
|
||||||
|
* 数据存储:
|
||||||
|
* - mpc-service: 存储公钥、share(分片)
|
||||||
|
* - identity-service: 存储收款地址(BSC、KAVA、DST)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Get,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { Public } from '../../shared/decorators/public.decorator';
|
||||||
|
import { MPCCoordinatorService } from '../../application/services/mpc-coordinator.service';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Request DTOs
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export class CreateKeygenDto {
|
||||||
|
username: string;
|
||||||
|
thresholdN: number;
|
||||||
|
thresholdT: number;
|
||||||
|
requireDelegate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateSigningDto {
|
||||||
|
username: string;
|
||||||
|
messageHash: string;
|
||||||
|
userShare?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Response DTOs
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export class KeygenSessionResponseDto {
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KeygenStatusResponseDto {
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
publicKey?: string;
|
||||||
|
delegateShare?: {
|
||||||
|
partyId: string;
|
||||||
|
partyIndex: number;
|
||||||
|
encryptedShare: string;
|
||||||
|
};
|
||||||
|
serverParties?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SigningSessionResponseDto {
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SigningStatusResponseDto {
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
signature?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('MPC')
|
||||||
|
@Controller('mpc')
|
||||||
|
export class MPCController {
|
||||||
|
private readonly logger = new Logger(MPCController.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly mpcCoordinatorService: MPCCoordinatorService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create keygen session
|
||||||
|
* Called by identity-service to start MPC keygen
|
||||||
|
*/
|
||||||
|
@Public()
|
||||||
|
@Post('keygen')
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '创建 MPC Keygen 会话',
|
||||||
|
description: '创建一个新的 MPC keygen 会话,协调 mpc-system 完成密钥生成',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Keygen session created',
|
||||||
|
type: KeygenSessionResponseDto,
|
||||||
|
})
|
||||||
|
async createKeygen(@Body() dto: CreateKeygenDto): Promise<KeygenSessionResponseDto> {
|
||||||
|
this.logger.log(`Creating keygen session: username=${dto.username}, ${dto.thresholdT}-of-${dto.thresholdN}, delegate=${dto.requireDelegate}`);
|
||||||
|
|
||||||
|
const result = await this.mpcCoordinatorService.createKeygenSession({
|
||||||
|
username: dto.username,
|
||||||
|
thresholdN: dto.thresholdN,
|
||||||
|
thresholdT: dto.thresholdT,
|
||||||
|
requireDelegate: dto.requireDelegate,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: result.sessionId,
|
||||||
|
status: result.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get keygen session status
|
||||||
|
*/
|
||||||
|
@Public()
|
||||||
|
@Get('keygen/:sessionId/status')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取 Keygen 会话状态',
|
||||||
|
description: '获取 keygen 会话的当前状态,包括公钥和 delegate share',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'sessionId', description: 'Session ID' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Keygen session status',
|
||||||
|
type: KeygenStatusResponseDto,
|
||||||
|
})
|
||||||
|
async getKeygenStatus(@Param('sessionId') sessionId: string): Promise<KeygenStatusResponseDto> {
|
||||||
|
this.logger.debug(`Getting keygen status: sessionId=${sessionId}`);
|
||||||
|
|
||||||
|
const result = await this.mpcCoordinatorService.getKeygenStatus(sessionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: result.sessionId,
|
||||||
|
status: result.status,
|
||||||
|
publicKey: result.publicKey,
|
||||||
|
delegateShare: result.delegateShare,
|
||||||
|
serverParties: result.serverParties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create signing session
|
||||||
|
* Called by identity-service to start MPC signing
|
||||||
|
*/
|
||||||
|
@Public()
|
||||||
|
@Post('sign')
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '创建 MPC 签名会话',
|
||||||
|
description: '创建一个新的 MPC signing 会话',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Signing session created',
|
||||||
|
type: SigningSessionResponseDto,
|
||||||
|
})
|
||||||
|
async createSigning(@Body() dto: CreateSigningDto): Promise<SigningSessionResponseDto> {
|
||||||
|
this.logger.log(`Creating signing session: username=${dto.username}, messageHash=${dto.messageHash.slice(0, 16)}...`);
|
||||||
|
|
||||||
|
const result = await this.mpcCoordinatorService.createSigningSession({
|
||||||
|
username: dto.username,
|
||||||
|
messageHash: dto.messageHash,
|
||||||
|
userShare: dto.userShare,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: result.sessionId,
|
||||||
|
status: result.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get signing session status
|
||||||
|
*/
|
||||||
|
@Public()
|
||||||
|
@Get('sign/:sessionId/status')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取签名会话状态',
|
||||||
|
description: '获取 signing 会话的当前状态,包括签名结果',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'sessionId', description: 'Session ID' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Signing session status',
|
||||||
|
type: SigningStatusResponseDto,
|
||||||
|
})
|
||||||
|
async getSigningStatus(@Param('sessionId') sessionId: string): Promise<SigningStatusResponseDto> {
|
||||||
|
this.logger.debug(`Getting signing status: sessionId=${sessionId}`);
|
||||||
|
|
||||||
|
const result = await this.mpcCoordinatorService.getSigningStatus(sessionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: result.sessionId,
|
||||||
|
status: result.status,
|
||||||
|
signature: result.signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get public key by username
|
||||||
|
* Used for address derivation
|
||||||
|
*/
|
||||||
|
@Public()
|
||||||
|
@Get('wallet/:username/public-key')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取用户公钥',
|
||||||
|
description: '根据用户名获取 MPC 公钥',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'username', description: 'Username' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Public key info',
|
||||||
|
})
|
||||||
|
async getPublicKey(@Param('username') username: string) {
|
||||||
|
this.logger.debug(`Getting public key: username=${username}`);
|
||||||
|
|
||||||
|
const result = await this.mpcCoordinatorService.getWalletByUsername(username);
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: result.username,
|
||||||
|
publicKey: result.publicKey,
|
||||||
|
keygenSessionId: result.keygenSessionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { DomainModule } from '../domain/domain.module';
|
import { DomainModule } from '../domain/domain.module';
|
||||||
import { InfrastructureModule } from '../infrastructure/infrastructure.module';
|
import { InfrastructureModule } from '../infrastructure/infrastructure.module';
|
||||||
|
|
||||||
|
|
@ -19,11 +21,17 @@ import { ListSharesHandler } from './queries/list-shares';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { MPCPartyApplicationService } from './services/mpc-party-application.service';
|
import { MPCPartyApplicationService } from './services/mpc-party-application.service';
|
||||||
|
import { MPCCoordinatorService } from './services/mpc-coordinator.service';
|
||||||
|
|
||||||
|
// Entities
|
||||||
|
import { MpcWallet, MpcShare, MpcSession } from '../domain/entities';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
DomainModule,
|
DomainModule,
|
||||||
InfrastructureModule,
|
InfrastructureModule,
|
||||||
|
HttpModule,
|
||||||
|
TypeOrmModule.forFeature([MpcWallet, MpcShare, MpcSession]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Command Handlers
|
// Command Handlers
|
||||||
|
|
@ -37,9 +45,11 @@ import { MPCPartyApplicationService } from './services/mpc-party-application.ser
|
||||||
|
|
||||||
// Application Services
|
// Application Services
|
||||||
MPCPartyApplicationService,
|
MPCPartyApplicationService,
|
||||||
|
MPCCoordinatorService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
MPCPartyApplicationService,
|
MPCPartyApplicationService,
|
||||||
|
MPCCoordinatorService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ApplicationModule {}
|
export class ApplicationModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,463 @@
|
||||||
|
/**
|
||||||
|
* MPC Coordinator Service
|
||||||
|
*
|
||||||
|
* 协调 mpc-system (Go) 完成 MPC 操作。
|
||||||
|
* 存储公钥和 share(分片)到本地数据库。
|
||||||
|
*
|
||||||
|
* 调用路径 (DDD 分领域):
|
||||||
|
* identity-service (身份域) → mpc-service (MPC域) → mpc-system (Go/TSS实现)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { MpcWallet } from '../../domain/entities/mpc-wallet.entity';
|
||||||
|
import { MpcShare } from '../../domain/entities/mpc-share.entity';
|
||||||
|
import { MpcSession } from '../../domain/entities/mpc-session.entity';
|
||||||
|
|
||||||
|
export interface CreateKeygenInput {
|
||||||
|
username: string;
|
||||||
|
thresholdN: number;
|
||||||
|
thresholdT: number;
|
||||||
|
requireDelegate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateKeygenOutput {
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeygenStatusOutput {
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
publicKey?: string;
|
||||||
|
delegateShare?: {
|
||||||
|
partyId: string;
|
||||||
|
partyIndex: number;
|
||||||
|
encryptedShare: string;
|
||||||
|
};
|
||||||
|
serverParties?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSigningInput {
|
||||||
|
username: string;
|
||||||
|
messageHash: string;
|
||||||
|
userShare?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSigningOutput {
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SigningStatusOutput {
|
||||||
|
sessionId: string;
|
||||||
|
status: string;
|
||||||
|
signature?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletOutput {
|
||||||
|
username: string;
|
||||||
|
publicKey: string;
|
||||||
|
keygenSessionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MPCCoordinatorService {
|
||||||
|
private readonly logger = new Logger(MPCCoordinatorService.name);
|
||||||
|
private readonly mpcSystemUrl: string;
|
||||||
|
private readonly mpcApiKey: string;
|
||||||
|
private readonly pollIntervalMs = 2000;
|
||||||
|
private readonly maxPollAttempts = 150;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
@InjectRepository(MpcWallet)
|
||||||
|
private readonly walletRepository: Repository<MpcWallet>,
|
||||||
|
@InjectRepository(MpcShare)
|
||||||
|
private readonly shareRepository: Repository<MpcShare>,
|
||||||
|
@InjectRepository(MpcSession)
|
||||||
|
private readonly sessionRepository: Repository<MpcSession>,
|
||||||
|
) {
|
||||||
|
this.mpcSystemUrl = this.configService.get<string>('MPC_SYSTEM_URL', 'http://localhost:4000');
|
||||||
|
this.mpcApiKey = this.configService.get<string>('MPC_API_KEY', 'test-api-key');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 keygen 会话
|
||||||
|
* 调用 mpc-system 的 /api/v1/mpc/keygen API
|
||||||
|
*/
|
||||||
|
async createKeygenSession(input: CreateKeygenInput): Promise<CreateKeygenOutput> {
|
||||||
|
this.logger.log(`Creating keygen session: username=${input.username}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用 mpc-system 创建 keygen session
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.post<{
|
||||||
|
session_id: string;
|
||||||
|
session_type: string;
|
||||||
|
username: string;
|
||||||
|
threshold_n: number;
|
||||||
|
threshold_t: number;
|
||||||
|
selected_parties: string[];
|
||||||
|
delegate_party: string;
|
||||||
|
status: string;
|
||||||
|
}>(
|
||||||
|
`${this.mpcSystemUrl}/api/v1/mpc/keygen`,
|
||||||
|
{
|
||||||
|
username: input.username,
|
||||||
|
threshold_n: input.thresholdN,
|
||||||
|
threshold_t: input.thresholdT,
|
||||||
|
require_delegate: input.requireDelegate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': this.mpcApiKey,
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionId = response.data.session_id;
|
||||||
|
|
||||||
|
// 保存 session 到本地数据库
|
||||||
|
const session = this.sessionRepository.create({
|
||||||
|
sessionId,
|
||||||
|
sessionType: 'keygen',
|
||||||
|
username: input.username,
|
||||||
|
thresholdN: input.thresholdN,
|
||||||
|
thresholdT: input.thresholdT,
|
||||||
|
selectedParties: response.data.selected_parties,
|
||||||
|
delegateParty: response.data.delegate_party,
|
||||||
|
status: 'created',
|
||||||
|
});
|
||||||
|
await this.sessionRepository.save(session);
|
||||||
|
|
||||||
|
this.logger.log(`Keygen session created: ${sessionId}`);
|
||||||
|
|
||||||
|
// 启动后台轮询任务
|
||||||
|
this.pollKeygenCompletion(sessionId, input.username).catch(err => {
|
||||||
|
this.logger.error(`Keygen polling failed: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
status: 'created',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to create keygen session: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台轮询 keygen 完成状态
|
||||||
|
*/
|
||||||
|
private async pollKeygenCompletion(sessionId: string, username: string): Promise<void> {
|
||||||
|
for (let i = 0; i < this.maxPollAttempts; i++) {
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get<{
|
||||||
|
session_id: string;
|
||||||
|
status: string;
|
||||||
|
session_type: string;
|
||||||
|
completed_parties: number;
|
||||||
|
total_parties: number;
|
||||||
|
public_key?: string;
|
||||||
|
has_delegate?: boolean;
|
||||||
|
delegate_share?: {
|
||||||
|
encrypted_share: string;
|
||||||
|
party_index: number;
|
||||||
|
party_id: string;
|
||||||
|
};
|
||||||
|
}>(
|
||||||
|
`${this.mpcSystemUrl}/api/v1/mpc/sessions/${sessionId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': this.mpcApiKey,
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
// 更新 session 状态
|
||||||
|
await this.sessionRepository.update(
|
||||||
|
{ sessionId },
|
||||||
|
{
|
||||||
|
status: 'completed',
|
||||||
|
publicKey: data.public_key,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 保存钱包信息
|
||||||
|
const wallet = this.walletRepository.create({
|
||||||
|
username,
|
||||||
|
publicKey: data.public_key,
|
||||||
|
keygenSessionId: sessionId,
|
||||||
|
});
|
||||||
|
await this.walletRepository.save(wallet);
|
||||||
|
|
||||||
|
// 保存 delegate share
|
||||||
|
if (data.delegate_share) {
|
||||||
|
const share = this.shareRepository.create({
|
||||||
|
sessionId,
|
||||||
|
username,
|
||||||
|
partyId: data.delegate_share.party_id,
|
||||||
|
partyIndex: data.delegate_share.party_index,
|
||||||
|
encryptedShare: data.delegate_share.encrypted_share,
|
||||||
|
shareType: 'delegate',
|
||||||
|
});
|
||||||
|
await this.shareRepository.save(share);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Keygen completed: sessionId=${sessionId}, publicKey=${data.public_key}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'failed' || data.status === 'expired') {
|
||||||
|
await this.sessionRepository.update(
|
||||||
|
{ sessionId },
|
||||||
|
{ status: data.status },
|
||||||
|
);
|
||||||
|
this.logger.error(`Keygen failed: sessionId=${sessionId}, status=${data.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sleep(this.pollIntervalMs);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Error polling keygen status: ${error.message}`);
|
||||||
|
await this.sleep(this.pollIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时
|
||||||
|
await this.sessionRepository.update(
|
||||||
|
{ sessionId },
|
||||||
|
{ status: 'expired' },
|
||||||
|
);
|
||||||
|
this.logger.error(`Keygen timed out: sessionId=${sessionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 keygen 会话状态
|
||||||
|
*/
|
||||||
|
async getKeygenStatus(sessionId: string): Promise<KeygenStatusOutput> {
|
||||||
|
const session = await this.sessionRepository.findOne({
|
||||||
|
where: { sessionId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
status: 'not_found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: KeygenStatusOutput = {
|
||||||
|
sessionId,
|
||||||
|
status: session.status,
|
||||||
|
serverParties: session.selectedParties?.filter(p => p !== session.delegateParty),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (session.status === 'completed') {
|
||||||
|
result.publicKey = session.publicKey;
|
||||||
|
|
||||||
|
// 获取 delegate share
|
||||||
|
const share = await this.shareRepository.findOne({
|
||||||
|
where: { sessionId, shareType: 'delegate' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (share) {
|
||||||
|
result.delegateShare = {
|
||||||
|
partyId: share.partyId,
|
||||||
|
partyIndex: share.partyIndex,
|
||||||
|
encryptedShare: share.encryptedShare,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建签名会话
|
||||||
|
*/
|
||||||
|
async createSigningSession(input: CreateSigningInput): Promise<CreateSigningOutput> {
|
||||||
|
this.logger.log(`Creating signing session: username=${input.username}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用 mpc-system 创建签名 session
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.post<{
|
||||||
|
session_id: string;
|
||||||
|
session_type: string;
|
||||||
|
username: string;
|
||||||
|
message_hash: string;
|
||||||
|
threshold_t: number;
|
||||||
|
selected_parties: string[];
|
||||||
|
has_delegate: boolean;
|
||||||
|
status: string;
|
||||||
|
}>(
|
||||||
|
`${this.mpcSystemUrl}/api/v1/mpc/sign`,
|
||||||
|
{
|
||||||
|
username: input.username,
|
||||||
|
message_hash: input.messageHash,
|
||||||
|
user_share: input.userShare,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': this.mpcApiKey,
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionId = response.data.session_id;
|
||||||
|
|
||||||
|
// 保存 session 到本地数据库
|
||||||
|
const session = this.sessionRepository.create({
|
||||||
|
sessionId,
|
||||||
|
sessionType: 'sign',
|
||||||
|
username: input.username,
|
||||||
|
messageHash: input.messageHash,
|
||||||
|
selectedParties: response.data.selected_parties,
|
||||||
|
status: 'created',
|
||||||
|
});
|
||||||
|
await this.sessionRepository.save(session);
|
||||||
|
|
||||||
|
this.logger.log(`Signing session created: ${sessionId}`);
|
||||||
|
|
||||||
|
// 启动后台轮询任务
|
||||||
|
this.pollSigningCompletion(sessionId).catch(err => {
|
||||||
|
this.logger.error(`Signing polling failed: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
status: 'created',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to create signing session: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台轮询签名完成状态
|
||||||
|
*/
|
||||||
|
private async pollSigningCompletion(sessionId: string): Promise<void> {
|
||||||
|
for (let i = 0; i < this.maxPollAttempts; i++) {
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get<{
|
||||||
|
session_id: string;
|
||||||
|
status: string;
|
||||||
|
session_type: string;
|
||||||
|
completed_parties: number;
|
||||||
|
total_parties: number;
|
||||||
|
signature?: string;
|
||||||
|
}>(
|
||||||
|
`${this.mpcSystemUrl}/api/v1/mpc/sessions/${sessionId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': this.mpcApiKey,
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
await this.sessionRepository.update(
|
||||||
|
{ sessionId },
|
||||||
|
{
|
||||||
|
status: 'completed',
|
||||||
|
signature: data.signature,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.logger.log(`Signing completed: sessionId=${sessionId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'failed' || data.status === 'expired') {
|
||||||
|
await this.sessionRepository.update(
|
||||||
|
{ sessionId },
|
||||||
|
{ status: data.status },
|
||||||
|
);
|
||||||
|
this.logger.error(`Signing failed: sessionId=${sessionId}, status=${data.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sleep(this.pollIntervalMs);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Error polling signing status: ${error.message}`);
|
||||||
|
await this.sleep(this.pollIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sessionRepository.update(
|
||||||
|
{ sessionId },
|
||||||
|
{ status: 'expired' },
|
||||||
|
);
|
||||||
|
this.logger.error(`Signing timed out: sessionId=${sessionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取签名会话状态
|
||||||
|
*/
|
||||||
|
async getSigningStatus(sessionId: string): Promise<SigningStatusOutput> {
|
||||||
|
const session = await this.sessionRepository.findOne({
|
||||||
|
where: { sessionId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
status: 'not_found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
status: session.status,
|
||||||
|
signature: session.signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名获取钱包信息
|
||||||
|
*/
|
||||||
|
async getWalletByUsername(username: string): Promise<WalletOutput> {
|
||||||
|
const wallet = await this.walletRepository.findOne({
|
||||||
|
where: { username },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!wallet) {
|
||||||
|
throw new Error(`Wallet not found for username: ${username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: wallet.username,
|
||||||
|
publicKey: wallet.publicKey,
|
||||||
|
keygenSessionId: wallet.keygenSessionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,6 @@
|
||||||
|
|
||||||
export * from './party-share.entity';
|
export * from './party-share.entity';
|
||||||
export * from './session-state.entity';
|
export * from './session-state.entity';
|
||||||
|
export * from './mpc-wallet.entity';
|
||||||
|
export * from './mpc-share.entity';
|
||||||
|
export * from './mpc-session.entity';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* MPC Session Entity
|
||||||
|
*
|
||||||
|
* 存储 MPC 会话信息(keygen 和 signing)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('mpc_sessions')
|
||||||
|
export class MpcSession {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'session_id', unique: true })
|
||||||
|
@Index()
|
||||||
|
sessionId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'session_type' })
|
||||||
|
sessionType: string; // 'keygen' | 'sign'
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@Index()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Column({ name: 'threshold_n', nullable: true })
|
||||||
|
thresholdN: number;
|
||||||
|
|
||||||
|
@Column({ name: 'threshold_t', nullable: true })
|
||||||
|
thresholdT: number;
|
||||||
|
|
||||||
|
@Column({ name: 'selected_parties', type: 'simple-array', nullable: true })
|
||||||
|
selectedParties: string[];
|
||||||
|
|
||||||
|
@Column({ name: 'delegate_party', nullable: true })
|
||||||
|
delegateParty: string;
|
||||||
|
|
||||||
|
@Column({ name: 'message_hash', nullable: true })
|
||||||
|
messageHash: string;
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', nullable: true })
|
||||||
|
publicKey: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
signature: string;
|
||||||
|
|
||||||
|
@Column({ default: 'created' })
|
||||||
|
status: string; // 'created' | 'in_progress' | 'completed' | 'failed' | 'expired'
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* MPC Share Entity
|
||||||
|
*
|
||||||
|
* 存储 MPC 分片信息(delegate share)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('mpc_shares')
|
||||||
|
export class MpcShare {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'session_id' })
|
||||||
|
@Index()
|
||||||
|
sessionId: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@Index()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Column({ name: 'party_id' })
|
||||||
|
partyId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'party_index' })
|
||||||
|
partyIndex: number;
|
||||||
|
|
||||||
|
@Column({ name: 'encrypted_share', type: 'text' })
|
||||||
|
encryptedShare: string;
|
||||||
|
|
||||||
|
@Column({ name: 'share_type' })
|
||||||
|
shareType: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* MPC Wallet Entity
|
||||||
|
*
|
||||||
|
* 存储用户的 MPC 钱包信息(公钥和 keygen session ID)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('mpc_wallets')
|
||||||
|
export class MpcWallet {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
@Index()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Column({ name: 'public_key' })
|
||||||
|
publicKey: string;
|
||||||
|
|
||||||
|
@Column({ name: 'keygen_session_id' })
|
||||||
|
@Index()
|
||||||
|
keygenSessionId: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue