From 7ab28dced0d9b6433237fa606aeb5454e8712a27 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 31 Dec 2025 11:25:22 -0800 Subject: [PATCH] fix(tss): use BuildLocalSaveDataSubset for threshold signing with party subsets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When signing with fewer parties than keygen (e.g., 2-of-3 signing with only 2 parties), the TSS-lib requires filtered save data containing only the participating parties. Without this fix, signing fails with "U doesn't equal T" error because: - Keygen creates save data for all N parties (e.g., 3 parties with indices 0, 1, 2) - Sign uses only T parties (e.g., 2 parties with indices 1, 2) - TSS-lib internal index validation fails due to mismatch Changes: - pkg/tss/signing.go: Use len(sortedPartyIDs) for partyCount and call BuildLocalSaveDataSubset - tss-party/main.go: Add BuildLocalSaveDataSubset call for Electron app - tss-wasm/main.go: Add BuildLocalSaveDataSubset call for WASM builds This fix is backward compatible - when all parties participate, the subset equals the original data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/mpc-system/pkg/tss/signing.go | 22 ++++++++++++++----- .../service-party-app/tss-party/main.go | 10 +++++++-- backend/mpc-system/services/tss-wasm/main.go | 8 +++++-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/backend/mpc-system/pkg/tss/signing.go b/backend/mpc-system/pkg/tss/signing.go index b22a7156..3dc5eab9 100644 --- a/backend/mpc-system/pkg/tss/signing.go +++ b/backend/mpc-system/pkg/tss/signing.go @@ -132,10 +132,13 @@ func NewSigningSession( keygenIndexToSortedIndex, selfParty.PartyID) // 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 + // IMPORTANT: Use len(sortedPartyIDs) as partyCount - this is the number of CURRENT signers + // For 2-of-3 threshold signing with only 2 parties participating: + // - partyCount = 2 (current signers) + // - threshold = 2 (minimum required from keygen) + // The BuildLocalSaveDataSubset call in Start() will filter the save data to match peerCtx := tss.NewPeerContext(sortedPartyIDs) - params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, config.TotalParties, config.Threshold) + params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, len(sortedPartyIDs), config.Threshold) // Convert message hash to big.Int msgHash := new(big.Int).SetBytes(messageHash) @@ -167,8 +170,17 @@ func (s *SigningSession) Start(ctx context.Context) (*SigningResult, error) { s.started = true s.mu.Unlock() - // Create local party for signing - s.localParty = signing.NewLocalParty(s.messageHash, s.params, *s.saveData, s.outCh, s.endCh) + // CRITICAL: Build a subset of the save data for the current signing parties + // When signing with fewer parties than keygen (e.g., 2-of-3 signing with only 2 parties), + // we must filter the save data to only include the participating parties' data. + // This ensures TSS-lib's internal indices match the actual signers. + subsetSaveData := keygen.BuildLocalSaveDataSubset(*s.saveData, s.tssPartyIDs) + + fmt.Printf("[TSS-SIGN] Built save data subset for %d signing parties (original keygen had %d parties) party_id=%s\n", + len(s.tssPartyIDs), len(s.saveData.Ks), s.selfParty.PartyID) + + // Create local party for signing with the SUBSET save data + s.localParty = signing.NewLocalParty(s.messageHash, s.params, subsetSaveData, s.outCh, s.endCh) // Start the local party go func() { diff --git a/backend/mpc-system/services/service-party-app/tss-party/main.go b/backend/mpc-system/services/service-party-app/tss-party/main.go index 287fb233..2d227420 100644 --- a/backend/mpc-system/services/service-party-app/tss-party/main.go +++ b/backend/mpc-system/services/service-party-app/tss-party/main.go @@ -607,13 +607,19 @@ func executeSign( peerCtx := tss.NewPeerContext(sortedPartyIDs) params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, len(sortedPartyIDs), thresholdT-1) + // CRITICAL: Build a subset of the keygen save data for the current signing parties + // This is required when signing with a subset of the original keygen participants. + // BuildLocalSaveDataSubset filters the Ks, BigXj, NTildej, H1j, H2j, and PaillierPKs + // arrays to only include data for the participating signers. + subsetKeygenData := keygen.BuildLocalSaveDataSubset(keygenData, sortedPartyIDs) + // Create channels outCh := make(chan tss.Message, thresholdT*10) endCh := make(chan *common.SignatureData, 1) errCh := make(chan error, 1) - // Create local party for signing - localParty := signing.NewLocalParty(msgBigInt, params, keygenData, outCh, endCh) + // Create local party for signing with the SUBSET keygen data + localParty := signing.NewLocalParty(msgBigInt, params, subsetKeygenData, outCh, endCh) // Build party index map for incoming messages partyIndexMap := make(map[int]*tss.PartyID) diff --git a/backend/mpc-system/services/tss-wasm/main.go b/backend/mpc-system/services/tss-wasm/main.go index e5a63bdf..b7cd1ad1 100644 --- a/backend/mpc-system/services/tss-wasm/main.go +++ b/backend/mpc-system/services/tss-wasm/main.go @@ -298,8 +298,12 @@ func startSigning(this js.Value, args []js.Value) interface{} { // Create message hash as big.Int msgHashBig := new(big.Int).SetBytes(messageHash) - // Create local signing party - session.LocalParty = signing.NewLocalParty(msgHashBig, params, saveData, session.OutCh, session.EndChSign) + // CRITICAL: Build a subset of the keygen save data for the current signing parties + // This is required when signing with a subset of the original keygen participants. + subsetSaveData := keygen.BuildLocalSaveDataSubset(saveData, sortedPartyIDs) + + // Create local signing party with the SUBSET save data + session.LocalParty = signing.NewLocalParty(msgHashBig, params, subsetSaveData, session.OutCh, session.EndChSign) // Store session sessionMutex.Lock()