Skip to content

Commit

Permalink
Merge #472: Have getmasterxpub account for BIP 44 things
Browse files Browse the repository at this point in the history
645636b Have getmasterxpub account for address type, chain, and account (Andrew Chow)
5092788 Add BIP 44 path helper functions (Andrew Chow)

Pull request description:

  BIP 44 specifies some additional arguments that are used in the derivation path; these are the address type (purpose), the chain type, and account number. Previously `getmasterxpub` would only output the xpub at the fixed path `m/44'/0'/0'`. This PR adds parameters to `getmasterxpub` to specify the purpose (via `--addr-type`), and the account (via `--account`). Additionally, the chain type will also be taken into account when determining the path to use.

  Fixes #309

  Based on #470 for the addrtype rename.

Top commit has no ACKs.

Tree-SHA512: e02f88f3050f9ac57be4e63a7974e90ef187442fc52310f39d5d999c5179603db2f02111355138fea23071b1ad28ff14b73d4ea007d15a44f2ded02c2a3d6f97
  • Loading branch information
achow101 committed Mar 5, 2021
2 parents b635ec1 + 645636b commit cb7c7c7
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 31 deletions.
6 changes: 4 additions & 2 deletions hwilib/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def enumerate_handler(args: argparse.Namespace) -> List[Dict[str, Any]]:
return enumerate(password=args.password)

def getmasterxpub_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]:
return getmasterxpub(client)
return getmasterxpub(client, addrtype=args.addr_type, account=args.account)

def getxpub_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]:
return getxpub(client, path=args.path, expert=args.expert)
Expand Down Expand Up @@ -152,7 +152,9 @@ def get_parser() -> HWIArgumentParser:
enumerate_parser = subparsers.add_parser('enumerate', help='List all available devices')
enumerate_parser.set_defaults(func=enumerate_handler)

getmasterxpub_parser = subparsers.add_parser('getmasterxpub', help='Get the extended public key at m/44\'/0\'/0\'')
getmasterxpub_parser = subparsers.add_parser('getmasterxpub', help='Get the extended public key for BIP 44 standard derivation paths. Convenience function to get xpubs given the address type, account, and chain type.')
getmasterxpub_parser.add_argument("--addr-type", help="Get the master xpub used to derive addresses for this address type", type=AddressType.argparse, choices=list(AddressType), default=AddressType.WIT_V0) # type: ignore
getmasterxpub_parser.add_argument("--account", help="The account number", type=int, default=0)
getmasterxpub_parser.set_defaults(func=getmasterxpub_handler)

signtx_parser = subparsers.add_parser('signtx', help='Sign a PSBT')
Expand Down
20 changes: 6 additions & 14 deletions hwilib/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

