Skip to content

Commit

Permalink
asset: turn Pederson key into xpub for hardware wallet support
Browse files Browse the repository at this point in the history
Hardware wallets that support miniscript policies only accept a
pk() fragment that specifies an extended key, for example with:
pk(@1/<0;1>/*). When signing the transaction the actual derivation path
to use (for example 0/0) is defined in the PSBT itself. So the signing
policy is generic and the user only has to accept it once.
A child key derived from an extended NUMS key is still a NUMS key, as
it's just tweaked again. The initial private key is still unknown.
  • Loading branch information
guggero committed Jan 16, 2025
1 parent 3740685 commit 2389629
Showing 1 changed file with 74 additions and 13 deletions.
87 changes: 74 additions & 13 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
Expand Down Expand Up @@ -1142,19 +1143,13 @@ func NewNonSpendableScriptLeaf(version NonSpendLeafVersion,
// For the Pedersen commitment based version, we'll use a single
// OP_CEHCKSIG with an un-spendable key.
case PedersenVersion:
var msg [sha256.Size]byte
copy(msg[:], data)

// Make a Pedersen opening that uses no mask (we don't carry on
// the random value, as we don't care about hiding here). We'll
// also use the existing NUMs point.
op := pedersen.Opening{
Msg: msg,
_, commitPoint, err := TweakedNumsKey(data)
if err != nil {
return txscript.TapLeaf{}, fmt.Errorf("failed to "+
"derive tweaked NUMS key: %w", err)
}
commitPoint := pedersen.NewCommitment(op).Point()

commitBytes := schnorr.SerializePubKey(&commitPoint)

commitBytes := schnorr.SerializePubKey(commitPoint)
builder = txscript.NewScriptBuilder().AddData(commitBytes).
AddOp(txscript.OP_CHECKSIG)

Expand Down Expand Up @@ -1202,6 +1197,68 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record {
return tlv.MakePrimitiveRecord(GKRCustomSubtreeRoot, (*[32]byte)(root))
}

// NumsXPub turns the given NUMS key into an extended public key (using the x
// coordinate of the public key as the chain code), then derives the actual key
// to use from the derivation path 0/0. The extended key always has the mainnet
// version, but can be converted to any network on demand by the caller with
// CloneWithVersion().
func NumsXPub(numsKey *btcec.PublicKey) (*hdkeychain.ExtendedKey,
*btcec.PublicKey, error) {

keyBytes := numsKey.SerializeCompressed()
chainCode := keyBytes[1:]

// We use a depth of 3, emulating BIP44/49/84/86 style derivation for
// xpubs. We also always use mainnet to not require the caller to pass
// in the net params. Converting to another network is possible with
// CloneWithVersion().
const depth = 3
extendedNumsKey := hdkeychain.NewExtendedKey(
chaincfg.MainNetParams.HDPublicKeyID[:], keyBytes, chainCode,
[]byte{0, 0, 0, 0}, depth, 0, false,
)

// Derive the actual key to use from the xpub.
changeBranch, err := extendedNumsKey.Derive(0)
if err != nil {
return nil, nil, err
}

indexBranch, err := changeBranch.Derive(0)
if err != nil {
return nil, nil, err
}

actualKey, err := indexBranch.ECPubKey()
if err != nil {
return nil, nil, err
}

return extendedNumsKey, actualKey, nil
}

// TweakedNumsKey derives the NUMS key from the given data, then creates the
// extended key from it and derives the actual (derived child) key to use from
// the derivation path 0/0. The extended key always has the mainnet version, but
// can be converted to any network on demand by the caller with
// CloneWithVersion().
func TweakedNumsKey(data []byte) (*hdkeychain.ExtendedKey, *btcec.PublicKey,
error) {

var msg [sha256.Size]byte
copy(msg[:], data)

// Make a Pedersen opening that uses no mask (we don't carry on
// the random value, as we don't care about hiding here). We'll
// also use the existing NUMs point.
op := pedersen.Opening{
Msg: msg,
}
commitPoint := pedersen.NewCommitment(op).Point()

return NumsXPub(&commitPoint)
}

// GroupKeyRevealTapscript holds data used to derive the tapscript root, which
// is then used to calculate the asset group key.
//
Expand Down Expand Up @@ -1273,7 +1330,9 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record {
// - One that uses a normal OP_CHECKSIG operator where the pubkey
// argument is a key that cannot be signed with. We generate this
// special public key using a Pedersen commitment, where the message is
// the asset ID.
// the asset ID. To achieve hardware wallet support, that key is then turned
// into an extended key (xpub) and a child key at path 0/0 is used as the
// actual public key that goes into the OP_CHECKSIG script.
//
// If `custom_root_hash` is not provided, then there is no sibling to the asset
// ID leaf, meaning the tree only has a single leaf. This makes it possible to
Expand All @@ -1300,7 +1359,9 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record {
// - One that uses a normal OP_CHECKSIG operator where the pubkey
// argument is a key that cannot be signed with. We generate this
// special public key using a Pedersen commitment, where the message is
// the asset ID.
// the asset ID. To achieve hardware wallet support, that key is then turned
// into an extended key (xpub) and a child key at path 0/0 is used as the
// actual public key that goes into the OP_CHECKSIG script.
type GroupKeyRevealTapscript struct {
// version is the version of the group key reveal that determines how
// the non-spendable leaf is created.
Expand Down

0 comments on commit 2389629

Please sign in to comment.