diff --git a/frost/bip340_test.go b/frost/bip340_test.go index ea34f20..3149f99 100644 --- a/frost/bip340_test.go +++ b/frost/bip340_test.go @@ -95,7 +95,7 @@ func TestBip340CurveDeserialize(t *testing.T) { serialized := curve.SerializePoint(point) - var tests = map[string]struct { + tests := map[string]struct { input []byte }{ "nil": { @@ -133,7 +133,7 @@ func TestBip340CiphersuiteH1(t *testing.T) { // There are no official test vectors available. Yet, we want to ensure the // function does not panic for empty or nil. We also want to make sure the // happy path works producing a non-zero value. - var tests = map[string]struct { + tests := map[string]struct { m []byte expected string }{ @@ -160,7 +160,7 @@ func TestBip340CiphersuiteH2(t *testing.T) { // There are no official test vectors available. Yet, we want to ensure the // function does not panic for empty or nil. We also want to make sure the // happy path works producing a non-zero value. - var tests = map[string]struct { + tests := map[string]struct { m []byte ms [][]byte expected string @@ -200,7 +200,7 @@ func TestBip340CiphersuiteH3(t *testing.T) { // There are no official test vectors available. Yet, we want to ensure the // function does not panic for empty or nil. We also want to make sure the // happy path works producing a non-zero value. - var tests = map[string]struct { + tests := map[string]struct { m []byte ms [][]byte expected string @@ -239,7 +239,7 @@ func TestBip340CiphersuiteH4(t *testing.T) { // There are no official test vectors available. Yet, we want to ensure the // function does not panic for empty or nil. We also want to make sure the // happy path works producing a non-zero value. - var tests = map[string]struct { + tests := map[string]struct { m []byte expected string }{ @@ -269,7 +269,7 @@ func TestBip340CiphersuiteH5(t *testing.T) { // There are no official test vectors available. Yet, we want to ensure the // function does not panic for empty or nil. We also want to make sure the // happy path works producing a non-zero value. - var tests = map[string]struct { + tests := map[string]struct { m []byte expected string }{ @@ -296,7 +296,7 @@ func TestBip340CiphersuiteH5(t *testing.T) { } func TestBip340CiphersuiteHashToScalar(t *testing.T) { - var tests = map[string]struct { + tests := map[string]struct { tag []byte msg []byte }{ @@ -345,7 +345,7 @@ func TestBip340CiphersuiteHashToScalar(t *testing.T) { } func TestBip340CiphersuiteHash(t *testing.T) { - var tests = map[string]struct { + tests := map[string]struct { tag []byte msg []byte }{ @@ -395,7 +395,7 @@ func TestBip340CiphersuiteHash(t *testing.T) { } func TestConcat(t *testing.T) { - var tests = map[string]struct { + tests := map[string]struct { expected []byte a []byte b [][]byte @@ -453,7 +453,7 @@ func TestConcat(t *testing.T) { } func TestOs2Ip(t *testing.T) { - var tests = map[string]struct { + tests := map[string]struct { expected *big.Int input []byte }{ diff --git a/frost/signer.go b/frost/signer.go index 1300334..624b408 100644 --- a/frost/signer.go +++ b/frost/signer.go @@ -94,22 +94,64 @@ func (s *Signer) generateNonce(secret []byte) (*big.Int, error) { return s.ciphersuite.H3(b, secret), nil } -func (s *Signer) Round2(message []byte, commitments []*NonceCommitment) (*big.Int, error) { - validationErrors := s.validateGroupCommitments(commitments) +// Round2 implements the Round Two - Signature Share Generation phase from +// [FROST], section 5.2 Round Two - Signature Share Generation. +func (s *Signer) Round2( + message []byte, + nonce *Nonce, + commitments []*NonceCommitment, +) (*big.Int, error) { + // TODO: validate number of commitments? + + // participant_list = participants_from_commitment_list(commitment_list) + validationErrors, participants := s.validateGroupCommitments(commitments) if len(validationErrors) != 0 { return nil, errors.Join(validationErrors...) } - return nil, nil // TODO: return signature share + // binding_factor_list = compute_binding_factors(group_public_key, commitment_list, msg) + bindingFactors := s.computeBindingFactors(message, commitments) + // binding_factor = binding_factor_for_participant(binding_factor_list, identifier) + bindingFactor := bindingFactors[s.signerIndex] + + // group_commitment = compute_group_commitment(commitment_list, binding_factor_list) + groupCommitment := s.computeGroupCommitment(commitments, bindingFactors) + + // lambda_i = derive_interpolating_value(participant_list, identifier) + lambda := s.deriveInterpolatingValue(s.signerIndex, participants) + + // challenge = compute_challenge(group_commitment, group_public_key, msg) + challenge := s.computeChallenge(message, groupCommitment) + + bnbf := new(big.Int).Mul(nonce.bindingNonce, bindingFactor) // (binding_nonce * binding_factor) + lski := new(big.Int).Mul(lambda, s.secretKeyShare) // lambda_i * sk_i + lskic := new(big.Int).Mul(lski, challenge) // (lambda_i * sk_i * challenge) + + // sig_share = hiding_nonce + (binding_nonce * binding_factor) + (lambda_i * sk_i * challenge) + sigShare := new(big.Int).Add( + nonce.hidingNonce, + new(big.Int).Add(bnbf, lskic), + ) + + return sigShare, nil } // validateGroupCommitments is a helper function used internally by -// encodeGroupCommitment to validate the group commitments. Two validations are -// done: +// encodeGroupCommitment to validate the group commitments. Four validations +// are done: // - None of the commitments is a point not lying on the curve. // - The list of commitments is sorted in ascending order by signer identifier. -func (s *Signer) validateGroupCommitments(commitments []*NonceCommitment) []error { - // From [FROST]: +// - This signer's commitment is included in the commitments. +// - None of the commitments is nil. +// +// Additionally, the function returns the list of participants if there were no +// validation errors. This way, the function implements +// def participants_from_commitment_list(commitment_list) function from [FROST] +// section 4.3. List Operations. +func (s *Signer) validateGroupCommitments( + commitments []*NonceCommitment, +) ([]error, []uint64) { + // Validations required, as specified in [FROST]: // // 3.1 Prime-Order Group // @@ -129,19 +171,31 @@ func (s *Signer) validateGroupCommitments(commitments []*NonceCommitment) []erro // NonZeroScalar identifier i and two commitment Element values // (hiding_nonce_commitment_i, binding_nonce_commitment_i). This list // MUST be sorted in ascending order by identifier. + + participants := make([]uint64, len(commitments)) var errors []error curve := s.ciphersuite.Curve() + found := false + // we index from 1 so this number will always be lower lastSignerIndex := uint64(0) for i, c := range commitments { + if c == nil { + errors = append( + errors, + fmt.Errorf("commitment at position [%d] is nil", i), + ) + continue + } + if c.signerIndex <= lastSignerIndex { errors = append( errors, fmt.Errorf( "commitments not sorted in ascending order: "+ - "commitments[%v].signerIndex=%v, commitments[%v].signerIndex=%v", + "commitments[%d].signerIndex=%d, commitments[%d].signerIndex=%d", i-1, lastSignerIndex, i, @@ -151,10 +205,15 @@ func (s *Signer) validateGroupCommitments(commitments []*NonceCommitment) []erro } lastSignerIndex = c.signerIndex + participants[i] = c.signerIndex + + if c.signerIndex == s.signerIndex { + found = true + } if !curve.IsPointOnCurve(c.bindingNonceCommitment) { errors = append(errors, fmt.Errorf( - "binding nonce commitment from signer [%v] is not a valid "+ + "binding nonce commitment from signer [%d] is not a valid "+ "non-identity point on the curve: [%s]", c.signerIndex, c.bindingNonceCommitment, @@ -163,7 +222,7 @@ func (s *Signer) validateGroupCommitments(commitments []*NonceCommitment) []erro if !curve.IsPointOnCurve(c.hidingNonceCommitment) { errors = append(errors, fmt.Errorf( - "hiding nonce commitment from signer [%v] is not a valid "+ + "hiding nonce commitment from signer [%d] is not a valid "+ "non-identity point on the curve: [%s]", c.signerIndex, c.hidingNonceCommitment, @@ -171,7 +230,19 @@ func (s *Signer) validateGroupCommitments(commitments []*NonceCommitment) []erro } } - return errors + if !found { + errors = append( + errors, + fmt.Errorf("current signer's commitment not found on the list"), + ) + } + + // return participants only when there were no validation errors + if len(errors) == 0 { + return nil, participants + } + + return errors, nil } // computeBindingFactors implements def compute_binding_factors(group_public_key, @@ -182,8 +253,8 @@ func (s *Signer) validateGroupCommitments(commitments []*NonceCommitment) []erro // commitments have been received and call validateGroupCommitment to validate // the received commitments. func (s *Signer) computeBindingFactors( - commitments []*NonceCommitment, message []byte, + commitments []*NonceCommitment, ) bindingFactors { // From [FROST]: // @@ -369,7 +440,11 @@ func (s *Signer) encodeGroupCommitment(commitments []*NonceCommitment) []byte { // function from [FROST], as defined in section 4.2 Polynomials. // L is the list of the indices of the members of the particular group. // xi is the index of the participant i. -func (s *Signer) deriveInterpolatingValue(xi uint64, L []uint64) (*big.Int, error) { +// +// The function calling deriveInterpolatingValue must ensure a valid number of +// commitments have been received and call validateGroupCommitment to validate +// the received commitments. +func (s *Signer) deriveInterpolatingValue(xi uint64, L []uint64) *big.Int { // From [FROST]: // // 4.2. Polynomials @@ -399,8 +474,9 @@ func (s *Signer) deriveInterpolatingValue(xi uint64, L []uint64) (*big.Int, erro // // def derive_interpolating_value(L, x_i): + // Note that the validation is handled in validateGroupCommitment function. + order := s.ciphersuite.Curve().Order() - found := false // numerator = Scalar(1) num := big.NewInt(1) // denominator = Scalar(1) @@ -408,17 +484,6 @@ func (s *Signer) deriveInterpolatingValue(xi uint64, L []uint64) (*big.Int, erro // for x_j in L: for _, xj := range L { if xj == xi { - // for x_j in L: - // if count(x_j, L) > 1: - // raise "invalid parameters" - if found { - return nil, fmt.Errorf( - "invalid parameters: xi=[%v] present more than one time in L=[%v]", - xi, - L, - ) - } - found = true // if x_j == x_i: continue continue } @@ -430,21 +495,48 @@ func (s *Signer) deriveInterpolatingValue(xi uint64, L []uint64) (*big.Int, erro den.Mod(den, order) } - // if x_i not in L: - // raise "invalid parameters" - if !found { - return nil, fmt.Errorf( - "invalid parameters: xi=[%v] not present in L=[%v]", - xi, - L, - ) - } - // value = numerator / denominator denInv := new(big.Int).ModInverse(den, order) res := new(big.Int).Mul(num, denInv) res = res.Mod(res, order) // return value - return res, nil + return res +} + +// computeChallenge implements def compute_group_commitment(commitment_list, +// binding_factor_list) from [FROST] as defined in section 4.6. Signature +// Challenge Computation. +func (s *Signer) computeChallenge( + message []byte, + groupCommitment *Point, +) *big.Int { + + // From [FROST]: + // + // 4.6. Signature Challenge Computation + // + // This section describes the subroutine for creating the per-message + // challenge. + // + // Inputs: + // - group_commitment, the group commitment, an Element. + // - group_public_key, the public key corresponding to the group signing + // key, an Element. + // - msg, the message to be signed, a byte string. + // + // Outputs: + // - challenge, a Scalar. + // + // def compute_group_commitment(commitment_list, binding_factor_list) + + curve := s.ciphersuite.Curve() + // group_comm_enc = G.SerializeElement(group_commitment) + groupCommitmentEncoded := curve.SerializePoint(groupCommitment) + // group_public_key_enc = G.SerializeElement(group_public_key) + publicKeyEncoded := curve.SerializePoint(s.publicKey) + // challenge_input = group_comm_enc || group_public_key_enc || msg + // challenge = H2(challenge_input) + // return challenge + return s.ciphersuite.H2(groupCommitmentEncoded, publicKeyEncoded, message) } diff --git a/frost/signer_test.go b/frost/signer_test.go index 4321979..b3cc1cb 100644 --- a/frost/signer_test.go +++ b/frost/signer_test.go @@ -3,7 +3,9 @@ package frost import ( "crypto/rand" "encoding/hex" + "fmt" "math/big" + "slices" "testing" "threshold.network/roast/internal/testutils" @@ -15,64 +17,174 @@ var groupSize = 100 func TestRound2_ValidationError(t *testing.T) { // just a basic test checking if Round2 calls validateGroupCommitments signers := createSigners(t) - _, commitments := executeRound1(t, signers) + nonces, commitments := executeRound1(t, signers) commitments[0].bindingNonceCommitment = &Point{big.NewInt(99), big.NewInt(88)} signer := signers[1] + nonce := nonces[1] - _, err := signer.Round2([]byte("dummy"), commitments) + _, err := signer.Round2([]byte("dummy"), nonce, commitments) if err == nil { t.Fatalf("expected a non-nil error") } + // assert if this is indeed a validation error expectedError := "binding nonce commitment from signer [1] is not a valid non-identity point on the curve: [Point[X=0x63, Y=0x58]]" testutils.AssertStringsEqual(t, "validation error", expectedError, err.Error()) } func TestValidateGroupCommitments(t *testing.T) { + // happy path signers := createSigners(t) _, commitments := executeRound1(t, signers) signer := signers[0] - validationErrors := signer.validateGroupCommitments(commitments) + validationErrors, participants := signer.validateGroupCommitments(commitments) testutils.AssertIntsEqual(t, "number of validation errors", 0, len(validationErrors)) + testutils.AssertIntsEqual(t, "number of participants", groupSize, len(participants)) + + for i, p := range participants { + expected := uint64(i + 1) + if p != expected { + testutils.AssertUintsEqual(t, "participant index", expected, p) + } + } } func TestValidateGroupCommitments_Errors(t *testing.T) { - signers := createSigners(t) - _, commitments := executeRound1(t, signers) + tests := map[string]struct { + modifyCommitments func([]*NonceCommitment) []*NonceCommitment + expectedErrors []string + }{ + "nil in the array": { + modifyCommitments: func(commitments []*NonceCommitment) []*NonceCommitment { + commitments[30] = nil + return commitments + }, + expectedErrors: []string{ + "commitment at position [30] is nil", + }, + }, + "commitment from the current signer is missing": { + modifyCommitments: func(commitments []*NonceCommitment) []*NonceCommitment { + // the test uses signers[0] so let remove commitment from this signer + return slices.Delete(commitments, 0, 1) + }, + expectedErrors: []string{ + "current signer's commitment not found on the list", + }, + }, + "duplicate commitment": { + modifyCommitments: func(commitments []*NonceCommitment) []*NonceCommitment { + // duplicate commitment from signer 5 at positions 4 and 5 + commitments[5] = commitments[4] + return commitments + }, + expectedErrors: []string{ + "commitments not sorted in ascending order: commitments[4].signerIndex=5, commitments[5].signerIndex=5", + }, + }, + "commitments in invalid order": { + modifyCommitments: func(commitments []*NonceCommitment) []*NonceCommitment { + // at the position where we'd expect a commitment from signer 32 we have + // a commitment from signer 51 + tmp := commitments[31] + commitments[31] = commitments[50] + // at the position where we'd expect a commitment from signer 51 we have + // a commitment from signer 32 + commitments[50] = tmp + return commitments + }, + expectedErrors: []string{ + "commitments not sorted in ascending order: commitments[31].signerIndex=51, commitments[32].signerIndex=33", + "commitments not sorted in ascending order: commitments[49].signerIndex=50, commitments[50].signerIndex=32", + }, + }, + "invalid binding nonce commitment": { + modifyCommitments: func(commitments []*NonceCommitment) []*NonceCommitment { + // binding nonce commitment for signer 81 is an invalid curve point + commitments[80].bindingNonceCommitment = &Point{big.NewInt(100), big.NewInt(200)} + return commitments + }, + expectedErrors: []string{ + "binding nonce commitment from signer [81] is not a valid non-identity point on the curve: [Point[X=0x64, Y=0xc8]]", + }, + }, + "invalid hiding nonce commitment": { + modifyCommitments: func(commitments []*NonceCommitment) []*NonceCommitment { + // hiding nonce commitment for signer 100 is an invalid curve point + commitments[99].hidingNonceCommitment = &Point{big.NewInt(300), big.NewInt(400)} + return commitments + }, + expectedErrors: []string{ + "hiding nonce commitment from signer [100] is not a valid non-identity point on the curve: [Point[X=0x12c, Y=0x190]]", + }, + }, + "multiple problems": { + modifyCommitments: func(commitments []*NonceCommitment) []*NonceCommitment { + // the test uses signers[0] so let remove commitment from this signer + modified := slices.Delete(commitments, 0, 1) + // duplicate commitment from signer 6 at positions 4 and 5 + modified[5] = modified[4] + // at the position where we'd expect a commitment from signer 33 we have + // a commitment from signer 52 + tmp := modified[31] + modified[31] = modified[50] + // at the position where we'd expect a commitment from signer 52 we have + // a commitment from signer 33 + modified[50] = tmp + // binding nonce commitment for signer 82 is an invalid curve point + modified[80].bindingNonceCommitment = &Point{big.NewInt(100), big.NewInt(200)} + // hiding nonce commitment for signer 100 is an invalid curve point + modified[98].hidingNonceCommitment = &Point{big.NewInt(300), big.NewInt(400)} + // finally, we'll set the nil commitment at position 97 where we would + // expect a commitment from signer 99 + modified[97] = nil + return modified + }, + expectedErrors: []string{ + "commitments not sorted in ascending order: commitments[4].signerIndex=6, commitments[5].signerIndex=6", + "commitments not sorted in ascending order: commitments[31].signerIndex=52, commitments[32].signerIndex=34", + "commitments not sorted in ascending order: commitments[49].signerIndex=51, commitments[50].signerIndex=33", + "binding nonce commitment from signer [82] is not a valid non-identity point on the curve: [Point[X=0x64, Y=0xc8]]", + "commitment at position [97] is nil", + "hiding nonce commitment from signer [100] is not a valid non-identity point on the curve: [Point[X=0x12c, Y=0x190]]", + "current signer's commitment not found on the list", + }, + }, + } - // duplicate commitment from signer 5 at positions 4 and 5 - commitments[5] = commitments[4] - // at the position where we'd expect a commitment from signer 32 we have - // a commitment from signer 51 - tmp := commitments[31] - commitments[31] = commitments[50] - // at the position where we'd expect a commitment from signer 51 we have - // a commitment from signer 32 - commitments[50] = tmp - // binding nonce commitment for signer 81 is an invalid curve point - commitments[80].bindingNonceCommitment = &Point{big.NewInt(100), big.NewInt(200)} - // hiding nonce commitment for signer 100 is an invalid curve point - commitments[99].hidingNonceCommitment = &Point{big.NewInt(300), big.NewInt(400)} + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + signers := createSigners(t) + _, commitments := executeRound1(t, signers) + signer := signers[0] - signer := signers[0] + modified := test.modifyCommitments(commitments) + validationErrors, participants := signer.validateGroupCommitments(modified) - validationErrors := signer.validateGroupCommitments(commitments) + if participants != nil { + t.Fatalf("expected nil participants list, has [%v]", participants) + } - expectedError1 := "commitments not sorted in ascending order: commitments[4].signerIndex=5, commitments[5].signerIndex=5" - expectedError2 := "commitments not sorted in ascending order: commitments[31].signerIndex=51, commitments[32].signerIndex=33" - expectedError3 := "commitments not sorted in ascending order: commitments[49].signerIndex=50, commitments[50].signerIndex=32" - expectedError4 := "binding nonce commitment from signer [81] is not a valid non-identity point on the curve: [Point[X=0x64, Y=0xc8]]" - expectedError5 := "hiding nonce commitment from signer [100] is not a valid non-identity point on the curve: [Point[X=0x12c, Y=0x190]]" + testutils.AssertIntsEqual( + t, + "number of validation errors", + len(test.expectedErrors), + len(validationErrors), + ) - testutils.AssertIntsEqual(t, "number of validation errors", 5, len(validationErrors)) - testutils.AssertStringsEqual(t, "validation error #1", expectedError1, validationErrors[0].Error()) - testutils.AssertStringsEqual(t, "validation error #2", expectedError2, validationErrors[1].Error()) - testutils.AssertStringsEqual(t, "validation error #3", expectedError3, validationErrors[2].Error()) - testutils.AssertStringsEqual(t, "validation error #4", expectedError4, validationErrors[3].Error()) - testutils.AssertStringsEqual(t, "validation error #5", expectedError5, validationErrors[4].Error()) + for i, expectedError := range test.expectedErrors { + testutils.AssertStringsEqual( + t, + fmt.Sprintf("validation error #%d", i), + expectedError, + validationErrors[i].Error(), + ) + } + }) + } } func TestEncodeGroupCommitments(t *testing.T) { @@ -187,10 +299,7 @@ func TestDeriveInterpolatingValue(t *testing.T) { signer := createSigners(t)[0] for testName, test := range tests { t.Run(testName, func(t *testing.T) { - result, err := signer.deriveInterpolatingValue(test.xi, test.L) - if err != nil { - t.Fatal(err) - } + result := signer.deriveInterpolatingValue(test.xi, test.L) testutils.AssertStringsEqual( t, "interpolating value", @@ -201,41 +310,6 @@ func TestDeriveInterpolatingValue(t *testing.T) { } } -func TestDeriveInterpolatingValue_InvalidParameters(t *testing.T) { - var tests = map[string]struct { - xi uint64 - L []uint64 - expectedErr string - }{ - "xi present more than one time in L": { - xi: 5, - L: []uint64{1, 4, 5, 5}, - expectedErr: "invalid parameters: xi=[5] present more than one time in L=[[1 4 5 5]]", - }, - "xi not present in L": { - xi: 3, - L: []uint64{1, 4, 5}, - expectedErr: "invalid parameters: xi=[3] not present in L=[[1 4 5]]", - }, - } - - signer := createSigners(t)[0] - for testName, test := range tests { - t.Run(testName, func(t *testing.T) { - _, err := signer.deriveInterpolatingValue(test.xi, test.L) - if err == nil { - t.Fatalf("expected a non-nil error") - } - testutils.AssertStringsEqual( - t, - "parameters error", - test.expectedErr, - err.Error(), - ) - }) - } -} - func createSigners(t *testing.T) []*Signer { var signers []*Signer diff --git a/internal/testutils/testutils.go b/internal/testutils/testutils.go index 6bf0b04..5cafd82 100644 --- a/internal/testutils/testutils.go +++ b/internal/testutils/testutils.go @@ -27,6 +27,19 @@ func AssertBigIntsEqual(t *testing.T, description string, expected *big.Int, act } } +// AssertUintsEqual checks if two unsigned integers are equal. If not, it +// reports a test failure. +func AssertUintsEqual(t *testing.T, description string, expected uint64, actual uint64) { + if expected != actual { + t.Errorf( + "unexpected %s\nexpected: %v\nactual: %v\n", + description, + expected, + actual, + ) + } +} + // AssertIntsEqual checks if two integers are equal. If not, it reports a test // failure. func AssertIntsEqual(t *testing.T, description string, expected int, actual int) {