Files
lens/reshare/complaint_test.go
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

97 lines
2.8 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package reshare
import (
"crypto/ed25519"
"crypto/rand"
"testing"
)
// TestComplaintSignAndVerify — sign+verify round-trips cleanly.
func TestComplaintSignAndVerify(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
c := &Complaint{
TranscriptHash: [32]byte{0xAA, 0xBB},
SenderID: 3,
ComplainerID: 5,
Reason: ComplaintBadDelivery,
Evidence: []byte("bad-share-bytes"),
}
c.Sign(priv)
if err := c.Verify(); err != nil {
t.Fatalf("Verify: %v", err)
}
if string(c.ComplainerKey) != string(pub) {
t.Fatal("ComplainerKey not pinned to signer")
}
// Tampered: changing reason invalidates signature.
c.Reason = ComplaintMissing
if err := c.Verify(); err == nil {
t.Fatal("tampered complaint accepted")
}
}
// TestDisqualificationThreshold matches the rule t_old - 1 (default).
func TestDisqualificationThreshold(t *testing.T) {
if got := DisqualificationThreshold(5); got != 4 {
t.Errorf("threshold(5): want 4 got %d", got)
}
if got := DisqualificationThreshold(2); got != 1 {
t.Errorf("threshold(2): want 1 got %d", got)
}
if got := DisqualificationThreshold(1); got != 1 {
t.Errorf("threshold(1): want 1 got %d", got)
}
}
// TestComputeDisqualifiedSet deduplicates and applies the threshold.
func TestComputeDisqualifiedSet(t *testing.T) {
mk := func(sender, complainer int) *Complaint {
return &Complaint{SenderID: sender, ComplainerID: complainer, Reason: ComplaintBadDelivery}
}
complaints := []*Complaint{
// sender 1: 3 distinct complainers (>= t_old-1 = 4)? no, 3 < 4 so NOT disqualified
mk(1, 2), mk(1, 3), mk(1, 4),
// sender 2: 4 distinct complainers >= 4
mk(2, 1), mk(2, 3), mk(2, 4), mk(2, 5),
// duplicate complainer for sender 2 — does not count again
mk(2, 1),
}
dq := ComputeDisqualifiedSet(complaints, 5)
if _, ok := dq[1]; ok {
t.Error("sender 1 wrongly disqualified")
}
if _, ok := dq[2]; !ok {
t.Error("sender 2 should be disqualified")
}
}
// TestFilterQualifiedQuorum removes disqualified, errors if too few left.
func TestFilterQualifiedQuorum(t *testing.T) {
dq := map[int]struct{}{2: {}, 4: {}}
out, err := FilterQualifiedQuorum([]int{1, 2, 3, 4, 5}, dq, 3)
if err != nil {
t.Fatalf("FilterQualifiedQuorum: %v", err)
}
want := []int{1, 3, 5}
if len(out) != len(want) {
t.Fatalf("want %d survivors got %d", len(want), len(out))
}
for i, id := range want {
if out[i] != id {
t.Errorf("survivor[%d]: want %d got %d", i, id, out[i])
}
}
// Too few survivors: error.
if _, err := FilterQualifiedQuorum([]int{1, 2}, map[int]struct{}{1: {}}, 2); err == nil {
t.Fatal("expected ErrInsufficientQuorum")
}
}