electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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:
Melectrum/base_wizard.py | 4+++-
Melectrum/bip32.py | 28+++++++++++++++++++++++++++-
Melectrum/tests/test_bitcoin.py | 21+++++++++++++++++++--
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'/"))