Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multisignature Wallets #56

Merged
merged 15 commits into from
Apr 5, 2015
15 changes: 7 additions & 8 deletions gui/qt/installwizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ def restore_or_create(self):
self.wallet_types = [
('standard', _("Standard wallet")),
('twofactor', _("Wallet with two-factor authentication")),
# Multisig is disabled (TODO)
#('multisig', _("Multi-signature wallet")),
('multisig', _("Multi-signature wallet")),
('hardware', _("Hardware wallet")),
]

Expand Down Expand Up @@ -143,13 +142,13 @@ def enter_seed_dialog(self, msg, sid, func=None):

def multi_mpk_dialog(self, xpub_hot, n):
vbox = QVBoxLayout()
vbox0, seed_e0 = seed_dialog.enter_seed_box(MSG_SHOW_MPK, 'hot')
vbox0, seed_e0 = seed_dialog.enter_seed_box(MSG_SHOW_MPK, self, 'hot')
vbox.addLayout(vbox0)
seed_e0.setText(xpub_hot)
seed_e0.setReadOnly(True)
entries = []
for i in range(n):
vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_COLD_MPK, 'cold')
vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_COLD_MPK, self, 'cold')
vbox.addLayout(vbox2)
entries.append(seed_e2)
vbox.addStretch(1)
Expand All @@ -167,11 +166,11 @@ def multi_mpk_dialog(self, xpub_hot, n):

def multi_seed_dialog(self, n):
vbox = QVBoxLayout()
vbox1, seed_e1 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, 'hot')
vbox1, seed_e1 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, self, 'hot')
vbox.addLayout(vbox1)
entries = [seed_e1]
for i in range(n):
vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, 'cold')
vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, self, 'cold')
vbox.addLayout(vbox2)
entries.append(seed_e2)
vbox.addStretch(1)
Expand Down Expand Up @@ -368,7 +367,7 @@ def run(self, action):
wallet_type = '2fa'

if action == 'create':
self.storage.put('wallet_type', wallet_type, False)
self.storage.put_above_chain('wallet_type', wallet_type, False)

if action is None:
return
Expand Down Expand Up @@ -569,7 +568,7 @@ def restore(self, t):
wallet.create_main_account(password)

else:
self.storage.put('wallet_type', t)
self.storage.put_above_chain('wallet_type', t)
wallet = run_hook('installwizard_restore', self, self.storage)
if not wallet:
return
Expand Down
4 changes: 2 additions & 2 deletions gui/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -1138,12 +1138,12 @@ def sign_thread():
return tx

def sign_done(tx):
if label:
self.wallet.set_label(tx.hash(), label)
if not tx.is_complete() or self.config.get('show_before_broadcast'):
self.show_transaction(tx)
self.do_clear()
return
if label:
self.wallet.set_label(tx.hash(), label)
self.broadcast_transaction(tx)

# keep a reference to WaitingDialog or the gui might crash
Expand Down
27 changes: 26 additions & 1 deletion lib/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ class BIP32_Account_2of2(BIP32_Account):
def __init__(self, v):
BIP32_Account.__init__(self, v)
self.xpub2 = v['xpub2']
# chain-specific derivation
# self.xpub = bip32_public_derivation(self.xpub, "", "/{}".format(self.active_chain.chain_index))
# self.xpub2 = bip32_public_derivation(self.xpub2, "", "/{}".format(self.active_chain.chain_index))

def dump(self):
d = BIP32_Account.dump(self)
Expand All @@ -389,7 +392,7 @@ def redeem_script(self, for_change, n):

def pubkeys_to_address(self, pubkeys):
redeem_script = Transaction.multisig_script(sorted(pubkeys), 2)
address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5)
address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), self.active_chain.p2sh_version)
return address

def get_address(self, for_change, n):
Expand All @@ -401,12 +404,34 @@ def get_master_pubkeys(self):
def get_type(self):
return _('Multisig 2 of 2')

def get_private_key(self, sequence, wallet, password):
out = []
if len(sequence) == 2:
sequence.insert(0, self.active_chain.chain_index)
# xpubs = self.get_master_pubkeys()
# roots = [k for k, v in wallet.master_public_keys.iteritems() if v in xpubs]
# for root in roots:
# xpriv = wallet.get_master_private_key(root, password)
# if not xpriv:
# continue
# _, _, _, c, k = deserialize_xkey(xpriv)
# pk = bip32_private_key( sequence, k, c )
# out.append(pk)

xpriv = wallet.get_master_private_key("x1/", password)
_, _, _, c, k = deserialize_xkey(xpriv)
pk = bip32_private_key( sequence, k, c )
out.append(pk)

return out

class BIP32_Account_2of3(BIP32_Account_2of2):

def __init__(self, v):
BIP32_Account_2of2.__init__(self, v)
self.xpub3 = v['xpub3']
# chain-specific derivation
# self.xpub3 = bip32_public_derivation(self.xpub3, "", "/{}".format(self.active_chain.chain_index))

