diff --git a/backend/mpc-system/services/account/adapters/input/http/account_handler.go b/backend/mpc-system/services/account/adapters/input/http/account_handler.go index 66cede4f..bea94324 100644 --- a/backend/mpc-system/services/account/adapters/input/http/account_handler.go +++ b/backend/mpc-system/services/account/adapters/input/http/account_handler.go @@ -833,9 +833,11 @@ func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) { zap.String("keygen_session_id", accountOutput.Account.KeygenSessionID.String())) } + // CRITICAL: Pass keygenThresholdN (original n from keygen) for correct TSS math resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto( ctx, int32(accountOutput.Account.ThresholdT), + int32(accountOutput.Account.ThresholdN), signingParties, messageHash, 600, // 10 minutes expiry 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 6e19b4c6..40ecac63 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 @@ -507,6 +507,32 @@ func (h *CoManagedHTTPHandler) CreateSignSession(c *gin.Context) { return } + // Call session coordinator via gRPC + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // CRITICAL: Query keygen session to get the original threshold_n + // This is required for TSS signing to work correctly - the n value must match keygen + var keygenThresholdN, keygenThresholdT int + if h.db != nil { + err = h.db.QueryRowContext(ctx, ` + SELECT threshold_n, threshold_t + FROM mpc_sessions + WHERE id = $1 + `, req.KeygenSessionID).Scan(&keygenThresholdN, &keygenThresholdT) + if err != nil { + logger.Error("Failed to query keygen session for threshold values", + zap.String("keygen_session_id", req.KeygenSessionID), + zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to lookup keygen session"}) + return + } + } else { + logger.Error("Database connection not available for keygen session lookup") + c.JSON(http.StatusInternalServerError, gin.H{"error": "service configuration error"}) + return + } + // Generate invite code for sign session inviteCode := generateInviteCode() @@ -519,22 +545,22 @@ func (h *CoManagedHTTPHandler) CreateSignSession(c *gin.Context) { } } - // 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.Int("keygen_threshold_n", keygenThresholdN), + zap.Int("keygen_threshold_t", keygenThresholdT), + zap.Int("signing_threshold_t", req.ThresholdT), + zap.Int("num_signing_parties", len(req.Parties)), zap.String("invite_code", inviteCode)) // Create signing session // Note: delegateUserShare is nil for co-managed wallets (no delegate party) + // CRITICAL: Pass keygenThresholdN (original n from keygen) for correct TSS math resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto( ctx, int32(req.ThresholdT), + int32(keygenThresholdN), parties, messageHash, 86400, // 24 hour expiry @@ -682,19 +708,19 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) { var sessionID string var walletName string var keygenSessionID string - var thresholdN, thresholdT int var status string var expiresAt time.Time var messageHash []byte + // Query sign session basic info err := h.db.QueryRowContext(ctx, ` SELECT id, COALESCE(wallet_name, ''), COALESCE(keygen_session_id::text, ''), - threshold_n, threshold_t, status, expires_at, COALESCE(message_hash, '') + status, expires_at, COALESCE(message_hash, '') FROM mpc_sessions WHERE invite_code = $1 AND session_type = 'sign' AND status != 'failed' ORDER BY created_at DESC LIMIT 1 - `, inviteCode).Scan(&sessionID, &walletName, &keygenSessionID, &thresholdN, &thresholdT, &status, &expiresAt, &messageHash) + `, inviteCode).Scan(&sessionID, &walletName, &keygenSessionID, &status, &expiresAt, &messageHash) if err != nil { if err == sql.ErrNoRows { @@ -719,6 +745,60 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) { return } + // Get threshold_n and threshold_t from the KEYGEN session (the authoritative source) + // This is critical for TSS signing to work correctly + var keygenThresholdN, keygenThresholdT int + if keygenSessionID != "" { + err = h.db.QueryRowContext(ctx, ` + SELECT threshold_n, threshold_t + FROM mpc_sessions + WHERE id = $1 + `, keygenSessionID).Scan(&keygenThresholdN, &keygenThresholdT) + if err != nil { + logger.Error("Failed to query keygen session for threshold values", + zap.String("keygen_session_id", keygenSessionID), + zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to lookup keygen session"}) + return + } + } else { + logger.Error("Sign session has no keygen_session_id", + zap.String("session_id", sessionID)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "sign session missing keygen reference"}) + return + } + + // Get the signing parties list from the sign session's participants table + // These are the parties that were selected for this signing session + var parties []gin.H + rows, err := h.db.QueryContext(ctx, ` + SELECT party_id, party_index + FROM participants + WHERE session_id = $1 + ORDER BY party_index + `, sessionID) + if err != nil { + logger.Error("Failed to query sign session participants", + zap.String("session_id", sessionID), + zap.Error(err)) + // Continue without parties list, frontend will fallback + } else { + defer rows.Close() + for rows.Next() { + var partyID string + var partyIndex int + if err := rows.Scan(&partyID, &partyIndex); err != nil { + logger.Warn("Failed to scan participant row", + zap.Error(err)) + continue + } + parties = append(parties, gin.H{ + "party_id": partyID, + "party_index": partyIndex, + }) + } + } + // Get session status from coordinator statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID) if err != nil { @@ -731,11 +811,12 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) { "keygen_session_id": keygenSessionID, "wallet_name": walletName, "message_hash": hex.EncodeToString(messageHash), - "threshold_n": thresholdN, - "threshold_t": thresholdT, + "threshold_n": keygenThresholdN, + "threshold_t": keygenThresholdT, "status": status, "joined_count": 0, "expires_at": expiresAt.UnixMilli(), + "parties": parties, }) return } @@ -760,6 +841,10 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) { zap.String("invite_code", inviteCode), zap.String("session_id", sessionID), zap.String("wallet_name", walletName), + zap.String("keygen_session_id", keygenSessionID), + zap.Int("keygen_threshold_n", keygenThresholdN), + zap.Int("keygen_threshold_t", keygenThresholdT), + zap.Int("parties_count", len(parties)), zap.Bool("has_join_token", joinToken != "")) c.JSON(http.StatusOK, gin.H{ @@ -767,11 +852,12 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) { "keygen_session_id": keygenSessionID, "wallet_name": walletName, "message_hash": hex.EncodeToString(messageHash), - "threshold_n": thresholdN, - "threshold_t": thresholdT, + "threshold_n": keygenThresholdN, + "threshold_t": keygenThresholdT, "status": statusResp.Status, "joined_count": statusResp.CompletedParties, "expires_at": expiresAt.UnixMilli(), "join_token": joinToken, + "parties": parties, }) } diff --git a/backend/mpc-system/services/account/adapters/output/grpc/session_coordinator_client.go b/backend/mpc-system/services/account/adapters/output/grpc/session_coordinator_client.go index 4aad7a5a..2453409c 100644 --- a/backend/mpc-system/services/account/adapters/output/grpc/session_coordinator_client.go +++ b/backend/mpc-system/services/account/adapters/output/grpc/session_coordinator_client.go @@ -137,9 +137,11 @@ type SigningPartyInfo struct { // CreateSigningSessionAuto creates a new signing session with automatic party selection // Coordinator will select parties from the provided party info (from account shares) // delegateUserShare is required if any of the parties is a delegate party +// keygenThresholdN is the original threshold_n from the keygen session (required for TSS math) func (c *SessionCoordinatorClient) CreateSigningSessionAuto( ctx context.Context, thresholdT int32, + keygenThresholdN int32, parties []SigningPartyInfo, messageHash []byte, expiresInSeconds int64, @@ -155,9 +157,11 @@ func (c *SessionCoordinatorClient) CreateSigningSessionAuto( } } + // CRITICAL: Use keygenThresholdN (original n from keygen), NOT len(parties) + // TSS signing requires the same n value used during keygen for correct mathematical operations req := &coordinatorpb.CreateSessionRequest{ SessionType: "sign", - ThresholdN: int32(len(parties)), + ThresholdN: keygenThresholdN, ThresholdT: thresholdT, Participants: pbParticipants, MessageHash: messageHash, @@ -174,12 +178,14 @@ func (c *SessionCoordinatorClient) CreateSigningSessionAuto( } logger.Info("Sending CreateSigningSession gRPC request with delegate user share", zap.Int32("threshold_t", thresholdT), - zap.Int("num_parties", len(parties)), + zap.Int32("keygen_threshold_n", keygenThresholdN), + zap.Int("num_signing_parties", len(parties)), zap.String("delegate_party_id", delegateUserShare.DelegatePartyID)) } else { logger.Info("Sending CreateSigningSession gRPC request", zap.Int32("threshold_t", thresholdT), - zap.Int("num_parties", len(parties))) + zap.Int32("keygen_threshold_n", keygenThresholdN), + zap.Int("num_signing_parties", len(parties))) } resp, err := c.client.CreateSession(ctx, req)