URI: 
       tMerge pull request #4465 from SomberNight/purpose48_segwit_multisig_path2 - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 357ff8e833480c94f217a2deb5afd22a0a45330a
   DIR parent 41a257c638ed1a8f40e7130126f38f2baa02f53f
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue, 26 Jun 2018 19:43:52 +0200
       
       Merge pull request #4465 from SomberNight/purpose48_segwit_multisig_path2
       
       wizard: extend derivation dialog to also let user select script type
       Diffstat:
         M gui/qt/installwizard.py             |      32 +++++++++++++++++++++++++++++--
         M lib/base_wizard.py                  |      58 +++++++++++++++++--------------
         M lib/bitcoin.py                      |      17 ++++++++++++-----
         M lib/keystore.py                     |      28 ++++++++++++++++++++++++++--
         M lib/tests/test_bitcoin.py           |      18 ++++++++++++++++++
       
       5 files changed, 118 insertions(+), 35 deletions(-)
       ---
   DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py
       t@@ -520,6 +520,34 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
                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=()):
                vbox = QVBoxLayout()
       t@@ -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))
   DIR diff --git a/lib/base_wizard.py b/lib/base_wizard.py
       t@@ -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 _
       t@@ -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)
       t@@ -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:
       t@@ -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):
   DIR diff --git a/lib/bitcoin.py b/lib/bitcoin.py
       t@@ -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,
       t@@ -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,
       t@@ -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''
       t@@ -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)
       t@@ -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:
   DIR diff --git a/lib/keystore.py b/lib/keystore.py
       t@@ -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
        
       t@@ -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':
   DIR diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py
       t@@ -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
       t@@ -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',