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

Protocol Iterator

core/protocol — the Iterator and Message types that drive every interactive round-based protocol in this library, plus the crank loop you write to run them.

core/protocol is 110 lines and contains no cryptography. It is the transport contract for interactive protocols: a two-method interface, an envelope struct, base64/JSON codecs, and two sentinel errors. Everything in threshold ECDSA and the MPC enclave is driven through it.

Reach for this page when you are wiring a DKLs18 DKG, sign, or refresh into your own transport (HTTP, gRPC, a queue) and need to know what to serialize, when to stop, and how to get the result out.

The Iterator interface

type Iterator interface {
	// Next runs the next round of the protocol.
	// Returns `ErrProtocolFinished` when protocol has completed.
	Next(input *Message) (*Message, error)

	// Result returns the final result, if any, of the completed protocol.
	// Returns nil if the protocol has not yet terminated.
	// Returns an error if an error was encountered during protocol execution.
	Result(version uint) (*Message, error)
}

That is the whole abstraction. A protocol participant is a state machine holding a list of round functions and an index; Next runs the current round and advances. The concrete implementation in tecdsa/dklsv1 is a protoStepper:

type protoStepper struct {
	steps []func(input *protocol.Message) (*protocol.Message, error)
	step  int
}

func (p *protoStepper) Next(input *protocol.Message) (*protocol.Message, error) {
	if p.step >= len(p.steps) {
		return nil, protocol.ErrProtocolFinished
	}
	output, err := p.steps[p.step](input)
	if err != nil {
		return nil, err
	}
	p.step++
	return output, nil
}

The implications are worth stating plainly:

  • The iterator is stateful and single-use. There is no reset. One AliceDkg value runs one DKG.
  • It is not safe for concurrent use. step is a plain int. One goroutine per participant.
  • ErrProtocolFinished is a success signal, not a failure. It means “I have no more rounds”. Any other non-nil error is a real failure and the protocol must be abandoned.
  • Next(nil) is how you start. The first speaker receives a nil input message.

Message

type Message struct {
	Payloads map[string][]byte `json:"payloads"`
	Metadata map[string]string `json:"metadata"`
	Protocol string            `json:"protocol"`
	Version  uint              `json:"version"`
}
PropType
Payloadsmap[string][]byte

The round's actual wire data, keyed by a payload label. The dklsv1 serializers use the single key "direct".

Typemap[string][]byte
Metadata?map[string]string

String side channel. dklsv1 populates it with {"round": "1"} etc. — the round number as a decimal string. Nothing reads it back; the round sequencing comes from the iterator's own step index.

Typemap[string]string
Protocolstring

Which protocol this message belongs to — one of the Dkls18* constants.

Typestring
Versionuint

Serialization version of the payloads. Version0 = 100, Version1 = 200.

Typeuint

Protocol name constants

Verbatim from core/protocol:

Constant Value
protocol.Dkls18Dkg "DKLs18-DKG"
protocol.Dkls18Sign "DKLs18-Sign"
protocol.Dkls18Refresh "DKLs18-Refresh"

Those are the only three. There is no constant for the Ed25519 threshold scheme, FROST, or the Gennaro DKG — those packages do not use this envelope.

Version constants

Constant Value Note
protocol.Version0 100 Defined but not implemented by any serializer.
protocol.Version1 200 The only working value. Pass this to NewAliceDkg, Result, and the Encode*/Decode* helpers.

The source explains the numbering: “versions will increment in 100 intervals, to leave room for adding other versions in between them if it is ever needed in the future.” Note the doc comment on Version1 reads “Version1 is version 2!” — that is a copy-paste slip in the comment, not a semantic claim; the value is 200.

Sentinel errors

var (
	ErrNotInitialized   = fmt.Errorf("object has not been initialized")
	ErrProtocolFinished = fmt.Errorf("the protocol has finished")
)

