Skip to content

Commit

Permalink
Merge pull request #2296 from btcsuite/psbt-global-xpub
Browse files Browse the repository at this point in the history
psbt: add support for PSBT_GLOBAL_XPUB type
  • Loading branch information
guggero authored Jan 14, 2025
2 parents f3bd1f5 + bda0481 commit 09ba026
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 28 deletions.
96 changes: 86 additions & 10 deletions btcutil/psbt/bip32.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ 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 (
// uint32Size is the size of a uint32 in bytes.
uint32Size = 4
)

// Bip32Derivation encapsulates the data for the input and output
Expand Down Expand Up @@ -38,21 +47,23 @@ func (s Bip32Sorter) Less(i, j int) bool {

// ReadBip32Derivation deserializes a byte slice containing chunks of 4 byte
// little endian encodings of uint32 values, the first of which is the
// masterkeyfingerprint and the remainder of which are the derivation path.
// MasterKeyFingerprint and the remainder of which are the derivation path.
func ReadBip32Derivation(path []byte) (uint32, []uint32, error) {
// BIP-0174 defines the derivation path being encoded as
// "<32-bit uint> <32-bit uint>*"
// with the asterisk meaning 0 to n times. Which in turn means that an
// empty path is valid, only the key fingerprint is mandatory.
if len(path)%4 != 0 {
if len(path)%uint32Size != 0 {
return 0, nil, ErrInvalidPsbtFormat
}

masterKeyInt := binary.LittleEndian.Uint32(path[:4])
masterKeyInt := binary.LittleEndian.Uint32(path[:uint32Size])

var paths []uint32
for i := 4; i < len(path); i += 4 {
paths = append(paths, binary.LittleEndian.Uint32(path[i:i+4]))
for i := uint32Size; i < len(path); i += uint32Size {
paths = append(paths, binary.LittleEndian.Uint32(
path[i:i+uint32Size],
))
}

return masterKeyInt, paths, nil
Expand All @@ -65,16 +76,81 @@ func ReadBip32Derivation(path []byte) (uint32, []uint32, error) {
func SerializeBIP32Derivation(masterKeyFingerprint uint32,
bip32Path []uint32) []byte {

var masterKeyBytes [4]byte
var masterKeyBytes [uint32Size]byte
binary.LittleEndian.PutUint32(masterKeyBytes[:], masterKeyFingerprint)

derivationPath := make([]byte, 0, 4+4*len(bip32Path))
derivationPath := make([]byte, 0, uint32Size+uint32Size*len(bip32Path))
derivationPath = append(derivationPath, masterKeyBytes[:]...)
for _, path := range bip32Path {
var pathbytes [4]byte
binary.LittleEndian.PutUint32(pathbytes[:], path)
derivationPath = append(derivationPath, pathbytes[:]...)
var pathBytes [uint32Size]byte
binary.LittleEndian.PutUint32(pathBytes[:], path)
derivationPath = append(derivationPath, pathBytes[:]...)
}

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 09ba026

Please sign in to comment.