Skip to content
Sonr Crypto
Esc
navigateopen⌘Jpreview
On this page

Distributed Key Generation

FROST, Gennaro, and 2-party Gennaro DKG — interactive protocols that produce a signing key no single participant ever holds.

DKG replaces the trusted dealer. Instead of one process splitting a key it already has, every participant samples its own contribution, publishes a verifiable commitment to it, and privately sends one share to each peer. The joint signing key is the sum of every contribution; each party ends up holding a Shamir share of that sum plus the joint public key. The key itself is never assembled — not during generation, and not during signing.

Three protocols live here, and they are not interchangeable.

dkg/frost

2 rounds, t-of-n, modern curves.Curve API. Feeds ted25519/frost Schnorr signing.

dkg/gennaro

4 rounds, t-of-n, built on legacy sharing/v1. Produces the public shares tECDSA signing wants.

dkg/gennaro2p

2-of-2 façade over dkg/gennaro. Two rounds plus Finalize, one message type per round.

FROST DKG — dkg/frost

Two rounds, implementing the DKG half of eprint 2020/852 (the citation is in the package doc comment). Each participant runs Feldman VSS on its own secret and attaches a Schnorr proof of knowledge of the constant coefficient, which is what stops a participant from biasing the joint key by choosing its contribution after seeing everyone else’s.

import (
	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/dkg/frost"
	"github.com/sonr-io/crypto/sharing"
)

func twoPartyFrostDkg() error {
	curve := curves.ED25519()
	ctx := "1" // see the ctx warning below

	// Each participant knows its own id and the ids of all the others.
	p1, err := frost.NewDkgParticipant(1, 2, ctx, curve, 2)
	if err != nil {
		return err
	}
	p2, err := frost.NewDkgParticipant(2, 2, ctx, curve, 1)
	if err != nil {
		return err
	}

	// --- Round 1 --------------------------------------------------------
	bcast1, p2pSend1, err := p1.Round1(nil) // nil => sample a fresh secret
	if err != nil {
		return err
	}
	bcast2, p2pSend2, err := p2.Round1(nil)
	if err != nil {
		return err
	}

	// Broadcasts go to everyone, keyed by SENDER id, and include your own.
	bcast := map[uint32]*frost.Round1Bcast{1: bcast1, 2: bcast2}

	// P2P inputs are keyed by SENDER id too: p2p1[2] is what participant 2
	// sent to participant 1, i.e. p2pSend2[1].
	p2p1 := map[uint32]*sharing.ShamirShare{2: p2pSend2[1]}
	p2p2 := map[uint32]*sharing.ShamirShare{1: p2pSend1[2]}

	// --- Round 2 --------------------------------------------------------
	if _, err = p1.Round2(bcast, p2p1); err != nil {
		return err
	}
	if _, err = p2.Round2(bcast, p2p2); err != nil {
		return err
	}

	// p1.SkShare, p1.VkShare, p1.VerificationKey are now populated,
	// and p1.VerificationKey == p2.VerificationKey.
	_ = p1.SkShare
	return nil
}

Round1(secret []byte) (*Round1Bcast, Round1P2PSend, error)

Samples (or accepts) a secret s, runs Feldman VSS to get threshold commitments and limit shares, samples a nonce k, and computes the Schnorr-style proof c = H(i, CTX, a_0·G, k·G), w = s·c + k.

Broadcast (*Round1Bcast): the *sharing.FeldmanVerifier and the two scalars Wi, Ci. Point-to-point (Round1P2PSend, a type alias for map[uint32]*sharing.ShamirShare): one private share per peer, keyed by that peer’s id. Send p2pSend[j] to participant j only.

Pass nil for secret to sample. Passing a secret enables reshare-style flows, but a zero or out-of-range value is rejected (internal.ErrZeroValue or a scalar decode error).

Round2(bcast, p2psend) (*Round2Bcast, error)

