From eb63b9341b0930e01838e35624cfc68d05d46ea9 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 6 Dec 2025 07:16:24 -0800 Subject: [PATCH] fix(tss): correct threshold signing to support t-of-n properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, signing incorrectly required all n parties from keygen to participate. For 2-of-3 threshold, it required all 3 parties instead of just 2. Root cause: tss.NewParameters was using len(currentSigners) instead of the original n from keygen. Changes: - Added TotalParties field to SigningConfig to store original n from keygen - Modified participate_signing.go to read threshold_n from database - Updated tss.NewParameters to use TotalParties instead of current signer count - Added logging to show t, n, and current_signers For 2-of-3: threshold_t=2, threshold_n=3, any 2 parties can now sign. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/mpc-system/pkg/tss/signing.go | 9 ++++++--- .../use_cases/participate_signing.go | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/mpc-system/pkg/tss/signing.go b/backend/mpc-system/pkg/tss/signing.go index 72b4dec4..4db22a62 100644 --- a/backend/mpc-system/pkg/tss/signing.go +++ b/backend/mpc-system/pkg/tss/signing.go @@ -43,8 +43,9 @@ type SigningParty struct { // SigningConfig contains configuration for signing type SigningConfig struct { - Threshold int // t in t-of-n (number of signers required) - TotalSigners int // Number of parties participating in this signing + Threshold int // t in t-of-n threshold value from keygen + TotalParties int // n in t-of-n - total parties from keygen (NOT current signers) + TotalSigners int // Number of parties participating in this signing session Timeout time.Duration // Signing timeout } @@ -112,8 +113,10 @@ func NewSigningSession( sortedPartyIDs := tss.SortPartyIDs(tssPartyIDs) // Create peer context and parameters + // IMPORTANT: Use TotalParties from keygen, not len(sortedPartyIDs) which is current signers + // For 2-of-3: threshold=2, TotalParties=3, but only 2 parties might participate in signing peerCtx := tss.NewPeerContext(sortedPartyIDs) - params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, len(sortedPartyIDs), config.Threshold) + params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, config.TotalParties, config.Threshold) // Convert message hash to big.Int msgHash := new(big.Int).SetBytes(messageHash) 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 aa1b7bd1..71c7f4eb 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 @@ -81,6 +81,7 @@ func (uc *ParticipateSigningUseCase) Execute( // 2. Get share data - either from user input (delegate) or from database (persistent) var shareData []byte var keyShareForUpdate *entities.PartyKeyShare + var originalThresholdN int // Original total parties from keygen if len(input.UserShareData) > 0 { // Delegate party: use share provided by user @@ -88,6 +89,8 @@ func (uc *ParticipateSigningUseCase) Execute( if err != nil { return nil, err } + // 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())) @@ -101,6 +104,9 @@ func (uc *ParticipateSigningUseCase) Execute( // Use the most recent key share (in production, would match by public key or session reference) keyShareForUpdate = keyShares[len(keyShares)-1] + // Get original threshold_n from keygen + originalThresholdN = keyShareForUpdate.ThresholdN + // Decrypt share data shareData, err = uc.cryptoService.DecryptShare(keyShareForUpdate.ShareData, input.PartyID) if err != nil { @@ -108,7 +114,9 @@ func (uc *ParticipateSigningUseCase) Execute( } logger.Info("Using database share (persistent party)", zap.String("party_id", input.PartyID), - zap.String("session_id", input.SessionID.String())) + zap.String("session_id", input.SessionID.String()), + zap.Int("original_threshold_n", originalThresholdN), + zap.Int("threshold_t", keyShareForUpdate.ThresholdT)) } // 4. Find self in participants and build party index map @@ -141,6 +149,7 @@ func (uc *ParticipateSigningUseCase) Execute( selfIndex, sessionInfo.Participants, sessionInfo.ThresholdT, + originalThresholdN, shareData, messageHash, msgChan, @@ -179,6 +188,7 @@ func (uc *ParticipateSigningUseCase) runSigningProtocol( selfIndex int, participants []ParticipantInfo, t int, + n int, // Original total parties from keygen shareData []byte, messageHash []byte, msgChan <-chan *MPCMessage, @@ -189,6 +199,8 @@ func (uc *ParticipateSigningUseCase) runSigningProtocol( zap.String("party_id", partyID), zap.Int("self_index", selfIndex), zap.Int("t", t), + zap.Int("n", n), + zap.Int("current_signers", len(participants)), zap.Int("message_hash_len", len(messageHash))) // Create message handler adapter @@ -204,8 +216,11 @@ func (uc *ParticipateSigningUseCase) runSigningProtocol( go msgHandler.convertMessages(ctx, msgChan) // Create signing config + // IMPORTANT: TotalParties must be the original n from keygen, not current signers + // For 2-of-3: t=2, n=3, but only 2 parties participate in signing config := tss.SigningConfig{ Threshold: t, + TotalParties: n, // Original total from keygen TotalSigners: len(participants), Timeout: 5 * time.Minute, }