Secret Sharing
Shamir, Feldman, and Pedersen verifiable secret sharing over any supported curve — plus the legacy sharing/v1 layer and its known defects.
The sharing package splits a curve scalar into n shares such that any t of them reconstruct
it, and fewer than t reveal nothing. All three schemes share one share type and one
reconstruction routine; they differ only in what a shareholder can verify about the share it was
handed.
This is dealer-based sharing: one process holds the secret while Split runs. If you need a key
that never exists in one place, you want DKG instead.
Picking a scheme
Shamir
No verification. Fastest, smallest. Use only when every shareholder is trusted, or when a higher layer verifies for you.
Feldman
Adds polynomial commitments a_j · G. A holder can check its own share against them. The
commitments leak a_0 · G — i.e. the public key of the secret.
Pedersen
Feldman plus a blinding polynomial under a second generator H. The commitments are
information-theoretically hiding, so nothing about the secret leaks before reconstruction.
Shamir
import (
crand "crypto/rand"
"github.com/sonr-io/crypto/core/curves"
"github.com/sonr-io/crypto/sharing"
)
func shamirRoundTrip() error {
curve := curves.ED25519()
scheme, err := sharing.NewShamir(3, 5, curve) // 3-of-5
if err != nil {
return err
}
secret := curve.Scalar.Hash([]byte("test"))
shares, err := scheme.Split(secret, crand.Reader)
if err != nil {
return err
}
// Any 3 of the 5 shares reconstruct the secret.
recovered, err := scheme.Combine(shares[0], shares[2], shares[4])
if err != nil {
return err
}
_ = recovered.Cmp(secret) // == 0
return nil
}
Combine reconstructs the scalar. CombinePoints does the same interpolation in the group,
returning secret · G — useful when you want to check that a share set corresponds to a known
public key without materialising the key. Shamir.LagrangeCoeffs(identities []uint32) returns the
interpolation coefficients on their own, keyed by identifier, so a caller can compute
Σ λ_i · share_i itself; this is exactly what ted25519/frost
needs.
Feldman
Feldman.Split returns a *FeldmanVerifier alongside the shares. The verifier holds Threshold
points — a_j · G for each polynomial coefficient — and Verify recomputes
Σ a_j · id^j and compares it to share · G.
scheme, err := sharing.NewFeldman(3, 5, curves.ED25519())
if err != nil {
return err
}
verifier, shares, err := scheme.Split(secret, crand.Reader)
if err != nil {
return err
}
for _, s := range shares {
if err := verifier.Verify(s); err != nil { // nil == valid
return err
}
}
recovered, err := scheme.Combine(shares[0], shares[1], shares[2])
Verify returns fmt.Errorf("not equal") on a mismatch — there is no typed sentinel error, so
compare against nil rather than matching the message.
Pedersen
NewPedersen takes a generator point rather than a curve; the curve is derived from
generator.CurveName(). Split returns a single struct carrying both verifiers and both share
sets.
curve := curves.ED25519()
// H must have unknown discrete log with respect to G.
h := curve.Point.Generator().Hash([]byte("sonr/pedersen/H/v1"))
scheme, err := sharing.NewPedersen(3, 5, h)
if err != nil {
return err
}
result, err := scheme.Split(secret, crand.Reader)
if err != nil {
return err
}
for i := range result.SecretShares {
// Pedersen verification needs BOTH the secret share and its blinding share.
err = result.PedersenVerifier.Verify(result.SecretShares[i], result.BlindingShares[i])
if err != nil {
return err
}
// The Feldman verifier is also returned and checks the secret share alone.
if err = result.FeldmanVerifier.Verify(result.SecretShares[i]); err != nil {
return err
}
}
recovered, err := scheme.Combine(result.SecretShares[0], result.SecretShares[1], result.SecretShares[2])
Blindingcurves.Scalar
The blinding factor's intercept. Secret — leaking it collapses Pedersen to Feldman.
curves.ScalarSecretShares[]*ShamirShare
Shares of the secret. Length == limit.
[]*ShamirShareBlindingShares[]*ShamirShare
Shares of the blinding polynomial, index-aligned with SecretShares.
[]*ShamirShareFeldmanVerifier*FeldmanVerifier
Unblinded commitments a_j · G. Reveals the public key.
*FeldmanVerifierPedersenVerifier*PedersenVerifier
Blinded commitments a_j · G + b_j · H, plus the generator H.
*PedersenVerifierShamirShare
All three schemes emit the same share type.
Iduint32
The x-coordinate. 1-indexed; 0 is rejected. Must be ≤ limit.
uint32Value[]byte
The y-coordinate, as the curve's canonical scalar encoding.
[]byteBytes() returns the id as 4 big-endian bytes followed by Value — a stable wire form.
Validate(curve) rejects a zero id, a Value that does not decode as a scalar on curve, and a
zero scalar. The struct carries json tags (identifier, value) and round-trips through
encoding/json.
Constructor constraints
Identical across NewShamir, NewFeldman, and NewPedersen, checked in this order:
| Check | Error |
|---|---|
limit >= threshold |
limit cannot be less than threshold |
threshold >= 2 |
threshold cannot be less than 2 |
limit <= 255 |
cannot exceed 255 shares |
| curve resolvable / non-nil | invalid curve |
Shamir.Split and Feldman.Split additionally reject a zero secret with invalid secret.
The Polynomial degree gotcha
sharing.Polynomial is exported, and its Init signature reads as if it takes a degree:
func (p *Polynomial) Init(intercept curves.Scalar, degree uint32, reader io.Reader) *Polynomial
It does not. The implementation allocates degree coefficients — Coefficients[0] = intercept plus
degree - 1 random ones — so the resulting polynomial has algebraic degree degree - 1. The
parameter is really a coefficient count.
Inside the package this is consistent: every scheme calls Init(secret, threshold, reader), which
yields threshold coefficients and hence degree threshold - 1 — precisely what a t-of-n
scheme requires. But if you call Init yourself expecting the named semantics you will get a
polynomial one degree lower than you asked for, and Init(x, 0, r) panics on
Coefficients[0] before it can return an error.
Legacy: sharing/v1
Legacy — do not use for new codesharing/v1 is the pre-curves.Curve generation of the same three schemes. It operates on
[]byte secrets over curves.Field/curves.Element and uses curves.EcPoint (aliased locally as
ShareVerifier) rather than curves.Point. It survives because dkg/gennaro and
ted25519/ted25519 are built on it and have never been ported.
Differences that matter if you must read it:
v1.NewShamir(threshold, limit int, field *curves.Field)takes plainints and a field, not a curve. It enforces onlylimit >= thresholdandthreshold >= 2— no 255-share ceiling.v1.NewFeldman(threshold, limit uint32, curve elliptic.Curve)andv1.NewPedersen(threshold, limit uint32, generator *curves.EcPoint)take standard-library curves. Tests drive them withbtcec.S256()andelliptic.P256().Splittakes[]byteand reads randomness from an internal source — there is noio.Readerparameter, so you cannot inject a deterministic RNG.Verifyreturns(bool, error)rather than a bareerror, and the verifier list is a plain slice you pass in, not a struct.ShamirSharehere has fieldsIdentifier uint32andValue *curves.Element, and gains anAddmethod that panics if the two identifiers differ.ComputeLis theLagrangeCoeffsequivalent, returning an ordered[]*curves.Element.
Curve helpers provided by the package: Ed25519(), Bls12381G1(), Bls12381G2(), and
K256GeneratorFromHashedBytes(bytes []byte) (x, y *big.Int, err error) — which derives a generator
with unknown discrete log from a byte string, exactly the Pedersen requirement above. There is no
k256 or p256 curve constructor in v1; use btcec.S256() and elliptic.P256() directly, as the
tests do.