How the t-of-n Threshold Signature Scheme Works in Mpcium: A Technical Deep Dive
Mpcium implements a t-of-n threshold signature scheme using the tss-lib library, requiring t+1 parties to collaborate on signing while supporting distributed key generation and dynamic resharing for both ECDSA and EdDSA keys.
Mpcium is an open-source threshold signature service that provides secure, distributed key management for blockchain applications. The repository fystack/mpcium delivers a complete implementation of the t-of-n threshold signature scheme, wrapping the tss-lib cryptographic library to enable fault-tolerant signing where no single entity controls the private key.
Core Phases of the t-of-n Threshold Signature Scheme
The implementation organizes threshold cryptography into three distinct phases: distributed key generation, collaborative signing, and dynamic resharing. Each phase utilizes specific session handlers in the pkg/mpc/ directory.
Distributed Key Generation
During key generation, n parties execute a distributed protocol with a configurable threshold t. The private key is mathematically split into n shares, requiring t+1 shares for any cryptographic operation. The resulting public key is stored for external verification.
In pkg/mpc/ecdsa_keygen_session.go, the newECDSAKeygenSession function initializes the protocol:
// From pkg/mpc/ecdsa_keygen_session.go
s.party = keygen.NewLocalParty(params, outCh, endCh, *preParams)
The keygen.NewLocalParty call originates from the tss-lib library and manages the multi-round key generation protocol. Upon completion, each node stores its share locally while the system persists the KeyInfo record containing the threshold and participant list.
Collaborative Signing with t+1 Parties
When signing a transaction, exactly t+1 parties must participate. The ecdsaSigningSession enforces this requirement before invoking the cryptographic protocol.
The signing logic resides in pkg/mpc/ecdsa_signing_session.go:
// Threshold validation before signing
if len(s.participantPeerIDs) < keyInfo.Threshold+1 {
return ErrNotEnoughParticipants
}
result := lo.Intersect(s.participantPeerIDs, keyInfo.ParticipantPeerIDs)
if len(result) < keyInfo.Threshold+1 {
return fmt.Errorf("Incompatible peerIDs …")
}
With validation passed, the session instantiates the signing party:
s.party = signing.NewLocalParty(tx, params, data, s.outCh, s.endCh)
The signing.NewLocalParty function from tss-lib executes the threshold signing rounds. The resulting signature is verified locally using ecdsa.Verify before transmission to the client.
Dynamic Resharing and Threshold Updates
Mpcium supports changing the committee composition and threshold without regenerating the underlying key. The resharing protocol redistributes shares to a new set of parties while preserving the original secret.
Implemented in pkg/mpc/ecdsa_resharing_session.go:
reshareParams := tss.NewReSharingParameters(
tss.S256(),
tss.NewPeerContext(oldPartyIDs),
tss.NewPeerContext(newPartyIDs),
selfID,
len(oldPartyIDs),
oldThreshold,
len(newPartyIDs),
newThreshold,
)
s.party = resharing.NewLocalParty(reshareParams, share, s.outCh, s.endCh)
Upon completion, the system persists a new KeyInfo record with the updated Threshold and ParticipantPeerIDs, incrementing the version number to track the committee evolution.
Threshold Enforcement and Key Metadata
The threshold value t is not merely a runtime parameter; it is persisted and validated at every critical operation to maintain the security guarantees of the t-of-n scheme.
Storing the Threshold in KeyInfo
The KeyInfo structure in pkg/keyinfo/keyinfo.go stores the threshold alongside participant metadata:
type KeyInfo struct {
ParticipantPeerIDs []string `json:"participant_peer_ids"`
Threshold int `json:"threshold"` // the "t" value
Version int `json:"version"`
}
During wallet creation, ecdsaKeygenSession.GenerateKey writes this record to Consul KV with the user-specified threshold. This persistence ensures that all subsequent operations reference the same consensus parameters.
Validating Participant Count Before Signing
Before initiating the TSS protocol, the signing session performs strict validation in pkg/mpc/ecdsa_signing_session.go:
- Count Check: Ensures at least
threshold+1participants are present. - Identity Check: Verifies that all participants are authorized members of the original key generation committee by intersecting the supplied peer list with the stored
KeyInfo.ParticipantPeerIDs.
Only after both checks pass does the session invoke signing.NewLocalParty, ensuring that the cryptographic operation adheres to the t-of-n security model.
Message Transport and Round Coordination
Mpcium abstracts the network layer from the cryptographic state machine using a message wrapper and round mapping utilities, enabling secure communication over NATS.
TssMessage Structure
All protocol messages flow through the TssMessage type defined in pkg/types/tss.go:
type TssMessage struct {
WalletID string
MsgBytes []byte
IsBroadcast bool
From *tss.PartyID
To *tss.PartyID
}
The session's handleTssMessage method signs broadcast messages with the node's identity (s.identityStore.SignMessage) and transmits them over NATS Pub/Sub. Direct messages are encrypted per-recipient using s.identityStore.EncryptMessage, ensuring confidentiality during the multi-round protocol.
Mapping TSS Rounds for Debugging
To aid observability, pkg/mpc/ecdsa_rounds.go maps raw wire messages to human-readable round names:
func GetEcdsaMsgRound(msg []byte, partyID *tss.PartyID, isBroadcast bool) (RoundInfo, error) {
parsedMsg, err := tss.ParseWireMessage(msg, partyID, isBroadcast)
// ...
switch parsedMsg.Content().(type) {
case *signing.SignRound1Message1:
return RoundInfo{Index: 0, RoundMsg: KEYSIGN1aUnicast}, nil
// additional cases...
}
}
This mapping is used exclusively for logging and debugging; the underlying state machine is fully driven by the tss-lib party object.
Practical Implementation Examples
The following examples demonstrate complete workflows for wallet creation, signing, and resharing using the Mpcium client API.
Creating a 2-of-5 Wallet
To initialize a wallet with a 2-of-5 threshold, the client constructs a GenerateKeyMessage and publishes it:
// Client-side initiation (from cmd/mpcium-cli/generate-initiator.go)
walletID := "wallet-123"
threshold := 2 // t = 2, requires 3 parties to sign
initiatorMsg := &types.GenerateKeyMessage{WalletID: walletID}
raw, _ := initiatorMsg.Raw()
sig, _ := localSigner.SignMessage(raw)
initiatorMsg.Signature = sig
// Publish to trigger distributed key generation
mpcClient.CreateWallet(walletID)
Server-side, nodes invoke newECDSAKeygenSession, which calls keygen.NewLocalParty from tss-lib. Upon completion, each node stores its share locally while the system persists the KeyInfo record with Threshold: 2.
Signing with Three Parties
Given a 2-of-5 wallet, signing requires exactly three participants (t+1). The client initiates the process:
txID := "tx-abc"
msg := &types.SignTxMessage{
KeyType: types.KeyTypeSecp256k1,
WalletID: walletID,
TxID: txID,
Tx: rawTxBytes,
NetworkInternalCode: "mainnet",
}
raw, _ := msg.Raw()
sig, _ := localSigner.SignMessage(raw)
msg.Signature = sig
// Publish to initiate signing session
Nodes running ecdsaSigningSession validate the participant count:
if len(s.participantPeerIDs) < keyInfo.Threshold+1 {
return ErrNotEnoughParticipants
}
result := lo.Intersect(s.participantPeerIDs, keyInfo.ParticipantPeerIDs)
if len(result) < keyInfo.Threshold+1 {
return fmt.Errorf("Incompatible peerIDs …")
}
With validation passed, signing.NewLocalParty executes the threshold signing protocol. The resulting signature is verified locally using ecdsa.Verify before transmission to the client.
Resharing to a New Committee
To migrate from a 2-of-5 configuration to a 3-of-7 configuration without changing the underlying key:
reshareMsg := &types.ResharingMessage{
SessionID: walletID,
NodeIDs: []string{"nodeA", "nodeB", "nodeC", "nodeD", "nodeE", "nodeF", "nodeG"},
NewThreshold: 3, // new t = 3, requires 4 parties to sign
KeyType: types.KeyTypeSecp256k1,
WalletID: walletID,
}
raw, _ := reshareMsg.Raw()
sig, _ := localSigner.SignMessage(raw)
reshareMsg.Signature = sig
Each node creates a resharing session via NewECDSAReshareSession, which constructs resharing.NewLocalParty with parameters for both the old and new committees. The protocol redistributes shares to the seven new parties while preserving the original secret. Upon completion, the system persists a new KeyInfo record with Threshold: 3 and the updated participant list.
Summary
- Mpcium implements t-of-n threshold signatures for ECDSA and EdDSA using the
tss-libcryptographic library. - The threshold
tis stored in theKeyInfostructure and enforced during signing operations. - Key generation utilizes
keygen.NewLocalPartyto create distributed shares acrossnparties. - Signing requires exactly
t+1participants, validated inecdsa_signing_session.gobefore invokingsigning.NewLocalParty. - Resharing enables dynamic committee changes and threshold updates via
resharing.NewLocalPartywithout key regeneration. - All TSS messages flow through the
TssMessagestructure and are transported over NATS with identity-based encryption.
Frequently Asked Questions
What is a t-of-n threshold signature scheme?
A t-of-n threshold signature scheme distributes cryptographic signing authority across n parties such that any subset of t+1 participants can generate a valid signature, while fewer than t+1 parties cannot reconstruct the private key or produce signatures. This construction ensures that no single entity controls the key material, providing fault tolerance against the compromise of up to t nodes.
How does Mpcium enforce the threshold during signing?
Mpcium enforces the threshold in pkg/mpc/ecdsa_signing_session.go by performing two validations before invoking the TSS protocol. First, it checks that the number of participating peer IDs is at least threshold+1. Second, it verifies that the participants are authorized by intersecting the supplied peer list with the KeyInfo.ParticipantPeerIDs stored during key generation. Only after both checks pass does the session call signing.NewLocalParty.
Can the threshold be changed after key generation?
Yes, Mpcium supports dynamic resharing to modify the threshold and committee composition without regenerating the underlying key. The pkg/mpc/ecdsa_resharing_session.go file implements this using resharing.NewLocalParty, which redistributes shares to a new set of parties while preserving the original secret. Upon successful completion, the system persists a new KeyInfo record with the updated Threshold and ParticipantPeerIDs.
What cryptographic library does Mpcium use for threshold signatures?
Mpcium builds upon tss-lib, an open-source Go implementation of threshold signature schemes. The library provides the underlying state machines for key generation (keygen.NewLocalParty), signing (signing.NewLocalParty), and resharing (resharing.NewLocalParty), which Mpcium wraps with network transport, persistent storage, and identity-based authentication layers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →