--- title: Protocol Iterator description: 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. sidebar: order: 4 icon: arrow-left-right --- `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](/threshold/threshold-ecdsa) and the [MPC enclave](/identity/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 ```go 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`: ```go 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` ```go type Message struct { Payloads map[string][]byte `json:"payloads"` Metadata map[string]string `json:"metadata"` Protocol string `json:"protocol"` Version uint `json:"version"` } ``` ### 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`. :::warning[`Version0` is a dead constant, and the two version checks disagree] No serializer implements a `Version0` layout. Constructing an iterator with it fails at the first round: ```go bob := dklsv1.NewBobDkg(curves.K256(), protocol.Version0) m, err := bob.Next(nil) // m == nil, err == "only version 1 is supported" ``` The DKG and sign serializers gate on strict equality (`if version != protocol.Version1`). The refresh serializers instead use `versionIsSupported`, which rejects only `messageVersion < protocol.Version1` — so a hypothetical `300` would sail past the refresh check and then fail somewhere deeper. Pass `protocol.Version1` everywhere, never hardcode `200`, and store the version alongside any persisted keyshare. ::: ### Sentinel errors ```go 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 `Iterator`s pass one `*protocol.Message` back and forth. Whatever `first.Next` returns becomes the input to `second.Next`, and vice versa, until both report `ErrProtocolFinished`. Both sides need the same `*curves.Curve` and the same version. For DKG that is all the input there is. **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. 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. 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. `Result(version)` hands back a `*Message` carrying the serialized output. Feed it to the package's `Decode*` helper to get a typed struct. ```go title="crank.go" 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`). :::danger[`Result` returns `(nil, nil)` if the protocol has not finished] Calling `Result` on a fresh, un-cranked iterator returns a **nil message and a nil error** — the completion check comes before the initialization check. Verified against `dklsv1.AliceDkg.Result`: ```go m, err := dklsv1.NewAliceDkg(curve, protocol.Version1).Result(protocol.Version1) // m == nil, err == nil ``` Every `Decode*` helper will then nil-dereference on `m.Payloads`. Always nil-check the message, not just the error. ::: ## Crossing a real network Over a wire you serialize the envelope. `EncodeMessage` produces a base64-encoded JSON string: ```go wire, err := protocol.EncodeMessage(msg) // base64(json(msg)) if err != nil { return err } // ... send `wire` to the peer ... ``` :::danger[`DecodeMessage` panics on any non-trivial message — do not use it] `Message.UnmarshalJSON` decodes into a `map[string]any` and then type-asserts the values: ```go case "payloads": m.Payloads = v.(map[string][]byte) // v is always map[string]interface{} case "metadata": m.Metadata = v.(map[string]string) // same problem ``` `encoding/json` never produces `map[string][]byte` or `map[string]string` when decoding into `any` — it produces `map[string]interface{}`. So the assertion always fails, and because it is an unchecked single-value assertion it **panics** rather than erroring. Reproduced against the current source: encoding a message with one payload succeeds, and decoding it panics with ``` interface conversion: interface {} is map[string]interface {}, not map[string][]uint8 ``` `DecodeMessage` has **zero callers inside this repository**, which is why the defect has survived — `mpc` calls `EncodeMessage` on the way out but never `DecodeMessage` on the way in. **Workaround.** Do not call `protocol.DecodeMessage`. Because `Message` has correct `json` struct tags, plain `encoding/json` against a *shadow struct* works fine — you just have to bypass the broken method: ```go type wireMessage struct { Payloads map[string][]byte `json:"payloads"` Metadata map[string]string `json:"metadata"` Protocol string `json:"protocol"` Version uint `json:"version"` } func decode(s string) (*protocol.Message, error) { bz, err := base64.StdEncoding.DecodeString(s) if err != nil { return nil, err } var w wireMessage if err := json.Unmarshal(bz, &w); err != nil { return nil, err } return &protocol.Message{ Payloads: w.Payloads, Metadata: w.Metadata, Protocol: w.Protocol, Version: w.Version, }, nil } ``` (`EncodeMessage` is fine — `MarshalJSON` uses a type alias and produces correct output, with `[]byte` payloads base64-encoded per Go's normal rules.) ::: :::warning[The envelope carries no authentication or replay protection] `Message` is a plaintext struct. There is no MAC, no sender identity, and no session id — `Metadata` carries only `{"round": "N"}`, written by the serializer and never read back, so you cannot repurpose it without colliding with that key. The DKLs18 rounds are designed for an authenticated channel; the library gives you none. Run this over an authenticated, ordered, confidential transport and bind messages to a session at that layer. `mpc` layers AES-GCM over `EncodeMessage` output for keyshare storage (`mpc.EncryptKeyshare`), but that is at-rest encryption of a *result*, not channel security for the rounds. ::: ## Who consumes this `tecdsa/dklsv1` — `AliceDkg`/`BobDkg`, `AliceSign`/`BobSign`, `AliceRefresh`/`BobRefresh` all implement `Iterator`, plus the `Encode*`/`Decode*` result helpers. `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.