// Package main provides the TSS party subprocess for Electron app // // This program handles TSS (Threshold Signature Scheme) protocol execution // It communicates with the parent Electron process via stdin/stdout using JSON messages package main import ( "bufio" "context" "encoding/base64" "encoding/hex" "encoding/json" "flag" "fmt" "math/big" "os" "os/signal" "regexp" "strconv" "sync" "syscall" "time" "github.com/bnb-chain/tss-lib/v2/common" "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" "github.com/bnb-chain/tss-lib/v2/ecdsa/signing" "github.com/bnb-chain/tss-lib/v2/tss" ) // Regex to extract round number from tss-lib message type // Message types look like: "binance.tsslib.ecdsa.keygen.KGRound1Message" // or "binance.tsslib.ecdsa.signing.SignRound3Message" var roundRegex = regexp.MustCompile(`Round(\d+)`) // Message types for IPC type Message struct { Type string `json:"type"` IsBroadcast bool `json:"isBroadcast,omitempty"` ToParties []string `json:"toParties,omitempty"` Payload string `json:"payload,omitempty"` // base64 encoded PublicKey string `json:"publicKey,omitempty"` // base64 encoded EncryptedShare string `json:"encryptedShare,omitempty"` // base64 encoded PartyIndex int `json:"partyIndex,omitempty"` Round int `json:"round,omitempty"` TotalRounds int `json:"totalRounds,omitempty"` FromPartyIndex int `json:"fromPartyIndex,omitempty"` Error string `json:"error,omitempty"` } // Participant info type Participant struct { PartyID string `json:"partyId"` PartyIndex int `json:"partyIndex"` } func main() { // Parse command if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "Usage: tss-party [options]") os.Exit(1) } command := os.Args[1] switch command { case "keygen": runKeygen() case "sign": runSign() default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command) os.Exit(1) } } func runKeygen() { // Parse keygen flags fs := flag.NewFlagSet("keygen", flag.ExitOnError) sessionID := fs.String("session-id", "", "Session ID") partyID := fs.String("party-id", "", "Party ID") partyIndex := fs.Int("party-index", 0, "Party index (0-based)") thresholdT := fs.Int("threshold-t", 0, "Threshold T") thresholdN := fs.Int("threshold-n", 0, "Threshold N") participantsJSON := fs.String("participants", "[]", "Participants JSON array") password := fs.String("password", "", "Encryption password for share") if err := fs.Parse(os.Args[2:]); err != nil { sendError(fmt.Sprintf("Failed to parse flags: %v", err)) os.Exit(1) } // Validate required fields if *sessionID == "" || *partyID == "" || *thresholdT == 0 || *thresholdN == 0 { sendError("Missing required parameters") os.Exit(1) } // Parse participants var participants []Participant if err := json.Unmarshal([]byte(*participantsJSON), &participants); err != nil { sendError(fmt.Sprintf("Failed to parse participants: %v", err)) os.Exit(1) } if len(participants) != *thresholdN { sendError(fmt.Sprintf("Participant count mismatch: got %d, expected %d", len(participants), *thresholdN)) os.Exit(1) } // Run keygen protocol result, err := executeKeygen( *sessionID, *partyID, *partyIndex, *thresholdT, *thresholdN, participants, *password, ) if err != nil { sendError(fmt.Sprintf("Keygen failed: %v", err)) os.Exit(1) } // Send result sendResult(result.PublicKey, result.EncryptedShare, *partyIndex) } func runSign() { // Parse sign flags fs := flag.NewFlagSet("sign", flag.ExitOnError) sessionID := fs.String("session-id", "", "Session ID") partyID := fs.String("party-id", "", "Party ID") partyIndex := fs.Int("party-index", 0, "Party index (0-based)") thresholdT := fs.Int("threshold-t", 0, "Threshold T") thresholdN := fs.Int("threshold-n", 0, "Threshold N (total parties in keygen)") participantsJSON := fs.String("participants", "[]", "Participants JSON array") messageHash := fs.String("message-hash", "", "Message hash to sign (hex encoded)") shareData := fs.String("share-data", "", "Encrypted share data (base64 encoded)") password := fs.String("password", "", "Password to decrypt share") if err := fs.Parse(os.Args[2:]); err != nil { sendError(fmt.Sprintf("Failed to parse flags: %v", err)) os.Exit(1) } // Validate required fields if *sessionID == "" || *partyID == "" || *thresholdT == 0 || *thresholdN == 0 { sendError("Missing required parameters") os.Exit(1) } if *messageHash == "" { sendError("Missing message hash") os.Exit(1) } if *shareData == "" { sendError("Missing share data") os.Exit(1) } // Parse participants var participants []Participant if err := json.Unmarshal([]byte(*participantsJSON), &participants); err != nil { sendError(fmt.Sprintf("Failed to parse participants: %v", err)) os.Exit(1) } // Note: For signing, participant count equals threshold T (not N) // because only T parties participate in signing if len(participants) != *thresholdT { sendError(fmt.Sprintf("Participant count mismatch: got %d, expected %d (threshold T)", len(participants), *thresholdT)) os.Exit(1) } // Run sign protocol result, err := executeSign( *sessionID, *partyID, *partyIndex, *thresholdT, *thresholdN, participants, *messageHash, *shareData, *password, ) if err != nil { sendError(fmt.Sprintf("Sign failed: %v", err)) os.Exit(1) } // Send result sendSignResult(result.Signature, result.RecoveryID, *partyIndex) } type keygenResult struct { PublicKey []byte EncryptedShare []byte } type signResult struct { Signature []byte RecoveryID int } func executeKeygen( sessionID, partyID string, partyIndex, thresholdT, thresholdN int, participants []Participant, password string, ) (*keygenResult, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() // Handle signals for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan cancel() }() // Create TSS party IDs tssPartyIDs := make([]*tss.PartyID, len(participants)) var selfTSSID *tss.PartyID for i, p := range participants { partyKey := tss.NewPartyID( p.PartyID, fmt.Sprintf("party-%d", p.PartyIndex), big.NewInt(int64(p.PartyIndex+1)), ) tssPartyIDs[i] = partyKey if p.PartyID == partyID { selfTSSID = partyKey } } if selfTSSID == nil { return nil, fmt.Errorf("self party not found in participants") } // Sort party IDs sortedPartyIDs := tss.SortPartyIDs(tssPartyIDs) // Create peer context and parameters // IMPORTANT: TSS-lib threshold convention: threshold=t means (t+1) signers required // User says "2-of-3" meaning 2 signers needed, so we pass (thresholdT-1) to TSS-lib // For 2-of-3: thresholdT=2, tss-lib threshold=1, signers_needed=1+1=2 ✓ peerCtx := tss.NewPeerContext(sortedPartyIDs) tssThreshold := thresholdT - 1 params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, len(sortedPartyIDs), tssThreshold) fmt.Fprintf(os.Stderr, "[TSS-KEYGEN] NewParameters: partyCount=%d, tssThreshold=%d (from thresholdT=%d, means %d signers needed)\n", len(sortedPartyIDs), tssThreshold, thresholdT, thresholdT) // Create channels outCh := make(chan tss.Message, thresholdN*10) endCh := make(chan *keygen.LocalPartySaveData, 1) errCh := make(chan error, 1) // Create local party localParty := keygen.NewLocalParty(params, outCh, endCh) // Build party index map for incoming messages partyIndexMap := make(map[int]*tss.PartyID) for i, p := range sortedPartyIDs { for _, orig := range participants { if orig.PartyID == p.Id { partyIndexMap[orig.PartyIndex] = p break } } _ = i } // Start the local party go func() { if err := localParty.Start(); err != nil { errCh <- err } }() // Handle outgoing messages var outWg sync.WaitGroup outWg.Add(1) go func() { defer outWg.Done() for { select { case <-ctx.Done(): return case msg, ok := <-outCh: if !ok { return } handleOutgoingMessage(msg, true) // isKeygen = true } } }() // Handle incoming messages from stdin var inWg sync.WaitGroup inWg.Add(1) go func() { defer inWg.Done() scanner := bufio.NewScanner(os.Stdin) // 增加 buffer 大小到 1MB,默认 64KB 可能不够大消息 scanner.Buffer(make([]byte, 1024*1024), 1024*1024) for scanner.Scan() { select { case <-ctx.Done(): return default: } var msg Message if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil { continue } if msg.Type == "incoming" { handleIncomingMessage(msg, localParty, partyIndexMap, errCh) } } }() // Track progress (final completion reporting) totalRounds := 4 // GG20 keygen has 4 rounds // Wait for completion select { case <-ctx.Done(): return nil, ctx.Err() case err := <-errCh: return nil, err case saveData := <-endCh: // Keygen completed successfully sendProgress(totalRounds, totalRounds) // Get public key pubKey := saveData.ECDSAPub.ToECDSAPubKey() pubKeyBytes := make([]byte, 33) pubKeyBytes[0] = 0x02 + byte(pubKey.Y.Bit(0)) xBytes := pubKey.X.Bytes() copy(pubKeyBytes[33-len(xBytes):], xBytes) // Serialize and encrypt save data saveDataBytes, err := json.Marshal(saveData) if err != nil { return nil, fmt.Errorf("failed to serialize save data: %w", err) } // Encrypt with password (simple XOR for now - should use AES-GCM in production) encryptedShare := encryptShare(saveDataBytes, password) return &keygenResult{ PublicKey: pubKeyBytes, EncryptedShare: encryptedShare, }, nil } } // extractRoundFromMessageType parses the round number from a tss-lib message type string. // Returns 0 if parsing fails (safe fallback). // Example: "binance.tsslib.ecdsa.keygen.KGRound2Message1" -> 2 func extractRoundFromMessageType(msgType string) int { matches := roundRegex.FindStringSubmatch(msgType) if len(matches) >= 2 { if round, err := strconv.Atoi(matches[1]); err == nil { return round } } return 0 // Safe fallback - doesn't affect protocol, just shows 0 in UI } func handleOutgoingMessage(msg tss.Message, isKeygen bool) { msgBytes, _, err := msg.WireBytes() if err != nil { return } var toParties []string if !msg.IsBroadcast() { for _, to := range msg.GetTo() { toParties = append(toParties, to.Id) } } outMsg := Message{ Type: "outgoing", IsBroadcast: msg.IsBroadcast(), ToParties: toParties, Payload: base64.StdEncoding.EncodeToString(msgBytes), } data, _ := json.Marshal(outMsg) fmt.Println(string(data)) // Extract current round from message type and send progress update totalRounds := 4 // GG20 keygen has 4 rounds if !isKeygen { totalRounds = 9 // GG20 signing has 9 rounds } currentRound := extractRoundFromMessageType(msg.Type()) sendProgress(currentRound, totalRounds) } func handleIncomingMessage( msg Message, localParty tss.Party, partyIndexMap map[int]*tss.PartyID, errCh chan error, ) { fromParty, ok := partyIndexMap[msg.FromPartyIndex] if !ok { return } payload, err := base64.StdEncoding.DecodeString(msg.Payload) if err != nil { return } parsedMsg, err := tss.ParseWireMessage(payload, fromParty, msg.IsBroadcast) if err != nil { return } go func() { _, err := localParty.Update(parsedMsg) if err != nil { // Only send fatal errors if !isDuplicateError(err) { errCh <- err } } }() } func isDuplicateError(err error) bool { if err == nil { return false } errStr := err.Error() return contains(errStr, "duplicate") || contains(errStr, "already received") } func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsImpl(s, substr)) } func containsImpl(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false } func encryptShare(data []byte, password string) []byte { // TODO: Use proper AES-256-GCM encryption // For now, just prepend a marker and the password hash // This is NOT secure - just a placeholder result := make([]byte, len(data)+32) copy(result[:32], hashPassword(password)) copy(result[32:], data) return result } func hashPassword(password string) []byte { // Simple hash - should use PBKDF2 or Argon2 in production hash := make([]byte, 32) for i := 0; i < len(password) && i < 32; i++ { hash[i] = password[i] } return hash } func sendProgress(round, totalRounds int) { msg := Message{ Type: "progress", Round: round, TotalRounds: totalRounds, } data, _ := json.Marshal(msg) fmt.Println(string(data)) } func sendError(errMsg string) { msg := Message{ Type: "error", Error: errMsg, } data, _ := json.Marshal(msg) fmt.Println(string(data)) } func sendResult(publicKey, encryptedShare []byte, partyIndex int) { msg := Message{ Type: "result", PublicKey: base64.StdEncoding.EncodeToString(publicKey), EncryptedShare: base64.StdEncoding.EncodeToString(encryptedShare), PartyIndex: partyIndex, } data, _ := json.Marshal(msg) fmt.Println(string(data)) } func sendSignResult(signature []byte, recoveryID int, partyIndex int) { // Append recovery ID to signature (r + s + v = 64 + 1 = 65 bytes) // This is needed for EVM transaction signing signatureWithV := make([]byte, len(signature)+1) copy(signatureWithV, signature) signatureWithV[len(signature)] = byte(recoveryID) msg := Message{ Type: "result", Payload: base64.StdEncoding.EncodeToString(signatureWithV), PartyIndex: partyIndex, } data, _ := json.Marshal(msg) fmt.Println(string(data)) } func decryptShare(encryptedData []byte, password string) ([]byte, error) { // Match the encryption format: first 32 bytes are password hash, rest is data if len(encryptedData) < 32 { return nil, fmt.Errorf("encrypted data too short") } // Verify password (simple check - matches encryptShare) expectedHash := hashPassword(password) actualHash := encryptedData[:32] // Simple comparison match := true for i := 0; i < 32; i++ { if expectedHash[i] != actualHash[i] { match = false break } } if !match { return nil, fmt.Errorf("incorrect password") } return encryptedData[32:], nil } func executeSign( sessionID, partyID string, partyIndex, thresholdT, thresholdN int, participants []Participant, messageHashHex string, shareDataBase64 string, password string, ) (*signResult, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() // Handle signals for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan cancel() }() // Decode and decrypt share data encryptedShare, err := base64.StdEncoding.DecodeString(shareDataBase64) if err != nil { return nil, fmt.Errorf("failed to decode share data: %w", err) } shareBytes, err := decryptShare(encryptedShare, password) if err != nil { return nil, fmt.Errorf("failed to decrypt share: %w", err) } // Parse keygen save data var keygenData keygen.LocalPartySaveData if err := json.Unmarshal(shareBytes, &keygenData); err != nil { return nil, fmt.Errorf("failed to parse keygen data: %w", err) } // Decode message hash messageHash, err := hex.DecodeString(messageHashHex) if err != nil { return nil, fmt.Errorf("failed to decode message hash: %w", err) } if len(messageHash) != 32 { return nil, fmt.Errorf("message hash must be 32 bytes, got %d", len(messageHash)) } msgBigInt := new(big.Int).SetBytes(messageHash) // Create TSS party IDs for signing participants // IMPORTANT: For tss-lib signing, we must reconstruct the party IDs in the same way // as during keygen. The signing subset (T parties) must use their original keys from keygen. // // The keygenData.Ks contains the public keys for all N parties from keygen. // We need to create party IDs that match the original keygen party structure, // but only include the T parties that are participating in this signing session. // Create party IDs only for the signing participants tssPartyIDs := make([]*tss.PartyID, 0, len(participants)) var selfTSSID *tss.PartyID for _, p := range participants { // Use the keygen key at this party's index // The party key in tss-lib uses the key's big.Int representation partyKey := tss.NewPartyID( p.PartyID, fmt.Sprintf("party-%d", p.PartyIndex), big.NewInt(int64(p.PartyIndex+1)), ) tssPartyIDs = append(tssPartyIDs, partyKey) if p.PartyID == partyID { selfTSSID = partyKey } } if selfTSSID == nil { return nil, fmt.Errorf("self party not found in participants") } // Sort party IDs (important for tss-lib) sortedPartyIDs := tss.SortPartyIDs(tssPartyIDs) // Create peer context and parameters // IMPORTANT: TSS-lib threshold convention: threshold=t means (t+1) signers required // User says "2-of-3" meaning 2 signers needed, so we pass (thresholdT-1) to TSS-lib // This MUST match keygen exactly! // For 2-of-3: thresholdT=2, tss-lib threshold=1, signers_needed=1+1=2 ✓ peerCtx := tss.NewPeerContext(sortedPartyIDs) tssThreshold := thresholdT - 1 params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, len(sortedPartyIDs), tssThreshold) fmt.Fprintf(os.Stderr, "[TSS-SIGN] NewParameters: partyCount=%d, tssThreshold=%d (from thresholdT=%d, means %d signers needed)\n", len(sortedPartyIDs), tssThreshold, thresholdT, thresholdT) // 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. fmt.Fprintf(os.Stderr, "[TSS-SIGN] Original keygenData has %d parties (Ks length)\n", len(keygenData.Ks)) fmt.Fprintf(os.Stderr, "[TSS-SIGN] Building subset for %d signing parties\n", len(sortedPartyIDs)) // Debug: print keygenData.Ks keys for i, k := range keygenData.Ks { keyHex := hex.EncodeToString(k.Bytes()) fmt.Fprintf(os.Stderr, "[TSS-SIGN] keygenData.Ks[%d] = %s\n", i, keyHex) } // Debug: print sortedPartyIDs keys for i, p := range sortedPartyIDs { keyHex := hex.EncodeToString(p.Key) idPrefix := p.Id if len(idPrefix) > 8 { idPrefix = idPrefix[:8] } fmt.Fprintf(os.Stderr, "[TSS-SIGN] sortedPartyIDs[%d]: Id=%s, Moniker=%s, Key=%s\n", i, idPrefix, p.Moniker, keyHex) } subsetKeygenData := keygen.BuildLocalSaveDataSubset(keygenData, sortedPartyIDs) fmt.Fprintf(os.Stderr, "[TSS-SIGN] Subset keygenData has %d parties (Ks length)\n", len(subsetKeygenData.Ks)) // 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 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) for _, p := range sortedPartyIDs { for _, orig := range participants { if orig.PartyID == p.Id { partyIndexMap[orig.PartyIndex] = p break } } } // Start the local party go func() { if err := localParty.Start(); err != nil { errCh <- err } }() // Handle outgoing messages var outWg sync.WaitGroup outWg.Add(1) go func() { defer outWg.Done() for { select { case <-ctx.Done(): return case msg, ok := <-outCh: if !ok { return } handleOutgoingMessage(msg, false) // isKeygen = false (signing) } } }() // Handle incoming messages from stdin var inWg sync.WaitGroup inWg.Add(1) go func() { defer inWg.Done() scanner := bufio.NewScanner(os.Stdin) // 增加 buffer 大小到 1MB scanner.Buffer(make([]byte, 1024*1024), 1024*1024) for scanner.Scan() { select { case <-ctx.Done(): return default: } var msg Message if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil { continue } if msg.Type == "incoming" { handleIncomingMessage(msg, localParty, partyIndexMap, errCh) } } }() // Track progress - GG20 signing has 9 rounds totalRounds := 9 // Wait for completion select { case <-ctx.Done(): return nil, ctx.Err() case err := <-errCh: return nil, err case sigData := <-endCh: // Signing completed successfully sendProgress(totalRounds, totalRounds) // Construct signature in DER format or raw R||S format // sigData contains R, S, and recovery ID rBytes := sigData.R sBytes := sigData.S // Create raw signature: R (32 bytes) || S (32 bytes) signature := make([]byte, 64) copy(signature[32-len(rBytes):32], rBytes) copy(signature[64-len(sBytes):64], sBytes) // Recovery ID for Ethereum-style signatures recoveryID := int(sigData.SignatureRecovery[0]) return &signResult{ Signature: signature, RecoveryID: recoveryID, }, nil } }