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:

  1. Count Check: Ensures at least threshold+1 participants are present.
  2. 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-lib cryptographic library.
  • The threshold t is stored in the KeyInfo structure and enforced during signing operations.
  • Key generation utilizes keygen.NewLocalParty to create distributed shares across n parties.
  • Signing requires exactly t+1 participants, validated in ecdsa_signing_session.go before invoking signing.NewLocalParty.
  • Resharing enables dynamic committee changes and threshold updates via resharing.NewLocalParty without key regeneration.
  • All TSS messages flow through the TssMessage structure 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →