fix(co-sign): use keygen session threshold_n for TSS signing

- Query keygen session from mpc_sessions table to get correct threshold_n
- Pass keygenThresholdN to CreateSigningSessionAuto instead of len(parties)
- Return parties list and correct threshold values in GetSignSessionByInviteCode
- This fixes TSS signing failure "U doesn't equal T" caused by mismatched n values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-31 05:31:02 -08:00
parent e284a46e83
commit 042212eae6
3 changed files with 110 additions and 16 deletions

View File

@ -833,9 +833,11 @@ func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) {
zap.String("keygen_session_id", accountOutput.Account.KeygenSessionID.String())) 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( resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto(
ctx, ctx,
int32(accountOutput.Account.ThresholdT), int32(accountOutput.Account.ThresholdT),
int32(accountOutput.Account.ThresholdN),
signingParties, signingParties,
messageHash, messageHash,
600, // 10 minutes expiry 600, // 10 minutes expiry

View File

@ -507,6 +507,32 @@ func (h *CoManagedHTTPHandler) CreateSignSession(c *gin.Context) {
return 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 // Generate invite code for sign session
inviteCode := generateInviteCode() 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", logger.Info("Creating co-managed sign session",
zap.String("keygen_session_id", req.KeygenSessionID), zap.String("keygen_session_id", req.KeygenSessionID),
zap.String("wallet_name", req.WalletName), zap.String("wallet_name", req.WalletName),
zap.Int("threshold_t", req.ThresholdT), zap.Int("keygen_threshold_n", keygenThresholdN),
zap.Int("num_parties", len(req.Parties)), 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)) zap.String("invite_code", inviteCode))
// Create signing session // Create signing session
// Note: delegateUserShare is nil for co-managed wallets (no delegate party) // 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( resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto(
ctx, ctx,
int32(req.ThresholdT), int32(req.ThresholdT),
int32(keygenThresholdN),
parties, parties,
messageHash, messageHash,
86400, // 24 hour expiry 86400, // 24 hour expiry
@ -682,19 +708,19 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) {
var sessionID string var sessionID string
var walletName string var walletName string
var keygenSessionID string var keygenSessionID string
var thresholdN, thresholdT int
var status string var status string
var expiresAt time.Time var expiresAt time.Time
var messageHash []byte var messageHash []byte
// Query sign session basic info
err := h.db.QueryRowContext(ctx, ` err := h.db.QueryRowContext(ctx, `
SELECT id, COALESCE(wallet_name, ''), COALESCE(keygen_session_id::text, ''), 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 FROM mpc_sessions
WHERE invite_code = $1 AND session_type = 'sign' AND status != 'failed' WHERE invite_code = $1 AND session_type = 'sign' AND status != 'failed'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 1 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 != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -719,6 +745,60 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) {
return 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 // Get session status from coordinator
statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID) statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID)
if err != nil { if err != nil {
@ -731,11 +811,12 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) {
"keygen_session_id": keygenSessionID, "keygen_session_id": keygenSessionID,
"wallet_name": walletName, "wallet_name": walletName,
"message_hash": hex.EncodeToString(messageHash), "message_hash": hex.EncodeToString(messageHash),
"threshold_n": thresholdN, "threshold_n": keygenThresholdN,
"threshold_t": thresholdT, "threshold_t": keygenThresholdT,
"status": status, "status": status,
"joined_count": 0, "joined_count": 0,
"expires_at": expiresAt.UnixMilli(), "expires_at": expiresAt.UnixMilli(),
"parties": parties,
}) })
return return
} }
@ -760,6 +841,10 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) {
zap.String("invite_code", inviteCode), zap.String("invite_code", inviteCode),
zap.String("session_id", sessionID), zap.String("session_id", sessionID),
zap.String("wallet_name", walletName), 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 != "")) zap.Bool("has_join_token", joinToken != ""))
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@ -767,11 +852,12 @@ func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) {
"keygen_session_id": keygenSessionID, "keygen_session_id": keygenSessionID,
"wallet_name": walletName, "wallet_name": walletName,
"message_hash": hex.EncodeToString(messageHash), "message_hash": hex.EncodeToString(messageHash),
"threshold_n": thresholdN, "threshold_n": keygenThresholdN,
"threshold_t": thresholdT, "threshold_t": keygenThresholdT,
"status": statusResp.Status, "status": statusResp.Status,
"joined_count": statusResp.CompletedParties, "joined_count": statusResp.CompletedParties,
"expires_at": expiresAt.UnixMilli(), "expires_at": expiresAt.UnixMilli(),
"join_token": joinToken, "join_token": joinToken,
"parties": parties,
}) })
} }

View File

@ -137,9 +137,11 @@ type SigningPartyInfo struct {
// CreateSigningSessionAuto creates a new signing session with automatic party selection // CreateSigningSessionAuto creates a new signing session with automatic party selection
// Coordinator will select parties from the provided party info (from account shares) // Coordinator will select parties from the provided party info (from account shares)
// delegateUserShare is required if any of the parties is a delegate party // 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( func (c *SessionCoordinatorClient) CreateSigningSessionAuto(
ctx context.Context, ctx context.Context,
thresholdT int32, thresholdT int32,
keygenThresholdN int32,
parties []SigningPartyInfo, parties []SigningPartyInfo,
messageHash []byte, messageHash []byte,
expiresInSeconds int64, 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{ req := &coordinatorpb.CreateSessionRequest{
SessionType: "sign", SessionType: "sign",
ThresholdN: int32(len(parties)), ThresholdN: keygenThresholdN,
ThresholdT: thresholdT, ThresholdT: thresholdT,
Participants: pbParticipants, Participants: pbParticipants,
MessageHash: messageHash, MessageHash: messageHash,
@ -174,12 +178,14 @@ func (c *SessionCoordinatorClient) CreateSigningSessionAuto(
} }
logger.Info("Sending CreateSigningSession gRPC request with delegate user share", logger.Info("Sending CreateSigningSession gRPC request with delegate user share",
zap.Int32("threshold_t", thresholdT), 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)) zap.String("delegate_party_id", delegateUserShare.DelegatePartyID))
} else { } else {
logger.Info("Sending CreateSigningSession gRPC request", logger.Info("Sending CreateSigningSession gRPC request",
zap.Int32("threshold_t", thresholdT), 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) resp, err := c.client.CreateSession(ctx, req)