wizard: normalize bip32 derivation path

so that what gets put in storage is "canonical"
(from now on... we could storage upgrade existing wallets
but it's not critical)
This commit is contained in:
SomberNight 2019-02-22 00:13:37 +01:00
parent 85a7aa291e
commit 11733d6bc2
No known key found for this signature in database
GPG key ID: B33B5F232C6271E9
3 changed files with 49 additions and 4 deletions

View file

@ -32,7 +32,7 @@ from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any
from . import bitcoin from . import bitcoin
from . import keystore from . import keystore
from . import mnemonic from . import mnemonic
from .bip32 import is_bip32_derivation, xpub_type from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation
from .keystore import bip44_derivation, purpose48_derivation from .keystore import bip44_derivation, purpose48_derivation
from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet,
wallet_types, Wallet, Abstract_Wallet) wallet_types, Wallet, Abstract_Wallet)
@ -340,6 +340,7 @@ class BaseWizard(object):
return return
if purpose == HWD_SETUP_NEW_WALLET: if purpose == HWD_SETUP_NEW_WALLET:
def f(derivation, script_type): def f(derivation, script_type):
derivation = normalize_bip32_derivation(derivation)
self.run('on_hw_derivation', name, device_info, derivation, script_type) self.run('on_hw_derivation', name, device_info, derivation, script_type)
self.derivation_and_script_type_dialog(f) self.derivation_and_script_type_dialog(f)
elif purpose == HWD_SETUP_DECRYPT_WALLET: elif purpose == HWD_SETUP_DECRYPT_WALLET:
@ -452,6 +453,7 @@ class BaseWizard(object):
def on_restore_bip39(self, seed, passphrase): def on_restore_bip39(self, seed, passphrase):
def f(derivation, script_type): def f(derivation, script_type):
derivation = normalize_bip32_derivation(derivation)
self.run('on_bip43', seed, passphrase, derivation, script_type) self.run('on_bip43', seed, passphrase, derivation, script_type)
self.derivation_and_script_type_dialog(f) self.derivation_and_script_type_dialog(f)

View file

@ -292,6 +292,8 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
x = x[:-1] x = x[:-1]
prime = BIP32_PRIME prime = BIP32_PRIME
if x.startswith('-'): if x.startswith('-'):
if prime:
raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways")
prime = BIP32_PRIME prime = BIP32_PRIME
child_index = abs(int(x)) | prime child_index = abs(int(x)) | prime
if child_index > UINT32_MAX: if child_index > UINT32_MAX:
@ -300,12 +302,36 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
return path return path
def convert_bip32_intpath_to_strpath(path: List[int]) -> str:
s = "m/"
for child_index in path:
if not isinstance(child_index, int):
raise TypeError(f"bip32 path child index must be int: {child_index}")
if not (0 <= child_index <= UINT32_MAX):
raise ValueError(f"bip32 path child index out of range: {child_index}")
prime = ""
if child_index & BIP32_PRIME:
prime = "'"
child_index = child_index ^ BIP32_PRIME
s += str(child_index) + prime + '/'
# cut trailing "/"
s = s[:-1]
return s
def is_bip32_derivation(s: str) -> bool: def is_bip32_derivation(s: str) -> bool:
try: try:
if not s.startswith('m/'): if not (s == 'm' or s.startswith('m/')):
return False return False
convert_bip32_path_to_list_of_uint32(s) convert_bip32_path_to_list_of_uint32(s)
except: except:
return False return False
else: else:
return True return True
def normalize_bip32_derivation(s: str) -> str:
if not is_bip32_derivation(s):
raise ValueError(f"invalid bip32 derivation: {s}")
ints = convert_bip32_path_to_list_of_uint32(s)
return convert_bip32_intpath_to_strpath(ints)

View file

