From f8c9c09320c876ad7bd20796ca1536abd0a3694d Mon Sep 17 00:00:00 2001 From: Oli Date: Wed, 12 Aug 2026 21:13:46 +0200 Subject: [PATCH 1/9] btcec: export ComputeSigningNonce Allows other packages to use the ComputeSigningNonce function. --- btcec/schnorr/musig2/sign.go | 6 +++--- btcec/schnorr/musig2/sign_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/btcec/schnorr/musig2/sign.go b/btcec/schnorr/musig2/sign.go index fadae01f..8eb256e4 100644 --- a/btcec/schnorr/musig2/sign.go +++ b/btcec/schnorr/musig2/sign.go @@ -195,9 +195,9 @@ func WithBip86SignTweak() SignOption { } } -// computeSigningNonce calculates the final nonce used for signing. This will +// ComputeSigningNonce calculates the final nonce used for signing. This will // be the R value used in the final signature. -func computeSigningNonce(combinedNonce [PubNonceSize]byte, +func ComputeSigningNonce(combinedNonce [PubNonceSize]byte, combinedKey *btcec.PublicKey, msg [32]byte) ( *btcec.JacobianPoint, *btcec.ModNScalar, error) { @@ -313,7 +313,7 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, // We'll now combine both the public nonces, using the blinding factor // to tweak the second nonce: // * R = R_1 + b*R_2 - nonce, nonceBlinder, err := computeSigningNonce( + nonce, nonceBlinder, err := ComputeSigningNonce( combinedNonce, combinedKey.FinalKey, msg, ) if err != nil { diff --git a/btcec/schnorr/musig2/sign_test.go b/btcec/schnorr/musig2/sign_test.go index 19647398..f8680c73 100644 --- a/btcec/schnorr/musig2/sign_test.go +++ b/btcec/schnorr/musig2/sign_test.go @@ -391,7 +391,7 @@ func TestMusig2SignCombine(t *testing.T) { combinedNonce, err := AggregateNonces(pubNonces) require.NoError(t, err) - finalNonceJ, _, err := computeSigningNonce( + finalNonceJ, _, err := ComputeSigningNonce( combinedNonce, combinedKey.FinalKey, msg, ) From 7c66ef2c2cc6f629628021712b54800db9a2c793 Mon Sep 17 00:00:00 2001 From: Oli Date: Wed, 12 Aug 2026 21:16:38 +0200 Subject: [PATCH 2/9] btcec: export ScalarSize --- btcec/schnorr/signature.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/btcec/schnorr/signature.go b/btcec/schnorr/signature.go index 05bc808f..ea554822 100644 --- a/btcec/schnorr/signature.go +++ b/btcec/schnorr/signature.go @@ -15,8 +15,8 @@ const ( // SignatureSize is the size of an encoded Schnorr signature. SignatureSize = 64 - // scalarSize is the size of an encoded big endian scalar. - scalarSize = 32 + // ScalarSize is the size of an encoded big endian scalar. + ScalarSize = 32 ) var ( @@ -132,9 +132,9 @@ func schnorrVerify(sig *Signature, hash []byte, pubKeyBytes []byte) error { // Step 1. // // Fail if m is not 32 bytes - if len(hash) != scalarSize { + if len(hash) != ScalarSize { str := fmt.Sprintf("wrong size for message (got %v, want %v)", - len(hash), scalarSize) + len(hash), ScalarSize) return signatureError(ecdsa_schnorr.ErrInvalidHashLen, str) } @@ -234,8 +234,8 @@ func (sig *Signature) Verify(hash []byte, pubKey *btcec.PublicKey) bool { } // zeroArray zeroes the memory of a scalar array. -func zeroArray(a *[scalarSize]byte) { - for i := 0; i < scalarSize; i++ { +func zeroArray(a *[ScalarSize]byte) { + for i := 0; i < ScalarSize; i++ { a[i] = 0x00 } } @@ -444,9 +444,9 @@ func Sign(privKey *btcec.PrivateKey, hash []byte, // Step 2. // // Fail if m is not 32 bytes - if len(hash) != scalarSize { + if len(hash) != ScalarSize { str := fmt.Sprintf("wrong size for message hash (got %v, want %v)", - len(hash), scalarSize) + len(hash), ScalarSize) return nil, signatureError(ecdsa_schnorr.ErrInvalidHashLen, str) } @@ -519,7 +519,7 @@ func Sign(privKey *btcec.PrivateKey, hash []byte, return sig, nil } - var privKeyBytes [scalarSize]byte + var privKeyBytes [ScalarSize]byte privKeyScalar.PutBytes(&privKeyBytes) defer zeroArray(&privKeyBytes) for iteration := uint32(0); ; iteration++ { From d023af295c2d8d508e865218f549f8adbc16c558 Mon Sep 17 00:00:00 2001 From: Oli Date: Wed, 12 Aug 2026 21:17:53 +0200 Subject: [PATCH 3/9] psbt: bump version of btcec/v2 dependency --- psbt/go.mod | 3 +++ psbt/go.sum | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/psbt/go.mod b/psbt/go.mod index 7e2d1491..e192897c 100644 --- a/psbt/go.mod +++ b/psbt/go.mod @@ -24,3 +24,6 @@ require ( golang.org/x/sys v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +// TODO(guggero): Remove this once PR #2198 is merged. +replace github.com/btcsuite/btcd/btcec/v2 => ../btcec diff --git a/psbt/go.sum b/psbt/go.sum index 2297f4e4..049d4379 100644 --- a/psbt/go.sum +++ b/psbt/go.sum @@ -1,7 +1,5 @@ github.com/btcsuite/btcd/address/v2 v2.0.0 h1:UVu8Hal6Siu4XastFe+JX5JkeBYONbDUIY5E+SVTs6I= github.com/btcsuite/btcd/address/v2 v2.0.0/go.mod h1:htJK1AtaeK3bKNfZY63ep2oN8LbrI6qvmPGe1vekb3I= -github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8= -github.com/btcsuite/btcd/btcec/v2 v2.5.0/go.mod h1:+K/MYXcLBtHEQjRbjHuJChuybk4LCgjdjgRwil+e+Kk= github.com/btcsuite/btcd/btcutil/v2 v2.0.0 h1:77pgf/4tjWaSBLdos8yiWVWL3rSphxWNqkLwcyONExA= github.com/btcsuite/btcd/btcutil/v2 v2.0.0/go.mod h1:ZF8MMdsx1JGgvHJUanxbigekSO+8bN/ai34LBk/lg3c= github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 h1:M/RTtXfXA9odC1RUEOyZFXj/NXKVHPYZXVjb60xTOok= From e219ff7f936cc9f13721e2dcae47bec46d4a71b5 Mon Sep 17 00:00:00 2001 From: Oli Date: Fri, 14 Jun 2024 11:06:34 +0200 Subject: [PATCH 4/9] psbt: add new structs for MuSig2 fields --- psbt/musig2.go | 385 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 psbt/musig2.go diff --git a/psbt/musig2.go b/psbt/musig2.go new file mode 100644 index 00000000..f80f24fc --- /dev/null +++ b/psbt/musig2.go @@ -0,0 +1,385 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package psbt + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" +) + +var ( + // ErrMissingKey is returned if one of the structs is missing a public + // key (the error is always wrapped with the name of the public key that + // is missing). + ErrMissingKey = errors.New("missing public key") + + // ErrInvalidTapLeafHash is returned if the length of the tap leaf hash + // is neither zero (not set) or 32 bytes long. + ErrInvalidTapLeafHash = errors.New("invalid value for tap leaf hash") + + // ErrMissingPartialSignature is returned if a partial signature is + // missing. + ErrMissingPartialSignature = errors.New("missing partial signature") +) + +// MuSig2Participants represents a set of participants in a MuSig2 signing +// session. +type MuSig2Participants struct { + // AggregateKey is the plain (non-tweaked) aggregate public key of all + // participants, from the `KeyAgg` algorithm as described in the MuSig2 + // BIP. This key may or may not be in the script directly (x-only). It + // may instead be a parent public key from which the public key in the + // script were derived. + AggregateKey *btcec.PublicKey + + // Keys is a list of the public keys of the participants in the MuSig2 + // aggregate key in the order required for aggregation. If sorting was + // done, then the keys must be in the sorted order. + Keys []*btcec.PublicKey +} + +// Validate asserts the data contained within the MuSig2Participants is sane for +// being serialized into a PSBT packet. +func (m *MuSig2Participants) Validate() error { + if m.AggregateKey == nil { + return fmt.Errorf("aggregate key: %w", ErrMissingKey) + } + + // An empty list of participants would be serialized as a zero-length + // value, which the read path rejects as invalid. + if len(m.Keys) == 0 { + return fmt.Errorf("participant keys: %w", ErrMissingKey) + } + + for idx, key := range m.Keys { + if key == nil { + return fmt.Errorf("participant key %d: %w", idx, + ErrMissingKey) + } + } + + return nil +} + +// KeyData returns the key data for the MuSig2Participants struct. +func (m *MuSig2Participants) KeyData() []byte { + return m.AggregateKey.SerializeCompressed() +} + +// ReadMuSig2Participants reads a set of MuSig2 participants from a key-value +// pair in a PSBT. +func ReadMuSig2Participants(keyData, + value []byte) (*MuSig2Participants, error) { + + if len(keyData) != btcec.PubKeyBytesLenCompressed { + return nil, ErrInvalidKeyData + } + + if len(value) == 0 || len(value)%btcec.PubKeyBytesLenCompressed != 0 { + return nil, ErrInvalidPsbtFormat + } + + numKeys := len(value) / btcec.PubKeyBytesLenCompressed + participants := &MuSig2Participants{ + Keys: make([]*btcec.PublicKey, numKeys), + } + + var err error + participants.AggregateKey, err = btcec.ParsePubKey(keyData) + if err != nil { + return nil, err + } + + for idx := range numKeys { + start := idx * btcec.PubKeyBytesLenCompressed + participants.Keys[idx], err = btcec.ParsePubKey( + value[start : start+btcec.PubKeyBytesLenCompressed], + ) + if err != nil { + return nil, err + } + } + + return participants, nil +} + +// SerializeMuSig2Participants serializes a set of MuSig2 participants to a +// key-value pair in a PSBT. +func SerializeMuSig2Participants(w io.Writer, typ uint8, + participants *MuSig2Participants) error { + + // Make sure what we write would also be accepted again when reading. + if err := participants.Validate(); err != nil { + return err + } + + value := make( + []byte, len(participants.Keys)*btcec.PubKeyBytesLenCompressed, + ) + + for idx, key := range participants.Keys { + copy( + value[idx*btcec.PubKeyBytesLenCompressed:], + key.SerializeCompressed(), + ) + } + + return serializeKVPairWithType(w, typ, participants.KeyData(), value) +} + +// MuSig2PubNonce represents a public nonce provided by a participant in a +// MuSig2 signing session. +type MuSig2PubNonce struct { + // PubKey is the public key of the participant providing this nonce. + PubKey *btcec.PublicKey + + // AggregateKey is the plain (non-tweaked) aggregate public key the + // participant is providing the nonce for. This must be the key found in + // the script and not the aggregate public key that it was derived from, + // if it was derived from an aggregate key. + AggregateKey *btcec.PublicKey + + // TapLeafHash is the optional hash of the BIP-0341 tap leaf hash of the + // Taproot leaf script that will be signed. If the aggregate key is the + // taproot internal key or the taproot output key, then the tap leaf + // hash must be omitted. + TapLeafHash []byte + + // PubNonce is the public nonce provided by the participant, produced + // by the `NonceGen` algorithm as described in the MuSig2 BIP. + PubNonce [musig2.PubNonceSize]byte +} + +// Validate asserts the data contained within the MuSig2PubNonce is sane for +// being serialized into a PSBT packet. +func (m *MuSig2PubNonce) Validate() error { + if m.PubKey == nil { + return fmt.Errorf("public key: %w", ErrMissingKey) + } + + if m.AggregateKey == nil { + return fmt.Errorf("aggregate key: %w", ErrMissingKey) + } + + if len(m.TapLeafHash) != 0 && len(m.TapLeafHash) != sha256.Size { + return ErrInvalidTapLeafHash + } + + return nil +} + +// KeyData returns the key data for the MuSig2PubNonce struct. +func (m *MuSig2PubNonce) KeyData() []byte { + // The tap leaf hash is optional. + keyLen := 2*btcec.PubKeyBytesLenCompressed + len(m.TapLeafHash) + keyData := make([]byte, keyLen) + + copy(keyData, m.PubKey.SerializeCompressed()) + copy( + keyData[btcec.PubKeyBytesLenCompressed:], + m.AggregateKey.SerializeCompressed(), + ) + + if len(m.TapLeafHash) != 0 { + copy(keyData[2*btcec.PubKeyBytesLenCompressed:], m.TapLeafHash) + } + + return keyData +} + +// ReadMuSig2PubNonce reads a MuSig2 public nonce from a key-value pair in a +// PSBT. +func ReadMuSig2PubNonce(keyData, value []byte) (*MuSig2PubNonce, error) { + const pubKeyLen = btcec.PubKeyBytesLenCompressed + const minLength = 2 * pubKeyLen + const maxLength = minLength + sha256.Size + + if len(keyData) != minLength && len(keyData) != maxLength { + return nil, ErrInvalidKeyData + } + + if len(value) != musig2.PubNonceSize { + return nil, ErrInvalidPsbtFormat + } + + var ( + nonce MuSig2PubNonce + err error + ) + nonce.PubKey, err = btcec.ParsePubKey(keyData[0:pubKeyLen]) + if err != nil { + return nil, err + } + + nonce.AggregateKey, err = btcec.ParsePubKey( + keyData[pubKeyLen : 2*pubKeyLen], + ) + if err != nil { + return nil, err + } + + if len(keyData) == maxLength { + nonce.TapLeafHash = make([]byte, sha256.Size) + copy(nonce.TapLeafHash, keyData[2*pubKeyLen:]) + } + + copy(nonce.PubNonce[:], value) + + return &nonce, nil +} + +// SerializeMuSig2PubNonce serializes a MuSig2 public nonce to a key-value pair +// in a PSBT. +func SerializeMuSig2PubNonce(w io.Writer, typ uint8, + nonce *MuSig2PubNonce) error { + + // Make sure what we write would also be accepted again when reading. + if err := nonce.Validate(); err != nil { + return err + } + + return serializeKVPairWithType( + w, typ, nonce.KeyData(), nonce.PubNonce[:], + ) +} + +// MuSig2PartialSig represents a partial signature provided by a participant in +// a MuSig2 signing session. +type MuSig2PartialSig struct { + // PubKey is the public key of the participant providing this partial + // signature. + PubKey *btcec.PublicKey + + // AggregateKey is the plain (non-tweaked) aggregate public key the + // participant is providing the partial signature for. This must be the + // key found in the script and not the aggregate public key that it was + // derived from, if it was derived from an aggregate key. + AggregateKey *btcec.PublicKey + + // TapLeafHash is the optional hash of the BIP-0341 tap leaf hash of the + // Taproot leaf script that will be signed. If the aggregate key is the + // taproot internal key or the taproot output key, then the tap leaf + // hash must be omitted. + TapLeafHash []byte + + // PartialSig is the partial signature provided by the participant, + // produced by the `Sign` algorithm as described in the MuSig2 BIP. + PartialSig musig2.PartialSignature +} + +// Validate asserts the data contained within the MuSig2PartialSig is sane for +// being serialized into a PSBT packet. +func (m *MuSig2PartialSig) Validate() error { + if m.PubKey == nil { + return fmt.Errorf("public key: %w", ErrMissingKey) + } + + if m.AggregateKey == nil { + return fmt.Errorf("aggregate key: %w", ErrMissingKey) + } + + if len(m.TapLeafHash) != 0 && len(m.TapLeafHash) != sha256.Size { + return ErrInvalidTapLeafHash + } + + // Only the S value of a partial signature is serialized (and therefore + // only S is restored when parsing), so we must not require R here. + if m.PartialSig.S == nil { + return ErrMissingPartialSignature + } + + return nil +} + +// KeyData returns the key data for the MuSig2PartialSig struct. +func (m *MuSig2PartialSig) KeyData() []byte { + // The tap leaf hash is optional. + keyLen := 2*btcec.PubKeyBytesLenCompressed + len(m.TapLeafHash) + keyData := make([]byte, keyLen) + + copy(keyData, m.PubKey.SerializeCompressed()) + copy( + keyData[btcec.PubKeyBytesLenCompressed:], + m.AggregateKey.SerializeCompressed(), + ) + + if len(m.TapLeafHash) != 0 { + copy(keyData[2*btcec.PubKeyBytesLenCompressed:], m.TapLeafHash) + } + + return keyData +} + +// ReadMuSig2PartialSig reads a MuSig2 partial signature from a key-value pair +// in a PSBT. +func ReadMuSig2PartialSig(keyData, value []byte) (*MuSig2PartialSig, error) { + const pubKeyLen = btcec.PubKeyBytesLenCompressed + const minLength = 2 * pubKeyLen + const maxLength = minLength + sha256.Size + + if len(keyData) != minLength && len(keyData) != maxLength { + return nil, ErrInvalidKeyData + } + + if len(value) != schnorr.ScalarSize { + return nil, ErrInvalidPsbtFormat + } + + var ( + partialSig MuSig2PartialSig + err error + ) + partialSig.PubKey, err = btcec.ParsePubKey(keyData[0:pubKeyLen]) + if err != nil { + return nil, err + } + + partialSig.AggregateKey, err = btcec.ParsePubKey( + keyData[pubKeyLen : 2*pubKeyLen], + ) + if err != nil { + return nil, err + } + + if len(keyData) == maxLength { + partialSig.TapLeafHash = make([]byte, sha256.Size) + copy(partialSig.TapLeafHash, keyData[2*pubKeyLen:]) + } + + err = partialSig.PartialSig.Decode(bytes.NewReader(value)) + if err != nil { + return nil, err + } + + return &partialSig, nil +} + +// SerializeMuSig2PartialSig serializes a MuSig2 partial signature to a +// key-value pair in a PSBT. +func SerializeMuSig2PartialSig(w io.Writer, typ uint8, + partialSig *MuSig2PartialSig) error { + + // Make sure what we write would also be accepted again when reading. + if err := partialSig.Validate(); err != nil { + return err + } + + var buf bytes.Buffer + err := partialSig.PartialSig.Encode(&buf) + if err != nil { + return err + } + + return serializeKVPairWithType( + w, typ, partialSig.KeyData(), buf.Bytes(), + ) +} From 6942cff10c645ace3c3735f0ac9ce0cc717fb5f8 Mon Sep 17 00:00:00 2001 From: Oli Date: Fri, 14 Jun 2024 11:06:45 +0200 Subject: [PATCH 5/9] psbt: add new MuSig2 fields to inputs and outputs --- psbt/partial_input.go | 104 ++++++++++++++++++++++++++++++++++++++ psbt/partial_output.go | 36 +++++++++++++ psbt/types.go | 24 +++++++++ psbt/updater.go | 112 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 276 insertions(+) diff --git a/psbt/partial_input.go b/psbt/partial_input.go index 06ba5b26..5c5e3033 100644 --- a/psbt/partial_input.go +++ b/psbt/partial_input.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "fmt" "io" + "slices" "sort" "github.com/btcsuite/btcd/txscript/v2" @@ -29,6 +30,9 @@ type PInput struct { TaprootBip32Derivation []*TaprootBip32Derivation TaprootInternalKey []byte TaprootMerkleRoot []byte + MuSig2Participants []*MuSig2Participants + MuSig2PubNonces []*MuSig2PubNonce + MuSig2PartialSigs []*MuSig2PartialSig Unknowns []*Unknown } @@ -362,6 +366,60 @@ func (pi *PInput) deserialize(r io.Reader) error { pi.TaprootMerkleRoot = value + case MuSig2ParticipantsInputType: + participants, err := ReadMuSig2Participants( + keyData, value, + ) + if err != nil { + return err + } + + // Duplicate keys are not allowed. + newKey := participants.KeyData() + for _, x := range pi.MuSig2Participants { + if bytes.Equal(x.KeyData(), newKey) { + return ErrDuplicateKey + } + } + + pi.MuSig2Participants = append( + pi.MuSig2Participants, participants, + ) + + case MuSig2PubNoncesInputType: + nonce, err := ReadMuSig2PubNonce(keyData, value) + if err != nil { + return err + } + + // Duplicate keys are not allowed. + newKey := nonce.KeyData() + for _, x := range pi.MuSig2PubNonces { + if bytes.Equal(x.KeyData(), newKey) { + return ErrDuplicateKey + } + } + + pi.MuSig2PubNonces = append(pi.MuSig2PubNonces, nonce) + + case MuSig2PartialSigsInputType: + partialSig, err := ReadMuSig2PartialSig(keyData, value) + if err != nil { + return err + } + + // Duplicate keys are not allowed. + newKey := partialSig.KeyData() + for _, x := range pi.MuSig2PartialSigs { + if bytes.Equal(x.KeyData(), newKey) { + return ErrDuplicateKey + } + } + + pi.MuSig2PartialSigs = append( + pi.MuSig2PartialSigs, partialSig, + ) + default: // A fall through case for any proprietary types. keyCodeAndData := append( @@ -590,6 +648,52 @@ func (pi *PInput) serialize(w io.Writer) error { return err } } + + slices.SortFunc( + pi.MuSig2Participants, + func(a, b *MuSig2Participants) int { + return bytes.Compare(a.KeyData(), b.KeyData()) + }, + ) + for _, participants := range pi.MuSig2Participants { + err := SerializeMuSig2Participants( + w, uint8(MuSig2ParticipantsInputType), + participants, + ) + if err != nil { + return err + } + } + + slices.SortFunc( + pi.MuSig2PubNonces, func(a, b *MuSig2PubNonce) int { + return bytes.Compare(a.KeyData(), b.KeyData()) + }, + ) + for _, nonce := range pi.MuSig2PubNonces { + err := SerializeMuSig2PubNonce( + w, uint8(MuSig2PubNoncesInputType), + nonce, + ) + if err != nil { + return err + } + } + + slices.SortFunc( + pi.MuSig2PartialSigs, func(a, b *MuSig2PartialSig) int { + return bytes.Compare(a.KeyData(), b.KeyData()) + }, + ) + for _, sig := range pi.MuSig2PartialSigs { + err := SerializeMuSig2PartialSig( + w, uint8(MuSig2PartialSigsInputType), + sig, + ) + if err != nil { + return err + } + } } if pi.FinalScriptSig != nil { diff --git a/psbt/partial_output.go b/psbt/partial_output.go index 93233e0e..55c91ae3 100644 --- a/psbt/partial_output.go +++ b/psbt/partial_output.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "slices" "sort" "github.com/btcsuite/btcd/wire/v2" @@ -18,6 +19,7 @@ type POutput struct { TaprootInternalKey []byte TaprootTapTree []byte TaprootBip32Derivation []*TaprootBip32Derivation + MuSig2Participants []*MuSig2Participants Unknowns []*Unknown } @@ -145,6 +147,26 @@ func (po *POutput) deserialize(r io.Reader) error { po.TaprootBip32Derivation, taprootDerivation, ) + case MuSig2ParticipantsOutputType: + participants, err := ReadMuSig2Participants( + keyData, value, + ) + if err != nil { + return err + } + + // Duplicate keys are not allowed. + newKey := participants.KeyData() + for _, x := range po.MuSig2Participants { + if bytes.Equal(x.KeyData(), newKey) { + return ErrDuplicateKey + } + } + + po.MuSig2Participants = append( + po.MuSig2Participants, participants, + ) + default: // A fall through case for any proprietary types. keyCodeAndData := append( @@ -253,6 +275,20 @@ func (po *POutput) serialize(w io.Writer) error { } } + slices.SortFunc( + po.MuSig2Participants, func(a, b *MuSig2Participants) int { + return bytes.Compare(a.KeyData(), b.KeyData()) + }, + ) + for _, participants := range po.MuSig2Participants { + err := SerializeMuSig2Participants( + w, uint8(MuSig2ParticipantsOutputType), participants, + ) + if err != nil { + return err + } + } + // Unknown is a special case; we don't have a key type, only a key and // a value field for _, kv := range po.Unknowns { diff --git a/psbt/types.go b/psbt/types.go index ca555101..64fc9fbd 100644 --- a/psbt/types.go +++ b/psbt/types.go @@ -151,6 +151,24 @@ const ( // 32-byte hash denoting the root hash of a merkle tree of scripts. TaprootMerkleRootType InputType = 0x18 + // MuSig2ParticipantsInputType is a type that carries the participant + // public keys and aggregated key for a MuSig2 signing session + // ({0x1a}|{aggregate_key}). The value is a list of 33-byte compressed + // public keys in the order required for aggregation. + MuSig2ParticipantsInputType InputType = 0x1a + + // MuSig2PubNoncesInputType is a type that carries the public nonces + // provided by participants in a MuSig2 signing session + // ({0x1b}|{participant_key}|{aggregate_key}[|{tapleaf_hash}]). The + // value is the 66-byte public nonces provided by the participant. + MuSig2PubNoncesInputType InputType = 0x1b + + // MuSig2PartialSigsInputType is a type that carries the partial + // signatures provided by participants in a MuSig2 signing session + // ({0x1c}|{participant_key}|{aggregate_key}[|{tapleaf_hash}]). The + // value is the 32-byte partial signature provided by the participant. + MuSig2PartialSigsInputType InputType = 0x1c + // ProprietaryInputType is a custom type for use by devs. // // The key ({0xFC}||{subtype}|{key data}), is a Variable length @@ -200,4 +218,10 @@ const ( // followed by said number of 32-byte leaf hashes. The rest of the value // is then identical to the Bip32DerivationInputType value. TaprootBip32DerivationOutputType OutputType = 7 + + // MuSig2ParticipantsOutputType is a type that carries the participant + // public keys and aggregated key for a MuSig2 signing session + // ({0x08}|{aggregate_key}). The value is a list of 33-byte compressed + // public keys in the order required for aggregation. + MuSig2ParticipantsOutputType OutputType = 0x08 ) diff --git a/psbt/updater.go b/psbt/updater.go index efd0a761..7a385495 100644 --- a/psbt/updater.go +++ b/psbt/updater.go @@ -375,3 +375,115 @@ func (u *Updater) AddOutWitnessScript(witnessScript []byte, return nil } + +// AddInMuSig2Participants adds a PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS field +// to the input at index inIndex. Returns ErrDuplicateKey if a participants +// record with the same aggregate key is already present. +func (u *Updater) AddInMuSig2Participants(inIndex int, + participants *MuSig2Participants) error { + + if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) { + return ErrInvalidPsbtFormat + } + if participants == nil || participants.AggregateKey == nil { + return ErrInvalidPsbtFormat + } + + newKey := participants.KeyData() + for _, x := range u.Upsbt.Inputs[inIndex].MuSig2Participants { + if bytes.Equal(x.KeyData(), newKey) { + return ErrDuplicateKey + } + } + + u.Upsbt.Inputs[inIndex].MuSig2Participants = append( + u.Upsbt.Inputs[inIndex].MuSig2Participants, participants, + ) + + return u.Upsbt.SanityCheck() +} + +// AddInMuSig2PubNonce adds a PSBT_IN_MUSIG2_PUB_NONCE field to the input at +// index inIndex. Returns ErrDuplicateKey if a nonce with the same +// (participant pubkey, aggregate pubkey, optional tap leaf hash) is already +// present. +func (u *Updater) AddInMuSig2PubNonce(inIndex int, + nonce *MuSig2PubNonce) error { + + if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) { + return ErrInvalidPsbtFormat + } + if nonce == nil || nonce.PubKey == nil || nonce.AggregateKey == nil { + return ErrInvalidPsbtFormat + } + + newKey := nonce.KeyData() + for _, x := range u.Upsbt.Inputs[inIndex].MuSig2PubNonces { + if bytes.Equal(x.KeyData(), newKey) { + return ErrDuplicateKey + } + } + + u.Upsbt.Inputs[inIndex].MuSig2PubNonces = append( + u.Upsbt.Inputs[inIndex].MuSig2PubNonces, nonce, + ) + + return u.Upsbt.SanityCheck() +} + +// AddInMuSig2PartialSig adds a PSBT_IN_MUSIG2_PARTIAL_SIG field to the input +// at index inIndex. Returns ErrDuplicateKey if a partial signature with the +// same (participant pubkey, aggregate pubkey, optional tap leaf hash) is +// already present. +func (u *Updater) AddInMuSig2PartialSig(inIndex int, + partialSig *MuSig2PartialSig) error { + + if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) { + return ErrInvalidPsbtFormat + } + if partialSig == nil || partialSig.PubKey == nil || + partialSig.AggregateKey == nil { + + return ErrInvalidPsbtFormat + } + + newKey := partialSig.KeyData() + for _, x := range u.Upsbt.Inputs[inIndex].MuSig2PartialSigs { + if bytes.Equal(x.KeyData(), newKey) { + return ErrDuplicateKey + } + } + + u.Upsbt.Inputs[inIndex].MuSig2PartialSigs = append( + u.Upsbt.Inputs[inIndex].MuSig2PartialSigs, partialSig, + ) + + return u.Upsbt.SanityCheck() +} + +// AddOutMuSig2Participants adds a PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS field +// to the output at index outIndex. Returns ErrDuplicateKey if a participants +// record with the same aggregate key is already present. +func (u *Updater) AddOutMuSig2Participants(outIndex int, + participants *MuSig2Participants) error { + + if outIndex < 0 || outIndex >= len(u.Upsbt.Outputs) { + return ErrInvalidPsbtFormat + } + if participants == nil || participants.AggregateKey == nil { + return ErrInvalidPsbtFormat + } + + newKey := participants.KeyData() + for _, x := range u.Upsbt.Outputs[outIndex].MuSig2Participants { + if bytes.Equal(x.KeyData(), newKey) { + return ErrDuplicateKey + } + } + + u.Upsbt.Outputs[outIndex].MuSig2Participants = append( + u.Upsbt.Outputs[outIndex].MuSig2Participants, participants, + ) + + return u.Upsbt.SanityCheck() +} From 066134c566e756d1fc7dfbdd5e280779adfb0691 Mon Sep 17 00:00:00 2001 From: Oli Date: Fri, 14 Jun 2024 17:43:22 +0200 Subject: [PATCH 6/9] psbt: add PrevOutFetcher helper func --- psbt/utils.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/psbt/utils.go b/psbt/utils.go index 4e01641d..4f81e813 100644 --- a/psbt/utils.go +++ b/psbt/utils.go @@ -531,3 +531,44 @@ func FindLeafScript(pInput *PInput, return nil, fmt.Errorf("leaf script for target leaf hash %x not "+ "found in input", targetLeafHash) } + +// PrevOutputFetcher returns a txscript.PrevOutFetcher built from the UTXO +// information in a PSBT packet. +func PrevOutputFetcher(packet *Packet) *txscript.MultiPrevOutFetcher { + fetcher := txscript.NewMultiPrevOutFetcher(nil) + for idx, txIn := range packet.UnsignedTx.TxIn { + in := packet.Inputs[idx] + + // Skip any input that has no UTXO. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + continue + } + + if in.NonWitnessUtxo != nil { + prevIndex := txIn.PreviousOutPoint.Index + + // Prevent a panic by checking the index is actually + // valid. If it isn't, there's nothing we can do, so we + // skip, since the data is simply invalid. + if prevIndex >= uint32(len(in.NonWitnessUtxo.TxOut)) { + continue + } + + fetcher.AddPrevOut( + txIn.PreviousOutPoint, + in.NonWitnessUtxo.TxOut[prevIndex], + ) + + continue + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + fetcher.AddPrevOut( + txIn.PreviousOutPoint, in.WitnessUtxo, + ) + } + } + + return fetcher +} \ No newline at end of file From 358cbcf3a533bdea66f36e39a8f86318d0ec2b67 Mon Sep 17 00:00:00 2001 From: Oli Date: Fri, 14 Jun 2024 18:10:07 +0200 Subject: [PATCH 7/9] psbt: support finalizing MuSig2 partial signatures --- psbt/finalizer.go | 651 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 649 insertions(+), 2 deletions(-) diff --git a/psbt/finalizer.go b/psbt/finalizer.go index a2a1e2d2..2db8ff66 100644 --- a/psbt/finalizer.go +++ b/psbt/finalizer.go @@ -13,9 +13,16 @@ package psbt import ( "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/binary" "fmt" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/btcutil/v2/hdkeychain" + "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/txscript/v2" "github.com/btcsuite/btcd/wire/v2" ) @@ -47,11 +54,25 @@ func isFinalizableWitnessInput(pInput *PInput) bool { case txscript.IsPayToTaproot(pkScript): if pInput.TaprootKeySpendSig == nil && - pInput.TaprootScriptSpendSig == nil { + pInput.TaprootScriptSpendSig == nil && + pInput.MuSig2PartialSigs == nil { return false } + // MuSig2 partial sigs are only useful when there is a + // matching nonce per participant AND every partial sig + // and nonce references the same aggregate key and tap + // leaf hash. The pre-aggregated TaprootKeySpendSig / + // TaprootScriptSpendSig branch above is preferred when + // either is also present. + if len(pInput.MuSig2PartialSigs) > 0 && + pInput.TaprootKeySpendSig == nil && + pInput.TaprootScriptSpendSig == nil { + + return musig2InputReady(pInput) + } + // For each of the script spend signatures we need a // corresponding tap script leaf with the control block. for _, sig := range pInput.TaprootScriptSpendSig { @@ -137,7 +158,8 @@ func isFinalizable(p *Packet, inIndex int) bool { // The input cannot be finalized without any signatures. if pInput.PartialSigs == nil && pInput.TaprootKeySpendSig == nil && - pInput.TaprootScriptSpendSig == nil { + pInput.TaprootScriptSpendSig == nil && + pInput.MuSig2PartialSigs == nil { return false } @@ -588,6 +610,22 @@ func finalizeTaprootInput(p *Packet, inIndex int) error { serializedWitness, err = writeWitness(witnessStack...) + // MuSig2 spend path. Dispatch on the presence of a tap leaf hash on + // the partial signatures: if all partial sigs reference a tap leaf + // hash, this is a tapscript leaf spend; otherwise it's a top-level + // keyspend. + case len(pInput.MuSig2PartialSigs) > 0: + firstSig := pInput.MuSig2PartialSigs[0] + if len(firstSig.TapLeafHash) > 0 { + serializedWitness, err = finalizeMuSig2ScriptSpend( + p, inIndex, + ) + } else { + serializedWitness, err = finalizeMuSig2KeySpend( + p, inIndex, + ) + } + default: return ErrInvalidPsbtFormat } @@ -606,3 +644,612 @@ func finalizeTaprootInput(p *Packet, inIndex int) error { p.Inputs[inIndex] = *newInput return nil } + +// finalizeMuSig2KeySpend handles BIP-373 test vector cases 1 and 2: a top-level +// taproot key spend where the aggregate MuSig2 key is either the output key +// directly (no tweak) or the internal key (BIP-86 or merkle-root taproot +// tweak). Returns the serialized witness containing the aggregated BIP-340 +// Schnorr signature. +func finalizeMuSig2KeySpend(p *Packet, inIndex int) ([]byte, error) { + pInput := &p.Inputs[inIndex] + + set, err := extractMuSig2SigningSet(pInput) + if err != nil { + return nil, err + } + + prevOutFetcher := PrevOutputFetcher(p) + sigHashes := txscript.NewTxSigHashes(p.UnsignedTx, prevOutFetcher) + sigHash, err := txscript.CalcTaprootSignatureHash( + sigHashes, pInput.SighashType, p.UnsignedTx, inIndex, + prevOutFetcher, + ) + if err != nil { + return nil, fmt.Errorf("error calculating signature hash: %w", + err) + } + + var sigHashMsg [32]byte + copy(sigHashMsg[:], sigHash) + + keyAggOpts, combineOpts, err := selectMuSig2KeyAggTweaks( + p, pInput, set.aggregateKey, set.keys, sigHashMsg, + ) + if err != nil { + return nil, err + } + + schnorrSig, err := combineMuSig2Sig( + sigHashMsg, set, keyAggOpts, combineOpts, + ) + if err != nil { + return nil, err + } + + sig := appendSighashType(schnorrSig.Serialize(), pInput.SighashType) + return writeWitness(sig) +} + +// selectMuSig2KeyAggTweaks decides which (if any) tweaks the finalizer must +// apply when combining the keyspend MuSig2 partial signatures. +// +// The aggregate key in the partial signature keydata is the key found in the +// script, which may be the result of tweaking or deriving one of the plain +// aggregate keys recorded in PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS. Each such +// record is tried as the bare aggregate, comparing it against the key in the +// script: +// +// - Equal → no tweak was applied at sign time (BIP-373 test vector cases 1 +// and 3: the aggregate is the output key, or the key in the leaf script). +// - Differ + TaprootInternalKey == bare aggregate → BIP-86 tweak (or +// taproot tweak with the merkle root, if present). BIP-373 test vector +// case 2. +// - Differ + TaprootInternalKey ≠ bare aggregate → BIP-373 test vector case 4 +// (the internal key was derived from the aggregate via BIP-32). +// +// The tweaks a record yields are only accepted once they are shown to actually +// reproduce the key in the script, so a record for an unrelated aggregate key +// is skipped rather than silently producing a wrong signature. +func selectMuSig2KeyAggTweaks(p *Packet, pInput *PInput, + partialSigAggregate *btcec.PublicKey, keys []*btcec.PublicKey, + sigHashMsg [32]byte) ([]musig2.KeyAggOption, []musig2.CombineOption, + error) { + + // PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS is keyed by the aggregate pubkey, + // so an input may carry a record for more than one aggregate key (for + // example one per spend path). We therefore cannot just assume the + // first record is the one the partial signatures belong to, and instead + // try each of them below. + candidates := make( + []*btcec.PublicKey, 0, len(pInput.MuSig2Participants), + ) + for _, participants := range pInput.MuSig2Participants { + candidates = append(candidates, participants.AggregateKey) + } + + // If MuSig2Participants is missing, fall back to treating the partial + // sig aggregate as the bare aggregate. This mirrors the behavior of a + // PSBT in which an updater only set the partial sigs. + if len(candidates) == 0 { + candidates = []*btcec.PublicKey{partialSigAggregate} + } + + // Keep the first candidate whose tweak chain actually reproduces the + // key in the script. If none of them do, we report the error of the + // last one, which is the most specific diagnostic we have for the + // common case of a single participants record. + var lastErr error + for _, bareAggregate := range candidates { + keyAggOpts, combineOpts, err := muSig2TweaksForAggregate( + p, pInput, bareAggregate, partialSigAggregate, keys, + sigHashMsg, + ) + if err != nil { + lastErr = err + continue + } + + // Confirm the tweaks turn the participant keys into the key the + // signers actually signed for. This both rules out a + // participants record belonging to a different aggregate key + // and catches a tweak chain we inferred incorrectly, before we + // hand out a signature that cannot be verified. + match, err := muSig2TweaksMatch( + keys, keyAggOpts, partialSigAggregate, + ) + if err != nil { + lastErr = err + continue + } + if !match { + lastErr = fmt.Errorf("MuSig2 finalize: aggregate key "+ + "%x does not produce the key in the script "+ + "(%x) under the inferred tweaks", + bareAggregate.SerializeCompressed(), + partialSigAggregate.SerializeCompressed()) + continue + } + + return keyAggOpts, combineOpts, nil + } + + return nil, nil, lastErr +} + +// muSig2TweaksMatch reports whether aggregating the given participant keys +// under the given tweaks yields the key found in the script. Only the x +// coordinate is compared, as that is all a taproot script commits to. +func muSig2TweaksMatch(keys []*btcec.PublicKey, + keyAggOpts []musig2.KeyAggOption, + scriptKey *btcec.PublicKey) (bool, error) { + + aggKey, _, _, err := musig2.AggregateKeys(keys, true, keyAggOpts...) + if err != nil { + return false, fmt.Errorf("error aggregating keys: %w", err) + } + + return bytes.Equal( + schnorr.SerializePubKey(aggKey.FinalKey), + schnorr.SerializePubKey(scriptKey), + ), nil +} + +// muSig2TweaksForAggregate returns the key aggregation and combine options +// needed to turn the given plain aggregate key into the key found in the +// script. See selectMuSig2KeyAggTweaks for the cases this distinguishes. +func muSig2TweaksForAggregate(p *Packet, pInput *PInput, + bareAggregate, partialSigAggregate *btcec.PublicKey, + keys []*btcec.PublicKey, sigHashMsg [32]byte) ([]musig2.KeyAggOption, + []musig2.CombineOption, error) { + + // Case 1: no tweak. The signers signed against the bare aggregate. + if bareAggregate.IsEqual(partialSigAggregate) { + return nil, []musig2.CombineOption{ + musig2.WithTweakedCombine(sigHashMsg, keys, nil, true), + }, nil + } + + // A tweak was applied at sign time. We can only recover the right + // tweak when the PSBT pins down the internal key. + if pInput.TaprootInternalKey == nil { + return nil, nil, fmt.Errorf("MuSig2 finalize: tweaked " + + "aggregate without PSBT_IN_TAP_INTERNAL_KEY is not " + + "supported") + } + + // Case 4: TaprootInternalKey was derived from the bare aggregate via + // BIP-32. Walk the path on the synthetic xpub from PSBT_GLOBAL_XPUB + // to compute the per-step BIP-32 tweaks, then add the taproot tweak. + if !bytes.Equal( + schnorr.SerializePubKey(bareAggregate), + pInput.TaprootInternalKey, + ) { + + return musig2BIP32DerivedTweaks( + p, pInput, bareAggregate, keys, sigHashMsg, + ) + } + + // Case 2: TaprootInternalKey is the bare aggregate; the output key is + // either BIP-86 tweaked (no script tree) or taproot-tweaked with a + // known merkle root. + if pInput.TaprootMerkleRoot != nil { + return []musig2.KeyAggOption{ + musig2.WithTaprootKeyTweak( + pInput.TaprootMerkleRoot, + ), + }, []musig2.CombineOption{ + musig2.WithTaprootTweakedCombine( + sigHashMsg, keys, + pInput.TaprootMerkleRoot, true, + ), + }, nil + } + + return []musig2.KeyAggOption{ + musig2.WithBIP86KeyTweak(), + }, []musig2.CombineOption{ + musig2.WithBip86TweakedCombine(sigHashMsg, keys, true), + }, nil +} + +// musig2BIP32DerivedTweaks handles BIP-373 test vector case 4: the taproot +// internal key is a BIP-32 unhardened child of the bare MuSig2 aggregate. The +// PSBT must include a PSBT_GLOBAL_XPUB whose serialized public key matches the +// bare aggregate; the chain code from that xpub is used to walk the path +// recorded on the internal key's PSBT_IN_TAP_BIP32_DERIVATION entry. +// +// The resulting list of tweaks is: +// - one non-x-only KeyTweakDesc per BIP-32 derivation step +// - one x-only KeyTweakDesc carrying the taproot tweak hash, computed +// against the post-BIP-32 derived internal key (BIP-86 if no merkle +// root; tagged hash with the merkle root otherwise) +// +// The combined Schnorr signature produced via these tweaks verifies under +// the post-derivation, post-taproot-tweak output key. +func musig2BIP32DerivedTweaks(p *Packet, pInput *PInput, + bareAggregate *btcec.PublicKey, keys []*btcec.PublicKey, + sigHashMsg [32]byte) ([]musig2.KeyAggOption, []musig2.CombineOption, + error) { + + xpub, err := findAggregateXpub(p, bareAggregate) + if err != nil { + return nil, nil, err + } + if xpub == nil { + return nil, nil, fmt.Errorf("MuSig2 finalize: aggregate is " + + "BIP-32 derived but no matching PSBT_GLOBAL_XPUB " + + "found (BIP-328 fallback is not supported)") + } + + path, err := internalKeyDerivationPath(pInput) + if err != nil { + return nil, nil, err + } + + bip32Tweaks, derivedXpub, err := bip32TweaksForPath(xpub, path) + if err != nil { + return nil, nil, err + } + + // Sanity-check: the derived xpub must equal the taproot internal key + // (compared as x-only). If it doesn't match, the path on the input + // doesn't correspond to the synthetic xpub we used and the partial + // signatures were produced for a different signing context. + derivedKey, err := derivedXpub.ECPubKey() + if err != nil { + return nil, nil, err + } + if !bytes.Equal( + schnorr.SerializePubKey(derivedKey), + pInput.TaprootInternalKey, + ) { + + return nil, nil, fmt.Errorf("MuSig2 finalize: BIP-32 derived " + + "key does not match taproot internal key on input") + } + + // Compute the taproot tweak: BIP-86 (empty merkle root) or a tagged + // hash with the merkle root. + internalKeyXOnly := schnorr.SerializePubKey(derivedKey) + var merkleRoot []byte + if pInput.TaprootMerkleRoot != nil { + merkleRoot = pInput.TaprootMerkleRoot + } + tapTweakHash := chainhash.TaggedHash( + chainhash.TagTapTweak, internalKeyXOnly, merkleRoot, + ) + allTweaks := append(bip32Tweaks, musig2.KeyTweakDesc{ + Tweak: *tapTweakHash, + IsXOnly: true, + }) + + return []musig2.KeyAggOption{ + musig2.WithKeyTweaks(allTweaks...), + }, []musig2.CombineOption{ + musig2.WithTweakedCombine( + sigHashMsg, keys, allTweaks, true, + ), + }, nil +} + +// findAggregateXpub returns the PSBT_GLOBAL_XPUB whose serialized public key +// matches the given bare MuSig2 aggregate, or nil if no such xpub is present. +func findAggregateXpub(p *Packet, + aggregate *btcec.PublicKey) (*hdkeychain.ExtendedKey, error) { + + want := aggregate.SerializeCompressed() + for _, x := range p.XPubs { + ext, err := DecodeExtendedKey(x.ExtendedKey) + if err != nil { + return nil, err + } + + pub, err := ext.ECPubKey() + if err != nil { + return nil, err + } + + if bytes.Equal(pub.SerializeCompressed(), want) { + return ext, nil + } + } + + return nil, nil +} + +// internalKeyDerivationPath returns the BIP-32 path recorded on the +// taproot internal key's PSBT_IN_TAP_BIP32_DERIVATION entry. Returns an +// error if no derivation entry matches the internal key. +func internalKeyDerivationPath(pInput *PInput) ([]uint32, error) { + if pInput.TaprootInternalKey == nil { + return nil, fmt.Errorf("input has no taproot internal key") + } + + for _, d := range pInput.TaprootBip32Derivation { + if bytes.Equal(d.XOnlyPubKey, pInput.TaprootInternalKey) { + return d.Bip32Path, nil + } + } + + return nil, fmt.Errorf("no PSBT_IN_TAP_BIP32_DERIVATION entry for " + + "taproot internal key") +} + +// bip32TweaksForPath walks the unhardened BIP-32 derivation path on the given +// parent extended key. For each step it computes the per-step scalar tweak (the +// IL half of HMAC-SHA512) and returns it as a non-x-only KeyTweakDesc. The +// fully derived child xpub is also returned so callers can compute follow-up +// tweaks (e.g. the taproot tweak) over its public key. +func bip32TweaksForPath(parent *hdkeychain.ExtendedKey, + path []uint32) ([]musig2.KeyTweakDesc, *hdkeychain.ExtendedKey, + error) { + + tweaks := make([]musig2.KeyTweakDesc, 0, len(path)) + current := parent + + for _, idx := range path { + if idx >= hdkeychain.HardenedKeyStart { + return nil, nil, fmt.Errorf("hardened derivation step "+ + "%d not supported with public-only xpub", idx) + } + + parentPub, err := current.ECPubKey() + if err != nil { + return nil, nil, err + } + + // I = HMAC-SHA512(parent.ChainCode, + // parent.SerializedCompressed || idx_be). + var idxBytes [4]byte + binary.BigEndian.PutUint32(idxBytes[:], idx) + + h := hmac.New(sha512.New, current.ChainCode()) + h.Write(parentPub.SerializeCompressed()) + h.Write(idxBytes[:]) + ilr := h.Sum(nil) + + var tweak [32]byte + copy(tweak[:], ilr[:32]) + tweaks = append(tweaks, musig2.KeyTweakDesc{ + Tweak: tweak, + IsXOnly: false, + }) + + next, err := current.Derive(idx) + if err != nil { + return nil, nil, err + } + current = next + } + + return tweaks, current, nil +} + +// finalizeMuSig2ScriptSpend handles BIP-373 test vector case 3: a tapscript +// leaf spend where the aggregate MuSig2 key is the key in the leaf script. +// Returns the serialized witness as [aggregatedSig, leafScript, controlBlock], +// mirroring the regular taproot script-spend witness shape. +func finalizeMuSig2ScriptSpend(p *Packet, inIndex int) ([]byte, error) { + pInput := &p.Inputs[inIndex] + + set, err := extractMuSig2SigningSet(pInput) + if err != nil { + return nil, err + } + if len(set.tapLeafHash) == 0 { + return nil, fmt.Errorf("script spend MuSig2 signing requires " + + "a tap leaf hash on partial signatures") + } + + leaf, err := FindLeafScript(pInput, set.tapLeafHash) + if err != nil { + return nil, fmt.Errorf("leaf script for tap leaf hash %x not "+ + "found: %w", set.tapLeafHash, err) + } + + prevOutFetcher := PrevOutputFetcher(p) + sigHashes := txscript.NewTxSigHashes(p.UnsignedTx, prevOutFetcher) + sigHash, err := txscript.CalcTapscriptSignaturehash( + sigHashes, pInput.SighashType, p.UnsignedTx, inIndex, + prevOutFetcher, txscript.TapLeaf{ + LeafVersion: leaf.LeafVersion, + Script: leaf.Script, + }, + ) + if err != nil { + return nil, fmt.Errorf("error calculating tapscript signature "+ + "hash: %w", err) + } + + var sigHashMsg [32]byte + copy(sigHashMsg[:], sigHash) + + // No taproot tweak: the aggregate key is the key in the script and is + // committed to directly via the leaf script's CHECKSIG opcode. + combineOpts := []musig2.CombineOption{ + musig2.WithTweakedCombine(sigHashMsg, set.keys, nil, true), + } + + schnorrSig, err := combineMuSig2Sig( + sigHashMsg, set, nil, combineOpts, + ) + if err != nil { + return nil, err + } + + sig := appendSighashType(schnorrSig.Serialize(), pInput.SighashType) + return writeWitness(sig, leaf.Script, leaf.ControlBlock) +} + +// musig2InputReady reports whether a taproot input has a complete and +// internally consistent set of MuSig2 fields ready for finalization. The rules +// are exactly the ones the finalizer itself enforces, so we simply try to +// assemble the signing set and throw it away again. +func musig2InputReady(pInput *PInput) bool { + _, err := extractMuSig2SigningSet(pInput) + return err == nil +} + +// muSig2SigningSet is the set of MuSig2 fields required to combine the partial +// signatures of an input into a single BIP-340 Schnorr signature. The keys, +// pubNonces and partialSigs slices are parallel: index i of each of them refers +// to the same participant. +type muSig2SigningSet struct { + // keys, pubNonces and partialSigs are the participants' public keys, + // public nonces and partial signatures, in matching order. + keys []*btcec.PublicKey + pubNonces [][musig2.PubNonceSize]byte + partialSigs []*musig2.PartialSignature + + // aggregateKey is the plain (non-tweaked) aggregate key that every + // nonce and partial signature on the input agrees on. + aggregateKey *btcec.PublicKey + + // tapLeafHash is the optional tap leaf hash that every nonce and + // partial signature on the input agrees on. It is empty for a key + // spend. + tapLeafHash []byte +} + +// extractMuSig2SigningSet validates that all MuSig2 nonces and partial +// signatures on the input reference the same aggregate key (and the same +// optional tap leaf hash) and returns them paired up by participant pubkey, so +// the parallel slices are aligned regardless of the order the fields appear on +// the input. The aggregate key and tap leaf hash are taken from the partial +// signatures and verified to agree with the nonces. +func extractMuSig2SigningSet(pInput *PInput) (*muSig2SigningSet, error) { + numSigs := len(pInput.MuSig2PartialSigs) + if numSigs == 0 { + return nil, fmt.Errorf("no MuSig2 partial signatures on input") + } + + if len(pInput.MuSig2PubNonces) != numSigs { + return nil, fmt.Errorf("number of MuSig2 pub nonces does not "+ + "match number of partial signatures (%d != %d)", + len(pInput.MuSig2PubNonces), numSigs) + } + + first := pInput.MuSig2PartialSigs[0] + set := &muSig2SigningSet{ + keys: make([]*btcec.PublicKey, numSigs), + pubNonces: make([][musig2.PubNonceSize]byte, numSigs), + partialSigs: make([]*musig2.PartialSignature, numSigs), + aggregateKey: first.AggregateKey, + tapLeafHash: first.TapLeafHash, + } + + // All partial sigs must agree on the aggregate key and tap leaf hash. + for idx, ps := range pInput.MuSig2PartialSigs { + err := set.assertAgrees( + "partial sig", idx, ps.AggregateKey, ps.TapLeafHash, + ) + if err != nil { + return nil, err + } + } + + // The nonces must agree on the same values, and each of them must have + // a matching partial signature from the same participant. + for idx, nonce := range pInput.MuSig2PubNonces { + err := set.assertAgrees( + "pub nonce", idx, nonce.AggregateKey, nonce.TapLeafHash, + ) + if err != nil { + return nil, err + } + + partialSig := findMuSig2PartialSig(pInput, nonce.PubKey) + if partialSig == nil { + return nil, fmt.Errorf("no MuSig2 partial signature "+ + "found for participant key %x", + nonce.PubKey.SerializeCompressed()) + } + + set.keys[idx] = nonce.PubKey + set.pubNonces[idx] = nonce.PubNonce + set.partialSigs[idx] = partialSig + } + + return set, nil +} + +// assertAgrees returns an error if the given aggregate key or tap leaf hash +// differs from the ones the signing set was created with. The name and index +// are only used to point at the offending field in the error message. +func (m *muSig2SigningSet) assertAgrees(name string, idx int, + aggregateKey *btcec.PublicKey, tapLeafHash []byte) error { + + if !aggregateKey.IsEqual(m.aggregateKey) { + return fmt.Errorf("%s %d references different aggregate key "+ + "than first partial signature", name, idx) + } + + if !bytes.Equal(tapLeafHash, m.tapLeafHash) { + return fmt.Errorf("%s %d references different tap leaf hash "+ + "than first partial signature", name, idx) + } + + return nil +} + +// findMuSig2PartialSig returns the partial signature the given participant +// provided for the input, or nil if there is none. +func findMuSig2PartialSig(pInput *PInput, + pubKey *btcec.PublicKey) *musig2.PartialSignature { + + for _, ps := range pInput.MuSig2PartialSigs { + if ps.PubKey.IsEqual(pubKey) { + return &ps.PartialSig + } + } + + return nil +} + +// combineMuSig2Sig aggregates the keys and nonces of the given signing set and +// combines its partial signatures into a single BIP-340 Schnorr signature. The +// keyAggOpts and combineOpts must describe the same tweak chain: tweaks applied +// during key aggregation must match the tweaks accumulated by the combine +// option. +func combineMuSig2Sig(sigHashMsg [32]byte, set *muSig2SigningSet, + keyAggOpts []musig2.KeyAggOption, + combineOpts []musig2.CombineOption) (*schnorr.Signature, error) { + + aggKey, _, _, err := musig2.AggregateKeys(set.keys, true, keyAggOpts...) + if err != nil { + return nil, fmt.Errorf("error aggregating keys: %w", err) + } + + aggregateNonce, err := musig2.AggregateNonces(set.pubNonces) + if err != nil { + return nil, fmt.Errorf("error aggregating pub nonces: %w", err) + } + + // The final nonce cannot be taken from the partial signatures the way + // musig2.CombineSigs is normally called with partialSigs[0].R: only the + // S value of a partial signature is serialized in a PSBT, so R is + // always nil for a signature we read out of a packet. We therefore have + // to re-derive the final nonce from the participants' public nonces. + nonceJ, _, err := musig2.ComputeSigningNonce( + aggregateNonce, aggKey.FinalKey, sigHashMsg, + ) + if err != nil { + return nil, fmt.Errorf("error computing signing nonce: %w", err) + } + nonceJ.ToAffine() + + return musig2.CombineSigs( + btcec.NewPublicKey(&nonceJ.X, &nonceJ.Y), set.partialSigs, + combineOpts..., + ), nil +} + +// appendSighashType appends a one-byte sighash type to the signature if it +// differs from the default sighash (which is omitted on the wire). +func appendSighashType(sig []byte, sht txscript.SigHashType) []byte { + if sht == txscript.SigHashDefault { + return sig + } + return append(sig, byte(sht)) +} From f1da42ba2d187d26c93a55680fe0ddbea12b2847 Mon Sep 17 00:00:00 2001 From: Oli Date: Wed, 20 May 2026 15:01:49 +0200 Subject: [PATCH 8/9] psbt: support adding partial MuSig2 signatures --- psbt/signer.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/psbt/signer.go b/psbt/signer.go index ad5e7fd3..2769fa67 100644 --- a/psbt/signer.go +++ b/psbt/signer.go @@ -10,6 +10,9 @@ package psbt // is in the correct state. import ( + "bytes" + "fmt" + "github.com/btcsuite/btcd/txscript/v2" ) @@ -136,6 +139,143 @@ func (u *Updater) Sign(inIndex int, sig []byte, pubKey []byte, return SignSuccesful, nil } +// SignMuSig2 attaches a MuSig2 partial signature to the input at index +// inIndex, following the BIP-174 Signer role for the MuSig2 fields defined by +// BIP-373. +// +// Before appending, SignMuSig2 enforces the invariants a finalizer will later +// rely on: +// +// - The input must not be finalized. +// - The input must carry a witness UTXO (every BIP-373 MuSig2 spend is +// segwit v1). +// - The participant pubkey on the partial sig must appear in at least one +// PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record on the input. The aggregate +// key recorded on the partial sig itself does not need to equal the +// bare aggregate in the participants record: BIP-373 case 4 deliberately +// records the BIP-32-derived aggregate on the partial sigs while the +// participants record carries the parent (bare) aggregate, and the +// finalizer reconciles the two via PSBT_GLOBAL_XPUB. Enforcing equality +// here would reject the legitimate derived-aggregate case. +// - A PSBT_IN_MUSIG2_PUB_NONCE field with the same key data prefix +// (participant pubkey || aggregate pubkey || optional tap leaf hash) must +// already be present — partial sigs cannot be combined without their +// matching nonces. +// - If TapLeafHash is set, it must resolve to a leaf script recorded on +// the input (i.e. the script the partial sig commits to is actually +// part of this spend). +// +// On any of these checks failing the input is left untouched and SignInvalid +// is returned together with the underlying error. Shape validation +// (compressed pubkeys, 32-byte partial sig) and duplicate-key detection are +// handled by AddInMuSig2PartialSig, which SignMuSig2 delegates to once the +// Signer-role checks pass. +// +// SignMuSig2 does not itself compute the partial signature; callers are +// expected to feed in the output of musig2.Sign (held in the +// MuSig2PartialSig.PartialSig field). This mirrors the existing Sign() +// helper, which accepts a pre-computed ECDSA signature rather than driving +// the signing key directly. +func (u *Updater) SignMuSig2(inIndex int, + partialSig *MuSig2PartialSig) (SignOutcome, error) { + + if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) { + return SignInvalid, ErrInvalidPsbtFormat + } + + if isFinalized(u.Upsbt, inIndex) { + return SignFinalized, nil + } + + if partialSig == nil || partialSig.PubKey == nil || + partialSig.AggregateKey == nil { + + return SignInvalid, ErrInvalidPsbtFormat + } + + pInput := &u.Upsbt.Inputs[inIndex] + + // BIP-373 inputs are taproot (segwit v1); a witness UTXO is required + // for the finalizer to recompute the sighash. + if pInput.WitnessUtxo == nil { + return SignInvalid, ErrInvalidPsbtFormat + } + + // Make sure the participant is actually a member of a known aggregate + // on this input. Without a PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record + // the finalizer cannot reproduce the aggregate, so the partial sig is + // not usable. + if !musig2ParticipantRegistered(pInput, partialSig) { + return SignInvalid, fmt.Errorf("%w: participant pubkey not "+ + "found in any MuSig2 participants record matching the "+ + "supplied aggregate key", ErrInvalidSignatureForInput) + } + + // A matching pub nonce (same participant pubkey || aggregate key || + // optional tap leaf hash) must exist; nonces are the precondition for + // signing per BIP-373 §Signer. + keyData := partialSig.KeyData() + if !musig2HasMatchingPubNonce(pInput, keyData) { + return SignInvalid, fmt.Errorf("%w: no matching "+ + "PSBT_IN_MUSIG2_PUB_NONCE found for partial signature", + ErrInvalidSignatureForInput) + } + + // If the partial sig commits to a tap leaf, the leaf script must + // actually be present on the input, otherwise the finalizer will not + // be able to assemble the script-spend witness. + if len(partialSig.TapLeafHash) > 0 { + _, err := FindLeafScript(pInput, partialSig.TapLeafHash) + if err != nil { + return SignInvalid, fmt.Errorf("%w: tap leaf hash %x "+ + "on partial signature does not match any leaf "+ + "script on input: %v", + ErrInvalidSignatureForInput, + partialSig.TapLeafHash, err) + } + } + + // Shape validation, duplicate-key detection and the actual append are + // done by the existing low-level updater helper. + if err := u.AddInMuSig2PartialSig(inIndex, partialSig); err != nil { + return SignInvalid, err + } + + return SignSuccesful, nil +} + +// musig2ParticipantRegistered reports whether the partial sig's participant +// pubkey appears in any PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record on the +// input. The aggregate key on the partial sig is intentionally not compared +// against the record's aggregate; in BIP-373 case 4 the partial sig records +// the BIP-32 derived aggregate while the record carries the bare aggregate. +func musig2ParticipantRegistered(pInput *PInput, + partialSig *MuSig2PartialSig) bool { + + for _, participants := range pInput.MuSig2Participants { + for _, key := range participants.Keys { + if key.IsEqual(partialSig.PubKey) { + return true + } + } + } + + return false +} + +// musig2HasMatchingPubNonce reports whether the input carries a +// PSBT_IN_MUSIG2_PUB_NONCE whose key data matches the partial signature's +// key data (participant pubkey || aggregate key || optional tap leaf hash). +func musig2HasMatchingPubNonce(pInput *PInput, partialSigKeyData []byte) bool { + for _, n := range pInput.MuSig2PubNonces { + if bytes.Equal(n.KeyData(), partialSigKeyData) { + return true + } + } + + return false +} + // nonWitnessToWitness extracts the TxOut from the existing NonWitnessUtxo // field in the given PSBT input and sets it as type witness by replacing the // NonWitnessUtxo field with a WitnessUtxo field. See From c9f640702c720d1a6b7ec81dea8b7fad95fd8252 Mon Sep 17 00:00:00 2001 From: Oli Date: Sat, 2 May 2026 08:44:38 +0200 Subject: [PATCH 9/9] psbt: add unit tests and test vectors --- psbt/finalizer_musig2_bip32_test.go | 283 +++++ psbt/finalizer_musig2_test.go | 257 +++++ psbt/go.mod | 2 +- psbt/musig2_test.go | 1304 +++++++++++++++++++++++ psbt/signer_musig2_test.go | 295 +++++ psbt/testdata/bip-373-test-vectors.json | 122 +++ 6 files changed, 2262 insertions(+), 1 deletion(-) create mode 100644 psbt/finalizer_musig2_bip32_test.go create mode 100644 psbt/finalizer_musig2_test.go create mode 100644 psbt/musig2_test.go create mode 100644 psbt/signer_musig2_test.go create mode 100644 psbt/testdata/bip-373-test-vectors.json diff --git a/psbt/finalizer_musig2_bip32_test.go b/psbt/finalizer_musig2_bip32_test.go new file mode 100644 index 00000000..b5c55fe1 --- /dev/null +++ b/psbt/finalizer_musig2_bip32_test.go @@ -0,0 +1,283 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package psbt + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/binary" + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/btcutil/v2/hdkeychain" + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/txscript/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/stretchr/testify/require" +) + +// makeTestParticipants returns three deterministic (priv, pub) pairs to use +// as the MuSig2 signing set. The test only needs a self-consistent 3-party +// setup; it does not depend on the specific keys matching any external +// vector. +func makeTestParticipants(t *testing.T) ( + []*btcec.PrivateKey, []*btcec.PublicKey) { + + t.Helper() + + scalars := []string{ + "f5dd1de7b85c0e8c1ada7c0c95eaa42d2bcb29ee71f5e0e63d8df1eb9e3a0e75", + "7da2bf6e2e09f9da7e1f60af26b0e94649ada55da00bdc8a7d8b4eaf72fe69bb", + "0000000000000000000000000000000000000000000000000000000000000003", + } + + privs := make([]*btcec.PrivateKey, len(scalars)) + pubs := make([]*btcec.PublicKey, len(scalars)) + for i, hexStr := range scalars { + raw, err := hex.DecodeString(hexStr) + require.NoError(t, err) + + priv, pub := btcec.PrivKeyFromBytes(raw) + privs[i] = priv + pubs[i] = pub + } + + return privs, pubs +} + +// bip32ChildTweak computes the per-step BIP-32 tweak (the IL half of the +// HMAC-SHA512) for an unhardened child derivation. +func bip32ChildTweak(t *testing.T, parent *hdkeychain.ExtendedKey, + idx uint32) [32]byte { + + t.Helper() + + pub, err := parent.ECPubKey() + require.NoError(t, err) + + var idxBytes [4]byte + binary.BigEndian.PutUint32(idxBytes[:], idx) + + h := hmac.New(sha512.New, parent.ChainCode()) + h.Write(pub.SerializeCompressed()) + h.Write(idxBytes[:]) + ilr := h.Sum(nil) + + var tweak [32]byte + copy(tweak[:], ilr[:32]) + return tweak +} + +// computeTaprootTweak computes BIP-86 (when merkleRoot is nil) or +// taproot-with-merkle-root tap tweak for the given x-only key. +func computeTaprootTweak(t *testing.T, xOnlyKey []byte, + merkleRoot []byte) [32]byte { + + t.Helper() + + hashedTweak := chainhash.TaggedHash( + chainhash.TagTapTweak, xOnlyKey, merkleRoot, + ) + var out [32]byte + copy(out[:], hashedTweak[:]) + return out +} + +// TestFinalize_MuSig2_BIP32Derived_WithGlobalXpub builds a synthetic PSBT +// that exercises the BIP-373 case 4 finalize path: the taproot internal +// key is an unhardened BIP-32 child of the bare MuSig2 aggregate, and the +// PSBT carries the synthetic aggregate xpub via PSBT_GLOBAL_XPUB. The +// test generates fresh nonces and partial signatures programmatically, +// then runs Finalize and verifies the produced witness with the script +// engine. +func TestFinalize_MuSig2_BIP32Derived_WithGlobalXpub(t *testing.T) { + privs, pubs := makeTestParticipants(t) + + // Bare aggregate is the KeyAgg of the three participant keys. + bareAgg, _, _, err := musig2.AggregateKeys(pubs, true) + require.NoError(t, err) + + // Synthetic aggregate xpub: pick a deterministic 32-byte chain code. + chainCode := bytes.Repeat([]byte{0xa5}, 32) + parentFP := []byte{0, 0, 0, 0} + xpubAgg := hdkeychain.NewExtendedKey( + chaincfg.MainNetParams.HDPublicKeyID[:], + bareAgg.PreTweakedKey.SerializeCompressed(), + chainCode, parentFP, 0, 0, false, + ) + + // Derive the internal key at path 1/2. + derivPath := []uint32{1, 2} + + bip32T1 := bip32ChildTweak(t, xpubAgg, derivPath[0]) + xpubChild1, err := xpubAgg.Derive(derivPath[0]) + require.NoError(t, err) + + bip32T2 := bip32ChildTweak(t, xpubChild1, derivPath[1]) + xpubChild2, err := xpubChild1.Derive(derivPath[1]) + require.NoError(t, err) + + internalKey, err := xpubChild2.ECPubKey() + require.NoError(t, err) + + // BIP-86: output key = internal_key + tap_tweak * G, where + // tap_tweak = TaggedHash("TapTweak", x_only(internal_key)). + internalKeyXOnly := schnorr.SerializePubKey(internalKey) + tapTweak := computeTaprootTweak(t, internalKeyXOnly, nil) + + allTweaks := []musig2.KeyTweakDesc{ + {Tweak: bip32T1, IsXOnly: false}, + {Tweak: bip32T2, IsXOnly: false}, + {Tweak: tapTweak, IsXOnly: true}, + } + + // FinalKey of the full tweak chain = the taproot output key. + fullAgg, _, _, err := musig2.AggregateKeys( + pubs, true, musig2.WithKeyTweaks(allTweaks...), + ) + require.NoError(t, err) + outputKey := fullAgg.FinalKey + + // Build a P2TR pkScript for the output key and a dummy spending + // transaction. The transaction sends a single input (referencing an + // arbitrary outpoint) to a single dummy P2WPKH output. + pkScript, err := txscript.PayToTaprootScript(outputKey) + require.NoError(t, err) + + const inputAmount = int64(100_000_000) + prevHash := chainhash.Hash{0xde, 0xad, 0xbe, 0xef} + + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{Hash: prevHash, Index: 0}, + Sequence: 0xfffffffd, + }) + dummyOutScript := append( + []byte{0x00, 0x14}, bytes.Repeat([]byte{1}, 20)..., + ) + tx.AddTxOut(wire.NewTxOut(inputAmount-1000, dummyOutScript)) + + // Compute the sighash that the participants will sign over. + prevFetcher := txscript.NewCannedPrevOutputFetcher( + pkScript, inputAmount, + ) + sigHashes := txscript.NewTxSigHashes(tx, prevFetcher) + sigHash, err := txscript.CalcTaprootSignatureHash( + sigHashes, txscript.SigHashDefault, tx, 0, prevFetcher, + ) + require.NoError(t, err) + var sigHashMsg [32]byte + copy(sigHashMsg[:], sigHash) + + // Each participant generates a (sec, pub) nonce pair. + type nonceEntry struct { + sec [musig2.SecNonceSize]byte + pub [musig2.PubNonceSize]byte + } + nonces := make([]nonceEntry, len(privs)) + for i, priv := range privs { + n, err := musig2.GenNonces( + musig2.WithPublicKey(priv.PubKey()), + musig2.WithNonceCombinedKeyAux(outputKey), + ) + require.NoError(t, err) + nonces[i] = nonceEntry{sec: n.SecNonce, pub: n.PubNonce} + } + + pubNonces := make([][musig2.PubNonceSize]byte, len(nonces)) + for i, n := range nonces { + pubNonces[i] = n.pub + } + combinedNonce, err := musig2.AggregateNonces(pubNonces) + require.NoError(t, err) + + // Each participant computes a partial signature using the full tweak + // chain so the resulting sigs combine under the post-tweak output + // key. + partialSigs := make([]*musig2.PartialSignature, len(privs)) + for i, priv := range privs { + ps, err := musig2.Sign( + nonces[i].sec, priv, combinedNonce, pubs, sigHashMsg, + musig2.WithSortedKeys(), + musig2.WithTweaks(allTweaks...), + ) + require.NoError(t, err) + partialSigs[i] = ps + } + + // Construct the PSBT. + p, err := NewFromUnsignedTx(tx) + require.NoError(t, err) + + updater, err := NewUpdater(p) + require.NoError(t, err) + + require.NoError(t, updater.AddInWitnessUtxo( + &wire.TxOut{Value: inputAmount, PkScript: pkScript}, 0, + )) + + // Add the synthetic aggregate xpub to PSBT_GLOBAL_XPUB. We use a + // zero master fingerprint and an empty path because the xpub *is* + // the master in this synthetic setup. + p.XPubs = append(p.XPubs, XPub{ + ExtendedKey: EncodeExtendedKey(xpubAgg), + MasterKeyFingerprint: 0, + Bip32Path: nil, + }) + + // Internal key + its derivation entry pinning the BIP-32 path. + p.Inputs[0].TaprootInternalKey = internalKeyXOnly + p.Inputs[0].TaprootBip32Derivation = append( + p.Inputs[0].TaprootBip32Derivation, + &TaprootBip32Derivation{ + XOnlyPubKey: internalKeyXOnly, + MasterKeyFingerprint: 0, + Bip32Path: derivPath, + }, + ) + + // MuSig2 fields. + require.NoError(t, updater.AddInMuSig2Participants( + 0, &MuSig2Participants{ + AggregateKey: bareAgg.PreTweakedKey, + Keys: pubs, + }, + )) + + for i, priv := range privs { + require.NoError(t, updater.AddInMuSig2PubNonce( + 0, &MuSig2PubNonce{ + PubKey: priv.PubKey(), + AggregateKey: outputKey, + PubNonce: nonces[i].pub, + }, + )) + require.NoError(t, updater.AddInMuSig2PartialSig( + 0, &MuSig2PartialSig{ + PubKey: priv.PubKey(), + AggregateKey: outputKey, + PartialSig: *partialSigs[i], + }, + )) + } + + // Round-trip through serialize/deserialize to make sure the wire + // form survives, then finalize and verify. + var buf bytes.Buffer + require.NoError(t, p.Serialize(&buf)) + + parsed, err := NewFromRawBytes(bytes.NewReader(buf.Bytes()), false) + require.NoError(t, err) + + require.NoError(t, MaybeFinalizeAll(parsed)) + require.NotNil(t, parsed.Inputs[0].FinalScriptWitness) + + verifyFinalized(t, parsed) +} diff --git a/psbt/finalizer_musig2_test.go b/psbt/finalizer_musig2_test.go new file mode 100644 index 00000000..8795cb82 --- /dev/null +++ b/psbt/finalizer_musig2_test.go @@ -0,0 +1,257 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package psbt + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/txscript/v2" + "github.com/stretchr/testify/require" +) + +// finalizeAndExtract finalizes every input in the parsed PSBT and returns +// the extracted (signed) transaction. +func finalizeAndExtract(t *testing.T, hexStr string) (*Packet, []byte) { + t.Helper() + + raw := mustDecodeHex(t, hexStr) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + require.NoError(t, MaybeFinalizeAll(p)) + + require.Len(t, p.Inputs, 1) + require.NotNil(t, p.Inputs[0].FinalScriptWitness) + + return p, p.Inputs[0].FinalScriptWitness +} + +// verifyFinalized runs the txscript engine over the finalized transaction +// to confirm the produced witness is consensus-valid. +func verifyFinalized(t *testing.T, p *Packet) { + t.Helper() + + finalTx, err := Extract(p) + require.NoError(t, err) + + pInput := p.Inputs[0] + require.NotNil(t, pInput.WitnessUtxo) + + pkScript := pInput.WitnessUtxo.PkScript + amount := pInput.WitnessUtxo.Value + prevFetcher := txscript.NewCannedPrevOutputFetcher(pkScript, amount) + hashCache := txscript.NewTxSigHashes(finalTx, prevFetcher) + + vm, err := txscript.NewEngine( + pkScript, finalTx, 0, txscript.StandardVerifyFlags, nil, + hashCache, amount, prevFetcher, + ) + require.NoError(t, err) + require.NoError(t, vm.Execute()) +} + +// TestFinalize_MuSig2_Case1c_BIP86Keyspend asserts that the BIP-373 case 1c +// PSBT (output key IS the aggregate, BIP-86 keyspend) finalizes to a +// consensus-valid taproot key spend witness. +func TestFinalize_MuSig2_Case1c_BIP86Keyspend(t *testing.T) { + p, witness := finalizeAndExtract(t, findVector(t, "case 1c")) + + // The witness should contain exactly one element: the 64-byte + // BIP-340 Schnorr signature (default sighash, so no flag byte + // appended). Total serialized form is: + // varint(1) || varint(64) || sig(64) = 1 + 1 + 64 = 66 bytes. + require.Len(t, witness, 66) + + verifyFinalized(t, p) +} + +// TestFinalize_MuSig2_Case2c_InternalKeyAggregate asserts that case 2c +// (internal key IS aggregate, BIP-86 tweak — no script tree on the +// taproot output) finalizes to a consensus-valid keyspend witness. The +// vector ships with a pre-aggregated PSBT_IN_TAP_KEY_SIG, which the +// finalizer would normally consume directly; we strip it here to force +// the MuSig2 keyspend path through finalizeMuSig2KeySpend and exercise +// the BIP-86 tweak branch. +func TestFinalize_MuSig2_Case2c_InternalKeyAggregate(t *testing.T) { + raw := mustDecodeHex(t, findVector(t, "case 2c")) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + // Force the MuSig2 finalize path. + p.Inputs[0].TaprootKeySpendSig = nil + + require.NoError(t, MaybeFinalizeAll(p)) + require.NotNil(t, p.Inputs[0].FinalScriptWitness) + require.Len(t, p.Inputs[0].FinalScriptWitness, 66) + + verifyFinalized(t, p) +} + +// TestFinalize_MuSig2_Case3c_TapscriptLeaf asserts that case 3c (key in +// tapscript leaf is aggregate) finalizes to a consensus-valid script +// spend witness of the form [aggSig, leafScript, controlBlock]. The +// vector ships with a pre-aggregated PSBT_IN_TAP_SCRIPT_SIG, which we +// strip to force the new finalizeMuSig2ScriptSpend path. +func TestFinalize_MuSig2_Case3c_TapscriptLeaf(t *testing.T) { + raw := mustDecodeHex(t, findVector(t, "case 3c")) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + // Force the MuSig2 script-spend finalize path. + p.Inputs[0].TaprootScriptSpendSig = nil + + require.NoError(t, MaybeFinalizeAll(p)) + require.NotNil(t, p.Inputs[0].FinalScriptWitness) + + // Decode the witness to check the stack shape: 3 elements (sig, + // script, controlBlock). + finalTx, err := Extract(p) + require.NoError(t, err) + require.Len(t, finalTx.TxIn[0].Witness, 3) + + // First element must be a 64-byte BIP-340 signature (default sighash). + require.Len(t, finalTx.TxIn[0].Witness[0], 64) + + verifyFinalized(t, p) +} + +// TestFinalize_MuSig2_MultipleParticipantRecords asserts that the finalizer +// picks the participants record the partial signatures actually belong to when +// an input carries records for more than one aggregate key. BIP-373 keys +// PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS by the aggregate pubkey, so several +// records may be present, and their order on the input says nothing about which +// one the signatures were produced under. +func TestFinalize_MuSig2_MultipleParticipantRecords(t *testing.T) { + // A record for an unrelated aggregate key. None of the tweak chains the + // finalizer knows about turn this into the key in the script, so it + // must be skipped. + agg, keys := bip373Participants(t) + decoy := &MuSig2Participants{ + AggregateKey: keys[0], + Keys: keys, + } + require.False(t, decoy.AggregateKey.IsEqual(agg)) + + tests := []struct { + name string + records []*MuSig2Participants + }{ + { + name: "decoy record first", + records: []*MuSig2Participants{ + decoy, {AggregateKey: agg, Keys: keys}, + }, + }, + { + name: "decoy record last", + records: []*MuSig2Participants{ + {AggregateKey: agg, Keys: keys}, decoy, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Case 2c is the interesting one: the key in the script + // is the BIP-86 tweak of the record's aggregate, so the + // finalizer cannot find the right record by a plain + // equality check against the partial sig keydata. + raw := mustDecodeHex(t, findVector(t, "case 2c")) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + // Force the MuSig2 keyspend finalize path. + p.Inputs[0].TaprootKeySpendSig = nil + + // Sanity check that we are replacing a single record + // with the one real record plus the decoy. + require.Len(t, p.Inputs[0].MuSig2Participants, 1) + p.Inputs[0].MuSig2Participants = tc.records + + require.NoError(t, MaybeFinalizeAll(p)) + require.Len(t, p.Inputs[0].FinalScriptWitness, 66) + + verifyFinalized(t, p) + }) + } +} + +// TestFinalize_MuSig2_NoMatchingParticipantRecord asserts that an input whose +// participants record cannot account for the key in the script is rejected, +// rather than finalizing to a signature that does not verify. +func TestFinalize_MuSig2_NoMatchingParticipantRecord(t *testing.T) { + _, keys := bip373Participants(t) + + tests := []struct { + name string + mutate func(pInput *PInput) + expectErr string + }{ + { + // The record is for an unrelated aggregate key, so the + // finalizer concludes a tweak must have been applied + // but has no internal key to recover it from. + name: "record for unrelated aggregate key", + mutate: func(pInput *PInput) { + pInput.MuSig2Participants[0].AggregateKey = + keys[0] + }, + expectErr: "without PSBT_IN_TAP_INTERNAL_KEY", + }, + { + // Everything agrees on an aggregate key that is not + // actually the aggregate of the participant keys, so no + // tweak is inferred but the key aggregation does not + // reproduce it either. + name: "aggregate is not the aggregate of the keys", + mutate: func(pInput *PInput) { + pInput.MuSig2Participants[0].AggregateKey = + keys[0] + for _, n := range pInput.MuSig2PubNonces { + n.AggregateKey = keys[0] + } + for _, ps := range pInput.MuSig2PartialSigs { + ps.AggregateKey = keys[0] + } + }, + expectErr: "does not produce the key in the script", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + raw := mustDecodeHex(t, findVector(t, "case 1c")) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + tc.mutate(&p.Inputs[0]) + + err = MaybeFinalizeAll(p) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectErr) + }) + } +} + +// TestFinalize_MuSig2_Case4c_NoXpub asserts that case 4c (internal key +// derived from aggregate via BIP-32) without a PSBT_GLOBAL_XPUB for the +// aggregate is rejected with a clear error. The BIP-373 vector ships with +// a pre-aggregated keyspend sig (which would otherwise let Finalize +// succeed), so we strip it to force the MuSig2 finalize path. The vector +// has no PSBT_GLOBAL_XPUB, so the BIP-328 fallback path triggers — which +// is intentionally not supported because BIP-328 requires per-participant +// chain codes that the BIP-373 vector does not provide. +func TestFinalize_MuSig2_Case4c_NoXpub(t *testing.T) { + raw := mustDecodeHex(t, findVector(t, "case 4c")) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + p.Inputs[0].TaprootKeySpendSig = nil + + err = MaybeFinalizeAll(p) + require.Error(t, err) + require.Contains(t, err.Error(), "no matching PSBT_GLOBAL_XPUB") +} diff --git a/psbt/go.mod b/psbt/go.mod index e192897c..0c49c092 100644 --- a/psbt/go.mod +++ b/psbt/go.mod @@ -6,6 +6,7 @@ require ( github.com/btcsuite/btcd/address/v2 v2.0.0 github.com/btcsuite/btcd/btcec/v2 v2.5.0 github.com/btcsuite/btcd/btcutil/v2 v2.0.0 + github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 github.com/btcsuite/btcd/chainhash/v2 v2.0.0 github.com/btcsuite/btcd/txscript/v2 v2.0.0 github.com/btcsuite/btcd/wire/v2 v2.0.0 @@ -14,7 +15,6 @@ require ( ) require ( - github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 // indirect github.com/btcsuite/btclog v1.0.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect diff --git a/psbt/musig2_test.go b/psbt/musig2_test.go new file mode 100644 index 00000000..601137c9 --- /dev/null +++ b/psbt/musig2_test.go @@ -0,0 +1,1304 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package psbt + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "os" + "strings" + "sync" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/stretchr/testify/require" +) + +// Test data from +// https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki#test-vectors + +const ( + // bip373AggregateKeyHex is the compressed MuSig2 aggregate pubkey used + // throughout the BIP-373 test vectors. + bip373AggregateKeyHex = "030b58e337aa4d3852a8c29387c42408d8" + + "cfbe3a613a5e397e0a9f01a5fb7107d4" + + // bip373AggregateKeyXOnlyHex is the x-only encoding of the same key, + // used to assert that x-only keydata is rejected by the parser. + bip373AggregateKeyXOnlyHex = "0b58e337aa4d3852a8c29387c42408d8" + + "cfbe3a613a5e397e0a9f01a5fb7107d4" + + // bip373TestVectorsPath is the on-disk location of the JSON-encoded + // BIP-373 test vectors. + bip373TestVectorsPath = "testdata/bip-373-test-vectors.json" +) + +// bip373ParticipantHex are the three participant pubkeys (in aggregation +// order) used by every BIP-373 test vector. +var bip373ParticipantHex = []string{ + "02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00", + "024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c", + "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", +} + +// mustParsePubKey parses a hex-encoded compressed pubkey or fails the test. +func mustParsePubKey(t *testing.T, hexStr string) *btcec.PublicKey { + t.Helper() + + raw, err := hex.DecodeString(hexStr) + require.NoError(t, err) + + pk, err := btcec.ParsePubKey(raw) + require.NoError(t, err) + + return pk +} + +// mustDecodeHex decodes a hex string or fails the test. +func mustDecodeHex(t *testing.T, hexStr string) []byte { + t.Helper() + + raw, err := hex.DecodeString(hexStr) + require.NoError(t, err) + + return raw +} + +// bip373Vector is one BIP-373 PSBT test vector as encoded in +// testdata/bip-373-test-vectors.json. Valid is true for vectors that must +// parse cleanly and false for vectors that must be rejected. +type bip373Vector struct { + Name string + Hex string + Valid bool +} + +var ( + bip373VectorsOnce sync.Once + bip373Vectors []bip373Vector +) + +// loadBIP373Vectors reads and decodes the BIP-373 test vector JSON file +// exactly once per test binary invocation. +func loadBIP373Vectors(t *testing.T) []bip373Vector { + t.Helper() + + bip373VectorsOnce.Do(func() { + raw, err := os.ReadFile(bip373TestVectorsPath) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(raw, &bip373Vectors)) + }) + + return bip373Vectors +} + +// bip373ValidVectors returns the subset of BIP-373 test vectors that must +// parse cleanly, in the order they appear in the JSON file. +func bip373ValidVectors(t *testing.T) []bip373Vector { + t.Helper() + + all := loadBIP373Vectors(t) + out := make([]bip373Vector, 0, len(all)) + for _, v := range all { + if v.Valid { + out = append(out, v) + } + } + return out +} + +// bip373InvalidVectors returns the subset of BIP-373 test vectors that must +// be rejected at parse time, in the order they appear in the JSON file. +func bip373InvalidVectors(t *testing.T) []bip373Vector { + t.Helper() + + all := loadBIP373Vectors(t) + out := make([]bip373Vector, 0, len(all)) + for _, v := range all { + if !v.Valid { + out = append(out, v) + } + } + return out +} + +// findVector returns the BIP-373 valid test vector hex blob whose name +// contains the given case-insensitive substring. Fails the test if no +// matching vector exists. +func findVector(t *testing.T, namePrefix string) string { + t.Helper() + + for _, tc := range bip373ValidVectors(t) { + if strings.HasPrefix(tc.Name, namePrefix) { + return tc.Hex + } + } + t.Fatalf("no BIP-373 valid vector matching %q", namePrefix) + return "" +} + +// TestBIP373ValidPsbts asserts that every valid BIP-373 test vector parses +// and round-trips through serialization without modification. +func TestBIP373ValidPsbts(t *testing.T) { + for _, tc := range bip373ValidVectors(t) { + t.Run(tc.Name, func(t *testing.T) { + raw := mustDecodeHex(t, tc.Hex) + + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + var b bytes.Buffer + require.NoError(t, p.Serialize(&b)) + require.Equal(t, raw, b.Bytes()) + }) + } +} + +// TestBIP373InvalidPsbts asserts that every invalid BIP-373 test vector is +// rejected at parse time. +func TestBIP373InvalidPsbts(t *testing.T) { + for _, tc := range bip373InvalidVectors(t) { + t.Run(tc.Name, func(t *testing.T) { + raw := mustDecodeHex(t, tc.Hex) + + _, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.Error(t, err) + }) + } +} + +// TestMuSig2Participants_RoundTrip exercises ReadMuSig2Participants and +// SerializeMuSig2Participants together, asserting the round trip is lossless. +func TestMuSig2Participants_RoundTrip(t *testing.T) { + agg := mustParsePubKey(t, bip373AggregateKeyHex) + + keys := make([]*btcec.PublicKey, len(bip373ParticipantHex)) + for i, h := range bip373ParticipantHex { + keys[i] = mustParsePubKey(t, h) + } + + original := &MuSig2Participants{ + AggregateKey: agg, + Keys: keys, + } + + // Build the value bytes the same way SerializeMuSig2Participants does + // internally and feed them back through the read path. + var value bytes.Buffer + for _, k := range keys { + value.Write(k.SerializeCompressed()) + } + + parsed, err := ReadMuSig2Participants(original.KeyData(), value.Bytes()) + require.NoError(t, err) + require.True(t, parsed.AggregateKey.IsEqual(agg)) + require.Equal(t, len(keys), len(parsed.Keys)) + for i := range keys { + require.True(t, keys[i].IsEqual(parsed.Keys[i])) + } +} + +// TestReadMuSig2Participants_Rejects validates that ReadMuSig2Participants +// rejects malformed keydata and valuedata. +func TestReadMuSig2Participants_Rejects(t *testing.T) { + validValue := mustDecodeHex(t, bip373ParticipantHex[0]) + + tests := []struct { + name string + keyData []byte + value []byte + expectErr error + }{ + { + name: "x-only aggregate key", + keyData: mustDecodeHex(t, bip373AggregateKeyXOnlyHex), + value: validValue, + expectErr: ErrInvalidKeyData, + }, + { + name: "uncompressed aggregate key (65 bytes)", + keyData: append( + []byte{0x04}, make([]byte, 64)..., + ), + value: validValue, + expectErr: ErrInvalidKeyData, + }, + { + name: "empty value", + keyData: mustDecodeHex(t, bip373AggregateKeyHex), + value: []byte{}, + expectErr: ErrInvalidPsbtFormat, + }, + { + name: "value not multiple of 33", + keyData: mustDecodeHex(t, bip373AggregateKeyHex), + value: append(validValue, 0x00), + expectErr: ErrInvalidPsbtFormat, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ReadMuSig2Participants(tc.keyData, tc.value) + require.ErrorIs(t, err, tc.expectErr) + }) + } +} + +// TestMuSig2PubNonce_RoundTrip exercises the nonce read/write path with and +// without a tap leaf hash. +func TestMuSig2PubNonce_RoundTrip(t *testing.T) { + pubKey := mustParsePubKey(t, bip373ParticipantHex[0]) + agg := mustParsePubKey(t, bip373AggregateKeyHex) + + var pubNonce [musig2.PubNonceSize]byte + for i := range pubNonce { + pubNonce[i] = byte(i + 1) + } + + tapLeaf := bytes.Repeat([]byte{0xab}, sha256.Size) + + tests := []struct { + name string + leaf []byte + }{ + {name: "without tap leaf hash", leaf: nil}, + {name: "with tap leaf hash", leaf: tapLeaf}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + original := &MuSig2PubNonce{ + PubKey: pubKey, + AggregateKey: agg, + TapLeafHash: tc.leaf, + PubNonce: pubNonce, + } + + parsed, err := ReadMuSig2PubNonce( + original.KeyData(), pubNonce[:], + ) + require.NoError(t, err) + require.True(t, parsed.PubKey.IsEqual(pubKey)) + require.True(t, parsed.AggregateKey.IsEqual(agg)) + require.Equal(t, tc.leaf, parsed.TapLeafHash) + require.Equal(t, pubNonce, parsed.PubNonce) + }) + } +} + +// TestReadMuSig2PubNonce_Rejects validates input rejection for malformed +// keydata and valuedata. +func TestReadMuSig2PubNonce_Rejects(t *testing.T) { + pubKey := mustDecodeHex(t, bip373ParticipantHex[0]) + agg := mustDecodeHex(t, bip373AggregateKeyHex) + leaf := bytes.Repeat([]byte{0xab}, sha256.Size) + + validKey := append(append([]byte{}, pubKey...), agg...) + validValue := make([]byte, musig2.PubNonceSize) + + tests := []struct { + name string + keyData []byte + value []byte + expectErr error + }{ + { + name: "x-only participant key (32+33 bytes)", + keyData: append(pubKey[1:], agg...), + value: validValue, + expectErr: ErrInvalidKeyData, + }, + { + name: "x-only aggregate key (33+32 bytes)", + keyData: append( + append([]byte{}, pubKey...), agg[1:]..., + ), + value: validValue, + expectErr: ErrInvalidKeyData, + }, + { + name: "keydata with bad leaf hash length (97 bytes)", + keyData: append( + append([]byte{}, validKey...), leaf[:31]..., + ), + value: validValue, + expectErr: ErrInvalidKeyData, + }, + { + name: "value too short (65 bytes)", + keyData: validKey, + value: make([]byte, musig2.PubNonceSize-1), + expectErr: ErrInvalidPsbtFormat, + }, + { + name: "value too long (67 bytes)", + keyData: validKey, + value: make([]byte, musig2.PubNonceSize+1), + expectErr: ErrInvalidPsbtFormat, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ReadMuSig2PubNonce(tc.keyData, tc.value) + require.ErrorIs(t, err, tc.expectErr) + }) + } +} + +// TestMuSig2PartialSig_RoundTrip exercises the partial-sig read/write path +// with and without a tap leaf hash. +func TestMuSig2PartialSig_RoundTrip(t *testing.T) { + pubKey := mustParsePubKey(t, bip373ParticipantHex[0]) + agg := mustParsePubKey(t, bip373AggregateKeyHex) + + // Use a small but non-zero scalar so PartialSignature.Decode succeeds. + value := make([]byte, 32) + value[31] = 0x01 + + tapLeaf := bytes.Repeat([]byte{0xab}, sha256.Size) + + tests := []struct { + name string + leaf []byte + }{ + {name: "without tap leaf hash", leaf: nil}, + {name: "with tap leaf hash", leaf: tapLeaf}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + template := &MuSig2PartialSig{ + PubKey: pubKey, + AggregateKey: agg, + TapLeafHash: tc.leaf, + } + + parsed, err := ReadMuSig2PartialSig( + template.KeyData(), value, + ) + require.NoError(t, err) + require.True(t, parsed.PubKey.IsEqual(pubKey)) + require.True(t, parsed.AggregateKey.IsEqual(agg)) + require.Equal(t, tc.leaf, parsed.TapLeafHash) + + // Re-serialize the partial sig and compare against the + // original 32-byte input. + var buf bytes.Buffer + require.NoError(t, parsed.PartialSig.Encode(&buf)) + require.Equal(t, value, buf.Bytes()) + }) + } +} + +// TestReadMuSig2PartialSig_Rejects validates input rejection for malformed +// keydata and valuedata. +func TestReadMuSig2PartialSig_Rejects(t *testing.T) { + pubKey := mustDecodeHex(t, bip373ParticipantHex[0]) + agg := mustDecodeHex(t, bip373AggregateKeyHex) + + validKey := append(append([]byte{}, pubKey...), agg...) + validValue := make([]byte, 32) + validValue[31] = 0x01 + + tests := []struct { + name string + keyData []byte + value []byte + expectErr error + }{ + { + name: "x-only participant key", + keyData: append(pubKey[1:], agg...), + value: validValue, + expectErr: ErrInvalidKeyData, + }, + { + name: "x-only aggregate key", + keyData: append( + append([]byte{}, pubKey...), agg[1:]..., + ), + value: validValue, + expectErr: ErrInvalidKeyData, + }, + { + name: "value too short (31 bytes)", + keyData: validKey, + value: make([]byte, 31), + expectErr: ErrInvalidPsbtFormat, + }, + { + name: "value too long (33 bytes)", + keyData: validKey, + value: make([]byte, 33), + expectErr: ErrInvalidPsbtFormat, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ReadMuSig2PartialSig(tc.keyData, tc.value) + require.ErrorIs(t, err, tc.expectErr) + }) + } +} + +// TestPInput_DuplicateMuSig2Fields asserts that the deserializer rejects +// duplicate MuSig2 keys (participants, nonces, partial sigs) in the same +// input. We synthesize a malformed input by re-using one of the BIP-373 +// valid hex vectors, doubling the participants field, and parsing the +// result. +func TestPInput_DuplicateMuSig2Fields(t *testing.T) { + // Take case 1a (smallest vector with a participants field) and inject + // a duplicate. The participants record starts with the byte sequence + // 0x22 0x1a (varint key length 0x22 = 34, key type 0x1a). Find that + // record and append a copy. + src := bip373ValidVectors(t)[0].Hex + + // The participants record in case 1a, found by searching for the + // known prefix once. + const recordPrefix = "221a030b58e337aa4d3852a8c29387c42408d8" + + "cfbe3a613a5e397e0a9f01a5fb7107d463" + idx := strings.Index(src, recordPrefix) + require.GreaterOrEqual(t, idx, 0) + + // The record extends from idx through the end of the value (3 + // concatenated 33-byte pubkeys = 99 bytes plus 1 varint length byte + // for the value). 0x63 right after the keydata is the value length + // (0x63 = 99 == 3*33). So total record length = 1 (keylen) + 0x22 + // (34 = key type + 33 byte agg key) + 1 (value len) + 99 = 135 bytes + // = 270 hex chars. + const recordHexLen = 1*2 + 0x22*2 + 1*2 + 0x63*2 + require.GreaterOrEqual(t, len(src)-idx, recordHexLen) + + dup := src[:idx+recordHexLen] + src[idx:idx+recordHexLen] + + src[idx+recordHexLen:] + + raw := mustDecodeHex(t, dup) + + _, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.ErrorIs(t, err, ErrDuplicateKey) +} + +// TestUpdater_AddInMuSig2 exercises the AddIn* MuSig2 updater helpers, +// asserting that values are appended on the input and that duplicate keys +// are rejected. +func TestUpdater_AddInMuSig2(t *testing.T) { + // Start from BIP-373 case 1a (smallest vector with MuSig2 fields) + // and clear the input's MuSig2 fields so we can re-populate them via + // the updater helpers. + raw := mustDecodeHex(t, bip373ValidVectors(t)[0].Hex) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + p.Inputs[0].MuSig2Participants = nil + p.Inputs[0].MuSig2PubNonces = nil + p.Inputs[0].MuSig2PartialSigs = nil + + updater, err := NewUpdater(p) + require.NoError(t, err) + + agg := mustParsePubKey(t, bip373AggregateKeyHex) + pub0 := mustParsePubKey(t, bip373ParticipantHex[0]) + pub1 := mustParsePubKey(t, bip373ParticipantHex[1]) + pub2 := mustParsePubKey(t, bip373ParticipantHex[2]) + keys := []*btcec.PublicKey{pub0, pub1, pub2} + + participants := &MuSig2Participants{ + AggregateKey: agg, Keys: keys, + } + require.NoError(t, updater.AddInMuSig2Participants(0, participants)) + require.Len(t, p.Inputs[0].MuSig2Participants, 1) + + // Re-adding the same record fails with ErrDuplicateKey. + require.ErrorIs(t, + updater.AddInMuSig2Participants(0, participants), + ErrDuplicateKey, + ) + + // Out-of-range input index is rejected. + require.ErrorIs(t, + updater.AddInMuSig2Participants(99, participants), + ErrInvalidPsbtFormat, + ) + + // Add three nonces and three partial sigs. + var dummyNonce [musig2.PubNonceSize]byte + for i := range dummyNonce { + dummyNonce[i] = byte(i + 1) + } + for _, k := range keys { + require.NoError(t, updater.AddInMuSig2PubNonce( + 0, &MuSig2PubNonce{ + PubKey: k, AggregateKey: agg, + PubNonce: dummyNonce, + }, + )) + } + require.Len(t, p.Inputs[0].MuSig2PubNonces, 3) + + // Duplicate nonce keydata is rejected. + require.ErrorIs(t, + updater.AddInMuSig2PubNonce( + 0, &MuSig2PubNonce{ + PubKey: pub0, AggregateKey: agg, + PubNonce: dummyNonce, + }, + ), + ErrDuplicateKey, + ) +} + +// TestUpdater_AddOutMuSig2Participants exercises the AddOutMuSig2Participants +// helper. +func TestUpdater_AddOutMuSig2Participants(t *testing.T) { + // Build a minimal one-output PSBT. + raw := mustDecodeHex(t, bip373ValidVectors(t)[0].Hex) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + updater, err := NewUpdater(p) + require.NoError(t, err) + + agg := mustParsePubKey(t, bip373AggregateKeyHex) + keys := []*btcec.PublicKey{ + mustParsePubKey(t, bip373ParticipantHex[0]), + mustParsePubKey(t, bip373ParticipantHex[1]), + mustParsePubKey(t, bip373ParticipantHex[2]), + } + + require.NoError(t, updater.AddOutMuSig2Participants( + 0, &MuSig2Participants{AggregateKey: agg, Keys: keys}, + )) + require.Len(t, p.Outputs[0].MuSig2Participants, 1) + + // Duplicate aggregate is rejected. + require.ErrorIs(t, updater.AddOutMuSig2Participants( + 0, &MuSig2Participants{AggregateKey: agg, Keys: keys}, + ), ErrDuplicateKey) +} + +// musig2Field is the interface implemented by all three MuSig2 PSBT field +// types. It allows the round trip assertions below to be expressed once +// instead of once per field type. +type musig2Field interface { + // Validate asserts the field is sane for being serialized into a PSBT + // packet. + Validate() error + + // KeyData returns the key data of the field's PSBT key-value pair. + KeyData() []byte +} + +// bip373Participants returns the aggregate key and the three participant keys +// (in aggregation order) of the BIP-373 test vectors. +func bip373Participants(t *testing.T) (*btcec.PublicKey, []*btcec.PublicKey) { + t.Helper() + + keys := make([]*btcec.PublicKey, len(bip373ParticipantHex)) + for idx, keyHex := range bip373ParticipantHex { + keys[idx] = mustParsePubKey(t, keyHex) + } + + return mustParsePubKey(t, bip373AggregateKeyHex), keys +} + +// readKVPair reads back a single key-value pair as written by +// serializeKVPairWithType, returning the key type, the key data and the value. +func readKVPair(t *testing.T, r io.Reader) (int, []byte, []byte) { + t.Helper() + + keyType, keyData, err := getKey(r) + require.NoError(t, err) + + value, err := wire.ReadVarBytes(r, 0, MaxPsbtValueLength, "value") + require.NoError(t, err) + + return keyType, keyData, value +} + +// assertMuSig2RoundTrip serializes the given MuSig2 field into a PSBT +// key-value pair, reads that pair back out and then serializes the parsed +// result a second time. Both serializations must produce the exact same bytes, +// which in turn requires the parsed field to pass Validate again. That is the +// property the Validate calls in the Serialize functions exist to uphold: what +// we write must be readable, and what we read must be writable again. +func assertMuSig2RoundTrip[T musig2Field](t *testing.T, typ uint8, field T, + serialize func(io.Writer, uint8, T) error, + read func(keyData, value []byte) (T, error)) T { + + t.Helper() + + var buf bytes.Buffer + require.NoError(t, serialize(&buf, typ, field)) + written := buf.Bytes() + + // The written key must be the type byte followed by exactly the field's + // key data, and the value must survive the varint framing untouched. + keyType, keyData, value := readKVPair(t, bytes.NewReader(written)) + require.EqualValues(t, typ, keyType) + require.Equal(t, field.KeyData(), keyData) + + parsed, err := read(keyData, value) + require.NoError(t, err) + require.NoError(t, parsed.Validate()) + + var reSerialized bytes.Buffer + require.NoError(t, serialize(&reSerialized, typ, parsed)) + require.Equal(t, written, reSerialized.Bytes()) + + return parsed +} + +// assertMuSig2SerializeRejects asserts that serializing an invalid MuSig2 field +// surfaces the expected error from Validate instead of panicking, and that no +// partial record was written to the writer. +func assertMuSig2SerializeRejects[T musig2Field](t *testing.T, typ uint8, + field T, serialize func(io.Writer, uint8, T) error, + expectErr error) { + + t.Helper() + + require.ErrorIs(t, field.Validate(), expectErr) + + var ( + buf bytes.Buffer + err error + ) + require.NotPanics(t, func() { + err = serialize(&buf, typ, field) + }) + require.ErrorIs(t, err, expectErr) + require.Zero(t, buf.Len()) +} + +// TestMuSig2Participants_Validate asserts that Validate accepts every +// participants record that survives an encode/decode round trip, and rejects +// every record that would either panic during serialization or produce a +// key-value pair the read path no longer accepts. +func TestMuSig2Participants_Validate(t *testing.T) { + const typ = uint8(MuSig2ParticipantsInputType) + + agg, keys := bip373Participants(t) + + tests := []struct { + name string + participants *MuSig2Participants + expectErr error + }{ + { + name: "three participants", + participants: &MuSig2Participants{ + AggregateKey: agg, + Keys: keys, + }, + }, + { + name: "single participant", + participants: &MuSig2Participants{ + AggregateKey: agg, + Keys: keys[:1], + }, + }, + { + name: "aggregate key repeated as participant", + participants: &MuSig2Participants{ + AggregateKey: agg, + Keys: append( + []*btcec.PublicKey{agg}, keys..., + ), + }, + }, + { + name: "missing aggregate key", + participants: &MuSig2Participants{ + Keys: keys, + }, + expectErr: ErrMissingKey, + }, + { + name: "no participant keys", + participants: &MuSig2Participants{ + AggregateKey: agg, + }, + expectErr: ErrMissingKey, + }, + { + name: "empty (non-nil) participant keys", + participants: &MuSig2Participants{ + AggregateKey: agg, + Keys: []*btcec.PublicKey{}, + }, + expectErr: ErrMissingKey, + }, + { + name: "nil participant key", + participants: &MuSig2Participants{ + AggregateKey: agg, + Keys: []*btcec.PublicKey{ + keys[0], nil, keys[2], + }, + }, + expectErr: ErrMissingKey, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expectErr != nil { + assertMuSig2SerializeRejects( + t, typ, tc.participants, + SerializeMuSig2Participants, + tc.expectErr, + ) + + return + } + + parsed := assertMuSig2RoundTrip( + t, typ, tc.participants, + SerializeMuSig2Participants, + ReadMuSig2Participants, + ) + + require.True(t, parsed.AggregateKey.IsEqual( + tc.participants.AggregateKey, + )) + require.Len(t, parsed.Keys, len(tc.participants.Keys)) + for idx, key := range tc.participants.Keys { + require.True(t, key.IsEqual(parsed.Keys[idx])) + } + }) + } +} + +// TestMuSig2PubNonce_Validate asserts that Validate accepts every public nonce +// record that survives an encode/decode round trip, and rejects every record +// that would either panic during serialization or produce key data the read +// path no longer accepts. +func TestMuSig2PubNonce_Validate(t *testing.T) { + const typ = uint8(MuSig2PubNoncesInputType) + + agg, keys := bip373Participants(t) + + var pubNonce [musig2.PubNonceSize]byte + for idx := range pubNonce { + pubNonce[idx] = byte(idx + 1) + } + + tapLeaf := bytes.Repeat([]byte{0xab}, sha256.Size) + + tests := []struct { + name string + nonce *MuSig2PubNonce + expectErr error + }{ + { + name: "without tap leaf hash", + nonce: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + PubNonce: pubNonce, + }, + }, + { + name: "with tap leaf hash", + nonce: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: tapLeaf, + PubNonce: pubNonce, + }, + }, + { + // An empty but non-nil tap leaf hash is treated the + // same as an absent one, and comes back as nil. + name: "empty (non-nil) tap leaf hash", + nonce: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: []byte{}, + PubNonce: pubNonce, + }, + }, + { + // The nonce bytes themselves are opaque to the codec, + // so an all-zero nonce still round trips cleanly. + name: "zero pub nonce", + nonce: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + }, + }, + { + name: "aggregate key as participant key", + nonce: &MuSig2PubNonce{ + PubKey: agg, + AggregateKey: agg, + PubNonce: pubNonce, + }, + }, + { + name: "missing participant key", + nonce: &MuSig2PubNonce{ + AggregateKey: agg, + PubNonce: pubNonce, + }, + expectErr: ErrMissingKey, + }, + { + name: "missing aggregate key", + nonce: &MuSig2PubNonce{ + PubKey: keys[0], + PubNonce: pubNonce, + }, + expectErr: ErrMissingKey, + }, + { + name: "tap leaf hash too short", + nonce: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: tapLeaf[:sha256.Size-1], + PubNonce: pubNonce, + }, + expectErr: ErrInvalidTapLeafHash, + }, + { + name: "tap leaf hash too long", + nonce: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: bytes.Repeat( + []byte{0xab}, sha256.Size+1, + ), + PubNonce: pubNonce, + }, + expectErr: ErrInvalidTapLeafHash, + }, + { + name: "single byte tap leaf hash", + nonce: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: []byte{0xab}, + PubNonce: pubNonce, + }, + expectErr: ErrInvalidTapLeafHash, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expectErr != nil { + assertMuSig2SerializeRejects( + t, typ, tc.nonce, + SerializeMuSig2PubNonce, tc.expectErr, + ) + + return + } + + parsed := assertMuSig2RoundTrip( + t, typ, tc.nonce, SerializeMuSig2PubNonce, + ReadMuSig2PubNonce, + ) + + require.True(t, parsed.PubKey.IsEqual(tc.nonce.PubKey)) + require.True(t, parsed.AggregateKey.IsEqual( + tc.nonce.AggregateKey, + )) + require.Equal(t, tc.nonce.PubNonce, parsed.PubNonce) + + if len(tc.nonce.TapLeafHash) == 0 { + require.Empty(t, parsed.TapLeafHash) + } else { + require.Equal( + t, tc.nonce.TapLeafHash, + parsed.TapLeafHash, + ) + } + }) + } +} + +// TestMuSig2PartialSig_Validate asserts that Validate accepts every partial +// signature record that survives an encode/decode round trip, and rejects +// every record that would either panic during serialization or produce key +// data the read path no longer accepts. +// +// The R value of a partial signature is deliberately not required: it is not +// part of the 32-byte serialization, so a parsed record never has it set. +// Requiring it would make any partial signature read from an existing PSBT +// impossible to write back out again. +func TestMuSig2PartialSig_Validate(t *testing.T) { + const typ = uint8(MuSig2PartialSigsInputType) + + agg, keys := bip373Participants(t) + tapLeaf := bytes.Repeat([]byte{0xab}, sha256.Size) + + // A small but non-zero scalar, once paired with the R value that is + // dropped by the serialization and once without it. + sigWithR := musig2.NewPartialSignature( + new(btcec.ModNScalar).SetInt(1), keys[1], + ) + sigWithoutR := musig2.PartialSignature{ + S: new(btcec.ModNScalar).SetInt(1), + } + + tests := []struct { + name string + partialSig *MuSig2PartialSig + expectErr error + }{ + { + name: "without tap leaf hash", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + PartialSig: sigWithR, + }, + }, + { + name: "with tap leaf hash", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: tapLeaf, + PartialSig: sigWithR, + }, + }, + { + // This is what a record parsed out of a PSBT looks + // like, so it must be serializable again. + name: "partial signature without R", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + PartialSig: sigWithoutR, + }, + }, + { + // A zero scalar is not a usable signature, but it is a + // valid serialization that round trips, so Validate + // has no reason to reject it. + name: "zero scalar", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + PartialSig: musig2.PartialSignature{ + S: new(btcec.ModNScalar), + }, + }, + }, + { + name: "empty (non-nil) tap leaf hash", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: []byte{}, + PartialSig: sigWithR, + }, + }, + { + name: "missing participant key", + partialSig: &MuSig2PartialSig{ + AggregateKey: agg, + PartialSig: sigWithR, + }, + expectErr: ErrMissingKey, + }, + { + name: "missing aggregate key", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + PartialSig: sigWithR, + }, + expectErr: ErrMissingKey, + }, + { + name: "missing partial signature", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + }, + expectErr: ErrMissingPartialSignature, + }, + { + // R alone is not enough, S is what gets serialized. + name: "partial signature with R but without S", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + PartialSig: musig2.PartialSignature{ + R: keys[1], + }, + }, + expectErr: ErrMissingPartialSignature, + }, + { + name: "tap leaf hash too short", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: tapLeaf[:sha256.Size-1], + PartialSig: sigWithR, + }, + expectErr: ErrInvalidTapLeafHash, + }, + { + name: "tap leaf hash too long", + partialSig: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: bytes.Repeat( + []byte{0xab}, sha256.Size+1, + ), + PartialSig: sigWithR, + }, + expectErr: ErrInvalidTapLeafHash, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expectErr != nil { + assertMuSig2SerializeRejects( + t, typ, tc.partialSig, + SerializeMuSig2PartialSig, tc.expectErr, + ) + + return + } + + parsed := assertMuSig2RoundTrip( + t, typ, tc.partialSig, + SerializeMuSig2PartialSig, + ReadMuSig2PartialSig, + ) + + require.True(t, parsed.PubKey.IsEqual( + tc.partialSig.PubKey, + )) + require.True(t, parsed.AggregateKey.IsEqual( + tc.partialSig.AggregateKey, + )) + + if len(tc.partialSig.TapLeafHash) == 0 { + require.Empty(t, parsed.TapLeafHash) + } else { + require.Equal( + t, tc.partialSig.TapLeafHash, + parsed.TapLeafHash, + ) + } + + // Only S makes it into the serialization, so R is + // always nil on a parsed record. + require.Equal( + t, tc.partialSig.PartialSig.S, + parsed.PartialSig.S, + ) + require.Nil(t, parsed.PartialSig.R) + }) + } +} + +// TestMuSig2Validate_PreventsBrokenRoundTrip asserts that the records rejected +// by Validate would in fact serialize into PSBT key-value pairs that can no +// longer be parsed. The Serialize functions are bypassed here (the pair is +// written directly) to demonstrate what the Validate calls inside of them +// protect against. +func TestMuSig2Validate_PreventsBrokenRoundTrip(t *testing.T) { + agg, keys := bip373Participants(t) + + readParticipants := func(keyData, value []byte) error { + _, err := ReadMuSig2Participants(keyData, value) + return err + } + readPubNonce := func(keyData, value []byte) error { + _, err := ReadMuSig2PubNonce(keyData, value) + return err + } + readPartialSig := func(keyData, value []byte) error { + _, err := ReadMuSig2PartialSig(keyData, value) + return err + } + + shortLeaf := bytes.Repeat([]byte{0xab}, sha256.Size-1) + longLeaf := bytes.Repeat([]byte{0xab}, sha256.Size+1) + + nonceValue := make([]byte, musig2.PubNonceSize) + partialSigValue := make([]byte, 32) + partialSigValue[31] = 0x01 + + tests := []struct { + name string + typ uint8 + field musig2Field + value []byte + read func(keyData, value []byte) error + expectErr error + }{ + { + // Zero participant keys means a zero-length value, + // which the read path rejects outright. + name: "empty participant list", + typ: uint8(MuSig2ParticipantsInputType), + field: &MuSig2Participants{ + AggregateKey: agg, + }, + value: []byte{}, + read: readParticipants, + expectErr: ErrInvalidPsbtFormat, + }, + { + name: "pub nonce with short tap leaf hash", + typ: uint8(MuSig2PubNoncesInputType), + field: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: shortLeaf, + }, + value: nonceValue, + read: readPubNonce, + expectErr: ErrInvalidKeyData, + }, + { + name: "pub nonce with long tap leaf hash", + typ: uint8(MuSig2PubNoncesInputType), + field: &MuSig2PubNonce{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: longLeaf, + }, + value: nonceValue, + read: readPubNonce, + expectErr: ErrInvalidKeyData, + }, + { + name: "partial sig with short tap leaf hash", + typ: uint8(MuSig2PartialSigsInputType), + field: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: shortLeaf, + }, + value: partialSigValue, + read: readPartialSig, + expectErr: ErrInvalidKeyData, + }, + { + name: "partial sig with long tap leaf hash", + typ: uint8(MuSig2PartialSigsInputType), + field: &MuSig2PartialSig{ + PubKey: keys[0], + AggregateKey: agg, + TapLeafHash: longLeaf, + }, + value: partialSigValue, + read: readPartialSig, + expectErr: ErrInvalidKeyData, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Validate must catch this before anything is written. + require.Error(t, tc.field.Validate()) + + // Now bypass Validate and write the key-value pair the + // field would have produced, then read it back again. + var buf bytes.Buffer + require.NoError(t, serializeKVPairWithType( + &buf, tc.typ, tc.field.KeyData(), tc.value, + )) + + _, keyData, value := readKVPair(t, &buf) + require.ErrorIs( + t, tc.read(keyData, value), tc.expectErr, + ) + }) + } +} + +// TestMuSig2Validate_PreventsPanic asserts that the missing key and missing +// signature cases rejected by Validate would otherwise panic deep inside the +// serialization code, where the nil pointers are dereferenced. That the +// Serialize functions return a clean error for those inputs instead is +// asserted by the per-field Validate tests above. +func TestMuSig2Validate_PreventsPanic(t *testing.T) { + agg, keys := bip373Participants(t) + + var nilKey *btcec.PublicKey + + tests := []struct { + name string + call func() + }{ + { + name: "participants without aggregate key", + call: func() { + p := &MuSig2Participants{Keys: keys} + _ = p.KeyData() + }, + }, + { + // This is the call SerializeMuSig2Participants makes + // for every participant key when building the value. + name: "participants with nil participant key", + call: func() { + _ = nilKey.SerializeCompressed() + }, + }, + { + name: "pub nonce without participant key", + call: func() { + n := &MuSig2PubNonce{AggregateKey: agg} + _ = n.KeyData() + }, + }, + { + name: "pub nonce without aggregate key", + call: func() { + n := &MuSig2PubNonce{PubKey: keys[0]} + _ = n.KeyData() + }, + }, + { + name: "partial sig without participant key", + call: func() { + s := &MuSig2PartialSig{AggregateKey: agg} + _ = s.KeyData() + }, + }, + { + name: "partial sig without aggregate key", + call: func() { + s := &MuSig2PartialSig{PubKey: keys[0]} + _ = s.KeyData() + }, + }, + { + name: "partial sig without S value", + call: func() { + var sig musig2.PartialSignature + _ = sig.Encode(io.Discard) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require.Panics(t, tc.call) + }) + } +} diff --git a/psbt/signer_musig2_test.go b/psbt/signer_musig2_test.go new file mode 100644 index 00000000..c6d54d00 --- /dev/null +++ b/psbt/signer_musig2_test.go @@ -0,0 +1,295 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package psbt + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +// loadCase loads a BIP-373 valid vector by case-name substring and returns +// the parsed packet. Fails the test if no matching vector exists. +func loadCase(t *testing.T, namePrefix string) *Packet { + t.Helper() + + raw := mustDecodeHex(t, findVector(t, namePrefix)) + p, err := NewFromRawBytes(bytes.NewReader(raw), false) + require.NoError(t, err) + + return p +} + +// stripPartialSigs returns the input's pre-loaded MuSig2 partial signatures +// and clears the field on the packet so the test can re-add them via +// SignMuSig2 from a clean slate. +func stripPartialSigs(t *testing.T, p *Packet, + inIndex int) []*MuSig2PartialSig { + + t.Helper() + + require.Less(t, inIndex, len(p.Inputs)) + original := p.Inputs[inIndex].MuSig2PartialSigs + require.NotEmpty(t, original) + + p.Inputs[inIndex].MuSig2PartialSigs = nil + + // Strip any pre-aggregated key/script-spend sigs that would otherwise + // short-circuit finalization away from the MuSig2 path. + p.Inputs[inIndex].TaprootKeySpendSig = nil + p.Inputs[inIndex].TaprootScriptSpendSig = nil + + // Return a shallow copy so callers can safely iterate. + out := make([]*MuSig2PartialSig, len(original)) + copy(out, original) + return out +} + +// reattachAll round-trips every partial sig through SignMuSig2 and asserts +// each call reports SignSuccesful with no error. Returns the updater used, +// so callers can probe further state (e.g. attempt duplicates). +func reattachAll(t *testing.T, p *Packet, sigs []*MuSig2PartialSig) *Updater { + t.Helper() + + updater, err := NewUpdater(p) + require.NoError(t, err) + + for i, sig := range sigs { + outcome, err := updater.SignMuSig2(0, sig) + require.NoError(t, err, "partial sig %d", i) + require.Equal(t, SignOutcome(SignSuccesful), outcome) + } + + return updater +} + +// TestSignMuSig2_Case1c_OutputKeyIsAggregate exercises the case where the +// taproot output key IS the MuSig2 aggregate (BIP-86 keyspend). After +// re-attaching the partial sigs via SignMuSig2 the resulting PSBT must +// finalize to a consensus-valid witness. +func TestSignMuSig2_Case1c_OutputKeyIsAggregate(t *testing.T) { + p := loadCase(t, "case 1c") + sigs := stripPartialSigs(t, p, 0) + + reattachAll(t, p, sigs) + require.Len(t, p.Inputs[0].MuSig2PartialSigs, len(sigs)) + + require.NoError(t, MaybeFinalizeAll(p)) + verifyFinalized(t, p) +} + +// TestSignMuSig2_Case2c_InternalKeyIsAggregate exercises the case where the +// taproot internal key IS the MuSig2 aggregate and the output commits to a +// merkle root (or BIP-86 with no merkle root). +func TestSignMuSig2_Case2c_InternalKeyIsAggregate(t *testing.T) { + p := loadCase(t, "case 2c") + sigs := stripPartialSigs(t, p, 0) + + reattachAll(t, p, sigs) + require.Len(t, p.Inputs[0].MuSig2PartialSigs, len(sigs)) + + require.NoError(t, MaybeFinalizeAll(p)) + verifyFinalized(t, p) +} + +// TestSignMuSig2_Case3c_TapscriptLeaf exercises the tapscript-leaf MuSig2 +// path: each partial sig carries a non-empty TapLeafHash referencing a leaf +// on the taproot input. +func TestSignMuSig2_Case3c_TapscriptLeaf(t *testing.T) { + p := loadCase(t, "case 3c") + sigs := stripPartialSigs(t, p, 0) + + // Sanity-check the test fixture: tapscript-leaf vectors must have a + // tap leaf hash on their partial sigs, otherwise we'd be testing the + // wrong path. + require.NotEmpty(t, sigs[0].TapLeafHash) + + reattachAll(t, p, sigs) + require.Len(t, p.Inputs[0].MuSig2PartialSigs, len(sigs)) + + require.NoError(t, MaybeFinalizeAll(p)) + verifyFinalized(t, p) +} + +// TestSignMuSig2_Case4c_BIP32DerivedAggregate exercises the BIP-32 derived +// aggregate path. The BIP-373 case 4c vector does not ship with a +// PSBT_GLOBAL_XPUB, so the finalizer is expected to reject the finalize +// step; SignMuSig2 itself only enforces signer-role invariants and must +// happily attach the partial sigs. +func TestSignMuSig2_Case4c_BIP32DerivedAggregate(t *testing.T) { + p := loadCase(t, "case 4c") + sigs := stripPartialSigs(t, p, 0) + + reattachAll(t, p, sigs) + require.Len(t, p.Inputs[0].MuSig2PartialSigs, len(sigs)) +} + +// TestSignMuSig2_AlreadyFinalized asserts SignMuSig2 short-circuits and +// returns SignFinalized when the input has already been finalized. +func TestSignMuSig2_AlreadyFinalized(t *testing.T) { + p := loadCase(t, "case 1c") + sigs := p.Inputs[0].MuSig2PartialSigs + + require.NoError(t, MaybeFinalizeAll(p)) + require.NotNil(t, p.Inputs[0].FinalScriptWitness) + + updater, err := NewUpdater(p) + require.NoError(t, err) + + outcome, err := updater.SignMuSig2(0, sigs[0]) + require.NoError(t, err) + require.Equal(t, SignOutcome(SignFinalized), outcome) +} + +// TestSignMuSig2_OutOfRangeInput asserts that an out-of-range input index +// is rejected with ErrInvalidPsbtFormat and SignInvalid. +func TestSignMuSig2_OutOfRangeInput(t *testing.T) { + p := loadCase(t, "case 1c") + sigs := stripPartialSigs(t, p, 0) + + updater, err := NewUpdater(p) + require.NoError(t, err) + + outcome, err := updater.SignMuSig2(99, sigs[0]) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) + + outcome, err = updater.SignMuSig2(-1, sigs[0]) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) +} + +// TestSignMuSig2_NilArgs asserts that nil partialSig / nil sub-fields are +// rejected. +func TestSignMuSig2_NilArgs(t *testing.T) { + p := loadCase(t, "case 1c") + stripPartialSigs(t, p, 0) + + updater, err := NewUpdater(p) + require.NoError(t, err) + + outcome, err := updater.SignMuSig2(0, nil) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) + + outcome, err = updater.SignMuSig2(0, &MuSig2PartialSig{}) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) +} + +// TestSignMuSig2_MissingWitnessUtxo asserts that a taproot input without a +// witness UTXO is rejected (the finalizer needs it to recompute sighashes). +func TestSignMuSig2_MissingWitnessUtxo(t *testing.T) { + p := loadCase(t, "case 1c") + sigs := stripPartialSigs(t, p, 0) + + p.Inputs[0].WitnessUtxo = nil + + updater, err := NewUpdater(p) + require.NoError(t, err) + + outcome, err := updater.SignMuSig2(0, sigs[0]) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) +} + +// TestSignMuSig2_ParticipantNotInList asserts that a partial sig whose +// participant pubkey is not registered under the supplied aggregate key is +// rejected with ErrInvalidSignatureForInput. +func TestSignMuSig2_ParticipantNotInList(t *testing.T) { + p := loadCase(t, "case 1c") + sigs := stripPartialSigs(t, p, 0) + + // Replace the participant pubkey with an unrelated key while keeping + // the rest of the partial sig intact. + bogus := mustParsePubKey( + t, "020000000000000000000000000000000000000000000000000000000"+ + "000000003", + ) + rogue := *sigs[0] + rogue.PubKey = bogus + + updater, err := NewUpdater(p) + require.NoError(t, err) + + outcome, err := updater.SignMuSig2(0, &rogue) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrInvalidSignatureForInput) +} + +// TestSignMuSig2_MissingNonce asserts that attempting to attach a partial +// sig without the matching pub nonce is rejected. +func TestSignMuSig2_MissingNonce(t *testing.T) { + p := loadCase(t, "case 1c") + sigs := stripPartialSigs(t, p, 0) + + // Wipe all nonces — every partial sig will now lack a matching one. + p.Inputs[0].MuSig2PubNonces = nil + + updater, err := NewUpdater(p) + require.NoError(t, err) + + outcome, err := updater.SignMuSig2(0, sigs[0]) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrInvalidSignatureForInput) +} + +// TestSignMuSig2_UnknownTapLeafHash asserts that a tap-leaf partial sig +// whose TapLeafHash doesn't resolve to any leaf script on the input is +// rejected. We start from case 3c (which has tap leaf hashes set) and +// rewrite the hash to one that won't match. +func TestSignMuSig2_UnknownTapLeafHash(t *testing.T) { + p := loadCase(t, "case 3c") + sigs := stripPartialSigs(t, p, 0) + + bogus := bytes.Repeat([]byte{0xff}, 32) + + // Rewrite the partial sig to carry the bogus hash. The nonce on the + // input still has the original hash, so even if the leaf check were + // skipped this would also fail the nonce-match check — but the leaf + // check fires first because we additionally rewrite the matching + // nonce so the nonce check passes. + rogueSig := *sigs[0] + rogueSig.TapLeafHash = bogus + roguePartialSig := &MuSig2PartialSig{ + PubKey: rogueSig.PubKey, + AggregateKey: rogueSig.AggregateKey, + TapLeafHash: rogueSig.TapLeafHash, + } + + for _, n := range p.Inputs[0].MuSig2PubNonces { + if bytes.Equal(n.KeyData(), roguePartialSig.KeyData()) { + n.TapLeafHash = bogus + } + } + + updater, err := NewUpdater(p) + require.NoError(t, err) + + outcome, err := updater.SignMuSig2(0, &rogueSig) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrInvalidSignatureForInput) +} + +// TestSignMuSig2_Duplicate asserts that re-attaching the same partial sig +// is rejected with ErrDuplicateKey via the underlying AddInMuSig2PartialSig +// helper. +func TestSignMuSig2_Duplicate(t *testing.T) { + p := loadCase(t, "case 1c") + sigs := stripPartialSigs(t, p, 0) + + updater, err := NewUpdater(p) + require.NoError(t, err) + + // First attach succeeds, second attach must be rejected. + outcome, err := updater.SignMuSig2(0, sigs[0]) + require.NoError(t, err) + require.Equal(t, SignOutcome(SignSuccesful), outcome) + + outcome, err = updater.SignMuSig2(0, sigs[0]) + require.Equal(t, SignOutcome(SignInvalid), outcome) + require.ErrorIs(t, err, ErrDuplicateKey) +} diff --git a/psbt/testdata/bip-373-test-vectors.json b/psbt/testdata/bip-373-test-vectors.json new file mode 100644 index 00000000..2a5d8af3 --- /dev/null +++ b/psbt/testdata/bip-373-test-vectors.json @@ -0,0 +1,122 @@ +[ + { + "name": "case 1a: output key is aggregate, participant pubkeys only", + "hex": "70736274ff01005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd65592221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90000", + "valid": true + }, + { + "name": "case 1b: output key is aggregate, with all pubnonces", + "hex": "70736274ff01005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd65592221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9431b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44202529b19d7879ccc04c915487f5f1341bc6858b0bc74e5036c643d37b53a4371b503b7f8afe3263fcb3ef2454fe16f3a6759c5600c78e637b8dfa0d83e8552882056431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44203a81d973499e74f82d2b5ab13f1e69e9e11a5fdb40f5466ef672e6e4ceba0be7903499593767150394eece384d051c45b615c52dbe1c8eb3610d7b344b43b7dad6d431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d442033949dbff31b9d50251d8821e66cc5c97b7e3afd7d95bfdc77500fda8947f3ae0023f20e4f0bf0b76f0d40cd3abfe7ce373d6685bbd77b5fa28e7f49fb429d189b00000", + "valid": true + }, + { + "name": "case 1c: output key is aggregate, with all partial sigs", + "hex": "70736274ff01005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd65592221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9431b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44202529b19d7879ccc04c915487f5f1341bc6858b0bc74e5036c643d37b53a4371b503b7f8afe3263fcb3ef2454fe16f3a6759c5600c78e637b8dfa0d83e8552882056431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44203a81d973499e74f82d2b5ab13f1e69e9e11a5fdb40f5466ef672e6e4ceba0be7903499593767150394eece384d051c45b615c52dbe1c8eb3610d7b344b43b7dad6d431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d442033949dbff31b9d50251d8821e66cc5c97b7e3afd7d95bfdc77500fda8947f3ae0023f20e4f0bf0b76f0d40cd3abfe7ce373d6685bbd77b5fa28e7f49fb429d189b0431c02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4200e57ca4ca0de1a3116d3fd6baf19d38572e47e8ff024e7efc39512751e54ed31431c024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4207254079cab166b0d50b54283fccb4aea15f776747a5d2a53d7da06239340dbcc431c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d42001245e8861e62ac5bfc0008418fd057ce6b03c17b1d6b5c6980413c5c4e358970000", + "valid": true + }, + { + "name": "case 2a: internal key is aggregate, participant pubkeys only", + "hex": "70736274ff01005202000000015818a9cd644b369c306c7fb191ec014ff625e63c283f00f9d17a959fefa3e8f60000000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251202967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a3494221160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd655920117200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90000", + "valid": true + }, + { + "name": "case 2b: internal key is aggregate, with all pubnonces", + "hex": "70736274ff01005202000000015818a9cd644b369c306c7fb191ec014ff625e63c283f00f9d17a959fefa3e8f60000000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251202967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a3494221160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd655920117200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9431b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349424203cc17485ca01c2ebb0eba1c80b3eadaf5ee9cc14629fa4f1071d19820e2d07fcc02adfcad45a68cfb67759c9849cdd579639e4d1867272b991a0363d1ca76283efc431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349424202343bb8359c434f5f6a9d6b46f7573258fbfbb561bb5212cbf5850a82a9a02f7e0246e83221664427aca6d13c10809a2cb4b54dae9a7b2f5ab764213dd479a6a942431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a3494242039dee4258b8dfe34460086ff1703209e478437c6ab0f598a2e6e809fd30a9ff3a03e06b4e04ec4de4f757c84d51acdaf6cb1ef4bccbfd8103703bc01a845dcf33650000", + "valid": true + }, + { + "name": "case 2c: internal key is aggregate, with all partial sigs", + "hex": "70736274ff01005202000000015818a9cd644b369c306c7fb191ec014ff625e63c283f00f9d17a959fefa3e8f60000000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251202967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349420113402e89a7bdf9085c6438d15ddf1a86772a65222244276e9302ffdd9fa93b1c20ae58a6b11a6be98b151d8582daa84c10017c994d9235b13ec518a94782c67c40e221160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd655920117200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9431b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349424203cc17485ca01c2ebb0eba1c80b3eadaf5ee9cc14629fa4f1071d19820e2d07fcc02adfcad45a68cfb67759c9849cdd579639e4d1867272b991a0363d1ca76283efc431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349424202343bb8359c434f5f6a9d6b46f7573258fbfbb561bb5212cbf5850a82a9a02f7e0246e83221664427aca6d13c10809a2cb4b54dae9a7b2f5ab764213dd479a6a942431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a3494242039dee4258b8dfe34460086ff1703209e478437c6ab0f598a2e6e809fd30a9ff3a03e06b4e04ec4de4f757c84d51acdaf6cb1ef4bccbfd8103703bc01a845dcf3365431c02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a3494220109ad32155722014ddca21ae3b8d3c3a93a6d0ab5c7e19d0c693d565c47c1626431c024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349422035d5eecc404fa2a63644f30cf8af43fdbd829e5cd9c74707ca9b33a9134c756e431c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9032967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a3494220b9c855defd44676ff3fc5342adbc90c2dec15624eab55b1ff29765315603db6e0000", + "valid": true + }, + { + "name": "case 3a: key in script is aggregate, participant pubkeys only", + "hex": "70736274ff01005202000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f5050000000022512056bdb481b4d67103f6d5dea8a9aafd3684a6a79b4a2e247799db8d4b1a86e1f82215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac023200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4acc021160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d42501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c2680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd002501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c2501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2cc3249a82211650929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac005007c461e5d2116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f92501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c7dd6559201172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0011820b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90000", + "valid": true + }, + { + "name": "case 3b: key in script is aggregate, with all pubnonces", + "hex": "70736274ff01005202000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f5050000000022512056bdb481b4d67103f6d5dea8a9aafd3684a6a79b4a2e247799db8d4b1a86e1f82215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac023200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4acc021160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d42501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c2680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd002501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c2501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2cc3249a82211650929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac005007c461e5d2116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f92501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c7dd6559201172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0011820b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9631b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202d99e7c8719b3ad08566b0cb9c7d5eda3127c9e8119185b7d584d939b173915f50240df22aab78332cf0f25329d103dc0d2060a03742e9448026e736bcf3db98f3c631b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202e702a7075226a0d9313a77e6db10b8e742d4cffdc948a0edc9b856c13b412e5403386a0298f308fb3099155772e45b2aa8e770f435bedfe2041d1cc4d3d37538c6631b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202b1d912d45d753ceed0954417ba982656d2aec53f8638bd6f297dae3b743d71b0032f424537d599d28f14d59fe0a11b82fea2aa226a2980ffdacad5fdab20f806830000", + "valid": true + }, + { + "name": "case 3c: key in script is aggregate, with all partial sigs", + "hex": "70736274ff01005202000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f5050000000022512056bdb481b4d67103f6d5dea8a9aafd3684a6a79b4a2e247799db8d4b1a86e1f841140b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c402667d52f6cc07fe06db31b1a5f7efe81903f9cbeef40fa64dafca01d2cb1d56403bc7504898e55872557d16d2ca79bc55fef10973841a33ec032d884758c9fe62215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac023200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4acc021160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d42501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c2680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd002501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c2501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2cc3249a82211650929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac005007c461e5d2116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f92501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c7dd6559201172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0011820b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9631b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202d99e7c8719b3ad08566b0cb9c7d5eda3127c9e8119185b7d584d939b173915f50240df22aab78332cf0f25329d103dc0d2060a03742e9448026e736bcf3db98f3c631b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202e702a7075226a0d9313a77e6db10b8e742d4cffdc948a0edc9b856c13b412e5403386a0298f308fb3099155772e45b2aa8e770f435bedfe2041d1cc4d3d37538c6631b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202b1d912d45d753ceed0954417ba982656d2aec53f8638bd6f297dae3b743d71b0032f424537d599d28f14d59fe0a11b82fea2aa226a2980ffdacad5fdab20f80683631c02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c20ada78b70af8cffa4c50863aef515ac58a327cd58c55bad29a162e67d9c413322631c024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c201fa8b7e6741c21eb8d9b0f65269df9e423369fc390ad2e918420c57feb2a1b61631c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c20366c31ad65e533f6d2b45e5910f3f587543f8061918167bf5a818b13be5792a40000", + "valid": true + }, + { + "name": "case 4a: internal key derived from aggregate, participant pubkeys only", + "hex": "70736274ff01005202000000012589e7767958ba154f9018cccf0dedea6147bb60cd1a194b6e3590a9965690d60100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f50500000000225120d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa42116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a8221168dd96ab858b259c518218c014a46eb4e6ac899e51c675ef774fbb68a8799ce2f0d002680dd6e01000000020000002116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd655920117208dd96ab858b259c518218c014a46eb4e6ac899e51c675ef774fbb68a8799ce2f221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90000", + "valid": true + }, + { + "name": "case 4b: internal key derived from aggregate, with all pubnonces", + "hex": "70736274ff01005202000000012589e7767958ba154f9018cccf0dedea6147bb60cd1a194b6e3590a9965690d60100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f50500000000225120d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa42116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a8221168dd96ab858b259c518218c014a46eb4e6ac899e51c675ef774fbb68a8799ce2f0d002680dd6e01000000020000002116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd655920117208dd96ab858b259c518218c014a46eb4e6ac899e51c675ef774fbb68a8799ce2f221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9431b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd0002d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa442024eefc9fdd12be74746485c678b8268949cc9236e9fce82241395523af04262bf038c276e832aad4bfb9e90485162f574c8b2619df5bd19db8ffa2eb0059493bf1b431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa44203a324a3f4221bfab2b6fa0786a7048f3345ce344f1458e151ba187b59aab498cb02a3638259ba4ccc46fe79d8d79a4d9e704ec0f7701b5e664d4d8733838af49196431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f902d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa44202c12eb4ef8760321c072a51d8fb65c34ee8b452a14818557f61df061285a5809103a08e41b262942c6e5932fd1fb5d99236897989f6e912dc21476d2771874114930000", + "valid": true + }, + { + "name": "case 4c: internal key derived from aggregate, with all partial sigs", + "hex": "70736274ff01005202000000012589e7767958ba154f9018cccf0dedea6147bb60cd1a194b6e3590a9965690d60100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f50500000000225120d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa40113409e39897ac2ffe27525dc460f8584fddd11fe9a97ce2e50c1489b8c1a4e92fcc07e48db63a1a4ccb9d297537d0c038838378bbf278de7aa1a128995d1625cc5cd2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a8221168dd96ab858b259c518218c014a46eb4e6ac899e51c675ef774fbb68a8799ce2f0d002680dd6e01000000020000002116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd655920117208dd96ab858b259c518218c014a46eb4e6ac899e51c675ef774fbb68a8799ce2f221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9431b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd0002d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa442024eefc9fdd12be74746485c678b8268949cc9236e9fce82241395523af04262bf038c276e832aad4bfb9e90485162f574c8b2619df5bd19db8ffa2eb0059493bf1b431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa44203a324a3f4221bfab2b6fa0786a7048f3345ce344f1458e151ba187b59aab498cb02a3638259ba4ccc46fe79d8d79a4d9e704ec0f7701b5e664d4d8733838af49196431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f902d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa44202c12eb4ef8760321c072a51d8fb65c34ee8b452a14818557f61df061285a5809103a08e41b262942c6e5932fd1fb5d99236897989f6e912dc21476d277187411493431c02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd0002d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa420657db286be14e80ece0dd84b4d17c4dc414c3e56bc8cef0827b061b4dd2c8f43431c024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa420e78552fb4ce9b2d00e1ed0e2cb98e087199127477e8f1a1c681a00b9c4ce7098431c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f902d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa420cbcc95786a1c674d48d8ac52e322dd7c6eabac3bfc4be79b750de28df82e2f730000", + "valid": true + }, + { + "name": "case 5: receiving, internal key is aggregate", + "hex": "70736274ff01007d02000000012589e7767958ba154f9018cccf0dedea6147bb60cd1a194b6e3590a9965690d60000000000fdffffff0280969800000000002251202967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349420fc0927c00000000160014349c5d330278c3002a64f597d2b01aa3dc1bd903000000000001007d02000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370000000000fdffffff02895c2b7d00000000160014cfd98ba1027ea4ed4bd2ae1b348b6156a015037500e1f50500000000225120d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa4e100000001011f895c2b7d00000000160014cfd98ba1027ea4ed4bd2ae1b348b6156a0150375220602a66650f08bffa4f089eb22edcdbe7616645ff6cd180a36484d4bc81054595b7b18bfff44a3540000800100008000000080010000008a020000000105200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421070b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2107346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721074fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822107f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd655922208030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f900220203be4ae53d37cc075f40b30d6da4729d40adeeb983533af1616192bc76d5b2612a18bfff44a3540000800100008000000080010000008d02000000", + "valid": true + }, + { + "name": "case 6: receiving, internal key derived from aggregate", + "hex": "70736274ff01007d0200000001f835a5ec8e4008f96f17407e13f0c34912c39d27620800fae02baf86b8a78e760000000000fdffffff028096980000000000225120d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa49e405d05000000001600149f94ac2db46420b95dc0db1cc8f4bec0bb9234d8000000000001005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd0000000001011f18ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd220603293c8d8dc47b712d7c13a5d0536b7f2e3193267e60f7e66e563939dbe507479c18bfff44a35400008001000080000000800000000097010000000105208dd96ab858b259c518218c014a46eb4e6ac899e51c675ef774fbb68a8799ce2f2107346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721074fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a8221078dd96ab858b259c518218c014a46eb4e6ac899e51c675ef774fbb68a8799ce2f0d002680dd6e01000000020000002107f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd655922208030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f900220202ce69de967622b1c82eb0a0f80ceb0573a0f768b02fdb047b13c171e79174fcce18bfff44a3540000800100008000000080010000008e02000000", + "valid": true + }, + { + "name": "invalid 1: x-only aggregate in input participant pubkeys keydata", + "hex": "70736274ff01005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd65592211a0b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90000", + "valid": false + }, + { + "name": "invalid 2: x-only input participant pubkey", + "hex": "70736274ff01005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd65592221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d462346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90000", + "valid": false + }, + { + "name": "invalid 3: x-only aggregate in output participant pubkeys keydata", + "hex": "70736274ff01007d02000000012589e7767958ba154f9018cccf0dedea6147bb60cd1a194b6e3590a9965690d60000000000fdffffff0280969800000000002251202967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349420fc0927c00000000160014349c5d330278c3002a64f597d2b01aa3dc1bd903000000000001007d02000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370000000000fdffffff02895c2b7d00000000160014cfd98ba1027ea4ed4bd2ae1b348b6156a015037500e1f50500000000225120d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa4e100000001011f895c2b7d00000000160014cfd98ba1027ea4ed4bd2ae1b348b6156a0150375220602a66650f08bffa4f089eb22edcdbe7616645ff6cd180a36484d4bc81054595b7b18bfff44a3540000800100008000000080010000008a020000000105200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421070b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2107346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721074fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822107f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd6559221080b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f900220203be4ae53d37cc075f40b30d6da4729d40adeeb983533af1616192bc76d5b2612a18bfff44a3540000800100008000000080010000008d02000000", + "valid": false + }, + { + "name": "invalid 4: x-only output participant pubkey", + "hex": "70736274ff01007d02000000012589e7767958ba154f9018cccf0dedea6147bb60cd1a194b6e3590a9965690d60000000000fdffffff0280969800000000002251202967d2d020a9795da72b51be4f3fca25bb0e57e91c5b3e7a81abfa7232a349420fc0927c00000000160014349c5d330278c3002a64f597d2b01aa3dc1bd903000000000001007d02000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370000000000fdffffff02895c2b7d00000000160014cfd98ba1027ea4ed4bd2ae1b348b6156a015037500e1f50500000000225120d0b226c6599f273874df8fe684ab6c3028081bee8a2cbed31a136f5865f6cfa4e100000001011f895c2b7d00000000160014cfd98ba1027ea4ed4bd2ae1b348b6156a0150375220602a66650f08bffa4f089eb22edcdbe7616645ff6cd180a36484d4bc81054595b7b18bfff44a3540000800100008000000080010000008a020000000105200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421070b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2107346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721074fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822107f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd6559221080b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f900220203be4ae53d37cc075f40b30d6da4729d40adeeb983533af1616192bc76d5b2612a18bfff44a3540000800100008000000080010000008d02000000", + "valid": false + }, + { + "name": "invalid 5: x-only aggregate in public nonce keydata", + "hex": "70736274ff01005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd65592221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9421b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44202529b19d7879ccc04c915487f5f1341bc6858b0bc74e5036c643d37b53a4371b503b7f8afe3263fcb3ef2454fe16f3a6759c5600c78e637b8dfa0d83e8552882056431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44203a81d973499e74f82d2b5ab13f1e69e9e11a5fdb40f5466ef672e6e4ceba0be7903499593767150394eece384d051c45b615c52dbe1c8eb3610d7b344b43b7dad6d431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d442033949dbff31b9d50251d8821e66cc5c97b7e3afd7d95bfdc77500fda8947f3ae0023f20e4f0bf0b76f0d40cd3abfe7ce373d6685bbd77b5fa28e7f49fb429d189b00000", + "valid": false + }, + { + "name": "invalid 6: x-only participant pubkey in public nonce keydata", + "hex": "70736274ff01005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd65592221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9421b346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44202529b19d7879ccc04c915487f5f1341bc6858b0bc74e5036c643d37b53a4371b503b7f8afe3263fcb3ef2454fe16f3a6759c5600c78e637b8dfa0d83e8552882056431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44203a81d973499e74f82d2b5ab13f1e69e9e11a5fdb40f5466ef672e6e4ceba0be7903499593767150394eece384d051c45b615c52dbe1c8eb3610d7b344b43b7dad6d431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d442033949dbff31b9d50251d8821e66cc5c97b7e3afd7d95bfdc77500fda8947f3ae0023f20e4f0bf0b76f0d40cd3abfe7ce373d6685bbd77b5fa28e7f49fb429d189b00000", + "valid": false + }, + { + "name": "invalid 7: invalid public nonce valuedata length", + "hex": "70736274ff01005202000000015686dff400165f4e040a5855f658093472c9bcf8108b272a5d31f181f7b4ffb10100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f505000000002251200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d421160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d405002680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000500580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c0500c3249a822116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f905007dd65592221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9431b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44102529b19d7879ccc04c915487f5f1341bc6858b0bc74e5036c643d37b53a4371b503b7f8afe3263fcb3ef2454fe16f3a6759c5600c78e637b8dfa0d83e85528820431b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d44203a81d973499e74f82d2b5ab13f1e69e9e11a5fdb40f5466ef672e6e4ceba0be7903499593767150394eece384d051c45b615c52dbe1c8eb3610d7b344b43b7dad6d431b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d442033949dbff31b9d50251d8821e66cc5c97b7e3afd7d95bfdc77500fda8947f3ae0023f20e4f0bf0b76f0d40cd3abfe7ce373d6685bbd77b5fa28e7f49fb429d189b00000", + "valid": false + }, + { + "name": "invalid 8: x-only aggregate in partial sig keydata", + "hex": "70736274ff01005202000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f5050000000022512056bdb481b4d67103f6d5dea8a9aafd3684a6a79b4a2e247799db8d4b1a86e1f841140b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c402667d52f6cc07fe06db31b1a5f7efe81903f9cbeef40fa64dafca01d2cb1d56403bc7504898e55872557d16d2ca79bc55fef10973841a33ec032d884758c9fe62215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac023200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4acc021160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d42501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c2680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd002501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c2501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2cc3249a82211650929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac005007c461e5d2116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f92501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c7dd6559201172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0011820b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9631b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202d99e7c8719b3ad08566b0cb9c7d5eda3127c9e8119185b7d584d939b173915f50240df22aab78332cf0f25329d103dc0d2060a03742e9448026e736bcf3db98f3c631b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202e702a7075226a0d9313a77e6db10b8e742d4cffdc948a0edc9b856c13b412e5403386a0298f308fb3099155772e45b2aa8e770f435bedfe2041d1cc4d3d37538c6631b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202b1d912d45d753ceed0954417ba982656d2aec53f8638bd6f297dae3b743d71b0032f424537d599d28f14d59fe0a11b82fea2aa226a2980ffdacad5fdab20f80683621c02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd000b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c20ada78b70af8cffa4c50863aef515ac58a327cd58c55bad29a162e67d9c413322631c024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c201fa8b7e6741c21eb8d9b0f65269df9e423369fc390ad2e918420c57feb2a1b61631c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c20366c31ad65e533f6d2b45e5910f3f587543f8061918167bf5a818b13be5792a40000", + "valid": false + }, + { + "name": "invalid 9: x-only participant pubkey in partial sig keydata", + "hex": "70736274ff01005202000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f5050000000022512056bdb481b4d67103f6d5dea8a9aafd3684a6a79b4a2e247799db8d4b1a86e1f841140b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c402667d52f6cc07fe06db31b1a5f7efe81903f9cbeef40fa64dafca01d2cb1d56403bc7504898e55872557d16d2ca79bc55fef10973841a33ec032d884758c9fe62215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac023200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4acc021160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d42501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c2680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd002501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c2501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2cc3249a82211650929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac005007c461e5d2116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f92501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c7dd6559201172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0011820b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9631b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202d99e7c8719b3ad08566b0cb9c7d5eda3127c9e8119185b7d584d939b173915f50240df22aab78332cf0f25329d103dc0d2060a03742e9448026e736bcf3db98f3c631b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202e702a7075226a0d9313a77e6db10b8e742d4cffdc948a0edc9b856c13b412e5403386a0298f308fb3099155772e45b2aa8e770f435bedfe2041d1cc4d3d37538c6631b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202b1d912d45d753ceed0954417ba982656d2aec53f8638bd6f297dae3b743d71b0032f424537d599d28f14d59fe0a11b82fea2aa226a2980ffdacad5fdab20f80683621c346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c20ada78b70af8cffa4c50863aef515ac58a327cd58c55bad29a162e67d9c413322631c024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c201fa8b7e6741c21eb8d9b0f65269df9e423369fc390ad2e918420c57feb2a1b61631c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c20366c31ad65e533f6d2b45e5910f3f587543f8061918167bf5a818b13be5792a40000", + "valid": false + }, + { + "name": "invalid 10: invalid partial sig valuedata length", + "hex": "70736274ff01005202000000019a8b4a50796b9600990f7fe11dfa00bc70efd296048afc86719af0fb1fa919370100000000fdffffff0118ddf50500000000160014c9123e06e8d7f0966c5d1cd0f933002d4eb757cd000000000001012b00e1f5050000000022512056bdb481b4d67103f6d5dea8a9aafd3684a6a79b4a2e247799db8d4b1a86e1f841140b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c402667d52f6cc07fe06db31b1a5f7efe81903f9cbeef40fa64dafca01d2cb1d56403bc7504898e55872557d16d2ca79bc55fef10973841a33ec032d884758c9fe62215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac023200b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4acc021160b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d42501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c2680dd6e2116346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd002501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c580b088721164fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c2501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2cc3249a82211650929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac005007c461e5d2116f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f92501b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c7dd6559201172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0011820b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c221a030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d46302346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9631b02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202d99e7c8719b3ad08566b0cb9c7d5eda3127c9e8119185b7d584d939b173915f50240df22aab78332cf0f25329d103dc0d2060a03742e9448026e736bcf3db98f3c631b024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202e702a7075226a0d9313a77e6db10b8e742d4cffdc948a0edc9b856c13b412e5403386a0298f308fb3099155772e45b2aa8e770f435bedfe2041d1cc4d3d37538c6631b02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c4202b1d912d45d753ceed0954417ba982656d2aec53f8638bd6f297dae3b743d71b0032f424537d599d28f14d59fe0a11b82fea2aa226a2980ffdacad5fdab20f80683631c02346b99593357107c9d3459e9deba8d3eaf44e6636c85c7f853eb90ba52e8cd00030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c1fa78b70af8cffa4c50863aef515ac58a327cd58c55bad29a162e67d9c413322631c024fafd65f8169186fc2bfdb2233c77e630d10be280a24c7165c09a27611775c2c030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c201fa8b7e6741c21eb8d9b0f65269df9e423369fc390ad2e918420c57feb2a1b61631c02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9030b58e337aa4d3852a8c29387c42408d8cfbe3a613a5e397e0a9f01a5fb7107d4b11fedaa63a0956501a7308c93b5637371e7613d9b8ade1783d49e26c06cfa2c20366c31ad65e533f6d2b45e5910f3f587543f8061918167bf5a818b13be5792a40000", + "valid": false + } +]