From de29fa480005c79aabe569fcc6384b6d96f3f09f Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 28 Dec 2025 22:34:35 -0800 Subject: [PATCH] =?UTF-8?q?feat(co-managed-wallet):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E4=BC=9A=E8=AF=9DAPI=E5=92=8CService-Party-A?= =?UTF-8?q?pp=20HTTP=E5=AE=A2=E6=88=B7=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Account Service 新增 API - GET /api/v1/co-managed/sessions/by-invite-code/:inviteCode - 通过邀请码查询keygen会话 - POST /api/v1/co-managed/sign - 创建签名会话 - GET /api/v1/co-managed/sign/by-invite-code/:inviteCode - 通过邀请码查询签名会话 ## Service-Party-App 变更 - 新增 account-client.ts HTTP客户端模块 - 集成Account服务API到Electron主进程 - 添加account相关IPC处理器 - 更新preload.ts暴露account API到渲染进程 - Settings页面添加Account服务URL配置 ## 文档更新 - 更新 docs/service-party-app.md 反映实际实现 - 添加Account Service HTTP API说明 - 添加签名流程文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../adapters/input/http/co_managed_handler.go | 340 ++++++++++++++++++ .../services/account/cmd/server/main.go | 5 +- .../service-party-app/electron/main.ts | 230 +++++++++++- .../electron/modules/account-client.ts | 333 +++++++++++++++++ .../service-party-app/electron/preload.ts | 30 ++ .../service-party-app/src/pages/Settings.tsx | 54 ++- .../service-party-app/src/types/electron.d.ts | 59 +++ docs/service-party-app.md | 324 +++++++++++------ 8 files changed, 1250 insertions(+), 125 deletions(-) create mode 100644 backend/mpc-system/services/service-party-app/electron/modules/account-client.ts diff --git a/backend/mpc-system/services/account/adapters/input/http/co_managed_handler.go b/backend/mpc-system/services/account/adapters/input/http/co_managed_handler.go index e5afb00d..801ae200 100644 --- a/backend/mpc-system/services/account/adapters/input/http/co_managed_handler.go +++ b/backend/mpc-system/services/account/adapters/input/http/co_managed_handler.go @@ -3,6 +3,7 @@ package http import ( "context" "crypto/rand" + "database/sql" "encoding/hex" "fmt" "net/http" @@ -19,6 +20,7 @@ import ( // This is a completely independent handler that does not affect existing functionality type CoManagedHTTPHandler struct { sessionCoordinatorClient *grpcclient.SessionCoordinatorClient + db *sql.DB // Database connection for invite_code lookups } // NewCoManagedHTTPHandler creates a new CoManagedHTTPHandler @@ -27,6 +29,18 @@ func NewCoManagedHTTPHandler( ) *CoManagedHTTPHandler { return &CoManagedHTTPHandler{ sessionCoordinatorClient: sessionCoordinatorClient, + db: nil, + } +} + +// NewCoManagedHTTPHandlerWithDB creates a new CoManagedHTTPHandler with database support +func NewCoManagedHTTPHandlerWithDB( + sessionCoordinatorClient *grpcclient.SessionCoordinatorClient, + db *sql.DB, +) *CoManagedHTTPHandler { + return &CoManagedHTTPHandler{ + sessionCoordinatorClient: sessionCoordinatorClient, + db: db, } } @@ -34,9 +48,15 @@ func NewCoManagedHTTPHandler( func (h *CoManagedHTTPHandler) RegisterRoutes(router *gin.RouterGroup) { coManaged := router.Group("/co-managed") { + // Keygen session routes coManaged.POST("/sessions", h.CreateSession) coManaged.POST("/sessions/:sessionId/join", h.JoinSession) coManaged.GET("/sessions/:sessionId", h.GetSessionStatus) + coManaged.GET("/sessions/by-invite-code/:inviteCode", h.GetSessionByInviteCode) + + // Sign session routes (new - does not affect existing functionality) + coManaged.POST("/sign", h.CreateSignSession) + coManaged.GET("/sign/by-invite-code/:inviteCode", h.GetSignSessionByInviteCode) } } @@ -300,3 +320,323 @@ func (h *CoManagedHTTPHandler) GetSessionStatus(c *gin.Context) { c.JSON(http.StatusOK, result) } + +// ============================================ +// Get Session By Invite Code +// ============================================ + +// GetSessionByInviteCode handles looking up a session by its invite code +// This allows Service-Party-App to find the session_id from an invite_code +func (h *CoManagedHTTPHandler) GetSessionByInviteCode(c *gin.Context) { + inviteCode := c.Param("inviteCode") + if inviteCode == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invite_code is required"}) + return + } + + // Check if database connection is available + if h.db == nil { + logger.Error("Database connection not available for invite_code lookup") + c.JSON(http.StatusInternalServerError, gin.H{"error": "service configuration error"}) + return + } + + // Query database for session by invite_code + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var sessionID string + var walletName string + var thresholdN, thresholdT int + var status string + var expiresAt time.Time + + err := h.db.QueryRowContext(ctx, ` + SELECT id, COALESCE(wallet_name, ''), threshold_n, threshold_t, status, expires_at + FROM mpc_sessions + WHERE invite_code = $1 AND session_type = 'co_managed_keygen' + `, inviteCode).Scan(&sessionID, &walletName, &thresholdN, &thresholdT, &status, &expiresAt) + + if err != nil { + if err == sql.ErrNoRows { + logger.Info("Session not found for invite_code", + zap.String("invite_code", inviteCode)) + c.JSON(http.StatusNotFound, gin.H{"error": "session not found or expired"}) + return + } + logger.Error("Failed to query session by invite_code", + zap.String("invite_code", inviteCode), + zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to lookup session"}) + return + } + + // Check if session is expired + if time.Now().After(expiresAt) { + logger.Info("Session expired for invite_code", + zap.String("invite_code", inviteCode), + zap.String("session_id", sessionID)) + c.JSON(http.StatusGone, gin.H{"error": "session has expired"}) + return + } + + // Get wildcard join token from session coordinator + statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID) + if err != nil { + logger.Error("Failed to get session status from coordinator", + zap.String("session_id", sessionID), + zap.Error(err)) + // Return basic info without join token + c.JSON(http.StatusOK, gin.H{ + "session_id": sessionID, + "wallet_name": walletName, + "threshold_n": thresholdN, + "threshold_t": thresholdT, + "status": status, + "expires_at": expiresAt.UnixMilli(), + }) + return + } + + logger.Info("Found session for invite_code", + zap.String("invite_code", inviteCode), + zap.String("session_id", sessionID), + zap.String("wallet_name", walletName)) + + c.JSON(http.StatusOK, gin.H{ + "session_id": sessionID, + "wallet_name": walletName, + "threshold_n": thresholdN, + "threshold_t": thresholdT, + "status": statusResp.Status, + "completed_parties": statusResp.CompletedParties, + "total_parties": statusResp.TotalParties, + "expires_at": expiresAt.UnixMilli(), + }) +} + +// ============================================ +// Co-Managed Sign Session (NEW - Independent) +// ============================================ + +// SignPartyInfo contains party information for signing +type SignPartyInfo struct { + PartyID string `json:"party_id" binding:"required"` + PartyIndex int32 `json:"party_index" binding:"required"` +} + +// CreateSignSessionRequest represents the request for creating a co-managed sign session +type CreateSignSessionRequest struct { + KeygenSessionID string `json:"keygen_session_id" binding:"required"` // The keygen session that created the wallet + WalletName string `json:"wallet_name"` // Wallet name (for display) + MessageHash string `json:"message_hash" binding:"required"` // Hex-encoded message hash to sign + Parties []SignPartyInfo `json:"parties" binding:"required,min=1"` // Parties to participate in signing (t+1) + ThresholdT int `json:"threshold_t" binding:"required,min=1"` // Signing threshold + InitiatorName string `json:"initiator_name"` // Initiator's display name +} + +// CreateSignSession handles creating a new co-managed sign session +// This is a completely new endpoint that does not affect existing sign functionality +func (h *CoManagedHTTPHandler) CreateSignSession(c *gin.Context) { + var req CreateSignSessionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validate keygen_session_id format + if _, err := uuid.Parse(req.KeygenSessionID); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid keygen_session_id format"}) + return + } + + // Validate message_hash (should be hex encoded) + messageHash, err := hex.DecodeString(req.MessageHash) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "message_hash must be hex encoded"}) + return + } + + // Validate party count >= threshold + 1 + if len(req.Parties) < req.ThresholdT+1 { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("need at least %d parties for threshold %d", req.ThresholdT+1, req.ThresholdT), + }) + return + } + + // Generate invite code for sign session + inviteCode := generateInviteCode() + + // Convert parties to gRPC format + parties := make([]grpcclient.SigningPartyInfo, len(req.Parties)) + for i, p := range req.Parties { + parties[i] = grpcclient.SigningPartyInfo{ + PartyID: p.PartyID, + PartyIndex: p.PartyIndex, + } + } + + // Call session coordinator via gRPC + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + logger.Info("Creating co-managed sign session", + zap.String("keygen_session_id", req.KeygenSessionID), + zap.String("wallet_name", req.WalletName), + zap.Int("threshold_t", req.ThresholdT), + zap.Int("num_parties", len(req.Parties)), + zap.String("invite_code", inviteCode)) + + // Create signing session + // Note: delegateUserShare is nil for co-managed wallets (no delegate party) + resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto( + ctx, + int32(req.ThresholdT), + parties, + messageHash, + 86400, // 24 hour expiry + nil, // No delegate share for co-managed wallets + req.KeygenSessionID, + ) + + if err != nil { + logger.Error("Failed to create co-managed sign session", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Store invite_code mapping in database (for lookup) + if h.db != nil { + _, dbErr := h.db.ExecContext(ctx, ` + UPDATE mpc_sessions + SET invite_code = $1, wallet_name = $2 + WHERE id = $3 + `, inviteCode, req.WalletName, resp.SessionID) + if dbErr != nil { + logger.Warn("Failed to store invite_code for sign session", + zap.String("session_id", resp.SessionID), + zap.Error(dbErr)) + // Don't fail the request, just log the warning + } + } + + // Get wildcard join token for participants + wildcardToken := "" + if token, ok := resp.JoinTokens["*"]; ok { + wildcardToken = token + } + + logger.Info("Co-managed sign session created successfully", + zap.String("session_id", resp.SessionID), + zap.String("invite_code", inviteCode), + zap.Int("num_parties", len(resp.SelectedParties))) + + c.JSON(http.StatusCreated, gin.H{ + "session_id": resp.SessionID, + "keygen_session_id": req.KeygenSessionID, + "wallet_name": req.WalletName, + "invite_code": inviteCode, + "join_token": wildcardToken, + "threshold_t": req.ThresholdT, + "selected_parties": resp.SelectedParties, + "status": "waiting_for_participants", + "current_participants": 0, + "required_participants": len(req.Parties), + "expires_at": resp.ExpiresAt, + }) +} + +// GetSignSessionByInviteCode handles looking up a sign session by its invite code +// This is a completely new endpoint that does not affect existing functionality +func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) { + inviteCode := c.Param("inviteCode") + if inviteCode == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invite_code is required"}) + return + } + + // Check if database connection is available + if h.db == nil { + logger.Error("Database connection not available for sign invite_code lookup") + c.JSON(http.StatusInternalServerError, gin.H{"error": "service configuration error"}) + return + } + + // Query database for sign session by invite_code + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var sessionID string + var walletName string + var keygenSessionID string + var thresholdN, thresholdT int + var status string + var expiresAt time.Time + + err := h.db.QueryRowContext(ctx, ` + SELECT id, COALESCE(wallet_name, ''), COALESCE(keygen_session_id::text, ''), + threshold_n, threshold_t, status, expires_at + FROM mpc_sessions + WHERE invite_code = $1 AND session_type = 'sign' + `, inviteCode).Scan(&sessionID, &walletName, &keygenSessionID, &thresholdN, &thresholdT, &status, &expiresAt) + + if err != nil { + if err == sql.ErrNoRows { + logger.Info("Sign session not found for invite_code", + zap.String("invite_code", inviteCode)) + c.JSON(http.StatusNotFound, gin.H{"error": "sign session not found or expired"}) + return + } + logger.Error("Failed to query sign session by invite_code", + zap.String("invite_code", inviteCode), + zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to lookup sign session"}) + return + } + + // Check if session is expired + if time.Now().After(expiresAt) { + logger.Info("Sign session expired for invite_code", + zap.String("invite_code", inviteCode), + zap.String("session_id", sessionID)) + c.JSON(http.StatusGone, gin.H{"error": "sign session has expired"}) + return + } + + // Get session status from coordinator + statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID) + if err != nil { + logger.Error("Failed to get sign session status from coordinator", + zap.String("session_id", sessionID), + zap.Error(err)) + // Return basic info without detailed status + c.JSON(http.StatusOK, gin.H{ + "session_id": sessionID, + "keygen_session_id": keygenSessionID, + "wallet_name": walletName, + "threshold_n": thresholdN, + "threshold_t": thresholdT, + "status": status, + "expires_at": expiresAt.UnixMilli(), + }) + return + } + + logger.Info("Found sign session for invite_code", + zap.String("invite_code", inviteCode), + zap.String("session_id", sessionID), + zap.String("wallet_name", walletName)) + + c.JSON(http.StatusOK, gin.H{ + "session_id": sessionID, + "keygen_session_id": keygenSessionID, + "wallet_name": walletName, + "threshold_n": thresholdN, + "threshold_t": thresholdT, + "status": statusResp.Status, + "completed_parties": statusResp.CompletedParties, + "total_parties": statusResp.TotalParties, + "expires_at": expiresAt.UnixMilli(), + }) +} diff --git a/backend/mpc-system/services/account/cmd/server/main.go b/backend/mpc-system/services/account/cmd/server/main.go index 0b9b6077..21fbd047 100644 --- a/backend/mpc-system/services/account/cmd/server/main.go +++ b/backend/mpc-system/services/account/cmd/server/main.go @@ -137,6 +137,7 @@ func main() { getRecoveryStatusUC, cancelRecoveryUC, sessionCoordinatorClient, + db, ); err != nil { errChan <- fmt.Errorf("HTTP server error: %w", err) } @@ -239,6 +240,7 @@ func startHTTPServer( getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase, cancelRecoveryUC *use_cases.CancelRecoveryUseCase, sessionCoordinatorClient *grpcadapter.SessionCoordinatorClient, + db *sql.DB, ) error { // Set Gin mode if cfg.Server.Environment == "production" { @@ -301,7 +303,8 @@ func startHTTPServer( }) // Create co-managed wallet handler (independent from existing functionality) - coManagedHandler := httphandler.NewCoManagedHTTPHandler(sessionCoordinatorClient) + // Uses database connection for invite_code lookups + coManagedHandler := httphandler.NewCoManagedHTTPHandlerWithDB(sessionCoordinatorClient, db) // Configure authentication middleware // Skip paths that don't require authentication diff --git a/backend/mpc-system/services/service-party-app/electron/main.ts b/backend/mpc-system/services/service-party-app/electron/main.ts index c89c4970..69b20bb9 100644 --- a/backend/mpc-system/services/service-party-app/electron/main.ts +++ b/backend/mpc-system/services/service-party-app/electron/main.ts @@ -6,6 +6,7 @@ import { GrpcClient } from './modules/grpc-client'; import { DatabaseManager } from './modules/database'; import { addressDerivationService, CHAIN_CONFIGS } from './modules/address-derivation'; import { KavaTxService, KAVA_MAINNET_TX_CONFIG } from './modules/kava-tx-service'; +import { AccountClient } from './modules/account-client'; // 内置 HTTP 服务器端口 const HTTP_PORT = 3456; @@ -15,6 +16,7 @@ let grpcClient: GrpcClient | null = null; let database: DatabaseManager | null = null; let httpServer: ReturnType | null = null; let kavaTxService: KavaTxService | null = null; +let accountClient: AccountClient | null = null; // 创建主窗口 function createWindow() { @@ -92,6 +94,12 @@ async function initServices() { // 初始化 Kava 交易服务 kavaTxService = new KavaTxService(KAVA_MAINNET_TX_CONFIG); + // 初始化 Account 服务 HTTP 客户端 + // 从数据库读取 Account 服务 URL,默认使用生产环境地址 + const settings = database.getAllSettings(); + const accountServiceUrl = settings['account_service_url'] || 'https://api.szaiai.com'; + accountClient = new AccountClient(accountServiceUrl); + // 设置 IPC 处理器 setupIpcHandlers(); } @@ -132,22 +140,70 @@ function setupIpcHandlers() { } }); - // gRPC - 创建会话 - ipcMain.handle('grpc:createSession', async (_event, _params) => { - // TODO: 实现创建会话逻辑 - return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' }; + // gRPC - 创建会话 (通过 Account 服务 HTTP API) + ipcMain.handle('grpc:createSession', async (_event, params) => { + try { + const result = await accountClient?.createKeygenSession({ + wallet_name: params.walletName, + threshold_t: params.thresholdT, + threshold_n: params.thresholdN, + persistent_count: 0, // 服务端 party 数量,共管钱包模式下为 0 + external_count: params.thresholdN, // 所有参与方都是外部 party + expires_in_seconds: 86400, // 24 小时 + }); + return { + success: true, + sessionId: result?.session_id, + inviteCode: result?.invite_code, + walletName: result?.wallet_name, + expiresAt: result?.expires_at, + joinTokens: result?.join_tokens, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } }); - // gRPC - 验证邀请码 - ipcMain.handle('grpc:validateInviteCode', async (_event, { code: _code }) => { - // TODO: 实现验证邀请码逻辑 - return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' }; + // gRPC - 验证邀请码 (通过 Account 服务 HTTP API) + ipcMain.handle('grpc:validateInviteCode', async (_event, { code }) => { + try { + const result = await accountClient?.getSessionByInviteCode(code); + return { + success: true, + sessionInfo: { + sessionId: result?.session_id, + walletName: result?.wallet_name, + threshold: { + t: result?.threshold_t, + n: result?.threshold_n, + }, + status: result?.status, + currentParticipants: result?.joined_parties || 0, + }, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } }); - // gRPC - 获取会话状态 - ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId: _sessionId }) => { - // TODO: 实现获取会话状态逻辑 - return { success: false, error: '功能尚未实现' }; + // gRPC - 获取会话状态 (通过 Account 服务 HTTP API) + ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId }) => { + try { + const result = await accountClient?.getSessionStatus(sessionId); + return { + success: true, + session: { + sessionId: result?.session_id, + status: result?.status, + completedParties: result?.completed_parties, + totalParties: result?.total_parties, + sessionType: result?.session_type, + publicKey: result?.public_key, + }, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } }); // gRPC - 测试连接 @@ -161,16 +217,152 @@ function setupIpcHandlers() { } }); - // gRPC - 验证签名会话 - ipcMain.handle('grpc:validateSigningSession', async (_event, { code: _code }) => { - // TODO: 实现验证签名会话逻辑 - return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' }; + // gRPC - 验证签名会话 (通过 Account 服务 HTTP API) + ipcMain.handle('grpc:validateSigningSession', async (_event, { code }) => { + try { + const result = await accountClient?.getSignSessionByInviteCode(code); + return { + success: true, + session: { + sessionId: result?.session_id, + keygenSessionId: result?.keygen_session_id, + walletName: result?.wallet_name, + messageHash: result?.message_hash, + threshold: { + t: result?.threshold_t, + n: result?.parties?.length || 0, + }, + currentParticipants: result?.joined_count || 0, + status: result?.status, + parties: result?.parties, + }, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } }); // gRPC - 加入签名会话 - ipcMain.handle('grpc:joinSigningSession', async (_event, _params) => { - // TODO: 实现加入签名会话逻辑 - return { success: false, error: '功能尚未实现' }; + ipcMain.handle('grpc:joinSigningSession', async (_event, params) => { + try { + // 从本地 SQLite 获取 share 数据 + const share = database?.getShare(params.shareId, params.password); + if (!share) { + return { success: false, error: 'Share not found or incorrect password' }; + } + + // 加入签名会话 (通过 gRPC) + // TODO: 实际加入会话逻辑需要使用 gRPC client + // 这里先返回成功,表示验证通过 + return { + success: true, + partyId: share.party_id, + partyIndex: share.party_index, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // =========================================================================== + // Account 服务相关 (HTTP API) + // =========================================================================== + + // 创建签名会话 + ipcMain.handle('account:createSignSession', async (_event, params) => { + try { + // 从本地 SQLite 获取 share 数据 + const share = database?.getShare(params.shareId, params.password); + if (!share) { + return { success: false, error: 'Share not found or incorrect password' }; + } + + // 解析 participants_json 获取参与方列表 + const participants = JSON.parse(share.participants_json || '[]'); + const parties = participants.map((p: { partyId: string }, index: number) => ({ + party_id: p.partyId, + party_index: index, + })); + + const result = await accountClient?.createSignSession({ + keygen_session_id: share.session_id, + wallet_name: share.wallet_name, + message_hash: params.messageHash, + parties: parties, + threshold_t: share.threshold_t, + initiator_name: params.initiatorName, + }); + + return { + success: true, + sessionId: result?.session_id, + inviteCode: result?.invite_code, + expiresAt: result?.expires_at, + joinToken: result?.join_token, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // 获取签名会话状态 (通过邀请码) + ipcMain.handle('account:getSignSessionByInviteCode', async (_event, { inviteCode }) => { + try { + const result = await accountClient?.getSignSessionByInviteCode(inviteCode); + return { + success: true, + session: { + sessionId: result?.session_id, + keygenSessionId: result?.keygen_session_id, + walletName: result?.wallet_name, + messageHash: result?.message_hash, + thresholdT: result?.threshold_t, + status: result?.status, + inviteCode: result?.invite_code, + expiresAt: result?.expires_at, + parties: result?.parties, + joinedCount: result?.joined_count, + }, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // Account 服务健康检查 + ipcMain.handle('account:healthCheck', async () => { + try { + const result = await accountClient?.healthCheck(); + return { success: true, data: result }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // 测试 Account 服务连接 + ipcMain.handle('account:testConnection', async () => { + try { + const connected = await accountClient?.testConnection(); + return { success: connected }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // 更新 Account 服务 URL + ipcMain.handle('account:updateUrl', async (_event, { url }) => { + try { + accountClient?.setBaseUrl(url); + database?.setSetting('account_service_url', url); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // 获取 Account 服务 URL + ipcMain.handle('account:getUrl', async () => { + return accountClient?.getBaseUrl() || 'https://api.szaiai.com'; }); // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/electron/modules/account-client.ts b/backend/mpc-system/services/service-party-app/electron/modules/account-client.ts new file mode 100644 index 00000000..b8f1be8f --- /dev/null +++ b/backend/mpc-system/services/service-party-app/electron/modules/account-client.ts @@ -0,0 +1,333 @@ +/** + * Account Service HTTP Client + * + * 用于 Service-Party-App 调用 Account 服务的 HTTP API + * 主要用于创建/查询 keygen 和 sign 会话 + */ + +// ============================================================================= +// 类型定义 +// ============================================================================= + +// Keygen 会话相关 +export interface CreateKeygenSessionRequest { + wallet_name: string; + threshold_t: number; + threshold_n: number; + persistent_count: number; + external_count: number; + expires_in_seconds?: number; +} + +export interface CreateKeygenSessionResponse { + session_id: string; + invite_code: string; + wallet_name: string; + threshold_n: number; + threshold_t: number; + selected_server_parties: string[]; + join_tokens: Record; + expires_at: number; +} + +export interface JoinSessionRequest { + party_id: string; + join_token: string; + device_type?: string; + device_id?: string; +} + +export interface PartyInfo { + party_id: string; + party_index: number; +} + +export interface SessionInfo { + session_id: string; + session_type: string; + threshold_n: number; + threshold_t: number; + status: string; + wallet_name: string; + invite_code: string; + keygen_session_id?: string; +} + +export interface JoinSessionResponse { + success: boolean; + party_index: number; + session_info: SessionInfo; + other_parties: PartyInfo[]; +} + +export interface GetSessionStatusResponse { + session_id: string; + status: string; + completed_parties: number; + total_parties: number; + session_type: string; + public_key?: string; + signature?: string; + has_delegate: boolean; +} + +export interface GetSessionByInviteCodeResponse { + session_id: string; + wallet_name: string; + threshold_n: number; + threshold_t: number; + status: string; + invite_code: string; + expires_at: number; + joined_parties: number; +} + +// Sign 会话相关 +export interface SignPartyInfo { + party_id: string; + party_index: number; +} + +export interface CreateSignSessionRequest { + keygen_session_id: string; + wallet_name: string; + message_hash: string; + parties: SignPartyInfo[]; + threshold_t: number; + initiator_name?: string; +} + +export interface CreateSignSessionResponse { + session_id: string; + invite_code: string; + keygen_session_id: string; + message_hash: string; + threshold_t: number; + parties: SignPartyInfo[]; + expires_at: number; + join_token: string; +} + +export interface GetSignSessionByInviteCodeResponse { + session_id: string; + keygen_session_id: string; + wallet_name: string; + message_hash: string; + threshold_t: number; + status: string; + invite_code: string; + expires_at: number; + parties: SignPartyInfo[]; + joined_count: number; +} + +// 错误响应 +export interface ErrorResponse { + error: string; + message?: string; +} + +// ============================================================================= +// HTTP 客户端类 +// ============================================================================= + +export class AccountClient { + private baseUrl: string; + private timeout: number; + + /** + * 构造函数 + * @param baseUrl Account 服务的基础 URL (例如: https://api.szaiai.com 或 http://localhost:8080) + * @param timeout 请求超时时间 (毫秒) + */ + constructor(baseUrl: string, timeout: number = 30000) { + // 移除末尾的斜杠 + this.baseUrl = baseUrl.replace(/\/$/, ''); + this.timeout = timeout; + } + + /** + * 更新基础 URL + */ + setBaseUrl(baseUrl: string): void { + this.baseUrl = baseUrl.replace(/\/$/, ''); + } + + /** + * 获取当前基础 URL + */ + getBaseUrl(): string { + return this.baseUrl; + } + + /** + * 发送 HTTP 请求 + */ + private async request( + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + path: string, + body?: unknown + ): Promise { + const url = `${this.baseUrl}${path}`; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + + try { + const options: RequestInit = { + method, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + signal: controller.signal, + }; + + if (body) { + options.body = JSON.stringify(body); + } + + console.log(`[AccountClient] ${method} ${url}`, body ? JSON.stringify(body) : ''); + + const response = await fetch(url, options); + + const text = await response.text(); + let data: T | ErrorResponse; + + try { + data = JSON.parse(text); + } catch { + throw new Error(`Invalid JSON response: ${text}`); + } + + if (!response.ok) { + const errorData = data as ErrorResponse; + throw new Error(errorData.message || errorData.error || `HTTP ${response.status}`); + } + + console.log(`[AccountClient] Response:`, data); + return data as T; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Request timeout after ${this.timeout}ms`); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } + + // =========================================================================== + // Keygen 会话 API + // =========================================================================== + + /** + * 创建 Keygen 会话 + */ + async createKeygenSession( + params: CreateKeygenSessionRequest + ): Promise { + return this.request( + 'POST', + '/api/v1/co-managed/sessions', + params + ); + } + + /** + * 加入会话 + */ + async joinSession( + sessionId: string, + params: JoinSessionRequest + ): Promise { + return this.request( + 'POST', + `/api/v1/co-managed/sessions/${sessionId}/join`, + params + ); + } + + /** + * 获取会话状态 + */ + async getSessionStatus(sessionId: string): Promise { + return this.request( + 'GET', + `/api/v1/co-managed/sessions/${sessionId}` + ); + } + + /** + * 通过邀请码查询 Keygen 会话 + */ + async getSessionByInviteCode(inviteCode: string): Promise { + return this.request( + 'GET', + `/api/v1/co-managed/sessions/by-invite-code/${inviteCode}` + ); + } + + // =========================================================================== + // Sign 会话 API + // =========================================================================== + + /** + * 创建 Sign 会话 + */ + async createSignSession( + params: CreateSignSessionRequest + ): Promise { + return this.request( + 'POST', + '/api/v1/co-managed/sign', + params + ); + } + + /** + * 通过邀请码查询 Sign 会话 + */ + async getSignSessionByInviteCode(inviteCode: string): Promise { + return this.request( + 'GET', + `/api/v1/co-managed/sign/by-invite-code/${inviteCode}` + ); + } + + // =========================================================================== + // 健康检查 + // =========================================================================== + + /** + * 健康检查 + */ + async healthCheck(): Promise<{ status: string; service: string }> { + return this.request<{ status: string; service: string }>( + 'GET', + '/health' + ); + } + + /** + * 测试连接 + */ + async testConnection(): Promise { + try { + const result = await this.healthCheck(); + return result.status === 'healthy'; + } catch { + return false; + } + } +} + +// ============================================================================= +// 默认实例 +// ============================================================================= + +// 默认使用生产环境地址 +const DEFAULT_ACCOUNT_SERVICE_URL = 'https://api.szaiai.com'; + +// 创建默认客户端实例 +export const accountClient = new AccountClient(DEFAULT_ACCOUNT_SERVICE_URL); diff --git a/backend/mpc-system/services/service-party-app/electron/preload.ts b/backend/mpc-system/services/service-party-app/electron/preload.ts index 6777b2e5..9520c7b8 100644 --- a/backend/mpc-system/services/service-party-app/electron/preload.ts +++ b/backend/mpc-system/services/service-party-app/electron/preload.ts @@ -67,6 +67,36 @@ contextBridge.exposeInMainWorld('electronAPI', { }, }, + // =========================================================================== + // Account 服务相关 (HTTP API) + // =========================================================================== + account: { + // 创建签名会话 + createSignSession: (params: { + shareId: string; + password: string; + messageHash: string; + initiatorName?: string; + }) => ipcRenderer.invoke('account:createSignSession', params), + + // 获取签名会话状态 (通过邀请码) + getSignSessionByInviteCode: (inviteCode: string) => + ipcRenderer.invoke('account:getSignSessionByInviteCode', { inviteCode }), + + // 健康检查 + healthCheck: () => ipcRenderer.invoke('account:healthCheck'), + + // 测试连接 + testConnection: () => ipcRenderer.invoke('account:testConnection'), + + // 更新服务 URL + updateUrl: (url: string) => + ipcRenderer.invoke('account:updateUrl', { url }), + + // 获取当前服务 URL + getUrl: () => ipcRenderer.invoke('account:getUrl'), + }, + // =========================================================================== // 存储相关 (SQLite) // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/src/pages/Settings.tsx b/backend/mpc-system/services/service-party-app/src/pages/Settings.tsx index 2d684cf6..a9fa5e56 100644 --- a/backend/mpc-system/services/service-party-app/src/pages/Settings.tsx +++ b/backend/mpc-system/services/service-party-app/src/pages/Settings.tsx @@ -4,6 +4,7 @@ import styles from './Settings.module.css'; interface Settings { messageRouterUrl: string; + accountServiceUrl: string; autoBackup: boolean; backupPath: string; } @@ -13,6 +14,7 @@ export default function Settings() { const [settings, setSettings] = useState({ messageRouterUrl: 'mpc-grpc.szaiai.com:443', // 生产环境默认地址 + accountServiceUrl: 'https://api.szaiai.com', // Account 服务默认地址 autoBackup: false, backupPath: '', }); @@ -27,8 +29,12 @@ export default function Settings() { const loadSettings = async () => { try { const result = await window.electronAPI.storage.getSettings(); + const accountUrl = await window.electronAPI.account.getUrl(); if (result) { - setSettings(result); + setSettings({ + ...result, + accountServiceUrl: accountUrl || 'https://api.szaiai.com', + }); } } catch (err) { console.error('Failed to load settings:', err); @@ -43,6 +49,7 @@ export default function Settings() { try { await window.electronAPI.storage.saveSettings(settings); + await window.electronAPI.account.updateUrl(settings.accountServiceUrl); setMessage({ type: 'success', text: '设置已保存' }); } catch (err) { setMessage({ type: 'error', text: '保存设置失败' }); @@ -57,12 +64,29 @@ export default function Settings() { try { const result = await window.electronAPI.grpc.testConnection(settings.messageRouterUrl); if (result.success) { - setMessage({ type: 'success', text: '连接成功' }); + setMessage({ type: 'success', text: 'Message Router 连接成功' }); } else { - setMessage({ type: 'error', text: result.error || '连接失败' }); + setMessage({ type: 'error', text: result.error || 'Message Router 连接失败' }); } } catch (err) { - setMessage({ type: 'error', text: '连接测试失败' }); + setMessage({ type: 'error', text: 'Message Router 连接测试失败' }); + } + }; + + const handleTestAccountConnection = async () => { + setMessage(null); + + try { + // 先更新 URL + await window.electronAPI.account.updateUrl(settings.accountServiceUrl); + const result = await window.electronAPI.account.testConnection(); + if (result.success) { + setMessage({ type: 'success', text: 'Account 服务连接成功' }); + } else { + setMessage({ type: 'error', text: result.error || 'Account 服务连接失败' }); + } + } catch (err) { + setMessage({ type: 'error', text: 'Account 服务连接测试失败' }); } }; @@ -120,6 +144,28 @@ export default function Settings() { 输入 Message Router 服务的 gRPC 地址 (生产环境: mpc-grpc.szaiai.com:443)

