This commit is contained in:
Oliver Gugger 2025-03-10 14:00:38 +00:00 committed by GitHub
commit e9a838b50b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 620 additions and 8 deletions

View file

@ -14,6 +14,9 @@ package psbt
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
@ -47,11 +50,19 @@ func isFinalizableWitnessInput(pInput *PInput) bool {
case txscript.IsPayToTaproot(pkScript): case txscript.IsPayToTaproot(pkScript):
if pInput.TaprootKeySpendSig == nil && if pInput.TaprootKeySpendSig == nil &&
pInput.TaprootScriptSpendSig == nil { pInput.TaprootScriptSpendSig == nil &&
pInput.MuSig2PartialSigs == nil {
return false return false
} }
// For each participant, we need a corresponding
// MuSig2 partial signature.
if len(pInput.MuSig2PartialSigs) > 0 {
return len(pInput.MuSig2PartialSigs) ==
len(pInput.MuSig2PubNonces)
}
// For each of the script spend signatures we need a // For each of the script spend signatures we need a
// corresponding tap script leaf with the control block. // corresponding tap script leaf with the control block.
for _, sig := range pInput.TaprootScriptSpendSig { for _, sig := range pInput.TaprootScriptSpendSig {
@ -133,7 +144,8 @@ func isFinalizable(p *Packet, inIndex int) bool {
// The input cannot be finalized without any signatures. // The input cannot be finalized without any signatures.
if pInput.PartialSigs == nil && pInput.TaprootKeySpendSig == nil && if pInput.PartialSigs == nil && pInput.TaprootKeySpendSig == nil &&
pInput.TaprootScriptSpendSig == nil { pInput.TaprootScriptSpendSig == nil &&
pInput.MuSig2PartialSigs == nil {
return false return false
} }
@ -577,6 +589,91 @@ func finalizeTaprootInput(p *Packet, inIndex int) error {
serializedWitness, err = writeWitness(witnessStack...) serializedWitness, err = writeWitness(witnessStack...)
// MuSig2 spend path.
case len(pInput.MuSig2PartialSigs) > 0:
if len(pInput.MuSig2PubNonces) !=
len(pInput.MuSig2PartialSigs) {
return fmt.Errorf("number of MuSig2 pub nonces " +
"does not match number of partial signatures")
}
// We'll need to combine MuSig2 partial signatures into a single
// one, which requires the message that was signed over.
firstSig := pInput.MuSig2PartialSigs[0]
// We don't (yet) support signing over a tap leaf hash.
// TODO(guggero): Add support for signing over a tap leaf hash.
if len(firstSig.TapLeafHash) > 0 {
return fmt.Errorf("combining partial MuSig2 " +
"signatures for a tap leaf is not supported")
}
prevOutFetcher := PrevOutputFetcher(p)
sigHashes := txscript.NewTxSigHashes(
p.UnsignedTx, prevOutFetcher,
)
sigHash, err := txscript.CalcTaprootSignatureHash(
sigHashes, pInput.SighashType, p.UnsignedTx,
inIndex, prevOutFetcher,
)
if err != nil {
return fmt.Errorf("error calculating signature hash: "+
"%w", err)
}
var sigHashMsg [32]byte
copy(sigHashMsg[:], sigHash)
var (
pubNonces = make(
[][musig2.PubNonceSize]byte,
len(pInput.MuSig2PubNonces),
)
keys = make(
[]*btcec.PublicKey, len(pInput.MuSig2PubNonces),
)
partialSigs = make(
[]*musig2.PartialSignature,
len(pInput.MuSig2PartialSigs),
)
)
for i, pubNonce := range pInput.MuSig2PubNonces {
copy(pubNonces[i][:], pubNonce.PubNonce[:])
keys[i] = pubNonce.PubKey
partialSigs[i] = &pInput.MuSig2PartialSigs[i].PartialSig
}
aggregateNonce, err := musig2.AggregateNonces(pubNonces)
if err != nil {
return fmt.Errorf("error aggregating pub nonces: %w",
err)
}
aggKey, _, _, err := musig2.AggregateKeys(
keys, true, musig2.WithBIP86KeyTweak(),
)
if err != nil {
return fmt.Errorf("error aggregating keys: %w", err)
}
combinedNonce, err := computeSigningNonce(
aggregateNonce, aggKey.FinalKey, sigHashMsg,
)
if err != nil {
return fmt.Errorf("error computing signing nonce: %w",
err)
}
combineOpt := musig2.WithBip86TweakedCombine(
sigHashMsg, keys, true,
)
schnorrSig := musig2.CombineSigs(
combinedNonce, partialSigs, combineOpt,
)
serializedWitness, err = writeWitness(schnorrSig.Serialize())
default: default:
return ErrInvalidPsbtFormat return ErrInvalidPsbtFormat
} }
@ -595,3 +692,57 @@ func finalizeTaprootInput(p *Packet, inIndex int) error {
p.Inputs[inIndex] = *newInput p.Inputs[inIndex] = *newInput
return nil return nil
} }
// computeSigningNonce calculates the final nonce used for signing. This will
// be the R value used in the final signature.
func computeSigningNonce(combinedNonce [musig2.PubNonceSize]byte,
combinedKey *btcec.PublicKey, msg [32]byte) (*btcec.PublicKey, error) {
// Next we'll compute the value b, that blinds our second public
// nonce:
// * b = h(tag=NonceBlindTag, combinedNonce || combinedKey || m).
var (
nonceMsgBuf bytes.Buffer
nonceBlinder btcec.ModNScalar
)
nonceMsgBuf.Write(combinedNonce[:])
nonceMsgBuf.Write(schnorr.SerializePubKey(combinedKey))
nonceMsgBuf.Write(msg[:])
nonceBlindHash := chainhash.TaggedHash(
musig2.NonceBlindTag, nonceMsgBuf.Bytes(),
)
nonceBlinder.SetByteSlice(nonceBlindHash[:])
// Next, we'll parse the public nonces into R1 and R2.
r1J, err := btcec.ParseJacobian(
combinedNonce[:btcec.PubKeyBytesLenCompressed],
)
if err != nil {
return nil, err
}
r2J, err := btcec.ParseJacobian(
combinedNonce[btcec.PubKeyBytesLenCompressed:],
)
if err != nil {
return nil, err
}
// With our nonce blinding value, we'll now combine both the public
// nonces, using the blinding factor to tweak the second nonce:
// * R = R_1 + b*R_2
var nonce btcec.JacobianPoint
btcec.ScalarMultNonConst(&nonceBlinder, &r2J, &r2J)
btcec.AddNonConst(&r1J, &r2J, &nonce)
// If the combined nonce is the point at infinity, we'll use the
// generator point instead.
var infinityPoint btcec.JacobianPoint
if nonce == infinityPoint {
G := btcec.Generator()
G.AsJacobian(&nonce)
}
nonce.ToAffine()
return btcec.NewPublicKey(&nonce.X, &nonce.Y), nil
}

View file

@ -4,11 +4,11 @@ go 1.22
require ( require (
github.com/btcsuite/btcd v0.23.5-0.20231219003633-4c2ce6daed8f github.com/btcsuite/btcd v0.23.5-0.20231219003633-4c2ce6daed8f
github.com/btcsuite/btcd/btcec/v2 v2.1.3 github.com/btcsuite/btcd/btcec/v2 v2.3.3
github.com/btcsuite/btcd/btcutil v1.1.4 github.com/btcsuite/btcd/btcutil v1.1.4
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.8.0
) )
require ( require (
@ -18,5 +18,5 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View file

@ -5,8 +5,9 @@ github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6
github.com/btcsuite/btcd v0.23.5-0.20231219003633-4c2ce6daed8f h1:E+dQ8sNtK/lOdfeflUKkRLXe/zW7I333C7HhaoASjZA= github.com/btcsuite/btcd v0.23.5-0.20231219003633-4c2ce6daed8f h1:E+dQ8sNtK/lOdfeflUKkRLXe/zW7I333C7HhaoASjZA=
github.com/btcsuite/btcd v0.23.5-0.20231219003633-4c2ce6daed8f/go.mod h1:KVEB81PybLGYzpf1db/kKNi1ZEbUsiVGeTGhKuOl5AM= github.com/btcsuite/btcd v0.23.5-0.20231219003633-4c2ce6daed8f/go.mod h1:KVEB81PybLGYzpf1db/kKNi1ZEbUsiVGeTGhKuOl5AM=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.4 h1:mWvWRLRIPuoeZsVRpc0xNCkfeNxWy1E4jIZ06ZpGI1A= github.com/btcsuite/btcd/btcutil v1.1.4 h1:mWvWRLRIPuoeZsVRpc0xNCkfeNxWy1E4jIZ06ZpGI1A=
@ -65,8 +66,11 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -107,5 +111,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

282
btcutil/psbt/musig2.go Normal file
View file

@ -0,0 +1,282 @@
package psbt
import (
"bytes"
"crypto/sha256"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
)
// 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
}
// 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 := 0; idx < numKeys; idx++ {
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 {
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
}
// 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 {
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 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
// PartialSig is the partial signature provided by the participant,
// produced by the `Sign` algorithm as described in the MuSig2 BIP.
PartialSig musig2.PartialSignature
}
// 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) != 32 {
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 {
var buf bytes.Buffer
err := partialSig.PartialSig.Encode(&buf)
if err != nil {
return err
}
return serializeKVPairWithType(
w, typ, partialSig.KeyData(), buf.Bytes(),
)
}

View file

@ -28,6 +28,9 @@ type PInput struct {
TaprootBip32Derivation []*TaprootBip32Derivation TaprootBip32Derivation []*TaprootBip32Derivation
TaprootInternalKey []byte TaprootInternalKey []byte
TaprootMerkleRoot []byte TaprootMerkleRoot []byte
MuSig2Participants []*MuSig2Participants
MuSig2PubNonces []*MuSig2PubNonce
MuSig2PartialSigs []*MuSig2PartialSig
Unknowns []*Unknown Unknowns []*Unknown
} }
@ -363,6 +366,60 @@ func (pi *PInput) deserialize(r io.Reader) error {
pi.TaprootMerkleRoot = value 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: default:
// A fall through case for any proprietary types. // A fall through case for any proprietary types.
keyCodeAndData := append( keyCodeAndData := append(
@ -572,6 +629,36 @@ func (pi *PInput) serialize(w io.Writer) error {
return err return err
} }
} }
for _, participants := range pi.MuSig2Participants {
err := SerializeMuSig2Participants(
w, uint8(MuSig2ParticipantsInputType),
participants,
)
if err != nil {
return err
}
}
for _, nonce := range pi.MuSig2PubNonces {
err := SerializeMuSig2PubNonce(
w, uint8(MuSig2PubNoncesInputType),
nonce,
)
if err != nil {
return err
}
}
for _, sig := range pi.MuSig2PartialSigs {
err := SerializeMuSig2PartialSig(
w, uint8(MuSig2PartialSigsInputType),
sig,
)
if err != nil {
return err
}
}
} }
if pi.FinalScriptSig != nil { if pi.FinalScriptSig != nil {

View file

@ -17,6 +17,7 @@ type POutput struct {
TaprootInternalKey []byte TaprootInternalKey []byte
TaprootTapTree []byte TaprootTapTree []byte
TaprootBip32Derivation []*TaprootBip32Derivation TaprootBip32Derivation []*TaprootBip32Derivation
MuSig2Participants []*MuSig2Participants
Unknowns []*Unknown Unknowns []*Unknown
} }
@ -144,6 +145,26 @@ func (po *POutput) deserialize(r io.Reader) error {
po.TaprootBip32Derivation, taprootDerivation, po.TaprootBip32Derivation, taprootDerivation,
) )
case MuSig2ParticipantsOutputType:
participants, err := ReadMuSig2Participants(
keyData, value,
)
if err != nil {
return err
}
// Duplicate keys are not allowed.
newKey := participants.AggregateKey
for _, x := range po.MuSig2Participants {
if x.AggregateKey.IsEqual(newKey) {
return ErrDuplicateKey
}
}
po.MuSig2Participants = append(
po.MuSig2Participants, participants,
)
default: default:
// A fall through case for any proprietary types. // A fall through case for any proprietary types.
keyCodeAndData := append( keyCodeAndData := append(
@ -246,6 +267,15 @@ func (po *POutput) serialize(w io.Writer) error {
} }
} }
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 // Unknown is a special case; we don't have a key type, only a key and
// a value field // a value field
for _, kv := range po.Unknowns { for _, kv := range po.Unknowns {

View file

@ -151,6 +151,24 @@ const (
// 32-byte hash denoting the root hash of a merkle tree of scripts. // 32-byte hash denoting the root hash of a merkle tree of scripts.
TaprootMerkleRootType InputType = 0x18 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. // ProprietaryInputType is a custom type for use by devs.
// //
// The key ({0xFC}|<prefix>|{subtype}|{key data}), is a Variable length // 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 // followed by said number of 32-byte leaf hashes. The rest of the value
// is then identical to the Bip32DerivationInputType value. // is then identical to the Bip32DerivationInputType value.
TaprootBip32DerivationOutputType OutputType = 7 TaprootBip32DerivationOutputType OutputType = 7
// MuSig2ParticipantsOutputType is a type that carries the participant
// public keys and aggregated key for a MuSig2 signing session
// ({0x08}|{aggregate_key}). The value is a list of 33-byte compressed
// public keys in the order required for aggregation.
MuSig2ParticipantsOutputType OutputType = 0x08
) )

View file

@ -478,3 +478,36 @@ func FindLeafScript(pInput *PInput,
return nil, fmt.Errorf("leaf script for target leaf hash %x not "+ return nil, fmt.Errorf("leaf script for target leaf hash %x not "+
"found in input", targetLeafHash) "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
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
}