Files
zeekay 7d1d279ff9 fix: resolve race condition in protocol handler channel operations
- Add outClosed atomic bool to track when output channel is closed
- Set outClosed before closing channel to prevent sends on closed channel
- Add safeSend helper with recover() to handle race between check and send
- Update handleError to use safeSend for abort message sending
- Update sendRoundMessage to check outClosed before sending
- Fix sign tests to use 32-byte SHA256 hashes as required

This prevents the "panic: send on closed channel" that could occur when
handleError tried to send after Stop() closed the output channel.
2025-12-24 18:33:17 -08:00

224 lines
5.5 KiB
Go

package sign_test
import (
"crypto/sha256"
"testing"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/pkg/pool"
"github.com/luxfi/threshold/protocols/lss/config"
"github.com/luxfi/threshold/protocols/lss/sign"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// hash32 returns a 32-byte SHA256 hash of the input (required for signing)
func hash32(msg string) []byte {
h := sha256.Sum256([]byte(msg))
return h[:]
}
func TestSignStart(t *testing.T) {
group := curve.Secp256k1{}
// Create a mock config
cfg := &config.Config{
ID: party.ID("alice"),
Group: group,
Threshold: 2,
ECDSA: group.NewScalar(),
Public: map[party.ID]*config.Public{
"alice": {ECDSA: group.NewScalar().ActOnBase()},
"bob": {ECDSA: group.NewScalar().ActOnBase()},
"charlie": {ECDSA: group.NewScalar().ActOnBase()},
},
ChainKey: []byte("test-chain-key"),
RID: []byte("test-rid"),
}
// Use 3 signers so threshold < number of signers
signers := []party.ID{"alice", "bob", "charlie"}
message := hash32("test message") // Must be 32 bytes
pl := pool.NewPool(0)
defer pl.TearDown()
startFunc := sign.Start(cfg, signers, message, pl)
assert.NotNil(t, startFunc)
// Test that the start function creates a session
sessionID := []byte("test-session")
session, err := startFunc(sessionID)
require.NoError(t, err)
assert.NotNil(t, session)
}
func TestSignValidation(t *testing.T) {
group := curve.Secp256k1{}
pl := pool.NewPool(0)
defer pl.TearDown()
// Base config
cfg := &config.Config{
ID: party.ID("alice"),
Group: group,
Threshold: 2,
ECDSA: group.NewScalar(),
Public: map[party.ID]*config.Public{
"alice": {ECDSA: group.NewScalar().ActOnBase()},
"bob": {ECDSA: group.NewScalar().ActOnBase()},
"charlie": {ECDSA: group.NewScalar().ActOnBase()},
},
}
testCases := []struct {
name string
signers []party.ID
message []byte
expectError bool
}{
{
name: "valid signers",
signers: []party.ID{"alice", "bob", "charlie"},
message: hash32("test"),
expectError: false,
},
{
name: "too few signers",
signers: []party.ID{"alice"},
message: hash32("test"),
expectError: true,
},
{
name: "wrong message length",
signers: []party.ID{"alice", "bob", "charlie"},
message: []byte{}, // Not 32 bytes - should error
expectError: true,
},
{
name: "unknown signer",
signers: []party.ID{"alice", "bob", "unknown"},
message: hash32("test"),
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
startFunc := sign.Start(cfg, tc.signers, tc.message, pl)
assert.NotNil(t, startFunc)
// Validation happens when creating the session
session, err := startFunc([]byte("session"))
if tc.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, session)
}
})
}
}
func TestSignMessage(t *testing.T) {
group := curve.Secp256k1{}
// Create configs for multiple parties
configs := make([]*config.Config, 3)
partyIDs := []party.ID{"alice", "bob", "charlie"}
// Generate private keys and public shares
publicShares := make(map[party.ID]*config.Public)
for i, id := range partyIDs {
privKey := group.NewScalar()
// Set to a simple non-zero value
bytes := make([]byte, 32)
bytes[0] = byte(i + 1)
privKey.UnmarshalBinary(bytes)
publicShares[id] = &config.Public{
ECDSA: privKey.ActOnBase(),
}
configs[i] = &config.Config{
ID: id,
Group: group,
Threshold: 2,
ECDSA: privKey,
Public: publicShares,
ChainKey: []byte("chain-key"),
RID: []byte("rid"),
}
}
// Test signing with more than threshold parties
signers := []party.ID{"alice", "bob", "charlie"}
message := hash32("important message") // Must be 32 bytes
pl := pool.NewPool(0)
defer pl.TearDown()
// Create sign protocol for first signer
startFunc := sign.Start(configs[0], signers, message, pl)
assert.NotNil(t, startFunc)
// Try to start the protocol
session, err := startFunc([]byte("session-id"))
require.NoError(t, err)
assert.NotNil(t, session)
}
func TestConcurrentSigning(t *testing.T) {
group := curve.Secp256k1{}
pl := pool.NewPool(0)
defer pl.TearDown()
cfg := &config.Config{
ID: party.ID("alice"),
Group: group,
Threshold: 2,
ECDSA: group.NewScalar(),
Public: map[party.ID]*config.Public{
"alice": {ECDSA: group.NewScalar().ActOnBase()},
"bob": {ECDSA: group.NewScalar().ActOnBase()},
"charlie": {ECDSA: group.NewScalar().ActOnBase()},
},
}
signers := []party.ID{"alice", "bob", "charlie"}
// Start multiple sign sessions concurrently
numSessions := 5
done := make(chan bool, numSessions)
for i := 0; i < numSessions; i++ {
go func(idx int) {
message := hash32(string(rune('a' + idx))) // Must be 32 bytes
startFunc := sign.Start(cfg, signers, message, pl)
if startFunc != nil {
sessionID := []byte(string(rune('0' + idx)))
_, err := startFunc(sessionID)
if err != nil {
// Protocol not fully implemented
done <- false
} else {
done <- true
}
} else {
done <- false
}
}(i)
}
// Wait for all sessions
successCount := 0
for i := 0; i < numSessions; i++ {
if <-done {
successCount++
}
}
// At least one should succeed
assert.Greater(t, successCount, 0, "At least one concurrent session should succeed")
}