Those two are the complete set. ErrProtocolFinished is returned by Next once the step list is exhausted. ErrNotInitialized is returned by Result when the iterator’s inner protocol object is nil — i.e. you constructed the wrapper but the underlying dkg.Alice/dkg.Bob was never built.

Both are fmt.Errorf values with no wrapping, so errors.Is and == are equivalent for them. The repository’s own loops use !=; errors.Is is the better habit for your code.

The crank pattern

Two Iterators pass one *protocol.Message back and forth. Whatever first.Next returns becomes the input to second.Next, and vice versa, until both report ErrProtocolFinished.

Construct both participants

Both sides need the same *curves.Curve and the same version. For DKG that is all the input there is.

Call Next on the first speaker with a nil message

Who speaks first depends on the protocol. For DKLs18 DKG, Bob starts. For sign and refresh, Alice starts. Getting this backwards makes the first round fail on an unexpected input.

Feed each output into the other party

The message returned by one Next is the input to the other’s Next. This is where your transport goes: EncodeMessage on the way out, DecodeMessage on the way in.

Stop when both report ErrProtocolFinished

Not one — both. A participant can finish a round earlier than its peer, so the loop condition is a conjunction of two “still not finished” tests.

Pull the output with Result

Result(version) hands back a *Message carrying the serialized output. Feed it to the package’s Decode* helper to get a typed struct.

package main

import (
	"errors"
	"fmt"

	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/core/protocol"
	"github.com/sonr-io/crypto/tecdsa/dklsv1"
)

// crank drives two Iterators against each other until both are finished.
// `first` is whoever speaks first: Bob for DKG, Alice for sign and refresh.
func crank(first, second protocol.Iterator) error {
	var (
		msg       *protocol.Message
		firstErr  error
		secondErr error
	)

	for !errors.Is(firstErr, protocol.ErrProtocolFinished) ||
		!errors.Is(secondErr, protocol.ErrProtocolFinished) {

		msg, firstErr = first.Next(msg)
		if firstErr != nil && !errors.Is(firstErr, protocol.ErrProtocolFinished) {
			return firstErr
		}

		msg, secondErr = second.Next(msg)
		if secondErr != nil && !errors.Is(secondErr, protocol.ErrProtocolFinished) {
			return secondErr
		}
	}
	return nil
}

func main() {
	curve := curves.K256()

	alice := dklsv1.NewAliceDkg(curve, protocol.Version1)
	bob := dklsv1.NewBobDkg(curve, protocol.Version1)

	// Bob speaks first for DKG.
	if err := crank(bob, alice); err != nil {
		panic(err)
	}

	aliceResult, err := alice.Result(protocol.Version1)
	if err != nil {
		panic(err)
	}
	fmt.Println(aliceResult.Protocol, aliceResult.Version, len(aliceResult.Payloads))
	// DKLs18-DKG 200 1

	out, err := dklsv1.DecodeAliceDkgResult(aliceResult)
	if err != nil {
		panic(err)
	}
	fmt.Println(out.PublicKey.CurveName()) // secp256k1
}

This is exactly the shape of mpc.RunProtocol(firstParty, secondParty) and of runIteratedProtocol in tecdsa/dklsv1’s own tests. mpc.CheckIteratedErrors(aErr, bErr) is the helper that collapses the two returned errors into a single error (nil when both are ErrProtocolFinished).

Crossing a real network

Over a wire you serialize the envelope. EncodeMessage produces a base64-encoded JSON string:

wire, err := protocol.EncodeMessage(msg) // base64(json(msg))
if err != nil {
	return err
}
// ... send `wire` to the peer ...

Who consumes this

Protocols that do not use core/protocol: dkg/frost, dkg/gennaro, dkg/gennaro2p, ted25519, and the ot/* packages all expose their own round methods directly. If you are working with those, you write the round sequencing by hand rather than in a crank loop.

Last updated on September 2, 2026

Was this page helpful?