Browse Source

Merge pull request #4465 from SomberNight/purpose48_segwit_multisig_path2

wizard: extend derivation dialog to also let user select script type
3.2.x
ThomasV 7 years ago
committed by GitHub
parent
commit
357ff8e833
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      gui/qt/installwizard.py
  2. 58
      lib/base_wizard.py
  3. 17
      lib/bitcoin.py
  4. 28
      lib/keystore.py
  5. 18
      lib/tests/test_bitcoin.py

32
gui/qt/installwizard.py

@ -519,6 +519,34 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
self.exec_layout(vbox, '') self.exec_layout(vbox, '')
return clayout.selected_index() 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 @wizard_dialog
def line_dialog(self, run_next, title, message, default, test, warning='', def line_dialog(self, run_next, title, message, default, test, warning='',
presets=()): presets=()):
@ -535,9 +563,9 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
for preset in presets: for preset in presets:
button = QPushButton(preset[0]) button = QPushButton(preset[0])
button.clicked.connect(lambda __, text=preset[1]: line.setText(text)) button.clicked.connect(lambda __, text=preset[1]: line.setText(text))
button.setMaximumWidth(150) button.setMinimumWidth(150)
hbox = QHBoxLayout() hbox = QHBoxLayout()
hbox.addWidget(button, Qt.AlignCenter) hbox.addWidget(button, alignment=Qt.AlignCenter)
vbox.addLayout(hbox) vbox.addLayout(hbox)
self.exec_layout(vbox, title, next_enabled=test(default)) self.exec_layout(vbox, title, next_enabled=test(default))

58
lib/base_wizard.py

@ -30,7 +30,7 @@ from functools import partial
from . import bitcoin from . import bitcoin
from . import keystore 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 .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 .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
from .i18n import _ from .i18n import _
@ -279,13 +279,9 @@ class BaseWizard(object):
self.choose_hw_device(purpose) self.choose_hw_device(purpose)
return return
if purpose == HWD_SETUP_NEW_WALLET: if purpose == HWD_SETUP_NEW_WALLET:
if self.wallet_type=='multisig': def f(derivation, script_type):
# There is no general standard for HD multisig. self.run('on_hw_derivation', name, device_info, derivation, script_type)
# This is partially compatible with BIP45; assumes index=0 self.derivation_and_script_type_dialog(f)
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)
elif purpose == HWD_SETUP_DECRYPT_WALLET: elif purpose == HWD_SETUP_DECRYPT_WALLET:
derivation = get_derivation_used_for_hw_device_encryption() derivation = get_derivation_used_for_hw_device_encryption()
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self) xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self)
@ -302,30 +298,39 @@ class BaseWizard(object):
else: else:
raise Exception('unknown purpose: %s' % purpose) raise Exception('unknown purpose: %s' % purpose)
def derivation_dialog(self, f): def derivation_and_script_type_dialog(self, f):
default = bip44_derivation(0, bip43_purpose=44) message1 = _('Choose the type of addresses in your wallet.')
message = '\n'.join([ message2 = '\n'.join([
_('Enter your wallet derivation here.'), _('You can override the suggested derivation path.'),
_('If you are not sure what this is, leave this field unchanged.') _('If you are not sure what this is, leave this field unchanged.')
]) ])
presets = ( if self.wallet_type == 'multisig':
('legacy BIP44', bip44_derivation(0, bip43_purpose=44)), # There is no general standard for HD multisig.
('p2sh-segwit BIP49', bip44_derivation(0, bip43_purpose=49)), # For legacy, this is partially compatible with BIP45; assumes index=0
('native-segwit BIP84', bip44_derivation(0, bip43_purpose=84)), # 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: while True:
try: try:
self.line_dialog(run_next=f, title=_('Derivation'), message=message, self.choice_and_line_dialog(
default=default, test=bitcoin.is_bip32_derivation, run_next=f, title=_('Script type and Derivation path'), message1=message1,
presets=presets) message2=message2, choices=choices, test_text=bitcoin.is_bip32_derivation)
return return
except ScriptTypeNotSupported as e: except ScriptTypeNotSupported as e:
self.show_error(e) self.show_error(e)
# let the user choose again # 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 from .keystore import hardware_keystore
xtype = keystore.xtype_from_derivation(derivation)
try: try:
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
except ScriptTypeNotSupported: except ScriptTypeNotSupported:
@ -379,15 +384,16 @@ class BaseWizard(object):
raise Exception('Unknown seed type', self.seed_type) raise Exception('Unknown seed type', self.seed_type)
def on_restore_bip39(self, seed, passphrase): def on_restore_bip39(self, seed, passphrase):
f = lambda x: self.run('on_bip43', seed, passphrase, str(x)) def f(derivation, script_type):
self.derivation_dialog(f) self.run('on_bip43', seed, passphrase, derivation, script_type)
self.derivation_and_script_type_dialog(f)
def create_keystore(self, seed, passphrase): def create_keystore(self, seed, passphrase):
k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig') k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig')
self.on_keystore(k) self.on_keystore(k)
def on_bip43(self, seed, passphrase, derivation): def on_bip43(self, seed, passphrase, derivation, script_type):
k = keystore.from_bip39_seed(seed, passphrase, derivation) k = keystore.from_bip39_seed(seed, passphrase, derivation, xtype=script_type)
self.on_keystore(k) self.on_keystore(k)
def on_keystore(self, k): def on_keystore(self, k):

