mirror of
https://github.com/luxfi/threshold.git
synced 2026-10-30 15:15:53 +00:00
This package wires the github.com/luxfi/corona Module-LWE kernel
(Ringtail/Raccoon, q=0x1000000004A01), but carried stale "Pulsar"
labels from when Corona was historically named Pulsar:
- package doc claimed it wired the Pulsar kernel (github.com/luxfi/pulsar)
and was "the equivalent of protocols/corona" — self-contradictory; the
imports are github.com/luxfi/corona/{keyera,threshold}. Rewrote the doc
to state the scheme accurately and note the Corona-vs-Pulsar distinction
(both Module-LWE; Pulsar is FIPS-204/ML-DSA q=8380417, Corona is
Ringtail/Raccoon q=0x1000000004A01).
- error strings "pulsar:" → "corona:" (sentinel identities unchanged;
substrings asserted by tests preserved).
- exported aliases PulsarKeyEraID/PulsarGroupID → CoronaKeyEraID/
CoronaGroupID (the corona kernel already uses the Corona* names; these
wrappers had zero external consumers across ~/work/lux).
No wire format, modulus, or test vector changed. Doc-, string-, and
identifier-only. corona pkg builds + tests green.
224 lines
7.3 KiB
Go
224 lines
7.3 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package corona
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/luxfi/threshold/pkg/party"
|
|
)
|
|
|
|
// The corona package is a thin alias surface over luxfi/corona/{keyera,threshold}.
|
|
// These tests pin the argument-validation contracts in Bootstrap / Reshare /
|
|
// Reanchor / ShareForParty and the alias-surface round-trip for the trusted-
|
|
// dealer keygen + Verify / VerifyBatch / VerifyBatchAll. They are package
|
|
// tests (not _test) so unexported helpers (validatorIDs) can be reached.
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Bootstrap — argument validation
|
|
// -----------------------------------------------------------------------------
|
|
|
|
func TestBootstrap_RejectsEmptyValidators(t *testing.T) {
|
|
era, err := Bootstrap(1, nil, CoronaGroupID(0), 0, rand.Reader)
|
|
if era != nil {
|
|
t.Fatalf("era should be nil on error, got %v", era)
|
|
}
|
|
if !errors.Is(err, ErrEmptyValidators) {
|
|
t.Fatalf("want ErrEmptyValidators, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBootstrap_RejectsThresholdZero(t *testing.T) {
|
|
_, err := Bootstrap(0, []party.ID{"a", "b"}, CoronaGroupID(0), 0, rand.Reader)
|
|
if !errors.Is(err, ErrInvalidThreshold) {
|
|
t.Fatalf("want ErrInvalidThreshold, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBootstrap_RejectsThresholdAboveN(t *testing.T) {
|
|
_, err := Bootstrap(3, []party.ID{"a", "b"}, CoronaGroupID(0), 0, rand.Reader)
|
|
if !errors.Is(err, ErrInvalidThreshold) {
|
|
t.Fatalf("want ErrInvalidThreshold, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBootstrap_NilEntropyDefaultsToCryptoRand(t *testing.T) {
|
|
// Passing nil entropy must NOT panic and must not return an entropy-related
|
|
// error — Bootstrap is documented to fall back to crypto/rand.Reader.
|
|
era, err := Bootstrap(1, []party.ID{"only"}, CoronaGroupID(0), 0, nil)
|
|
if err != nil {
|
|
// Bootstrap may still fail for kernel reasons in a constrained env;
|
|
// the contract we are pinning here is "no panic from nil entropy".
|
|
// We accept either a non-nil era or a non-entropy error.
|
|
t.Logf("kernel returned err=%v (acceptable so long as no panic)", err)
|
|
}
|
|
_ = era
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Reshare — argument validation
|
|
// -----------------------------------------------------------------------------
|
|
|
|
func TestReshare_RejectsNilKeyEra(t *testing.T) {
|
|
_, err := Reshare(nil, []party.ID{"a"}, 1, rand.Reader)
|
|
if err == nil || !strings.Contains(err.Error(), "nil key era") {
|
|
t.Fatalf("want nil-key-era error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReshare_RejectsEmptyValidators(t *testing.T) {
|
|
// Even with a nil era we expect the empty-validators path to short-circuit
|
|
// only when era is non-nil — confirm the nil-era guard wins first.
|
|
_, err := Reshare(nil, nil, 1, rand.Reader)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
// And confirm a synthesized empty-validators path on a non-nil era hits
|
|
// ErrEmptyValidators if reachable. (Constructing a real *KeyEra here is
|
|
// out of scope; the nil-era guard above covers the validation surface.)
|
|
}
|
|
|
|
func TestReshare_RejectsThresholdZeroOrAboveN(t *testing.T) {
|
|
// We test through Bootstrap-then-Reshare to exercise the t guard, but
|
|
// can also build a synthetic era. The nil-era path is the most reliable
|
|
// here without a full Bootstrap. The threshold guard is tested via
|
|
// Reanchor below which shares the same shape.
|
|
_, err := Reshare(nil, []party.ID{"a"}, 2, rand.Reader)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Reanchor — argument validation
|
|
// -----------------------------------------------------------------------------
|
|
|
|
func TestReanchor_RejectsEmptyValidators(t *testing.T) {
|
|
_, err := Reanchor(nil, 1, nil, CoronaGroupID(0), rand.Reader)
|
|
if !errors.Is(err, ErrEmptyValidators) {
|
|
t.Fatalf("want ErrEmptyValidators, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReanchor_RejectsThresholdZero(t *testing.T) {
|
|
_, err := Reanchor(nil, 0, []party.ID{"a"}, CoronaGroupID(0), rand.Reader)
|
|
if !errors.Is(err, ErrInvalidThreshold) {
|
|
t.Fatalf("want ErrInvalidThreshold, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReanchor_RejectsThresholdAboveN(t *testing.T) {
|
|
_, err := Reanchor(nil, 2, []party.ID{"a"}, CoronaGroupID(0), rand.Reader)
|
|
if !errors.Is(err, ErrInvalidThreshold) {
|
|
t.Fatalf("want ErrInvalidThreshold, got %v", err)
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// ShareForParty
|
|
// -----------------------------------------------------------------------------
|
|
|
|
func TestShareForParty_NilState(t *testing.T) {
|
|
share, err := ShareForParty(nil, "anyone")
|
|
if share != nil {
|
|
t.Fatalf("share should be nil, got %v", share)
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "nil share state") {
|
|
t.Fatalf("want nil-state error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestShareForParty_PartyNotInSet(t *testing.T) {
|
|
state := &EpochShareState{Shares: map[string]*KeyShare{
|
|
"alice": {},
|
|
}}
|
|
_, err := ShareForParty(state, party.ID("eve"))
|
|
if !errors.Is(err, ErrPartyNotInSet) {
|
|
t.Fatalf("want ErrPartyNotInSet, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "eve") {
|
|
t.Fatalf("error should include the missing party id, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestShareForParty_HappyPath(t *testing.T) {
|
|
want := &KeyShare{}
|
|
state := &EpochShareState{Shares: map[string]*KeyShare{
|
|
"alice": want,
|
|
}}
|
|
got, err := ShareForParty(state, party.ID("alice"))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("share pointer mismatch: got %p want %p", got, want)
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Alias surface — NewParams + trusted-dealer GenerateKeys + Verify roundtrip
|
|
// -----------------------------------------------------------------------------
|
|
|
|
func TestNewParams_NonNil(t *testing.T) {
|
|
p, err := NewParams()
|
|
if err != nil {
|
|
t.Fatalf("NewParams: %v", err)
|
|
}
|
|
if p == nil {
|
|
t.Fatal("NewParams returned nil with no error")
|
|
}
|
|
}
|
|
|
|
func TestGenerateKeys_TrustedDealer_SmokeRoundtrip(t *testing.T) {
|
|
// Smallest committee that exercises the threshold path: t-of-n = 2-of-3.
|
|
const tThreshold, n = 2, 3
|
|
seed := bytes.NewReader(bytes.Repeat([]byte{0xA5}, 4096))
|
|
shares, gk, err := GenerateKeys(tThreshold, n, seed)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeys: %v", err)
|
|
}
|
|
if gk == nil {
|
|
t.Fatal("GroupKey should be non-nil on success")
|
|
}
|
|
if got := len(shares); got != n {
|
|
t.Fatalf("want %d shares, got %d", n, got)
|
|
}
|
|
for i, sh := range shares {
|
|
if sh == nil {
|
|
t.Fatalf("shares[%d] is nil", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// validatorIDs (unexported helper)
|
|
// -----------------------------------------------------------------------------
|
|
|
|
func TestValidatorIDs_PreservesOrderAndLength(t *testing.T) {
|
|
in := []party.ID{"alice", "bob", "carol"}
|
|
out := validatorIDs(in)
|
|
if len(out) != len(in) {
|
|
t.Fatalf("len mismatch: in=%d out=%d", len(in), len(out))
|
|
}
|
|
for i := range in {
|
|
if string(in[i]) != out[i] {
|
|
t.Fatalf("element %d mismatch: in=%q out=%q", i, in[i], out[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidatorIDs_EmptyInput(t *testing.T) {
|
|
out := validatorIDs(nil)
|
|
if out == nil {
|
|
t.Fatal("validatorIDs(nil) should return a non-nil empty slice, not nil")
|
|
}
|
|
if len(out) != 0 {
|
|
t.Fatalf("want empty slice, got len=%d", len(out))
|
|
}
|
|
}
|