mirror of
https://github.com/luxfi/threshold.git
synced 2026-10-30 15:15:53 +00:00
- Remove unnecessary trailing newlines - Fix comment formatting - Adjust indentation and spacing - Remove unused nolint directives
334 lines
10 KiB
Go
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++
|
|
}
|