mirror of
https://github.com/sonr-io/crypto.git
synced 2026-09-17 01:26:23 +00:00
feat: init docs
This commit is contained in:
@@ -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, Fiat–Shamir, 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 Fiat–Shamir 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)` Miller–Rabin 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>
|
||||
@@ -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`.
|
||||
@@ -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, Fiat–Shamir, 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.
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({
|
||||
title: "Foundations",
|
||||
icon: "layers",
|
||||
order: 2,
|
||||
pages: ["index", "curves", "arithmetic", "protocol"],
|
||||
});
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user