def dump(self):
d = BIP32_Account_2of2.dump(self)
Expand Down
3 changes: 2 additions & 1 deletion lib/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ def parse_xpub(x_pubkey):


def parse_scriptSig(d, bytes):
active_chain = chainparams.get_active_chain()
try:
decoded = [ x for x in script_GetOp(bytes) ]
except Exception:
Expand Down Expand Up @@ -416,7 +417,7 @@ def parse_scriptSig(d, bytes):
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)
d['address'] = hash_160_to_bc_address(hash_160(redeemScript.decode('hex')), active_chain.p2sh_version)



Expand Down
131 changes: 123 additions & 8 deletions lib/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,10 @@ def __init__(self, storage):
self.update_tx_outputs(tx_hash)

# save wallet type the first time
if self.storage.get('wallet_type') is None:
self.storage.put('wallet_type', self.wallet_type, True)
if self.storage.get_above_chain('wallet_type') is None:
self.storage.put_above_chain('wallet_type', self.wallet_type, True)
#if self.storage.get('wallet_type') is None:
# self.storage.put('wallet_type', self.wallet_type, True)

def set_chain(self, chaincode):
result = self.storage.config.set_active_chain_code(chaincode)
Expand Down Expand Up @@ -840,6 +842,8 @@ def sign_transaction(self, tx, password):
keypairs = {}
x_pubkeys = tx.inputs_to_sign()
for x in x_pubkeys:
if not self.can_sign_xpubkey(x):
continue
sec = self.get_private_key_from_xpubkey(x, password)
print "sec", sec
if sec:
Expand Down Expand Up @@ -1552,21 +1556,126 @@ def get_action(self):
if not self.accounts:
return 'create_accounts'

# Multisig wallets use a different derivation path
# Instead of m/44'/coin'/... we use m/44'/0'/coin/...
# Keys are derived in this manner:
# Cosigners share public keys. For a given chain, the public key used
# in the main account is the chain_index-th non-hardened child of
# the master public key.
#
# Example
# The public key that we share with our cosigner is m/44'/0'
# To generate addresses for Bitcoin, we use m/44'/0'/0/for_change/index as the key in the script hash.
# To generate addresses for Mazacoin, we use m/44'/0'/13/for_change/index as the key in the script hash.
class Multisig_Wallet(BIP32_Wallet, Mnemonic):
root_name = "x1/"
root_derivation = "m/44'/0'"

def __init__(self, storage):
BIP32_Wallet.__init__(self, storage)
try:
chain_code = storage.config.get_active_chain_code()
# constructor was passed a dict instead of a config object
except AttributeError:
chain_code = chainparams.get_active_chain().code
if chain_code is None:
chain_code = chainparams.get_active_chain().code

chain_index = chainparams.get_chain_index(chain_code)
self.root_derivation = "m/44'/0'"

self.master_public_keys = storage.get_above_chain('master_public_keys', {})
self.master_private_keys = storage.get_above_chain('master_private_keys', {})

def can_import(self):
return False

class Wallet_2of2(BIP32_Wallet, Mnemonic):
def add_master_public_key(self, name, xpub):
self.master_public_keys[name] = xpub
self.storage.put_above_chain('master_public_keys', self.master_public_keys, True)

def add_master_private_key(self, name, xpriv, password):
self.master_private_keys[name] = pw_encode(xpriv, password)
self.storage.put_above_chain('master_private_keys', self.master_private_keys, True)

def can_sign_xpubkey(self, x_pubkey):
if x_pubkey[0:2] in ['02','03','04']:
addr = bitcoin.public_key_to_bc_address(x_pubkey.decode('hex'), self.active_chain.p2pkh_version)
return self.is_mine(addr)
elif x_pubkey[0:2] == 'ff':
xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey)
for k, account in self.accounts.items():
if xpub == account.get_master_pubkeys()[0]:
return True
return False
elif x_pubkey[0:2] == 'fe':
xpub, sequence = OldAccount.parse_xpubkey(x_pubkey)
return xpub == self.get_master_public_key()
elif x_pubkey[0:2] == 'fd':
addrtype = ord(x_pubkey[2:4].decode('hex'))
addr = hash_160_to_bc_address(x_pubkey[4:].decode('hex'), addrtype)
return self.is_mine(addr)
else:
raise BaseException("z")

def get_private_key_from_xpubkey(self, x_pubkey, password):
if x_pubkey[0:2] in ['02','03','04']:
addr = bitcoin.public_key_to_bc_address(x_pubkey.decode('hex'), self.active_chain.p2pkh_version)
if self.is_mine(addr):
return self.get_private_key(addr, password)[0]
elif x_pubkey[0:2] == 'ff':
xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey)
for k, account in self.accounts.items():
if xpub == account.get_master_pubkeys()[0]:
pk = account.get_private_key(sequence, self, password)
return pk[0]
elif x_pubkey[0:2] == 'fe':
xpub, sequence = OldAccount.parse_xpubkey(x_pubkey)
for k, account in self.accounts.items():
if xpub in account.get_master_pubkeys():
pk = account.get_private_key(sequence, self, password)
return pk[0]
elif x_pubkey[0:2] == 'fd':
addrtype = ord(x_pubkey[2:4].decode('hex'))
addr = hash_160_to_bc_address(x_pubkey[4:].decode('hex'), addrtype)
if self.is_mine(addr):
return self.get_private_key(addr, password)[0]
else:
raise BaseException("z")

