Permissively MIT Licensed.
This is an implementation of multi-party {t,n}-threshold ECDSA (Elliptic Curve Digital Signature Algorithm) based on Gennaro and Goldfeder CCS 2018 1 and EdDSA (Edwards-curve Digital Signature Algorithm) following a similar approach.
This library includes three protocols:
- Key Generation for creating secret shares with no trusted dealer ("keygen").
- Signing for using the secret shares to generate a signature ("signing").
- Dynamic Groups to change the group of participants while keeping the secret ("resharing").
ECDSA is used extensively for crypto-currencies such as Bitcoin, Ethereum (secp256k1 curve), NEO (NIST P-256 curve) and many more.
EdDSA is used extensively for crypto-currencies such as Cardano, Aeternity, Stellar Lumens and many more.
For such currencies this technique may be used to create crypto wallets where multiple parties must collaborate to sign transactions. See MultiSig Use Cases
One secret share per key/address is stored locally by each participant and these are kept safe by the protocol – they are never revealed to others at any time. Moreover, there is no trusted dealer of the shares.
In contrast to MultiSig solutions, transactions produced by TSS preserve the privacy of the signers by not revealing which t+1 participants were involved in their signing.
There is also a performance bonus in that blockchain nodes may check the validity of a signature without any extra MultiSig logic or processing.
The ecdsatss and eddsatss packages provide a broker-based API that is simpler to use than the legacy channel-based API. Messages are routed automatically through a tss.MessageBroker, and protocol rounds chain via callbacks — no manual channel management or message routing required.
// Create PartyIDs for each participant
parties := tss.SortPartyIDs(getParticipantPartyIDs())
ctx := tss.NewPeerContext(parties)
thisParty := tss.NewPartyID(id, moniker, uniqueKey)
// Select a curve: tss.S256() for ECDSA, tss.Edwards() for EdDSA
params := tss.NewParameters(tss.S256(), ctx, thisParty, len(parties), threshold)
// Set a MessageBroker that routes messages between parties over your transport
params.SetBroker(myBroker)The broker must implement tss.MessageBroker:
Receive(msg *tss.JsonMessage) error— called by the protocol to send outgoing messages; your implementation should route them to the destination party's broker.Connect(typ string, dest tss.MessageReceiver)— called by the protocol to register handlers for incoming messages by type.
// Pre-compute Paillier key and safe primes (recommended out-of-band)
preParams, _ := ecdsatss.GeneratePreParams(5 * time.Minute)
kg, err := ecdsatss.NewKeygen(ctx, params, *preParams)
// Wait for result:
select {
case key := <-kg.Done:
// Persist key to secure storage
case err := <-kg.Err:
// Handle error
}sig, err := key.NewSigning(ctx, msgHash, params)
select {
case result := <-sig.Done:
// result.Signature contains R || S
// result.Recovery contains the recovery byte
case err := <-sig.Err:
// Handle error
}// Old committee members pass their key; new committee members pass nil
rs, err := ecdsatss.NewResharing(ctx, resharingParams, oldKey, *newPreParams)
select {
case newKey := <-rs.Done:
// New committee: persist newKey
// Old committee: receives nil (old key is zeroed)
case err := <-rs.Err:
// Handle error
}The eddsatss package follows the same pattern but is simpler (no Paillier keys or pre-params):
// Keygen
kg, err := eddsatss.NewKeygen(ctx, params)
key := <-kg.Done
// Signing
sig, err := key.NewSigning(ctx, msg, params)
result := <-sig.Done // result.Signature is 64-byte Ed25519 signature
// Re-sharing
rs, err := eddsatss.NewResharing(ctx, resharingParams, oldKey)
newKey := <-rs.DoneThe ecdsa/keygen, ecdsa/signing, ecdsa/resharing, eddsa/keygen, eddsa/signing, and eddsa/resharing packages are now deprecated. They still work but will not receive new features.
| Legacy (deprecated) | Replacement |
|---|---|
ecdsa/keygen.NewLocalParty |
ecdsatss.NewKeygen |
ecdsa/keygen.GeneratePreParams |
ecdsatss.GeneratePreParams |
ecdsa/signing.NewLocalParty |
ecdsatss.Key.NewSigning |
ecdsa/resharing.NewLocalParty |
ecdsatss.NewResharing |
eddsa/keygen.NewLocalParty |
eddsatss.NewKeygen |
eddsa/signing.NewLocalParty |
eddsatss.Key.NewSigning |
eddsa/resharing.NewLocalParty |
eddsatss.NewResharing |
Key differences:
- No channels: Replace
outCh/endChwith atss.MessageBrokerandDone/Errchannels on the returned object. - No
Start()/Update()loop: The protocol runs automatically via broker callbacks. - No protobuf wire format: Messages use JSON via
tss.JsonMessage. - Key data:
ecdsatss.Keyandeddsatss.Keyhave the same fields as the oldLocalPartySaveDatastructs.
Legacy API usage (deprecated)
preParams, _ := keygen.GeneratePreParams(1 * time.Minute)
parties := tss.SortPartyIDs(getParticipantPartyIDs())
thisParty := tss.NewPartyID(id, moniker, uniqueKey)
ctx := tss.NewPeerContext(parties)
params := tss.NewParameters(tss.S256(), ctx, thisParty, len(parties), threshold)party := keygen.NewLocalParty(params, outCh, endCh, preParams)
go func() {
err := party.Start()
// handle err ...
}()party := signing.NewLocalParty(message, params, ourKeyData, outCh, endCh)
go func() {
err := party.Start()
// handle err ...
}()party := resharing.NewLocalParty(params, ourKeyData, outCh, endCh)
go func() {
err := party.Start()
// handle err ...
}()// Receiving updates from other parties
party.UpdateFromBytes(wireBytes, from, isBroadcast)
// Getting wire bytes from outgoing messages
wireBytes, routing, err := msg.WireBytes()Two fields PaillierSK.P and PaillierSK.Q is added in version 2.0. They are used to generate Paillier key proofs. Key valuts generated from versions before 2.0 need to regenerate(resharing) the key valuts to update the praparams with the necessary fileds filled.
The transport for messaging is left to the application layer and is not provided by this library. Each one of the following paragraphs should be read and followed carefully as it is crucial that you implement a secure transport to ensure safety of the protocol.
When you build a transport, it should offer a broadcast channel as well as point-to-point channels connecting every pair of parties. Your transport should also employ suitable end-to-end encryption (TLS with an AEAD cipher is recommended) between parties to ensure that a party can only read the messages sent to it.
Within your transport, each message should be wrapped with a session ID that is unique to a single run of the keygen, signing or re-sharing rounds. This session ID should be agreed upon out-of-band and known only by the participating parties before the rounds begin. Upon receiving any message, your program should make sure that the received session ID matches the one that was agreed upon at the start.
Additionally, there should be a mechanism in your transport to allow for "reliable broadcasts", meaning parties can broadcast a message to other parties such that it's guaranteed that each one receives the same message. There are several examples of algorithms online that do this by sharing and comparing hashes of received messages.
Timeouts and errors should be handled by your application. The method WaitingFor may be called on a Party to get the set of other parties that it is still waiting for messages from. You may also get the set of culprit parties that caused an error from a *tss.Error.
A full review of this library was carried out by Kudelski Security and their final report was made available in October, 2019. A copy of this report audit-binance-tss-lib-final-20191018.pdf may be found in the v1.0.0 release notes of this repository.