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

105 lines
3.3 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package hash
import (
"bytes"
"testing"
)
func suites() []HashSuite {
return []HashSuite{NewLensSHA3(), NewLensBLAKE3()}
}
func TestSuiteIDsAreDistinct(t *testing.T) {
if NewLensSHA3().ID() == NewLensBLAKE3().ID() {
t.Fatal("suite IDs must differ across profiles")
}
}
func TestSuiteOperationsAreDeterministic(t *testing.T) {
for _, s := range suites() {
t.Run(s.ID(), func(t *testing.T) {
a := s.Hc([]byte("test"))
b := s.Hc([]byte("test"))
if !bytes.Equal(a, b) {
t.Fatal("Hc not deterministic")
}
if len(a) != 32 {
t.Fatalf("Hc output length %d, want 32", len(a))
}
a2 := s.Hu([]byte("test"), 64)
b2 := s.Hu([]byte("test"), 64)
if !bytes.Equal(a2, b2) {
t.Fatal("Hu not deterministic")
}
if len(a2) != 64 {
t.Fatalf("Hu output length %d, want 64", len(a2))
}
a3 := s.TranscriptHash([]byte("a"), []byte("b"))
b3 := s.TranscriptHash([]byte("a"), []byte("b"))
if a3 != b3 {
t.Fatal("TranscriptHash not deterministic")
}
})
}
}
func TestSuitesProduceDifferentBytes(t *testing.T) {
a := NewLensSHA3().Hc([]byte("test"))
b := NewLensBLAKE3().Hc([]byte("test"))
if bytes.Equal(a, b) {
t.Fatal("suites should not produce identical bytes for the same input")
}
}
func TestTagsAreDistinct(t *testing.T) {
for _, s := range suites() {
t.Run(s.ID(), func(t *testing.T) {
tr := s.TranscriptHash([]byte("x"))
prf := s.PRF([]byte("k"), []byte("x"), 32)
mac := s.MAC([]byte("k"), []byte("x"), 32)
pairwise := s.DerivePairwise([]byte("k"), []byte("c"), []byte("g"), 0, 0, 0, 1, 32)
if bytes.Equal(tr[:], prf) || bytes.Equal(prf, mac) || bytes.Equal(mac, pairwise) {
t.Fatal("operations with distinct tags collided")
}
})
}
}
func TestPairwiseCanonicalizesPair(t *testing.T) {
for _, s := range suites() {
t.Run(s.ID(), func(t *testing.T) {
a := s.DerivePairwise([]byte("k"), []byte("c"), []byte("g"), 1, 2, 3, 5, 32)
b := s.DerivePairwise([]byte("k"), []byte("c"), []byte("g"), 1, 2, 5, 3, 32)
if !bytes.Equal(a, b) {
t.Fatal("DerivePairwise should canonicalize pair order")
}
})
}
}
func TestDerivePairwiseDistinguishesFields(t *testing.T) {
s := NewLensSHA3()
base := s.DerivePairwise([]byte("k"), []byte("c"), []byte("g"), 1, 2, 3, 5, 32)
if v := s.DerivePairwise([]byte("k2"), []byte("c"), []byte("g"), 1, 2, 3, 5, 32); bytes.Equal(base, v) {
t.Fatal("kex change did not affect output")
}
if v := s.DerivePairwise([]byte("k"), []byte("c2"), []byte("g"), 1, 2, 3, 5, 32); bytes.Equal(base, v) {
t.Fatal("chainID change did not affect output")
}
if v := s.DerivePairwise([]byte("k"), []byte("c"), []byte("g2"), 1, 2, 3, 5, 32); bytes.Equal(base, v) {
t.Fatal("groupID change did not affect output")
}
if v := s.DerivePairwise([]byte("k"), []byte("c"), []byte("g"), 99, 2, 3, 5, 32); bytes.Equal(base, v) {
t.Fatal("eraID change did not affect output")
}
if v := s.DerivePairwise([]byte("k"), []byte("c"), []byte("g"), 1, 99, 3, 5, 32); bytes.Equal(base, v) {
t.Fatal("generation change did not affect output")
}
if v := s.DerivePairwise([]byte("k"), []byte("c"), []byte("g"), 1, 2, 99, 5, 32); bytes.Equal(base, v) {
t.Fatal("party-i change did not affect output")
}
}