Skip to content
Sonr Crypto
Esc
navigateopen⌘Jpreview
On this page

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])
PropType
Blindingcurves.Scalar

The blinding factor's intercept. Secret — leaking it collapses Pedersen to Feldman.

Typecurves.Scalar
SecretShares[]*ShamirShare

Shares of the secret. Length == limit.

Type[]*ShamirShare
BlindingShares[]*ShamirShare

Shares of the blinding polynomial, index-aligned with SecretShares.

Type[]*ShamirShare
FeldmanVerifier*FeldmanVerifier

Unblinded commitments a_j · G. Reveals the public key.

Type*FeldmanVerifier
PedersenVerifier*PedersenVerifier

Blinded commitments a_j · G + b_j · H, plus the generator H.

Type*PedersenVerifier

ShamirShare

All three schemes emit the same share type.

PropType
Iduint32

The x-coordinate. 1-indexed; 0 is rejected. Must be ≤ limit.

Typeuint32
Value[]byte

The y-coordinate, as the curve's canonical scalar encoding.

Type[]byte

Bytes() 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 code

sharing/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 plain ints and a field, not a curve. It enforces only limit >= threshold and threshold >= 2no 255-share ceiling.
  • v1.NewFeldman(threshold, limit uint32, curve elliptic.Curve) and v1.NewPedersen(threshold, limit uint32, generator *curves.EcPoint) take standard-library curves. Tests drive them with btcec.S256() and elliptic.P256().
  • Split takes []byte and reads randomness from an internal source — there is no io.Reader parameter, so you cannot inject a deterministic RNG.
  • Verify returns (bool, error) rather than a bare error, and the verifier list is a plain slice you pass in, not a struct.
  • ShamirShare here has fields Identifier uint32 and Value *curves.Element, and gains an Add method that panics if the two identifiers differ.
  • ComputeL is the LagrangeCoeffs equivalent, 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.

Caveats

Next

Last updated on September 2, 2026

Was this page helpful?