Files
zeekay 75be0e607e round: rename Helper to Base — name the value, not the laziness
round.Helper was the canonical anti-pattern: a bare-noun
qualifier-name that told you nothing about what the type does. The
type is the embeddable Session implementation that protocol round
structs anchor on — its VALUE is "the base every round builds on".
Rename it to that.

  type Helper struct { ... }   → type Base struct { ... }
  func (h *Helper) ...         → func (h *Base) ...
  func NewSession(...) (*Helper, error) → (*Base, error)
  Output { *Helper }           → Output { *Base }
  Abort  { *Helper }           → Abort  { *Base }
  round.Helper                 → round.Base   (18 files)
  Helper: helper               → Base: helper (struct literal init)
  r.Helper.X / r.Helper        → r.Base.X / r.Base (field access)

internal/round/helper.go    → base.go
internal/round/helper_test.go → base_test.go

Forward-only — no type alias, no deprecation. The Go FAQ pointer-
receiver consistency landed in the prior commit (f7b1365) so the
method set is already aligned; this commit only renames the type.

Build + vet clean; round / CMP keygen smoke / FROST keygen all
PASS unchanged.
2026-06-01 00:26:33 -07:00

313 lines
8.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lss
import (
"crypto/rand"
"errors"
"fmt"
"github.com/cronokirby/saferith"
"github.com/luxfi/threshold/internal/round"
"github.com/luxfi/threshold/pkg/ecdsa"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/math/polynomial"
"github.com/luxfi/threshold/pkg/math/sample"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/pkg/pool"
"github.com/luxfi/threshold/pkg/protocol"
"github.com/luxfi/threshold/protocols/lss/config"
)
// BlindingProtocol represents the blinding protocol version
type BlindingProtocol int
const (
// BlindingProtocolI is the basic multiplicative blinding
BlindingProtocolI BlindingProtocol = iota
// BlindingProtocolII is enhanced blinding with additional security
BlindingProtocolII
)
// SignWithBlinding performs threshold signing with multiplicative blinding
// This provides enhanced privacy by hiding individual shares during signing
func SignWithBlinding(c *config.Config, signers []party.ID, messageHash []byte, protocol BlindingProtocol, pl *pool.Pool) protocol.StartFunc {
if len(signers) < c.Threshold {
return func(_ []byte) (round.Session, error) {
return nil, fmt.Errorf("lss: insufficient signers: have %d, need %d", len(signers), c.Threshold)
}
}
if len(messageHash) != 32 {
return func(_ []byte) (round.Session, error) {
return nil, errors.New("lss: message hash must be 32 bytes")
}
}
return func(sessionID []byte) (round.Session, error) {
switch protocol {
case BlindingProtocolI:
return startBlindingProtocolI(c, signers, messageHash, sessionID, pl)
case BlindingProtocolII:
return startBlindingProtocolII(c, signers, messageHash, sessionID, pl)
default:
return nil, fmt.Errorf("unknown blinding protocol: %v", protocol)
}
}
}
// blindingRoundI implements Protocol I from the LSS paper
type blindingRoundI struct {
*round.Base
config *config.Config
signers []party.ID
messageHash []byte
pool *pool.Pool
// Blinding factors
alpha curve.Scalar // Random blinding factor
beta curve.Scalar // Random blinding factor
// Blinded shares
blindedShares map[party.ID]curve.Scalar
// Final signature components
r curve.Scalar
s curve.Scalar
}
func startBlindingProtocolI(c *config.Config, signers []party.ID, messageHash []byte, sessionID []byte, pl *pool.Pool) (round.Session, error) {
// Generate random blinding factors
alpha := sample.Scalar(rand.Reader, c.Group)
beta := sample.Scalar(rand.Reader, c.Group)
r := &blindingRoundI{
config: c,
signers: signers,
messageHash: messageHash,
pool: pl,
alpha: alpha,
beta: beta,
blindedShares: make(map[party.ID]curve.Scalar),
}
// Create helper with proper session info
info := round.Info{
ProtocolID: "lss-sign-blinding-I",
FinalRoundNumber: 3,
SelfID: c.ID,
PartyIDs: signers,
Threshold: c.Threshold,
Group: c.Group,
}
helper, err := round.NewSession(info, sessionID, pl)
if err != nil {
return nil, err
}
r.Base = helper
return r, nil
}
// Number implements round.Round
func (r *blindingRoundI) Number() round.Number {
return 1
}
// BroadcastContent implements round.BroadcastRound
func (r *blindingRoundI) BroadcastContent() round.BroadcastContent {
return &blindedShareMessage{}
}
// MessageContent implements round.Round
func (r *blindingRoundI) MessageContent() round.Content {
return nil
}
// blindedShareMessage contains the blinded share for Protocol I
type blindedShareMessage struct {
round.NormalBroadcastContent
BlindedShare curve.Scalar // α * x_i + β
}
// RoundNumber implements round.Content
func (blindedShareMessage) RoundNumber() round.Number {
return 1
}
// VerifyMessage implements round.Round
func (r *blindingRoundI) VerifyMessage(_ round.Message) error {
return nil
}
// StoreMessage implements round.Round
func (r *blindingRoundI) StoreMessage(_ round.Message) error {
return nil
}
// Finalize implements round.Round
func (r *blindingRoundI) Finalize(out chan<- *round.Message) (round.Session, error) {
// Compute our blinded share: α * x_i + β
blindedShare := r.config.Group.NewScalar()
blindedShare = blindedShare.Set(r.config.ECDSA)
blindedShare = blindedShare.Mul(r.alpha)
blindedShare = blindedShare.Add(r.beta)
// Broadcast the blinded share
if err := r.BroadcastMessage(out, &blindedShareMessage{
BlindedShare: blindedShare,
}); err != nil {
return nil, err
}
// Store our own blinded share
r.blindedShares[r.SelfID()] = blindedShare
return &blindingRoundII{
blindingRoundI: r,
}, nil
}
// StoreBroadcastMessage implements round.BroadcastRound
func (r *blindingRoundI) StoreBroadcastMessage(msg round.Message) error {
from := msg.From
body, ok := msg.Content.(*blindedShareMessage)
if !ok || body == nil {
return round.ErrInvalidContent
}
r.blindedShares[from] = body.BlindedShare
return nil
}
// blindingRoundII collects blinded shares and computes signature
type blindingRoundII struct {
*blindingRoundI
}
// Number implements round.Round
func (r *blindingRoundII) Number() round.Number {
return 2
}
// BroadcastContent implements round.BroadcastRound
func (r *blindingRoundII) BroadcastContent() round.BroadcastContent {
return nil
}
// Finalize implements round.Round
func (r *blindingRoundII) Finalize(_ chan<- *round.Message) (round.Session, error) {
// Verify we have enough blinded shares
if len(r.blindedShares) < r.config.Threshold {
return nil, fmt.Errorf("insufficient blinded shares: have %d, need %d",
len(r.blindedShares), r.config.Threshold)
}
// Use first threshold signers for computation
contributingSigners := r.signers[:r.config.Threshold]
// Compute Lagrange coefficients
lagrange := polynomial.Lagrange(r.config.Group, contributingSigners)
// Interpolate the blinded shares to get α * a + β * |L|
// where a is the secret and |L| is the number of Lagrange coefficients
blindedSecret := r.config.Group.NewScalar()
for _, pid := range contributingSigners {
share := r.blindedShares[pid]
if coeff, exists := lagrange[pid]; exists {
contribution := r.config.Group.NewScalar().Set(coeff).Mul(share)
blindedSecret = blindedSecret.Add(contribution)
}
}
// Remove blinding: secret = (blindedSecret - β * t) / α
// where t is the threshold
betaTimesT := r.config.Group.NewScalar().Set(r.beta)
tNat := new(saferith.Nat).SetUint64(uint64(r.config.Threshold))
tScalar := r.config.Group.NewScalar().SetNat(tNat.Mod(tNat, r.config.Group.Order()))
betaTimesT = betaTimesT.Mul(tScalar)
unblindedSecret := r.config.Group.NewScalar()
unblindedSecret = unblindedSecret.Set(blindedSecret)
unblindedSecret = unblindedSecret.Sub(betaTimesT)
// Divide by alpha
alphaInv := r.config.Group.NewScalar().Set(r.alpha).Invert()
unblindedSecret = unblindedSecret.Mul(alphaInv)
// At this point, unblindedSecret should be the original secret a
// Now proceed with standard ECDSA signing using the recovered secret
// For simplicity, we'll create a signature directly
// In practice, this would follow the full ECDSA protocol
k := sample.Scalar(rand.Reader, r.config.Group) // Nonce
R := k.ActOnBase()
// Extract r from R
rBytes, _ := R.MarshalBinary()
r.r = r.config.Group.NewScalar()
_ = r.r.UnmarshalBinary(rBytes[:32])
// Compute s = k^{-1} * (hash + r * secret)
kInv := r.config.Group.NewScalar().Set(k).Invert()
hashNat := new(saferith.Nat).SetBytes(r.messageHash)
hash := r.config.Group.NewScalar()
hash.SetNat(hashNat.Mod(hashNat, r.config.Group.Order())) // Simplified hash conversion
r.s = r.config.Group.NewScalar()
r.s = r.s.Set(r.r)
r.s = r.s.Mul(unblindedSecret)
r.s = r.s.Add(hash)
r.s = r.s.Mul(kInv)
return &blindingRoundIII{
blindingRoundII: r,
}, nil
}
// blindingRoundIII finalizes the signature
type blindingRoundIII struct {
*blindingRoundII
}
// Number implements round.Round
func (r *blindingRoundIII) Number() round.Number {
return 3
}
// Finalize implements round.Round
func (r *blindingRoundIII) Finalize(_ chan<- *round.Message) (round.Session, error) {
// Create the final signature
sig := &ecdsa.Signature{
R: r.r.ActOnBase(),
S: r.s,
}
// Verify the signature
publicKey, err := r.config.PublicPoint()
if err != nil {
return nil, err
}
if !sig.Verify(publicKey, r.messageHash) {
return nil, errors.New("signature verification failed")
}
return r.ResultRound(sig), nil
}
// startBlindingProtocolII implements Protocol II with enhanced security
func startBlindingProtocolII(c *config.Config, signers []party.ID, messageHash []byte, sessionID []byte, pl *pool.Pool) (round.Session, error) {
// Protocol II uses additional commitment rounds and verification
// This provides stronger security guarantees against malicious adversaries
// For now, we'll use Protocol I as a base
// Full Protocol II would add:
// 1. Commitment phase for blinding factors
// 2. Zero-knowledge proofs of correct blinding
// 3. Verification of blinded share consistency
// 4. Additional rounds for enhanced security
return startBlindingProtocolI(c, signers, messageHash, sessionID, pl)
}