URI: 
       tMerge pull request #5692 from matejcik/trezor-shamir - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit ace61d2d20f07db743eb716e71021cb8aacf70e0
   DIR parent 9b28f6df7beab821422537f816cbacbe6035f706
  HTML Author: ghost43 <somber.night@protonmail.com>
       Date:   Thu, 19 Dec 2019 15:54:41 +0000
       
       Merge pull request #5692 from matejcik/trezor-shamir
       
       Trezor: support for Shamir backup and recovery
       Diffstat:
         M contrib/requirements/requirements-… |       2 +-
         M electrum/plugins/trezor/clientbase… |      26 +++++++++++++++++---------
         M electrum/plugins/trezor/qt.py       |      97 ++++++++++++++++++++++++++-----
         M electrum/plugins/trezor/trezor.py   |      30 +++++++++++++++++++++---------
       
       4 files changed, 123 insertions(+), 32 deletions(-)
       ---
   DIR diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
       t@@ -1,4 +1,4 @@
       -trezor[hidapi]>=0.11.0
       +trezor[hidapi]>=0.11.5
        safet[hidapi]>=0.1.0
        keepkey>=6.0.3
        btchip-python>=0.1.26
   DIR diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py
       t@@ -11,19 +11,27 @@ from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, Hardw
        
        from trezorlib.client import TrezorClient
        from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError
       -from trezorlib.messages import WordRequestType, FailureType, RecoveryDeviceType
       +from trezorlib.messages import WordRequestType, FailureType, RecoveryDeviceType, ButtonRequestType
        import trezorlib.btc
        import trezorlib.device
        
        MESSAGES = {
       -    3: _("Confirm the transaction output on your {} device"),
       -    4: _("Confirm internal entropy on your {} device to begin"),
       -    5: _("Write down the seed word shown on your {}"),
       -    6: _("Confirm on your {} that you want to wipe it clean"),
       -    7: _("Confirm on your {} device the message to sign"),
       -    8: _("Confirm the total amount spent and the transaction fee on your {} device"),
       -    10: _("Confirm wallet address on your {} device"),
       -    14: _("Choose on your {} device where to enter your passphrase"),
       +    ButtonRequestType.ConfirmOutput:
       +        _("Confirm the transaction output on your {} device"),
       +    ButtonRequestType.ResetDevice:
       +        _("Complete the initialization process on your {} device"),
       +    ButtonRequestType.ConfirmWord:
       +        _("Write down the seed word shown on your {}"),
       +    ButtonRequestType.WipeDevice:
       +        _("Confirm on your {} that you want to wipe it clean"),
       +    ButtonRequestType.ProtectCall:
       +        _("Confirm on your {} device the message to sign"),
       +    ButtonRequestType.SignTx:
       +        _("Confirm the total amount spent and the transaction fee on your {} device"),
       +    ButtonRequestType.Address:
       +        _("Confirm wallet address on your {} device"),
       +    ButtonRequestType.PassphraseType:
       +        _("Choose on your {} device where to enter your passphrase"),
            'default': _("Check your {} device to continue"),
        }
        
   DIR diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py
       t@@ -16,7 +16,7 @@ from electrum.util import bh2u
        from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
        from ..hw_wallet.plugin import only_hook_if_libraries_available
        from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
       -                     RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX)
       +                     Capability, BackupType, RecoveryDeviceType)
        
        
        PASSPHRASE_HELP_SHORT =_(
       t@@ -199,6 +199,8 @@ class QtPlugin(QtPluginBase):
                    raise Exception(_("The device was disconnected."))
                model = client.get_trezor_model()
                fw_version = client.client.version
       +        capabilities = client.client.features.capabilities
       +        have_shamir = Capability.Shamir in capabilities
        
                # label
                label = QLabel(_("Enter a label to name your device:"))
       t@@ -209,22 +211,88 @@ class QtPlugin(QtPluginBase):
                hl.addStretch(1)
                vbox.addLayout(hl)
        
       +        # Backup type
       +        gb_backuptype = QGroupBox()
       +        hbox_backuptype = QHBoxLayout()
       +        gb_backuptype.setLayout(hbox_backuptype)
       +        vbox.addWidget(gb_backuptype)
       +        gb_backuptype.setTitle(_('Select backup type:'))
       +        bg_backuptype = QButtonGroup()
       +
       +        rb_single = QRadioButton(gb_backuptype)
       +        rb_single.setText(_('Single seed (BIP39)'))
       +        bg_backuptype.addButton(rb_single)
       +        bg_backuptype.setId(rb_single, BackupType.Bip39)
       +        hbox_backuptype.addWidget(rb_single)
       +        rb_single.setChecked(True)
       +
       +        rb_shamir = QRadioButton(gb_backuptype)
       +        rb_shamir.setText(_('Shamir'))
       +        bg_backuptype.addButton(rb_shamir)
       +        bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic)
       +        hbox_backuptype.addWidget(rb_shamir)
       +        rb_shamir.setEnabled(Capability.Shamir in capabilities)
       +        rb_shamir.setVisible(False)  # visible with "expert settings"
       +
       +        rb_shamir_groups = QRadioButton(gb_backuptype)
       +        rb_shamir_groups.setText(_('Super Shamir'))
       +        bg_backuptype.addButton(rb_shamir_groups)
       +        bg_backuptype.setId(rb_shamir_groups, BackupType.Slip39_Advanced)
       +        hbox_backuptype.addWidget(rb_shamir_groups)
       +        rb_shamir_groups.setEnabled(Capability.ShamirGroups in capabilities)
       +        rb_shamir_groups.setVisible(False)  # visible with "expert settings"
       +
                # word count
       -        gb = QGroupBox()
       +        word_count_buttons = {}
       +
       +        gb_numwords = QGroupBox()
                hbox1 = QHBoxLayout()
       -        gb.setLayout(hbox1)
       -        vbox.addWidget(gb)
       -        gb.setTitle(_("Select your seed length:"))
       +        gb_numwords.setLayout(hbox1)
       +        vbox.addWidget(gb_numwords)
       +        gb_numwords.setTitle(_("Select seed/share length:"))
                bg_numwords = QButtonGroup()
       -        word_counts = (12, 18, 24)
       -        for i, count in enumerate(word_counts):
       -            rb = QRadioButton(gb)
       +        for count in (12, 18, 20, 24, 33):
       +            rb = QRadioButton(gb_numwords)
       +            word_count_buttons[count] = rb
                    rb.setText(_("{:d} words").format(count))
                    bg_numwords.addButton(rb)
       -            bg_numwords.setId(rb, i)
       +            bg_numwords.setId(rb, count)
                    hbox1.addWidget(rb)
                    rb.setChecked(True)
        
       +        def configure_word_counts():
       +            if model == "1":
       +                checked_wordcount = 24
       +            else:
       +                checked_wordcount = 12
       +
       +            if method == TIM_RECOVER:
       +                if have_shamir:
       +                    valid_word_counts = (12, 18, 20, 24, 33)
       +                else:
       +                    valid_word_counts = (12, 18, 24)
       +            elif rb_single.isChecked():
       +                valid_word_counts = (12, 18, 24)
       +                gb_numwords.setTitle(_('Select seed length:'))
       +            else:
       +                valid_word_counts = (20, 33)
       +                checked_wordcount = 20
       +                gb_numwords.setTitle(_('Select share length:'))
       +
       +            word_count_buttons[checked_wordcount].setChecked(True)
       +            for c, btn in word_count_buttons.items():
       +                btn.setVisible(c in valid_word_counts)
       +
       +        bg_backuptype.buttonClicked.connect(configure_word_counts)
       +        configure_word_counts()
       +
       +        # set up conditional visibility:
       +        # 1. backup_type is only visible when creating new seed
       +        gb_backuptype.setVisible(method == TIM_NEW)
       +        # 2. word_count is not visible when recovering on TT
       +        if method == TIM_RECOVER and model != "1":
       +            gb_numwords.setVisible(False)
       +
                # PIN
                cb_pin = QCheckBox(_('Enable PIN protection'))
                cb_pin.setChecked(True)
       t@@ -240,6 +308,8 @@ class QtPlugin(QtPluginBase):
                def show_expert_settings():
                    expert_button.setVisible(False)
                    expert_widget.setVisible(True)
       +            rb_shamir.setVisible(True)
       +            rb_shamir_groups.setVisible(True)
                expert_button.clicked.connect(show_expert_settings)
                vbox.addWidget(expert_button)
        
       t@@ -255,7 +325,7 @@ class QtPlugin(QtPluginBase):
        
                # ask for recovery type (random word order OR matrix)
                bg_rectype = None
       -        if method == TIM_RECOVER and not model == 'T':
       +        if method == TIM_RECOVER and model == '1':
                    gb_rectype = QGroupBox()
                    hbox_rectype = QHBoxLayout()
                    gb_rectype.setLayout(hbox_rectype)
       t@@ -266,14 +336,14 @@ class QtPlugin(QtPluginBase):
                    rb1 = QRadioButton(gb_rectype)
                    rb1.setText(_('Scrambled words'))
                    bg_rectype.addButton(rb1)
       -            bg_rectype.setId(rb1, RECOVERY_TYPE_SCRAMBLED_WORDS)
       +            bg_rectype.setId(rb1, RecoveryDeviceType.ScrambledWords)
                    hbox_rectype.addWidget(rb1)
                    rb1.setChecked(True)
        
                    rb2 = QRadioButton(gb_rectype)
                    rb2.setText(_('Matrix'))
                    bg_rectype.addButton(rb2)
       -            bg_rectype.setId(rb2, RECOVERY_TYPE_MATRIX)
       +            bg_rectype.setId(rb2, RecoveryDeviceType.Matrix)
                    hbox_rectype.addWidget(rb2)
        
                # no backup
       t@@ -293,11 +363,12 @@ class QtPlugin(QtPluginBase):
                wizard.exec_layout(vbox, next_enabled=next_enabled)
        
                return TrezorInitSettings(
       -            word_count=word_counts[bg_numwords.checkedId()],
       +            word_count=bg_numwords.checkedId(),
                    label=name.text(),
                    pin_enabled=cb_pin.isChecked(),
                    passphrase_enabled=cb_phrase.isChecked(),
                    recovery_type=bg_rectype.checkedId() if bg_rectype else None,
       +            backup_type=bg_backuptype.checkedId(),
                    no_backup=cb_no_backup.isChecked() if cb_no_backup else False,
                )
        
   DIR diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py
       t@@ -28,19 +28,29 @@ try:
            from .clientbase import TrezorClientBase
        
            from trezorlib.messages import (
       -        RecoveryDeviceType, HDNodeType, HDNodePathType,
       +        Capability, BackupType, RecoveryDeviceType, HDNodeType, HDNodePathType,
                InputScriptType, OutputScriptType, MultisigRedeemScriptType,
                TxInputType, TxOutputType, TxOutputBinType, TransactionType, SignTx)
        
       -    RECOVERY_TYPE_SCRAMBLED_WORDS = RecoveryDeviceType.ScrambledWords
       -    RECOVERY_TYPE_MATRIX = RecoveryDeviceType.Matrix
       -
            TREZORLIB = True
        except Exception as e:
            _logger.exception('error importing trezorlib')
            TREZORLIB = False
        
       -    RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX = range(2)
       +    class _EnumMissing:
       +        def __init__(self):
       +            self.counter = 0
       +            self.values = {}
       +
       +        def __getattr__(self, key):
       +            if key not in self.values:
       +                self.values[key] = self.counter
       +                self.counter += 1
       +            return self.values[key]
       +
       +    Capability = _EnumMissing()
       +    BackupType = _EnumMissing()
       +    RecoveryDeviceType = _EnumMissing()
        
        
        # Trezor initialization methods
       t@@ -87,6 +97,7 @@ class TrezorInitSettings(NamedTuple):
            pin_enabled: bool
            passphrase_enabled: bool
            recovery_type: Any = None
       +    backup_type: int = BackupType.Bip39
            no_backup: bool = False
        
        
       t@@ -101,7 +112,7 @@ class TrezorPlugin(HW_PluginBase):
            libraries_URL = 'https://github.com/trezor/python-trezor'
            minimum_firmware = (1, 5, 2)
            keystore_class = TrezorKeyStore
       -    minimum_library = (0, 11, 0)
       +    minimum_library = (0, 11, 5)
            maximum_library = (0, 12)
            SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
            DEVICE_IDS = (TREZOR_PRODUCT_KEY,)
       t@@ -211,7 +222,7 @@ class TrezorPlugin(HW_PluginBase):
                    wizard.loop.exit(exit_code)
        
            def _initialize_device(self, settings: TrezorInitSettings, method, device_id, wizard, handler):
       -        if method == TIM_RECOVER and settings.recovery_type == RECOVERY_TYPE_SCRAMBLED_WORDS:
       +        if method == TIM_RECOVER and settings.recovery_type == RecoveryDeviceType.ScrambledWords:
                    handler.show_error(_(
                        "You will be asked to enter 24 words regardless of your "
                        "seed's actual length.  If you enter a word incorrectly or "
       t@@ -226,12 +237,13 @@ class TrezorPlugin(HW_PluginBase):
                    raise Exception(_("The device was disconnected."))
        
                if method == TIM_NEW:
       -            strength_from_word_count = {12: 128, 18: 192, 24: 256}
       +            strength_from_word_count = {12: 128, 18: 192, 20: 128, 24: 256, 33: 256}
                    client.reset_device(
                        strength=strength_from_word_count[settings.word_count],
                        passphrase_protection=settings.passphrase_enabled,
                        pin_protection=settings.pin_enabled,
                        label=settings.label,
       +                backup_type=settings.backup_type,
                        no_backup=settings.no_backup)
                elif method == TIM_RECOVER:
                    client.recover_device(
       t@@ -240,7 +252,7 @@ class TrezorPlugin(HW_PluginBase):
                        passphrase_protection=settings.passphrase_enabled,
                        pin_protection=settings.pin_enabled,
                        label=settings.label)
       -            if settings.recovery_type == RECOVERY_TYPE_MATRIX:
       +            if settings.recovery_type == RecoveryDeviceType.Matrix:
                        handler.close_matrix_dialog()
                else:
                    raise RuntimeError("Unsupported recovery method")