Skip to content

Commit

Permalink
waddrmgr: add taproot address type
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Feb 16, 2022
1 parent 091a28b commit 3ad07da
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 3 deletions.
17 changes: 17 additions & 0 deletions waddrmgr/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sync"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript"
Expand Down Expand Up @@ -51,6 +52,9 @@ const (
// WitnessScript represents a p2wsh (pay-to-witness-script-hash) address
// type.
WitnessScript

// TaprootPubKey represents a p2tr (pay-to-taproot) address type.
TaprootPubKey
)

// ManagedAddress is an interface that provides acces to information regarding
Expand Down Expand Up @@ -269,6 +273,9 @@ func (a *managedAddress) PubKey() *btcec.PublicKey {
// pubKeyBytes returns the serialized public key bytes for the managed address
// based on whether or not the managed address is marked as compressed.
func (a *managedAddress) pubKeyBytes() []byte {
if a.addrType == TaprootPubKey {
return schnorr.SerializePubKey(a.pubKey)
}
if a.compressed {
return a.pubKey.SerializeCompressed()
}
Expand Down Expand Up @@ -415,6 +422,16 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager,
if err != nil {
return nil, err
}

case TaprootPubKey:
tapKey := txscript.ComputeTaprootKeyNoScript(pubKey)
address, err = btcutil.NewAddressTaproot(
schnorr.SerializePubKey(tapKey),
m.rootManager.chainParams,
)
if err != nil {
return nil, err
}
}

return &managedAddress{
Expand Down
125 changes: 125 additions & 0 deletions waddrmgr/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2957,3 +2957,128 @@ func TestDeriveFromKeyPathCache(t *testing.T) {
require.Equal(t, cachedKey.Serialize(), cachedKey2.Serialize())
require.Equal(t, derivedKey.Serialize(), cachedKey2.Serialize())
}

// TestTaprootPubKeyDerivation tests that p2tr addresses can be derived from the
// scoped manager when using the BIP0086 key scope.
func TestTaprootPubKeyDerivation(t *testing.T) {
t.Parallel()

teardown, db := emptyDB(t)
defer teardown()

// From: https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
rootKey, _ := hdkeychain.NewKeyFromString(
"xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLi" +
"sriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu",
)

// We'll start the test by creating a new root manager that will be
// used for the duration of the test.
var mgr *Manager
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey)
if err != nil {
return err
}
err = Create(
ns, rootKey, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{},
)
if err != nil {
return err
}
mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams)
if err != nil {
return err
}

return mgr.Unlock(ns, privPassphrase)
})
require.NoError(t, err, "create/open: unexpected error: %v", err)

defer mgr.Close()

// Now that we have the manager created, we'll fetch one of the default
// scopes for usage within this test.
scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0086)
require.NoError(
t, err, "unable to fetch scope %v: %v", KeyScopeBIP0086, err,
)

externalPath := DerivationPath{
InternalAccount: 0,
Account: hdkeychain.HardenedKeyStart,
Branch: 0,
Index: 0,
}
internalPath := DerivationPath{
InternalAccount: 0,
Account: hdkeychain.HardenedKeyStart,
Branch: 1,
Index: 0,
}

assertAddressDerivation(
t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) {
return scopedMgr.DeriveFromKeyPath(ns, externalPath)
},
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
)
assertAddressDerivation(
t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) {
addrs, err := scopedMgr.NextExternalAddresses(ns, 0, 1)
if err != nil {
return nil, err
}
return addrs[0], nil
},
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
)
assertAddressDerivation(
t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) {
return scopedMgr.LastExternalAddress(ns, 0)
},
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
)
assertAddressDerivation(
t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) {
return scopedMgr.DeriveFromKeyPath(ns, internalPath)
},
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
)
assertAddressDerivation(
t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) {
addrs, err := scopedMgr.NextInternalAddresses(ns, 0, 1)
if err != nil {
return nil, err
}
return addrs[0], nil
},
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
)
assertAddressDerivation(
t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) {
return scopedMgr.LastInternalAddress(ns, 0)
},
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
)
}

// assertAddressDerivation makes sure the address derived in the given callback
// is the one that is expected.
func assertAddressDerivation(t *testing.T, db walletdb.DB,
fn func(walletdb.ReadWriteBucket) (ManagedAddress, error),
expectedAddr string) {

var address ManagedAddress
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)

var err error
address, err = fn(ns)
return err
})
require.NoError(t, err, "unable to derive addr: %v", err)

require.Equal(t, expectedAddr, address.Address().String())
}
18 changes: 15 additions & 3 deletions waddrmgr/scoped_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ var (
Coin: 0,
}

// KeyScopeBIP0086 is the key scope for BIP0086 derivation. BIP0086
// will be used to derive all p2tr addresses.
KeyScopeBIP0086 = KeyScope{
Purpose: 86,
Coin: 0,
}

// KeyScopeBIP0044 is the key scope for BIP0044 derivation. Legacy
// wallets will only be able to use this key scope, and no keys beyond
// it.
Expand All @@ -170,6 +177,7 @@ var (
DefaultKeyScopes = []KeyScope{
KeyScopeBIP0049Plus,
KeyScopeBIP0084,
KeyScopeBIP0086,
KeyScopeBIP0044,
}

Expand All @@ -185,6 +193,10 @@ var (
ExternalAddrType: WitnessPubKey,
InternalAddrType: WitnessPubKey,
},
KeyScopeBIP0086: {
ExternalAddrType: TaprootPubKey,
InternalAddrType: TaprootPubKey,
},
KeyScopeBIP0044: {
InternalAddrType: PubKeyHash,
ExternalAddrType: PubKeyHash,
Expand Down Expand Up @@ -2413,7 +2425,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) (
switch net {
case wire.MainNet:
switch s.scope {
case KeyScopeBIP0044:
case KeyScopeBIP0044, KeyScopeBIP0086:
version = HDVersionMainNetBIP0044
case KeyScopeBIP0049Plus:
version = HDVersionMainNetBIP0049
Expand All @@ -2427,7 +2439,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) (
netparams.SigNetWire(s.rootManager.ChainParams()):

switch s.scope {
case KeyScopeBIP0044:
case KeyScopeBIP0044, KeyScopeBIP0086:
version = HDVersionTestNetBIP0044
case KeyScopeBIP0049Plus:
version = HDVersionTestNetBIP0049
Expand All @@ -2439,7 +2451,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) (

case wire.SimNet:
switch s.scope {
case KeyScopeBIP0044:
case KeyScopeBIP0044, KeyScopeBIP0086:
version = HDVersionSimNetBIP0044
// We use the mainnet versions for simnet keys when the keys
// belong to a key scope which simnet doesn't have a defined
Expand Down

0 comments on commit 3ad07da

Please sign in to comment.