mirror of
https://github.com/luxfi/threshold.git
synced 2026-10-30 15:15:53 +00:00
Squashed batch covering:
- protocols/pulsar/: lattice threshold lane (round-based wrapper for
github.com/luxfi/pulsar kernel); doc.go documents Photon/Lumen/Beam/
Pulsar/Pulse/Prism/Horizon/Quasar vocabulary stack from LP-105
- protocols/lens/: planned curve threshold sister kernel (design
intent only; mirrors Pulsar shape; replaces stub LSS-FROST)
- protocols/lss/lss_pulsar.go + tests: LSS-Pulsar adapter
(DynamicResharePulsar + PulsarSnapshotManager + BuildActivationTranscript).
Now SHA3-suite-aware: PulsarConfig carries NebulaRoot, HashSuiteID,
ImplementationVersion as optional transcript-binding fields that
flow through to the activation message.
10/10 acceptance tests pass:
1. GroupKey preservation
2. KeyEraID preservation
3. Generation +1
4. RollbackFrom=0 on forward
5. t_old != t_new
6. Disjoint set rotation
7. Valid signature under unchanged GroupKey
8. Pairwise material regenerated
9. Rollback semantics
10. Error surface
Plus 1 new test for BuildActivationTranscript Nebula+suite fields.
- protocols/lss/lss_frost.go: marked DEPRECATED (placeholder Sign()/
Refresh(), single-process simulation, hardcoded ChainKey/RID); will
be replaced by lss_lens.go.
- ringtail KAT cross-oracle vs Go reference (N=16 deterministic seeds)
- ringtail+protocol+harness hardening against parallel-run flakes
- lss/adapters: drop NewXRPL/NewCardano panic; ship real XRPL address
derivation
- ringtail SignWithConfig API expansion + test fixes
- v1.6.5 changelog
- go.sum h1 hashes for luxfi/log and 4 deps
Architecture (LP-105 / pulsar/DESIGN.md):
LSS owns lifecycle (Generation, Rollback, snapshots, dealer/
coordinator role separation). Pulsar owns lattice math (R_q shares,
lattice Pedersen commits, Sign1/Sign2/Combine, Pulsar-SHA3 hash
profile). The lss_pulsar adapter wires them. lss_lens (planned) does
the same for curve math.
go.mod adds local replace github.com/luxfi/pulsar => ../pulsar.
141 lines
3.3 KiB
Go
141 lines
3.3 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package mldsa
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
)
|
|
|
|
func TestRSSSubsets_Count(t *testing.T) {
|
|
// Subsets of size N-T+1 from [N], counted.
|
|
cases := []struct {
|
|
tt, n, want int
|
|
}{
|
|
{2, 2, 2}, // C(2,1) = 2
|
|
{2, 3, 3}, // C(3,2) = 3
|
|
{3, 3, 3}, // C(3,1) = 3
|
|
{3, 5, 10}, // C(5,3) = 10
|
|
{4, 6, 20}, // C(6,3) = 20
|
|
{6, 6, 6}, // C(6,1) = 6
|
|
}
|
|
for _, c := range cases {
|
|
got := RSSSubsets(c.tt, c.n)
|
|
if len(got) != c.want {
|
|
t.Errorf("RSSSubsets(T=%d,N=%d): got %d subsets, want %d",
|
|
c.tt, c.n, len(got), c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecover_TNCoversAllSecrets(t *testing.T) {
|
|
// For every supported (T, N), the recovery partition must cover every RSS
|
|
// subset exactly once across the active parties.
|
|
for n := 2; n <= 6; n++ {
|
|
for tt := 2; tt <= n; tt++ {
|
|
act := make([]int, tt)
|
|
for i := range act {
|
|
act[i] = i
|
|
}
|
|
rec := Recover(act, n)
|
|
seen := map[string]int{}
|
|
for _, shares := range rec {
|
|
for _, sh := range shares {
|
|
sort.Ints(sh)
|
|
key := shareKey(sh)
|
|
seen[key]++
|
|
}
|
|
}
|
|
subs := RSSSubsets(tt, n)
|
|
for _, s := range subs {
|
|
sort.Ints(s)
|
|
c := seen[shareKey(s)]
|
|
if c != 1 {
|
|
t.Errorf("T=%d N=%d: subset %v covered %d times, want 1",
|
|
tt, n, []int(s), c)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecover_TEqualsN(t *testing.T) {
|
|
// When T=N the only subsets are singletons; each party gets exactly one.
|
|
for n := 2; n <= 6; n++ {
|
|
act := make([]int, n)
|
|
for i := range act {
|
|
act[i] = i
|
|
}
|
|
rec := Recover(act, n)
|
|
for p, shares := range rec {
|
|
if len(shares) != 1 {
|
|
t.Errorf("T=N=%d party %d: got %d shares, want 1", n, p, len(shares))
|
|
}
|
|
if len(shares[0]) != 1 || shares[0][0] != p {
|
|
t.Errorf("T=N=%d party %d: got share %v, want {%d}", n, p, []int(shares[0]), p)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecover_BalanceBound(t *testing.T) {
|
|
// Appendix B: each party uses at most ⌈C(N, N-T+1) / T⌉ secrets.
|
|
for n := 2; n <= 6; n++ {
|
|
for tt := 2; tt < n; tt++ {
|
|
act := make([]int, tt)
|
|
for i := range act {
|
|
act[i] = i
|
|
}
|
|
total := len(RSSSubsets(tt, n))
|
|
bound := (total + tt - 1) / tt // ceiling
|
|
rec := Recover(act, n)
|
|
for _, shares := range rec {
|
|
if len(shares) > bound {
|
|
t.Errorf("T=%d N=%d: party had %d shares, bound %d",
|
|
tt, n, len(shares), bound)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParams_Coverage(t *testing.T) {
|
|
// Every (level, T, N) in the valid range must have a parameter entry.
|
|
for _, lvl := range []SecurityLevel{LevelI, LevelIII, LevelV} {
|
|
for n := 2; n <= 6; n++ {
|
|
for tt := 2; tt <= n; tt++ {
|
|
if _, ok := Params(lvl, tt, n); !ok {
|
|
t.Errorf("missing params for level=%d T=%d N=%d", lvl, tt, n)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParams_OutOfRange(t *testing.T) {
|
|
for _, bad := range [][2]int{{1, 2}, {3, 2}, {2, 7}, {0, 0}} {
|
|
if _, ok := Params(LevelI, bad[0], bad[1]); ok {
|
|
t.Errorf("expected out-of-range for T=%d N=%d", bad[0], bad[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
func shareKey(s Share) string {
|
|
ints := make([]int, len(s))
|
|
copy(ints, s)
|
|
return intsToKey(ints)
|
|
}
|
|
|
|
func intsToKey(a []int) string {
|
|
b := make([]byte, 0, len(a))
|
|
for _, v := range a {
|
|
b = append(b, byte('0'+v))
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// Sanity: Share compares correctly.
|
|
var _ = reflect.DeepEqual
|