mirror of
https://github.com/btcsuite/btcd.git
synced 2026-08-18 13:08:09 +02:00
Merge c9f640702c into 05585e037b
This commit is contained in:
commit
b9d552f3db
18 changed files with 3769 additions and 18 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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++ {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
283
psbt/finalizer_musig2_bip32_test.go
Normal file
283
psbt/finalizer_musig2_bip32_test.go
Normal 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)
|
||||
}
|
||||
257
psbt/finalizer_musig2_test.go
Normal file
257
psbt/finalizer_musig2_test.go
Normal 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")
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
385
psbt/musig2.go
Normal 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
1304
psbt/musig2_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
140
psbt/signer.go
140
psbt/signer.go
|
|
@ -10,6 +10,9 @@ package psbt
|
|||
// is in the correct state.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/txscript/v2"
|
||||
)
|
||||
|
||||
|
|
@ -136,6 +139,143 @@ func (u *Updater) Sign(inIndex int, sig []byte, pubKey []byte,
|
|||
return SignSuccesful, nil
|
||||
}
|
||||
|
||||
// SignMuSig2 attaches a MuSig2 partial signature to the input at index
|
||||
// inIndex, following the BIP-174 Signer role for the MuSig2 fields defined by
|
||||
// BIP-373.
|
||||
//
|
||||
// Before appending, SignMuSig2 enforces the invariants a finalizer will later
|
||||
// rely on:
|
||||
//
|
||||
// - The input must not be finalized.
|
||||
// - The input must carry a witness UTXO (every BIP-373 MuSig2 spend is
|
||||
// segwit v1).
|
||||
// - The participant pubkey on the partial sig must appear in at least one
|
||||
// PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record on the input. The aggregate
|
||||
// key recorded on the partial sig itself does not need to equal the
|
||||
// bare aggregate in the participants record: BIP-373 case 4 deliberately
|
||||
// records the BIP-32-derived aggregate on the partial sigs while the
|
||||
// participants record carries the parent (bare) aggregate, and the
|
||||
// finalizer reconciles the two via PSBT_GLOBAL_XPUB. Enforcing equality
|
||||
// here would reject the legitimate derived-aggregate case.
|
||||
// - A PSBT_IN_MUSIG2_PUB_NONCE field with the same key data prefix
|
||||
// (participant pubkey || aggregate pubkey || optional tap leaf hash) must
|
||||
// already be present — partial sigs cannot be combined without their
|
||||
// matching nonces.
|
||||
// - If TapLeafHash is set, it must resolve to a leaf script recorded on
|
||||
// the input (i.e. the script the partial sig commits to is actually
|
||||
// part of this spend).
|
||||
//
|
||||
// On any of these checks failing the input is left untouched and SignInvalid
|
||||
// is returned together with the underlying error. Shape validation
|
||||
// (compressed pubkeys, 32-byte partial sig) and duplicate-key detection are
|
||||
// handled by AddInMuSig2PartialSig, which SignMuSig2 delegates to once the
|
||||
// Signer-role checks pass.
|
||||
//
|
||||
// SignMuSig2 does not itself compute the partial signature; callers are
|
||||
// expected to feed in the output of musig2.Sign (held in the
|
||||
// MuSig2PartialSig.PartialSig field). This mirrors the existing Sign()
|
||||
// helper, which accepts a pre-computed ECDSA signature rather than driving
|
||||
// the signing key directly.
|
||||
func (u *Updater) SignMuSig2(inIndex int,
|
||||
partialSig *MuSig2PartialSig) (SignOutcome, error) {
|
||||
|
||||
if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) {
|
||||
return SignInvalid, ErrInvalidPsbtFormat
|
||||
}
|
||||
|
||||
if isFinalized(u.Upsbt, inIndex) {
|
||||
return SignFinalized, nil
|
||||
}
|
||||
|
||||
if partialSig == nil || partialSig.PubKey == nil ||
|
||||
partialSig.AggregateKey == nil {
|
||||
|
||||
return SignInvalid, ErrInvalidPsbtFormat
|
||||
}
|
||||
|
||||
pInput := &u.Upsbt.Inputs[inIndex]
|
||||
|
||||
// BIP-373 inputs are taproot (segwit v1); a witness UTXO is required
|
||||
// for the finalizer to recompute the sighash.
|
||||
if pInput.WitnessUtxo == nil {
|
||||
return SignInvalid, ErrInvalidPsbtFormat
|
||||
}
|
||||
|
||||
// Make sure the participant is actually a member of a known aggregate
|
||||
// on this input. Without a PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record
|
||||
// the finalizer cannot reproduce the aggregate, so the partial sig is
|
||||
// not usable.
|
||||
if !musig2ParticipantRegistered(pInput, partialSig) {
|
||||
return SignInvalid, fmt.Errorf("%w: participant pubkey not "+
|
||||
"found in any MuSig2 participants record matching the "+
|
||||
"supplied aggregate key", ErrInvalidSignatureForInput)
|
||||
}
|
||||
|
||||
// A matching pub nonce (same participant pubkey || aggregate key ||
|
||||
// optional tap leaf hash) must exist; nonces are the precondition for
|
||||
// signing per BIP-373 §Signer.
|
||||
keyData := partialSig.KeyData()
|
||||
if !musig2HasMatchingPubNonce(pInput, keyData) {
|
||||
return SignInvalid, fmt.Errorf("%w: no matching "+
|
||||
"PSBT_IN_MUSIG2_PUB_NONCE found for partial signature",
|
||||
ErrInvalidSignatureForInput)
|
||||
}
|
||||
|
||||
// If the partial sig commits to a tap leaf, the leaf script must
|
||||
// actually be present on the input, otherwise the finalizer will not
|
||||
// be able to assemble the script-spend witness.
|
||||
if len(partialSig.TapLeafHash) > 0 {
|
||||
_, err := FindLeafScript(pInput, partialSig.TapLeafHash)
|
||||
if err != nil {
|
||||
return SignInvalid, fmt.Errorf("%w: tap leaf hash %x "+
|
||||
"on partial signature does not match any leaf "+
|
||||
"script on input: %v",
|
||||
ErrInvalidSignatureForInput,
|
||||
partialSig.TapLeafHash, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shape validation, duplicate-key detection and the actual append are
|
||||
// done by the existing low-level updater helper.
|
||||
if err := u.AddInMuSig2PartialSig(inIndex, partialSig); err != nil {
|
||||
return SignInvalid, err
|
||||
}
|
||||
|
||||
return SignSuccesful, nil
|
||||
}
|
||||
|
||||
// musig2ParticipantRegistered reports whether the partial sig's participant
|
||||
// pubkey appears in any PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record on the
|
||||
// input. The aggregate key on the partial sig is intentionally not compared
|
||||
// against the record's aggregate; in BIP-373 case 4 the partial sig records
|
||||
// the BIP-32 derived aggregate while the record carries the bare aggregate.
|
||||
func musig2ParticipantRegistered(pInput *PInput,
|
||||
partialSig *MuSig2PartialSig) bool {
|
||||
|
||||
for _, participants := range pInput.MuSig2Participants {
|
||||
for _, key := range participants.Keys {
|
||||
if key.IsEqual(partialSig.PubKey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// musig2HasMatchingPubNonce reports whether the input carries a
|
||||
// PSBT_IN_MUSIG2_PUB_NONCE whose key data matches the partial signature's
|
||||
// key data (participant pubkey || aggregate key || optional tap leaf hash).
|
||||
func musig2HasMatchingPubNonce(pInput *PInput, partialSigKeyData []byte) bool {
|
||||
for _, n := range pInput.MuSig2PubNonces {
|
||||
if bytes.Equal(n.KeyData(), partialSigKeyData) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// nonWitnessToWitness extracts the TxOut from the existing NonWitnessUtxo
|
||||
// field in the given PSBT input and sets it as type witness by replacing the
|
||||
// NonWitnessUtxo field with a WitnessUtxo field. See
|
||||
|
|
|
|||
295
psbt/signer_musig2_test.go
Normal file
295
psbt/signer_musig2_test.go
Normal 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
122
psbt/testdata/bip-373-test-vectors.json
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
112
psbt/updater.go
112
psbt/updater.go
|
|
@ -375,3 +375,115 @@ func (u *Updater) AddOutWitnessScript(witnessScript []byte,
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddInMuSig2Participants adds a PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS field
|
||||
// to the input at index inIndex. Returns ErrDuplicateKey if a participants
|
||||
// record with the same aggregate key is already present.
|
||||
func (u *Updater) AddInMuSig2Participants(inIndex int,
|
||||
participants *MuSig2Participants) error {
|
||||
|
||||
if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) {
|
||||
return ErrInvalidPsbtFormat
|
||||
}
|
||||
if participants == nil || participants.AggregateKey == nil {
|
||||
return ErrInvalidPsbtFormat
|
||||
}
|
||||
|
||||
newKey := participants.KeyData()
|
||||
for _, x := range u.Upsbt.Inputs[inIndex].MuSig2Participants {
|
||||
if bytes.Equal(x.KeyData(), newKey) {
|
||||
return ErrDuplicateKey
|
||||
}
|
||||
}
|
||||
|
||||
u.Upsbt.Inputs[inIndex].MuSig2Participants = append(
|
||||
u.Upsbt.Inputs[inIndex].MuSig2Participants, participants,
|
||||
)
|
||||
|
||||
return u.Upsbt.SanityCheck()
|
||||
}
|
||||
|
||||
// AddInMuSig2PubNonce adds a PSBT_IN_MUSIG2_PUB_NONCE field to the input at
|
||||
// index inIndex. Returns ErrDuplicateKey if a nonce with the same
|
||||
// (participant pubkey, aggregate pubkey, optional tap leaf hash) is already
|
||||
// present.
|
||||
func (u *Updater) AddInMuSig2PubNonce(inIndex int,
|
||||
nonce *MuSig2PubNonce) error {
|
||||
|
||||
if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) {
|
||||
return ErrInvalidPsbtFormat
|
||||
}
|
||||
if nonce == nil || nonce.PubKey == nil || nonce.AggregateKey == nil {
|
||||
return ErrInvalidPsbtFormat
|
||||
}
|
||||
|
||||
newKey := nonce.KeyData()
|
||||
for _, x := range u.Upsbt.Inputs[inIndex].MuSig2PubNonces {
|
||||
if bytes.Equal(x.KeyData(), newKey) {
|
||||
return ErrDuplicateKey
|
||||
}
|
||||
}
|
||||
|
||||
u.Upsbt.Inputs[inIndex].MuSig2PubNonces = append(
|
||||
u.Upsbt.Inputs[inIndex].MuSig2PubNonces, nonce,
|
||||
)
|
||||
|
||||
return u.Upsbt.SanityCheck()
|
||||
}
|
||||
|
||||
// AddInMuSig2PartialSig adds a PSBT_IN_MUSIG2_PARTIAL_SIG field to the input
|
||||
// at index inIndex. Returns ErrDuplicateKey if a partial signature with the
|
||||
// same (participant pubkey, aggregate pubkey, optional tap leaf hash) is
|
||||
// already present.
|
||||
func (u *Updater) AddInMuSig2PartialSig(inIndex int,
|
||||
partialSig *MuSig2PartialSig) error {
|
||||
|
||||
if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) {
|
||||
return ErrInvalidPsbtFormat
|
||||
}
|
||||
if partialSig == nil || partialSig.PubKey == nil ||
|
||||
partialSig.AggregateKey == nil {
|
||||
|
||||
return ErrInvalidPsbtFormat
|
||||
}
|
||||
|
||||
newKey := partialSig.KeyData()
|
||||
for _, x := range u.Upsbt.Inputs[inIndex].MuSig2PartialSigs {
|
||||
if bytes.Equal(x.KeyData(), newKey) {
|
||||
return ErrDuplicateKey
|
||||
}
|
||||
}
|
||||
|
||||
u.Upsbt.Inputs[inIndex].MuSig2PartialSigs = append(
|
||||
u.Upsbt.Inputs[inIndex].MuSig2PartialSigs, partialSig,
|
||||
)
|
||||
|
||||
return u.Upsbt.SanityCheck()
|
||||
}
|
||||
|
||||
// AddOutMuSig2Participants adds a PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS field
|
||||
// to the output at index outIndex. Returns ErrDuplicateKey if a participants
|
||||
// record with the same aggregate key is already present.
|
||||
func (u *Updater) AddOutMuSig2Participants(outIndex int,
|
||||
participants *MuSig2Participants) error {
|
||||
|
||||
if outIndex < 0 || outIndex >= len(u.Upsbt.Outputs) {
|
||||
return ErrInvalidPsbtFormat
|
||||
}
|
||||
if participants == nil || participants.AggregateKey == nil {
|
||||
return ErrInvalidPsbtFormat
|
||||
}
|
||||
|
||||
newKey := participants.KeyData()
|
||||
for _, x := range u.Upsbt.Outputs[outIndex].MuSig2Participants {
|
||||
if bytes.Equal(x.KeyData(), newKey) {
|
||||
return ErrDuplicateKey
|
||||
}
|
||||
}
|
||||
|
||||
u.Upsbt.Outputs[outIndex].MuSig2Participants = append(
|
||||
u.Upsbt.Outputs[outIndex].MuSig2Participants, participants,
|
||||
)
|
||||
|
||||
return u.Upsbt.SanityCheck()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue