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

171 lines
5.1 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"
"sort"
)
// Polynomial represents f(X) = a_0 + a_1·X + ... + a_t·X^t with
// coefficients in the curve's scalar field.
type Polynomial struct {
curve Curve
coefficients []Scalar
}
// NewPolynomial constructs a polynomial of the given degree with the
// supplied constant term and (degree) random higher-order coefficients
// drawn from rand.
//
// If constant == nil, the constant term is set to zero (used by
// HJKY97 zero-poly Refresh).
func NewPolynomial(curve Curve, degree int, constant Scalar, rand io.Reader) (*Polynomial, error) {
if degree < 0 {
return nil, fmt.Errorf("lens/primitives: negative polynomial degree %d", degree)
}
p := &Polynomial{
curve: curve,
coefficients: make([]Scalar, degree+1),
}
if constant == nil {
p.coefficients[0] = curve.NewScalar()
} else {
p.coefficients[0] = curve.NewScalar().Set(constant)
}
for i := 1; i <= degree; i++ {
s, err := curve.SampleScalar(rand)
if err != nil {
return nil, fmt.Errorf("lens/primitives: polynomial sample: %w", err)
}
p.coefficients[i] = s
}
return p, nil
}
// PolynomialFromCoefficients wraps an explicit coefficient slice as a
// polynomial. The slice is shared, not copied — the caller is
// responsible for not mutating it externally.
func PolynomialFromCoefficients(curve Curve, coeffs []Scalar) *Polynomial {
return &Polynomial{curve: curve, coefficients: coeffs}
}
// Degree returns the polynomial's degree.
func (p *Polynomial) Degree() int { return len(p.coefficients) - 1 }
// Coefficients returns the underlying coefficient slice (not a copy).
// Used by the commitment layer to compute Pedersen commits per term.
func (p *Polynomial) Coefficients() []Scalar { return p.coefficients }
// Constant returns a copy of the constant term.
func (p *Polynomial) Constant() Scalar {
return p.curve.NewScalar().Set(p.coefficients[0])
}
// Evaluate computes f(x) via Horner's method. Panics if x is zero, to
// avoid leaking the secret constant term.
func (p *Polynomial) Evaluate(x Scalar) Scalar {
if x.IsZero() {
panic("lens/primitives: polynomial.Evaluate at 0 leaks secret")
}
result := p.curve.NewScalar()
for i := len(p.coefficients) - 1; i >= 0; i-- {
result.Mul(x).Add(p.coefficients[i])
}
return result
}
// EvaluateUnchecked is like Evaluate but allows x = 0. Used in test/KAT
// paths to recover f(0) (the secret) for verification.
func (p *Polynomial) EvaluateUnchecked(x Scalar) Scalar {
result := p.curve.NewScalar()
for i := len(p.coefficients) - 1; i >= 0; i-- {
result.Mul(x).Add(p.coefficients[i])
}
return result
}
// Lagrange returns the Lagrange basis coefficients λ_i^T evaluated at
// X = 0 for the set of one-indexed party IDs T:
//
// λ_i = Π_{j ∈ T, j ≠ i} (-x_j) / (x_i - x_j)
//
// where x_k is the scalar representation of party ID k. Used to
// reconstruct s = Σ_i λ_i · s_i over the curve.
//
// IDs MUST be distinct, non-zero, and < group order.
func Lagrange(curve Curve, ids []int) (map[int]Scalar, error) {
if len(ids) == 0 {
return nil, errors.New("lens/primitives: Lagrange on empty set")
}
for _, id := range ids {
if id == 0 {
return nil, errors.New("lens/primitives: Lagrange at zero ID leaks secret")
}
}
out := make(map[int]Scalar, len(ids))
for _, i := range ids {
xi := curve.NewScalar().SetUint64(uint64(i))
num := curve.NewScalar().SetUint64(1)
den := curve.NewScalar().SetUint64(1)
for _, j := range ids {
if i == j {
continue
}
xj := curve.NewScalar().SetUint64(uint64(j))
// num *= -x_j
negXj := curve.NewScalar().Set(xj).Negate()
num.Mul(negXj)
// den *= (x_i - x_j)
diff := curve.NewScalar().Set(xi).Sub(xj)
den.Mul(diff)
}
if den.IsZero() {
return nil, errors.New("lens/primitives: Lagrange denominator is zero (duplicate IDs?)")
}
coeff := curve.NewScalar().Set(num).Mul(curve.NewScalar().Set(den).Invert())
out[i] = coeff
}
return out, nil
}
// LagrangeAt returns Lagrange coefficients evaluated at an arbitrary
// scalar `x` (rather than at 0). Used during VSR to evaluate the
// composite polynomial G(x_j) = Σ_i λ_i · g_i(x_j).
func LagrangeAt(curve Curve, ids []int, x Scalar) (map[int]Scalar, error) {
if len(ids) == 0 {
return nil, errors.New("lens/primitives: LagrangeAt on empty set")
}
out := make(map[int]Scalar, len(ids))
for _, i := range ids {
xi := curve.NewScalar().SetUint64(uint64(i))
num := curve.NewScalar().SetUint64(1)
den := curve.NewScalar().SetUint64(1)
for _, j := range ids {
if i == j {
continue
}
xj := curve.NewScalar().SetUint64(uint64(j))
diffNum := curve.NewScalar().Set(x).Sub(xj)
num.Mul(diffNum)
diffDen := curve.NewScalar().Set(xi).Sub(xj)
den.Mul(diffDen)
}
if den.IsZero() {
return nil, errors.New("lens/primitives: LagrangeAt denominator zero")
}
out[i] = curve.NewScalar().Set(num).Mul(curve.NewScalar().Set(den).Invert())
}
return out, nil
}
// SortedInts returns a copy of `ids` sorted ascending.
func SortedInts(ids []int) []int {
out := append([]int(nil), ids...)
sort.Ints(out)
return out
}