From 8b86f21520681f535817a67682ae659221adf9d3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 26 Jun 2018 19:31:05 +0200 Subject: [PATCH] wizard: extend derivation dialog to also let user select script type. enable segwit multisig for bip39/hw wallets. --- gui/qt/installwizard.py | 32 +++++++++++++++++++-- lib/base_wizard.py | 58 +++++++++++++++++++++------------------ lib/bitcoin.py | 17 ++++++++---- lib/keystore.py | 28 +++++++++++++++++-- lib/tests/test_bitcoin.py | 18 ++++++++++++ 5 files changed, 118 insertions(+), 35 deletions(-) diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py index 7f7328a59..70bac5e92 100644 --- a/gui/qt/installwizard.py +++ b/gui/qt/installwizard.py @@ -519,6 +519,34 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): self.exec_layout(vbox, '') return clayout.selected_index() + @wizard_dialog + def choice_and_line_dialog(self, title, message1, choices, message2, + test_text, run_next) -> (str, str): + vbox = QVBoxLayout() + + c_values = [x[0] for x in choices] + c_titles = [x[1] for x in choices] + c_default_text = [x[2] for x in choices] + def on_choice_click(clayout): + idx = clayout.selected_index() + line.setText(c_default_text[idx]) + clayout = ChoicesLayout(message1, c_titles, on_choice_click) + vbox.addLayout(clayout.layout()) + + vbox.addSpacing(50) + vbox.addWidget(WWLabel(message2)) + + line = QLineEdit() + def on_text_change(text): + self.next_button.setEnabled(test_text(text)) + line.textEdited.connect(on_text_change) + on_choice_click(clayout) # set default text for "line" + vbox.addWidget(line) + + self.exec_layout(vbox, title) + choice = c_values[clayout.selected_index()] + return str(line.text()), choice + @wizard_dialog def line_dialog(self, run_next, title, message, default, test, warning='', presets=()): @@ -535,9 +563,9 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): for preset in presets: button = QPushButton(preset[0]) button.clicked.connect(lambda __, text=preset[1]: line.setText(text)) - button.setMaximumWidth(150) + button.setMinimumWidth(150) hbox = QHBoxLayout() - hbox.addWidget(button, Qt.AlignCenter) + hbox.addWidget(button, alignment=Qt.AlignCenter) vbox.addLayout(hbox) self.exec_layout(vbox, title, next_enabled=test(default)) diff --git a/lib/base_wizard.py b/lib/base_wizard.py index 132ac001f..fd3c8cc39 100644 --- a/lib/base_wizard.py +++ b/lib/base_wizard.py @@ -30,7 +30,7 @@ from functools import partial from . import bitcoin from . import keystore -from .keystore import bip44_derivation +from .keystore import bip44_derivation, purpose48_derivation from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption from .i18n import _ @@ -279,13 +279,9 @@ class BaseWizard(object): self.choose_hw_device(purpose) return if purpose == HWD_SETUP_NEW_WALLET: - if self.wallet_type=='multisig': - # There is no general standard for HD multisig. - # This is partially compatible with BIP45; assumes index=0 - self.on_hw_derivation(name, device_info, "m/45'/0") - else: - f = lambda x: self.run('on_hw_derivation', name, device_info, str(x)) - self.derivation_dialog(f) + def f(derivation, script_type): + self.run('on_hw_derivation', name, device_info, derivation, script_type) + self.derivation_and_script_type_dialog(f) elif purpose == HWD_SETUP_DECRYPT_WALLET: derivation = get_derivation_used_for_hw_device_encryption() xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self) @@ -302,30 +298,39 @@ class BaseWizard(object): else: raise Exception('unknown purpose: %s' % purpose) - def derivation_dialog(self, f): - default = bip44_derivation(0, bip43_purpose=44) - message = '\n'.join([ - _('Enter your wallet derivation here.'), + def derivation_and_script_type_dialog(self, f): + message1 = _('Choose the type of addresses in your wallet.') + message2 = '\n'.join([ + _('You can override the suggested derivation path.'), _('If you are not sure what this is, leave this field unchanged.') ]) - presets = ( - ('legacy BIP44', bip44_derivation(0, bip43_purpose=44)), - ('p2sh-segwit BIP49', bip44_derivation(0, bip43_purpose=49)), - ('native-segwit BIP84', bip44_derivation(0, bip43_purpose=84)), - ) + if self.wallet_type == 'multisig': + # There is no general standard for HD multisig. + # For legacy, this is partially compatible with BIP45; assumes index=0 + # For segwit, a custom path is used, as there is no standard at all. + choices = [ + ('standard', 'legacy multisig (p2sh)', "m/45'/0"), + ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), + ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), + ] + else: + choices = [ + ('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), + ('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), + ('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)), + ] while True: try: - self.line_dialog(run_next=f, title=_('Derivation'), message=message, - default=default, test=bitcoin.is_bip32_derivation, - presets=presets) + self.choice_and_line_dialog( + run_next=f, title=_('Script type and Derivation path'), message1=message1, + message2=message2, choices=choices, test_text=bitcoin.is_bip32_derivation) return except ScriptTypeNotSupported as e: self.show_error(e) # let the user choose again - def on_hw_derivation(self, name, device_info, derivation): + def on_hw_derivation(self, name, device_info, derivation, xtype): from .keystore import hardware_keystore - xtype = keystore.xtype_from_derivation(derivation) try: xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) except ScriptTypeNotSupported: @@ -379,15 +384,16 @@ class BaseWizard(object): raise Exception('Unknown seed type', self.seed_type) def on_restore_bip39(self, seed, passphrase): - f = lambda x: self.run('on_bip43', seed, passphrase, str(x)) - self.derivation_dialog(f) + def f(derivation, script_type): + self.run('on_bip43', seed, passphrase, derivation, script_type) + self.derivation_and_script_type_dialog(f) def create_keystore(self, seed, passphrase): k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig') self.on_keystore(k) - def on_bip43(self, seed, passphrase, derivation): - k = keystore.from_bip39_seed(seed, passphrase, derivation) + def on_bip43(self, seed, passphrase, derivation, script_type): + k = keystore.from_bip39_seed(seed, passphrase, derivation, xtype=script_type) self.on_keystore(k) def on_keystore(self, k): diff --git a/lib/bitcoin.py b/lib/bitcoin.py index a452ef7bc..ad205efb8 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -398,7 +398,7 @@ def DecodeBase58Check(psz): # backwards compat # extended WIF for segwit (used in 3.0.x; but still used internally) # the keys in this dict should be a superset of what Imported Wallets can import -SCRIPT_TYPES = { +WIF_SCRIPT_TYPES = { 'p2pkh':0, 'p2wpkh':1, 'p2wpkh-p2sh':2, @@ -406,6 +406,14 @@ SCRIPT_TYPES = { 'p2wsh':6, 'p2wsh-p2sh':7 } +WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES) + + +PURPOSE48_SCRIPT_TYPES = { + 'p2wsh-p2sh': 1, # specifically multisig + 'p2wsh': 2, # specifically multisig +} +PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES) def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, @@ -413,7 +421,7 @@ def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, # we only export secrets inside curve range secret = ecc.ECPrivkey.normalize_secret_bytes(secret) if internal_use: - prefix = bytes([(SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255]) + prefix = bytes([(WIF_SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255]) else: prefix = bytes([constants.net.WIF_PREFIX]) suffix = b'\01' if compressed else b'' @@ -432,7 +440,7 @@ def deserialize_privkey(key: str) -> (str, bytes, bool): txin_type = None if ':' in key: txin_type, key = key.split(sep=':', maxsplit=1) - if txin_type not in SCRIPT_TYPES: + if txin_type not in WIF_SCRIPT_TYPES: raise BitcoinException('unknown script type: {}'.format(txin_type)) try: vch = DecodeBase58Check(key) @@ -444,9 +452,8 @@ def deserialize_privkey(key: str) -> (str, bytes, bool): if txin_type is None: # keys exported in version 3.0.x encoded script type in first byte prefix_value = vch[0] - constants.net.WIF_PREFIX - inverse_script_types = inv_dict(SCRIPT_TYPES) try: - txin_type = inverse_script_types[prefix_value] + txin_type = WIF_SCRIPT_TYPES_INV[prefix_value] except KeyError: raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0])) else: diff --git a/lib/keystore.py b/lib/keystore.py index 4c5a82852..098dd3f9b 100644 --- a/lib/keystore.py +++ b/lib/keystore.py @@ -600,15 +600,27 @@ def from_bip39_seed(seed, passphrase, derivation, xtype=None): return k -def xtype_from_derivation(derivation): +def xtype_from_derivation(derivation: str) -> str: """Returns the script type to be used for this derivation.""" if derivation.startswith("m/84'"): return 'p2wpkh' elif derivation.startswith("m/49'"): return 'p2wpkh-p2sh' - else: + elif derivation.startswith("m/44'"): + return 'standard' + elif derivation.startswith("m/45'"): return 'standard' + bip32_indices = list(bip32_derivation(derivation)) + if len(bip32_indices) >= 4: + if bip32_indices[0] == 48 + BIP32_PRIME: + # m / purpose' / coin_type' / account' / script_type' / change / address_index + script_type_int = bip32_indices[3] - BIP32_PRIME + script_type = PURPOSE48_SCRIPT_TYPES_INV.get(script_type_int) + if script_type is not None: + return script_type + return 'standard' + # extended pubkeys @@ -719,6 +731,18 @@ def bip44_derivation(account_id, bip43_purpose=44): coin = constants.net.BIP44_COIN_TYPE return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id)) + +def purpose48_derivation(account_id: int, xtype: str) -> str: + # m / purpose' / coin_type' / account' / script_type' / change / address_index + bip43_purpose = 48 + coin = constants.net.BIP44_COIN_TYPE + account_id = int(account_id) + script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype) + if script_type_int is None: + raise Exception('unknown xtype: {}'.format(xtype)) + return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int) + + def from_seed(seed, passphrase, is_p2sh): t = seed_type(seed) if t == 'old': diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py index 20ec5a30f..46a72e030 100644 --- a/lib/tests/test_bitcoin.py +++ b/lib/tests/test_bitcoin.py @@ -19,6 +19,7 @@ from lib.transaction import opcodes from lib.util import bfh, bh2u from lib import constants from lib.storage import WalletStorage +from lib.keystore import xtype_from_derivation from . import SequentialTestCase from . import TestCaseForTestnet @@ -469,6 +470,23 @@ class Test_xprv_xpub(SequentialTestCase): self.assertFalse(is_bip32_derivation("")) self.assertFalse(is_bip32_derivation("m/q8462")) + 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'/0'/0'")) + self.assertEqual('standard', xtype_from_derivation("m/44'/5241'/221")) + self.assertEqual('standard', xtype_from_derivation("m/45'")) + self.assertEqual('standard', xtype_from_derivation("m/45'/56165/271'")) + self.assertEqual('p2wpkh-p2sh', xtype_from_derivation("m/49'")) + self.assertEqual('p2wpkh-p2sh', xtype_from_derivation("m/49'/134")) + self.assertEqual('p2wpkh', xtype_from_derivation("m/84'")) + self.assertEqual('p2wpkh', xtype_from_derivation("m/84'/112'/992/112/33'/0/2")) + self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/0'/0'/1'")) + self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/0'/0'/1'/52112/52'")) + self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/9'/2'/1'")) + self.assertEqual('p2wsh', xtype_from_derivation("m/48'/0'/0'/2'")) + self.assertEqual('p2wsh', xtype_from_derivation("m/48'/1'/0'/2'/77'/0")) + def test_version_bytes(self): xprv_headers_b58 = { 'standard': 'xprv',