def sign_transaction(self, tx, password):
if self.is_watching_only():
return
# check that the password is correct. This will raise if it's not.
self.check_password(password)
keypairs = {}
x_pubkeys = tx.inputs_to_sign()
for x in x_pubkeys:
sec = self.get_private_key_from_xpubkey(x, password)
print "sec", sec
if sec:
keypairs[ x ] = sec
if keypairs:
tx.sign(keypairs)
run_hook('sign_transaction', tx, password)

class Wallet_2of2(Multisig_Wallet):
# Wallet with multisig addresses.
# Cannot create accounts
root_name = "x1/"
root_derivation = "m/44'/0'"
wallet_type = '2of2'

def __init__(self, storage):
Multisig_Wallet.__init__(self, storage)

def can_import(self):
return False

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})
acc_xpub1 = bip32_public_derivation(xpub1, "", "/{}".format(self.active_chain.chain_index))
acc_xpub2 = bip32_public_derivation(xpub2, "", "/{}".format(self.active_chain.chain_index))
account = BIP32_Account_2of2({'xpub':acc_xpub1, 'xpub2':acc_xpub2})
self.add_account('0', account)

def get_master_public_keys(self):
Expand Down Expand Up @@ -1594,7 +1703,10 @@ 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})
acc_xpub1 = bip32_public_derivation(xpub1, "", "/{}".format(self.active_chain.chain_index))
acc_xpub2 = bip32_public_derivation(xpub2, "", "/{}".format(self.active_chain.chain_index))
acc_xpub3 = bip32_public_derivation(xpub3, "", "/{}".format(self.active_chain.chain_index))
account = BIP32_Account_2of3({'xpub':acc_xpub1, 'xpub2':acc_xpub2, 'xpub3':acc_xpub3})
self.add_account('0', account)

def get_master_public_keys(self):
Expand Down Expand Up @@ -1717,7 +1829,10 @@ def __new__(self, storage):
sys.exit(1)

run_hook('add_wallet_types', wallet_types)
wallet_type = storage.get('wallet_type')
wallet_type = storage.get_above_chain('wallet_type')
# If wallet_type isn't above chain, get it from where it used to be
if wallet_type is None:
wallet_type = storage.get('wallet_type')
if wallet_type:
for cat, t, name, c in wallet_types:
if t == wallet_type:
Expand Down
29 changes: 12 additions & 17 deletions plugins/cosigner_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,12 @@ def description(self):
def init_qt(self, gui):
self.win = gui.main_window
self.win.connect(self.win, SIGNAL('cosigner:receive'), self.on_receive)
if self.listener is None:
self.listener = Listener(self)
self.listener.start()

def enable(self):
self.set_enabled(True)
self.init_qt()
if self.win.wallet:
self.load_wallet(self.win.wallet)
return True

def is_available(self):
# Disabled until compatibility is ensured
return False
if self.wallet is None:
return True
return self.wallet.wallet_type in ['2of2', '2of3']
Expand All @@ -116,16 +108,18 @@ def load_wallet(self, wallet):
self.wallet = wallet
if not self.is_available():
return
mpk = self.wallet.get_master_public_keys()
if self.listener is None:
self.listener = Listener(self)
self.listener.start()
self.cosigner_list = []
for key, xpub in mpk.items():
keyname = key + '/' # fixme
K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex')
for key, xpub in self.wallet.master_public_keys.items():
xpub_chain = bitcoin.bip32_public_derivation(xpub, "", "/{}".format(self.wallet.active_chain.chain_index))
K = bitcoin.deserialize_xkey(xpub_chain)[-1].encode('hex')
_hash = bitcoin.Hash(K).encode('hex')
if self.wallet.master_private_keys.get(keyname):
self.listener.set_key(keyname, _hash)
if self.wallet.master_private_keys.get(key):
self.listener.set_key(key, _hash)
else:
self.cosigner_list.append((xpub, K, _hash))
self.cosigner_list.append((xpub_chain, K, _hash))

@hook
def transaction_dialog(self, d):
Expand Down Expand Up @@ -182,10 +176,11 @@ def on_receive(self):
message = self.listener.message
key = self.listener.keyname
xprv = self.wallet.get_master_private_key(key, password)
if not xprv:
xprv_chain = bitcoin.bip32_private_derivation(xprv, "", "/{}".format(self.wallet.active_chain.chain_index))[0]
if not xprv_chain:
return
try:
k = bitcoin.deserialize_xkey(xprv)[-1].encode('hex')
k = bitcoin.deserialize_xkey(xprv_chain)[-1].encode('hex')
EC = bitcoin.EC_KEY(k.decode('hex'))
message = EC.decrypt_message(message)
except Exception as e:
Expand Down