commit 11733d6bc271646a00b69ff07657119598874da4
parent 85a7aa291ed67399f4300ce6ff7a0ad16444e2db
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:
3 files changed, 49 insertions(+), 4 deletions(-)
diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
@@ -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)
@@ -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:
@@ -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)
diff --git a/electrum/bip32.py b/electrum/bip32.py
@@ -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:
@@ -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)
diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py
@@ -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
@@ -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'/"))