from ._base58 import xpub_to_pub_hex
from .key import (
get_bip44_purpose,
get_bip44_chain,
H_,
HARDENED_FLAG,
is_hardened,
Expand All @@ -49,7 +51,6 @@
from .devices import __all__ as all_devs
from .common import (
AddressType,
Chain,
)
from .hwwclient import HardwareWalletClient
from .psbt import PSBT
Expand Down Expand Up @@ -165,15 +166,15 @@ def find_device(
pass # Ignore things we wouldn't get fingerprints for
return None

def getmasterxpub(client: HardwareWalletClient) -> Dict[str, str]:
def getmasterxpub(client: HardwareWalletClient, addrtype: AddressType = AddressType.WIT_V0, account: int = 0) -> Dict[str, str]:
"""
Get the master extended public key from a client
:param client: The client to interact with
:return: A dictionary containing the public key at the ``m/44'/0'/0'`` derivation path.
Returned as ``{"xpub": <xpub string>}``.
"""
return {"xpub": client.get_master_xpub().to_string()}
return {"xpub": client.get_master_xpub(addrtype, account).to_string()}

def signtx(client: HardwareWalletClient, psbt: str) -> Dict[str, str]:
"""
Expand Down Expand Up @@ -292,19 +293,10 @@ def getdescriptor(
parsed_path = []
if not path:
# Purpose
if is_wpkh:
parsed_path.append(H_(84))
elif is_sh_wpkh:
parsed_path.append(H_(49))
else:
assert addr_type == AddressType.LEGACY
parsed_path.append(H_(44))
parsed_path.append(H_(get_bip44_purpose(addr_type)))

# Coin type
if client.chain == Chain.MAIN:
parsed_path.append(H_(0))
else:
parsed_path.append(H_(1))
parsed_path.append(H_(get_bip44_chain(client.chain)))

# Account
parsed_path.append(H_(account))
Expand Down
19 changes: 12 additions & 7 deletions hwilib/hwwclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
Union,
)
from .descriptor import PubkeyProvider
from .key import ExtendedKey
from .key import (
ExtendedKey,
get_bip44_purpose,
get_bip44_chain,
)
from .psbt import PSBT
from .common import AddressType, Chain

Expand Down Expand Up @@ -41,16 +45,17 @@ def __init__(self, path: str, password: str, expert: bool) -> None:
self.xpub_cache: Dict[str, str] = {}
self.expert = expert

def get_master_xpub(self) -> ExtendedKey:
def get_master_xpub(self, addrtype: AddressType = AddressType.WIT_V0, account: int = 0) -> ExtendedKey:
"""
Get the master BIP 44 public key.
Retrieves a BIP 44 master public key
Retrieves the public key at the "m/44h/0h/0h" derivation path.
Get the extended public key used to derive receiving and change addresses with the BIP 44 derivation path scheme.
The returned xpub will be dependent on the address type requested, the chain type, and the BIP 44 account number.
:return: The extended public key at "m/44h/0h/0h"
:return: The extended public key
"""
# FIXME testnet is not handled yet
return self.get_pubkey_at_path("m/44h/0h/0h")
path = f"m/{get_bip44_purpose(addrtype)}h/{get_bip44_chain(self.chain)}h/{account}h"
return self.get_pubkey_at_path(path)

def get_master_fingerprint(self) -> bytes:
"""
Expand Down
32 changes: 32 additions & 0 deletions hwilib/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

from . import _base58 as base58
from .common import (
AddressType,
Chain,
hash256,
hash160,
)
Expand Down Expand Up @@ -350,3 +352,33 @@ def str_to_harden(x: str) -> int:
return [str_to_harden(x) for x in n]
except Exception:
raise ValueError("Invalid BIP32 path", nstr)


def get_bip44_purpose(addrtype: AddressType) -> int:
"""
Determine the BIP 44 purpose based on the given :class:`~hwilib.common.AddressType`.
:param addrtype: The address type
"""
if addrtype == AddressType.LEGACY:
return 44
elif addrtype == AddressType.SH_WIT_V0:
return 49
elif addrtype == AddressType.WIT_V0:
return 84
else:
raise ValueError("Unknown address type")


def get_bip44_chain(chain: Chain) -> int:
"""
Determine the BIP 44 coin type based on the Bitcoin chain type.
For the Bitcoin mainnet chain, this returns 0. For the other chains, this returns 1.
:param chain: The chain
"""
if chain == Chain.MAIN:
return 0
else:
return 1
12 changes: 6 additions & 6 deletions test/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,31 +154,31 @@ def test_enumerate(self):
self.assertTrue(found)

def test_no_type(self):
gmxp_res = self.do_command(['getmasterxpub'])
gmxp_res = self.do_command(['getmasterxpub', "--addr-type", "legacy"])
self.assertIn('error', gmxp_res)
self.assertEqual(gmxp_res['error'], 'You must specify a device type or fingerprint for all commands except enumerate')
self.assertIn('code', gmxp_res)
self.assertEqual(gmxp_res['code'], -1)

def test_path_type(self):
gmxp_res = self.do_command(self.get_password_args() + ['-t', self.type, '-d', self.path, 'getmasterxpub'])
gmxp_res = self.do_command(self.get_password_args() + ['-t', self.type, '-d', self.path, 'getmasterxpub', "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], self.master_xpub)

def test_fingerprint_autodetect(self):
gmxp_res = self.do_command(self.get_password_args() + ['-f', self.fingerprint, 'getmasterxpub'])
gmxp_res = self.do_command(self.get_password_args() + ['-f', self.fingerprint, 'getmasterxpub', "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], self.master_xpub)

# Nonexistent fingerprint
gmxp_res = self.do_command(self.get_password_args() + ['-f', '0000ffff', 'getmasterxpub'])
gmxp_res = self.do_command(self.get_password_args() + ['-f', '0000ffff', 'getmasterxpub', "--addr-type", "legacy"])
self.assertEqual(gmxp_res['error'], 'Could not find device with specified fingerprint')
self.assertEqual(gmxp_res['code'], -3)

def test_type_only_autodetect(self):
gmxp_res = self.do_command(self.get_password_args() + ['-t', self.type, 'getmasterxpub'])
gmxp_res = self.do_command(self.get_password_args() + ['-t', self.type, 'getmasterxpub', "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], self.master_xpub)

# Unknown device type
gmxp_res = self.do_command(['-t', 'fakedev', '-d', 'fakepath', 'getmasterxpub'])
gmxp_res = self.do_command(['-t', 'fakedev', '-d', 'fakepath', 'getmasterxpub', "--addr-type", "legacy"])
self.assertEqual(gmxp_res['error'], 'Unknown device type specified')
self.assertEqual(gmxp_res['code'], -4)

Expand Down
2 changes: 1 addition & 1 deletion test/test_keepkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def test_getxpub(self):
load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english')

# Test getmasterxpub
gmxp_res = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:21324', 'getmasterxpub'])
gmxp_res = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:21324', 'getmasterxpub', "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], vec['master_xpub'])

# Test the path derivs
Expand Down
2 changes: 1 addition & 1 deletion test/test_trezor.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def test_getxpub(self):
load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english')

# Test getmasterxpub
gmxp_res = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', 'getmasterxpub'])
gmxp_res = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', 'getmasterxpub', "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], vec['master_xpub'])

# Test the path derivs
Expand Down

0 comments on commit cb7c7c7

Please sign in to comment.