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
AliceDkgvalue runs one DKG. - It is not safe for concurrent use.
stepis a plainint. One goroutine per participant. ErrProtocolFinishedis 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"`
}
Payloadsmap[string][]byte
The round's actual wire data, keyed by a payload label. The dklsv1 serializers use the single key "direct".
map[string][]byteMetadata?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.
map[string]stringProtocolstring
Which protocol this message belongs to — one of the Dkls18* constants.
stringVersionuint
Serialization version of the payloads. Version0 = 100, Version1 = 200.
uintProtocol 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
Threshold ECDSA
tecdsa/dklsv1 — AliceDkg/BobDkg, AliceSign/BobSign, AliceRefresh/BobRefresh all implement Iterator, plus the Encode*/Decode* result helpers.
MPC enclave
mpc wraps the DKLs18 iterators with RunProtocol, CheckIteratedErrors, and keyshare encryption.
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.