@ -9,9 +9,10 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key,
is_compressed_privkey, EncodeBase58Check, is_compressed_privkey, EncodeBase58Check,
script_num_to_hex, push_script, add_number_to_script, int_to_hex, script_num_to_hex, push_script, add_number_to_script, int_to_hex,
opcodes) opcodes)
from electrum.bip32 import (BIP32Node, from electrum.bip32 import (BIP32Node, convert_bip32_intpath_to_strpath,
xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation,
is_xpub, convert_bip32_path_to_list_of_uint32) is_xpub, convert_bip32_path_to_list_of_uint32,
normalize_bip32_derivation)
from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS
from electrum import ecc, crypto, constants from electrum import ecc, crypto, constants
from electrum.ecc import number_to_string, string_to_number from electrum.ecc import number_to_string, string_to_number
@ -463,18 +464,34 @@ class Test_xprv_xpub(SequentialTestCase):
def test_is_bip32_derivation(self): def test_is_bip32_derivation(self):
self.assertTrue(is_bip32_derivation("m/0'/1")) self.assertTrue(is_bip32_derivation("m/0'/1"))
self.assertTrue(is_bip32_derivation("m/0'/0'")) self.assertTrue(is_bip32_derivation("m/0'/0'"))
self.assertTrue(is_bip32_derivation("m/3'/-5/8h/"))
self.assertTrue(is_bip32_derivation("m/44'/0'/0'/0/0")) self.assertTrue(is_bip32_derivation("m/44'/0'/0'/0/0"))
self.assertTrue(is_bip32_derivation("m/49'/0'/0'/0/0")) self.assertTrue(is_bip32_derivation("m/49'/0'/0'/0/0"))
self.assertTrue(is_bip32_derivation("m"))
self.assertTrue(is_bip32_derivation("m/"))
self.assertFalse(is_bip32_derivation("m5"))
self.assertFalse(is_bip32_derivation("mmmmmm")) self.assertFalse(is_bip32_derivation("mmmmmm"))
self.assertFalse(is_bip32_derivation("n/")) self.assertFalse(is_bip32_derivation("n/"))
self.assertFalse(is_bip32_derivation("")) self.assertFalse(is_bip32_derivation(""))
self.assertFalse(is_bip32_derivation("m/q8462")) self.assertFalse(is_bip32_derivation("m/q8462"))
self.assertFalse(is_bip32_derivation("m/-8h"))
def test_convert_bip32_path_to_list_of_uint32(self): def test_convert_bip32_path_to_list_of_uint32(self):
self.assertEqual([0, 0x80000001, 0x80000001], convert_bip32_path_to_list_of_uint32("m/0/-1/1'")) self.assertEqual([0, 0x80000001, 0x80000001], convert_bip32_path_to_list_of_uint32("m/0/-1/1'"))
self.assertEqual([], convert_bip32_path_to_list_of_uint32("m/")) self.assertEqual([], convert_bip32_path_to_list_of_uint32("m/"))
self.assertEqual([2147483692, 2147488889, 221], convert_bip32_path_to_list_of_uint32("m/44'/5241h/221")) self.assertEqual([2147483692, 2147488889, 221], convert_bip32_path_to_list_of_uint32("m/44'/5241h/221"))
def test_convert_bip32_intpath_to_strpath(self):
self.assertEqual("m/0/1'/1'", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001]))
self.assertEqual("m", convert_bip32_intpath_to_strpath([]))
self.assertEqual("m/44'/5241'/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221]))
def test_normalize_bip32_derivation(self):
self.assertEqual("m/0/1'/1'", normalize_bip32_derivation("m/0/1h/1'"))
self.assertEqual("m", normalize_bip32_derivation("m////"))
self.assertEqual("m/0/2/1'", normalize_bip32_derivation("m/0/2/-1/"))
self.assertEqual("m/0/1'/1'/5'", normalize_bip32_derivation("m/0//-1/1'///5h"))
def test_xtype_from_derivation(self): def test_xtype_from_derivation(self):
self.assertEqual('standard', xtype_from_derivation("m/44'")) self.assertEqual('standard', xtype_from_derivation("m/44'"))
self.assertEqual('standard', xtype_from_derivation("m/44'/")) self.assertEqual('standard', xtype_from_derivation("m/44'/"))