wallet: add derived public key import
Co-authored-by: Oliver Gugger <gugger@gmail.com>
This commit is contained in:
parent
9d909110f9
commit
b0a4956231
5 changed files with 593 additions and 1 deletions
1
go.mod
1
go.mod
|
@ -17,6 +17,7 @@ require (
|
||||||
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec // indirect
|
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec // indirect
|
||||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf
|
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf
|
||||||
github.com/lightninglabs/neutrino v0.11.0
|
github.com/lightninglabs/neutrino v0.11.0
|
||||||
|
github.com/stretchr/testify v1.5.1
|
||||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
|
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
|
||||||
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 // indirect
|
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 // indirect
|
||||||
|
|
|
@ -168,6 +168,15 @@ var (
|
||||||
ExternalAddrType: PubKeyHash,
|
ExternalAddrType: PubKeyHash,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyScopeBIP0049AddrSchema is the address schema for the traditional
|
||||||
|
// BIP-0049 derivation scheme. This exists in order to support importing
|
||||||
|
// accounts from other wallets that don't use our modified BIP-0049
|
||||||
|
// derivation scheme (internal addresses are P2WKH instead of NP2WKH).
|
||||||
|
KeyScopeBIP0049AddrSchema = ScopeAddrSchema{
|
||||||
|
ExternalAddrType: NestedWitnessPubKey,
|
||||||
|
InternalAddrType: NestedWitnessPubKey,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ScopedKeyManager is a sub key manager under the main root key manager. The
|
// ScopedKeyManager is a sub key manager under the main root key manager. The
|
||||||
|
@ -1842,7 +1851,8 @@ func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket,
|
||||||
// The start block needs to be updated when the newly imported address
|
// The start block needs to be updated when the newly imported address
|
||||||
// is before the current one.
|
// is before the current one.
|
||||||
s.rootManager.mtx.Lock()
|
s.rootManager.mtx.Lock()
|
||||||
updateStartBlock := bs.Height < s.rootManager.syncState.startBlock.Height
|
updateStartBlock := bs != nil &&
|
||||||
|
bs.Height < s.rootManager.syncState.startBlock.Height
|
||||||
s.rootManager.mtx.Unlock()
|
s.rootManager.mtx.Unlock()
|
||||||
|
|
||||||
// Save the new imported address to the db and update start block (if
|
// Save the new imported address to the db and update start block (if
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcutil/hdkeychain"
|
"github.com/btcsuite/btcutil/hdkeychain"
|
||||||
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultDBTimeout specifies the timeout value when opening the wallet
|
// defaultDBTimeout specifies the timeout value when opening the wallet
|
||||||
|
@ -51,3 +53,48 @@ func testWallet(t *testing.T) (*Wallet, func()) {
|
||||||
|
|
||||||
return w, cleanup
|
return w, cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testWalletWatchingOnly creates a test watch only wallet and unlocks it.
|
||||||
|
func testWalletWatchingOnly(t *testing.T) (*Wallet, func()) {
|
||||||
|
// Set up a wallet.
|
||||||
|
dir, err := ioutil.TempDir("", "test_wallet_watch_only")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create db dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup := func() {
|
||||||
|
if err := os.RemoveAll(dir); err != nil {
|
||||||
|
t.Fatalf("could not cleanup test: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pubPass := []byte("hello")
|
||||||
|
loader := NewLoader(
|
||||||
|
&chaincfg.TestNet3Params, dir, true, defaultDBTimeout, 250,
|
||||||
|
)
|
||||||
|
w, err := loader.CreateNewWatchingOnlyWallet(pubPass, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create wallet: %v", err)
|
||||||
|
}
|
||||||
|
chainClient := &mockChainClient{}
|
||||||
|
w.chainClient = chainClient
|
||||||
|
|
||||||
|
err = walletdb.Update(w.Database(), func(tx walletdb.ReadWriteTx) error {
|
||||||
|
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||||
|
for scope, schema := range waddrmgr.ScopeAddrMap {
|
||||||
|
_, err := w.Manager.NewScopedKeyManager(
|
||||||
|
ns, scope, schema,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create default scopes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, cleanup
|
||||||
|
}
|
||||||
|
|
252
wallet/import.go
252
wallet/import.go
|
@ -1,13 +1,265 @@
|
||||||
package wallet
|
package wallet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/btcsuite/btcutil/hdkeychain"
|
||||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
"github.com/btcsuite/btcwallet/walletdb"
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// accountPubKeyDepth is the maximum depth of an extended key for an
|
||||||
|
// account public key.
|
||||||
|
accountPubKeyDepth = 3
|
||||||
|
|
||||||
|
// pubKeyDepth is the depth of an extended key for a derived public key.
|
||||||
|
pubKeyDepth = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// keyScopeFromPubKey returns the corresponding wallet key scope for the given
|
||||||
|
// extended public key. The address type can usually be inferred from the key's
|
||||||
|
// version, but may be required for certain keys to map them into the proper
|
||||||
|
// scope.
|
||||||
|
func keyScopeFromPubKey(pubKey *hdkeychain.ExtendedKey,
|
||||||
|
addrType *waddrmgr.AddressType) (waddrmgr.KeyScope,
|
||||||
|
*waddrmgr.ScopeAddrSchema, error) {
|
||||||
|
|
||||||
|
switch waddrmgr.HDVersion(binary.BigEndian.Uint32(pubKey.Version())) {
|
||||||
|
// For BIP-0044 keys, an address type must be specified as we intend to
|
||||||
|
// not support importing BIP-0044 keys into the wallet using the legacy
|
||||||
|
// pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will
|
||||||
|
// force the standard BIP-0049 derivation scheme (nested witness pubkeys
|
||||||
|
// everywhere), while a witness address type will force the standard
|
||||||
|
// BIP-0084 derivation scheme.
|
||||||
|
case waddrmgr.HDVersionMainNetBIP0044, waddrmgr.HDVersionTestNetBIP0044:
|
||||||
|
if addrType == nil {
|
||||||
|
return waddrmgr.KeyScope{}, nil, errors.New("address " +
|
||||||
|
"type must be specified for account public " +
|
||||||
|
"key with legacy version")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch *addrType {
|
||||||
|
case waddrmgr.NestedWitnessPubKey:
|
||||||
|
return waddrmgr.KeyScopeBIP0049Plus,
|
||||||
|
&waddrmgr.KeyScopeBIP0049AddrSchema, nil
|
||||||
|
|
||||||
|
case waddrmgr.WitnessPubKey:
|
||||||
|
return waddrmgr.KeyScopeBIP0084, nil, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return waddrmgr.KeyScope{}, nil,
|
||||||
|
fmt.Errorf("unsupported address type %v",
|
||||||
|
*addrType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For BIP-0049 keys, we'll need to make a distinction between the
|
||||||
|
// traditional BIP-0049 address schema (nested witness pubkeys
|
||||||
|
// everywhere) and our own BIP-0049Plus address schema (nested
|
||||||
|
// externally, witness internally).
|
||||||
|
case waddrmgr.HDVersionMainNetBIP0049, waddrmgr.HDVersionTestNetBIP0049:
|
||||||
|
if addrType == nil {
|
||||||
|
return waddrmgr.KeyScope{}, nil, errors.New("address " +
|
||||||
|
"type must be specified for account public " +
|
||||||
|
"key with BIP-0049 version")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch *addrType {
|
||||||
|
case waddrmgr.NestedWitnessPubKey:
|
||||||
|
return waddrmgr.KeyScopeBIP0049Plus,
|
||||||
|
&waddrmgr.KeyScopeBIP0049AddrSchema, nil
|
||||||
|
|
||||||
|
case waddrmgr.WitnessPubKey:
|
||||||
|
return waddrmgr.KeyScopeBIP0049Plus, nil, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return waddrmgr.KeyScope{}, nil,
|
||||||
|
fmt.Errorf("unsupported address type %v",
|
||||||
|
*addrType)
|
||||||
|
}
|
||||||
|
|
||||||
|
case waddrmgr.HDVersionMainNetBIP0084, waddrmgr.HDVersionTestNetBIP0084:
|
||||||
|
if addrType != nil && *addrType != waddrmgr.WitnessPubKey {
|
||||||
|
return waddrmgr.KeyScope{}, nil,
|
||||||
|
errors.New("address type mismatch")
|
||||||
|
}
|
||||||
|
return waddrmgr.KeyScopeBIP0084, nil, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return waddrmgr.KeyScope{}, nil, fmt.Errorf("unknown version %x",
|
||||||
|
pubKey.Version())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPubKeyForNet determines if the given public key is for the current network
|
||||||
|
// the wallet is operating under.
|
||||||
|
func (w *Wallet) isPubKeyForNet(pubKey *hdkeychain.ExtendedKey) bool {
|
||||||
|
version := waddrmgr.HDVersion(binary.BigEndian.Uint32(pubKey.Version()))
|
||||||
|
switch w.chainParams.Net {
|
||||||
|
case wire.MainNet:
|
||||||
|
return version == waddrmgr.HDVersionMainNetBIP0044 ||
|
||||||
|
version == waddrmgr.HDVersionMainNetBIP0049 ||
|
||||||
|
version == waddrmgr.HDVersionMainNetBIP0084
|
||||||
|
|
||||||
|
case wire.TestNet, wire.TestNet3:
|
||||||
|
return version == waddrmgr.HDVersionTestNetBIP0044 ||
|
||||||
|
version == waddrmgr.HDVersionTestNetBIP0049 ||
|
||||||
|
version == waddrmgr.HDVersionTestNetBIP0084
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateExtendedPubKey ensures a sane derived public key is provided.
|
||||||
|
func (w *Wallet) validateExtendedPubKey(pubKey *hdkeychain.ExtendedKey,
|
||||||
|
isAccountKey bool) error {
|
||||||
|
|
||||||
|
// Private keys are not allowed.
|
||||||
|
if pubKey.IsPrivate() {
|
||||||
|
return errors.New("private keys cannot be imported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The public key must have a version corresponding to the current
|
||||||
|
// chain.
|
||||||
|
if !w.isPubKeyForNet(pubKey) {
|
||||||
|
return fmt.Errorf("expected extended public key for current "+
|
||||||
|
"network %v", w.chainParams.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the extended public key's depth and child index based on
|
||||||
|
// whether it's an account key or not.
|
||||||
|
if isAccountKey {
|
||||||
|
if pubKey.Depth() != accountPubKeyDepth {
|
||||||
|
return errors.New("invalid account key, must be of the " +
|
||||||
|
"form m/purpose'/coin_type'/account'")
|
||||||
|
}
|
||||||
|
if pubKey.ChildIndex() < hdkeychain.HardenedKeyStart {
|
||||||
|
return errors.New("invalid account key, must be hardened")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pubKey.Depth() != pubKeyDepth {
|
||||||
|
return errors.New("invalid account key, must be of the " +
|
||||||
|
"form m/purpose'/coin_type'/account'/change/" +
|
||||||
|
"address_index")
|
||||||
|
}
|
||||||
|
if pubKey.ChildIndex() >= hdkeychain.HardenedKeyStart {
|
||||||
|
return errors.New("invalid pulic key, must not be " +
|
||||||
|
"hardened")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportAccount imports an account backed by an account extended public key.
|
||||||
|
// The master key fingerprint denotes the fingerprint of the root key
|
||||||
|
// corresponding to the account public key (also known as the key with
|
||||||
|
// derivation path m/). This may be required by some hardware wallets for proper
|
||||||
|
// identification and signing.
|
||||||
|
//
|
||||||
|
// The address type can usually be inferred from the key's version, but may be
|
||||||
|
// required for certain keys to map them into the proper scope.
|
||||||
|
//
|
||||||
|
// For BIP-0044 keys, an address type must be specified as we intend to not
|
||||||
|
// support importing BIP-0044 keys into the wallet using the legacy
|
||||||
|
// pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will force
|
||||||
|
// the standard BIP-0049 derivation scheme, while a witness address type will
|
||||||
|
// force the standard BIP-0084 derivation scheme.
|
||||||
|
//
|
||||||
|
// For BIP-0049 keys, an address type must also be specified to make a
|
||||||
|
// distinction between the traditional BIP-0049 address schema (nested witness
|
||||||
|
// pubkeys everywhere) and our own BIP-0049Plus address schema (nested
|
||||||
|
// externally, witness internally).
|
||||||
|
func (w *Wallet) ImportAccount(name string, accountPubKey *hdkeychain.ExtendedKey,
|
||||||
|
masterKeyFingerprint uint32, addrType *waddrmgr.AddressType) (
|
||||||
|
*waddrmgr.AccountProperties, error) {
|
||||||
|
|
||||||
|
// Ensure we have a valid account public key.
|
||||||
|
if err := w.validateExtendedPubKey(accountPubKey, true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine what key scope the account public key should belong to and
|
||||||
|
// whether it should use a custom address schema.
|
||||||
|
keyScope, addrSchema, err := keyScopeFromPubKey(accountPubKey, addrType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scopedMgr, err := w.Manager.FetchScopedKeyManager(keyScope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the account as watch-only within the database.
|
||||||
|
var accountProps *waddrmgr.AccountProperties
|
||||||
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
||||||
|
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||||
|
account, err := scopedMgr.NewAccountWatchingOnly(
|
||||||
|
ns, name, accountPubKey, masterKeyFingerprint,
|
||||||
|
addrSchema,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
accountProps, err = scopedMgr.AccountProperties(ns, account)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return accountProps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportPublicKey imports a single derived public key into the address manager.
|
||||||
|
// The address type can usually be inferred from the key's version, but in the
|
||||||
|
// case of legacy versions (xpub, tpub), an address type must be specified as we
|
||||||
|
// intend to not support importing BIP-44 keys into the wallet using the legacy
|
||||||
|
// pay-to-pubkey-hash (P2PKH) scheme.
|
||||||
|
func (w *Wallet) ImportPublicKey(pubKey *btcec.PublicKey,
|
||||||
|
addrType waddrmgr.AddressType) error {
|
||||||
|
|
||||||
|
// Determine what key scope the public key should belong to and import
|
||||||
|
// it into the key scope's default imported account.
|
||||||
|
var keyScope waddrmgr.KeyScope
|
||||||
|
switch addrType {
|
||||||
|
case waddrmgr.NestedWitnessPubKey:
|
||||||
|
keyScope = waddrmgr.KeyScopeBIP0049Plus
|
||||||
|
case waddrmgr.WitnessPubKey:
|
||||||
|
keyScope = waddrmgr.KeyScopeBIP0084
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("address type %v is not supported", addrType)
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedKeyManager, err := w.Manager.FetchScopedKeyManager(keyScope)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Perform rescan if requested.
|
||||||
|
var addr waddrmgr.ManagedAddress
|
||||||
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
||||||
|
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||||
|
addr, err = scopedKeyManager.ImportPublicKey(ns, pubKey, nil)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Imported address %v", addr.Address())
|
||||||
|
|
||||||
|
err = w.chainClient.NotifyReceived([]btcutil.Address{addr.Address()})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to subscribe for address "+
|
||||||
|
"notifications: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ImportPrivateKey imports a private key to the wallet and writes the new
|
// ImportPrivateKey imports a private key to the wallet and writes the new
|
||||||
// wallet to disk.
|
// wallet to disk.
|
||||||
//
|
//
|
||||||
|
|
282
wallet/import_test.go
Normal file
282
wallet/import_test.go
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/btcsuite/btcutil/hdkeychain"
|
||||||
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hardenedKey(key uint32) uint32 {
|
||||||
|
return key + hdkeychain.HardenedKeyStart
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveAcctPubKey(t *testing.T, root *hdkeychain.ExtendedKey,
|
||||||
|
scope waddrmgr.KeyScope, paths ...uint32) *hdkeychain.ExtendedKey {
|
||||||
|
|
||||||
|
path := []uint32{hardenedKey(scope.Purpose), hardenedKey(scope.Coin)}
|
||||||
|
path = append(path, paths...)
|
||||||
|
|
||||||
|
var (
|
||||||
|
currentKey = root
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for _, pathPart := range path {
|
||||||
|
currentKey, err = currentKey.Derive(pathPart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Neuter() method checks the version and doesn't know any
|
||||||
|
// non-standard methods. We need to convert them to standard, neuter,
|
||||||
|
// then convert them back with the target extended public key version.
|
||||||
|
pubVersionBytes := make([]byte, 4)
|
||||||
|
copy(pubVersionBytes, chaincfg.TestNet3Params.HDPublicKeyID[:])
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(root.String(), "uprv"):
|
||||||
|
binary.BigEndian.PutUint32(pubVersionBytes, uint32(
|
||||||
|
waddrmgr.HDVersionTestNetBIP0049,
|
||||||
|
))
|
||||||
|
|
||||||
|
case strings.HasPrefix(root.String(), "vprv"):
|
||||||
|
binary.BigEndian.PutUint32(pubVersionBytes, uint32(
|
||||||
|
waddrmgr.HDVersionTestNetBIP0084,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
currentKey, err = currentKey.CloneWithVersion(
|
||||||
|
chaincfg.TestNet3Params.HDPrivateKeyID[:],
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
currentKey, err = currentKey.Neuter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
currentKey, err = currentKey.CloneWithVersion(pubVersionBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return currentKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
masterPriv string
|
||||||
|
accountIndex uint32
|
||||||
|
addrType waddrmgr.AddressType
|
||||||
|
addrSchemaOverride *waddrmgr.ScopeAddrSchema
|
||||||
|
expectedScope waddrmgr.KeyScope
|
||||||
|
expectedAddr string
|
||||||
|
expectedChangeAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
testCases = []*testCase{{
|
||||||
|
name: "bip44 with nested witness address type",
|
||||||
|
masterPriv: "tprv8ZgxMBicQKsPeWwrFuNjEGTTDSY4mRLwd2KDJAPGa1AY" +
|
||||||
|
"quw38bZqNMSuB3V1Va3hqJBo9Pt8Sx7kBQer5cNMrb8SYquoWPt9" +
|
||||||
|
"Y3BZdhdtUcw",
|
||||||
|
accountIndex: 0,
|
||||||
|
addrType: waddrmgr.NestedWitnessPubKey,
|
||||||
|
expectedScope: waddrmgr.KeyScopeBIP0049Plus,
|
||||||
|
expectedAddr: "2N5YTxG9XtGXx1YyhZb7N2pwEjoZLLMHGKj",
|
||||||
|
expectedChangeAddr: "2N7wpz5Gy2zEJTvq2MAuU6BCTEBLXNQ8dUw",
|
||||||
|
}, {
|
||||||
|
name: "bip44 with witness address type",
|
||||||
|
masterPriv: "tprv8ZgxMBicQKsPeWwrFuNjEGTTDSY4mRLwd2KDJAPGa1AY" +
|
||||||
|
"quw38bZqNMSuB3V1Va3hqJBo9Pt8Sx7kBQer5cNMrb8SYquoWPt9" +
|
||||||
|
"Y3BZdhdtUcw",
|
||||||
|
accountIndex: 777,
|
||||||
|
addrType: waddrmgr.WitnessPubKey,
|
||||||
|
expectedScope: waddrmgr.KeyScopeBIP0084,
|
||||||
|
expectedAddr: "tb1qllxcutkzsukf8u8c8stkp464j0esu9xq7qju8x",
|
||||||
|
expectedChangeAddr: "tb1qu6jmqglrthscptjqj3egx54wy8xqvzn5hslgw7",
|
||||||
|
}, {
|
||||||
|
name: "traditional bip49",
|
||||||
|
masterPriv: "uprv8tXDerPXZ1QsVp8y6GAMSMYxPQgWi3LSY8qS5ZH9x1YRu" +
|
||||||
|
"1kGPFjPzR73CFSbVUhdEwJbtsUgucUJ4hGQoJnNepp3RBcE6Jhdom" +
|
||||||
|
"FD2KeY6G9",
|
||||||
|
accountIndex: 9,
|
||||||
|
addrType: waddrmgr.NestedWitnessPubKey,
|
||||||
|
addrSchemaOverride: &waddrmgr.KeyScopeBIP0049AddrSchema,
|
||||||
|
expectedScope: waddrmgr.KeyScopeBIP0049Plus,
|
||||||
|
expectedAddr: "2NBCJ9WzGXZqpLpXGq3Hacybj3c4eHRcqgh",
|
||||||
|
expectedChangeAddr: "2N3bankFu6F3ZNU41iVJQqyS9MXqp9dvn1M",
|
||||||
|
}, {
|
||||||
|
name: "bip49+",
|
||||||
|
masterPriv: "uprv8tXDerPXZ1QsVp8y6GAMSMYxPQgWi3LSY8qS5ZH9x1YRu" +
|
||||||
|
"1kGPFjPzR73CFSbVUhdEwJbtsUgucUJ4hGQoJnNepp3RBcE6Jhdom" +
|
||||||
|
"FD2KeY6G9",
|
||||||
|
accountIndex: 9,
|
||||||
|
addrType: waddrmgr.WitnessPubKey,
|
||||||
|
expectedScope: waddrmgr.KeyScopeBIP0049Plus,
|
||||||
|
expectedAddr: "2NBCJ9WzGXZqpLpXGq3Hacybj3c4eHRcqgh",
|
||||||
|
expectedChangeAddr: "tb1qeqn05w2hfq6axpdprhs4y7x65gxkkvfvyxqk4u",
|
||||||
|
}, {
|
||||||
|
name: "bip84",
|
||||||
|
masterPriv: "vprv9DMUxX4ShgxMM7L5vcwyeSeTZNpxefKwTFMerxB3L1vJ" +
|
||||||
|
"x7ZVdutxcUmBDTQBVPMYeaRQeM5FNGpqwysyX1CPT4VeHXJegDX8" +
|
||||||
|
"5VJrQvaFaz3",
|
||||||
|
accountIndex: 1,
|
||||||
|
addrType: waddrmgr.WitnessPubKey,
|
||||||
|
expectedScope: waddrmgr.KeyScopeBIP0084,
|
||||||
|
expectedAddr: "tb1q5vepvcl0z8xj7kps4rsux722r4dvfwlhk6j532",
|
||||||
|
expectedChangeAddr: "tb1qlwe2kgxcsa8x4huu79yff4rze0l5mwafg5c7xd",
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestImportAccount tests that extended public keys can successfully be
|
||||||
|
// imported into both watch only and normal wallets.
|
||||||
|
func TestImportAccount(t *testing.T) {
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w, cleanup := testWallet(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
testImportAccount(t, w, tc, false, tc.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
name := tc.name + " watch-only"
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w, cleanup := testWalletWatchingOnly(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
testImportAccount(t, w, tc, true, name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
|
||||||
|
name string) {
|
||||||
|
|
||||||
|
// First derive the master public key of the account we want to import.
|
||||||
|
root, err := hdkeychain.NewKeyFromString(tc.masterPriv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Derive the extended private and public key for our target account.
|
||||||
|
acct1Pub := deriveAcctPubKey(
|
||||||
|
t, root, tc.expectedScope, hardenedKey(tc.accountIndex),
|
||||||
|
)
|
||||||
|
|
||||||
|
// We want to make sure we can import and handle multiple accounts, so
|
||||||
|
// we create another one.
|
||||||
|
acct2Pub := deriveAcctPubKey(
|
||||||
|
t, root, tc.expectedScope, hardenedKey(tc.accountIndex+1),
|
||||||
|
)
|
||||||
|
|
||||||
|
// And we also want to be able to import loose extended public keys
|
||||||
|
// without needing to specify an explicit scope.
|
||||||
|
acct3ExternalExtPub := deriveAcctPubKey(
|
||||||
|
t, root, tc.expectedScope, hardenedKey(tc.accountIndex+2), 0, 0,
|
||||||
|
)
|
||||||
|
acct3ExternalPub, err := acct3ExternalExtPub.ECPubKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Import the extended public keys into new accounts.
|
||||||
|
acct1, err := w.ImportAccount(
|
||||||
|
name+"1", acct1Pub, root.ParentFingerprint(), &tc.addrType,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expectedScope, acct1.KeyScope)
|
||||||
|
|
||||||
|
acct2, err := w.ImportAccount(
|
||||||
|
name+"2", acct2Pub, root.ParentFingerprint(), &tc.addrType,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expectedScope, acct2.KeyScope)
|
||||||
|
|
||||||
|
err = w.ImportPublicKey(acct3ExternalPub, tc.addrType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// If the wallet is watch only, there is no default account and our
|
||||||
|
// imported account will be index 0.
|
||||||
|
firstAccountIndex := uint32(1)
|
||||||
|
if watchOnly {
|
||||||
|
firstAccountIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have 3 additional accounts now.
|
||||||
|
acctResult, err := w.Accounts(tc.expectedScope)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, acctResult.Accounts, int(firstAccountIndex*2)+2)
|
||||||
|
|
||||||
|
// Validate the state of the accounts.
|
||||||
|
require.Equal(t, firstAccountIndex, acct1.AccountNumber)
|
||||||
|
require.Equal(t, name+"1", acct1.AccountName)
|
||||||
|
require.Equal(t, true, acct1.IsWatchOnly)
|
||||||
|
require.Equal(t, root.ParentFingerprint(), acct1.MasterKeyFingerprint)
|
||||||
|
require.NotNil(t, acct1.AccountPubKey)
|
||||||
|
require.Equal(t, acct1Pub.String(), acct1.AccountPubKey.String())
|
||||||
|
require.Equal(t, uint32(0), acct1.InternalKeyCount)
|
||||||
|
require.Equal(t, uint32(0), acct1.ExternalKeyCount)
|
||||||
|
require.Equal(t, uint32(0), acct1.ImportedKeyCount)
|
||||||
|
|
||||||
|
require.Equal(t, firstAccountIndex+1, acct2.AccountNumber)
|
||||||
|
require.Equal(t, name+"2", acct2.AccountName)
|
||||||
|
require.Equal(t, true, acct2.IsWatchOnly)
|
||||||
|
require.Equal(t, root.ParentFingerprint(), acct2.MasterKeyFingerprint)
|
||||||
|
require.NotNil(t, acct2.AccountPubKey)
|
||||||
|
require.Equal(t, acct2Pub.String(), acct2.AccountPubKey.String())
|
||||||
|
require.Equal(t, uint32(0), acct2.InternalKeyCount)
|
||||||
|
require.Equal(t, uint32(0), acct2.ExternalKeyCount)
|
||||||
|
require.Equal(t, uint32(0), acct2.ImportedKeyCount)
|
||||||
|
|
||||||
|
// Test address derivation.
|
||||||
|
addr, err := w.NewAddress(acct1.AccountNumber, tc.expectedScope)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expectedAddr, addr.String())
|
||||||
|
addr, err = w.NewChangeAddress(acct1.AccountNumber, tc.expectedScope)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expectedChangeAddr, addr.String())
|
||||||
|
|
||||||
|
// Make sure the key count was increased.
|
||||||
|
acct1, err = w.AccountProperties(tc.expectedScope, acct1.AccountNumber)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(1), acct1.InternalKeyCount)
|
||||||
|
require.Equal(t, uint32(1), acct1.ExternalKeyCount)
|
||||||
|
require.Equal(t, uint32(0), acct1.ImportedKeyCount)
|
||||||
|
|
||||||
|
// Make sure we can't get private keys for the imported accounts.
|
||||||
|
_, err = w.DumpWIFPrivateKey(addr)
|
||||||
|
require.True(t, waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly))
|
||||||
|
|
||||||
|
// Get the address info for the single key we imported.
|
||||||
|
switch tc.addrType {
|
||||||
|
case waddrmgr.NestedWitnessPubKey:
|
||||||
|
witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||||
|
btcutil.Hash160(acct3ExternalPub.SerializeCompressed()),
|
||||||
|
&chaincfg.TestNet3Params,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
witnessProg, err := txscript.PayToAddrScript(witnessAddr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
addr, err = btcutil.NewAddressScriptHash(
|
||||||
|
witnessProg, &chaincfg.TestNet3Params,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
case waddrmgr.WitnessPubKey:
|
||||||
|
addr, err = btcutil.NewAddressWitnessPubKeyHash(
|
||||||
|
btcutil.Hash160(acct3ExternalPub.SerializeCompressed()),
|
||||||
|
&chaincfg.TestNet3Params,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.Fatalf("unhandled address type %v", tc.addrType)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrManaged, err := w.AddressInfo(addr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, addrManaged.Imported())
|
||||||
|
}
|
Loading…
Reference in a new issue