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

View File

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

View File

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