mirror of
https://github.com/luxfi/threshold.git
synced 2026-10-30 15:15:53 +00:00
TestCMPBasicKeygen and TestCMPMinimalKeygen were panicking under -race
with "send on closed channel" because pool.TearDown closed p.commands
while Parallelize was mid-send. The previous code papered over this with
a defer/recover() in Search/Parallelize, but the race detector still
flagged the underlying read-vs-close on the channel field. Replaced the
panic-and-recover hack with a sync.RWMutex (inFlight): senders hold
RLock for the duration of the send loop, TearDown takes the write lock
so it drains every in-flight caller before close(p.commands). Same
pattern applied to Handler's outClosed/close(h.out) sites and safeSend,
which previously check-then-acted with an atomic.Bool — race-prone.
Additionally TestCMPBasicKeygen tripped a saferith.Nat.Cmp race on
shared Pedersen parameters when round4.VerifyMessage and round4.Finalize
ran concurrently in the handler. saferith's Cmp mutates limb storage
even for equal values, so concurrent reads on the same Modulus race.
Added Handler.roundMu serializing VerifyMessage/StoreMessage/
StoreBroadcastMessage/Finalize on the live round.Session — same pattern
in internal/test/round.go for the test harness path used by cmp/sign.
Also tightened pool.Search/Parallelize signal accounting so the caller
receives exactly count signals (one per result write), giving a clean
happens-before edge with every results[i] write — previously the
"for ctr > 0" wait loop could exit before workers had finished writing.
Drive-by: stripped debug fmt.Printf calls from pkg/math/sample/prime.go
and protocols/cmp/keygen/round1.go that had leaked in from a prior
debugging session.
Verification:
go test -race -count=10 -timeout 1200s -run "TestCMPMinimalKeygen|TestCMPBasicKeygen" ./protocols/cmp/
ok 639s
go test -race -count=10 -timeout 600s ./protocols/lss/...
ok (all sub-packages PASS, 200s for ./protocols/lss alone)
go test -race -count=10 -timeout 60s ./pkg/pool/
ok 7s
174 lines
4.2 KiB
Go
174 lines
4.2 KiB
Go
package test
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
"github.com/luxfi/threshold/internal/round"
|
|
"github.com/luxfi/threshold/pkg/party"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// Rule describes various hooks that can be applied to a protocol execution.
|
|
type Rule interface {
|
|
// ModifyBefore modifies r before r.Finalize() is called.
|
|
ModifyBefore(r round.Session)
|
|
// ModifyAfter modifies rNext, which is the round returned by r.Finalize().
|
|
ModifyAfter(rNext round.Session)
|
|
// ModifyContent modifies content for the message that is delivered in rNext.
|
|
ModifyContent(rNext round.Session, to party.ID, content round.Content)
|
|
}
|
|
|
|
func Rounds(rounds []round.Session, rule Rule) (error, bool) {
|
|
var (
|
|
err error
|
|
roundType reflect.Type
|
|
errGroup errgroup.Group
|
|
N = len(rounds)
|
|
out = make(chan *round.Message, N*(N+1))
|
|
mu sync.Mutex
|
|
)
|
|
// roundMu serializes VerifyMessage / StoreMessage / StoreBroadcastMessage
|
|
// calls so concurrent goroutines verifying messages for different parties
|
|
// don't race on shared cryptographic state (configs share a common Public
|
|
// map whose Pedersen parameters use saferith.Nat.Cmp — and Cmp mutates
|
|
// limb storage even on equal values).
|
|
var roundMu sync.Mutex
|
|
|
|
if _, err = checkAllRoundsSame(rounds); err != nil {
|
|
return err, false
|
|
}
|
|
// get the second set of messages
|
|
for id := range rounds {
|
|
idx := id
|
|
r := rounds[idx]
|
|
errGroup.Go(func() error {
|
|
var rNew, rNewReal round.Session
|
|
var finalizeErr error
|
|
roundMu.Lock()
|
|
if rule != nil {
|
|
rReal := getRound(r)
|
|
rule.ModifyBefore(rReal)
|
|
outFake := make(chan *round.Message, N+1)
|
|
rNew, finalizeErr = r.Finalize(outFake)
|
|
close(outFake)
|
|
rNewReal = getRound(rNew)
|
|
rule.ModifyAfter(rNewReal)
|
|
for msg := range outFake {
|
|
rule.ModifyContent(rNewReal, msg.To, getContent(msg.Content))
|
|
out <- msg
|
|
}
|
|
} else {
|
|
rNew, finalizeErr = r.Finalize(out)
|
|
}
|
|
roundMu.Unlock()
|
|
|
|
if finalizeErr != nil {
|
|
return finalizeErr
|
|
}
|
|
|
|
if rNew != nil {
|
|
mu.Lock()
|
|
rounds[idx] = rNew
|
|
mu.Unlock()
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
if err = errGroup.Wait(); err != nil {
|
|
return err, false
|
|
}
|
|
close(out)
|
|
|
|
// Check that all rounds are the same type
|
|
if roundType, err = checkAllRoundsSame(rounds); err != nil {
|
|
return err, false
|
|
}
|
|
if roundType.String() == reflect.TypeOf(&round.Output{}).String() {
|
|
return nil, true
|
|
}
|
|
if roundType.String() == reflect.TypeOf(&round.Abort{}).String() {
|
|
return nil, true
|
|
}
|
|
|
|
for msg := range out {
|
|
msgBytes, err := cbor.Marshal(msg.Content)
|
|
if err != nil {
|
|
return err, false
|
|
}
|
|
for _, r := range rounds {
|
|
m := *msg
|
|
r := r
|
|
if msg.From == r.SelfID() || msg.Content.RoundNumber() != r.Number() {
|
|
continue
|
|
}
|
|
msgBytesCopy := make([]byte, len(msgBytes))
|
|
copy(msgBytesCopy, msgBytes)
|
|
errGroup.Go(func() error {
|
|
roundMu.Lock()
|
|
defer roundMu.Unlock()
|
|
if m.Broadcast {
|
|
b, ok := r.(round.BroadcastRound)
|
|
if !ok {
|
|
return errors.New("broadcast message but not broadcast round")
|
|
}
|
|
m.Content = b.BroadcastContent()
|
|
if unmarshalErr := cbor.Unmarshal(msgBytesCopy, m.Content); unmarshalErr != nil {
|
|
return unmarshalErr
|
|
}
|
|
|
|
if storeErr := b.StoreBroadcastMessage(m); storeErr != nil {
|
|
return storeErr
|
|
}
|
|
} else {
|
|
m.Content = r.MessageContent()
|
|
if unmarshalErr := cbor.Unmarshal(msgBytesCopy, m.Content); unmarshalErr != nil {
|
|
return unmarshalErr
|
|
}
|
|
|
|
if m.To == "" || m.To == r.SelfID() {
|
|
if verifyErr := r.VerifyMessage(m); verifyErr != nil {
|
|
return verifyErr
|
|
}
|
|
if storeErr := r.StoreMessage(m); storeErr != nil {
|
|
return storeErr
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
if err = errGroup.Wait(); err != nil {
|
|
return err, false
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func checkAllRoundsSame(rounds []round.Session) (reflect.Type, error) {
|
|
var t reflect.Type
|
|
for _, r := range rounds {
|
|
rReal := getRound(r)
|
|
t2 := reflect.TypeOf(rReal)
|
|
if t == nil {
|
|
t = t2
|
|
} else if t != t2 {
|
|
return t, fmt.Errorf("two different rounds: %s %s", t, t2)
|
|
}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func getRound(outerRound round.Session) round.Session {
|
|
return outerRound
|
|
}
|
|
|
|
func getContent(outerContent round.Content) round.Content {
|
|
return outerContent
|
|
}
|