diff --git a/frost/coordinator.go b/frost/coordinator.go index 0cb0b80..94584de 100644 --- a/frost/coordinator.go +++ b/frost/coordinator.go @@ -2,24 +2,31 @@ package frost import ( "errors" + "fmt" "math/big" ) // Coordinator represents a coordinator of the [FROST] signing protocol. type Coordinator struct { Participant + threshold int + groupSize int } // NewCoordinator creates a new [FROST] Coordinator instance. func NewCoordinator( ciphersuite Ciphersuite, publicKey *Point, + threshold int, + groupSize int, ) *Coordinator { return &Coordinator{ Participant: Participant{ ciphersuite: ciphersuite, publicKey: publicKey, }, + threshold: threshold, + groupSize: groupSize, } } @@ -64,7 +71,32 @@ func (c *Coordinator) Aggregate( // - (R, z), a Schnorr signature consisting of an Element R and // Scalar z. - // TODO: validate the number of signature shares + // MIN_PARTICIPANTS <= NUM_PARTICIPANTS + if len(signatureShares) < c.threshold { + return nil, fmt.Errorf( + "not enough shares; has [%d] for threshold [%d]", + len(signatureShares), + c.threshold, + ) + } + + // NUM_PARTICIPANTS <= MAX_PARTICIPANTS + if len(signatureShares) > c.groupSize { + return nil, fmt.Errorf( + "too many shares; has [%d] for group size [%d]", + len(signatureShares), + c.groupSize, + ) + } + + if len(commitments) != len(signatureShares) { + return nil, fmt.Errorf( + "the number of commitments and signature shares do not match; "+ + "has [%d] commitments and [%d] signature shares", + len(commitments), + len(signatureShares), + ) + } validationErrors, _ := c.validateGroupCommitmentsBase(commitments) if len(validationErrors) != 0 { diff --git a/frost/coordinator_test.go b/frost/coordinator_test.go new file mode 100644 index 0000000..7b0c6e7 --- /dev/null +++ b/frost/coordinator_test.go @@ -0,0 +1,65 @@ +package frost + +import ( + "math/big" + "testing" + + "threshold.network/roast/internal/testutils" +) + +// This test covers failure paths in the Aggregate function. The happy path is +// covered as a part of the roundtrip test in frost_test.go. +func TestAggregate_Failures(t *testing.T) { + message := []byte("For even the very wise cannot see all ends") + + signers := createSigners(t) + publicKey := signers[0].publicKey + + nonces, commitments := executeRound1(t, signers) + signatureShares := executeRound2(t, signers, message, nonces, commitments) + + coordinator := NewCoordinator(ciphersuite, publicKey, threshold, groupSize) + + tests := map[string]struct { + commitments []*NonceCommitment + signatureShares []*big.Int + expectedErr string + }{ + "number of commitments and signature shares do not match": { + commitments: commitments[:groupSize], + signatureShares: signatureShares[:groupSize-1], + expectedErr: "the number of commitments and signature shares do not match; has [100] commitments and [99] signature shares", + }, + "number of commitments and signature shares below threshold": { + commitments: commitments[:threshold-1], + signatureShares: signatureShares[:threshold-1], + expectedErr: "not enough shares; has [50] for threshold [51]", + }, + "number of commitments and signatures above group size": { + commitments: append(commitments, commitments[0]), + signatureShares: append(signatureShares, signatureShares[0]), + expectedErr: "too many shares; has [101] for group size [100]", + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + signature, err := coordinator.Aggregate( + message, + test.commitments, + test.signatureShares, + ) + + testutils.AssertStringsEqual( + t, + "aggregate signature share error message", + test.expectedErr, + err.Error(), + ) + + if signature != nil { + t.Error("expected nil signature") + } + }) + } +} diff --git a/frost/frost_test.go b/frost/frost_test.go index fe37a8c..c3702cb 100644 --- a/frost/frost_test.go +++ b/frost/frost_test.go @@ -51,7 +51,7 @@ func TestFrostRoundtrip(t *testing.T) { nonces, commitments := executeRound1(t, signers) signatureShares := executeRound2(t, signers, message, nonces, commitments) - coordinator := NewCoordinator(ciphersuite, publicKey) + coordinator := NewCoordinator(ciphersuite, publicKey, threshold, groupSize) signature, err := coordinator.Aggregate(message, commitments, signatureShares) if err != nil { t.Fatal(err)