Files
zeekay cdfb27641d threshold: Pulsar/Lens kernel adapters + corona KAT + Pulsar-SHA3 wiring
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.
2026-03-03 12:00:00 -08:00

406 lines
13 KiB
Go

// Package lss provides dynamic resharing extensions for FROST protocols.
//
// DEPRECATED — superseded by the Lens curve-threshold kernel and its
// LSS-Lens adapter (LP-103). Retained only for binary compatibility
// with callers in transit; new integrations MUST use Lens.
//
// This file's surface predates the LSS-Pulsar contract and reflects
// in-process simulation conventions:
//
// - DynamicReshareFROST runs in one Go process. No round-based
// message passing; no JVSS distribution of the auxiliary w / q
// secrets. The single-process simulation is correct mathematically
// but is not the on-wire protocol.
// - Refresh() and Sign() are intentionally short-circuited: the
// underlying FROST primitives expose protocol.StartFunc, and
// callers that need real distributed signing run frost.Sign
// directly through the round-based protocol harness.
// - ConvertToLSSConfig writes static ChainKey / RID byte tags,
// "frost-chainkey" / "frost-rid". Lens supersedes this with values
// bound to the genesis ceremony or proper distributed FROST DKG.
// - verifyResharingFROST checks public-key consistency, which is
// sufficient for the in-process correctness contract used by
// existing tests; it does not enforce on-wire protocol correctness.
//
// The replacement is the Lens curve-threshold kernel (LP-103,
// github.com/luxfi/lens) plus an LSS-Lens adapter mirroring
// lss_pulsar.go: Lens owns curve math (Pedersen commits, FROST 2-round
// signing, curve resharing); the adapter wires Generation tracking,
// Rollback, snapshot persistence, and dealer/coordinator role
// separation. Production resharing flows through that path.
package lss
import (
"errors"
"fmt"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/math/polynomial"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/pkg/pool"
"github.com/luxfi/threshold/protocols/frost/keygen"
"github.com/luxfi/threshold/protocols/lss/config"
)
// FROSTConfig wraps the FROST keygen config for LSS compatibility
type FROSTConfig struct {
*keygen.Config
Generation uint64
RollbackFrom uint64
}
// FROST extends the FROST protocol with LSS dynamic resharing capabilities.
// This allows FROST to perform membership changes without reconstructing the master key.
type FROST struct {
config *FROSTConfig
generation uint64
pool *pool.Pool
}
// NewLSSFROST creates a new LSS-extended FROST instance
func NewLSSFROST(frostConfig *keygen.Config, pool *pool.Pool) *FROST {
return &FROST{
config: &FROSTConfig{
Config: frostConfig,
Generation: 0,
},
generation: 0,
pool: pool,
}
}
// DynamicReshareFROST performs the LSS dynamic resharing protocol on FROST configurations.
// This implements the protocol from Section 4 of the LSS paper, allowing
// transition from T-of-N to T'-of-(N±k) without reconstructing the master key.
func DynamicReshareFROST(
oldConfigs map[party.ID]*keygen.Config,
newPartyIDs []party.ID,
newThreshold int,
_ *pool.Pool,
) (map[party.ID]*keygen.Config, error) {
if len(oldConfigs) == 0 {
return nil, errors.New("lss-frost: no old configurations provided")
}
if newThreshold < 1 || newThreshold > len(newPartyIDs) {
return nil, fmt.Errorf("lss-frost: invalid threshold %d for %d parties", newThreshold, len(newPartyIDs))
}
// Get reference config and validate consistency
var refConfig *keygen.Config
var group curve.Curve
oldPartyIDs := make([]party.ID, 0, len(oldConfigs))
for pid, cfg := range oldConfigs {
if refConfig == nil {
refConfig = cfg
group = cfg.Curve()
} else {
// Verify all configs are from the same keygen
if !cfg.PublicKey.Equal(refConfig.PublicKey) {
return nil, errors.New("lss-frost: inconsistent public keys in old configs")
}
}
oldPartyIDs = append(oldPartyIDs, pid)
}
// Ensure we have enough old parties to reconstruct the secret
if len(oldPartyIDs) < refConfig.Threshold {
return nil, fmt.Errorf("lss-frost: need at least %d old parties, have %d",
refConfig.Threshold, len(oldPartyIDs))
}
// Step 1: Generate auxiliary secrets w and q using polynomial secret sharing
// These are temporary secrets used only during the resharing protocol
wPoly := polynomial.NewPolynomial(group, newThreshold-1, nil)
qPoly := polynomial.NewPolynomial(group, newThreshold-1, nil)
// All parties (old and new) get shares of w and q
allParties := make(map[party.ID]bool)
for _, pid := range oldPartyIDs {
allParties[pid] = true
}
for _, pid := range newPartyIDs {
allParties[pid] = true
}
wShares := make(map[party.ID]curve.Scalar)
qShares := make(map[party.ID]curve.Scalar)
for pid := range allParties {
wShares[pid] = wPoly.Evaluate(pid.Scalar(group))
qShares[pid] = qPoly.Evaluate(pid.Scalar(group))
}
// Step 2: Compute the blinded secret a * w
// Each old party computes a_i * w_i, then we interpolate to get a * w
blindedProducts := make(map[party.ID]curve.Scalar)
// Use first threshold old parties
contributingParties := oldPartyIDs[:refConfig.Threshold]
for _, pid := range contributingParties {
cfg := oldConfigs[pid]
wShare := wShares[pid]
// Compute a_i * w_i
product := group.NewScalar().Set(cfg.PrivateShare).Mul(wShare)
blindedProducts[pid] = product
}
// Interpolate the blinded products to get a * w
lagrange := polynomial.Lagrange(group, contributingParties)
aTimesW := group.NewScalar()
for pid, product := range blindedProducts {
if coeff, exists := lagrange[pid]; exists {
contribution := group.NewScalar().Set(coeff).Mul(product)
aTimesW.Add(contribution)
}
}
// Step 3: Compute z = (q * w)^{-1}
// First, parties compute q_j * w_j and we interpolate to get q * w
qwProducts := make(map[party.ID]curve.Scalar)
// Use first newThreshold parties for this computation
computingParties := make([]party.ID, 0, newThreshold)
for pid := range allParties {
if len(computingParties) >= newThreshold {
break
}
computingParties = append(computingParties, pid)
qShare := qShares[pid]
wShare := wShares[pid]
product := group.NewScalar().Set(qShare).Mul(wShare)
qwProducts[pid] = product
}
// Interpolate to get q * w
newLagrange := polynomial.Lagrange(group, computingParties)
qTimesW := group.NewScalar()
for pid, product := range qwProducts {
if coeff, exists := newLagrange[pid]; exists {
contribution := group.NewScalar().Set(coeff).Mul(product)
qTimesW.Add(contribution)
}
}
// Compute z = (q * w)^{-1}
z := group.NewScalar().Set(qTimesW)
z = z.Invert()
// Create shares of z for distribution to new parties
zPoly := polynomial.NewPolynomial(group, newThreshold-1, z)
zShares := make(map[party.ID]curve.Scalar)
for _, pid := range newPartyIDs {
zShares[pid] = zPoly.Evaluate(pid.Scalar(group))
}
// Step 4: Each new party computes their new share
// a'_j = (a * w) * q_j * z_j
newConfigs := make(map[party.ID]*keygen.Config)
// First, compute all new shares
newShares := make(map[party.ID]curve.Scalar)
for _, pid := range newPartyIDs {
qShare := qShares[pid]
zShare := zShares[pid]
// Compute new private share: a'_j = (a * w) * q_j * z_j
newPrivateShare := group.NewScalar().Set(aTimesW)
newPrivateShare.Mul(qShare).Mul(zShare)
newShares[pid] = newPrivateShare
}
// Now create configs with all the public information
// Compute new verification shares
newVerificationShares := make(map[party.ID]curve.Point)
for _, pid := range newPartyIDs {
newVerificationShares[pid] = newShares[pid].ActOnBase()
}
for _, pid := range newPartyIDs {
newConfig := &keygen.Config{
ID: pid,
Threshold: newThreshold,
PrivateShare: newShares[pid],
PublicKey: refConfig.PublicKey, // Preserve the public key
VerificationShares: party.NewPointMap(newVerificationShares),
}
newConfigs[pid] = newConfig
}
// Verify the resharing was correct
if err := verifyResharingFROST(oldConfigs, newConfigs, refConfig.Threshold, newThreshold); err != nil {
return nil, fmt.Errorf("lss-frost: resharing verification failed: %w", err)
}
return newConfigs, nil
}
// verifyResharingFROST validates that new shares correctly reconstruct the original public key
func verifyResharingFROST(
oldConfigs map[party.ID]*keygen.Config,
newConfigs map[party.ID]*keygen.Config,
oldThreshold int,
newThreshold int,
) error {
// Get the original public key from old configs
var oldPublicKey curve.Point
var group curve.Curve
// Get first old config to extract public key and group
for _, cfg := range oldConfigs {
oldPublicKey = cfg.PublicKey
group = cfg.Curve()
break
}
// Verify new shares reconstruct to the same public key
// Use Lagrange interpolation with new threshold parties
newPartyIDs := make([]party.ID, 0, len(newConfigs))
for pid := range newConfigs {
newPartyIDs = append(newPartyIDs, pid)
if len(newPartyIDs) >= newThreshold {
break
}
}
if len(newPartyIDs) < newThreshold {
return fmt.Errorf("insufficient new parties for verification: have %d, need %d",
len(newPartyIDs), newThreshold)
}
// Compute Lagrange coefficients for the new parties
lagrange := polynomial.Lagrange(group, newPartyIDs)
// Reconstruct public key from new shares
reconstructedKey := group.NewPoint()
for _, pid := range newPartyIDs {
cfg := newConfigs[pid]
if cfg == nil {
return fmt.Errorf("missing config for party %s", pid)
}
// Get the verification share for this party
verificationShare := cfg.VerificationShares.Points[pid]
// Apply Lagrange coefficient
if coeff, exists := lagrange[pid]; exists {
contribution := coeff.Act(verificationShare)
reconstructedKey = reconstructedKey.Add(contribution)
}
}
// Verify the reconstructed key matches the original
if !reconstructedKey.Equal(oldPublicKey) {
return errors.New("resharing verification failed: public keys do not match")
}
// Additional verification: check threshold consistency
if oldThreshold > len(oldConfigs) {
return fmt.Errorf("old threshold %d exceeds old party count %d",
oldThreshold, len(oldConfigs))
}
if newThreshold > len(newConfigs) {
return fmt.Errorf("new threshold %d exceeds new party count %d",
newThreshold, len(newConfigs))
}
return nil
}
// Sign is deprecated. The underlying FROST primitives expose a
// protocol.StartFunc; production callers MUST drive the 2-round
// distributed signing protocol via frost.Sign through the round-based
// protocol harness. This LSS-FROST shim is retained only for binary
// compatibility and always returns errFROSTSignDeprecated.
func (f *FROST) Sign(_ []party.ID, _ []byte) ([]byte, error) {
return nil, errFROSTSignDeprecated
}
// errFROSTSignDeprecated is returned by the deprecated LSS-FROST
// signing entrypoint. Use frost.Sign through the round-based protocol
// harness for real distributed signing; LSS-Lens will replace this
// shim entirely.
var errFROSTSignDeprecated = errors.New("lss-frost: deprecated; drive frost.Sign through the round-based protocol harness or use the LSS-Lens adapter")
// Refresh is deprecated. Real proactive-refresh flows through Lens's
// HJKY97 zero-poly round-based protocol and the LSS-Lens adapter.
// This shim only advances the LSS Generation counter so existing
// in-process tests keep working; it does NOT execute the FROST refresh
// math.
func (f *FROST) Refresh() (*FROSTConfig, error) {
f.generation++
f.config.Generation = f.generation
return f.config, nil
}
// GetGeneration returns the current resharing generation number
func (f *FROST) GetGeneration() uint64 {
return f.generation
}
// GetConfig returns the current FROST configuration
func (f *FROST) GetConfig() *FROSTConfig {
return f.config
}
// UpdateConfig updates the configuration after a successful resharing
func (f *FROST) UpdateConfig(newConfig *FROSTConfig) {
f.config = newConfig
f.generation++
}
// ConvertToLSSConfig converts a FROST config to LSS config format for
// in-process compatibility. The static ChainKey / RID tags are
// deterministic so legacy callers see stable bytes; production paths
// derive these values from the genesis ceremony or proper distributed
// FROST DKG via the LSS-Lens adapter.
func ConvertToLSSConfig(frostConfig *keygen.Config, generation uint64) *Config {
return &Config{
ID: frostConfig.ID,
Group: frostConfig.Curve(),
Threshold: frostConfig.Threshold,
Generation: generation,
ECDSA: frostConfig.PrivateShare,
Public: convertVerificationShares(frostConfig.VerificationShares.Points),
ChainKey: []byte("frost-chainkey-deprecated"),
RID: []byte("frost-rid-deprecated"),
}
}
// convertVerificationShares converts FROST verification shares to LSS public format
func convertVerificationShares(verificationShares map[party.ID]curve.Point) map[party.ID]*config.Public {
public := make(map[party.ID]*config.Public)
for id, point := range verificationShares {
public[id] = &config.Public{
ECDSA: point,
}
}
return public
}
// ConvertFromLSSConfig converts an LSS config to FROST config format
func ConvertFromLSSConfig(lssConfig *Config) *keygen.Config {
verificationShares := make(map[party.ID]curve.Point)
for id, pub := range lssConfig.Public {
verificationShares[id] = pub.ECDSA
}
// Compute public key from shares
publicKey, _ := lssConfig.PublicPoint()
return &keygen.Config{
ID: lssConfig.ID,
Threshold: lssConfig.Threshold,
PrivateShare: lssConfig.ECDSA,
PublicKey: publicKey,
VerificationShares: party.NewPointMap(verificationShares),
}
}