URI: 
       tMerge pull request #4880 from spesmilo/2fa_segwit - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit dd848304e6772a7736614c62579a14a1aa791d82
   DIR parent 4681ac8c23cc587f7bd0842e6aa922a9feff01de
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue, 11 Dec 2018 18:33:41 +0100
       
       Merge pull request #4880 from spesmilo/2fa_segwit
       
       2fa segwit (from ghost43's PR)
       Diffstat:
         M electrum/base_wizard.py             |      26 ++++++++++++++------------
         M electrum/bitcoin.py                 |       6 ++++++
         M electrum/plugins/trustedcoin/qt.py  |      12 ------------
         M electrum/plugins/trustedcoin/trust… |     140 ++++++++++++++++++++-----------
         M electrum/storage.py                 |       2 --
         M electrum/tests/test_wallet_vertica… |       5 +++--
         M electrum/version.py                 |       9 ++++++---
       
       7 files changed, 122 insertions(+), 78 deletions(-)
       ---
   DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
       t@@ -417,7 +417,7 @@ class BaseWizard(object):
                    self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
                elif self.seed_type == 'old':
                    self.run('create_keystore', seed, '')
       -        elif self.seed_type == '2fa':
       +        elif bitcoin.is_any_2fa_seed_type(self.seed_type):
                    self.load_2fa()
                    self.run('on_restore_seed', seed, is_ext)
                else:
       t@@ -540,18 +540,20 @@ class BaseWizard(object):
            def show_xpub_and_add_cosigners(self, xpub):
                self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
        
       -    def choose_seed_type(self):
       +    def choose_seed_type(self, message=None, choices=None):
                title = _('Choose Seed type')
       -        message = ' '.join([
       -            _("The type of addresses used by your wallet will depend on your seed."),
       -            _("Segwit wallets use bech32 addresses, defined in BIP173."),
       -            _("Please note that websites and other wallets may not support these addresses yet."),
       -            _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.")
       -        ])
       -        choices = [
       -            ('create_segwit_seed', _('Segwit')),
       -            ('create_standard_seed', _('Legacy')),
       -        ]
       +        if message is None:
       +            message = ' '.join([
       +                _("The type of addresses used by your wallet will depend on your seed."),
       +                _("Segwit wallets use bech32 addresses, defined in BIP173."),
       +                _("Please note that websites and other wallets may not support these addresses yet."),
       +                _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.")
       +            ])
       +        if choices is None:
       +            choices = [
       +                ('create_segwit_seed', _('Segwit')),
       +                ('create_standard_seed', _('Legacy')),
       +            ]
                self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
        
            def create_segwit_seed(self): self.create_seed('segwit')
   DIR diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py
       t@@ -207,6 +207,8 @@ def seed_type(x: str) -> str:
                return 'segwit'
            elif is_new_seed(x, version.SEED_PREFIX_2FA):
                return '2fa'
       +    elif is_new_seed(x, version.SEED_PREFIX_2FA_SW):
       +        return '2fa_segwit'
            return ''
        
        
       t@@ -214,6 +216,10 @@ def is_seed(x: str) -> bool:
            return bool(seed_type(x))
        
        
       +def is_any_2fa_seed_type(seed_type):
       +    return seed_type in ['2fa', '2fa_segwit']
       +
       +
        ############ functions from pywallet #####################
        
        def hash160_to_b58_address(h160: bytes, addrtype: int) -> str:
   DIR diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py
       t@@ -195,18 +195,6 @@ class Plugin(TrustedCoinPlugin):
                vbox.addLayout(Buttons(CloseButton(d)))
                d.exec_()
        
       -    def on_buy(self, window, k, v, d):
       -        d.close()
       -        if window.pluginsdialog:
       -            window.pluginsdialog.close()
       -        wallet = window.wallet
       -        uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000)
       -        wallet.is_billing = True
       -        window.pay_to_URI(uri)
       -        window.payto_e.setFrozen(True)
       -        window.message_e.setFrozen(True)
       -        window.amount_e.setFrozen(True)
       -
            def go_online_dialog(self, wizard):
                msg = [
                    _("Your wallet file is: {}.").format(os.path.abspath(wizard.storage.path)),
   DIR diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py
       t@@ -28,15 +28,17 @@ import json
        import base64
        import time
        import hashlib
       +from collections import defaultdict
       +from typing import Dict
        
        from urllib.parse import urljoin
        from urllib.parse import quote
        from aiohttp import ClientResponse
        
       -from electrum import ecc, constants, keystore, version, bip32
       -from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh
       +from electrum import ecc, constants, keystore, version, bip32, bitcoin
       +from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, seed_type, is_any_2fa_seed_type
        from electrum.bip32 import (deserialize_xpub, deserialize_xprv, bip32_private_key, CKD_pub,
       -                            serialize_xpub, bip32_root, bip32_private_derivation)
       +                            serialize_xpub, bip32_root, bip32_private_derivation, xpub_type)
        from electrum.crypto import sha256
        from electrum.transaction import TxOutput
        from electrum.mnemonic import Mnemonic
       t@@ -47,12 +49,18 @@ from electrum.util import NotEnoughFunds
        from electrum.storage import STO_EV_USER_PW
        from electrum.network import Network
        
       -# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server
       -def get_signing_xpub():
       -    if constants.net.TESTNET:
       -        return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY"
       +def get_signing_xpub(xtype):
       +    if not constants.net.TESTNET:
       +        xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL"
            else:
       -        return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL"
       +        xpub = "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY"
       +    if xtype not in ('standard', 'p2wsh'):
       +        raise NotImplementedError('xtype: {}'.format(xtype))
       +    if xtype == 'standard':
       +        return xpub
       +    _, depth, fingerprint, child_number, c, cK = bip32.deserialize_xpub(xpub)
       +    xpub = bip32.serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
       +    return xpub
        
        def get_billing_xpub():
            if constants.net.TESTNET:
       t@@ -60,7 +68,6 @@ def get_billing_xpub():
            else:
                return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU"
        
       -SEED_PREFIX = version.SEED_PREFIX_2FA
        
        DISCLAIMER = [
            _("Two-factor authentication is a service provided by TrustedCoin.  "
       t@@ -239,12 +246,18 @@ class Wallet_2fa(Multisig_Wallet):
                self._load_billing_addresses()
        
            def _load_billing_addresses(self):
       -        billing_addresses = self.storage.get('trustedcoin_billing_addresses', {})
       -        self._billing_addresses = {}  # index -> addr
       -        # convert keys from str to int
       -        for index, addr in list(billing_addresses.items()):
       -            self._billing_addresses[int(index)] = addr
       -        self._billing_addresses_set = set(self._billing_addresses.values())  # set of addrs
       +        billing_addresses = {
       +            'legacy': self.storage.get('trustedcoin_billing_addresses', {}),
       +            'segwit': self.storage.get('trustedcoin_billing_addresses_segwit', {})
       +        }
       +        self._billing_addresses = {}  # type: Dict[str, Dict[int, str]]  # addr_type -> index -> addr
       +        self._billing_addresses_set = set()  # set of addrs
       +        for addr_type, d in list(billing_addresses.items()):
       +            self._billing_addresses[addr_type] = {}
       +            # convert keys from str to int
       +            for index, addr in d.items():
       +                self._billing_addresses[addr_type][int(index)] = addr
       +                self._billing_addresses_set.add(addr)
        
            def can_sign_without_server(self):
                return not self.keystores['x2/'].is_watching_only()
       t@@ -284,7 +297,7 @@ class Wallet_2fa(Multisig_Wallet):
                    self, coins, o, config, fixed_fee, change_addr)
                fee = self.extra_fee(config) if not is_sweep else 0
                if fee:
       -            address = self.billing_info['billing_address']
       +            address = self.billing_info['billing_address_segwit']
                    fee_output = TxOutput(TYPE_ADDRESS, address, fee)
                    try:
                        tx = mk_tx(outputs + [fee_output])
       t@@ -305,7 +318,7 @@ class Wallet_2fa(Multisig_Wallet):
                    return
                otp = int(otp)
                long_user_id, short_id = self.get_user_id()
       -        raw_tx = tx.serialize_to_network()
       +        raw_tx = tx.serialize()
                r = server.sign(short_id, raw_tx, otp)
                if r:
                    raw_tx = r.get('transaction')
       t@@ -315,8 +328,9 @@ class Wallet_2fa(Multisig_Wallet):
                self.billing_info = None
                self.plugin.start_request_thread(self)
        
       -    def add_new_billing_address(self, billing_index: int, address: str):
       -        saved_addr = self._billing_addresses.get(billing_index)
       +    def add_new_billing_address(self, billing_index: int, address: str, addr_type: str):
       +        billing_addresses_of_this_type = self._billing_addresses[addr_type]
       +        saved_addr = billing_addresses_of_this_type.get(billing_index)
                if saved_addr is not None:
                    if saved_addr == address:
                        return  # already saved this address
       t@@ -325,16 +339,18 @@ class Wallet_2fa(Multisig_Wallet):
                                        'for index {}, already saved {}, now got {}'
                                        .format(billing_index, saved_addr, address))
                # do we have all prior indices? (are we synced?)
       -        largest_index_we_have = max(self._billing_addresses) if self._billing_addresses else -1
       +        largest_index_we_have = max(billing_addresses_of_this_type) if billing_addresses_of_this_type else -1
                if largest_index_we_have + 1 < billing_index:  # need to sync
                    for i in range(largest_index_we_have + 1, billing_index):
       -                addr = make_billing_address(self, i)
       -                self._billing_addresses[i] = addr
       +                addr = make_billing_address(self, i, addr_type=addr_type)
       +                billing_addresses_of_this_type[i] = addr
                        self._billing_addresses_set.add(addr)
                # save this address; and persist to disk
       -        self._billing_addresses[billing_index] = address
       +        billing_addresses_of_this_type[billing_index] = address
                self._billing_addresses_set.add(address)
       -        self.storage.put('trustedcoin_billing_addresses', self._billing_addresses)
       +        self._billing_addresses[addr_type] = billing_addresses_of_this_type
       +        self.storage.put('trustedcoin_billing_addresses', self._billing_addresses['legacy'])
       +        self.storage.put('trustedcoin_billing_addresses_segwit', self._billing_addresses['segwit'])
                # FIXME this often runs in a daemon thread, where storage.write will fail
                self.storage.write()
        
       t@@ -358,12 +374,17 @@ def make_xpub(xpub, s):
            cK2, c2 = bip32._CKD_pub(cK, c, s)
            return serialize_xpub(version, c2, cK2)
        
       -def make_billing_address(wallet, num):
       +def make_billing_address(wallet, num, addr_type):
            long_id, short_id = wallet.get_user_id()
            xpub = make_xpub(get_billing_xpub(), long_id)
            version, _, _, _, c, cK = deserialize_xpub(xpub)
            cK, c = CKD_pub(cK, c, num)
       -    return public_key_to_p2pkh(cK)
       +    if addr_type == 'legacy':
       +        return bitcoin.public_key_to_p2pkh(cK)
       +    elif addr_type == 'segwit':
       +        return bitcoin.public_key_to_p2wpkh(cK)
       +    else:
       +        raise ValueError(f'unexpected billing type: {addr_type}')
        
        
        class TrustedCoinPlugin(BasePlugin):
       t@@ -377,7 +398,8 @@ class TrustedCoinPlugin(BasePlugin):
        
            @staticmethod
            def is_valid_seed(seed):
       -        return is_new_seed(seed, SEED_PREFIX)
       +        t = seed_type(seed)
       +        return is_any_2fa_seed_type(t)
        
            def is_available(self):
                return True
       t@@ -420,7 +442,7 @@ class TrustedCoinPlugin(BasePlugin):
                return f
        
            @finish_requesting
       -    def request_billing_info(self, wallet):
       +    def request_billing_info(self, wallet: 'Wallet_2fa'):
                if wallet.can_sign_without_server():
                    return
                self.print_error("request billing info")
       t@@ -430,11 +452,16 @@ class TrustedCoinPlugin(BasePlugin):
                    self.print_error('cannot connect to TrustedCoin server: {}'.format(repr(e)))
                    return
                billing_index = billing_info['billing_index']
       -        billing_address = make_billing_address(wallet, billing_index)
       -        if billing_address != billing_info['billing_address']:
       -            raise Exception('unexpected trustedcoin billing address: expected {}, received {}'
       -                            .format(billing_address, billing_info['billing_address']))
       -        wallet.add_new_billing_address(billing_index, billing_address)
       +        # add segwit billing address; this will be used for actual billing
       +        billing_address = make_billing_address(wallet, billing_index, addr_type='segwit')
       +        if billing_address != billing_info['billing_address_segwit']:
       +            raise Exception(f'unexpected trustedcoin billing address: '
       +                            f'calculated {billing_address}, received {billing_info["billing_address_segwit"]}')
       +        wallet.add_new_billing_address(billing_index, billing_address, addr_type='segwit')
       +        # also add legacy billing address; only used for detecting past payments in GUI
       +        billing_address = make_billing_address(wallet, billing_index, addr_type='legacy')
       +        wallet.add_new_billing_address(billing_index, billing_address, addr_type='legacy')
       +
                wallet.billing_info = billing_info
                wallet.price_per_tx = dict(billing_info['price_per_tx'])
                wallet.price_per_tx.pop(1, None)
       t@@ -449,8 +476,10 @@ class TrustedCoinPlugin(BasePlugin):
                    t.start()
                    return t
        
       -    def make_seed(self):
       -        return Mnemonic('english').make_seed(seed_type='2fa', num_bits=128)
       +    def make_seed(self, seed_type):
       +        if not is_any_2fa_seed_type(seed_type):
       +            raise BaseException('unexpected seed type: {}'.format(seed_type))
       +        return Mnemonic('english').make_seed(seed_type=seed_type, num_bits=128)
        
            @hook
            def do_clear(self, window):
       t@@ -465,25 +494,40 @@ class TrustedCoinPlugin(BasePlugin):
                title = _('Create or restore')
                message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
                choices = [
       -            ('create_seed', _('Create a new seed')),
       +            ('choose_seed_type', _('Create a new seed')),
                    ('restore_wallet', _('I already have a seed')),
                ]
                wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run)
        
       -    def create_seed(self, wizard):
       -        seed = self.make_seed()
       +    def choose_seed_type(self, wizard):
       +        choices = [
       +            ('create_2fa_segwit_seed', _('Segwit 2FA')),
       +            ('create_2fa_seed', _('Legacy 2FA')),
       +        ]
       +        wizard.choose_seed_type(choices=choices)
       +
       +    def create_2fa_seed(self, wizard): self.create_seed(wizard, '2fa')
       +    def create_2fa_segwit_seed(self, wizard): self.create_seed(wizard, '2fa_segwit')
       +
       +    def create_seed(self, wizard, seed_type):
       +        seed = self.make_seed(seed_type)
                f = lambda x: wizard.request_passphrase(seed, x)
                wizard.show_seed_dialog(run_next=f, seed_text=seed)
        
            @classmethod
       -    def get_xkeys(self, seed, passphrase, derivation):
       +    def get_xkeys(self, seed, t, passphrase, derivation):
       +        assert is_any_2fa_seed_type(t)
       +        xtype = 'standard' if t == '2fa' else 'p2wsh'
                bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase)
       -        xprv, xpub = bip32_root(bip32_seed, 'standard')
       +        xprv, xpub = bip32_root(bip32_seed, xtype)
                xprv, xpub = bip32_private_derivation(xprv, "m/", derivation)
                return xprv, xpub
        
            @classmethod
            def xkeys_from_seed(self, seed, passphrase):
       +        t = seed_type(seed)
       +        if not is_any_2fa_seed_type(t):
       +            raise BaseException('unexpected seed type: {}'.format(t))
                words = seed.split()
                n = len(words)
                # old version use long seed phrases
       t@@ -493,11 +537,11 @@ class TrustedCoinPlugin(BasePlugin):
                    # the probability of it being < 20 words is about 2^(-(256+12-19*11)) = 2^(-59)
                    if passphrase != '':
                        raise Exception('old 2fa seed cannot have passphrase')
       -            xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/")
       -            xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/")
       -        elif n==12:
       -            xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/")
       -            xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/")
       +            xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), t, '', "m/")
       +            xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), t, '', "m/")
       +        elif not t == '2fa' or n == 12:
       +            xprv1, xpub1 = self.get_xkeys(seed, t, passphrase, "m/0'/")
       +            xprv2, xpub2 = self.get_xkeys(seed, t, passphrase, "m/1'/")
                else:
                    raise Exception('unrecognized seed length: {} words'.format(n))
                return xprv1, xpub1, xprv2, xpub2
       t@@ -561,7 +605,8 @@ class TrustedCoinPlugin(BasePlugin):
                storage.put('x1/', k1.dump())
                storage.put('x2/', k2.dump())
                long_user_id, short_id = get_user_id(storage)
       -        xpub3 = make_xpub(get_signing_xpub(), long_user_id)
       +        xtype = xpub_type(xpub1)
       +        xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id)
                k3 = keystore.from_xpub(xpub3)
                storage.put('x3/', k3.dump())
        
       t@@ -578,7 +623,8 @@ class TrustedCoinPlugin(BasePlugin):
                xpub2 = wizard.storage.get('x2/')['xpub']
                # Generate third key deterministically.
                long_user_id, short_id = get_user_id(wizard.storage)
       -        xpub3 = make_xpub(get_signing_xpub(), long_user_id)
       +        xtype = xpub_type(xpub1)
       +        xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id)
                # secret must be sent by the server
                try:
                    r = server.create(xpub1, xpub2, email)
   DIR diff --git a/electrum/storage.py b/electrum/storage.py
       t@@ -572,9 +572,7 @@ class WalletStorage(JsonDB):
                # delete verified_tx3 as its structure changed
                if not self._is_upgrade_method_needed(17, 17):
                    return
       -
                self.put('verified_tx3', None)
       -
                self.put('seed_version', 18)
        
            def convert_imported(self):
   DIR diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py
       t@@ -4,7 +4,7 @@ import tempfile
        from typing import Sequence
        import asyncio
        
       -from electrum import storage, bitcoin, keystore
       +from electrum import storage, bitcoin, keystore, bip32
        from electrum import Transaction
        from electrum import SimpleConfig
        from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
       t@@ -178,7 +178,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(SequentialTestCase):
                long_user_id, short_id = trustedcoin.get_user_id(
                    {'x1/': {'xpub': xpub1},
                     'x2/': {'xpub': xpub2}})
       -        xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(), long_user_id)
       +        xtype = bip32.xpub_type(xpub1)
       +        xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)
                ks3 = keystore.from_xpub(xpub3)
                WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3)
                self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))
   DIR diff --git a/electrum/version.py b/electrum/version.py
       t@@ -4,9 +4,10 @@ APK_VERSION = '3.3.0.0'      # read by buildozer.spec
        PROTOCOL_VERSION = '1.4'     # protocol version requested
        
        # The hash of the mnemonic seed must begin with this
       -SEED_PREFIX      = '01'      # Standard wallet
       -SEED_PREFIX_2FA  = '101'     # Two-factor authentication
       -SEED_PREFIX_SW   = '100'     # Segwit wallet
       +SEED_PREFIX        = '01'      # Standard wallet
       +SEED_PREFIX_SW     = '100'     # Segwit wallet
       +SEED_PREFIX_2FA    = '101'     # Two-factor authentication
       +SEED_PREFIX_2FA_SW = '102'     # Two-factor auth, using segwit
        
        
        def seed_prefix(seed_type):
       t@@ -16,3 +17,5 @@ def seed_prefix(seed_type):
                return SEED_PREFIX_SW
            elif seed_type == '2fa':
                return SEED_PREFIX_2FA
       +    elif seed_type == '2fa_segwit':
       +        return SEED_PREFIX_2FA_SW