Files
zeekay a3ba7d96f5 fix: apply golangci-lint auto-fixes
- Remove unnecessary trailing newlines
- Fix comment formatting
- Adjust indentation and spacing
- Remove unused nolint directives
2025-08-15 21:45:17 -05:00

334 lines
10 KiB
Go

// Package lss provides dynamic resharing extensions for CMP and FROST protocols.
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/cmp/config"
)
// CMP extends the CMP protocol with LSS dynamic resharing capabilities.
// This allows CMP to perform membership changes without reconstructing the master key.
type CMP struct {
config *config.Config
generation uint64
pool *pool.Pool
}
// NewLSSCMP creates a new LSS-extended CMP instance
func NewLSSCMP(cmpConfig *config.Config, pool *pool.Pool) *CMP {
return &CMP{
config: cmpConfig,
generation: 0,
pool: pool,
}
}
// DynamicReshare performs the LSS dynamic resharing protocol on CMP 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 DynamicReshareCMP(
oldConfigs map[party.ID]*config.Config,
newPartyIDs []party.ID,
newThreshold int,
_ *pool.Pool,
) (map[party.ID]*config.Config, error) {
if len(oldConfigs) == 0 {
return nil, errors.New("lss-cmp: no old configurations provided")
}
if newThreshold < 1 || newThreshold > len(newPartyIDs) {
return nil, fmt.Errorf("lss-cmp: invalid threshold %d for %d parties", newThreshold, len(newPartyIDs))
}
// Get reference config and validate consistency
var refConfig *config.Config
var group curve.Curve
oldPartyIDs := make([]party.ID, 0, len(oldConfigs))
for pid, cfg := range oldConfigs {
if refConfig == nil {
refConfig = cfg
group = cfg.Group
} else {
// Verify all configs are from the same keygen
if !cfg.PublicPoint().Equal(refConfig.PublicPoint()) {
return nil, errors.New("lss-cmp: 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-cmp: 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.ECDSA).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)
// Invert returns the inverted scalar, not an error
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]*config.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 ECDSA share: a'_j = (a * w) * q_j * z_j
newECDSAShare := group.NewScalar().Set(aTimesW)
newECDSAShare.Mul(qShare).Mul(zShare)
newShares[pid] = newECDSAShare
}
// Now create configs with all the public information
for _, pid := range newPartyIDs {
newConfig := &config.Config{
Group: group,
ID: pid,
Threshold: newThreshold,
ECDSA: newShares[pid],
// For now, reuse auxiliary values from reference config
// In production, these should be refreshed independently
ElGamal: refConfig.ElGamal,
Paillier: refConfig.Paillier,
RID: refConfig.RID,
ChainKey: refConfig.ChainKey,
Public: make(map[party.ID]*config.Public),
}
// Store public key shares for all new parties
for _, otherPID := range newPartyIDs {
// Get the actual public values from the reference config if available
var publicInfo *config.Public
if refPublic, exists := refConfig.Public[refConfig.ID]; exists {
publicInfo = &config.Public{
ECDSA: newShares[otherPID].ActOnBase(),
ElGamal: refPublic.ElGamal, // Temporary reuse
Paillier: refPublic.Paillier, // Temporary reuse
Pedersen: refPublic.Pedersen, // Temporary reuse
}
} else {
// Create minimal public info
publicInfo = &config.Public{
ECDSA: newShares[otherPID].ActOnBase(),
ElGamal: refConfig.ElGamal.ActOnBase(),
Paillier: refConfig.Paillier.PublicKey,
Pedersen: nil,
}
}
newConfig.Public[otherPID] = publicInfo
}
newConfigs[pid] = newConfig
}
// Verify the resharing was correct
if err := verifyResharingCMP(oldConfigs, newConfigs, refConfig.Threshold, newThreshold); err != nil {
return nil, fmt.Errorf("lss-cmp: resharing verification failed: %w", err)
}
return newConfigs, nil
}
// verifyResharingCMP validates that new shares correctly reconstruct the original public key
func verifyResharingCMP(
oldConfigs map[party.ID]*config.Config,
newConfigs map[party.ID]*config.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.PublicPoint()
group = cfg.Group
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 public share for this party
publicShare := cfg.ECDSA.ActOnBase()
// Apply Lagrange coefficient
if coeff, exists := lagrange[pid]; exists {
contribution := coeff.Act(publicShare)
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 performs CMP signing with the current configuration
func (c *CMP) Sign(_ []party.ID, _ []byte) ([]byte, error) {
// CMP.Sign returns a protocol.StartFunc, we need to execute it
// In a real implementation, this would run the protocol
// For now, return a placeholder
return nil, errors.New("sign execution not implemented - use cmp.Sign directly")
}
// Refresh performs a proactive refresh of shares without changing membership
func (c *CMP) Refresh() (*config.Config, error) {
// CMP's Refresh returns a protocol.StartFunc
// For this implementation, we'll just increment generation
// In practice, you'd execute the refresh protocol
c.generation++
return c.config, nil
}
// GetGeneration returns the current resharing generation number
func (c *CMP) GetGeneration() uint64 {
return c.generation
}
// GetConfig returns the current CMP configuration
func (c *CMP) GetConfig() *config.Config {
return c.config
}
// UpdateConfig updates the configuration after a successful resharing
func (c *CMP) UpdateConfig(newConfig *config.Config) {
c.config = newConfig
c.generation++
}