+ +
+ +
+ setSettings(prev => ({ ...prev, accountServiceUrl: e.target.value }))} + placeholder="https://api.szaiai.com" + className={styles.input} + /> + +
+

+ 输入 Account 服务的 HTTP 地址 (生产环境: https://api.szaiai.com) +

+
diff --git a/backend/mpc-system/services/service-party-app/src/types/electron.d.ts b/backend/mpc-system/services/service-party-app/src/types/electron.d.ts index 573c16a8..0909d45b 100644 --- a/backend/mpc-system/services/service-party-app/src/types/electron.d.ts +++ b/backend/mpc-system/services/service-party-app/src/types/electron.d.ts @@ -178,6 +178,55 @@ interface JoinSigningSessionParams { } interface JoinSigningSessionResult { + success: boolean; + partyId?: string; + partyIndex?: number; + error?: string; +} + +// Account 服务相关类型 +interface CreateSignSessionParams { + shareId: string; + password: string; + messageHash: string; + initiatorName?: string; +} + +interface CreateSignSessionResult { + success: boolean; + sessionId?: string; + inviteCode?: string; + expiresAt?: number; + joinToken?: string; + error?: string; +} + +interface SignSessionInfo { + sessionId: string; + keygenSessionId: string; + walletName: string; + messageHash: string; + thresholdT: number; + status: string; + inviteCode: string; + expiresAt: number; + parties: Array<{ party_id: string; party_index: number }>; + joinedCount: number; +} + +interface GetSignSessionByInviteCodeResult { + success: boolean; + session?: SignSessionInfo; + error?: string; +} + +interface AccountHealthCheckResult { + success: boolean; + data?: { status: string; service: string }; + error?: string; +} + +interface AccountTestConnectionResult { success: boolean; error?: string; } @@ -346,6 +395,16 @@ interface KavaHealthCheckResult { // =========================================================================== interface ElectronAPI { + // Account 服务相关 (HTTP API) + account: { + createSignSession: (params: CreateSignSessionParams) => Promise; + getSignSessionByInviteCode: (inviteCode: string) => Promise; + healthCheck: () => Promise; + testConnection: () => Promise; + updateUrl: (url: string) => Promise<{ success: boolean; error?: string }>; + getUrl: () => Promise; + }; + // gRPC 相关 grpc: { createSession: (params: CreateSessionParams) => Promise; diff --git a/docs/service-party-app.md b/docs/service-party-app.md index e0dc8273..9ee86bcf 100644 --- a/docs/service-party-app.md +++ b/docs/service-party-app.md @@ -2,38 +2,56 @@ ## 概述 -Service Party App 是一个**跨平台 Electron 桌面应用**,用于参与分布式多方共管钱包的创建和签名过程。用户可以在各自的电脑上运行此应用,通过扫描邀请二维码加入共管钱包创建会话,参与 TSS (Threshold Signature Scheme) 密钥生成协议。 +Service Party App 是一个**跨平台 Electron 桌面应用**,用于参与分布式多方共管钱包的创建和签名过程。用户可以在各自的电脑上运行此应用,通过扫描邀请二维码或输入邀请码加入共管钱包创建会话,参与 TSS (Threshold Signature Scheme) 密钥生成协议。 ## 应用场景 ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Admin Web │ │ Service Party │ │ Service Party │ -│ (管理员电脑) │ │ App (用户A电脑) │ │ App (用户B电脑) │ +│ Service Party │ │ Service Party │ │ Service Party │ +│ App (用户A) │ │ App (用户B) │ │ App (用户C) │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ - │ 创建会话/生成邀请码 │ 扫码加入 │ 扫码加入 + │ 创建会话/生成邀请码 │ 输入邀请码加入 │ 输入邀请码加入 │ │ │ └───────────────────────┼───────────────────────┘ │ ┌────────────┴────────────┐ + │ Account Service │ + │ (HTTP API - 会话管理) │ + └────────────┬────────────┘ + │ + ┌────────────┴────────────┐ │ Message Router │ │ (gRPC 消息路由) │ └────────────┬────────────┘ │ ┌────────────┴────────────┐ - │ TSS Keygen Protocol │ - │ (分布式密钥生成) │ + │ TSS Keygen/Sign │ + │ (分布式密钥生成/签名) │ └─────────────────────────┘ ``` ### 典型使用流程 -1. **管理员** 在 Admin Web 创建共管钱包会话,配置阈值 (如 2-of-3) -2. **管理员** 生成邀请二维码,分享给参与方 -3. **参与方** 在各自电脑上打开 Service Party App,扫描二维码加入 -4. **所有参与方就绪后**,自动开始 TSS 密钥生成协议 -5. **完成后**,每个参与方本地保存自己的加密 share,管理员获得钱包公钥 +#### Keygen 流程 (密钥生成) + +1. **发起方** 在 Service Party App 输入钱包名称和用户名称,创建共管钱包会话 +2. **发起方** 获得邀请码 (格式: XXXX-XXXX-XXXX),分享给其他参与方 +3. **参与方** 在各自电脑上打开 Service Party App,输入邀请码加入 +4. **等待所有参与方就绪** (24小时超时) +5. **所有参与方到齐后**,自动开始 TSS 密钥生成协议 +6. **完成后**,每个参与方在本地 SQLite 保存自己的加密 share + +#### Sign 流程 (签名) + +1. **发起方** 从本地 SQLite 选择要签名的钱包 +2. **发起方** 输入待签名的消息哈希,创建签名会话 +3. **发起方** 获得签名邀请码,分享给其他参与方 (必须是 keygen 时的参与方) +4. **参与方** 输入邀请码加入签名会话 +5. **等待足够数量的参与方** (threshold_t + 1 人,24小时超时) +6. **参与方到齐后**,自动开始 TSS 签名协议 +7. **完成后**,返回签名结果,可用于广播交易 ## 目录结构 @@ -44,45 +62,44 @@ backend/mpc-system/services/service-party-app/ │ ├── preload.ts # 预加载脚本 (安全 IPC) │ └── modules/ │ ├── grpc-client.ts # gRPC 客户端 (连接 Message Router) +│ ├── account-client.ts # HTTP 客户端 (连接 Account Service) │ ├── tss-handler.ts # TSS 协议处理器 -│ └── storage.ts # 本地加密存储 (AES-256-GCM) +│ ├── database.ts # 本地 SQLite 存储 (AES-256-GCM 加密) +│ ├── address-derivation.ts # 地址派生 (多链支持) +│ ├── kava-client.ts # Kava 区块链客户端 +│ └── kava-tx-service.ts # Kava 交易构建服务 │ ├── src/ # React 前端 (渲染进程) │ ├── App.tsx # 应用入口 │ ├── pages/ │ │ ├── Home.tsx # 主页 - Share 列表 │ │ ├── Join.tsx # 加入会话页面 -│ │ ├── Create.tsx # 创建会话页面 (可选) +│ │ ├── Create.tsx # 创建会话页面 │ │ ├── Session.tsx # 会话进度页面 +│ │ ├── Sign.tsx # 签名页面 │ │ └── Settings.tsx # 设置页面 -│ ├── components/ -│ │ ├── ShareCard.tsx # Share 卡片组件 -│ │ ├── JoinForm.tsx # 加入表单 -│ │ ├── ProgressSteps.tsx # 进度步骤 -│ │ ├── ExportDialog.tsx # 导出对话框 -│ │ └── ImportDialog.tsx # 导入对话框 -│ └── hooks/ -│ ├── useGrpc.ts # gRPC 连接 Hook -│ ├── useSession.ts # 会话状态 Hook -│ └── useStorage.ts # 本地存储 Hook +│ ├── components/ # UI 组件 +│ ├── stores/ +│ │ └── appStore.ts # Zustand 状态管理 +│ ├── types/ +│ │ └── electron.d.ts # TypeScript 类型定义 +│ └── utils/ +│ └── address.ts # 地址工具函数 │ ├── tss-party/ # Go TSS 子进程 │ ├── main.go # TSS 协议执行程序 │ ├── go.mod # Go 模块定义 │ └── go.sum # 依赖锁定 │ -├── bin/ # 编译后的二进制文件 -│ ├── win32-x64/ -│ │ └── tss-party.exe -│ ├── darwin-x64/ -│ │ └── tss-party -│ └── linux-x64/ -│ └── tss-party +├── proto/ # gRPC Proto 文件 +│ └── message_router.proto # Message Router 接口定义 │ ├── package.json # Node.js 依赖 ├── electron-builder.json # Electron 打包配置 ├── tsconfig.json # TypeScript 配置 -└── vite.config.ts # Vite 构建配置 +├── tsconfig.electron.json # Electron TypeScript 配置 +├── vite.config.ts # Vite 构建配置 +└── build-windows.bat # Windows 一键编译脚本 ``` ## 技术架构 @@ -95,12 +112,50 @@ backend/mpc-system/services/service-party-app/ | 前端框架 | React 18 + TypeScript | 与 admin-web 保持一致 | | 构建工具 | Vite | 快速开发和构建 | | 状态管理 | Zustand | 轻量级状态管理 | -| gRPC 客户端 | @grpc/grpc-js | Node.js gRPC 实现 | +| gRPC 客户端 | @grpc/grpc-js | Node.js gRPC 实现 (连接 Message Router) | +| HTTP 客户端 | Fetch API | 连接 Account Service | | TSS 协议 | Go 子进程 | 使用 bnb-chain/tss-lib | -| 本地存储 | electron-store | 加密本地存储 | -| 加密算法 | AES-256-GCM | Share 加密存储 | +| 本地存储 | sql.js (SQLite) | 纯 JavaScript SQLite 实现 | +| 加密算法 | AES-256-GCM | Share 加密存储,PBKDF2 密钥派生 | | 打包工具 | electron-builder | 多平台打包 | +### 服务连接 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Service Party App │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ main.ts ││ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ +│ │ │ grpcClient │ │accountClient │ │ database │ ││ +│ │ │ (gRPC) │ │ (HTTP) │ │ (SQLite) │ ││ +│ │ └──────┬───────┘ └──────┬───────┘ └──────────────┘ ││ +│ │ │ │ ││ +│ └─────────┼─────────────────┼──────────────────────────────┘│ +└────────────┼─────────────────┼───────────────────────────────┘ + │ │ + │ gRPC (TLS) │ HTTPS + ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ +│ Message Router │ │ Account Service │ +│ (消息路由) │ │ (会话管理) │ +└─────────────────┘ └─────────────────┘ +``` + +### Account Service HTTP API + +Service Party App 通过 HTTP 调用 Account Service 管理会话: + +| 端点 | 方法 | 说明 | +|------|------|------| +| `/api/v1/co-managed/sessions` | POST | 创建 Keygen 会话 | +| `/api/v1/co-managed/sessions/:sessionId/join` | POST | 加入会话 | +| `/api/v1/co-managed/sessions/:sessionId` | GET | 获取会话状态 | +| `/api/v1/co-managed/sessions/by-invite-code/:code` | GET | 通过邀请码查询会话 | +| `/api/v1/co-managed/sign` | POST | 创建 Sign 会话 | +| `/api/v1/co-managed/sign/by-invite-code/:code` | GET | 通过邀请码查询签名会话 | +| `/health` | GET | 健康检查 | + ### TSS 子进程架构 为什么使用 Go 子进程而不是 WASM? @@ -128,9 +183,10 @@ backend/mpc-system/services/service-party-app/ │ ┌────────────────────────────────────────────────────────┐ │ │ │ - 使用 bnb-chain/tss-lib v2 │ │ │ │ - 执行 GG20 Keygen 协议 (4 轮) │ │ +│ │ - 执行 GG20 Signing 协议 │ │ │ │ - 通过 stdin 接收 MPC 消息 │ │ │ │ - 通过 stdout 发送 MPC 消息 │ │ -│ │ - 完成后返回公钥 + 加密 share │ │ +│ │ - 完成后返回公钥 + 加密 share / 签名结果 │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ ``` @@ -154,8 +210,9 @@ interface OutgoingMessage { isBroadcast?: boolean; toParties?: string[]; payload?: string; // base64 encoded - publicKey?: string; // base64 encoded (result) - encryptedShare?: string; // base64 encoded (result) + publicKey?: string; // base64 encoded (keygen result) + encryptedShare?: string; // base64 encoded (keygen result) + signature?: string; // base64 encoded (sign result) partyIndex?: number; round?: number; totalRounds?: number; @@ -167,13 +224,13 @@ interface OutgoingMessage { ### 1. 加入会话 -用户通过以下方式加入共管钱包创建会话: +用户通过以下方式加入共管钱包创建/签名会话: -- **扫描二维码**: 使用摄像头扫描 Admin Web 生成的邀请二维码 -- **粘贴邀请链接**: 手动粘贴邀请链接 -- **输入邀请码**: 手动输入 6 位邀请码 +- **输入邀请码**: 手动输入 12 位邀请码 (格式: XXXX-XXXX-XXXX) +- **扫描二维码**: 使用摄像头扫描邀请二维码 (可选) +- **粘贴邀请链接**: 手动粘贴邀请链接 (可选) -### 2. TSS 密钥生成 +### 2. TSS 密钥生成 (Keygen) 参与 GG20 (Gennaro-Goldfeder 2020) 门限签名密钥生成协议: @@ -181,35 +238,58 @@ interface OutgoingMessage { - 4 轮消息交换 - 零知识证明保证安全性 - 每个参与方获得自己的 share,无需信任其他方 +- 24 小时超时等待所有参与方 -### 3. 本地加密存储 +### 3. TSS 签名 (Sign) -Share 使用 AES-256-GCM 加密后存储在本地: +参与 GG20 门限签名协议: + +- 使用 Keygen 时保存的 share +- 仅需 threshold_t + 1 个参与方即可签名 +- 24 小时超时等待足够参与方 +- 返回可用于广播的签名结果 + +### 4. 本地 SQLite 存储 + +Share 使用 AES-256-GCM 加密后存储在本地 SQLite 数据库: ```typescript -interface ShareEntry { - id: string; // Share 唯一标识 - sessionId: string; // 会话 ID - walletName: string; // 钱包名称 - partyId: string; // 参与方 ID - partyIndex: number; // 参与方索引 - threshold: { - t: number; // 签名阈值 - n: number; // 总参与方数 - }; - publicKey: string; // 钱包公钥 (hex) - encryptedShare: string; // AES-256-GCM 加密的 share - createdAt: string; // 创建时间 - metadata: { - participants: Array<{ - partyId: string; - name: string; - }>; - }; +interface ShareRecord { + id: string; // Share 唯一标识 (UUID) + session_id: string; // Keygen 会话 ID + wallet_name: string; // 钱包名称 + party_id: string; // 参与方 ID + party_index: number; // 参与方索引 + threshold_t: number; // 签名阈值 + threshold_n: number; // 总参与方数 + public_key_hex: string; // 钱包公钥 (hex) + encrypted_share: string; // AES-256-GCM 加密的 share + created_at: string; // 创建时间 + last_used_at: string | null; // 最后使用时间 + participants_json: string; // JSON: 参与方列表 [{partyId, name}] } ``` -### 4. 备份与恢复 +**重要**: `participants_json` 字段存储了 Keygen 时所有参与方的信息,Sign 时必须从这里选择参与方。 + +### 5. 地址派生 + +支持从公钥派生多链地址: + +- **Kava** (主链) +- **Cosmos** 系列链 +- **Ethereum** 兼容链 + +### 6. Kava 交易 + +支持构建和广播 Kava 交易: + +- 查询余额 +- 构建转账交易 (待 TSS 签名) +- 广播已签名交易 +- 查询交易状态 + +### 7. 备份与恢复 - **导出备份**: 将加密的 share 导出为文件 - **导入恢复**: 从备份文件恢复 share @@ -221,7 +301,14 @@ interface ShareEntry { - Node.js 18+ - Go 1.21+ -- pnpm 或 npm +- npm 或 pnpm + +### Windows 一键编译 + +```bash +cd backend/mpc-system/services/service-party-app +build-windows.bat +``` ### 编译 TSS 子进程 @@ -230,9 +317,12 @@ interface ShareEntry { cd backend/mpc-system/services/service-party-app/tss-party go build -o ../bin/win32-x64/tss-party.exe . -# macOS +# macOS (Intel) GOOS=darwin GOARCH=amd64 go build -o ../bin/darwin-x64/tss-party . +# macOS (Apple Silicon) +GOOS=darwin GOARCH=arm64 go build -o ../bin/darwin-arm64/tss-party . + # Linux GOOS=linux GOARCH=amd64 go build -o ../bin/linux-x64/tss-party . ``` @@ -252,39 +342,51 @@ npm run dev ### 生产构建 ```bash -# 构建前端 +# 构建前端 + Electron npm run build -# 打包 Electron 应用 -npm run package +# 打包 Windows 安装包 +npm run build:win -# 输出目录: dist/ +# 输出目录: release/ ``` +## 配置 + +### 设置页面配置项 + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| Message Router 地址 | gRPC 服务地址 | mpc-grpc.szaiai.com:443 | +| Account 服务地址 | HTTP API 地址 | https://api.szaiai.com | +| 自动备份 | 创建 share 后自动备份 | false | +| 备份目录 | 自动备份文件保存位置 | (空) | + ## 安全考虑 ### Share 安全 1. **本地加密**: Share 使用 AES-256-GCM 加密存储 -2. **密钥派生**: 加密密钥由用户密码通过 PBKDF2 派生 +2. **密钥派生**: 加密密钥由用户密码通过 PBKDF2 派生 (100,000 次迭代) 3. **内存保护**: Share 在内存中的时间尽量短 -4. **安全删除**: 删除 share 时安全擦除 +4. **安全删除**: 删除 share 时同时删除派生地址和签名历史 ### 网络安全 -1. **TLS 加密**: 与 Message Router 的 gRPC 连接使用 TLS -2. **消息签名**: MPC 消息包含签名验证 -3. **会话隔离**: 每个会话使用独立的密钥对 +1. **TLS 加密**: 与 Message Router 的 gRPC 连接使用 TLS (端口 443) +2. **HTTPS**: 与 Account Service 的 HTTP 连接使用 HTTPS +3. **消息签名**: MPC 消息包含签名验证 +4. **会话隔离**: 每个会话使用独立的密钥对 ### 应用安全 1. **代码签名**: 发布的应用经过代码签名 -2. **自动更新**: 支持安全的自动更新机制 -3. **沙箱隔离**: Electron 渲染进程在沙箱中运行 +2. **沙箱隔离**: Electron 渲染进程在沙箱中运行 +3. **Context Isolation**: preload 脚本使用 contextBridge 安全暴露 API ## 与现有系统的集成 -### 与 Message Router 的通信 +### 与 Message Router 的通信 (gRPC) ``` Service Party App @@ -295,7 +397,7 @@ Service Party App │ Message Router │ │ ┌───────────────┐ │ │ │ RegisterParty │ │ ← 注册为参与方 -│ │ Heartbeat │ │ ← 心跳保活 +│ │ Heartbeat │ │ ← 心跳保活 (30秒) │ │ JoinSession │ │ ← 加入会话 │ │ Subscribe │ │ ← 订阅消息 │ │ RouteMessage │ │ ← 发送消息 @@ -304,36 +406,56 @@ Service Party App └─────────────────────┘ ``` -### 与 Admin Web 的协作 +### 与 Account Service 的通信 (HTTP) -1. Admin Web 创建 `co_managed_keygen` 类型的会话 -2. Session Coordinator 生成邀请码 -3. Admin Web 显示邀请二维码 -4. Service Party App 扫码获取会话信息 -5. 双方通过 Message Router 交换 MPC 消息 -6. 完成后各自获得结果 +``` +Service Party App + │ + │ HTTPS + ▼ +┌─────────────────────┐ +│ Account Service │ +│ ┌───────────────┐ │ +│ │ CreateSession │ │ ← 创建 Keygen/Sign 会话 +│ │ JoinSession │ │ ← 加入会话 +│ │ GetSession │ │ ← 查询会话状态 +│ │ ByInviteCode │ │ ← 通过邀请码查询 +│ └───────────────┘ │ +└─────────────────────┘ +``` -## 未来扩展 +### 数据流 -### 签名功能 +1. **Keygen 创建**: + - App → Account Service: 创建会话,获取 session_id + invite_code + - App → Message Router: RegisterParty + - App → Message Router: SubscribeSessionEvents + - 等待其他参与方加入... -当前版本仅支持密钥生成,未来将支持: +2. **Keygen 加入**: + - App → Account Service: 通过 invite_code 查询 session_id + - App → Message Router: RegisterParty + - App → Account Service: JoinSession + - 等待所有参与方就绪... -- 参与门限签名 -- 支持多种签名算法 (ECDSA, EdDSA) -- 批量签名 +3. **Keygen 执行**: + - Message Router 通知所有参与方开始 + - 各 App 启动 TSS 子进程 + - 通过 Message Router 交换 MPC 消息 + - 完成后各自保存 share 到本地 SQLite -### 密钥刷新 +4. **Sign 创建**: + - App → SQLite: 读取 share 和 participants_json + - App → Account Service: 创建签名会话,指定参与方列表 + - 分享 invite_code 给其他参与方 -- 支持 share 刷新而不改变公钥 -- 支持增加/减少参与方 - -### 硬件钱包集成 - -- 支持将 share 存储在硬件安全模块 -- 支持 Ledger/Trezor 等硬件钱包 +5. **Sign 执行**: + - 足够参与方加入后自动开始 + - 通过 Message Router 交换 MPC 消息 + - 完成后返回签名结果 ## 相关文档 - [共管钱包实现计划](./co-managed-wallet-implementation-plan.md) -- [MPC 系统架构](./architecture/) +- [MPC 系统架构](../backend/mpc-system/docs/01-architecture.md) +- [API 参考](../backend/mpc-system/docs/02-api-reference.md)