Skip to content

Commit

Permalink
psbt: add global XPubs to packet
Browse files Browse the repository at this point in the history
Adds support for the PSBT_GLOBAL_XPUB type as defined in BIP-0174.
  • Loading branch information
guggero committed Jan 14, 2025
1 parent bdb0b3d commit bda0481
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 10 deletions.
69 changes: 69 additions & 0 deletions btcutil/psbt/bip32.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package psbt
import (
"bytes"
"encoding/binary"

"github.com/btcsuite/btcd/btcutil/base58"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)

const (
Expand Down Expand Up @@ -85,3 +89,68 @@ func SerializeBIP32Derivation(masterKeyFingerprint uint32,

return derivationPath
}

// XPub is a struct that encapsulates an extended public key, as defined in
// BIP-0032.
type XPub struct {
// ExtendedKey is the serialized extended public key as defined in
// BIP-0032.
ExtendedKey []byte

// MasterFingerprint is the fingerprint of the master pubkey.
MasterKeyFingerprint uint32

// Bip32Path is the derivation path of the key, with hardened elements
// having the 0x80000000 offset added, as defined in BIP-0032. The
// number of path elements must match the depth provided in the extended
// public key.
Bip32Path []uint32
}

// ReadXPub deserializes a byte slice containing an extended public key and a
// BIP-0032 derivation path.
func ReadXPub(keyData []byte, path []byte) (*XPub, error) {
xPub, err := DecodeExtendedKey(keyData)
if err != nil {
return nil, ErrInvalidPsbtFormat
}
numPathElements := xPub.Depth()

// The path also contains the master key fingerprint,
expectedSize := int(uint32Size * (numPathElements + 1))
if len(path) != expectedSize {
return nil, ErrInvalidPsbtFormat
}

masterKeyFingerprint, bip32Path, err := ReadBip32Derivation(path)
if err != nil {
return nil, err
}

return &XPub{
ExtendedKey: keyData,
MasterKeyFingerprint: masterKeyFingerprint,
Bip32Path: bip32Path,
}, nil
}

// EncodeExtendedKey serializes an extended key to a byte slice, without the
// checksum.
func EncodeExtendedKey(key *hdkeychain.ExtendedKey) []byte {
serializedKey := key.String()
decodedKey := base58.Decode(serializedKey)
return decodedKey[:len(decodedKey)-uint32Size]
}

// DecodeExtendedKey deserializes an extended key from a byte slice that does
// not contain the checksum.
func DecodeExtendedKey(encodedKey []byte) (*hdkeychain.ExtendedKey, error) {
checkSum := chainhash.DoubleHashB(encodedKey)[:uint32Size]
serializedBytes := append(encodedKey, checkSum...)
xPub, err := hdkeychain.NewKeyFromString(base58.Encode(serializedBytes))
if err != nil {
return nil, err
}

return xPub, nil
}
60 changes: 52 additions & 8 deletions btcutil/psbt/psbt.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ type Packet struct {
// produced by this PSBT.
Outputs []POutput

// XPubs is a list of extended public keys that can be used to derive
// public keys used in the inputs and outputs of this transaction. It
// should be the public key at the highest hardened derivation index so
// that the unhardened child keys used in the transaction can be
// derived.
XPubs []XPub

// Unknowns are the set of custom types (global only) within this PSBT.
Unknowns []*Unknown
}
Expand All @@ -161,12 +168,14 @@ func NewFromUnsignedTx(tx *wire.MsgTx) (*Packet, error) {

inSlice := make([]PInput, len(tx.TxIn))
outSlice := make([]POutput, len(tx.TxOut))
xPubSlice := make([]XPub, 0)
unknownSlice := make([]*Unknown, 0)

return &Packet{
UnsignedTx: tx,
Inputs: inSlice,
Outputs: outSlice,
XPubs: xPubSlice,
Unknowns: unknownSlice,
}, nil
}
Expand Down Expand Up @@ -230,7 +239,10 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) {

// Next we parse any unknowns that may be present, making sure that we
// break at the separator.
var unknownSlice []*Unknown
var (
xPubSlice []XPub
unknownSlice []*Unknown
)
for {
keyint, keydata, err := getKey(r)
if err != nil {
Expand All @@ -247,14 +259,32 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) {
return nil, err
}

keyintanddata := []byte{byte(keyint)}
keyintanddata = append(keyintanddata, keydata...)

newUnknown := &Unknown{
Key: keyintanddata,
Value: value,
switch GlobalType(keyint) {
case XPubType:
xPub, err := ReadXPub(keydata, value)
if err != nil {
return nil, err
}

// Duplicate keys are not allowed
for _, x := range xPubSlice {
if bytes.Equal(x.ExtendedKey, keyData) {
return nil, ErrDuplicateKey
}
}

xPubSlice = append(xPubSlice, *xPub)

default:
keyintanddata := []byte{byte(keyint)}
keyintanddata = append(keyintanddata, keydata...)

newUnknown := &Unknown{
Key: keyintanddata,
Value: value,
}
unknownSlice = append(unknownSlice, newUnknown)
}
unknownSlice = append(unknownSlice, newUnknown)
}

// Next we parse the INPUT section.
Expand Down Expand Up @@ -286,6 +316,7 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) {
UnsignedTx: msgTx,
Inputs: inSlice,
Outputs: outSlice,
XPubs: xPubSlice,
Unknowns: unknownSlice,
}

Expand Down Expand Up @@ -325,6 +356,19 @@ func (p *Packet) Serialize(w io.Writer) error {
return err
}

// Serialize the global xPubs.
for _, xPub := range p.XPubs {
pathBytes := SerializeBIP32Derivation(
xPub.MasterKeyFingerprint, xPub.Bip32Path,
)
err := serializeKVPairWithType(
w, uint8(XPubType), xPub.ExtendedKey, pathBytes,
)
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 p.Unknowns {
Expand Down
Loading

0 comments on commit bda0481

Please sign in to comment.