mirror of
https://github.com/btcsuite/btcd.git
synced 2025-02-23 06:35:20 +01:00
btcec/schnorr/musig2: update key agg test vectors to musig2 1.0.0
This commit is contained in:
parent
3d9f4484df
commit
4e55273815
4 changed files with 361 additions and 186 deletions
88
btcec/schnorr/musig2/data/key_agg_vectors.json
Normal file
88
btcec/schnorr/musig2/data/key_agg_vectors.json
Normal file
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
16
btcec/schnorr/musig2/data/key_sort_vectors.json
Normal file
16
btcec/schnorr/musig2/data/key_sort_vectors.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"pubkeys": [
|
||||
"02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
|
||||
"02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
|
||||
"03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
|
||||
"023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66",
|
||||
"02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8"
|
||||
],
|
||||
"sorted_pubkeys": [
|
||||
"023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66",
|
||||
"02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
|
||||
"02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
|
||||
"02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
|
||||
"03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"
|
||||
]
|
||||
}
|
257
btcec/schnorr/musig2/keys_test.go
Normal file
257
btcec/schnorr/musig2/keys_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue