feat: init docs

This commit is contained in:
Prad Nukala
2026-09-02 15:29:51 -04:00
parent d2390a8aad
commit 69425e2b7a
45 changed files with 11790 additions and 18 deletions
+285
View File
@@ -0,0 +1,285 @@
---
title: Arithmetic & Commitments
description: The core package — modular arithmetic over big.Int with explicit moduli, constant-time comparison, hash-to-field, FiatShamir, safe primes, and the HMAC commitment scheme.
sidebar:
order: 3
icon: sigma
---
`core` is the one package in this library that does **not** use the [curve abstraction](/foundations/curves). It works directly on `math/big` integers with an explicit modulus, and it exists because a handful of constructions — Paillier, the legacy `sharing/v1` and `dkg/gennaro` layers, the older threshold ECDSA code — need integer arithmetic in a group whose order is not a curve order.
**Reach for `core` when** you are implementing something over the integers mod `m` for an `m` you chose yourself, need a byte-level HMAC commitment, or need RFC-shaped hash-to-field. **Do not reach for it** to do scalar arithmetic on a curve — `curve.Scalar` is faster, constant-time-oriented, and cannot silently escape its field.
## Modular arithmetic
Every helper takes the modulus as its *last* argument and returns `(*big.Int, error)`. The error is not decoration: it is how the package refuses nil inputs instead of panicking.
<TypeTable
type={{
"Add(x, y, m)": {
type: "(*big.Int, error)",
description: "z = x + y mod m. If m is nil the result is the unbounded integer sum. Errors only if x or y is nil.",
},
"Mul(x, y, m)": {
type: "(*big.Int, error)",
description: "z = x * y mod m. If m is nil the result is the unbounded product. Errors only if x or y is nil.",
},
"Exp(x, y, m)": {
type: "(*big.Int, error)",
description: "z = x^y mod m. Thin wrapper over big.Int.Exp; a nil m means no reduction. Errors only if x or y is nil.",
},
"Neg(x, m)": {
type: "(*big.Int, error)",
description: "z = -x mod m, reduced into [0, m). m is required — nil m is an error.",
},
"Inv(x, m)": {
type: "(*big.Int, error)",
description: "y such that x*y = 1 mod m. Errors if x is not invertible mod m ('cannot compute the multiplicative inverse').",
},
"Rand(m)": {
type: "(*big.Int, error)",
description: "Cryptographically secure random integer strictly in the range 1 < r < m. Rejection-samples until r > 1.",
},
"In(x, m)": {
type: "error",
description: "Membership test: nil if 0 <= x < m, otherwise internal.ErrZmMembership.",
},
"AnyNil(values ...)": {
type: "bool",
description: "true if any argument is nil. Used as the guard clause in every function above.",
},
}}
/>
Package-level integer constants are provided so you are not allocating them in loops: `core.Zero`, `core.One`, `core.Two`.
```go title="modular.go"
package main
import (
"fmt"
"math/big"
"github.com/sonr-io/crypto/core"
)
func main() {
m, _ := new(big.Int).SetString(
"208351617316091241234326746312124448251235562226470491514186331217050270460481", 10)
a, err := core.Rand(m)
if err != nil {
panic(err)
}
b, err := core.Rand(m)
if err != nil {
panic(err)
}
ab, _ := core.Mul(a, b, m)
aInv, err := core.Inv(a, m)
if err != nil {
panic(err) // a shares a factor with m
}
// (a*b) * a^-1 == b
back, _ := core.Mul(ab, aInv, m)
fmt.Println(core.ConstantTimeEq(back, b)) // true
fmt.Println(core.In(ab, m) == nil) // true
}
```
:::warning[`Add`, `Mul`, and `Exp` treat a nil modulus as "no reduction"]
This is deliberate — the source comment says *"we leave the value as an unbound integer"* — and it is a live footgun. `core.Add(x, y, nil)` succeeds and returns an unreduced integer that will fail a later `In` check or leak the un-modded value into a transcript. `Neg` and `Inv` require the modulus and error on nil. Do not rely on the guard clauses to catch a forgotten modulus.
:::
:::danger[`Rand` never returns 0 or 1]
The range is strictly `1 < r < m`. The source explains why: a 1 offers no hiding when multiplied into a FiatShamir combination, and a 0 collapses the result. This is the right default for blinding factors, but it means `core.Rand` is **not** a uniform sample over the whole of `Z_m` — if a protocol's soundness argument needs uniformity over the full range, this is the wrong function.
:::
## Constant-time comparison
```go
func ConstantTimeEqByte(a, b *big.Int) byte // 0x1 if equal, 0x0 otherwise
func ConstantTimeEq(a, b *big.Int) bool // ConstantTimeEqByte(a, b) == 1
```
Both compare `a.Bytes()` against `b.Bytes()` via `crypto/subtle.ConstantTimeCompare` **and** compare `Sign()`. Two nil arguments compare equal; one nil compares unequal.
:::warning[Constant time in the byte comparison only]
`big.Int.Bytes()` returns the minimal big-endian encoding, so its *length* leaks the magnitude of the value. `subtle.ConstantTimeCompare` also returns 0 immediately when the two lengths differ. So these functions are constant-time with respect to the *contents* of equal-length values, not with respect to bit length. For comparing secrets of unknown width, pad to a fixed width first.
:::
## Hashing and hash-to-field
| Function | Signature | Purpose |
| --- | --- | --- |
| `Hash` | `Hash(msg []byte, curve elliptic.Curve) (*big.Int, error)` | Hash-to-field: one field element for the given curve. |
| `ExpandMessageXmd` | `ExpandMessageXmd(f func() hash.Hash, msg, DST []byte, lenInBytes int) ([]byte, error)` | `expand_message_xmd` from the CFRG hash-to-curve draft, §5.4.1. |
| `I2OSP` | `I2OSP(b, n int) []byte` | Integer-to-octet-string, `n` bytes, big-endian. |
| `OS2IP` | `OS2IP(os []byte) *big.Int` | Octet-string-to-integer. |
| `FiatShamir` | `FiatShamir(values ...*big.Int) ([]byte, error)` | Iterated HKDF challenge derivation; 32-byte output. |
| `ComputeHMAC` | `ComputeHMAC(f func() hash.Hash, msg, k []byte) ([]byte, error)` | HMAC with an explicit hash constructor. |
| `Size` | `const Size = sha256.Size` | 32 — the width of commitments and nonces in this package. |
| `HashField` | `struct{ Order, Characteristic, ExtensionDegree *big.Int }` | Describes the field `F_p^k` for the curve being hashed to. |
| `Params` | `struct{ F *HashField; SecurityParameter int; Hash func() hash.Hash; L int }` | Per-curve hash-to-field parameters. |
### `Hash` — curve support and its fixed DST
`Hash` looks up a `Params` for the curve, then runs `expand_message_xmd` and reduces to one field element. Supported curves and their parameters, read from `getParams`:
| Curve (`Params().Name`) | Security parameter | Hash | `L` (bytes) |
| --- | --- | --- | --- |
| `secp256k1` (btcec) | 128 | SHA-256 | 48 |
| `P-256` | 128 | SHA-256 | 48 |
| `P-384` / `secp384r1` | 192 | SHA3-384 | 72 |
| `P-521` / `secp521r1` | 256 | SHA-512 | 98 |
| `Bls12381G1` | 128 | SHA-256 | 48 |
| `ed25519` | 128 | SHA-256 | 48 |
Any other curve returns `unsupported curve: <name>`.
:::danger[`Hash` uses a hard-coded domain separation tag]
The DST is the literal string `Coinbase_tECDSA`, baked into `hashToField`. There is no parameter to change it. That means:
- You get **no domain separation** between two different protocols that both call `core.Hash`. Two unrelated proofs over the same curve with the same message produce the same field element.
- It is not interoperable with any standard hash-to-curve suite ID, so it will not match another implementation's `hash_to_field`.
If you need a DST you control, call `ExpandMessageXmd` directly with your own tag and reduce yourself.
:::
```go title="hash_to_field.go"
// Custom DST, correct expansion, your own reduction.
okm, err := core.ExpandMessageXmd(sha256.New, msg, []byte("MYPROTO-V01-CS01"), 48)
if err != nil {
return nil, err
}
e := new(big.Int).Mod(core.OS2IP(okm), fieldCharacteristic)
```
`ExpandMessageXmd` errors only when `ceil(lenInBytes / hashSize) > 255`. It takes the hash *constructor*, not a `hash.Hash`, and it will nil-dereference if you pass `nil` — there is no guard.
### `FiatShamir`
Derives a 32-byte challenge from a sequence of integers. The construction is an iterated HKDF-SHA256: for each value, `okm_i = HKDF(f_i || value_i || okm_{i-1})`, where `f_i` is a 32-byte prefix whose leading byte decrements per iteration (`0xFF`, then `0xFE`, …). The source cites Signal's [X3DH](https://signal.org/docs/specifications/x3dh/#cryptographic-notation) and [XEdDSA](https://signal.org/docs/specifications/xeddsa/#hash-functions) notes as the design source. `info` is the fixed string `Coinbase tECDSA 1.0`; the salt is 32 zero bytes.
```go
challenge, err := core.FiatShamir(commitment, publicKey, nonce)
```
:::warning[Chaining, not concatenation — and the prefix trick is unusual]
Because each value is folded in separately and the previous output is appended to the next input, `FiatShamir(a, b)` is **not** `FiatShamir(concat(a, b))` — good, that is the point. But note the values are folded as `value.Bytes()`, the minimal big-endian encoding, so a value's length is not committed to. Two different value *sequences* whose concatenated minimal encodings coincide are still distinguished by the chaining, but the `info` string is fixed at `Coinbase tECDSA 1.0`, so there is again no per-protocol domain separation. Prefix your own protocol label as the first `*big.Int` if you need it.
:::
## Safe primes
```go
func GenerateSafePrime(bits uint) (*big.Int, error)
```
Returns a prime `p = 2q + 1` where `q` is also prime (a Sophie Germain prime), with `p` of the requested bit length. `bits` must be at least 3. The implementation picks a `bits-1`-bit prime `q`, computes `2q + 1`, and retries until `ProbablyPrime` accepts it with `max(bits/16, 8)` MillerRabin rounds.
:::warning[Expensive by construction]
This is a rejection loop over `rand.Prime`, and the density of safe primes makes it dramatically slower than generating an ordinary prime of the same size. It is the dominant cost of [Paillier](/zero-knowledge/paillier) key generation, which needs two of them. Generate keys ahead of time, off the request path; never call this inside a handler.
:::
## Commitments
`core` ships one commitment scheme, and it is a hash commitment — not Pedersen, not polynomial. It commits to *bytes*, not to a group element.
```go
type Commitment []byte // 32 bytes: HMAC-SHA256(key = nonce, msg)
type Witness struct {
Msg []byte
// unexported: r [32]byte, the random nonce
}
func Commit(msg []byte) (Commitment, *Witness, error)
func Open(c Commitment, d Witness) (bool, error)
```
`Commit` draws a 32-byte nonce from `crypto/rand` and returns `HMAC-SHA256(msg, key = nonce)` as the commitment, with the nonce hidden inside the `Witness`. `Open` recomputes the HMAC from `d.Msg` and the witness nonce and compares against `c` with `subtle.ConstantTimeCompare`.
```go title="commit.go"
package main
import (
"encoding/json"
"fmt"
"github.com/sonr-io/crypto/core"
)
func main() {
// Committer: publish c, keep w secret until the reveal phase.
c, w, err := core.Commit([]byte("bid: 42"))
if err != nil {
panic(err)
}
fmt.Println(len(c) == core.Size) // true, 32 bytes
// Witness marshals to JSON (msg + nonce) so it can be sent on reveal.
wire, _ := json.Marshal(w)
// Verifier: after receiving the witness.
var got core.Witness
if err := json.Unmarshal(wire, &got); err != nil {
panic(err)
}
ok, err := core.Open(c, got)
if err != nil {
panic(err)
}
fmt.Println(ok) // true
}
```
**Properties as implemented.** Hiding rests on HMAC-SHA256 being a PRF under the fresh 32-byte random key — the commitment is a PRF evaluation keyed by a secret nonce, so it reveals nothing about `msg` to anyone without the nonce. Binding rests on collision resistance: to open the same 32-byte commitment to a different message you would need `HMAC(msg', k') == HMAC(msg, k)`.
:::warning[Length is not committed independently, and `Open` only length-checks the commitment]
`Open` rejects a commitment whose length is not exactly `core.Size` (32), then does a constant-time compare. It performs no validation on the witness beyond that. In particular:
- The nonce is unexported and has no accessor — the only way to move a `Witness` between processes is its JSON marshalling. The wire shape has **no `json` tags**, so the field names are the Go defaults: `{"Msg":"<base64>","R":[209,218,...]}` — capital `Msg`, capital `R`, and the nonce as a 32-element JSON array of numbers, not base64 (it is a `[32]byte` array, not a slice). Anything reimplementing this format in another language must match that exactly.
- `UnmarshalJSON` performs no validation. A witness with an all-zero `R` decodes fine, and `Open` will then verify any commitment that was (incorrectly) produced with a zero nonce. Since `Commit` is the only way to get a nonce and it always reads 32 bytes from `crypto/rand`, this only bites if you hand-construct witnesses.
- There is no transcript or context binding. If your protocol has multiple concurrent commitments, include a session/index label inside `msg` yourself; the scheme will not do it for you.
:::
:::note[This is not a Pedersen commitment]
If you need additive homomorphism, or to commit to a scalar so that the commitment can be combined in the group, use the Pedersen VSS machinery in [secret sharing](/threshold/secret-sharing) or the Pedersen vector commitments inside [Bulletproofs](/zero-knowledge/bulletproof). `core.Commit` is the right tool for "reveal these bytes later" and nothing more.
:::
## The `internal` package
`go doc github.com/sonr-io/crypto/internal` lists a handful of tempting helpers:
```
func B10(s string) *big.Int
func BigInt2Ed25519Point(y *big.Int) (*edwards25519.Point, error)
func BigInt2Ed25519Scalar(x *big.Int) (*edwards25519.Scalar, error)
func ByteSub(b []byte)
func CalcFieldSize(curve elliptic.Curve) int
func Hash(info []byte, values ...[]byte) ([]byte, error)
func ReverseScalarBytes(inBytes []byte) []byte
```
plus the sentinel errors this library returns from `core`: `ErrNotOnCurve`, `ErrPointsDistinctCurves`, `ErrZmMembership`, `ErrResidueOne`, `ErrNCannotBeZero`, `ErrNilArguments`, `ErrZeroValue`, `ErrInvalidRound`, `ErrIncorrectCount`, `ErrInvalidJson`. There are also two vendored Ed25519 helper packages, `internal/ed25519/edwards25519` and `internal/ed25519/extra25519` (the latter with `PrivateKeyToCurve25519`, `PublicKeyToCurve25519`, `HashToEdwards`, `RepresentativeToPublicKey`, `ScalarBaseMult`).
:::danger[You cannot import any of these]
Go's `internal/` visibility rule confines them to `github.com/sonr-io/crypto/...`. Downstream modules cannot import `github.com/sonr-io/crypto/internal` at all — the compiler rejects it. This matters because `core`'s errors are *values from that package*: `core.In` returns `internal.ErrZmMembership` and `core.Add` returns `internal.ErrNilArguments`, but you have no way to name those variables in your own code.
**Workaround:** compare the message (`err.Error() == "x ∉ Z_m"`), or — better — treat these as opaque failures and validate your inputs before calling. Do not build control flow on `errors.Is` against a sentinel you cannot reference.
:::
`internal.ReverseScalarBytes` and `internal.CalcFieldSize` are the two you will most want and most miss; both are two lines and trivially reimplemented (`(curve.Params().BitSize + 7) / 8` for the latter).
## Next
<CardGroup cols={2}>
<Card title="Protocol iterator" href="/foundations/protocol" icon="arrow-left-right">
How interactive protocols in this library are cranked round by round.
</Card>
<Card title="Paillier" href="/zero-knowledge/paillier" icon="binary">
The main consumer of `GenerateSafePrime` and the modular arithmetic helpers.
</Card>
</CardGroup>
+300
View File
@@ -0,0 +1,300 @@
---
title: Curves
description: Every named curve constructor in core/curves, the complete Point and Scalar method sets, pairing curves, and a map of the low-level native field arithmetic underneath.
sidebar:
order: 2
icon: circle-dot
---
`core/curves` is the catalog. It exposes one constructor per supported group, all of which hand back a `*curves.Curve` (or a `*curves.PairingCurve` for the pairing-friendly ones). Everything on this page was read out of `go doc github.com/sonr-io/crypto/core/curves`.
**Reach for this page when** you need to know which curve a package will accept, what a serialized point looks like on the wire, or which method on `Point`/`Scalar` does the thing you want. **You do not need this page** if you are just passing a curve through — `curves.K256()` and go.
## Named curves
| Constructor | `Name` value | Constant | Notes |
| --- | --- | --- | --- |
| `curves.K256()` | `secp256k1` | `K256Name` | Bitcoin/Ethereum curve. 33-byte compressed points. |
| `curves.P256()` | `P-256` | `P256Name` | NIST P-256 / secp256r1. |
| `curves.ED25519()` | `ed25519` | `ED25519Name` | Edwards curve; 32-byte compressed points. |
| `curves.BLS12381G1()` | `BLS12381G1` | `BLS12381G1Name` | G1 of BLS12-381; 48-byte compressed points. |
| `curves.BLS12381G2()` | `BLS12381G2` | `BLS12381G2Name` | G2 of BLS12-381; 96-byte compressed points. |
| `curves.BLS12377G1()` | `BLS12377G1` | `BLS12377G1Name` | G1 of BLS12-377 (gnark-crypto backed). |
| `curves.BLS12377G2()` | `BLS12377G2` | `BLS12377G2Name` | G2 of BLS12-377. |
| `curves.PALLAS()` | `pallas` | `PallasName` | Pasta/Pallas curve. |
Two extra string constants exist for "the pairing construction, group unspecified": `BLS12831Name = "BLS12831"` and `BLS12377Name = "BLS12377"`.
### Lookup by name
```go
curve := curves.GetCurveByName(curves.K256Name)
if curve == nil {
return fmt.Errorf("unsupported curve")
}
```
`GetCurveByName` accepts every constant above. `BLS12831Name` and `BLS12377Name` both resolve to the **G1** curve. Anything else returns `nil`.
:::warning[`GetCurveByName` returns nil, not an error]
There is no second return value. If you feed it a name from user input or a wire format, you must nil-check before dereferencing, or you get a nil-pointer panic on the first field access.
:::
:::note[`BLS12831Name` is a typo that is now load-bearing]
The constant is spelled `BLS12831` (digits transposed) and its *value* is the string `"BLS12831"`. This is not cosmetic: `curves.BLS12381(...)` sets `PairingCurve.Name` to that string, so a BLS12-381 pairing curve reports `Name == "BLS12831"`. If you round-trip a pairing curve through its name, use the constant — never a hand-typed `"BLS12381"`.
:::
## Pairing curves
BBS+ and the accumulator need a pairing, so they take a `*curves.PairingCurve` — a *different type* from `*curves.Curve`. Passing `curves.BLS12381G1()` where a `*PairingCurve` is wanted will not compile.
```go
type PairingCurve struct {
Scalar PairingScalar
PointG1 PairingPoint
PointG2 PairingPoint
GT Scalar
Name string
}
```
<TypeTable
type={{
"BLS12381(preferredPoint Point)": {
type: "*PairingCurve",
description: "Builds the BLS12-381 pairing curve. The argument selects which group the curve's scalars prefer to project into — pass BLS12381G1().NewIdentityPoint() or the G2 equivalent.",
},
"GetPairingCurveByName(name string)": {
type: "*PairingCurve",
description: "Accepts BLS12381G1Name, BLS12381G2Name, or BLS12831Name. Returns nil for anything else.",
},
"NewG1GeneratorPoint() / NewG2GeneratorPoint()": {
type: "PairingPoint",
description: "Generators of G1 and G2.",
},
"NewG1IdentityPoint() / NewG2IdentityPoint()": {
type: "PairingPoint",
description: "Identity elements of G1 and G2.",
},
"NewScalar()": {
type: "PairingScalar",
description: "Zero scalar carrying the preferred-point projection.",
},
"ScalarG1BaseMult(sc) / ScalarG2BaseMult(sc)": {
type: "PairingPoint",
description: "Fixed-base multiplication in G1 or G2 respectively.",
},
}}
/>
`PairingPoint` and `PairingScalar` are extensions of the ordinary interfaces, so every `Point`/`Scalar` method is still available:
```go
type PairingPoint interface {
Point
OtherGroup() PairingPoint // G1 <-> G2
Pairing(rhs PairingPoint) Scalar // e(self, rhs) as a GT element
MultiPairing(...PairingPoint) Scalar
}
type PairingScalar interface {
Scalar
SetPoint(p Point) PairingScalar
}
```
Note that `Pairing` returns a `Scalar`, not a distinct GT type — the target group element is modelled as a `ScalarBls12381Gt`. It supports the `Scalar` arithmetic surface (`Mul`, `Add`, `Invert`, `Bytes`) but it is a group element in the target group `GT`, not a field element mod the group order. Do not feed it back into `ScalarBaseMult`.
```go title="pairing.go"
package main
import (
"crypto/rand"
"fmt"
"github.com/sonr-io/crypto/core/curves"
)
func main() {
pc := curves.BLS12381(curves.BLS12381G1().NewIdentityPoint())
s := pc.NewScalar().Random(rand.Reader)
g1 := pc.ScalarG1BaseMult(s) // s·G1
g2 := pc.NewG2GeneratorPoint()
gt := g1.Pairing(g2) // e(s·G1, G2)
fmt.Println(pc.Name, len(gt.Bytes()))
fmt.Println(g1.OtherGroup().CurveName()) // BLS12381G2
}
```
## The `Point` interface
Twenty methods, no error returns except on the two deserializers and `Set`.
| Method | Signature | Purpose |
| --- | --- | --- |
| `Random` | `Random(reader io.Reader) Point` | Uniform random group element from the reader. |
| `Hash` | `Hash(bytes []byte) Point` | Hash-to-curve. Deterministic; the domain separation tag is fixed inside each implementation. |
| `Identity` | `Identity() Point` | Point at infinity. |
| `Generator` | `Generator() Point` | Group generator. |
| `IsIdentity` | `IsIdentity() bool` | Identity test. |
| `IsNegative` | `IsNegative() bool` | Sign-of-`y` test, curve-specific convention. |
| `IsOnCurve` | `IsOnCurve() bool` | Curve-equation check. |
| `Double` | `Double() Point` | `2·self`. |
| `Scalar` | `Scalar() Scalar` | A zero scalar of the matching field — a convenience constructor, **not** a discrete log. |
| `Neg` | `Neg() Point` | `-self`. |
| `Add` / `Sub` | `Add(rhs Point) Point` | Group law. |
| `Mul` | `Mul(rhs Scalar) Point` | Variable-base scalar multiplication. |
| `Equal` | `Equal(rhs Point) bool` | Group equality (compares in affine, handles differing projective representations). |
| `Set` | `Set(x, y *big.Int) (Point, error)` | Build from affine coordinates; errors if off-curve. |
| `ToAffineCompressed` | `ToAffineCompressed() []byte` | Canonical short encoding. |
| `ToAffineUncompressed` | `ToAffineUncompressed() []byte` | Canonical long encoding. |
| `FromAffineCompressed` | `FromAffineCompressed(bytes []byte) (Point, error)` | Inverse of the above. |
| `FromAffineUncompressed` | `FromAffineUncompressed(bytes []byte) (Point, error)` | Inverse of the above. |
| `CurveName` | `CurveName() string` | The `Name` string of the owning curve. |
| `SumOfProducts` | `SumOfProducts(points []Point, scalars []Scalar) Point` | Multi-scalar multiplication. |
### Serialization
Concrete point types also implement `MarshalBinary`/`UnmarshalBinary`, `MarshalText`/`UnmarshalText`, and `MarshalJSON`/`UnmarshalJSON` — that is how the higher layers (BBS+ proofs, accumulator witnesses, DKG round messages) persist points. The interface itself does not declare them, so if you need marshalling through the interface you type-assert to `encoding.BinaryMarshaler`.
:::tip[Compressed lengths worth memorizing]
K256 and P-256: 33 bytes compressed, 65 uncompressed. Ed25519 and Pallas: 32 / 64. BLS12-381 and BLS12-377 G1: 48 / 96. BLS12-381 and BLS12-377 G2: 96 / 192. Scalars are 32 bytes on every curve in the catalog. Always deserialize via `curve.Point.FromAffineCompressed` — the prototype knows the expected length and will reject a short or wrong-curve buffer.
:::
### `SumOfProducts` — multi-scalar multiplication
This is the MSM entry point, and the reason Bulletproofs and the accumulator are tractable. Call it on the curve's point prototype; the receiver's own value is ignored.
```go
// Computes sum(scalars[i] · points[i]) using a 4-bit windowed bucket
// (Pippenger-style) multi-exponentiation, not n independent scalar mults.
result := curve.Point.SumOfProducts(points, scalars)
if result == nil {
return errors.New("length mismatch or foreign point/scalar type")
}
```
:::warning[`SumOfProducts` signals failure with a nil return]
The interface method has no error channel. It returns `nil` if the two slices differ in length, or if any element is not the concrete `Point`/`Scalar` type belonging to this curve. Since `nil` is a valid-looking `Point` interface value until you call a method on it, an unchecked result turns a length bug into a nil-pointer panic several frames away. Check it.
:::
## The `Scalar` interface
The doc comment describes it as "an element of the scalar field `F_q` of the elliptic curve construction" — that is, arithmetic is mod the **group order**, not the field characteristic.
| Group | Methods |
| --- | --- |
| Construction | `Random(io.Reader)`, `Hash([]byte)`, `Zero()`, `One()`, `New(value int)`, `Clone()` |
| Predicates | `IsZero()`, `IsOne()`, `IsOdd()`, `IsEven()`, `Cmp(rhs) int` |
| Arithmetic | `Add`, `Sub`, `Mul`, `Div`, `Neg`, `Double`, `Square`, `Cube`, `MulAdd(y, z)` |
| Fallible arithmetic | `Invert() (Scalar, error)`, `Sqrt() (Scalar, error)` |
| Conversion | `SetBigInt(*big.Int) (Scalar, error)`, `BigInt() *big.Int`, `Bytes() []byte`, `SetBytes([]byte) (Scalar, error)`, `SetBytesWide([]byte) (Scalar, error)` |
| Crossing over | `Point() Point` — the associated point type's prototype |
Three behaviours that catch people:
- **`Cmp` returns `-2`** if the two scalars belong to different fields. It is the library's only cross-curve mismatch signal. `-1`/`0`/`1` are the usual ordering.
- **`New(value int)` takes a signed int** and reduces it, so `New(-1)` is `q - 1`. Since `q` is odd for these curves, `New(-1).IsEven()` is `true` — the parity predicates describe the *reduced representative*, not the integer you passed.
- **`SetBytes` demands the exact width**, while `SetBytesWide` wants double the width and reduces. Use `SetBytesWide` when converting hash output into a scalar without modulo bias; use `Hash` if you just want "bytes to scalar" done correctly.
```go
// Uniform scalar from arbitrary input, no bias, no length constraints:
s := curve.Scalar.Hash([]byte("some transcript bytes"))
// Exact-width canonical decoding, e.g. reading a stored private key:
s, err := curve.Scalar.SetBytes(keyBytes) // len(keyBytes) must be exactly 32 for K256
```
## The `crypto/elliptic` bridge
Some code (Go's `crypto/ecdsa`, X.509 marshalling, the legacy `EcPoint` API) needs an `elliptic.Curve`. Several shims exist, and they are not interchangeable:
| Function | Returns | Backing implementation |
| --- | --- | --- |
| `curves.K256Curve()` | `*Koblitz256` | native k256 field arithmetic |
| `curves.NistP256Curve()` | `*NistP256` | native p256 field arithmetic |
| `curves.SP256()` | `elliptic.Curve` | `github.com/dustinxie/ecc` secp256k1 |
| `secp256k1.S256()` | `*secp256k1.BitCurve` | the vendored Koblitz `a=0` implementation |
| `curves.Pallas()` | `*PallasCurve` | Pallas as an `elliptic.Curve` |
All of them satisfy `elliptic.Curve`. `Curve.ToEllipticCurve()` is the generic entry point:
```go
ec, err := curves.K256().ToEllipticCurve() // -> *Koblitz256, nil
ec, err = curves.ED25519().ToEllipticCurve() // -> nil, "can't convert ed25519"
```
:::danger[`ToEllipticCurve` only supports two curves]
Only `K256Name` and `P256Name` return a curve. `ED25519`, `PALLAS`, and all four BLS variants return `nil` plus the error `can't convert <name>` — which is correct, since none of them are short-Weierstrass curves over a prime field in the `crypto/elliptic` sense. Handle the error; do not assume it is a curve-agnostic conversion.
:::
:::warning[`NistP256.ScalarMult` is not the native implementation]
`*NistP256` defines `ScalarMul` — missing the trailing `t`. So the `elliptic.Curve` interface method `ScalarMult` resolves to the promoted `*elliptic.CurveParams.ScalarMult`, the generic deprecated `math/big` implementation, rather than the native p256 code the type was written to use. `ScalarBaseMult`, `Add`, `Double`, and `IsOnCurve` *are* wired to the native path. If you care about the variable-base path on P-256, use `curves.P256()` and the `Point.Mul` interface instead of the `elliptic.Curve` shim.
:::
`secp256k1.BitCurve` additionally offers `Marshal(x, y) []byte` / `Unmarshal(data) (x, y)` and exposes its parameters as public fields (`P`, `N`, `B`, `Gx`, `Gy`, `BitSize`).
## `core/curves/native` — the layer below
`native` is the constant-time-oriented field and point arithmetic that the modern `Point`/`Scalar` implementations sit on. It is a *building block*, and almost nothing outside `core/curves` should import it.
Fields are represented as four 64-bit limbs in the Montgomery domain:
```go
const (
FieldBytes = 32 // canonical byte width
FieldLimbs = 4 // uint64 limbs
WideFieldBytes = 64 // width for bias-free reduction
MaxDstLen = 255
)
type Field struct {
Value [FieldLimbs]uint64
Params *FieldParams // R, R2, R3, Modulus, BiModulus
Arithmetic FieldArithmetic // per-curve limb routines
}
```
`Field` provides `Add`, `Sub`, `Mul`, `Square`, `Double`, `Neg`, `Exp`, `Invert`, `Sqrt`, `CMove`, `Equal`, `Cmp`, plus `SetBytes`/`SetBytesWide`/`SetBigInt`/`SetLimbs`/`SetRaw` and their `Bytes`/`BigInt`/`Raw` inverses. `EllipticPoint` provides Weierstrass point arithmetic in Jacobian coordinates (`Add`, `Double`, `Generator`, `Hash`, `Equal`, `BigInt`, `GetX`, `GetY`).
Which fields and groups are actually implemented:
| Package | Contents |
| --- | --- |
| `native/bls12381` | `G1`, `G2`, `Gt`, the pairing `Engine`, `Fq`, `Bls12381FqNew()` |
| `native/k256` | `K256PointNew()`; subpackages `k256/fp` (base field) and `k256/fq` (scalar field) |
| `native/p256` | `P256PointNew()`; subpackages `p256/fp` and `p256/fq` |
| `native/pasta` | Pallas/Vesta point code; subpackages `pasta/fp` and `pasta/fq` |
### Hash-to-curve hashers
`EllipticPointHasher` bundles a hash function with its expansion mode. It is what `Point.Hash` uses internally, and the only reason to construct one yourself is if you are calling `native.ExpandMsgXmd` / `native.ExpandMsgXof` or `EllipticPoint.Hash` directly.
| Constructor | `Name()` | `Type()` |
| --- | --- | --- |
| `EllipticPointHasherSha256()` | `SHA-256` | XMD |
| `EllipticPointHasherSha512()` | `SHA-512` | XMD |
| `EllipticPointHasherSha3256()` | `SHA3-256` | XMD |
| `EllipticPointHasherSha3384()` | `SHA3-384` | XMD |
| `EllipticPointHasherSha3512()` | `SHA3-512` | XMD |
| `EllipticPointHasherBlake2b()` | `BLAKE2b` | XMD |
| `EllipticPointHasherShake128()` | `SHAKE-128` | XOF |
| `EllipticPointHasherShake256()` | `SHAKE-256` | XOF |
`ExpandMsgXmd` and `ExpandMsgXof` implement §5.4.1 and §5.4.2 of the CFRG hash-to-curve draft (the source links to `draft-irtf-cfrg-hash-to-curve-13`). Domain separation tags longer than `MaxDstLen` are hashed down using the `OversizeDstSalt` prefix `H2C-OVERSIZE-DST-`.
:::warning[`native` is unforgiving]
`ExpandMsgXmd` and `ExpandMsgXof` return `[]byte` with **no error channel** and will nil-dereference on a nil hasher. `Field` methods write into the receiver and return it, so aliasing the output with an input is only safe where the implementation says so. `Pow` and `Pow2k` are documented as "public only for convenience for some internal implementations". Treat the whole package as internal and use `Point`/`Scalar` instead.
:::
## Legacy curve types
For completeness, since they show up in `go doc` next to everything above. These belong to the older API described on the [foundations overview](/foundations) and are used by `sharing/v1`, `dkg/gennaro`, and `ted25519` keygen.
- `EcPoint{Curve elliptic.Curve; X, Y *big.Int}` with `NewScalarBaseMult`, `PointFromBytesUncompressed`, `Add`, `Neg`, `ScalarMult`, `Bytes`, `Equals`, `IsOnCurve`, `IsIdentity`, `IsBasePoint`, `IsValid`, and binary/JSON marshalling (plus `EcPointJSON` as the wire shape).
- `Field`/`Element` — generic `big.Int` modular arithmetic over an explicit modulus, with `ElementJSON` for serialization.
- `EcScalar` — a strategy interface (`Add`, `Sub`, `Neg`, `Mul`, `Div`, `Hash`, `Random`, `IsValid`, `Bytes`) implemented by `NewK256Scalar()`, `NewP256Scalar()`, `NewEd25519Scalar()`, `NewBls12381Scalar()`, and `NewPallasScalar()`.
- `EcdsaSignature`, `EcdsaVerify`, and `VerifyEcdsa(pk *EcPoint, hash []byte, sig *EcdsaSignature) bool` — the verification hook used by threshold ECDSA. See [ECDSA](/signatures/ecdsa).
- `Ed25519Order() *big.Int` — the Ed25519 group order as a `big.Int`.
+172
View File
@@ -0,0 +1,172 @@
---
title: Foundations
description: The curve abstraction, the arithmetic helpers, and the protocol iterator — the three things almost every other package in this library is built on top of.
sidebar:
order: 1
icon: layers
---
Nearly every package in this repository is generic over one type: `*curves.Curve`. BLS signatures, BBS+, Shamir sharing, Feldman/Pedersen VSS, Schnorr proofs, the accumulator, threshold ECDSA, and the DID key layer all take a curve value and do their work through two interfaces — `curves.Point` and `curves.Scalar`. If you understand those three things, the rest of the library reads as variations on a theme.
This section covers the shared substrate:
<CardGroup cols={3}>
<Card title="Curves" href="/foundations/curves" icon="circle-dot">
Every named curve constructor, the full `Point` / `Scalar` method sets, pairing curves, and the low-level `native` field arithmetic.
</Card>
<Card title="Arithmetic" href="/foundations/arithmetic" icon="sigma">
The `core` package: modular arithmetic over `big.Int`, hash-to-field, FiatShamir, safe primes, and the HMAC commitment scheme.
</Card>
<Card title="Protocol" href="/foundations/protocol" icon="arrow-left-right">
The `Iterator` / `Message` crank pattern that drives every DKLs18-family interactive protocol.
</Card>
</CardGroup>
## The `Curve` value
`curves.Curve` is a plain struct, not an interface. It is a *bundle of prototypes*:
```go
type Curve struct {
Scalar Scalar
Point Point
Name string
}
```
`Scalar` and `Point` are not "the" scalar or "the" point — they are zero-valued exemplars you call constructor-shaped methods on. This is how the library gets generic behaviour without Go generics: `curve.Scalar.Random(rand.Reader)` dispatches to the K256 or Ed25519 or BLS12-381 implementation depending on which curve you were handed.
Curve constructors are memoized behind `sync.Once`, so `curves.K256()` returns the same pointer on every call and is safe to call in a hot loop.
<TypeTable
type={{
"curve.NewScalar()": {
type: "Scalar",
description: "A fresh scalar set to zero. Equivalent to curve.Scalar.Zero().",
},
"curve.NewGeneratorPoint()": {
type: "Point",
description: "The group generator G. Equivalent to curve.Point.Generator().",
},
"curve.NewIdentityPoint()": {
type: "Point",
description: "The point at infinity. Equivalent to curve.Point.Identity().",
},
"curve.ScalarBaseMult(sc)": {
type: "Point",
description: "Fixed-base multiplication sc·G. Use this instead of NewGeneratorPoint().Mul(sc).",
},
"curve.ToEllipticCurve()": {
type: "(elliptic.Curve, error)",
description: "Bridge to crypto/elliptic. Only K256 and P-256 succeed; every other curve returns an error.",
},
}}
/>
## Arithmetic on K256
`Point` and `Scalar` methods are chainable and return new values — they never mutate the receiver, so you can hold onto intermediates freely.
```go title="arith.go"
package main
import (
"crypto/rand"
"fmt"
"github.com/sonr-io/crypto/core/curves"
)
func main() {
curve := curves.K256()
// Two random field elements.
x := curve.Scalar.Random(rand.Reader)
y := curve.Scalar.Random(rand.Reader)
// Scalar field arithmetic: mod q, where q is the group order.
sum := x.Add(y)
xInv, err := x.Invert()
if err != nil {
panic(err) // only fails for zero
}
fmt.Println(x.Mul(xInv).IsOne()) // true
// Group arithmetic. Note the homomorphism:
// (x + y)·G == x·G + y·G
P := curve.ScalarBaseMult(x)
Q := curve.NewGeneratorPoint().Mul(y)
fmt.Println(P.Add(Q).Equal(curve.ScalarBaseMult(sum))) // true
// Identity behaves as expected.
fmt.Println(P.Sub(P).Equal(curve.NewIdentityPoint())) // true
// Serialization round-trip: 33 bytes compressed for K256.
enc := P.ToAffineCompressed()
P2, err := curve.Point.FromAffineCompressed(enc)
if err != nil {
panic(err)
}
fmt.Println(len(enc), P2.Equal(P), P.CurveName()) // 33 true secp256k1
}
```
Two habits worth forming immediately:
- **Deserialize through the curve's prototype**, i.e. `curve.Point.FromAffineCompressed(b)` and `curve.Scalar.SetBytes(b)`. These are the only entry points that know which concrete type to produce.
- **Check the error on `Invert`, `Sqrt`, `SetBytes`, and `SetBigInt`.** The arithmetic methods (`Add`, `Mul`, `Neg`, `Double`) return no error and will happily produce garbage if you fed them a value from a different curve.
:::warning[Cross-curve values do not panic]
`Scalar.Cmp` returns `-2` when the two scalars belong to different fields — that is the only place the library tells you about a curve mismatch. `Add`, `Mul`, and friends have no error channel. Mixing a `ScalarK256` into a P-256 computation produces a silently wrong result, so keep a single `*curves.Curve` threaded through a computation rather than calling constructors ad hoc.
:::
## Two generations of API coexist
This is the single most important orientation fact about the repository. There are **two** unrelated curve APIs in `core/curves`, and which one you get depends entirely on which package you called.
<Tabs>
<Tab title="Modern (Point / Scalar)">
Interface-based, generic over the curve, supports every curve in the catalog including pairing-friendly ones.
```go
curve := curves.K256()
s := curve.Scalar.Random(rand.Reader) // curves.Scalar
P := curve.ScalarBaseMult(s) // curves.Point
```
Used by: `signatures/bbs`, `signatures/bls/bls_sig`, `signatures/schnorr/mina`, `signatures/schnorr/nem`, `sharing`, `dkg/frost`, `zkp/schnorr`, `accumulator`, `bulletproof`, `tecdsa/dklsv1`, `ted25519/frost`, `ot/*`.
</Tab>
<Tab title="Legacy (EcPoint / Field / Element)">
Concrete structs over `crypto/elliptic` and `math/big`. No pairing support, no hash-to-curve, and scalars are raw `*big.Int` wrapped by an `EcScalar` strategy object.
```go
// EcPoint wraps an elliptic.Curve plus affine X, Y as *big.Int.
P, err := curves.NewScalarBaseMult(btcec.S256(), k)
// Field/Element is generic modular arithmetic over an explicit modulus.
f := curves.NewField(order)
e := f.NewElement(big.NewInt(3))
e = e.Mul(f.NewElement(big.NewInt(4))) // 12 mod order
```
Used by: `sharing/v1`, `dkg/gennaro`, `dkg/gennaro2p`, `ted25519/ted25519` keygen, `paillier` (`psf.go`), and the ECDSA public-key conversion helpers in `keys` and `mpc`.
</Tab>
</Tabs>
The two worlds share nothing. There is no conversion helper between `curves.Point` and `*curves.EcPoint`, and no helper between `curves.Scalar` and `*curves.Element`. If you need to move a value across, you go through bytes or `big.Int` yourself and take responsibility for the encoding.
:::danger[The legacy `Field` is not constant time]
The package documentation for `core/curves` says so outright: *"Field implementation IS NOT constant time as it leverages math/big for big number operations."* This applies to `Field`, `Element`, `EcPoint`, and everything built on them — which includes `sharing/v1` and `dkg/gennaro`. Do not use those packages on secret-dependent inputs where timing is observable by an attacker. Prefer the modern `Point`/`Scalar` path for new code.
:::
## Where the pieces are used
| Layer | Packages | What it needs from foundations |
| --- | --- | --- |
| [Signatures](/signatures) | `signatures/bls/bls_sig`, `signatures/bbs`, `signatures/schnorr/*` | `*curves.Curve`, or `*curves.PairingCurve` for BBS+ |
| [Threshold](/threshold) | `sharing`, `dkg/*`, `tecdsa/dklsv1`, `ted25519/*`, `ot/*` | `*curves.Curve`, plus `core/protocol` for `tecdsa/dklsv1` |
| [Zero-knowledge](/zero-knowledge) | `zkp/schnorr`, `accumulator`, `bulletproof` | `*curves.Curve`; the accumulator needs a `*PairingCurve` |
| [Identity](/identity) | `keys`, `mpc`, `ucan`, `ecies`, `wasm` | `keys` and `mpc` use `curves`; `mpc` also uses `core/protocol`. `ucan`, `wasm`, and most of `ecies` do not touch the curve abstraction at all. |
| [Symmetric](/symmetric) | `aead`, `daed`, `argon2`, `subtle`, `secure`, `salt`, `password` | nothing — these are pure `[]byte` APIs |
Two more packages sit outside the curve abstraction entirely: `ecdsa` and `vrf` do not import `core/curves` at all, and `paillier` — like `core` itself — works directly over `math/big` integers with an explicit modulus. See [arithmetic](/foundations/arithmetic) for that world.
+8
View File
@@ -0,0 +1,8 @@
import { defineMeta } from "blume";
export default defineMeta({
title: "Foundations",
icon: "layers",
order: 2,
pages: ["index", "curves", "arithmetic", "protocol"],
});
+312
View File
@@ -0,0 +1,312 @@
---
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"`
}
```
<TypeTable
type={{
Payloads: {
type: "map[string][]byte",
required: true,
description: "The round's actual wire data, keyed by a payload label. The dklsv1 serializers use the single key \"direct\".",
},
Metadata: {
type: "map[string]string",
description: "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.",
},
Protocol: {
type: "string",
required: true,
description: "Which protocol this message belongs to — one of the Dkls18* constants.",
},
Version: {
type: "uint",
required: true,
description: "Serialization version of the payloads. Version0 = 100, Version1 = 200.",
},
}}
/>
### 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`.
<Steps>
<Step title="Construct both participants">
Both sides need the same `*curves.Curve` and the same version. For DKG that is all the input there is.
</Step>
<Step title="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.
</Step>
<Step title="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.
</Step>
<Step title="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.
</Step>
<Step title="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.
</Step>
</Steps>
```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
<CardGroup cols={2}>
<Card title="Threshold ECDSA" href="/threshold/threshold-ecdsa" icon="users">
`tecdsa/dklsv1` — `AliceDkg`/`BobDkg`, `AliceSign`/`BobSign`, `AliceRefresh`/`BobRefresh` all implement `Iterator`, plus the `Encode*`/`Decode*` result helpers.
</Card>
<Card title="MPC enclave" href="/identity/mpc-enclave" icon="fingerprint">
`mpc` wraps the DKLs18 iterators with `RunProtocol`, `CheckIteratedErrors`, and keyshare encryption.
</Card>
</CardGroup>
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.