Files
threshold/protocols/lss/keygen/round2.go
T
Hanzo AI 9bce6722b4 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

179 lines
4.3 KiB
Go

package keygen
import (
"bytes"
"errors"
"sync"
"github.com/luxfi/threshold/internal/round"
"github.com/luxfi/threshold/internal/types"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/math/polynomial"
"github.com/luxfi/threshold/pkg/party"
)
// round2 receives commitments and sends shares
type round2 struct {
*round.Helper
// Our polynomial from round 1
poly *polynomial.Polynomial
// Commitments from all parties: commitments[i][j] = g^f_i(j)
commitments map[party.ID]map[party.ID]curve.Point
// Chain keys from all parties
chainKeys map[party.ID]types.RID
// Shares we receive - using sync.Map for thread safety
shares sync.Map // map[party.ID]curve.Scalar
}
// message2 contains the secret share for a party
type message2 struct {
// Share encoded as binary for CBOR compatibility
Share []byte
}
// Round2 doesn't broadcast, so we don't implement BroadcastContent
// This ensures round2 doesn't implement the BroadcastRound interface
// Number implements round.Round
func (r *round2) Number() round.Number {
return 2
}
// MessageContent implements round.Round
func (r *round2) MessageContent() round.Content {
return &message2{}
}
// RoundNumber implements round.Content
func (message2) RoundNumber() round.Number {
return 2
}
// VerifyMessage implements round.Round
func (r *round2) VerifyMessage(msg round.Message) error {
from, to := msg.From, msg.To
body, ok := msg.Content.(*message2)
if !ok || body == nil {
return round.ErrInvalidContent
}
if to != r.SelfID() {
return errors.New("message not for us")
}
// Unmarshal the share
share := r.Group().NewScalar()
if err := share.UnmarshalBinary(body.Share); err != nil {
return errors.New("invalid share encoding")
}
// Verify share against commitment
commitments, ok := r.commitments[from]
if !ok {
return errors.New("missing commitments from sender")
}
// Check g^share = commitment[to]
expectedCommitment, ok := commitments[to]
if !ok {
return errors.New("missing commitment for our ID")
}
sharePoint := share.ActOnBase()
// Use MarshalBinary for comparison to avoid race in dcrd/secp256k1 ToAffine
spBytes, _ := sharePoint.MarshalBinary()
ecBytes, _ := expectedCommitment.MarshalBinary()
if !bytes.Equal(spBytes, ecBytes) {
return errors.New("share doesn't match commitment")
}
return nil
}
// StoreMessage implements round.Round
func (r *round2) StoreMessage(msg round.Message) error {
from := msg.From
body := msg.Content.(*message2)
// Unmarshal the share
share := r.Group().NewScalar()
if err := share.UnmarshalBinary(body.Share); err != nil {
return errors.New("invalid share encoding")
}
// Store using sync.Map for thread safety
r.shares.Store(from, share)
return nil
}
// Finalize implements round.Round
func (r *round2) Finalize(out chan<- *round.Message) (round.Session, error) {
// Check if we've already sent shares
_, hasSelfShare := r.shares.Load(r.SelfID())
// First time: Send shares to each party
if !hasSelfShare {
for _, id := range r.OtherPartyIDs() {
x := id.Scalar(r.Group())
share := r.poly.Evaluate(x)
// Marshal the share for CBOR
shareBytes, err := share.MarshalBinary()
if err != nil {
return nil, errors.New("failed to marshal share")
}
if err := r.SendMessage(out, &message2{
Share: shareBytes,
}, id); err != nil {
return nil, err
}
}
// Our own share
ownX := r.SelfID().Scalar(r.Group())
r.shares.Store(r.SelfID(), r.poly.Evaluate(ownX))
// Return self to wait for incoming shares
return r, nil
}
// Count the number of shares we have
shareCount := 0
r.shares.Range(func(_, _ interface{}) bool {
shareCount++
return true
})
// Check if we have all shares before advancing
if shareCount < r.N() {
// Still waiting for shares
return r, nil
}
// Convert sync.Map back to regular map for round3
shares := make(map[party.ID]curve.Scalar)
r.shares.Range(func(key, value interface{}) bool {
id := key.(party.ID)
shares[id] = value.(curve.Scalar)
return true
})
// We have all shares, advance to round3
return &round3{
Helper: r.Helper,
commitments: r.commitments,
chainKeys: r.chainKeys,
shares: shares,
}, nil
}
// Note: Round2 processes the broadcasts that were sent in round1.
// The broadcasts are already stored in the handler and passed to round2
// when it's created.