For every peer: recomputes c_j and aborts unless it matches the broadcast Ci (this verifies the proof of knowledge), then runs FeldmanVerifier.Verify on the private share that peer sent. Both maps are keyed by sender id; bcast must include your own entry, p2psend must not.

Then sums the shares into the signing share and sums every peer’s Commitments[0] into the joint verification key.

Sets SkShare (curves.Scalar), VkShare (curves.Point, = SkShare · G), and VerificationKey (curves.Point, the joint public key) on the participant, and returns the latter two as *Round2Bcast.

Result fields

PropType
Iduint32

This participant's identifier.

Typeuint32
Curve*curves.Curve

The curve the DKG ran on.

Type*curves.Curve
SkSharecurves.Scalar

Secret signing share. Set by Round2. This is the value to persist and protect.

Typecurves.Scalar
VkSharecurves.Point

SkShare · G. Public; lets peers attribute a partial signature to this id.

Typecurves.Point
VerificationKeycurves.Point

The joint public key. Identical across all participants after Round2.

Typecurves.Point

The SkShare values are ordinary Shamir shares of the joint key, so sharing.NewShamir(t, n, curve).Combine(...) over {Id, SkShare.Bytes()} pairs reconstructs it — which the package’s own test does to prove correctness, and which production code should never do.

Transport

Round1Result bundles the two halves of round 1 for one recipient:

result := &frost.Round1Result{Broadcast: bcast1, P2P: p2pSend1[2]}
wire, err := result.Encode() // gob
// ...
decoded := &frost.Round1Result{}
err = decoded.Decode(wire)

Encode uses encoding/gob and registers the concrete commitment point and Ci scalar types on each call. There is no matching helper for round 2 — serialise Round2Bcast yourself.

Gennaro DKG — dkg/gennaro

Four rounds, implementing the DKG of eprint 2020/540 (cited in the package doc). The extra rounds buy a two-phase VSS that FROST’s single Feldman pass does not provide.

import (
	"math/big"

	"github.com/btcsuite/btcd/btcec/v2"

	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/dkg/gennaro"
)

func twoPartyGennaroDkg() error {
	// The blinding generator for Pedersen VSS. Must have unknown discrete log
	// w.r.t. the base point in real use — this fixed multiple is test-only.
	generator, err := curves.NewScalarBaseMult(btcec.S256(), big.NewInt(3333))
	if err != nil {
		return err
	}

	p1, err := gennaro.NewParticipant(1, 2, generator, curves.NewK256Scalar(), 2)
	if err != nil {
		return err
	}
	p2, err := gennaro.NewParticipant(2, 2, generator, curves.NewK256Scalar(), 1)
	if err != nil {
		return err
	}

	// Round 1
	bcast1, p2pSend1, err := p1.Round1(nil)
	if err != nil {
		return err
	}
	bcast2, p2pSend2, err := p2.Round1(nil)
	if err != nil {
		return err
	}
	bcast := map[uint32]gennaro.Round1Bcast{1: bcast1, 2: bcast2}
	p2p1 := map[uint32]*gennaro.Round1P2PSendPacket{2: p2pSend2[1]}
	p2p2 := map[uint32]*gennaro.Round1P2PSendPacket{1: p2pSend1[2]}

	// Round 2
	r2out1, err := p1.Round2(bcast, p2p1)
	if err != nil {
		return err
	}
	r2out2, err := p2.Round2(bcast, p2p2)
	if err != nil {
		return err
	}
	round3Input := map[uint32]gennaro.Round2Bcast{1: r2out1, 2: r2out2}

	// Round 3 — yields the joint public key and this party's secret share
	pubKey1, share1, err := p1.Round3(round3Input)
	if err != nil {
		return err
	}
	if _, _, err = p2.Round3(round3Input); err != nil {
		return err
	}

	// Round 4 — public shares for tECDSA signing (idempotent)
	publicShares1, err := p1.Round4()
	if err != nil {
		return err
	}
	_, _, _ = pubKey1, share1, publicShares1
	return nil
}

Round1(secret []byte) (Round1Bcast, Round1P2PSend, error)

