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.
- corona KAT cross-oracle vs Go reference (N=16 deterministic seeds)
- corona+protocol+harness hardening against parallel-run flakes
- lss/adapters: drop NewXRPL/NewCardano panic; ship real XRPL address
derivation
- corona 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.
195 lines
5.2 KiB
Go
195 lines
5.2 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package mldsa
|
|
|
|
import "sort"
|
|
|
|
// Share identifies a subset I ⊂ [N] with |I| = N-T+1. The RSS scheme produces
|
|
// one secret s_I per such subset, known only to parties in I.
|
|
type Share []int
|
|
|
|
// RSSSubsets enumerates all subsets of [N] of size N-T+1 in lexicographic
|
|
// order. For each subset I, the RSS scheme samples one s_I ← χ_s and sends it
|
|
// to every party in I.
|
|
func RSSSubsets(t, n int) []Share {
|
|
k := n - t + 1
|
|
if k <= 0 || k > n {
|
|
return nil
|
|
}
|
|
var out []Share
|
|
var walk func(start int, acc []int)
|
|
walk = func(start int, acc []int) {
|
|
if len(acc) == k {
|
|
s := make(Share, k)
|
|
copy(s, acc)
|
|
out = append(out, s)
|
|
return
|
|
}
|
|
for i := start; i < n; i++ {
|
|
walk(i+1, append(acc, i))
|
|
}
|
|
}
|
|
walk(0, nil)
|
|
return out
|
|
}
|
|
|
|
// Recover computes, for an active signing set act of size T, the partition
|
|
// (m_i)_{i∈act} of the RSS subsets such that each secret s_I is assigned to
|
|
// exactly one party in act and max_i |m_i| is minimized.
|
|
//
|
|
// For 2 ≤ T < N ≤ 6 the optimal partitions are hardcoded (Appendix B of the
|
|
// paper, Algorithm 6). For T = N each party holds exactly one secret.
|
|
func Recover(act []int, n int) map[int][]Share {
|
|
if len(act) == 0 {
|
|
return nil
|
|
}
|
|
sorted := make([]int, len(act))
|
|
copy(sorted, act)
|
|
sort.Ints(sorted)
|
|
t := len(sorted)
|
|
|
|
result := make(map[int][]Share)
|
|
|
|
// T == N: each party holds exactly one secret (the one with |I| = 1).
|
|
// The only subsets of size N-T+1 = 1 are singletons.
|
|
if t == n {
|
|
for _, p := range sorted {
|
|
result[p] = []Share{{p}}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// For small (T, N) use the hardcoded optimal partition for
|
|
// act = {0, 1, ..., T-1} and translate by symmetry.
|
|
tmpl, ok := recoverTemplates[[2]int{t, n}]
|
|
if !ok {
|
|
return fallbackRecover(sorted, n, t)
|
|
}
|
|
|
|
// Build a permutation φ mapping template indices {0..T-1} to the actual
|
|
// active parties, and {T..N-1} to the inactive parties (any order).
|
|
phi := make([]int, n)
|
|
idxAct, idxInact := 0, t
|
|
inActive := make(map[int]bool, t)
|
|
for _, p := range sorted {
|
|
inActive[p] = true
|
|
}
|
|
for j := 0; j < n; j++ {
|
|
if inActive[j] {
|
|
phi[idxAct] = j
|
|
idxAct++
|
|
} else {
|
|
phi[idxInact] = j
|
|
idxInact++
|
|
}
|
|
}
|
|
|
|
for templateIdx, shares := range tmpl {
|
|
party := phi[templateIdx]
|
|
for _, sh := range shares {
|
|
translated := make(Share, len(sh))
|
|
for i, p := range sh {
|
|
translated[i] = phi[p]
|
|
}
|
|
sort.Ints(translated)
|
|
result[party] = append(result[party], translated)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// fallbackRecover runs a greedy balanced assignment for (T, N) not in the
|
|
// hardcoded table. Each secret s_I is assigned to the active-party member of
|
|
// I with the lightest current load.
|
|
func fallbackRecover(act []int, n, t int) map[int][]Share {
|
|
result := make(map[int][]Share)
|
|
for _, p := range act {
|
|
result[p] = nil
|
|
}
|
|
subsets := RSSSubsets(t, n)
|
|
actSet := make(map[int]bool, len(act))
|
|
for _, p := range act {
|
|
actSet[p] = true
|
|
}
|
|
for _, sh := range subsets {
|
|
best := -1
|
|
bestLoad := 1 << 30
|
|
for _, p := range sh {
|
|
if !actSet[p] {
|
|
continue
|
|
}
|
|
if len(result[p]) < bestLoad {
|
|
best = p
|
|
bestLoad = len(result[p])
|
|
}
|
|
}
|
|
if best < 0 {
|
|
// No active party in this subset — skip; this means an honest
|
|
// subset's secret remains unknown to signers, which is fine for
|
|
// the security argument but means signing this session uses
|
|
// fewer secrets. In practice for valid (T, N) every N-T+1
|
|
// subset has at least one active party since |act| = T.
|
|
continue
|
|
}
|
|
result[best] = append(result[best], sh)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// recoverTemplates encodes Algorithm 6 from Appendix B of the paper. Each
|
|
// entry maps a template party index in [0..T-1] to the list of share subsets
|
|
// that party is responsible for when act = {0, 1, ..., T-1}.
|
|
var recoverTemplates = map[[2]int]map[int][]Share{
|
|
{2, 3}: {
|
|
0: {{0, 1}, {0, 2}},
|
|
1: {{1, 2}},
|
|
},
|
|
{2, 4}: {
|
|
0: {{0, 1, 3}, {0, 2, 3}},
|
|
1: {{0, 1, 2}, {1, 2, 3}},
|
|
},
|
|
{3, 4}: {
|
|
0: {{0, 1}, {0, 3}},
|
|
1: {{1, 2}, {1, 3}},
|
|
2: {{2, 3}, {0, 2}},
|
|
},
|
|
{2, 5}: {
|
|
0: {{0, 1, 3, 4}, {0, 2, 3, 4}, {0, 1, 2, 4}},
|
|
1: {{1, 2, 3, 4}, {0, 1, 2, 3}},
|
|
},
|
|
{3, 5}: {
|
|
0: {{0, 3, 4}, {0, 1, 3}, {0, 1, 4}, {0, 2, 3}},
|
|
1: {{0, 1, 2}, {1, 2, 3}, {1, 2, 4}, {1, 3, 4}},
|
|
2: {{2, 3, 4}, {0, 2, 4}},
|
|
},
|
|
{4, 5}: {
|
|
0: {{0, 1}, {0, 3}, {0, 4}},
|
|
1: {{1, 2}, {1, 3}, {1, 4}},
|
|
2: {{2, 3}, {0, 2}, {2, 4}},
|
|
3: {{3, 4}},
|
|
},
|
|
{2, 6}: {
|
|
0: {{0, 2, 3, 4, 5}, {0, 1, 2, 3, 5}, {0, 1, 2, 4, 5}},
|
|
1: {{1, 2, 3, 4, 5}, {0, 1, 2, 3, 4}, {0, 1, 3, 4, 5}},
|
|
},
|
|
{3, 6}: {
|
|
0: {{0, 1, 3, 4}, {0, 1, 2, 4}, {0, 1, 3, 5}, {0, 3, 4, 5}, {0, 1, 2, 5}},
|
|
1: {{0, 1, 4, 5}, {1, 3, 4, 5}, {1, 2, 3, 5}, {1, 2, 3, 4}, {1, 2, 4, 5}},
|
|
2: {{0, 2, 3, 5}, {0, 2, 4, 5}, {0, 2, 3, 4}, {0, 1, 2, 3}, {2, 3, 4, 5}},
|
|
},
|
|
{4, 6}: {
|
|
0: {{0, 1, 4}, {0, 2, 3}, {0, 1, 5}, {0, 1, 2}, {0, 4, 5}},
|
|
1: {{1, 3, 5}, {1, 3, 4}, {1, 2, 5}, {1, 4, 5}, {1, 2, 4}},
|
|
2: {{2, 4, 5}, {0, 2, 4}, {2, 3, 5}, {2, 3, 4}, {0, 2, 5}},
|
|
3: {{0, 3, 4}, {0, 1, 3}, {1, 2, 3}, {3, 4, 5}, {0, 3, 5}},
|
|
},
|
|
{5, 6}: {
|
|
0: {{0, 1}, {0, 2}, {0, 5}},
|
|
1: {{1, 2}, {1, 3}, {1, 5}},
|
|
2: {{2, 3}, {2, 4}, {2, 5}},
|
|
3: {{0, 3}, {3, 4}, {3, 5}},
|
|
4: {{4, 5}, {0, 4}, {1, 4}},
|
|
},
|
|
}
|