feat(co-managed): 支持 2-of-3 服务器参与签名功能

修改内容:
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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-26 18:53:38 -08:00
parent dbeef9f415
commit ad4549e767
2 changed files with 191 additions and 101 deletions

View File

@ -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)))
}
}
}()

View File

@ -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))
}