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

194 lines
6.5 KiB
Go

package keygen
import (
"crypto/rand"
"fmt"
"sync"
"github.com/luxfi/threshold/internal/round"
"github.com/luxfi/threshold/internal/types"
"github.com/luxfi/threshold/pkg/hash"
"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"
zksch "github.com/luxfi/threshold/pkg/zk/sch"
)
// This round corresponds with the steps 1-4 of Round 1, Figure 1 in the Frost paper:
//
// https://eprint.iacr.org/2020/852.pdf
type round1 struct {
*round.Base
// taproot indicates whether or not to make taproot compatible keys.
//
// This means taking the necessary steps to ensure that the shared secret generates
// a public key with even y coordinate.
//
// We also end up returning a different result, to accommodate this fact.
taproot bool
// threshold is the integer t which defines the maximum number of corruptions tolerated for this session.
//
// Alternatively, the degree of the polynomial used to share the secret.
//
// Alternatively, t + 1 participants are needed to make a signature.
threshold int
// refresh indicates whether or not we're doing a refresh instead of a key-generation.
refresh bool
// These fields are set to accommodate both key-generation, in which case they'll
// take on identity values, and refresh, in which case their values are meaningful.
// These values should be modifiable.
// privateShare is our previous private share when refreshing, and 0 otherwise.
privateShare curve.Scalar
// verificationShares should hold the previous verification shares when refreshing, and identity points otherwise.
verificationShares map[party.ID]curve.Point
// publicKey should be the previous public key when refreshing, and 0 otherwise.
publicKey curve.Point
}
// VerifyMessage implements round.Round.
//
// Since this is the start of the protocol, we aren't expecting to have received
// any messages yet, so we do nothing.
func (r *round1) VerifyMessage(round.Message) error { return nil }
// StoreMessage implements round.Round.
func (r *round1) StoreMessage(round.Message) error { return nil }
// Finalize implements round.Round.
//
// The overall goal of this round is to generate a secret value, create a polynomial
// sharing of that value, and then send commitments to these values.
func (r *round1) Finalize(out chan<- *round.Message) (round.Session, error) {
group := r.Group()
// These steps come from Figure 1, Round 1 of the Frost paper.
// 1. "Every participant P_i samples t + 1 random values (aᵢ₀, ..., aᵢₜ)) <-$ Z/(q)
// and uses these values as coefficients to define a degree t polynomial
// fᵢ(x) = ∑ⱼ₌₀ᵗ⁻¹ aᵢⱼ xʲ"
//
// Note: I've adjusted the thresholds in this quote to reflect our convention
// that t + 1 participants are needed to create a signature.
// Refresh: Instead of creating a new secret, instead use 0, so that our result doesn't change.
aI0 := group.NewScalar()
aI0TimesG := group.NewPoint()
if !r.refresh {
aI0 = sample.Scalar(rand.Reader, r.Group())
aI0TimesG = aI0.ActOnBase()
}
fI := polynomial.NewPolynomial(r.Group(), r.threshold, aI0)
// 2. "Every Pᵢ computes a proof of knowledge to the corresponding secret aᵢ₀
// by calculating σᵢ = (Rᵢ, μᵢ), such that:
//
// k <-$ Z/(q)
// Rᵢ = k * G
// cᵢ = H(i, ctx, aᵢ₀ • G, Rᵢ)
// μᵢ = k + aᵢ₀ cᵢ
//
// with ctx being a context string to prevent replay attacks"
// We essentially follow this, although the order of hashing ends up being slightly
// different.
// At this point, we've already hashed context inside of helper, so we just
// add in our own ID, and then we're good to go.
// Refresh: Don't create a proof.
var SigmaI *zksch.Proof
if !r.refresh {
SigmaI = zksch.NewProof(r.Base.HashForID(r.SelfID()), aI0TimesG, aI0, nil)
}
// 3. "Every participant Pᵢ computes a public comment Φᵢ = <ϕᵢ₀, ..., ϕᵢₜ>
// where ϕᵢⱼ = aᵢⱼ * G."
//
// Note: I've once again adjusted the threshold indices, I've also taken
// the liberty of renaming "Cᵢ" to "Φᵢ" so that we can later do Phi_i[j]
// for each individual commitment.
// This method conveniently calculates all of that for us
// PhiI = Φᵢ
PhiI := polynomial.NewPolynomialExponent(fI)
// cI is our contribution to the chaining key
// During refresh, we don't need to regenerate the chain key
var cI types.RID
var commitment hash.Commitment
var decommitment hash.Decommitment
var err error
if !r.refresh {
cI, err = types.NewRID(rand.Reader)
if err != nil {
return r, fmt.Errorf("failed to sample ChainKey")
}
// Use session-based hash for commitments - with OUR ID
commitment, decommitment, err = r.Base.HashForID(r.SelfID()).Commit(cI)
if err != nil {
return r, fmt.Errorf("failed to commit to chain key")
}
// Debug: Log commitment details (commented out for production)
// fmt.Printf("[ROUND1] Party %s created commitment: %x (len=%d) for chainkey: %x\n", r.SelfID(), commitment, len(commitment), cI)
} else {
// During refresh, use empty values for chain key
cI = types.EmptyRID()
// Use nil for refresh - they will be properly handled
commitment = nil
decommitment = nil
}
// 4. "Every Pᵢ broadcasts Φᵢ, σᵢ to all other participants
err = r.BroadcastMessage(out, &broadcast2{
PhiI: PhiI,
SigmaI: SigmaI,
Commitment: commitment,
})
if err != nil {
return r, err
}
// Make a defensive copy of the commitment to avoid potential issues with shared slices
var commitmentCopy []byte
if commitment != nil {
commitmentCopy = make([]byte, len(commitment))
copy(commitmentCopy, commitment)
}
// Initialize sync.Maps with initial values
phi := &sync.Map{}
phi.Store(r.SelfID(), PhiI)
chainKeys := &sync.Map{}
chainKeys.Store(r.SelfID(), cI)
chainKeyCommitments := &sync.Map{}
nextRound := &round2{
round1: r,
fI: fI,
ChainKeyDecommitment: decommitment,
}
// Copy values from the maps instead of copying the maps themselves
phi.Range(func(key, value interface{}) bool {
nextRound.Phi.Store(key, value)
return true
})
chainKeys.Range(func(key, value interface{}) bool {
nextRound.ChainKeys.Store(key, value)
return true
})
chainKeyCommitments.Range(func(key, value interface{}) bool {
nextRound.ChainKeyCommitments.Store(key, value)
return true
})
return nextRound, nil
}
// MessageContent implements round.Round.
func (*round1) MessageContent() round.Content { return nil }
// Number implements round.Round.
func (*round1) Number() round.Number { return 1 }