17
lib/bitcoin.py

@ -398,7 +398,7 @@ def DecodeBase58Check(psz):
# backwards compat # backwards compat
# extended WIF for segwit (used in 3.0.x; but still used internally) # 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 # the keys in this dict should be a superset of what Imported Wallets can import
SCRIPT_TYPES = { WIF_SCRIPT_TYPES = {
'p2pkh':0, 'p2pkh':0,
'p2wpkh':1, 'p2wpkh':1,
'p2wpkh-p2sh':2, 'p2wpkh-p2sh':2,
@ -406,6 +406,14 @@ SCRIPT_TYPES = {
'p2wsh':6, 'p2wsh':6,
'p2wsh-p2sh':7 '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, 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 # we only export secrets inside curve range
secret = ecc.ECPrivkey.normalize_secret_bytes(secret) secret = ecc.ECPrivkey.normalize_secret_bytes(secret)
if internal_use: 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: else:
prefix = bytes([constants.net.WIF_PREFIX]) prefix = bytes([constants.net.WIF_PREFIX])
suffix = b'\01' if compressed else b'' suffix = b'\01' if compressed else b''
@ -432,7 +440,7 @@ def deserialize_privkey(key: str) -> (str, bytes, bool):
txin_type = None txin_type = None
if ':' in key: if ':' in key:
txin_type, key = key.split(sep=':', maxsplit=1) 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)) raise BitcoinException('unknown script type: {}'.format(txin_type))
try: try:
vch = DecodeBase58Check(key) vch = DecodeBase58Check(key)
@ -444,9 +452,8 @@ def deserialize_privkey(key: str) -> (str, bytes, bool):
if txin_type is None: if txin_type is None:
# keys exported in version 3.0.x encoded script type in first byte # keys exported in version 3.0.x encoded script type in first byte
prefix_value = vch[0] - constants.net.WIF_PREFIX prefix_value = vch[0] - constants.net.WIF_PREFIX
inverse_script_types = inv_dict(SCRIPT_TYPES)
try: try:
txin_type = inverse_script_types[prefix_value] txin_type = WIF_SCRIPT_TYPES_INV[prefix_value]
except KeyError: except KeyError:
raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0])) raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0]))
else: else:

28
lib/keystore.py

@ -600,15 +600,27 @@ def from_bip39_seed(seed, passphrase, derivation, xtype=None):
return k return k
def xtype_from_derivation(derivation): def xtype_from_derivation(derivation: str) -> str:
"""Returns the script type to be used for this derivation.""" """Returns the script type to be used for this derivation."""
if derivation.startswith("m/84'"): if derivation.startswith("m/84'"):
return 'p2wpkh' return 'p2wpkh'
elif derivation.startswith("m/49'"): elif derivation.startswith("m/49'"):
return 'p2wpkh-p2sh' return 'p2wpkh-p2sh'
else: elif derivation.startswith("m/44'"):
return 'standard'
elif derivation.startswith("m/45'"):
return 'standard' 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 # extended pubkeys
@ -719,6 +731,18 @@ def bip44_derivation(account_id, bip43_purpose=44):
coin = constants.net.BIP44_COIN_TYPE coin = constants.net.BIP44_COIN_TYPE
return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id)) 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): def from_seed(seed, passphrase, is_p2sh):
t = seed_type(seed) t = seed_type(seed)
if t == 'old': if t == 'old':

18
lib/tests/test_bitcoin.py

@ -19,6 +19,7 @@ from lib.transaction import opcodes
from lib.util import bfh, bh2u from lib.util import bfh, bh2u
from lib import constants from lib import constants
from lib.storage import WalletStorage from lib.storage import WalletStorage
from lib.keystore import xtype_from_derivation
from . import SequentialTestCase from . import SequentialTestCase
from . import TestCaseForTestnet from . import TestCaseForTestnet
@ -469,6 +470,23 @@ class Test_xprv_xpub(SequentialTestCase):
self.assertFalse(is_bip32_derivation("")) self.assertFalse(is_bip32_derivation(""))
self.assertFalse(is_bip32_derivation("m/q8462")) 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): def test_version_bytes(self):
xprv_headers_b58 = { xprv_headers_b58 = {
'standard': 'xprv', 'standard': 'xprv',

Loading…
Cancel
Save