tgeneric m of n multisig - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 56b3c9833267e888c205cd3f67e95c59e150ff68 DIR parent 547886d6f151c0f5175fcb2d1150292e9c7b484b HTML Author: ThomasV <thomasv@gitorious> Date: Fri, 26 Jun 2015 14:29:26 +0200 generic m of n multisig Diffstat: M gui/qt/installwizard.py | 114 +++++++++++++++++++++++++------ M gui/qt/main_window.py | 1 + M lib/__init__.py | 2 +- M lib/account.py | 36 ++++++++++--------------------- M lib/transaction.py | 62 +++++++++++-------------------- M lib/wallet.py | 79 +++++++++++++------------------ M plugins/trustedcoin.py | 14 +++++++++----- 7 files changed, 169 insertions(+), 139 deletions(-) --- DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py t@@ -1,10 +1,14 @@ +import re +import sys +import threading + from PyQt4.QtGui import * from PyQt4.QtCore import * import PyQt4.QtCore as QtCore import electrum from electrum.i18n import _ -from electrum import Wallet, Wallet_2of2, Wallet_2of3 +from electrum import Wallet from electrum import bitcoin from electrum import util t@@ -13,8 +17,6 @@ from network_dialog import NetworkDialog from util import * from amountedit import AmountEdit -import sys -import threading from electrum.plugins import always_hook, run_hook from electrum.mnemonic import prepare_seed t@@ -26,6 +28,42 @@ MSG_ENTER_SEED_OR_MPK = _("Please enter a wallet seed, BIP32 private key, or mas MSG_VERIFY_SEED = _("Your seed is important!") + "\n" + _("To make sure that you have properly saved your seed, please retype it here.") +class CosignWidget(QWidget): + size = 120 + + def __init__(self, m, n): + QWidget.__init__(self) + self.R = QRect(0, 0, self.size, self.size) + self.setGeometry(self.R) + self.m = m + self.n = n + + def set_n(self, n): + self.n = n + self.update() + + def set_m(self, m): + self.m = m + self.update() + + def paintEvent(self, event): + import math + bgcolor = self.palette().color(QPalette.Background) + pen = QPen(bgcolor, 7, QtCore.Qt.SolidLine) + qp = QPainter() + qp.begin(self) + qp.setPen(pen) + qp.setRenderHint(QPainter.Antialiasing) + qp.setBrush(Qt.gray) + for i in range(self.n): + alpha = int(16* 360 * i/self.n) + alpha2 = int(16* 360 * 1/self.n) + qp.setBrush(Qt.green if i<self.m else Qt.gray) + qp.drawPie(self.R, alpha, alpha2) + qp.end() + + + class InstallWizard(QDialog): def __init__(self, config, network, storage, app): t@@ -36,7 +74,7 @@ class InstallWizard(QDialog): self.storage = storage self.setMinimumSize(575, 400) self.setMaximumSize(575, 400) - self.setWindowTitle('Electrum' + ' - ' + os.path.basename(self.storage.path)) + self.setWindowTitle('Electrum' + ' - ' + _('Install Wizard')) self.connect(self, QtCore.SIGNAL('accept'), self.accept) self.stack = QStackedLayout() self.setLayout(self.stack) t@@ -283,6 +321,48 @@ class InstallWizard(QDialog): return wallet_type + def multisig_choice(self): + + vbox = QVBoxLayout() + self.set_layout(vbox) + vbox.addWidget(QLabel(_("Multi Signature Wallet"))) + + cw = CosignWidget(2, 2) + vbox.addWidget(cw, 1) + vbox.addWidget(QLabel(_("Please choose the number of signatures needed to unlock funds in your wallet") + ':')) + + m_edit = QSpinBox() + n_edit = QSpinBox() + m_edit.setValue(2) + n_edit.setValue(2) + n_edit.setMinimum(2) + n_edit.setMaximum(15) + m_edit.setMinimum(1) + m_edit.setMaximum(2) + n_edit.valueChanged.connect(m_edit.setMaximum) + + n_edit.valueChanged.connect(cw.set_n) + m_edit.valueChanged.connect(cw.set_m) + + hbox = QHBoxLayout() + hbox.addWidget(QLabel(_('Require'))) + hbox.addWidget(m_edit) + hbox.addWidget(QLabel(_('of'))) + hbox.addWidget(n_edit) + hbox.addWidget(QLabel(_('signatures'))) + hbox.addStretch(1) + + vbox.addLayout(hbox) + vbox.addStretch(1) + vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _('Next')))) + if not self.exec_(): + return + m = int(m_edit.value()) + n = int(n_edit.value()) + wallet_type = '%dof%d'%(m,n) + return wallet_type + + def question(self, msg, yes_label=_('OK'), no_label=_('Cancel'), icon=None): vbox = QVBoxLayout() self.set_layout(vbox) t@@ -319,7 +399,7 @@ class InstallWizard(QDialog): if action in ['create', 'restore']: if wallet_type == 'multisig': - wallet_type = self.choice(_("Multi Signature Wallet"), 'Select wallet type', [('2of2', _("2 of 2")),('2of3',_("2 of 3"))]) + wallet_type = self.multisig_choice() if not wallet_type: return elif wallet_type == 'hardware': t@@ -363,22 +443,14 @@ class InstallWizard(QDialog): wallet.add_seed(seed, password) wallet.create_master_keys(password) - elif action == 'add_cosigner': - xpub1 = wallet.master_public_keys.get("x1/") - r = self.multi_mpk_dialog(xpub1, 1) - if not r: - return - xpub2 = r[0] - wallet.add_master_public_key("x2/", xpub2) - - elif action == 'add_two_cosigners': + elif action == 'add_cosigners': + n = int(re.match('(\d+)of(\d+)', wallet.wallet_type).group(2)) xpub1 = wallet.master_public_keys.get("x1/") - r = self.multi_mpk_dialog(xpub1, 2) + r = self.multi_mpk_dialog(xpub1, n - 1) if not r: return - xpub2, xpub3 = r - wallet.add_master_public_key("x2/", xpub2) - wallet.add_master_public_key("x3/", xpub3) + for i, xpub in enumerate(r): + wallet.add_master_public_key("x%d/"%(i+2), xpub) elif action == 'create_accounts': wallet.create_main_account(password) t@@ -443,8 +515,10 @@ class InstallWizard(QDialog): else: raise BaseException('unknown wallet type') - elif t in ['2of2', '2of3']: - key_list = self.multi_seed_dialog(1 if t == '2of2' else 2) + elif re.match('(\d+)of(\d+)', t): + n = int(re.match('(\d+)of(\d+)', t).group(2)) + print t, n + key_list = self.multi_seed_dialog(n - 1) if not key_list: return password = self.password_dialog() if any(map(lambda x: Wallet.is_seed(x) or Wallet.is_xprv(x), key_list)) else None DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py t@@ -276,6 +276,7 @@ class ElectrumWindow(QMainWindow): try: wallet = Wallet(storage) except BaseException as e: + traceback.print_exc(file=sys.stdout) QMessageBox.warning(None, _('Warning'), str(e), _('OK')) return action = wallet.get_action() DIR diff --git a/lib/__init__.py b/lib/__init__.py t@@ -1,7 +1,7 @@ from version import ELECTRUM_VERSION from util import format_satoshis, print_msg, print_json, print_error, set_verbosity from wallet import WalletSynchronizer, WalletStorage -from wallet import Wallet, Wallet_2of2, Wallet_2of3, Imported_Wallet +from wallet import Wallet, Imported_Wallet from network import Network, DEFAULT_SERVERS, DEFAULT_PORTS, pick_random_server from interface import Interface from simple_config import SimpleConfig, get_config, set_config DIR diff --git a/lib/account.py b/lib/account.py t@@ -366,15 +366,17 @@ class BIP32_Account(Account): -class BIP32_Account_2of2(BIP32_Account): +class Multisig_Account(BIP32_Account): def __init__(self, v): - BIP32_Account.__init__(self, v) - self.xpub2 = v['xpub2'] + self.m = v.get('m', 2) + Account.__init__(self, v) + self.xpub_list = v['xpubs'] def dump(self): - d = BIP32_Account.dump(self) - d['xpub2'] = self.xpub2 + d = Account.dump(self) + d['xpubs'] = self.xpub_list + d['m'] = self.m return d def get_pubkeys(self, for_change, n): t@@ -385,10 +387,10 @@ class BIP32_Account_2of2(BIP32_Account): def redeem_script(self, for_change, n): pubkeys = self.get_pubkeys(for_change, n) - return Transaction.multisig_script(sorted(pubkeys), 2) + return Transaction.multisig_script(sorted(pubkeys), self.m) def pubkeys_to_address(self, pubkeys): - redeem_script = Transaction.multisig_script(sorted(pubkeys), 2) + redeem_script = Transaction.multisig_script(sorted(pubkeys), self.m) address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5) return address t@@ -396,25 +398,9 @@ class BIP32_Account_2of2(BIP32_Account): return self.pubkeys_to_address(self.get_pubkeys(for_change, n)) def get_master_pubkeys(self): - return [self.xpub, self.xpub2] + return self.xpub_list def get_type(self): - return _('Multisig 2 of 2') - + return _('Multisig %d of %d'%(self.m, len(self.xpub_list))) -class BIP32_Account_2of3(BIP32_Account_2of2): - def __init__(self, v): - BIP32_Account_2of2.__init__(self, v) - self.xpub3 = v['xpub3'] - - def dump(self): - d = BIP32_Account_2of2.dump(self) - d['xpub3'] = self.xpub3 - return d - - def get_master_pubkeys(self): - return [self.xpub, self.xpub2, self.xpub3] - - def get_type(self): - return _('Multisig 2 of 3') DIR diff --git a/lib/transaction.py b/lib/transaction.py t@@ -368,32 +368,29 @@ def parse_scriptSig(d, bytes): d['address'] = address return - # p2sh transaction, 2 of n + # p2sh transaction, m of n match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1) - if not match_decoded(decoded, match): print_error("cannot find address in input script", bytes.encode('hex')) return - x_sig = [x[1].encode('hex') for x in decoded[1:-1]] - d['signatures'] = parse_sig(x_sig) - d['num_sig'] = 2 - dec2 = [ x for x in script_GetOp(decoded[-1][1]) ] - match_2of2 = [ opcodes.OP_2, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_2, opcodes.OP_CHECKMULTISIG ] - match_2of3 = [ opcodes.OP_2, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_3, opcodes.OP_CHECKMULTISIG ] - if match_decoded(dec2, match_2of2): - x_pubkeys = [ dec2[1][1].encode('hex'), dec2[2][1].encode('hex') ] - elif match_decoded(dec2, match_2of3): - x_pubkeys = [ dec2[1][1].encode('hex'), dec2[2][1].encode('hex'), dec2[3][1].encode('hex') ] - else: + m = dec2[0][0] - opcodes.OP_1 + 1 + n = dec2[-2][0] - opcodes.OP_1 + 1 + op_m = opcodes.OP_1 + m - 1 + op_n = opcodes.OP_1 + n - 1 + match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] + if not match_decoded(dec2, match_multisig): print_error("cannot find address in input script", bytes.encode('hex')) return - - d['x_pubkeys'] = x_pubkeys + x_pubkeys = map(lambda x: x[1].encode('hex'), dec2[1:-2]) pubkeys = [parse_xpub(x)[0] for x in x_pubkeys] # xpub, addr = parse_xpub() + redeemScript = Transaction.multisig_script(pubkeys, m) + # write result in d + d['num_sig'] = m + d['signatures'] = parse_sig(x_sig) + d['x_pubkeys'] = x_pubkeys d['pubkeys'] = pubkeys - redeemScript = Transaction.multisig_script(pubkeys,2) d['redeemScript'] = redeemScript d['address'] = hash_160_to_bc_address(hash_160(redeemScript.decode('hex')), 5) t@@ -535,31 +532,14 @@ class Transaction: return self @classmethod - def multisig_script(klass, public_keys, num=None): + def multisig_script(klass, public_keys, m): n = len(public_keys) - if num is None: num = n - - assert num <= n and n in [2,3] , 'Only "2 of 2", and "2 of 3" transactions are supported' - - if num==2: - s = '52' - elif num == 3: - s = '53' - else: - raise - - for k in public_keys: - s += op_push(len(k)/2) + k - if n==2: - s += '52' - elif n==3: - s += '53' - else: - raise - s += 'ae' - - return s - + assert n <= 15 + assert m <= n + op_m = format(opcodes.OP_1 + m - 1, 'x') + op_n = format(opcodes.OP_1 + n - 1, 'x') + keylist = [op_push(len(k)/2) + k for k in public_keys] + return op_m + ''.join(keylist) + op_n + 'ae' @classmethod def pay_script(self, output_type, addr): t@@ -617,7 +597,7 @@ class Transaction: script += push_script(x_pubkey) else: script = '00' + script # put op_0 in front of script - redeem_script = self.multisig_script(pubkeys,2) + redeem_script = self.multisig_script(pubkeys, num_sig) script += push_script(redeem_script) elif for_sig==i: DIR diff --git a/lib/wallet.py b/lib/wallet.py t@@ -240,7 +240,6 @@ class Abstract_Wallet(object): def load_accounts(self): self.accounts = {} - d = self.storage.get('accounts', {}) for k, v in d.items(): if self.wallet_type == 'old' and k in [0, '0']: t@@ -248,10 +247,6 @@ class Abstract_Wallet(object): self.accounts['0'] = OldAccount(v) elif v.get('imported'): self.accounts[k] = ImportedAccount(v) - elif v.get('xpub3'): - self.accounts[k] = BIP32_Account_2of3(v) - elif v.get('xpub2'): - self.accounts[k] = BIP32_Account_2of2(v) elif v.get('xpub'): self.accounts[k] = BIP32_Account(v) elif v.get('pending'): t@@ -942,7 +937,7 @@ class Abstract_Wallet(object): if redeemScript: txin['redeemScript'] = redeemScript - txin['num_sig'] = 2 + txin['num_sig'] = account.m else: txin['redeemPubkey'] = account.get_pubkey(*sequence) txin['num_sig'] = 1 t@@ -1732,55 +1727,45 @@ class NewWallet(BIP32_Wallet, Mnemonic): self.add_account('0', account) -class Wallet_2of2(BIP32_Wallet, Mnemonic): - # Wallet with multisig addresses. +class Multisig_Wallet(BIP32_Wallet, Mnemonic): + # generic m of n root_name = "x1/" root_derivation = "m/" - wallet_type = '2of2' + + def __init__(self, storage): + BIP32_Wallet.__init__(self, storage) + self.wallet_type = storage.get('wallet_type') + m = re.match('(\d+)of(\d+)', self.wallet_type) + self.m = int(m.group(1)) + self.n = int(m.group(2)) + + def load_accounts(self): + self.accounts = {} + d = self.storage.get('accounts', {}) + v = d.get('0') + if v: + if v.get('xpub3'): + v['xpubs'] = [v['xpub'], v['xpub2'], v['xpub3']] + elif v.get('xpub2'): + v['xpubs'] = [v['xpub'], v['xpub2']] + self.accounts = {'0': Multisig_Account(v)} def create_main_account(self, password): - xpub1 = self.master_public_keys.get("x1/") - xpub2 = self.master_public_keys.get("x2/") - account = BIP32_Account_2of2({'xpub':xpub1, 'xpub2':xpub2}) + account = Multisig_Account({'xpubs': self.master_public_keys.values(), 'm': self.m}) self.add_account('0', account) def get_master_public_keys(self): return self.master_public_keys def get_action(self): - xpub1 = self.master_public_keys.get("x1/") - xpub2 = self.master_public_keys.get("x2/") - if xpub1 is None: - return 'create_seed' - if xpub2 is None: - return 'add_cosigner' + for i in range(self.n): + if self.master_public_keys.get("x%d/"%(i+1)) is None: + return 'create_seed' if i == 0 else 'add_cosigners' if not self.accounts: return 'create_accounts' -class Wallet_2of3(Wallet_2of2): - # multisig 2 of 3 - wallet_type = '2of3' - - def create_main_account(self, password): - xpub1 = self.master_public_keys.get("x1/") - xpub2 = self.master_public_keys.get("x2/") - xpub3 = self.master_public_keys.get("x3/") - account = BIP32_Account_2of3({'xpub':xpub1, 'xpub2':xpub2, 'xpub3':xpub3}) - self.add_account('0', account) - - def get_action(self): - xpub1 = self.master_public_keys.get("x1/") - xpub2 = self.master_public_keys.get("x2/") - xpub3 = self.master_public_keys.get("x3/") - if xpub1 is None: - return 'create_seed' - if xpub2 is None or xpub3 is None: - return 'add_two_cosigners' - if not self.accounts: - return 'create_accounts' - class OldWallet(Deterministic_Wallet): wallet_type = 'old' t@@ -1859,8 +1844,8 @@ wallet_types = [ ('standard', 'xpub', ("BIP32 Import"), BIP32_Simple_Wallet), ('standard', 'standard', ("Standard wallet"), NewWallet), ('standard', 'imported', ("Imported wallet"), Imported_Wallet), - ('multisig', '2of2', ("Multisig wallet (2 of 2)"), Wallet_2of2), - ('multisig', '2of3', ("Multisig wallet (2 of 3)"), Wallet_2of3) + ('multisig', '2of2', ("Multisig wallet (2 of 2)"), Multisig_Wallet), + ('multisig', '2of3', ("Multisig wallet (2 of 3)"), Multisig_Wallet) ] # former WalletFactory t@@ -1898,7 +1883,10 @@ class Wallet(object): WalletClass = c break else: - raise BaseException('unknown wallet type', wallet_type) + if re.match('(\d+)of(\d+)', wallet_type): + WalletClass = Multisig_Wallet + else: + raise BaseException('unknown wallet type', wallet_type) else: if seed_version == OLD_SEED_VERSION: WalletClass = OldWallet t@@ -2012,10 +2000,7 @@ class Wallet(object): @classmethod def from_multisig(klass, key_list, password, storage): - if len(key_list) == 2: - self = Wallet_2of2(storage) - elif len(key_list) == 3: - self = Wallet_2of3(storage) + self = Multisig_Wallet(storage) key_list = sorted(key_list, key = lambda x: klass.is_xpub(x)) for i, text in enumerate(key_list): assert klass.is_seed(text) or klass.is_xprv(text) or klass.is_xpub(text) DIR diff --git a/plugins/trustedcoin.py b/plugins/trustedcoin.py t@@ -34,7 +34,7 @@ from electrum import bitcoin from electrum.bitcoin import * from electrum.mnemonic import Mnemonic from electrum import version -from electrum.wallet import Wallet_2of3 +from electrum.wallet import Multisig_Wallet, BIP32_Wallet from electrum.i18n import _ from electrum.plugins import BasePlugin, run_hook, hook t@@ -170,9 +170,13 @@ class TrustedCoinCosignerClient(object): server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION) -class Wallet_2fa(Wallet_2of3): +class Wallet_2fa(Multisig_Wallet): - wallet_type = '2fa' + def __init__(self, storage): + BIP32_Wallet.__init__(self, storage) + self.wallet_type = '2fa' + self.m = 2 + self.n = 3 def get_action(self): xpub1 = self.master_public_keys.get("x1/") t@@ -191,13 +195,13 @@ class Wallet_2fa(Wallet_2of3): return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX) def estimated_fee(self, tx): - fee = Wallet_2of3.estimated_fee(self, tx) + fee = Multisig_Wallet.estimated_fee(self, tx) x = run_hook('extra_fee', tx) if x: fee += x return fee def get_tx_fee(self, tx): - fee = Wallet_2of3.get_tx_fee(self, tx) + fee = Multisig_Wallet.get_tx_fee(self, tx) x = run_hook('extra_fee', tx) if x: fee += x return fee