Files
zeekay 2c63fd34fc lens: classical curve threshold kernel — sister to Pulsar
Lens is the curve-based threshold signing kernel for Lux. It is the
classical analogue of Pulsar (the lattice kernel at github.com/luxfi/pulsar).
Both kernels share the same lifecycle shape — Bootstrap → Reshare* →
Reanchor — and the same activation circuit-breaker contract. They differ
only in the math field: Pulsar over R_q (Module-LWE), Lens over a
prime-order curve group (discrete log).

Curves shipped: Ed25519, secp256k1, Ristretto255. Each has a
deterministically-derived secondary Pedersen generator H from a
Lens-specific nothing-up-my-sleeve tag.

Sign math: FROST 2-round Schnorr threshold signing per RFC 9591
(Komlo-Goldberg 2020). Verifier checks z·G ?= R + c·X under the
unchanged group public key.

Reshare math: HJKY97 zero-poly Refresh + Desmedt-Jajodia ReshareToNewSet
over the curve scalar field. Master secret s is preserved; only the
share distribution rotates per epoch.

Activation cert message format: QUASAR-LENS-ACTIVATE-v1, byte-equivalent
shape to QUASAR-PULSAR-ACTIVATE-v1.

Hash suite: HashSuite interface with Lens-SHA3 (production, cSHAKE256 /
KMAC256 / TupleHash256) and Lens-BLAKE3 (legacy) profiles.

DKG: distributed FROST DKG with Feldman commitments. No trusted dealer
required at genesis if the chain prefers proper distributed DKG.

KAT oracles: cmd/reshare_oracle and cmd/dkg_oracle emit deterministic
JSON vectors for cross-language byte-equality validation against a
future luxcpp/crypto/lens/ port.

Tests: full unit + integration coverage on every shipped curve. 60
unique tests, 0 failures.
2026-03-03 12:00:00 -08:00

80 lines
2.3 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package primitives
import (
"errors"
"fmt"
"io"
)
// Shamir produces a (t, n)-Shamir secret sharing of `secret` over the
// curve's scalar field. Returns:
//
// - The polynomial f with f(0) = secret and (t-1) random higher-
// order coefficients drawn from `rand`.
// - A map partyID → f(partyID) for partyID in [1, n].
//
// The threshold t is the minimum number of shares needed to recover
// the secret (so the polynomial degree is t-1).
//
// Note that party IDs are 1-indexed; ID 0 is reserved (evaluating at 0
// reveals the secret).
func Shamir(
curve Curve,
secret Scalar,
threshold, n int,
rand io.Reader,
) (*Polynomial, map[int]Scalar, error) {
if threshold < 1 {
return nil, nil, fmt.Errorf("lens/primitives: Shamir threshold %d < 1", threshold)
}
if n < threshold {
return nil, nil, fmt.Errorf("lens/primitives: Shamir n=%d < t=%d", n, threshold)
}
poly, err := NewPolynomial(curve, threshold-1, secret, rand)
if err != nil {
return nil, nil, err
}
shares := make(map[int]Scalar, n)
for j := 1; j <= n; j++ {
x := curve.NewScalar().SetUint64(uint64(j))
shares[j] = poly.Evaluate(x)
}
return poly, shares, nil
}
// LagrangeRecover reconstructs the secret f(0) from the supplied
// (partyID → share) map, using the smallest-ID t-element subset.
// Intended for tests and KAT verification — calling this in production
// gives the caller the master secret.
func LagrangeRecover(curve Curve, shares map[int]Scalar, threshold int) (Scalar, error) {
if len(shares) < threshold {
return nil, fmt.Errorf("lens/primitives: LagrangeRecover need %d shares, got %d",
threshold, len(shares))
}
ids := make([]int, 0, len(shares))
for id := range shares {
ids = append(ids, id)
}
ids = SortedInts(ids)
if len(ids) > threshold {
ids = ids[:threshold]
}
lambda, err := Lagrange(curve, ids)
if err != nil {
return nil, err
}
out := curve.NewScalar()
for _, id := range ids {
term := curve.NewScalar().Set(lambda[id]).Mul(shares[id])
out.Add(term)
}
return out, nil
}
// ErrShamirInsufficient is returned when fewer than `threshold` shares
// are supplied to a reconstruction routine.
var ErrShamirInsufficient = errors.New("lens/primitives: insufficient shares for Shamir recovery")