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