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

226 lines
6.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package primitives
import (
"crypto/sha512"
"fmt"
"io"
"github.com/gtank/ristretto255"
)
// Ristretto255 is the prime-order group from RFC 9496, built on
// Curve25519 via the Ristretto encoding. Group order is identical to
// Ed25519's ℓ.
type Ristretto255 struct{}
// NewRistretto255 returns the singleton Ristretto255 curve.
func NewRistretto255() Curve { return Ristretto255{} }
const (
ristretto255ScalarSize = 32
ristretto255PointSize = 32
)
var (
ristretto255H *ristretto255.Element
ristretto255HOnce bool
)
// Ristretto255 implements Curve.
func (Ristretto255) Name() string { return "ristretto255" }
func (Ristretto255) ScalarBytes() int { return ristretto255ScalarSize }
func (Ristretto255) PointBytes() int { return ristretto255PointSize }
func (Ristretto255) NewScalar() Scalar {
return &ristrettoScalar{s: ristretto255.NewScalar()}
}
func (Ristretto255) NewPoint() Point {
return &ristrettoPoint{p: ristretto255.NewIdentityElement()}
}
func (Ristretto255) BasePoint() Point {
return &ristrettoPoint{p: ristretto255.NewGeneratorElement()}
}
// SecondaryGenerator derives H from a fixed nothing-up-my-sleeve tag
// expanded to 64 uniform bytes via SHA-512, then mapped to the group
// via the FromUniformBytes Elligator construction (the standard
// hash-to-curve for Ristretto255).
func (Ristretto255) SecondaryGenerator() Point {
if !ristretto255HOnce {
seed := sha512.Sum512([]byte("lens.ristretto255.h-base.v1"))
e := ristretto255.NewElement().FromUniformBytes(seed[:])
ristretto255H = e
ristretto255HOnce = true
}
out := ristretto255.NewIdentityElement().Add(ristretto255H, ristretto255.NewIdentityElement())
return &ristrettoPoint{p: out}
}
// SampleScalar draws a uniform scalar via 64-byte read + reduction.
func (Ristretto255) SampleScalar(rand io.Reader) (Scalar, error) {
var buf [64]byte
if _, err := io.ReadFull(rand, buf[:]); err != nil {
return nil, fmt.Errorf("lens/primitives: ristretto255 sample: %w", err)
}
s := ristretto255.NewScalar().FromUniformBytes(buf[:])
return &ristrettoScalar{s: s}, nil
}
// HashToScalar produces a uniform scalar via SHA-512 + reduction.
func (Ristretto255) HashToScalar(data []byte) Scalar {
h := sha512.New()
_, _ = h.Write([]byte("lens.ristretto255.hash-to-scalar.v1"))
_, _ = h.Write(data)
digest := h.Sum(nil)
s := ristretto255.NewScalar().FromUniformBytes(digest)
return &ristrettoScalar{s: s}
}
// ristrettoScalar wraps ristretto255.Scalar to satisfy Scalar.
type ristrettoScalar struct {
s *ristretto255.Scalar
}
func (r *ristrettoScalar) Curve() Curve { return Ristretto255{} }
func (r *ristrettoScalar) Set(other Scalar) Scalar {
o := other.(*ristrettoScalar)
r.s.Set(o.s)
return r
}
func (r *ristrettoScalar) MarshalBinary() ([]byte, error) {
return r.s.Encode(nil), nil
}
func (r *ristrettoScalar) UnmarshalBinary(data []byte) error {
if len(data) != ristretto255ScalarSize {
return fmt.Errorf("lens/primitives: ristretto255 scalar size %d, want %d",
len(data), ristretto255ScalarSize)
}
if _, err := r.s.SetCanonicalBytes(data); err != nil {
return fmt.Errorf("lens/primitives: ristretto255 scalar non-canonical: %w", err)
}
return nil
}
func (r *ristrettoScalar) SetBytes(data []byte) error {
return r.UnmarshalBinary(data)
}
func (r *ristrettoScalar) SetUint64(v uint64) Scalar {
var buf [32]byte
for i := 0; i < 8; i++ {
buf[i] = byte(v >> (8 * i))
}
if _, err := r.s.SetCanonicalBytes(buf[:]); err != nil {
panic(fmt.Sprintf("lens/primitives: ristretto255 SetUint64: %v", err))
}
return r
}
func (r *ristrettoScalar) Add(other Scalar) Scalar {
o := other.(*ristrettoScalar)
r.s.Add(r.s, o.s)
return r
}
func (r *ristrettoScalar) Sub(other Scalar) Scalar {
o := other.(*ristrettoScalar)
r.s.Subtract(r.s, o.s)
return r
}
func (r *ristrettoScalar) Mul(other Scalar) Scalar {
o := other.(*ristrettoScalar)
r.s.Multiply(r.s, o.s)
return r
}
func (r *ristrettoScalar) Negate() Scalar {
r.s.Negate(r.s)
return r
}
func (r *ristrettoScalar) Invert() Scalar {
r.s.Invert(r.s)
return r
}
func (r *ristrettoScalar) IsZero() bool {
return r.s.Equal(ristretto255.NewScalar()) == 1
}
func (r *ristrettoScalar) Equal(other Scalar) bool {
o, ok := other.(*ristrettoScalar)
if !ok {
return false
}
return r.s.Equal(o.s) == 1
}
func (r *ristrettoScalar) ActOnBase() Point {
out := ristretto255.NewIdentityElement().ScalarBaseMult(r.s)
return &ristrettoPoint{p: out}
}
func (r *ristrettoScalar) Act(p Point) Point {
pt := p.(*ristrettoPoint)
out := ristretto255.NewIdentityElement().ScalarMult(r.s, pt.p)
return &ristrettoPoint{p: out}
}
// ristrettoPoint wraps ristretto255.Element to satisfy Point.
type ristrettoPoint struct {
p *ristretto255.Element
}
func (r *ristrettoPoint) Curve() Curve { return Ristretto255{} }
func (r *ristrettoPoint) MarshalBinary() ([]byte, error) {
return r.p.Encode(nil), nil
}
func (r *ristrettoPoint) UnmarshalBinary(data []byte) error {
if len(data) != ristretto255PointSize {
return fmt.Errorf("lens/primitives: ristretto255 point size %d, want %d",
len(data), ristretto255PointSize)
}
if _, err := r.p.SetCanonicalBytes(data); err != nil {
return fmt.Errorf("lens/primitives: ristretto255 point unmarshal: %w", err)
}
return nil
}
func (r *ristrettoPoint) Add(other Point) Point {
o := other.(*ristrettoPoint)
out := ristretto255.NewIdentityElement().Add(r.p, o.p)
return &ristrettoPoint{p: out}
}
func (r *ristrettoPoint) Sub(other Point) Point {
o := other.(*ristrettoPoint)
out := ristretto255.NewIdentityElement().Subtract(r.p, o.p)
return &ristrettoPoint{p: out}
}
func (r *ristrettoPoint) Negate() Point {
out := ristretto255.NewIdentityElement().Negate(r.p)
return &ristrettoPoint{p: out}
}
func (r *ristrettoPoint) IsIdentity() bool {
return r.p.Equal(ristretto255.NewIdentityElement()) == 1
}
func (r *ristrettoPoint) Equal(other Point) bool {
o, ok := other.(*ristrettoPoint)
if !ok {
return false
}
return r.p.Equal(o.p) == 1
}