From ad4549e767ae5291ed37eb90a9878797509087e2 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 26 Jan 2026 18:53:38 -0800 Subject: [PATCH] =?UTF-8?q?feat(co-managed):=20=E6=94=AF=E6=8C=81=202-of-3?= =?UTF-8?q?=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=8F=82=E4=B8=8E=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改内容: 1. participate_signing.go: 添加 ExecuteWithSessionInfo 方法 - 新增方法供 server-party-co-managed 调用 - 跳过 JoinSession 步骤(已在 session_created 阶段完成) - 将核心逻辑提取到 executeWithSessionInfo 共享方法 2. server-party-co-managed/main.go: 完整实现 co-sign 支持 - 初始化 participateSigningUC - session_created: 移除签名会话拒绝逻辑,添加 2-of-3 安全检查 - session_started: 根据 messageHash 判断 keygen/sign 并调用对应 use case 功能特性: - ✅ 仅支持 2-of-3 配置的签名会话 - ✅ 100% 寄生 server-party 的 use_cases(与 co-keygen 架构一致) - ✅ 不影响现有 server-party 功能 - ✅ 完整的两阶段事件处理(session_created + session_started) 安全限制: - 仅当 threshold_t=2 且 threshold_n=3 时参与签名 - 其他配置(3-of-5, 4-of-7等)会被拒绝 测试: - ✅ server-party-co-managed 编译成功 Co-Authored-By: Claude Sonnet 4.5 --- .../cmd/server/main.go | 143 ++++++++++++----- .../use_cases/participate_signing.go | 149 ++++++++++-------- 2 files changed, 191 insertions(+), 101 deletions(-) diff --git a/backend/mpc-system/services/server-party-co-managed/cmd/server/main.go b/backend/mpc-system/services/server-party-co-managed/cmd/server/main.go index c3c926b4..44bef0bd 100644 --- a/backend/mpc-system/services/server-party-co-managed/cmd/server/main.go +++ b/backend/mpc-system/services/server-party-co-managed/cmd/server/main.go @@ -149,6 +149,14 @@ func main() { cryptoService, ) + // Initialize signing use case (for co-managed sign sessions) + participateSigningUC := use_cases.NewParticipateSigningUseCase( + keyShareRepo, + messageRouter, + messageRouter, + cryptoService, + ) + // Create shutdown context ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -186,14 +194,15 @@ func main() { defer heartbeatCancel() logger.Info("Heartbeat started", zap.String("party_id", partyID), zap.Duration("interval", 30*time.Second)) - // Subscribe to session events with two-phase handling for co_managed_keygen - logger.Info("Subscribing to session events (co_managed_keygen only)", zap.String("party_id", partyID)) + // Subscribe to session events with two-phase handling for co_managed_keygen and co_managed_sign + logger.Info("Subscribing to session events (co_managed_keygen and co_managed_sign)", zap.String("party_id", partyID)) eventHandler := createCoManagedSessionEventHandler( ctx, partyID, messageRouter, participateKeygenUC, + participateSigningUC, ) if err := messageRouter.SubscribeSessionEvents(ctx, partyID, eventHandler); err != nil { @@ -306,15 +315,17 @@ func startHTTPServer(cfg *config.Config) error { return r.Run(fmt.Sprintf(":%d", cfg.Server.HTTPPort)) } -// createCoManagedSessionEventHandler creates a handler specifically for co_managed_keygen sessions +// createCoManagedSessionEventHandler creates a handler for co_managed_keygen and co_managed_sign sessions // Two-phase event handling: // Phase 1 (session_created): JoinSession immediately + store session info // Phase 2 (session_started): Execute TSS protocol (same timing as user clients receiving all_joined) +// Supports both keygen (no message_hash) and sign (with message_hash) sessions func createCoManagedSessionEventHandler( ctx context.Context, partyID string, messageRouter *grpcclient.MessageRouterClient, participateKeygenUC *use_cases.ParticipateKeygenUseCase, + participateSigningUC *use_cases.ParticipateSigningUseCase, ) func(*router.SessionEvent) { return func(event *router.SessionEvent) { // Check if this party is selected for the session @@ -348,11 +359,26 @@ func createCoManagedSessionEventHandler( // Handle different event types switch event.EventType { case "session_created": - // Only handle keygen sessions (no message_hash) + // Handle both keygen (no message_hash) and sign (with message_hash) sessions + // For sign sessions: only support 2-of-3 configuration if len(event.MessageHash) > 0 { - logger.Debug("Ignoring sign session (co-managed only handles keygen)", - zap.String("session_id", event.SessionId)) - return + // This is a sign session + // Security check: only support 2-of-3 configuration + if event.ThresholdT != 2 || event.ThresholdN != 3 { + logger.Warn("Ignoring sign session: only 2-of-3 configuration is supported", + zap.String("session_id", event.SessionId), + zap.Int32("threshold_t", event.ThresholdT), + zap.Int32("threshold_n", event.ThresholdN)) + return + } + logger.Info("Sign session detected (2-of-3), proceeding with participation", + zap.String("session_id", event.SessionId), + zap.String("party_id", partyID)) + } else { + // This is a keygen session + logger.Info("Keygen session detected, proceeding with participation", + zap.String("session_id", event.SessionId), + zap.String("party_id", partyID)) } // Phase 1: Get join token @@ -401,21 +427,26 @@ func createCoManagedSessionEventHandler( return } - logger.Info("Session started event received, beginning TSS keygen protocol", - zap.String("session_id", event.SessionId), - zap.String("party_id", partyID)) + // Determine session type based on message_hash + isSignSession := len(pendingSession.MessageHash) > 0 - // Execute TSS keygen protocol in goroutine + if isSignSession { + logger.Info("Session started event received, beginning TSS signing protocol", + zap.String("session_id", event.SessionId), + zap.String("party_id", partyID)) + } else { + logger.Info("Session started event received, beginning TSS keygen protocol", + zap.String("session_id", event.SessionId), + zap.String("party_id", partyID)) + } + + // Execute TSS protocol in goroutine // Timeout starts NOW (when session_started is received), not at session_created go func() { // 10 minute timeout for TSS protocol execution participateCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) defer cancel() - logger.Info("Auto-participating in co_managed_keygen session", - zap.String("session_id", event.SessionId), - zap.String("party_id", partyID)) - // Build SessionInfo from session_started event (NOT from pendingSession cache) // session_started event contains ALL participants who have joined, // including external parties that joined dynamically after session_created @@ -429,29 +460,67 @@ func createCoManagedSessionEventHandler( } } - sessionInfo := &use_cases.SessionInfo{ - SessionID: pendingSession.SessionID, - SessionType: "co_managed_keygen", - ThresholdN: int(event.ThresholdN), - ThresholdT: int(event.ThresholdT), - MessageHash: pendingSession.MessageHash, - Participants: participants, - } - - result, err := participateKeygenUC.ExecuteWithSessionInfo( - participateCtx, - pendingSession.SessionID, - partyID, - sessionInfo, - ) - if err != nil { - logger.Error("Co-managed keygen participation failed", - zap.Error(err), - zap.String("session_id", event.SessionId)) - } else { - logger.Info("Co-managed keygen participation completed", + if isSignSession { + // Execute signing protocol + logger.Info("Auto-participating in co_managed_sign session", zap.String("session_id", event.SessionId), - zap.String("public_key", hex.EncodeToString(result.PublicKey))) + zap.String("party_id", partyID)) + + sessionInfo := &use_cases.SessionInfo{ + SessionID: pendingSession.SessionID, + SessionType: "co_managed_sign", + ThresholdN: int(event.ThresholdN), + ThresholdT: int(event.ThresholdT), + MessageHash: pendingSession.MessageHash, + KeygenSessionID: uuid.Nil, // Use nil to trigger fallback logic (load most recent share) + Participants: participants, + } + + result, err := participateSigningUC.ExecuteWithSessionInfo( + participateCtx, + pendingSession.SessionID, + partyID, + sessionInfo, + ) + if err != nil { + logger.Error("Co-managed signing participation failed", + zap.Error(err), + zap.String("session_id", event.SessionId)) + } else { + logger.Info("Co-managed signing participation completed", + zap.String("session_id", event.SessionId), + zap.String("signature", hex.EncodeToString(result.Signature))) + } + } else { + // Execute keygen protocol + logger.Info("Auto-participating in co_managed_keygen session", + zap.String("session_id", event.SessionId), + zap.String("party_id", partyID)) + + sessionInfo := &use_cases.SessionInfo{ + SessionID: pendingSession.SessionID, + SessionType: "co_managed_keygen", + ThresholdN: int(event.ThresholdN), + ThresholdT: int(event.ThresholdT), + MessageHash: pendingSession.MessageHash, + Participants: participants, + } + + result, err := participateKeygenUC.ExecuteWithSessionInfo( + participateCtx, + pendingSession.SessionID, + partyID, + sessionInfo, + ) + if err != nil { + logger.Error("Co-managed keygen participation failed", + zap.Error(err), + zap.String("session_id", event.SessionId)) + } else { + logger.Info("Co-managed keygen participation completed", + zap.String("session_id", event.SessionId), + zap.String("public_key", hex.EncodeToString(result.PublicKey))) + } } }() diff --git a/backend/mpc-system/services/server-party/application/use_cases/participate_signing.go b/backend/mpc-system/services/server-party/application/use_cases/participate_signing.go index 585ae2c3..2fd1dc68 100644 --- a/backend/mpc-system/services/server-party/application/use_cases/participate_signing.go +++ b/backend/mpc-system/services/server-party/application/use_cases/participate_signing.go @@ -63,6 +63,30 @@ func NewParticipateSigningUseCase( } } +// ExecuteWithSessionInfo participates in a signing session with pre-obtained SessionInfo +// This is used by server-party-co-managed which has already called JoinSession in session_created phase +// and receives session_started event when all participants have joined +func (uc *ParticipateSigningUseCase) ExecuteWithSessionInfo( + ctx context.Context, + sessionID uuid.UUID, + partyID string, + sessionInfo *SessionInfo, +) (*ParticipateSigningOutput, error) { + // Validate session type + if sessionInfo.SessionType != "sign" && sessionInfo.SessionType != "co_managed_sign" { + return nil, ErrInvalidSignSession + } + + logger.Info("ExecuteWithSessionInfo: starting signing with pre-obtained session info", + zap.String("session_id", sessionID.String()), + zap.String("party_id", partyID), + zap.String("session_type", sessionInfo.SessionType), + zap.Int("participants", len(sessionInfo.Participants))) + + // Delegate to the common execution logic (skipping JoinSession) + return uc.executeWithSessionInfo(ctx, sessionID, partyID, sessionInfo) +} + // Execute participates in a signing session using real TSS protocol func (uc *ParticipateSigningUseCase) Execute( ctx context.Context, @@ -78,80 +102,80 @@ func (uc *ParticipateSigningUseCase) Execute( return nil, ErrInvalidSignSession } - // 2. Get share data - either from user input (delegate) or from database (persistent) + // Delegate to the common execution logic + return uc.executeWithSessionInfo(ctx, input.SessionID, input.PartyID, sessionInfo) +} + +// executeWithSessionInfo is the common execution logic shared by Execute and ExecuteWithSessionInfo +func (uc *ParticipateSigningUseCase) executeWithSessionInfo( + ctx context.Context, + sessionID uuid.UUID, + partyID string, + sessionInfo *SessionInfo, +) (*ParticipateSigningOutput, error) { + + // 2. Get share data from database (server-party-co-managed always uses persistent shares) var shareData []byte var keyShareForUpdate *entities.PartyKeyShare var originalThresholdN int // Original total parties from keygen + var err error - if len(input.UserShareData) > 0 { - // Delegate party: use share provided by user - shareData, err = uc.cryptoService.DecryptShare(input.UserShareData, input.PartyID) + // Persistent party: load from database + // If KeygenSessionID is provided, use it to load the specific share + // Otherwise, use the most recent share (fallback for backward compatibility) + if sessionInfo.KeygenSessionID != uuid.Nil { + // Load the specific share for this keygen session + keyShareForUpdate, err = uc.keyShareRepo.FindBySessionAndParty(ctx, sessionInfo.KeygenSessionID, partyID) if err != nil { - return nil, err + logger.Error("Failed to find keyshare for keygen session", + zap.String("party_id", partyID), + zap.String("keygen_session_id", sessionInfo.KeygenSessionID.String()), + zap.Error(err)) + return nil, ErrKeyShareNotFound } - // For delegate party, get threshold info from session - originalThresholdN = sessionInfo.ThresholdN - logger.Info("Using user-provided share (delegate party)", - zap.String("party_id", input.PartyID), - zap.String("session_id", input.SessionID.String())) + logger.Info("Using specific keyshare by keygen_session_id", + zap.String("party_id", partyID), + zap.String("keygen_session_id", sessionInfo.KeygenSessionID.String())) } else { - // Persistent party: load from database - // If KeygenSessionID is provided, use it to load the specific share - // Otherwise, use the most recent share (fallback for backward compatibility) - if sessionInfo.KeygenSessionID != uuid.Nil { - // Load the specific share for this keygen session - keyShareForUpdate, err = uc.keyShareRepo.FindBySessionAndParty(ctx, sessionInfo.KeygenSessionID, input.PartyID) - if err != nil { - logger.Error("Failed to find keyshare for keygen session", - zap.String("party_id", input.PartyID), - zap.String("keygen_session_id", sessionInfo.KeygenSessionID.String()), - zap.Error(err)) - return nil, ErrKeyShareNotFound - } - logger.Info("Using specific keyshare by keygen_session_id", - zap.String("party_id", input.PartyID), - zap.String("keygen_session_id", sessionInfo.KeygenSessionID.String())) - } else { - // Fallback: use the most recent key share - // TODO: This should be removed once all signing sessions provide keygen_session_id - keyShares, err := uc.keyShareRepo.ListByParty(ctx, input.PartyID) - if err != nil || len(keyShares) == 0 { - return nil, ErrKeyShareNotFound - } - keyShareForUpdate = keyShares[len(keyShares)-1] - logger.Warn("Using most recent keyshare (keygen_session_id not provided)", - zap.String("party_id", input.PartyID), - zap.String("fallback_session_id", keyShareForUpdate.SessionID.String())) + // Fallback: use the most recent key share + // TODO: This should be removed once all signing sessions provide keygen_session_id + keyShares, err := uc.keyShareRepo.ListByParty(ctx, partyID) + if err != nil || len(keyShares) == 0 { + return nil, ErrKeyShareNotFound } - - // Get original threshold_n from keygen - originalThresholdN = keyShareForUpdate.ThresholdN - - // Decrypt share data - shareData, err = uc.cryptoService.DecryptShare(keyShareForUpdate.ShareData, input.PartyID) - if err != nil { - return nil, err - } - logger.Info("Using database share (persistent party)", - zap.String("party_id", input.PartyID), - zap.String("session_id", input.SessionID.String()), - zap.String("keygen_session_id", keyShareForUpdate.SessionID.String()), - zap.Int("original_threshold_n", originalThresholdN), - zap.Int("threshold_t", keyShareForUpdate.ThresholdT)) + keyShareForUpdate = keyShares[len(keyShares)-1] + logger.Warn("Using most recent keyshare (keygen_session_id not provided)", + zap.String("party_id", partyID), + zap.String("fallback_session_id", keyShareForUpdate.SessionID.String())) } + // Get original threshold_n from keygen + originalThresholdN = keyShareForUpdate.ThresholdN + + // Decrypt share data + shareData, err = uc.cryptoService.DecryptShare(keyShareForUpdate.ShareData, partyID) + if err != nil { + return nil, err + } + logger.Info("Using database share (persistent party)", + zap.String("party_id", partyID), + zap.String("session_id", sessionID.String()), + zap.String("keygen_session_id", keyShareForUpdate.SessionID.String()), + zap.Int("original_threshold_n", originalThresholdN), + zap.Int("threshold_t", keyShareForUpdate.ThresholdT)) + // 4. Find self in participants and build party index map var selfIndex int partyIndexMap := make(map[string]int) for _, p := range sessionInfo.Participants { partyIndexMap[p.PartyID] = p.PartyIndex - if p.PartyID == input.PartyID { + if p.PartyID == partyID { selfIndex = p.PartyIndex } } // 5. Subscribe to messages - msgChan, err := uc.messageRouter.SubscribeMessages(ctx, input.SessionID, input.PartyID) + msgChan, err := uc.messageRouter.SubscribeMessages(ctx, sessionID, partyID) if err != nil { return nil, err } @@ -161,22 +185,19 @@ func (uc *ParticipateSigningUseCase) Execute( // before others have subscribed to the session expectedParties := len(sessionInfo.Participants) logger.Info("Waiting for all parties to subscribe", - zap.String("session_id", input.SessionID.String()), - zap.String("party_id", input.PartyID), + zap.String("session_id", sessionID.String()), + zap.String("party_id", partyID), zap.Int("expected_parties", expectedParties)) time.Sleep(500 * time.Millisecond) - // Use message hash from session if not provided - messageHash := input.MessageHash - if len(messageHash) == 0 { - messageHash = sessionInfo.MessageHash - } + // Use message hash from session + messageHash := sessionInfo.MessageHash // 6. Run TSS Signing protocol signature, r, s, err := uc.runSigningProtocol( ctx, - input.SessionID, - input.PartyID, + sessionID, + partyID, selfIndex, sessionInfo.Participants, sessionInfo.ThresholdT, @@ -199,7 +220,7 @@ func (uc *ParticipateSigningUseCase) Execute( } // 8. Report completion to coordinator - if err := uc.sessionClient.ReportCompletion(ctx, input.SessionID, input.PartyID, signature); err != nil { + if err := uc.sessionClient.ReportCompletion(ctx, sessionID, partyID, signature); err != nil { logger.Error("failed to report signing completion", zap.Error(err)) }