mirror of
https://github.com/luxfi/lens.git
synced 2026-10-30 16:21:16 +00:00
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.
126 lines
3.6 KiB
Go
126 lines
3.6 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dkg
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"testing"
|
|
|
|
"github.com/luxfi/lens/primitives"
|
|
"github.com/luxfi/lens/sign"
|
|
"github.com/luxfi/lens/threshold"
|
|
)
|
|
|
|
func curves() []primitives.Curve {
|
|
return []primitives.Curve{
|
|
primitives.NewEd25519(),
|
|
primitives.NewSecp256k1(),
|
|
primitives.NewRistretto255(),
|
|
}
|
|
}
|
|
|
|
// TestDKGRunAndSign — distributed key generation followed by FROST
|
|
// signing. The signature must verify under the joint group public key.
|
|
func TestDKGRunAndSign(t *testing.T) {
|
|
for _, c := range curves() {
|
|
t.Run(c.Name(), func(t *testing.T) {
|
|
const n, tThr = 5, 3
|
|
shares, gk, err := Run(c, n, tThr, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("dkg.Run: %v", err)
|
|
}
|
|
if got := len(shares); got != n {
|
|
t.Fatalf("share count: want %d got %d", n, got)
|
|
}
|
|
for id, ks := range shares {
|
|
if err := ks.CheckConsistency(); err != nil {
|
|
t.Errorf("party %d: %v", id, err)
|
|
}
|
|
if ks.GroupKey != gk {
|
|
t.Errorf("party %d: GroupKey pointer mismatch", id)
|
|
}
|
|
}
|
|
|
|
// FROST sign with all n parties so we can use the
|
|
// already-cached Lambda from Run.
|
|
signers := []int{1, 2, 3, 4, 5}
|
|
signersByID := make(map[int]*sign.Signer, n)
|
|
commits := make(map[int]*sign.CommitMsg, n)
|
|
for _, id := range signers {
|
|
signersByID[id] = sign.NewSigner(shares[id])
|
|
cm, err := signersByID[id].Round1([]byte("dkg-sign-test"), signers, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("Round1 for %d: %v", id, err)
|
|
}
|
|
commits[id] = cm
|
|
}
|
|
responses := make(map[int]*sign.ResponseMsg, n)
|
|
for _, id := range signers {
|
|
rm, err := signersByID[id].Round2([]byte("dkg-sign-test"), commits)
|
|
if err != nil {
|
|
t.Fatalf("Round2 for %d: %v", id, err)
|
|
}
|
|
responses[id] = rm
|
|
}
|
|
verShares := threshold.VerificationShares(shares)
|
|
sig, err := sign.Aggregate(gk, []byte("dkg-sign-test"), signers, commits, responses, verShares)
|
|
if err != nil {
|
|
t.Fatalf("Aggregate: %v", err)
|
|
}
|
|
if err := sign.Verify(gk, []byte("dkg-sign-test"), sig); err != nil {
|
|
t.Fatalf("Verify: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDKGTamperedShare — a single bad share fails Round 2 and is
|
|
// attributed to the sender.
|
|
func TestDKGTamperedShare(t *testing.T) {
|
|
c := primitives.NewEd25519()
|
|
const n, tThr = 4, 3
|
|
|
|
sessions := make(map[int]*Session, n)
|
|
round1 := make(map[int]*Round1Output, n)
|
|
for j := 1; j <= n; j++ {
|
|
s, _ := NewSession(c, j, n, tThr)
|
|
sessions[j] = s
|
|
out, _ := s.Round1(rand.Reader)
|
|
round1[j] = out
|
|
}
|
|
|
|
// Tamper party 2's share for receiver 3: add 1.
|
|
bad := c.NewScalar().Set(round1[2].Shares[3])
|
|
bad.Add(c.NewScalar().SetUint64(1))
|
|
round1[2].Shares[3] = bad
|
|
|
|
// Receiver 3 runs Round 2 — must reject with attribution to sender 2.
|
|
recvShares := make(map[int]primitives.Scalar, n)
|
|
recvCommits := make(map[int][]primitives.Point, n)
|
|
for i := 1; i <= n; i++ {
|
|
recvShares[i] = round1[i].Shares[3]
|
|
recvCommits[i] = round1[i].Commits
|
|
}
|
|
if _, _, err := sessions[3].Round2(recvShares, recvCommits); err == nil {
|
|
t.Fatal("Round2 accepted tampered share")
|
|
}
|
|
}
|
|
|
|
// TestDKGSessionErrors covers the input-validation paths.
|
|
func TestDKGSessionErrors(t *testing.T) {
|
|
c := primitives.NewEd25519()
|
|
if _, err := NewSession(c, 1, 1, 1); err == nil {
|
|
t.Error("expected ErrInvalidPartyCount for n=1")
|
|
}
|
|
if _, err := NewSession(c, 0, 3, 2); err == nil {
|
|
t.Error("expected ErrInvalidPartyID for partyID=0")
|
|
}
|
|
if _, err := NewSession(c, 1, 3, 0); err == nil {
|
|
t.Error("expected ErrInvalidThreshold for t=0")
|
|
}
|
|
if _, err := NewSession(c, 1, 3, 4); err == nil {
|
|
t.Error("expected ErrInvalidThreshold for t > n")
|
|
}
|
|
}
|