From 4e55273815ee5d33969fc371742dc0e43cd30bd5 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 19 Oct 2022 19:07:21 -0700 Subject: [PATCH] btcec/schnorr/musig2: update key agg test vectors to musig2 1.0.0 --- .../schnorr/musig2/data/key_agg_vectors.json | 88 ++++++ .../schnorr/musig2/data/key_sort_vectors.json | 16 ++ btcec/schnorr/musig2/keys_test.go | 257 ++++++++++++++++++ btcec/schnorr/musig2/musig2_test.go | 186 ------------- 4 files changed, 361 insertions(+), 186 deletions(-) create mode 100644 btcec/schnorr/musig2/data/key_agg_vectors.json create mode 100644 btcec/schnorr/musig2/data/key_sort_vectors.json create mode 100644 btcec/schnorr/musig2/keys_test.go diff --git a/btcec/schnorr/musig2/data/key_agg_vectors.json b/btcec/schnorr/musig2/data/key_agg_vectors.json new file mode 100644 index 00000000..b2e623de --- /dev/null +++ b/btcec/schnorr/musig2/data/key_agg_vectors.json @@ -0,0 +1,88 @@ +{ + "pubkeys": [ + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "020000000000000000000000000000000000000000000000000000000000000005", + "02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "04F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" + ], + "tweaks": [ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "252E4BD67410A76CDF933D30EAA1608214037F1B105A013ECCD3C5C184A6110B" + ], + "valid_test_cases": [ + { + "key_indices": [0, 1, 2], + "expected": "90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C" + }, + { + "key_indices": [2, 1, 0], + "expected": "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B" + }, + { + "key_indices": [0, 0, 0], + "expected": "B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935" + }, + { + "key_indices": [0, 0, 1, 1], + "expected": "69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E" + } + ], + "error_test_cases": [ + { + "key_indices": [0, 3], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 1, + "contrib": "pubkey" + }, + "comment": "Invalid public key" + }, + { + "key_indices": [0, 4], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 1, + "contrib": "pubkey" + }, + "comment": "Public key exceeds field size" + }, + { + "key_indices": [5, 0], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubkey" + }, + "comment": "First byte of public key is not 2 or 3" + }, + { + "key_indices": [0, 1], + "tweak_indices": [0], + "is_xonly": [true], + "error": { + "type": "value", + "message": "The tweak must be less than n." + }, + "comment": "Tweak is out of range" + }, + { + "key_indices": [6], + "tweak_indices": [1], + "is_xonly": [false], + "error": { + "type": "value", + "message": "The result of tweaking cannot be infinity." + }, + "comment": "Intermediate tweaking result is point at infinity" + } + ] +} diff --git a/btcec/schnorr/musig2/data/key_sort_vectors.json b/btcec/schnorr/musig2/data/key_sort_vectors.json new file mode 100644 index 00000000..022f3417 --- /dev/null +++ b/btcec/schnorr/musig2/data/key_sort_vectors.json @@ -0,0 +1,16 @@ +{ + "pubkeys": [ + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8" + ], + "sorted_pubkeys": [ + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" + ] +} diff --git a/btcec/schnorr/musig2/keys_test.go b/btcec/schnorr/musig2/keys_test.go new file mode 100644 index 00000000..f9a6e056 --- /dev/null +++ b/btcec/schnorr/musig2/keys_test.go @@ -0,0 +1,257 @@ +// Copyright 2013-2022 The btcsuite developers + +package musig2 + +import ( + "encoding/json" + "fmt" + "os" + "path" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/stretchr/testify/require" +) + +const ( + keySortTestVectorFileName = "key_sort_vectors.json" + + keyAggTestVectorFileName = "key_agg_vectors.json" +) + +type keySortTestVector struct { + PubKeys []string `json:"pubkeys"` + + SortedKeys []string `json:"sorted_pubkeys"` +} + +// TestMusig2KeySort tests that keys are properly sorted according to the +// musig2 test vectors. +func TestMusig2KeySort(t *testing.T) { + t.Parallel() + + testVectorPath := path.Join( + testVectorBaseDir, keySortTestVectorFileName, + ) + testVectorBytes, err := os.ReadFile(testVectorPath) + require.NoError(t, err) + + var testCase keySortTestVector + require.NoError(t, json.Unmarshal(testVectorBytes, &testCase)) + + keys := make([]*btcec.PublicKey, len(testCase.PubKeys)) + for i, keyStr := range testCase.PubKeys { + pubKey, err := btcec.ParsePubKey(mustParseHex(keyStr)) + require.NoError(t, err) + + keys[i] = pubKey + } + + sortedKeys := sortKeys(keys) + + expectedKeys := make([]*btcec.PublicKey, len(testCase.PubKeys)) + for i, keyStr := range testCase.SortedKeys { + pubKey, err := btcec.ParsePubKey(mustParseHex(keyStr)) + require.NoError(t, err) + + expectedKeys[i] = pubKey + } + + require.Equal(t, sortedKeys, expectedKeys) +} + +type keyAggValidTest struct { + Indices []int `json:"key_indices"` + Expected string `json:"expected"` +} + +type keyAggError struct { + Type string `json:"type"` + Signer int `json:"signer"` + Contring string `json:"contrib"` +} + +type keyAggInvalidTest struct { + Indices []int `json:"key_indices"` + + TweakIndices []int `json:"tweak_indices"` + + IsXOnly []bool `json:"is_xonly"` + + Comment string `json:"comment"` +} + +type keyAggTestVectors struct { + PubKeys []string `json:"pubkeys"` + + Tweaks []string `json:"tweaks"` + + ValidCases []keyAggValidTest `json:"valid_test_cases"` + + InvalidCases []keyAggInvalidTest `json:"error_test_cases"` +} + +func keysFromIndices(t *testing.T, indices []int, + pubKeys []string) ([]*btcec.PublicKey, error) { + + t.Helper() + + inputKeys := make([]*btcec.PublicKey, len(indices)) + for i, keyIdx := range indices { + var err error + inputKeys[i], err = btcec.ParsePubKey( + mustParseHex(pubKeys[keyIdx]), + ) + if err != nil { + return nil, err + } + } + + return inputKeys, nil +} + +func tweaksFromIndices(t *testing.T, indices []int, + tweaks []string, isXonly bool) []KeyTweakDesc { + + t.Helper() + + testTweaks := make([]KeyTweakDesc, len(indices)) + for i, idx := range indices { + var rawTweak [32]byte + copy(rawTweak[:], mustParseHex(tweaks[idx])) + + testTweaks[i] = KeyTweakDesc{ + Tweak: rawTweak, + IsXOnly: isXonly, + } + } + + return testTweaks +} + +// TestMuSig2KeyAggTestVectors tests that this implementation of musig2 key +// aggregation lines up with the secp256k1-zkp test vectors. +func TestMuSig2KeyAggTestVectors(t *testing.T) { + t.Parallel() + + testVectorPath := path.Join( + testVectorBaseDir, keyAggTestVectorFileName, + ) + testVectorBytes, err := os.ReadFile(testVectorPath) + require.NoError(t, err) + + var testCases keyAggTestVectors + require.NoError(t, json.Unmarshal(testVectorBytes, &testCases)) + + tweaks := make([][]byte, len(testCases.Tweaks)) + for i := range testCases.Tweaks { + tweaks[i] = mustParseHex(testCases.Tweaks[i]) + } + + for i, testCase := range testCases.ValidCases { + testCase := testCase + + // Assemble the set of keys we'll pass in based on their key + // index. We don't use sorting to ensure we send the keys in + // the exact same order as the test vectors do. + inputKeys, err := keysFromIndices( + t, testCase.Indices, testCases.PubKeys, + ) + require.NoError(t, err) + + t.Run(fmt.Sprintf("test_case=%v", i), func(t *testing.T) { + uniqueKeyIndex := secondUniqueKeyIndex(inputKeys, false) + opts := []KeyAggOption{WithUniqueKeyIndex(uniqueKeyIndex)} + + combinedKey, _, _, err := AggregateKeys( + inputKeys, false, opts..., + ) + require.NoError(t, err) + + require.Equal( + t, schnorr.SerializePubKey(combinedKey.FinalKey), + mustParseHex(testCase.Expected), + ) + }) + } + + for _, testCase := range testCases.InvalidCases { + testCase := testCase + + testName := fmt.Sprintf("invalid_%v", + strings.ToLower(testCase.Comment)) + t.Run(testName, func(t *testing.T) { + // For each test, we'll extract the set of input keys + // as well as the tweaks since this set of cases also + // exercises error cases related to the set of tweaks. + inputKeys, err := keysFromIndices( + t, testCase.Indices, testCases.PubKeys, + ) + + // In this set of test cases, we should only get this + // for the very first vector. + if err != nil { + switch testCase.Comment { + case "Invalid public key": + require.ErrorIs( + t, err, + secp.ErrPubKeyNotOnCurve, + ) + + case "Public key exceeds field size": + require.ErrorIs( + t, err, secp.ErrPubKeyXTooBig, + ) + + case "First byte of public key is not 2 or 3": + require.ErrorIs( + t, err, + secp.ErrPubKeyInvalidFormat, + ) + + default: + t.Fatalf("uncaught err: %v", err) + } + + return + } + + var tweaks []KeyTweakDesc + if len(testCase.TweakIndices) != 0 { + tweaks = tweaksFromIndices( + t, testCase.TweakIndices, testCases.Tweaks, + testCase.IsXOnly, + ) + } + + uniqueKeyIndex := secondUniqueKeyIndex(inputKeys, false) + opts := []KeyAggOption{ + WithUniqueKeyIndex(uniqueKeyIndex), + } + + if len(tweaks) != 0 { + opts = append(opts, WithKeyTweaks(tweaks...)) + } + + _, _, _, err = AggregateKeys( + inputKeys, false, opts..., + ) + require.Error(t, err) + + switch testCase.Comment { + case "Tweak is out of range": + require.ErrorIs(t, err, ErrTweakedKeyOverflows) + + case "Intermediate tweaking result is point at infinity": + + require.ErrorIs(t, err, ErrTweakedKeyIsInfinity) + + default: + t.Fatalf("uncaught err: %v", err) + } + }) + } +} diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index 1b4a68fd..501ef41a 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -75,198 +75,12 @@ func getInfinityTweak() KeyTweakDesc { } const ( - keyAggTestVectorName = "key_agg_vectors.json" - - nonceAggTestVectorName = "nonce_agg_vectors.json" - signTestVectorName = "sign_vectors.json" ) var dumpJson = flag.Bool("dumpjson", false, "if true, a JSON version of the "+ "test vectors will be written to the cwd") -type jsonKeyAggTestCase struct { - Keys []string `json:"keys"` - Tweaks []jsonTweak `json:"tweaks"` - ExpectedKey string `json:"expected_key"` - ExpectedError string `json:"expected_error"` -} - -// TestMuSig2KeyAggTestVectors tests that this implementation of musig2 key -// aggregation lines up with the secp256k1-zkp test vectors. -func TestMuSig2KeyAggTestVectors(t *testing.T) { - t.Parallel() - - var jsonCases []jsonKeyAggTestCase - - testCases := []struct { - keyOrder []int - explicitKeys []*btcec.PublicKey - tweaks []KeyTweakDesc - expectedKey []byte - expectedError error - }{ - // Keys in backwards lexicographical order. - { - keyOrder: []int{0, 1, 2}, - expectedKey: keyCombo1, - }, - - // Keys in sorted order. - { - keyOrder: []int{2, 1, 0}, - expectedKey: keyCombo2, - }, - - // Only the first key. - { - keyOrder: []int{0, 0, 0}, - expectedKey: keyCombo3, - }, - - // Duplicate the first key and second keys. - { - keyOrder: []int{0, 0, 1, 1}, - expectedKey: keyCombo4, - }, - - // Invalid public key. - { - keyOrder: []int{0, 3}, - expectedError: secp256k1.ErrPubKeyNotOnCurve, - }, - - // Public key exceeds field size. - { - keyOrder: []int{0, 4}, - expectedError: secp256k1.ErrPubKeyXTooBig, - }, - - // Tweak is out of range. - { - keyOrder: []int{0, 1}, - tweaks: []KeyTweakDesc{ - KeyTweakDesc{ - Tweak: to32ByteSlice(invalidTweak), - IsXOnly: true, - }, - }, - expectedError: ErrTweakedKeyOverflows, - }, - - // Intermediate tweaking result is point at infinity. - { - explicitKeys: []*secp256k1.PublicKey{btcec.Generator()}, - tweaks: []KeyTweakDesc{ - getInfinityTweak(), - }, - expectedError: ErrTweakedKeyIsInfinity, - }, - } - for i, testCase := range testCases { - testName := fmt.Sprintf("%v", testCase.keyOrder) - t.Run(testName, func(t *testing.T) { - var ( - keys []*btcec.PublicKey - strKeys []string - strTweaks []jsonTweak - jsonError string - ) - for _, keyIndex := range testCase.keyOrder { - keyBytes := testKeys[keyIndex] - pub, err := schnorr.ParsePubKey(keyBytes) - - switch { - case testCase.expectedError != nil && - errors.Is(err, testCase.expectedError): - return - case err != nil: - t.Fatalf("unable to parse pubkeys: %v", err) - } - - keys = append(keys, pub) - strKeys = append(strKeys, hex.EncodeToString(keyBytes)) - } - - for _, explicitKey := range testCase.explicitKeys { - keys = append(keys, explicitKey) - strKeys = append( - strKeys, - hex.EncodeToString( - explicitKey.SerializeCompressed(), - )) - } - - for _, tweak := range testCase.tweaks { - strTweaks = append( - strTweaks, - jsonTweak{ - Tweak: hex.EncodeToString( - tweak.Tweak[:], - ), - XOnly: tweak.IsXOnly, - }) - } - - if testCase.expectedError != nil { - jsonError = testCase.expectedError.Error() - } - - jsonCases = append( - jsonCases, - jsonKeyAggTestCase{ - Keys: strKeys, - Tweaks: strTweaks, - ExpectedKey: hex.EncodeToString( - testCase.expectedKey), - ExpectedError: jsonError, - }) - - uniqueKeyIndex := secondUniqueKeyIndex(keys, false) - opts := []KeyAggOption{WithUniqueKeyIndex(uniqueKeyIndex)} - if len(testCase.tweaks) > 0 { - opts = append(opts, WithKeyTweaks(testCase.tweaks...)) - } - - combinedKey, _, _, err := AggregateKeys( - keys, false, opts..., - ) - - switch { - case testCase.expectedError != nil && - errors.Is(err, testCase.expectedError): - return - - case err != nil: - t.Fatalf("case #%v, got error %v", i, err) - } - - combinedKeyBytes := schnorr.SerializePubKey(combinedKey.FinalKey) - if !bytes.Equal(combinedKeyBytes, testCase.expectedKey) { - t.Fatalf("case: #%v, invalid aggregation: "+ - "expected %x, got %x", i, testCase.expectedKey, - combinedKeyBytes) - } - }) - } - - if *dumpJson { - jsonBytes, err := json.Marshal(jsonCases) - if err != nil { - t.Fatalf("unable to encode json: %v", err) - } - - var formattedJson bytes.Buffer - json.Indent(&formattedJson, jsonBytes, "", "\t") - err = ioutil.WriteFile( - keyAggTestVectorName, formattedJson.Bytes(), 0644, - ) - if err != nil { - t.Fatalf("unable to write file: %v", err) - } - } -} - func mustParseHex(str string) []byte { b, err := hex.DecodeString(str) if err != nil {