From b73854318651c8cfc50c970cca81eae271891bfc Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 14 Jun 2024 11:06:03 +0200 Subject: [PATCH 1/5] psbt: update btcec/v2 to v2.3.3 --- btcutil/psbt/go.mod | 6 +++--- btcutil/psbt/go.sum | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/btcutil/psbt/go.mod b/btcutil/psbt/go.mod index 81ccca26..6dad0ff0 100644 --- a/btcutil/psbt/go.mod +++ b/btcutil/psbt/go.mod @@ -4,11 +4,11 @@ go 1.17 require ( 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/chaincfg/chainhash v1.1.0 github.com/davecgh/go-spew v1.1.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.8.0 ) require ( @@ -18,5 +18,5 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // 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 ) diff --git a/btcutil/psbt/go.sum b/btcutil/psbt/go.sum index 74a2ce8a..4dbd3407 100644 --- a/btcutil/psbt/go.sum +++ b/btcutil/psbt/go.sum @@ -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/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.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.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.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.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= 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= @@ -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.4/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From b949c67c069c12330d68793f78bca2d0642a2d68 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 14 Jun 2024 11:06:34 +0200 Subject: [PATCH 2/5] psbt: add new structs for MuSig2 fields --- btcutil/psbt/musig2.go | 282 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 btcutil/psbt/musig2.go diff --git a/btcutil/psbt/musig2.go b/btcutil/psbt/musig2.go new file mode 100644 index 00000000..33ad89f3 --- /dev/null +++ b/btcutil/psbt/musig2.go @@ -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(), + ) +} From bf1e09679f6e452f1474aa8fe0f86d0a3b70a3f4 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 14 Jun 2024 11:06:45 +0200 Subject: [PATCH 3/5] psbt: add new MuSig2 fields to inputs and outputs --- btcutil/psbt/partial_input.go | 87 ++++++++++++++++++++++++++++++++++ btcutil/psbt/partial_output.go | 30 ++++++++++++ btcutil/psbt/types.go | 24 ++++++++++ 3 files changed, 141 insertions(+) diff --git a/btcutil/psbt/partial_input.go b/btcutil/psbt/partial_input.go index 73595d25..8d4cccb8 100644 --- a/btcutil/psbt/partial_input.go +++ b/btcutil/psbt/partial_input.go @@ -28,6 +28,9 @@ type PInput struct { TaprootBip32Derivation []*TaprootBip32Derivation TaprootInternalKey []byte TaprootMerkleRoot []byte + MuSig2Participants []*MuSig2Participants + MuSig2PubNonces []*MuSig2PubNonce + MuSig2PartialSigs []*MuSig2PartialSig Unknowns []*Unknown } @@ -363,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( @@ -572,6 +629,36 @@ func (pi *PInput) serialize(w io.Writer) error { 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 { diff --git a/btcutil/psbt/partial_output.go b/btcutil/psbt/partial_output.go index 86e47645..d73265bf 100644 --- a/btcutil/psbt/partial_output.go +++ b/btcutil/psbt/partial_output.go @@ -17,6 +17,7 @@ type POutput struct { TaprootInternalKey []byte TaprootTapTree []byte TaprootBip32Derivation []*TaprootBip32Derivation + MuSig2Participants []*MuSig2Participants Unknowns []*Unknown } @@ -144,6 +145,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.AggregateKey + for _, x := range po.MuSig2Participants { + if x.AggregateKey.IsEqual(newKey) { + return ErrDuplicateKey + } + } + + po.MuSig2Participants = append( + po.MuSig2Participants, participants, + ) + default: // A fall through case for any proprietary types. 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 // a value field for _, kv := range po.Unknowns { diff --git a/btcutil/psbt/types.go b/btcutil/psbt/types.go index ca555101..64fc9fbd 100644 --- a/btcutil/psbt/types.go +++ b/btcutil/psbt/types.go @@ -151,6 +151,24 @@ const ( // 32-byte hash denoting the root hash of a merkle tree of scripts. TaprootMerkleRootType InputType = 0x18 + // MuSig2ParticipantsInputType is a type that carries the participant + // public keys and aggregated key for a MuSig2 signing session + // ({0x1a}|{aggregate_key}). The value is a list of 33-byte compressed + // public keys in the order required for aggregation. + MuSig2ParticipantsInputType InputType = 0x1a + + // MuSig2PubNoncesInputType is a type that carries the public nonces + // provided by participants in a MuSig2 signing session + // ({0x1b}|{participant_key}|{aggregate_key}[|{tapleaf_hash}]). The + // value is the 66-byte public nonces provided by the participant. + MuSig2PubNoncesInputType InputType = 0x1b + + // MuSig2PartialSigsInputType is a type that carries the partial + // signatures provided by participants in a MuSig2 signing session + // ({0x1c}|{participant_key}|{aggregate_key}[|{tapleaf_hash}]). The + // value is the 32-byte partial signature provided by the participant. + MuSig2PartialSigsInputType InputType = 0x1c + // ProprietaryInputType is a custom type for use by devs. // // The key ({0xFC}||{subtype}|{key data}), is a Variable length @@ -200,4 +218,10 @@ const ( // followed by said number of 32-byte leaf hashes. The rest of the value // is then identical to the Bip32DerivationInputType value. TaprootBip32DerivationOutputType OutputType = 7 + + // MuSig2ParticipantsOutputType is a type that carries the participant + // public keys and aggregated key for a MuSig2 signing session + // ({0x08}|{aggregate_key}). The value is a list of 33-byte compressed + // public keys in the order required for aggregation. + MuSig2ParticipantsOutputType OutputType = 0x08 ) From 53dfb9915dad2bac24037cd624c95589f44c6cb8 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 14 Jun 2024 17:43:22 +0200 Subject: [PATCH 4/5] psbt: add PrevOutFetcher helper func --- btcutil/psbt/utils.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/btcutil/psbt/utils.go b/btcutil/psbt/utils.go index c47f6afd..88a7c8ab 100644 --- a/btcutil/psbt/utils.go +++ b/btcutil/psbt/utils.go @@ -478,3 +478,36 @@ 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 + fetcher.AddPrevOut( + txIn.PreviousOutPoint, + in.NonWitnessUtxo.TxOut[prevIndex], + ) + + continue + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + fetcher.AddPrevOut( + txIn.PreviousOutPoint, in.WitnessUtxo, + ) + } + } + + return fetcher +} \ No newline at end of file From 8e879e1b39b31b9c87f9af4f7c0725d0e3954516 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 14 Jun 2024 18:10:07 +0200 Subject: [PATCH 5/5] psbt: support finalizing MuSig2 partial signatures --- btcutil/psbt/finalizer.go | 155 +++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 2 deletions(-) diff --git a/btcutil/psbt/finalizer.go b/btcutil/psbt/finalizer.go index b1bf12d1..df6a70a1 100644 --- a/btcutil/psbt/finalizer.go +++ b/btcutil/psbt/finalizer.go @@ -14,6 +14,9 @@ package psbt import ( "bytes" "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/txscript" @@ -47,11 +50,19 @@ func isFinalizableWitnessInput(pInput *PInput) bool { case txscript.IsPayToTaproot(pkScript): if pInput.TaprootKeySpendSig == nil && - pInput.TaprootScriptSpendSig == nil { + pInput.TaprootScriptSpendSig == nil && + pInput.MuSig2PartialSigs == nil { 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 // corresponding tap script leaf with the control block. for _, sig := range pInput.TaprootScriptSpendSig { @@ -133,7 +144,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 } @@ -577,6 +589,91 @@ func finalizeTaprootInput(p *Packet, inIndex int) error { 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: return ErrInvalidPsbtFormat } @@ -595,3 +692,57 @@ func finalizeTaprootInput(p *Packet, inIndex int) error { p.Inputs[inIndex] = *newInput 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 +}