fix: align mpc-service migration with schema and fix identity-service compile errors
- Update mpc-service migration to match new gateway mode schema (mpc_wallets, mpc_shares) - Remove old MySQL migrations (party_shares, session_states, share_backups) - Fix MpcSignature type to use string format (64 bytes hex: R + S) - Add persistence layer conversion functions for DB compatibility - Fix method names in domain services (checkDeviceNotRegistered, generateNextUserSequence) - Update wallet generator interface to use delegateShare instead of clientShareData 🤖 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
ba91a89b16
commit
289691dc3c
|
|
@ -259,6 +259,7 @@
|
|||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export class AutoCreateAccountHandler {
|
|||
this.logger.log(`Generating MPC wallet for user=${account.userId.toString()}`);
|
||||
const mpcResult = await this.walletGenerator.generateMpcWalletSystem({
|
||||
userId: account.userId.toString(),
|
||||
username: accountSequence.value.toString(), // 使用账户序列号作为用户名
|
||||
deviceId: command.deviceId,
|
||||
});
|
||||
|
||||
|
|
@ -62,11 +63,11 @@ export class AutoCreateAccountHandler {
|
|||
mpcResult.wallets,
|
||||
);
|
||||
|
||||
// 保存备份分片到备份服务
|
||||
this.logger.log(`Storing backup share for user=${account.userId.toString()}`);
|
||||
// 保存 delegate share 到备份服务 (用于恢复)
|
||||
this.logger.log(`Storing delegate share for user=${account.userId.toString()}`);
|
||||
await this.mpcShareStorage.storeBackupShare({
|
||||
userId: account.userId.toString(),
|
||||
shareData: mpcResult.backupShareData,
|
||||
shareData: mpcResult.delegateShare,
|
||||
publicKey: mpcResult.publicKey,
|
||||
});
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ export class AutoCreateAccountHandler {
|
|||
accountSequence: account.accountSequence.value,
|
||||
referralCode: account.referralCode.value,
|
||||
mnemonic: '', // MPC 模式下不再使用助记词
|
||||
clientShareData: mpcResult.clientShareData, // 客户端需安全存储此分片
|
||||
delegateShare: mpcResult.delegateShare, // delegate share (客户端需安全存储)
|
||||
publicKey: mpcResult.publicKey,
|
||||
walletAddresses: {
|
||||
kava: wallets.get(ChainType.KAVA)!.address,
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ export interface AutoCreateAccountResult {
|
|||
accountSequence: number;
|
||||
referralCode: string;
|
||||
mnemonic: string; // 兼容字段,MPC模式下为空
|
||||
clientShareData?: string; // MPC 客户端分片数据 (需安全存储)
|
||||
delegateShare?: string; // MPC delegate share (客户端需安全存储)
|
||||
publicKey?: string; // MPC 公钥
|
||||
walletAddresses: { kava: string; dst: string; bsc: string };
|
||||
accessToken: string;
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ export class UserApplicationService {
|
|||
async autoCreateAccount(command: AutoCreateAccountCommand): Promise<AutoCreateAccountResult> {
|
||||
this.logger.log(`Creating account with MPC 2-of-3 for device: ${command.deviceId}`);
|
||||
|
||||
// 1. 验证设备ID
|
||||
const deviceValidation = await this.validatorService.validateDeviceId(command.deviceId);
|
||||
// 1. 验证设备ID (检查设备是否已创建过账户)
|
||||
const deviceValidation = await this.validatorService.checkDeviceNotRegistered(command.deviceId);
|
||||
if (!deviceValidation.isValid) throw new ApplicationError(deviceValidation.errorMessage!);
|
||||
|
||||
// 2. 验证邀请码
|
||||
|
|
@ -90,6 +90,7 @@ export class UserApplicationService {
|
|||
this.logger.log(`Generating MPC wallet for account sequence: ${accountSequence.value}`);
|
||||
const mpcResult = await this.mpcWalletService.generateMpcWallet({
|
||||
userId: account.userId.toString(),
|
||||
username: accountSequence.value.toString(), // 使用账户序列号作为用户名
|
||||
deviceId: command.deviceId,
|
||||
});
|
||||
|
||||
|
|
@ -115,37 +116,26 @@ export class UserApplicationService {
|
|||
await this.userRepository.save(account);
|
||||
await this.userRepository.saveWallets(account.userId, Array.from(wallets.values()));
|
||||
|
||||
// 9. 保存服务端 MPC 分片到数据库 (用于后续签名)
|
||||
await this.mpcKeyShareRepository.saveServerShare({
|
||||
userId: account.userId.value,
|
||||
publicKey: mpcResult.publicKey,
|
||||
partyIndex: 0, // SERVER = party 0
|
||||
threshold: 2,
|
||||
totalParties: 3,
|
||||
encryptedShareData: mpcResult.serverShareData,
|
||||
});
|
||||
this.logger.log(`Server MPC share saved for user: ${account.userId.toString()}`);
|
||||
|
||||
// 10. 保存备份 MPC 分片到 backup-service (异地服务器)
|
||||
// 注意: backup-service 必须部署在不同物理服务器,否则 MPC 安全性失效
|
||||
if (mpcResult.backupShareData) {
|
||||
// 9. 保存 delegate share 到 backup-service (用于恢复)
|
||||
// 注意: delegate share 由 mpc-service 代理生成,用户设备也应安全存储一份
|
||||
if (mpcResult.delegateShare) {
|
||||
await this.backupClient.storeBackupShare({
|
||||
userId: account.userId.toString(),
|
||||
accountSequence: account.accountSequence.value,
|
||||
publicKey: mpcResult.publicKey,
|
||||
encryptedShareData: mpcResult.backupShareData,
|
||||
encryptedShareData: mpcResult.delegateShare,
|
||||
});
|
||||
this.logger.log(`Backup MPC share sent to backup-service for user: ${account.userId.toString()}`);
|
||||
this.logger.log(`Delegate share sent to backup-service for user: ${account.userId.toString()}`);
|
||||
}
|
||||
|
||||
// 11. 生成 Token
|
||||
// 10. 生成 Token
|
||||
const tokens = await this.tokenService.generateTokenPair({
|
||||
userId: account.userId.toString(),
|
||||
accountSequence: account.accountSequence.value,
|
||||
deviceId: command.deviceId,
|
||||
});
|
||||
|
||||
// 12. 发布领域事件
|
||||
// 11. 发布领域事件
|
||||
await this.eventPublisher.publishAll(account.domainEvents);
|
||||
account.clearDomainEvents();
|
||||
|
||||
|
|
@ -155,10 +145,9 @@ export class UserApplicationService {
|
|||
userId: account.userId.toString(),
|
||||
accountSequence: account.accountSequence.value,
|
||||
referralCode: account.referralCode.value,
|
||||
// MPC 模式下返回客户端分片数据 (而不是助记词)
|
||||
mnemonic: '', // MPC 模式不使用助记词,返回空字符串
|
||||
clientShareData: mpcResult.clientShareData, // 客户端需要安全存储的分片数据
|
||||
publicKey: mpcResult.publicKey, // MPC 公钥
|
||||
mnemonic: '', // MPC 模式不使用助记词
|
||||
delegateShare: mpcResult.delegateShare, // delegate share (客户端需安全存储)
|
||||
publicKey: mpcResult.publicKey,
|
||||
walletAddresses: {
|
||||
kava: wallets.get(ChainType.KAVA)!.address,
|
||||
dst: wallets.get(ChainType.DST)!.address,
|
||||
|
|
|
|||
|
|
@ -15,12 +15,9 @@ import {
|
|||
|
||||
/**
|
||||
* MPC 签名信息
|
||||
* 64 bytes hex (R 32 bytes + S 32 bytes)
|
||||
*/
|
||||
export interface MpcSignature {
|
||||
r: string;
|
||||
s: string;
|
||||
v: number;
|
||||
}
|
||||
export type MpcSignature = string;
|
||||
|
||||
export class WalletAddress {
|
||||
private readonly _addressId: AddressId;
|
||||
|
|
@ -104,9 +101,7 @@ export class WalletAddress {
|
|||
address: string;
|
||||
publicKey: string;
|
||||
addressDigest: string;
|
||||
mpcSignatureR: string;
|
||||
mpcSignatureS: string;
|
||||
mpcSignatureV: number;
|
||||
mpcSignature: string; // 64 bytes hex
|
||||
status: AddressStatus;
|
||||
boundAt: Date;
|
||||
}): WalletAddress {
|
||||
|
|
@ -117,11 +112,7 @@ export class WalletAddress {
|
|||
params.address,
|
||||
params.publicKey,
|
||||
params.addressDigest,
|
||||
{
|
||||
r: params.mpcSignatureR,
|
||||
s: params.mpcSignatureS,
|
||||
v: params.mpcSignatureV,
|
||||
},
|
||||
params.mpcSignature,
|
||||
params.status,
|
||||
params.boundAt,
|
||||
);
|
||||
|
|
@ -149,18 +140,31 @@ export class WalletAddress {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
// 签名格式: R (32 bytes) + S (32 bytes) = 64 bytes hex = 128 chars
|
||||
if (this._mpcSignature.length !== 128) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const r = '0x' + this._mpcSignature.slice(0, 64);
|
||||
const s = '0x' + this._mpcSignature.slice(64, 128);
|
||||
const digestBytes = Buffer.from(this._addressDigest, 'hex');
|
||||
const sig = ethers.Signature.from({
|
||||
r: '0x' + this._mpcSignature.r,
|
||||
s: '0x' + this._mpcSignature.s,
|
||||
v: this._mpcSignature.v + 27,
|
||||
});
|
||||
|
||||
const recoveredPubKey = ethers.SigningKey.recoverPublicKey(digestBytes, sig);
|
||||
const compressedRecovered = ethers.SigningKey.computePublicKey(recoveredPubKey, true);
|
||||
// 尝试两种 recovery id
|
||||
for (const v of [27, 28]) {
|
||||
try {
|
||||
const sig = ethers.Signature.from({ r, s, v });
|
||||
const recoveredPubKey = ethers.SigningKey.recoverPublicKey(digestBytes, sig);
|
||||
const compressedRecovered = ethers.SigningKey.computePublicKey(recoveredPubKey, true);
|
||||
|
||||
return compressedRecovered.slice(2).toLowerCase() === this._publicKey.toLowerCase();
|
||||
if (compressedRecovered.slice(2).toLowerCase() === this._publicKey.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// 尝试下一个 v 值
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -197,7 +201,7 @@ export class WalletAddress {
|
|||
params.address,
|
||||
'',
|
||||
'',
|
||||
{ r: '', s: '', v: 0 },
|
||||
'', // empty signature
|
||||
AddressStatus.ACTIVE,
|
||||
new Date(),
|
||||
);
|
||||
|
|
@ -220,7 +224,7 @@ export class WalletAddress {
|
|||
address,
|
||||
'',
|
||||
'',
|
||||
{ r: '', s: '', v: 0 },
|
||||
'', // empty signature
|
||||
AddressStatus.ACTIVE,
|
||||
new Date(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export class AccountSequenceGeneratorService {
|
|||
private readonly repository: UserAccountRepository,
|
||||
) {}
|
||||
|
||||
async generateNext(): Promise<AccountSequence> {
|
||||
async generateNextUserSequence(): Promise<AccountSequence> {
|
||||
return this.repository.getNextAccountSequence();
|
||||
}
|
||||
}
|
||||
|
|
@ -53,9 +53,9 @@ export class UserValidatorService {
|
|||
return ValidationResult.success();
|
||||
}
|
||||
|
||||
async validateDeviceId(deviceId: string): Promise<ValidationResult> {
|
||||
async checkDeviceNotRegistered(deviceId: string): Promise<ValidationResult> {
|
||||
const existing = await this.repository.findByDeviceId(deviceId);
|
||||
if (existing) return ValidationResult.failure('该设备已创建账户');
|
||||
if (existing) return ValidationResult.failure('该设备已创建过账户');
|
||||
return ValidationResult.success();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ChainType, Mnemonic, UserId } from '@/domain/value-objects';
|
|||
*/
|
||||
export interface MpcWalletGenerationParams {
|
||||
userId: string;
|
||||
username: string; // 用户名 (用于 MPC keygen)
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ export interface ChainWalletInfo {
|
|||
address: string;
|
||||
publicKey: string;
|
||||
addressDigest: string;
|
||||
signature: MpcSignature;
|
||||
signature: MpcSignature; // 64 bytes hex (R + S)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -25,9 +26,8 @@ export interface ChainWalletInfo {
|
|||
*/
|
||||
export interface MpcWalletGenerationResult {
|
||||
publicKey: string;
|
||||
serverShareData: string;
|
||||
clientShareData: string;
|
||||
backupShareData: string;
|
||||
delegateShare: string; // delegate share (加密的用户分片)
|
||||
serverParties: string[]; // 服务器 party IDs
|
||||
wallets: ChainWalletInfo[];
|
||||
sessionId: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,17 @@
|
|||
*
|
||||
* 使用 MPC 2-of-3 协议生成三链钱包地址
|
||||
* 并对地址进行签名验证
|
||||
*
|
||||
* 调用路径: identity-service → mpc-service → mpc-system
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createHash } from 'crypto';
|
||||
import { MpcClientService, KeygenResult, SigningResult } from './mpc-client.service';
|
||||
import { MpcClientService } from './mpc-client.service';
|
||||
|
||||
export interface MpcWalletGenerationParams {
|
||||
userId: string;
|
||||
username: string; // 用户名 (用于 MPC keygen)
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
|
|
@ -20,18 +22,13 @@ export interface ChainWalletInfo {
|
|||
address: string;
|
||||
publicKey: string;
|
||||
addressDigest: string;
|
||||
signature: {
|
||||
r: string;
|
||||
s: string;
|
||||
v: number;
|
||||
};
|
||||
signature: string; // 64 bytes hex (R + S)
|
||||
}
|
||||
|
||||
export interface MpcWalletGenerationResult {
|
||||
publicKey: string; // MPC 公钥
|
||||
serverShareData: string; // 服务端分片 (加密后存储)
|
||||
clientShareData: string; // 客户端分片 (返回给用户设备)
|
||||
backupShareData: string; // 备份分片 (存储在备份服务)
|
||||
delegateShare: string; // delegate share (加密的用户分片)
|
||||
serverParties: string[]; // 服务器 party IDs
|
||||
wallets: ChainWalletInfo[]; // 三条链的钱包信息
|
||||
sessionId: string; // MPC 会话ID
|
||||
}
|
||||
|
|
@ -39,7 +36,6 @@ export interface MpcWalletGenerationResult {
|
|||
@Injectable()
|
||||
export class MpcWalletService {
|
||||
private readonly logger = new Logger(MpcWalletService.name);
|
||||
private readonly useLocalMpc: boolean;
|
||||
|
||||
// 三条链的地址生成配置
|
||||
private readonly chainConfigs = {
|
||||
|
|
@ -65,11 +61,7 @@ export class MpcWalletService {
|
|||
|
||||
constructor(
|
||||
private readonly mpcClient: MpcClientService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
// 开发环境使用本地模拟 MPC
|
||||
this.useLocalMpc = this.configService.get<string>('MPC_MODE', 'local') === 'local';
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 使用 MPC 2-of-3 生成三链钱包
|
||||
|
|
@ -82,11 +74,16 @@ export class MpcWalletService {
|
|||
* 5. 返回完整的钱包信息
|
||||
*/
|
||||
async generateMpcWallet(params: MpcWalletGenerationParams): Promise<MpcWalletGenerationResult> {
|
||||
this.logger.log(`Generating MPC wallet for user=${params.userId}, device=${params.deviceId}`);
|
||||
this.logger.log(`Generating MPC wallet for user=${params.userId}, username=${params.username}`);
|
||||
|
||||
// Step 1: 生成 MPC 密钥
|
||||
const sessionId = this.mpcClient.generateSessionId();
|
||||
const keygenResult = await this.executeKeygen(sessionId, params);
|
||||
const keygenResult = await this.mpcClient.executeKeygen({
|
||||
sessionId: this.mpcClient.generateSessionId(),
|
||||
username: params.username,
|
||||
threshold: 1, // t in t-of-n (2-of-3 means t=1)
|
||||
totalParties: 3,
|
||||
requireDelegate: true,
|
||||
});
|
||||
|
||||
this.logger.log(`MPC keygen completed: publicKey=${keygenResult.publicKey}`);
|
||||
|
||||
|
|
@ -97,14 +94,12 @@ export class MpcWalletService {
|
|||
const addressDigest = this.computeAddressDigest(walletAddresses);
|
||||
|
||||
// Step 4: 使用 MPC 签名对摘要进行签名
|
||||
const signingSessionId = this.mpcClient.generateSessionId();
|
||||
const signingResult = await this.executeSigning(
|
||||
signingSessionId,
|
||||
keygenResult.publicKey,
|
||||
addressDigest,
|
||||
);
|
||||
const signingResult = await this.mpcClient.executeSigning({
|
||||
username: params.username,
|
||||
messageHash: addressDigest,
|
||||
});
|
||||
|
||||
this.logger.log(`MPC signing completed: r=${signingResult.signature.r}`);
|
||||
this.logger.log(`MPC signing completed: signature=${signingResult.signature.slice(0, 16)}...`);
|
||||
|
||||
// Step 5: 构建钱包信息
|
||||
const wallets: ChainWalletInfo[] = walletAddresses.map((wa) => ({
|
||||
|
|
@ -115,18 +110,12 @@ export class MpcWalletService {
|
|||
signature: signingResult.signature,
|
||||
}));
|
||||
|
||||
// 提取各方分片
|
||||
const serverShare = keygenResult.partyShares.find((s) => s.partyIndex === 0);
|
||||
const clientShare = keygenResult.partyShares.find((s) => s.partyIndex === 1);
|
||||
const backupShare = keygenResult.partyShares.find((s) => s.partyIndex === 2);
|
||||
|
||||
return {
|
||||
publicKey: keygenResult.publicKey,
|
||||
serverShareData: serverShare?.encryptedShareData || '',
|
||||
clientShareData: clientShare?.encryptedShareData || '',
|
||||
backupShareData: backupShare?.encryptedShareData || '',
|
||||
delegateShare: keygenResult.delegateShare.encryptedShare,
|
||||
serverParties: keygenResult.serverParties,
|
||||
wallets,
|
||||
sessionId,
|
||||
sessionId: keygenResult.sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -134,86 +123,56 @@ export class MpcWalletService {
|
|||
* 验证钱包地址签名
|
||||
*
|
||||
* 用于检测地址是否被篡改
|
||||
*
|
||||
* @param address 钱包地址
|
||||
* @param chainType 链类型
|
||||
* @param publicKey 公钥 (hex)
|
||||
* @param signature 签名 (64 bytes hex: R + S)
|
||||
*/
|
||||
async verifyWalletSignature(
|
||||
address: string,
|
||||
chainType: string,
|
||||
publicKey: string,
|
||||
signature: { r: string; s: string; v: number },
|
||||
signature: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const { ethers } = await import('ethers');
|
||||
|
||||
// 签名格式: R (32 bytes) + S (32 bytes) = 64 bytes hex
|
||||
if (signature.length !== 128) {
|
||||
this.logger.error(`Invalid signature length: ${signature.length}, expected 128`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const r = '0x' + signature.slice(0, 64);
|
||||
const s = '0x' + signature.slice(64, 128);
|
||||
|
||||
// 计算地址摘要
|
||||
const digest = this.computeSingleAddressDigest(address, chainType);
|
||||
const digestBytes = Buffer.from(digest, 'hex');
|
||||
|
||||
// 重建签名
|
||||
const sig = ethers.Signature.from({
|
||||
r: '0x' + signature.r,
|
||||
s: '0x' + signature.s,
|
||||
v: signature.v + 27,
|
||||
});
|
||||
// 尝试两种 recovery id
|
||||
for (const v of [27, 28]) {
|
||||
try {
|
||||
const sig = ethers.Signature.from({ r, s, v });
|
||||
const recoveredPubKey = ethers.SigningKey.recoverPublicKey(digestBytes, sig);
|
||||
const compressedRecovered = ethers.SigningKey.computePublicKey(recoveredPubKey, true);
|
||||
|
||||
// 从签名恢复公钥
|
||||
const recoveredPubKey = ethers.SigningKey.recoverPublicKey(digestBytes, sig);
|
||||
if (compressedRecovered.slice(2).toLowerCase() === publicKey.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// 尝试下一个 v 值
|
||||
}
|
||||
}
|
||||
|
||||
// 压缩公钥并比较
|
||||
const compressedRecovered = ethers.SigningKey.computePublicKey(recoveredPubKey, true);
|
||||
|
||||
return compressedRecovered.slice(2).toLowerCase() === publicKey.toLowerCase();
|
||||
return false;
|
||||
} catch (error) {
|
||||
this.logger.error(`Signature verification failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 MPC 密钥生成
|
||||
*/
|
||||
private async executeKeygen(sessionId: string, params: MpcWalletGenerationParams): Promise<KeygenResult> {
|
||||
const request = {
|
||||
sessionId,
|
||||
threshold: 2,
|
||||
totalParties: 3,
|
||||
parties: [
|
||||
{ partyId: `server-${params.userId}`, partyIndex: 0, partyType: 'SERVER' as const },
|
||||
{ partyId: `client-${params.deviceId}`, partyIndex: 1, partyType: 'CLIENT' as const },
|
||||
{ partyId: `backup-${params.userId}`, partyIndex: 2, partyType: 'BACKUP' as const },
|
||||
],
|
||||
};
|
||||
|
||||
if (this.useLocalMpc) {
|
||||
return this.mpcClient.executeLocalKeygen(request);
|
||||
}
|
||||
return this.mpcClient.executeKeygen(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 MPC 签名
|
||||
*/
|
||||
private async executeSigning(
|
||||
sessionId: string,
|
||||
publicKey: string,
|
||||
messageHash: string,
|
||||
): Promise<SigningResult> {
|
||||
const request = {
|
||||
sessionId,
|
||||
publicKey,
|
||||
messageHash,
|
||||
threshold: 2,
|
||||
signerParties: [
|
||||
{ partyId: 'server', partyIndex: 0, partyType: 'SERVER' as const },
|
||||
{ partyId: 'backup', partyIndex: 2, partyType: 'BACKUP' as const },
|
||||
],
|
||||
};
|
||||
|
||||
if (this.useLocalMpc) {
|
||||
return this.mpcClient.executeLocalSigning(request);
|
||||
}
|
||||
return this.mpcClient.executeSigning(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MPC 公钥派生三条链的地址
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,9 +5,32 @@ export interface WalletAddressEntity {
|
|||
address: string;
|
||||
publicKey: string;
|
||||
addressDigest: string;
|
||||
// 数据库存储格式 (兼容旧格式)
|
||||
mpcSignatureR: string;
|
||||
mpcSignatureS: string;
|
||||
mpcSignatureV: number;
|
||||
status: string;
|
||||
boundAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据库签名格式转换为应用层格式
|
||||
* 数据库: {r, s, v} -> 应用: string (64 bytes hex)
|
||||
*/
|
||||
export function toMpcSignatureString(entity: WalletAddressEntity): string {
|
||||
// 兼容旧格式: r + s
|
||||
return (entity.mpcSignatureR || '') + (entity.mpcSignatureS || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将应用层签名格式转换为数据库格式
|
||||
* 应用: string (64 bytes hex) -> 数据库: {r, s, v}
|
||||
*/
|
||||
export function fromMpcSignatureString(signature: string): { r: string; s: string; v: number } {
|
||||
// 签名格式: R (32 bytes = 64 hex) + S (32 bytes = 64 hex)
|
||||
return {
|
||||
r: signature.slice(0, 64) || '',
|
||||
s: signature.slice(64, 128) || '',
|
||||
v: 0, // 默认 v=0,实际验证时尝试 27 和 28
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggre
|
|||
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
||||
import { DeviceInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects';
|
||||
import { UserAccountEntity } from '../entities/user-account.entity';
|
||||
import { toMpcSignatureString } from '../entities/wallet-address.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserAccountMapper {
|
||||
|
|
@ -19,9 +20,7 @@ export class UserAccountMapper {
|
|||
address: w.address,
|
||||
publicKey: w.publicKey,
|
||||
addressDigest: w.addressDigest,
|
||||
mpcSignatureR: w.mpcSignatureR,
|
||||
mpcSignatureS: w.mpcSignatureS,
|
||||
mpcSignatureV: w.mpcSignatureV,
|
||||
mpcSignature: toMpcSignatureString(w), // 64 bytes hex (r + s)
|
||||
status: w.status as AddressStatus,
|
||||
boundAt: w.boundAt,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
UserId, AccountSequence, PhoneNumber, ReferralCode, ChainType,
|
||||
AccountStatus, KYCStatus, DeviceInfo, KYCInfo, AddressStatus,
|
||||
} from '@/domain/value-objects';
|
||||
import { toMpcSignatureString, fromMpcSignatureString } from '../entities/wallet-address.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserAccountRepositoryImpl implements UserAccountRepository {
|
||||
|
|
@ -88,18 +89,21 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
|
|||
|
||||
async saveWallets(userId: UserId, wallets: WalletAddress[]): Promise<void> {
|
||||
await this.prisma.walletAddress.createMany({
|
||||
data: wallets.map((w) => ({
|
||||
userId: BigInt(userId.value),
|
||||
chainType: w.chainType,
|
||||
address: w.address,
|
||||
publicKey: w.publicKey,
|
||||
addressDigest: w.addressDigest,
|
||||
mpcSignatureR: w.mpcSignature.r,
|
||||
mpcSignatureS: w.mpcSignature.s,
|
||||
mpcSignatureV: w.mpcSignature.v,
|
||||
status: w.status,
|
||||
boundAt: w.boundAt,
|
||||
})),
|
||||
data: wallets.map((w) => {
|
||||
const sig = fromMpcSignatureString(w.mpcSignature);
|
||||
return {
|
||||
userId: BigInt(userId.value),
|
||||
chainType: w.chainType,
|
||||
address: w.address,
|
||||
publicKey: w.publicKey,
|
||||
addressDigest: w.addressDigest,
|
||||
mpcSignatureR: sig.r,
|
||||
mpcSignatureS: sig.s,
|
||||
mpcSignatureV: sig.v,
|
||||
status: w.status,
|
||||
boundAt: w.boundAt,
|
||||
};
|
||||
}),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -213,9 +217,7 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
|
|||
address: w.address,
|
||||
publicKey: w.publicKey || '',
|
||||
addressDigest: w.addressDigest || '',
|
||||
mpcSignatureR: w.mpcSignatureR || '',
|
||||
mpcSignatureS: w.mpcSignatureS || '',
|
||||
mpcSignatureV: w.mpcSignatureV || 0,
|
||||
mpcSignature: toMpcSignatureString(w), // 64 bytes hex (r + s)
|
||||
status: w.status as AddressStatus,
|
||||
boundAt: w.boundAt,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
-- =============================================================================
|
||||
-- Migration: Create party_shares table
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `party_shares` (
|
||||
`id` VARCHAR(255) NOT NULL,
|
||||
`party_id` VARCHAR(255) NOT NULL COMMENT 'Party identifier (format: {userId}-server)',
|
||||
`session_id` VARCHAR(255) NOT NULL COMMENT 'MPC session ID that created this share',
|
||||
`share_type` VARCHAR(20) NOT NULL COMMENT 'Type: wallet, admin, recovery',
|
||||
`share_data` TEXT NOT NULL COMMENT 'Encrypted share data (JSON: {data, iv, authTag})',
|
||||
`public_key` TEXT NOT NULL COMMENT 'Group public key (hex)',
|
||||
`threshold_n` INT NOT NULL COMMENT 'Total number of parties',
|
||||
`threshold_t` INT NOT NULL COMMENT 'Minimum required signers',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'Status: active, rotated, revoked',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`last_used_at` TIMESTAMP NULL,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_party_session` (`party_id`, `session_id`),
|
||||
INDEX `idx_party_id` (`party_id`),
|
||||
INDEX `idx_session_id` (`session_id`),
|
||||
INDEX `idx_status` (`status`),
|
||||
INDEX `idx_public_key` (`public_key`(255)),
|
||||
|
||||
CONSTRAINT `chk_share_type` CHECK (`share_type` IN ('wallet', 'admin', 'recovery')),
|
||||
CONSTRAINT `chk_status` CHECK (`status` IN ('active', 'rotated', 'revoked')),
|
||||
CONSTRAINT `chk_threshold` CHECK (`threshold_t` <= `threshold_n` AND `threshold_t` >= 2)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='MPC party shares - stores encrypted key shares';
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
-- =============================================================================
|
||||
-- Migration: Create session_states table
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `session_states` (
|
||||
`id` VARCHAR(255) NOT NULL,
|
||||
`session_id` VARCHAR(255) NOT NULL COMMENT 'MPC session ID',
|
||||
`party_id` VARCHAR(255) NOT NULL COMMENT 'This party identifier',
|
||||
`party_index` INT NOT NULL COMMENT 'Party index in the session',
|
||||
`session_type` VARCHAR(20) NOT NULL COMMENT 'Type: keygen, sign, refresh',
|
||||
`participants` TEXT NOT NULL COMMENT 'JSON array of participants',
|
||||
`threshold_n` INT NOT NULL COMMENT 'Total parties',
|
||||
`threshold_t` INT NOT NULL COMMENT 'Required signers',
|
||||
`status` VARCHAR(20) NOT NULL COMMENT 'Status: pending, in_progress, completed, failed, timeout',
|
||||
`current_round` INT NOT NULL DEFAULT 0 COMMENT 'Current protocol round',
|
||||
`error_message` TEXT NULL COMMENT 'Error message if failed',
|
||||
`public_key` TEXT NULL COMMENT 'Group public key (for keygen)',
|
||||
`message_hash` VARCHAR(66) NULL COMMENT 'Message hash (for signing)',
|
||||
`signature` TEXT NULL COMMENT 'Final signature (for signing)',
|
||||
`started_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`completed_at` TIMESTAMP NULL,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_session_party` (`session_id`, `party_id`),
|
||||
INDEX `idx_session_id` (`session_id`),
|
||||
INDEX `idx_party_id` (`party_id`),
|
||||
INDEX `idx_status` (`status`),
|
||||
INDEX `idx_started_at` (`started_at`),
|
||||
|
||||
CONSTRAINT `chk_session_type` CHECK (`session_type` IN ('keygen', 'sign', 'refresh')),
|
||||
CONSTRAINT `chk_session_status` CHECK (`status` IN ('pending', 'in_progress', 'completed', 'failed', 'timeout', 'cancelled'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='MPC session states - tracks party participation in sessions';
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
-- =============================================================================
|
||||
-- Migration: Create share_backups table
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `share_backups` (
|
||||
`id` VARCHAR(255) NOT NULL,
|
||||
`share_id` VARCHAR(255) NOT NULL COMMENT 'Reference to party_shares.id',
|
||||
`backup_data` TEXT NOT NULL COMMENT 'Encrypted backup data',
|
||||
`backup_type` VARCHAR(20) NOT NULL COMMENT 'Type: manual, auto, recovery',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`created_by` VARCHAR(255) NULL COMMENT 'User/system that created the backup',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_share_id` (`share_id`),
|
||||
INDEX `idx_created_at` (`created_at`),
|
||||
INDEX `idx_backup_type` (`backup_type`),
|
||||
|
||||
CONSTRAINT `chk_backup_type` CHECK (`backup_type` IN ('manual', 'auto', 'recovery'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Share backups for disaster recovery';
|
||||
|
|
@ -1,82 +1,45 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "party_shares" (
|
||||
"id" VARCHAR(255) NOT NULL,
|
||||
"party_id" VARCHAR(255) NOT NULL,
|
||||
"session_id" VARCHAR(255) NOT NULL,
|
||||
"share_type" VARCHAR(20) NOT NULL,
|
||||
"share_data" TEXT NOT NULL,
|
||||
"public_key" TEXT NOT NULL,
|
||||
"threshold_n" INTEGER NOT NULL,
|
||||
"threshold_t" INTEGER NOT NULL,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"last_used_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "party_shares_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "session_states" (
|
||||
"id" VARCHAR(255) NOT NULL,
|
||||
"session_id" VARCHAR(255) NOT NULL,
|
||||
"party_id" VARCHAR(255) NOT NULL,
|
||||
"party_index" INTEGER NOT NULL,
|
||||
"session_type" VARCHAR(20) NOT NULL,
|
||||
"participants" TEXT NOT NULL,
|
||||
"threshold_n" INTEGER NOT NULL,
|
||||
"threshold_t" INTEGER NOT NULL,
|
||||
"status" VARCHAR(20) NOT NULL,
|
||||
"current_round" INTEGER NOT NULL DEFAULT 0,
|
||||
"error_message" TEXT,
|
||||
"public_key" TEXT,
|
||||
"message_hash" VARCHAR(66),
|
||||
"signature" TEXT,
|
||||
"started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completed_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "session_states_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "share_backups" (
|
||||
"id" VARCHAR(255) NOT NULL,
|
||||
"share_id" VARCHAR(255) NOT NULL,
|
||||
"backup_data" TEXT NOT NULL,
|
||||
"backup_type" VARCHAR(20) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" VARCHAR(255),
|
||||
|
||||
CONSTRAINT "share_backups_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_ps_party_id" ON "party_shares"("party_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_ps_session_id" ON "party_shares"("session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_ps_status" ON "party_shares"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "party_shares_party_id_session_id_key" ON "party_shares"("party_id", "session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_ss_session_id" ON "session_states"("session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_ss_party_id" ON "session_states"("party_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_ss_status" ON "session_states"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "session_states_session_id_party_id_key" ON "session_states"("session_id", "party_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_share_id" ON "share_backups"("share_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_created_at" ON "share_backups"("created_at");
|
||||
|
||||
-- =============================================================================
|
||||
-- MPC Service - Database Migration
|
||||
-- =============================================================================
|
||||
--
|
||||
-- mpc-service 作为 MPC 服务网关:
|
||||
-- 1. 缓存 username + publicKey 的映射关系 (mpc_wallets)
|
||||
-- 2. 存储 delegate share (mpc_shares)
|
||||
--
|
||||
-- =============================================================================
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "mpc_wallets" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" VARCHAR(255) NOT NULL,
|
||||
"public_key" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "mpc_wallets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "mpc_shares" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" VARCHAR(255) NOT NULL,
|
||||
"party_id" VARCHAR(255) NOT NULL,
|
||||
"party_index" INTEGER NOT NULL,
|
||||
"encrypted_share" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "mpc_shares_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "mpc_wallets_username_key" ON "mpc_wallets"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_mw_username" ON "mpc_wallets"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_ms_username" ON "mpc_shares"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "uq_ms_username" ON "mpc_shares"("username");
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
|
|
|
|||
Loading…
Reference in New Issue