Pedersen-committed sharing. The participant runs Pedersen VSS on its secret, producing a secret polynomial and a blinding polynomial. Round1Bcast is a type alias for []*v1.ShareVerifier — the threshold blinded commitments a_j·G + b_j·H, which reveal nothing about the secret. Round1P2PSend maps each peer id to a *Round1P2PSendPacket carrying that peer’s SecretShare and its matching BlindingShare.

Passing a non-nil secret performs proactive secret resharing rather than fresh key generation: the public key stays the same and only the shares change.

Round2(bcast, p2p) (Round2Bcast, error)

Verifies every received (secretShare, blindingShare) pair against the sender’s blinded commitments, then de-blinds: broadcasts the unblinded Feldman commitments a_j·G as Round2Bcast (also []*v1.ShareVerifier). Splitting the commit and reveal across two rounds is what makes the joint key unbiasable — nobody can see any a_0·G until every participant has already committed.

Round3(bcast) (*Round3Bcast, *v1.ShamirShare, error)

Checks each peer’s Feldman commitments against the Pedersen commitments it already holds, then assembles the joint public key. Returns the verification key (*Round3Bcast, an alias for v1.ShareVerifier) and this participant’s secret share.

Round4() (map[uint32]*curves.EcPoint, error)

Computes the per-participant public shares that tECDSA signing needs — skShare_i · G for every i — which get converted to additive shares once the signing set is known. Takes no arguments and is idempotent: calling it repeatedly returns the same map.

2-party Gennaro — dkg/gennaro2p

A façade over dkg/gennaro specialised for the 2-of-2 case. Its package doc states the simplification directly: no distinction between broadcast and peer messages, and only the counterparty’s message is used as round input because self-inputs are always ignored.

import (
	"github.com/btcsuite/btcd/btcec/v2"

	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/dkg/gennaro2p"
)

func twoPartyDkg() (*gennaro2p.DkgResult, *gennaro2p.DkgResult, error) {
	curve := btcec.S256()
	scalar := curves.NewK256Scalar()

	// Passing nil blind makes the client generate a secure blinding generator.
	client, err := gennaro2p.NewParticipant(1, 2, nil, scalar, curve)
	if err != nil {
		return nil, nil, err
	}

	// Round 1 carries the blind, so the server can adopt the client's.
	clientR1, err := client.Round1(nil)
	if err != nil {
		return nil, nil, err
	}

	server, err := gennaro2p.NewParticipant(2, 1, clientR1.Blind, scalar, curve)
	if err != nil {
		return nil, nil, err
	}
	serverR1, err := server.Round1(nil)
	if err != nil {
		return nil, nil, err
	}

	// Round 2 consumes the *counterparty's* round 1 output.
	clientR2, err := client.Round2(serverR1)
	if err != nil {
		return nil, nil, err
	}
	serverR2, err := server.Round2(clientR1)
	if err != nil {
		return nil, nil, err
	}

	// Finalize consumes the counterparty's round 2 output.
	clientResult, err := client.Finalize(serverR2)
	if err != nil {
		return nil, nil, err
	}
	serverResult, err := server.Finalize(clientR2)
	if err != nil {
		return nil, nil, err
	}
	return clientResult, serverResult, nil
}

Round1(secret []byte) (*Round1Message, error)

Wraps gennaro.Round1. Returns one flat message carrying Verifiers []*v1.ShareVerifier, SecretShare, BlindingShare, and Blind *curves.EcPoint.

Round2(msg *Round1Message) (*Round2Message, error)

Wraps gennaro.Round2 with the counterparty’s round-1 message as the sole input. Returns Round2Message{Verifiers}.

Finalize(msg *Round2Message) (*DkgResult, error)

Runs gennaro.Round3 and gennaro.Round4 back to back and packages the outcome as DkgResult{PublicKey *curves.EcPoint, SecretShare *v1.ShamirShare, PublicShares map[uint32]*curves.EcPoint}.

Caveats

Next

Last updated on September 2, 2026

Was this page helpful?