URI: 
       twizard: normalize bip32 derivation path - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 11733d6bc271646a00b69ff07657119598874da4
   DIR parent 85a7aa291ed67399f4300ce6ff7a0ad16444e2db
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Fri, 22 Feb 2019 00:13:37 +0100
       
       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)
       
       Diffstat:
         M electrum/base_wizard.py             |       4 +++-
         M electrum/bip32.py                   |      28 +++++++++++++++++++++++++++-
         M electrum/tests/test_bitcoin.py      |      21 +++++++++++++++++++--
       
       3 files changed, 49 insertions(+), 4 deletions(-)
       ---
   DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
       t@@ -32,7 +32,7 @@ from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any
        from . import bitcoin
        from . import keystore
        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 .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet,
                             wallet_types, Wallet, Abstract_Wallet)
       t@@ -340,6 +340,7 @@ class BaseWizard(object):
                    return
                if purpose == HWD_SETUP_NEW_WALLET:
                    def f(derivation, script_type):
       +                derivation = normalize_bip32_derivation(derivation)
                        self.run('on_hw_derivation', name, device_info, derivation, script_type)
                    self.derivation_and_script_type_dialog(f)
                elif purpose == HWD_SETUP_DECRYPT_WALLET:
       t@@ -452,6 +453,7 @@ class BaseWizard(object):
        
            def on_restore_bip39(self, seed, passphrase):
                def f(derivation, script_type):
       +            derivation = normalize_bip32_derivation(derivation)
                    self.run('on_bip43', seed, passphrase, derivation, script_type)
                self.derivation_and_script_type_dialog(f)
        
   DIR diff --git a/electrum/bip32.py b/electrum/bip32.py
       t@@ -292,6 +292,8 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
                    x = x[:-1]
                    prime = BIP32_PRIME
                if x.startswith('-'):
       +            if prime:
       +                raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways")
                    prime = BIP32_PRIME
                child_index = abs(int(x)) | prime
                if child_index > UINT32_MAX:
       t@@ -300,12 +302,36 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
            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:
            try:
       -        if not s.startswith('m/'):
       +        if not (s == 'm' or s.startswith('m/')):
                    return False
                convert_bip32_path_to_list_of_uint32(s)
            except:
                return False
            else:
                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)
   DIR diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py
       t@@ -9,9 +9,10 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key,
                                      is_compressed_privkey, EncodeBase58Check,
                                      script_num_to_hex, push_script, add_number_to_script, int_to_hex,
                                      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,
       -                            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 import ecc, crypto, constants
        from electrum.ecc import number_to_string, string_to_number
       t@@ -463,18 +464,34 @@ class Test_xprv_xpub(SequentialTestCase):
            def test_is_bip32_derivation(self):
                self.assertTrue(is_bip32_derivation("m/0'/1"))
                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/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("n/"))
                self.assertFalse(is_bip32_derivation(""))
                self.assertFalse(is_bip32_derivation("m/q8462"))
       +        self.assertFalse(is_bip32_derivation("m/-8h"))
        
            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([], convert_bip32_path_to_list_of_uint32("m/"))
                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):
                self.assertEqual('standard', xtype_from_derivation("m/44'"))
                self.assertEqual('standard', xtype_from_derivation("m/44'/"))