This commit is contained in:
Oli 2026-08-12 19:35:58 +00:00 committed by GitHub
commit b9d552f3db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 3769 additions and 18 deletions

View file

@ -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 {

View file

@ -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,
)

View file

@ -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++ {

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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
@ -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

View file

@ -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=

385
psbt/musig2.go Normal file
View file

@ -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(),
)
}

1304
psbt/musig2_test.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -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 {

View file

@ -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 {

View file

@ -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

295
psbt/signer_musig2_test.go Normal file
View file

@ -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)
}

122
psbt/testdata/bip-373-test-vectors.json vendored Normal file
View file

@ -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
}
]

View file

@ -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}|<prefix>|{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
)

View file

@ -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()
}

View file

@ -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
}